mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-10-27 20:33:42 +08:00
Compare commits
852 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54b45ec2ff | ||
|
|
c434f85045 | ||
|
|
4d10279870 | ||
|
|
9de9489673 | ||
|
|
9814fec930 | ||
|
|
53ba731159 | ||
|
|
b2f57aa483 | ||
|
|
4c2dba1004 | ||
|
|
79adc871ef | ||
|
|
8144fada25 | ||
|
|
754ba02263 | ||
|
|
7ddf57ae06 | ||
|
|
cc5180a6f7 | ||
|
|
96f1126d02 | ||
|
|
7f9b8d8246 | ||
|
|
5132d52a44 | ||
|
|
abdf5298fe | ||
|
|
2129f7a8b7 | ||
|
|
f6f8748521 | ||
|
|
59301df073 | ||
|
|
e17dcf4d5f | ||
|
|
09f44e6d9b | ||
|
|
59824bffc5 | ||
|
|
cb0dacd5e0 | ||
|
|
7463cfc66c | ||
|
|
b248560ba2 | ||
|
|
37368fe13f | ||
|
|
246b023624 | ||
|
|
9f44c34d34 | ||
|
|
a6b9f57a50 | ||
|
|
42bc23cacf | ||
|
|
282f55c7a3 | ||
|
|
44798f89ba | ||
|
|
596cb2b206 | ||
|
|
d1965deff1 | ||
|
|
b793b81768 | ||
|
|
a5ef4299ec | ||
|
|
cdb1a8bde1 | ||
|
|
233f6e00f0 | ||
|
|
b7dba68549 | ||
|
|
64e5fc48ba | ||
|
|
a692cf1338 | ||
|
|
6998dd7af4 | ||
|
|
9343c73e0f | ||
|
|
739cd46539 | ||
|
|
f8fed83507 | ||
|
|
d63536d5ef | ||
|
|
4905fb28d4 | ||
|
|
a3a2a8abcb | ||
|
|
839dd8dbf4 | ||
|
|
0375164f40 | ||
|
|
691294b444 | ||
|
|
bdea12c51a | ||
|
|
a27d9ea259 | ||
|
|
7cd824c284 | ||
|
|
e27d95e2b5 | ||
|
|
c24b4d7074 | ||
|
|
6839827db0 | ||
|
|
ab24398748 | ||
|
|
6110522b54 | ||
|
|
bcdf5e3776 | ||
|
|
2207830db9 | ||
|
|
d52dfbfef4 | ||
|
|
d6a04f96fe | ||
|
|
66ccb387e8 | ||
|
|
5f820b9dc1 | ||
|
|
3cc2263dc7 | ||
|
|
f0a3c5d8ae | ||
|
|
2a4ef27774 | ||
|
|
2b057f32aa | ||
|
|
bc6451026f | ||
|
|
99fd596862 | ||
|
|
f0959b5df6 | ||
|
|
6788edbe9d | ||
|
|
3895305882 | ||
|
|
1b0938b33f | ||
|
|
c2acbaaa94 | ||
|
|
02faff461a | ||
|
|
e18e5a38c6 | ||
|
|
2f9b1b7835 | ||
|
|
717b137a6d | ||
|
|
f755bdccae | ||
|
|
4bba77ab47 | ||
|
|
6944a32ff3 | ||
|
|
5742b40aee | ||
|
|
7f1ec90748 | ||
|
|
4a99be2f15 | ||
|
|
bee19392c1 | ||
|
|
27c816cf3b | ||
|
|
0d81776212 | ||
|
|
00d31a2379 | ||
|
|
cccab31c0f | ||
|
|
5d65505ab7 | ||
|
|
3dc7d0516a | ||
|
|
50335ebc2d | ||
|
|
bcadee7290 | ||
|
|
cac3194d5b | ||
|
|
4ddf3bf2bf | ||
|
|
d45f9fbad6 | ||
|
|
d98b08d7cd | ||
|
|
5a8fe5a6cf | ||
|
|
36c27d6092 | ||
|
|
3ab29da8f0 | ||
|
|
3699f024f1 | ||
|
|
3d37a3d367 | ||
|
|
73d8236697 | ||
|
|
114d0088dc | ||
|
|
43b6665370 | ||
|
|
5fb9f84182 | ||
|
|
e35c34ad9a | ||
|
|
1a4d798f8b | ||
|
|
afb91a7023 | ||
|
|
dc4c1f7877 | ||
|
|
bbc8fe2b40 | ||
|
|
3c34e8e0e7 | ||
|
|
57c932f07c | ||
|
|
922202734a | ||
|
|
8b3b0139b0 | ||
|
|
31828a3336 | ||
|
|
b270960a04 | ||
|
|
5c4899df6e | ||
|
|
9a797bb4a5 | ||
|
|
b0c9ffc5a6 | ||
|
|
f527cc5b98 | ||
|
|
debe8dc209 | ||
|
|
2f0215ac87 | ||
|
|
dd5cc206e5 | ||
|
|
142cd553a3 | ||
|
|
657ecccee3 | ||
|
|
1232c3cd9c | ||
|
|
3ac04a3938 | ||
|
|
b7abc42209 | ||
|
|
a48179ce0e | ||
|
|
e589f25a05 | ||
|
|
cc1a3ce343 | ||
|
|
7bb76d581c | ||
|
|
0d733c0be0 | ||
|
|
8b40ac5b5c | ||
|
|
24479814e9 | ||
|
|
99df028237 | ||
|
|
b354b88876 | ||
|
|
5e0be4d10e | ||
|
|
468b48151f | ||
|
|
fa5c036041 | ||
|
|
0fdc588167 | ||
|
|
2e023cb8dc | ||
|
|
e933f32d9c | ||
|
|
bd4b0c4d65 | ||
|
|
0b2501c1d8 | ||
|
|
9d28e62142 | ||
|
|
c1d892069e | ||
|
|
61b2dbc9f1 | ||
|
|
be3245666e | ||
|
|
dacdd6fe74 | ||
|
|
6807f7e88a | ||
|
|
087f5ab2d1 | ||
|
|
47c5a0387b | ||
|
|
f9da18ad52 | ||
|
|
5c9025ca22 | ||
|
|
d02cb573fd | ||
|
|
caa538a1d0 | ||
|
|
b584b4bfb6 | ||
|
|
bda335212d | ||
|
|
06f4cdc649 | ||
|
|
336a7d5b56 | ||
|
|
a0f464830f | ||
|
|
9bf7fa4081 | ||
|
|
96ead65774 | ||
|
|
7ad41927aa | ||
|
|
4ca9dfd9c0 | ||
|
|
8a9f386d8f | ||
|
|
adfee8bf58 | ||
|
|
fbfa2a71a9 | ||
|
|
9a1368ef17 | ||
|
|
31b02b97d3 | ||
|
|
42da38c5c3 | ||
|
|
0a01b55713 | ||
|
|
3b292c2a12 | ||
|
|
db0ba0d9a0 | ||
|
|
3a23ff6b42 | ||
|
|
1e9c5adb0a | ||
|
|
abab76ccc6 | ||
|
|
6efd92806f | ||
|
|
cfe333e89f | ||
|
|
a7237fe62f | ||
|
|
c3c454b7d7 | ||
|
|
d4d708d44b | ||
|
|
7f0b6a3a46 | ||
|
|
c2a7c089d2 | ||
|
|
df5bd4df60 | ||
|
|
79b6010104 | ||
|
|
97b0a98793 | ||
|
|
5230f90540 | ||
|
|
803db4e895 | ||
|
|
7cee9f2ebb | ||
|
|
8be9a21efd | ||
|
|
6a3e26b566 | ||
|
|
0355c37bef | ||
|
|
9b7ee538c4 | ||
|
|
d900a3d08e | ||
|
|
cdf5b66729 | ||
|
|
1cff4b63cd | ||
|
|
da14309ef9 | ||
|
|
fbb216fe3b | ||
|
|
95efbd5659 | ||
|
|
4596c1049c | ||
|
|
b35d95f0c7 | ||
|
|
01419df998 | ||
|
|
a6c00c42fa | ||
|
|
4cc9db7115 | ||
|
|
4f1ed54059 | ||
|
|
8227a73e35 | ||
|
|
adfd8c1939 | ||
|
|
8eed7ff534 | ||
|
|
c79c4e74d0 | ||
|
|
f1855fd0a1 | ||
|
|
1f964c74e9 | ||
|
|
4fb2c5803c | ||
|
|
b5947545cb | ||
|
|
342b76f666 | ||
|
|
49b5906bc7 | ||
|
|
3075bfb7fc | ||
|
|
82e06fad33 | ||
|
|
4a9028747b | ||
|
|
4a8ff0ccf0 | ||
|
|
99341f0484 | ||
|
|
f58ac29ad0 | ||
|
|
7060edb3e5 | ||
|
|
41ae411f9b | ||
|
|
79b7fee47c | ||
|
|
0044bf10af | ||
|
|
e9348d3611 | ||
|
|
b9236e09a7 | ||
|
|
09b38d5f42 | ||
|
|
7bb539a06e | ||
|
|
5cdada8265 | ||
|
|
4147c217b1 | ||
|
|
8dda639b23 | ||
|
|
8487d2c9eb | ||
|
|
c5e583b215 | ||
|
|
549f618cff | ||
|
|
e9a3510346 | ||
|
|
30e6e963b3 | ||
|
|
c72d963f45 | ||
|
|
172d498618 | ||
|
|
313993532e | ||
|
|
e53db3582c | ||
|
|
72c6bd3f77 | ||
|
|
ca8b349df3 | ||
|
|
1b206c3640 | ||
|
|
c60276fc9f | ||
|
|
d00a3167c0 | ||
|
|
6b1cd8c30c | ||
|
|
46f12dc9ad | ||
|
|
a3e1d8ae21 | ||
|
|
72a066b93e | ||
|
|
0327a829ac | ||
|
|
882e9b8819 | ||
|
|
ef58cfadaa | ||
|
|
bf958d6113 | ||
|
|
71611273d7 | ||
|
|
b27c654311 | ||
|
|
90930ea9f9 | ||
|
|
1ab2185ff1 | ||
|
|
0f2f978d4c | ||
|
|
f61963b0b0 | ||
|
|
2aa413960d | ||
|
|
aa4bbba5ec | ||
|
|
eba61fea2d | ||
|
|
34e3455128 | ||
|
|
07dca3e739 | ||
|
|
4cb4b145f9 | ||
|
|
1ed417cb69 | ||
|
|
6cf91a84ca | ||
|
|
0b566980fc | ||
|
|
f86176b342 | ||
|
|
c700b32670 | ||
|
|
22641b452a | ||
|
|
d3fbb8c19e | ||
|
|
e3bb69ff10 | ||
|
|
770360c614 | ||
|
|
f302a0478f | ||
|
|
a88697b43a | ||
|
|
cc6f140812 | ||
|
|
424f2b3bdc | ||
|
|
ec0c13a600 | ||
|
|
a1f03bec4c | ||
|
|
b5bd4a5e0e | ||
|
|
7c2e49bfdb | ||
|
|
f80fe6d041 | ||
|
|
72f80a96bc | ||
|
|
2de655a1cf | ||
|
|
da2bd4a501 | ||
|
|
e0aa62c40d | ||
|
|
9d26a892d1 | ||
|
|
4ece7f2847 | ||
|
|
32368caf1b | ||
|
|
e91f54e79e | ||
|
|
bb8f4c57c4 | ||
|
|
43bfac99b6 | ||
|
|
be379b6d63 | ||
|
|
17f3c9b840 | ||
|
|
24de97fac2 | ||
|
|
bf27b44fee | ||
|
|
1802b4fe4d | ||
|
|
241a5c7bc9 | ||
|
|
557d547bf1 | ||
|
|
2e7b75affb | ||
|
|
bc21a1d443 | ||
|
|
3fc9e10a24 | ||
|
|
5fa1aa2060 | ||
|
|
be8a0ec184 | ||
|
|
b02e3aad95 | ||
|
|
08eca511ad | ||
|
|
c34e911596 | ||
|
|
8a452c3072 | ||
|
|
13bfb14107 | ||
|
|
4188b0969e | ||
|
|
0c27795a10 | ||
|
|
d05693c5c1 | ||
|
|
c0b2063b38 | ||
|
|
4d183747b1 | ||
|
|
08fe1b2f75 | ||
|
|
db3e8a267e | ||
|
|
8fc62682c4 | ||
|
|
75031914a3 | ||
|
|
a4c9fdd95a | ||
|
|
6a9bfeb5aa | ||
|
|
e654766f60 | ||
|
|
0ef6955f96 | ||
|
|
b4501557c9 | ||
|
|
a2ed99e6cb | ||
|
|
6bd6bb3885 | ||
|
|
399cf65fc9 | ||
|
|
24906a6df1 | ||
|
|
d772bbebe6 | ||
|
|
14988853a3 | ||
|
|
7b3f16ac9f | ||
|
|
82b2755c18 | ||
|
|
ff4b267858 | ||
|
|
a590d0497f | ||
|
|
ac30d906f0 | ||
|
|
5bc071e038 | ||
|
|
88b956cf98 | ||
|
|
f725cf4661 | ||
|
|
057cc1e8a6 | ||
|
|
de122735b8 | ||
|
|
e87ede981c | ||
|
|
606fb498e1 | ||
|
|
a0c06e40a4 | ||
|
|
aba8f57279 | ||
|
|
960286a350 | ||
|
|
8c93fa51f6 | ||
|
|
cb0e7d64ff | ||
|
|
8e7413da97 | ||
|
|
a36f14eb94 | ||
|
|
f2f9f6e488 | ||
|
|
85068b8ca2 | ||
|
|
f2cfcfeefc | ||
|
|
755273a898 | ||
|
|
d4a24a0f1d | ||
|
|
92281fcbb7 | ||
|
|
636db4afcc | ||
|
|
ba25b8755e | ||
|
|
6399d13a49 | ||
|
|
06fa54fd25 | ||
|
|
a335b965d0 | ||
|
|
725adaa7d0 | ||
|
|
7e7e81e974 | ||
|
|
8cfe6bfc17 | ||
|
|
33de83f2ac | ||
|
|
3f856afec8 | ||
|
|
4e4dc4cb73 | ||
|
|
02a9c422fe | ||
|
|
ca69341024 | ||
|
|
169bf069ce | ||
|
|
1bee0ab04d | ||
|
|
440d91dd0e | ||
|
|
8168e246a8 | ||
|
|
2ef07574ae | ||
|
|
37392f2bb2 | ||
|
|
a80cd3848e | ||
|
|
db6ed84451 | ||
|
|
4463cc5963 | ||
|
|
d316158fe2 | ||
|
|
e02a8d7586 | ||
|
|
9988dff885 | ||
|
|
35ef5674ff | ||
|
|
976da45bce | ||
|
|
c83ac48bd2 | ||
|
|
3d159a833e | ||
|
|
4b09878bdd | ||
|
|
b0162e6a92 | ||
|
|
8ab15e5dc4 | ||
|
|
d2ac807252 | ||
|
|
0af01f6f1f | ||
|
|
013b319fab | ||
|
|
2899ba5949 | ||
|
|
a558b7e104 | ||
|
|
7a833e2233 | ||
|
|
bf65746d00 | ||
|
|
f08a7862de | ||
|
|
023a2c2f09 | ||
|
|
1bcd0f4c1a | ||
|
|
a0f3bc8ccb | ||
|
|
dea72738c1 | ||
|
|
a1d1fe7763 | ||
|
|
a39ed9764c | ||
|
|
aaa5ba99aa | ||
|
|
2113508b6d | ||
|
|
7fe4212684 | ||
|
|
8bdda64794 | ||
|
|
ec08c24dca | ||
|
|
a992a5b3b3 | ||
|
|
0f05970141 | ||
|
|
e5e762efcd | ||
|
|
b3d0c1ef9c | ||
|
|
397078f7ff | ||
|
|
3ad8065e20 | ||
|
|
66c7717f04 | ||
|
|
412f8ecc6c | ||
|
|
51dcf642b3 | ||
|
|
bfeea555b2 | ||
|
|
479f94c372 | ||
|
|
0140713e86 | ||
|
|
15b2ec9721 | ||
|
|
c9cd082855 | ||
|
|
d7c002890c | ||
|
|
348dd22279 | ||
|
|
3e99b4cbf6 | ||
|
|
6968da3ac7 | ||
|
|
bf1c1b84c3 | ||
|
|
c70314d930 | ||
|
|
9104ca8e49 | ||
|
|
2af33b3630 | ||
|
|
654e795545 | ||
|
|
c62ba2451e | ||
|
|
d72d1b8a99 | ||
|
|
b939d6016b | ||
|
|
36a2626ccc | ||
|
|
bd057a4cc9 | ||
|
|
dc24a8c781 | ||
|
|
59fa21779b | ||
|
|
a140671aad | ||
|
|
5fe8990fb4 | ||
|
|
12799b7159 | ||
|
|
9929746b1d | ||
|
|
d70035ff0c | ||
|
|
eec90274d8 | ||
|
|
e8fff55c42 | ||
|
|
3cf3cdd705 | ||
|
|
9801fce659 | ||
|
|
4c1f51110b | ||
|
|
913d538587 | ||
|
|
9e704365fc | ||
|
|
485bdbc56a | ||
|
|
7000168fd4 | ||
|
|
5694f97a6b | ||
|
|
b677d3fac7 | ||
|
|
dc6719cf54 | ||
|
|
7de5b55091 | ||
|
|
76c5101092 | ||
|
|
2f8d2f4854 | ||
|
|
b1ee34ba0c | ||
|
|
069ad6a09a | ||
|
|
bf1403c818 | ||
|
|
bcc622a24d | ||
|
|
a06a81a415 | ||
|
|
d1950acd01 | ||
|
|
039b70eed2 | ||
|
|
d8e4308b1b | ||
|
|
434fbb3463 | ||
|
|
de3eb8969c | ||
|
|
fbd6eac877 | ||
|
|
1fecab177b | ||
|
|
b1b385c455 | ||
|
|
3c6e86d04b | ||
|
|
3d2035d08a | ||
|
|
da86f916d8 | ||
|
|
e7a07f7e92 | ||
|
|
b01e6387fc | ||
|
|
d86aca0f5d | ||
|
|
09414fe36a | ||
|
|
df0e7508db | ||
|
|
92b1f01118 | ||
|
|
8fb8bd932b | ||
|
|
3f74b94784 | ||
|
|
e9467341fa | ||
|
|
131e051ddc | ||
|
|
f626fe3166 | ||
|
|
6bc57b6132 | ||
|
|
d972e97c88 | ||
|
|
3991f4daec | ||
|
|
f6b567d6fc | ||
|
|
8addba8203 | ||
|
|
3ab930a107 | ||
|
|
de512a5ea2 | ||
|
|
113cfae2dc | ||
|
|
33aebf9cb5 | ||
|
|
6e58ddf681 | ||
|
|
cae5c049e4 | ||
|
|
ff76e4bd89 | ||
|
|
a0a506a3c4 | ||
|
|
aa5a4a9977 | ||
|
|
abf4f061c1 | ||
|
|
245cd3ee1a | ||
|
|
45cb29d9a0 | ||
|
|
d974b1ff0e | ||
|
|
56269170cb | ||
|
|
4290c4ca22 | ||
|
|
7f7c8e831e | ||
|
|
8f057ca9d1 | ||
|
|
4a56621ec3 | ||
|
|
a398e7a550 | ||
|
|
96816c12ca | ||
|
|
9984926f69 | ||
|
|
a2a6081027 | ||
|
|
5a10ed37a7 | ||
|
|
1a9dd9de0b | ||
|
|
0dae5bef71 | ||
|
|
b4413ed726 | ||
|
|
5e1fe88b8b | ||
|
|
91ed41b536 | ||
|
|
024c0032eb | ||
|
|
4a9f7e3bce | ||
|
|
cf4dcc34ec | ||
|
|
4d612c15af | ||
|
|
8aec87cc02 | ||
|
|
442e411cde | ||
|
|
acec0194de | ||
|
|
8557f5b94a | ||
|
|
babef8baae | ||
|
|
efd4ab46f5 | ||
|
|
ae8239e5de | ||
|
|
f0994ba457 | ||
|
|
dae91ed243 | ||
|
|
de42a428e6 | ||
|
|
63c7041e1f | ||
|
|
b1263ddc69 | ||
|
|
7e50e17aaf | ||
|
|
a7265c4251 | ||
|
|
6f39f639bd | ||
|
|
a7db123437 | ||
|
|
241c714a8b | ||
|
|
67ac3cfe32 | ||
|
|
c926e0afcc | ||
|
|
5bc07e6d57 | ||
|
|
c3666a9a71 | ||
|
|
23b5ffa97d | ||
|
|
a2c7a75705 | ||
|
|
d68f2ef12c | ||
|
|
67d30353f0 | ||
|
|
4813163eac | ||
|
|
5c5210625e | ||
|
|
a4a1eec30b | ||
|
|
d35164506a | ||
|
|
1ed08f01ea | ||
|
|
eca07ab830 | ||
|
|
3512715704 | ||
|
|
6d07881141 | ||
|
|
251fe626f2 | ||
|
|
5fee3a9288 | ||
|
|
9b68d8101e | ||
|
|
cfe6f27d48 | ||
|
|
b314dd0900 | ||
|
|
950fab6374 | ||
|
|
9d1f5c42ce | ||
|
|
a84046390b | ||
|
|
aa29323a8a | ||
|
|
d5617b7c3a | ||
|
|
1ef60a9e5e | ||
|
|
fb6e395ad8 | ||
|
|
d9216060bc | ||
|
|
bcaa9a92e5 | ||
|
|
576adc9036 | ||
|
|
00de18be9a | ||
|
|
c61d32816a | ||
|
|
f3fbb0b89c | ||
|
|
e311a39632 | ||
|
|
51407abe44 | ||
|
|
8a470b1038 | ||
|
|
baddabaa16 | ||
|
|
427b434ce3 | ||
|
|
5f921965e6 | ||
|
|
1e705c8ed5 | ||
|
|
b8ae65bb30 | ||
|
|
321e2087ea | ||
|
|
aac60edce2 | ||
|
|
9dc9a6923e | ||
|
|
7ca4dfe09b | ||
|
|
c584b82ddb | ||
|
|
5f17ab2501 | ||
|
|
c84e912dd8 | ||
|
|
2ebff2623f | ||
|
|
72418ce4d7 | ||
|
|
e221b1eed4 | ||
|
|
696306f066 | ||
|
|
1807d5b5d4 | ||
|
|
85c12aa322 | ||
|
|
da9d0dc3bc | ||
|
|
daaca822ac | ||
|
|
59ced3f947 | ||
|
|
22ae7dd1f3 | ||
|
|
1816e9d5cf | ||
|
|
9be6755f65 | ||
|
|
e5fb986463 | ||
|
|
55d24e577e | ||
|
|
7f50fa3fcf | ||
|
|
fff8b78aba | ||
|
|
7ca1989d98 | ||
|
|
4595dcb7ed | ||
|
|
a688d3feb5 | ||
|
|
7d1d88a32f | ||
|
|
d95c048edd | ||
|
|
df2fc9d77c | ||
|
|
d7e815d2bb | ||
|
|
f58b0a65f0 | ||
|
|
b59ad521ca | ||
|
|
b47ff975b0 | ||
|
|
d043a87b30 | ||
|
|
4cae7525d9 | ||
|
|
76966d2ce7 | ||
|
|
5a740aecb0 | ||
|
|
1ae79331e7 | ||
|
|
8b14e141d0 | ||
|
|
9cbc6c91c4 | ||
|
|
21c3a419a5 | ||
|
|
287fac3a89 | ||
|
|
ba206bb387 | ||
|
|
4fc01f3f7b | ||
|
|
f5ed71bcc6 | ||
|
|
8fc26183e9 | ||
|
|
e8ae8fddb7 | ||
|
|
b876867297 | ||
|
|
91dfd59731 | ||
|
|
5fdff90a10 | ||
|
|
96c62619e6 | ||
|
|
083155413d | ||
|
|
d83019cbe4 | ||
|
|
cc7271aa73 | ||
|
|
f873d6b375 | ||
|
|
c86169022a | ||
|
|
db0a79da93 | ||
|
|
48393e0e83 | ||
|
|
7b4730271d | ||
|
|
9cbe36d4c6 | ||
|
|
b25bb2cc53 | ||
|
|
79ded6018b | ||
|
|
59f316b341 | ||
|
|
f307b8ba7a | ||
|
|
5034a20345 | ||
|
|
26944f9e39 | ||
|
|
e64946c3b6 | ||
|
|
e0a62d9b35 | ||
|
|
39dbffd8d0 | ||
|
|
952d6183ed | ||
|
|
3365a6008d | ||
|
|
2e13ddf405 | ||
|
|
1d3acc8ed3 | ||
|
|
fa341bab30 | ||
|
|
036a6e3e41 | ||
|
|
f4c6ca4554 | ||
|
|
327929243c | ||
|
|
f4349c7a8c | ||
|
|
4b46d847f0 | ||
|
|
c3f016eae8 | ||
|
|
ebd3ef842f | ||
|
|
18c033d57f | ||
|
|
b676f80110 | ||
|
|
f7fbaa534d | ||
|
|
ea93a22e14 | ||
|
|
9f7e6778c5 | ||
|
|
6c31a2bfa6 | ||
|
|
f943669e18 | ||
|
|
3b26735998 | ||
|
|
79d25769ee | ||
|
|
1dd6800987 | ||
|
|
5e673a9ee0 | ||
|
|
92eb67a2af | ||
|
|
b1bed59be2 | ||
|
|
0ac732a3a3 | ||
|
|
bf3f68fa19 | ||
|
|
46a551df16 | ||
|
|
20a12462b1 | ||
|
|
a49fb1940e | ||
|
|
32774d23c7 | ||
|
|
7ecd7eeba1 | ||
|
|
0cc9cf8b45 | ||
|
|
d06f94bddd | ||
|
|
b5955f08c9 | ||
|
|
c120569894 | ||
|
|
aa376f1737 | ||
|
|
0f8a0f89e3 | ||
|
|
68dc261b44 | ||
|
|
4cf3af0c7b | ||
|
|
b99b6735d9 | ||
|
|
52189b7880 | ||
|
|
3dbeb1ccb6 | ||
|
|
5a0f272fa8 | ||
|
|
6561b99f8f | ||
|
|
329e3eee21 | ||
|
|
07049c9afb | ||
|
|
36c5dd7eaa | ||
|
|
b84039b506 | ||
|
|
fab43097dc | ||
|
|
c8998ba294 | ||
|
|
40b2466adc | ||
|
|
35fedbe817 | ||
|
|
827acdd3f9 | ||
|
|
6c76086916 | ||
|
|
373370fde5 | ||
|
|
2165ba3406 | ||
|
|
b0e02b43fc | ||
|
|
2107c13b3d | ||
|
|
5f41aecc8d | ||
|
|
6840a13370 | ||
|
|
8f1e28c0ab | ||
|
|
7903eed284 | ||
|
|
0d49ea0d41 | ||
|
|
2ee4db5e48 | ||
|
|
48c4789505 | ||
|
|
4e65a5b1a1 | ||
|
|
b09d23f97f | ||
|
|
3529649ba9 | ||
|
|
fdd659f393 | ||
|
|
9eb8da2789 | ||
|
|
ffb1ef0470 | ||
|
|
862c6aea43 | ||
|
|
54fe4b7588 | ||
|
|
c6062ee70e | ||
|
|
bed184dc1f | ||
|
|
29094ba3b3 | ||
|
|
a18188876c | ||
|
|
4faee3e48e | ||
|
|
1a6afcd266 | ||
|
|
f567831d92 | ||
|
|
cf36ca4285 | ||
|
|
0e4ae01498 | ||
|
|
7b90f8cb13 | ||
|
|
c33215529a | ||
|
|
c5be114db2 | ||
|
|
cab955c292 | ||
|
|
ca8c8e6490 | ||
|
|
253951b4b3 | ||
|
|
4d6444ebf3 | ||
|
|
94d8d8a9d4 | ||
|
|
e02badf7bb | ||
|
|
dd88622c64 | ||
|
|
c4d7126c4d | ||
|
|
86bc063941 | ||
|
|
dce85eb519 | ||
|
|
4ab879d697 | ||
|
|
681e52df50 | ||
|
|
fb554c0315 | ||
|
|
accf8eeb77 | ||
|
|
3e41edd3b5 | ||
|
|
9126cfff20 | ||
|
|
9806d5ff4c | ||
|
|
d1d13a72e4 | ||
|
|
00c520d066 | ||
|
|
797ff66474 | ||
|
|
9d51a478b9 | ||
|
|
1d4179df75 | ||
|
|
917b6012e8 | ||
|
|
da14632794 | ||
|
|
a868a8a8b7 | ||
|
|
5037df744f | ||
|
|
da88a501ad | ||
|
|
b9885e8de4 | ||
|
|
22efe81080 | ||
|
|
2926717aef | ||
|
|
a49d54d66c | ||
|
|
ce0267e25b | ||
|
|
9088d22a66 | ||
|
|
1ff32d5d0a | ||
|
|
adf6916598 | ||
|
|
31c14bf748 | ||
|
|
5395385d1e | ||
|
|
0035da548b | ||
|
|
9bceaade05 | ||
|
|
3194becdad | ||
|
|
6174b17c24 | ||
|
|
53fa4a20e9 | ||
|
|
43c1de51f5 | ||
|
|
7eb8c5ec35 | ||
|
|
296bf63196 | ||
|
|
6c65a21692 | ||
|
|
daf83cfc84 | ||
|
|
871f5d39e4 | ||
|
|
3f91f37aff | ||
|
|
a08981f876 | ||
|
|
5187a43543 | ||
|
|
6a733de556 | ||
|
|
b9e9eae93f | ||
|
|
811f12135a | ||
|
|
2c172c0851 | ||
|
|
399a16fa28 | ||
|
|
d971e95900 | ||
|
|
0b6940b121 | ||
|
|
ad0f96fcb1 | ||
|
|
063b5655f7 | ||
|
|
d03ed6570b | ||
|
|
1795a891ce | ||
|
|
05bdd81646 | ||
|
|
cba54be913 | ||
|
|
da0acfe851 | ||
|
|
133a127d8a | ||
|
|
8da9b52eae | ||
|
|
2700b63887 | ||
|
|
17713d05ec | ||
|
|
3674d9da85 | ||
|
|
0e6606e469 | ||
|
|
feff1684c4 | ||
|
|
81e08e02ff | ||
|
|
f593526bd4 | ||
|
|
d244ad9983 | ||
|
|
65a01f4776 | ||
|
|
5f812ae649 | ||
|
|
fda811de97 | ||
|
|
fe7f021ddb | ||
|
|
120e54fb29 | ||
|
|
cd809d17d3 | ||
|
|
e7fd29b9cb | ||
|
|
c008f33bc3 | ||
|
|
7b5cacd6f1 | ||
|
|
d88219c726 | ||
|
|
a3ee7ca2d8 | ||
|
|
6f37024e34 | ||
|
|
3347b4c990 | ||
|
|
6a2122e1ac | ||
|
|
4b024e0ad7 | ||
|
|
b06a065d44 | ||
|
|
15b77ad10a | ||
|
|
180bd2a1db | ||
|
|
06fb7b41b4 | ||
|
|
088fd14c03 | ||
|
|
1e84332119 | ||
|
|
01d61ab19b | ||
|
|
b4569d7fe2 | ||
|
|
a9ecaed5bd | ||
|
|
654057e7a7 | ||
|
|
f4e5265aaf | ||
|
|
85d85fbe10 | ||
|
|
d4bd071487 | ||
|
|
3cee291997 | ||
|
|
1849d3ddaa | ||
|
|
7c6dfa545a | ||
|
|
9a72e51a55 | ||
|
|
c2d5da27e1 | ||
|
|
176e614457 | ||
|
|
a008b89b42 | ||
|
|
fb2108d88b |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
deploy
|
||||
docs
|
||||
api/static
|
||||
web/node_modules
|
||||
desktop
|
||||
|
||||
47
.github/ISSUE_TEMPLATE/1.bug.yml
vendored
Normal file
47
.github/ISSUE_TEMPLATE/1.bug.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Bug 报告 🐛
|
||||
description: 为 geekai 提交错误报告
|
||||
labels: ['Bug']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: ⚠️ 确认 issue 是否已存在 ⚠️
|
||||
description: 在提交 Issue 之前,请在 issue 列表搜索一下,确保你不是在提交一个重复的 issue。
|
||||
options:
|
||||
- label: 我已经搜索了现有的问题,没有找到跟我问题相关的问题。
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: GPT-3 or GPT-4
|
||||
description: 请选择你使用的 GPT 模型
|
||||
options:
|
||||
- GPT-3.5
|
||||
- GPT-4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统
|
||||
description: 请选择你使用的操作系统
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- MacOS
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug 描述 📝
|
||||
description: 请简单描述你发现的问题。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 重现步骤 🕹
|
||||
description: |
|
||||
**⚠️ 无法重现的 issue 将会被关闭**
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 你的应用配置信息
|
||||
description: 请提供你的配置文档,**请注意数据脱敏**。
|
||||
value: |
|
||||
```toml
|
||||
把你的配置信息粘贴到这里
|
||||
```
|
||||
26
.github/ISSUE_TEMPLATE/2.feature.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/2.feature.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: 功能优化 🚀
|
||||
description: 为 geekai 提交优化建议
|
||||
labels: ['feature']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: ⚠️ 确认 issue 是否已存在 ⚠️
|
||||
description: >
|
||||
在提交 Issue 之前,请在 issue 列表搜索一下,确保你不是在提交一个重复的 issue。
|
||||
options:
|
||||
- label: 我已经搜索了现有的问题,没有找到相关 issue。
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 功能描述 📝
|
||||
description: |
|
||||
描述此功能该如何工作。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 示例 🌈
|
||||
description: 是否可以提供类似的样例,比如提供参考项目的链接或者截图。
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 动机 🔦
|
||||
description: 为什么要新增或者优化这个功能,缺少这个功能会给你或者其他用户带来什么不便?
|
||||
|
||||
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- 📢 注意:
|
||||
|
||||
在提交的 PR 的时候请确保每个 PR 只包含一个功能修改或者优化,请不要把多个更改组合到一个 PR 提交。保持干净可管理的 git 历史记录至关重要。 为确保我们存储库的质量,我们恳请您在提交 PR 时遵守以下准则:
|
||||
|
||||
1. 每个 PR 专注于一个单一的、具体的改进。
|
||||
2. 不要包括任何不相关或[额外]的修改。
|
||||
3. 为所做的更改提供清晰的文档和解释。
|
||||
-->
|
||||
|
||||
### Background
|
||||
<!-- 简要概述此更改背后的基本原理。 包括相关背景、先前的讨论或相关 issue 的链接。 确保变更与项目的总体方向一致。-->
|
||||
|
||||
### Changes
|
||||
<!-- 简要说明你改动的内容 -->
|
||||
|
||||
### Test Plan
|
||||
<!-- 描述您如何测试此功能。 包括重现步骤、相关测试用例和任何其他相关信息-->
|
||||
|
||||
### PR 规则验证列表
|
||||
- [ ] 确保本次 PR 只包含单一的功能修改。
|
||||
- [ ] 我已经对我的代码更改进行了充分的测试。
|
||||
- [ ] 我已经考虑了我的更改的潜在风险和缓解措施。
|
||||
- [ ] 我已经修正了相关文档
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,15 +1,7 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
src/dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
@@ -22,8 +14,3 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
src/tmp
|
||||
src/bin
|
||||
src/data
|
||||
web/.env.development
|
||||
config.toml
|
||||
|
||||
425
CHANGELOG.md
Normal file
425
CHANGELOG.md
Normal file
@@ -0,0 +1,425 @@
|
||||
# 更新日志
|
||||
## v4.1.1
|
||||
* Bug修复:修复 GPT 模型 function call 调用后没有输出的问题
|
||||
* 功能新增:允许获取 License 授权用户可以自定义版权信息
|
||||
* 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件
|
||||
* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
|
||||
* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
|
||||
* 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
|
||||
* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
|
||||
* 功能新增:允许在管理后台设置首页显示的导航菜单
|
||||
* Bug修复:修复注册页面先显示关闭注册组件,然后再显示注册组件
|
||||
* 功能新增:增加 Suno 文生歌曲功能
|
||||
* 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
|
||||
* 功能优化:在所有列表页面增加返回顶部按钮
|
||||
|
||||
## v4.1.0
|
||||
* bug修复:修复移动端修改聊天标题不生效的问题
|
||||
* Bug修复:修复用户注册不显示用户名的问题
|
||||
* Bug修复:修复管理后台拖动排序不生效的问题
|
||||
* 功能优化:允许用户设置自定义首页背景图片
|
||||
* 功能新增:**支持AI解读 PDF, Word, Excel等文件**
|
||||
* 功能优化:优化聊天界面的用户上传文件的列表样式
|
||||
* 功能优化:优化聊天页面对话样式,支持列表样式和对话样式切换
|
||||
* 功能新增:支持微信扫码登录,未注册用户微信扫码后会自动注册并登录。移动使用微信浏览器打开可以实现无感登录。
|
||||
|
||||
|
||||
## v4.0.9
|
||||
* 环境升级:升级 Golang 到 go1.22.4
|
||||
* 功能增加:接入微信商户号支付渠道
|
||||
* Bug修复:修复前端页面菜单把页面撑开,底部留白问题
|
||||
* 功能优化:聊天页面自动根据内容调整输入框的高度
|
||||
* Bug修复:修复Dalle绘图失败退回算力的问题
|
||||
* 功能优化:邀请码注册时被邀请人也可以获得赠送的算力
|
||||
* 功能优化:允许设置邮件验证码的抬头
|
||||
* Bug修复:修复免费模型不会记录聊天记录的bug
|
||||
* Bug修复:修复聊天输入公式显示异常的Bug
|
||||
|
||||
## v4.0.8
|
||||
* 功能优化:升级 mathjax 公式解析插件,修复公式因为图片访问限制而无法显示的问题
|
||||
* 功能优化:当数据库更新失败的时候记录错误日志
|
||||
* 功能优化:聊天输入框会随着输入内容的增多自动调整高度
|
||||
* Bug修复:修复移动端聊天页面模型切换不生效的Bug
|
||||
* 功能优化:给PC端扫码支付增加签名验证和有效期验证
|
||||
* Bug修复:修复支付码生成API权限控制的问题
|
||||
* Bug修复:模型算力设置为0时,不扣减用户算力,并且不记录算力消费日志
|
||||
* 功能优化:新增随机背景配置项,可以在后台设置,首页使用 Bing 壁纸作为背景图片
|
||||
* 功能新增:H5端支持 Dalle 绘图
|
||||
|
||||
## v4.0.7
|
||||
|
||||
* 功能优化:升级quic-go,支持 Go1.21
|
||||
* 功能优化:添加导航菜单的时候支持框入外部链接,并支持上传自定义菜单图片
|
||||
* Bug修复:修复弹窗等于图形验证码一直验证失败的问题
|
||||
* 功能重构:重构前端 UI 页面,增加顶部导航
|
||||
* 功能优化:优化 Vue 非父子组件之间的通信方式
|
||||
* 功能优化:优化 ItemList 组件,自动根据页面宽度计算 cols 数量
|
||||
|
||||
## v4.0.6
|
||||
|
||||
* Bug修复:修复PC端画廊页面的瀑布流组件样式错乱问题
|
||||
* 功能新增:给思维导图增加 ToolBar,实现思维导图的放大缩小和定位
|
||||
* Bug修复:修复思维导图不扣费的Bug
|
||||
* Bug修复:修复管理后台角色删除失败的Bug
|
||||
* Bug修复:兼容最新版秋叶SD懒人包的 SD API,新增 scheduler 参数
|
||||
* 功能优化:支持在管理后台配置 AI 绘图相关配置,包括 SD, MJ-PLUS, MJ-PROXY
|
||||
* Bug修复:修复注册用户提示注册人数达到上限的 Bug
|
||||
* 功能优化:将MJ,SD,Dall绘画页面的任务列表全改成瀑布流组件
|
||||
|
||||
## v4.0.5
|
||||
|
||||
* 功能优化:已授权系统在后台显示授权信息
|
||||
* 功能优化:使用思维链提示词生成思维导图,确保生成的思维导图不会出现格式错误
|
||||
* 功能优化:优化首页登录注册页面的 UI
|
||||
* BUG修复:修复License验证的逻辑漏洞
|
||||
* Bug修复:后台添加用户的时候密码规则限制跟前台注册保持一致
|
||||
* 功能新增:管理后台支持切换主题,支持 light 和 dark 两种主题
|
||||
* 功能新增:移动端新增 DALL-E 绘画功能
|
||||
* 功能新增:新增移动端首页功能,移动端支持 light 和 dark 两种主题
|
||||
* 功能新增:移动支持免登录预览功能
|
||||
* Bug修复:解决在同一个浏览器开启多个对话时候对话内容会相互乱串的问题
|
||||
* Bug修复:修复部分中转 API 模型会出现第一输出的字符被淹没的Bug
|
||||
|
||||
## v4.0.4
|
||||
|
||||
* Bug修复:修复统一千问第二句不回复的问题
|
||||
* 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度
|
||||
* 功能新增:Dalle AI 绘画功能实现
|
||||
* Bug修复:修复思维导图格式乱码问题
|
||||
* 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题
|
||||
* 功能新增:支持从应用列表直接和某个应用对话
|
||||
* 功能优化:优化算力日志的页面和首页的UI
|
||||
* 功能新增:支持思维导图导出 PNG 图片下载
|
||||
|
||||
## v4.0.3
|
||||
|
||||
* 功能新增:允许为角色应用绑定模型,如指定某个角色只能使用某个模型
|
||||
* Bug修复:兼容 gpt-4-turbo-2024-04-09 模型的函数调用 Bug
|
||||
* Bug修复:修复MidJourney在任务超时后出现后面的任务覆盖前面任务的问题
|
||||
* 功能新增:支持上传图片和视觉模型
|
||||
* 功能优化:优化聊天页面的复制代码按钮样式乱码
|
||||
* 功能新增:增加思维导图功能,支持选择不同的对话模型来生成思维导图
|
||||
* 功能新增:支持为角色绑定对话模型,比如绑定某个角色只能用GPT3.5或者 GPT4
|
||||
* 功能新增:支持为模型绑定 API KEY,比如为 GPT3.5 模型绑定免费的 API KEY 给用户免费使用来引流不至于消耗你的收费 KEY。
|
||||
* 功能新增:支持管理后台 Logo 修改
|
||||
|
||||
## 4.0.2
|
||||
|
||||
* 功能新增:支持前端菜单可以配置
|
||||
* 功能优化:在登录和注册界面标题显示软件版本号
|
||||
* 功能优化:MJ 绘画支持 --sref 和 --cref 图片一致性参数
|
||||
* 功能优化:使用 leveldb 解决 SD 绘图进度图片预览问题
|
||||
* Bug修复:解决因为图片上传使用相对路径而导致融图失败的问题。
|
||||
* 功能新增:手机端支持 Stable-Diffusion 绘画
|
||||
* 功能新增:管理后台登录页面增加行为验证码,防止爆破
|
||||
|
||||
## v4.0.1
|
||||
|
||||
* 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion
|
||||
发行版,稳定性更强一些
|
||||
* 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容
|
||||
MJ-Plus 中转
|
||||
* 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力
|
||||
* Bug修复:修复 iphone 手机无法通过图形验证码的Bug,使用滑动验证码替换
|
||||
* Bug修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的Bug
|
||||
|
||||
## v4.0.0
|
||||
|
||||
非兼容版本,重大重构,引入算力概念,将系统中所有的能力(AI对话,MJ绘画,SD绘画,DALL绘画)全部使用算力来兑换。
|
||||
只要你的算力值余额不为0,你就可以进行任何操作。比如一次 GPT3.5 对话消耗1个单位算力,一次 GPT4 对话消耗10个算力。一次 MJ
|
||||
对话消耗15个算力...
|
||||
|
||||
* 功能重构:重构整体系统,全部采用算力来进行结算
|
||||
* 功能优化:SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽
|
||||
* 功能优化:移动端聊天页面图片支持预览和放大功能
|
||||
* 功能优化:MJ 和 SD 页面数据分页加载,解决一次性加载太多数据导致页面卡顿的问题
|
||||
* 功能优化:**PC端不登录也可以预览功能,只有在发起操作的时候才需要登录**
|
||||
* 功能优化:控制台订单管理页面显示未支付订单,并提供订单删除功能
|
||||
* 功能新增:支持H5支付
|
||||
* 功能优化:支持数学公式的识别和美化输出
|
||||
* 功能新增:新增算力消费日志功能
|
||||
* 功能优化:整合 XXL-JOB 实现订单清理,每日算力派发,VIP 算力重置等任务
|
||||
* 功能新增:管理后台新增7日内新增用户和新增订单统计
|
||||
|
||||
## v3.2.7
|
||||
|
||||
* 功能重构:采用 Vant 重构移动页面,新增 MidJourney 功能
|
||||
* 功能优化:优化 PC 端 MidJourney 页面布局,新增融图和换脸功能
|
||||
* Bug修复:修复 issue [
|
||||
管理界面操作用户存在的两个问题](https://github.com/yangjian102621/chatgpt-plus/issues/117#issuecomment-1909201532)
|
||||
* 功能优化:在对话和聊天记录表中新增冗余字段 model,存储对话模型
|
||||
* Bug修复:IPhone 手机验证码触摸事件坐标错位 [issue 144](https://github.com/yangjian102621/chatgpt-plus/issues/144)
|
||||
* Bug修复:重新生成按钮功能失效问题
|
||||
* Bug修复:对话输入HTML标签不显示的问题
|
||||
* 功能优化:gpt-4-all/gpts/midjourney-plus 支持第三方平台的 API KEY
|
||||
* 功能新增:新增删除文件功能
|
||||
* Bug修复:解决 MJ-Plus discord 图片下载失败问题,使用第三方平台中转地址下载
|
||||
* 功能新增:后台管理新怎对话查看和检索功能
|
||||
|
||||
## v3.2.6
|
||||
|
||||
* 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号
|
||||
* 功能优化:兼用旧版本微信收款消息解析
|
||||
* 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源
|
||||
* 功能新增:新增图片发布功能,画廊只显示用户已发布的图片
|
||||
* 功能新增:后台新增配置微信客服二维码,可以上传自己的微信客服二维码
|
||||
* 功能新增:新增网站公告,可以在管理后台自定义配置
|
||||
* 功能新增:新增阿里通义千问大模型支持
|
||||
* Bug修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug
|
||||
* 功能优化:新增虎皮椒和PayJS订单状态校验功能,增加安全性
|
||||
* Bug修复:修复微信转账交易 ID 提取失败 Bug
|
||||
* 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug
|
||||
* 功能新增:新增短信宝短信平台发送平台集成
|
||||
|
||||
## v3.2.5
|
||||
|
||||
* 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。
|
||||
* 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅
|
||||
Plus 账号了!!!
|
||||
* 功能优化:增强 markdown 图片和引用块解析。
|
||||
* 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。
|
||||
* 功能优化:function call 兼用中转 API。
|
||||
* Bug修复:修复部分已知的 Bug。
|
||||
|
||||
## v3.2.4.1
|
||||
|
||||
* 功能新增:新增 PayJs 支付通道
|
||||
* Bug修复:紧急修复后台添加用户失败问题
|
||||
* Bug修复:紧急修复使用中转 API-KEY 无法绘图的问题
|
||||
* Bug修复:允许用户关闭手机和邮箱注册通道,移除验证码依赖
|
||||
|
||||
## v3.2.4
|
||||
|
||||
* 功能新增:重磅更新,支持邮箱注册
|
||||
* 功能优化:优化函数调用授权
|
||||
* 功能优化:给用户表新增 nickname 字段
|
||||
* 功能优化:管理后台给聊天角色增加启用/禁用开关
|
||||
* Bug修复:SD绘画出现重复扣减绘图次数
|
||||
* 功能优化:优化聊天对话导出样式,适应移动端
|
||||
* 功能新增:众筹核销可以选择兑换对话还是绘图的额度
|
||||
* Bug修复:修复[从历史记录获取reply有并发风险 #92](https://github.com/yangjian102621/chatgpt-plus/issues/92)
|
||||
* Bug修复:修复 MidJourney 绘图任务调度Bug,为 task_id 建议唯一索引
|
||||
* 功能重构:重构了 API KEY模块,支持为每个 API KEY 都设置不同的 API 地址,并可以单独开启是否使用代理。
|
||||
|
||||
## v3.2.3
|
||||
|
||||
* 功能重构:重构函数工具模块,设计成可以后台动态管理函数。支持添加自定义函数实现
|
||||
* 功能新增:为充值产品数据表添加 img_calls 字段,支持充值绘图次数
|
||||
* Bug修复:修复 [MJ 机器人空指针异常的 Bug](https://github.com/yangjian102621/chatgpt-plus/issues/73)
|
||||
* Bug修复:确保相同 Prompt 的绘图任务的 Upscale 和 Variation 任务调度给相同的频道
|
||||
* 功能新增:新增删除绘图任何和图片功能
|
||||
* Bug修复:修复虎皮椒支付二维码重复扫码时报错问题
|
||||
* 功能优化:自动将 AI 绘画中的中文提示词翻译成英文
|
||||
* 功能优化:优化AI绘画的大图压缩算法,新增图片缓存
|
||||
* 功能优化:支持为 MJ 绘图 API 增加反代功能,提高图片的加载速度,大大降低绘图任务的失败率
|
||||
* Bug修复:修复[Azure Api 更换api-version参数后请求失败的问题](https://github.com/yangjian102621/chatgpt-plus/pull/71)
|
||||
* Bug修复:修复科大讯飞 V1.5 API 请求失败的问题
|
||||
* Bug修复:绘图失败后,自动恢复用户的剩余绘图次数
|
||||
* 功能新增:为移动端新增 SD 绘图功能,分享功能
|
||||
|
||||
## v3.2.2
|
||||
|
||||
* 功能重构:重构 MidJourney 和 Stable-Diffusion 绘图模块,支持使用多组配置创建池子提供绘画服务
|
||||
* 功能新增:AI绘画页面增加翻译和重写提示词功能
|
||||
* 功能优化:OSS上传组件支持在 Bucket 下设置二级目录
|
||||
* Bug修复:修复阿里云 OSS 访问路径错误
|
||||
* 功能优化:在 AI 绘图页面使用 HTTP 轮询替换 Websocket
|
||||
|
||||
## v3.2.1
|
||||
|
||||
* 功能优化:切换角色和模型的时候自动创建新的对话
|
||||
* Bug修复:修复文件上传失败No such file bug
|
||||
* 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
|
||||
* Bug修复:[PC端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
|
||||
* 功能新增:增加 arm64 架构打包脚本
|
||||
* 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
|
||||
* 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
|
||||
|
||||
## v3.2.0
|
||||
|
||||
* 功能新增:新增邀请注册功能
|
||||
* 功能优化:增加中间件自动对HTTP请求的参数去掉首尾空格
|
||||
* 功能优化:增加中间件自动为大图片生成缩略图
|
||||
* 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
|
||||
* 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
|
||||
* Bug修复:修复阿里云 OSS 域名设置不起做用的bug
|
||||
* Bug修复:修复MidJourney绘图失败后重复添加到队列的问题
|
||||
|
||||
## v3.1.9
|
||||
|
||||
* 功能新增:增加讯飞星火大模型 v3.0 支持
|
||||
* 功能新增:新增找回密码功能
|
||||
* 功能新增:支持 Markdown 代码复制功能
|
||||
* Bug修复: xxl-job 任务调度失败的 Bug
|
||||
* 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
|
||||
* Bug修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数
|
||||
* 功能优化:优化会员充值页面 ItemList 组件
|
||||
* 功能优化:给首页 Logo 增加链接
|
||||
* Bug修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
|
||||
* Bug修复:聊天上下文失效问题
|
||||
* 功能优化:关闭注册时显示联系管理员二维码
|
||||
* 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
|
||||
* Bug修复:后台启用用户 VIP 不生效问题
|
||||
* 功能优化:充值支付页面的支付说明文字可以后台配置
|
||||
* Bug修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题
|
||||
|
||||
## v3.1.8
|
||||
|
||||
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
|
||||
2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败问题
|
||||
3. Bug修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
|
||||
4. Bug修复:修复七牛云上传报错 expired token
|
||||
5. Bug修复:修复高权重模型导致的对话次数为负数的漏洞
|
||||
6. 功能优化:将聊天报错信息定义为统一常量,方便修改
|
||||
7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式
|
||||
8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单
|
||||
|
||||
## v3.1.7
|
||||
|
||||
1. 功能新增:支持文心4.0 AI 模型
|
||||
2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型
|
||||
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如GPT4是GPT3.5的10倍
|
||||
4. 功能新增:新增系统配置关闭 AI 模型的函数功能
|
||||
5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式
|
||||
|
||||
## v3.1.6
|
||||
|
||||
1. 功能新增:新增AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
|
||||
2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用
|
||||
3. 功能优化:优化瀑布流组件的页面布局
|
||||
4. 功能优化:新注册用户成功之后自动登录
|
||||
5. 功能优化:优化更新对话标题的操作体验,绑定回车事件
|
||||
|
||||
## v3.1.5
|
||||
|
||||
1. 功能新增:新增百度文心一言大模型 API 接入支持
|
||||
2. 功能新增:新增科大讯飞星火大模型 API 接入支持
|
||||
3. 功能重构:将 chat_handler 的所有功能实现放入单独的包中
|
||||
4. 功能新增:新增系统配置 `enabled_function` 用于启用和关闭函数功能
|
||||
5. Bug修复:修复管理后台更新 API Key 失败的 Bug
|
||||
6. Bug修复:修复新建的对话无法更新对话标题的 Bug
|
||||
7. 功能优化:其他一些小的体验优化工作
|
||||
|
||||
## v3.1.4
|
||||
|
||||
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio和阿里云 OSS 四种存储介质。
|
||||
2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。
|
||||
3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。
|
||||
4. Bug修复:修复[用户注册报错BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
|
||||
5. Bug修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
|
||||
6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
|
||||
7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。
|
||||
8. 功能优化:优化绘画页面的弹窗样式和页面布局。
|
||||
|
||||
## v3.1.3
|
||||
|
||||
1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。
|
||||
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
|
||||
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
|
||||
4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。
|
||||
5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。
|
||||
6. Bug修复:修复 JWT token 有效期计算错误的 Bug。
|
||||
|
||||
## v3.1.2
|
||||
|
||||
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
|
||||
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
|
||||
3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
|
||||
4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
|
||||
5. 功能优化:取消导出聊天页面的授权验证
|
||||
6. 功能优化:所有路由跳转都使用绝对路径
|
||||
|
||||
## v3.1.1
|
||||
|
||||
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug
|
||||
|
||||
## v3.1.0(大版本更新)
|
||||
|
||||
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及
|
||||
ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
|
||||
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
|
||||
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。
|
||||
4. 功能优化:更新头像自动删除旧的图片资源。
|
||||
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
|
||||
6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。
|
||||
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
|
||||
8. 升级 gin, element-plus,redis 组件到最新版本。
|
||||
9. Bug修复:修复若干已知的的 Bug
|
||||
|
||||
## v3.0.7
|
||||
|
||||
1. 聊天主界面:新增聊天引导页面,介绍产品功能
|
||||
2. 功能重构:拆分项目,将函数插件以及微信机器人,MidJourney 机器人等功能拆分新项目独立部署。
|
||||
3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。
|
||||
4. 功能新增:支持导出聊天记录为 PDF 文件。
|
||||
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
|
||||
6. 功能优化:支持用户设置默认的 GPT 模型
|
||||
7. Bug修复:修复若干已知的的 Bug
|
||||
|
||||
## v3.0.6
|
||||
|
||||
1. 管理后台:新增用户名和手机号码搜索功能
|
||||
2. 管理后台:新增重置用户密码功能
|
||||
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
|
||||
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
|
||||
5. Bug修复:修复注册页面验证码不显示 Bug
|
||||
6. Bug修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
|
||||
7. 功能修正:允许用户使用手机号码登录
|
||||
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
|
||||
9. 功能优化:优化打包脚本,减少容器镜像大小
|
||||
|
||||
## v3.0.5
|
||||
|
||||
重磅功能更新!!! 新增函数插件支持,可以轻松地接入你的第三方插件服务,ChatGPT 自动帮您调用对应的函数完成任务。
|
||||
|
||||
1. 新增函数功能支持,全球早报,今日头条和微博热搜等插件服务,您也可以接入自己的第三方服务。
|
||||
2. 集成微信机器人模块,可以通过微信个人收款码来完成充值,无需接入微信支付功能也可以完成收款功能。
|
||||
3. 用户注册添加短信验证码功能,引入交互安全认证服务,有效防刷短信。
|
||||
4. 支持配置聊天上下文深度,精确统计每轮对话所消耗的总 TOKEN 数量。
|
||||
5. 修复已知的 Bug。
|
||||
|
||||
## v3.0.4
|
||||
|
||||
1. 调整项目目录结构,移除其他语言 API 目录
|
||||
2. 修复 nodejs apple M1 跨平台打包,运行报错 exec format error
|
||||
3. 增加用户 token 消耗统计功能
|
||||
|
||||
## v3.0.3
|
||||
|
||||
1. 优化启动参数接收处理,支持环境变量传参
|
||||
2. 修复 PC 端聊天界面出现滚动条的 Bug
|
||||
3. 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题
|
||||
4. 更改 docker 构建镜像的基础镜像,改用作者的阿里云镜像,这样打包更快一些。
|
||||
|
||||
## v3.0.2
|
||||
|
||||
1. Feat:新增移动端的聊天和用户设置功能
|
||||
2. Fix: 修复 markdown 换行符解析的 Bug
|
||||
3. Feat: 新增头像上传功能
|
||||
4. Docs: 增加容器部署支持,支持 docker-compose 一键部署
|
||||
5. Fix: 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug
|
||||
|
||||
## v3.0.1
|
||||
|
||||
1. 紧急修复前端 Home 组件路由被后台管理 Home 组件路由覆盖的 Bug。
|
||||
2. 增加 docker-compose 部署脚本
|
||||
|
||||
## v3.0.0
|
||||
|
||||
全新的重构版本!!!
|
||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
|
||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
|
||||
前后台技术架构还是基于 `Vue3 + Element-Plus` ,但是页面风格已经全部变了,几乎所有页面样式代码都重写了,希望会你是希望的风格!
|
||||
|
||||
此次重构改版主要是为了后面功能的扩展准备了。
|
||||
|
||||
新版本已经实现的功能如下:
|
||||
|
||||
1. 引入用户体系,新增用户注册和登录功能。
|
||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
|
||||
3. 创建会话的时候可以选择聊天角色和模型。
|
||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
|
||||
5. 保存聊天记录,支持聊天上下文。
|
||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
|
||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
|
||||
8. 支持会话搜索过滤。
|
||||
214
LICENSE
214
LICENSE
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2023 RockYang
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
114
README.md
114
README.md
@@ -1,68 +1,92 @@
|
||||
# ChatGPT-Plus
|
||||
# GeekAI
|
||||
> 根据[《生成式人工智能服务管理暂行办法》](https://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
|
||||
基于 OpenAI API 实现的 ChatGPT Web 应用,一共分为两个版本:
|
||||
**GeekAI** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure,
|
||||
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。
|
||||
|
||||
* 通用版:交互体验跟 ChatGPT 官方一致,聊天记录保存在客户端(浏览器)
|
||||
* 角色版:内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||
聊天记录保存在云端(可以配置是否保存聊天记录)
|
||||
每个版本都有 PC 版和移动版,PC 版本的体验要略优于移动版。
|
||||
主要特性:
|
||||
|
||||
**本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。**
|
||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
- 基于 Websocket 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
- 支持 Suno 文生音乐
|
||||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,文生图,图生图,换脸,融图。开箱即用。
|
||||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
|
||||
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
|
||||
绘画函数插件。
|
||||
|
||||
## 项目介绍
|
||||
这一套完整的系统,包括两套前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供 ChatGPT 的服务。
|
||||
### 🚀 更多功能请查看 [GeekAI-PLUS](https://github.com/yangjian102621/geekai-plus)
|
||||
|
||||
项目的技术架构是
|
||||
|
||||
> Go + Vue3 + element-plus
|
||||
|
||||
后端采用的是 Go 语言开发的 Gin Web 框架。前端用的是 Vue3 + element-plus UI 框架
|
||||
|
||||
目前已经实现了以下功能:
|
||||
1. 通用版的 ChatGPT 聊天界面和功能,聊天记录保存在客户端。
|
||||
2. 口令机制:输入口令才可以访问,支持设置口令的对话次数,有效期。
|
||||
3. 角色版的聊天界面和功能,角色设定,预设一些角色,比如程序员,客服,作家,老师,艺术家...
|
||||
4. 保存聊天记录,支持聊天上下文。
|
||||
5. OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次,防止被封。
|
||||
6. 支持配置多个代理,保证高可用。
|
||||
7. 实现 markdown 语法解析和代码高亮,支持复制回复内容功能。
|
||||
8. 后台管理功能,实现系统的动态配置,用户和角色的动态管理。
|
||||
- [x] 更友好的 UI 界面
|
||||
- [x] 支持 Dall-E 文生图功能
|
||||
- [x] 支持文生思维导图
|
||||
- [x] 支持为模型绑定指定的 API KEY,支持为角色绑定指定的模型等功能
|
||||
- [x] 支持网站 Logo 版权等信息的修改
|
||||
|
||||
## 功能截图
|
||||
请参考 [GeekAI 项目介绍](https://docs.geekai.me/info/)。
|
||||
|
||||
### 1. 角色版PC端
|
||||

|
||||
### 体验地址
|
||||
|
||||

|
||||
> 免费体验地址:[https://chat.geekai.me](https://chat.geekai.me) <br/>
|
||||
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
|
||||
|
||||
### 2. 角色版移动端
|
||||

|
||||
## 快速部署
|
||||
|
||||
### 3. 通用版
|
||||

|
||||
请参考文档 [**GeekAI 快速部署**](https://docs.geekai.me/install/)。
|
||||
|
||||
### 4. 管理后台
|
||||

|
||||
## 使用须知
|
||||
|
||||

|
||||
1. 本项目基于 Apache2.0 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
|
||||
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
|
||||
|
||||

|
||||
## 项目地址
|
||||
|
||||
### 5. 体验地址
|
||||
> 体验地址:[https://www.chat-plus.net/chat/#/free](https://www.chat-plus.net/chat/#/free) </br>
|
||||
> 口令:GeekMaster
|
||||
* Github 地址:https://github.com/yangjian102621/geekai
|
||||
* 码云地址:https://gitee.com/blackfox/geekai
|
||||
|
||||
## 客户端下载
|
||||
|
||||
目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/geekai/releases/tag/v3.1.2
|
||||
|
||||
## TODOLIST
|
||||
* [ ] 让用户配置自己的 API KEY,调用自己的 API Key,将不记 Token 的使用次数
|
||||
* [ ] 嵌入 AI 绘画功能,支持根据描述词生成图片
|
||||
* [ ] 接入自己训练的开源大语言模型
|
||||
* [ ] 接入 Google 语音 API,支持语音聊天
|
||||
|
||||
## 本地部署
|
||||
## 线上发布
|
||||
* [ ] 支持基于知识库的 AI 问答
|
||||
* [ ] 文生视频,文生歌曲功能
|
||||
* [ ] 微信支付功能
|
||||
|
||||
## 注意事项
|
||||
## 项目文档
|
||||
|
||||
最新的部署视频教程:[https://www.bilibili.com/video/BV1Cc411t7CX/](https://www.bilibili.com/video/BV1Cc411t7CX/)
|
||||
|
||||
详细的部署和开发文档请参考 [**GeekAI 文档**](https://docs.geekai.me)。
|
||||
|
||||
加微信进入微信讨论群可获取 **一键部署脚本(添加好友时请注明来自Github!!!)。**
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||
21
api/.gitignore
vendored
Normal file
21
api/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
tmp
|
||||
bin
|
||||
data
|
||||
config.toml
|
||||
static/upload
|
||||
storage.json
|
||||
res/certs/wechat/apiclient_key.pem
|
||||
15
api/Makefile
Normal file
15
api/Makefile
Normal file
@@ -0,0 +1,15 @@
|
||||
SHELL=/usr/bin/env bash
|
||||
NAME := geekai
|
||||
all: amd64 arm64
|
||||
|
||||
amd64:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-linux main.go
|
||||
.PHONY: amd64
|
||||
|
||||
arm64:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -o bin/$(NAME)-linux main.go
|
||||
.PHONY: arm64
|
||||
|
||||
clean:
|
||||
rm -rf bin/$(NAME)-*
|
||||
.PHONY: clean
|
||||
5
api/README.md
Normal file
5
api/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# chatgpt-plus-go
|
||||
|
||||
chatgpt-plus 后端 API Go 语言实现。技术选型采用 Gin + Mysql 架构,依赖注入使用的是 fx 框架,ORM 采用的是 GORM 框架。
|
||||
|
||||
|
||||
138
api/config.sample.toml
Normal file
138
api/config.sample.toml
Normal file
@@ -0,0 +1,138 @@
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
AesEncryptKey = ""
|
||||
WeChatBot = false
|
||||
TikaHost = "http://tika:9998"
|
||||
|
||||
[Session]
|
||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
|
||||
MaxAge = 86400
|
||||
|
||||
[Redis] # redis 配置信息
|
||||
Host = "localhost"
|
||||
Port = 6379
|
||||
Password = ""
|
||||
DB = 0
|
||||
|
||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||
ApiURL = "https://sapi.geekai.me"
|
||||
AppId = ""
|
||||
Token = ""
|
||||
|
||||
|
||||
[SMS] # Sms 配置,用于发送短信
|
||||
Active = "Ali" # 当前启用的短信服务,默认使用阿里云
|
||||
[SMS.Bao]
|
||||
Username = ""
|
||||
Password = ""
|
||||
Domain = "api.smsbao.com"
|
||||
Sign = "【极客学长】"
|
||||
CodeTemplate = "您的验证码是{code}。5分钟有效,若非本人操作,请忽略本短信。"
|
||||
[SMS.Ali]
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Product = "Dysmsapi"
|
||||
Domain = "dysmsapi.aliyuncs.com"
|
||||
Sign = ""
|
||||
CodeTempId = ""
|
||||
|
||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
||||
Active = "local" # 默认使用本地文件存储引擎
|
||||
[OSS.Local]
|
||||
BasePath = "./static/upload" # 本地文件上传根路径
|
||||
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件前缀 URL,线上需要把 localhost 替换成自己的实际域名或者IP
|
||||
[OSS.Minio]
|
||||
Endpoint = "" # 如 172.22.11.200:9000
|
||||
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
||||
AccessSecret = ""
|
||||
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
|
||||
UseSSL = false
|
||||
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
|
||||
[OSS.QiNiu] # 七牛云 OSS 配置
|
||||
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Bucket = ""
|
||||
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
|
||||
[OSS.AliYun]
|
||||
Endpoint = "oss-cn-hangzhou.aliyuncs.com"
|
||||
AccessKey = ""
|
||||
AccessSecret = ""
|
||||
Bucket = "chatgpt-plus"
|
||||
SubDir = ""
|
||||
Domain = ""
|
||||
|
||||
[[MjProxyConfigs]]
|
||||
Enabled = true
|
||||
ApiURL = "http://midjourney-proxy:8082"
|
||||
ApiKey = "sk-geekmaster"
|
||||
|
||||
[[MjPlusConfigs]]
|
||||
Enabled = false
|
||||
ApiURL = "https://api.chat-plus.net"
|
||||
Mode = "fast" # MJ 绘画模式,可选值 relax/fast/turbo
|
||||
ApiKey = "sk-xxx"
|
||||
|
||||
[[SdConfigs]]
|
||||
Enabled = false
|
||||
ApiURL = ""
|
||||
ApiKey = ""
|
||||
Txt2ImgJsonPath = "res/sd/text2img.json"
|
||||
|
||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||
Enabled = false # 是否启用 XXL JOB 服务
|
||||
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||
ExecutorPort = "9999" # 执行器服务端口
|
||||
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||
|
||||
[AlipayConfig]
|
||||
Enabled = false # 启用支付宝支付通道
|
||||
SandBox = false # 是否启用沙盒模式
|
||||
UserId = "2088721020750581" # 商户ID
|
||||
AppId = "9021000131658023" # App Id
|
||||
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址
|
||||
|
||||
[HuPiPayConfig]
|
||||
Enabled = false
|
||||
Name = "wechat"
|
||||
AppId = ""
|
||||
AppSecret = ""
|
||||
ApiURL = "https://api.xunhupay.com"
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify"
|
||||
|
||||
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接
|
||||
UseTls = false
|
||||
Host = "smtp.163.com"
|
||||
Port = 25
|
||||
AppName = "极客学长"
|
||||
From = "test@163.com" # 发件邮箱人地址
|
||||
Password = "" #邮箱 stmp 服务授权码
|
||||
|
||||
[JPayConfig] # PayJs 支付配置
|
||||
Enabled = false
|
||||
Name = "wechat" # 请不要改动
|
||||
AppId = "" # 商户 ID
|
||||
PrivateKey = "" # 秘钥
|
||||
ApiURL = "https://payjs.cn"
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的
|
||||
|
||||
# 微信商户支付
|
||||
[WechatPayConfig]
|
||||
Enabled = false
|
||||
AppId = "" # 商户应用ID
|
||||
MchId = "" # 商户号
|
||||
SerialNo = "" # API 证书序列号
|
||||
PrivateKey = "certs/alipay/privateKey.txt" # API 证书私钥文件路径,跟支付宝一样,把私钥文件拷贝到对应的路径,证书路径要映射到容器内
|
||||
ApiV3Key = "" # APIV3 私钥,这个是你自己在微信支付平台设置的
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/wechat/notify" # 支付成功异步回调地址,域名改成自己的
|
||||
ReturnURL = "" # 支付成功同步回调地址
|
||||
|
||||
373
api/core/app_server.go
Normal file
373
api/core/app_server.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package core
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/nfnt/resize"
|
||||
"golang.org/x/image/webp"
|
||||
"gorm.io/gorm"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AppServer struct {
|
||||
Debug bool
|
||||
Config *types.AppConfig
|
||||
Engine *gin.Engine
|
||||
SysConfig *types.SystemConfig // system config cache
|
||||
}
|
||||
|
||||
func NewServer(appConfig *types.AppConfig) *AppServer {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
gin.DefaultWriter = io.Discard
|
||||
return &AppServer{
|
||||
Debug: false,
|
||||
Config: appConfig,
|
||||
Engine: gin.Default(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
if debug { // 调试模式允许跨域请求 API
|
||||
s.Debug = debug
|
||||
logger.Info("Enabled debug mode")
|
||||
}
|
||||
s.Engine.Use(corsMiddleware())
|
||||
s.Engine.Use(staticResourceMiddleware())
|
||||
s.Engine.Use(authorizeMiddleware(s, client))
|
||||
s.Engine.Use(parameterHandlerMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
}
|
||||
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
// load system configs
|
||||
var sysConfig model.Config
|
||||
res := db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
err := utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("http://%s", s.Config.Listen)
|
||||
return s.Engine.Run(s.Config.Listen)
|
||||
}
|
||||
|
||||
// 全局异常处理
|
||||
func errorHandler(c *gin.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Errorf("Handler Panic: %v", r)
|
||||
debug.PrintStack()
|
||||
c.JSON(http.StatusBadRequest, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
//加载完 defer recover,继续后续接口调用
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// 跨域中间件设置
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
if origin != "" {
|
||||
// 设置允许的请求源
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
//允许跨域设置可以返回其他子段,可以自定义字段
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token, Admin-Authorization")
|
||||
// 允许浏览器(客户端)可以解析的头部 (重要)
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||
//设置缓存时间
|
||||
c.Header("Access-Control-Max-Age", "172800")
|
||||
//允许客户端传递校验信息比如 cookie (重要)
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
if method == http.MethodOptions {
|
||||
c.JSON(http.StatusOK, "ok!")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Info("Panic info is: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 用户授权验证
|
||||
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var tokenString string
|
||||
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
|
||||
if isAdminApi { // 后台管理 API
|
||||
tokenString = c.GetHeader(types.AdminAuthHeader)
|
||||
} else if c.Request.URL.Path == "/api/chat/new" {
|
||||
tokenString = c.Query("token")
|
||||
} else {
|
||||
tokenString = c.GetHeader(types.UserAuthHeader)
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
if needLogin(c) {
|
||||
resp.NotAuth(c, "You should put Authorization in request headers")
|
||||
c.Abort()
|
||||
return
|
||||
} else { // 直接放行
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok && needLogin(c) {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
if isAdminApi {
|
||||
return []byte(s.Config.AdminSession.SecretKey), nil
|
||||
} else {
|
||||
return []byte(s.Config.Session.SecretKey), nil
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if err != nil && needLogin(c) {
|
||||
resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is invalid")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is expired")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("users/%v", claims["user_id"])
|
||||
if isAdminApi {
|
||||
key = fmt.Sprintf("admin/%v", claims["user_id"])
|
||||
}
|
||||
if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) {
|
||||
resp.NotAuth(c, "Token is not found in redis")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(types.LoginUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func needLogin(c *gin.Context) bool {
|
||||
if c.Request.URL.Path == "/api/user/login" ||
|
||||
c.Request.URL.Path == "/api/user/logout" ||
|
||||
c.Request.URL.Path == "/api/user/resetPass" ||
|
||||
c.Request.URL.Path == "/api/admin/login" ||
|
||||
c.Request.URL.Path == "/api/admin/logout" ||
|
||||
c.Request.URL.Path == "/api/admin/login/captcha" ||
|
||||
c.Request.URL.Path == "/api/user/register" ||
|
||||
c.Request.URL.Path == "/api/user/session" ||
|
||||
c.Request.URL.Path == "/api/chat/history" ||
|
||||
c.Request.URL.Path == "/api/chat/detail" ||
|
||||
c.Request.URL.Path == "/api/chat/list" ||
|
||||
c.Request.URL.Path == "/api/role/list" ||
|
||||
c.Request.URL.Path == "/api/model/list" ||
|
||||
c.Request.URL.Path == "/api/mj/imgWall" ||
|
||||
c.Request.URL.Path == "/api/mj/client" ||
|
||||
c.Request.URL.Path == "/api/mj/notify" ||
|
||||
c.Request.URL.Path == "/api/invite/hits" ||
|
||||
c.Request.URL.Path == "/api/sd/imgWall" ||
|
||||
c.Request.URL.Path == "/api/sd/client" ||
|
||||
c.Request.URL.Path == "/api/dall/imgWall" ||
|
||||
c.Request.URL.Path == "/api/dall/client" ||
|
||||
c.Request.URL.Path == "/api/product/list" ||
|
||||
c.Request.URL.Path == "/api/menu/list" ||
|
||||
c.Request.URL.Path == "/api/markMap/client" ||
|
||||
c.Request.URL.Path == "/api/payment/alipay/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/hupipay/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/payjs/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/wechat/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/doPay" ||
|
||||
c.Request.URL.Path == "/api/payment/payWays" ||
|
||||
c.Request.URL.Path == "/api/suno/client" ||
|
||||
c.Request.URL.Path == "/api/suno/Detail" ||
|
||||
c.Request.URL.Path == "/api/suno/play" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 统一参数处理
|
||||
func parameterHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// GET 参数处理
|
||||
params := c.Request.URL.Query()
|
||||
for key, values := range params {
|
||||
for i, value := range values {
|
||||
params[key][i] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
// update get parameters
|
||||
c.Request.URL.RawQuery = params.Encode()
|
||||
// skip file upload requests
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
// process POST JSON request body
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 还原请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// 将请求体解析为 JSON
|
||||
var jsonData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&jsonData); err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 对 JSON 数据中的字符串值去除两端空格
|
||||
trimJSONStrings(jsonData)
|
||||
// 更新请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 递归对 JSON 数据中的字符串值去除两端空格
|
||||
func trimJSONStrings(data interface{}) {
|
||||
switch v := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[key] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for i, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[i] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 静态资源中间件
|
||||
func staticResourceMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
// 拦截生成缩略图请求
|
||||
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
|
||||
r := strings.SplitAfter(url, "imageView2")
|
||||
size := strings.Split(r[1], "/")
|
||||
if len(size) != 8 {
|
||||
c.String(http.StatusNotFound, "invalid thumb args")
|
||||
return
|
||||
}
|
||||
with := utils.IntValue(size[3], 0)
|
||||
height := utils.IntValue(size[5], 0)
|
||||
quality := utils.IntValue(size[7], 75)
|
||||
|
||||
// 打开图片文件
|
||||
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "Image not found")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(file)
|
||||
// for .webp image
|
||||
if err != nil {
|
||||
img, err = webp.Decode(file)
|
||||
}
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error decoding image")
|
||||
return
|
||||
}
|
||||
|
||||
var newImg image.Image
|
||||
if height == 0 || with == 0 {
|
||||
// 固定宽度,高度自适应
|
||||
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
|
||||
} else {
|
||||
// 生成缩略图
|
||||
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 设置图片缓存有效期为一年 (365天)
|
||||
c.Header("Cache-Control", "max-age=31536000, public")
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
c.Abort() // 中断请求
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
77
api/core/config.go
Normal file
77
api/core/config.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package core
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/utils"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewDefaultConfig() *types.AppConfig {
|
||||
return &types.AppConfig{
|
||||
Listen: "0.0.0.0:5678",
|
||||
ProxyURL: "",
|
||||
StaticDir: "./static",
|
||||
StaticUrl: "http://localhost/5678/static",
|
||||
Redis: types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
|
||||
Session: types.Session{
|
||||
SecretKey: utils.RandString(64),
|
||||
MaxAge: 86400,
|
||||
},
|
||||
ApiConfig: types.ApiConfig{},
|
||||
OSS: types.OSSConfig{
|
||||
Active: "local",
|
||||
Local: types.LocalStorageConfig{
|
||||
BaseURL: "http://localhost/5678/static/upload",
|
||||
BasePath: "./static/upload",
|
||||
},
|
||||
},
|
||||
WeChatBot: false,
|
||||
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(configFile string) (*types.AppConfig, error) {
|
||||
var config *types.AppConfig
|
||||
_, err := os.Stat(configFile)
|
||||
if err != nil {
|
||||
logger.Info("creating new config file: ", configFile)
|
||||
config = NewDefaultConfig()
|
||||
config.Path = configFile
|
||||
// save config
|
||||
err := SaveConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
_, err = toml.DecodeFile(configFile, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, err
|
||||
}
|
||||
|
||||
func SaveConfig(config *types.AppConfig) error {
|
||||
buf := new(bytes.Buffer)
|
||||
encoder := toml.NewEncoder(buf)
|
||||
if err := encoder.Encode(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(config.Path, buf.Bytes(), 0644)
|
||||
}
|
||||
118
api/core/types/chat.go
Normal file
118
api/core/types/chat.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// ApiRequest API 请求实体
|
||||
type ApiRequest struct {
|
||||
Model string `json:"model,omitempty"` // 兼容百度文心一言
|
||||
Temperature float32 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"` // 兼容百度文心一言
|
||||
Stream bool `json:"stream"`
|
||||
Messages []interface{} `json:"messages,omitempty"`
|
||||
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台
|
||||
|
||||
ToolChoice string `json:"tool_choice,omitempty"`
|
||||
|
||||
Input map[string]interface{} `json:"input,omitempty"` //兼容阿里通义千问
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty"` //兼容阿里通义千问
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type ApiResponse struct {
|
||||
Choices []ChoiceItem `json:"choices"`
|
||||
}
|
||||
|
||||
// ChoiceItem API 响应实体
|
||||
type ChoiceItem struct {
|
||||
Delta Delta `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type Delta struct {
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Content interface{} `json:"content"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
FunctionCall struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments string `json:"arguments,omitempty"`
|
||||
} `json:"function_call,omitempty"`
|
||||
}
|
||||
|
||||
// ChatSession 聊天会话对象
|
||||
type ChatSession struct {
|
||||
SessionId string `json:"session_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
ClientIP string `json:"client_ip"` // 客户端 IP
|
||||
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
||||
Model ChatModel `json:"model"` // GPT 模型
|
||||
}
|
||||
|
||||
type ChatModel struct {
|
||||
Id uint `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id"` // 绑定 API KEY
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
Error struct {
|
||||
Message string
|
||||
Type string
|
||||
Param interface{}
|
||||
Code string
|
||||
}
|
||||
}
|
||||
|
||||
const PromptMsg = "prompt" // prompt message
|
||||
const ReplyMsg = "reply" // reply message
|
||||
|
||||
// PowerType 算力日志类型
|
||||
type PowerType int
|
||||
|
||||
const (
|
||||
PowerRecharge = PowerType(1) // 充值
|
||||
PowerConsume = PowerType(2) // 消费
|
||||
PowerRefund = PowerType(3) // 任务(SD,MJ)执行失败,退款
|
||||
PowerInvite = PowerType(4) // 邀请奖励
|
||||
PowerReward = PowerType(5) // 众筹
|
||||
PowerGift = PowerType(6) // 系统赠送
|
||||
)
|
||||
|
||||
func (t PowerType) String() string {
|
||||
switch t {
|
||||
case PowerRecharge:
|
||||
return "充值"
|
||||
case PowerConsume:
|
||||
return "消费"
|
||||
case PowerRefund:
|
||||
return "退款"
|
||||
case PowerReward:
|
||||
return "众筹"
|
||||
|
||||
}
|
||||
return "其他"
|
||||
}
|
||||
|
||||
type PowerMark int
|
||||
|
||||
const (
|
||||
PowerSub = PowerMark(0)
|
||||
PowerAdd = PowerMark(1)
|
||||
)
|
||||
@@ -1,4 +1,11 @@
|
||||
package server
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -6,18 +13,14 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrConClosed = errors.New("connection closed")
|
||||
|
||||
type Client interface {
|
||||
Close()
|
||||
}
|
||||
var ErrConClosed = errors.New("connection Closed")
|
||||
|
||||
// WsClient websocket client
|
||||
type WsClient struct {
|
||||
Conn *websocket.Conn
|
||||
lock sync.Mutex
|
||||
mt int
|
||||
closed bool
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func NewWsClient(conn *websocket.Conn) *WsClient {
|
||||
@@ -25,7 +28,7 @@ func NewWsClient(conn *websocket.Conn) *WsClient {
|
||||
Conn: conn,
|
||||
lock: sync.Mutex{},
|
||||
mt: 2, // fixed bug for 'Invalid UTF-8 in text frame'
|
||||
closed: false,
|
||||
Closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,15 +36,25 @@ func (wc *WsClient) Send(message []byte) error {
|
||||
wc.lock.Lock()
|
||||
defer wc.lock.Unlock()
|
||||
|
||||
if wc.closed {
|
||||
if wc.Closed {
|
||||
return ErrConClosed
|
||||
}
|
||||
|
||||
return wc.Conn.WriteMessage(wc.mt, message)
|
||||
}
|
||||
|
||||
func (wc *WsClient) SendJson(value interface{}) error {
|
||||
wc.lock.Lock()
|
||||
defer wc.lock.Unlock()
|
||||
|
||||
if wc.Closed {
|
||||
return ErrConClosed
|
||||
}
|
||||
return wc.Conn.WriteJSON(value)
|
||||
}
|
||||
|
||||
func (wc *WsClient) Receive() (int, []byte, error) {
|
||||
if wc.closed {
|
||||
if wc.Closed {
|
||||
return 0, nil, ErrConClosed
|
||||
}
|
||||
|
||||
@@ -52,10 +65,10 @@ func (wc *WsClient) Close() {
|
||||
wc.lock.Lock()
|
||||
defer wc.lock.Unlock()
|
||||
|
||||
if wc.closed {
|
||||
if wc.Closed {
|
||||
return
|
||||
}
|
||||
|
||||
_ = wc.Conn.Close()
|
||||
wc.closed = true
|
||||
wc.Closed = true
|
||||
}
|
||||
195
api/core/types/config.go
Normal file
195
api/core/types/config.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
Path string `toml:"-"`
|
||||
Listen string
|
||||
Session Session
|
||||
AdminSession Session
|
||||
ProxyURL string
|
||||
MysqlDns string // mysql 连接地址
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
ApiConfig ApiConfig // ChatPlus API authorization configs
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
MjProxyConfigs []MjProxyConfig // MJ proxy config
|
||||
MjPlusConfigs []MjPlusConfig // MJ plus config
|
||||
WeChatBot bool // 是否启用微信机器人
|
||||
SdConfigs []StableDiffusionConfig // sd AI draw service pool
|
||||
|
||||
XXLConfig XXLConfig
|
||||
AlipayConfig AlipayConfig // 支付宝支付渠道配置
|
||||
HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置
|
||||
SmtpConfig SmtpConfig // 邮件发送配置
|
||||
JPayConfig JPayConfig // payjs 支付配置
|
||||
WechatPayConfig WechatPayConfig // 微信支付渠道配置
|
||||
TikaHost string // TiKa 服务器地址
|
||||
}
|
||||
|
||||
type SmtpConfig struct {
|
||||
UseTls bool // 是否使用 TLS 发送
|
||||
Host string
|
||||
Port int
|
||||
AppName string // 应用名称
|
||||
From string // 发件人邮箱地址
|
||||
Password string // 发件人邮箱密码
|
||||
}
|
||||
|
||||
type ApiConfig struct {
|
||||
ApiURL string
|
||||
AppId string
|
||||
Token string
|
||||
}
|
||||
|
||||
type MjProxyConfig struct {
|
||||
Enabled bool
|
||||
ApiURL string // api 地址
|
||||
Mode string // 绘画模式,可选值:fast/turbo/relax
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type StableDiffusionConfig struct {
|
||||
Enabled bool
|
||||
Model string // 模型名称
|
||||
ApiURL string
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type MjPlusConfig struct {
|
||||
Enabled bool // 如果启用了 MidJourney Plus,将会自动禁用原生的MidJourney服务
|
||||
ApiURL string // api 地址
|
||||
Mode string // 绘画模式,可选值:fast/turbo/relax
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
Enabled bool // 是否启用该支付通道
|
||||
SandBox bool // 是否沙盒环境
|
||||
AppId string // 应用 ID
|
||||
UserId string // 支付宝用户 ID
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
PublicKey string // 用户公钥文件路径
|
||||
AlipayPublicKey string // 支付宝公钥文件路径
|
||||
RootCert string // Root 秘钥路径
|
||||
NotifyURL string // 异步通知回调
|
||||
ReturnURL string // 支付成功返回地址
|
||||
}
|
||||
|
||||
type WechatPayConfig struct {
|
||||
Enabled bool // 是否启用该支付通道
|
||||
AppId string // 公众号的APPID,如:wxd678efh567hg6787
|
||||
MchId string // 直连商户的商户号,由微信支付生成并下发
|
||||
SerialNo string // 商户证书的证书序列号
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
ApiV3Key string // API V3 秘钥
|
||||
NotifyURL string // 异步通知回调
|
||||
ReturnURL string // 支付成功返回地址
|
||||
}
|
||||
|
||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
|
||||
Enabled bool // 是否启用该支付通道
|
||||
Name string // 支付名称,如:wechat/alipay
|
||||
AppId string // App ID
|
||||
AppSecret string // app 密钥
|
||||
ApiURL string // 支付网关
|
||||
NotifyURL string // 异步通知回调
|
||||
ReturnURL string // 支付成功返回地址
|
||||
}
|
||||
|
||||
// JPayConfig PayJs 支付配置
|
||||
type JPayConfig struct {
|
||||
Enabled bool
|
||||
Name string // 支付名称,默认 wechat
|
||||
AppId string // 商户 ID
|
||||
PrivateKey string // 私钥
|
||||
ApiURL string // API 网关
|
||||
NotifyURL string // 异步回调地址
|
||||
ReturnURL string // 支付成功返回地址
|
||||
}
|
||||
|
||||
type XXLConfig struct { // XXL 任务调度配置
|
||||
Enabled bool
|
||||
ServerAddr string
|
||||
ExecutorIp string
|
||||
ExecutorPort string
|
||||
AccessToken string
|
||||
RegistryKey string
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Password string
|
||||
DB int
|
||||
}
|
||||
|
||||
// LicenseKey 存储许可证书的 KEY
|
||||
const LicenseKey = "Geek-AI-License"
|
||||
|
||||
type License struct {
|
||||
Key string `json:"key"` // 许可证书密钥
|
||||
MachineId string `json:"machine_id"` // 机器码
|
||||
ExpiredAt int64 `json:"expired_at"` // 过期时间
|
||||
IsActive bool `json:"is_active"` // 是否激活
|
||||
Configs LicenseConfig `json:"configs"`
|
||||
}
|
||||
|
||||
type LicenseConfig struct {
|
||||
UserNum int `json:"user_num"` // 用户数量
|
||||
DeCopy bool `json:"de_copy"` // 去版权
|
||||
}
|
||||
|
||||
func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
Title string `json:"title,omitempty"` // 网站标题
|
||||
Slogan string `json:"slogan,omitempty"` // 网站 slogan
|
||||
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
|
||||
Logo string `json:"logo,omitempty"`
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
VipMonthPower int `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
|
||||
|
||||
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
|
||||
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
|
||||
|
||||
RewardImg string `json:"reward_img,omitempty"` // 众筹收款二维码地址
|
||||
EnabledReward bool `json:"enabled_reward,omitempty"` // 启用众筹功能
|
||||
PowerPrice float64 `json:"power_price,omitempty"` // 算力单价
|
||||
|
||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
|
||||
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
|
||||
DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型
|
||||
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
|
||||
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
||||
|
||||
EnableContext bool `json:"enable_context,omitempty"`
|
||||
ContextDeep int `json:"context_deep,omitempty"`
|
||||
|
||||
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
|
||||
|
||||
IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
}
|
||||
27
api/core/types/function.go
Normal file
27
api/core/types/function.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type ToolCall struct {
|
||||
Type string `json:"type"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
Function Function `json:"function"`
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
70
api/core/types/locked_map.go
Normal file
70
api/core/types/locked_map.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MKey interface {
|
||||
string | int | uint
|
||||
}
|
||||
type MValue interface {
|
||||
*WsClient | *ChatSession | context.CancelFunc | []Message
|
||||
}
|
||||
type LMap[K MKey, T MValue] struct {
|
||||
lock sync.RWMutex
|
||||
data map[K]T
|
||||
}
|
||||
|
||||
func NewLMap[K MKey, T MValue]() *LMap[K, T] {
|
||||
return &LMap[K, T]{
|
||||
lock: sync.RWMutex{},
|
||||
data: make(map[K]T),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Put(key K, value T) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.data[key] = value
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Get(key K) T {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
return m.data[key]
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Has(key K) bool {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
_, ok := m.data[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) Delete(key K) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
delete(m.data, key)
|
||||
}
|
||||
|
||||
func (m *LMap[K, T]) ToList() []T {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
var s = make([]T, 0)
|
||||
for _, v := range m.data {
|
||||
s = append(s, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
24
api/core/types/order.go
Normal file
24
api/core/types/order.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type OrderStatus int
|
||||
|
||||
const (
|
||||
OrderNotPaid = OrderStatus(0)
|
||||
OrderScanned = OrderStatus(1) // 已扫码
|
||||
OrderPaidSuccess = OrderStatus(2)
|
||||
)
|
||||
|
||||
type OrderRemark struct {
|
||||
Days int `json:"days"` // 有效期
|
||||
Power int `json:"power"` // 增加算力点数
|
||||
Name string `json:"name"` // 产品名称
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
}
|
||||
48
api/core/types/oss.go
Normal file
48
api/core/types/oss.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type OSSConfig struct {
|
||||
Active string
|
||||
Local LocalStorageConfig
|
||||
Minio MiniOssConfig
|
||||
QiNiu QiNiuOssConfig
|
||||
AliYun AliYunOssConfig
|
||||
}
|
||||
type MiniOssConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
UseSSL bool
|
||||
Domain string
|
||||
}
|
||||
|
||||
type QiNiuOssConfig struct {
|
||||
Zone string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type AliYunOssConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type LocalStorageConfig struct {
|
||||
BasePath string
|
||||
BaseURL string
|
||||
}
|
||||
20
api/core/types/session.go
Normal file
20
api/core/types/session.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
const LoginUserID = "LOGIN_USER_ID"
|
||||
const LoginUserCache = "LOGIN_USER_CACHE"
|
||||
|
||||
const UserAuthHeader = "Authorization"
|
||||
const AdminAuthHeader = "Admin-Authorization"
|
||||
|
||||
// Session configs struct
|
||||
type Session struct {
|
||||
SecretKey string
|
||||
MaxAge int
|
||||
}
|
||||
33
api/core/types/sms.go
Normal file
33
api/core/types/sms.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type SMSConfig struct {
|
||||
Active string
|
||||
Ali SmsConfigAli
|
||||
Bao SmsConfigBao
|
||||
}
|
||||
|
||||
// SmsConfigAli 阿里云短信平台配置
|
||||
type SmsConfigAli struct {
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Product string
|
||||
Domain string
|
||||
Sign string // 短信签名
|
||||
CodeTempId string // 验证码短信模板 ID
|
||||
}
|
||||
|
||||
// SmsConfigBao 短信宝平台配置
|
||||
type SmsConfigBao struct {
|
||||
Username string //短信宝平台注册的用户名
|
||||
Password string //短信宝平台注册的密码
|
||||
Domain string //域名
|
||||
Sign string // 短信签名
|
||||
CodeTemplate string // 验证码短信模板 匹配
|
||||
}
|
||||
96
api/core/types/task.go
Normal file
96
api/core/types/task.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// TaskType 任务类别
|
||||
type TaskType string
|
||||
|
||||
func (t TaskType) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
const (
|
||||
TaskImage = TaskType("image")
|
||||
TaskBlend = TaskType("blend")
|
||||
TaskSwapFace = TaskType("swapFace")
|
||||
TaskUpscale = TaskType("upscale")
|
||||
TaskVariation = TaskType("variation")
|
||||
)
|
||||
|
||||
// MjTask MidJourney 任务
|
||||
type MjTask struct {
|
||||
Id uint `json:"id"`
|
||||
TaskId string `json:"task_id"`
|
||||
ImgArr []string `json:"img_arr"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
NegPrompt string `json:"neg_prompt,omitempty"`
|
||||
Params string `json:"full_prompt"`
|
||||
Index int `json:"index,omitempty"`
|
||||
MessageId string `json:"message_id,omitempty"`
|
||||
MessageHash string `json:"message_hash,omitempty"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
}
|
||||
|
||||
type SdTask struct {
|
||||
Id int `json:"id"` // job 数据库ID
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
Params SdTaskParams `json:"params"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
}
|
||||
|
||||
type SdTaskParams struct {
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
NegPrompt string `json:"neg_prompt"` // 反向提示词
|
||||
Steps int `json:"steps"` // 迭代步数,默认20
|
||||
Sampler string `json:"sampler"` // 采样器
|
||||
Scheduler string `json:"scheduler"` // 采样调度
|
||||
FaceFix bool `json:"face_fix"` // 面部修复
|
||||
CfgScale float32 `json:"cfg_scale"` //引导系数,默认 7
|
||||
Seed int64 `json:"seed"` // 随机数种子
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
HdFix bool `json:"hd_fix"` // 启用高清修复
|
||||
HdRedrawRate float32 `json:"hd_redraw_rate"` // 高清修复重绘幅度
|
||||
HdScale int `json:"hd_scale"` // 放大倍数
|
||||
HdScaleAlg string `json:"hd_scale_alg"` // 放大算法
|
||||
HdSteps int `json:"hd_steps"` // 高清修复迭代步数
|
||||
}
|
||||
|
||||
// DallTask DALL-E task
|
||||
type DallTask struct {
|
||||
JobId uint `json:"job_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
|
||||
Power int `json:"power"`
|
||||
}
|
||||
|
||||
type SunoTask struct {
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
Type int `json:"type"`
|
||||
TaskId string `json:"task_id"`
|
||||
Title string `json:"title"`
|
||||
RefTaskId string `json:"ref_task_id"`
|
||||
RefSongId string `json:"ref_song_id"`
|
||||
Prompt string `json:"prompt"` // 提示词/歌词
|
||||
Tags string `json:"tags"`
|
||||
Model string `json:"model"`
|
||||
Instrumental bool `json:"instrumental"` // 是否纯音乐
|
||||
ExtendSecs int `json:"extend_secs"` // 延长秒杀
|
||||
}
|
||||
45
api/core/types/web.go
Normal file
45
api/core/types/web.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package types
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// BizVo 业务返回 VO
|
||||
type BizVo struct {
|
||||
Code BizCode `json:"code"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PageSize int `json:"page_size,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// WsMessage Websocket message
|
||||
type WsMessage struct {
|
||||
Type WsMsgType `json:"type"` // 消息类别,start, end, img
|
||||
Content interface{} `json:"content"`
|
||||
}
|
||||
|
||||
type WsMsgType string
|
||||
|
||||
const (
|
||||
WsStart = WsMsgType("start")
|
||||
WsMiddle = WsMsgType("middle")
|
||||
WsEnd = WsMsgType("end")
|
||||
WsErr = WsMsgType("error")
|
||||
)
|
||||
|
||||
type BizCode int
|
||||
|
||||
const (
|
||||
Success = BizCode(0)
|
||||
Failed = BizCode(1)
|
||||
NotAuthorized = BizCode(401) // 未授权
|
||||
|
||||
OkMsg = "Success"
|
||||
ErrorMsg = "系统开小差了"
|
||||
InvalidArgs = "非法参数或参数解析失败"
|
||||
)
|
||||
123
api/go.mod
Normal file
123
api/go.mod
Normal file
@@ -0,0 +1,123 @@
|
||||
module geekai
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.4
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.1.0
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
|
||||
github.com/eatmoreapple/openwechat v1.2.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/imroc/req/v3 v3.37.2
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
|
||||
github.com/minio/minio-go/v7 v7.0.62
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
go.uber.org/zap v1.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/mysql v1.4.7
|
||||
)
|
||||
|
||||
require github.com/xxl-job/xxl-job-executor-go v1.2.0
|
||||
|
||||
require (
|
||||
github.com/go-pay/gopay v1.5.101
|
||||
github.com/google/go-tika v0.3.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/mojocn/base64Captcha v1.3.6
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/image v0.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-pay/crypto v0.0.1 // indirect
|
||||
github.com/go-pay/errgroup v0.0.2 // indirect
|
||||
github.com/go-pay/util v0.0.2 // indirect
|
||||
github.com/go-pay/xlog v0.0.2 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
||||
github.com/go-basic/ipv4 v1.0.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/quic-go v0.45.0 // indirect
|
||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
go.uber.org/dig v1.16.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/fx v1.19.3
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
gorm.io/gorm v1.25.1
|
||||
)
|
||||
364
api/go.sum
Normal file
364
api/go.sum
Normal file
@@ -0,0 +1,364 @@
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
|
||||
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
||||
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
|
||||
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-pay/crypto v0.0.1 h1:B6InT8CLfSLc6nGRVx9VMJRBBazFMjr293+jl0lLXUY=
|
||||
github.com/go-pay/crypto v0.0.1/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes=
|
||||
github.com/go-pay/errgroup v0.0.2 h1:5mZMdm0TDClDm2S3G0/sm0f8AuQRtz0dOrTHDR9R8Cc=
|
||||
github.com/go-pay/errgroup v0.0.2/go.mod h1:0+4b8mvFMS71MIzsaC+gVvB4x37I93lRb2dqrwuU8x8=
|
||||
github.com/go-pay/gopay v1.5.101 h1:rVb+sfv6hiQtknAlZnTTLvU27NvFJ4p0yglN/vPpGXI=
|
||||
github.com/go-pay/gopay v1.5.101/go.mod h1:AW4Yj8jDZX9BM1/GTLTY1Gy5SHjiq8kQvG5sBTN2sxI=
|
||||
github.com/go-pay/util v0.0.2 h1:goJ4f6kNY5zzdtg1Cj8oWC+Cw7bfg/qq2rJangMAb9U=
|
||||
github.com/go-pay/util v0.0.2/go.mod h1:qM8VbyF1n7YAPZBSJONSPMPsPedhUTktewUAdf1AjPg=
|
||||
github.com/go-pay/xlog v0.0.2 h1:kUg5X8/5VZAPDg1J5eGjA3MG0/H5kK6Ew0dW/Bycsws=
|
||||
github.com/go-pay/xlog v0.0.2/go.mod h1:DbjMADPK4+Sjxj28ekK9goqn4zmyY4hql/zRiab+S9E=
|
||||
github.com/go-pay/xtime v0.0.2 h1:7YR4/iuELsEHpJ6LUO0SVK80hQxDO9MLCfuVYIiTCRM=
|
||||
github.com/go-pay/xtime v0.0.2/go.mod h1:W1yRbJaSt4CSBcdAtLBQ8xajiN/Pl5hquGczUcUE9xE=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
|
||||
github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
|
||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc=
|
||||
github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw=
|
||||
github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
|
||||
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
||||
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w=
|
||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
|
||||
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
|
||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFUE=
|
||||
github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
|
||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
|
||||
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
|
||||
go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA=
|
||||
go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
|
||||
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
272
api/handler/admin/admin_handler.go
Normal file
272
api/handler/admin/admin_handler.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// Manager 管理员
|
||||
type Manager struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"` // 验证码
|
||||
CaptchaId string `json:"captcha_id"` // 验证码id
|
||||
}
|
||||
|
||||
const SuperManagerID = 1
|
||||
|
||||
type ManagerHandler struct {
|
||||
handler.BaseHandler
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client) *ManagerHandler {
|
||||
return &ManagerHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, redis: client}
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
var data Manager
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "请检查用户名或者密码是否填写正确")
|
||||
return
|
||||
}
|
||||
password := utils.GenPassword(data.Password, manager.Salt)
|
||||
if password != manager.Password {
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 超级管理员默认是ID:1
|
||||
if manager.Id != SuperManagerID && manager.Status == false {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": manager.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.AdminSession.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key := fmt.Sprintf("admin/%d", manager.Id)
|
||||
if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后登录时间和IP
|
||||
manager.LastLoginIp = c.ClientIP()
|
||||
manager.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Updates(&manager)
|
||||
|
||||
var result = struct {
|
||||
IsSuperAdmin bool `json:"is_super_admin"`
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
IsSuperAdmin: manager.Id == 1,
|
||||
Token: tokenString,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, result)
|
||||
}
|
||||
|
||||
// Logout 注销
|
||||
func (h *ManagerHandler) Logout(c *gin.Context) {
|
||||
key := h.GetUserKey(c)
|
||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||
logger.Error("error with delete session: ", err)
|
||||
} else {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Session 会话检测
|
||||
func (h *ManagerHandler) Session(c *gin.Context) {
|
||||
id := h.GetLoginUserId(c)
|
||||
key := fmt.Sprintf("admin/%d", id)
|
||||
if _, err := h.redis.Get(context.Background(), key).Result(); err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Where("id", id).First(&manager)
|
||||
if res.Error != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, manager)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *ManagerHandler) List(c *gin.Context) {
|
||||
var items []model.AdminUser
|
||||
res := h.DB.Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
users := make([]vo.AdminUser, 0)
|
||||
for _, item := range items {
|
||||
var u vo.AdminUser
|
||||
err := utils.CopyObject(item, &u)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
u.Id = item.Id
|
||||
u.CreatedAt = item.CreatedAt.Unix()
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, users)
|
||||
|
||||
}
|
||||
|
||||
func (h *ManagerHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("username", data.Username).First(&user)
|
||||
if res.Error == nil {
|
||||
resp.ERROR(c, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成密码
|
||||
salt := utils.RandString(8)
|
||||
password := utils.GenPassword(data.Password, salt)
|
||||
res = h.DB.Save(&model.AdminUser{
|
||||
Username: data.Username,
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
Status: data.Status,
|
||||
})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "failed with update database")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Remove 删除管理员
|
||||
func (h *ManagerHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if id == SuperManagerID {
|
||||
resp.ERROR(c, "超级管理员不能删除")
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("id", id).Delete(&model.AdminUser{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Enable 启用/禁用
|
||||
func (h *ManagerHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("id", data.Id).UpdateColumn("status", data.Enabled)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *ManagerHandler) ResetPass(c *gin.Context) {
|
||||
id := h.GetLoginUserId(c)
|
||||
if id != SuperManagerID {
|
||||
resp.ERROR(c, "只有超级管理员能够进行该操作")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.AdminUser
|
||||
res := h.DB.Where("id", data.Id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
145
api/handler/admin/api_key_handler.go
Normal file
145
api/handler/admin/api_key_handler.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApiKeyHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
|
||||
return &ApiKeyHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}}
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
ApiURL string `json:"api_url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := model.ApiKey{}
|
||||
if data.Id > 0 {
|
||||
h.DB.Find(&apiKey, data.Id)
|
||||
}
|
||||
apiKey.Value = data.Value
|
||||
apiKey.Type = data.Type
|
||||
apiKey.ApiURL = data.ApiURL
|
||||
apiKey.Enabled = data.Enabled
|
||||
apiKey.ProxyURL = data.ProxyURL
|
||||
apiKey.Name = data.Name
|
||||
res := h.DB.Save(&apiKey)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
var keyVo vo.ApiKey
|
||||
err := utils.CopyObject(apiKey, &keyVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败!")
|
||||
return
|
||||
}
|
||||
keyVo.Id = apiKey.Id
|
||||
keyVo.CreatedAt = apiKey.CreatedAt.Unix()
|
||||
resp.SUCCESS(c, keyVo)
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) List(c *gin.Context) {
|
||||
status := h.GetBool(c, "status")
|
||||
t := h.GetTrim(c, "type")
|
||||
platform := h.GetTrim(c, "platform")
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if status {
|
||||
session = session.Where("enabled", true)
|
||||
}
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
if platform != "" {
|
||||
session = session.Where("platform", platform)
|
||||
}
|
||||
|
||||
var items []model.ApiKey
|
||||
var keys = make([]vo.ApiKey, 0)
|
||||
res := session.Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var key vo.ApiKey
|
||||
err := utils.CopyObject(item, &key)
|
||||
if err == nil {
|
||||
key.Id = item.Id
|
||||
key.CreatedAt = item.CreatedAt.Unix()
|
||||
key.UpdatedAt = item.UpdatedAt.Unix()
|
||||
keys = append(keys, key)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, keys)
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("id", id).Delete(&model.ApiKey{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
269
api/handler/admin/chat_handler.go
Normal file
269
api/handler/admin/chat_handler.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler {
|
||||
return &ChatHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
type chatItemVo struct {
|
||||
Username string `json:"username"`
|
||||
UserId uint `json:"user_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
Role vo.ChatRole `json:"role"`
|
||||
Model string `json:"model"`
|
||||
Token int `json:"token"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
MsgNum int `json:"msg_num"` // 消息数量
|
||||
}
|
||||
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Title string `json:"title"`
|
||||
UserId uint `json:"user_id"`
|
||||
Model string `json:"model"`
|
||||
CreateAt []string `json:"created_time"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Title != "" {
|
||||
session = session.Where("title LIKE ?", "%"+data.Title+"%")
|
||||
}
|
||||
if data.UserId > 0 {
|
||||
session = session.Where("user_id = ?", data.UserId)
|
||||
}
|
||||
if data.Model != "" {
|
||||
session = session.Where("model = ?", data.Model)
|
||||
}
|
||||
if len(data.CreateAt) == 2 {
|
||||
start := utils.Str2stamp(data.CreateAt[0] + " 00:00:00")
|
||||
end := utils.Str2stamp(data.CreateAt[1] + " 00:00:00")
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.ChatItem{}).Count(&total)
|
||||
var items []model.ChatItem
|
||||
var list = make([]chatItemVo, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
userIds := make([]uint, 0)
|
||||
chatIds := make([]string, 0)
|
||||
roleIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
chatIds = append(chatIds, item.ChatId)
|
||||
roleIds = append(roleIds, item.RoleId)
|
||||
}
|
||||
var messages []model.ChatMessage
|
||||
var users []model.User
|
||||
var roles []model.ChatRole
|
||||
h.DB.Where("chat_id IN ?", chatIds).Find(&messages)
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
h.DB.Where("id IN ?", roleIds).Find(&roles)
|
||||
|
||||
tokenMap := make(map[string]int)
|
||||
userMap := make(map[uint]string)
|
||||
msgMap := make(map[string]int)
|
||||
roleMap := make(map[uint]vo.ChatRole)
|
||||
for _, msg := range messages {
|
||||
tokenMap[msg.ChatId] += msg.Tokens
|
||||
msgMap[msg.ChatId] += 1
|
||||
}
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
for _, r := range roles {
|
||||
var roleVo vo.ChatRole
|
||||
err := utils.CopyObject(r, &roleVo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
roleMap[r.Id] = roleVo
|
||||
}
|
||||
for _, item := range items {
|
||||
list = append(list, chatItemVo{
|
||||
UserId: item.UserId,
|
||||
Username: userMap[item.UserId],
|
||||
ChatId: item.ChatId,
|
||||
Title: item.Title,
|
||||
Model: item.Model,
|
||||
Token: tokenMap[item.ChatId],
|
||||
MsgNum: msgMap[item.ChatId],
|
||||
Role: roleMap[item.RoleId],
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
type chatMessageVo struct {
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Model string `json:"model"`
|
||||
Token int `json:"token"`
|
||||
Icon string `json:"icon"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// Messages 读取聊天记录列表
|
||||
func (h *ChatHandler) Messages(c *gin.Context) {
|
||||
var data struct {
|
||||
UserId uint `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
Model string `json:"model"`
|
||||
CreateAt []string `json:"created_time"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Content != "" {
|
||||
session = session.Where("content LIKE ?", "%"+data.Content+"%")
|
||||
}
|
||||
if data.UserId > 0 {
|
||||
session = session.Where("user_id = ?", data.UserId)
|
||||
}
|
||||
if data.Model != "" {
|
||||
session = session.Where("model = ?", data.Model)
|
||||
}
|
||||
if len(data.CreateAt) == 2 {
|
||||
start := utils.Str2stamp(data.CreateAt[0] + " 00:00:00")
|
||||
end := utils.Str2stamp(data.CreateAt[1] + " 00:00:00")
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.ChatMessage{}).Count(&total)
|
||||
var items []model.ChatMessage
|
||||
var list = make([]chatMessageVo, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
userIds := make([]uint, 0)
|
||||
for _, item := range items {
|
||||
userIds = append(userIds, item.UserId)
|
||||
}
|
||||
var users []model.User
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
userMap := make(map[uint]string)
|
||||
for _, user := range users {
|
||||
userMap[user.Id] = user.Username
|
||||
}
|
||||
for _, item := range items {
|
||||
list = append(list, chatMessageVo{
|
||||
Id: item.Id,
|
||||
UserId: item.UserId,
|
||||
Username: userMap[item.UserId],
|
||||
Content: item.Content,
|
||||
Model: item.Model,
|
||||
Token: item.Tokens,
|
||||
Icon: item.Icon,
|
||||
Type: item.Type,
|
||||
CreatedAt: item.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
// History 获取聊天历史记录
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, messages)
|
||||
}
|
||||
|
||||
// RemoveChat 删除对话
|
||||
func (h *ChatHandler) RemoveChat(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
if chatId == "" {
|
||||
resp.ERROR(c, "请传入 ChatId")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
// 删除聊天记录
|
||||
res := tx.Unscoped().Debug().Where("chat_id = ?", chatId).Delete(&model.ChatMessage{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "failed to remove chat message")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除对话
|
||||
res = tx.Unscoped().Where("chat_id = ?", chatId).Delete(model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
tx.Rollback() // 回滚
|
||||
resp.ERROR(c, "failed to remove chat")
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// RemoveMessage 删除聊天记录
|
||||
func (h *ChatHandler) RemoveMessage(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
tx := h.DB.Unscoped().Where("id = ?", id).Delete(&model.ChatMessage{})
|
||||
if tx.Error != nil {
|
||||
logger.Error("error with update database:", tx.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
196
api/handler/admin/chat_model_handler.go
Normal file
196
api/handler/admin/chat_model_handler.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatModelHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
return &ChatModelHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortNum int `json:"sort_num"`
|
||||
Open bool `json:"open"`
|
||||
Platform string `json:"platform"`
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
item := model.ChatModel{}
|
||||
// 更新
|
||||
if data.Id > 0 {
|
||||
h.DB.Where("id", data.Id).First(&item)
|
||||
}
|
||||
|
||||
item.Name = data.Name
|
||||
item.Value = data.Value
|
||||
item.Enabled = data.Enabled
|
||||
item.SortNum = data.SortNum
|
||||
item.Open = data.Open
|
||||
item.Power = data.Power
|
||||
item.MaxTokens = data.MaxTokens
|
||||
item.MaxContext = data.MaxContext
|
||||
item.Temperature = data.Temperature
|
||||
item.KeyId = data.KeyId
|
||||
|
||||
var res *gorm.DB
|
||||
if data.Id > 0 {
|
||||
res = h.DB.Save(&item)
|
||||
} else {
|
||||
res = h.DB.Create(&item)
|
||||
}
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.ChatModel
|
||||
err := utils.CopyObject(item, &itemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败!")
|
||||
return
|
||||
}
|
||||
itemVo.Id = item.Id
|
||||
itemVo.CreatedAt = item.CreatedAt.Unix()
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
name := h.GetTrim(c, "name")
|
||||
if enable {
|
||||
session = session.Where("enabled", enable)
|
||||
}
|
||||
if name != "" {
|
||||
session = session.Where("name LIKE ?", name+"%")
|
||||
}
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.SUCCESS(c, cms)
|
||||
return
|
||||
}
|
||||
|
||||
// initialize key name
|
||||
keyIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
keyIds = append(keyIds, v.KeyId)
|
||||
}
|
||||
var keys []model.ApiKey
|
||||
keyMap := make(map[uint]string)
|
||||
h.DB.Where("id IN ?", keyIds).Find(&keys)
|
||||
for _, v := range keys {
|
||||
keyMap[v.Id] = v.Name
|
||||
}
|
||||
for _, item := range items {
|
||||
var cm vo.ChatModel
|
||||
err := utils.CopyObject(item, &cm)
|
||||
if err == nil {
|
||||
cm.Id = item.Id
|
||||
cm.CreatedAt = item.CreatedAt.Unix()
|
||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||
cm.KeyName = keyMap[uint(item.KeyId)]
|
||||
cms = append(cms, cm)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, cms)
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatModelHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("id = ?", id).Delete(&model.ChatModel{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
163
api/handler/admin/chat_role_handler.go
Normal file
163
api/handler/admin/chat_role_handler.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatRoleHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
||||
return &ChatRoleHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// Save 创建或者更新某个角色
|
||||
func (h *ChatRoleHandler) Save(c *gin.Context) {
|
||||
var data vo.ChatRole
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var role model.ChatRole
|
||||
err := utils.CopyObject(data, &role)
|
||||
if err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
role.Id = data.Id
|
||||
if data.CreatedAt > 0 {
|
||||
role.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
}
|
||||
res := h.DB.Save(&role)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
// 填充 ID 数据
|
||||
data.Id = role.Id
|
||||
data.CreatedAt = role.CreatedAt.Unix()
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
var items []model.ChatRole
|
||||
var roles = make([]vo.ChatRole, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No data found")
|
||||
return
|
||||
}
|
||||
|
||||
// initialize model mane for role
|
||||
modelIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
if v.ModelId > 0 {
|
||||
modelIds = append(modelIds, v.ModelId)
|
||||
}
|
||||
}
|
||||
|
||||
modelNameMap := make(map[int]string)
|
||||
if len(modelIds) > 0 {
|
||||
var models []model.ChatModel
|
||||
tx := h.DB.Where("id IN ?", modelIds).Find(&models)
|
||||
if tx.Error == nil {
|
||||
for _, m := range models {
|
||||
modelNameMap[int(m.Id)] = m.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var role vo.ChatRole
|
||||
err := utils.CopyObject(v, &role)
|
||||
if err == nil {
|
||||
role.Id = v.Id
|
||||
role.CreatedAt = v.CreatedAt.Unix()
|
||||
role.UpdatedAt = v.UpdatedAt.Unix()
|
||||
role.ModelName = modelNameMap[role.ModelId]
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, roles)
|
||||
}
|
||||
|
||||
// Sort 更新角色排序
|
||||
func (h *ChatRoleHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatRoleHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatRoleHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
res := h.DB.Where("id", id).Delete(&model.ChatRole{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "删除失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
203
api/handler/admin/config_handler.go
Normal file
203
api/handler/admin/config_handler.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
mjServicePool *mj.ServicePool
|
||||
sdServicePool *sd.ServicePool
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService, mjPool *mj.ServicePool, sdPool *sd.ServicePool) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
levelDB: levelDB,
|
||||
mjServicePool: mjPool,
|
||||
sdServicePool: sdPool,
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
Config struct {
|
||||
types.SystemConfig
|
||||
Content string `json:"content,omitempty"`
|
||||
Updated bool `json:"updated,omitempty"`
|
||||
} `json:"config"`
|
||||
ConfigBak types.SystemConfig `json:"config_bak,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// ONLY authorized user can change the copyright
|
||||
if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy {
|
||||
resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权")
|
||||
return
|
||||
}
|
||||
|
||||
value := utils.JsonEncode(&data.Config)
|
||||
config := model.Config{Key: data.Key, Config: value}
|
||||
res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if config.Id > 0 {
|
||||
config.Config = value
|
||||
res := h.DB.Updates(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// update config cache for AppServer
|
||||
var cfg model.Config
|
||||
h.DB.Where("marker", data.Key).First(&cfg)
|
||||
var err error
|
||||
if data.Key == "system" {
|
||||
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to update config cache: "+err.Error())
|
||||
return
|
||||
}
|
||||
logger.Infof("Update AppServer's config successfully: %v", config.Config)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, config)
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// Active 激活系统
|
||||
func (h *ConfigHandler) Active(c *gin.Context) {
|
||||
var data struct {
|
||||
License string `json:"license"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
info, err := host.Info()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.licenseService.ActiveLicense(data.License, info.HostID)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, info.HostID)
|
||||
}
|
||||
|
||||
// GetLicense 获取 License 信息
|
||||
func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license)
|
||||
}
|
||||
|
||||
// GetAppConfig 获取内置配置
|
||||
func (h *ConfigHandler) GetAppConfig(c *gin.Context) {
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"mj_plus": h.App.Config.MjPlusConfigs,
|
||||
"mj_proxy": h.App.Config.MjProxyConfigs,
|
||||
"sd": h.App.Config.SdConfigs,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveDrawingConfig 保存AI绘画配置
|
||||
func (h *ConfigHandler) SaveDrawingConfig(c *gin.Context) {
|
||||
var data struct {
|
||||
Sd []types.StableDiffusionConfig `json:"sd"`
|
||||
MjPlus []types.MjPlusConfig `json:"mj_plus"`
|
||||
MjProxy []types.MjProxyConfig `json:"mj_proxy"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
changed := false
|
||||
if configChanged(data.Sd, h.App.Config.SdConfigs) {
|
||||
logger.Debugf("SD 配置变动了")
|
||||
h.App.Config.SdConfigs = data.Sd
|
||||
h.sdServicePool.InitServices(data.Sd)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if configChanged(data.MjPlus, h.App.Config.MjPlusConfigs) || configChanged(data.MjProxy, h.App.Config.MjProxyConfigs) {
|
||||
logger.Debugf("MidJourney 配置变动了")
|
||||
h.App.Config.MjPlusConfigs = data.MjPlus
|
||||
h.App.Config.MjProxyConfigs = data.MjProxy
|
||||
h.mjServicePool.InitServices(data.MjPlus, data.MjProxy)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
err := core.SaveConfig(h.App.Config)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新配置文档失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
|
||||
func configChanged(c1 interface{}, c2 interface{}) bool {
|
||||
encode1 := utils.JsonEncode(c1)
|
||||
encode2 := utils.JsonEncode(c2)
|
||||
return utils.Md5(encode1) != utils.Md5(encode2)
|
||||
}
|
||||
124
api/handler/admin/dashboard_handler.go
Normal file
124
api/handler/admin/dashboard_handler.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DashboardHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
|
||||
return &DashboardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
type statsVo struct {
|
||||
Users int64 `json:"users"`
|
||||
Chats int64 `json:"chats"`
|
||||
Tokens int `json:"tokens"`
|
||||
Income float64 `json:"income"`
|
||||
Chart map[string]map[string]float64 `json:"chart"`
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
stats := statsVo{}
|
||||
// new users statistic
|
||||
var userCount int64
|
||||
now := time.Now()
|
||||
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
res := h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount)
|
||||
if res.Error == nil {
|
||||
stats.Users = userCount
|
||||
}
|
||||
|
||||
// new chats statistic
|
||||
var chatCount int64
|
||||
res = h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
|
||||
if res.Error == nil {
|
||||
stats.Chats = chatCount
|
||||
}
|
||||
|
||||
// tokens took stats
|
||||
var historyMessages []model.ChatMessage
|
||||
res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
stats.Tokens += item.Tokens
|
||||
}
|
||||
|
||||
// 众筹收入
|
||||
var rewards []model.Reward
|
||||
res = h.DB.Where("created_at > ?", zeroTime).Find(&rewards)
|
||||
for _, item := range rewards {
|
||||
stats.Income += item.Amount
|
||||
}
|
||||
|
||||
// 订单收入
|
||||
var orders []model.Order
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
|
||||
for _, item := range orders {
|
||||
stats.Income += item.Amount
|
||||
}
|
||||
|
||||
// 统计7天的订单的图表
|
||||
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
|
||||
var statsChart = make(map[string]map[string]float64)
|
||||
//// 初始化
|
||||
var userStatistic, historyMessagesStatistic, incomeStatistic = make(map[string]float64), make(map[string]float64), make(map[string]float64)
|
||||
for i := 0; i < 7; i++ {
|
||||
var initTime = time.Date(now.Year(), now.Month(), now.Day()-i, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
|
||||
userStatistic[initTime] = float64(0)
|
||||
historyMessagesStatistic[initTime] = float64(0)
|
||||
incomeStatistic[initTime] = float64(0)
|
||||
}
|
||||
|
||||
// 统计用户7天增加的曲线
|
||||
var users []model.User
|
||||
res = h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users)
|
||||
if res.Error == nil {
|
||||
for _, item := range users {
|
||||
userStatistic[item.CreatedAt.Format("2006-01-02")] += 1
|
||||
}
|
||||
}
|
||||
|
||||
// 统计7天Token 消耗
|
||||
res = h.DB.Where("created_at > ?", startDate).Find(&historyMessages)
|
||||
for _, item := range historyMessages {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens)
|
||||
}
|
||||
|
||||
// 浮点数相加?
|
||||
// 统计最近7天的众筹
|
||||
res = h.DB.Where("created_at > ?", startDate).Find(&rewards)
|
||||
for _, item := range rewards {
|
||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||
}
|
||||
|
||||
// 统计最近7天的订单
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
|
||||
for _, item := range orders {
|
||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||
}
|
||||
|
||||
statsChart["users"] = userStatistic
|
||||
statsChart["historyMessage"] = historyMessagesStatistic
|
||||
statsChart["orders"] = incomeStatistic
|
||||
|
||||
stats.Chart = statsChart
|
||||
|
||||
resp.SUCCESS(c, stats)
|
||||
}
|
||||
130
api/handler/admin/function_handler.go
Normal file
130
api/handler/admin/function_handler.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FunctionHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewFunctionHandler(app *core.AppServer, db *gorm.DB) *FunctionHandler {
|
||||
return &FunctionHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) Save(c *gin.Context) {
|
||||
var data vo.Function
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var f = model.Function{
|
||||
Id: data.Id,
|
||||
Name: data.Name,
|
||||
Label: data.Label,
|
||||
Description: data.Description,
|
||||
Parameters: utils.JsonEncode(data.Parameters),
|
||||
Action: data.Action,
|
||||
Token: data.Token,
|
||||
Enabled: data.Enabled,
|
||||
}
|
||||
|
||||
res := h.DB.Save(&f)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "error with save data:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
data.Id = f.Id
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) List(c *gin.Context) {
|
||||
var items []model.Function
|
||||
res := h.DB.Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No data found")
|
||||
return
|
||||
}
|
||||
|
||||
functions := make([]vo.Function, 0)
|
||||
for _, v := range items {
|
||||
var f vo.Function
|
||||
err := utils.CopyObject(v, &f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
functions = append(functions, f)
|
||||
}
|
||||
resp.SUCCESS(c, functions)
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
res := h.DB.Delete(&model.Function{Id: uint(id)})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// GenToken generate function api access token
|
||||
func (h *FunctionHandler) GenToken(c *gin.Context) {
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": 0,
|
||||
"expired": 0,
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
logger.Error("error with generate token", err)
|
||||
resp.ERROR(c)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokenString)
|
||||
}
|
||||
132
api/handler/admin/menu_handler.go
Normal file
132
api/handler/admin/menu_handler.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MenuHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
return &MenuHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
URL string `json:"url"`
|
||||
SortNum int `json:"sort_num"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Save(&model.Menu{
|
||||
Id: data.Id,
|
||||
Name: data.Name,
|
||||
Icon: data.Icon,
|
||||
URL: data.URL,
|
||||
SortNum: data.SortNum,
|
||||
Enabled: data.Enabled,
|
||||
})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *MenuHandler) List(c *gin.Context) {
|
||||
var items []model.Menu
|
||||
var list = make([]vo.Menu, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Menu
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
list = append(list, product)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *MenuHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
res := h.DB.Where("id", id).Delete(&model.Menu{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
103
api/handler/admin/order_handler.go
Normal file
103
api/handler/admin/order_handler.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
return &OrderHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Status int `json:"status"`
|
||||
PayTime []string `json:"pay_time"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.OrderNo != "" {
|
||||
session = session.Where("order_no", data.OrderNo)
|
||||
}
|
||||
if len(data.PayTime) == 2 {
|
||||
start := utils.Str2stamp(data.PayTime[0] + " 00:00:00")
|
||||
end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
|
||||
session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
|
||||
}
|
||||
if data.Status >= 0 {
|
||||
session = session.Where("status", data.Status)
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.Order{}).Count(&total)
|
||||
var items []model.Order
|
||||
var list = make([]vo.Order, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var order vo.Order
|
||||
err := utils.CopyObject(item, &order)
|
||||
if err == nil {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
var item model.Order
|
||||
res := h.DB.First(&item, id)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if item.Status == types.OrderPaidSuccess {
|
||||
resp.ERROR(c, "已支付订单不允许删除!")
|
||||
return
|
||||
}
|
||||
|
||||
res = h.DB.Unscoped().Where("id = ?", id).Delete(&model.Order{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
84
api/handler/admin/power_log_handler.go
Normal file
84
api/handler/admin/power_log_handler.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PowerLogHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
|
||||
return &PowerLogHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Type int `json:"type"`
|
||||
Model string `json:"model"`
|
||||
Date []string `json:"date"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Model != "" {
|
||||
session = session.Where("model", data.Model)
|
||||
}
|
||||
if data.Type > 0 {
|
||||
session = session.Where("type", data.Type)
|
||||
}
|
||||
if len(data.Date) == 2 {
|
||||
start := data.Date[0] + " 00:00:00"
|
||||
end := data.Date[1] + " 00:00:00"
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.PowerLog{}).Count(&total)
|
||||
var items []model.PowerLog
|
||||
var list = make([]vo.PowerLog, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var log vo.PowerLog
|
||||
err := utils.CopyObject(item, &log)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log.Id = item.Id
|
||||
log.CreatedAt = item.CreatedAt.Unix()
|
||||
log.TypeStr = item.Type.String()
|
||||
list = append(list, log)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计消费算力总和
|
||||
var totalPower float64
|
||||
if len(data.Date) == 2 {
|
||||
session.Where("mark", 0).Select("SUM(amount) as total_sum").Scan(&totalPower)
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"data": vo.NewPage(total, data.Page, data.PageSize, list), "stat": totalPower})
|
||||
}
|
||||
153
api/handler/admin/product_handler.go
Normal file
153
api/handler/admin/product_handler.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
return &ProductHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Days int `json:"days"`
|
||||
Power int `json:"power"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
item := model.Product{
|
||||
Name: data.Name,
|
||||
Price: data.Price,
|
||||
Discount: data.Discount,
|
||||
Days: data.Days,
|
||||
Power: data.Power,
|
||||
Enabled: data.Enabled}
|
||||
item.Id = data.Id
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
}
|
||||
res := h.DB.Save(&item)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.Product
|
||||
err := utils.CopyObject(item, &itemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败!")
|
||||
return
|
||||
}
|
||||
itemVo.Id = item.Id
|
||||
itemVo.UpdatedAt = item.UpdatedAt.Unix()
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
var items []model.Product
|
||||
var list = make([]vo.Product, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Product
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
product.Id = item.Id
|
||||
product.CreatedAt = item.CreatedAt.Unix()
|
||||
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, product)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.Product{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
res := h.DB.Where("id", id).Delete(&model.Product{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
81
api/handler/admin/reward_handler.go
Normal file
81
api/handler/admin/reward_handler.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RewardHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
|
||||
return &RewardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *RewardHandler) List(c *gin.Context) {
|
||||
var items []model.Reward
|
||||
res := h.DB.Order("id DESC").Find(&items)
|
||||
var rewards = make([]vo.Reward, 0)
|
||||
if res.Error == nil {
|
||||
userIds := make([]uint, 0)
|
||||
for _, v := range items {
|
||||
userIds = append(userIds, v.UserId)
|
||||
}
|
||||
var users []model.User
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
var userMap = make(map[uint]model.User)
|
||||
for _, u := range users {
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var r vo.Reward
|
||||
err := utils.CopyObject(v, &r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Id = v.Id
|
||||
r.Username = userMap[v.UserId].Username
|
||||
r.CreatedAt = v.CreatedAt.Unix()
|
||||
r.UpdatedAt = v.UpdatedAt.Unix()
|
||||
rewards = append(rewards, r)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, rewards)
|
||||
}
|
||||
|
||||
func (h *RewardHandler) Remove(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if data.Id > 0 {
|
||||
res := h.DB.Where("id = ?", data.Id).Delete(&model.Reward{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
52
api/handler/admin/upload_handler.go
Normal file
52
api/handler/admin/upload_handler.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/handler"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
handler.BaseHandler
|
||||
uploaderManager *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
|
||||
return &UploadHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, uploaderManager: manager}
|
||||
}
|
||||
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
userId := 0
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: userId,
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
Ext: file.Ext,
|
||||
Size: file.Size,
|
||||
CreatedAt: time.Time{},
|
||||
})
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with update database: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, file)
|
||||
}
|
||||
262
api/handler/admin/user_handler.go
Normal file
262
api/handler/admin/user_handler.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
handler.BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
||||
}
|
||||
|
||||
// List 用户列表
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
username := h.GetTrim(c, "username")
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
var items []model.User
|
||||
var users = make([]vo.User, 0)
|
||||
var total int64
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if username != "" {
|
||||
session = session.Where("username LIKE ?", "%"+username+"%")
|
||||
}
|
||||
|
||||
session.Model(&model.User{}).Count(&total)
|
||||
res := session.Offset(offset).Limit(pageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var user vo.User
|
||||
err := utils.CopyObject(item, &user)
|
||||
if err == nil {
|
||||
user.Id = item.Id
|
||||
user.CreatedAt = item.CreatedAt.Unix()
|
||||
user.UpdatedAt = item.UpdatedAt.Unix()
|
||||
users = append(users, user)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
pageVo := vo.NewPage(total, page, pageSize, users)
|
||||
resp.SUCCESS(c, pageVo)
|
||||
}
|
||||
|
||||
func (h *UserHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
ChatRoles []string `json:"chat_roles"`
|
||||
ChatModels []int `json:"chat_models"`
|
||||
ExpiredTime string `json:"expired_time"`
|
||||
Status bool `json:"status"`
|
||||
Vip bool `json:"vip"`
|
||||
Power int `json:"power"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
var user = model.User{}
|
||||
var res *gorm.DB
|
||||
var userVo vo.User
|
||||
if data.Id > 0 { // 更新
|
||||
res = h.DB.Where("id", data.Id).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "user not found")
|
||||
return
|
||||
}
|
||||
var oldPower = user.Power
|
||||
user.Username = data.Username
|
||||
user.Status = data.Status
|
||||
user.Vip = data.Vip
|
||||
user.Power = data.Power
|
||||
user.ChatRoles = utils.JsonEncode(data.ChatRoles)
|
||||
user.ChatModels = utils.JsonEncode(data.ChatModels)
|
||||
user.ExpiredTime = utils.Str2stamp(data.ExpiredTime)
|
||||
|
||||
res = h.DB.Select("username", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
// 记录算力日志
|
||||
if oldPower != user.Power {
|
||||
mark := types.PowerAdd
|
||||
amount := user.Power - oldPower
|
||||
if oldPower > user.Power {
|
||||
mark = types.PowerSub
|
||||
amount = oldPower - user.Power
|
||||
}
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerGift,
|
||||
Amount: amount,
|
||||
Balance: user.Power,
|
||||
Mark: mark,
|
||||
Model: "管理员",
|
||||
Remark: fmt.Sprintf("后台管理员强制修改用户算力,修改前:%d,修改后:%d, 管理员ID:%d", oldPower, user.Power, h.GetLoginUserId(c)),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 检查用户是否已经存在
|
||||
h.DB.Where("username", data.Username).First(&user)
|
||||
if user.Id > 0 {
|
||||
resp.ERROR(c, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
u := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Power: data.Power,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
}
|
||||
res = h.DB.Create(&u)
|
||||
_ = utils.CopyObject(u, &userVo)
|
||||
userVo.Id = u.Id
|
||||
userVo.CreatedAt = u.CreatedAt.Unix()
|
||||
userVo.UpdatedAt = u.UpdatedAt.Unix()
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, userVo)
|
||||
}
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint
|
||||
Password string
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.First(&user, data.Id)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No user found")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c)
|
||||
} else {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UserHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 删除用户
|
||||
res := h.DB.Where("id = ?", id).Delete(&model.User{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除聊天记录
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.ChatItem{})
|
||||
// 删除聊天历史记录
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.ChatMessage{})
|
||||
// 删除登录日志
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.UserLoginLog{})
|
||||
// 删除算力日志
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.PowerLog{})
|
||||
// 删除众筹日志
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.Reward{})
|
||||
// 删除绘图任务
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.MidJourneyJob{})
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.SdJob{})
|
||||
// 删除订单
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.Order{})
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *UserHandler) LoginLog(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
var total int64
|
||||
h.DB.Model(&model.UserLoginLog{}).Count(&total)
|
||||
offset := (page - 1) * pageSize
|
||||
var items []model.UserLoginLog
|
||||
res := h.DB.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "获取数据失败")
|
||||
return
|
||||
}
|
||||
var logs []vo.UserLoginLog
|
||||
for _, v := range items {
|
||||
var log vo.UserLoginLog
|
||||
err := utils.CopyObject(v, &log)
|
||||
if err == nil {
|
||||
log.Id = v.Id
|
||||
log.CreatedAt = v.CreatedAt.Unix()
|
||||
logs = append(logs, log)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs))
|
||||
}
|
||||
94
api/handler/base_handler.go
Normal file
94
api/handler/base_handler.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type BaseHandler struct {
|
||||
App *core.AppServer
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
|
||||
return strings.TrimSpace(c.Query(key))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
|
||||
return utils.IntValue(c.PostForm(key), defaultValue)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int {
|
||||
return utils.IntValue(c.Query(key), defaultValue)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
|
||||
return utils.FloatValue(c.Query(key))
|
||||
}
|
||||
func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 {
|
||||
return utils.FloatValue(c.PostForm(key))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
|
||||
return utils.BoolValue(c.Query(key))
|
||||
}
|
||||
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
|
||||
return utils.BoolValue(c.PostForm(key))
|
||||
}
|
||||
func (h *BaseHandler) GetUserKey(c *gin.Context) string {
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("users/%v", userId)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) IsLogin(c *gin.Context) bool {
|
||||
return h.GetLoginUserId(c) > 0
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetLoginUser(c *gin.Context) (model.User, error) {
|
||||
value, exists := c.Get(types.LoginUserCache)
|
||||
if exists {
|
||||
return value.(model.User), nil
|
||||
}
|
||||
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return model.User{}, errors.New("user not login")
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.First(&user, userId)
|
||||
// 更新缓存
|
||||
if res.Error == nil {
|
||||
c.Set(types.LoginUserCache, user)
|
||||
}
|
||||
return user, res.Error
|
||||
}
|
||||
84
api/handler/captcha_handler.go
Normal file
84
api/handler/captcha_handler.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 今日头条函数实现
|
||||
|
||||
type CaptchaHandler struct {
|
||||
service *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
|
||||
return &CaptchaHandler{service: s}
|
||||
}
|
||||
|
||||
func (h *CaptchaHandler) Get(c *gin.Context) {
|
||||
data, err := h.service.Get()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// Check verify the captcha data
|
||||
func (h *CaptchaHandler) Check(c *gin.Context) {
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
Dots string `json:"dots"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if h.service.Check(data) {
|
||||
resp.SUCCESS(c)
|
||||
} else {
|
||||
resp.ERROR(c)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// SlideGet 获取滑动验证图片
|
||||
func (h *CaptchaHandler) SlideGet(c *gin.Context) {
|
||||
data, err := h.service.SlideGet()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// SlideCheck 滑动验证结果校验
|
||||
func (h *CaptchaHandler) SlideCheck(c *gin.Context) {
|
||||
var data struct {
|
||||
Key string `json:"key"`
|
||||
X int `json:"x"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if h.service.SlideCheck(data) {
|
||||
resp.SUCCESS(c)
|
||||
} else {
|
||||
resp.ERROR(c)
|
||||
}
|
||||
|
||||
}
|
||||
71
api/handler/chat_model_handler.go
Normal file
71
api/handler/chat_model_handler.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatModelHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||
return &ChatModelHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
var items []model.ChatModel
|
||||
var chatModels = make([]vo.ChatModel, 0)
|
||||
var res *gorm.DB
|
||||
session := h.DB.Session(&gorm.Session{}).Where("enabled", true)
|
||||
t := c.Query("type")
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
// 如果用户没有登录,则加载所有开放模型
|
||||
if !h.IsLogin(c) {
|
||||
res = session.Where("open", true).Order("sort_num ASC").Find(&items)
|
||||
} else {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
var models []int
|
||||
err := utils.JsonDecode(user.ChatModels, &models)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "当前用户没有订阅任何模型")
|
||||
return
|
||||
}
|
||||
// 查询用户有权限访问的模型以及所有开放的模型
|
||||
res = h.DB.Where("enabled = ?", true).Where(
|
||||
h.DB.Where("id IN ?", models).Or("open", true),
|
||||
).Order("sort_num ASC").Find(&items)
|
||||
}
|
||||
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var cm vo.ChatModel
|
||||
err := utils.CopyObject(item, &cm)
|
||||
if err == nil {
|
||||
cm.Id = item.Id
|
||||
cm.CreatedAt = item.CreatedAt.Unix()
|
||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||
chatModels = append(chatModels, cm)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, chatModels)
|
||||
}
|
||||
92
api/handler/chat_role_handler.go
Normal file
92
api/handler/chat_role_handler.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatRoleHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
||||
return &ChatRoleHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// List 获取用户聊天应用列表
|
||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var roles []model.ChatRole
|
||||
query := h.DB.Where("enable", true)
|
||||
if userId > 0 {
|
||||
var user model.User
|
||||
h.DB.First(&user, userId)
|
||||
var roleKeys []string
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
return
|
||||
}
|
||||
query = query.Where("marker IN ?", roleKeys)
|
||||
}
|
||||
if id > 0 {
|
||||
query = query.Or("id", id)
|
||||
}
|
||||
res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatRole
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
roleVos = append(roleVos, v)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, roleVos)
|
||||
}
|
||||
|
||||
// UpdateRole 更新用户聊天角色
|
||||
func (h *ChatRoleHandler) UpdateRole(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Keys []string `json:"keys"`
|
||||
}
|
||||
if err = c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys))
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
620
api/handler/chatimpl/chat_handler.go
Normal file
620
api/handler/chatimpl/chat_handler.go
Normal file
@@ -0,0 +1,620 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type ChatHandler struct {
|
||||
handler.BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
ChatContexts: types.NewLMap[string, []types.Message](),
|
||||
}
|
||||
}
|
||||
|
||||
// ChatHandle 处理聊天 WebSocket 请求
|
||||
func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
sessionId := c.Query("session_id")
|
||||
roleId := h.GetInt(c, "role_id", 0)
|
||||
chatId := c.Query("chat_id")
|
||||
modelId := h.GetInt(c, "model_id", 0)
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
var chatRole model.ChatRole
|
||||
res := h.DB.First(&chatRole, roleId)
|
||||
if res.Error != nil || !chatRole.Enable {
|
||||
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// if the role bind a model_id, use role's bind model_id
|
||||
if chatRole.ModelId > 0 {
|
||||
modelId = chatRole.ModelId
|
||||
}
|
||||
// get model info
|
||||
var chatModel model.ChatModel
|
||||
res = h.DB.First(&chatModel, modelId)
|
||||
if res.Error != nil || chatModel.Enabled == false {
|
||||
utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
session := &types.ChatSession{
|
||||
SessionId: sessionId,
|
||||
ClientIP: c.ClientIP(),
|
||||
UserId: h.GetLoginUserId(c),
|
||||
}
|
||||
|
||||
// use old chat data override the chat model and role ID
|
||||
var chat model.ChatItem
|
||||
res = h.DB.Where("chat_id = ?", chatId).First(&chat)
|
||||
if res.Error == nil {
|
||||
chatModel.Id = chat.ModelId
|
||||
roleId = int(chat.RoleId)
|
||||
}
|
||||
|
||||
session.ChatId = chatId
|
||||
session.Model = types.ChatModel{
|
||||
Id: chatModel.Id,
|
||||
Name: chatModel.Name,
|
||||
Value: chatModel.Value,
|
||||
Power: chatModel.Power,
|
||||
MaxTokens: chatModel.MaxTokens,
|
||||
MaxContext: chatModel.MaxContext,
|
||||
Temperature: chatModel.Temperature,
|
||||
KeyId: chatModel.KeyId}
|
||||
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
|
||||
client.Close()
|
||||
cancelFunc := h.ReqCancelFunc.Get(sessionId)
|
||||
if cancelFunc != nil {
|
||||
cancelFunc()
|
||||
h.ReqCancelFunc.Delete(sessionId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var message types.WsMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 心跳消息
|
||||
if message.Type == "heartbeat" {
|
||||
logger.Debug("收到 Chat 心跳消息:", message.Content)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Receive a message: ", message.Content)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.ReqCancelFunc.Put(sessionId, cancel)
|
||||
// 回复消息
|
||||
err = h.sendMessage(ctx, session, chatRole, utils.InterfaceToString(message.Content), client)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.ReplyMessage(client, err.Error())
|
||||
} else {
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
||||
logger.Infof("回答完毕: %v", message.Content)
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
||||
if !h.App.Debug {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Error("Recover message from error: ", r)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, session.UserId)
|
||||
if res.Error != nil {
|
||||
return errors.New("未授权用户,您正在进行非法操作!")
|
||||
}
|
||||
var userVo vo.User
|
||||
err := utils.CopyObject(user, &userVo)
|
||||
userVo.Id = user.Id
|
||||
if err != nil {
|
||||
return errors.New("User 对象转换失败," + err.Error())
|
||||
}
|
||||
|
||||
if userVo.Status == false {
|
||||
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
}
|
||||
|
||||
if userVo.Power < session.Model.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power)
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
return errors.New("您的账号已经过期,请联系管理员!")
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
|
||||
if promptTokens > session.Model.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
}
|
||||
|
||||
var req = types.ApiRequest{
|
||||
Model: session.Model.Value,
|
||||
Stream: true,
|
||||
}
|
||||
req.Temperature = session.Model.Temperature
|
||||
req.MaxTokens = session.Model.MaxTokens
|
||||
// OpenAI 支持函数功能
|
||||
var items []model.Function
|
||||
res = h.DB.Where("enabled", true).Find(&items)
|
||||
if res.Error == nil {
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
var parameters map[string]interface{}
|
||||
err = utils.JsonDecode(v.Parameters, ¶meters)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tool := types.Tool{
|
||||
Type: "function",
|
||||
Function: types.Function{
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
Parameters: parameters,
|
||||
},
|
||||
}
|
||||
if v, ok := parameters["required"]; v == nil || !ok {
|
||||
tool.Function.Parameters["required"] = []string{}
|
||||
}
|
||||
tools = append(tools, tool)
|
||||
}
|
||||
|
||||
if len(tools) > 0 {
|
||||
req.Tools = tools
|
||||
req.ToolChoice = "auto"
|
||||
}
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]types.Message, 0)
|
||||
messages := make([]types.Message, 0)
|
||||
if h.App.SysConfig.EnableContext {
|
||||
if h.ChatContexts.Has(session.ChatId) {
|
||||
messages = h.ChatContexts.Get(session.ChatId)
|
||||
} else {
|
||||
_ = utils.JsonDecode(role.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
|
||||
if res.Error == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算当前请求的 token 总长度,确保不会超出最大上下文长度
|
||||
// MaxContextLength = Response + Tool + Prompt + Context
|
||||
tokens := req.MaxTokens // 最大响应长度
|
||||
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
|
||||
tokens += tks + promptTokens
|
||||
|
||||
for _, v := range messages {
|
||||
tks, _ := utils.CalcTokens(v.Content, req.Model)
|
||||
// 上下文 token 超出了模型的最大上下文长度
|
||||
if tokens+tks >= session.Model.MaxContext {
|
||||
break
|
||||
}
|
||||
|
||||
// 上下文的深度超出了模型的最大上下文深度
|
||||
if len(chatCtx) >= h.App.SysConfig.ContextDeep {
|
||||
break
|
||||
}
|
||||
|
||||
tokens += tks
|
||||
chatCtx = append(chatCtx, v)
|
||||
}
|
||||
|
||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
||||
}
|
||||
reqMgs := make([]interface{}, 0)
|
||||
for _, m := range chatCtx {
|
||||
reqMgs = append(reqMgs, m)
|
||||
}
|
||||
|
||||
fullPrompt := prompt
|
||||
text := prompt
|
||||
// extract files in prompt
|
||||
files := utils.ExtractFileURLs(prompt)
|
||||
logger.Debugf("detected FILES: %+v", files)
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
|
||||
strings.HasSuffix(session.Model.Value, "claude-3")) {
|
||||
contents := make([]string, 0)
|
||||
var file model.File
|
||||
for _, v := range files {
|
||||
h.DB.Where("url = ?", v).First(&file)
|
||||
content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
} else {
|
||||
contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
|
||||
}
|
||||
|
||||
tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
|
||||
if tokens > session.Model.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
}
|
||||
logger.Debug("最终Prompt:", fullPrompt)
|
||||
|
||||
// extract images from prompt
|
||||
imgURLs := utils.ExtractImgURLs(prompt)
|
||||
logger.Debugf("detected IMG: %+v", imgURLs)
|
||||
var content interface{}
|
||||
if len(imgURLs) > 0 {
|
||||
data := make([]interface{}, 0)
|
||||
for _, v := range imgURLs {
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
data = append(data, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": v,
|
||||
},
|
||||
})
|
||||
}
|
||||
data = append(data, gin.H{
|
||||
"type": "text",
|
||||
"text": strings.TrimSpace(text),
|
||||
})
|
||||
content = data
|
||||
} else {
|
||||
content = fullPrompt
|
||||
}
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
})
|
||||
|
||||
logger.Debugf("%+v", req.Messages)
|
||||
|
||||
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
func (h *ChatHandler) Tokens(c *gin.Context) {
|
||||
var data struct {
|
||||
Text string `json:"text"`
|
||||
Model string `json:"model"`
|
||||
ChatId string `json:"chat_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
|
||||
if data.Text == "" && data.ChatId != "" {
|
||||
var item model.ChatMessage
|
||||
userId, _ := c.Get(types.LoginUserID)
|
||||
res := h.DB.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).Last(&item)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, item.Tokens)
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := utils.CalcTokens(data.Text, data.Model)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokens)
|
||||
}
|
||||
|
||||
func getTotalTokens(req types.ApiRequest) int {
|
||||
encode := utils.JsonEncode(req.Messages)
|
||||
var items []map[string]interface{}
|
||||
err := utils.JsonDecode(encode, &items)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
tokens := 0
|
||||
for _, item := range items {
|
||||
content, ok := item["content"]
|
||||
if ok && !utils.IsEmptyValue(content) {
|
||||
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
|
||||
if err == nil {
|
||||
tokens += t
|
||||
}
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// StopGenerate 停止生成
|
||||
func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
sessionId := c.Query("session_id")
|
||||
if h.ReqCancelFunc.Has(sessionId) {
|
||||
h.ReqCancelFunc.Get(sessionId)()
|
||||
h.ReqCancelFunc.Delete(sessionId)
|
||||
}
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if session.Model.KeyId > 0 {
|
||||
h.DB.Where("id", session.Model.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
// ONLY allow apiURL in blank list
|
||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debugf(utils.JsonEncode(req))
|
||||
|
||||
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request = request.WithContext(ctx)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
logger.Debugf("Sending %s request, Channel:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
// 更新API KEY 最后使用时间
|
||||
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
return client.Do(request)
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
|
||||
power := 1
|
||||
if session.Model.Power > 0 {
|
||||
power = session.Model.Power
|
||||
}
|
||||
res := h.DB.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("power", gorm.Expr("power - ?", power))
|
||||
if res.Error == nil {
|
||||
// 记录算力消费日志
|
||||
var u model.User
|
||||
h.DB.Where("id", userVo.Id).First(&u)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: userVo.Id,
|
||||
Username: userVo.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: power,
|
||||
Mark: types.PowerSub,
|
||||
Balance: u.Power,
|
||||
Model: session.Model.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *ChatHandler) saveChatHistory(
|
||||
req types.ApiRequest,
|
||||
prompt string,
|
||||
contents []string,
|
||||
message types.Message,
|
||||
chatCtx []types.Message,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
userVo vo.User,
|
||||
promptCreatedAt time.Time,
|
||||
replyCreatedAt time.Time) {
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
}
|
||||
message.Content = strings.Join(contents, "")
|
||||
useMsg := types.Message{Role: "user", Content: prompt}
|
||||
|
||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||
if h.App.SysConfig.EnableContext {
|
||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
// for prompt
|
||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
historyUserMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: template.HTMLEscapeString(prompt),
|
||||
Tokens: promptToken,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
historyUserMsg.CreatedAt = promptCreatedAt
|
||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||
res := h.DB.Save(&historyUserMsg)
|
||||
if res.Error != nil {
|
||||
logger.Error("failed to save prompt history message: ", res.Error)
|
||||
}
|
||||
|
||||
// for reply
|
||||
// 计算本次对话消耗的总 token 数量
|
||||
replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens := replyTokens + getTotalTokens(req)
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: message.Content,
|
||||
Tokens: totalTokens,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||
res = h.DB.Create(&historyReplyMsg)
|
||||
if res.Error != nil {
|
||||
logger.Error("failed to save reply history message: ", res.Error)
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
if session.Model.Power > 0 {
|
||||
h.subUserPower(userVo, session, promptToken, replyTokens)
|
||||
}
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
res = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||
if res.Error != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.UserId = userVo.Id
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
if utf8.RuneCountInString(prompt) > 30 {
|
||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||
} else {
|
||||
chatItem.Title = prompt
|
||||
}
|
||||
chatItem.Model = req.Model
|
||||
h.DB.Create(&chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
// 将AI回复消息中生成的图片链接下载到本地
|
||||
func (h *ChatHandler) extractImgUrl(text string) string {
|
||||
pattern := `!\[([^\]]*)]\(([^)]+)\)`
|
||||
re := regexp.MustCompile(pattern)
|
||||
matches := re.FindAllStringSubmatch(text, -1)
|
||||
|
||||
// 下载图片并替换链接地址
|
||||
for _, match := range matches {
|
||||
imageURL := match[2]
|
||||
logger.Debug(imageURL)
|
||||
// 对于相同地址的图片,已经被替换了,就不再重复下载了
|
||||
if !strings.Contains(text, imageURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false)
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
text = strings.ReplaceAll(text, imageURL, newImgURL)
|
||||
}
|
||||
return text
|
||||
}
|
||||
211
api/handler/chatimpl/chat_item_handler.go
Normal file
211
api/handler/chatimpl/chat_item_handler.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// List 获取会话列表
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
if !h.IsLogin(c) {
|
||||
resp.SUCCESS(c)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items = make([]vo.ChatItem, 0)
|
||||
var chats []model.ChatItem
|
||||
res := h.DB.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
|
||||
if res.Error == nil {
|
||||
var roleIds = make([]uint, 0)
|
||||
for _, chat := range chats {
|
||||
roleIds = append(roleIds, chat.RoleId)
|
||||
}
|
||||
var roles []model.ChatRole
|
||||
res = h.DB.Find(&roles, roleIds)
|
||||
if res.Error == nil {
|
||||
roleMap := make(map[uint]model.ChatRole)
|
||||
for _, role := range roles {
|
||||
roleMap[role.Id] = role
|
||||
}
|
||||
|
||||
for _, chat := range chats {
|
||||
var item vo.ChatItem
|
||||
err := utils.CopyObject(chat, &item)
|
||||
if err == nil {
|
||||
item.Id = chat.Id
|
||||
item.Icon = roleMap[chat.RoleId].Icon
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
resp.SUCCESS(c, items)
|
||||
}
|
||||
|
||||
// Update 更新会话标题
|
||||
func (h *ChatHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
res := h.DB.Model(&model.ChatItem{}).Where("chat_id = ?", data.ChatId).UpdateColumn("title", data.Title)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to update database")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// Clear 清空所有聊天记录
|
||||
func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
// 获取当前登录用户所有的聊天会话
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var chats []model.ChatItem
|
||||
res := h.DB.Where("user_id = ?", user.Id).Find(&chats)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No chats found")
|
||||
return
|
||||
}
|
||||
|
||||
var chatIds = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
chatIds = append(chatIds, chat.ChatId)
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chat.ChatId)
|
||||
}
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
res = h.DB.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.ChatMessage{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error with delete chats: %+v", err)
|
||||
resp.ERROR(c, "Failed to remove chat from database.")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// History 获取聊天历史记录
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, messages)
|
||||
}
|
||||
|
||||
// Remove 删除会话
|
||||
func (h *ChatHandler) Remove(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
if chatId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to update database")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除当前会话的聊天记录
|
||||
res = h.DB.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to remove chat from database.")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chatId)
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// Detail 对话详情,用户导出对话
|
||||
func (h *ChatHandler) Detail(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
if utils.IsEmptyValue(chatId) {
|
||||
resp.ERROR(c, "Invalid chatId")
|
||||
return
|
||||
}
|
||||
|
||||
var chatItem model.ChatItem
|
||||
res := h.DB.Where("chat_id = ?", chatId).First(&chatItem)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No chat found")
|
||||
return
|
||||
}
|
||||
|
||||
// 填充角色名称
|
||||
var role model.ChatRole
|
||||
res = h.DB.Where("id", chatItem.RoleId).First(&role)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Role not found")
|
||||
return
|
||||
}
|
||||
|
||||
var chatItemVo vo.ChatItem
|
||||
err := utils.CopyObject(chatItem, &chatItemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
chatItemVo.RoleName = role.Name
|
||||
resp.SUCCESS(c, chatItemVo)
|
||||
}
|
||||
188
api/handler/chatimpl/openai_handler.go
Normal file
188
api/handler/chatimpl/openai_handler.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
req2 "github.com/imroc/req/v3"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OPenAI 消息发送实现
|
||||
func (h *ChatHandler) sendOpenAiMessage(
|
||||
chatCtx []types.Message,
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
prompt string,
|
||||
ws *types.WsClient) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
replyCreatedAt := time.Now() // 记录回复时间
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
var contents = make([]string, 0)
|
||||
var function model.Function
|
||||
var toolCall = false
|
||||
var arguments = make([]string, 0)
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
var isNew = true
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
continue
|
||||
}
|
||||
var responseBody = types.ApiResponse{}
|
||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
if err != nil { // 数据解析出错
|
||||
return errors.New(line)
|
||||
}
|
||||
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
|
||||
continue
|
||||
}
|
||||
if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
break
|
||||
}
|
||||
|
||||
var tool types.ToolCall
|
||||
if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
|
||||
tool = responseBody.Choices[0].Delta.ToolCalls[0]
|
||||
if toolCall && tool.Function.Name == "" {
|
||||
arguments = append(arguments, tool.Function.Arguments)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容 Function Call
|
||||
fun := responseBody.Choices[0].Delta.FunctionCall
|
||||
if fun.Name != "" {
|
||||
tool = *new(types.ToolCall)
|
||||
tool.Function.Name = fun.Name
|
||||
} else if toolCall {
|
||||
arguments = append(arguments, fun.Arguments)
|
||||
continue
|
||||
}
|
||||
|
||||
if !utils.IsEmptyValue(tool) {
|
||||
res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
|
||||
if res.Error == nil {
|
||||
toolCall = true
|
||||
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: callMsg})
|
||||
contents = append(contents, callMsg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "tool_calls" ||
|
||||
responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
||||
break
|
||||
}
|
||||
|
||||
// output stopped
|
||||
if responseBody.Choices[0].FinishReason != "" {
|
||||
break // 输出完成或者输出中断了
|
||||
} else {
|
||||
content := responseBody.Choices[0].Delta.Content
|
||||
contents = append(contents, utils.InterfaceToString(content))
|
||||
if isNew {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
isNew = false
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||
})
|
||||
}
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
} else {
|
||||
logger.Error("信息读取出错:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if toolCall { // 调用函数完成任务
|
||||
params := make(map[string]interface{})
|
||||
_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
||||
logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
|
||||
params["user_id"] = userVo.Id
|
||||
var apiRes types.BizVo
|
||||
r, err := req2.C().R().SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", function.Token).
|
||||
SetBody(params).
|
||||
SetSuccessResult(&apiRes).Post(function.Action)
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
} else if r.IsErrorState() {
|
||||
errMsg = r.Status
|
||||
}
|
||||
if errMsg != "" || apiRes.Code != types.Success {
|
||||
msg := "调用函数工具出错:" + apiRes.Message + errMsg
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: msg,
|
||||
})
|
||||
contents = append(contents, msg)
|
||||
} else {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: apiRes.Data,
|
||||
})
|
||||
contents = append(contents, utils.InterfaceToString(apiRes.Data))
|
||||
}
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求 OpenAI API 失败:%s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
54
api/handler/config_handler.go
Normal file
54
api/handler/config_handler.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
||||
}
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// License 获取 License 配置
|
||||
func (h *ConfigHandler) License(c *gin.Context) {
|
||||
license := h.licenseService.GetLicense()
|
||||
resp.SUCCESS(c, license.Configs)
|
||||
}
|
||||
255
api/handler/dalle_handler.go
Normal file
255
api/handler/dalle_handler.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DallJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
service *dalle.Service
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager) *DallJobHandler {
|
||||
return &DallJobHandler{
|
||||
service: service,
|
||||
uploader: manager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *DallJobHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
if userId == 0 {
|
||||
logger.Info("Invalid user ID")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.service.Clients.Put(uint(userId), client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
client.Close()
|
||||
h.service.Clients.Delete(uint(userId))
|
||||
return
|
||||
}
|
||||
|
||||
var message types.WsMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 心跳消息
|
||||
if message.Type == "heartbeat" {
|
||||
logger.Debug("收到 DallE 心跳消息:", message.Content)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *DallJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return false
|
||||
}
|
||||
if user.Power < h.App.SysConfig.DallPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var data types.DallTask
|
||||
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
job := model.DallJob{
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
}
|
||||
res := h.DB.Create(&job)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "error with save job: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.service.PushTask(types.DallTask{
|
||||
JobId: job.Id,
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
Power: job.Power,
|
||||
})
|
||||
|
||||
client := h.service.Clients.Get(job.UserId)
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
func (h *DallJobHandler) ImgWall(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
err, jobs := h.getData(true, 0, page, pageSize, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 SD 任务列表
|
||||
func (h *DallJobHandler) JobList(c *gin.Context) {
|
||||
finish := h.GetBool(c, "finish")
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
publish := h.GetBool(c, "publish")
|
||||
|
||||
err, jobs := h.getData(finish, userId, page, pageSize, publish)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取任务列表
|
||||
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.DallJob) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if publish {
|
||||
session = session.Where("publish", publish)
|
||||
}
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
var items []model.DallJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, nil
|
||||
}
|
||||
|
||||
var jobs = make([]vo.DallJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.DallJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return nil, jobs
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *DallJobHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
var job model.DallJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// remove job recode
|
||||
res := h.DB.Delete(&model.DallJob{Id: job.Id})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Publish 发布/取消发布图片到画廊显示
|
||||
func (h *DallJobHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
res := h.DB.Model(&model.DallJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
226
api/handler/function_handler.go
Normal file
226
api/handler/function_handler.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FunctionHandler struct {
|
||||
BaseHandler
|
||||
config types.ApiConfig
|
||||
uploadManager *oss.UploaderManager
|
||||
dallService *dalle.Service
|
||||
}
|
||||
|
||||
func NewFunctionHandler(
|
||||
server *core.AppServer,
|
||||
db *gorm.DB,
|
||||
config *types.AppConfig,
|
||||
manager *oss.UploaderManager,
|
||||
dallService *dalle.Service) *FunctionHandler {
|
||||
return &FunctionHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
config: config.ApiConfig,
|
||||
uploadManager: manager,
|
||||
dallService: dallService,
|
||||
}
|
||||
}
|
||||
|
||||
type resVo struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Title string `json:"title"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Items []dataItem `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type dataItem struct {
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// check authorization
|
||||
func (h *FunctionHandler) checkAuth(c *gin.Context) error {
|
||||
tokenString := c.GetHeader(types.UserAuthHeader)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(h.App.Config.Session.SecretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with parse auth token: %v", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return errors.New("token is invalid")
|
||||
}
|
||||
|
||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||
return errors.New("token is expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WeiBo 微博热搜
|
||||
func (h *FunctionHandler) WeiBo(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Token == "" {
|
||||
resp.ERROR(c, "无效的 API Token")
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/weibo/fetch", h.config.ApiURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("AppId", h.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err))
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
builder := make([]string, 0)
|
||||
builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
|
||||
for i, v := range res.Data.Items {
|
||||
builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark))
|
||||
}
|
||||
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
|
||||
}
|
||||
|
||||
// ZaoBao 今日早报
|
||||
func (h *FunctionHandler) ZaoBao(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Token == "" {
|
||||
resp.ERROR(c, "无效的 API Token")
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/zaobao/fetch", h.config.ApiURL)
|
||||
var res resVo
|
||||
r, err := req.C().R().
|
||||
SetHeader("AppId", h.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err))
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
builder := make([]string, 0)
|
||||
builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
|
||||
for _, v := range res.Data.Items {
|
||||
builder = append(builder, v.Title)
|
||||
}
|
||||
builder = append(builder, fmt.Sprintf("%s", res.Data.Title))
|
||||
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
|
||||
}
|
||||
|
||||
// Dall3 DallE3 AI 绘图
|
||||
func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var params map[string]interface{}
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("绘画参数:%+v", params)
|
||||
var user model.User
|
||||
res := h.DB.Where("id = ?", params["user_id"]).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "当前用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.DallPower {
|
||||
resp.ERROR(c, "创建 DALL-E 绘图任务失败,算力不足")
|
||||
return
|
||||
}
|
||||
|
||||
// create dall task
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
job := model.DallJob{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
}
|
||||
res = h.DB.Create(&job)
|
||||
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
content, err := h.dallService.Image(types.DallTask{
|
||||
JobId: job.Id,
|
||||
UserId: user.Id,
|
||||
Prompt: job.Prompt,
|
||||
N: 1,
|
||||
Quality: "standard",
|
||||
Size: "1024x1024",
|
||||
Style: "vivid",
|
||||
Power: job.Power,
|
||||
}, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务执行失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
100
api/handler/invite_handler.go
Normal file
100
api/handler/invite_handler.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InviteHandler 用户邀请
|
||||
type InviteHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
|
||||
return &InviteHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// Code 获取当前用户邀请码
|
||||
func (h *InviteHandler) Code(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var inviteCode model.InviteCode
|
||||
res := h.DB.Where("user_id = ?", userId).First(&inviteCode)
|
||||
// 如果邀请码不存在,则创建一个
|
||||
if res.Error != nil {
|
||||
code := strings.ToUpper(utils.RandString(8))
|
||||
for {
|
||||
res = h.DB.Where("code = ?", code).First(&inviteCode)
|
||||
if res.Error != nil { // 不存在相同的邀请码则退出
|
||||
break
|
||||
}
|
||||
}
|
||||
inviteCode.UserId = userId
|
||||
inviteCode.Code = code
|
||||
h.DB.Create(&inviteCode)
|
||||
}
|
||||
|
||||
var codeVo vo.InviteCode
|
||||
err := utils.CopyObject(inviteCode, &codeVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "拷贝对象失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, codeVo)
|
||||
}
|
||||
|
||||
// List Log 用户邀请记录
|
||||
func (h *InviteHandler) List(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("inviter_id = ?", userId)
|
||||
var total int64
|
||||
session.Model(&model.InviteLog{}).Count(&total)
|
||||
var items []model.InviteLog
|
||||
var list = make([]vo.InviteLog, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var v vo.InviteLog
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err == nil {
|
||||
v.Id = item.Id
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
list = append(list, v)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
// Hits 访问邀请码
|
||||
func (h *InviteHandler) Hits(c *gin.Context) {
|
||||
code := c.Query("code")
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
257
api/handler/markmap_handler.go
Normal file
257
api/handler/markmap_handler.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MarkMapHandler 生成思维导图
|
||||
type MarkMapHandler struct {
|
||||
BaseHandler
|
||||
clients *types.LMap[int, *types.WsClient]
|
||||
}
|
||||
|
||||
func NewMarkMapHandler(app *core.AppServer, db *gorm.DB) *MarkMapHandler {
|
||||
return &MarkMapHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
clients: types.NewLMap[int, *types.WsClient](),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
modelId := h.GetInt(c, "model_id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.clients.Put(userId, client)
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
client.Close()
|
||||
h.clients.Delete(userId)
|
||||
return
|
||||
}
|
||||
|
||||
var message types.WsMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 心跳消息
|
||||
if message.Type == "heartbeat" {
|
||||
logger.Debug("收到 MarkMap 心跳消息:", message.Content)
|
||||
continue
|
||||
}
|
||||
// change model
|
||||
if message.Type == "model_id" {
|
||||
modelId = utils.IntValue(utils.InterfaceToString(message.Content), 0)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Receive a message: ", message.Content)
|
||||
err = h.sendMessage(client, utils.InterfaceToString(message.Content), modelId, userId)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsErr, Content: err.Error()})
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, modelId int, userId int) error {
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, userId)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("error with query user info: %v", res.Error)
|
||||
}
|
||||
var chatModel model.ChatModel
|
||||
res = h.DB.Where("id", modelId).First(&chatModel)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("error with query chat model: %v", res.Error)
|
||||
}
|
||||
|
||||
if user.Status == false {
|
||||
return errors.New("当前用户被禁用")
|
||||
}
|
||||
|
||||
if user.Power < chatModel.Power {
|
||||
return fmt.Errorf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power)
|
||||
}
|
||||
|
||||
messages := make([]interface{}, 0)
|
||||
messages = append(messages, types.Message{Role: "system", Content: `
|
||||
你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
|
||||
# Geek-AI 助手
|
||||
|
||||
## 完整的开源系统
|
||||
### 前端开源
|
||||
### 后端开源
|
||||
|
||||
## 支持各种大模型
|
||||
### OpenAI
|
||||
### Azure
|
||||
### 文心一言
|
||||
### 通义千问
|
||||
|
||||
## 集成多种收费方式
|
||||
### 支付宝
|
||||
### 微信
|
||||
|
||||
另外,除此之外不要任何解释性语句。
|
||||
`})
|
||||
messages = append(messages, types.Message{Role: "user", Content: prompt})
|
||||
var req = types.ApiRequest{
|
||||
Model: chatModel.Value,
|
||||
Stream: true,
|
||||
Messages: messages,
|
||||
}
|
||||
|
||||
var apiKey model.ApiKey
|
||||
response, err := h.doRequest(req, chatModel, &apiKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求 OpenAI API 失败: %s", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
// 循环读取 Chunk 消息
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
var isNew = true
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
continue
|
||||
}
|
||||
|
||||
var responseBody = types.ApiResponse{}
|
||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
if err != nil { // 数据解析出错
|
||||
return fmt.Errorf("error with decode data: %v", line)
|
||||
}
|
||||
|
||||
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
|
||||
continue
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" {
|
||||
break
|
||||
}
|
||||
|
||||
if isNew {
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsStart})
|
||||
isNew = false
|
||||
}
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||
})
|
||||
} // end for
|
||||
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
||||
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求 OpenAI API 失败:%s", string(body))
|
||||
}
|
||||
|
||||
// 扣减算力
|
||||
if chatModel.Power > 0 {
|
||||
res = h.DB.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", chatModel.Power))
|
||||
if res.Error == nil {
|
||||
// 记录算力消费日志
|
||||
var u model.User
|
||||
h.DB.Where("id", userId).First(&u)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: u.Id,
|
||||
Username: u.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: chatModel.Power,
|
||||
Mark: types.PowerSub,
|
||||
Balance: u.Power,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatModel, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if chatModel.KeyId > 0 {
|
||||
session = session.Where("id", chatModel.KeyId)
|
||||
} else { // use the last unused key
|
||||
session = session.Where("type", "chat").
|
||||
Where("enabled", true).Order("last_used_at ASC")
|
||||
}
|
||||
|
||||
res := session.First(apiKey)
|
||||
if res.Error != nil {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
return client.Do(request)
|
||||
}
|
||||
49
api/handler/menu_handler.go
Normal file
49
api/handler/menu_handler.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MenuHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// List 数据列表
|
||||
func (h *MenuHandler) List(c *gin.Context) {
|
||||
index := h.GetBool(c, "index")
|
||||
var items []model.Menu
|
||||
var list = make([]vo.Menu, 0)
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session = session.Where("enabled", true)
|
||||
if index {
|
||||
session = session.Where("id IN ?", h.App.SysConfig.IndexNavs)
|
||||
}
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Menu
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
list = append(list, product)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
522
api/handler/mj_handler.go
Normal file
522
api/handler/mj_handler.go
Normal file
@@ -0,0 +1,522 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MidJourneyHandler struct {
|
||||
BaseHandler
|
||||
pool *mj.ServicePool
|
||||
snowflake *service.Snowflake
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, pool *mj.ServicePool, manager *oss.UploaderManager) *MidJourneyHandler {
|
||||
return &MidJourneyHandler{
|
||||
snowflake: snowflake,
|
||||
pool: pool,
|
||||
uploader: manager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.MjPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.pool.HasAvailableService() {
|
||||
resp.ERROR(c, "MidJourney 池子中没有没有可用的服务!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *MidJourneyHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
if userId == 0 {
|
||||
logger.Info("Invalid user ID")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.pool.Clients.Put(uint(userId), client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
var data struct {
|
||||
TaskType string `json:"task_type"`
|
||||
Prompt string `json:"prompt"`
|
||||
NegPrompt string `json:"neg_prompt"`
|
||||
Rate string `json:"rate"`
|
||||
Model string `json:"model"` // 模型
|
||||
Chaos int `json:"chaos"` // 创意度取值范围: 0-100
|
||||
Raw bool `json:"raw"` // 是否开启原始模型
|
||||
Seed int64 `json:"seed"` // 随机数
|
||||
Stylize int `json:"stylize"` // 风格化
|
||||
ImgArr []string `json:"img_arr"`
|
||||
Tile bool `json:"tile"` // 重复平铺
|
||||
Quality float32 `json:"quality"` // 画质
|
||||
Iw float32 `json:"iw"`
|
||||
CRef string `json:"cref"` //生成角色一致的图像
|
||||
SRef string `json:"sref"` //生成风格一致的图像
|
||||
Cw int `json:"cw"` // 参考程度
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var params = ""
|
||||
if data.Rate != "" && !strings.Contains(params, "--ar") {
|
||||
params += " --ar " + data.Rate
|
||||
}
|
||||
if data.Seed > 0 && !strings.Contains(params, "--seed") {
|
||||
params += fmt.Sprintf(" --seed %d", data.Seed)
|
||||
}
|
||||
if data.Stylize > 0 && !strings.Contains(params, "--s") && !strings.Contains(params, "--stylize") {
|
||||
params += fmt.Sprintf(" --s %d", data.Stylize)
|
||||
}
|
||||
if data.Chaos > 0 && !strings.Contains(params, "--c") && !strings.Contains(params, "--chaos") {
|
||||
params += fmt.Sprintf(" --c %d", data.Chaos)
|
||||
}
|
||||
if len(data.ImgArr) > 0 && data.Iw > 0 {
|
||||
params += fmt.Sprintf(" --iw %.2f", data.Iw)
|
||||
}
|
||||
if data.Raw {
|
||||
params += " --style raw"
|
||||
}
|
||||
if data.Quality > 0 {
|
||||
params += fmt.Sprintf(" --q %.2f", data.Quality)
|
||||
}
|
||||
if data.Tile {
|
||||
params += " --tile "
|
||||
}
|
||||
if data.CRef != "" {
|
||||
params += fmt.Sprintf(" --cref %s", data.CRef)
|
||||
if data.Cw > 0 {
|
||||
params += fmt.Sprintf(" --cw %d", data.Cw)
|
||||
} else {
|
||||
params += " --cw 100"
|
||||
}
|
||||
}
|
||||
|
||||
if data.SRef != "" {
|
||||
params += fmt.Sprintf(" --sref %s", data.SRef)
|
||||
}
|
||||
if data.Model != "" && !strings.Contains(params, "--v") && !strings.Contains(params, "--niji") {
|
||||
params += fmt.Sprintf(" %s", data.Model)
|
||||
}
|
||||
|
||||
// 处理融图和换脸的提示词
|
||||
if data.TaskType == types.TaskSwapFace.String() || data.TaskType == types.TaskBlend.String() {
|
||||
params = fmt.Sprintf("%s:%s", data.TaskType, strings.Join(data.ImgArr, ","))
|
||||
}
|
||||
|
||||
// 如果本地图片上传的是相对地址,处理成绝对地址
|
||||
for k, v := range data.ImgArr {
|
||||
if !strings.HasPrefix(v, "http") {
|
||||
data.ImgArr[k] = fmt.Sprintf("http://localhost:5678/%s", strings.TrimLeft(v, "/"))
|
||||
}
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
// generate task id
|
||||
taskId, err := h.snowflake.Next(true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate task id: "+err.Error())
|
||||
return
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: data.TaskType,
|
||||
UserId: userId,
|
||||
TaskId: taskId,
|
||||
Progress: 0,
|
||||
Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
|
||||
Power: h.App.SysConfig.MjPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
opt := "绘图"
|
||||
if data.TaskType == types.TaskBlend.String() {
|
||||
job.Prompt = "融图:" + strings.Join(data.ImgArr, ",")
|
||||
opt = "融图"
|
||||
} else if data.TaskType == types.TaskSwapFace.String() {
|
||||
job.Prompt = "换脸:" + strings.Join(data.ImgArr, ",")
|
||||
opt = "换脸"
|
||||
}
|
||||
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.MjTask{
|
||||
Id: job.Id,
|
||||
TaskId: taskId,
|
||||
Type: types.TaskType(data.TaskType),
|
||||
Prompt: data.Prompt,
|
||||
NegPrompt: data.NegPrompt,
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
ImgArr: data.ImgArr,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("%s操作,任务ID:%s", opt, job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
type reqVo struct {
|
||||
Index int `json:"index"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
MessageHash string `json:"message_hash"`
|
||||
}
|
||||
|
||||
// Upscale send upscale command to MidJourney Bot
|
||||
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
var data reqVo
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
taskId, _ := h.snowflake.Next(true)
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskUpscale.String(),
|
||||
ReferenceId: data.MessageId,
|
||||
UserId: userId,
|
||||
TaskId: taskId,
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.MjActionPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.MjTask{
|
||||
Id: job.Id,
|
||||
Type: types.TaskUpscale,
|
||||
UserId: userId,
|
||||
ChannelId: data.ChannelId,
|
||||
Index: data.Index,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("Upscale 操作,任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Variation send variation command to MidJourney Bot
|
||||
func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
var data reqVo
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
taskId, _ := h.snowflake.Next(true)
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskVariation.String(),
|
||||
ChannelId: data.ChannelId,
|
||||
ReferenceId: data.MessageId,
|
||||
UserId: userId,
|
||||
TaskId: taskId,
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.MjActionPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.MjTask{
|
||||
Id: job.Id,
|
||||
Type: types.TaskVariation,
|
||||
UserId: userId,
|
||||
Index: data.Index,
|
||||
ChannelId: data.ChannelId,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("Variation 操作,任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
func (h *MidJourneyHandler) ImgWall(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
err, jobs := h.getData(true, 0, page, pageSize, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||
finish := h.GetBool(c, "finish")
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
publish := h.GetBool(c, "publish")
|
||||
|
||||
err, jobs := h.getData(finish, userId, page, pageSize, publish)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.MidJourneyJob) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress >= ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if publish {
|
||||
session = session.Where("publish = ?", publish)
|
||||
}
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
var items []model.MidJourneyJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, nil
|
||||
}
|
||||
|
||||
var jobs = make([]vo.MidJourneyJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.MidJourneyJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" {
|
||||
image, err := utils.DownloadImage(item.OrgURL, h.App.Config.ProxyURL)
|
||||
if err == nil {
|
||||
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||
}
|
||||
}
|
||||
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
return nil, jobs
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *MidJourneyHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
var job model.MidJourneyJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// remove job recode
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Delete(&job).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// refund power
|
||||
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
h.DB.Where("id = ?", job.UserId).First(&user)
|
||||
err = tx.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power + job.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("绘画任务失败,退回算力。任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// remove image
|
||||
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Publish 发布图片到画廊显示
|
||||
func (h *MidJourneyHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
res := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
88
api/handler/order_handler.go
Normal file
88
api/handler/order_handler.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
return &OrderHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// List 订单列表
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
userId := h.GetLoginUserId(c)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id = ? AND status = ?", userId, types.OrderPaidSuccess)
|
||||
var total int64
|
||||
session.Model(&model.Order{}).Count(&total)
|
||||
var items []model.Order
|
||||
var list = make([]vo.Order, 0)
|
||||
offset := (page - 1) * pageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var order vo.Order
|
||||
err := utils.CopyObject(item, &order)
|
||||
if err == nil {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
|
||||
}
|
||||
|
||||
// Query 查询订单状态
|
||||
func (h *OrderHandler) Query(c *gin.Context) {
|
||||
orderNo := h.GetTrim(c, "order_no")
|
||||
var order model.Order
|
||||
res := h.DB.Where("order_no = ?", orderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Order not found")
|
||||
return
|
||||
}
|
||||
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
return
|
||||
}
|
||||
|
||||
counter := 0
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
var item model.Order
|
||||
h.DB.Where("order_no = ?", orderNo).First(&item)
|
||||
if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status {
|
||||
order.Status = item.Status
|
||||
break
|
||||
}
|
||||
counter++
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
}
|
||||
637
api/handler/payment_handler.go
Normal file
637
api/handler/payment_handler.go
Normal file
@@ -0,0 +1,637 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/shopspring/decimal"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PayWay struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var (
|
||||
PayWayAlipay = PayWay{Name: "支付宝", Value: "alipay"}
|
||||
PayWayXunHu = PayWay{Name: "虎皮椒", Value: "hupi"}
|
||||
PayWayJs = PayWay{Name: "PayJS", Value: "payjs"}
|
||||
PayWayWechat = PayWay{Name: "微信支付", Value: "wechat"}
|
||||
)
|
||||
|
||||
// PaymentHandler 支付服务回调 handler
|
||||
type PaymentHandler struct {
|
||||
BaseHandler
|
||||
alipayService *payment.AlipayService
|
||||
huPiPayService *payment.HuPiPayService
|
||||
jsPayService *payment.JPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
snowflake *service.Snowflake
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
signKey string // 用来签名的随机秘钥
|
||||
}
|
||||
|
||||
func NewPaymentHandler(
|
||||
server *core.AppServer,
|
||||
alipayService *payment.AlipayService,
|
||||
huPiPayService *payment.HuPiPayService,
|
||||
jsPayService *payment.JPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
db *gorm.DB,
|
||||
snowflake *service.Snowflake,
|
||||
fs embed.FS) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
alipayService: alipayService,
|
||||
huPiPayService: huPiPayService,
|
||||
jsPayService: jsPayService,
|
||||
wechatPayService: wechatPayService,
|
||||
snowflake: snowflake,
|
||||
fs: fs,
|
||||
lock: sync.Mutex{},
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
DB: db,
|
||||
},
|
||||
signKey: utils.RandString(32),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) DoPay(c *gin.Context) {
|
||||
orderNo := h.GetTrim(c, "order_no")
|
||||
payWay := h.GetTrim(c, "pay_way")
|
||||
t := h.GetInt(c, "t", 0)
|
||||
sign := h.GetTrim(c, "sign")
|
||||
signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, payWay, t, h.signKey)
|
||||
newSign := utils.Sha256(signStr)
|
||||
if newSign != sign {
|
||||
resp.ERROR(c, "订单签名错误!")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查二维码是否过期
|
||||
if time.Now().Unix()-int64(t) > int64(h.App.SysConfig.OrderPayTimeout) {
|
||||
resp.ERROR(c, "支付二维码已过期,请重新生成!")
|
||||
return
|
||||
}
|
||||
|
||||
if orderNo == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
res := h.DB.Where("order_no = ?", orderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Order not found")
|
||||
return
|
||||
}
|
||||
|
||||
// fix: 这里先检查一下订单状态,如果已经支付了,就直接返回
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
resp.ERROR(c, "订单已支付成功,无需重复支付!")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新扫码状态
|
||||
h.DB.Model(&order).UpdateColumn("status", types.OrderScanned)
|
||||
|
||||
if payWay == "alipay" { // 支付宝
|
||||
amount := fmt.Sprintf("%.2f", order.Amount)
|
||||
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, amount, order.Subject)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, uri)
|
||||
return
|
||||
} else if payWay == "hupi" { // 虎皮椒支付
|
||||
params := payment.HuPiPayReq{
|
||||
Version: "1.1",
|
||||
TradeOrderId: orderNo,
|
||||
TotalFee: fmt.Sprintf("%f", order.Amount),
|
||||
Title: order.Subject,
|
||||
NotifyURL: h.App.Config.HuPiPayConfig.NotifyURL,
|
||||
WapName: "极客学长",
|
||||
}
|
||||
r, err := h.huPiPayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, r.URL)
|
||||
}
|
||||
resp.ERROR(c, "Invalid operations")
|
||||
}
|
||||
|
||||
// PayQrcode 生成支付 URL 二维码
|
||||
func (h *PaymentHandler) PayQrcode(c *gin.Context) {
|
||||
var data struct {
|
||||
PayWay string `json:"pay_way"` // 支付方式
|
||||
ProductId uint `json:"product_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
res := h.DB.First(&product, data.ProductId)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, err := h.snowflake.Next(false)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var payWay string
|
||||
var notifyURL string
|
||||
switch data.PayWay {
|
||||
case "hupi":
|
||||
payWay = PayWayXunHu.Value
|
||||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||||
break
|
||||
case "payjs":
|
||||
payWay = PayWayJs.Value
|
||||
notifyURL = h.App.Config.JPayConfig.NotifyURL
|
||||
break
|
||||
case "alipay":
|
||||
payWay = PayWayAlipay.Value
|
||||
notifyURL = h.App.Config.AlipayConfig.NotifyURL
|
||||
break
|
||||
default:
|
||||
payWay = PayWayWechat.Value
|
||||
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
|
||||
|
||||
}
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
Discount: product.Discount,
|
||||
}
|
||||
|
||||
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
|
||||
order := model.Order{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
ProductId: product.Id,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: payWay,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
res = h.DB.Create(&order)
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with create order: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// PayJs 单独处理,只能用官方生成的二维码
|
||||
if data.PayWay == "payjs" {
|
||||
params := payment.JPayReq{
|
||||
TotalFee: int(math.Ceil(order.Amount * 100)),
|
||||
OutTradeNo: order.OrderNo,
|
||||
Subject: product.Name,
|
||||
}
|
||||
r := h.jsPayService.Pay(params)
|
||||
if r.IsOK() {
|
||||
resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode})
|
||||
return
|
||||
} else {
|
||||
resp.ERROR(c, "error with generating payment qrcode: "+r.ReturnMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var logo string
|
||||
if data.PayWay == "alipay" {
|
||||
logo = "res/img/alipay.jpg"
|
||||
} else if data.PayWay == "hupi" {
|
||||
if h.App.Config.HuPiPayConfig.Name == "wechat" {
|
||||
logo = "res/img/wechat-pay.jpg"
|
||||
} else {
|
||||
logo = "res/img/alipay.jpg"
|
||||
}
|
||||
} else if data.PayWay == "wechat" {
|
||||
logo = "res/img/wechat-pay.jpg"
|
||||
}
|
||||
|
||||
file, err := h.fs.Open(logo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with open qrcode log file: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
parse, err := url.Parse(notifyURL)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
timestamp := time.Now().Unix()
|
||||
signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, data.PayWay, timestamp, h.signKey)
|
||||
sign := utils.Sha256(signStr)
|
||||
var imageURL string
|
||||
if data.PayWay == "wechat" {
|
||||
payUrl, err := h.wechatPayService.PayUrlNative(order.OrderNo, int(math.Floor(order.Amount*100)), product.Name)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generating wechat payment qrcode: "+err.Error())
|
||||
return
|
||||
} else {
|
||||
imageURL = payUrl
|
||||
}
|
||||
} else {
|
||||
imageURL = fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s&t=%d&sign=%s", parse.Scheme, parse.Host, orderNo, data.PayWay, timestamp, sign)
|
||||
}
|
||||
imgData, err := utils.GenQrcode(imageURL, 400, file)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
imgDataBase64 := base64.StdEncoding.EncodeToString(imgData)
|
||||
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
|
||||
}
|
||||
|
||||
// Mobile 移动端支付
|
||||
func (h *PaymentHandler) Mobile(c *gin.Context) {
|
||||
var data struct {
|
||||
PayWay string `json:"pay_way"` // 支付方式
|
||||
ProductId uint `json:"product_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
res := h.DB.First(&product, data.ProductId)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, err := h.snowflake.Next(false)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
|
||||
var payWay string
|
||||
var notifyURL, returnURL string
|
||||
var payURL string
|
||||
switch data.PayWay {
|
||||
case "hupi":
|
||||
payWay = PayWayXunHu.Name
|
||||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||||
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
|
||||
parse, _ := url.Parse(h.App.Config.HuPiPayConfig.ReturnURL)
|
||||
baseURL := fmt.Sprintf("%s://%s", parse.Scheme, parse.Host)
|
||||
params := payment.HuPiPayReq{
|
||||
Version: "1.1",
|
||||
TradeOrderId: orderNo,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
Title: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ReturnURL: returnURL,
|
||||
CallbackURL: returnURL,
|
||||
WapName: "极客学长",
|
||||
WapUrl: baseURL,
|
||||
Type: "WAP",
|
||||
}
|
||||
r, err := h.huPiPayService.Pay(params)
|
||||
if err != nil {
|
||||
errMsg := "error with generating Pay Hupi URL: " + err.Error()
|
||||
logger.Error(errMsg)
|
||||
resp.ERROR(c, errMsg)
|
||||
return
|
||||
}
|
||||
payURL = r.URL
|
||||
case "payjs":
|
||||
payWay = PayWayJs.Name
|
||||
notifyURL = h.App.Config.JPayConfig.NotifyURL
|
||||
returnURL = h.App.Config.JPayConfig.ReturnURL
|
||||
totalFee := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Mul(decimal.NewFromInt(100)).IntPart()
|
||||
params := url.Values{}
|
||||
params.Add("total_fee", fmt.Sprintf("%d", totalFee))
|
||||
params.Add("out_trade_no", orderNo)
|
||||
params.Add("body", product.Name)
|
||||
params.Add("notify_url", notifyURL)
|
||||
params.Add("auto", "0")
|
||||
payURL = h.jsPayService.PayH5(params)
|
||||
case "alipay":
|
||||
payWay = PayWayAlipay.Name
|
||||
payURL, err = h.alipayService.PayUrlMobile(orderNo, fmt.Sprintf("%.2f", amount), product.Name)
|
||||
if err != nil {
|
||||
errMsg := "error with generating Alipay URL: " + err.Error()
|
||||
resp.ERROR(c, errMsg)
|
||||
return
|
||||
}
|
||||
case "wechat":
|
||||
payWay = PayWayWechat.Name
|
||||
payURL, err = h.wechatPayService.PayUrlH5(orderNo, int(amount*100), product.Name, c.ClientIP())
|
||||
if err != nil {
|
||||
errMsg := "error with generating Wechat URL: " + err.Error()
|
||||
logger.Error(errMsg)
|
||||
resp.ERROR(c, errMsg)
|
||||
return
|
||||
}
|
||||
default:
|
||||
resp.ERROR(c, "Unsupported pay way: "+data.PayWay)
|
||||
return
|
||||
}
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
Discount: product.Discount,
|
||||
}
|
||||
|
||||
order := model.Order{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
ProductId: product.Id,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: payWay,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
res = h.DB.Create(&order)
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with create order: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"url": payURL, "order_no": orderNo})
|
||||
}
|
||||
|
||||
// 异步通知回调公共逻辑
|
||||
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
var order model.Order
|
||||
res := h.DB.Where("order_no = ?", orderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
err := fmt.Errorf("error with fetch order: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
// 已支付订单,直接返回
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res = h.DB.First(&user, order.UserId)
|
||||
if res.Error != nil {
|
||||
err := fmt.Errorf("error with fetch user info: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var remark types.OrderRemark
|
||||
err := utils.JsonDecode(order.Remark, &remark)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error with decode order remark: %v", err)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var opt string
|
||||
var power int
|
||||
if remark.Days > 0 { // VIP 充值
|
||||
if user.ExpiredTime >= time.Now().Unix() {
|
||||
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
|
||||
opt = "VIP充值,VIP 没到期,只延期不增加算力"
|
||||
} else {
|
||||
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
|
||||
user.Power += h.App.SysConfig.VipMonthPower
|
||||
power = h.App.SysConfig.VipMonthPower
|
||||
opt = "VIP充值"
|
||||
}
|
||||
user.Vip = true
|
||||
} else { // 充值点卡,直接增加次数即可
|
||||
user.Power += remark.Power
|
||||
opt = "点卡充值"
|
||||
power = remark.Power
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
res = h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
err := fmt.Errorf("error with update user info: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
order.PayTime = time.Now().Unix()
|
||||
order.Status = types.OrderPaidSuccess
|
||||
order.TradeNo = tradeNo
|
||||
res = h.DB.Updates(&order)
|
||||
if res.Error != nil {
|
||||
err := fmt.Errorf("error with update order info: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新产品销量
|
||||
h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
|
||||
|
||||
// 记录算力充值日志
|
||||
if power > 0 {
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerRecharge,
|
||||
Amount: power,
|
||||
Balance: user.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: order.PayWay,
|
||||
Remark: fmt.Sprintf("%s,金额:%f,订单号:%s", opt, order.Amount, order.OrderNo),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPayWays 获取支付方式
|
||||
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
|
||||
data := gin.H{}
|
||||
if h.App.Config.AlipayConfig.Enabled {
|
||||
data["alipay"] = gin.H{"name": "alipay"}
|
||||
}
|
||||
if h.App.Config.HuPiPayConfig.Enabled {
|
||||
data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name}
|
||||
}
|
||||
if h.App.Config.JPayConfig.Enabled {
|
||||
data["payjs"] = gin.H{"name": h.App.Config.JPayConfig.Name}
|
||||
}
|
||||
if h.App.Config.WechatPayConfig.Enabled {
|
||||
data["wechat"] = gin.H{"name": "wechat"}
|
||||
}
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// HuPiPayNotify 虎皮椒支付异步回调
|
||||
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Request.Form.Get("trade_order_id")
|
||||
tradeNo := c.Request.Form.Get("open_order_id")
|
||||
logger.Infof("收到虎皮椒订单支付回调,订单 NO:%s,交易流水号:%s", orderNo, tradeNo)
|
||||
|
||||
if err = h.huPiPayService.Check(tradeNo); err != nil {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
err = h.notify(orderNo, tradeNo)
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// AlipayNotify 支付宝支付回调
|
||||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO:验证交易签名
|
||||
res := h.alipayService.TradeVerify(c.Request)
|
||||
logger.Infof("验证支付结果:%+v", res)
|
||||
if !res.Success() {
|
||||
logger.Error("订单校验失败:", res.Message)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
tradeNo := c.Request.Form.Get("trade_no")
|
||||
err = h.notify(res.OutTradeNo, tradeNo)
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// PayJsNotify PayJs 支付异步回调
|
||||
func (h *PaymentHandler) PayJsNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Request.Form.Get("out_trade_no")
|
||||
returnCode := c.Request.Form.Get("return_code")
|
||||
logger.Infof("收到PayJs订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode)
|
||||
// 支付失败
|
||||
if returnCode != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
// 校验订单支付状态
|
||||
tradeNo := c.Request.Form.Get("payjs_order_id")
|
||||
err = h.jsPayService.TradeVerify(tradeNo)
|
||||
if err != nil {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(orderNo, tradeNo)
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// WechatPayNotify 微信商户支付异步回调
|
||||
func (h *PaymentHandler) WechatPayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
result := h.wechatPayService.TradeVerify(c.Request)
|
||||
if !result.Success() {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": "FAIL",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(result.OutTradeNo, result.TradeId)
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
74
api/handler/power_log_handler.go
Normal file
74
api/handler/power_log_handler.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PowerLogHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
|
||||
return &PowerLogHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *PowerLogHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Model string `json:"model"`
|
||||
Date []string `json:"date"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
userId := h.GetLoginUserId(c)
|
||||
session = session.Where("user_id", userId)
|
||||
if data.Model != "" {
|
||||
session = session.Where("model", data.Model)
|
||||
}
|
||||
if len(data.Date) == 2 {
|
||||
start := data.Date[0] + " 00:00:00"
|
||||
end := data.Date[1] + " 00:00:00"
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.PowerLog{}).Count(&total)
|
||||
var items []model.PowerLog
|
||||
var list = make([]vo.PowerLog, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var log vo.PowerLog
|
||||
err := utils.CopyObject(item, &log)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log.Id = item.Id
|
||||
log.CreatedAt = item.CreatedAt.Unix()
|
||||
log.TypeStr = item.Type.String()
|
||||
list = append(list, log)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
48
api/handler/product_handler.go
Normal file
48
api/handler/product_handler.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||
return &ProductHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// List 模型列表
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
var items []model.Product
|
||||
var list = make([]vo.Product, 0)
|
||||
res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Product
|
||||
err := utils.CopyObject(item, &product)
|
||||
if err == nil {
|
||||
product.Id = item.Id
|
||||
product.CreatedAt = item.CreatedAt.Unix()
|
||||
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||
list = append(list, product)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, list)
|
||||
}
|
||||
108
api/handler/reward_handler.go
Normal file
108
api/handler/reward_handler.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RewardHandler struct {
|
||||
BaseHandler
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
|
||||
return &RewardHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// Verify 打赏码核销
|
||||
func (h *RewardHandler) Verify(c *gin.Context) {
|
||||
var data struct {
|
||||
TxId string `json:"tx_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.HACKER(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 移除转账单号中间的空格,防止有人复制的时候多复制了空格
|
||||
data.TxId = strings.ReplaceAll(data.TxId, " ", "")
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
var item model.Reward
|
||||
res := h.DB.Where("tx_id = ?", data.TxId).First(&item)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的交易流水号!")
|
||||
return
|
||||
}
|
||||
|
||||
if item.Status {
|
||||
resp.ERROR(c, "当前交易流水号已经被核销,请不要重复核销!")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
exchange := vo.RewardExchange{}
|
||||
power := math.Ceil(item.Amount / h.App.SysConfig.PowerPrice)
|
||||
exchange.Power = int(power)
|
||||
res = tx.Model(&user).UpdateColumn("power", gorm.Expr("power + ?", exchange.Power))
|
||||
if res.Error != nil {
|
||||
tx.Rollback()
|
||||
logger.Error("添加应用失败:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新核销状态
|
||||
item.Status = true
|
||||
item.UserId = user.Id
|
||||
item.Exchange = utils.JsonEncode(exchange)
|
||||
res = tx.Updates(&item)
|
||||
if res.Error != nil {
|
||||
tx.Rollback()
|
||||
logger.Error("添加应用失败:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
// 记录算力充值日志
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerReward,
|
||||
Amount: exchange.Power,
|
||||
Balance: user.Power + exchange.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "众筹支付",
|
||||
Remark: fmt.Sprintf("充值算力,金额:%f,价格:%f", item.Amount, h.App.SysConfig.PowerPrice),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
323
api/handler/sd_handler.go
Normal file
323
api/handler/sd_handler.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SdJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
pool *sd.ServicePool
|
||||
uploader *oss.UploaderManager
|
||||
snowflake *service.Snowflake
|
||||
leveldb *store.LevelDB
|
||||
}
|
||||
|
||||
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager, snowflake *service.Snowflake, levelDB *store.LevelDB) *SdJobHandler {
|
||||
return &SdJobHandler{
|
||||
pool: pool,
|
||||
uploader: manager,
|
||||
snowflake: snowflake,
|
||||
leveldb: levelDB,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *SdJobHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
if userId == 0 {
|
||||
logger.Info("Invalid user ID")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.pool.Clients.Put(uint(userId), client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
}
|
||||
|
||||
func (h *SdJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.pool.HasAvailableService() {
|
||||
resp.ERROR(c, "Stable-Diffusion 池子中没有没有可用的服务!")
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SdPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var data types.SdTaskParams
|
||||
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Width <= 0 {
|
||||
data.Width = 512
|
||||
}
|
||||
if data.Height <= 0 {
|
||||
data.Height = 512
|
||||
}
|
||||
if data.CfgScale <= 0 {
|
||||
data.CfgScale = 7
|
||||
}
|
||||
if data.Seed == 0 {
|
||||
data.Seed = -1
|
||||
}
|
||||
if data.Steps <= 0 {
|
||||
data.Steps = 20
|
||||
}
|
||||
if data.Sampler == "" {
|
||||
data.Sampler = "Euler a"
|
||||
}
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
taskId, err := h.snowflake.Next(true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate task id: "+err.Error())
|
||||
return
|
||||
}
|
||||
params := types.SdTaskParams{
|
||||
TaskId: taskId,
|
||||
Prompt: data.Prompt,
|
||||
NegPrompt: data.NegPrompt,
|
||||
Steps: data.Steps,
|
||||
Sampler: data.Sampler,
|
||||
FaceFix: data.FaceFix,
|
||||
CfgScale: data.CfgScale,
|
||||
Seed: data.Seed,
|
||||
Height: data.Height,
|
||||
Width: data.Width,
|
||||
HdFix: data.HdFix,
|
||||
HdRedrawRate: data.HdRedrawRate,
|
||||
HdScale: data.HdScale,
|
||||
HdScaleAlg: data.HdScaleAlg,
|
||||
HdSteps: data.HdSteps,
|
||||
}
|
||||
|
||||
job := model.SdJob{
|
||||
UserId: userId,
|
||||
Type: types.TaskImage.String(),
|
||||
TaskId: params.TaskId,
|
||||
Params: utils.JsonEncode(params),
|
||||
Prompt: data.Prompt,
|
||||
Progress: 0,
|
||||
Power: h.App.SysConfig.SdPower,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
res := h.DB.Create(&job)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "error with save job: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.SdTask{
|
||||
Id: int(job.Id),
|
||||
Type: types.TaskImage,
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "stable-diffusion",
|
||||
Remark: fmt.Sprintf("绘图操作,任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
func (h *SdJobHandler) ImgWall(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
err, jobs := h.getData(true, 0, page, pageSize, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 SD 任务列表
|
||||
func (h *SdJobHandler) JobList(c *gin.Context) {
|
||||
finish := h.GetBool(c, "finish")
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
publish := h.GetBool(c, "publish")
|
||||
|
||||
err, jobs := h.getData(finish, userId, page, pageSize, publish)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.SdJob) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if publish {
|
||||
session = session.Where("publish", publish)
|
||||
}
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
var items []model.SdJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, nil
|
||||
}
|
||||
|
||||
var jobs = make([]vo.SdJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.SdJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.Progress < 100 {
|
||||
// 从 leveldb 中获取图片预览数据
|
||||
var imageData string
|
||||
err = h.leveldb.Get(item.TaskId, &imageData)
|
||||
if err == nil {
|
||||
job.ImgURL = "data:image/png;base64," + imageData
|
||||
}
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return nil, jobs
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *SdJobHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
var job model.SdJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// remove job recode
|
||||
res := h.DB.Delete(&model.SdJob{Id: job.Id})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte(sd.Finished))
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Publish 发布/取消发布图片到画廊显示
|
||||
func (h *SdJobHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
res := h.DB.Model(&model.SdJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
103
api/handler/sms_handler.go
Normal file
103
api/handler/sms_handler.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/sms"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
const CodeStorePrefix = "/verify/codes/"
|
||||
|
||||
type SmsHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
sms *sms.ServiceManager
|
||||
smtp *service.SmtpService
|
||||
captcha *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewSmsHandler(
|
||||
app *core.AppServer,
|
||||
client *redis.Client,
|
||||
sms *sms.ServiceManager,
|
||||
smtp *service.SmtpService,
|
||||
captcha *service.CaptchaService) *SmsHandler {
|
||||
return &SmsHandler{
|
||||
redis: client,
|
||||
sms: sms,
|
||||
captcha: captcha,
|
||||
smtp: smtp,
|
||||
BaseHandler: BaseHandler{App: app}}
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
var data struct {
|
||||
Receiver string `json:"receiver"` // 接收者
|
||||
Key string `json:"key"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "验证码错误,请先完人机验证")
|
||||
return
|
||||
}
|
||||
|
||||
code := utils.RandomNumber(6)
|
||||
var err error
|
||||
if strings.Contains(data.Receiver, "@") { // email
|
||||
if !utils.Contains(h.App.SysConfig.RegisterWays, "email") {
|
||||
resp.ERROR(c, "系统已禁用邮箱注册!")
|
||||
return
|
||||
}
|
||||
err = h.smtp.SendVerifyCode(data.Receiver, code)
|
||||
} else {
|
||||
if !utils.Contains(h.App.SysConfig.RegisterWays, "mobile") {
|
||||
resp.ERROR(c, "系统已禁用手机号注册!")
|
||||
return
|
||||
}
|
||||
err = h.sms.GetService().SendVerifyCode(data.Receiver, code)
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 存储验证码,等待后面注册验证
|
||||
_, err = h.redis.Set(c, CodeStorePrefix+data.Receiver, code, 0).Result()
|
||||
if err != nil {
|
||||
resp.ERROR(c, "验证码保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.Debug {
|
||||
resp.SUCCESS(c, code)
|
||||
} else {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
345
api/handler/suno_handler.go
Normal file
345
api/handler/suno_handler.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/suno"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SunoHandler struct {
|
||||
BaseHandler
|
||||
service *suno.Service
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager) *SunoHandler {
|
||||
return &SunoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
service: service,
|
||||
uploader: uploader,
|
||||
}
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *SunoHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
if userId == 0 {
|
||||
logger.Info("Invalid user ID")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.service.Clients.Put(uint(userId), client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Instrumental bool `json:"instrumental"`
|
||||
Lyrics string `json:"lyrics"`
|
||||
Model string `json:"model"`
|
||||
Tags string `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
Type int `json:"type"`
|
||||
RefTaskId string `json:"ref_task_id"` // 续写的任务id
|
||||
ExtendSecs int `json:"extend_secs"` // 续写秒数
|
||||
RefSongId string `json:"ref_song_id"` // 续写的歌曲id
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
job := model.SunoJob{
|
||||
UserId: int(h.GetLoginUserId(c)),
|
||||
Prompt: data.Prompt,
|
||||
Instrumental: data.Instrumental,
|
||||
ModelName: data.Model,
|
||||
Tags: data.Tags,
|
||||
Title: data.Title,
|
||||
Type: data.Type,
|
||||
RefSongId: data.RefSongId,
|
||||
RefTaskId: data.RefTaskId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Power: h.App.SysConfig.SunoPower,
|
||||
}
|
||||
if data.Lyrics != "" {
|
||||
job.Prompt = data.Lyrics
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, tx.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
h.service.PushTask(types.SunoTask{
|
||||
Id: job.Id,
|
||||
UserId: job.UserId,
|
||||
Type: job.Type,
|
||||
Title: job.Title,
|
||||
RefTaskId: data.RefTaskId,
|
||||
RefSongId: data.RefSongId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Prompt: job.Prompt,
|
||||
Tags: data.Tags,
|
||||
Model: data.Model,
|
||||
Instrumental: data.Instrumental,
|
||||
})
|
||||
|
||||
// update user's power
|
||||
tx = h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: job.ModelName,
|
||||
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
client := h.service.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.SunoJob{}).Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
var list []model.SunoJob
|
||||
err := session.Order("id desc").Find(&list).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 初始化续写关系
|
||||
songIds := make([]string, 0)
|
||||
for _, v := range list {
|
||||
if v.RefTaskId != "" {
|
||||
songIds = append(songIds, v.RefSongId)
|
||||
}
|
||||
}
|
||||
var tasks []model.SunoJob
|
||||
h.DB.Where("song_id IN ?", songIds).Find(&tasks)
|
||||
songMap := make(map[string]model.SunoJob)
|
||||
for _, t := range tasks {
|
||||
songMap[t.SongId] = t
|
||||
}
|
||||
// 转换为 VO
|
||||
items := make([]vo.SunoJob, 0)
|
||||
for _, v := range list {
|
||||
var item vo.SunoJob
|
||||
err = utils.CopyObject(v, &item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
if s, ok := songMap[v.RefSongId]; ok {
|
||||
item.RefSong = map[string]interface{}{
|
||||
"id": s.Id,
|
||||
"title": s.Title,
|
||||
"cover": s.CoverURL,
|
||||
"audio": s.AudioURL,
|
||||
}
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.SunoJob
|
||||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 删除任务
|
||||
h.DB.Delete(&job)
|
||||
// 删除文件
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.AudioURL)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
publish := h.GetBool(c, "publish")
|
||||
err := h.DB.Model(&model.SunoJob{}).Where("id", id).Where("user_id", userId).UpdateColumn("publish", publish).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Id == 0 || data.Title == "" || data.Cover == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var item model.SunoJob
|
||||
if err := h.DB.Where("id", data.Id).Where("user_id", userId).First(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
item.Title = data.Title
|
||||
item.CoverURL = data.Cover
|
||||
|
||||
if err := h.DB.Updates(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Detail 歌曲详情
|
||||
func (h *SunoHandler) Detail(c *gin.Context) {
|
||||
songId := c.Query("song_id")
|
||||
if songId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var item model.SunoJob
|
||||
if err := h.DB.Where("song_id", songId).First(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 读取用户信息
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", item.UserId).First(&user).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.SunoJob
|
||||
if err := utils.CopyObject(item, &itemVo); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
itemVo.CreatedAt = item.CreatedAt.Unix()
|
||||
itemVo.User = map[string]interface{}{
|
||||
"nickname": user.Nickname,
|
||||
"avatar": user.Avatar,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// Play 增加歌曲播放次数
|
||||
func (h *SunoHandler) Play(c *gin.Context) {
|
||||
songId := c.Query("song_id")
|
||||
if songId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1))
|
||||
}
|
||||
|
||||
const genLyricTemplate = `
|
||||
你是一位才华横溢的作曲家,拥有丰富的情感和细腻的笔触,你对文字有着独特的感悟力,能将各种情感和意境巧妙地融入歌词中。
|
||||
请以【%s】为主题创作一首歌曲,歌曲时间不要太短,3分钟左右,不要输出任何解释性的内容。
|
||||
输出格式如下:
|
||||
歌曲名称
|
||||
第一节:
|
||||
{{歌词内容}}
|
||||
副歌:
|
||||
{{歌词内容}}
|
||||
|
||||
第二节:
|
||||
{{歌词内容}}
|
||||
副歌:
|
||||
{{歌词内容}}
|
||||
|
||||
尾声:
|
||||
{{歌词内容}}
|
||||
`
|
||||
|
||||
// Lyric 生成歌词
|
||||
func (h *SunoHandler) Lyric(c *gin.Context) {
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(genLyricTemplate, data.Prompt), "gpt-4o-mini")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
17
api/handler/test_handler.go
Normal file
17
api/handler/test_handler.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TestHandler struct {
|
||||
db *gorm.DB
|
||||
snowflake *service.Snowflake
|
||||
js *payment.JPayService
|
||||
}
|
||||
|
||||
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.JPayService) *TestHandler {
|
||||
return &TestHandler{db: db, snowflake: snowflake, js: js}
|
||||
}
|
||||
121
api/handler/upload_handler.go
Normal file
121
api/handler/upload_handler.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
BaseHandler
|
||||
uploaderManager *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
|
||||
return &UploadHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
|
||||
}
|
||||
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("upload file: ", file.Name)
|
||||
// cut the file name if it's too long
|
||||
if len(file.Name) > 100 {
|
||||
file.Name = file.Name[:90] + file.Ext
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: int(userId),
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
Ext: file.Ext,
|
||||
Size: file.Size,
|
||||
CreatedAt: time.Time{},
|
||||
})
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with update database: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, file)
|
||||
}
|
||||
|
||||
func (h *UploadHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items []model.File
|
||||
var files = make([]vo.File, 0)
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session = session.Where("user_id = ?", userId)
|
||||
if len(data.Urls) > 0 {
|
||||
session = session.Where("url IN ?", data.Urls)
|
||||
}
|
||||
session.Find(&items)
|
||||
if len(items) > 0 {
|
||||
for _, v := range items {
|
||||
var file vo.File
|
||||
err := utils.CopyObject(v, &file)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
file.CreatedAt = v.CreatedAt.Unix()
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, files)
|
||||
}
|
||||
|
||||
// Remove remove files
|
||||
func (h *UploadHandler) Remove(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
id := h.GetInt(c, "id", 0)
|
||||
var file model.File
|
||||
tx := h.DB.Where("user_id = ? AND id = ?", userId, id).First(&file)
|
||||
if tx.Error != nil || file.Id == 0 {
|
||||
resp.ERROR(c, "file not existed")
|
||||
return
|
||||
}
|
||||
|
||||
// remove database
|
||||
tx = h.DB.Model(&model.File{}).Delete("id = ?", id)
|
||||
if tx.Error != nil || tx.RowsAffected == 0 {
|
||||
resp.ERROR(c, "failed to update database")
|
||||
return
|
||||
}
|
||||
// remove files
|
||||
objectKey := file.ObjKey
|
||||
if objectKey == "" {
|
||||
objectKey = file.URL
|
||||
}
|
||||
_ = h.uploaderManager.GetUploadHandler().Delete(objectKey)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
574
api/handler/user_handler.go
Normal file
574
api/handler/user_handler.go
Normal file
@@ -0,0 +1,574 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/imroc/req/v3"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
BaseHandler
|
||||
searcher *xdb.Searcher
|
||||
redis *redis.Client
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewUserHandler(
|
||||
app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
searcher *xdb.Searcher,
|
||||
client *redis.Client,
|
||||
licenseService *service.LicenseService) *UserHandler {
|
||||
return &UserHandler{
|
||||
BaseHandler: BaseHandler{DB: db, App: app},
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
// Register user register
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
// parameters process
|
||||
var data struct {
|
||||
RegWay string `json:"reg_way"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
data.Password = strings.TrimSpace(data.Password)
|
||||
if len(data.Password) < 8 {
|
||||
resp.ERROR(c, "密码长度不能少于8个字符")
|
||||
return
|
||||
}
|
||||
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
var key string
|
||||
if data.RegWay == "email" || data.RegWay == "mobile" {
|
||||
key = CodeStorePrefix + data.Username
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邀请码
|
||||
inviteCode := model.InviteCode{}
|
||||
if data.InviteCode != "" {
|
||||
res := h.DB.Where("code = ?", data.InviteCode).First(&inviteCode)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的邀请码")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check if the username is existing
|
||||
var item model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&item)
|
||||
if item.Id > 0 {
|
||||
resp.ERROR(c, "该用户名已经被注册")
|
||||
return
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
}
|
||||
|
||||
// 被邀请人也获得赠送算力
|
||||
if data.InviteCode != "" {
|
||||
user.Power += h.App.SysConfig.InvitePower
|
||||
}
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
}
|
||||
|
||||
res = h.DB.Create(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "保存数据失败")
|
||||
logger.Error(res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if data.InviteCode != "" {
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.InvitePower > 0 {
|
||||
h.DB.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("power", gorm.Expr("power + ?", h.App.SysConfig.InvitePower))
|
||||
// 记录邀请算力充值日志
|
||||
var inviter model.User
|
||||
h.DB.Where("id", inviteCode.UserId).First(&inviter)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: inviter.Id,
|
||||
Username: inviter.Username,
|
||||
Type: types.PowerInvite,
|
||||
Amount: h.App.SysConfig.InvitePower,
|
||||
Balance: inviter.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
h.DB.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
InviteCode: inviteCode.Code,
|
||||
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
|
||||
})
|
||||
}
|
||||
|
||||
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
|
||||
|
||||
// 自动登录创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key = fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "用户名不存在")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
if password != user.Password {
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Status == false {
|
||||
resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = c.ClientIP()
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Model(&user).Updates(user)
|
||||
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: c.ClientIP(),
|
||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
||||
})
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
// Logout 注 销
|
||||
func (h *UserHandler) Logout(c *gin.Context) {
|
||||
key := h.GetUserKey(c)
|
||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||
logger.Error("error with delete session: ", err)
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// CLogin 第三方登录请求二维码
|
||||
func (h *UserHandler) CLogin(c *gin.Context) {
|
||||
returnURL := h.GetTrim(c, "return_url")
|
||||
var res types.BizVo
|
||||
apiURL := fmt.Sprintf("%s/api/clogin/request", h.App.Config.ApiConfig.ApiURL)
|
||||
r, err := req.C().R().SetBody(gin.H{"login_type": "wx", "return_url": returnURL}).
|
||||
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, "error with login http status: "+r.Status)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, "error with http response: "+res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, res.Data)
|
||||
}
|
||||
|
||||
// CLoginCallback 第三方登录回调
|
||||
func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
loginType := h.GetTrim(c, "login_type")
|
||||
code := h.GetTrim(c, "code")
|
||||
|
||||
var res types.BizVo
|
||||
apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL)
|
||||
r, err := req.C().R().SetBody(gin.H{"login_type": loginType, "code": code}).
|
||||
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, "error with login http status: "+r.Status)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
resp.ERROR(c, "error with http response: "+res.Message)
|
||||
return
|
||||
}
|
||||
|
||||
// login successfully
|
||||
data := res.Data.(map[string]interface{})
|
||||
session := gin.H{}
|
||||
var user model.User
|
||||
tx := h.DB.Debug().Where("openid", data["openid"]).First(&user)
|
||||
if tx.Error != nil { // user not exist, create new user
|
||||
// 检测最大注册人数
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
|
||||
return
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
password := fmt.Sprintf("%d", utils.RandomNumber(8))
|
||||
user = model.User{
|
||||
Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)),
|
||||
Password: utils.GenPassword(password, salt),
|
||||
Avatar: fmt.Sprintf("%s", data["avatar"]),
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
OpenId: fmt.Sprintf("%s", data["openid"]),
|
||||
Nickname: fmt.Sprintf("%s", data["nickname"]),
|
||||
}
|
||||
|
||||
tx = h.DB.Create(&user)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, "保存数据失败")
|
||||
logger.Error(tx.Error)
|
||||
return
|
||||
}
|
||||
session["username"] = user.Username
|
||||
session["password"] = password
|
||||
} else { // login directly
|
||||
// 更新最后登录时间和IP
|
||||
user.LastLoginIp = c.ClientIP()
|
||||
user.LastLoginAt = time.Now().Unix()
|
||||
h.DB.Model(&user).Updates(user)
|
||||
|
||||
h.DB.Create(&model.UserLoginLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
LoginIp: c.ClientIP(),
|
||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
||||
})
|
||||
}
|
||||
|
||||
// 创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
session["token"] = tokenString
|
||||
resp.SUCCESS(c, session)
|
||||
}
|
||||
|
||||
// Session 获取/验证会话
|
||||
func (h *UserHandler) Session(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err == nil {
|
||||
var userVo vo.User
|
||||
err := utils.CopyObject(user, &userVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c)
|
||||
}
|
||||
userVo.Id = user.Id
|
||||
resp.SUCCESS(c, userVo)
|
||||
} else {
|
||||
resp.NotAuth(c)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type userProfile struct {
|
||||
Id uint `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
Power int `json:"power"`
|
||||
ExpiredTime int64 `json:"expired_time"`
|
||||
Vip bool `json:"vip"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) Profile(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
h.DB.First(&user, user.Id)
|
||||
var profile userProfile
|
||||
err = utils.CopyObject(user, &profile)
|
||||
if err != nil {
|
||||
logger.Error("对象拷贝失败:", err.Error())
|
||||
resp.ERROR(c, "获取用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
profile.Id = user.Id
|
||||
resp.SUCCESS(c, profile)
|
||||
}
|
||||
|
||||
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
|
||||
var data userProfile
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
h.DB.First(&user, user.Id)
|
||||
user.Avatar = data.Avatar
|
||||
user.Nickname = data.Nickname
|
||||
res := h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新用户信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// UpdatePass 更新密码
|
||||
func (h *UserHandler) UpdatePass(c *gin.Context) {
|
||||
var data struct {
|
||||
OldPass string `json:"old_pass"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Password) < 8 {
|
||||
resp.ERROR(c, "密码长度不能少于8个字符")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.OldPass, user.Salt)
|
||||
logger.Debugf(user.Salt, ",", user.Password, ",", password, ",", data.OldPass)
|
||||
if password != user.Password {
|
||||
resp.ERROR(c, "原密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
newPass := utils.GenPassword(data.Password, user.Salt)
|
||||
res := h.DB.Model(&user).UpdateColumn("password", newPass)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ResetPass 重置密码
|
||||
func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Code string `json:"code"` // 验证码
|
||||
Password string `json:"password"` // 新密码
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.Where("username", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Username
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "短信验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c)
|
||||
} else {
|
||||
h.redis.Del(c, key)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BindUsername 重置账号
|
||||
func (h *UserHandler) BindUsername(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Username
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查手机号是否被其他账号绑定
|
||||
var item model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&item)
|
||||
if res.Error == nil {
|
||||
resp.ERROR(c, "该账号已经被其他账号绑定")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
res = h.DB.Model(&user).UpdateColumn("username", data.Username)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.redis.Del(c, key) // 删除短信验证码
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
81
api/logger/logger.go
Normal file
81
api/logger/logger.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package logger
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
var sugarLogger *zap.SugaredLogger
|
||||
|
||||
func GetLogger() *zap.SugaredLogger {
|
||||
if sugarLogger != nil {
|
||||
return sugarLogger
|
||||
}
|
||||
|
||||
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
|
||||
encoder := getEncoder()
|
||||
writerSyncer := getLogWriter()
|
||||
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
|
||||
consoleOutput := zapcore.Lock(os.Stdout)
|
||||
consoleCore := zapcore.NewCore(
|
||||
encoder,
|
||||
consoleOutput,
|
||||
logLevel,
|
||||
)
|
||||
core := zapcore.NewTee(fileCore, consoleCore)
|
||||
logger = zap.New(core, zap.AddCaller())
|
||||
sugarLogger = logger.Sugar()
|
||||
return sugarLogger
|
||||
}
|
||||
|
||||
// core 三个参数之 编码
|
||||
func getEncoder() zapcore.Encoder {
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||
}
|
||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||
}
|
||||
|
||||
func getLogWriter() zapcore.WriteSyncer {
|
||||
lumberJackLogger := &lumberjack.Logger{
|
||||
Filename: "logs/app.log",
|
||||
MaxSize: 10,
|
||||
MaxBackups: 5,
|
||||
MaxAge: 30,
|
||||
Compress: false,
|
||||
}
|
||||
return zapcore.AddSync(lumberJackLogger)
|
||||
}
|
||||
|
||||
func getLogLevel(level string) zapcore.Level {
|
||||
switch strings.ToUpper(level) {
|
||||
case "DEBUG":
|
||||
return zapcore.DebugLevel
|
||||
case "WARN":
|
||||
return zapcore.WarnLevel
|
||||
case "ERROR":
|
||||
return zapcore.ErrorLevel
|
||||
default:
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
540
api/main.go
Normal file
540
api/main.go
Normal file
@@ -0,0 +1,540 @@
|
||||
package main
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/handler/admin"
|
||||
"geekai/handler/chatimpl"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sd"
|
||||
"geekai/service/sms"
|
||||
"geekai/service/suno"
|
||||
"geekai/service/wx"
|
||||
"geekai/store"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
//go:embed res
|
||||
var xdbFS embed.FS
|
||||
|
||||
// AppLifecycle 应用程序生命周期
|
||||
type AppLifecycle struct {
|
||||
}
|
||||
|
||||
// OnStart 应用程序启动时执行
|
||||
func (l *AppLifecycle) OnStart(context.Context) error {
|
||||
logger.Info("AppLifecycle OnStart")
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop 应用程序停止时执行
|
||||
func (l *AppLifecycle) OnStop(context.Context) error {
|
||||
logger.Info("AppLifecycle OnStop")
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAppLifeCycle() *AppLifecycle {
|
||||
return &AppLifecycle{}
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := os.Getenv("CONFIG_FILE")
|
||||
if configFile == "" {
|
||||
configFile = "config.toml"
|
||||
}
|
||||
debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
|
||||
logger.Info("Loading config file: ", configFile)
|
||||
if !debug {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("Panic Error:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
app := fx.New(
|
||||
// 初始化配置应用配置
|
||||
fx.Provide(func() *types.AppConfig {
|
||||
config, err := core.LoadConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
config.Path = configFile
|
||||
if debug {
|
||||
_ = core.SaveConfig(config)
|
||||
}
|
||||
return config
|
||||
}),
|
||||
// 创建应用服务
|
||||
fx.Provide(core.NewServer),
|
||||
// 初始化
|
||||
fx.Invoke(func(s *core.AppServer, client *redis.Client) {
|
||||
s.Init(debug, client)
|
||||
}),
|
||||
|
||||
// 初始化数据库
|
||||
fx.Provide(store.NewGormConfig),
|
||||
fx.Provide(store.NewMysql),
|
||||
fx.Provide(store.NewRedisClient),
|
||||
fx.Provide(store.NewLevelDB),
|
||||
|
||||
fx.Provide(func() embed.FS {
|
||||
return xdbFS
|
||||
}),
|
||||
|
||||
// 创建 Ip2Region 查询对象
|
||||
fx.Provide(func() (*xdb.Searcher, error) {
|
||||
file, err := xdbFS.Open("res/ip2region.xdb")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cBuff, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xdb.NewWithBuffer(cBuff)
|
||||
}),
|
||||
|
||||
// 创建控制器
|
||||
fx.Provide(handler.NewChatRoleHandler),
|
||||
fx.Provide(handler.NewUserHandler),
|
||||
fx.Provide(chatimpl.NewChatHandler),
|
||||
fx.Provide(handler.NewUploadHandler),
|
||||
fx.Provide(handler.NewSmsHandler),
|
||||
fx.Provide(handler.NewRewardHandler),
|
||||
fx.Provide(handler.NewCaptchaHandler),
|
||||
fx.Provide(handler.NewMidJourneyHandler),
|
||||
fx.Provide(handler.NewChatModelHandler),
|
||||
fx.Provide(handler.NewSdJobHandler),
|
||||
fx.Provide(handler.NewPaymentHandler),
|
||||
fx.Provide(handler.NewOrderHandler),
|
||||
fx.Provide(handler.NewProductHandler),
|
||||
fx.Provide(handler.NewConfigHandler),
|
||||
fx.Provide(handler.NewPowerLogHandler),
|
||||
|
||||
fx.Provide(admin.NewConfigHandler),
|
||||
fx.Provide(admin.NewAdminHandler),
|
||||
fx.Provide(admin.NewApiKeyHandler),
|
||||
fx.Provide(admin.NewUserHandler),
|
||||
fx.Provide(admin.NewChatRoleHandler),
|
||||
fx.Provide(admin.NewRewardHandler),
|
||||
fx.Provide(admin.NewDashboardHandler),
|
||||
fx.Provide(admin.NewChatModelHandler),
|
||||
fx.Provide(admin.NewProductHandler),
|
||||
fx.Provide(admin.NewOrderHandler),
|
||||
fx.Provide(admin.NewChatHandler),
|
||||
fx.Provide(admin.NewPowerLogHandler),
|
||||
|
||||
// 创建服务
|
||||
fx.Provide(sms.NewSendServiceManager),
|
||||
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
|
||||
return service.NewCaptchaService(config.ApiConfig)
|
||||
}),
|
||||
fx.Provide(oss.NewUploaderManager),
|
||||
fx.Provide(mj.NewService),
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(service *dalle.Service) {
|
||||
service.Run()
|
||||
service.CheckTaskNotify()
|
||||
service.DownloadImages()
|
||||
service.CheckTaskStatus()
|
||||
}),
|
||||
|
||||
// 邮件服务
|
||||
fx.Provide(service.NewSmtpService),
|
||||
// License 服务
|
||||
fx.Provide(service.NewLicenseService),
|
||||
fx.Invoke(func(licenseService *service.LicenseService) {
|
||||
licenseService.SyncLicense()
|
||||
}),
|
||||
|
||||
// 微信机器人服务
|
||||
fx.Provide(wx.NewWeChatBot),
|
||||
fx.Invoke(func(config *types.AppConfig, bot *wx.Bot) {
|
||||
if config.WeChatBot {
|
||||
err := bot.Run()
|
||||
if err != nil {
|
||||
logger.Error("微信登录失败:", err)
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// MidJourney service pool
|
||||
fx.Provide(mj.NewServicePool),
|
||||
fx.Invoke(func(pool *mj.ServicePool, config *types.AppConfig) {
|
||||
pool.InitServices(config.MjPlusConfigs, config.MjProxyConfigs)
|
||||
if pool.HasAvailableService() {
|
||||
pool.DownloadImages()
|
||||
pool.CheckTaskNotify()
|
||||
pool.SyncTaskProgress()
|
||||
}
|
||||
}),
|
||||
|
||||
// Stable Diffusion 机器人
|
||||
fx.Provide(sd.NewServicePool),
|
||||
fx.Invoke(func(pool *sd.ServicePool, config *types.AppConfig) {
|
||||
pool.InitServices(config.SdConfigs)
|
||||
if pool.HasAvailableService() {
|
||||
pool.CheckTaskNotify()
|
||||
pool.CheckTaskStatus()
|
||||
}
|
||||
}),
|
||||
|
||||
fx.Provide(suno.NewService),
|
||||
fx.Invoke(func(s *suno.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadImages()
|
||||
}),
|
||||
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewHuPiPay),
|
||||
fx.Provide(payment.NewJPayService),
|
||||
fx.Provide(payment.NewWechatService),
|
||||
fx.Provide(service.NewSnowflake),
|
||||
fx.Provide(service.NewXXLJobExecutor),
|
||||
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
|
||||
if config.XXLConfig.Enabled {
|
||||
go func() {
|
||||
log.Fatal(exec.Run())
|
||||
}()
|
||||
}
|
||||
}),
|
||||
|
||||
// 注册路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
||||
group := s.Engine.Group("/api/role/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("update", h.UpdateRole)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
|
||||
group := s.Engine.Group("/api/user/")
|
||||
group.POST("register", h.Register)
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
group.GET("session", h.Session)
|
||||
group.GET("profile", h.Profile)
|
||||
group.POST("profile/update", h.ProfileUpdate)
|
||||
group.POST("password", h.UpdatePass)
|
||||
group.POST("bind/username", h.BindUsername)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
group.GET("clogin", h.CLogin)
|
||||
group.GET("clogin/callback", h.CLoginCallback)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *chatimpl.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
group.Any("new", h.ChatHandle)
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("history", h.History)
|
||||
group.GET("clear", h.Clear)
|
||||
group.POST("tokens", h.Tokens)
|
||||
group.GET("stop", h.StopGenerate)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
|
||||
s.Engine.POST("/api/upload", h.Upload)
|
||||
s.Engine.POST("/api/upload/list", h.List)
|
||||
s.Engine.GET("/api/upload/remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
||||
group := s.Engine.Group("/api/sms/")
|
||||
group.POST("code", h.SendCode)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
|
||||
group := s.Engine.Group("/api/captcha/")
|
||||
group.GET("get", h.Get)
|
||||
group.POST("check", h.Check)
|
||||
group.GET("slide/get", h.SlideGet)
|
||||
group.POST("slide/check", h.SlideCheck)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
|
||||
group := s.Engine.Group("/api/reward/")
|
||||
group.POST("verify", h.Verify)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
|
||||
group := s.Engine.Group("/api/mj/")
|
||||
group.Any("client", h.Client)
|
||||
group.POST("image", h.Image)
|
||||
group.POST("upscale", h.Upscale)
|
||||
group.POST("variation", h.Variation)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
|
||||
group := s.Engine.Group("/api/sd")
|
||||
group.Any("client", h.Client)
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/config/")
|
||||
group.GET("get", h.Get)
|
||||
group.GET("license", h.License)
|
||||
}),
|
||||
|
||||
// 管理后台控制器
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
group.POST("config/update", h.Update)
|
||||
group.GET("config/get", h.Get)
|
||||
group.POST("active", h.Active)
|
||||
group.GET("config/get/license", h.GetLicense)
|
||||
group.GET("config/get/app", h.GetAppConfig)
|
||||
group.POST("config/update/draw", h.SaveDrawingConfig)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
group.POST("login", h.Login)
|
||||
group.GET("logout", h.Logout)
|
||||
group.GET("session", h.Session)
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("enable", h.Enable)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
|
||||
group := s.Engine.Group("/api/admin/apikey/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
|
||||
group := s.Engine.Group("/api/admin/user/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("loginLog", h.LoginLog)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) {
|
||||
group := s.Engine.Group("/api/admin/role/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
group.POST("sort", h.Sort)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
|
||||
group := s.Engine.Group("/api/admin/reward/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
|
||||
group := s.Engine.Group("/api/admin/dashboard/")
|
||||
group.GET("stats", h.Stats)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
|
||||
group := s.Engine.Group("/api/model/")
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
|
||||
group := s.Engine.Group("/api/admin/model/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("set", h.Set)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||
group := s.Engine.Group("/api/payment/")
|
||||
group.GET("doPay", h.DoPay)
|
||||
group.GET("payWays", h.GetPayWays)
|
||||
group.POST("qrcode", h.PayQrcode)
|
||||
group.POST("mobile", h.Mobile)
|
||||
group.POST("alipay/notify", h.AlipayNotify)
|
||||
group.POST("hupipay/notify", h.HuPiPayNotify)
|
||||
group.POST("payjs/notify", h.PayJsNotify)
|
||||
group.POST("wechat/notify", h.WechatPayNotify)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||
group := s.Engine.Group("/api/admin/product/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
|
||||
group := s.Engine.Group("/api/admin/order/")
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||
group := s.Engine.Group("/api/order/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("query", h.Query)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
|
||||
group := s.Engine.Group("/api/product/")
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewInviteHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
|
||||
group := s.Engine.Group("/api/invite/")
|
||||
group.GET("code", h.Code)
|
||||
group.POST("list", h.List)
|
||||
group.GET("hits", h.Hits)
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
|
||||
group := s.Engine.Group("/api/admin/function/")
|
||||
group.POST("save", h.Save)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("token", h.GenToken)
|
||||
}),
|
||||
|
||||
fx.Provide(admin.NewUploadHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
|
||||
s.Engine.POST("/api/admin/upload", h.Upload)
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewFunctionHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
|
||||
group := s.Engine.Group("/api/function/")
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("dalle3", h.Dall3)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
|
||||
group := s.Engine.Group("/api/admin/chat/")
|
||||
group.POST("list", h.List)
|
||||
group.POST("message", h.Messages)
|
||||
group.GET("history", h.History)
|
||||
group.GET("remove", h.RemoveChat)
|
||||
group.GET("message/remove", h.RemoveMessage)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
|
||||
group := s.Engine.Group("/api/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) {
|
||||
group := s.Engine.Group("/api/admin/powerLog/")
|
||||
group.POST("list", h.List)
|
||||
}),
|
||||
fx.Provide(admin.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
|
||||
group := s.Engine.Group("/api/admin/menu/")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
group.GET("remove", h.Remove)
|
||||
}),
|
||||
fx.Provide(handler.NewMenuHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
|
||||
group := s.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
fx.Provide(handler.NewMarkMapHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
|
||||
group := s.Engine.Group("/api/markMap/")
|
||||
group.Any("client", h.Client)
|
||||
}),
|
||||
fx.Provide(handler.NewDallJobHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
|
||||
group := s.Engine.Group("/api/dall")
|
||||
group.Any("client", h.Client)
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}),
|
||||
fx.Provide(handler.NewSunoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
|
||||
group := s.Engine.Group("/api/suno")
|
||||
group.Any("client", h.Client)
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("detail", h.Detail)
|
||||
group.GET("play", h.Play)
|
||||
group.POST("lyric", h.Lyric)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
go func() {
|
||||
err := s.Run(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}),
|
||||
fx.Provide(NewAppLifeCycle),
|
||||
// 注册生命周期回调函数
|
||||
fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
|
||||
lifecycle.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
return lc.OnStart(ctx)
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return lc.OnStop(ctx)
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
// 启动应用程序
|
||||
go func() {
|
||||
if err := app.Start(context.Background()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 监听退出信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
// 关闭应用程序
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := app.Stop(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
38
api/res/certs/alipay/alipayPublicCert.crt
Normal file
38
api/res/certs/alipay/alipayPublicCert.crt
Normal file
@@ -0,0 +1,38 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDszCCApugAwIBAgIQICMRB0rBU2/rZJbfJGMYIzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDYzNTQxWhcNMjQxMTA2MDYzNTQxWjCB
|
||||
hDELMAkGA1UEBhMCQ04xHzAdBgNVBAoMFm1ib25meTkwMTVAc2FuZGJveC5jb20xDzANBgNVBAsM
|
||||
BkFsaXBheTFDMEEGA1UEAww65pSv5LuY5a6dKOS4reWbvSnnvZHnu5zmioDmnK/mnInpmZDlhazl
|
||||
j7gtMjA4ODcyMTAyMDc1MDU4MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsoKcw5
|
||||
sxaiyV7mpWzDtnQ1K518eQLP0+dJlZAf06aBep/Aj9DIqrba/k7DHt8dKQvILMLAMpN1+2IRxbaO
|
||||
yxMa/laj3lZ1eHrB6F077O3D62oHcE3noZtXL0N1zZAxpmkNmYIHeLZS2oLMS4ANu47O/wpDC7BV
|
||||
HjdpZugtdPJ4mxdCpM9GDdLs7W4s5QI4PUPK4skFNMFoKI+0cYP/9ju87UP//IHC/K510GWNl+Gn
|
||||
Cvgag3AmiIB0utJNsGhxm6zT1T9tUWjW9iz/BxBKiPatsCX9VpPQzGnW7ZonRQtiZSokIlP2IPvl
|
||||
H5DcwpWUz3/LUY0SmKxnKOEYeOOqCW8CAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQAtgxF2EzjOndEFxBUD9tFwcSt6XKGggOp52oft1pvynPg4ALTLafOtfEPDrFBH
|
||||
PwpYrSu9s9C8NJtaA2HrlCfBjIuwEFTXiN+HPvS0SwSPKt9AXEiTcOF8vDcGamEen8QI4fo5Jia7
|
||||
2VRKkerkww5/+FzSaVO7ZUKuL80M1QJStmAZc8kPPwdYOTTW2bGf8BcmSDL6SPElBkt7tCCRd4sn
|
||||
+jq4cZ0yb2i77rBZCwHcTvfTqIBblPwLv4uGvg3+83BxIB5w6Kqp06bKEAPmobFY5IVHa+ON0/qi
|
||||
BXxXr+WQ3piKRVQEN64+PTAjSc67Ix1umvpLl3Ko6Ry7NJmpDcUn
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDszCCApugAwIBAgIQIBkIGbgVxq210KxLJ+YA/TANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UE
|
||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxJTAjBgNVBAsMHENlcnRpZmljYXRpb24gQXV0
|
||||
aG9yaXR5IHRlc3QxNjA0BgNVBAMMLUFudCBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
||||
dHkgUjEgdGVzdDAeFw0xOTA4MTkxMTE2MDBaFw0yNDA4MDExMTE2MDBaMIGRMQswCQYDVQQGEwJD
|
||||
TjEbMBkGA1UECgwSQW50IEZpbmFuY2lhbCB0ZXN0MSUwIwYDVQQLDBxDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSB0ZXN0MT4wPAYDVQQDDDVBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
|
||||
aXR5IENsYXNzIDIgUjEgdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMh4FKYO
|
||||
ZyRQHD6eFbPKZeSAnrfjfU7xmS9Yoozuu+iuqZlb6Z0SPLUqqTZAFZejOcmr07ln/pwZxluqplxC
|
||||
5+B48End4nclDMlT5HPrDr3W0frs6Xsa2ZNcyil/iKNB5MbGll8LRAxntsKvZZj6vUTMb705gYgm
|
||||
VUMILwi/ZxKTQqBtkT/kQQ5y6nOZsj7XI5rYdz6qqOROrpvS/d7iypdHOMIM9Iz9DlL1mrCykbBi
|
||||
t25y+gTeXmuisHUwqaRpwtCGK4BayCqxRGbNipe6W73EK9lBrrzNtTr9NaysesT/v+l25JHCL9tG
|
||||
wpNr1oWFzk4IHVOg0ORiQ6SUgxZUTYcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQBWThEoIaQoBX2YeRY/I8gu6TYnFXtyuCljANnXnM38ft+ikhE5mMNgKmJYLHvT
|
||||
yWWWgwHoSAWEuml7EGbE/2AK2h3k0MdfiWLzdmpPCRG/RJHk6UB1pMHPilI+c0MVu16OPpKbg5Vf
|
||||
LTv7dsAB40AzKsvyYw88/Ezi1osTXo6QQwda7uefvudirtb8FcQM9R66cJxl3kt1FXbpYwheIm/p
|
||||
j1mq64swCoIYu4NrsUYtn6CV542DTQMI5QdXkn+PzUUly8F6kDp+KpMNd0avfWNL5+O++z+F5Szy
|
||||
1CPta1D7EQ/eYmMP+mOQ35oifWIoFCpN6qQVBS/Hob1J/UUyg7BW
|
||||
-----END CERTIFICATE-----
|
||||
88
api/res/certs/alipay/alipayRootCert.crt
Normal file
88
api/res/certs/alipay/alipayRootCert.crt
Normal file
@@ -0,0 +1,88 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG
|
||||
EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw
|
||||
MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO
|
||||
UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE
|
||||
MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT
|
||||
V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti
|
||||
W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ
|
||||
MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b
|
||||
53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI
|
||||
pDoiVhsLwg==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE
|
||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj
|
||||
YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj
|
||||
YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0
|
||||
MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV
|
||||
BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j
|
||||
aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk
|
||||
rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2
|
||||
xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp
|
||||
dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6
|
||||
vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl
|
||||
YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1
|
||||
Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H
|
||||
DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98
|
||||
SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG
|
||||
PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe
|
||||
9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC
|
||||
AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90
|
||||
tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy
|
||||
nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf
|
||||
tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq
|
||||
JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3
|
||||
IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW
|
||||
05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41
|
||||
T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI
|
||||
kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop
|
||||
PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N
|
||||
1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y
|
||||
jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02
|
||||
77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi
|
||||
kT9qhqn+lw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG
|
||||
EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh
|
||||
dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh
|
||||
dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0
|
||||
WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE
|
||||
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp
|
||||
YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
|
||||
IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU
|
||||
WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt
|
||||
rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ
|
||||
4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2
|
||||
zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg
|
||||
wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH
|
||||
Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF
|
||||
BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM
|
||||
E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg
|
||||
MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq
|
||||
MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp
|
||||
bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv
|
||||
b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV
|
||||
nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5
|
||||
4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg
|
||||
wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw
|
||||
WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN
|
||||
z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g
|
||||
KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA
|
||||
uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp
|
||||
emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3
|
||||
U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I
|
||||
UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn
|
||||
DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU
|
||||
1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX
|
||||
Yf4Zr0fJsGuv
|
||||
-----END CERTIFICATE-----
|
||||
19
api/res/certs/alipay/appPublicCert.crt
Normal file
19
api/res/certs/alipay/appPublicCert.crt
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDmTCCAoGgAwIBAgIQICMRB2LW76yahgdg3IFNPDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
|
||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
|
||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDU0NjE5WhcNMjQxMTExMDU0NjE5WjBr
|
||||
MQswCQYDVQQGEwJDTjEfMB0GA1UECgwWbWJvbmZ5OTAxNUBzYW5kYm94LmNvbTEPMA0GA1UECwwG
|
||||
QWxpcGF5MSowKAYDVQQDDCEyMDg4NzIxMDIwNzUwNTgxLTkwMjEwMDAxMzE2NTgwMjMwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxihQPf1Q+g9ArgM46shVqL5sbRha/df95D1PsWyEq
|
||||
ANmWmG4zZ+ksYDVQrc4KzhSRoi56sm/7TDFYTmM6bW99e/nKW58WxyZB4ie5qA3F4n17psPyDqb8
|
||||
IokcQmCphSFDaXQD6AoXoLNtTM0vAI2cWxAgebZ/vsrdj5Ntjt+Rp3NYMCk1i5xovHcfILzLEGbX
|
||||
QXoT9fo5AhHotTWa6xHVLPUGY9qwLzQxHzBmvy5ZMfnOfJkm/mDisTSqAUB59F3dzU/1ARVkEZ1w
|
||||
Mgb4XohWBw6iurQfbMnH2mIomAAwwZVFv+sXDbL9yMbSMo/SjVsTQprn0Q0EnwLo7nmmOM6HAgMB
|
||||
AAGjEjAQMA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAn3Y4/C1h9R6ONsBqX3/q
|
||||
XfHX7yX1FM0Y1x48X3/Yxk6HivAkTukhhhVYVKJsbrbzRqHDp9vhAP/FR6o6pAevaYMmLov0VMXU
|
||||
7oAuetgkaYEYkDuNen5/Hpdhqi2vTtdT+q9w8zHJd6MDQ0aoHgIxpLKw5vof2R1N4fwSgNXMiXE5
|
||||
kmllKQMem/+on2p+Sj80/2asxryHIGlH87qPzkffv+kIOkZthbTApTFLLjdVri2QHGe8/cc4xy01
|
||||
/9iR3IUzNahotT41lJ4bMevBY7XMAS3n5ekyABN/9ZRJqhWdXgmFCRN/u56qd6lDgu7R2M2QUoyc
|
||||
LuW5DfgRItKlmUB7sw==
|
||||
-----END CERTIFICATE-----
|
||||
1
api/res/certs/alipay/privateKey.txt
Normal file
1
api/res/certs/alipay/privateKey.txt
Normal file
@@ -0,0 +1 @@
|
||||
MIIEpQIBAAKCAQEAsYoUD39UPoPQK4DOOrIVai+bG0YWv3X/eQ9T7FshKgDZlphuM2fpLGA1UK3OCs4UkaIuerJv+0wxWE5jOm1vfXv5ylufFscmQeInuagNxeJ9e6bD8g6m/CKJHEJgqYUhQ2l0A+gKF6CzbUzNLwCNnFsQIHm2f77K3Y+TbY7fkadzWDApNYucaLx3HyC8yxBm10F6E/X6OQIR6LU1musR1Sz1BmPasC80MR8wZr8uWTH5znyZJv5g4rE0qgFAefRd3c1P9QEVZBGdcDIG+F6IVgcOorq0H2zJx9piKJgAMMGVRb/rFw2y/cjG0jKP0o1bE0Ka59ENBJ8C6O55pjjOhwIDAQABAoIBAFetNfz1R7hbxjlFshMAkVzQR8wvT9qbvl+dtzdZRcaFhu89NecDIP7+QDYor0FcxoGpU0TazDyRQyk2BQD8vHt+9zv9BVLtZLJSqoWgPbUFBi1DjS8EF2ka8RVYnn35NhUhhd7L//ftL88Bh673mfembQ9srDjoEy1Z01feoABAnCMkNFl986DmEwnarvEufXSDIgeN4ioMxha4NvfIPuI0zpVdV1O9sv+SGC+VEWZBtN3GNsaf4zS/f8FVGvTiU/Abz0gSw/iwSPHclDWQDTN3yFHf/tfqlzh0mH0WfhnuOBFWXzK+R7fbnM+asI9ttvzRcfpzgRGXdPcNcOv/6cECgYEA3DVqpi1k8MYfJixju6SG5gfyhM4VFksFmCMaNPgtatDMBKLMTgV/Ej6LXREojcy29uZl83F09pVlpd41eG39ULIPktixA/BqErQ2UaWh6kOxifycpu22Jh0r09hax6UgVrcBrrnCJEjcFsuJlrZvXQSzc3PBxjWy5gjabS5h9iECgYEAzmVAIh2frF01Y95zsLueAhhZwCtPanm6kf7ivR4r1plIX3b2sNRhWGmEHFgaCE6Braa0ogQ73Hd26kw4ZW+D6QMGC/zjCBEzDLLf++SjdVUHiY5AR4WHqXzq1jdAlsVyo9R661oAOp3lhiJVGLNXkHyEfEVPHsaxJh4osYSbX6cCgYEAx32Qx0i6eDFTyLZQB46uMrgiaVN04QRH5iJuvGvUYT8UhGKjaU8rZfDJOh+wOH2rhxMEaz1uc3C2bERY9mfWI4Ob/jFWc7YZsiYWS3Mcsuhubw4tMECLUg39RWZsHw8ls8kIuixIh6yFzhTH6YQOcRswIrhMZG8DScfdcSmiz2ECgYEAkWP1t5KSpkLKl11etcKUXfl1T8+yk9jIOowIgRw92WAFAWq2AH67TCKYM7dEL1HOO9tRJ0hAOt/U3ttuZtYVYBEHM26jJ02mXm2rJrA7DS4mrxmL4lYH6LbcXqZxU0Qnq4zEQgIWYzRTORf6Rfof1uJAGaJhR9bDd4yLMfGt2cUCgYEAo216Y61xOHUTA4AF1eekk+r+uOcQgQDvLXfs9FkDdJLk0mPG48/+eIYpPFnANJ/riF/DWOp8WGEe2IzA9yUFexzDbNQK8ha9kGcxaSAyiCwzjZ/t9/+hScDSV8kNqWSRSisu/YOFleEHbokT6mbLZ+gdqES8mUUanaEBzRQYGxo=
|
||||
BIN
api/res/img/alipay.jpg
Normal file
BIN
api/res/img/alipay.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
api/res/img/wechat-pay.jpg
Normal file
BIN
api/res/img/wechat-pay.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
api/res/ip2region.xdb
Normal file
BIN
api/res/ip2region.xdb
Normal file
Binary file not shown.
110
api/service/captcha_service.go
Normal file
110
api/service/captcha_service.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"github.com/imroc/req/v3"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CaptchaService struct {
|
||||
config types.ApiConfig
|
||||
client *req.Client
|
||||
}
|
||||
|
||||
func NewCaptchaService(config types.ApiConfig) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
config: config,
|
||||
client: req.C().SetTimeout(10 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Get() (interface{}, error) {
|
||||
if s.config.Token == "" {
|
||||
return nil, errors.New("无效的 API Token")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return nil, fmt.Errorf("请求 API 失败:%v", err)
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("请求 API 失败:%s", res.Message)
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Check(data interface{}) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetBodyJsonMarshal(data).
|
||||
SetSuccessResult(&res).Post(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return false
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *CaptchaService) SlideGet() (interface{}, error) {
|
||||
if s.config.Token == "" {
|
||||
return nil, errors.New("无效的 API Token")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/get", s.config.ApiURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetSuccessResult(&res).Get(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return nil, fmt.Errorf("请求 API 失败:%v", err)
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("请求 API 失败:%s", res.Message)
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) SlideCheck(data interface{}) bool {
|
||||
url := fmt.Sprintf("%s/api/captcha/slide/check", s.config.ApiURL)
|
||||
var res types.BizVo
|
||||
r, err := s.client.R().
|
||||
SetHeader("AppId", s.config.AppId).
|
||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
||||
SetBodyJsonMarshal(data).
|
||||
SetSuccessResult(&res).Post(url)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return false
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
314
api/service/dalle/service.go
Normal file
314
api/service/dalle/service.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package dalle
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// DALL-E 绘画服务
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client) *Service {
|
||||
return &Service{
|
||||
httpClient: req.C().SetTimeout(time.Minute * 3),
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
|
||||
Clients: types.NewLMap[uint, *types.WsClient](),
|
||||
uploadManager: manager,
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (s *Service) PushTask(task types.DallTask) {
|
||||
logger.Infof("add a new DALL-E task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
logger.Info("Starting DALL-E job consumer...")
|
||||
go func() {
|
||||
for {
|
||||
var task types.DallTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("handle a new DALL-E task: %+v", task)
|
||||
_, err = s.Image(task, false)
|
||||
if err != nil {
|
||||
logger.Errorf("error with image task: %v", err)
|
||||
s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
|
||||
"progress": -1,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Failed})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Size string `json:"size"`
|
||||
Quality string `json:"quality"`
|
||||
Style string `json:"style"`
|
||||
}
|
||||
|
||||
type imgRes struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []struct {
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
logger.Debugf("绘画参数:%+v", task)
|
||||
prompt := task.Prompt
|
||||
// translate prompt
|
||||
if utils.HasChinese(prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt), "gpt-4o-mini")
|
||||
if err == nil {
|
||||
prompt = content
|
||||
logger.Debugf("重写后提示词:%s", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
s.db.Where("id", task.UserId).First(&user)
|
||||
if user.Power < task.Power {
|
||||
return "", errors.New("insufficient of power")
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
tx := s.db.Model(&model.User{}).Where("id", user.Id).UpdateColumn("power", gorm.Expr("power - ?", task.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
var u model.User
|
||||
s.db.Where("id", user.Id).First(&u)
|
||||
s.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: task.Power,
|
||||
Balance: u.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// get image generation API KEY
|
||||
var apiKey model.ApiKey
|
||||
tx = s.db.Where("type", "dalle").
|
||||
Where("enabled", true).
|
||||
Order("last_used_at ASC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return "", fmt.Errorf("no available IMG api key: %v", tx.Error)
|
||||
}
|
||||
|
||||
var res imgRes
|
||||
var errRes ErrRes
|
||||
if len(apiKey.ProxyURL) > 5 {
|
||||
s.httpClient.SetProxyURL(apiKey.ProxyURL).R()
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
||||
reqBody := imgReq{
|
||||
Model: "dall-e-3",
|
||||
Prompt: prompt,
|
||||
N: 1,
|
||||
Size: task.Size,
|
||||
Style: task.Style,
|
||||
Quality: task.Quality,
|
||||
}
|
||||
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
|
||||
r, err := s.httpClient.R().SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
SetErrorResult(&errRes).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with send request: %v", err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return "", fmt.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
|
||||
}
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
// update task progress
|
||||
tx = s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
|
||||
"progress": 100,
|
||||
"org_url": res.Data[0].Url,
|
||||
"prompt": prompt,
|
||||
})
|
||||
if tx.Error != nil {
|
||||
return "", fmt.Errorf("err with update database: %v", tx.Error)
|
||||
}
|
||||
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Finished})
|
||||
var content string
|
||||
if sync {
|
||||
imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", prompt, imgURL)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running DALL-E task notify checking ...")
|
||||
for {
|
||||
var message sd.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
client := s.Clients.Get(uint(message.UserId))
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
err = client.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadImages() {
|
||||
go func() {
|
||||
var items []model.DallJob
|
||||
for {
|
||||
res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// download images
|
||||
for _, v := range items {
|
||||
if v.OrgURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try to download image: %s", v.OrgURL)
|
||||
imgURL, err := s.downloadImage(v.Id, int(v.UserId), v.OrgURL)
|
||||
if err != nil {
|
||||
logger.Error("error with download image: %s, error: %v", imgURL, err)
|
||||
continue
|
||||
} else {
|
||||
logger.Infof("download image %s successfully.", v.OrgURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
|
||||
// sava image
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// update img_url
|
||||
res := s.db.Model(&model.DallJob{Id: jobId}).UpdateColumn("img_url", imgURL)
|
||||
if res.Error != nil {
|
||||
return "", err
|
||||
}
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: userId, JobId: int(jobId), Message: sd.Finished})
|
||||
return imgURL, nil
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
func (s *Service) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task status checking ...")
|
||||
for {
|
||||
var jobs []model.DallJob
|
||||
res := s.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 5 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
|
||||
s.db.Delete(&job)
|
||||
var user model.User
|
||||
s.db.Where("id = ?", job.UserId).First(&user)
|
||||
// 退回绘图次数
|
||||
res = s.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
|
||||
if res.Error == nil && res.RowsAffected > 0 {
|
||||
s.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power + job.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d", job.Id),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
197
api/service/license_service.go
Normal file
197
api/service/license_service.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package service
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
type LicenseService struct {
|
||||
config types.ApiConfig
|
||||
levelDB *store.LevelDB
|
||||
license *types.License
|
||||
urlWhiteList []string
|
||||
machineId string
|
||||
}
|
||||
|
||||
func NewLicenseService(server *core.AppServer, levelDB *store.LevelDB) *LicenseService {
|
||||
var license types.License
|
||||
return &LicenseService{
|
||||
config: server.Config.ApiConfig,
|
||||
levelDB: levelDB,
|
||||
license: &license,
|
||||
machineId: "",
|
||||
}
|
||||
}
|
||||
|
||||
type License struct {
|
||||
Name string `json:"name"`
|
||||
License string `json:"license"`
|
||||
MachineId string `json:"mid"`
|
||||
ActiveAt int64 `json:"active_at"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
UserNum int `json:"user_num"`
|
||||
Configs types.LicenseConfig `json:"configs"`
|
||||
}
|
||||
|
||||
// ActiveLicense 激活 License
|
||||
func (s *LicenseService) ActiveLicense(license string, machineId string) error {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data License `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/active")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": license, "machine_id": machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送激活请求失败: %v", err)
|
||||
}
|
||||
|
||||
if response.IsErrorState() {
|
||||
return fmt.Errorf("发送激活请求失败:%v", response.Status)
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return fmt.Errorf("激活失败:%v", res.Message)
|
||||
}
|
||||
|
||||
s.license = &types.License{
|
||||
Key: license,
|
||||
MachineId: machineId,
|
||||
Configs: res.Data.Configs,
|
||||
ExpiredAt: res.Data.ExpiredAt,
|
||||
IsActive: true,
|
||||
}
|
||||
err = s.levelDB.Put(types.LicenseKey, s.license)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存许可证书失败:%v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncLicense 定期同步 License
|
||||
func (s *LicenseService) SyncLicense() {
|
||||
go func() {
|
||||
retryCounter := 0
|
||||
for {
|
||||
license, err := s.fetchLicense()
|
||||
if err != nil {
|
||||
retryCounter++
|
||||
if retryCounter < 5 {
|
||||
logger.Warn(err)
|
||||
}
|
||||
s.license.IsActive = false
|
||||
} else {
|
||||
s.license = license
|
||||
}
|
||||
|
||||
urls, err := s.fetchUrlWhiteList()
|
||||
if err == nil {
|
||||
s.urlWhiteList = urls
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *LicenseService) fetchLicense() (*types.License, error) {
|
||||
//var res struct {
|
||||
// Code types.BizCode `json:"code"`
|
||||
// Message string `json:"message"`
|
||||
// Data License `json:"data"`
|
||||
//}
|
||||
//apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/check")
|
||||
//response, err := req.C().R().
|
||||
// SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
|
||||
// SetSuccessResult(&res).Post(apiURL)
|
||||
//if err != nil {
|
||||
// return nil, fmt.Errorf("发送激活请求失败: %v", err)
|
||||
//}
|
||||
//if response.IsErrorState() {
|
||||
// return nil, fmt.Errorf("激活失败:%v", response.Status)
|
||||
//}
|
||||
//if res.Code != types.Success {
|
||||
// return nil, fmt.Errorf("激活失败:%v", res.Message)
|
||||
//}
|
||||
|
||||
return &types.License{
|
||||
Key: "abc",
|
||||
MachineId: "abc",
|
||||
Configs: types.LicenseConfig{
|
||||
UserNum: 10000,
|
||||
DeCopy: false,
|
||||
},
|
||||
ExpiredAt: 0,
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/urls")
|
||||
response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return nil, fmt.Errorf("发送请求失败:%v", response.Status)
|
||||
}
|
||||
if res.Code != types.Success {
|
||||
return nil, fmt.Errorf("获取白名单失败:%v", res.Message)
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
// GetLicense 获取许可信息
|
||||
func (s *LicenseService) GetLicense() *types.License {
|
||||
return s.license
|
||||
}
|
||||
|
||||
// IsValidApiURL 判断是否合法的中转 URL
|
||||
func (s *LicenseService) IsValidApiURL(uri string) error {
|
||||
// 获得许可授权的直接放行
|
||||
return nil
|
||||
//if s.license.IsActive {
|
||||
// if s.license.MachineId != s.machineId {
|
||||
// return errors.New("系统使用了盗版的许可证书")
|
||||
// }
|
||||
//
|
||||
// if time.Now().Unix() > s.license.ExpiredAt {
|
||||
// return errors.New("系统许可证书已经过期")
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//if len(s.urlWhiteList) == 0 {
|
||||
// urls, err := s.fetchUrlWhiteList()
|
||||
// if err == nil {
|
||||
// s.urlWhiteList = urls
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//for _, v := range s.urlWhiteList {
|
||||
// if strings.HasPrefix(uri, v) {
|
||||
// return nil
|
||||
// }
|
||||
//}
|
||||
//return fmt.Errorf("当前 API 地址 %s 不在白名单列表当中。", uri)
|
||||
}
|
||||
68
api/service/mj/client.go
Normal file
68
api/service/mj/client.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import "geekai/core/types"
|
||||
|
||||
type Client interface {
|
||||
Imagine(task types.MjTask) (ImageRes, error)
|
||||
Blend(task types.MjTask) (ImageRes, error)
|
||||
SwapFace(task types.MjTask) (ImageRes, error)
|
||||
Upscale(task types.MjTask) (ImageRes, error)
|
||||
Variation(task types.MjTask) (ImageRes, error)
|
||||
QueryTask(taskId string) (QueryRes, error)
|
||||
}
|
||||
|
||||
type ImageReq struct {
|
||||
BotType string `json:"botType,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Dimensions string `json:"dimensions,omitempty"`
|
||||
Base64Array []string `json:"base64Array,omitempty"`
|
||||
AccountFilter interface{} `json:"accountFilter,omitempty"`
|
||||
NotifyHook string `json:"notifyHook,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
type ImageRes struct {
|
||||
Code int `json:"code"`
|
||||
Description string `json:"description"`
|
||||
Properties struct {
|
||||
} `json:"properties"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type QueryRes struct {
|
||||
Action string `json:"action"`
|
||||
Buttons []struct {
|
||||
CustomId string `json:"customId"`
|
||||
Emoji string `json:"emoji"`
|
||||
Label string `json:"label"`
|
||||
Style int `json:"style"`
|
||||
Type int `json:"type"`
|
||||
} `json:"buttons"`
|
||||
Description string `json:"description"`
|
||||
FailReason string `json:"failReason"`
|
||||
FinishTime int `json:"finishTime"`
|
||||
Id string `json:"id"`
|
||||
ImageUrl string `json:"imageUrl"`
|
||||
Progress string `json:"progress"`
|
||||
Prompt string `json:"prompt"`
|
||||
PromptEn string `json:"promptEn"`
|
||||
Properties struct {
|
||||
} `json:"properties"`
|
||||
StartTime int `json:"startTime"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
SubmitTime int `json:"submitTime"`
|
||||
}
|
||||
204
api/service/mj/plus_client.go
Normal file
204
api/service/mj/plus_client.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/utils"
|
||||
"github.com/imroc/req/v3"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PlusClient MidJourney Plus ProxyClient
|
||||
type PlusClient struct {
|
||||
Config types.MjPlusConfig
|
||||
apiURL string
|
||||
client *req.Client
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewPlusClient(config types.MjPlusConfig, licenseService *service.LicenseService) *PlusClient {
|
||||
return &PlusClient{
|
||||
Config: config,
|
||||
apiURL: config.ApiURL,
|
||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PlusClient) preCheck() error {
|
||||
return c.licenseService.IsValidApiURL(c.Config.ApiURL)
|
||||
}
|
||||
|
||||
func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/imagine", c.apiURL, c.Config.Mode)
|
||||
prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
|
||||
if task.NegPrompt != "" {
|
||||
prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
|
||||
}
|
||||
body := ImageReq{
|
||||
BotType: "MID_JOURNEY",
|
||||
Prompt: prompt,
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
|
||||
}
|
||||
return c.doRequest(body, apiURL)
|
||||
}
|
||||
|
||||
// Blend 融图
|
||||
func (c *PlusClient) Blend(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/blend", c.apiURL, c.Config.Mode)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
body := ImageReq{
|
||||
BotType: "MID_JOURNEY",
|
||||
Dimensions: "SQUARE",
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
for _, imgURL := range task.ImgArr {
|
||||
imageData, err := utils.DownloadImage(imgURL, "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.doRequest(body, apiURL)
|
||||
}
|
||||
|
||||
// SwapFace 换脸
|
||||
func (c *PlusClient) SwapFace(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/insight-face/swap", c.apiURL, c.Config.Mode)
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) != 2 {
|
||||
return ImageRes{}, errors.New("参数错误,必须上传2张图片")
|
||||
}
|
||||
var sourceBase64 string
|
||||
var targetBase64 string
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
sourceBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
|
||||
}
|
||||
imageData, err = utils.DownloadImage(task.ImgArr[1], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
targetBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
|
||||
}
|
||||
|
||||
body := gin.H{
|
||||
"sourceBase64": sourceBase64,
|
||||
"targetBase64": targetBase64,
|
||||
"accountFilter": gin.H{
|
||||
"instanceId": "",
|
||||
},
|
||||
"state": "",
|
||||
}
|
||||
return c.doRequest(body, apiURL)
|
||||
}
|
||||
|
||||
// Upscale 放大指定的图片
|
||||
func (c *PlusClient) Upscale(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"customId": fmt.Sprintf("MJ::JOB::upsample::%d::%s", task.Index, task.MessageHash),
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
|
||||
return c.doRequest(body, apiURL)
|
||||
}
|
||||
|
||||
// Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
|
||||
func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"customId": fmt.Sprintf("MJ::JOB::variation::%d::%s", task.Index, task.MessageHash),
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
|
||||
|
||||
return c.doRequest(body, apiURL)
|
||||
}
|
||||
|
||||
func (c *PlusClient) doRequest(body interface{}, apiURL string) (ImageRes, error) {
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
logger.Info("API URL: ", apiURL)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *PlusClient) QueryTask(taskId string) (QueryRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", c.apiURL, taskId)
|
||||
var res QueryRes
|
||||
r, err := c.client.R().SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
|
||||
SetSuccessResult(&res).
|
||||
Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return QueryRes{}, err
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return QueryRes{}, errors.New("error status:" + r.Status)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var _ Client = &PlusClient{}
|
||||
207
api/service/mj/pool.go
Normal file
207
api/service/mj/pool.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ServicePool Mj service pool
|
||||
type ServicePool struct {
|
||||
services []*Service
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
uploaderManager *oss.UploaderManager
|
||||
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService) *ServicePool {
|
||||
services := make([]*Service, 0)
|
||||
taskQueue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli)
|
||||
notifyQueue := store.NewRedisQueue("MidJourney_Notify_Queue", redisCli)
|
||||
return &ServicePool{
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
services: services,
|
||||
uploaderManager: manager,
|
||||
db: db,
|
||||
Clients: types.NewLMap[uint, *types.WsClient](),
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServicePool) InitServices(plusConfigs []types.MjPlusConfig, proxyConfigs []types.MjProxyConfig) {
|
||||
// stop old service
|
||||
for _, s := range p.services {
|
||||
s.Stop()
|
||||
}
|
||||
p.services = make([]*Service, 0)
|
||||
|
||||
for _, config := range plusConfigs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
|
||||
cli := NewPlusClient(config, p.licenseService)
|
||||
name := utils.Md5(config.ApiURL)
|
||||
plusService := NewService(name, p.taskQueue, p.notifyQueue, p.db, cli)
|
||||
go func() {
|
||||
plusService.Run()
|
||||
}()
|
||||
p.services = append(p.services, plusService)
|
||||
}
|
||||
|
||||
// for mid-journey proxy
|
||||
for _, config := range proxyConfigs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
cli := NewProxyClient(config)
|
||||
name := utils.Md5(config.ApiURL)
|
||||
proxyService := NewService(name, p.taskQueue, p.notifyQueue, p.db, cli)
|
||||
go func() {
|
||||
proxyService.Run()
|
||||
}()
|
||||
p.services = append(p.services, proxyService)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServicePool) CheckTaskNotify() {
|
||||
go func() {
|
||||
for {
|
||||
var message sd.NotifyMessage
|
||||
err := p.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cli := p.Clients.Get(uint(message.UserId))
|
||||
if cli == nil {
|
||||
continue
|
||||
}
|
||||
err = cli.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *ServicePool) DownloadImages() {
|
||||
go func() {
|
||||
var items []model.MidJourneyJob
|
||||
for {
|
||||
res := p.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// download images
|
||||
for _, v := range items {
|
||||
if v.OrgURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try to download image: %s", v.OrgURL)
|
||||
mjService := p.getService(v.ChannelId)
|
||||
if mjService == nil {
|
||||
logger.Errorf("Invalid task: %+v", v)
|
||||
continue
|
||||
}
|
||||
|
||||
task, _ := mjService.Client.QueryTask(v.TaskId)
|
||||
if len(task.Buttons) > 0 {
|
||||
v.Hash = GetImageHash(task.Buttons[0].CustomId)
|
||||
}
|
||||
// 如果是返回的是 discord 图片地址,则使用代理下载
|
||||
proxy := false
|
||||
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
|
||||
proxy = true
|
||||
}
|
||||
imgURL, err := p.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
|
||||
continue
|
||||
} else {
|
||||
logger.Infof("download image %s successfully.", v.OrgURL)
|
||||
}
|
||||
|
||||
v.ImgURL = imgURL
|
||||
p.db.Updates(&v)
|
||||
|
||||
cli := p.Clients.Get(uint(v.UserId))
|
||||
if cli == nil {
|
||||
continue
|
||||
}
|
||||
err = cli.Send([]byte(sd.Finished))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (p *ServicePool) PushTask(task types.MjTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
p.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
// HasAvailableService check if it has available mj service in pool
|
||||
func (p *ServicePool) HasAvailableService() bool {
|
||||
return len(p.services) > 0
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
func (p *ServicePool) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.MidJourneyJob
|
||||
for {
|
||||
res := p.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
if servicePlus := p.getService(job.ChannelId); servicePlus != nil {
|
||||
_ = servicePlus.Notify(job)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *ServicePool) getService(name string) *Service {
|
||||
for _, s := range p.services {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
185
api/service/mj/proxy_client.go
Normal file
185
api/service/mj/proxy_client.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"github.com/imroc/req/v3"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ProxyClient MidJourney Proxy Client
|
||||
type ProxyClient struct {
|
||||
Config types.MjProxyConfig
|
||||
apiURL string
|
||||
}
|
||||
|
||||
func NewProxyClient(config types.MjProxyConfig) *ProxyClient {
|
||||
return &ProxyClient{Config: config, apiURL: config.ApiURL}
|
||||
}
|
||||
|
||||
func (c *ProxyClient) Imagine(task types.MjTask) (ImageRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/imagine", c.apiURL)
|
||||
prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
|
||||
if task.NegPrompt != "" {
|
||||
prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
|
||||
}
|
||||
body := ImageReq{
|
||||
Prompt: prompt,
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
|
||||
}
|
||||
logger.Info("API URL: ", apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
errStr, _ := io.ReadAll(r.Body)
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s,%v", errRes.Error.Message, string(errStr))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Blend 融图
|
||||
func (c *ProxyClient) Blend(task types.MjTask) (ImageRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/blend", c.apiURL)
|
||||
body := ImageReq{
|
||||
Dimensions: "SQUARE",
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
for _, imgURL := range task.ImgArr {
|
||||
imageData, err := utils.DownloadImage(imgURL, "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
}
|
||||
}
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SwapFace 换脸
|
||||
func (c *ProxyClient) SwapFace(_ types.MjTask) (ImageRes, error) {
|
||||
return ImageRes{}, errors.New("MidJourney-Proxy暂未实现该功能,请使用 MidJourney-Plus")
|
||||
}
|
||||
|
||||
// Upscale 放大指定的图片
|
||||
func (c *ProxyClient) Upscale(task types.MjTask) (ImageRes, error) {
|
||||
body := map[string]interface{}{
|
||||
"action": "UPSCALE",
|
||||
"index": task.Index,
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/change", c.apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
|
||||
func (c *ProxyClient) Variation(task types.MjTask) (ImageRes, error) {
|
||||
body := map[string]interface{}{
|
||||
"action": "VARIATION",
|
||||
"index": task.Index,
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/change", c.apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *ProxyClient) QueryTask(taskId string) (QueryRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", c.apiURL, taskId)
|
||||
var res QueryRes
|
||||
r, err := req.C().R().SetHeader("mj-api-secret", c.Config.ApiKey).
|
||||
SetSuccessResult(&res).
|
||||
Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return QueryRes{}, err
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return QueryRes{}, errors.New("error status:" + r.Status)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var _ Client = &ProxyClient{}
|
||||
213
api/service/mj/service.go
Normal file
213
api/service/mj/service.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Service MJ 绘画服务
|
||||
type Service struct {
|
||||
Name string // service Name
|
||||
Client Client // MJ Client
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
running bool
|
||||
retryCount map[uint]int
|
||||
}
|
||||
|
||||
func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, cli Client) *Service {
|
||||
return &Service{
|
||||
Name: name,
|
||||
db: db,
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
Client: cli,
|
||||
running: true,
|
||||
retryCount: make(map[uint]int),
|
||||
}
|
||||
}
|
||||
|
||||
const failedProgress = 101
|
||||
|
||||
func (s *Service) Run() {
|
||||
logger.Infof("Starting MidJourney job consumer for %s", s.Name)
|
||||
for s.running {
|
||||
var task types.MjTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果配置了多个中转平台的 API KEY
|
||||
// U,V 操作必须和 Image 操作属于同一个平台,否则找不到关联任务,需重新放回任务列表
|
||||
if task.ChannelId != "" && task.ChannelId != s.Name {
|
||||
if s.retryCount[task.Id] > 5 {
|
||||
s.db.Model(model.MidJourneyJob{Id: task.Id}).Delete(&model.MidJourneyJob{})
|
||||
continue
|
||||
}
|
||||
logger.Debugf("handle other service task, name: %s, channel_id: %s, drop it.", s.Name, task.ChannelId)
|
||||
s.taskQueue.RPush(task)
|
||||
s.retryCount[task.Id]++
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt), "gpt-4o-mini")
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
// translate negative prompt
|
||||
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt), "gpt-4o-mini")
|
||||
if err == nil {
|
||||
task.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var job model.MidJourneyJob
|
||||
tx := s.db.Where("id = ?", task.Id).First(&job)
|
||||
if tx.Error != nil {
|
||||
logger.Error("任务不存在,任务ID:", task.TaskId)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("%s handle a new MidJourney task: %+v", s.Name, task)
|
||||
var res ImageRes
|
||||
switch task.Type {
|
||||
case types.TaskImage:
|
||||
res, err = s.Client.Imagine(task)
|
||||
break
|
||||
case types.TaskUpscale:
|
||||
res, err = s.Client.Upscale(task)
|
||||
break
|
||||
case types.TaskVariation:
|
||||
res, err = s.Client.Variation(task)
|
||||
break
|
||||
case types.TaskBlend:
|
||||
res, err = s.Client.Blend(task)
|
||||
break
|
||||
case types.TaskSwapFace:
|
||||
res, err = s.Client.SwapFace(task)
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil || (res.Code != 1 && res.Code != 22) {
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
} else {
|
||||
errMsg = fmt.Sprintf("%v,%s", err, res.Description)
|
||||
}
|
||||
|
||||
logger.Error("绘画任务执行失败:", errMsg)
|
||||
job.Progress = failedProgress
|
||||
job.ErrMsg = errMsg
|
||||
// update the task progress
|
||||
s.db.Updates(&job)
|
||||
// 任务失败,通知前端
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: task.UserId, JobId: int(job.Id), Message: sd.Failed})
|
||||
continue
|
||||
}
|
||||
logger.Infof("任务提交成功:%+v", res)
|
||||
// 更新任务 ID/频道
|
||||
job.TaskId = res.Result
|
||||
job.MessageId = res.Result
|
||||
job.ChannelId = s.Name
|
||||
s.db.Updates(&job)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
s.running = false
|
||||
}
|
||||
|
||||
type CBReq struct {
|
||||
Id string `json:"id"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
Prompt string `json:"prompt"`
|
||||
PromptEn string `json:"promptEn"`
|
||||
Description string `json:"description"`
|
||||
SubmitTime int64 `json:"submitTime"`
|
||||
StartTime int64 `json:"startTime"`
|
||||
FinishTime int64 `json:"finishTime"`
|
||||
Progress string `json:"progress"`
|
||||
ImageUrl string `json:"imageUrl"`
|
||||
FailReason interface{} `json:"failReason"`
|
||||
Properties struct {
|
||||
FinalPrompt string `json:"finalPrompt"`
|
||||
} `json:"properties"`
|
||||
}
|
||||
|
||||
func (s *Service) Notify(job model.MidJourneyJob) error {
|
||||
task, err := s.Client.QueryTask(job.TaskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 任务执行失败了
|
||||
if task.FailReason != "" {
|
||||
s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": failedProgress,
|
||||
"err_msg": task.FailReason,
|
||||
})
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed})
|
||||
return fmt.Errorf("task failed: %v", task.FailReason)
|
||||
}
|
||||
|
||||
if len(task.Buttons) > 0 {
|
||||
job.Hash = GetImageHash(task.Buttons[0].CustomId)
|
||||
}
|
||||
oldProgress := job.Progress
|
||||
job.Progress = utils.IntValue(strings.Replace(task.Progress, "%", "", 1), 0)
|
||||
job.Prompt = task.PromptEn
|
||||
if task.ImageUrl != "" {
|
||||
job.OrgURL = task.ImageUrl
|
||||
}
|
||||
tx := s.db.Updates(&job)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("error with update database: %v", tx.Error)
|
||||
}
|
||||
// 通知前端更新任务进度
|
||||
if oldProgress != job.Progress {
|
||||
message := sd.Running
|
||||
if job.Progress == 100 {
|
||||
message = sd.Finished
|
||||
}
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: message})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetImageHash(action string) string {
|
||||
split := strings.Split(action, "::")
|
||||
if len(split) > 5 {
|
||||
return split[4]
|
||||
}
|
||||
return split[len(split)-1]
|
||||
}
|
||||
137
api/service/oss/aliyun_oss.go
Normal file
137
api/service/oss/aliyun_oss.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package oss
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AliYunOss struct {
|
||||
config *types.AliYunOssConfig
|
||||
bucket *oss.Bucket
|
||||
proxyURL string
|
||||
}
|
||||
|
||||
func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
|
||||
config := &appConfig.OSS.AliYun
|
||||
// 创建 OSS 客户端
|
||||
client, err := oss.New(config.Endpoint, config.AccessKey, config.AccessSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取存储空间
|
||||
bucket, err := client.Bucket(config.Bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.SubDir == "" {
|
||||
config.SubDir = "gpt"
|
||||
}
|
||||
|
||||
return &AliYunOss{
|
||||
config: config,
|
||||
bucket: bucket,
|
||||
proxyURL: appConfig.ProxyURL,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
// 解析表单
|
||||
file, err := ctx.FormFile(name)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
// 打开上传文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件
|
||||
err = s.bucket.PutObject(objectKey, src)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
|
||||
return File{
|
||||
Name: file.Filename,
|
||||
ObjKey: objectKey,
|
||||
URL: fmt.Sprintf("%s/%s", s.config.Domain, objectKey),
|
||||
Ext: fileExt,
|
||||
Size: file.Size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||
} else {
|
||||
fileData, err = utils.DownloadImage(fileURL, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := utils.GetImgExt(parse.Path)
|
||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件字节数据
|
||||
err = s.bucket.PutObject(objectKey, bytes.NewReader(fileData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", s.config.Domain, objectKey), nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) PutBase64(base64Img string) (string, error) {
|
||||
imageData, err := base64.StdEncoding.DecodeString(base64Img)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64:%v", err)
|
||||
}
|
||||
objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
|
||||
// 上传文件字节数据
|
||||
err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", s.config.Domain, objectKey), nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) Delete(fileURL string) error {
|
||||
var objectKey string
|
||||
if strings.HasPrefix(fileURL, "http") {
|
||||
filename := filepath.Base(fileURL)
|
||||
objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
|
||||
} else {
|
||||
objectKey = fileURL
|
||||
}
|
||||
return s.bucket.DeleteObject(objectKey)
|
||||
}
|
||||
|
||||
var _ Uploader = AliYunOss{}
|
||||
105
api/service/oss/localstorage.go
Normal file
105
api/service/oss/localstorage.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package oss
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LocalStorage struct {
|
||||
config *types.LocalStorageConfig
|
||||
proxyURL string
|
||||
}
|
||||
|
||||
func NewLocalStorage(config *types.AppConfig) LocalStorage {
|
||||
return LocalStorage{
|
||||
config: &config.OSS.Local,
|
||||
proxyURL: config.ProxyURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
file, err := ctx.FormFile(name)
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error with get form: %v", err)
|
||||
}
|
||||
|
||||
path, err := utils.GenUploadPath(s.config.BasePath, file.Filename, false)
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error with generate filename: %s", err.Error())
|
||||
}
|
||||
// 将文件保存到指定路径
|
||||
err = ctx.SaveUploadedFile(file, path)
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error with save upload file: %s", err.Error())
|
||||
}
|
||||
|
||||
ext := filepath.Ext(file.Filename)
|
||||
return File{
|
||||
Name: file.Filename,
|
||||
ObjKey: path,
|
||||
URL: utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, path),
|
||||
Ext: ext,
|
||||
Size: file.Size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
filename := filepath.Base(parse.Path)
|
||||
filePath, err := utils.GenUploadPath(s.config.BasePath, filename, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with generate image dir: %v", err)
|
||||
}
|
||||
|
||||
if useProxy {
|
||||
err = utils.DownloadFile(fileURL, filePath, s.proxyURL)
|
||||
} else {
|
||||
err = utils.DownloadFile(fileURL, filePath, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
|
||||
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
|
||||
}
|
||||
|
||||
func (s LocalStorage) PutBase64(base64Img string) (string, error) {
|
||||
imageData, err := base64.StdEncoding.DecodeString(base64Img)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64:%v", err)
|
||||
}
|
||||
filePath, err := utils.GenUploadPath(s.config.BasePath, "", true)
|
||||
err = os.WriteFile(filePath, imageData, 0644)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error writing to file:%v", err)
|
||||
}
|
||||
|
||||
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
|
||||
}
|
||||
|
||||
func (s LocalStorage) Delete(fileURL string) error {
|
||||
if _, err := os.Stat(fileURL); err == nil {
|
||||
return os.Remove(fileURL)
|
||||
}
|
||||
filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
var _ Uploader = LocalStorage{}
|
||||
137
api/service/oss/minio_oss.go
Normal file
137
api/service/oss/minio_oss.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package oss
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
type MiniOss struct {
|
||||
config *types.MiniOssConfig
|
||||
client *minio.Client
|
||||
proxyURL string
|
||||
}
|
||||
|
||||
func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
|
||||
config := &appConfig.OSS.Minio
|
||||
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
|
||||
Secure: config.UseSSL,
|
||||
})
|
||||
if err != nil {
|
||||
return MiniOss{}, err
|
||||
}
|
||||
if config.SubDir == "" {
|
||||
config.SubDir = "gpt"
|
||||
}
|
||||
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
||||
}
|
||||
|
||||
func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||
} else {
|
||||
fileData, err = utils.DownloadImage(fileURL, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := filepath.Ext(parse.Path)
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
info, err := s.client.PutObject(
|
||||
context.Background(),
|
||||
s.config.Bucket,
|
||||
filename,
|
||||
strings.NewReader(string(fileData)),
|
||||
int64(len(fileData)),
|
||||
minio.PutObjectOptions{ContentType: "image/png"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
|
||||
}
|
||||
|
||||
func (s MiniOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
file, err := ctx.FormFile(name)
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error with get form: %v", err)
|
||||
}
|
||||
// Open the uploaded file
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error opening file: %v", err)
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
fileExt := utils.GetImgExt(file.Filename)
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
|
||||
ContentType: file.Header.Get("Content-Type"),
|
||||
})
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error uploading to MinIO: %v", err)
|
||||
}
|
||||
|
||||
return File{
|
||||
Name: file.Filename,
|
||||
ObjKey: info.Key,
|
||||
URL: fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key),
|
||||
Ext: fileExt,
|
||||
Size: file.Size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s MiniOss) PutBase64(base64Img string) (string, error) {
|
||||
imageData, err := base64.StdEncoding.DecodeString(base64Img)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64:%v", err)
|
||||
}
|
||||
objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
|
||||
info, err := s.client.PutObject(
|
||||
context.Background(),
|
||||
s.config.Bucket,
|
||||
objectKey,
|
||||
strings.NewReader(string(imageData)),
|
||||
int64(len(imageData)),
|
||||
minio.PutObjectOptions{ContentType: "image/png"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
|
||||
}
|
||||
|
||||
func (s MiniOss) Delete(fileURL string) error {
|
||||
var objectKey string
|
||||
if strings.HasPrefix(fileURL, "http") {
|
||||
filename := filepath.Base(fileURL)
|
||||
objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
|
||||
} else {
|
||||
objectKey = fileURL
|
||||
}
|
||||
return s.client.RemoveObject(context.Background(), s.config.Bucket, objectKey, minio.RemoveObjectOptions{})
|
||||
}
|
||||
|
||||
var _ Uploader = MiniOss{}
|
||||
151
api/service/oss/qiniu_oss.go
Normal file
151
api/service/oss/qiniu_oss.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package oss
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
)
|
||||
|
||||
type QinNiuOss struct {
|
||||
config *types.QiNiuOssConfig
|
||||
mac *qbox.Mac
|
||||
putPolicy storage.PutPolicy
|
||||
uploader *storage.FormUploader
|
||||
manager *storage.BucketManager
|
||||
proxyURL string
|
||||
}
|
||||
|
||||
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||
config := &appConfig.OSS.QiNiu
|
||||
// build storage uploader
|
||||
zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
|
||||
if !ok {
|
||||
zone = storage.ZoneHuanan
|
||||
}
|
||||
storeConfig := storage.Config{Zone: &zone}
|
||||
formUploader := storage.NewFormUploader(&storeConfig)
|
||||
// generate token
|
||||
mac := qbox.NewMac(config.AccessKey, config.AccessSecret)
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: config.Bucket,
|
||||
}
|
||||
if config.SubDir == "" {
|
||||
config.SubDir = "gpt"
|
||||
}
|
||||
return QinNiuOss{
|
||||
config: config,
|
||||
mac: mac,
|
||||
putPolicy: putPolicy,
|
||||
uploader: formUploader,
|
||||
manager: storage.NewBucketManager(mac, &storeConfig),
|
||||
proxyURL: appConfig.ProxyURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
// 解析表单
|
||||
file, err := ctx.FormFile(name)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
// 打开上传文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
err = s.uploader.Put(ctx, &ret, s.putPolicy.UploadToken(s.mac), key, src, file.Size, &extra)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
|
||||
return File{
|
||||
Name: file.Filename,
|
||||
ObjKey: key,
|
||||
URL: fmt.Sprintf("%s/%s", s.config.Domain, ret.Key),
|
||||
Ext: fileExt,
|
||||
Size: file.Size,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||
} else {
|
||||
fileData, err = utils.DownloadImage(fileURL, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := utils.GetImgExt(parse.Path)
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(fileData), int64(len(fileData)), &extra)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
|
||||
}
|
||||
|
||||
func (s QinNiuOss) PutBase64(base64Img string) (string, error) {
|
||||
imageData, err := base64.StdEncoding.DecodeString(base64Img)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64:%v", err)
|
||||
}
|
||||
objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), objectKey, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
|
||||
}
|
||||
|
||||
func (s QinNiuOss) Delete(fileURL string) error {
|
||||
var objectKey string
|
||||
if strings.HasPrefix(fileURL, "http") {
|
||||
filename := filepath.Base(fileURL)
|
||||
objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
|
||||
} else {
|
||||
objectKey = fileURL
|
||||
}
|
||||
|
||||
return s.manager.Delete(s.config.Bucket, objectKey)
|
||||
}
|
||||
|
||||
var _ Uploader = QinNiuOss{}
|
||||
29
api/service/oss/uploader.go
Normal file
29
api/service/oss/uploader.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package oss
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
const Local = "LOCAL"
|
||||
const Minio = "MINIO"
|
||||
const QiNiu = "QINIU"
|
||||
const AliYun = "ALIYUN"
|
||||
|
||||
type File struct {
|
||||
Name string `json:"name"`
|
||||
ObjKey string `json:"obj_key"`
|
||||
Size int64 `json:"size"`
|
||||
URL string `json:"url"`
|
||||
Ext string `json:"ext"`
|
||||
}
|
||||
type Uploader interface {
|
||||
PutFile(ctx *gin.Context, name string) (File, error)
|
||||
PutUrlFile(url string, useProxy bool) (string, error)
|
||||
PutBase64(imageData string) (string, error)
|
||||
Delete(fileURL string) error
|
||||
}
|
||||
53
api/service/oss/uploader_manager.go
Normal file
53
api/service/oss/uploader_manager.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package oss
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UploaderManager struct {
|
||||
handler Uploader
|
||||
}
|
||||
|
||||
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
|
||||
active := Local
|
||||
if config.OSS.Active != "" {
|
||||
active = strings.ToUpper(config.OSS.Active)
|
||||
}
|
||||
var handler Uploader
|
||||
switch active {
|
||||
case Local:
|
||||
handler = NewLocalStorage(config)
|
||||
break
|
||||
case Minio:
|
||||
client, err := NewMiniOss(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handler = client
|
||||
break
|
||||
case QiNiu:
|
||||
handler = NewQiNiuOss(config)
|
||||
break
|
||||
case AliYun:
|
||||
client, err := NewAliYunOss(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handler = client
|
||||
break
|
||||
}
|
||||
|
||||
return &UploaderManager{handler: handler}, nil
|
||||
}
|
||||
|
||||
func (m *UploaderManager) GetUploadHandler() Uploader {
|
||||
return m.handler
|
||||
}
|
||||
134
api/service/payment/alipay_service.go
Normal file
134
api/service/payment/alipay_service.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package payment
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"github.com/go-pay/gopay"
|
||||
"github.com/go-pay/gopay/alipay"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AlipayService struct {
|
||||
config *types.AlipayConfig
|
||||
client *alipay.Client
|
||||
}
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
|
||||
config := appConfig.AlipayConfig
|
||||
if !config.Enabled {
|
||||
logger.Info("Disabled Alipay service")
|
||||
return nil, nil
|
||||
}
|
||||
priKey, err := readKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with read App Private key: %v", err)
|
||||
}
|
||||
|
||||
client, err := alipay.NewClient(config.AppId, priKey, !config.SandBox)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with initialize alipay service: %v", err)
|
||||
}
|
||||
|
||||
//client.DebugSwitch = gopay.DebugOn // 开启调试模式
|
||||
client.SetLocation(alipay.LocationShanghai). // 设置时区,不设置或出错均为默认服务器时间
|
||||
SetCharset(alipay.UTF8). // 设置字符编码,不设置默认 utf-8
|
||||
SetSignType(alipay.RSA2). // 设置签名类型,不设置默认 RSA2
|
||||
SetReturnUrl(config.ReturnURL). // 设置返回URL
|
||||
SetNotifyUrl(config.NotifyURL)
|
||||
|
||||
if err = client.SetCertSnByPath(config.PublicKey, config.RootCert, config.AlipayPublicKey); err != nil {
|
||||
return nil, fmt.Errorf("error with load payment public key: %v", err)
|
||||
}
|
||||
|
||||
return &AlipayService{config: &config, client: client}, nil
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlMobile(outTradeNo string, amount string, subject string) (string, error) {
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("subject", subject)
|
||||
bm.Set("out_trade_no", outTradeNo)
|
||||
bm.Set("quit_url", s.config.ReturnURL)
|
||||
bm.Set("total_amount", amount)
|
||||
bm.Set("product_code", "QUICK_WAP_WAY")
|
||||
return s.client.TradeWapPay(context.Background(), bm)
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlPc(outTradeNo string, amount string, subject string) (string, error) {
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("subject", subject)
|
||||
bm.Set("out_trade_no", outTradeNo)
|
||||
bm.Set("total_amount", amount)
|
||||
bm.Set("product_code", "FAST_INSTANT_TRADE_PAY")
|
||||
return s.client.TradePagePay(context.Background(), bm)
|
||||
}
|
||||
|
||||
// TradeVerify 交易验证
|
||||
func (s *AlipayService) TradeVerify(request *http.Request) NotifyVo {
|
||||
notifyReq, err := alipay.ParseNotifyToBodyMap(request) // c.Request 是 gin 框架的写法
|
||||
if err != nil {
|
||||
return NotifyVo{
|
||||
Status: Failure,
|
||||
Message: "error with parse notify request: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
_, err = alipay.VerifySignWithCert(s.config.AlipayPublicKey, notifyReq)
|
||||
if err != nil {
|
||||
return NotifyVo{
|
||||
Status: Failure,
|
||||
Message: "error with verify sign: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return s.TradeQuery(request.Form.Get("out_trade_no"))
|
||||
}
|
||||
|
||||
func (s *AlipayService) TradeQuery(outTradeNo string) NotifyVo {
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("out_trade_no", outTradeNo)
|
||||
|
||||
//查询订单
|
||||
rsp, err := s.client.TradeQuery(context.Background(), bm)
|
||||
if err != nil {
|
||||
return NotifyVo{
|
||||
Status: Failure,
|
||||
Message: "异步查询验证订单信息发生错误" + outTradeNo + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if rsp.Response.TradeStatus == "TRADE_SUCCESS" {
|
||||
return NotifyVo{
|
||||
Status: Success,
|
||||
OutTradeNo: rsp.Response.OutTradeNo,
|
||||
TradeId: rsp.Response.TradeNo,
|
||||
Amount: rsp.Response.TotalAmount,
|
||||
Subject: rsp.Response.Subject,
|
||||
Message: "OK",
|
||||
}
|
||||
} else {
|
||||
return NotifyVo{
|
||||
Status: Failure,
|
||||
Message: "异步查询验证订单信息发生错误" + outTradeNo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readKey(filename string) (string, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
171
api/service/payment/hupipay_serive.go
Normal file
171
api/service/payment/hupipay_serive.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package payment
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HuPiPayService struct {
|
||||
appId string
|
||||
appSecret string
|
||||
apiURL string
|
||||
}
|
||||
|
||||
func NewHuPiPay(config *types.AppConfig) *HuPiPayService {
|
||||
return &HuPiPayService{
|
||||
appId: config.HuPiPayConfig.AppId,
|
||||
appSecret: config.HuPiPayConfig.AppSecret,
|
||||
apiURL: config.HuPiPayConfig.ApiURL,
|
||||
}
|
||||
}
|
||||
|
||||
type HuPiPayReq struct {
|
||||
AppId string `json:"appid"`
|
||||
Version string `json:"version"`
|
||||
TradeOrderId string `json:"trade_order_id"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
Title string `json:"title"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
WapName string `json:"wap_name"`
|
||||
CallbackURL string `json:"callback_url"`
|
||||
Time string `json:"time"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Type string `json:"type"`
|
||||
WapUrl string `json:"wap_url"`
|
||||
}
|
||||
|
||||
type HuPiResp struct {
|
||||
Openid interface{} `json:"openid"`
|
||||
UrlQrcode string `json:"url_qrcode"`
|
||||
URL string `json:"url"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg,omitempty"`
|
||||
}
|
||||
|
||||
// Pay 执行支付请求操作
|
||||
func (s *HuPiPayService) Pay(params HuPiPayReq) (HuPiResp, error) {
|
||||
data := url.Values{}
|
||||
simple := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
params.AppId = s.appId
|
||||
params.Time = simple
|
||||
params.NonceStr = simple
|
||||
encode := utils.JsonEncode(params)
|
||||
m := make(map[string]string)
|
||||
_ = utils.JsonDecode(encode, &m)
|
||||
for k, v := range m {
|
||||
data.Add(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
// 生成签名
|
||||
data.Add("hash", s.Sign(data))
|
||||
// 发送支付请求
|
||||
apiURL := fmt.Sprintf("%s/payment/do.html", s.apiURL)
|
||||
resp, err := http.PostForm(apiURL, data)
|
||||
if err != nil {
|
||||
return HuPiResp{}, fmt.Errorf("error with requst api: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
all, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return HuPiResp{}, fmt.Errorf("error with reading response: %v", err)
|
||||
}
|
||||
|
||||
var res HuPiResp
|
||||
err = utils.JsonDecode(string(all), &res)
|
||||
if err != nil {
|
||||
return HuPiResp{}, fmt.Errorf("error with decode payment result: %v", err)
|
||||
}
|
||||
|
||||
if res.ErrCode != 0 {
|
||||
return HuPiResp{}, fmt.Errorf("error with generate pay url: %s", res.ErrMsg)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Sign 签名方法
|
||||
func (s *HuPiPayService) Sign(params url.Values) string {
|
||||
params.Del(`Sign`)
|
||||
var keys = make([]string, 0, 0)
|
||||
for key := range params {
|
||||
if params.Get(key) != `` {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var pList = make([]string, 0, 0)
|
||||
for _, key := range keys {
|
||||
var value = strings.TrimSpace(params.Get(key))
|
||||
if len(value) > 0 {
|
||||
pList = append(pList, key+"="+value)
|
||||
}
|
||||
}
|
||||
var src = strings.Join(pList, "&")
|
||||
src += s.appSecret
|
||||
|
||||
md5bs := md5.Sum([]byte(src))
|
||||
return hex.EncodeToString(md5bs[:])
|
||||
}
|
||||
|
||||
// Check 校验订单状态
|
||||
func (s *HuPiPayService) Check(tradeNo string) error {
|
||||
data := url.Values{}
|
||||
data.Add("appid", s.appId)
|
||||
data.Add("open_order_id", tradeNo)
|
||||
stamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
data.Add("time", stamp)
|
||||
data.Add("nonce_str", stamp)
|
||||
data.Add("hash", s.Sign(data))
|
||||
|
||||
apiURL := fmt.Sprintf("%s/payment/query.html", s.apiURL)
|
||||
resp, err := http.PostForm(apiURL, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with http reqeust: %v", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with reading response: %v", err)
|
||||
}
|
||||
|
||||
var r struct {
|
||||
ErrCode int `json:"errcode"`
|
||||
Data struct {
|
||||
Status string `json:"status"`
|
||||
OpenOrderId string `json:"open_order_id"`
|
||||
} `json:"data,omitempty"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
err = utils.JsonDecode(string(body), &r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with decode response: %v", err)
|
||||
}
|
||||
|
||||
if r.ErrCode == 0 && r.Data.Status == "OD" {
|
||||
return nil
|
||||
} else {
|
||||
logger.Debugf("%+v", r)
|
||||
return errors.New("order not paid:" + r.ErrMsg)
|
||||
}
|
||||
}
|
||||
153
api/service/payment/payjs_service.go
Normal file
153
api/service/payment/payjs_service.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package payment
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type JPayService struct {
|
||||
config *types.JPayConfig
|
||||
}
|
||||
|
||||
func NewJPayService(appConfig *types.AppConfig) *JPayService {
|
||||
return &JPayService{
|
||||
config: &appConfig.JPayConfig,
|
||||
}
|
||||
}
|
||||
|
||||
type JPayReq struct {
|
||||
TotalFee int `json:"total_fee"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
Subject string `json:"body"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
ReturnURL string `json:"callback_url"`
|
||||
}
|
||||
type JPayReps struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
OrderId string `json:"payjs_order_id"`
|
||||
ReturnCode int `json:"return_code"`
|
||||
ReturnMsg string `json:"return_msg"`
|
||||
Sign string `json:"Sign"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
CodeUrl string `json:"code_url,omitempty"`
|
||||
Qrcode string `json:"qrcode,omitempty"`
|
||||
}
|
||||
|
||||
func (r JPayReps) IsOK() bool {
|
||||
return r.ReturnMsg == "SUCCESS"
|
||||
}
|
||||
|
||||
func (js *JPayService) Pay(param JPayReq) JPayReps {
|
||||
param.NotifyURL = js.config.NotifyURL
|
||||
var p = url.Values{}
|
||||
encode := utils.JsonEncode(param)
|
||||
m := make(map[string]interface{})
|
||||
_ = utils.JsonDecode(encode, &m)
|
||||
for k, v := range m {
|
||||
p.Add(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
p.Add("mchid", js.config.AppId)
|
||||
|
||||
p.Add("sign", js.sign(p))
|
||||
|
||||
cli := http.Client{}
|
||||
apiURL := fmt.Sprintf("%s/api/native", js.config.ApiURL)
|
||||
r, err := cli.PostForm(apiURL, p)
|
||||
if err != nil {
|
||||
return JPayReps{ReturnMsg: err.Error()}
|
||||
}
|
||||
defer r.Body.Close()
|
||||
bs, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return JPayReps{ReturnMsg: err.Error()}
|
||||
}
|
||||
|
||||
var data JPayReps
|
||||
err = utils.JsonDecode(string(bs), &data)
|
||||
if err != nil {
|
||||
return JPayReps{ReturnMsg: err.Error()}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (js *JPayService) PayH5(p url.Values) string {
|
||||
p.Add("mchid", js.config.AppId)
|
||||
p.Add("sign", js.sign(p))
|
||||
return fmt.Sprintf("%s/api/cashier?%s", js.config.ApiURL, p.Encode())
|
||||
}
|
||||
|
||||
func (js *JPayService) sign(params url.Values) string {
|
||||
params.Del(`sign`)
|
||||
var keys = make([]string, 0, 0)
|
||||
for key := range params {
|
||||
if params.Get(key) != `` {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var pList = make([]string, 0, 0)
|
||||
for _, key := range keys {
|
||||
var value = strings.TrimSpace(params.Get(key))
|
||||
if len(value) > 0 {
|
||||
pList = append(pList, key+"="+value)
|
||||
}
|
||||
}
|
||||
var src = strings.Join(pList, "&")
|
||||
src += "&key=" + js.config.PrivateKey
|
||||
|
||||
md5bs := md5.Sum([]byte(src))
|
||||
md5res := hex.EncodeToString(md5bs[:])
|
||||
return strings.ToUpper(md5res)
|
||||
}
|
||||
|
||||
// TradeVerify 查询订单支付状态
|
||||
// @param tradeNo 支付平台交易 ID
|
||||
func (js *JPayService) TradeVerify(tradeNo string) error {
|
||||
apiURL := fmt.Sprintf("%s/api/check", js.config.ApiURL)
|
||||
params := url.Values{}
|
||||
params.Add("payjs_order_id", tradeNo)
|
||||
params.Add("sign", js.sign(params))
|
||||
data := strings.NewReader(params.Encode())
|
||||
resp, err := http.Post(apiURL, "application/x-www-form-urlencoded", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with http reqeust: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with reading response: %v", err)
|
||||
}
|
||||
|
||||
var r struct {
|
||||
ReturnCode int `json:"return_code"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
err = utils.JsonDecode(string(body), &r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with decode response: %v", err)
|
||||
}
|
||||
|
||||
if r.ReturnCode == 1 && r.Status == 1 {
|
||||
return nil
|
||||
} else {
|
||||
logger.Errorf("PayJs 支付验证响应:%s", string(body))
|
||||
return errors.New("order not paid")
|
||||
}
|
||||
}
|
||||
19
api/service/payment/types.go
Normal file
19
api/service/payment/types.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package payment
|
||||
|
||||
type NotifyVo struct {
|
||||
Status int
|
||||
OutTradeNo string // 商户订单号
|
||||
TradeId string // 交易ID
|
||||
Amount string // 交易金额
|
||||
Message string
|
||||
Subject string
|
||||
}
|
||||
|
||||
func (v NotifyVo) Success() bool {
|
||||
return v.Status == Success
|
||||
}
|
||||
|
||||
const (
|
||||
Success = 0
|
||||
Failure = 1
|
||||
)
|
||||
135
api/service/payment/wepay_service.go
Normal file
135
api/service/payment/wepay_service.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package payment
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"github.com/go-pay/gopay"
|
||||
"github.com/go-pay/gopay/wechat/v3"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WechatPayService struct {
|
||||
config *types.WechatPayConfig
|
||||
client *wechat.ClientV3
|
||||
}
|
||||
|
||||
func NewWechatService(appConfig *types.AppConfig) (*WechatPayService, error) {
|
||||
config := appConfig.WechatPayConfig
|
||||
if !config.Enabled {
|
||||
logger.Info("Disabled WechatPay service")
|
||||
return nil, nil
|
||||
}
|
||||
priKey, err := readKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with read App Private key: %v", err)
|
||||
}
|
||||
|
||||
client, err := wechat.NewClientV3(config.MchId, config.SerialNo, config.ApiV3Key, priKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with initialize WechatPay service: %v", err)
|
||||
}
|
||||
err = client.AutoVerifySign()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with autoVerifySign: %v", err)
|
||||
}
|
||||
//client.DebugSwitch = gopay.DebugOn
|
||||
|
||||
return &WechatPayService{config: &config, client: client}, nil
|
||||
}
|
||||
|
||||
func (s *WechatPayService) PayUrlNative(outTradeNo string, amount int, subject string) (string, error) {
|
||||
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
|
||||
// 初始化 BodyMap
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("appid", s.config.AppId).
|
||||
Set("mchid", s.config.MchId).
|
||||
Set("description", subject).
|
||||
Set("out_trade_no", outTradeNo).
|
||||
Set("time_expire", expire).
|
||||
Set("notify_url", s.config.NotifyURL).
|
||||
SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||
bm.Set("total", amount).
|
||||
Set("currency", "CNY")
|
||||
})
|
||||
|
||||
wxRsp, err := s.client.V3TransactionNative(context.Background(), bm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with client v3 transaction Native: %v", err)
|
||||
}
|
||||
if wxRsp.Code != wechat.Success {
|
||||
return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error)
|
||||
}
|
||||
return wxRsp.Response.CodeUrl, nil
|
||||
}
|
||||
|
||||
func (s *WechatPayService) PayUrlH5(outTradeNo string, amount int, subject string, ip string) (string, error) {
|
||||
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
|
||||
// 初始化 BodyMap
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("appid", s.config.AppId).
|
||||
Set("mchid", s.config.MchId).
|
||||
Set("description", subject).
|
||||
Set("out_trade_no", outTradeNo).
|
||||
Set("time_expire", expire).
|
||||
Set("notify_url", s.config.NotifyURL).
|
||||
SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||
bm.Set("total", amount).
|
||||
Set("currency", "CNY")
|
||||
}).
|
||||
SetBodyMap("scene_info", func(bm gopay.BodyMap) {
|
||||
bm.Set("payer_client_ip", ip).
|
||||
SetBodyMap("h5_info", func(bm gopay.BodyMap) {
|
||||
bm.Set("type", "Wap")
|
||||
})
|
||||
})
|
||||
|
||||
wxRsp, err := s.client.V3TransactionH5(context.Background(), bm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with client v3 transaction H5: %v", err)
|
||||
}
|
||||
if wxRsp.Code != wechat.Success {
|
||||
return "", fmt.Errorf("error with generating pay url: %v", wxRsp.Error)
|
||||
}
|
||||
return wxRsp.Response.H5Url, nil
|
||||
}
|
||||
|
||||
type NotifyResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `xml:"message"`
|
||||
}
|
||||
|
||||
// TradeVerify 交易验证
|
||||
func (s *WechatPayService) TradeVerify(request *http.Request) NotifyVo {
|
||||
notifyReq, err := wechat.V3ParseNotify(request)
|
||||
if err != nil {
|
||||
return NotifyVo{Status: 1, Message: fmt.Sprintf("error with client v3 parse notify: %v", err)}
|
||||
}
|
||||
|
||||
// TODO: 这里验签程序有 Bug,一直报错:crypto/rsa: verification error,先暂时取消验签
|
||||
//err = notifyReq.VerifySignByPK(s.client.WxPublicKey())
|
||||
//if err != nil {
|
||||
// return fmt.Errorf("error with client v3 verify sign: %v", err)
|
||||
//}
|
||||
|
||||
// 解密支付密文,验证订单信息
|
||||
result, err := notifyReq.DecryptPayCipherText(s.config.ApiV3Key)
|
||||
if err != nil {
|
||||
return NotifyVo{Status: Failure, Message: fmt.Sprintf("error with client v3 decrypt: %v", err)}
|
||||
}
|
||||
|
||||
return NotifyVo{
|
||||
Status: Success,
|
||||
OutTradeNo: result.OutTradeNo,
|
||||
TradeId: result.TransactionId,
|
||||
Amount: fmt.Sprintf("%.2f", float64(result.Amount.Total)/100),
|
||||
}
|
||||
}
|
||||
143
api/service/sd/pool.go
Normal file
143
api/service/sd/pool.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package sd
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ServicePool struct {
|
||||
services []*Service
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
|
||||
uploader *oss.UploaderManager
|
||||
levelDB *store.LevelDB
|
||||
}
|
||||
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, levelDB *store.LevelDB) *ServicePool {
|
||||
services := make([]*Service, 0)
|
||||
taskQueue := store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli)
|
||||
notifyQueue := store.NewRedisQueue("StableDiffusion_Queue", redisCli)
|
||||
|
||||
return &ServicePool{
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
services: services,
|
||||
db: db,
|
||||
Clients: types.NewLMap[uint, *types.WsClient](),
|
||||
uploader: manager,
|
||||
levelDB: levelDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServicePool) InitServices(configs []types.StableDiffusionConfig) {
|
||||
// stop old service
|
||||
for _, s := range p.services {
|
||||
s.Stop()
|
||||
}
|
||||
p.services = make([]*Service, 0)
|
||||
|
||||
for k, config := range configs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
|
||||
// create sd service
|
||||
name := fmt.Sprintf(" sd-service-%d", k)
|
||||
service := NewService(name, config, p.taskQueue, p.notifyQueue, p.db, p.uploader, p.levelDB)
|
||||
// run sd service
|
||||
go func() {
|
||||
service.Run()
|
||||
}()
|
||||
|
||||
p.services = append(p.services, service)
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (p *ServicePool) PushTask(task types.SdTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
p.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (p *ServicePool) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task notify checking ...")
|
||||
for {
|
||||
var message NotifyMessage
|
||||
err := p.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
client := p.Clients.Get(uint(message.UserId))
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
err = client.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
func (p *ServicePool) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task status checking ...")
|
||||
for {
|
||||
var jobs []model.SdJob
|
||||
res := p.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 5 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
|
||||
p.db.Delete(&job)
|
||||
var user model.User
|
||||
p.db.Where("id = ?", job.UserId).First(&user)
|
||||
// 退回绘图次数
|
||||
res = p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
|
||||
if res.Error == nil && res.RowsAffected > 0 {
|
||||
p.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power + job.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "stable-diffusion",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HasAvailableService check if it has available mj service in pool
|
||||
func (p *ServicePool) HasAvailableService() bool {
|
||||
return len(p.services) > 0
|
||||
}
|
||||
247
api/service/sd/service.go
Normal file
247
api/service/sd/service.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package sd
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SD 绘画服务
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
config types.StableDiffusionConfig
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
name string // service name
|
||||
leveldb *store.LevelDB
|
||||
running bool // 运行状态
|
||||
}
|
||||
|
||||
func NewService(name string, config types.StableDiffusionConfig, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB) *Service {
|
||||
config.ApiURL = strings.TrimRight(config.ApiURL, "/")
|
||||
return &Service{
|
||||
name: name,
|
||||
config: config,
|
||||
httpClient: req.C(),
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
db: db,
|
||||
leveldb: levelDB,
|
||||
uploadManager: manager,
|
||||
running: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
logger.Infof("Starting Stable-Diffusion job consumer for %s", s.name)
|
||||
for s.running {
|
||||
var task types.SdTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Params.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt), "gpt-4o-mini")
|
||||
if err == nil {
|
||||
task.Params.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// translate negative prompt
|
||||
if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), "gpt-4o-mini")
|
||||
if err == nil {
|
||||
task.Params.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("%s handle a new Stable-Diffusion task: %+v", s.name, task)
|
||||
err = s.Txt2Img(task)
|
||||
if err != nil {
|
||||
logger.Error("绘画任务执行失败:", err.Error())
|
||||
// update the task progress
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{
|
||||
"progress": -1,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
// 通知前端,任务失败
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Failed})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
s.running = false
|
||||
}
|
||||
|
||||
// Txt2ImgReq 文生图请求实体
|
||||
type Txt2ImgReq struct {
|
||||
Prompt string `json:"prompt"`
|
||||
NegativePrompt string `json:"negative_prompt"`
|
||||
Seed int64 `json:"seed,omitempty"`
|
||||
Steps int `json:"steps"`
|
||||
CfgScale float32 `json:"cfg_scale"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
SamplerName string `json:"sampler_name"`
|
||||
Scheduler string `json:"scheduler"`
|
||||
EnableHr bool `json:"enable_hr,omitempty"`
|
||||
HrScale int `json:"hr_scale,omitempty"`
|
||||
HrUpscaler string `json:"hr_upscaler,omitempty"`
|
||||
HrSecondPassSteps int `json:"hr_second_pass_steps,omitempty"`
|
||||
DenoisingStrength float32 `json:"denoising_strength,omitempty"`
|
||||
ForceTaskId string `json:"force_task_id,omitempty"`
|
||||
}
|
||||
|
||||
// Txt2ImgResp 文生图响应实体
|
||||
type Txt2ImgResp struct {
|
||||
Images []string `json:"images"`
|
||||
Parameters struct {
|
||||
} `json:"parameters"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
// TaskProgressResp 任务进度响应实体
|
||||
type TaskProgressResp struct {
|
||||
Progress float64 `json:"progress"`
|
||||
EtaRelative float64 `json:"eta_relative"`
|
||||
CurrentImage string `json:"current_image"`
|
||||
}
|
||||
|
||||
// Txt2Img 文生图 API
|
||||
func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
body := Txt2ImgReq{
|
||||
Prompt: task.Params.Prompt,
|
||||
NegativePrompt: task.Params.NegPrompt,
|
||||
Steps: task.Params.Steps,
|
||||
CfgScale: task.Params.CfgScale,
|
||||
Width: task.Params.Width,
|
||||
Height: task.Params.Height,
|
||||
SamplerName: task.Params.Sampler,
|
||||
Scheduler: task.Params.Scheduler,
|
||||
ForceTaskId: task.Params.TaskId,
|
||||
}
|
||||
if task.Params.Seed > 0 {
|
||||
body.Seed = task.Params.Seed
|
||||
}
|
||||
if task.Params.HdFix {
|
||||
body.EnableHr = true
|
||||
body.HrScale = task.Params.HdScale
|
||||
body.HrUpscaler = task.Params.HdScaleAlg
|
||||
body.HrSecondPassSteps = task.Params.HdSteps
|
||||
body.DenoisingStrength = task.Params.HdRedrawRate
|
||||
}
|
||||
var res Txt2ImgResp
|
||||
var errChan = make(chan error)
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", s.config.ApiURL)
|
||||
logger.Debugf("send image request to %s", apiURL)
|
||||
// send a request to sd api endpoint
|
||||
go func() {
|
||||
response, err := s.httpClient.R().
|
||||
SetHeader("Authorization", s.config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
errChan <- fmt.Errorf("error http code status: %v", response.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存 Base64 图片
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutBase64(res.Images[0])
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("error with upload image: %v", err)
|
||||
return
|
||||
}
|
||||
// 获取绘画真实的 seed
|
||||
var info map[string]interface{}
|
||||
err = utils.JsonDecode(res.Info, &info)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("error with decode task response: %v", err)
|
||||
return
|
||||
}
|
||||
task.Params.Seed = int64(utils.IntValue(utils.InterfaceToString(info["seed"]), -1))
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(model.SdJob{ImgURL: imgURL, Params: utils.JsonEncode(task.Params), Prompt: task.Params.Prompt})
|
||||
errChan <- nil
|
||||
}()
|
||||
|
||||
// waiting for task finish
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// task finished
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Finished})
|
||||
// 从 leveldb 中删除预览图片数据
|
||||
_ = s.leveldb.Delete(task.Params.TaskId)
|
||||
return nil
|
||||
default:
|
||||
err, resp := s.checkTaskProgress()
|
||||
// 更新任务进度
|
||||
if err == nil && resp.Progress > 0 {
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100))
|
||||
// 发送更新状态信号
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Running})
|
||||
// 保存预览图片数据
|
||||
if resp.CurrentImage != "" {
|
||||
_ = s.leveldb.Put(task.Params.TaskId, resp.CurrentImage)
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
func (s *Service) checkTaskProgress() (error, *TaskProgressResp) {
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", s.config.ApiURL)
|
||||
var res TaskProgressResp
|
||||
response, err := s.httpClient.R().
|
||||
SetHeader("Authorization", s.config.ApiKey).
|
||||
SetSuccessResult(&res).
|
||||
Get(apiURL)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return fmt.Errorf("error http code status: %v", response.Status), nil
|
||||
}
|
||||
|
||||
return nil, &res
|
||||
}
|
||||
24
api/service/sd/types.go
Normal file
24
api/service/sd/types.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package sd
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import logger2 "geekai/logger"
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type NotifyMessage struct {
|
||||
UserId int `json:"user_id"`
|
||||
JobId int `json:"job_id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const (
|
||||
Running = "RUNNING"
|
||||
Finished = "FINISH"
|
||||
Failed = "FAIL"
|
||||
)
|
||||
60
api/service/sms/aliyun.go
Normal file
60
api/service/sms/aliyun.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package sms
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||
)
|
||||
|
||||
type AliYunSmsService struct {
|
||||
config *types.SmsConfigAli
|
||||
client *dysmsapi.Client
|
||||
}
|
||||
|
||||
func NewAliYunSmsService(appConfig *types.AppConfig) (*AliYunSmsService, error) {
|
||||
config := &appConfig.SMS.Ali
|
||||
// 创建阿里云短信客户端
|
||||
client, err := dysmsapi.NewClientWithAccessKey(
|
||||
"cn-hangzhou",
|
||||
config.AccessKey,
|
||||
config.AccessSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
return &AliYunSmsService{
|
||||
config: config,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
|
||||
// 创建短信请求并设置参数
|
||||
request := dysmsapi.CreateSendSmsRequest()
|
||||
request.Scheme = "https"
|
||||
request.Domain = s.config.Domain
|
||||
request.PhoneNumbers = mobile
|
||||
request.SignName = s.config.Sign
|
||||
request.TemplateCode = s.config.CodeTempId
|
||||
request.TemplateParam = fmt.Sprintf("{\"code\":\"%d\"}", code) // 短信模板中的参数
|
||||
|
||||
// 发送短信
|
||||
response, err := s.client.SendSms(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send SMS:%v", err)
|
||||
}
|
||||
|
||||
if response.Code != "OK" {
|
||||
return fmt.Errorf("failed to send SMS:%v", response.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ Service = &AliYunSmsService{}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user