Spaces:
Running
Running
ffreemt
commited on
Commit
•
f25d8c5
1
Parent(s):
9a2365b
Update from redteam repo
Browse files- Dockerfile +21 -0
- Dockerfile-- +21 -0
- LICENSE +674 -0
- README.md +2 -1
- README.md-- +513 -0
- configs/dev/service.yml +6 -0
- configs/dev/system.yml +14 -0
- doc/example-0.png +0 -0
- doc/example-1.png +0 -0
- doc/example-2.png +0 -0
- doc/example-3.png +0 -0
- doc/example-4.png +0 -0
- doc/example-5.png +0 -0
- doc/example-6.png +0 -0
- doc/example-7.png +0 -0
- libs.d.ts +0 -0
- package.json +50 -0
- public/welcome.html +10 -0
- src/api/consts/exceptions.ts +11 -0
- src/api/controllers/chat.ts +966 -0
- src/api/routes/chat.ts +40 -0
- src/api/routes/images.ts +39 -0
- src/api/routes/index.ts +29 -0
- src/api/routes/models.ts +56 -0
- src/api/routes/ping.ts +6 -0
- src/api/routes/token.ts +25 -0
- src/daemon.ts +82 -0
- src/index.ts +32 -0
- src/lib/config.ts +14 -0
- src/lib/configs/service-config.ts +68 -0
- src/lib/configs/system-config.ts +84 -0
- src/lib/consts/exceptions.ts +5 -0
- src/lib/environment.ts +44 -0
- src/lib/exceptions/APIException.ts +14 -0
- src/lib/exceptions/Exception.ts +47 -0
- src/lib/http-status-codes.ts +61 -0
- src/lib/initialize.ts +28 -0
- src/lib/interfaces/ICompletionMessage.ts +4 -0
- src/lib/logger.ts +184 -0
- src/lib/request/Request.ts +72 -0
- src/lib/response/Body.ts +41 -0
- src/lib/response/FailureBody.ts +31 -0
- src/lib/response/Response.ts +63 -0
- src/lib/response/SuccessfulBody.ts +19 -0
- src/lib/server.ts +173 -0
- src/lib/util.ts +307 -0
- tsconfig.json +16 -0
- vercel.json +27 -0
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:
|
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 |
+
}
|