m19921414377 commited on
Commit
c8c6dbf
·
verified ·
1 Parent(s): c7e505b

Upload folder using huggingface_hub

Browse files
Files changed (50) hide show
  1. .dockerignore +9 -0
  2. .gitattributes +4 -35
  3. .gitignore +15 -0
  4. Dockerfile +21 -0
  5. LICENSE +674 -0
  6. README.md +11 -6
  7. configs/dev/service.yml +6 -0
  8. configs/dev/system.yml +14 -0
  9. doc/example-0.png +3 -0
  10. doc/example-1.jpeg +3 -0
  11. docs/JIMENG-4.1-4.5.md +170 -0
  12. libs.d.ts +0 -0
  13. package-lock.json +0 -0
  14. package.json +52 -0
  15. public/welcome.html +10 -0
  16. src/api/consts/exceptions.ts +13 -0
  17. src/api/controllers/chat.ts +499 -0
  18. src/api/controllers/core.ts +437 -0
  19. src/api/controllers/images.ts +1476 -0
  20. src/api/controllers/videos.ts +1039 -0
  21. src/api/routes/chat.ts +36 -0
  22. src/api/routes/images.ts +199 -0
  23. src/api/routes/index.ts +31 -0
  24. src/api/routes/models.ts +105 -0
  25. src/api/routes/ping.ts +6 -0
  26. src/api/routes/token.ts +39 -0
  27. src/api/routes/videos.ts +111 -0
  28. src/daemon.ts +82 -0
  29. src/index.ts +32 -0
  30. src/lib/config.ts +14 -0
  31. src/lib/configs/model-config.ts +322 -0
  32. src/lib/configs/service-config.ts +68 -0
  33. src/lib/configs/system-config.ts +84 -0
  34. src/lib/consts/exceptions.ts +5 -0
  35. src/lib/environment.ts +44 -0
  36. src/lib/exceptions/APIException.ts +14 -0
  37. src/lib/exceptions/Exception.ts +47 -0
  38. src/lib/http-status-codes.ts +61 -0
  39. src/lib/initialize.ts +28 -0
  40. src/lib/interfaces/ICompletionMessage.ts +4 -0
  41. src/lib/logger.ts +184 -0
  42. src/lib/request/Request.ts +72 -0
  43. src/lib/response/Body.ts +41 -0
  44. src/lib/response/FailureBody.ts +31 -0
  45. src/lib/response/Response.ts +63 -0
  46. src/lib/response/SuccessfulBody.ts +19 -0
  47. src/lib/server.ts +223 -0
  48. src/lib/util.ts +307 -0
  49. tsconfig.json +16 -0
  50. vercel.json +27 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ logs
2
+ dist
3
+ doc
4
+ node_modules
5
+ .vscode
6
+ .git
7
+ .gitignore
8
+ README.md
9
+ *.tar.gz
.gitattributes CHANGED
@@ -1,35 +1,4 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+ doc/example-0.png filter=lfs diff=lfs merge=lfs -text
4
+ doc/example-1.jpeg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dist/
2
+ node_modules/
3
+ logs/
4
+ .vercel
5
+ .ipynb_checkpoints/文生视频首位帧-checkpoint.txt
6
+ 文生视频首位帧.txt
7
+ yarn.lock
8
+ jimeng-free-api-all.tar
9
+ yarn.lock
10
+ .claude/settings.local.json
11
+ .ipynb_checkpoints/Dockerfile-checkpoint
12
+ .ipynb_checkpoints/README-checkpoint.md
13
+ .ipynb_checkpoints/package-checkpoint.json
14
+ .ipynb_checkpoints/tsconfig-checkpoint.json
15
+ src/api/controllers/.ipynb_checkpoints/images-checkpoint.ts
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/ --ignore-engines && 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
@@ -1,10 +1,15 @@
1
  ---
2
- title: Jimeng Api
3
- emoji:
4
- colorFrom: pink
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
+ title: "jimeng-api"
3
+ emoji: "🚀"
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 8000
8
  ---
9
 
