ffreemt commited on
Commit
f25d8c5
1 Parent(s): 9a2365b

Update from redteam repo

Browse files
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts AS BUILD_IMAGE
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+
7
+ RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8
+
9
+ FROM node:lts-alpine
10
+
11
+ COPY --from=BUILD_IMAGE /app/configs /app/configs
12
+ COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13
+ COPY --from=BUILD_IMAGE /app/dist /app/dist
14
+ COPY --from=BUILD_IMAGE /app/public /app/public
15
+ COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16
+
17
+ WORKDIR /app
18
+
19
+ EXPOSE 7860
20
+
21
+ CMD ["npm", "start"]
Dockerfile-- ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts AS BUILD_IMAGE
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+
7
+ RUN yarn install --registry https://registry.npmmirror.com/ && yarn run build
8
+
9
+ FROM node:lts-alpine
10
+
11
+ COPY --from=BUILD_IMAGE /app/configs /app/configs
12
+ COPY --from=BUILD_IMAGE /app/package.json /app/package.json
13
+ COPY --from=BUILD_IMAGE /app/dist /app/dist
14
+ COPY --from=BUILD_IMAGE /app/public /app/public
15
+ COPY --from=BUILD_IMAGE /app/node_modules /app/node_modules
16
+
17
+ WORKDIR /app
18
+
19
+ EXPOSE 8000
20
+
21
+ CMD ["npm", "start"]
LICENSE ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users. We, the Free Software Foundation, use the
18
+ GNU General Public License for most of our software; it applies also to
19
+ any other work released this way by its authors. You can apply it to
20
+ your programs, too.
21
+
22
+ When we speak of free software, we are referring to freedom, not
23
+ price. Our General Public Licenses are designed to make sure that you
24
+ have the freedom to distribute copies of free software (and charge for
25
+ them if you wish), that you receive source code or can get it if you
26
+ want it, that you can change the software or use pieces of it in new
27
+ free programs, and that you know you can do these things.
28
+
29
+ To protect your rights, we need to prevent others from denying you
30
+ these rights or asking you to surrender the rights. Therefore, you have
31
+ certain responsibilities if you distribute copies of the software, or if
32
+ you modify it: responsibilities to respect the freedom of others.
33
+
34
+ For example, if you distribute copies of such a program, whether
35
+ gratis or for a fee, you must pass on to the recipients the same
36
+ freedoms that you received. You must make sure that they, too, receive
37
+ or can get the source code. And you must show them these terms so they
38
+ know their rights.
39
+
40
+ Developers that use the GNU GPL protect your rights with two steps:
41
+ (1) assert copyright on the software, and (2) offer you this License
42
+ giving you legal permission to copy, distribute and/or modify it.
43
+
44
+ For the developers' and authors' protection, the GPL clearly explains
45
+ that there is no warranty for this free software. For both users' and
46
+ authors' sake, the GPL requires that modified versions be marked as
47
+ changed, so that their problems will not be attributed erroneously to
48
+ authors of previous versions.
49
+
50
+ Some devices are designed to deny users access to install or run
51
+ modified versions of the software inside them, although the manufacturer
52
+ can do so. This is fundamentally incompatible with the aim of
53
+ protecting users' freedom to change the software. The systematic
54
+ pattern of such abuse occurs in the area of products for individuals to
55
+ use, which is precisely where it is most unacceptable. Therefore, we
56
+ have designed this version of the GPL to prohibit the practice for those
57
+ products. If such problems arise substantially in other domains, we
58
+ stand ready to extend this provision to those domains in future versions
59
+ of the GPL, as needed to protect the freedom of users.
60
+
61
+ Finally, every program is threatened constantly by software patents.
62
+ States should not allow patents to restrict development and use of
63
+ software on general-purpose computers, but in those that do, we wish to
64
+ avoid the special danger that patents applied to a free program could
65
+ make it effectively proprietary. To prevent this, the GPL assures that
66
+ patents cannot be used to render the program non-free.
67
+
68
+ The precise terms and conditions for copying, distribution and
69
+ modification follow.
70
+
71
+ TERMS AND CONDITIONS
72
+
73
+ 0. Definitions.
74
+
75
+ "This License" refers to version 3 of the GNU General Public License.
76
+
77
+ "Copyright" also means copyright-like laws that apply to other kinds of
78
+ works, such as semiconductor masks.
79
+
80
+ "The Program" refers to any copyrightable work licensed under this
81
+ License. Each licensee is addressed as "you". "Licensees" and
82
+ "recipients" may be individuals or organizations.
83
+
84
+ To "modify" a work means to copy from or adapt all or part of the work
85
+ in a fashion requiring copyright permission, other than the making of an
86
+ exact copy. The resulting work is called a "modified version" of the
87
+ earlier work or a work "based on" the earlier work.
88
+
89
+ A "covered work" means either the unmodified Program or a work based
90
+ on the Program.
91
+
92
+ To "propagate" a work means to do anything with it that, without
93
+ permission, would make you directly or secondarily liable for
94
+ infringement under applicable copyright law, except executing it on a
95
+ computer or modifying a private copy. Propagation includes copying,
96
+ distribution (with or without modification), making available to the
97
+ public, and in some countries other activities as well.
98
+
99
+ To "convey" a work means any kind of propagation that enables other
100
+ parties to make or receive copies. Mere interaction with a user through
101
+ a computer network, with no transfer of a copy, is not conveying.
102
+
103
+ An interactive user interface displays "Appropriate Legal Notices"
104
+ to the extent that it includes a convenient and prominently visible
105
+ feature that (1) displays an appropriate copyright notice, and (2)
106
+ tells the user that there is no warranty for the work (except to the
107
+ extent that warranties are provided), that licensees may convey the
108
+ work under this License, and how to view a copy of this License. If
109
+ the interface presents a list of user commands or options, such as a
110
+ menu, a prominent item in the list meets this criterion.
111
+
112
+ 1. Source Code.
113
+
114
+ The "source code" for a work means the preferred form of the work
115
+ for making modifications to it. "Object code" means any non-source
116
+ form of a work.
117
+
118
+ A "Standard Interface" means an interface that either is an official
119
+ standard defined by a recognized standards body, or, in the case of
120
+ interfaces specified for a particular programming language, one that
121
+ is widely used among developers working in that language.
122
+
123
+ The "System Libraries" of an executable work include anything, other
124
+ than the work as a whole, that (a) is included in the normal form of
125
+ packaging a Major Component, but which is not part of that Major
126
+ Component, and (b) serves only to enable use of the work with that
127
+ Major Component, or to implement a Standard Interface for which an
128
+ implementation is available to the public in source code form. A
129
+ "Major Component", in this context, means a major essential component
130
+ (kernel, window system, and so on) of the specific operating system
131
+ (if any) on which the executable work runs, or a compiler used to
132
+ produce the work, or an object code interpreter used to run it.
133
+
134
+ The "Corresponding Source" for a work in object code form means all
135
+ the source code needed to generate, install, and (for an executable
136
+ work) run the object code and to modify the work, including scripts to
137
+ control those activities. However, it does not include the work's
138
+ System Libraries, or general-purpose tools or generally available free
139
+ programs which are used unmodified in performing those activities but
140
+ which are not part of the work. For example, Corresponding Source
141
+ includes interface definition files associated with source files for
142
+ the work, and the source code for shared libraries and dynamically
143
+ linked subprograms that the work is specifically designed to require,
144
+ such as by intimate data communication or control flow between those
145
+ subprograms and other parts of the work.
146
+
147
+ The Corresponding Source need not include anything that users
148
+ can regenerate automatically from other parts of the Corresponding
149
+ Source.
150
+
151
+ The Corresponding Source for a work in source code form is that
152
+ same work.
153
+
154
+ 2. Basic Permissions.
155
+
156
+ All rights granted under this License are granted for the term of
157
+ copyright on the Program, and are irrevocable provided the stated
158
+ conditions are met. This License explicitly affirms your unlimited
159
+ permission to run the unmodified Program. The output from running a
160
+ covered work is covered by this License only if the output, given its
161
+ content, constitutes a covered work. This License acknowledges your
162
+ rights of fair use or other equivalent, as provided by copyright law.
163
+
164
+ You may make, run and propagate covered works that you do not
165
+ convey, without conditions so long as your license otherwise remains
166
+ in force. You may convey covered works to others for the sole purpose
167
+ of having them make modifications exclusively for you, or provide you
168
+ with facilities for running those works, provided that you comply with
169
+ the terms of this License in conveying all material for which you do
170
+ not control copyright. Those thus making or running the covered works
171
+ for you must do so exclusively on your behalf, under your direction
172
+ and control, on terms that prohibit them from making any copies of
173
+ your copyrighted material outside their relationship with you.
174
+
175
+ Conveying under any other circumstances is permitted solely under
176
+ the conditions stated below. Sublicensing is not allowed; section 10
177
+ makes it unnecessary.
178
+
179
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180
+
181
+ No covered work shall be deemed part of an effective technological
182
+ measure under any applicable law fulfilling obligations under article
183
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184
+ similar laws prohibiting or restricting circumvention of such
185
+ measures.
186
+
187
+ When you convey a covered work, you waive any legal power to forbid
188
+ circumvention of technological measures to the extent such circumvention
189
+ is effected by exercising rights under this License with respect to
190
+ the covered work, and you disclaim any intention to limit operation or
191
+ modification of the work as a means of enforcing, against the work's
192
+ users, your or third parties' legal rights to forbid circumvention of
193
+ technological measures.
194
+
195
+ 4. Conveying Verbatim Copies.
196
+
197
+ You may convey verbatim copies of the Program's source code as you
198
+ receive it, in any medium, provided that you conspicuously and
199
+ appropriately publish on each copy an appropriate copyright notice;
200
+ keep intact all notices stating that this License and any
201
+ non-permissive terms added in accord with section 7 apply to the code;
202
+ keep intact all notices of the absence of any warranty; and give all
203
+ recipients a copy of this License along with the Program.
204
+
205
+ You may charge any price or no price for each copy that you convey,
206
+ and you may offer support or warranty protection for a fee.
207
+
208
+ 5. Conveying Modified Source Versions.
209
+
210
+ You may convey a work based on the Program, or the modifications to
211
+ produce it from the Program, in the form of source code under the
212
+ terms of section 4, provided that you also meet all of these conditions:
213
+
214
+ a) The work must carry prominent notices stating that you modified
215
+ it, and giving a relevant date.
216
+
217
+ b) The work must carry prominent notices stating that it is
218
+ released under this License and any conditions added under section
219
+ 7. This requirement modifies the requirement in section 4 to
220
+ "keep intact all notices".
221
+
222
+ c) You must license the entire work, as a whole, under this
223
+ License to anyone who comes into possession of a copy. This
224
+ License will therefore apply, along with any applicable section 7
225
+ additional terms, to the whole of the work, and all its parts,
226
+ regardless of how they are packaged. This License gives no
227
+ permission to license the work in any other way, but it does not
228
+ invalidate such permission if you have separately received it.
229
+
230
+ d) If the work has interactive user interfaces, each must display
231
+ Appropriate Legal Notices; however, if the Program has interactive
232
+ interfaces that do not display Appropriate Legal Notices, your
233
+ work need not make them do so.
234
+
235
+ A compilation of a covered work with other separate and independent
236
+ works, which are not by their nature extensions of the covered work,
237
+ and which are not combined with it such as to form a larger program,
238
+ in or on a volume of a storage or distribution medium, is called an
239
+ "aggregate" if the compilation and its resulting copyright are not
240
+ used to limit the access or legal rights of the compilation's users
241
+ beyond what the individual works permit. Inclusion of a covered work
242
+ in an aggregate does not cause this License to apply to the other
243
+ parts of the aggregate.
244
+
245
+ 6. Conveying Non-Source Forms.
246
+
247
+ You may convey a covered work in object code form under the terms
248
+ of sections 4 and 5, provided that you also convey the
249
+ machine-readable Corresponding Source under the terms of this License,
250
+ in one of these ways:
251
+
252
+ a) Convey the object code in, or embodied in, a physical product
253
+ (including a physical distribution medium), accompanied by the
254
+ Corresponding Source fixed on a durable physical medium
255
+ customarily used for software interchange.
256
+
257
+ b) Convey the object code in, or embodied in, a physical product
258
+ (including a physical distribution medium), accompanied by a
259
+ written offer, valid for at least three years and valid for as
260
+ long as you offer spare parts or customer support for that product
261
+ model, to give anyone who possesses the object code either (1) a
262
+ copy of the Corresponding Source for all the software in the
263
+ product that is covered by this License, on a durable physical
264
+ medium customarily used for software interchange, for a price no
265
+ more than your reasonable cost of physically performing this
266
+ conveying of source, or (2) access to copy the
267
+ Corresponding Source from a network server at no charge.
268
+
269
+ c) Convey individual copies of the object code with a copy of the
270
+ written offer to provide the Corresponding Source. This
271
+ alternative is allowed only occasionally and noncommercially, and
272
+ only if you received the object code with such an offer, in accord
273
+ with subsection 6b.
274
+
275
+ d) Convey the object code by offering access from a designated
276
+ place (gratis or for a charge), and offer equivalent access to the
277
+ Corresponding Source in the same way through the same place at no
278
+ further charge. You need not require recipients to copy the
279
+ Corresponding Source along with the object code. If the place to
280
+ copy the object code is a network server, the Corresponding Source
281
+ may be on a different server (operated by you or a third party)
282
+ that supports equivalent copying facilities, provided you maintain
283
+ clear directions next to the object code saying where to find the
284
+ Corresponding Source. Regardless of what server hosts the
285
+ Corresponding Source, you remain obligated to ensure that it is
286
+ available for as long as needed to satisfy these requirements.
287
+
288
+ e) Convey the object code using peer-to-peer transmission, provided
289
+ you inform other peers where the object code and Corresponding
290
+ Source of the work are being offered to the general public at no
291
+ charge under subsection 6d.
292
+
293
+ A separable portion of the object code, whose source code is excluded
294
+ from the Corresponding Source as a System Library, need not be
295
+ included in conveying the object code work.
296
+
297
+ A "User Product" is either (1) a "consumer product", which means any
298
+ tangible personal property which is normally used for personal, family,
299
+ or household purposes, or (2) anything designed or sold for incorporation
300
+ into a dwelling. In determining whether a product is a consumer product,
301
+ doubtful cases shall be resolved in favor of coverage. For a particular
302
+ product received by a particular user, "normally used" refers to a
303
+ typical or common use of that class of product, regardless of the status
304
+ of the particular user or of the way in which the particular user
305
+ actually uses, or expects or is expected to use, the product. A product
306
+ is a consumer product regardless of whether the product has substantial
307
+ commercial, industrial or non-consumer uses, unless such uses represent
308
+ the only significant mode of use of the product.
309
+
310
+ "Installation Information" for a User Product means any methods,
311
+ procedures, authorization keys, or other information required to install
312
+ and execute modified versions of a covered work in that User Product from
313
+ a modified version of its Corresponding Source. The information must
314
+ suffice to ensure that the continued functioning of the modified object
315
+ code is in no case prevented or interfered with solely because
316
+ modification has been made.
317
+
318
+ If you convey an object code work under this section in, or with, or
319
+ specifically for use in, a User Product, and the conveying occurs as
320
+ part of a transaction in which the right of possession and use of the
321
+ User Product is transferred to the recipient in perpetuity or for a
322
+ fixed term (regardless of how the transaction is characterized), the
323
+ Corresponding Source conveyed under this section must be accompanied
324
+ by the Installation Information. But this requirement does not apply
325
+ if neither you nor any third party retains the ability to install
326
+ modified object code on the User Product (for example, the work has
327
+ been installed in ROM).
328
+
329
+ The requirement to provide Installation Information does not include a
330
+ requirement to continue to provide support service, warranty, or updates
331
+ for a work that has been modified or installed by the recipient, or for
332
+ the User Product in which it has been modified or installed. Access to a
333
+ network may be denied when the modification itself materially and
334
+ adversely affects the operation of the network or violates the rules and
335
+ protocols for communication across the network.
336
+
337
+ Corresponding Source conveyed, and Installation Information provided,
338
+ in accord with this section must be in a format that is publicly
339
+ documented (and with an implementation available to the public in
340
+ source code form), and must require no special password or key for
341
+ unpacking, reading or copying.
342
+
343
+ 7. Additional Terms.
344
+
345
+ "Additional permissions" are terms that supplement the terms of this
346
+ License by making exceptions from one or more of its conditions.
347
+ Additional permissions that are applicable to the entire Program shall
348
+ be treated as though they were included in this License, to the extent
349
+ that they are valid under applicable law. If additional permissions
350
+ apply only to part of the Program, that part may be used separately
351
+ under those permissions, but the entire Program remains governed by
352
+ this License without regard to the additional permissions.
353
+
354
+ When you convey a copy of a covered work, you may at your option
355
+ remove any additional permissions from that copy, or from any part of
356
+ it. (Additional permissions may be written to require their own
357
+ removal in certain cases when you modify the work.) You may place
358
+ additional permissions on material, added by you to a covered work,
359
+ for which you have or can give appropriate copyright permission.
360
+
361
+ Notwithstanding any other provision of this License, for material you
362
+ add to a covered work, you may (if authorized by the copyright holders of
363
+ that material) supplement the terms of this License with terms:
364
+
365
+ a) Disclaiming warranty or limiting liability differently from the
366
+ terms of sections 15 and 16 of this License; or
367
+
368
+ b) Requiring preservation of specified reasonable legal notices or
369
+ author attributions in that material or in the Appropriate Legal
370
+ Notices displayed by works containing it; or
371
+
372
+ c) Prohibiting misrepresentation of the origin of that material, or
373
+ requiring that modified versions of such material be marked in
374
+ reasonable ways as different from the original version; or
375
+
376
+ d) Limiting the use for publicity purposes of names of licensors or
377
+ authors of the material; or
378
+
379
+ e) Declining to grant rights under trademark law for use of some
380
+ trade names, trademarks, or service marks; or
381
+
382
+ f) Requiring indemnification of licensors and authors of that
383
+ material by anyone who conveys the material (or modified versions of
384
+ it) with contractual assumptions of liability to the recipient, for
385
+ any liability that these contractual assumptions directly impose on
386
+ those licensors and authors.
387
+
388
+ All other non-permissive additional terms are considered "further
389
+ restrictions" within the meaning of section 10. If the Program as you
390
+ received it, or any part of it, contains a notice stating that it is
391
+ governed by this License along with a term that is a further
392
+ restriction, you may remove that term. If a license document contains
393
+ a further restriction but permits relicensing or conveying under this
394
+ License, you may add to a covered work material governed by the terms
395
+ of that license document, provided that the further restriction does
396
+ not survive such relicensing or conveying.
397
+
398
+ If you add terms to a covered work in accord with this section, you
399
+ must place, in the relevant source files, a statement of the
400
+ additional terms that apply to those files, or a notice indicating
401
+ where to find the applicable terms.
402
+
403
+ Additional terms, permissive or non-permissive, may be stated in the
404
+ form of a separately written license, or stated as exceptions;
405
+ the above requirements apply either way.
406
+
407
+ 8. Termination.
408
+
409
+ You may not propagate or modify a covered work except as expressly
410
+ provided under this License. Any attempt otherwise to propagate or
411
+ modify it is void, and will automatically terminate your rights under
412
+ this License (including any patent licenses granted under the third
413
+ paragraph of section 11).
414
+
415
+ However, if you cease all violation of this License, then your
416
+ license from a particular copyright holder is reinstated (a)
417
+ provisionally, unless and until the copyright holder explicitly and
418
+ finally terminates your license, and (b) permanently, if the copyright
419
+ holder fails to notify you of the violation by some reasonable means
420
+ prior to 60 days after the cessation.
421
+
422
+ Moreover, your license from a particular copyright holder is
423
+ reinstated permanently if the copyright holder notifies you of the
424
+ violation by some reasonable means, this is the first time you have
425
+ received notice of violation of this License (for any work) from that
426
+ copyright holder, and you cure the violation prior to 30 days after
427
+ your receipt of the notice.
428
+
429
+ Termination of your rights under this section does not terminate the
430
+ licenses of parties who have received copies or rights from you under
431
+ this License. If your rights have been terminated and not permanently
432
+ reinstated, you do not qualify to receive new licenses for the same
433
+ material under section 10.
434
+
435
+ 9. Acceptance Not Required for Having Copies.
436
+
437
+ You are not required to accept this License in order to receive or
438
+ run a copy of the Program. Ancillary propagation of a covered work
439
+ occurring solely as a consequence of using peer-to-peer transmission
440
+ to receive a copy likewise does not require acceptance. However,
441
+ nothing other than this License grants you permission to propagate or
442
+ modify any covered work. These actions infringe copyright if you do
443
+ not accept this License. Therefore, by modifying or propagating a
444
+ covered work, you indicate your acceptance of this License to do so.
445
+
446
+ 10. Automatic Licensing of Downstream Recipients.
447
+
448
+ Each time you convey a covered work, the recipient automatically
449
+ receives a license from the original licensors, to run, modify and
450
+ propagate that work, subject to this License. You are not responsible
451
+ for enforcing compliance by third parties with this License.
452
+
453
+ An "entity transaction" is a transaction transferring control of an
454
+ organization, or substantially all assets of one, or subdividing an
455
+ organization, or merging organizations. If propagation of a covered
456
+ work results from an entity transaction, each party to that
457
+ transaction who receives a copy of the work also receives whatever
458
+ licenses to the work the party's predecessor in interest had or could
459
+ give under the previous paragraph, plus a right to possession of the
460
+ Corresponding Source of the work from the predecessor in interest, if
461
+ the predecessor has it or can get it with reasonable efforts.
462
+
463
+ You may not impose any further restrictions on the exercise of the
464
+ rights granted or affirmed under this License. For example, you may
465
+ not impose a license fee, royalty, or other charge for exercise of
466
+ rights granted under this License, and you may not initiate litigation
467
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
468
+ any patent claim is infringed by making, using, selling, offering for
469
+ sale, or importing the Program or any portion of it.
470
+
471
+ 11. Patents.
472
+
473
+ A "contributor" is a copyright holder who authorizes use under this
474
+ License of the Program or a work on which the Program is based. The
475
+ work thus licensed is called the contributor's "contributor version".
476
+
477
+ A contributor's "essential patent claims" are all patent claims
478
+ owned or controlled by the contributor, whether already acquired or
479
+ hereafter acquired, that would be infringed by some manner, permitted
480
+ by this License, of making, using, or selling its contributor version,
481
+ but do not include claims that would be infringed only as a
482
+ consequence of further modification of the contributor version. For
483
+ purposes of this definition, "control" includes the right to grant
484
+ patent sublicenses in a manner consistent with the requirements of
485
+ this License.
486
+
487
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
488
+ patent license under the contributor's essential patent claims, to
489
+ make, use, sell, offer for sale, import and otherwise run, modify and
490
+ propagate the contents of its contributor version.
491
+
492
+ In the following three paragraphs, a "patent license" is any express
493
+ agreement or commitment, however denominated, not to enforce a patent
494
+ (such as an express permission to practice a patent or covenant not to
495
+ sue for patent infringement). To "grant" such a patent license to a
496
+ party means to make such an agreement or commitment not to enforce a
497
+ patent against the party.
498
+
499
+ If you convey a covered work, knowingly relying on a patent license,
500
+ and the Corresponding Source of the work is not available for anyone
501
+ to copy, free of charge and under the terms of this License, through a
502
+ publicly available network server or other readily accessible means,
503
+ then you must either (1) cause the Corresponding Source to be so
504
+ available, or (2) arrange to deprive yourself of the benefit of the
505
+ patent license for this particular work, or (3) arrange, in a manner
506
+ consistent with the requirements of this License, to extend the patent
507
+ license to downstream recipients. "Knowingly relying" means you have
508
+ actual knowledge that, but for the patent license, your conveying the
509
+ covered work in a country, or your recipient's use of the covered work
510
+ in a country, would infringe one or more identifiable patents in that
511
+ country that you have reason to believe are valid.
512
+
513
+ If, pursuant to or in connection with a single transaction or
514
+ arrangement, you convey, or propagate by procuring conveyance of, a
515
+ covered work, and grant a patent license to some of the parties
516
+ receiving the covered work authorizing them to use, propagate, modify
517
+ or convey a specific copy of the covered work, then the patent license
518
+ you grant is automatically extended to all recipients of the covered
519
+ work and works based on it.
520
+
521
+ A patent license is "discriminatory" if it does not include within
522
+ the scope of its coverage, prohibits the exercise of, or is
523
+ conditioned on the non-exercise of one or more of the rights that are
524
+ specifically granted under this License. You may not convey a covered
525
+ work if you are a party to an arrangement with a third party that is
526
+ in the business of distributing software, under which you make payment
527
+ to the third party based on the extent of your activity of conveying
528
+ the work, and under which the third party grants, to any of the
529
+ parties who would receive the covered work from you, a discriminatory
530
+ patent license (a) in connection with copies of the covered work
531
+ conveyed by you (or copies made from those copies), or (b) primarily
532
+ for and in connection with specific products or compilations that
533
+ contain the covered work, unless you entered into that arrangement,
534
+ or that patent license was granted, prior to 28 March 2007.
535
+
536
+ Nothing in this License shall be construed as excluding or limiting
537
+ any implied license or other defenses to infringement that may
538
+ otherwise be available to you under applicable patent law.
539
+
540
+ 12. No Surrender of Others' Freedom.
541
+
542
+ If conditions are imposed on you (whether by court order, agreement or
543
+ otherwise) that contradict the conditions of this License, they do not
544
+ excuse you from the conditions of this License. If you cannot convey a
545
+ covered work so as to satisfy simultaneously your obligations under this
546
+ License and any other pertinent obligations, then as a consequence you may
547
+ not convey it at all. For example, if you agree to terms that obligate you
548
+ to collect a royalty for further conveying from those to whom you convey
549
+ the Program, the only way you could satisfy both those terms and this
550
+ License would be to refrain entirely from conveying the Program.
551
+
552
+ 13. Use with the GNU Affero General Public License.
553
+
554
+ Notwithstanding any other provision of this License, you have
555
+ permission to link or combine any covered work with a work licensed
556
+ under version 3 of the GNU Affero General Public License into a single
557
+ combined work, and to convey the resulting work. The terms of this
558
+ License will continue to apply to the part which is the covered work,
559
+ but the special requirements of the GNU Affero General Public License,
560
+ section 13, concerning interaction through a network will apply to the
561
+ combination as such.
562
+
563
+ 14. Revised Versions of this License.
564
+
565
+ The Free Software Foundation may publish revised and/or new versions of
566
+ the GNU General Public License from time to time. Such new versions will
567
+ be similar in spirit to the present version, but may differ in detail to
568
+ address new problems or concerns.
569
+
570
+ Each version is given a distinguishing version number. If the
571
+ Program specifies that a certain numbered version of the GNU General
572
+ Public License "or any later version" applies to it, you have the
573
+ option of following the terms and conditions either of that numbered
574
+ version or of any later version published by the Free Software
575
+ Foundation. If the Program does not specify a version number of the
576
+ GNU General Public License, you may choose any version ever published
577
+ by the Free Software Foundation.
578
+
579
+ If the Program specifies that a proxy can decide which future
580
+ versions of the GNU General Public License can be used, that proxy's
581
+ public statement of acceptance of a version permanently authorizes you
582
+ to choose that version for the Program.
583
+
584
+ Later license versions may give you additional or different
585
+ permissions. However, no additional obligations are imposed on any
586
+ author or copyright holder as a result of your choosing to follow a
587
+ later version.
588
+
589
+ 15. Disclaimer of Warranty.
590
+
591
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
+
600
+ 16. Limitation of Liability.
601
+
602
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610
+ SUCH DAMAGES.
611
+
612
+ 17. Interpretation of Sections 15 and 16.
613
+
614
+ If the disclaimer of warranty and limitation of liability provided
615
+ above cannot be given local legal effect according to their terms,
616
+ reviewing courts shall apply local law that most closely approximates
617
+ an absolute waiver of all civil liability in connection with the
618
+ Program, unless a warranty or assumption of liability accompanies a
619
+ copy of the Program in return for a fee.
620
+
621
+ END OF TERMS AND CONDITIONS
622
+
623
+ How to Apply These Terms to Your New Programs
624
+
625
+ If you develop a new program, and you want it to be of the greatest
626
+ possible use to the public, the best way to achieve this is to make it
627
+ free software which everyone can redistribute and change under these terms.
628
+
629
+ To do so, attach the following notices to the program. It is safest
630
+ to attach them to the start of each source file to most effectively
631
+ state the exclusion of warranty; and each file should have at least
632
+ the "copyright" line and a pointer to where the full notice is found.
633
+
634
+ <one line to give the program's name and a brief idea of what it does.>
635
+ Copyright (C) <year> <name of author>
636
+
637
+ This program is free software: you can redistribute it and/or modify
638
+ it under the terms of the GNU General Public License as published by
639
+ the Free Software Foundation, either version 3 of the License, or
640
+ (at your option) any later version.
641
+
642
+ This program is distributed in the hope that it will be useful,
643
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
644
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645
+ GNU General Public License for more details.
646
+
647
+ You should have received a copy of the GNU General Public License
648
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
649
+
650
+ Also add information on how to contact you by electronic and paper mail.
651
+
652
+ If the program does terminal interaction, make it output a short
653
+ notice like this when it starts in an interactive mode:
654
+
655
+ <program> Copyright (C) <year> <name of author>
656
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657
+ This is free software, and you are welcome to redistribute it
658
+ under certain conditions; type `show c' for details.
659
+
660
+ The hypothetical commands `show w' and `show c' should show the appropriate
661
+ parts of the General Public License. Of course, your program's commands
662
+ might be different; for a GUI interface, you would use an "about box".
663
+
664
+ You should also get your employer (if you work as a programmer) or school,
665
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
666
+ For more information on this, and how to apply and follow the GNU GPL, see
667
+ <https://www.gnu.org/licenses/>.
668
+
669
+ The GNU General Public License does not permit incorporating your program
670
+ into proprietary programs. If your program is a subroutine library, you
671
+ may consider it more useful to permit linking proprietary applications with
672
+ the library. If this is what you want to do, use the GNU Lesser General
673
+ Public License instead of this License. But first, please read
674
+ <https://www.gnu.org/licenses/why-not-lgpl.html>.
README.md CHANGED
@@ -4,7 +4,8 @@ emoji: 📊
4
  colorFrom: yellow