10
+ ### 🚀 一键部署
11
+ [![Deploy with HFSpaceDeploy](https://img.shields.io/badge/Deploy_with-HFSpaceDeploy-green?style=social&logo=rocket)](https://github.com/kfcx/HFSpaceDeploy)
12
+
13
+ 本项目由[HFSpaceDeploy](https://github.com/kfcx/HFSpaceDeploy)一键部署
14
+
15
+
configs/dev/service.yml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # 服务名称
2
+ name: jimeng-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

Git LFS Details

  • SHA256: a3f80fec0ed607802d7a1c91a1c0592d77952fa39841749391415b4ba2064568
  • Pointer size: 131 Bytes
  • Size of remote file: 459 kB
doc/example-1.jpeg ADDED

Git LFS Details

  • SHA256: f581d9c180ba7501081f11c759ee65f8ee35b375e81ea90e5de0d39ec67bbbf1
  • Pointer size: 131 Bytes
  • Size of remote file: 315 kB
docs/JIMENG-4.1-4.5.md ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 即梦 AI 4.1 和 4.5 模型支持
2
+
3
+ 本文档介绍了 jimeng-free-api-all 项目对即梦 AI 新增的 4.1 和 4.5 模型的支持。
4
+
5
+ ## 新增模型
6
+
7
+ ### jimeng-4.5
8
+ - **内部模型名**: `high_aes_general_v45`
9
+ - **版本**: 3.2.9
10
+ - **特性**:
11
+ - 支持文生图(Text-to-Image)
12
+ - 支持图生图(Image-to-Image)
13
+ - 支持多图连续生成
14
+ - 最高支持 2048x2048 分辨率
15
+
16
+ ### jimeng-4.1
17
+ - **内部模型名**: `high_aes_general_v41`
18
+ - **版本**: 3.2.9
19
+ - **特性**:
20
+ - 支持文生图(Text-to-Image)
21
+ - 支持图生图(Image-to-Image)
22
+ - 支持多图连续生成
23
+ - 最高支持 2048x2048 分辨率
24
+
25
+ ## 与旧版本的差异
26
+
27
+ | 特性 | jimeng-4.5/4.1 | jimeng-4.0 | jimeng-3.1 |
28
+ |------|----------------|------------|------------|
29
+ | Draft版本 | 3.2.9 | 3.0.2 | 3.0.2 |
30
+ | 多图生成 | ✅ | ✅ | ❌ |
31
+ | 最大分辨率 | 2048x2048 | 2048x2048 | 1024x1024 |
32
+ | 采样强度范围 | 0.1-1.0 | 0.1-1.0 | 0.1-0.8 |
33
+
34
+ ## API 使用
35
+
36
+ ### 1. 文生图
37
+
38
+ ```bash
39
+ curl -X POST http://localhost:8000/v1/images/generations \
40
+ -H "Content-Type: application/json" \
41
+ -H "Authorization: Bearer YOUR_SESSION_ID" \
42
+ -d '{
43
+ "model": "jimeng-4.5",
44
+ "prompt": "一只可爱的小猫在花园里玩耍",
45
+ "width": 1024,
46
+ "height": 1024,
47
+ "sample_strength": 0.7
48
+ }'
49
+ ```
50
+
51
+ ### 2. 多图连续生成
52
+
53
+ ```bash
54
+ curl -X POST http://localhost:8000/v1/images/generations \
55
+ -H "Content-Type: application/json" \
56
+ -H "Authorization: Bearer YOUR_SESSION_ID" \
57
+ -d '{
58
+ "model": "jimeng-4.5",
59
+ "prompt": "生成4张连续场景的图片:春夏秋冬四季风景",
60
+ "width": 1024,
61
+ "height": 1024,
62
+ "sample_strength": 0.6
63
+ }'
64
+ ```
65
+
66
+ ### 3. 图生图
67
+
68
+ ```bash
69
+ curl -X POST http://localhost:8000/v1/images/compositions \
70
+ -H "Content-Type: application/json" \
71
+ -H "Authorization: Bearer YOUR_SESSION_ID" \
72
+ -d '{
73
+ "model": "jimeng-4.1",
74
+ "prompt": "将这些图片合成为一幅美丽的风景画",
75
+ "images": [
76
+ "https://example.com/image1.jpg",
77
+ "https://example.com/image2.jpg"
78
+ ],
79
+ "width": 2560,
80
+ "height": 1440,
81
+ "sample_strength": 0.6
82
+ }'
83
+ ```
84
+
85
+ ## 参数说明
86
+
87
+ ### 支持的分辨率
88
+
89
+ - 512x512
90
+ - 768x768
91
+ - 1024x1024(默认)
92
+ - 1280x720
93
+ - 720x1280
94
+ - 1536x864
95
+ - 864x1536
96
+ - 2048x2048(仅 4.1 和 4.5)
97
+
98
+ ### 采样强度(sample_strength)
99
+
100
+ - 范围:0.1 - 1.0
101
+ - 默认:0.5
102
+ - 说明:控制生成图片与提示词的契合度,值越高越贴近提示词
103
+
104
+ ## 最佳实践
105
+
106
+ 1. **使用提示词**
107
+ - 4.5 和 4.1 版本对中文提示词支持更好
108
+ - 建议使用详细的描述性提示词
109
+
110
+ 2. **多图生成**
111
+ - 使用 "连续"、"绘本"、"故事" 等关键词触发多图生成
112
+ - 使用 "X张" 指定生成图片数量
113
+
114
+ 3. **分辨率选择**
115
+ - 普通场景使用 1024x1024
116
+ - 需要高清细节时使用 2048x2048
117
+ - 宽屏场景使用 1280x720
118
+
119
+ ## 测试
120
+
121
+ 项目提供了测试脚本 `test/test-new-models.js`:
122
+
123
+ ```bash
124
+ # 安装依赖
125
+ npm install
126
+
127
+ # 修改脚本中的 SESSION_ID
128
+ vim test/test-new-models.js
129
+
130
+ # 运行测试
131
+ npm run test:models
132
+ # 或直接运行
133
+ node test/test-new-models.js
134
+ ```
135
+
136
+ ## 注意事项
137
+
138
+ 1. 新模型需要更多的积分消耗
139
+ 2. 生成时间可能比旧模型稍长
140
+ 3. 建议在生产环境使用前充分测试
141
+ 4. 遵守即梦 AI 的使用条款和限制
142
+
143
+ ## 故障排除
144
+
145
+ ### 常见错误
146
+
147
+ 1. **参数验证失败**
148
+ - 检查分辨率是否在支持列表中
149
+ - 检查采样强度是否在 0.1-1.0 范围内
150
+
151
+ 2. **模型不支持**
152
+ - 确保使用的是正确的模型名称(jimeng-4.5 或 jimeng-4.1)
153
+ - 查看模型列表确认可用性
154
+
155
+ 3. **生成失败**
156
+ - 检查积分是否充足
157
+ - 检查提示词是否符合内容规范
158
+ - 查看日志获取详细错误信息
159
+
160
+ ## 更新日志
161
+
162
+ ### v4.5 支持 (2024-12-07)
163
+ - 新增 jimeng-4.5 模型支持
164
+ - 支持最高 2048x2048 分辨率
165
+ - 优化了提示词理解能力
166
+
167
+ ### v4.1 支持 (2024-12-07)
168
+ - 新增 jimeng-4.1 模型支持
169
+ - 改进了图像生成质量
170
+ - 增强了多图生成功能
libs.d.ts ADDED
File without changes
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "jimeng-free-api",
3
+ "version": "0.0.6",
4
+ "description": "jimeng 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 --enable-source-maps --no-node-snapshot dist/index.js\"",
17
+ "start": "node --enable-source-maps --no-node-snapshot 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
+ "build": "^0.1.4",
25
+ "colors": "^1.4.0",
26
+ "crc-32": "^1.2.2",
27
+ "cron": "^3.1.6",
28
+ "date-fns": "^3.3.1",
29
+ "eventsource-parser": "^1.1.2",
30
+ "form-data": "^4.0.0",
31
+ "fs-extra": "^11.2.0",
32
+ "koa": "^2.15.0",
33
+ "koa-body": "^5.0.0",
34
+ "koa-bodyparser": "^4.4.1",
35
+ "koa-range": "^0.3.0",
36
+ "koa-router": "^12.0.1",
37
+ "koa2-cors": "^2.0.6",
38
+ "lodash": "^4.17.21",
39
+ "mime": "^4.0.1",
40
+ "minimist": "^1.2.8",
41
+ "randomstring": "^1.3.0",
42
+ "semver": "^7.7.2",
43
+ "uuid": "^9.0.1",
44
+ "yaml": "^2.3.4"
45
+ },
46
+ "devDependencies": {
47
+ "@types/lodash": "^4.14.202",
48
+ "@types/mime": "^3.0.4",
49
+ "tsup": "^8.0.2",
50
+ "typescript": "^5.3.3"
51
+ }
52
+ }
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>jimeng-free-api已启动!<br>请通过LobeChat / NextChat / Dify等客户端或OpenAI SDK接入!</p>
9
+ </body>
10
+ </html>
src/api/consts/exceptions.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ API_VIDEO_GENERATION_FAILED: [-2008, '视频生成失败'],
12
+ API_IMAGE_GENERATION_INSUFFICIENT_POINTS: [-2009, '即梦积分不足'],
13
+ }
src/api/controllers/chat.ts ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from "lodash";
2
+ import { PassThrough } from "stream";
3
+
4
+ import APIException from "@/lib/exceptions/APIException.ts";
5
+ import EX from "@/api/consts/exceptions.ts";
6
+ import logger from "@/lib/logger.ts";
7
+ import util from "@/lib/util.ts";
8
+ import { generateImages, DEFAULT_MODEL } from "./images.ts";
9
+ import { generateVideo, DEFAULT_MODEL as DEFAULT_VIDEO_MODEL } from "./videos.ts";
10
+
11
+ // 最大重试次数
12
+ const MAX_RETRY_COUNT = 3;
13
+ // 重试延迟
14
+ const RETRY_DELAY = 5000;
15
+
16
+ /**
17
+ * 解析模型
18
+ *
19
+ * @param model 模型名称
20
+ * @returns 模型信息
21
+ */
22
+ function parseModel(model: string) {
23
+ const [_model, size] = model.split(":");
24
+ const [_, width, height] = /(\d+)[\W\w](\d+)/.exec(size) ?? [];
25
+ return {
26
+ model: _model,
27
+ width: size ? Math.ceil(parseInt(width) / 2) * 2 : 1024,
28
+ height: size ? Math.ceil(parseInt(height) / 2) * 2 : 1024,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * 检测是否为视频生成请求
34
+ *
35
+ * @param model 模型名称
36
+ * @returns 是否为视频生成请求
37
+ */
38
+ function isVideoModel(model: string) {
39
+ return model.startsWith("jimeng-video");
40
+ }
41
+
42
+ /**
43
+ * 同步对话补全
44
+ *
45
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
46
+ * @param refreshToken 用于刷新access_token的refresh_token
47
+ * @param assistantId 智能体ID,默认使用jimeng原版
48
+ * @param retryCount 重试次数
49
+ */
50
+ export async function createCompletion(
51
+ messages: any[],
52
+ refreshToken: string,
53
+ _model = DEFAULT_MODEL,
54
+ retryCount = 0
55
+ ) {
56
+ return (async () => {
57
+ if (messages.length === 0)
58
+ throw new APIException(EX.API_REQUEST_PARAMS_INVALID, "消息不能为空");
59
+
60
+ const { model, width, height } = parseModel(_model);
61
+ logger.info(messages);
62
+
63
+ // 检查是否为视频生成请求
64
+ if (isVideoModel(_model)) {
65
+ try {
66
+ // 视频生成
67
+ logger.info(`开始生成视频,模型: ${_model}`);
68
+ const videoUrl = await generateVideo(
69
+ _model,
70
+ messages[messages.length - 1].content,
71
+ {
72
+ width,
73
+ height,
74
+ resolution: "720p", // 默认分辨率
75
+ },
76
+ refreshToken
77
+ );
78
+
79
+ logger.info(`视频生成成功,URL: ${videoUrl}`);
80
+ return {
81
+ id: util.uuid(),
82
+ model: _model,
83
+ object: "chat.completion",
84
+ choices: [
85
+ {
86
+ index: 0,
87
+ message: {
88
+ role: "assistant",
89
+ content: `![video](${videoUrl})\n`,
90
+ },
91
+ finish_reason: "stop",
92
+ },
93
+ ],
94
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
95
+ created: util.unixTimestamp(),
96
+ };
97
+ } catch (error) {
98
+ logger.error(`视频生成失败: ${error.message}`);
99
+ // 如果是积分不足等特定错误,直接抛出
100
+ if (error instanceof APIException) {
101
+ throw error;
102
+ }
103
+
104
+ // 其他错误返回友好提示
105
+ return {
106
+ id: util.uuid(),
107
+ model: _model,
108
+ object: "chat.completion",
109
+ choices: [
110
+ {
111
+ index: 0,
112
+ message: {
113
+ role: "assistant",
114
+ content: `生成视频失败: ${error.message}\n\n如果您在即梦官网看到已生成的视频,可能是获取结果时出现了问题,请前往即梦官网查看。`,
115
+ },
116
+ finish_reason: "stop",
117
+ },
118
+ ],
119
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
120
+ created: util.unixTimestamp(),
121
+ };
122
+ }
123
+ } else {
124
+ // 图像生成
125
+ const imageUrls = await generateImages(
126
+ model,
127
+ messages[messages.length - 1].content,
128
+ {
129
+ width,
130
+ height,
131
+ },
132
+ refreshToken
133
+ );
134
+
135
+ return {
136
+ id: util.uuid(),
137
+ model: _model || model,
138
+ object: "chat.completion",
139
+ choices: [
140
+ {
141
+ index: 0,
142
+ message: {
143
+ role: "assistant",
144
+ content: imageUrls.reduce(
145
+ (acc, url, i) => acc + `![image_${i}](${url})\n`,
146
+ ""
147
+ ),
148
+ },
149
+ finish_reason: "stop",
150
+ },
151
+ ],
152
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
153
+ created: util.unixTimestamp(),
154
+ };
155
+ }
156
+ })().catch((err) => {
157
+ if (retryCount < MAX_RETRY_COUNT) {
158
+ logger.error(`Response error: ${err.stack}`);
159
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
160
+ return (async () => {
161
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
162
+ return createCompletion(messages, refreshToken, _model, retryCount + 1);
163
+ })();
164
+ }
165
+ throw err;
166
+ });
167
+ }
168
+
169
+ /**
170
+ * 流式对话补全
171
+ *
172
+ * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
173
+ * @param refreshToken 用于刷新access_token的refresh_token
174
+ * @param assistantId 智能体ID,默认使用jimeng原版
175
+ * @param retryCount 重试次数
176
+ */
177
+ export async function createCompletionStream(
178
+ messages: any[],
179
+ refreshToken: string,
180
+ _model = DEFAULT_MODEL,
181
+ retryCount = 0
182
+ ) {
183
+ return (async () => {
184
+ const { model, width, height } = parseModel(_model);
185
+ logger.info(messages);
186
+
187
+ const stream = new PassThrough();
188
+
189
+ if (messages.length === 0) {
190
+ logger.warn("消息为空,返回空流");
191
+ stream.end("data: [DONE]\n\n");
192
+ return stream;
193
+ }
194
+
195
+ // 检查是否为视频生成请求
196
+ if (isVideoModel(_model)) {
197
+ // 视频生成
198
+ stream.write(
199
+ "data: " +
200
+ JSON.stringify({
201
+ id: util.uuid(),
202
+ model: _model,
203
+ object: "chat.completion.chunk",
204
+ choices: [
205
+ {
206
+ index: 0,
207
+ delta: { role: "assistant", content: "🎬 视频生成中,请稍候...\n这可能需要1-2分钟,请耐心等待" },
208
+ finish_reason: null,
209
+ },
210
+ ],
211
+ }) +
212
+ "\n\n"
213
+ );
214
+
215
+ // 视频生成
216
+ logger.info(`开始生成视频,提示词: ${messages[messages.length - 1].content}`);
217
+
218
+ // 进度更新定时器
219
+ const progressInterval = setInterval(() => {
220
+ stream.write(
221
+ "data: " +
222
+ JSON.stringify({
223
+ id: util.uuid(),
224
+ model: _model,
225
+ object: "chat.completion.chunk",
226
+ choices: [
227
+ {
228
+ index: 0,
229
+ delta: { role: "assistant", content: "." },
230
+ finish_reason: null,
231
+ },
232
+ ],
233
+ }) +
234
+ "\n\n"
235
+ );
236
+ }, 5000);
237
+
238
+ // 设置超时,防止无限等待
239
+ const timeoutId = setTimeout(() => {
240
+ clearInterval(progressInterval);
241
+ logger.warn(`视频生成超时(2分钟),提示用户前往即梦官网查看`);
242
+ stream.write(
243
+ "data: " +
244
+ JSON.stringify({
245
+ id: util.uuid(),
246
+ model: _model,
247
+ object: "chat.completion.chunk",
248
+ choices: [
249
+ {
250
+ index: 1,
251
+ delta: {
252
+ role: "assistant",
253
+ content: "\n\n视频生成时间较长(已等待2分钟),但视频可能仍在生成中。\n\n请前往即梦官网查看您的视频:\n1. 访问 https://jimeng.jianying.com/ai-tool/video/generate\n2. 登录后查看您的创作历史\n3. 如果视频已生成,您可以直接在官网下载或分享\n\n您也可以继续等待,系统将在后台继续尝试获取视频(最长约20分钟)。",
254
+ },
255
+ finish_reason: "stop",
256
+ },
257
+ ],
258
+ }) +
259
+ "\n\n"
260
+ );
261
+ // 注意:这里不结束流,让后台继续尝试获取视频
262
+ // stream.end("data: [DONE]\n\n");
263
+ }, 2 * 60 * 1000);
264
+
265
+ logger.info(`开始生成视频,模型: ${_model}, 提示词: ${messages[messages.length - 1].content.substring(0, 50)}...`);
266
+
267
+ // 先给用户一个初始提示
268
+ stream.write(
269
+ "data: " +
270
+ JSON.stringify({
271
+ id: util.uuid(),
272
+ model: _model,
273
+ object: "chat.completion.chunk",
274
+ choices: [
275
+ {
276
+ index: 0,
277
+ delta: {
278
+ role: "assistant",
279
+ content: "\n\n🎬 视频生成已开始,这可能需要几分钟时间...",
280
+ },
281
+ finish_reason: null,
282
+ },
283
+ ],
284
+ }) +
285
+ "\n\n"
286
+ );
287
+
288
+ generateVideo(
289
+ _model,
290
+ messages[messages.length - 1].content,
291
+ { width, height, resolution: "720p" },
292
+ refreshToken
293
+ )
294
+ .then((videoUrl) => {
295
+ clearInterval(progressInterval);
296
+ clearTimeout(timeoutId);
297
+
298
+ logger.info(`视频生成成功,URL: ${videoUrl}`);
299
+
300
+ stream.write(
301
+ "data: " +
302
+ JSON.stringify({
303
+ id: util.uuid(),
304
+ model: _model,
305
+ object: "chat.completion.chunk",
306
+ choices: [
307
+ {
308
+ index: 1,
309
+ delta: {
310
+ role: "assistant",
311
+ content: `\n\n✅ 视频生成完成!\n\n![video](${videoUrl})\n\n您可以:\n1. 直接查看上方视频\n2. 使用以下链接下载或分享:${videoUrl}`,
312
+ },
313
+ finish_reason: null,
314
+ },
315
+ ],
316
+ }) +
317
+ "\n\n"
318
+ );
319
+
320
+ stream.write(
321
+ "data: " +
322
+ JSON.stringify({
323
+ id: util.uuid(),
324
+ model: _model,
325
+ object: "chat.completion.chunk",
326
+ choices: [
327
+ {
328
+ index: 2,
329
+ delta: {
330
+ role: "assistant",
331
+ content: "",
332
+ },
333
+ finish_reason: "stop",
334
+ },
335
+ ],
336
+ }) +
337
+ "\n\n"
338
+ );
339
+ stream.end("data: [DONE]\n\n");
340
+ })
341
+ .catch((err) => {
342
+ clearInterval(progressInterval);
343
+ clearTimeout(timeoutId);
344
+
345
+ logger.error(`视频生成失败: ${err.message}`);
346
+ logger.error(`错误详情: ${JSON.stringify(err)}`);
347
+
348
+ // 记录详细错误信息
349
+ logger.error(`视频生成失败: ${err.message}`);
350
+ logger.error(`错误详情: ${JSON.stringify(err)}`);
351
+
352
+ // 构建更详细的错误信息
353
+ let errorMessage = `⚠️ 视频生成过程中遇到问题: ${err.message}`;
354
+
355
+ // 如果是历史记录不存在的错误,提供更具体的建议
356
+ if (err.message.includes("历史记录不存在")) {
357
+ errorMessage += "\n\n可能原因:\n1. 视频生成请求已发送,但API无法获取历史记录\n2. 视频生成服务暂时不可用\n3. 历史记录ID无效或已过期\n\n建议操作:\n1. 请前往即梦官网查看您的视频是否已生成:https://jimeng.jianying.com/ai-tool/video/generate\n2. 如果官网已显示视频,但这里无法获取,可能是API连接问题\n3. 如果官网也没有显示,请稍后再试或重新生成视频";
358
+ } else if (err.message.includes("获取视频生成结果超时")) {
359
+ errorMessage += "\n\n视频生成可能仍在进行中,但等待时间已超过系统设定的限制。\n\n请前往即梦官网查看您的视频:https://jimeng.jianying.com/ai-tool/video/generate\n\n如果您在官网上看到视频已生成,但这里无法显示,可能是因为:\n1. 获取结果的过程超时\n2. 网络连接问题\n3. API访问限制";
360
+ } else {
361
+ errorMessage += "\n\n如果您在即梦官网看到已生成的视频,可能是获取结果时出现了问题。\n\n请访问即梦官网查看您的创作历史:https://jimeng.jianying.com/ai-tool/video/generate";
362
+ }
363
+
364
+ // 添加历史ID信息,方便用户在官网查找
365
+ if (err.historyId) {
366
+ errorMessage += `\n\n历史记录ID: ${err.historyId}(您可以使用此ID在官网搜索您的视频)`;
367
+ }
368
+
369
+ stream.write(
370
+ "data: " +
371
+ JSON.stringify({
372
+ id: util.uuid(),
373
+ model: _model,
374
+ object: "chat.completion.chunk",
375
+ choices: [
376
+ {
377
+ index: 1,
378
+ delta: {
379
+ role: "assistant",
380
+ content: `\n\n${errorMessage}`,
381
+ },
382
+ finish_reason: "stop",
383
+ },
384
+ ],
385
+ }) +
386
+ "\n\n"
387
+ );
388
+ stream.end("data: [DONE]\n\n");
389
+ });
390
+ } else {
391
+ // 图像生成
392
+ stream.write(
393
+ "data: " +
394
+ JSON.stringify({
395
+ id: util.uuid(),
396
+ model: _model || model,
397
+ object: "chat.completion.chunk",
398
+ choices: [
399
+ {
400
+ index: 0,
401
+ delta: { role: "assistant", content: "🎨 图像生成中,请稍候..." },
402
+ finish_reason: null,
403
+ },
404
+ ],
405
+ }) +
406
+ "\n\n"
407
+ );
408
+
409
+ generateImages(
410
+ model,
411
+ messages[messages.length - 1].content,
412
+ { width, height },
413
+ refreshToken
414
+ )
415
+ .then((imageUrls) => {
416
+ for (let i = 0; i < imageUrls.length; i++) {
417
+ const url = imageUrls[i];
418
+ stream.write(
419
+ "data: " +
420
+ JSON.stringify({
421
+ id: util.uuid(),
422
+ model: _model || model,
423
+ object: "chat.completion.chunk",
424
+ choices: [
425
+ {
426
+ index: i + 1,
427
+ delta: {
428
+ role: "assistant",
429
+ content: `![image_${i}](${url})\n`,
430
+ },
431
+ finish_reason: i < imageUrls.length - 1 ? null : "stop",
432
+ },
433
+ ],
434
+ }) +
435
+ "\n\n"
436
+ );
437
+ }
438
+ stream.write(
439
+ "data: " +
440
+ JSON.stringify({
441
+ id: util.uuid(),
442
+ model: _model || model,
443
+ object: "chat.completion.chunk",
444
+ choices: [
445
+ {
446
+ index: imageUrls.length + 1,
447
+ delta: {
448
+ role: "assistant",
449
+ content: "图像生成完成!",
450
+ },
451
+ finish_reason: "stop",
452
+ },
453
+ ],
454
+ }) +
455
+ "\n\n"
456
+ );
457
+ stream.end("data: [DONE]\n\n");
458
+ })
459
+ .catch((err) => {
460
+ stream.write(
461
+ "data: " +
462
+ JSON.stringify({
463
+ id: util.uuid(),
464
+ model: _model || model,
465
+ object: "chat.completion.chunk",
466
+ choices: [
467
+ {
468
+ index: 1,
469
+ delta: {
470
+ role: "assistant",
471
+ content: `生成图片失败: ${err.message}`,
472
+ },
473
+ finish_reason: "stop",
474
+ },
475
+ ],
476
+ }) +
477
+ "\n\n"
478
+ );
479
+ stream.end("data: [DONE]\n\n");
480
+ });
481
+ }
482
+ return stream;
483
+ })().catch((err) => {
484
+ if (retryCount < MAX_RETRY_COUNT) {
485
+ logger.error(`Response error: ${err.stack}`);
486
+ logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`);
487
+ return (async () => {
488
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
489
+ return createCompletionStream(
490
+ messages,
491
+ refreshToken,
492
+ _model,
493
+ retryCount + 1
494
+ );
495
+ })();
496
+ }
497
+ throw err;
498
+ });
499
+ }
src/api/controllers/core.ts ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PassThrough } from "stream";
2
+ import path from "path";
3
+ import _ from "lodash";
4
+ import mime from "mime";
5
+ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
6
+
7
+ import APIException from "@/lib/exceptions/APIException.ts";
8
+ import EX from "@/api/consts/exceptions.ts";
9
+ import { createParser } from "eventsource-parser";
10
+ import logger from "@/lib/logger.ts";
11
+ import util from "@/lib/util.ts";
12
+
13
+ // 模型名称
14
+ const MODEL_NAME = "jimeng";
15
+ // 默认的AgentID
16
+ const DEFAULT_ASSISTANT_ID = 513695;
17
+ // 版本号
18
+ const VERSION_CODE = "5.8.0";
19
+ // 平台代码
20
+ const PLATFORM_CODE = "7";
21
+ // 设备ID
22
+ const DEVICE_ID = Math.random() * 999999999999999999 + 7000000000000000000;
23
+ // WebID
24
+ const WEB_ID = Math.random() * 999999999999999999 + 7000000000000000000;
25
+ // 用户ID
26
+ const USER_ID = util.uuid(false);
27
+ // 最大重试次数
28
+ const MAX_RETRY_COUNT = 3;
29
+ // 重试延迟
30
+ const RETRY_DELAY = 5000;
31
+ // 伪装headers
32
+ const FAKE_HEADERS = {
33
+ Accept: "application/json, text/plain, */*",
34
+ "Accept-Encoding": "gzip, deflate, br, zstd",
35
+ "Accept-language": "zh-CN,zh;q=0.9",
36
+ "Cache-control": "no-cache",
37
+ Appid: DEFAULT_ASSISTANT_ID,
38
+ Appvr: VERSION_CODE,
39
+ Origin: "https://jimeng.jianying.com",
40
+ Pragma: "no-cache",
41
+ Priority: "u=1, i",
42
+ Referer: "https://jimeng.jianying.com",
43
+ Pf: PLATFORM_CODE,
44
+ "Sec-Ch-Ua":
45
+ '"Google Chrome";v="142", "Chromium";v="142", "Not_A Brand";v="99"',
46
+ "Sec-Ch-Ua-Mobile": "?0",
47
+ "Sec-Ch-Ua-Platform": '"Windows"',
48
+ "Sec-Fetch-Dest": "empty",
49
+ "Sec-Fetch-Mode": "cors",
50
+ "Sec-Fetch-Site": "same-origin",
51
+ "User-Agent":
52
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
53
+ };
54
+ // 文件最大大小
55
+ const FILE_MAX_SIZE = 100 * 1024 * 1024;
56
+
57
+ /**
58
+ * 获取缓存中的access_token
59
+ *
60
+ * 目前jimeng的access_token是固定的,暂无刷新功能
61
+ *
62
+ * @param refreshToken 用于刷新access_token的refresh_token
63
+ */
64
+ export async function acquireToken(refreshToken: string): Promise<string> {
65
+ return refreshToken;
66
+ }
67
+
68
+ /**
69
+ * 生成cookie
70
+ */
71
+ export function generateCookie(refreshToken: string) {
72
+ return [
73
+ `_tea_web_id=${WEB_ID}`,
74
+ `is_staff_user=false`,
75
+ `store-region=cn-gd`,
76
+ `store-region-src=uid`,
77
+ `sid_guard=${refreshToken}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`,
78
+ `uid_tt=${USER_ID}`,
79
+ `uid_tt_ss=${USER_ID}`,
80
+ `sid_tt=${refreshToken}`,
81
+ `sessionid=${refreshToken}`,
82
+ `sessionid_ss=${refreshToken}`,
83
+ `sid_tt=${refreshToken}`
84
+ ].join("; ");
85
+ }
86
+
87
+ /**
88
+ * 获取积分信息
89
+ *
90
+ * @param refreshToken 用于刷新access_token的refresh_token
91
+ */
92
+ export async function getCredit(refreshToken: string) {
93
+ const {
94
+ credit: { gift_credit, purchase_credit, vip_credit }
95
+ } = await request("POST", "/commerce/v1/benefits/user_credit", refreshToken, {
96
+ data: {},
97
+ headers: {
98
+ // Cookie: 'x-web-secsdk-uid=ef44bd0d-0cf6-448c-b517-fd1b5a7267ba; s_v_web_id=verify_m4b1lhlu_DI8qKRlD_7mJJ_4eqx_9shQ_s8eS2QLAbc4n; passport_csrf_token=86f3619c0c4a9c13f24117f71dc18524; passport_csrf_token_default=86f3619c0c4a9c13f24117f71dc18524; n_mh=9-mIeuD4wZnlYrrOvfzG3MuT6aQmCUtmr8FxV8Kl8xY; sid_guard=a7eb745aec44bb3186dbc2083ea9e1a6%7C1733386629%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT; uid_tt=59a46c7d3f34bda9588b93590cca2e12; uid_tt_ss=59a46c7d3f34bda9588b93590cca2e12; sid_tt=a7eb745aec44bb3186dbc2083ea9e1a6; sessionid=a7eb745aec44bb3186dbc2083ea9e1a6; sessionid_ss=a7eb745aec44bb3186dbc2083ea9e1a6; is_staff_user=false; sid_ucp_v1=1.0.0-KGRiOGY2ODQyNWU1OTk3NzRhYTE2ZmZhYmFjNjdmYjY3NzRmZGRiZTgKHgjToPCw0cwbEIXDxboGGJ-tHyAMMITDxboGOAhAJhoCaGwiIGE3ZWI3NDVhZWM0NGJiMzE4NmRiYzIwODNlYTllMWE2; ssid_ucp_v1=1.0.0-KGRiOGY2ODQyNWU1OTk3NzRhYTE2ZmZhYmFjNjdmYjY3NzRmZGRiZTgKHgjToPCw0cwbEIXDxboGGJ-tHyAMMITDxboGOAhAJhoCaGwiIGE3ZWI3NDVhZWM0NGJiMzE4NmRiYzIwODNlYTllMWE2; store-region=cn-gd; store-region-src=uid; user_spaces_idc={"7444764277623653426":"lf"}; ttwid=1|cxHJViEev1mfkjntdMziir8SwbU8uPNVSaeh9QpEUs8|1733966961|d8d52f5f56607427691be4ac44253f7870a34d25dd05a01b4d89b8a7c5ea82ad; _tea_web_id=7444838473275573797; fpk1=fa6c6a4d9ba074b90003896f36b6960066521c1faec6a60bdcb69ec8ddf85e8360b4c0704412848ec582b2abca73d57a; odin_tt=efe9dc150207879b88509e651a1c4af4e7ffb4cfcb522425a75bd72fbf894eda570bbf7ffb551c8b1de0aa2bfa0bd1be6c4157411ecdcf4464fcaf8dd6657d66',
99
+ Referer: "https://jimeng.jianying.com/ai-tool/image/generate",
100
+ // "Device-Time": 1733966964,
101
+ // Sign: "f3dbb824b378abea7c03cbb152b3a365"
102
+ }
103
+ });
104
+ logger.info(`\n积分信息: \n赠送积分: ${gift_credit}, 购买积分: ${purchase_credit}, VIP积分: ${vip_credit}`);
105
+ return {
106
+ giftCredit: gift_credit,
107
+ purchaseCredit: purchase_credit,
108
+ vipCredit: vip_credit,
109
+ totalCredit: gift_credit + purchase_credit + vip_credit
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 接收今日积分
115
+ *
116
+ * @param refreshToken 用于刷新access_token的refresh_token
117
+ */
118
+ export async function receiveCredit(refreshToken: string) {
119
+ logger.info("���在收取今日积分...")
120
+ const { cur_total_credits, receive_quota } = await request("POST", "/commerce/v1/benefits/credit_receive", refreshToken, {
121
+ data: {
122
+ time_zone: "Asia/Shanghai"
123
+ },
124
+ headers: {
125
+ Referer: "https://jimeng.jianying.com/ai-tool/image/generate"
126
+ }
127
+ });
128
+ logger.info(`\n今日${receive_quota}积分收取成功\n剩余积分: ${cur_total_credits}`);
129
+ return cur_total_credits;
130
+ }
131
+
132
+ /**
133
+ * 请求jimeng
134
+ *
135
+ * @param method 请求方法
136
+ * @param uri 请求路径
137
+ * @param params 请求参数
138
+ * @param headers 请求头
139
+ */
140
+ export async function request(
141
+ method: string,
142
+ uri: string,
143
+ refreshToken: string,
144
+ options: AxiosRequestConfig = {}
145
+ ) {
146
+ const token = await acquireToken(refreshToken);
147
+ const deviceTime = util.unixTimestamp();
148
+ const sign = util.md5(
149
+ `9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac`
150
+ );
151
+
152
+ const fullUrl = `https://jimeng.jianying.com${uri}`;
153
+ const requestParams = {
154
+ aid: DEFAULT_ASSISTANT_ID,
155
+ device_platform: "web",
156
+ region: "CN",
157
+ webId: WEB_ID,
158
+ da_version: "3.3.2",
159
+ web_component_open_flag: 1,
160
+ web_version: "7.5.0",
161
+ aigc_features: "app_lip_sync",
162
+ ...(options.params || {}),
163
+ };
164
+
165
+ const headers = {
166
+ ...FAKE_HEADERS,
167
+ Cookie: generateCookie(token),
168
+ "Device-Time": deviceTime,
169
+ Sign: sign,
170
+ "Sign-Ver": "1",
171
+ ...(options.headers || {}),
172
+ };
173
+
174
+ logger.info(`发送请求: ${method.toUpperCase()} ${fullUrl}`);
175
+ logger.info(`请求参数: ${JSON.stringify(requestParams)}`);
176
+ logger.info(`请求数据: ${JSON.stringify(options.data || {})}`);
177
+
178
+ // 添加重试逻辑
179
+ let retries = 0;
180
+ const maxRetries = 3; // 最大重试次数
181
+ let lastError = null;
182
+
183
+ while (retries <= maxRetries) {
184
+ try {
185
+ if (retries > 0) {
186
+ logger.info(`第 ${retries} 次重试请求: ${method.toUpperCase()} ${fullUrl}`);
187
+ // 重试前等待一段时间
188
+ await new Promise(resolve => setTimeout(resolve, 1000 * retries));
189
+ }
190
+
191
+ const response = await axios.request({
192
+ method,
193
+ url: fullUrl,
194
+ params: requestParams,
195
+ headers: headers,
196
+ timeout: 45000, // 增加超时时间到45秒
197
+ validateStatus: () => true, // 允许任何状态码
198
+ ..._.omit(options, "params", "headers"),
199
+ });
200
+
201
+ // 记录响应状态和头信息
202
+ logger.info(`响应状态: ${response.status} ${response.statusText}`);
203
+
204
+ // 流式响应直接返回response
205
+ if (options.responseType == "stream") return response;
206
+
207
+ // 记录响应数据摘要
208
+ const responseDataSummary = JSON.stringify(response.data).substring(0, 500) +
209
+ (JSON.stringify(response.data).length > 500 ? "..." : "");
210
+ logger.info(`响应数据摘要: ${responseDataSummary}`);
211
+
212
+ // 检查HTTP状态码
213
+ if (response.status >= 400) {
214
+ logger.warn(`HTTP错误: ${response.status} ${response.statusText}`);
215
+ if (retries < maxRetries) {
216
+ retries++;
217
+ continue;
218
+ }
219
+ }
220
+
221
+ return checkResult(response);
222
+ }
223
+ catch (error) {
224
+ lastError = error;
225
+ logger.error(`请求失败 (尝试 ${retries + 1}/${maxRetries + 1}): ${error.message}`);
226
+
227
+ // 如果是网络错误或超时,尝试重试
228
+ if ((error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' ||
229
+ error.message.includes('timeout') || error.message.includes('network')) &&
230
+ retries < maxRetries) {
231
+ retries++;
232
+ continue;
233
+ }
234
+
235
+ // 其他错误直接抛出
236
+ break;
237
+ }
238
+ }
239
+
240
+ // 所有重试都失败了,抛出最后一个错误
241
+ logger.error(`请求失败,已重试 ${retries} 次: ${lastError.message}`);
242
+ if (lastError.response) {
243
+ logger.error(`响应状态: ${lastError.response.status}`);
244
+ logger.error(`响应数据: ${JSON.stringify(lastError.response.data)}`);
245
+ }
246
+ throw lastError;
247
+ }
248
+
249
+ /**
250
+ * 预检查文件URL有效性
251
+ *
252
+ * @param fileUrl 文件URL
253
+ */
254
+ export async function checkFileUrl(fileUrl: string) {
255
+ if (util.isBASE64Data(fileUrl)) return;
256
+ const result = await axios.head(fileUrl, {
257
+ timeout: 15000,
258
+ validateStatus: () => true,
259
+ });
260
+ if (result.status >= 400)
261
+ throw new APIException(
262
+ EX.API_FILE_URL_INVALID,
263
+ `File ${fileUrl} is not valid: [${result.status}] ${result.statusText}`
264
+ );
265
+ // 检查文件大小
266
+ if (result.headers && result.headers["content-length"]) {
267
+ const fileSize = parseInt(result.headers["content-length"], 10);
268
+ if (fileSize > FILE_MAX_SIZE)
269
+ throw new APIException(
270
+ EX.API_FILE_EXECEEDS_SIZE,
271
+ `File ${fileUrl} is not valid`
272
+ );
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 上传文件
278
+ *
279
+ * @param refreshToken 用于刷新access_token的refresh_token
280
+ * @param fileUrl 文件URL或BASE64数据
281
+ * @param isVideoImage 是否是用于视频图像
282
+ * @returns 上传结果,包含image_uri
283
+ */
284
+ export async function uploadFile(
285
+ refreshToken: string,
286
+ fileUrl: string,
287
+ isVideoImage: boolean = false
288
+ ) {
289
+ try {
290
+ logger.info(`开始上传文件: ${fileUrl}, 视频图像模式: ${isVideoImage}`);
291
+
292
+ // 预检查远程文件URL可用性
293
+ await checkFileUrl(fileUrl);
294
+
295
+ let filename, fileData, mimeType;
296
+ // 如果是BASE64数据则直接转换为Buffer
297
+ if (util.isBASE64Data(fileUrl)) {
298
+ mimeType = util.extractBASE64DataFormat(fileUrl);
299
+ const ext = mime.getExtension(mimeType);
300
+ filename = `${util.uuid()}.${ext}`;
301
+ fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), "base64");
302
+ logger.info(`处理BASE64数据,文件名: ${filename}, 类型: ${mimeType}, 大小: ${fileData.length}字节`);
303
+ }
304
+ // 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存
305
+ else {
306
+ filename = path.basename(fileUrl);
307
+ logger.info(`开始下载远程文件: ${fileUrl}`);
308
+ ({ data: fileData } = await axios.get(fileUrl, {
309
+ responseType: "arraybuffer",
310
+ // 100M限制
311
+ maxContentLength: FILE_MAX_SIZE,
312
+ // 60秒超时
313
+ timeout: 60000,
314
+ }));
315
+ logger.info(`文件下载完成,文件名: ${filename}, 大小: ${fileData.length}字节`);
316
+ }
317
+
318
+ // 获取文件的MIME类型
319
+ mimeType = mimeType || mime.getType(filename);
320
+ logger.info(`文件MIME类型: ${mimeType}`);
321
+
322
+ // 构建FormData
323
+ const formData = new FormData();
324
+ const blob = new Blob([fileData], { type: mimeType });
325
+ formData.append('file', blob, filename);
326
+
327
+ // 获取上传凭证
328
+ logger.info(`请求上传凭证,场景: ${isVideoImage ? 'video_cover' : 'aigc_image'}`);
329
+ const uploadProofUrl = 'https://imagex.bytedanceapi.com/';
330
+ const proofResult = await request(
331
+ 'POST',
332
+ '/mweb/v1/get_upload_image_proof',
333
+ refreshToken,
334
+ {
335
+ data: {
336
+ scene: isVideoImage ? 'video_cover' : 'aigc_image',
337
+ file_name: filename,
338
+ file_size: fileData.length,
339
+ }
340
+ }
341
+ );
342
+
343
+ if (!proofResult || !proofResult.proof_info) {
344
+ logger.error(`获取上传凭证失败: ${JSON.stringify(proofResult)}`);
345
+ throw new APIException(EX.API_REQUEST_FAILED, '获取上传凭证失败');
346
+ }
347
+
348
+ logger.info(`获取上传凭证成功`);
349
+
350
+ // 上传文件
351
+ const { proof_info } = proofResult;
352
+ logger.info(`开始上传文件到: ${uploadProofUrl}`);
353
+
354
+ const uploadResult = await axios.post(
355
+ uploadProofUrl,
356
+ formData,
357
+ {
358
+ headers: {
359
+ ...proof_info.headers,
360
+ 'Content-Type': 'multipart/form-data',
361
+ },
362
+ params: proof_info.query_params,
363
+ timeout: 60000,
364
+ validateStatus: () => true, // 允许任何状态码以便详细处理
365
+ }
366
+ );
367
+
368
+ logger.info(`上传响应状态: ${uploadResult.status}`);
369
+
370
+ if (!uploadResult || uploadResult.status !== 200) {
371
+ logger.error(`上传文件失败: 状态码 ${uploadResult?.status}, 响应: ${JSON.stringify(uploadResult?.data)}`);
372
+ throw new APIException(EX.API_REQUEST_FAILED, `上传文件失败: 状态码 ${uploadResult?.status}`);
373
+ }
374
+
375
+ // 验证 proof_info.image_uri 是否存在
376
+ if (!proof_info.image_uri) {
377
+ logger.error(`上传凭证中缺少 image_uri: ${JSON.stringify(proof_info)}`);
378
+ throw new APIException(EX.API_REQUEST_FAILED, '上传凭证中缺少 image_uri');
379
+ }
380
+
381
+ logger.info(`文件上传成功: ${proof_info.image_uri}`);
382
+
383
+ // 返回上传结果
384
+ return {
385
+ image_uri: proof_info.image_uri,
386
+ uri: proof_info.image_uri,
387
+ }
388
+ } catch (error) {
389
+ logger.error(`文件上传过程中发生错误: ${error.message}`);
390
+ throw error;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * 检查请求结果
396
+ *
397
+ * @param result 结果
398
+ */
399
+ export function checkResult(result: AxiosResponse) {
400
+ const { ret, errmsg, data } = result.data;
401
+ if (!_.isFinite(Number(ret))) return result.data;
402
+ if (ret === '0') return data;
403
+ if (ret === '5000')
404
+ throw new APIException(EX.API_IMAGE_GENERATION_INSUFFICIENT_POINTS, `[无法生成图像]: 即梦积分可能不足,${errmsg}`);
405
+ throw new APIException(EX.API_REQUEST_FAILED, `[请求jimeng失败]: ${errmsg}`);
406
+ }
407
+
408
+ /**
409
+ * Token切分
410
+ *
411
+ * @param authorization 认证字符串
412
+ */
413
+ export function tokenSplit(authorization: string) {
414
+ return authorization.replace("Bearer ", "").split(",");
415
+ }
416
+
417
+ /**
418
+ * 获取Token存活状态
419
+ */
420
+ export async function getTokenLiveStatus(refreshToken: string) {
421
+ const result = await request(
422
+ "POST",
423
+ "/passport/account/info/v2",
424
+ refreshToken,
425
+ {
426
+ params: {
427
+ account_sdk_source: "web",
428
+ },
429
+ }
430
+ );
431
+ try {
432
+ const { user_id } = checkResult(result);
433
+ return !!user_id;
434
+ } catch (err) {
435
+ return false;
436
+ }
437
+ }
src/api/controllers/images.ts ADDED
@@ -0,0 +1,1476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from "lodash";
2
+ import crypto from "crypto";
3
+
4
+ import APIException from "@/lib/exceptions/APIException.ts";
5
+ import EX from "@/api/consts/exceptions.ts";
6
+ import util from "@/lib/util.ts";
7
+ import { getCredit, receiveCredit, request } from "./core.ts";
8
+ import logger from "@/lib/logger.ts";
9
+ import { getModelConfig } from "@/lib/configs/model-config.ts";
10
+
11
+ const DEFAULT_ASSISTANT_ID = 513695;
12
+ export const DEFAULT_MODEL = "jimeng-4.5";
13
+ const DRAFT_VERSION = "3.3.4";
14
+ const DRAFT_MIN_VERSION = "3.0.2";
15
+
16
+ // 支持的图片比例和分辨率配置
17
+ const RESOLUTION_OPTIONS: {
18
+ [resolution: string]: {
19
+ [ratio: string]: { width: number; height: number; ratio: number };
20
+ };
21
+ } = {
22
+ "1k": {
23
+ "1:1": { width: 1024, height: 1024, ratio: 1 },
24
+ "4:3": { width: 768, height: 1024, ratio: 4 },
25
+ "3:4": { width: 1024, height: 768, ratio: 2 },
26
+ "16:9": { width: 1024, height: 576, ratio: 3 },
27
+ "9:16": { width: 576, height: 1024, ratio: 5 },
28
+ "3:2": { width: 1024, height: 682, ratio: 7 },
29
+ "2:3": { width: 682, height: 1024, ratio: 6 },
30
+ "21:9": { width: 1195, height: 512, ratio: 8 },
31
+ },
32
+ "2k": {
33
+ "1:1": { width: 2048, height: 2048, ratio: 1 },
34
+ "4:3": { width: 2304, height: 1728, ratio: 4 },
35
+ "3:4": { width: 1728, height: 2304, ratio: 2 },
36
+ "16:9": { width: 2560, height: 1440, ratio: 3 },
37
+ "9:16": { width: 1440, height: 2560, ratio: 5 },
38
+ "3:2": { width: 2496, height: 1664, ratio: 7 },
39
+ "2:3": { width: 1664, height: 2496, ratio: 6 },
40
+ "21:9": { width: 3024, height: 1296, ratio: 8 },
41
+ },
42
+ "4k": {
43
+ "1:1": { width: 4096, height: 4096, ratio: 101 },
44
+ "4:3": { width: 4608, height: 3456, ratio: 104 },
45
+ "3:4": { width: 3456, height: 4608, ratio: 102 },
46
+ "16:9": { width: 5120, height: 2880, ratio: 103 },
47
+ "9:16": { width: 2880, height: 5120, ratio: 105 },
48
+ "3:2": { width: 4992, height: 3328, ratio: 107 },
49
+ "2:3": { width: 3328, height: 4992, ratio: 106 },
50
+ "21:9": { width: 6048, height: 2592, ratio: 108 },
51
+ },
52
+ };
53
+
54
+ // 解析分辨率参数
55
+ function resolveResolution(
56
+ resolution: string = "2k",
57
+ ratio: string = "1:1"
58
+ ): { width: number; height: number; imageRatio: number; resolutionType: string } {
59
+ const resolutionGroup = RESOLUTION_OPTIONS[resolution];
60
+ if (!resolutionGroup) {
61
+ const supportedResolutions = Object.keys(RESOLUTION_OPTIONS).join(", ");
62
+ throw new Error(`不支持的分辨率 "${resolution}"。支持的分辨率: ${supportedResolutions}`);
63
+ }
64
+
65
+ const ratioConfig = resolutionGroup[ratio];
66
+ if (!ratioConfig) {
67
+ const supportedRatios = Object.keys(resolutionGroup).join(", ");
68
+ throw new Error(`在 "${resolution}" 分辨率下,不支持的比例 "${ratio}"。支持的比例: ${supportedRatios}`);
69
+ }
70
+
71
+ return {
72
+ width: ratioConfig.width,
73
+ height: ratioConfig.height,
74
+ imageRatio: ratioConfig.ratio,
75
+ resolutionType: resolution,
76
+ };
77
+ }
78
+
79
+ // 模型特定的版本配置
80
+ const MODEL_DRAFT_VERSIONS: { [key: string]: string } = {
81
+ "jimeng-4.5": "3.3.4",
82
+ "jimeng-4.1": "3.3.4",
83
+ "jimeng-4.0": "3.3.4",
84
+ "jimeng-3.1": "3.0.2",
85
+ "jimeng-3.0": "3.0.2",
86
+ "jimeng-2.1": "3.0.2",
87
+ "jimeng-2.0-pro": "3.0.2",
88
+ "jimeng-2.0": "3.0.2",
89
+ "jimeng-1.4": "3.0.2",
90
+ "jimeng-xl-pro": "3.0.2",
91
+ };
92
+
93
+ // 获取模型对应的draft版本
94
+ function getDraftVersion(model: string): string {
95
+ try {
96
+ const config = getModelConfig(model);
97
+ return config.draftVersion;
98
+ } catch (e) {
99
+ // 如果配置中没有,使用旧的映射
100
+ return MODEL_DRAFT_VERSIONS[model] || DRAFT_VERSION;
101
+ }
102
+ }
103
+ const MODEL_MAP = {
104
+ "jimeng-4.5": "high_aes_general_v40l",
105
+ "jimeng-4.1": "high_aes_general_v41",
106
+ "jimeng-4.0": "high_aes_general_v40",
107
+ "jimeng-3.1": "high_aes_general_v30l_art_fangzhou:general_v3.0_18b",
108
+ "jimeng-3.0": "high_aes_general_v30l:general_v3.0_18b",
109
+ "jimeng-2.1": "high_aes_general_v21_L:general_v2.1_L",
110
+ "jimeng-2.0-pro": "high_aes_general_v20_L:general_v2.0_L",
111
+ "jimeng-2.0": "high_aes_general_v20:general_v2.0",
112
+ "jimeng-1.4": "high_aes_general_v14:general_v1.4",
113
+ "jimeng-xl-pro": "text2img_xl_sft",
114
+ };
115
+
116
+ // 向后兼容的函数
117
+ export function getModel(model: string) {
118
+ try {
119
+ const config = getModelConfig(model);
120
+ return config.internalModel;
121
+ } catch (e) {
122
+ // 如果配置中没有,使用旧的映射
123
+ return MODEL_MAP[model] || MODEL_MAP[DEFAULT_MODEL];
124
+ }
125
+ }
126
+
127
+
128
+ // AWS4-HMAC-SHA256 签名生成函数
129
+ function createSignature(
130
+ method: string,
131
+ url: string,
132
+ headers: { [key: string]: string },
133
+ accessKeyId: string,
134
+ secretAccessKey: string,
135
+ sessionToken?: string,
136
+ payload: string = ''
137
+ ) {
138
+ const urlObj = new URL(url);
139
+ const pathname = urlObj.pathname || '/';
140
+ const search = urlObj.search;
141
+
142
+ // 创建规范请求
143
+ const timestamp = headers['x-amz-date'];
144
+ const date = timestamp.substr(0, 8);
145
+ const region = 'cn-north-1';
146
+ const service = 'imagex';
147
+
148
+ // 规范化查询参数 - 手动处理以确保正确的顺序
149
+ const queryParams: Array<[string, string]> = [];
150
+ const searchParams = new URLSearchParams(search);
151
+ searchParams.forEach((value, key) => {
152
+ queryParams.push([key, value]);
153
+ });
154
+
155
+ // 按键名排序 - 大小写敏感,先大写字母,后小写字母
156
+ queryParams.sort(([a], [b]) => {
157
+ // AWS要求大小写敏感的ASCII排序
158
+ if (a < b) return -1;
159
+ if (a > b) return 1;
160
+ return 0;
161
+ });
162
+
163
+ // 构建规范查询字符串(不进行额外编码,因为URL中已经编码)
164
+ const canonicalQueryString = queryParams
165
+ .map(([key, value]) => `${key}=${value}`)
166
+ .join('&');
167
+
168
+ // 规范化头部 - 只包含必要的头部
169
+ const headersToSign: { [key: string]: string } = {
170
+ 'x-amz-date': timestamp
171
+ };
172
+
173
+ // 添加 session token
174
+ if (sessionToken) {
175
+ headersToSign['x-amz-security-token'] = sessionToken;
176
+ }
177
+
178
+ // 如果是POST请求且包含payload,添加content-sha256头
179
+ let payloadHash = crypto.createHash('sha256').update('').digest('hex'); // 默认空payload
180
+ if (method.toUpperCase() === 'POST' && payload) {
181
+ payloadHash = crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
182
+ headersToSign['x-amz-content-sha256'] = payloadHash;
183
+ }
184
+
185
+ const signedHeaders = Object.keys(headersToSign)
186
+ .map(key => key.toLowerCase())
187
+ .sort()
188
+ .join(';');
189
+
190
+ const canonicalHeaders = Object.keys(headersToSign)
191
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
192
+ .map(key => `${key.toLowerCase()}:${headersToSign[key].trim()}\n`)
193
+ .join('');
194
+
195
+ // 创建规范请求
196
+ const canonicalRequest = [
197
+ method.toUpperCase(),
198
+ pathname,
199
+ canonicalQueryString,
200
+ canonicalHeaders,
201
+ signedHeaders,
202
+ payloadHash
203
+ ].join('\n');
204
+
205
+ // 调试输出
206
+ logger.debug(`规范请求:
207
+ Method: ${method.toUpperCase()}
208
+ Path: ${pathname}
209
+ Query: ${canonicalQueryString}
210
+ Headers: ${canonicalHeaders}
211
+ SignedHeaders: ${signedHeaders}
212
+ PayloadHash: ${payloadHash}
213
+ ---完整规范请求---
214
+ ${canonicalRequest}
215
+ ---结束---`);
216
+
217
+ // 创建待签名字符串
218
+ const credentialScope = `${date}/${region}/${service}/aws4_request`;
219
+ const stringToSign = [
220
+ 'AWS4-HMAC-SHA256',
221
+ timestamp,
222
+ credentialScope,
223
+ crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex')
224
+ ].join('\n');
225
+
226
+ logger.debug(`待签名字符串:
227
+ ${stringToSign}`);
228
+
229
+ // 生成签名
230
+ const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(date).digest();
231
+ const kRegion = crypto.createHmac('sha256', kDate).update(region).digest();
232
+ const kService = crypto.createHmac('sha256', kRegion).update(service).digest();
233
+ const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest();
234
+ const signature = crypto.createHmac('sha256', kSigning).update(stringToSign, 'utf8').digest('hex');
235
+
236
+ return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
237
+ }
238
+
239
+ // 计算文件的CRC32值
240
+ function calculateCRC32(buffer: ArrayBuffer): string {
241
+ const crcTable = [];
242
+ for (let i = 0; i < 256; i++) {
243
+ let crc = i;
244
+ for (let j = 0; j < 8; j++) {
245
+ crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
246
+ }
247
+ crcTable[i] = crc;
248
+ }
249
+
250
+ let crc = 0 ^ (-1);
251
+ const bytes = new Uint8Array(buffer);
252
+ for (let i = 0; i < bytes.length; i++) {
253
+ crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
254
+ }
255
+ return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
256
+ }
257
+
258
+ // 图片上传功能:将外部图片URL上传到即梦系统
259
+ async function uploadImageFromUrl(imageUrl: string, refreshToken: string): Promise<string> {
260
+ try {
261
+ logger.info(`开始上传图片: ${imageUrl}`);
262
+
263
+ // 第一步:获取上传令牌
264
+ const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
265
+ data: {
266
+ scene: 2, // AIGC 图片上传场景
267
+ },
268
+ });
269
+
270
+ const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
271
+ if (!access_key_id || !secret_access_key || !session_token) {
272
+ throw new Error("获取上传令牌失败");
273
+ }
274
+
275
+ // 使用固定的service_id
276
+ const actualServiceId = service_id || "tb4s082cfz";
277
+
278
+ logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
279
+
280
+ // 下载图片数据
281
+ const imageResponse = await fetch(imageUrl);
282
+ if (!imageResponse.ok) {
283
+ throw new Error(`下载图片失败: ${imageResponse.status}`);
284
+ }
285
+
286
+ const imageBuffer = await imageResponse.arrayBuffer();
287
+ const fileSize = imageBuffer.byteLength;
288
+ const crc32 = calculateCRC32(imageBuffer);
289
+
290
+ logger.info(`图片下载完成: 大小=${fileSize}字节, CRC32=${crc32}`);
291
+
292
+ // 第二步:申请图片上传权限
293
+ // 使用UTC时间格式 YYYYMMDD'T'HHMMSS'Z'
294
+ const now = new Date();
295
+ const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
296
+
297
+ // 生成随机字符串作为���名参数
298
+ const randomStr = Math.random().toString(36).substring(2, 12);
299
+ // 保持原始的参数顺序(这是API期望的顺序)
300
+ const applyUrl = `https://imagex.bytedanceapi.com/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}`;
301
+
302
+ logger.debug(`原始URL: ${applyUrl}`);
303
+
304
+ // 构建AWS签名所需的头部
305
+ const requestHeaders = {
306
+ 'x-amz-date': timestamp,
307
+ 'x-amz-security-token': session_token
308
+ };
309
+
310
+ // 生成AWS签名
311
+ const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
312
+
313
+ // 调试日志
314
+ logger.info(`AWS签名调试信息:
315
+ URL: ${applyUrl}
316
+ AccessKeyId: ${access_key_id}
317
+ SessionToken: ${session_token ? '存在' : '不存在'}
318
+ Timestamp: ${timestamp}
319
+ Authorization: ${authorization}
320
+ `);
321
+
322
+ const applyResponse = await fetch(applyUrl, {
323
+ method: 'GET',
324
+ headers: {
325
+ 'accept': '*/*',
326
+ 'accept-language': 'zh-CN,zh;q=0.9',
327
+ 'authorization': authorization,
328
+ 'origin': 'https://jimeng.jianying.com',
329
+ 'referer': 'https://jimeng.jianying.com/ai-tool/generate',
330
+ 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
331
+ 'sec-ch-ua-mobile': '?0',
332
+ 'sec-ch-ua-platform': '"Windows"',
333
+ 'sec-fetch-dest': 'empty',
334
+ 'sec-fetch-mode': 'cors',
335
+ 'sec-fetch-site': 'cross-site',
336
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
337
+ 'x-amz-date': timestamp,
338
+ 'x-amz-security-token': session_token,
339
+ },
340
+ });
341
+
342
+ if (!applyResponse.ok) {
343
+ const errorText = await applyResponse.text();
344
+ throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`);
345
+ }
346
+
347
+ const applyResult = await applyResponse.json();
348
+
349
+ // 检查是否有错误
350
+ if (applyResult?.ResponseMetadata?.Error) {
351
+ throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`);
352
+ }
353
+
354
+ logger.info(`申请上传权限成功`);
355
+
356
+ // 解析上传信息
357
+ const uploadAddress = applyResult?.Result?.UploadAddress;
358
+ if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) {
359
+ throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`);
360
+ }
361
+
362
+ const storeInfo = uploadAddress.StoreInfos[0];
363
+ const uploadHost = uploadAddress.UploadHosts[0];
364
+ const auth = storeInfo.Auth;
365
+
366
+ // 构建上传URL
367
+ const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`;
368
+
369
+ // 提取图片ID (StoreUri最后一个斜杠后的部分)
370
+ const imageId = storeInfo.StoreUri.split('/').pop();
371
+
372
+ logger.info(`准备上传图片: imageId=${imageId}, uploadUrl=${uploadUrl}`);
373
+
374
+ // 第三步:上传图片文件
375
+ const uploadResponse = await fetch(uploadUrl, {
376
+ method: 'POST',
377
+ headers: {
378
+ 'Accept': '*/*',
379
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
380
+ 'Authorization': auth,
381
+ 'Connection': 'keep-alive',
382
+ 'Content-CRC32': crc32,
383
+ 'Content-Disposition': 'attachment; filename="undefined"',
384
+ 'Content-Type': 'application/octet-stream',
385
+ 'Origin': 'https://jimeng.jianying.com',
386
+ 'Referer': 'https://jimeng.jianying.com/ai-tool/generate',
387
+ 'Sec-Fetch-Dest': 'empty',
388
+ 'Sec-Fetch-Mode': 'cors',
389
+ 'Sec-Fetch-Site': 'cross-site',
390
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
391
+ 'X-Storage-U': '704135154117550', // 用户ID,可以从token或其他地方获取
392
+ },
393
+ body: imageBuffer,
394
+ });
395
+
396
+ if (!uploadResponse.ok) {
397
+ const errorText = await uploadResponse.text();
398
+ throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`);
399
+ }
400
+
401
+ logger.info(`图片文件上传成功`);
402
+
403
+ // 第四步:提交上传
404
+ const commitUrl = `https://imagex.bytedanceapi.com/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
405
+
406
+ const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
407
+ const commitPayload = JSON.stringify({
408
+ SessionKey: uploadAddress.SessionKey,
409
+ SuccessActionStatus: "200"
410
+ });
411
+
412
+ // 计算payload的SHA256哈希值
413
+ const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
414
+
415
+ // 构建AWS签名所需的头部
416
+ const commitRequestHeaders = {
417
+ 'x-amz-date': commitTimestamp,
418
+ 'x-amz-security-token': session_token,
419
+ 'x-amz-content-sha256': payloadHash
420
+ };
421
+
422
+ // 生成AWS签名
423
+ const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload);
424
+
425
+ const commitResponse = await fetch(commitUrl, {
426
+ method: 'POST',
427
+ headers: {
428
+ 'accept': '*/*',
429
+ 'accept-language': 'zh-CN,zh;q=0.9',
430
+ 'authorization': commitAuthorization,
431
+ 'content-type': 'application/json',
432
+ 'origin': 'https://jimeng.jianying.com',
433
+ 'referer': 'https://jimeng.jianying.com/ai-tool/generate',
434
+ 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
435
+ 'sec-ch-ua-mobile': '?0',
436
+ 'sec-ch-ua-platform': '"Windows"',
437
+ 'sec-fetch-dest': 'empty',
438
+ 'sec-fetch-mode': 'cors',
439
+ 'sec-fetch-site': 'cross-site',
440
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
441
+ 'x-amz-date': commitTimestamp,
442
+ 'x-amz-security-token': session_token,
443
+ 'x-amz-content-sha256': payloadHash,
444
+ },
445
+ body: commitPayload,
446
+ });
447
+
448
+ if (!commitResponse.ok) {
449
+ const errorText = await commitResponse.text();
450
+ throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`);
451
+ }
452
+
453
+ const commitResult = await commitResponse.json();
454
+
455
+ // 检查提交结果
456
+ if (commitResult?.ResponseMetadata?.Error) {
457
+ throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`);
458
+ }
459
+
460
+ if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) {
461
+ throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`);
462
+ }
463
+
464
+ const uploadResult = commitResult.Result.Results[0];
465
+ if (uploadResult.UriStatus !== 2000) {
466
+ throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`);
467
+ }
468
+
469
+ // 获取完整的URI(包含前缀)
470
+ const fullImageUri = uploadResult.Uri; // 如: "tos-cn-i-tb4s082cfz/bab623359bd9410da0c1f07897b16fec"
471
+
472
+ // 验证图片信息
473
+ const pluginResult = commitResult.Result?.PluginResult?.[0];
474
+ if (pluginResult) {
475
+ logger.info(`图片上传成功详情:`, {
476
+ imageUri: pluginResult.ImageUri,
477
+ sourceUri: pluginResult.SourceUri,
478
+ size: `${pluginResult.ImageWidth}x${pluginResult.ImageHeight}`,
479
+ format: pluginResult.ImageFormat,
480
+ fileSize: pluginResult.ImageSize,
481
+ md5: pluginResult.ImageMd5
482
+ });
483
+
484
+ // 优先使用PluginResult中的ImageUri,因为它可能是最准确的
485
+ if (pluginResult.ImageUri) {
486
+ logger.info(`图片上传完成: ${pluginResult.ImageUri}`);
487
+ return pluginResult.ImageUri; // 返回完整的URI
488
+ }
489
+ }
490
+
491
+ logger.info(`图片上传完成: ${fullImageUri}`);
492
+ return fullImageUri; // 返回完整的URI
493
+
494
+ } catch (error) {
495
+ logger.error(`图片上传失败: ${error.message}`);
496
+ throw error;
497
+ }
498
+ }
499
+
500
+ // 从Buffer上传图片
501
+ async function uploadImageBuffer(buffer: Buffer, refreshToken: string): Promise<string> {
502
+ try {
503
+ logger.info(`开始从Buffer上传图片,大小: ${buffer.length}字节`);
504
+
505
+ // 获取上传凭证
506
+ const proofResult = await request(
507
+ 'POST',
508
+ '/mweb/v1/get_upload_image_proof',
509
+ refreshToken,
510
+ {
511
+ data: {
512
+ scene: 'aigc_image',
513
+ file_name: `${util.uuid()}.jpg`,
514
+ file_size: buffer.length,
515
+ }
516
+ }
517
+ );
518
+
519
+ if (!proofResult || !proofResult.proof_info) {
520
+ logger.error(`获取上传凭证失败: ${JSON.stringify(proofResult)}`);
521
+ throw new APIException(EX.API_REQUEST_FAILED, '获取上传凭证失败');
522
+ }
523
+
524
+ logger.info(`获取上传凭证成功`);
525
+
526
+ // 上传文件
527
+ const { proof_info } = proofResult;
528
+ const uploadProofUrl = 'https://imagex.bytedanceapi.com/';
529
+
530
+ const formData = new FormData();
531
+ const blob = new Blob([buffer], { type: 'image/jpeg' });
532
+ formData.append('file', blob, `${util.uuid()}.jpg`);
533
+
534
+ const uploadResult = await fetch(uploadProofUrl + '?' + new URLSearchParams(proof_info.query_params).toString(), {
535
+ method: 'POST',
536
+ headers: proof_info.headers,
537
+ body: formData,
538
+ });
539
+
540
+ if (!uploadResult.ok) {
541
+ logger.error(`上传文件失败: 状态码 ${uploadResult.status}`);
542
+ throw new APIException(EX.API_REQUEST_FAILED, `上传文件失败: 状态码 ${uploadResult.status}`);
543
+ }
544
+
545
+ // 验证 proof_info.image_uri 是否存在
546
+ if (!proof_info.image_uri) {
547
+ logger.error(`上传凭证中缺少 image_uri: ${JSON.stringify(proof_info)}`);
548
+ throw new APIException(EX.API_REQUEST_FAILED, '上传凭证中缺少 image_uri');
549
+ }
550
+
551
+ logger.info(`Buffer图片上传成功: ${proof_info.image_uri}`);
552
+ return proof_info.image_uri;
553
+ } catch (error) {
554
+ logger.error(`Buffer图片上传失败: ${error.message}`);
555
+ throw error;
556
+ }
557
+ }
558
+
559
+ // 图片合成功能:先上传图片,然后进行图生图
560
+ export async function generateImageComposition(
561
+ _model: string,
562
+ prompt: string,
563
+ imageUrls: (string | Buffer)[],
564
+ {
565
+ ratio = "1:1",
566
+ resolution = "2k",
567
+ sampleStrength = 0.5,
568
+ negativePrompt = "",
569
+ intelligentRatio = false,
570
+ }: {
571
+ ratio?: string;
572
+ resolution?: string;
573
+ sampleStrength?: number;
574
+ negativePrompt?: string;
575
+ intelligentRatio?: boolean;
576
+ },
577
+ refreshToken: string
578
+ ) {
579
+ const model = getModel(_model);
580
+ const draftVersion = getDraftVersion(_model);
581
+ const imageCount = imageUrls.length;
582
+
583
+ // 解析分辨率
584
+ const resolutionResult = resolveResolution(resolution, ratio);
585
+ const { width, height, imageRatio, resolutionType } = resolutionResult;
586
+
587
+ logger.info(`使用模型: ${_model} 映射模型: ${model} 图生图功能 ${imageCount}张图片 ${width}x${height} (${ratio}@${resolution}) 精细度: ${sampleStrength}`);
588
+
589
+ const { totalCredit } = await getCredit(refreshToken);
590
+ if (totalCredit <= 0)
591
+ await receiveCredit(refreshToken);
592
+
593
+ // 上传所有输入图片
594
+ const uploadedImageIds: string[] = [];
595
+ for (let i = 0; i < imageUrls.length; i++) {
596
+ try {
597
+ const image = imageUrls[i];
598
+ let imageId: string;
599
+ if (typeof image === 'string') {
600
+ logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (URL)...`);
601
+ imageId = await uploadImageFromUrl(image, refreshToken);
602
+ } else {
603
+ logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (Buffer)...`);
604
+ imageId = await uploadImageBuffer(image, refreshToken);
605
+ }
606
+ uploadedImageIds.push(imageId);
607
+ logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`);
608
+ } catch (error) {
609
+ logger.error(`图片 ${i + 1}/${imageCount} 上传失败: ${error.message}`);
610
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `图片上传失败: ${error.message}`);
611
+ }
612
+ }
613
+
614
+ logger.info(`所有图片上传完成,开始图生图: ${uploadedImageIds.join(', ')}`);
615
+
616
+ const componentId = util.uuid();
617
+ const submitId = util.uuid();
618
+
619
+ // 构建图生图的 sceneOptions(不包含 benefitCount 以避免扣积分)
620
+ // 注意:sceneOptions 需要是对象,在 metrics_extra 中会被 JSON.stringify
621
+ const sceneOption = {
622
+ type: "image",
623
+ scene: "ImageBasicGenerate",
624
+ modelReqKey: _model,
625
+ resolutionType,
626
+ abilityList: uploadedImageIds.map(() => ({
627
+ abilityName: "byte_edit",
628
+ strength: sampleStrength,
629
+ source: {
630
+ imageUrl: `blob:https://jimeng.jianying.com/${util.uuid()}`
631
+ }
632
+ })),
633
+ reportParams: {
634
+ enterSource: "generate",
635
+ vipSource: "generate",
636
+ extraVipFunctionKey: `${_model}-${resolutionType}`,
637
+ useVipFunctionDetailsReporterHoc: true,
638
+ },
639
+ };
640
+
641
+ const { aigc_data } = await request(
642
+ "post",
643
+ "/mweb/v1/aigc_draft/generate",
644
+ refreshToken,
645
+ {
646
+ data: {
647
+ extend: {
648
+ root_model: model,
649
+ },
650
+ submit_id: submitId,
651
+ metrics_extra: JSON.stringify({
652
+ promptSource: "custom",
653
+ generateCount: 1,
654
+ enterFrom: "click",
655
+ sceneOptions: JSON.stringify([sceneOption]),
656
+ generateId: submitId,
657
+ isRegenerate: false
658
+ }),
659
+ draft_content: JSON.stringify({
660
+ type: "draft",
661
+ id: util.uuid(),
662
+ min_version: "3.2.9",
663
+ min_features: [],
664
+ is_from_tsn: true,
665
+ version: "3.2.9",
666
+ main_component_id: componentId,
667
+ component_list: [
668
+ {
669
+ type: "image_base_component",
670
+ id: componentId,
671
+ min_version: "3.0.2",
672
+ aigc_mode: "workbench",
673
+ metadata: {
674
+ type: "",
675
+ id: util.uuid(),
676
+ created_platform: 3,
677
+ created_platform_version: "",
678
+ created_time_in_ms: Date.now().toString(),
679
+ created_did: "",
680
+ },
681
+ generate_type: "blend",
682
+ abilities: {
683
+ type: "",
684
+ id: util.uuid(),
685
+ blend: {
686
+ type: "",
687
+ id: util.uuid(),
688
+ min_version: "3.2.9",
689
+ min_features: [],
690
+ core_param: {
691
+ type: "",
692
+ id: util.uuid(),
693
+ model,
694
+ prompt: `${'#'.repeat(imageCount * 2)}${prompt}`,
695
+ sample_strength: sampleStrength,
696
+ image_ratio: imageRatio,
697
+ large_image_info: {
698
+ type: "",
699
+ id: util.uuid(),
700
+ height,
701
+ width,
702
+ resolution_type: resolutionType
703
+ },
704
+ intelligent_ratio: intelligentRatio,
705
+ },
706
+ ability_list: uploadedImageIds.map((imageId) => ({
707
+ type: "",
708
+ id: util.uuid(),
709
+ name: "byte_edit",
710
+ image_uri_list: [imageId],
711
+ image_list: [{
712
+ type: "image",
713
+ id: util.uuid(),
714
+ source_from: "upload",
715
+ platform_type: 1,
716
+ name: "",
717
+ image_uri: imageId,
718
+ width: 0,
719
+ height: 0,
720
+ format: "",
721
+ uri: imageId
722
+ }],
723
+ strength: 0.5
724
+ })),
725
+ prompt_placeholder_info_list: uploadedImageIds.map((_, index) => ({
726
+ type: "",
727
+ id: util.uuid(),
728
+ ability_index: index
729
+ })),
730
+ postedit_param: {
731
+ type: "",
732
+ id: util.uuid(),
733
+ generate_type: 0
734
+ }
735
+ },
736
+ },
737
+ },
738
+ ],
739
+ }),
740
+ http_common_info: {
741
+ aid: DEFAULT_ASSISTANT_ID,
742
+ },
743
+ },
744
+ }
745
+ );
746
+
747
+ const historyId = aigc_data?.history_record_id;
748
+ if (!historyId)
749
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
750
+
751
+ logger.info(`图生图任务已提交,history_id: ${historyId},等待生成完成...`);
752
+
753
+ let status = 20, failCode, item_list = [];
754
+ let pollCount = 0;
755
+ const maxPollCount = 600; // 最多轮询10分钟
756
+
757
+ while (pollCount < maxPollCount) {
758
+ await new Promise((resolve) => setTimeout(resolve, 1000));
759
+ pollCount++;
760
+
761
+ if (pollCount % 30 === 0) {
762
+ logger.info(`图生图进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length} 张图片...`);
763
+ }
764
+
765
+ const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
766
+ data: {
767
+ history_ids: [historyId],
768
+ image_info: {
769
+ width: 2048,
770
+ height: 2048,
771
+ format: "webp",
772
+ image_scene_list: [
773
+ {
774
+ scene: "smart_crop",
775
+ width: 360,
776
+ height: 360,
777
+ uniq_key: "smart_crop-w:360-h:360",
778
+ format: "webp",
779
+ },
780
+ {
781
+ scene: "smart_crop",
782
+ width: 480,
783
+ height: 480,
784
+ uniq_key: "smart_crop-w:480-h:480",
785
+ format: "webp",
786
+ },
787
+ {
788
+ scene: "smart_crop",
789
+ width: 720,
790
+ height: 720,
791
+ uniq_key: "smart_crop-w:720-h:720",
792
+ format: "webp",
793
+ },
794
+ {
795
+ scene: "smart_crop",
796
+ width: 720,
797
+ height: 480,
798
+ uniq_key: "smart_crop-w:720-h:480",
799
+ format: "webp",
800
+ },
801
+ {
802
+ scene: "normal",
803
+ width: 2400,
804
+ height: 2400,
805
+ uniq_key: "2400",
806
+ format: "webp",
807
+ },
808
+ {
809
+ scene: "normal",
810
+ width: 1080,
811
+ height: 1080,
812
+ uniq_key: "1080",
813
+ format: "webp",
814
+ },
815
+ {
816
+ scene: "normal",
817
+ width: 720,
818
+ height: 720,
819
+ uniq_key: "720",
820
+ format: "webp",
821
+ },
822
+ {
823
+ scene: "normal",
824
+ width: 480,
825
+ height: 480,
826
+ uniq_key: "480",
827
+ format: "webp",
828
+ },
829
+ {
830
+ scene: "normal",
831
+ width: 360,
832
+ height: 360,
833
+ uniq_key: "360",
834
+ format: "webp",
835
+ },
836
+ ],
837
+ },
838
+ http_common_info: {
839
+ aid: DEFAULT_ASSISTANT_ID,
840
+ },
841
+ },
842
+ });
843
+
844
+ if (!result[historyId])
845
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
846
+
847
+ status = result[historyId].status;
848
+ failCode = result[historyId].fail_code;
849
+ item_list = result[historyId].item_list || [];
850
+
851
+ // 检查是否已生成图片
852
+ if (item_list.length > 0) {
853
+ logger.info(`图生图完成: 状态=${status}, 已生成 ${item_list.length} 张图片`);
854
+ break;
855
+ }
856
+
857
+ // 记录详细状态
858
+ if (pollCount % 60 === 0) {
859
+ logger.info(`图生图详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`);
860
+ }
861
+
862
+ // 如果状态是完成但图片数量为0,记录并继续等待
863
+ if (status === 10 && item_list.length === 0 && pollCount % 30 === 0) {
864
+ logger.info(`图生图状态已完成但无图片生成: 状态=${status}, 继续等待...`);
865
+ }
866
+ }
867
+
868
+ if (pollCount >= maxPollCount) {
869
+ logger.warn(`图生图超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`);
870
+ }
871
+
872
+ if (status === 30) {
873
+ if (failCode === '2038')
874
+ throw new APIException(EX.API_CONTENT_FILTERED);
875
+ else
876
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `图生图失败,错误代码: ${failCode}`);
877
+ }
878
+
879
+ const resultImageUrls = item_list.map((item) => {
880
+ if(!item?.image?.large_images?.[0]?.image_url)
881
+ return item?.common_attr?.cover_url || null;
882
+ return item.image.large_images[0].image_url;
883
+ }).filter(url => url !== null);
884
+
885
+ logger.info(`图生图结果: 成功生成 ${resultImageUrls.length} 张图片`);
886
+ return resultImageUrls;
887
+ }
888
+
889
+ // 多图生成函数(支持jimeng-4.0及以上版本)
890
+ async function generateMultiImages(
891
+ _model: string,
892
+ prompt: string,
893
+ {
894
+ ratio = "1:1",
895
+ resolution = "2k",
896
+ sampleStrength = 0.5,
897
+ negativePrompt = "",
898
+ intelligentRatio = false,
899
+ }: {
900
+ ratio?: string;
901
+ resolution?: string;
902
+ sampleStrength?: number;
903
+ negativePrompt?: string;
904
+ intelligentRatio?: boolean;
905
+ },
906
+ refreshToken: string
907
+ ) {
908
+ const model = getModel(_model);
909
+
910
+ // 解析分辨率
911
+ const resolutionResult = resolveResolution(resolution, ratio);
912
+ const { width, height, imageRatio, resolutionType } = resolutionResult;
913
+
914
+ // 从prompt中提取图片数量,默认为4张
915
+ const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4;
916
+
917
+ logger.info(`使用 ${_model} 多图生成: ${targetImageCount}张图片 ${width}x${height} (${ratio}@${resolution}) 精细度: ${sampleStrength}`);
918
+
919
+ const componentId = util.uuid();
920
+ const submitId = util.uuid();
921
+
922
+ // 构建多图模式的 sceneOptions(不包含 benefitCount 以避免扣积分)
923
+ const sceneOption = {
924
+ type: "image",
925
+ scene: "ImageMultiGenerate",
926
+ modelReqKey: _model,
927
+ resolutionType,
928
+ abilityList: [],
929
+ reportParams: {
930
+ enterSource: "generate",
931
+ vipSource: "generate",
932
+ extraVipFunctionKey: `${_model}-${resolutionType}`,
933
+ useVipFunctionDetailsReporterHoc: true,
934
+ },
935
+ };
936
+
937
+ const { aigc_data } = await request(
938
+ "post",
939
+ "/mweb/v1/aigc_draft/generate",
940
+ refreshToken,
941
+ {
942
+ data: {
943
+ extend: {
944
+ root_model: model,
945
+ },
946
+ submit_id: submitId,
947
+ metrics_extra: JSON.stringify({
948
+ promptSource: "custom",
949
+ generateCount: 1,
950
+ enterFrom: "click",
951
+ sceneOptions: JSON.stringify([sceneOption]),
952
+ generateId: submitId,
953
+ isRegenerate: false,
954
+ templateId: "",
955
+ templateSource: "",
956
+ lastRequestId: "",
957
+ originRequestId: "",
958
+ }),
959
+ draft_content: JSON.stringify({
960
+ type: "draft",
961
+ id: util.uuid(),
962
+ min_version: DRAFT_MIN_VERSION,
963
+ min_features: [],
964
+ is_from_tsn: true,
965
+ version: DRAFT_VERSION,
966
+ main_component_id: componentId,
967
+ component_list: [
968
+ {
969
+ type: "image_base_component",
970
+ id: componentId,
971
+ min_version: DRAFT_MIN_VERSION,
972
+ aigc_mode: "workbench",
973
+ metadata: {
974
+ type: "",
975
+ id: util.uuid(),
976
+ created_platform: 3,
977
+ created_platform_version: "",
978
+ created_time_in_ms: Date.now().toString(),
979
+ created_did: "",
980
+ },
981
+ generate_type: "generate",
982
+ abilities: {
983
+ type: "",
984
+ id: util.uuid(),
985
+ generate: {
986
+ type: "",
987
+ id: util.uuid(),
988
+ core_param: {
989
+ type: "",
990
+ id: util.uuid(),
991
+ model,
992
+ prompt,
993
+ negative_prompt: negativePrompt,
994
+ seed: Math.floor(Math.random() * 100000000) + 2500000000,
995
+ sample_strength: sampleStrength,
996
+ image_ratio: imageRatio,
997
+ large_image_info: {
998
+ type: "",
999
+ id: util.uuid(),
1000
+ min_version: DRAFT_MIN_VERSION,
1001
+ height,
1002
+ width,
1003
+ resolution_type: resolutionType,
1004
+ },
1005
+ intelligent_ratio: intelligentRatio,
1006
+ },
1007
+ gen_option: {
1008
+ type: "",
1009
+ id: util.uuid(),
1010
+ generate_all: false,
1011
+ },
1012
+ },
1013
+ },
1014
+ },
1015
+ ],
1016
+ }),
1017
+ http_common_info: {
1018
+ aid: DEFAULT_ASSISTANT_ID,
1019
+ },
1020
+ },
1021
+ }
1022
+ );
1023
+
1024
+ const historyId = aigc_data?.history_record_id;
1025
+ if (!historyId)
1026
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
1027
+
1028
+ logger.info(`多图生成任务已提交,submit_id: ${submitId}, history_id: ${historyId},等待生成 ${targetImageCount} 张图片...`);
1029
+
1030
+ // 直接使用 history_id 轮询生成结果(增加轮询时间)
1031
+ let status = 20, failCode, item_list = [];
1032
+ let pollCount = 0;
1033
+ const maxPollCount = 600; // 最多轮询10分钟(600次 * 1秒)
1034
+
1035
+ while (pollCount < maxPollCount) {
1036
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // 每1秒轮询一次
1037
+ pollCount++;
1038
+
1039
+ if (pollCount % 30 === 0) {
1040
+ logger.info(`多图生成进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length}/${targetImageCount} 张图片...`);
1041
+ }
1042
+
1043
+ const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
1044
+ data: {
1045
+ history_ids: [historyId],
1046
+ image_info: {
1047
+ width: 2048,
1048
+ height: 2048,
1049
+ format: "webp",
1050
+ image_scene_list: [
1051
+ {
1052
+ scene: "smart_crop",
1053
+ width: 360,
1054
+ height: 360,
1055
+ uniq_key: "smart_crop-w:360-h:360",
1056
+ format: "webp",
1057
+ },
1058
+ {
1059
+ scene: "smart_crop",
1060
+ width: 480,
1061
+ height: 480,
1062
+ uniq_key: "smart_crop-w:480-h:480",
1063
+ format: "webp",
1064
+ },
1065
+ {
1066
+ scene: "smart_crop",
1067
+ width: 720,
1068
+ height: 720,
1069
+ uniq_key: "smart_crop-w:720-h:720",
1070
+ format: "webp",
1071
+ },
1072
+ {
1073
+ scene: "smart_crop",
1074
+ width: 720,
1075
+ height: 480,
1076
+ uniq_key: "smart_crop-w:720-h:480",
1077
+ format: "webp",
1078
+ },
1079
+ {
1080
+ scene: "normal",
1081
+ width: 2400,
1082
+ height: 2400,
1083
+ uniq_key: "2400",
1084
+ format: "webp",
1085
+ },
1086
+ {
1087
+ scene: "normal",
1088
+ width: 1080,
1089
+ height: 1080,
1090
+ uniq_key: "1080",
1091
+ format: "webp",
1092
+ },
1093
+ {
1094
+ scene: "normal",
1095
+ width: 720,
1096
+ height: 720,
1097
+ uniq_key: "720",
1098
+ format: "webp",
1099
+ },
1100
+ {
1101
+ scene: "normal",
1102
+ width: 480,
1103
+ height: 480,
1104
+ uniq_key: "480",
1105
+ format: "webp",
1106
+ },
1107
+ {
1108
+ scene: "normal",
1109
+ width: 360,
1110
+ height: 360,
1111
+ uniq_key: "360",
1112
+ format: "webp",
1113
+ },
1114
+ ],
1115
+ },
1116
+ http_common_info: {
1117
+ aid: DEFAULT_ASSISTANT_ID,
1118
+ },
1119
+ },
1120
+ });
1121
+
1122
+ if (!result[historyId])
1123
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
1124
+
1125
+ status = result[historyId].status;
1126
+ failCode = result[historyId].fail_code;
1127
+ item_list = result[historyId].item_list || [];
1128
+
1129
+ // 检查是否已生成足够的图片
1130
+ if (item_list.length >= targetImageCount) {
1131
+ logger.info(`多图生成完成: 状态=${status}, 已生成 ${item_list.length} 张图片`);
1132
+ break;
1133
+ }
1134
+
1135
+ // 记录详细状态
1136
+ if (pollCount % 60 === 0) {
1137
+ logger.info(`jimeng-4.0 详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`);
1138
+ }
1139
+
1140
+ // 如果状态是完成但图片数量不够,记录并继续等待
1141
+ if (status === 10 && item_list.length < targetImageCount && pollCount % 30 === 0) {
1142
+ logger.info(`jimeng-4.0 状态已完成但图片数量不足: 状态=${status}, 已生成 ${item_list.length}/${targetImageCount} 张图片,继续等待...`);
1143
+ }
1144
+ }
1145
+
1146
+ if (pollCount >= maxPollCount) {
1147
+ logger.warn(`多图生成超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`);
1148
+ }
1149
+
1150
+ if (status === 30) {
1151
+ if (failCode === '2038')
1152
+ throw new APIException(EX.API_CONTENT_FILTERED);
1153
+ else
1154
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误代码: ${failCode}`);
1155
+ }
1156
+
1157
+ const imageUrls = item_list.map((item) => {
1158
+ if(!item?.image?.large_images?.[0]?.image_url)
1159
+ return item?.common_attr?.cover_url || null;
1160
+ return item.image.large_images[0].image_url;
1161
+ }).filter(url => url !== null);
1162
+
1163
+ logger.info(`多图生成结果: 成功生成 ${imageUrls.length} 张图片`);
1164
+ return imageUrls;
1165
+ }
1166
+
1167
+ export async function generateImages(
1168
+ _model: string,
1169
+ prompt: string,
1170
+ {
1171
+ ratio = "1:1",
1172
+ resolution = "2k",
1173
+ sampleStrength = 0.5,
1174
+ negativePrompt = "",
1175
+ intelligentRatio = false,
1176
+ }: {
1177
+ ratio?: string;
1178
+ resolution?: string;
1179
+ sampleStrength?: number;
1180
+ negativePrompt?: string;
1181
+ intelligentRatio?: boolean;
1182
+ },
1183
+ refreshToken: string
1184
+ ) {
1185
+ const model = getModel(_model);
1186
+
1187
+ // 解析分辨率
1188
+ const resolutionResult = resolveResolution(resolution, ratio);
1189
+ const { width, height, imageRatio, resolutionType } = resolutionResult;
1190
+
1191
+ logger.info(`使用模型: ${_model} 映射模型: ${model} ${width}x${height} (${ratio}@${resolution}) ���细度: ${sampleStrength}`);
1192
+
1193
+
1194
+ const { totalCredit } = await getCredit(refreshToken);
1195
+ if (totalCredit <= 0)
1196
+ await receiveCredit(refreshToken);
1197
+
1198
+ // 检测是否为多图生成请求
1199
+ const isMultiImageRequest = (/jimeng-4\.[0-9]+/.test(_model)) && (
1200
+ prompt.includes("连续") ||
1201
+ prompt.includes("绘本") ||
1202
+ prompt.includes("故事") ||
1203
+ /\d+张/.test(prompt)
1204
+ );
1205
+
1206
+ // 如果是多图请求,使用专门的处理逻辑
1207
+ if (isMultiImageRequest) {
1208
+ return await generateMultiImages(_model, prompt, { ratio, resolution, sampleStrength, negativePrompt, intelligentRatio }, refreshToken);
1209
+ }
1210
+
1211
+ const componentId = util.uuid();
1212
+ const submitId = util.uuid();
1213
+
1214
+ // 构建 sceneOptions 用于 metrics_extra(不包含 benefitCount 以避免扣积分)
1215
+ const sceneOption = {
1216
+ type: "image",
1217
+ scene: "ImageBasicGenerate",
1218
+ modelReqKey: _model,
1219
+ resolutionType,
1220
+ abilityList: [],
1221
+ reportParams: {
1222
+ enterSource: "generate",
1223
+ vipSource: "generate",
1224
+ extraVipFunctionKey: `${_model}-${resolutionType}`,
1225
+ useVipFunctionDetailsReporterHoc: true,
1226
+ },
1227
+ };
1228
+
1229
+ const { aigc_data } = await request(
1230
+ "post",
1231
+ "/mweb/v1/aigc_draft/generate",
1232
+ refreshToken,
1233
+ {
1234
+ data: {
1235
+ extend: {
1236
+ root_model: model,
1237
+ },
1238
+ submit_id: submitId,
1239
+ metrics_extra: JSON.stringify({
1240
+ promptSource: "custom",
1241
+ generateCount: 1,
1242
+ enterFrom: "click",
1243
+ sceneOptions: JSON.stringify([sceneOption]),
1244
+ generateId: submitId,
1245
+ isRegenerate: false,
1246
+ }),
1247
+ draft_content: JSON.stringify({
1248
+ type: "draft",
1249
+ id: util.uuid(),
1250
+ min_version: DRAFT_MIN_VERSION,
1251
+ min_features: [],
1252
+ is_from_tsn: true,
1253
+ version: DRAFT_VERSION,
1254
+ main_component_id: componentId,
1255
+ component_list: [
1256
+ {
1257
+ type: "image_base_component",
1258
+ id: componentId,
1259
+ min_version: DRAFT_MIN_VERSION,
1260
+ aigc_mode: "workbench",
1261
+ metadata: {
1262
+ type: "",
1263
+ id: util.uuid(),
1264
+ created_platform: 3,
1265
+ created_platform_version: "",
1266
+ created_time_in_ms: Date.now().toString(),
1267
+ created_did: "",
1268
+ },
1269
+ generate_type: "generate",
1270
+ abilities: {
1271
+ type: "",
1272
+ id: util.uuid(),
1273
+ generate: {
1274
+ type: "",
1275
+ id: util.uuid(),
1276
+ core_param: {
1277
+ type: "",
1278
+ id: util.uuid(),
1279
+ model,
1280
+ prompt,
1281
+ negative_prompt: negativePrompt,
1282
+ seed: Math.floor(Math.random() * 100000000) + 2500000000,
1283
+ sample_strength: sampleStrength,
1284
+ image_ratio: imageRatio,
1285
+ large_image_info: {
1286
+ type: "",
1287
+ id: util.uuid(),
1288
+ min_version: DRAFT_MIN_VERSION,
1289
+ height,
1290
+ width,
1291
+ resolution_type: resolutionType,
1292
+ },
1293
+ intelligent_ratio: intelligentRatio,
1294
+ },
1295
+ gen_option: {
1296
+ type: "",
1297
+ id: util.uuid(),
1298
+ generate_all: false,
1299
+ },
1300
+ },
1301
+ },
1302
+ },
1303
+ ],
1304
+ }),
1305
+ http_common_info: {
1306
+ aid: DEFAULT_ASSISTANT_ID,
1307
+ },
1308
+ },
1309
+ }
1310
+ );
1311
+ const historyId = aigc_data.history_record_id;
1312
+ if (!historyId)
1313
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
1314
+
1315
+ logger.info(`文生图任务已提交,submit_id: ${submitId}, history_id: ${historyId},等待生成完成...`);
1316
+
1317
+ let status = 20, failCode, item_list = [];
1318
+ let pollCount = 0;
1319
+ const maxPollCount = 600; // 最多轮询10分钟
1320
+
1321
+ while (pollCount < maxPollCount) {
1322
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1323
+ pollCount++;
1324
+
1325
+ if (pollCount % 30 === 0) {
1326
+ logger.info(`文生图进度: 第 ${pollCount} 次轮询 (history_id: ${historyId}),当前状态: ${status},已生成: ${item_list.length} 张图片...`);
1327
+ }
1328
+
1329
+ const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
1330
+ data: {
1331
+ history_ids: [historyId],
1332
+ image_info: {
1333
+ width: 2048,
1334
+ height: 2048,
1335
+ format: "webp",
1336
+ image_scene_list: [
1337
+ {
1338
+ scene: "smart_crop",
1339
+ width: 360,
1340
+ height: 360,
1341
+ uniq_key: "smart_crop-w:360-h:360",
1342
+ format: "webp",
1343
+ },
1344
+ {
1345
+ scene: "smart_crop",
1346
+ width: 480,
1347
+ height: 480,
1348
+ uniq_key: "smart_crop-w:480-h:480",
1349
+ format: "webp",
1350
+ },
1351
+ {
1352
+ scene: "smart_crop",
1353
+ width: 720,
1354
+ height: 720,
1355
+ uniq_key: "smart_crop-w:720-h:720",
1356
+ format: "webp",
1357
+ },
1358
+ {
1359
+ scene: "smart_crop",
1360
+ width: 720,
1361
+ height: 480,
1362
+ uniq_key: "smart_crop-w:720-h:480",
1363
+ format: "webp",
1364
+ },
1365
+ {
1366
+ scene: "smart_crop",
1367
+ width: 360,
1368
+ height: 240,
1369
+ uniq_key: "smart_crop-w:360-h:240",
1370
+ format: "webp",
1371
+ },
1372
+ {
1373
+ scene: "smart_crop",
1374
+ width: 240,
1375
+ height: 320,
1376
+ uniq_key: "smart_crop-w:240-h:320",
1377
+ format: "webp",
1378
+ },
1379
+ {
1380
+ scene: "smart_crop",
1381
+ width: 480,
1382
+ height: 640,
1383
+ uniq_key: "smart_crop-w:480-h:640",
1384
+ format: "webp",
1385
+ },
1386
+ {
1387
+ scene: "normal",
1388
+ width: 2400,
1389
+ height: 2400,
1390
+ uniq_key: "2400",
1391
+ format: "webp",
1392
+ },
1393
+ {
1394
+ scene: "normal",
1395
+ width: 1080,
1396
+ height: 1080,
1397
+ uniq_key: "1080",
1398
+ format: "webp",
1399
+ },
1400
+ {
1401
+ scene: "normal",
1402
+ width: 720,
1403
+ height: 720,
1404
+ uniq_key: "720",
1405
+ format: "webp",
1406
+ },
1407
+ {
1408
+ scene: "normal",
1409
+ width: 480,
1410
+ height: 480,
1411
+ uniq_key: "480",
1412
+ format: "webp",
1413
+ },
1414
+ {
1415
+ scene: "normal",
1416
+ width: 360,
1417
+ height: 360,
1418
+ uniq_key: "360",
1419
+ format: "webp",
1420
+ },
1421
+ ],
1422
+ },
1423
+ http_common_info: {
1424
+ aid: DEFAULT_ASSISTANT_ID,
1425
+ },
1426
+ },
1427
+ });
1428
+ if (!result[historyId])
1429
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
1430
+
1431
+ status = result[historyId].status;
1432
+ failCode = result[historyId].fail_code;
1433
+ item_list = result[historyId].item_list || [];
1434
+
1435
+ // 检查是否已生成图片
1436
+ if (item_list.length > 0) {
1437
+ logger.info(`文生图完成: 状态=${status}, 已生成 ${item_list.length} 张图片`);
1438
+ break;
1439
+ }
1440
+
1441
+ // 记录详细状态
1442
+ if (pollCount % 60 === 0) {
1443
+ logger.info(`文生图详细状态: status=${status}, item_list.length=${item_list.length}, failCode=${failCode || 'none'}`);
1444
+ }
1445
+
1446
+ // 如果状态是完成但图片数量为0,记录并继续等待
1447
+ if (status === 10 && item_list.length === 0 && pollCount % 30 === 0) {
1448
+ logger.info(`文生图状态已完成但无图片生成: 状态=${status}, 继续等待...`);
1449
+ }
1450
+ }
1451
+
1452
+ if (pollCount >= maxPollCount) {
1453
+ logger.warn(`文生图超时: 轮询了 ${pollCount} 次,当前状态: ${status},已生成图片数: ${item_list.length}`);
1454
+ }
1455
+
1456
+ if (status === 30) {
1457
+ if (failCode === '2038')
1458
+ throw new APIException(EX.API_CONTENT_FILTERED);
1459
+ else
1460
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED);
1461
+ }
1462
+
1463
+ const imageUrls = item_list.map((item) => {
1464
+ if(!item?.image?.large_images?.[0]?.image_url)
1465
+ return item?.common_attr?.cover_url || null;
1466
+ return item.image.large_images[0].image_url;
1467
+ }).filter(url => url !== null);
1468
+
1469
+ logger.info(`文生图结果: 成功生成 ${imageUrls.length} 张图片`);
1470
+ return imageUrls;
1471
+ }
1472
+
1473
+ export default {
1474
+ generateImages,
1475
+ generateImageComposition,
1476
+ };
src/api/controllers/videos.ts ADDED
@@ -0,0 +1,1039 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from "lodash";
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+
5
+ import APIException from "@/lib/exceptions/APIException.ts";
6
+ import EX from "@/api/consts/exceptions.ts";
7
+ import util from "@/lib/util.ts";
8
+ import { getCredit, receiveCredit, request } from "./core.ts";
9
+ import logger from "@/lib/logger.ts";
10
+
11
+ const DEFAULT_ASSISTANT_ID = 513695;
12
+ export const DEFAULT_MODEL = "jimeng-video-3.0";
13
+ const DRAFT_VERSION = "3.2.8";
14
+ const MODEL_MAP = {
15
+ "jimeng-video-3.0-pro": "dreamina_ic_generate_video_model_vgfm_3.0_pro",
16
+ "jimeng-video-3.0": "dreamina_ic_generate_video_model_vgfm_3.0",
17
+ "jimeng-video-2.0": "dreamina_ic_generate_video_model_vgfm_lite",
18
+ "jimeng-video-2.0-pro": "dreamina_ic_generate_video_model_vgfm1.0"
19
+ };
20
+
21
+ // 视频支持的分辨率和比例配置
22
+ const VIDEO_RESOLUTION_OPTIONS: {
23
+ [resolution: string]: {
24
+ [ratio: string]: { width: number; height: number };
25
+ };
26
+ } = {
27
+ "480p": {
28
+ "1:1": { width: 480, height: 480 },
29
+ "4:3": { width: 640, height: 480 },
30
+ "3:4": { width: 480, height: 640 },
31
+ "16:9": { width: 854, height: 480 },
32
+ "9:16": { width: 480, height: 854 },
33
+ },
34
+ "720p": {
35
+ "1:1": { width: 720, height: 720 },
36
+ "4:3": { width: 960, height: 720 },
37
+ "3:4": { width: 720, height: 960 },
38
+ "16:9": { width: 1280, height: 720 },
39
+ "9:16": { width: 720, height: 1280 },
40
+ },
41
+ "1080p": {
42
+ "1:1": { width: 1080, height: 1080 },
43
+ "4:3": { width: 1440, height: 1080 },
44
+ "3:4": { width: 1080, height: 1440 },
45
+ "16:9": { width: 1920, height: 1080 },
46
+ "9:16": { width: 1080, height: 1920 },
47
+ },
48
+ };
49
+
50
+ // 解析视频分辨率参数
51
+ function resolveVideoResolution(
52
+ resolution: string = "720p",
53
+ ratio: string = "1:1"
54
+ ): { width: number; height: number } {
55
+ const resolutionGroup = VIDEO_RESOLUTION_OPTIONS[resolution];
56
+ if (!resolutionGroup) {
57
+ const supportedResolutions = Object.keys(VIDEO_RESOLUTION_OPTIONS).join(", ");
58
+ throw new Error(`不支持的视频分辨率 "${resolution}"。支持的分辨率: ${supportedResolutions}`);
59
+ }
60
+
61
+ const ratioConfig = resolutionGroup[ratio];
62
+ if (!ratioConfig) {
63
+ const supportedRatios = Object.keys(resolutionGroup).join(", ");
64
+ throw new Error(`在 "${resolution}" 分辨率下,不支持的比例 "${ratio}"。支持的比例: ${supportedRatios}`);
65
+ }
66
+
67
+ return {
68
+ width: ratioConfig.width,
69
+ height: ratioConfig.height,
70
+ };
71
+ }
72
+
73
+ export function getModel(model: string) {
74
+ return MODEL_MAP[model] || MODEL_MAP[DEFAULT_MODEL];
75
+ }
76
+
77
+ // AWS4-HMAC-SHA256 签名生成函数(从 images.ts 复制)
78
+ function createSignature(
79
+ method: string,
80
+ url: string,
81
+ headers: { [key: string]: string },
82
+ accessKeyId: string,
83
+ secretAccessKey: string,
84
+ sessionToken?: string,
85
+ payload: string = ''
86
+ ) {
87
+ const urlObj = new URL(url);
88
+ const pathname = urlObj.pathname || '/';
89
+ const search = urlObj.search;
90
+
91
+ // 创建规范请求
92
+ const timestamp = headers['x-amz-date'];
93
+ const date = timestamp.substr(0, 8);
94
+ const region = 'cn-north-1';
95
+ const service = 'imagex';
96
+
97
+ // 规范化查询参数
98
+ const queryParams: Array<[string, string]> = [];
99
+ const searchParams = new URLSearchParams(search);
100
+ searchParams.forEach((value, key) => {
101
+ queryParams.push([key, value]);
102
+ });
103
+
104
+ // 按键名排序
105
+ queryParams.sort(([a], [b]) => {
106
+ if (a < b) return -1;
107
+ if (a > b) return 1;
108
+ return 0;
109
+ });
110
+
111
+ const canonicalQueryString = queryParams
112
+ .map(([key, value]) => `${key}=${value}`)
113
+ .join('&');
114
+
115
+ // 规范化头部
116
+ const headersToSign: { [key: string]: string } = {
117
+ 'x-amz-date': timestamp
118
+ };
119
+
120
+ if (sessionToken) {
121
+ headersToSign['x-amz-security-token'] = sessionToken;
122
+ }
123
+
124
+ let payloadHash = crypto.createHash('sha256').update('').digest('hex');
125
+ if (method.toUpperCase() === 'POST' && payload) {
126
+ payloadHash = crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
127
+ headersToSign['x-amz-content-sha256'] = payloadHash;
128
+ }
129
+
130
+ const signedHeaders = Object.keys(headersToSign)
131
+ .map(key => key.toLowerCase())
132
+ .sort()
133
+ .join(';');
134
+
135
+ const canonicalHeaders = Object.keys(headersToSign)
136
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
137
+ .map(key => `${key.toLowerCase()}:${headersToSign[key].trim()}\n`)
138
+ .join('');
139
+
140
+ const canonicalRequest = [
141
+ method.toUpperCase(),
142
+ pathname,
143
+ canonicalQueryString,
144
+ canonicalHeaders,
145
+ signedHeaders,
146
+ payloadHash
147
+ ].join('\n');
148
+
149
+ // 创建待签名字符串
150
+ const credentialScope = `${date}/${region}/${service}/aws4_request`;
151
+ const stringToSign = [
152
+ 'AWS4-HMAC-SHA256',
153
+ timestamp,
154
+ credentialScope,
155
+ crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex')
156
+ ].join('\n');
157
+
158
+ // 生成签名
159
+ const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(date).digest();
160
+ const kRegion = crypto.createHmac('sha256', kDate).update(region).digest();
161
+ const kService = crypto.createHmac('sha256', kRegion).update(service).digest();
162
+ const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest();
163
+ const signature = crypto.createHmac('sha256', kSigning).update(stringToSign, 'utf8').digest('hex');
164
+
165
+ return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
166
+ }
167
+
168
+ // 计算文件的CRC32值(从 images.ts 复制)
169
+ function calculateCRC32(buffer: ArrayBuffer): string {
170
+ const crcTable = [];
171
+ for (let i = 0; i < 256; i++) {
172
+ let crc = i;
173
+ for (let j = 0; j < 8; j++) {
174
+ crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
175
+ }
176
+ crcTable[i] = crc;
177
+ }
178
+
179
+ let crc = 0 ^ (-1);
180
+ const bytes = new Uint8Array(buffer);
181
+ for (let i = 0; i < bytes.length; i++) {
182
+ crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
183
+ }
184
+ return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
185
+ }
186
+
187
+ // 视频专用图片上传功能(基于 images.ts 的 uploadImageFromUrl)
188
+ async function uploadImageForVideo(imageUrl: string, refreshToken: string): Promise<string> {
189
+ try {
190
+ logger.info(`开始上传视频图片: ${imageUrl}`);
191
+
192
+ // 第一步:获取上传令牌
193
+ const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
194
+ data: {
195
+ scene: 2, // AIGC 图片上传场景
196
+ },
197
+ });
198
+
199
+ const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
200
+ if (!access_key_id || !secret_access_key || !session_token) {
201
+ throw new Error("获取上传令牌失败");
202
+ }
203
+
204
+ const actualServiceId = service_id || "tb4s082cfz";
205
+ logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
206
+
207
+ // 下载图片数据
208
+ const imageResponse = await fetch(imageUrl);
209
+ if (!imageResponse.ok) {
210
+ throw new Error(`下载图片失败: ${imageResponse.status}`);
211
+ }
212
+
213
+ const imageBuffer = await imageResponse.arrayBuffer();
214
+ const fileSize = imageBuffer.byteLength;
215
+ const crc32 = calculateCRC32(imageBuffer);
216
+
217
+ logger.info(`图片下载完成: 大小=${fileSize}字节, CRC32=${crc32}`);
218
+
219
+ // 第二步:申请图片上传权限
220
+ const now = new Date();
221
+ const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
222
+
223
+ const randomStr = Math.random().toString(36).substring(2, 12);
224
+ const applyUrl = `https://imagex.bytedanceapi.com/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}`;
225
+
226
+ const requestHeaders = {
227
+ 'x-amz-date': timestamp,
228
+ 'x-amz-security-token': session_token
229
+ };
230
+
231
+ const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
232
+
233
+ logger.info(`申请上传权限: ${applyUrl}`);
234
+
235
+ const applyResponse = await fetch(applyUrl, {
236
+ method: 'GET',
237
+ headers: {
238
+ 'accept': '*/*',
239
+ 'accept-language': 'zh-CN,zh;q=0.9',
240
+ 'authorization': authorization,
241
+ 'origin': 'https://jimeng.jianying.com',
242
+ 'referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
243
+ 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
244
+ 'sec-ch-ua-mobile': '?0',
245
+ 'sec-ch-ua-platform': '"Windows"',
246
+ 'sec-fetch-dest': 'empty',
247
+ 'sec-fetch-mode': 'cors',
248
+ 'sec-fetch-site': 'cross-site',
249
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
250
+ 'x-amz-date': timestamp,
251
+ 'x-amz-security-token': session_token,
252
+ },
253
+ });
254
+
255
+ if (!applyResponse.ok) {
256
+ const errorText = await applyResponse.text();
257
+ throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`);
258
+ }
259
+
260
+ const applyResult = await applyResponse.json();
261
+
262
+ if (applyResult?.ResponseMetadata?.Error) {
263
+ throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`);
264
+ }
265
+
266
+ logger.info(`申请上传权限成功`);
267
+
268
+ // 解析上传信息
269
+ const uploadAddress = applyResult?.Result?.UploadAddress;
270
+ if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) {
271
+ throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`);
272
+ }
273
+
274
+ const storeInfo = uploadAddress.StoreInfos[0];
275
+ const uploadHost = uploadAddress.UploadHosts[0];
276
+ const auth = storeInfo.Auth;
277
+
278
+ const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`;
279
+ const imageId = storeInfo.StoreUri.split('/').pop();
280
+
281
+ logger.info(`准备上传图片: imageId=${imageId}, uploadUrl=${uploadUrl}`);
282
+
283
+ // 第三步:上传图片文件
284
+ const uploadResponse = await fetch(uploadUrl, {
285
+ method: 'POST',
286
+ headers: {
287
+ 'Accept': '*/*',
288
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
289
+ 'Authorization': auth,
290
+ 'Connection': 'keep-alive',
291
+ 'Content-CRC32': crc32,
292
+ 'Content-Disposition': 'attachment; filename="undefined"',
293
+ 'Content-Type': 'application/octet-stream',
294
+ 'Origin': 'https://jimeng.jianying.com',
295
+ 'Referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
296
+ 'Sec-Fetch-Dest': 'empty',
297
+ 'Sec-Fetch-Mode': 'cors',
298
+ 'Sec-Fetch-Site': 'cross-site',
299
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
300
+ 'X-Storage-U': '704135154117550',
301
+ },
302
+ body: imageBuffer,
303
+ });
304
+
305
+ if (!uploadResponse.ok) {
306
+ const errorText = await uploadResponse.text();
307
+ throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`);
308
+ }
309
+
310
+ logger.info(`图片文件上传成功`);
311
+
312
+ // 第四步:提交上传
313
+ const commitUrl = `https://imagex.bytedanceapi.com/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
314
+
315
+ const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
316
+ const commitPayload = JSON.stringify({
317
+ SessionKey: uploadAddress.SessionKey,
318
+ SuccessActionStatus: "200"
319
+ });
320
+
321
+ const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
322
+
323
+ const commitRequestHeaders = {
324
+ 'x-amz-date': commitTimestamp,
325
+ 'x-amz-security-token': session_token,
326
+ 'x-amz-content-sha256': payloadHash
327
+ };
328
+
329
+ const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload);
330
+
331
+ const commitResponse = await fetch(commitUrl, {
332
+ method: 'POST',
333
+ headers: {
334
+ 'accept': '*/*',
335
+ 'accept-language': 'zh-CN,zh;q=0.9',
336
+ 'authorization': commitAuthorization,
337
+ 'content-type': 'application/json',
338
+ 'origin': 'https://jimeng.jianying.com',
339
+ 'referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
340
+ 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
341
+ 'sec-ch-ua-mobile': '?0',
342
+ 'sec-ch-ua-platform': '"Windows"',
343
+ 'sec-fetch-dest': 'empty',
344
+ 'sec-fetch-mode': 'cors',
345
+ 'sec-fetch-site': 'cross-site',
346
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
347
+ 'x-amz-date': commitTimestamp,
348
+ 'x-amz-security-token': session_token,
349
+ 'x-amz-content-sha256': payloadHash,
350
+ },
351
+ body: commitPayload,
352
+ });
353
+
354
+ if (!commitResponse.ok) {
355
+ const errorText = await commitResponse.text();
356
+ throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`);
357
+ }
358
+
359
+ const commitResult = await commitResponse.json();
360
+
361
+ if (commitResult?.ResponseMetadata?.Error) {
362
+ throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`);
363
+ }
364
+
365
+ if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) {
366
+ throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`);
367
+ }
368
+
369
+ const uploadResult = commitResult.Result.Results[0];
370
+ if (uploadResult.UriStatus !== 2000) {
371
+ throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`);
372
+ }
373
+
374
+ const fullImageUri = uploadResult.Uri;
375
+
376
+ // 验证图片信息
377
+ const pluginResult = commitResult.Result?.PluginResult?.[0];
378
+ if (pluginResult && pluginResult.ImageUri) {
379
+ logger.info(`视频图片上传完成: ${pluginResult.ImageUri}`);
380
+ return pluginResult.ImageUri;
381
+ }
382
+
383
+ logger.info(`视频图片上传完成: ${fullImageUri}`);
384
+ return fullImageUri;
385
+
386
+ } catch (error) {
387
+ logger.error(`视频图片上传失败: ${error.message}`);
388
+ throw error;
389
+ }
390
+ }
391
+
392
+ // 从Buffer上传视频图片
393
+ async function uploadImageBufferForVideo(buffer: Buffer, refreshToken: string): Promise<string> {
394
+ try {
395
+ logger.info(`开始从Buffer上传视频图片,大小: ${buffer.length}字节`);
396
+
397
+ // 第一步:获取上传令牌
398
+ const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
399
+ data: {
400
+ scene: 2,
401
+ },
402
+ });
403
+
404
+ const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
405
+ if (!access_key_id || !secret_access_key || !session_token) {
406
+ throw new Error("获取上传令牌失败");
407
+ }
408
+
409
+ const actualServiceId = service_id || "tb4s082cfz";
410
+ logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
411
+
412
+ const fileSize = buffer.length;
413
+ const crc32 = calculateCRC32(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
414
+
415
+ logger.info(`Buffer大小: ${fileSize}字节, CRC32=${crc32}`);
416
+
417
+ // 第二步���申请图片上传权限
418
+ const now = new Date();
419
+ const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
420
+
421
+ const randomStr = Math.random().toString(36).substring(2, 12);
422
+ const applyUrl = `https://imagex.bytedanceapi.com/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}`;
423
+
424
+ const requestHeaders = {
425
+ 'x-amz-date': timestamp,
426
+ 'x-amz-security-token': session_token
427
+ };
428
+
429
+ const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
430
+
431
+ const applyResponse = await fetch(applyUrl, {
432
+ method: 'GET',
433
+ headers: {
434
+ 'accept': '*/*',
435
+ 'accept-language': 'zh-CN,zh;q=0.9',
436
+ 'authorization': authorization,
437
+ 'origin': 'https://jimeng.jianying.com',
438
+ 'referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
439
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
440
+ 'x-amz-date': timestamp,
441
+ 'x-amz-security-token': session_token,
442
+ },
443
+ });
444
+
445
+ if (!applyResponse.ok) {
446
+ const errorText = await applyResponse.text();
447
+ throw new Error(`申请上传权限失败: ${applyResponse.status} - ${errorText}`);
448
+ }
449
+
450
+ const applyResult = await applyResponse.json();
451
+
452
+ if (applyResult?.ResponseMetadata?.Error) {
453
+ throw new Error(`申请上传权限失败: ${JSON.stringify(applyResult.ResponseMetadata.Error)}`);
454
+ }
455
+
456
+ const uploadAddress = applyResult?.Result?.UploadAddress;
457
+ if (!uploadAddress || !uploadAddress.StoreInfos || !uploadAddress.UploadHosts) {
458
+ throw new Error(`获取上传地址失败: ${JSON.stringify(applyResult)}`);
459
+ }
460
+
461
+ const storeInfo = uploadAddress.StoreInfos[0];
462
+ const uploadHost = uploadAddress.UploadHosts[0];
463
+ const auth = storeInfo.Auth;
464
+
465
+ const uploadUrl = `https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`;
466
+
467
+ // 第三步:上传图片文件
468
+ const uploadResponse = await fetch(uploadUrl, {
469
+ method: 'POST',
470
+ headers: {
471
+ 'Accept': '*/*',
472
+ 'Authorization': auth,
473
+ 'Content-CRC32': crc32,
474
+ 'Content-Disposition': 'attachment; filename="undefined"',
475
+ 'Content-Type': 'application/octet-stream',
476
+ 'Origin': 'https://jimeng.jianying.com',
477
+ 'Referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
478
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
479
+ },
480
+ body: buffer,
481
+ });
482
+
483
+ if (!uploadResponse.ok) {
484
+ const errorText = await uploadResponse.text();
485
+ throw new Error(`图片上传失败: ${uploadResponse.status} - ${errorText}`);
486
+ }
487
+
488
+ logger.info(`Buffer图片文件上传成功`);
489
+
490
+ // 第四步:提交上传
491
+ const commitUrl = `https://imagex.bytedanceapi.com/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
492
+
493
+ const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
494
+ const commitPayload = JSON.stringify({
495
+ SessionKey: uploadAddress.SessionKey,
496
+ SuccessActionStatus: "200"
497
+ });
498
+
499
+ const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
500
+
501
+ const commitRequestHeaders = {
502
+ 'x-amz-date': commitTimestamp,
503
+ 'x-amz-security-token': session_token,
504
+ 'x-amz-content-sha256': payloadHash
505
+ };
506
+
507
+ const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload);
508
+
509
+ const commitResponse = await fetch(commitUrl, {
510
+ method: 'POST',
511
+ headers: {
512
+ 'accept': '*/*',
513
+ 'authorization': commitAuthorization,
514
+ 'content-type': 'application/json',
515
+ 'origin': 'https://jimeng.jianying.com',
516
+ 'referer': 'https://jimeng.jianying.com/ai-tool/video/generate',
517
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
518
+ 'x-amz-date': commitTimestamp,
519
+ 'x-amz-security-token': session_token,
520
+ 'x-amz-content-sha256': payloadHash,
521
+ },
522
+ body: commitPayload,
523
+ });
524
+
525
+ if (!commitResponse.ok) {
526
+ const errorText = await commitResponse.text();
527
+ throw new Error(`提交上传失败: ${commitResponse.status} - ${errorText}`);
528
+ }
529
+
530
+ const commitResult = await commitResponse.json();
531
+
532
+ if (commitResult?.ResponseMetadata?.Error) {
533
+ throw new Error(`提交上传失败: ${JSON.stringify(commitResult.ResponseMetadata.Error)}`);
534
+ }
535
+
536
+ if (!commitResult?.Result?.Results || commitResult.Result.Results.length === 0) {
537
+ throw new Error(`提交上传响应缺少结果: ${JSON.stringify(commitResult)}`);
538
+ }
539
+
540
+ const uploadResult = commitResult.Result.Results[0];
541
+ if (uploadResult.UriStatus !== 2000) {
542
+ throw new Error(`图片上传状态异常: UriStatus=${uploadResult.UriStatus}`);
543
+ }
544
+
545
+ const fullImageUri = uploadResult.Uri;
546
+
547
+ const pluginResult = commitResult.Result?.PluginResult?.[0];
548
+ if (pluginResult && pluginResult.ImageUri) {
549
+ logger.info(`Buffer视频图片上传完成: ${pluginResult.ImageUri}`);
550
+ return pluginResult.ImageUri;
551
+ }
552
+
553
+ logger.info(`Buffer视频图片上传完成: ${fullImageUri}`);
554
+ return fullImageUri;
555
+
556
+ } catch (error) {
557
+ logger.error(`Buffer视频图片上传失败: ${error.message}`);
558
+ throw error;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * 生成视频
564
+ *
565
+ * @param _model 模型名称
566
+ * @param prompt 提示词
567
+ * @param options 选项
568
+ * @param refreshToken 刷新令牌
569
+ * @returns 视频URL
570
+ */
571
+ export async function generateVideo(
572
+ _model: string,
573
+ prompt: string,
574
+ {
575
+ ratio = "1:1",
576
+ resolution = "720p",
577
+ duration = 5,
578
+ filePaths = [],
579
+ files = [],
580
+ }: {
581
+ ratio?: string;
582
+ resolution?: string;
583
+ duration?: number;
584
+ filePaths?: string[];
585
+ files?: any[];
586
+ },
587
+ refreshToken: string
588
+ ) {
589
+ const model = getModel(_model);
590
+
591
+ // 解析分辨率参数获取实际的宽高
592
+ const { width, height } = resolveVideoResolution(resolution, ratio);
593
+
594
+ logger.info(`使用模型: ${_model} 映射模型: ${model} ${width}x${height} (${ratio}@${resolution}) 时长: ${duration}秒`);
595
+
596
+ // 检查积分
597
+ const { totalCredit } = await getCredit(refreshToken);
598
+ if (totalCredit <= 0)
599
+ await receiveCredit(refreshToken);
600
+
601
+ // 处理首帧和尾帧图片
602
+ let first_frame_image = undefined;
603
+ let end_frame_image = undefined;
604
+
605
+ // 处理上传的文件(multipart/form-data)
606
+ if (files && files.length > 0) {
607
+ let uploadIDs: string[] = [];
608
+ logger.info(`开始处理 ${files.length} 个上传文件用于视频生成`);
609
+
610
+ for (let i = 0; i < files.length; i++) {
611
+ const file = files[i];
612
+ if (!file || !file.filepath) {
613
+ logger.warn(`第 ${i + 1} 个文件无效,跳过`);
614
+ continue;
615
+ }
616
+
617
+ try {
618
+ logger.info(`开始上传第 ${i + 1} 个文件: ${file.originalFilename || file.filepath}`);
619
+
620
+ // 读取文件内容并上传
621
+ const buffer = fs.readFileSync(file.filepath);
622
+ const imageUri = await uploadImageBufferForVideo(buffer, refreshToken);
623
+
624
+ if (imageUri) {
625
+ uploadIDs.push(imageUri);
626
+ logger.info(`第 ${i + 1} 个文件上传成功: ${imageUri}`);
627
+ } else {
628
+ logger.error(`第 ${i + 1} 个文件上传失败: 未获取到 image_uri`);
629
+ }
630
+ } catch (error) {
631
+ logger.error(`第 ${i + 1} 个文件上传失败: ${error.message}`);
632
+
633
+ if (i === 0) {
634
+ logger.error(`首帧文件上传失败,停止视频生成以避免浪费积分`);
635
+ throw new APIException(EX.API_REQUEST_FAILED, `首帧文件上传失败: ${error.message}`);
636
+ } else {
637
+ logger.warn(`第 ${i + 1} 个文件上传失败,将跳过此文件继续处理`);
638
+ }
639
+ }
640
+ }
641
+
642
+ logger.info(`文件上传完成,成功上传 ${uploadIDs.length} 个文件`);
643
+
644
+ if (uploadIDs.length === 0) {
645
+ logger.error(`所有文件上传失败,停止视频生成以避免浪费积分`);
646
+ throw new APIException(EX.API_REQUEST_FAILED, '所有文件上传失败,请检查文件是否有效');
647
+ }
648
+
649
+ // 构建首帧图片对象
650
+ if (uploadIDs[0]) {
651
+ first_frame_image = {
652
+ format: "",
653
+ height: height,
654
+ id: util.uuid(),
655
+ image_uri: uploadIDs[0],
656
+ name: "",
657
+ platform_type: 1,
658
+ source_from: "upload",
659
+ type: "image",
660
+ uri: uploadIDs[0],
661
+ width: width,
662
+ };
663
+ logger.info(`设置首帧图片: ${uploadIDs[0]}`);
664
+ }
665
+
666
+ // 构建尾帧图片对象
667
+ if (uploadIDs[1]) {
668
+ end_frame_image = {
669
+ format: "",
670
+ height: height,
671
+ id: util.uuid(),
672
+ image_uri: uploadIDs[1],
673
+ name: "",
674
+ platform_type: 1,
675
+ source_from: "upload",
676
+ type: "image",
677
+ uri: uploadIDs[1],
678
+ width: width,
679
+ };
680
+ logger.info(`设置尾帧图片: ${uploadIDs[1]}`);
681
+ }
682
+ } else if (filePaths && filePaths.length > 0) {
683
+ let uploadIDs: string[] = [];
684
+ logger.info(`开始上传 ${filePaths.length} 张图片用于视频生成`);
685
+
686
+ for (let i = 0; i < filePaths.length; i++) {
687
+ const filePath = filePaths[i];
688
+ if (!filePath) {
689
+ logger.warn(`第 ${i + 1} 张图片路径为空,跳过`);
690
+ continue;
691
+ }
692
+
693
+ try {
694
+ logger.info(`开始上传第 ${i + 1} 张图片: ${filePath}`);
695
+
696
+ // 使用Amazon S3上传方式
697
+ const imageUri = await uploadImageForVideo(filePath, refreshToken);
698
+
699
+ if (imageUri) {
700
+ uploadIDs.push(imageUri);
701
+ logger.info(`第 ${i + 1} 张图片上传成功: ${imageUri}`);
702
+ } else {
703
+ logger.error(`第 ${i + 1} 张图片上���失败: 未获取到 image_uri`);
704
+ }
705
+ } catch (error) {
706
+ logger.error(`第 ${i + 1} 张图片上传失败: ${error.message}`);
707
+
708
+ // 图片上传失败时,停止视频生成避免浪费积分
709
+ if (i === 0) {
710
+ logger.error(`首帧图片上传失败,停止视频生成以避免浪费积分`);
711
+ throw new APIException(EX.API_REQUEST_FAILED, `首帧图片上传失败: ${error.message}`);
712
+ } else {
713
+ logger.warn(`第 ${i + 1} 张图片上传失败,将跳过此图片继续处理`);
714
+ }
715
+ }
716
+ }
717
+
718
+ logger.info(`图片上传完成,成功上传 ${uploadIDs.length} 张图片`);
719
+
720
+ // 如果没有成功上传任何图片,停止视频生成
721
+ if (uploadIDs.length === 0) {
722
+ logger.error(`所有图片上传失败,停止视频生成以避免浪费积分`);
723
+ throw new APIException(EX.API_REQUEST_FAILED, '所有图片上传失败,请检查图片URL是否有效');
724
+ }
725
+
726
+ // 构建首帧图片对象
727
+ if (uploadIDs[0]) {
728
+ first_frame_image = {
729
+ format: "",
730
+ height: height,
731
+ id: util.uuid(),
732
+ image_uri: uploadIDs[0],
733
+ name: "",
734
+ platform_type: 1,
735
+ source_from: "upload",
736
+ type: "image",
737
+ uri: uploadIDs[0],
738
+ width: width,
739
+ };
740
+ logger.info(`设置首帧图片: ${uploadIDs[0]}`);
741
+ }
742
+
743
+ // 构建尾帧图片对象
744
+ if (uploadIDs[1]) {
745
+ end_frame_image = {
746
+ format: "",
747
+ height: height,
748
+ id: util.uuid(),
749
+ image_uri: uploadIDs[1],
750
+ name: "",
751
+ platform_type: 1,
752
+ source_from: "upload",
753
+ type: "image",
754
+ uri: uploadIDs[1],
755
+ width: width,
756
+ };
757
+ logger.info(`设置尾帧图片: ${uploadIDs[1]}`);
758
+ } else if (filePaths.length > 1) {
759
+ logger.warn(`第二张图片上传失败或未提供,将仅使用首帧图片`);
760
+ }
761
+ } else {
762
+ logger.info(`未提供图片文件,将进行纯文本视频生成`);
763
+ }
764
+
765
+ const componentId = util.uuid();
766
+ const metricsExtra = JSON.stringify({
767
+ "enterFrom": "click",
768
+ "isDefaultSeed": 1,
769
+ "promptSource": "custom",
770
+ "isRegenerate": false,
771
+ "originSubmitId": util.uuid(),
772
+ });
773
+
774
+ // 计算视频宽高比
775
+ const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
776
+ const divisor = gcd(width, height);
777
+ const aspectRatio = `${width / divisor}:${height / divisor}`;
778
+
779
+ // 构建请求参数
780
+ const { aigc_data } = await request(
781
+ "post",
782
+ "/mweb/v1/aigc_draft/generate",
783
+ refreshToken,
784
+ {
785
+ params: {
786
+ aigc_features: "app_lip_sync",
787
+ web_version: "6.6.0",
788
+ da_version: DRAFT_VERSION,
789
+ },
790
+ data: {
791
+ "extend": {
792
+ "root_model": end_frame_image ? MODEL_MAP['jimeng-video-3.0'] : model,
793
+ "m_video_commerce_info": {
794
+ benefit_type: "basic_video_operation_vgfm_v_three",
795
+ resource_id: "generate_video",
796
+ resource_id_type: "str",
797
+ resource_sub_type: "aigc"
798
+ },
799
+ "m_video_commerce_info_list": [{
800
+ benefit_type: "basic_video_operation_vgfm_v_three",
801
+ resource_id: "generate_video",
802
+ resource_id_type: "str",
803
+ resource_sub_type: "aigc"
804
+ }]
805
+ },
806
+ "submit_id": util.uuid(),
807
+ "metrics_extra": metricsExtra,
808
+ "draft_content": JSON.stringify({
809
+ "type": "draft",
810
+ "id": util.uuid(),
811
+ "min_version": "3.0.5",
812
+ "is_from_tsn": true,
813
+ "version": DRAFT_VERSION,
814
+ "main_component_id": componentId,
815
+ "component_list": [{
816
+ "type": "video_base_component",
817
+ "id": componentId,
818
+ "min_version": "1.0.0",
819
+ "metadata": {
820
+ "type": "",
821
+ "id": util.uuid(),
822
+ "created_platform": 3,
823
+ "created_platform_version": "",
824
+ "created_time_in_ms": Date.now(),
825
+ "created_did": ""
826
+ },
827
+ "generate_type": "gen_video",
828
+ "aigc_mode": "workbench",
829
+ "abilities": {
830
+ "type": "",
831
+ "id": util.uuid(),
832
+ "gen_video": {
833
+ "id": util.uuid(),
834
+ "type": "",
835
+ "text_to_video_params": {
836
+ "type": "",
837
+ "id": util.uuid(),
838
+ "model_req_key": model,
839
+ "priority": 0,
840
+ "seed": Math.floor(Math.random() * 100000000) + 2500000000,
841
+ "video_aspect_ratio": aspectRatio,
842
+ "video_gen_inputs": [{
843
+ duration_ms: duration * 1000,
844
+ first_frame_image: first_frame_image,
845
+ end_frame_image: end_frame_image,
846
+ fps: 24,
847
+ id: util.uuid(),
848
+ min_version: "3.0.5",
849
+ prompt: prompt,
850
+ resolution: resolution,
851
+ type: "",
852
+ video_mode: 2
853
+ }]
854
+ },
855
+ "video_task_extra": metricsExtra,
856
+ }
857
+ }
858
+ }],
859
+ }),
860
+ http_common_info: {
861
+ aid: DEFAULT_ASSISTANT_ID,
862
+ },
863
+ },
864
+ }
865
+ );
866
+
867
+ const historyId = aigc_data.history_record_id;
868
+ if (!historyId)
869
+ throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
870
+
871
+ // 轮询获取结果
872
+ let status = 20, failCode, item_list = [];
873
+ let retryCount = 0;
874
+ const maxRetries = 60; // 增加重试次数,支持约20分钟的总重试时间
875
+
876
+ // 首次查询前等待更长时间,让服务器有时间处理请求
877
+ await new Promise((resolve) => setTimeout(resolve, 5000));
878
+
879
+ logger.info(`开始轮询视频生成结果,历史ID: ${historyId},最大重试次数: ${maxRetries}`);
880
+ logger.info(`即梦官网API地址: https://jimeng.jianying.com/mweb/v1/get_history_by_ids`);
881
+ logger.info(`视频生成请求已发送,请同时在即梦官网查看: https://jimeng.jianying.com/ai-tool/video/generate`);
882
+
883
+ while (status === 20 && retryCount < maxRetries) {
884
+ try {
885
+ // 构建请求URL和参数
886
+ const requestUrl = "/mweb/v1/get_history_by_ids";
887
+ const requestData = {
888
+ history_ids: [historyId],
889
+ };
890
+
891
+ // 尝试两种不同的API请求方式
892
+ let result;
893
+ let useAlternativeApi = retryCount > 10 && retryCount % 2 === 0; // 在重试10次后,每隔一次尝试备用API
894
+
895
+ if (useAlternativeApi) {
896
+ // 备用API请求方式
897
+ logger.info(`尝试备用API请求方式,URL: ${requestUrl}, 历史ID: ${historyId}, 重试次数: ${retryCount + 1}/${maxRetries}`);
898
+ const alternativeRequestData = {
899
+ history_record_ids: [historyId],
900
+ };
901
+ result = await request("post", "/mweb/v1/get_history_records", refreshToken, {
902
+ data: alternativeRequestData,
903
+ });
904
+ logger.info(`备用API响应: ${JSON.stringify(result)}`);
905
+
906
+ // 尝试直接从响应中提取视频URL
907
+ const responseStr = JSON.stringify(result);
908
+ const videoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-artist\.vlabvod\.com\/[^"\s]+/);
909
+ if (videoUrlMatch && videoUrlMatch[0]) {
910
+ logger.info(`从备用API响应中直接提取到视频URL: ${videoUrlMatch[0]}`);
911
+ // 提前返回找到的URL
912
+ return videoUrlMatch[0];
913
+ }
914
+ } else {
915
+ // 标准API请求方式
916
+ logger.info(`发送请求获取视频生成结果,URL: ${requestUrl}, 历史ID: ${historyId}, 重试次数: ${retryCount + 1}/${maxRetries}`);
917
+ result = await request("post", requestUrl, refreshToken, {
918
+ data: requestData,
919
+ });
920
+ const responseStr = JSON.stringify(result);
921
+ logger.info(`标准API响应摘要: ${responseStr.substring(0, 300)}...`);
922
+
923
+ // 尝试直接从响应中提取视频URL
924
+ const videoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-artist\.vlabvod\.com\/[^"\s]+/);
925
+ if (videoUrlMatch && videoUrlMatch[0]) {
926
+ logger.info(`从标准API响应中直接提取到视频URL: ${videoUrlMatch[0]}`);
927
+ // 提前返回找到的URL
928
+ return videoUrlMatch[0];
929
+ }
930
+ }
931
+
932
+
933
+ // 检查结果是否有效
934
+ let historyData;
935
+
936
+ if (useAlternativeApi && result.history_records && result.history_records.length > 0) {
937
+ // 处理备用API返回的数据格式
938
+ historyData = result.history_records[0];
939
+ logger.info(`从备用API获取到历史记录`);
940
+ } else if (result.history_list && result.history_list.length > 0) {
941
+ // 处理标准API返回的数据格式
942
+ historyData = result.history_list[0];
943
+ logger.info(`从标准API获取到历史记录`);
944
+ } else {
945
+ // 两种API都没有返回有效数据
946
+ logger.warn(`历史记录不存在,重试中 (${retryCount + 1}/${maxRetries})... 历史ID: ${historyId}`);
947
+ logger.info(`请同时在即梦官网检查视频是否已生成: https://jimeng.jianying.com/ai-tool/video/generate`);
948
+
949
+ retryCount++;
950
+ // 增加重试间隔时间,但设置上限为30秒
951
+ const waitTime = Math.min(2000 * (retryCount + 1), 30000);
952
+ logger.info(`等待 ${waitTime}ms 后进行第 ${retryCount + 1} 次重试`);
953
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
954
+ continue;
955
+ }
956
+
957
+ // 记录获取到的结果详情
958
+ logger.info(`获取到历史记录结果: ${JSON.stringify(historyData)}`);
959
+
960
+
961
+ // 从历史数据中提取状态和结果
962
+ status = historyData.status;
963
+ failCode = historyData.fail_code;
964
+ item_list = historyData.item_list || [];
965
+
966
+ logger.info(`视频生成状态: ${status}, 失败代码: ${failCode || '无'}, 项目列表长度: ${item_list.length}`);
967
+
968
+ // 如果有视频URL,提前记录
969
+ let tempVideoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
970
+ if (!tempVideoUrl) {
971
+ // 尝试从其他可能的路径获取
972
+ tempVideoUrl = item_list?.[0]?.video?.play_url ||
973
+ item_list?.[0]?.video?.download_url ||
974
+ item_list?.[0]?.video?.url;
975
+ }
976
+
977
+ if (tempVideoUrl) {
978
+ logger.info(`检测到视频URL: ${tempVideoUrl}`);
979
+ }
980
+
981
+ if (status === 30) {
982
+ const error = failCode === 2038
983
+ ? new APIException(EX.API_CONTENT_FILTERED, "内容被过滤")
984
+ : new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误码: ${failCode}`);
985
+ // 添加历史ID到错误对象,以便在chat.ts中显示
986
+ error.historyId = historyId;
987
+ throw error;
988
+ }
989
+
990
+ // 如果状态仍在处理中,等待后继续
991
+ if (status === 20) {
992
+ const waitTime = 2000 * (Math.min(retryCount + 1, 5)); // 随着重试次数增加等待时间,但最多10秒
993
+ logger.info(`视频生成中,状态码: ${status},等待 ${waitTime}ms 后继续查询`);
994
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
995
+ }
996
+ } catch (error) {
997
+ logger.error(`轮询视频生成结果出错: ${error.message}`);
998
+ retryCount++;
999
+ await new Promise((resolve) => setTimeout(resolve, 2000 * (retryCount + 1)));
1000
+ }
1001
+ }
1002
+
1003
+ // 如果达到最大重试次数仍未成功
1004
+ if (retryCount >= maxRetries && status === 20) {
1005
+ logger.error(`视频生成超时,已尝试 ${retryCount} 次,总耗时约 ${Math.floor(retryCount * 2000 / 1000 / 60)} 分钟`);
1006
+ const error = new APIException(EX.API_IMAGE_GENERATION_FAILED, "获取视频生成结果超时,请稍后在即梦官网查看您的视频");
1007
+ // 添加历史ID到错误对象,以便在chat.ts中显示
1008
+ error.historyId = historyId;
1009
+ throw error;
1010
+ }
1011
+
1012
+ // 提取视频URL
1013
+ let videoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
1014
+
1015
+ // 如果通过常规路径无法获取视频URL,尝试其他可能的路径
1016
+ if (!videoUrl) {
1017
+ // 尝试从item_list中的其他可能位置获取
1018
+ if (item_list?.[0]?.video?.play_url) {
1019
+ videoUrl = item_list[0].video.play_url;
1020
+ logger.info(`从play_url获取到视频URL: ${videoUrl}`);
1021
+ } else if (item_list?.[0]?.video?.download_url) {
1022
+ videoUrl = item_list[0].video.download_url;
1023
+ logger.info(`从download_url获取到视频URL: ${videoUrl}`);
1024
+ } else if (item_list?.[0]?.video?.url) {
1025
+ videoUrl = item_list[0].video.url;
1026
+ logger.info(`从url获取到视频URL: ${videoUrl}`);
1027
+ } else {
1028
+ // 如果仍然找不到,记录错误并抛出异常
1029
+ logger.error(`未能获取视频URL,item_list: ${JSON.stringify(item_list)}`);
1030
+ const error = new APIException(EX.API_IMAGE_GENERATION_FAILED, "未能获取视频URL,请稍后在即梦官网查看");
1031
+ // 添加历史ID到错误对象,以便在chat.ts中显示
1032
+ error.historyId = historyId;
1033
+ throw error;
1034
+ }
1035
+ }
1036
+
1037
+ logger.info(`视频生成成功,URL: ${videoUrl}`);
1038
+ return videoUrl;
1039
+ }
src/api/routes/chat.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Request from '@/lib/request/Request.ts';
4
+ import Response from '@/lib/response/Response.ts';
5
+ import { tokenSplit } from '@/api/controllers/core.ts';
6
+ import { createCompletion, createCompletionStream } from '@/api/controllers/chat.ts';
7
+
8
+ export default {
9
+
10
+ prefix: '/v1/chat',
11
+
12
+ post: {
13
+
14
+ '/completions': async (request: Request) => {
15
+ request
16
+ .validate('body.model', v => _.isUndefined(v) || _.isString(v))
17
+ .validate('body.messages', _.isArray)
18
+ .validate('headers.authorization', _.isString)
19
+ // refresh_token切分
20
+ const tokens = tokenSplit(request.headers.authorization);
21
+ // 随机挑选一个refresh_token
22
+ const token = _.sample(tokens);
23
+ const { model, messages, stream } = request.body;
24
+ if (stream) {
25
+ const stream = await createCompletionStream(messages, token, model);
26
+ return new Response(stream, {
27
+ type: "text/event-stream"
28
+ });
29
+ }
30
+ else
31
+ return await createCompletion(messages, token, model);
32
+ }
33
+
34
+ }
35
+
36
+ }
src/api/routes/images.ts ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "fs";
2
+ import _ from "lodash";
3
+
4
+ import Request from "@/lib/request/Request.ts";
5
+ import { generateImages, generateImageComposition } from "@/api/controllers/images.ts";
6
+ import { tokenSplit } from "@/api/controllers/core.ts";
7
+ import util from "@/lib/util.ts";
8
+
9
+ export default {
10
+ prefix: "/v1/images",
11
+
12
+ post: {
13
+ "/generations": async (request: Request) => {
14
+ // 检查是否使用了不支持的参数
15
+ const unsupportedParams = ['size', 'width', 'height'];
16
+ const bodyKeys = Object.keys(request.body);
17
+ const foundUnsupported = unsupportedParams.filter(param => bodyKeys.includes(param));
18
+
19
+ if (foundUnsupported.length > 0) {
20
+ throw new Error(`不支持的参数: ${foundUnsupported.join(', ')}。请使用 ratio 和 resolution 参数控制图像尺寸。`);
21
+ }
22
+
23
+ request
24
+ .validate("body.model", v => _.isUndefined(v) || _.isString(v))
25
+ .validate("body.prompt", _.isString)
26
+ .validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
27
+ .validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
28
+ .validate("body.resolution", v => _.isUndefined(v) || _.isString(v))
29
+ .validate("body.intelligent_ratio", v => _.isUndefined(v) || _.isBoolean(v))
30
+ .validate("body.sample_strength", v => _.isUndefined(v) || _.isFinite(v))
31
+ .validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
32
+ .validate("headers.authorization", _.isString);
33
+
34
+ // refresh_token切分
35
+ const tokens = tokenSplit(request.headers.authorization);
36
+ // 随机挑选一个refresh_token
37
+ const token = _.sample(tokens);
38
+ const {
39
+ model,
40
+ prompt,
41
+ negative_prompt: negativePrompt,
42
+ ratio,
43
+ resolution,
44
+ intelligent_ratio: intelligentRatio,
45
+ sample_strength: sampleStrength,
46
+ response_format,
47
+ } = request.body;
48
+
49
+ const responseFormat = _.defaultTo(response_format, "url");
50
+ const imageUrls = await generateImages(model, prompt, {
51
+ ratio,
52
+ resolution,
53
+ sampleStrength,
54
+ negativePrompt,
55
+ intelligentRatio,
56
+ }, token);
57
+
58
+ let data = [];
59
+ if (responseFormat == "b64_json") {
60
+ data = (
61
+ await Promise.all(imageUrls.map((url) => util.fetchFileBASE64(url)))
62
+ ).map((b64) => ({ b64_json: b64 }));
63
+ } else {
64
+ data = imageUrls.map((url) => ({
65
+ url,
66
+ }));
67
+ }
68
+ return {
69
+ created: util.unixTimestamp(),
70
+ data,
71
+ };
72
+ },
73
+
74
+ // 图片合成路由(图生图)
75
+ "/compositions": async (request: Request) => {
76
+ // 检查是否使用了不支持的参数
77
+ const unsupportedParams = ['size', 'width', 'height'];
78
+ const bodyKeys = Object.keys(request.body);
79
+ const foundUnsupported = unsupportedParams.filter(param => bodyKeys.includes(param));
80
+
81
+ if (foundUnsupported.length > 0) {
82
+ throw new Error(`不支持的参数: ${foundUnsupported.join(', ')}。请使用 ratio 和 resolution 参数控制图像尺寸。`);
83
+ }
84
+
85
+ const contentType = request.headers['content-type'] || '';
86
+ const isMultiPart = contentType.startsWith('multipart/form-data');
87
+
88
+ if (isMultiPart) {
89
+ request
90
+ .validate("body.model", v => _.isUndefined(v) || _.isString(v))
91
+ .validate("body.prompt", _.isString)
92
+ .validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
93
+ .validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
94
+ .validate("body.resolution", v => _.isUndefined(v) || _.isString(v))
95
+ .validate("body.intelligent_ratio", v => _.isUndefined(v) || (typeof v === 'string' && (v === 'true' || v === 'false')) || _.isBoolean(v))
96
+ .validate("body.sample_strength", v => _.isUndefined(v) || (typeof v === 'string' && !isNaN(parseFloat(v))) || _.isFinite(v))
97
+ .validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
98
+ .validate("headers.authorization", _.isString);
99
+ } else {
100
+ request
101
+ .validate("body.model", v => _.isUndefined(v) || _.isString(v))
102
+ .validate("body.prompt", _.isString)
103
+ .validate("body.images", _.isArray)
104
+ .validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
105
+ .validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
106
+ .validate("body.resolution", v => _.isUndefined(v) || _.isString(v))
107
+ .validate("body.intelligent_ratio", v => _.isUndefined(v) || _.isBoolean(v))
108
+ .validate("body.sample_strength", v => _.isUndefined(v) || _.isFinite(v))
109
+ .validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
110
+ .validate("headers.authorization", _.isString);
111
+ }
112
+
113
+ let images: (string | Buffer)[] = [];
114
+ if (isMultiPart) {
115
+ const files = request.files?.images;
116
+ if (!files) {
117
+ throw new Error("在form-data��缺少 'images' 字段");
118
+ }
119
+ const imageFiles = Array.isArray(files) ? files : [files];
120
+ if (imageFiles.length === 0) {
121
+ throw new Error("至少需要提供1张输入图片");
122
+ }
123
+ if (imageFiles.length > 10) {
124
+ throw new Error("最多支持10张输入图片");
125
+ }
126
+ images = imageFiles.map(file => fs.readFileSync(file.filepath));
127
+ } else {
128
+ const bodyImages = request.body.images;
129
+ if (!bodyImages || bodyImages.length === 0) {
130
+ throw new Error("至少需要提供1张输入图片");
131
+ }
132
+ if (bodyImages.length > 10) {
133
+ throw new Error("最多支持10张输入图片");
134
+ }
135
+ bodyImages.forEach((image: any, index: number) => {
136
+ if (!_.isString(image) && !_.isObject(image)) {
137
+ throw new Error(`图片 ${index + 1} 格式不正确:应为URL字符串或包含url字段的对象`);
138
+ }
139
+ if (_.isObject(image) && !image.url) {
140
+ throw new Error(`图片 ${index + 1} 缺少url字段`);
141
+ }
142
+ });
143
+ images = bodyImages.map((image: any) => _.isString(image) ? image : image.url);
144
+ }
145
+
146
+ // refresh_token切分
147
+ const tokens = tokenSplit(request.headers.authorization);
148
+ // 随机挑选一个refresh_token
149
+ const token = _.sample(tokens);
150
+
151
+ const {
152
+ model,
153
+ prompt,
154
+ negative_prompt: negativePrompt,
155
+ ratio,
156
+ resolution,
157
+ intelligent_ratio: intelligentRatio,
158
+ sample_strength: sampleStrength,
159
+ response_format,
160
+ } = request.body;
161
+
162
+ // 如果是 multipart/form-data,需要将字符串转换为数字和布尔值
163
+ const finalSampleStrength = isMultiPart && typeof sampleStrength === 'string'
164
+ ? parseFloat(sampleStrength)
165
+ : sampleStrength;
166
+
167
+ const finalIntelligentRatio = isMultiPart && typeof intelligentRatio === 'string'
168
+ ? intelligentRatio === 'true'
169
+ : intelligentRatio;
170
+
171
+ const responseFormat = _.defaultTo(response_format, "url");
172
+ const resultUrls = await generateImageComposition(model, prompt, images, {
173
+ ratio,
174
+ resolution,
175
+ sampleStrength: finalSampleStrength,
176
+ negativePrompt,
177
+ intelligentRatio: finalIntelligentRatio,
178
+ }, token);
179
+
180
+ let data = [];
181
+ if (responseFormat == "b64_json") {
182
+ data = (
183
+ await Promise.all(resultUrls.map((url) => util.fetchFileBASE64(url)))
184
+ ).map((b64) => ({ b64_json: b64 }));
185
+ } else {
186
+ data = resultUrls.map((url) => ({
187
+ url,
188
+ }));
189
+ }
190
+
191
+ return {
192
+ created: util.unixTimestamp(),
193
+ data,
194
+ input_images: images.length,
195
+ composition_type: "multi_image_synthesis",
196
+ };
197
+ },
198
+ },
199
+ };
src/api/routes/index.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs-extra';
2
+
3
+ import Response from '@/lib/response/Response.ts';
4
+ import images from "./images.ts";
5
+ import chat from "./chat.ts";
6
+ import ping from "./ping.ts";
7
+ import token from './token.js';
8
+ import models from './models.ts';
9
+ import videos from './videos.ts';
10
+
11
+ export default [
12
+ {
13
+ get: {
14
+ '/': async () => {
15
+ const content = await fs.readFile('public/welcome.html');
16
+ return new Response(content, {
17
+ type: 'html',
18
+ headers: {
19
+ Expires: '-1'
20
+ }
21
+ });
22
+ }
23
+ }
24
+ },
25
+ images,
26
+ chat,
27
+ ping,
28
+ token,
29
+ models,
30
+ videos
31
+ ];
src/api/routes/models.ts ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ export default {
4
+
5
+ prefix: '/v1',
6
+
7
+ get: {
8
+ '/models': async () => {
9
+ return {
10
+ "data": [
11
+ {
12
+ "id": "jimeng",
13
+ "object": "model",
14
+ "owned_by": "jimeng-free-api"
15
+ },
16
+ {
17
+ "id": "jimeng-4.5",
18
+ "object": "model",
19
+ "owned_by": "jimeng-free-api",
20
+ "description": "即梦AI图像生成模型 4.5 版本(最新)"
21
+ },
22
+ {
23
+ "id": "jimeng-4.1",
24
+ "object": "model",
25
+ "owned_by": "jimeng-free-api",
26
+ "description": "即梦AI图像生成模型 4.1 版本"
27
+ },
28
+ {
29
+ "id": "jimeng-4.0",
30
+ "object": "model",
31
+ "owned_by": "jimeng-free-api",
32
+ "description": "即梦AI图像生成模型 4.0 版本"
33
+ },
34
+ {
35
+ "id": "jimeng-3.1",
36
+ "object": "model",
37
+ "owned_by": "jimeng-free-api",
38
+ "description": "即梦AI图像生成模型 3.1 版本"
39
+ },
40
+ {
41
+ "id": "jimeng-3.0",
42
+ "object": "model",
43
+ "owned_by": "jimeng-free-api",
44
+ "description": "即梦AI图像生成模型 3.0 版本"
45
+ },
46
+ {
47
+ "id": "jimeng-2.1",
48
+ "object": "model",
49
+ "owned_by": "jimeng-free-api",
50
+ "description": "即梦AI图像生成模型 2.1 版本"
51
+ },
52
+ {
53
+ "id": "jimeng-2.0-pro",
54
+ "object": "model",
55
+ "owned_by": "jimeng-free-api",
56
+ "description": "即梦AI图像生成模型 2.0 专业版"
57
+ },
58
+ {
59
+ "id": "jimeng-2.0",
60
+ "object": "model",
61
+ "owned_by": "jimeng-free-api",
62
+ "description": "即梦AI图像生成模型 2.0 版本"
63
+ },
64
+ {
65
+ "id": "jimeng-1.4",
66
+ "object": "model",
67
+ "owned_by": "jimeng-free-api",
68
+ "description": "即梦AI图像生成模型 1.4 版本"
69
+ },
70
+ {
71
+ "id": "jimeng-xl-pro",
72
+ "object": "model",
73
+ "owned_by": "jimeng-free-api",
74
+ "description": "即梦AI图像生成模型 XL Pro 版本"
75
+ },
76
+ {
77
+ "id": "jimeng-video-3.0",
78
+ "object": "model",
79
+ "owned_by": "jimeng-free-api",
80
+ "description": "即梦AI视频生成模型 3.0 版本"
81
+ },
82
+ {
83
+ "id": "jimeng-video-3.0-pro",
84
+ "object": "model",
85
+ "owned_by": "jimeng-free-api",
86
+ "description": "即梦AI视频生成模型 3.0 专业版"
87
+ },
88
+ {
89
+ "id": "jimeng-video-2.0",
90
+ "object": "model",
91
+ "owned_by": "jimeng-free-api",
92
+ "description": "即梦AI视频生成模型 2.0 版本"
93
+ },
94
+ {
95
+ "id": "jimeng-video-2.0-pro",
96
+ "object": "model",
97
+ "owned_by": "jimeng-free-api",
98
+ "description": "即梦AI视频生成模型 2.0 专业版"
99
+ }
100
+ ]
101
+ };
102
+ }
103
+
104
+ }
105
+ }
src/api/routes/ping.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ prefix: '/ping',
3
+ get: {
4
+ '': async () => "pong"
5
+ }
6
+ }
src/api/routes/token.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Request from '@/lib/request/Request.ts';
4
+ import Response from '@/lib/response/Response.ts';
5
+ import { getTokenLiveStatus, getCredit, tokenSplit } from '@/api/controllers/core.ts';
6
+ import logger from '@/lib/logger.ts';
7
+
8
+ export default {
9
+
10
+ prefix: '/token',
11
+
12
+ post: {
13
+
14
+ '/check': async (request: Request) => {
15
+ request
16
+ .validate('body.token', _.isString)
17
+ const live = await getTokenLiveStatus(request.body.token);
18
+ return {
19
+ live
20
+ }
21
+ },
22
+
23
+ '/points': async (request: Request) => {
24
+ request
25
+ .validate('headers.authorization', _.isString)
26
+ // refresh_token切分
27
+ const tokens = tokenSplit(request.headers.authorization);
28
+ const points = await Promise.all(tokens.map(async (token) => {
29
+ return {
30
+ token,
31
+ points: await getCredit(token)
32
+ }
33
+ }))
34
+ return points;
35
+ }
36
+
37
+ }
38
+
39
+ }
src/api/routes/videos.ts ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import _ from 'lodash';
2
+
3
+ import Request from '@/lib/request/Request.ts';
4
+ import Response from '@/lib/response/Response.ts';
5
+ import { tokenSplit } from '@/api/controllers/core.ts';
6
+ import { generateVideo, DEFAULT_MODEL } from '@/api/controllers/videos.ts';
7
+ import util from '@/lib/util.ts';
8
+
9
+ export default {
10
+
11
+ prefix: '/v1/videos',
12
+
13
+ post: {
14
+
15
+ '/generations': async (request: Request) => {
16
+ // 检查是否使用了不支持的参数
17
+ const unsupportedParams = ['size', 'width', 'height'];
18
+ const bodyKeys = Object.keys(request.body);
19
+ const foundUnsupported = unsupportedParams.filter(param => bodyKeys.includes(param));
20
+
21
+ if (foundUnsupported.length > 0) {
22
+ throw new Error(`不支持的参数: ${foundUnsupported.join(', ')}。请使用 ratio 和 resolution 参数控制视频尺寸。`);
23
+ }
24
+
25
+ const contentType = request.headers['content-type'] || '';
26
+ const isMultiPart = contentType.startsWith('multipart/form-data');
27
+
28
+ request
29
+ .validate('body.model', v => _.isUndefined(v) || _.isString(v))
30
+ .validate('body.prompt', _.isString)
31
+ .validate('body.ratio', v => _.isUndefined(v) || _.isString(v))
32
+ .validate('body.resolution', v => _.isUndefined(v) || _.isString(v))
33
+ .validate('body.duration', v => {
34
+ if (_.isUndefined(v)) return true;
35
+ // 对于 multipart/form-data,允许字符串类型的数字
36
+ if (isMultiPart && typeof v === 'string') {
37
+ const num = parseInt(v);
38
+ return num === 5 || num === 10;
39
+ }
40
+ // 对于 JSON,要求数字类型
41
+ return _.isFinite(v) && (v === 5 || v === 10);
42
+ })
43
+ .validate('body.file_paths', v => _.isUndefined(v) || _.isArray(v))
44
+ .validate('body.filePaths', v => _.isUndefined(v) || _.isArray(v))
45
+ .validate('body.response_format', v => _.isUndefined(v) || _.isString(v))
46
+ .validate('headers.authorization', _.isString);
47
+
48
+ // refresh_token切分
49
+ const tokens = tokenSplit(request.headers.authorization);
50
+ // 随机挑选一个refresh_token
51
+ const token = _.sample(tokens);
52
+
53
+ const {
54
+ model = DEFAULT_MODEL,
55
+ prompt,
56
+ ratio = "1:1",
57
+ resolution = "720p",
58
+ duration = 5,
59
+ file_paths = [],
60
+ filePaths = [],
61
+ response_format = "url"
62
+ } = request.body;
63
+
64
+ // 如果是 multipart/form-data,需要将字符串转换为数字
65
+ const finalDuration = isMultiPart && typeof duration === 'string'
66
+ ? parseInt(duration)
67
+ : duration;
68
+
69
+ // 兼容两种参数名格式:file_paths 和 filePaths
70
+ const finalFilePaths = filePaths.length > 0 ? filePaths : file_paths;
71
+
72
+ // 生成视频
73
+ const videoUrl = await generateVideo(
74
+ model,
75
+ prompt,
76
+ {
77
+ ratio,
78
+ resolution,
79
+ duration: finalDuration,
80
+ filePaths: finalFilePaths,
81
+ files: request.files, // 传递上传的文件
82
+ },
83
+ token
84
+ );
85
+
86
+ // 根据response_format返回不同格式的结果
87
+ if (response_format === "b64_json") {
88
+ // 获取视频内容并转换为BASE64
89
+ const videoBase64 = await util.fetchFileBASE64(videoUrl);
90
+ return {
91
+ created: util.unixTimestamp(),
92
+ data: [{
93
+ b64_json: videoBase64,
94
+ revised_prompt: prompt
95
+ }]
96
+ };
97
+ } else {
98
+ // 默认返回URL
99
+ return {
100
+ created: util.unixTimestamp(),
101
+ data: [{
102
+ url: videoUrl,
103
+ revised_prompt: prompt
104
+ }]
105
+ };
106
+ }
107
+ }
108
+
109
+ }
110
+
111
+ }
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("<<<< jimeng 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/model-config.ts ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 模型配置文件
3
+ * 定义不同模型的特定参数和配置
4
+ */
5
+
6
+ export interface ModelConfig {
7
+ // 模型内部名称
8
+ internalModel: string;
9
+ // draft版本
10
+ draftVersion: string;
11
+ // 支持的功能
12
+ features: {
13
+ // 是否支持多图生成
14
+ multiImage: boolean;
15
+ // 是否支持图生图
16
+ imageToImage: boolean;
17
+ // 是否支持视频生成
18
+ videoGeneration: boolean;
19
+ };
20
+ // 默认参数
21
+ defaultParams: {
22
+ // 默认宽度
23
+ width: number;
24
+ // 默认高度
25
+ height: number;
26
+ // 支持的分辨率列表
27
+ resolutions: Array<{ width: number; height: number }>;
28
+ // 采样强度范围
29
+ sampleStrengthRange: [number, number];
30
+ };
31
+ // 特殊配置
32
+ specialConfig?: {
33
+ // 是否需要特定的头部信息
34
+ specialHeaders?: Record<string, string>;
35
+ // 是否有特殊的参数要求
36
+ extraParams?: Record<string, any>;
37
+ };
38
+ }
39
+
40
+ // 模型配置映射
41
+ export const MODEL_CONFIGS: Record<string, ModelConfig> = {
42
+ "jimeng-4.5": {
43
+ internalModel: "high_aes_general_v40l",
44
+ draftVersion: "3.3.4",
45
+ features: {
46
+ multiImage: true,
47
+ imageToImage: true,
48
+ videoGeneration: false,
49
+ },
50
+ defaultParams: {
51
+ width: 2048,
52
+ height: 2048,
53
+ resolutions: [
54
+ { width: 1024, height: 1024 },
55
+ { width: 768, height: 1024 },
56
+ { width: 1024, height: 768 },
57
+ { width: 1024, height: 576 },
58
+ { width: 576, height: 1024 },
59
+ { width: 1024, height: 682 },
60
+ { width: 682, height: 1024 },
61
+ { width: 1195, height: 512 },
62
+ { width: 2048, height: 2048 },
63
+ { width: 2304, height: 1728 },
64
+ { width: 1728, height: 2304 },
65
+ { width: 2560, height: 1440 },
66
+ { width: 1440, height: 2560 },
67
+ { width: 2496, height: 1664 },
68
+ { width: 1664, height: 2496 },
69
+ { width: 3024, height: 1296 },
70
+ ],
71
+ sampleStrengthRange: [0.1, 1.0],
72
+ },
73
+ },
74
+ "jimeng-4.1": {
75
+ internalModel: "high_aes_general_v41",
76
+ draftVersion: "3.3.4",
77
+ features: {
78
+ multiImage: true,
79
+ imageToImage: true,
80
+ videoGeneration: false,
81
+ },
82
+ defaultParams: {
83
+ width: 2048,
84
+ height: 2048,
85
+ resolutions: [
86
+ { width: 1024, height: 1024 },
87
+ { width: 768, height: 1024 },
88
+ { width: 1024, height: 768 },
89
+ { width: 1024, height: 576 },
90
+ { width: 576, height: 1024 },
91
+ { width: 1024, height: 682 },
92
+ { width: 682, height: 1024 },
93
+ { width: 1195, height: 512 },
94
+ { width: 2048, height: 2048 },
95
+ { width: 2304, height: 1728 },
96
+ { width: 1728, height: 2304 },
97
+ { width: 2560, height: 1440 },
98
+ { width: 1440, height: 2560 },
99
+ { width: 2496, height: 1664 },
100
+ { width: 1664, height: 2496 },
101
+ { width: 3024, height: 1296 },
102
+ ],
103
+ sampleStrengthRange: [0.1, 1.0],
104
+ },
105
+ },
106
+ "jimeng-4.0": {
107
+ internalModel: "high_aes_general_v40",
108
+ draftVersion: "3.3.4",
109
+ features: {
110
+ multiImage: true,
111
+ imageToImage: true,
112
+ videoGeneration: false,
113
+ },
114
+ defaultParams: {
115
+ width: 2048,
116
+ height: 2048,
117
+ resolutions: [
118
+ { width: 1024, height: 1024 },
119
+ { width: 768, height: 1024 },
120
+ { width: 1024, height: 768 },
121
+ { width: 1024, height: 576 },
122
+ { width: 576, height: 1024 },
123
+ { width: 1024, height: 682 },
124
+ { width: 682, height: 1024 },
125
+ { width: 1195, height: 512 },
126
+ { width: 2048, height: 2048 },
127
+ { width: 2304, height: 1728 },
128
+ { width: 1728, height: 2304 },
129
+ { width: 2560, height: 1440 },
130
+ { width: 1440, height: 2560 },
131
+ { width: 2496, height: 1664 },
132
+ { width: 1664, height: 2496 },
133
+ { width: 3024, height: 1296 },
134
+ ],
135
+ sampleStrengthRange: [0.1, 1.0],
136
+ },
137
+ },
138
+ "jimeng-3.1": {
139
+ internalModel: "high_aes_general_v30l_art_fangzhou:general_v3.0_18b",
140
+ draftVersion: "3.0.2",
141
+ features: {
142
+ multiImage: false,
143
+ imageToImage: true,
144
+ videoGeneration: false,
145
+ },
146
+ defaultParams: {
147
+ width: 1024,
148
+ height: 1024,
149
+ resolutions: [
150
+ { width: 512, height: 512 },
151
+ { width: 768, height: 768 },
152
+ { width: 1024, height: 1024 },
153
+ ],
154
+ sampleStrengthRange: [0.1, 0.8],
155
+ },
156
+ },
157
+ "jimeng-3.0": {
158
+ internalModel: "high_aes_general_v30l:general_v3.0_18b",
159
+ draftVersion: "3.0.2",
160
+ features: {
161
+ multiImage: false,
162
+ imageToImage: true,
163
+ videoGeneration: false,
164
+ },
165
+ defaultParams: {
166
+ width: 1024,
167
+ height: 1024,
168
+ resolutions: [
169
+ { width: 512, height: 512 },
170
+ { width: 768, height: 768 },
171
+ { width: 1024, height: 1024 },
172
+ ],
173
+ sampleStrengthRange: [0.1, 0.8],
174
+ },
175
+ },
176
+ "jimeng-2.1": {
177
+ internalModel: "high_aes_general_v21_L:general_v2.1_L",
178
+ draftVersion: "3.0.2",
179
+ features: {
180
+ multiImage: false,
181
+ imageToImage: true,
182
+ videoGeneration: false,
183
+ },
184
+ defaultParams: {
185
+ width: 512,
186
+ height: 512,
187
+ resolutions: [
188
+ { width: 512, height: 512 },
189
+ { width: 768, height: 768 },
190
+ ],
191
+ sampleStrengthRange: [0.1, 0.7],
192
+ },
193
+ },
194
+ "jimeng-2.0-pro": {
195
+ internalModel: "high_aes_general_v20_L:general_v2.0_L",
196
+ draftVersion: "3.0.2",
197
+ features: {
198
+ multiImage: false,
199
+ imageToImage: true,
200
+ videoGeneration: false,
201
+ },
202
+ defaultParams: {
203
+ width: 512,
204
+ height: 512,
205
+ resolutions: [
206
+ { width: 512, height: 512 },
207
+ { width: 768, height: 768 },
208
+ ],
209
+ sampleStrengthRange: [0.1, 0.7],
210
+ },
211
+ },
212
+ "jimeng-2.0": {
213
+ internalModel: "high_aes_general_v20",
214
+ draftVersion: "3.0.2",
215
+ features: {
216
+ multiImage: false,
217
+ imageToImage: true,
218
+ videoGeneration: false,
219
+ },
220
+ defaultParams: {
221
+ width: 512,
222
+ height: 512,
223
+ resolutions: [
224
+ { width: 512, height: 512 },
225
+ { width: 768, height: 768 },
226
+ ],
227
+ sampleStrengthRange: [0.1, 0.7],
228
+ },
229
+ },
230
+ "jimeng-1.4": {
231
+ internalModel: "high_aes_general_v14:general_v1.4",
232
+ draftVersion: "3.0.2",
233
+ features: {
234
+ multiImage: false,
235
+ imageToImage: true,
236
+ videoGeneration: false,
237
+ },
238
+ defaultParams: {
239
+ width: 512,
240
+ height: 512,
241
+ resolutions: [
242
+ { width: 512, height: 512 },
243
+ { width: 768, height: 768 },
244
+ ],
245
+ sampleStrengthRange: [0.1, 0.6],
246
+ },
247
+ },
248
+ "jimeng-xl-pro": {
249
+ internalModel: "text2img_xl_sft",
250
+ draftVersion: "3.0.2",
251
+ features: {
252
+ multiImage: false,
253
+ imageToImage: true,
254
+ videoGeneration: false,
255
+ },
256
+ defaultParams: {
257
+ width: 1024,
258
+ height: 1024,
259
+ resolutions: [
260
+ { width: 1024, height: 1024 },
261
+ { width: 1280, height: 720 },
262
+ { width: 720, height: 1280 },
263
+ ],
264
+ sampleStrengthRange: [0.1, 0.8],
265
+ },
266
+ },
267
+ };
268
+
269
+ // 获取模型配置
270
+ export function getModelConfig(modelName: string): ModelConfig {
271
+ const config = MODEL_CONFIGS[modelName];
272
+ if (!config) {
273
+ throw new Error(`Unsupported model: ${modelName}`);
274
+ }
275
+ return config;
276
+ }
277
+
278
+ // 获取所有支持的图像生成模型
279
+ export function getSupportedImageModels(): string[] {
280
+ return Object.keys(MODEL_CONFIGS);
281
+ }
282
+
283
+ // 检查模型是否支持特定功能
284
+ export function doesModelSupport(modelName: string, feature: keyof ModelConfig['features']): boolean {
285
+ const config = getModelConfig(modelName);
286
+ return config.features[feature];
287
+ }
288
+
289
+ // 验证参数是否在模型支持的范围内
290
+ export function validateModelParams(modelName: string, params: {
291
+ width?: number;
292
+ height?: number;
293
+ sampleStrength?: number;
294
+ }): { isValid: boolean; errors: string[] } {
295
+ const config = getModelConfig(modelName);
296
+ const errors: string[] = [];
297
+
298
+ // 验证分辨率
299
+ if (params.width && params.height) {
300
+ const isValidResolution = config.defaultParams.resolutions.some(
301
+ res => res.width === params.width && res.height === params.height
302
+ );
303
+ if (!isValidResolution) {
304
+ errors.push(
305
+ `Unsupported resolution ${params.width}x${params.height}. Supported resolutions: ${config.defaultParams.resolutions.map(r => `${r.width}x${r.height}`).join(', ')}`
306
+ );
307
+ }
308
+ }
309
+
310
+ // 验证采样强度
311
+ if (params.sampleStrength !== undefined) {
312
+ const [min, max] = config.defaultParams.sampleStrengthRange;
313
+ if (params.sampleStrength < min || params.sampleStrength > max) {
314
+ errors.push(`Sample strength must be between ${min} and ${max}`);
315
+ }
316
+ }
317
+
318
+ return {
319
+ isValid: errors.length === 0,
320
+ errors
321
+ };
322
+ }
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, 'jimeng-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: ['form', 'text', 'xml'], // 移除 json,由自定义中间件处理
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, message?: string) {
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, message || `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,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // 自定义 JSON 解析中间件
39
+ this.app.use(async (ctx: any, next: Function) => {
40
+ if (ctx.is('application/json') && ['POST', 'PUT', 'PATCH'].includes(ctx.method)) {
41
+ logger.debug('开始自定义 JSON 解析');
42
+ const chunks: Buffer[] = [];
43
+
44
+ await new Promise((resolve, reject) => {
45
+ ctx.req.on('data', (chunk: Buffer) => {
46
+ chunks.push(chunk);
47
+ });
48
+
49
+ ctx.req.on('end', () => {
50
+ resolve(null);
51
+ });
52
+
53
+ ctx.req.on('error', reject);
54
+ });
55
+
56
+ const body = Buffer.concat(chunks).toString('utf8');
57
+
58
+ // 清理问题字符
59
+ let cleanedBody = body
60
+ .replace(/\r\n/g, '\n')
61
+ .replace(/\r/g, '\n')
62
+ .replace(/\u00A0/g, ' ')
63
+ .replace(/[\u2000-\u200B]/g, ' ')
64
+ .replace(/\uFEFF/g, '')
65
+ .trim();
66
+
67
+ const parsedBody = JSON.parse(cleanedBody);
68
+
69
+ logger.debug('JSON 解析成功,跳过 koa-body');
70
+
71
+ ctx.request.body = parsedBody;
72
+ ctx.request.rawBody = cleanedBody;
73
+
74
+ // 标记已处理,避免 koa-body 再次处理
75
+ ctx._jsonProcessed = true;
76
+ }
77
+ await next();
78
+ });
79
+
80
+ // 载荷解析器支持(只处理未被自定义解析器处理的请求)
81
+ this.app.use(async (ctx: any, next: Function) => {
82
+ if (!ctx._jsonProcessed) {
83
+ await koaBody(Object.assign(_.clone(config.system.requestBody), {
84
+ enableTypes: ['form', 'text', 'xml']
85
+ }))(ctx, next);
86
+ } else {
87
+ await next();
88
+ }
89
+ });
90
+ this.app.on("error", (err: any) => {
91
+ // 忽略连接重试、中断、管道、取消错误
92
+ if (["ECONNRESET", "ECONNABORTED", "EPIPE", "ECANCELED"].includes(err.code)) return;
93
+ logger.error(err);
94
+ });
95
+ logger.success("Server initialized");
96
+ }
97
+
98
+ /**
99
+ * 附加路由
100
+ *
101
+ * @param routes 路由列表
102
+ */
103
+ attachRoutes(routes: any[]) {
104
+ routes.forEach((route: any) => {
105
+ const prefix = route.prefix || "";
106
+ for (let method in route) {
107
+ if(method === "prefix") continue;
108
+ if (!_.isObject(route[method])) {
109
+ logger.warn(`Router ${prefix} ${method} invalid`);
110
+ continue;
111
+ }
112
+ for (let uri in route[method]) {
113
+ this.router[method](`${prefix}${uri}`, async ctx => {
114
+ const { request, response } = await this.#requestProcessing(ctx, route[method][uri]);
115
+ if(response != null && config.system.requestLog)
116
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
117
+ });
118
+ }
119
+ }
120
+ logger.info(`Route ${config.service.urlPrefix || ""}${prefix} attached`);
121
+ });
122
+ this.app.use(this.router.routes());
123
+ this.app.use((ctx: any) => {
124
+ const request = new Request(ctx);
125
+ logger.debug(`-> ${ctx.request.method} ${ctx.request.url} request is not supported - ${request.remoteIP || "unknown"}`);
126
+ // const failureBody = new FailureBody(new Exception(EX.SYSTEM_NOT_ROUTE_MATCHING, "Request is not supported"));
127
+ // const response = new Response(failureBody);
128
+ const message = `[请求有误]: 正确请求为 POST -> /v1/chat/completions,当前请求为 ${ctx.request.method} -> ${ctx.request.url} 请纠正`;
129
+ logger.warn(message);
130
+ const failureBody = new FailureBody(new Error(message));
131
+ const response = new Response(failureBody);
132
+ response.injectTo(ctx);
133
+ if(config.system.requestLog)
134
+ logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`);
135
+ });
136
+ }
137
+
138
+ /**
139
+ * 请求处理
140
+ *
141
+ * @param ctx 上下文
142
+ * @param routeFn 路由方法
143
+ */
144
+ #requestProcessing(ctx: any, routeFn: Function): Promise<any> {
145
+ return new Promise(resolve => {
146
+ const request = new Request(ctx);
147
+ try {
148
+ if(config.system.requestLog)
149
+ logger.info(`-> ${request.method} ${request.url}`);
150
+ routeFn(request)
151
+ .then(response => {
152
+ try {
153
+ if(!Response.isInstance(response)) {
154
+ const _response = new Response(response);
155
+ _response.injectTo(ctx);
156
+ return resolve({ request, response: _response });
157
+ }
158
+ response.injectTo(ctx);
159
+ resolve({ request, response });
160
+ }
161
+ catch(err) {
162
+ logger.error(err);
163
+ const failureBody = new FailureBody(err);
164
+ const response = new Response(failureBody);
165
+ response.injectTo(ctx);
166
+ resolve({ request, response });
167
+ }
168
+ })
169
+ .catch(err => {
170
+ try {
171
+ logger.error(err);
172
+ const failureBody = new FailureBody(err);
173
+ const response = new Response(failureBody);
174
+ response.injectTo(ctx);
175
+ resolve({ request, response });
176
+ }
177
+ catch(err) {
178
+ logger.error(err);
179
+ const failureBody = new FailureBody(err);
180
+ const response = new Response(failureBody);
181
+ response.injectTo(ctx);
182
+ resolve({ request, response });
183
+ }
184
+ });
185
+ }
186
+ catch(err) {
187
+ logger.error(err);
188
+ const failureBody = new FailureBody(err);
189
+ const response = new Response(failureBody);
190
+ response.injectTo(ctx);
191
+ resolve({ request, response });
192
+ }
193
+ });
194
+ }
195
+
196
+ /**
197
+ * 监听端口
198
+ */
199
+ async listen() {
200
+ const host = config.service.host;
201
+ const port = config.service.port;
202
+ await Promise.all([
203
+ new Promise((resolve, reject) => {
204
+ if(host === "0.0.0.0" || host === "localhost" || host === "127.0.0.1")
205
+ return resolve(null);
206
+ this.app.listen(port, "localhost", err => {
207
+ if(err) return reject(err);
208
+ resolve(null);
209
+ });
210
+ }),
211
+ new Promise((resolve, reject) => {
212
+ this.app.listen(port, host, err => {
213
+ if(err) return reject(err);
214
+ resolve(null);
215
+ });
216
+ })
217
+ ]);
218
+ logger.success(`Server listening on port ${port} (${host})`);
219
+ }
220
+
221
+ }
222
+
223
+ 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 axios from "axios";
9
+ import fs from "fs-extra";
10
+ import { v1 as uuid } from "uuid";
11
+ import { format as dateFormat } from "date-fns";
12
+ import CRC32 from "crc-32";
13
+ import randomstring from "randomstring";
14
+ import _ from "lodash";
15
+ import { CronJob } from "cron";
16
+
17
+ import HTTP_STATUS_CODE from "./http-status-codes.ts";
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
+ }