5
  colorTo: yellow
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
4
  colorFrom: yellow
5
  colorTo: yellow
6
  sdk: docker
7
+ pinned: true
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
11
+
README.md-- ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Qwen AI Free 服务
2
+
3
+ [![](https://img.shields.io/github/license/llm-red-team/qwen-free-api.svg)](LICENSE)
4
+ ![](https://img.shields.io/github/stars/llm-red-team/qwen-free-api.svg)
5
+ ![](https://img.shields.io/github/forks/llm-red-team/qwen-free-api.svg)
6
+ ![](https://img.shields.io/docker/pulls/vinlic/qwen-free-api.svg)
7
+
8
+ 支持高速流式输出、支持多轮对话、支持无水印AI绘图、支持长文档解读、图像解析,零配置部署,多路token支持,自动清理会话痕迹。
9
+
10
+ 与ChatGPT接口完全兼容。
11
+
12
+ 还有以下八个free-api欢迎关注:
13
+
14
+ Moonshot AI(Kimi.ai)接口转API [kimi-free-api](https://github.com/LLM-Red-Team/kimi-free-api)
15
+
16
+ 阶跃星辰 (跃问StepChat) 接口转API [step-free-api](https://github.com/LLM-Red-Team/step-free-api)
17
+
18
+ 智谱AI (智谱清言) 接口转API [glm-free-api](https://github.com/LLM-Red-Team/glm-free-api)
19
+
20
+ 秘塔AI (Metaso) 接口转API [metaso-free-api](https://github.com/LLM-Red-Team/metaso-free-api)
21
+
22
+ 讯飞星火(Spark)接口转API [spark-free-api](https://github.com/LLM-Red-Team/spark-free-api)
23
+
24
+ MiniMax(海螺AI)接口转API [hailuo-free-api](https://github.com/LLM-Red-Team/hailuo-free-api)
25
+
26
+ 深度求索(DeepSeek)接口转API [deepseek-free-api](https://github.com/LLM-Red-Team/deepseek-free-api)
27
+
28
+ 聆心智能 (Emohaa) 接口转API [emohaa-free-api](https://github.com/LLM-Red-Team/emohaa-free-api)
29
+
30
+ ## 目录
31
+
32
+ * [免责声明](#免责声明)
33
+ * [在线体验](#在线体验)
34
+ * [效果示例](#效果示例)
35
+ * [接入准备](#接入准备)
36
+ * [Docker部署](#Docker部署)
37
+ * [Docker-compose部署](#Docker-compose部署)
38
+ * [Render部署](#Render部署)
39
+ * [Vercel部署](#Vercel部署)
40
+ * [原生部署](#原生部署)
41
+ * [推荐使用客户端](#推荐使用客户端)
42
+ * [接口列表](#接口列表)
43
+ * [对话补全](#对话补全)
44
+ * [AI绘图](#AI绘图)
45
+ * [文档解读](#文档解读)
46
+ * [图像解析](#图像解析)
47
+ * [ticket存活检测](#ticket存活检测)
48
+ * [注意事项](#注意事项)
49
+ * [Nginx反代优化](#Nginx反代优化)
50
+ * [Token统计](#Token统计)
51
+ * [Star History](#star-history)
52
+
53
+ ## 免责声明
54
+
55
+ **逆向API是不稳定的,建议前往阿里云官方 https://dashscope.console.aliyun.com/ 付费使用API,避免封禁的风险。**
56
+
57
+ **本组织和个人不接受任何资金捐助和交易,此项目是纯粹研究交流学习性质!**
58
+
59
+ **仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!**
60
+
61
+ **仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!**
62
+
63
+ **仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!**
64
+
65
+ ## 在线体验
66
+
67
+ 此链接仅临时测试功能,长期使用请自行部署。
68
+
69
+ https://udify.app/chat/qOXzVl5kkvhQXM8r
70
+
71
+ ## 效果示例
72
+
73
+ ### 验明正身Demo
74
+
75
+ ![验明正身](./doc/example-1.png)
76
+
77
+ ### 多轮对话Demo
78
+
79
+ ![多轮对话](./doc/example-2.png)
80
+
81
+ ### AI绘图Demo
82
+
83
+ ![AI绘图](./doc/example-3.png)
84
+
85
+ ### 长文档解读Demo
86
+
87
+ ![AI绘图](./doc/example-5.png)
88
+
89
+ ### 图像解析Demo
90
+
91
+ ![AI绘图](./doc/example-6.png)
92
+
93
+ ### 10线程并发测试
94
+
95
+ ![10线程并发测试](./doc/example-4.png)
96
+
97
+ ## 接入准备
98
+
99
+ ### 方法1
100
+
101
+ 从 [通义千问](https://tongyi.aliyun.com/qianwen) 登录
102
+
103
+ 进入通义千问随便发起一个对话,然后F12打开开发者工具,从Application > Cookies中找到`tongyi_sso_ticket`的值,这将作为Authorization的Bearer Token值:`Authorization: Bearer TOKEN`
104
+
105
+ ![获取tongyi_sso_ticket](./doc/example-0.png)
106
+
107
+ ### 方法2
108
+
109
+ 从 [阿里云](https://www.aliyun.com/) 登录(如果该账号有服务器等重要资产不建议使用),如果该账号之前未进入过[通义千问](https://tongyi.aliyun.com/qianwen) ,需要先进入同意协议,否则无法生效。
110
+
111
+ 然后F12打开开发者工具,从Application > Cookies中找到`login_aliyunid_ticket`的值,这将作为Authorization的Bearer Token值:`Authorization: Bearer TOKEN`
112
+
113
+ ![获取login_aliyunid_ticket](./doc/example-7.png)
114
+
115
+ ### 多账号接入
116
+
117
+ 你可以通过提供多个账号的tongyi_sso_ticket或login_aliyunid_ticket,并使用,拼接提供:
118
+
119
+ Authorization: Bearer TOKEN1,TOKEN2,TOKEN3
120
+
121
+ 每次请求服务会从中挑选一个。
122
+
123
+ ## Docker部署
124
+
125
+ 请准备一台具有公网IP的服务器并将8000端口开放。
126
+
127
+ 拉取镜像并启动服务
128
+
129
+ ```shell
130
+ docker run -it -d --init --name qwen-free-api -p 8000:8000 -e TZ=Asia/Shanghai vinlic/qwen-free-api:latest
131
+ ```
132
+
133
+ 查看服务实时日志
134
+
135
+ ```shell
136
+ docker logs -f qwen-free-api
137
+ ```
138
+
139
+ 重启服务
140
+
141
+ ```shell
142
+ docker restart qwen-free-api
143
+ ```
144
+
145
+ 停止服务
146
+
147
+ ```shell
148
+ docker stop qwen-free-api
149
+ ```
150
+
151
+ ### Docker-compose部署
152
+
153
+ ```yaml
154
+ version: '3'
155
+
156
+ services:
157
+ qwen-free-api:
158
+ container_name: qwen-free-api
159
+ image: vinlic/qwen-free-api:latest
160
+ restart: always
161
+ ports:
162
+ - "8000:8000"
163
+ environment:
164
+ - TZ=Asia/Shanghai
165
+ ```
166
+
167
+ ### Render部署
168
+
169
+ **注意:部分部署区域可能无法连接qwen,如容器日志出现请求超时或无法连接,请切换其他区域部署!**
170
+ **注意:免费账户的容器实例将在一段时间不活动时自动停止运行,这会导致下次请求时遇到50秒或更长的延迟,建议查看[Render容器保活](https://github.com/LLM-Red-Team/free-api-hub/#Render%E5%AE%B9%E5%99%A8%E4%BF%9D%E6%B4%BB)**
171
+
172
+ 1. fork本项目到你的github账号下。
173
+
174
+ 2. 访问 [Render](https://dashboard.render.com/) 并登录你的github账号。
175
+
176
+ 3. 构建你的 Web Service(New+ -> Build and deploy from a Git repository -> Connect你fork的项目 -> 选择部署区域 -> 选择实例类型为Free -> Create Web Service)。
177
+
178
+ 4. 等待构建完成后,复制分配的域名并拼接URL访问即可。
179
+
180
+ ### Vercel部署
181
+
182
+ **注意:Vercel免费账户的请求响应超时时间为10秒,但接口响应通常较久,可能会遇到Vercel返回的504超时错误!**
183
+
184
+ 请先确保安装了Node.js环境。
185
+
186
+ ```shell
187
+ npm i -g vercel --registry http://registry.npmmirror.com
188
+ vercel login
189
+ git clone https://github.com/LLM-Red-Team/qwen-free-api
190
+ cd qwen-free-api
191
+ vercel --prod
192
+ ```
193
+
194
+ ## 原生部署
195
+
196
+ 请准备一台具有公网IP的服务器并将8000端口开放。
197
+
198
+ 请先安装好Node.js环境并且配置好环境变量,确认node命令可用。
199
+
200
+ 安装依赖
201
+
202
+ ```shell
203
+ npm i
204
+ ```
205
+
206
+ 安装PM2进行进程守护
207
+
208
+ ```shell
209
+ npm i -g pm2
210
+ ```
211
+
212
+ 编译构建,看到dist目录就是构建完成
213
+
214
+ ```shell
215
+ npm run build
216
+ ```
217
+
218
+ 启动服务
219
+
220
+ ```shell
221
+ pm2 start dist/index.js --name "qwen-free-api"
222
+ ```
223
+
224
+ 查看服务实时日志
225
+
226
+ ```shell
227
+ pm2 logs qwen-free-api
228
+ ```
229
+
230
+ 重启服务
231
+
232
+ ```shell
233
+ pm2 reload qwen-free-api
234
+ ```
235
+
236
+ 停止服务
237
+
238
+ ```shell
239
+ pm2 stop qwen-free-api
240
+ ```
241
+
242
+ ## 推荐使用客户端
243
+
244
+ 使用以下二次开发客户端接入free-api系列项目更快更简单,支持文档/图像上传!
245
+
246
+ 由 [Clivia](https://github.com/Yanyutin753/lobe-chat) 二次开发的LobeChat [https://github.com/Yanyutin753/lobe-chat](https://github.com/Yanyutin753/lobe-chat)
247
+
248
+ 由 [时光@](https://github.com/SuYxh) 二次开发的ChatGPT Web [https://github.com/SuYxh/chatgpt-web-sea](https://github.com/SuYxh/chatgpt-web-sea)
249
+
250
+ ## 接口列表
251
+
252
+ 目前支持与openai兼容的 `/v1/chat/completions` 接口,可自行使用与openai或其他兼容的客户端接入接口,或者使用 [dify](https://dify.ai/) 等线上服务接入使用。
253
+
254
+ ### 对话补全
255
+
256
+ 对话补全接口,与openai的 [chat-completions-api](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) 兼容。
257
+
258
+ **POST /v1/chat/completions**
259
+
260
+ header 需要设置 Authorization 头部:
261
+
262
+ ```
263
+ Authorization: Bearer [tongyi_sso_ticket/login_aliyunid_ticket]
264
+ ```
265
+
266
+ 请求数据:
267
+ ```json
268
+ {
269
+ // 模型名称随意填写
270
+ "model": "qwen",
271
+ // 目前多轮对话基于消息合并实现,某些场景可能导致能力下降且受单轮最大token数限制
272
+ // 如果您想获得原生的多轮对话体验,可以传入上一轮消息获得的id,来接续上下文
273
+ // "conversation_id": "bc9ef150d0e44794ab624df958292300-40811965812e4782bb87f1a9e4e2b2cd",
274
+ "messages": [
275
+ {
276
+ "role": "user",
277
+ "content": "你是谁?"
278
+ }
279
+ ],
280
+ // 如果使用SSE流请设置为true,默认false
281
+ "stream": false
282
+ }
283
+ ```
284
+
285
+ 响应数据:
286
+ ```json
287
+ {
288
+ // 如果想获得原生多轮对话体验,此id,你可以传入到下一轮对话的conversation_id来接续上下文
289
+ "id": "bc9ef150d0e44794ab624df958292300-40811965812e4782bb87f1a9e4e2b2cd",
290
+ "model": "qwen",
291
+ "object": "chat.completion",
292
+ "choices": [
293
+ {
294
+ "index": 0,
295
+ "message": {
296
+ "role": "assistant",
297
+ "content": "我是阿里云研发的超大规模语言模型,我叫通义千问。"
298
+ },
299
+ "finish_reason": "stop"
300
+ }
301
+ ],
302
+ "usage": {
303
+ "prompt_tokens": 1,
304
+ "completion_tokens": 1,
305
+ "total_tokens": 2
306
+ },
307
+ "created": 1710152062
308
+ }
309
+ ```
310
+
311
+ ### AI绘图
312
+
313
+ 对话补全接口,与openai的 [images-create-api](https://platform.openai.com/docs/api-reference/images/create) 兼容。
314
+
315
+ **POST /v1/images/generations**
316
+
317
+ header 需要设置 Authorization 头部:
318
+
319
+ ```
320
+ Authorization: Bearer [tongyi_sso_ticket/login_aliyunid_ticket]
321
+ ```
322
+
323
+ 请求数据:
324
+ ```json
325
+ {
326
+ // 可以乱填
327
+ "model": "wanxiang",
328
+ "prompt": "一只可爱的猫"
329
+ }
330
+ ```
331
+
332
+ 响应数据:
333
+ ```json
334
+ {
335
+ "created": 1711507734,
336
+ "data": [
337
+ {
338
+ "url": "https://wanx.alicdn.com/wanx/1111111111/text_to_image/7248e85cfda6491aae59c54e7e679b17_0.png"
339
+ }
340
+ ]
341
+ }
342
+ ```
343
+
344
+ ### 文档解读
345
+
346
+ 提供一个可访问的文件URL或者BASE64_URL进行解析。
347
+
348
+ **POST /v1/chat/completions**
349
+
350
+ header 需要设置 Authorization 头部:
351
+
352
+ ```
353
+ Authorization: Bearer [refresh_token]
354
+ ```
355
+
356
+ 请求数据:
357
+ ```json
358
+ {
359
+ "model": "qwen",
360
+ "messages": [
361
+ {
362
+ "role": "user",
363
+ "content": [
364
+ {
365
+ "type": "file",
366
+ "file_url": {
367
+ "url": "https://mj101-1317487292.cos.ap-shanghai.myqcloud.com/ai/test.pdf"
368
+ }
369
+ },
370
+ {
371
+ "type": "text",
372
+ "text": "文档里说了什么?"
373
+ }
374
+ ]
375
+ }
376
+ ]
377
+ }
378
+ ```
379
+
380
+ 响应数据:
381
+ ```json
382
+ {
383
+ "id": "b56ea6c9e86140429fa2de6a6ec028ff",
384
+ "model": "qwen",
385
+ "object": "chat.completion",
386
+ "choices": [
387
+ {
388
+ "index": 0,
389
+ "message": {
390
+ "role": "assistant",
391
+ "content": "文档中包含了四个古代魔法仪式或咒语的描述,它们似乎旨在影响或控制一个特定女性的情感和行为,使其对施术者产生强烈的爱意。以下是每个仪式的具体内容:\n\n1. **仪式一**(PMG 4.1390 – 1495):\n - 施术者需留下一些面包,将其掰成七小块。\n - 前往一处英雄、角斗士或其他暴力死亡者丧生的地方。\n - 对着面包碎片念诵咒语后丢弃,并从该地取一些受污染的泥土扔进目标女性的住所。\n - 咒语内容包括向命运三女神(Moirai)、罗马版的命运女神(Fates)、自然力量(Daemons)、饥荒与嫉妒之神以及非正常死亡者献祭食物,并请求他们以痛苦折磨目标,使她在梦中惊醒,心生忧虑与恐惧,最终跟随施术者的步伐并顺从其意愿。此过程以赫卡忒(Hecate)女神为命令的源泉。\n\n2. **仪式二**(PMG 4.1342 – 57):\n - 施术者召唤恶魔(Daemon),通过一系列神秘的神祇名号(如Erekisephthe Araracharara Ephthesikere)要求其将名为Tereous的女子(Apia所生)带至施术者Didymos(Taipiam所生)身边。\n - 请求该女子在灵魂、心智及女性器官上遭受剧烈痛苦,直至她主动找寻Didymos并与之紧密相连(唇对唇、发对发、腹部对腹部)。整个过程要求立即执行。\n\n3. **仪式三**(PGM 4.1265 – 74):\n - 揭示了阿佛洛狄忒(Aphrodite)鲜为人知的名字——NEPHERIĒRI[nfr-iry-t]。\n - 如果想赢得一位美丽女子的芳心,施术者应保持三天纯净,献上乳香,并在心中默念该名字七次。\n - 这样的做法需持续七天,据说这样便能成功吸引女子。\n\n4. **仪式四**(PGM 4.1496 – 1):\n - 施术者在燃烧的煤炭上供奉没药(myrrh),同时念诵咒语。\n - 咒语将没药称为“苦涩的调和者”、“热力的激发者”,并命令它前往指定的女子(及其母亲的名字)处,阻止她进行日常活动(如坐、饮、食、注视他人、亲吻他人),迫使她心中只有施术者,对其产生强烈的欲望与爱意。\n - 咒语还指示没药直接穿透女子的灵魂,驻留在其心中,焚烧其内脏、胸部、肝脏、气息、骨骼、骨髓,直到她来到施术者身边。\n\n这些仪式反映了古代魔法实践中试图借助超自然力量操控他人情感与行为的企图,涉及对神灵、恶魔、神秘名字及特定物质(如面包、泥土、乳香、没药)的运用,通常伴随着严格的仪式规程和咒语念诵。此类行为在现代伦理和法律框架下被视为不恰当甚至违法,且缺乏科学依据。"
392
+ },
393
+ "finish_reason": "stop"
394
+ }
395
+ ],
396
+ "usage": {
397
+ "prompt_tokens": 1,
398
+ "completion_tokens": 1,
399
+ "total_tokens": 2
400
+ },
401
+ "created": 1712253736
402
+ }
403
+ ```
404
+
405
+ ### 图像解析
406
+
407
+ 提供一个可访问的图像URL或者BASE64_URL进行解析。
408
+
409
+ 此格式兼容 [gpt-4-vision-preview](https://platform.openai.com/docs/guides/vision) API格式,您也可以用这个格式传送文档进行解析。
410
+
411
+ **POST /v1/chat/completions**
412
+
413
+ header 需要设置 Authorization 头部:
414
+
415
+ ```
416
+ Authorization: Bearer [refresh_token]
417
+ ```
418
+
419
+ 请求数据:
420
+ ```json
421
+ {
422
+ "model": "qwen",
423
+ "messages": [
424
+ {
425
+ "role": "user",
426
+ "content": [
427
+ {
428
+ "type": "file",
429
+ "file_url": {
430
+ "url": "https://img.alicdn.com/imgextra/i1/O1CN01CC9kic1ig1r4sAY5d_!!6000000004441-2-tps-880-210.png"
431
+ }
432
+ },
433
+ {
434
+ "type": "text",
435
+ "text": "图像描述了什么?"
436
+ }
437
+ ]
438
+ }
439
+ ]
440
+ }
441
+ ```
442
+
443
+ 响应数据:
444
+ ```json
445
+ {
446
+ "id": "895fbe7fa22442d499ba67bb5213e842",
447
+ "model": "qwen",
448
+ "object": "chat.completion",
449
+ "choices": [
450
+ {
451
+ "index": 0,
452
+ "message": {
453
+ "role": "assistant",
454
+ "content": "图像展示了通义千问的标志,一个紫色的六边形和一个蓝色的三角形,以及“通义千问”四个白色的汉字。"
455
+ },
456
+ "finish_reason": "stop"
457
+ }
458
+ ],
459
+ "usage": {
460
+ "prompt_tokens": 1,
461
+ "completion_tokens": 1,
462
+ "total_tokens": 2
463
+ },
464
+ "created": 1712254066
465
+ }
466
+ ```
467
+
468
+ ### ticket存活检测
469
+
470
+ 检测tongyi_sso_ticket或login_aliyunid_ticket是否存活,如果存活live未true,否则为false,请不要频繁(小于10分钟)调用此接口。
471
+
472
+ **POST /token/check**
473
+
474
+ 请求数据:
475
+ ```json
476
+ {
477
+ "token": "QIhaHrrXUaIrWMUmL..."
478
+ }
479
+ ```
480
+
481
+ 响应数据:
482
+ ```json
483
+ {
484
+ "live": true
485
+ }
486
+ ```
487
+
488
+ ## 注意事项
489
+
490
+ ### Nginx反代优化
491
+
492
+ 如果您正在使用Nginx反向代理qwen-free-api,请添加以下配置项优化流的输出效果,优化体验感。
493
+
494
+ ```nginx
495
+ # 关闭代理缓冲。当设置为off时,Nginx会立即将客户端请求发送到后端服务器,并立即将从后端服务器接收到的响应发送回客户端。
496
+ proxy_buffering off;
497
+ # 启用分块传输编码。分块传输编码允许服务器为动态生成的内容分块发送数据,而不需要预先知道内容的大小。
498
+ chunked_transfer_encoding on;
499
+ # 开启TCP_NOPUSH,这告诉Nginx在数据包发送到客户端之前,尽可能地发送数据。这通常在sendfile使用时配合使用,可以提高网络效率。
500
+ tcp_nopush on;
501
+ # 开启TCP_NODELAY,这告诉Nginx不延迟发送数据,立即发送小数据包。在某些情况下,这可以减少网络的延迟。
502
+ tcp_nodelay on;
503
+ # 设置保持连接的超时时间,这里设置为120秒。如果在这段时间内,客户端和服务器之间没有进一步的通信,连接将被关闭。
504
+ keepalive_timeout 120;
505
+ ```
506
+
507
+ ### Token统计
508
+
509
+ 由于推理侧不在qwen-free-api,因此token不可统计,将以固定数字返回。
510
+
511
+ ## Star History
512
+
513
+ [![Star History Chart](https://api.star-history.com/svg?repos=LLM-Red-Team/qwen-free-api&type=Date)](https://star-history.com/#LLM-Red-Team/qwen-free-api&Date)
configs/dev/service.yml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # 服务名称
2
+ name: qwen-free-api
3
+ # 服务绑定主机地址
4
+ host: '0.0.0.0'
5
+ # 服务绑定端口
6
+ port: 8000
configs/dev/system.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 是否开启请求日志
2
+ requestLog: true
3
+ # 临时目录路径
4
+ tmpDir: ./tmp
5
+ # 日志目录路径
6
+ logDir: ./logs
7
+ # 日志写入间隔(毫秒)
8
+ logWriteInterval: 200
9
+ # 日志文件有效期(毫秒)
10
+ logFileExpires: 2626560000
11
+ # 公共目录路径
12
+ publicDir: ./public
13
+ # 临时文件有效期(毫秒)
14
+ tmpFileExpires: 86400000
doc/example-0.png ADDED
doc/example-1.png ADDED
doc/example-2.png ADDED
doc/example-3.png ADDED
doc/example-4.png ADDED
doc/example-5.png ADDED
doc/example-6.png ADDED
doc/example-7.png ADDED
libs.d.ts ADDED
File without changes
package.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "qwen-free-api",
3
+ "version": "0.0.20",
4
+ "description": "Qwen Free API Server",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "directories": {
10
+ "dist": "dist"
11
+ },
12
+ "files": [
13
+ "dist/"
14
+ ],
15
+ "scripts": {
16
+ "dev": "tsup src/index.ts --format cjs,esm --sourcemap --dts --publicDir public --watch --onSuccess \"node dist/index.js\"",
17
+ "start": "node dist/index.js",
18
+ "build": "tsup src/index.ts --format cjs,esm --sourcemap --dts --clean --publicDir public"
19
+ },
20
+ "author": "Vinlic",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "axios": "^1.6.7",
24
+ "colors": "^1.4.0",
25
+ "crc-32": "^1.2.2",
26
+ "cron": "^3.1.6",
27
+ "date-fns": "^3.3.1",
28
+ "eventsource-parser": "^1.1.2",
29
+ "form-data": "^4.0.0",
30
+ "fs-extra": "^11.2.0",
31
+ "koa": "^2.15.0",
32
+ "koa-body": "^5.0.0",
33
+ "koa-bodyparser": "^4.4.1",
34
+ "koa-range": "^0.3.0",
35
+ "koa-router": "^12.0.1",
36
+ "koa2-cors": "^2.0.6",
37
+ "lodash": "^4.17.21",
38
+ "mime": "^4.0.1",
39
+ "minimist": "^1.2.8",
40
+ "randomstring": "^1.3.0",
41
+ "uuid": "^9.0.1",
42
+ "yaml": "^2.3.4"
43
+ },
44
+ "devDependencies": {
45
+ "@types/lodash": "^4.14.202",
46
+ "@types/mime": "^3.0.4",
47
+ "tsup": "^8.0.2",
48
+ "typescript": "^5.3.3"
49
+ }
50
+ }
public/welcome.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title>🚀 服务已启动</title>
6
+ </head>
7
+ <body>
8
+ <p>qwen-free-api已启动!<br>请通过LobeChat / NextChat / Dify等客户端或OpenAI SDK接入!</p>
9
+ </body>
10
+ </html>
src/api/consts/exceptions.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default {
2
+ API_TEST: [-9999, 'API异常错误'],
3
+ API_REQUEST_PARAMS_INVALID: [-2000, '请求参数非法'],
4
+ API_REQUEST_FAILED: [-2001, '请求失败'],
5
+ API_TOKEN_EXPIRES: [-2002, 'Token已失效'],
6
+ API_FILE_URL_INVALID: [-2003, '远程文件URL非法'],
7
+ API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'],
8
+ API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'],
9
+ API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'],
10
+ API_IMAGE_GENERATION_FAILED: [-2007, '图像生成失败']
11
+ }
src/api/controllers/chat.ts ADDED
@@ -0,0 +1,966 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { URL } from "url";
2
+ import { PassThrough } from "stream";
3
+ import http2 from "http2";
4
+ import path from "path";
5
+ import _ from "lodash";
6
+ import mime from "mime";
7
+ import FormData from "form-data";
8
+ import axios, { AxiosResponse } from "axios";
9
+
10
+ import APIException from "@/lib/exceptions/APIException.ts";
11
+ import EX from "@/api/consts/exceptions.ts";
12
+ import { createParser } from "eventsource-parser";
13
+ import logger from "@/lib/logger.ts";
14
+ import util from "@/lib/util.ts";
15
+
16
+ // 模型名称
17
+ const MODEL_NAME = "qwen";
18
+ // 最大重试次数
19
+ const MAX_RETRY_COUNT = 3;
20
+ // 重试延迟
21
+ const RETRY_DELAY = 5000;
22
+ // 伪装headers
23
+ const FAKE_HEADERS = {
24
+ Accept: "application/json, text/plain, */*",
25
+ "Accept-Encoding": "gzip, deflate, br, zstd",
26
+ "Accept-Language": "zh-CN,zh;q=0.9",
27
+ "Cache-Control": "no-cache",
28
+ Origin: "https://tongyi.aliyun.com",
29
+ Pragma: "no-cache",
30
+ "Sec-Ch-Ua":
31
+ '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
32
+ "Sec-Ch-Ua-Mobile": "?0",
33
+ "Sec-Ch-Ua-Platform": '"Windows"',
34
+ "Sec-Fetch-Dest": "empty",
35
+ "Sec-Fetch-Mode": "cors",
36
+ "Sec-Fetch-Site": "same-site",
37
+ Referer: "https://tongyi.aliyun.com/",
38
+ "User-Agent":
39
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
40
+ "X-Platform": "pc_tongyi",
41
+ "X-Xsrf-Token": "48b9ee49-a184-45e2-9f67-fa87213edcdc",
42
+ };
43
+ // 文件最大大小
44
+ const FILE_MAX_SIZE = 100 * 1024 * 1024;
45
+
46
+ /**
47
+ * 移除会话
48
+ *
49
+ * 在对话流传输完毕后移除会话,避免创建的会话出现在用户的对话列表中
50
+ *
51
+ * @param ticket tongyi_sso_ticket或login_aliyunid_ticket
52
+ */
53
+ async function removeConversation(convId: string, ticket: string) {
54
+ const result = await axios.post(
55
+ `https://qianwen.biz.aliyun.com/dialog/session/delete`,
56
+ {
57
+ sessionId: convId,
58
+ },
59
+ {
60
+ headers: {
61
+ Cookie: generateCookie(ticket),
62
+ ...FAKE_HEADERS,
63
+ },
64
+ timeout: 15000,
65
+ validateStatus: () => true,
66
+ }
67
+ );
68
+ checkResult(result);
69
+ }
70
+
71
+ /**
72
+ * 同步对话补全
73
+ *
74
+ * @param model 模型名称
75
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
76
+ * @param ticket tongyi_sso_ticket或login_aliyunid_ticket
77
+ * @param refConvId 引用的会话ID
78
+ * @param retryCount 重试次数
79
+ */
80
+ async function createCompletion(
81
+ model = MODEL_NAME,
82
+ messages: any[],
83
+ ticket: string,
84
+ refConvId = '',
85
+ retryCount = 0
86
+ ) {
87
+ let session: http2.ClientHttp2Session;
88
+ return (async () => {
89
+ logger.info(messages);
90
+
91
+ // 提取引用文件URL并上传qwen获得引用的文件ID列表
92
+ const refFileUrls = extractRefFileUrls(messages);
93
+ const refs = refFileUrls.length
94
+ ? await Promise.all(
95
+ refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket))
96
+ )
97
+ : [];
98
+
99
+ // 如果引用对话ID不正确则重置引用
100
+ if (!/[0-9a-z]{32}/.test(refConvId))
101
+ refConvId = '';
102
+
103
+ // 请求流
104
+ const session: http2.ClientHttp2Session = await new Promise(
105
+ (resolve, reject) => {
106
+ const session = http2.connect("https://qianwen.biz.aliyun.com");
107
+ session.on("connect", () => resolve(session));
108
+ session.on("error", reject);
109
+ }
110
+ );
111
+ const [sessionId, parentMsgId = ''] = refConvId.split('-');
112
+ const req = session.request({
113
+ ":method": "POST",
114
+ ":path": "/dialog/conversation",
115
+ "Content-Type": "application/json",
116
+ Cookie: generateCookie(ticket),
117
+ ...FAKE_HEADERS,
118
+ Accept: "text/event-stream",
119
+ });
120
+ req.setTimeout(120000);
121
+ req.write(
122
+ JSON.stringify({
123
+ mode: "chat",
124
+ model: "",
125
+ action: "next",
126
+ userAction: "chat",
127
+ requestId: util.uuid(false),
128
+ sessionId,
129
+ sessionType: "text_chat",
130
+ parentMsgId,
131
+ params: {
132
+ "fileUploadBatchId": util.uuid()
133
+ },
134
+ contents: messagesPrepare(messages, refs, !!refConvId),
135
+ })
136
+ );
137
+ req.setEncoding("utf8");
138
+ const streamStartTime = util.timestamp();
139
+ // 接收流为输出文本
140
+ const answer = await receiveStream(req);
141
+ session.close();
142
+ logger.success(
143
+ `Stream has completed transfer ${util.timestamp() - streamStartTime}ms`
144
+ );
145
+
146
+ // 异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略
147
+ removeConversation(answer.id, ticket).catch((err) => console.error(err));
148
+
149
+ return answer;
150
+ })().catch((err) => {
151
+ session && session.close();
152
+ if (retryCount < MAX_RETRY_COUNT) {
153
+ logger.error(`Stream response error: ${err.message}`);
154
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
155
+ return (async () => {
156
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
157
+ return createCompletion(model, messages, ticket, refConvId, retryCount + 1);
158
+ })();
159
+ }
160
+ throw err;
161
+ });
162
+ }
163
+
164
+ /**
165
+ * 流式对话补全
166
+ *
167
+ * @param model 模���名称
168
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
169
+ * @param ticket tongyi_sso_ticket或login_aliyunid_ticket
170
+ * @param refConvId 引用的会话ID
171
+ * @param retryCount 重试次数
172
+ */
173
+ async function createCompletionStream(
174
+ model = MODEL_NAME,
175
+ messages: any[],
176
+ ticket: string,
177
+ refConvId = '',
178
+ retryCount = 0
179
+ ) {
180
+ let session: http2.ClientHttp2Session;
181
+ return (async () => {
182
+ logger.info(messages);
183
+
184
+ // 提取引用文件URL并上传qwen获得引用的文件ID列表
185
+ const refFileUrls = extractRefFileUrls(messages);
186
+ const refs = refFileUrls.length
187
+ ? await Promise.all(
188
+ refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket))
189
+ )
190
+ : [];
191
+
192
+ // 如果引用对话ID不正确则重置引用
193
+ if (!/[0-9a-z]{32}/.test(refConvId))
194
+ refConvId = ''
195
+
196
+ // 请求流
197
+ session = await new Promise((resolve, reject) => {
198
+ const session = http2.connect("https://qianwen.biz.aliyun.com");
199
+ session.on("connect", () => resolve(session));
200
+ session.on("error", reject);
201
+ });
202
+ const [sessionId, parentMsgId = ''] = refConvId.split('-');
203
+ const req = session.request({
204
+ ":method": "POST",
205
+ ":path": "/dialog/conversation",
206
+ "Content-Type": "application/json",
207
+ Cookie: generateCookie(ticket),
208
+ ...FAKE_HEADERS,
209
+ Accept: "text/event-stream",
210
+ });
211
+ req.setTimeout(120000);
212
+ req.write(
213
+ JSON.stringify({
214
+ mode: "chat",
215
+ model: "",
216
+ action: "next",
217
+ userAction: "chat",
218
+ requestId: util.uuid(false),
219
+ sessionId,
220
+ sessionType: "text_chat",
221
+ parentMsgId,
222
+ params: {
223
+ "fileUploadBatchId": util.uuid()
224
+ },
225
+ contents: messagesPrepare(messages, refs, !!refConvId),
226
+ })
227
+ );
228
+ req.setEncoding("utf8");
229
+ const streamStartTime = util.timestamp();
230
+ // 创建转换流将消息格式转换为gpt兼容格式
231
+ return createTransStream(req, (convId: string) => {
232
+ // 关闭请求会话
233
+ session.close();
234
+ logger.success(
235
+ `Stream has completed transfer ${util.timestamp() - streamStartTime}ms`
236
+ );
237
+ // 流传输结束后异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略
238
+ removeConversation(convId, ticket).catch((err) => console.error(err));
239
+ });
240
+ })().catch((err) => {
241
+ session && session.close();
242
+ if (retryCount < MAX_RETRY_COUNT) {
243
+ logger.error(`Stream response error: ${err.message}`);
244
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
245
+ return (async () => {
246
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
247
+ return createCompletionStream(model, messages, ticket, refConvId, retryCount + 1);
248
+ })();
249
+ }
250
+ throw err;
251
+ });
252
+ }
253
+
254
+ async function generateImages(
255
+ model = MODEL_NAME,
256
+ prompt: string,
257
+ ticket: string,
258
+ retryCount = 0
259
+ ) {
260
+ let session: http2.ClientHttp2Session;
261
+ return (async () => {
262
+ const messages = [
263
+ { role: "user", content: prompt.indexOf('画') == -1 ? `请画:${prompt}` : prompt },
264
+ ];
265
+ // 请求流
266
+ const session: http2.ClientHttp2Session = await new Promise(
267
+ (resolve, reject) => {
268
+ const session = http2.connect("https://qianwen.biz.aliyun.com");
269
+ session.on("connect", () => resolve(session));
270
+ session.on("error", reject);
271
+ }
272
+ );
273
+ const req = session.request({
274
+ ":method": "POST",
275
+ ":path": "/dialog/conversation",
276
+ "Content-Type": "application/json",
277
+ Cookie: generateCookie(ticket),
278
+ ...FAKE_HEADERS,
279
+ Accept: "text/event-stream",
280
+ });
281
+ req.setTimeout(120000);
282
+ req.write(
283
+ JSON.stringify({
284
+ mode: "chat",
285
+ model: "",
286
+ action: "next",
287
+ userAction: "chat",
288
+ requestId: util.uuid(false),
289
+ sessionId: "",
290
+ sessionType: "text_chat",
291
+ parentMsgId: "",
292
+ params: {
293
+ "fileUploadBatchId": util.uuid()
294
+ },
295
+ contents: messagesPrepare(messages),
296
+ })
297
+ );
298
+ req.setEncoding("utf8");
299
+ const streamStartTime = util.timestamp();
300
+ // 接收流为输出文本
301
+ const { convId, imageUrls } = await receiveImages(req);
302
+ session.close();
303
+ logger.success(
304
+ `Stream has completed transfer ${util.timestamp() - streamStartTime}ms`
305
+ );
306
+
307
+ // 异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略
308
+ removeConversation(convId, ticket).catch((err) => console.error(err));
309
+
310
+ return imageUrls;
311
+ })().catch((err) => {
312
+ session && session.close();
313
+ if (retryCount < MAX_RETRY_COUNT) {
314
+ logger.error(`Stream response error: ${err.message}`);
315
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
316
+ return (async () => {
317
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
318
+ return generateImages(model, prompt, ticket, retryCount + 1);
319
+ })();
320
+ }
321
+ throw err;
322
+ });
323
+ }
324
+
325
+ /**
326
+ * 提取消息中引用的文件URL
327
+ *
328
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
329
+ */
330
+ function extractRefFileUrls(messages: any[]) {
331
+ const urls = [];
332
+ // 如果没有消息,则返回[]
333
+ if (!messages.length) {
334
+ return urls;
335
+ }
336
+ // 只获取最新的消息
337
+ const lastMessage = messages[messages.length - 1];
338
+ if (_.isArray(lastMessage.content)) {
339
+ lastMessage.content.forEach((v) => {
340
+ if (!_.isObject(v) || !["file", "image_url"].includes(v["type"])) return;
341
+ // glm-free-api支持格式
342
+ if (
343
+ v["type"] == "file" &&
344
+ _.isObject(v["file_url"]) &&
345
+ _.isString(v["file_url"]["url"])
346
+ )
347
+ urls.push(v["file_url"]["url"]);
348
+ // 兼容gpt-4-vision-preview API格式
349
+ else if (
350
+ v["type"] == "image_url" &&
351
+ _.isObject(v["image_url"]) &&
352
+ _.isString(v["image_url"]["url"])
353
+ )
354
+ urls.push(v["image_url"]["url"]);
355
+ });
356
+ }
357
+ logger.info("本次请求上传:" + urls.length + "个文件");
358
+ return urls;
359
+ }
360
+
361
+ /**
362
+ * 消息预处理
363
+ *
364
+ * 由于接口只取第一条消息,此处会将多条消息合并为一条,实现多轮对话效果
365
+ * user:旧消息1
366
+ * assistant:旧消息2
367
+ * user:新消息
368
+ *
369
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
370
+ * @param refs 参考文件列表
371
+ * @param isRefConv 是否为引用会话
372
+ */
373
+ function messagesPrepare(messages: any[], refs: any[] = [], isRefConv = false) {
374
+ let content;
375
+ if (isRefConv || messages.length < 2) {
376
+ content = messages.reduce((content, message) => {
377
+ if (_.isArray(message.content)) {
378
+ return (
379
+ message.content.reduce((_content, v) => {
380
+ if (!_.isObject(v) || v["type"] != "text") return _content;
381
+ return _content + (v["text"] || "") + "\n";
382
+ }, content)
383
+ );
384
+ }
385
+ return content + `${message.content}\n`;
386
+ }, "");
387
+ logger.info("\n透传内容:\n" + content);
388
+ }
389
+ else {
390
+ content = messages.reduce((content, message) => {
391
+ if (_.isArray(message.content)) {
392
+ return message.content.reduce((_content, v) => {
393
+ if (!_.isObject(v) || v["type"] != "text") return _content;
394
+ return _content + `<|im_start|>${message.role || "user"}\n${v["text"] || ""}<|im_end|>\n`;
395
+ }, content);
396
+ }
397
+ return (content += `<|im_start|>${message.role || "user"}\n${
398
+ message.content
399
+ }<|im_end|>\n`);
400
+ }, "").replace(/\!\[.*\]\(.+\)/g, "");
401
+ logger.info("\n对话合并:\n" + content);
402
+ }
403
+ return [
404
+ {
405
+ content,
406
+ contentType: "text",
407
+ role: "user",
408
+ },
409
+ ...refs
410
+ ];
411
+ }
412
+
413
+ /**
414
+ * 检查请求结果
415
+ *
416
+ * @param result 结果
417
+ */
418
+ function checkResult(result: AxiosResponse) {
419
+ if (!result.data) return null;
420
+ const { success, errorCode, errorMsg } = result.data;
421
+ if (!_.isBoolean(success) || success) return result.data;
422
+ throw new APIException(
423
+ EX.API_REQUEST_FAILED,
424
+ `[请求qwen失败]: ${errorCode}-${errorMsg}`
425
+ );
426
+ }
427
+
428
+ /**
429
+ * 从流接收完整的消息内容
430
+ *
431
+ * @param stream 消息流
432
+ */
433
+ async function receiveStream(stream: any): Promise<any> {
434
+ return new Promise((resolve, reject) => {
435
+ // 消息初始化
436
+ const data = {
437
+ id: "",
438
+ model: MODEL_NAME,
439
+ object: "chat.completion",
440
+ choices: [
441
+ {
442
+ index: 0,
443
+ message: { role: "assistant", content: "" },
444
+ finish_reason: "stop",
445
+ },
446
+ ],
447
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
448
+ created: util.unixTimestamp(),
449
+ };
450
+ const parser = createParser((event) => {
451
+ try {
452
+ if (event.type !== "event") return;
453
+ if (event.data == "[DONE]") return;
454
+ // 解析JSON
455
+ const result = _.attempt(() => JSON.parse(event.data));
456
+ if (_.isError(result))
457
+ throw new Error(`Stream response invalid: ${event.data}`);
458
+ if (!data.id && result.sessionId && result.msgId)
459
+ data.id = `${result.sessionId}-${result.msgId}`;
460
+ const text = (result.contents || []).reduce((str, part) => {
461
+ const { contentType, role, content } = part;
462
+ if (contentType != "text" && contentType != "text2image") return str;
463
+ if (role != "assistant" && !_.isString(content)) return str;
464
+ return str + content;
465
+ }, "");
466
+ const exceptCharIndex = text.indexOf("�");
467
+ let chunk = text.substring(
468
+ exceptCharIndex != -1
469
+ ? Math.min(data.choices[0].message.content.length, exceptCharIndex)
470
+ : data.choices[0].message.content.length,
471
+ exceptCharIndex == -1 ? text.length : exceptCharIndex
472
+ );
473
+ if (chunk && result.contentType == "text2image") {
474
+ chunk = chunk.replace(
475
+ /https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi,
476
+ (url) => {
477
+ const urlObj = new URL(url);
478
+ urlObj.search = "";
479
+ return urlObj.toString();
480
+ }
481
+ );
482
+ }
483
+ if (result.msgStatus != "finished") {
484
+ if (result.contentType == "text")
485
+ data.choices[0].message.content += chunk;
486
+ } else {
487
+ data.choices[0].message.content += chunk;
488
+ if (!result.canShare)
489
+ data.choices[0].message.content +=
490
+ "\n[内容由于不合规被停止生成,我们换个话题吧]";
491
+ if (result.errorCode)
492
+ data.choices[0].message.content += `服务暂时不可用,第三方响应错误:${result.errorCode}`;
493
+ resolve(data);
494
+ }
495
+ } catch (err) {
496
+ logger.error(err);
497
+ reject(err);
498
+ }
499
+ });
500
+ // 将流数据喂给SSE转换器
501
+ stream.on("data", (buffer) => parser.feed(buffer.toString()));
502
+ stream.once("error", (err) => reject(err));
503
+ stream.once("close", () => resolve(data));
504
+ stream.end();
505
+ });
506
+ }
507
+
508
+ /**
509
+ * 创建转换流
510
+ *
511
+ * 将流格式转换为gpt兼容流格式
512
+ *
513
+ * @param stream 消息流
514
+ * @param endCallback 传输结束回调
515
+ */
516
+ function createTransStream(stream: any, endCallback?: Function) {
517
+ // 消息创建时间
518
+ const created = util.unixTimestamp();
519
+ // 创建转换流
520
+ const transStream = new PassThrough();
521
+ let content = "";
522
+ !transStream.closed &&
523
+ transStream.write(
524
+ `data: ${JSON.stringify({
525
+ id: "",
526
+ model: MODEL_NAME,
527
+ object: "chat.completion.chunk",
528
+ choices: [
529
+ {
530
+ index: 0,
531
+ delta: { role: "assistant", content: "" },
532
+ finish_reason: null,
533
+ },
534
+ ],
535
+ created,
536
+ })}\n\n`
537
+ );
538
+ const parser = createParser((event) => {
539
+ try {
540
+ if (event.type !== "event") return;
541
+ if (event.data == "[DONE]") return;
542
+ // 解析JSON
543
+ const result = _.attempt(() => JSON.parse(event.data));
544
+ if (_.isError(result))
545
+ throw new Error(`Stream response invalid: ${event.data}`);
546
+ const text = (result.contents || []).reduce((str, part) => {
547
+ const { contentType, role, content } = part;
548
+ if (contentType != "text" && contentType != "text2image") return str;
549
+ if (role != "assistant" && !_.isString(content)) return str;
550
+ return str + content;
551
+ }, "");
552
+ const exceptCharIndex = text.indexOf("�");
553
+ let chunk = text.substring(
554
+ exceptCharIndex != -1
555
+ ? Math.min(content.length, exceptCharIndex)
556
+ : content.length,
557
+ exceptCharIndex == -1 ? text.length : exceptCharIndex
558
+ );
559
+ if (chunk && result.contentType == "text2image") {
560
+ chunk = chunk.replace(
561
+ /https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi,
562
+ (url) => {
563
+ const urlObj = new URL(url);
564
+ urlObj.search = "";
565
+ return urlObj.toString();
566
+ }
567
+ );
568
+ }
569
+ if (result.msgStatus != "finished") {
570
+ if (chunk && result.contentType == "text") {
571
+ content += chunk;
572
+ const data = `data: ${JSON.stringify({
573
+ id: `${result.sessionId}-${result.msgId}`,
574
+ model: MODEL_NAME,
575
+ object: "chat.completion.chunk",
576
+ choices: [
577
+ { index: 0, delta: { content: chunk }, finish_reason: null },
578
+ ],
579
+ created,
580
+ })}\n\n`;
581
+ !transStream.closed && transStream.write(data);
582
+ }
583
+ } else {
584
+ const delta = { content: chunk || "" };
585
+ if (!result.canShare)
586
+ delta.content += "\n[内容由于不合规被停止生成,我们换个话题吧]";
587
+ if (result.errorCode)
588
+ delta.content += `服务暂时不可用,第三方响应错误:${result.errorCode}`;
589
+ const data = `data: ${JSON.stringify({
590
+ id: `${result.sessionId}-${result.msgId}`,
591
+ model: MODEL_NAME,
592
+ object: "chat.completion.chunk",
593
+ choices: [
594
+ {
595
+ index: 0,
596
+ delta,
597
+ finish_reason: "stop",
598
+ },
599
+ ],
600
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
601
+ created,
602
+ })}\n\n`;
603
+ !transStream.closed && transStream.write(data);
604
+ !transStream.closed && transStream.end("data: [DONE]\n\n");
605
+ content = "";
606
+ endCallback && endCallback(result.sessionId);
607
+ }
608
+ // else
609
+ // logger.warn(result.event, result);
610
+ } catch (err) {
611
+ logger.error(err);
612
+ !transStream.closed && transStream.end("\n\n");
613
+ }
614
+ });
615
+ // 将流数据喂给SSE转换器
616
+ stream.on("data", (buffer) => parser.feed(buffer.toString()));
617
+ stream.once(
618
+ "error",
619
+ () => !transStream.closed && transStream.end("data: [DONE]\n\n")
620
+ );
621
+ stream.once(
622
+ "close",
623
+ () => !transStream.closed && transStream.end("data: [DONE]\n\n")
624
+ );
625
+ stream.end();
626
+ return transStream;
627
+ }
628
+
629
+ /**
630
+ * 从流接收图像
631
+ *
632
+ * @param stream 消息流
633
+ */
634
+ async function receiveImages(
635
+ stream: any
636
+ ): Promise<{ convId: string; imageUrls: string[] }> {
637
+ return new Promise((resolve, reject) => {
638
+ let convId = "";
639
+ const imageUrls = [];
640
+ const parser = createParser((event) => {
641
+ try {
642
+ if (event.type !== "event") return;
643
+ if (event.data == "[DONE]") return;
644
+ // 解析JSON
645
+ const result = _.attempt(() => JSON.parse(event.data));
646
+ if (_.isError(result))
647
+ throw new Error(`Stream response invalid: ${event.data}`);
648
+ if (!convId && result.sessionId) convId = result.sessionId;
649
+ const text = (result.contents || []).reduce((str, part) => {
650
+ const { role, content } = part;
651
+ if (role != "assistant" && !_.isString(content)) return str;
652
+ return str + content;
653
+ }, "");
654
+ if (result.contentFrom == "text2image") {
655
+ const urls =
656
+ text.match(
657
+ /https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi
658
+ ) || [];
659
+ urls.forEach((url) => {
660
+ const urlObj = new URL(url);
661
+ urlObj.search = "";
662
+ const imageUrl = urlObj.toString();
663
+ if (imageUrls.indexOf(imageUrl) != -1) return;
664
+ imageUrls.push(imageUrl);
665
+ });
666
+ }
667
+ if (result.msgStatus == "finished") {
668
+ if (!result.canShare || imageUrls.length == 0)
669
+ throw new APIException(EX.API_CONTENT_FILTERED);
670
+ if (result.errorCode)
671
+ throw new APIException(
672
+ EX.API_REQUEST_FAILED,
673
+ `服务暂时不可用,第三方响应错误:${result.errorCode}`
674
+ );
675
+ }
676
+ } catch (err) {
677
+ logger.error(err);
678
+ reject(err);
679
+ }
680
+ });
681
+ // 将流数据喂给SSE转换器
682
+ stream.on("data", (buffer) => parser.feed(buffer.toString()));
683
+ stream.once("error", (err) => reject(err));
684
+ stream.once("close", () => resolve({ convId, imageUrls }));
685
+ stream.end();
686
+ });
687
+ }
688
+
689
+ /**
690
+ * 获取上传参数
691
+ *
692
+ * @param ticket tongyi_sso_ticket或login_aliyunid_ticket
693
+ */
694
+ async function acquireUploadParams(ticket: string) {
695
+ const result = await axios.post(
696
+ "https://qianwen.biz.aliyun.com/dialog/uploadToken",
697
+ {},
698
+ {
699
+ timeout: 15000,
700
+ headers: {
701
+ Cookie: generateCookie(ticket),
702
+ ...FAKE_HEADERS,
703
+ },
704
+ validateStatus: () => true,
705
+ }
706
+ );
707
+ const { data } = checkResult(result);
708
+ return data;
709
+ }
710
+
711
+ /**
712
+ * 预检查文件URL有效性
713
+ *
714
+ * @param fileUrl 文件URL
715
+ */
716
+ async function checkFileUrl(fileUrl: string) {
717
+ if (util.isBASE64Data(fileUrl)) return;
718
+ const result = await axios.head(fileUrl, {
719
+ timeout: 15000,
720
+ validateStatus: () => true,
721
+ });
722
+ if (result.status >= 400)
723
+ throw new APIException(
724
+ EX.API_FILE_URL_INVALID,
725
+ `File ${fileUrl} is not valid: [${result.status}] ${result.statusText}`
726
+ );
727
+ // 检查文件大小
728
+ if (result.headers && result.headers["content-length"]) {
729
+ const fileSize = parseInt(result.headers["content-length"], 10);
730
+ if (fileSize > FILE_MAX_SIZE)
731
+ throw new APIException(
732
+ EX.API_FILE_EXECEEDS_SIZE,
733
+ `File ${fileUrl} is not valid`
734
+ );
735
+ }
736
+ }
737
+
738
+ /**
739
+ * 上传文件
740
+ *
741
+ * @param fileUrl 文件URL
742
+ * @param ticket tongyi_sso_ticket或login_aliyunid_ticket
743
+ */
744
+ async function uploadFile(fileUrl: string, ticket: string) {
745
+ // 预检查远程文件URL可用性
746
+ await checkFileUrl(fileUrl);
747
+
748
+ let filename, fileData, mimeType;
749
+ // 如果是BASE64数据则直接转换为Buffer
750
+ if (util.isBASE64Data(fileUrl)) {
751
+ mimeType = util.extractBASE64DataFormat(fileUrl);
752
+ const ext = mime.getExtension(mimeType);
753
+ filename = `${util.uuid()}.${ext}`;
754
+ fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), "base64");
755
+ }
756
+ // 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存
757
+ else {
758
+ filename = path.basename(fileUrl);
759
+ ({ data: fileData } = await axios.get(fileUrl, {
760
+ responseType: "arraybuffer",
761
+ // 100M限制
762
+ maxContentLength: FILE_MAX_SIZE,
763
+ // 60秒超时
764
+ timeout: 60000,
765
+ }));
766
+ }
767
+
768
+ // 获取文件的MIME类型
769
+ mimeType = mimeType || mime.getType(filename);
770
+
771
+ // 获取上传参数
772
+ const { accessId, policy, signature, dir } = await acquireUploadParams(
773
+ ticket
774
+ );
775
+
776
+ const formData = new FormData();
777
+ formData.append("OSSAccessKeyId", accessId);
778
+ formData.append("policy", policy);
779
+ formData.append("signature", signature);
780
+ formData.append("key", `${dir}${filename}`);
781
+ formData.append("dir", dir);
782
+ formData.append("success_action_status", "200");
783
+ formData.append("file", fileData, {
784
+ filename,
785
+ contentType: mimeType,
786
+ });
787
+
788
+ // 上传文件到OSS
789
+ await axios.request({
790
+ method: "POST",
791
+ url: "https://broadscope-dialogue.oss-cn-beijing.aliyuncs.com/",
792
+ data: formData,
793
+ // 100M限制
794
+ maxBodyLength: FILE_MAX_SIZE,
795
+ // 60秒超时
796
+ timeout: 120000,
797
+ headers: {
798
+ ...FAKE_HEADERS,
799
+ "X-Requested-With": "XMLHttpRequest"
800
+ }
801
+ });
802
+
803
+ const isImage = [
804
+ 'image/jpeg',
805
+ 'image/jpg',
806
+ 'image/tiff',
807
+ 'image/png',
808
+ 'image/bmp',
809
+ 'image/gif',
810
+ 'image/svg+xml',
811
+ 'image/webp',
812
+ 'image/ico',
813
+ 'image/heic',
814
+ 'image/heif',
815
+ 'image/bmp',
816
+ 'image/x-icon',
817
+ 'image/vnd.microsoft.icon',
818
+ 'image/x-png'
819
+ ].includes(mimeType);
820
+
821
+ if(isImage) {
822
+ const result = await axios.post(
823
+ "https://qianwen.biz.aliyun.com/dialog/downloadLink",
824
+ {
825
+ fileKey: filename,
826
+ fileType: "image",
827
+ dir
828
+ },
829
+ {
830
+ timeout: 15000,
831
+ headers: {
832
+ Cookie: generateCookie(ticket),
833
+ ...FAKE_HEADERS,
834
+ },
835
+ validateStatus: () => true,
836
+ }
837
+ );
838
+ const { data } = checkResult(result);
839
+ return {
840
+ role: "user",
841
+ contentType: "image",
842
+ content: data.url
843
+ };
844
+ }
845
+ else {
846
+ let result = await axios.post(
847
+ "https://qianwen.biz.aliyun.com/dialog/downloadLink/batch",
848
+ {
849
+ fileKeys: [filename],
850
+ fileType: "file",
851
+ dir
852
+ },
853
+ {
854
+ timeout: 15000,
855
+ headers: {
856
+ Cookie: generateCookie(ticket),
857
+ ...FAKE_HEADERS,
858
+ },
859
+ validateStatus: () => true,
860
+ }
861
+ );
862
+ const { data } = checkResult(result);
863
+ if(!data.results[0] || !data.results[0].url)
864
+ throw new Error(`文件上传失败:${data.results[0] ? data.results[0].errorMsg : '未知错误'}`);
865
+ const url = data.results[0].url;
866
+ const startTime = util.timestamp();
867
+ while(true) {
868
+ result = await axios.post(
869
+ "https://qianwen.biz.aliyun.com/dialog/secResult/batch",
870
+ {
871
+ urls: [url]
872
+ },
873
+ {
874
+ timeout: 15000,
875
+ headers: {
876
+ Cookie: generateCookie(ticket),
877
+ ...FAKE_HEADERS,
878
+ },
879
+ validateStatus: () => true,
880
+ }
881
+ );
882
+ const { data } = checkResult(result);
883
+ if(data.pollEndFlag) {
884
+ if(data.statusList[0] && data.statusList[0].status === 0)
885
+ throw new Error(`文件处理失败:${data.statusList[0].errorMsg || '未知错误'}`);
886
+ break;
887
+ }
888
+ if(util.timestamp() > startTime + 120000)
889
+ throw new Error("文件处理超时:超出120秒");
890
+ }
891
+ return {
892
+ role: "user",
893
+ contentType: "file",
894
+ content: url,
895
+ ext: { fileSize: fileData.byteLength }
896
+ };
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Token切分
902
+ *
903
+ * @param authorization 认证字符串
904
+ */
905
+ function tokenSplit(authorization: string) {
906
+ return authorization.replace("Bearer ", "").split(",");
907
+ }
908
+
909
+ /**
910
+ * 生成Cookies
911
+ *
912
+ * @param ticket tongyi_sso_ticket或login_aliyunid_ticket
913
+ */
914
+ function generateCookie(ticket: string) {
915
+ return [
916
+ `${ticket.length > 100 ? 'login_aliyunid_ticket' : 'tongyi_sso_ticket'}=${ticket}`,
917
+ 'aliyun_choice=intl',
918
+ "_samesite_flag_=true",
919
+ `t=${util.uuid(false)}`,
920
+ // `login_aliyunid_csrf=_csrf_tk_${util.generateRandomString({ charset: 'numeric', length: 15 })}`,
921
+ // `cookie2=${util.uuid(false)}`,
922
+ // `munb=22${util.generateRandomString({ charset: 'numeric', length: 11 })}`,
923
+ // `csg=`,
924
+ // `_tb_token_=${util.generateRandomString({ length: 10, capitalization: 'lowercase' })}`,
925
+ // `cna=`,
926
+ // `cnaui=`,
927
+ // `atpsida=`,
928
+ // `isg=`,
929
+ // `tfstk=`,
930
+ // `aui=`,
931
+ // `sca=`
932
+ ].join("; ");
933
+ }
934
+
935
+ /**
936
+ * 获取Token存活状态
937
+ */
938
+ async function getTokenLiveStatus(ticket: string) {
939
+ const result = await axios.post(
940
+ "https://qianwen.biz.aliyun.com/dialog/session/list",
941
+ {},
942
+ {
943
+ headers: {
944
+ Cookie: generateCookie(ticket),
945
+ ...FAKE_HEADERS,
946
+ },
947
+ timeout: 15000,
948
+ validateStatus: () => true,
949
+ }
950
+ );
951
+ try {
952
+ const { data } = checkResult(result);
953
+ return _.isArray(data);
954
+ }
955
+ catch(err) {
956
+ return false;
957
+ }
958
+ }
959
+
960
+ export default {
961
+ createCompletion,
962
+ createCompletionStream,
963
+ generateImages,
964
+ getTokenLiveStatus,
965
+ tokenSplit,
966
+ };
src/api/routes/chat.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from "lodash";
2
+
3
+ import Request from "@/lib/request/Request.ts";
4
+ import Response from "@/lib/response/Response.ts";
5
+ import chat from "@/api/controllers/chat.ts";
6
+
7
+ export default {
8
+ prefix: "/hf/v1/chat",
9
+
10
+ post: {
11
+ "/completions": async (request: Request) => {
12
+ request
13
+ .validate('body.conversation_id', v => _.isUndefined(v) || _.isString(v))
14
+ .validate("body.messages", _.isArray)
15
+ .validate("headers.authorization", _.isString);
16
+ // ticket切分
17
+ const tokens = chat.tokenSplit(request.headers.authorization);
18
+ // 随机挑选一个ticket
19
+ const token = _.sample(tokens);
20
+ const { model, conversation_id: convId, messages, stream } = request.body;
21
+ if (stream) {
22
+ const stream = await chat.createCompletionStream(
23
+ model,
24
+ messages,
25
+ token,
26
+ convId
27
+ );
28
+ return new Response(stream, {
29
+ type: "text/event-stream",
30
+ });
31
+ } else
32
+ return await chat.createCompletion(
33
+ model,
34
+ messages,
35
+ token,
36
+ convId
37
+ );
38
+ },
39
+ },
40
+ };
src/api/routes/images.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from "lodash";
2
+
3
+ import Request from "@/lib/request/Request.ts";
4
+ import chat from "@/api/controllers/chat.ts";
5
+ import util from "@/lib/util.ts";
6
+
7
+ export default {
8
+ prefix: "/hf/v1/images",
9
+
10
+ post: {
11
+ "/generations": async (request: Request) => {
12
+ request
13
+ .validate("body.prompt", _.isString)
14
+ .validate("headers.authorization", _.isString);
15
+ // refresh_token切分
16
+ const tokens = chat.tokenSplit(request.headers.authorization);
17
+ // 随机挑选一个refresh_token
18
+ const token = _.sample(tokens);
19
+ const prompt = request.body.prompt;
20
+ const responseFormat = _.defaultTo(request.body.response_format, "url");
21
+ const model = request.body.model;
22
+ const imageUrls = await chat.generateImages(model, prompt, token);
23
+ let data = [];
24
+ if (responseFormat == "b64_json") {
25
+ data = (
26
+ await Promise.all(imageUrls.map((url) => util.fetchFileBASE64(url)))
27
+ ).map((b64) => ({ b64_json: b64 }));
28
+ } else {
29
+ data = imageUrls.map((url) => ({
30
+ url,
31
+ }));
32
+ }
33
+ return {
34
+ created: util.unixTimestamp(),
35
+ data,
36
+ };
37
+ },
38
+ },
39
+ };
src/api/routes/index.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs-extra';
2
+
3
+ import Response from '@/lib/response/Response.ts';
4
+ import chat from "./chat.ts";
5
+ import images from "./images.ts";
6
+ import ping from "./ping.ts";
7
+ import token from './token.ts';
8
+ import models from './models.ts';
9
+
10
+ export default [
11
+ {
12
+ get: {
13
+ '/': async () => {
14
+ const content = await fs.readFile('public/welcome.html');
15
+ return new Response(content, {
16
+ type: 'html',
17
+ headers: {
18
+ Expires: '-1'
19
+ }
20
+ });
21
+ }
22
+ }
23
+ },
24
+ chat,
25
+ images,
26
+ ping,
27
+ token,
28
+ models
29
+ ];
src/api/routes/models.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ export default {
4
+
5
+ prefix: '/hf/v1',
6
+
7
+ get: {
8
+ '/models': async () => {
9
+ return {
10
+ "data": [
11
+ {
12
+ "id": "qwen-max",
13
+ "object": "model",
14
+ "owned_by": "qwen-free-api"
15
+ },
16
+ {
17
+ "id": "qwen-max-longcontext",
18
+ "object": "model",
19
+ "owned_by": "qwen-free-api"
20
+ },
21
+ {
22
+ "id": "qwen-plus",
23
+ "object": "model",
24
+ "owned_by": "qwen-free-api"
25
+ },
26
+ {
27
+ "id": "qwen-turbo",
28
+ "object": "model",
29
+ "owned_by": "qwen-free-api"
30
+ },
31
+ {
32
+ "id": "qwen-vl-max",
33
+ "object": "model",
34
+ "owned_by": "qwen-free-api"
35
+ },
36
+ {
37
+ "id": "qwen-vl-plus",
38
+ "object": "model",
39
+ "owned_by": "qwen-free-api"
40
+ },
41
+ {
42
+ "id": "qwen-v1",
43
+ "object": "model",
44
+ "owned_by": "qwen-free-api"
45
+ },
46
+ {
47
+ "id": "qwen-v1-vision",
48
+ "object": "model",
49
+ "owned_by": "qwen-free-api"
50
+ }
51
+ ]
52
+ };
53
+ }
54
+
55
+ }
56
+ }
src/api/routes/ping.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ prefix: "/hf/ping",
3
+ get: {
4
+ "": async () => "pong",
5
+ },
6
+ };
src/api/routes/token.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Request from '@/lib/request/Request.ts';
4
+ import Response from '@/lib/response/Response.ts';
5
+ import chat from '@/api/controllers/chat.ts';
6
+ import logger from '@/lib/logger.ts';
7
+
8
+ export default {
9
+
10
+ prefix: '/hf/token',
11
+
12
+ post: {
13
+
14
+ '/check': async (request: Request) => {
15
+ request
16
+ .validate('body.token', _.isString)
17
+ const live = await chat.getTokenLiveStatus(request.body.token);
18
+ return {
19
+ live
20
+ }
21
+ }
22
+
23
+ }
24
+
25
+ }
src/daemon.ts ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 守护进程
3
+ */
4
+
5
+ import process from 'process';
6
+ import path from 'path';
7
+ import { spawn } from 'child_process';
8
+
9
+ import fs from 'fs-extra';
10
+ import { format as dateFormat } from 'date-fns';
11
+ import 'colors';
12
+
13
+ const CRASH_RESTART_LIMIT = 600; //进程崩溃重启次数限制
14
+ const CRASH_RESTART_DELAY = 5000; //进程崩溃重启延迟
15
+ const LOG_PATH = path.resolve("./logs/daemon.log"); //守护进程日志路径
16
+ let crashCount = 0; //进程崩溃次数
17
+ let currentProcess; //当前运行进程
18
+
19
+ /**
20
+ * 写入守护进程日志
21
+ */
22
+ function daemonLog(value, color?: string) {
23
+ try {
24
+ const head = `[daemon][${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")}] `;
25
+ value = head + value;
26
+ console.log(color ? value[color] : value);
27
+ fs.ensureDirSync(path.dirname(LOG_PATH));
28
+ fs.appendFileSync(LOG_PATH, value + "\n");
29
+ }
30
+ catch(err) {
31
+ console.error("daemon log write error:", err);
32
+ }
33
+ }
34
+
35
+ daemonLog(`daemon pid: ${process.pid}`);
36
+
37
+ function createProcess() {
38
+ const childProcess = spawn("node", ["index.js", ...process.argv.slice(2)]); //启动子进程
39
+ childProcess.stdout.pipe(process.stdout, { end: false }); //将子进程输出管道到当前进程输出
40
+ childProcess.stderr.pipe(process.stderr, { end: false }); //将子进程错误输出管道到当前进程输出
41
+ currentProcess = childProcess; //更新当前进程
42
+ daemonLog(`process(${childProcess.pid}) has started`);
43
+ childProcess.on("error", err => daemonLog(`process(${childProcess.pid}) error: ${err.stack}`, "red"));
44
+ childProcess.on("close", code => {
45
+ if(code === 0) //进程正常退出
46
+ daemonLog(`process(${childProcess.pid}) has exited`);
47
+ else if(code === 2) //进程已被杀死
48
+ daemonLog(`process(${childProcess.pid}) has been killed!`, "bgYellow");
49
+ else if(code === 3) { //进程主动重启
50
+ daemonLog(`process(${childProcess.pid}) has restart`, "yellow");
51
+ createProcess(); //重新创建进程
52
+ }
53
+ else { //进程发生崩溃
54
+ if(crashCount++ < CRASH_RESTART_LIMIT) { //进程崩溃次数未达重启次数上限前尝试重启
55
+ daemonLog(`process(${childProcess.pid}) has crashed! delay ${CRASH_RESTART_DELAY}ms try restarting...(${crashCount})`, "bgRed");
56
+ setTimeout(() => createProcess(), CRASH_RESTART_DELAY); //延迟指定时长后再重启
57
+ }
58
+ else //进程已崩溃,且无法重启
59
+ daemonLog(`process(${childProcess.pid}) has crashed! unable to restart`, "bgRed");
60
+ }
61
+ }); //子进程关闭监听
62
+ }
63
+
64
+ process.on("exit", code => {
65
+ if(code === 0)
66
+ daemonLog("daemon process exited");
67
+ else if(code === 2)
68
+ daemonLog("daemon process has been killed!");
69
+ }); //守护进程退出事件
70
+
71
+ process.on("SIGTERM", () => {
72
+ daemonLog("received kill signal", "yellow");
73
+ currentProcess && currentProcess.kill("SIGINT");
74
+ process.exit(2);
75
+ }); //kill退出守护进程
76
+
77
+ process.on("SIGINT", () => {
78
+ currentProcess && currentProcess.kill("SIGINT");
79
+ process.exit(0);
80
+ }); //主动退出守护进程
81
+
82
+ createProcess(); //创建进程
src/index.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ import environment from "@/lib/environment.ts";
4
+ import config from "@/lib/config.ts";
5
+ import "@/lib/initialize.ts";
6
+ import server from "@/lib/server.ts";
7
+ import routes from "@/api/routes/index.ts";
8
+ import logger from "@/lib/logger.ts";
9
+
10
+ const startupTime = performance.now();
11
+
12
+ (async () => {
13
+ logger.header();
14
+
15
+ logger.info("<<<< qwen free server >>>>");
16
+ logger.info("Version:", environment.package.version);
17
+ logger.info("Process id:", process.pid);
18
+ logger.info("Environment:", environment.env);
19
+ logger.info("Service name:", config.service.name);
20
+
21
+ server.attachRoutes(routes);
22
+ await server.listen();
23
+
24
+ config.service.bindAddress &&
25
+ logger.success("Service bind address:", config.service.bindAddress);
26
+ })()
27
+ .then(() =>
28
+ logger.success(
29
+ `Service startup completed (${Math.floor(performance.now() - startupTime)}ms)`
30
+ )
31
+ )
32
+ .catch((err) => console.error(err));
src/lib/config.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import serviceConfig from "./configs/service-config.ts";
2
+ import systemConfig from "./configs/system-config.ts";
3
+
4
+ class Config {
5
+
6
+ /** 服务配置 */
7
+ service = serviceConfig;
8
+
9
+ /** 系统配置 */
10
+ system = systemConfig;
11
+
12
+ }
13
+
14
+ export default new Config();
src/lib/configs/service-config.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+
3
+ import fs from 'fs-extra';
4
+ import yaml from 'yaml';
5
+ import _ from 'lodash';
6
+
7
+ import environment from '../environment.ts';
8
+ import util from '../util.ts';
9
+
10
+ const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/service.yml");
11
+
12
+ /**
13
+ * 服务配置
14
+ */
15
+ export class ServiceConfig {
16
+
17
+ /** 服务名称 */
18
+ name: string;
19
+ /** @type {string} 服务绑定主机地址 */
20
+ host;
21
+ /** @type {number} 服务绑定端口 */
22
+ port;
23
+ /** @type {string} 服务路由前缀 */
24
+ urlPrefix;
25
+ /** @type {string} 服务绑定地址(外部访问地址) */
26
+ bindAddress;
27
+
28
+ constructor(options?: any) {
29
+ const { name, host, port, urlPrefix, bindAddress } = options || {};
30
+ this.name = _.defaultTo(name, 'qwen-free-api');
31
+ this.host = _.defaultTo(host, '0.0.0.0');
32
+ this.port = _.defaultTo(port, 5566);
33
+ this.urlPrefix = _.defaultTo(urlPrefix, '');
34
+ this.bindAddress = bindAddress;
35
+ }
36
+
37
+ get addressHost() {
38
+ if(this.bindAddress) return this.bindAddress;
39
+ const ipAddresses = util.getIPAddressesByIPv4();
40
+ for(let ipAddress of ipAddresses) {
41
+ if(ipAddress === this.host)
42
+ return ipAddress;
43
+ }
44
+ return ipAddresses[0] || "127.0.0.1";
45
+ }
46
+
47
+ get address() {
48
+ return `${this.addressHost}:${this.port}`;
49
+ }
50
+
51
+ get pageDirUrl() {
52
+ return `http://127.0.0.1:${this.port}/page`;
53
+ }
54
+
55
+ get publicDirUrl() {
56
+ return `http://127.0.0.1:${this.port}/public`;
57
+ }
58
+
59
+ static load() {
60
+ const external = _.pickBy(environment, (v, k) => ["name", "host", "port"].includes(k) && !_.isUndefined(v));
61
+ if(!fs.pathExistsSync(CONFIG_PATH)) return new ServiceConfig(external);
62
+ const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
63
+ return new ServiceConfig({ ...data, ...external });
64
+ }
65
+
66
+ }
67
+
68
+ export default ServiceConfig.load();
src/lib/configs/system-config.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+
3
+ import fs from 'fs-extra';
4
+ import yaml from 'yaml';
5
+ import _ from 'lodash';
6
+
7
+ import environment from '../environment.ts';
8
+
9
+ const CONFIG_PATH = path.join(path.resolve(), 'configs/', environment.env, "/system.yml");
10
+
11
+ /**
12
+ * 系统配置
13
+ */
14
+ export class SystemConfig {
15
+
16
+ /** 是否开启请求日志 */
17
+ requestLog: boolean;
18
+ /** 临时目录路径 */
19
+ tmpDir: string;
20
+ /** 日志目录路径 */
21
+ logDir: string;
22
+ /** 日志写入间隔(毫秒) */
23
+ logWriteInterval: number;
24
+ /** 日志文件有效期(毫秒) */
25
+ logFileExpires: number;
26
+ /** 公共目录路径 */
27
+ publicDir: string;
28
+ /** 临时文件有效期(毫秒) */
29
+ tmpFileExpires: number;
30
+ /** 请求体配置 */
31
+ requestBody: any;
32
+ /** 是否调试模式 */
33
+ debug: boolean;
34
+
35
+ constructor(options?: any) {
36
+ const { requestLog, tmpDir, logDir, logWriteInterval, logFileExpires, publicDir, tmpFileExpires, requestBody, debug } = options || {};
37
+ this.requestLog = _.defaultTo(requestLog, false);
38
+ this.tmpDir = _.defaultTo(tmpDir, './tmp');
39
+ this.logDir = _.defaultTo(logDir, './logs');
40
+ this.logWriteInterval = _.defaultTo(logWriteInterval, 200);
41
+ this.logFileExpires = _.defaultTo(logFileExpires, 2626560000);
42
+ this.publicDir = _.defaultTo(publicDir, './public');
43
+ this.tmpFileExpires = _.defaultTo(tmpFileExpires, 86400000);
44
+ this.requestBody = Object.assign(requestBody || {}, {
45
+ enableTypes: ['json', 'form', 'text', 'xml'],
46
+ encoding: 'utf-8',
47
+ formLimit: '100mb',
48
+ jsonLimit: '100mb',
49
+ textLimit: '100mb',
50
+ xmlLimit: '100mb',
51
+ formidable: {
52
+ maxFileSize: '100mb'
53
+ },
54
+ multipart: true,
55
+ parsedMethods: ['POST', 'PUT', 'PATCH']
56
+ });
57
+ this.debug = _.defaultTo(debug, true);
58
+ }
59
+
60
+ get rootDirPath() {
61
+ return path.resolve();
62
+ }
63
+
64
+ get tmpDirPath() {
65
+ return path.resolve(this.tmpDir);
66
+ }
67
+
68
+ get logDirPath() {
69
+ return path.resolve(this.logDir);
70
+ }
71
+
72
+ get publicDirPath() {
73
+ return path.resolve(this.publicDir);
74
+ }
75
+
76
+ static load() {
77
+ if (!fs.pathExistsSync(CONFIG_PATH)) return new SystemConfig();
78
+ const data = yaml.parse(fs.readFileSync(CONFIG_PATH).toString());
79
+ return new SystemConfig(data);
80
+ }
81
+
82
+ }
83
+
84
+ export default SystemConfig.load();
src/lib/consts/exceptions.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export default {
2
+ SYSTEM_ERROR: [-1000, '系统异常'],
3
+ SYSTEM_REQUEST_VALIDATION_ERROR: [-1001, '请求参数校验错误'],
4
+ SYSTEM_NOT_ROUTE_MATCHING: [-1002, '无匹配的路由']
5
+ } as Record<string, [number, string]>
src/lib/environment.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+
3
+ import fs from 'fs-extra';
4
+ import minimist from 'minimist';
5
+ import _ from 'lodash';
6
+
7
+ const cmdArgs = minimist(process.argv.slice(2)); //获取命令行参数
8
+ const envVars = process.env; //获取环境变量
9
+
10
+ class Environment {
11
+
12
+ /** 命令行参数 */
13
+ cmdArgs: any;
14
+ /** 环境变量 */
15
+ envVars: any;
16
+ /** 环境名称 */
17
+ env?: string;
18
+ /** 服务名称 */
19
+ name?: string;
20
+ /** 服务地址 */
21
+ host?: string;
22
+ /** 服务端口 */
23
+ port?: number;
24
+ /** 包参数 */
25
+ package: any;
26
+
27
+ constructor(options: any = {}) {
28
+ const { cmdArgs, envVars, package: _package } = options;
29
+ this.cmdArgs = cmdArgs;
30
+ this.envVars = envVars;
31
+ this.env = _.defaultTo(cmdArgs.env || envVars.SERVER_ENV, 'dev');
32
+ this.name = cmdArgs.name || envVars.SERVER_NAME || undefined;
33
+ this.host = cmdArgs.host || envVars.SERVER_HOST || undefined;
34
+ this.port = Number(cmdArgs.port || envVars.SERVER_PORT) ? Number(cmdArgs.port || envVars.SERVER_PORT) : undefined;
35
+ this.package = _package;
36
+ }
37
+
38
+ }
39
+
40
+ export default new Environment({
41
+ cmdArgs,
42
+ envVars,
43
+ package: JSON.parse(fs.readFileSync(path.join(path.resolve(), "package.json")).toString())
44
+ });
src/lib/exceptions/APIException.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Exception from './Exception.js';
2
+
3
+ export default class APIException extends Exception {
4
+
5
+ /**
6
+ * 构造异常
7
+ *
8
+ * @param {[number, string]} exception 异常
9
+ */
10
+ constructor(exception: (string | number)[], errmsg?: string) {
11
+ super(exception, errmsg);
12
+ }
13
+
14
+ }
src/lib/exceptions/Exception.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'assert';
2
+
3
+ import _ from 'lodash';
4
+
5
+ export default class Exception extends Error {
6
+
7
+ /** 错误码 */
8
+ errcode: number;
9
+ /** 错误消息 */
10
+ errmsg: string;
11
+ /** 数据 */
12
+ data: any;
13
+ /** HTTP状态码 */
14
+ httpStatusCode: number;
15
+
16
+ /**
17
+ * 构造异常
18
+ *
19
+ * @param exception 异常
20
+ * @param _errmsg 异常消息
21
+ */
22
+ constructor(exception: (string | number)[], _errmsg?: string) {
23
+ assert(_.isArray(exception), 'Exception must be Array');
24
+ const [errcode, errmsg] = exception as [number, string];
25
+ assert(_.isFinite(errcode), 'Exception errcode invalid');
26
+ assert(_.isString(errmsg), 'Exception errmsg invalid');
27
+ super(_errmsg || errmsg);
28
+ this.errcode = errcode;
29
+ this.errmsg = _errmsg || errmsg;
30
+ }
31
+
32
+ compare(exception: (string | number)[]) {
33
+ const [errcode] = exception as [number, string];
34
+ return this.errcode == errcode;
35
+ }
36
+
37
+ setHTTPStatusCode(value: number) {
38
+ this.httpStatusCode = value;
39
+ return this;
40
+ }
41
+
42
+ setData(value: any) {
43
+ this.data = _.defaultTo(value, null);
44
+ return this;
45
+ }
46
+
47
+ }
src/lib/http-status-codes.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default {
2
+
3
+ CONTINUE: 100, //客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应
4
+ SWITCHING_PROTOCOLS: 101, //服务器已经理解了客户端的请求,并将通过Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP 版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源
5
+ PROCESSING: 102, //处理将被继续执行
6
+
7
+ OK: 200, //请求已成功,请求所希望的响应头或数据体将随此响应返回
8
+ CREATED: 201, //请求已经被实现,而且有一个新的资源已经依据请求的需要而建立,且其 URI 已经随Location 头信息返回。假如需要的资源无法及时建立的话,应当返回 '202 Accepted'
9
+ ACCEPTED: 202, //服务器已接受请求,但尚未处理。正如它可能被拒绝一样,最终该请求可能会也可能不会被执行。在异步操作的场合下,没有比发送这个状态码更方便的做法了。返回202状态码的响应的目的是允许服务器接受其他过程的请求(例如某个每天只执行一次的基于批处理的操作),而不必让客户端一直保持与服务器的连接直到批处理操作全部完成。在接受请求处理并返回202状态码的响应应当在返回的实体中包含一些指示处理当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便用户能够估计操作是否已经完成
10
+ NON_AUTHORITATIVE_INFO: 203, //服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前的信息可能是原始版本的子集或者超集。例如,包含资源的元数据可能导致原始服务器知道元信息的超级。使用此状态码不是必须的,而且只有在响应不使用此状态码便会返回200 OK的情况下才是合适的
11
+ NO_CONTENT: 204, //服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾
12
+ RESET_CONTENT: 205, //服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束
13
+ PARTIAL_CONTENT: 206, //服务器已经成功处理了部分 GET 请求。类似于FlashGet或者迅雷这类的HTTP下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。该请求必须包含 Range 头信息来指示客户端希望得到的内容范围,并且可能包含 If-Range 来作为请求条件。响应必须包含如下的头部域:Content-Range 用以指示本次响应中返回的内容的范围;如果是Content-Type为multipart/byteranges的多段下载,则每一段multipart中都应包含Content-Range域用以指示本段的内容范围。假如响应中包含Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。Date和ETag或Content-Location,假如同样的请求本应该返回200响应。Expires, Cache-Control,和/或 Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。假如本响应请求使用了 If-Range 强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了 If-Range 弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。假如 ETag 或 Latest-Modified 头部不能精确匹配的话,则客户端缓存应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。任何不支持 Range 以及 Content-Range 头的缓存都禁止缓存206响应返���的内容
14
+ MULTIPLE_STATUS: 207, //代表之后的消息体将是一个XML消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码
15
+
16
+ MULTIPLE_CHOICES: 300, //被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。除非这是一个HEAD请求,否则该响应应当包括一个资源特性及地址的列表的实体,以便用户或浏览器从中选择最合适的重定向地址。这个实体的格式由Content-Type定义的格式所决定。浏览器可能根据响应的格式以及浏览器自身能力,自动作出最合适的选择。当然,RFC 2616规范并没有规定这样的自动选择该如何进行。如果服务器本身已经有了首选的回馈选择,那么在Location中应当指明这个回馈的 URI;浏览器可能会将这个 Location 值作为自动重定向的地址。此外,除非额外指定,否则这个响应也是可缓存的
17
+ MOVED_PERMANENTLY: 301, //被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。新的永久性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,因此浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:对于某些使用 HTTP/1.0 协议的浏览器,当它们发送的POST请求得到了一个301响应的话,接下来的重定向请求将会变成GET方式
18
+ FOUND: 302, //请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI应当在响应的 Location 域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:虽然RFC 1945和RFC 2068规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视作为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应
19
+ SEE_OTHER: 303, //对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用 GET 的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源。这个新的 URI 不是原始资源的替代引用。同时,303响应禁止被缓存。当然,第二个请求(重定向)可能被缓存。新的 URI 应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。注意:许多 HTTP/1.1 版以前的浏览器不能正确理解303状态。如果需要考虑与这些浏览器之间的互动,302状态码应该可以胜任,因为大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的
20
+ NOT_MODIFIED: 304, //如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。该响应必须包含以下的头信息:Date,除非这个服务器没有时钟。假如没有时钟的服务器也遵守这些规则,那么代理服务器以及客户端可以自行将Date字段添加到接收到的响应头中去(正如RFC 2068中规定的一样),缓存机制将会正常工作。ETag或 Content-Location,假如同样的请求本应返回200响应。Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。假如本响应请求使用了强缓存验证,那么本次响应不应该包含其他实体头;否则(例如,某个带条件的 GET 请求使用了弱缓存验证),本次响应禁止包含其他实体头;这避免了缓存了的实体内容和更新了的实体头信息之间的不一致。假如某个304响应指明了当前某个实体没有缓存,那么缓存系统必须忽视这个响应,并且重复发送不包含限制条件的请求。假如接收到一个要求更新某个缓存条目的304响应,那么缓存系统必须更新整个条目以反映所有在响应中被更新的字段的值
21
+ USE_PROXY: 305, //被请求的资源必须通过指定的代理才能被访问。Location域中将给出指定的代理所在的URI信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应资源。只有原始服务器才能建立305响应。注意:RFC 2068中没有明确305响应是为了重定向一个单独的请求,而且只能被原始服务器建立。忽视这些限制可能导致严重的安全后果
22
+ UNUSED: 306, //在最新版的规范中,306状态码已经不再被使用
23
+ TEMPORARY_REDIRECT: 307, //请求的资源现在临时从不同的URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI 应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI 的超链接及简短说明。因为部分浏览器不能识别307响应,因此需要添加上述必要信息以便用户能够理解并向新的 URI 发出访问请求。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化
24
+
25
+ BAD_REQUEST: 400, //1.语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求 2.请求参数有误
26
+ UNAUTHORIZED: 401, //当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。参见RFC 2617
27
+ PAYMENT_REQUIRED: 402, //该状态码是为了将来可能的需求而预留的
28
+ FORBIDDEN: 403, //服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息
29
+ NOT_FOUND: 404, //请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下
30
+ METHOD_NOT_ALLOWED: 405, //请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表。鉴于PUT,DELETE方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误
31
+ NO_ACCEPTABLE: 406, //请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。除非这是一个 HEAD 请求,否则该响应就应当返回一个包含可以让用户或者浏览器从中选择最合适的实体特性以及地址列表的实体。实体的格式由Content-Type头中定义的媒体类型决定。浏览器可以根据格式及自身能力自行作出最佳选择。但是,规范中并没有定义任何作出此类自动选择的标准
32
+ PROXY_AUTHENTICATION_REQUIRED: 407, //与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个Proxy-Authenticate用以进行身份询问。客户端可以返回一个Proxy-Authorization信息头用以验证。参见RFC 2617
33
+ REQUEST_TIMEOUT: 408, //请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改
34
+ CONFLICT: 409, //由于和被请求的资源的当前状态之间存在冲突,请求无法完成。这个代码只允许用在这样的情况下才能被使用:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。冲突通常发生于对PUT请求的处理中。例如,在采用版本检查的环境下,某次PUT提交的对特定资源���修改请求所附带的版本信息与之前的某个(第三方)请求向冲突,那么此时服务器就应该返回一个409错误,告知用户请求无法完成。此时,响应实体中很可能会包含两个冲突版本之间的差异比较,以便用户重新提交归并以后的新版本
35
+ GONE: 410, //被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是否是永久的,那么就应该使用404状态码。除非额外说明,否则这个响应是可缓存的。410响应的目的主要是帮助网站管理员维护网站,通知用户该资源已经不再可用,并且服务器拥有者希望所有指向这个资源的远端连接也被删除。这类事件在限时、增值服务中很普遍。同样,410响应也被用于通知客户端在当前服务器站点上,原本属于某个个人的资源已经不再可用。当然,是否需要把所有永久不可用的资源标记为'410 Gone',以及是否需要保持此标记多长时间,完全取决于服务器拥有者
36
+ LENGTH_REQUIRED: 411, //服务器拒绝在没有定义Content-Length头的情况下接受请求。在添加了表明请求消息体长度的有效Content-Length头之后,客户端可以再次提交该请求
37
+ PRECONDITION_FAILED: 412, //服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上
38
+ REQUEST_ENTITY_TOO_LARGE: 413, //服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。如果这个状况是临时的,服务器应当返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试
39
+ REQUEST_URI_TOO_LONG: 414, //请求的URI长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括:本应使用POST方法的表单提交变成了GET方法,导致查询字符串(Query String)过长。重定向URI “黑洞”,例如每次重定向把旧的URI作为新的URI的一部分,导致在若干次重定向后URI超长。客户端正在尝试利用某些服务器中存在的安全漏洞攻击服务器。这类服务器使用固定长度的缓冲读取或操作请求的URI,当GET后的参数超过某个数值后,可能会产生缓冲区溢出,导致任意代码被执行[1]。没有此类漏洞的服务器,应当返回414状态码
40
+ UNSUPPORTED_MEDIA_TYPE: 415, //对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝
41
+ REQUESTED_RANGE_NOT_SATISFIABLE: 416, //如果请求中包含了Range请求头,并且Range中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义If-Range请求头,那么服务器就应当返回416状态码。假如Range使用的是字节范围,那么这种情况就是指请求指定的所有数据范围的首字节位置都超过了当前资源的长度。服务器也应当在返回416状态码的同时,包含一个Content-Range实体头,用以指明当前资源的长度。这个响应也被禁止使用multipart/byteranges作为其 Content-Type
42
+ EXPECTION_FAILED: 417, //在请求头Expect中指定的预期内容无法被服务器满足,或者这个服务器是一个代理服务器,它有明显的证据证明在当前路由的下一个节点上,Expect的内容无法被满足
43
+ TOO_MANY_CONNECTIONS: 421, //从当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关或者代理服务器地址)。在这种情况下,连接数的计算可能涉及到不止一个终端用户
44
+ UNPROCESSABLE_ENTITY: 422, //请求格式正确,但是由于含有语义错误,无法响应
45
+ FAILED_DEPENDENCY: 424, //由于之前的某个请求发生的错误,导致当前请求失败,例如PROPPATCH
46
+ UNORDERED_COLLECTION: 425, //在WebDav Advanced Collections 草案中定义,但是未出现在《WebDAV 顺序集协议》(RFC 3658)中
47
+ UPGRADE_REQUIRED: 426, //客户端应当切换到TLS/1.0
48
+ RETRY_WITH: 449, //由微软扩展,代表请求应当在执行完适当的操作后进行重试
49
+
50
+ INTERNAL_SERVER_ERROR: 500, //服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器的程序码出错时出现
51
+ NOT_IMPLEMENTED: 501, //服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求
52
+ BAD_GATEWAY: 502, //作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
53
+ SERVICE_UNAVAILABLE: 503, //由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间。如果没有给出这个 Retry-After 信息,那么客户端应当以处理500响应的方式处理它。注意:503状态码的存在并不意味着服务器在过载的时候必须使用它。某些服务器只不过是希望拒绝客户端的连接
54
+ GATEWAY_TIMEOUT: 504, //作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。注意:某些代理服务器在DNS查询超时时会返回400或者500错误
55
+ HTTP_VERSION_NOT_SUPPORTED: 505, //服务器不支持,或者拒绝支持在请求中使用的HTTP版本。这暗示着服务器不能或不愿使用与客户端相同的版本。响应中应当包含一个描述了为何版本不被支持以及服务器支持哪些协议的实体
56
+ VARIANT_ALSO_NEGOTIATES: 506, //服务器存在内部配置错误:被请求的协商变元资源被配置为在透明内容协商中使用自己,因此在一个协商处理中不是一个合适的重点
57
+ INSUFFICIENT_STORAGE: 507, //服务器无法存储完成请求所必须的内容。这个状况被认为是临时的
58
+ BANDWIDTH_LIMIT_EXCEEDED: 509, //服务器达到带宽限制。这不是一个官方的状态码,但是仍被广泛使用
59
+ NOT_EXTENDED: 510 //获取资源所需要的策略并没有没满足
60
+
61
+ };
src/lib/initialize.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logger from './logger.js';
2
+
3
+ // 允许无限量的监听器
4
+ process.setMaxListeners(Infinity);
5
+ // 输出未捕获异常
6
+ process.on("uncaughtException", (err, origin) => {
7
+ logger.error(`An unhandled error occurred: ${origin}`, err);
8
+ });
9
+ // 输出未处理的Promise.reject
10
+ process.on("unhandledRejection", (_, promise) => {
11
+ promise.catch(err => logger.error("An unhandled rejection occurred:", err));
12
+ });
13
+ // 输出系统警告信息
14
+ process.on("warning", warning => logger.warn("System warning: ", warning));
15
+ // 进程退出监听
16
+ process.on("exit", () => {
17
+ logger.info("Service exit");
18
+ logger.footer();
19
+ });
20
+ // 进程被kill
21
+ process.on("SIGTERM", () => {
22
+ logger.warn("received kill signal");
23
+ process.exit(2);
24
+ });
25
+ // Ctrl-C进程退出
26
+ process.on("SIGINT", () => {
27
+ process.exit(0);
28
+ });
src/lib/interfaces/ICompletionMessage.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export default interface ICompletionMessage {
2
+ role: 'system' | 'assistant' | 'user' | 'function';
3
+ content: string;
4
+ }
src/lib/logger.ts ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import _util from 'util';
3
+
4
+ import 'colors';
5
+ import _ from 'lodash';
6
+ import fs from 'fs-extra';
7
+ import { format as dateFormat } from 'date-fns';
8
+
9
+ import config from './config.ts';
10
+ import util from './util.ts';
11
+
12
+ const isVercelEnv = process.env.VERCEL;
13
+
14
+ class LogWriter {
15
+
16
+ #buffers = [];
17
+
18
+ constructor() {
19
+ !isVercelEnv && fs.ensureDirSync(config.system.logDirPath);
20
+ !isVercelEnv && this.work();
21
+ }
22
+
23
+ push(content) {
24
+ const buffer = Buffer.from(content);
25
+ this.#buffers.push(buffer);
26
+ }
27
+
28
+ writeSync(buffer) {
29
+ !isVercelEnv && fs.appendFileSync(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), buffer);
30
+ }
31
+
32
+ async write(buffer) {
33
+ !isVercelEnv && await fs.appendFile(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), buffer);
34
+ }
35
+
36
+ flush() {
37
+ if(!this.#buffers.length) return;
38
+ !isVercelEnv && fs.appendFileSync(path.join(config.system.logDirPath, `/${util.getDateString()}.log`), Buffer.concat(this.#buffers));
39
+ }
40
+
41
+ work() {
42
+ if (!this.#buffers.length) return setTimeout(this.work.bind(this), config.system.logWriteInterval);
43
+ const buffer = Buffer.concat(this.#buffers);
44
+ this.#buffers = [];
45
+ this.write(buffer)
46
+ .finally(() => setTimeout(this.work.bind(this), config.system.logWriteInterval))
47
+ .catch(err => console.error("Log write error:", err));
48
+ }
49
+
50
+ }
51
+
52
+ class LogText {
53
+
54
+ /** @type {string} 日志级别 */
55
+ level;
56
+ /** @type {string} 日志文本 */
57
+ text;
58
+ /** @type {string} 日志来源 */
59
+ source;
60
+ /** @type {Date} 日志发生时间 */
61
+ time = new Date();
62
+
63
+ constructor(level, ...params) {
64
+ this.level = level;
65
+ this.text = _util.format.apply(null, params);
66
+ this.source = this.#getStackTopCodeInfo();
67
+ }
68
+
69
+ #getStackTopCodeInfo() {
70
+ const unknownInfo = { name: "unknown", codeLine: 0, codeColumn: 0 };
71
+ const stackArray = new Error().stack.split("\n");
72
+ const text = stackArray[4];
73
+ if (!text)
74
+ return unknownInfo;
75
+ const match = text.match(/at (.+) \((.+)\)/) || text.match(/at (.+)/);
76
+ if (!match || !_.isString(match[2] || match[1]))
77
+ return unknownInfo;
78
+ const temp = match[2] || match[1];
79
+ const _match = temp.match(/([a-zA-Z0-9_\-\.]+)\:(\d+)\:(\d+)$/);
80
+ if (!_match)
81
+ return unknownInfo;
82
+ const [, scriptPath, codeLine, codeColumn] = _match as any;
83
+ return {
84
+ name: scriptPath ? scriptPath.replace(/.js$/, "") : "unknown",
85
+ path: scriptPath || null,
86
+ codeLine: parseInt(codeLine || 0),
87
+ codeColumn: parseInt(codeColumn || 0)
88
+ };
89
+ }
90
+
91
+ toString() {
92
+ return `[${dateFormat(this.time, "yyyy-MM-dd HH:mm:ss.SSS")}][${this.level}][${this.source.name}<${this.source.codeLine},${this.source.codeColumn}>] ${this.text}`;
93
+ }
94
+
95
+ }
96
+
97
+ class Logger {
98
+
99
+ /** @type {Object} 系统配置 */
100
+ config = {};
101
+ /** @type {Object} 日志级别映射 */
102
+ static Level = {
103
+ Success: "success",
104
+ Info: "info",
105
+ Log: "log",
106
+ Debug: "debug",
107
+ Warning: "warning",
108
+ Error: "error",
109
+ Fatal: "fatal"
110
+ };
111
+ /** @type {Object} 日志级别文本颜色樱色 */
112
+ static LevelColor = {
113
+ [Logger.Level.Success]: "green",
114
+ [Logger.Level.Info]: "brightCyan",
115
+ [Logger.Level.Debug]: "white",
116
+ [Logger.Level.Warning]: "brightYellow",
117
+ [Logger.Level.Error]: "brightRed",
118
+ [Logger.Level.Fatal]: "red"
119
+ };
120
+ #writer;
121
+
122
+ constructor() {
123
+ this.#writer = new LogWriter();
124
+ }
125
+
126
+ header() {
127
+ this.#writer.writeSync(Buffer.from(`\n\n===================== LOG START ${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")} =====================\n\n`));
128
+ }
129
+
130
+ footer() {
131
+ this.#writer.flush(); //将未写入文件的日志缓存写入
132
+ this.#writer.writeSync(Buffer.from(`\n\n===================== LOG END ${dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")} =====================\n\n`));
133
+ }
134
+
135
+ success(...params) {
136
+ const content = new LogText(Logger.Level.Success, ...params).toString();
137
+ console.info(content[Logger.LevelColor[Logger.Level.Success]]);
138
+ this.#writer.push(content + "\n");
139
+ }
140
+
141
+ info(...params) {
142
+ const content = new LogText(Logger.Level.Info, ...params).toString();
143
+ console.info(content[Logger.LevelColor[Logger.Level.Info]]);
144
+ this.#writer.push(content + "\n");
145
+ }
146
+
147
+ log(...params) {
148
+ const content = new LogText(Logger.Level.Log, ...params).toString();
149
+ console.log(content[Logger.LevelColor[Logger.Level.Log]]);
150
+ this.#writer.push(content + "\n");
151
+ }
152
+
153
+ debug(...params) {
154
+ if(!config.system.debug) return; //非调试模式忽略debug
155
+ const content = new LogText(Logger.Level.Debug, ...params).toString();
156
+ console.debug(content[Logger.LevelColor[Logger.Level.Debug]]);
157
+ this.#writer.push(content + "\n");
158
+ }
159
+
160
+ warn(...params) {
161
+ const content = new LogText(Logger.Level.Warning, ...params).toString();
162
+ console.warn(content[Logger.LevelColor[Logger.Level.Warning]]);
163
+ this.#writer.push(content + "\n");
164
+ }
165
+
166
+ error(...params) {
167
+ const content = new LogText(Logger.Level.Error, ...params).toString();
168
+ console.error(content[Logger.LevelColor[Logger.Level.Error]]);
169
+ this.#writer.push(content);
170
+ }
171
+
172
+ fatal(...params) {
173
+ const content = new LogText(Logger.Level.Fatal, ...params).toString();
174
+ console.error(content[Logger.LevelColor[Logger.Level.Fatal]]);
175
+ this.#writer.push(content);
176
+ }
177
+
178
+ destory() {
179
+ this.#writer.destory();
180
+ }
181
+
182
+ }
183
+
184
+ export default new Logger();
src/lib/request/Request.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import APIException from '@/lib/exceptions/APIException.ts';
4
+ import EX from '@/api/consts/exceptions.ts';
5
+ import logger from '@/lib/logger.ts';
6
+ import util from '@/lib/util.ts';
7
+
8
+ export interface RequestOptions {
9
+ time?: number;
10
+ }
11
+
12
+ export default class Request {
13
+
14
+ /** 请求方法 */
15
+ method: string;
16
+ /** 请求URL */
17
+ url: string;
18
+ /** 请求路径 */
19
+ path: string;
20
+ /** 请求载荷类型 */
21
+ type: string;
22
+ /** 请求headers */
23
+ headers: any;
24
+ /** 请求原始查询字符串 */
25
+ search: string;
26
+ /** 请求查询参数 */
27
+ query: any;
28
+ /** 请求URL参数 */
29
+ params: any;
30
+ /** 请求载荷 */
31
+ body: any;
32
+ /** 上传的文件 */
33
+ files: any[];
34
+ /** 客户端IP地址 */
35
+ remoteIP: string | null;
36
+ /** 请求接受时间戳(毫秒) */
37
+ time: number;
38
+
39
+ constructor(ctx, options: RequestOptions = {}) {
40
+ const { time } = options;
41
+ this.method = ctx.request.method;
42
+ this.url = ctx.request.url;
43
+ this.path = ctx.request.path;
44
+ this.type = ctx.request.type;
45
+ this.headers = ctx.request.headers || {};
46
+ this.search = ctx.request.search;
47
+ this.query = ctx.query || {};
48
+ this.params = ctx.params || {};
49
+ this.body = ctx.request.body || {};
50
+ this.files = ctx.request.files || {};
51
+ this.remoteIP = this.headers["X-Real-IP"] || this.headers["x-real-ip"] || this.headers["X-Forwarded-For"] || this.headers["x-forwarded-for"] || ctx.ip || null;
52
+ this.time = Number(_.defaultTo(time, util.timestamp()));
53
+ }
54
+
55
+ validate(key: string, fn?: Function) {
56
+ try {
57
+ const value = _.get(this, key);
58
+ if (fn) {
59
+ if (fn(value) === false)
60
+ throw `[Mismatch] -> ${fn}`;
61
+ }
62
+ else if (_.isUndefined(value))
63
+ throw '[Undefined]';
64
+ }
65
+ catch (err) {
66
+ logger.warn(`Params ${key} invalid:`, err);
67
+ throw new APIException(EX.API_REQUEST_PARAMS_INVALID, `Params ${key} invalid`);
68
+ }
69
+ return this;
70
+ }
71
+
72
+ }
src/lib/response/Body.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ export interface BodyOptions {
4
+ code?: number;
5
+ message?: string;
6
+ data?: any;
7
+ statusCode?: number;
8
+ }
9
+
10
+ export default class Body {
11
+
12
+ /** 状态码 */
13
+ code: number;
14
+ /** 状态消息 */
15
+ message: string;
16
+ /** 载荷 */
17
+ data: any;
18
+ /** HTTP状态码 */
19
+ statusCode: number;
20
+
21
+ constructor(options: BodyOptions = {}) {
22
+ const { code, message, data, statusCode } = options;
23
+ this.code = Number(_.defaultTo(code, 0));
24
+ this.message = _.defaultTo(message, 'OK');
25
+ this.data = _.defaultTo(data, null);
26
+ this.statusCode = Number(_.defaultTo(statusCode, 200));
27
+ }
28
+
29
+ toObject() {
30
+ return {
31
+ code: this.code,
32
+ message: this.message,
33
+ data: this.data
34
+ };
35
+ }
36
+
37
+ static isInstance(value) {
38
+ return value instanceof Body;
39
+ }
40
+
41
+ }
src/lib/response/FailureBody.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Body from './Body.ts';
4
+ import Exception from '../exceptions/Exception.ts';
5
+ import APIException from '../exceptions/APIException.ts';
6
+ import EX from '../consts/exceptions.ts';
7
+ import HTTP_STATUS_CODES from '../http-status-codes.ts';
8
+
9
+ export default class FailureBody extends Body {
10
+
11
+ constructor(error: APIException | Exception | Error, _data?: any) {
12
+ let errcode, errmsg, data = _data, httpStatusCode = HTTP_STATUS_CODES.OK;;
13
+ if(_.isString(error))
14
+ error = new Exception(EX.SYSTEM_ERROR, error);
15
+ else if(error instanceof APIException || error instanceof Exception)
16
+ ({ errcode, errmsg, data, httpStatusCode } = error);
17
+ else if(_.isError(error))
18
+ ({ errcode, errmsg, data, httpStatusCode } = new Exception(EX.SYSTEM_ERROR, error.message));
19
+ super({
20
+ code: errcode || -1,
21
+ message: errmsg || 'Internal error',
22
+ data,
23
+ statusCode: httpStatusCode
24
+ });
25
+ }
26
+
27
+ static isInstance(value) {
28
+ return value instanceof FailureBody;
29
+ }
30
+
31
+ }
src/lib/response/Response.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mime from 'mime';
2
+ import _ from 'lodash';
3
+
4
+ import Body from './Body.ts';
5
+ import util from '../util.ts';
6
+
7
+ export interface ResponseOptions {
8
+ statusCode?: number;
9
+ type?: string;
10
+ headers?: Record<string, any>;
11
+ redirect?: string;
12
+ body?: any;
13
+ size?: number;
14
+ time?: number;
15
+ }
16
+
17
+ export default class Response {
18
+
19
+ /** 响应HTTP状态码 */
20
+ statusCode: number;
21
+ /** 响应内容类型 */
22
+ type: string;
23
+ /** 响应headers */
24
+ headers: Record<string, any>;
25
+ /** 重定向目标 */
26
+ redirect: string;
27
+ /** 响应载荷 */
28
+ body: any;
29
+ /** 响应载荷大小 */
30
+ size: number;
31
+ /** 响应时间戳 */
32
+ time: number;
33
+
34
+ constructor(body: any, options: ResponseOptions = {}) {
35
+ const { statusCode, type, headers, redirect, size, time } = options;
36
+ this.statusCode = Number(_.defaultTo(statusCode, Body.isInstance(body) ? body.statusCode : undefined))
37
+ this.type = type;
38
+ this.headers = headers;
39
+ this.redirect = redirect;
40
+ this.size = size;
41
+ this.time = Number(_.defaultTo(time, util.timestamp()));
42
+ this.body = body;
43
+ }
44
+
45
+ injectTo(ctx) {
46
+ this.redirect && ctx.redirect(this.redirect);
47
+ this.statusCode && (ctx.status = this.statusCode);
48
+ this.type && (ctx.type = mime.getType(this.type) || this.type);
49
+ const headers = this.headers || {};
50
+ if(this.size && !headers["Content-Length"] && !headers["content-length"])
51
+ headers["Content-Length"] = this.size;
52
+ ctx.set(headers);
53
+ if(Body.isInstance(this.body))
54
+ ctx.body = this.body.toObject();
55
+ else
56
+ ctx.body = this.body;
57
+ }
58
+
59
+ static isInstance(value) {
60
+ return value instanceof Response;
61
+ }
62
+
63
+ }
src/lib/response/SuccessfulBody.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Body from './Body.ts';
4
+
5
+ export default class SuccessfulBody extends Body {
6
+
7
+ constructor(data: any, message?: string) {
8
+ super({
9
+ code: 0,
10
+ message: _.defaultTo(message, "OK"),
11
+ data
12
+ });
13
+ }
14
+
15
+ static isInstance(value) {
16
+ return value instanceof SuccessfulBody;
17
+ }
18
+
19
+ }
src/lib/server.ts ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Koa from 'koa';
2
+ import KoaRouter from 'koa-router';
3
+ import koaRange from 'koa-range';
4
+ import koaCors from "koa2-cors";
5
+ import koaBody from 'koa-body';
6
+ import _ from 'lodash';
7
+
8
+ import Exception from './exceptions/Exception.ts';
9
+ import Request from './request/Request.ts';
10
+ import Response from './response/Response.js';
11
+ import FailureBody from './response/FailureBody.ts';
12
+ import EX from './consts/exceptions.ts';
13
+ import logger from './logger.ts';
14
+ import config from './config.ts';
15
+
16
+ class Server {
17
+
18
+ app;
19
+ router;
20
+
21
+ constructor() {
22
+ this.app = new Koa();
23
+ this.app.use(koaCors());
24
+ // 范围请求支持
25
+ this.app.use(koaRange);
26
+ this.router = new KoaRouter({ prefix: config.service.urlPrefix });
27
+ // 前置处理异常拦截
28
+ this.app.use(async (ctx: any, next: Function) => {
29
+ if(ctx.request.type === "application/xml" || ctx.request.type === "application/ssml+xml")
30
+ ctx.req.headers["content-type"] = "text/xml";
31
+ try { await next() }
32
+ catch (err) {
33
+ logger.error(err);
34
+ const failureBody = new FailureBody(err);
35
+ new Response(failureBody).injectTo(ctx);
36
+ }
37
+ });
38
+ // 载荷解析器支持
39
+ this.app.use(koaBody(_.clone(config.system.requestBody)));
40
+ this.app.on("error", (err: any) => {
41
+ // 忽略连接重试、中断、管道、取消错误
42
+ if (["ECONNRESET", "ECONNABORTED", "EPIPE", "ECANCELED"].includes(err.code)) return;
43
+ logger.error(err);
44
+ });
45
+ logger.success("Server initialized");
46
+ }
47
+
48
+ /**
49
+ * 附加路由
50
+ *
51
+ * @param routes 路由列表
52
+ */
53
+ attachRoutes(routes: any[]) {
54
+ routes.forEach((route: any) => {
55
+ const prefix = route.prefix || "";
56
+ for (let method in route) {
57
+ if(method === "prefix") continue;
58
+ if (!_.isObject(route[method])) {
59
+ logger.warn(`Router ${prefix} ${method} invalid`);
60
+ continue;
61
+ }
62
+ for (let uri in route[method]) {
63
+ this.router[method](`${prefix}${uri}`, async ctx => {
64
+ const { request, response } = await this.#requestProcessing(ctx, route[method][uri]);
65
+ if(response != null && config.system.requestLog)
66
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
67
+ });
68
+ }
69
+ }
70
+ logger.info(`Route ${config.service.urlPrefix || ""}${prefix} attached`);
71
+ });
72
+ this.app.use(this.router.routes());
73
+ this.app.use((ctx: any) => {
74
+ const request = new Request(ctx);
75
+ logger.debug(`-> ${ctx.request.method} ${ctx.request.url} request is not supported - ${request.remoteIP || "unknown"}`);
76
+ // const failureBody = new FailureBody(new Exception(EX.SYSTEM_NOT_ROUTE_MATCHING, "Request is not supported"));
77
+ // const response = new Response(failureBody);
78
+ const message = `[请求有误]: 正确请求为 POST -> /v1/chat/completions,当前请求为 ${ctx.request.method} -> ${ctx.request.url} 请纠正`;
79
+ logger.warn(message);
80
+ const failureBody = new FailureBody(new Error(message));
81
+ const response = new Response(failureBody);
82
+ response.injectTo(ctx);
83
+ if(config.system.requestLog)
84
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
85
+ });
86
+ }
87
+
88
+ /**
89
+ * 请求处理
90
+ *
91
+ * @param ctx 上下文
92
+ * @param routeFn 路由方法
93
+ */
94
+ #requestProcessing(ctx: any, routeFn: Function): Promise<any> {
95
+ return new Promise(resolve => {
96
+ const request = new Request(ctx);
97
+ try {
98
+ if(config.system.requestLog)
99
+ logger.info(`-> ${request.method} ${request.url}`);
100
+ routeFn(request)
101
+ .then(response => {
102
+ try {
103
+ if(!Response.isInstance(response)) {
104
+ const _response = new Response(response);
105
+ _response.injectTo(ctx);
106
+ return resolve({ request, response: _response });
107
+ }
108
+ response.injectTo(ctx);
109
+ resolve({ request, response });
110
+ }
111
+ catch(err) {
112
+ logger.error(err);
113
+ const failureBody = new FailureBody(err);
114
+ const response = new Response(failureBody);
115
+ response.injectTo(ctx);
116
+ resolve({ request, response });
117
+ }
118
+ })
119
+ .catch(err => {
120
+ try {
121
+ logger.error(err);
122
+ const failureBody = new FailureBody(err);
123
+ const response = new Response(failureBody);
124
+ response.injectTo(ctx);
125
+ resolve({ request, response });
126
+ }
127
+ catch(err) {
128
+ logger.error(err);
129
+ const failureBody = new FailureBody(err);
130
+ const response = new Response(failureBody);
131
+ response.injectTo(ctx);
132
+ resolve({ request, response });
133
+ }
134
+ });
135
+ }
136
+ catch(err) {
137
+ logger.error(err);
138
+ const failureBody = new FailureBody(err);
139
+ const response = new Response(failureBody);
140
+ response.injectTo(ctx);
141
+ resolve({ request, response });
142
+ }
143
+ });
144
+ }
145
+
146
+ /**
147
+ * 监听端口
148
+ */
149
+ async listen() {
150
+ const host = config.service.host;
151
+ const port = config.service.port;
152
+ await Promise.all([
153
+ new Promise((resolve, reject) => {
154
+ if(host === "0.0.0.0" || host === "localhost" || host === "127.0.0.1")
155
+ return resolve(null);
156
+ this.app.listen(port, "localhost", err => {
157
+ if(err) return reject(err);
158
+ resolve(null);
159
+ });
160
+ }),
161
+ new Promise((resolve, reject) => {
162
+ this.app.listen(port, host, err => {
163
+ if(err) return reject(err);
164
+ resolve(null);
165
+ });
166
+ })
167
+ ]);
168
+ logger.success(`Server listening on port ${port} (${host})`);
169
+ }
170
+
171
+ }
172
+
173
+ export default new Server();
src/lib/util.ts ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os from "os";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import { Readable, Writable } from "stream";
5
+
6
+ import "colors";
7
+ import mime from "mime";
8
+ import fs from "fs-extra";
9
+ import { v1 as uuid } from "uuid";
10
+ import { format as dateFormat } from "date-fns";
11
+ import CRC32 from "crc-32";
12
+ import randomstring from "randomstring";
13
+ import _ from "lodash";
14
+ import { CronJob } from "cron";
15
+
16
+ import HTTP_STATUS_CODE from "./http-status-codes.ts";
17
+ import axios from "axios";
18
+
19
+ const autoIdMap = new Map();
20
+
21
+ const util = {
22
+ is2DArrays(value: any) {
23
+ return (
24
+ _.isArray(value) &&
25
+ (!value[0] || (_.isArray(value[0]) && _.isArray(value[value.length - 1])))
26
+ );
27
+ },
28
+
29
+ uuid: (separator = true) => (separator ? uuid() : uuid().replace(/\-/g, "")),
30
+
31
+ autoId: (prefix = "") => {
32
+ let index = autoIdMap.get(prefix);
33
+ if (index > 999999) index = 0; //超过最大数字则重置为0
34
+ autoIdMap.set(prefix, (index || 0) + 1);
35
+ return `${prefix}${index || 1}`;
36
+ },
37
+
38
+ ignoreJSONParse(value: string) {
39
+ const result = _.attempt(() => JSON.parse(value));
40
+ if (_.isError(result)) return null;
41
+ return result;
42
+ },
43
+
44
+ generateRandomString(options: any): string {
45
+ return randomstring.generate(options);
46
+ },
47
+
48
+ getResponseContentType(value: any): string | null {
49
+ return value.headers
50
+ ? value.headers["content-type"] || value.headers["Content-Type"]
51
+ : null;
52
+ },
53
+
54
+ mimeToExtension(value: string) {
55
+ let extension = mime.getExtension(value);
56
+ if (extension == "mpga") return "mp3";
57
+ return extension;
58
+ },
59
+
60
+ extractURLExtension(value: string) {
61
+ const extname = path.extname(new URL(value).pathname);
62
+ return extname.substring(1).toLowerCase();
63
+ },
64
+
65
+ createCronJob(cronPatterns: any, callback?: Function) {
66
+ if (!_.isFunction(callback))
67
+ throw new Error("callback must be an Function");
68
+ return new CronJob(
69
+ cronPatterns,
70
+ () => callback(),
71
+ null,
72
+ false,
73
+ "Asia/Shanghai"
74
+ );
75
+ },
76
+
77
+ getDateString(format = "yyyy-MM-dd", date = new Date()) {
78
+ return dateFormat(date, format);
79
+ },
80
+
81
+ getIPAddressesByIPv4(): string[] {
82
+ const interfaces = os.networkInterfaces();
83
+ const addresses = [];
84
+ for (let name in interfaces) {
85
+ const networks = interfaces[name];
86
+ const results = networks.filter(
87
+ (network) =>
88
+ network.family === "IPv4" &&
89
+ network.address !== "127.0.0.1" &&
90
+ !network.internal
91
+ );
92
+ if (results[0] && results[0].address) addresses.push(results[0].address);
93
+ }
94
+ return addresses;
95
+ },
96
+
97
+ getMACAddressesByIPv4(): string[] {
98
+ const interfaces = os.networkInterfaces();
99
+ const addresses = [];
100
+ for (let name in interfaces) {
101
+ const networks = interfaces[name];
102
+ const results = networks.filter(
103
+ (network) =>
104
+ network.family === "IPv4" &&
105
+ network.address !== "127.0.0.1" &&
106
+ !network.internal
107
+ );
108
+ if (results[0] && results[0].mac) addresses.push(results[0].mac);
109
+ }
110
+ return addresses;
111
+ },
112
+
113
+ generateSSEData(event?: string, data?: string, retry?: number) {
114
+ return `event: ${event || "message"}\ndata: ${(data || "")
115
+ .replace(/\n/g, "\\n")
116
+ .replace(/\s/g, "\\s")}\nretry: ${retry || 3000}\n\n`;
117
+ },
118
+
119
+ buildDataBASE64(type, ext, buffer) {
120
+ return `data:${type}/${ext.replace("jpg", "jpeg")};base64,${buffer.toString(
121
+ "base64"
122
+ )}`;
123
+ },
124
+
125
+ isLinux() {
126
+ return os.platform() !== "win32";
127
+ },
128
+
129
+ isIPAddress(value) {
130
+ return (
131
+ _.isString(value) &&
132
+ (/^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test(
133
+ value
134
+ ) ||
135
+ /\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*/.test(
136
+ value
137
+ ))
138
+ );
139
+ },
140
+
141
+ isPort(value) {
142
+ return _.isNumber(value) && value > 0 && value < 65536;
143
+ },
144
+
145
+ isReadStream(value): boolean {
146
+ return (
147
+ value &&
148
+ (value instanceof Readable || "readable" in value || value.readable)
149
+ );
150
+ },
151
+
152
+ isWriteStream(value): boolean {
153
+ return (
154
+ value &&
155
+ (value instanceof Writable || "writable" in value || value.writable)
156
+ );
157
+ },
158
+
159
+ isHttpStatusCode(value) {
160
+ return _.isNumber(value) && Object.values(HTTP_STATUS_CODE).includes(value);
161
+ },
162
+
163
+ isURL(value) {
164
+ return !_.isUndefined(value) && /^(http|https)/.test(value);
165
+ },
166
+
167
+ isSrc(value) {
168
+ return !_.isUndefined(value) && /^\/.+\.[0-9a-zA-Z]+(\?.+)?$/.test(value);
169
+ },
170
+
171
+ isBASE64(value) {
172
+ return !_.isUndefined(value) && /^[a-zA-Z0-9\/\+]+(=?)+$/.test(value);
173
+ },
174
+
175
+ isBASE64Data(value) {
176
+ return /^data:/.test(value);
177
+ },
178
+
179
+ extractBASE64DataFormat(value): string | null {
180
+ const match = value.trim().match(/^data:(.+);base64,/);
181
+ if (!match) return null;
182
+ return match[1];
183
+ },
184
+
185
+ removeBASE64DataHeader(value): string {
186
+ return value.replace(/^data:(.+);base64,/, "");
187
+ },
188
+
189
+ isDataString(value): boolean {
190
+ return /^(base64|json):/.test(value);
191
+ },
192
+
193
+ isStringNumber(value) {
194
+ return _.isFinite(Number(value));
195
+ },
196
+
197
+ isUnixTimestamp(value) {
198
+ return /^[0-9]{10}$/.test(`${value}`);
199
+ },
200
+
201
+ isTimestamp(value) {
202
+ return /^[0-9]{13}$/.test(`${value}`);
203
+ },
204
+
205
+ isEmail(value) {
206
+ return /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/.test(
207
+ value
208
+ );
209
+ },
210
+
211
+ isAsyncFunction(value) {
212
+ return Object.prototype.toString.call(value) === "[object AsyncFunction]";
213
+ },
214
+
215
+ async isAPNG(filePath) {
216
+ let head;
217
+ const readStream = fs.createReadStream(filePath, { start: 37, end: 40 });
218
+ const readPromise = new Promise((resolve, reject) => {
219
+ readStream.once("end", resolve);
220
+ readStream.once("error", reject);
221
+ });
222
+ readStream.once("data", (data) => (head = data));
223
+ await readPromise;
224
+ return head.compare(Buffer.from([0x61, 0x63, 0x54, 0x4c])) === 0;
225
+ },
226
+
227
+ unixTimestamp() {
228
+ return parseInt(`${Date.now() / 1000}`);
229
+ },
230
+
231
+ timestamp() {
232
+ return Date.now();
233
+ },
234
+
235
+ urlJoin(...values) {
236
+ let url = "";
237
+ for (let i = 0; i < values.length; i++)
238
+ url += `${i > 0 ? "/" : ""}${values[i]
239
+ .replace(/^\/*/, "")
240
+ .replace(/\/*$/, "")}`;
241
+ return url;
242
+ },
243
+
244
+ millisecondsToHmss(milliseconds) {
245
+ if (_.isString(milliseconds)) return milliseconds;
246
+ milliseconds = parseInt(milliseconds);
247
+ const sec = Math.floor(milliseconds / 1000);
248
+ const hours = Math.floor(sec / 3600);
249
+ const minutes = Math.floor((sec - hours * 3600) / 60);
250
+ const seconds = sec - hours * 3600 - minutes * 60;
251
+ const ms = (milliseconds % 60000) - seconds * 1000;
252
+ return `${hours > 9 ? hours : "0" + hours}:${
253
+ minutes > 9 ? minutes : "0" + minutes
254
+ }:${seconds > 9 ? seconds : "0" + seconds}.${ms}`;
255
+ },
256
+
257
+ millisecondsToTimeString(milliseconds) {
258
+ if (milliseconds < 1000) return `${milliseconds}ms`;
259
+ if (milliseconds < 60000)
260
+ return `${parseFloat((milliseconds / 1000).toFixed(2))}s`;
261
+ return `${Math.floor(milliseconds / 1000 / 60)}m${Math.floor(
262
+ (milliseconds / 1000) % 60
263
+ )}s`;
264
+ },
265
+
266
+ rgbToHex(r, g, b): string {
267
+ return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
268
+ },
269
+
270
+ hexToRgb(hex) {
271
+ const value = parseInt(hex.replace(/^#/, ""), 16);
272
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
273
+ },
274
+
275
+ md5(value) {
276
+ return crypto.createHash("md5").update(value).digest("hex");
277
+ },
278
+
279
+ crc32(value) {
280
+ return _.isBuffer(value) ? CRC32.buf(value) : CRC32.str(value);
281
+ },
282
+
283
+ arrayParse(value): any[] {
284
+ return _.isArray(value) ? value : [value];
285
+ },
286
+
287
+ booleanParse(value) {
288
+ return value === "true" || value === true ? true : false;
289
+ },
290
+
291
+ encodeBASE64(value) {
292
+ return Buffer.from(value).toString("base64");
293
+ },
294
+
295
+ decodeBASE64(value) {
296
+ return Buffer.from(value, "base64").toString();
297
+ },
298
+
299
+ async fetchFileBASE64(url: string) {
300
+ const result = await axios.get(url, {
301
+ responseType: "arraybuffer",
302
+ });
303
+ return result.data.toString("base64");
304
+ },
305
+ };
306
+
307
+ export default util;
tsconfig.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "allowImportingTsExtensions": true,
7
+ "allowSyntheticDefaultImports": true,
8
+ "noEmit": true,
9
+ "paths": {
10
+ "@/*": ["src/*"]
11
+ },
12
+ "outDir": "./dist"
13
+ },
14
+ "include": ["src/**/*", "libs.d.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
vercel.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "builds": [
3
+ {
4
+ "src": "./dist/*.html",
5
+ "use": "@vercel/static"
6
+ },
7
+ {
8
+ "src": "./dist/index.js",
9
+ "use": "@vercel/node"
10
+ }
11
+ ],
12
+ "routes": [
13
+ {
14
+ "src": "/",
15
+ "dest": "/dist/welcome.html"
16
+ },
17
+ {
18
+ "src": "/(.*)",
19
+ "dest": "/dist",
20
+ "headers": {
21
+ "Access-Control-Allow-Credentials": "true",
22
+ "Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
23
+ "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, Authorization"
24
+ }
25
+ }
26
+ ]
27
+ }