Switch to kiro-rs (Rust) - build from source
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +0 -21
- Dockerfile +22 -16
- LICENSE +0 -674
- README.md +3 -8
- VERSION +0 -1
- configs/api-potluck-data.json.example +0 -29
- configs/api-potluck-keys.json.example +0 -16
- configs/config.json +0 -30
- configs/config.json.example +0 -63
- configs/kiro/chatgpt_account1_kiro-auth-token/chatgpt_account1_kiro-auth-token.json +0 -9
- configs/kiro/chatgpt_account2_kiro-auth-token/chatgpt_account2_kiro-auth-token.json +0 -9
- configs/kiro/fresh1_kiro-auth-token/fresh1_kiro-auth-token.json +0 -9
- configs/kiro/personal_kiro-auth-token/personal_kiro-auth-token.json +0 -7
- configs/plugins.json +0 -6
- configs/plugins.json.example +0 -12
- configs/provider_pools.json +0 -43
- configs/provider_pools.json.example +0 -213
- configs/pwd +0 -1
- entrypoint.sh +28 -0
- healthcheck.js +0 -46
- package-lock.json +0 -0
- package.json +0 -42
- src/auth/codex-oauth.js +0 -1056
- src/auth/gemini-oauth.js +0 -504
- src/auth/iflow-oauth.js +0 -539
- src/auth/index.js +0 -35
- src/auth/kiro-oauth.js +0 -1149
- src/auth/oauth-handlers.js +0 -27
- src/auth/qwen-oauth.js +0 -343
- src/convert/convert-old.js +0 -0
- src/convert/convert.js +0 -392
- src/converters/BaseConverter.js +0 -115
- src/converters/ConverterFactory.js +0 -183
- src/converters/register-converters.js +0 -29
- src/converters/strategies/ClaudeConverter.js +0 -2234
- src/converters/strategies/CodexConverter.js +0 -1327
- src/converters/strategies/GeminiConverter.js +0 -1529
- src/converters/strategies/GrokConverter.js +0 -1153
- src/converters/strategies/OpenAIConverter.js +0 -1769
- src/converters/strategies/OpenAIResponsesConverter.js +0 -1032
- src/converters/utils.js +0 -369
- src/core/config-manager.js +0 -249
- src/core/master.js +0 -395
- src/core/plugin-manager.js +0 -549
- src/handlers/request-handler.js +0 -271
- src/plugins/ai-monitor/index.js +0 -145
- src/plugins/api-potluck/api-routes.js +0 -452
- src/plugins/api-potluck/index.js +0 -208
- src/plugins/api-potluck/key-manager.js +0 -486
- src/plugins/api-potluck/middleware.js +0 -166
.dockerignore
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
node_modules
|
| 2 |
-
npm-debug.log
|
| 3 |
-
.git
|
| 4 |
-
.gitignore
|
| 5 |
-
Dockerfile
|
| 6 |
-
.dockerignore
|
| 7 |
-
.nyc_output
|
| 8 |
-
coverage
|
| 9 |
-
.nyc_output
|
| 10 |
-
.coverage
|
| 11 |
-
.env
|
| 12 |
-
.env.local
|
| 13 |
-
.env.*.local
|
| 14 |
-
credentials.json
|
| 15 |
-
*.md
|
| 16 |
-
LICENSE
|
| 17 |
-
jest.config.js
|
| 18 |
-
babel.config.js
|
| 19 |
-
tests/
|
| 20 |
-
run-tests.js
|
| 21 |
-
test-summary.js
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,21 +1,27 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
RUN apk add --no-cache
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
WORKDIR /app
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
RUN
|
| 14 |
|
| 15 |
-
# HuggingFace Spaces requires port 7860
|
| 16 |
EXPOSE 7860
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
CMD node healthcheck.js || exit 1
|
| 20 |
-
|
| 21 |
-
CMD ["node", "src/core/master.js", "--port", "7860"]
|
|
|
|
| 1 |
+
# ── Stage 1: Build Admin UI ──
|
| 2 |
+
FROM node:22-alpine AS frontend-builder
|
| 3 |
+
RUN apk add --no-cache git
|
| 4 |
+
RUN git clone --depth 1 https://github.com/hank9999/kiro.rs.git /app
|
| 5 |
+
WORKDIR /app/admin-ui
|
| 6 |
+
RUN npm install -g pnpm && pnpm install && pnpm build
|
| 7 |
+
|
| 8 |
+
# ── Stage 2: Build Rust Binary ──
|
| 9 |
+
FROM rust:1.82-alpine AS builder
|
| 10 |
+
RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static git
|
| 11 |
+
RUN git clone --depth 1 https://github.com/hank9999/kiro.rs.git /app
|
| 12 |
+
COPY --from=frontend-builder /app/admin-ui/dist /app/admin-ui/dist
|
| 13 |
WORKDIR /app
|
| 14 |
+
RUN cargo build --release
|
| 15 |
|
| 16 |
+
# ── Stage 3: Runtime ──
|
| 17 |
+
FROM alpine:3.21
|
| 18 |
+
RUN apk add --no-cache ca-certificates
|
| 19 |
+
WORKDIR /app
|
| 20 |
+
COPY --from=builder /app/target/release/kiro-rs /app/kiro-rs
|
| 21 |
+
COPY entrypoint.sh /app/entrypoint.sh
|
| 22 |
+
RUN chmod +x /app/entrypoint.sh
|
| 23 |
|
|
|
|
| 24 |
EXPOSE 7860
|
| 25 |
+
VOLUME ["/app/config"]
|
| 26 |
|
| 27 |
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
|
|
|
|
|
|
|
LICENSE
DELETED
|
@@ -1,674 +0,0 @@
|
|
| 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,13 +1,8 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
app_port: 7860
|
| 9 |
---
|
| 10 |
-
|
| 11 |
-
# kiro2api
|
| 12 |
-
|
| 13 |
-
OpenAI/Anthropic-compatible API proxy for Kiro (Amazon Q Developer) powered by [aiclient-2-api](https://github.com/justlovemaki/aiclient-2-api).
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Kiro API Proxy
|
| 3 |
+
emoji: 🔑
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
app_port: 7860
|
| 8 |
---
|
|
|
|
|
|
|
|
|
|
|
|
VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
2.11.5.2
|
|
|
|
|
|
configs/api-potluck-data.json.example
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"config": {
|
| 3 |
-
"defaultDailyLimit": 500,
|
| 4 |
-
"bonusPerCredential": 300,
|
| 5 |
-
"bonusValidityDays": 30,
|
| 6 |
-
"persistInterval": 5000
|
| 7 |
-
},
|
| 8 |
-
"users": {
|
| 9 |
-
"maki_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": {
|
| 10 |
-
"credentials": [
|
| 11 |
-
{
|
| 12 |
-
"id": "cred_0000000000000_xxxxxx",
|
| 13 |
-
"path": "configs/kiro/example_kiro-auth-token/example_kiro-auth-token.json",
|
| 14 |
-
"provider": "claude-kiro-oauth",
|
| 15 |
-
"authMethod": "refresh-token",
|
| 16 |
-
"addedAt": "2026-01-01T00:00:00.000Z"
|
| 17 |
-
}
|
| 18 |
-
],
|
| 19 |
-
"credentialBonuses": [
|
| 20 |
-
{
|
| 21 |
-
"credentialId": "cred_0000000000000_xxxxxx",
|
| 22 |
-
"grantedAt": "2026-01-01T00:00:00.000Z",
|
| 23 |
-
"usedCount": 0
|
| 24 |
-
}
|
| 25 |
-
],
|
| 26 |
-
"createdAt": "2026-01-01T00:00:00.000Z"
|
| 27 |
-
}
|
| 28 |
-
}
|
| 29 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/api-potluck-keys.json.example
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"keys": {
|
| 3 |
-
"maki_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx": {
|
| 4 |
-
"id": "maki_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
| 5 |
-
"name": "示例用户",
|
| 6 |
-
"createdAt": "2026-01-01T00:00:00.000Z",
|
| 7 |
-
"dailyLimit": 500,
|
| 8 |
-
"todayUsage": 0,
|
| 9 |
-
"totalUsage": 0,
|
| 10 |
-
"lastResetDate": "2026-01-01",
|
| 11 |
-
"lastUsedAt": null,
|
| 12 |
-
"enabled": true,
|
| 13 |
-
"bonusRemaining": 0
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/config.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"REQUIRED_API_KEY": "sk-kiro2api",
|
| 3 |
-
"SERVER_PORT": 7860,
|
| 4 |
-
"HOST": "0.0.0.0",
|
| 5 |
-
"MODEL_PROVIDER": "claude-kiro-oauth",
|
| 6 |
-
"SYSTEM_PROMPT_FILE_PATH": "",
|
| 7 |
-
"SYSTEM_PROMPT_MODE": "overwrite",
|
| 8 |
-
"PROMPT_LOG_BASE_NAME": "prompt_log",
|
| 9 |
-
"PROMPT_LOG_MODE": "none",
|
| 10 |
-
"REQUEST_MAX_RETRIES": 3,
|
| 11 |
-
"REQUEST_BASE_DELAY": 1000,
|
| 12 |
-
"CRON_NEAR_MINUTES": 1,
|
| 13 |
-
"CRON_REFRESH_TOKEN": true,
|
| 14 |
-
"PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json",
|
| 15 |
-
"MAX_ERROR_COUNT": 3,
|
| 16 |
-
"providerFallbackChain": {},
|
| 17 |
-
"modelFallbackMapping": {},
|
| 18 |
-
"PROXY_URL": null,
|
| 19 |
-
"PROXY_ENABLED_PROVIDERS": [],
|
| 20 |
-
"LOG_ENABLED": true,
|
| 21 |
-
"LOG_OUTPUT_MODE": "all",
|
| 22 |
-
"LOG_LEVEL": "info",
|
| 23 |
-
"LOG_DIR": "logs",
|
| 24 |
-
"LOG_INCLUDE_REQUEST_ID": true,
|
| 25 |
-
"LOG_INCLUDE_TIMESTAMP": true,
|
| 26 |
-
"LOG_MAX_FILE_SIZE": 10485760,
|
| 27 |
-
"LOG_MAX_FILES": 5,
|
| 28 |
-
"TLS_SIDECAR_ENABLED": false,
|
| 29 |
-
"TLS_SIDECAR_PORT": 9090
|
| 30 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/config.json.example
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"REQUIRED_API_KEY": "123456",
|
| 3 |
-
"SERVER_PORT": 3000,
|
| 4 |
-
"HOST": "0.0.0.0",
|
| 5 |
-
"MODEL_PROVIDER": "gemini-cli-oauth",
|
| 6 |
-
"SYSTEM_PROMPT_FILE_PATH": "configs/input_system_prompt.txt",
|
| 7 |
-
"SYSTEM_PROMPT_MODE": "overwrite",
|
| 8 |
-
"PROMPT_LOG_BASE_NAME": "prompt_log",
|
| 9 |
-
"PROMPT_LOG_MODE": "none",
|
| 10 |
-
"REQUEST_MAX_RETRIES": 3,
|
| 11 |
-
"REQUEST_BASE_DELAY": 1000,
|
| 12 |
-
"CRON_NEAR_MINUTES": 1,
|
| 13 |
-
"CRON_REFRESH_TOKEN": false,
|
| 14 |
-
"PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json",
|
| 15 |
-
"MAX_ERROR_COUNT": 3,
|
| 16 |
-
"GROK_COOKIE_TOKEN": "your-sso-cookie-token",
|
| 17 |
-
"GROK_CF_CLEARANCE": "your-cf-clearance-cookie",
|
| 18 |
-
"GROK_USER_AGENT": "Mozilla/5.0 ...",
|
| 19 |
-
"GROK_BASE_URL": "https://grok.com",
|
| 20 |
-
"providerFallbackChain": {
|
| 21 |
-
"gemini-cli-oauth": ["gemini-antigravity"],
|
| 22 |
-
"gemini-antigravity": ["gemini-cli-oauth"],
|
| 23 |
-
"claude-kiro-oauth": ["claude-custom"],
|
| 24 |
-
"claude-custom": ["claude-kiro-oauth"]
|
| 25 |
-
},
|
| 26 |
-
"modelFallbackMapping": {
|
| 27 |
-
"gemini-claude-opus-4-5-thinking": {
|
| 28 |
-
"targetProviderType": "claude-kiro-oauth",
|
| 29 |
-
"targetModel": "claude-opus-4-5"
|
| 30 |
-
},
|
| 31 |
-
"gemini-claude-sonnet-4-5-thinking": {
|
| 32 |
-
"targetProviderType": "claude-kiro-oauth",
|
| 33 |
-
"targetModel": "claude-sonnet-4-5"
|
| 34 |
-
},
|
| 35 |
-
"gemini-claude-sonnet-4-5": {
|
| 36 |
-
"targetProviderType": "claude-kiro-oauth",
|
| 37 |
-
"targetModel": "claude-sonnet-4-5"
|
| 38 |
-
},
|
| 39 |
-
"claude-opus-4-5": {
|
| 40 |
-
"targetProviderType": "gemini-antigravity",
|
| 41 |
-
"targetModel": "gemini-claude-opus-4-5-thinking"
|
| 42 |
-
},
|
| 43 |
-
"claude-sonnet-4-5": {
|
| 44 |
-
"targetProviderType": "gemini-antigravity",
|
| 45 |
-
"targetModel": "gemini-claude-sonnet-4-5"
|
| 46 |
-
}
|
| 47 |
-
},
|
| 48 |
-
"PROXY_URL": "http://127.0.0.1:1089",
|
| 49 |
-
"PROXY_ENABLED_PROVIDERS": [
|
| 50 |
-
"gemini-cli-oauth",
|
| 51 |
-
"gemini-antigravity"
|
| 52 |
-
],
|
| 53 |
-
"LOG_ENABLED": true,
|
| 54 |
-
"LOG_OUTPUT_MODE": "all",
|
| 55 |
-
"LOG_LEVEL": "info",
|
| 56 |
-
"LOG_DIR": "logs",
|
| 57 |
-
"LOG_INCLUDE_REQUEST_ID": true,
|
| 58 |
-
"LOG_INCLUDE_TIMESTAMP": true,
|
| 59 |
-
"LOG_MAX_FILE_SIZE": 10485760,
|
| 60 |
-
"LOG_MAX_FILES": 10,
|
| 61 |
-
"TLS_SIDECAR_ENABLED": false,
|
| 62 |
-
"TLS_SIDECAR_PORT": 9090
|
| 63 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/kiro/chatgpt_account1_kiro-auth-token/chatgpt_account1_kiro-auth-token.json
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"accessToken": "aoaAAAAAGm84HYPHOFvOn_ATRhHeZMUuLcDZDbu_-hQrPp473ye8B5xlec8-gRa79ZftJpCSeNkJALBNwDuVc7wAkBkc0:MGUCMQDjWbPSbPmIX4c+42/YduE1MW3xBKwG4r32W2DCk8BYCBwvD638JC+R3VG4KYdWAE0CMGHsWX+omV0sLGLmX/jmmXOG1rzzXy81I///G7jJE2iYy4rPYXFwM/4Tujjufozm8A",
|
| 3 |
-
"refreshToken": "aorAAAAAGozeWAnmIAixMWulOIdkLKdTGAv5fe87WhdVZEfvvM7pWbuZLdqc2SaUFCrxxRm6MTp9Vm40nBH1nfrgwBkc0:MGQCMFN2i+2A0Ol6PaQ+7poIrguBIFxilhiO73eD/iMpgNADCXzc6eLmLurejfvf6yZ8GQIwY7EnLvAmpIlmR7FW2uhxC/6vLQoHHYnRJkx23sC8aqA+X03j24cDmXyxwlR5azIk",
|
| 4 |
-
"clientId": "gabI3snTPwTQZGF1TzuWc3VzLWVhc3QtMQ",
|
| 5 |
-
"clientSecret": "eyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0.eyJzZXJpYWxpemVkIjoie1wiY2xpZW50SWRcIjp7XCJ2YWx1ZVwiOlwiZ2FiSTNzblRQd1RRWkdGMVR6dVdjM1Z6TFdWaGMzUXRNUVwifSxcImlkZW1wb3RlbnRLZXlcIjpudWxsLFwidGVuYW50SWRcIjpudWxsLFwiY2xpZW50TmFtZVwiOlwiQW1hem9uIFEgRGV2ZWxvcGVyIGZvciBjb21tYW5kIGxpbmVcIixcImJhY2tmaWxsVmVyc2lvblwiOm51bGwsXCJjbGllbnRUeXBlXCI6XCJQVUJMSUNcIixcInRlbXBsYXRlQXJuXCI6bnVsbCxcInRlbXBsYXRlQ29udGV4dFwiOm51bGwsXCJleHBpcmF0aW9uVGltZXN0YW1wXCI6MTc4MTc1ODMxMC42NDQxMjIwNzYsXCJjcmVhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMC42NDQxMjIwNzYsXCJ1cGRhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMC42NDQxMjIwNzYsXCJjcmVhdGVkQnlcIjpudWxsLFwidXBkYXRlZEJ5XCI6bnVsbCxcInN0YXR1c1wiOm51bGwsXCJpbml0aWF0ZUxvZ2luVXJpXCI6bnVsbCxcImVudGl0bGVkUmVzb3VyY2VJZFwiOm51bGwsXCJlbnRpdGxlZFJlc291cmNlQ29udGFpbmVySWRcIjpudWxsLFwiZXh0ZXJuYWxJZFwiOm51bGwsXCJzb2Z0d2FyZUlkXCI6bnVsbCxcInNjb3Blc1wiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29tcGxldGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiY29tcGxldGlvbnNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6YW5hbHlzaXNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiYW5hbHlzaXNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn1dLFwiYXV0aGVudGljYXRpb25Db25maWd1cmF0aW9uXCI6bnVsbCxcInNoYWRvd0F1dGhlbnRpY2F0aW9uQ29uZmlndXJhdGlvblwiOm51bGwsXCJlbmFibGVkR3JhbnRzXCI6bnVsbCxcImVuZm9yY2VBdXRoTkNvbmZpZ3VyYXRpb25cIjpudWxsLFwib3duZXJBY2NvdW50SWRcIjpudWxsLFwic3NvSW5zdGFuY2VBY2NvdW50SWRcIjpudWxsLFwidXNlckNvbnNlbnRcIjpudWxsLFwibm9uSW50ZXJhY3RpdmVTZXNzaW9uc0VuYWJsZWRcIjpudWxsLFwiYXNzb2NpYXRlZEluc3RhbmNlQXJuXCI6bnVsbCxcImdyb3VwU2NvcGVzQnlGcmllbmRseUlkXCI6e1wiY29kZXdoaXNwZXJlclwiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmFuYWx5c2lzXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInVzZUNhc2VBY3Rpb25cIjpcImFuYWx5c2lzXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmNvbXBsZXRpb25zXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInVzZUNhc2VBY3Rpb25cIjpcImNvbXBsZXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn1dfSxcInNob3VsZEdldFZhbHVlRnJvbVRlbXBsYXRlXCI6dHJ1ZSxcImhhc1JlcXVlc3RlZFNjb3Blc1wiOmZhbHNlLFwiY29udGFpbnNPbmx5U3NvU2NvcGVzXCI6ZmFsc2UsXCJzc29TY29wZXNcIjpbXSxcImlzVjFCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc1YyQmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNWM0JhY2tmaWxsZWRcIjpmYWxzZSxcImlzVjRCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc0V4cGlyZWRcIjpmYWxzZSxcImlzQmFja2ZpbGxlZFwiOmZhbHNlLFwiaGFzSW5pdGlhbFNjb3Blc1wiOnRydWUsXCJhcmVBbGxTY29wZXNDb25zZW50ZWRUb1wiOmZhbHNlfSJ9.gsmsFkKFajDcDb4-c31lLTGxYQIqHKTzgZvwB4Wfta0xuDBNw0gzThEXSDZlqP6q",
|
| 6 |
-
"authMethod": "builder-id",
|
| 7 |
-
"idcRegion": "us-east-1",
|
| 8 |
-
"expiresAt": "2026-03-20T06:00:00Z"
|
| 9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/kiro/chatgpt_account2_kiro-auth-token/chatgpt_account2_kiro-auth-token.json
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"accessToken": "aoaAAAAAGm84HYLp9bxse52TpenBsslXdOVeCse0scBl5EXeX46G47SUJ7cg1Ok2VFQ8uNIg41dAzJ0NV9z7lWvmYBkc0:MGQCMC7JUwaYATAtN9fqgYsXBA7ajXJd0SuVa5jdmpHZ1b+unFh71kJdXsRs4Rs3VIQ+MwIwcCL2gGDRXPli8wGzvlgrtNti8orkDFfkqHkH7pyHJLoDu1bYq7iefaHErMY8kAlZ",
|
| 3 |
-
"refreshToken": "aorAAAAAGozeWIYaVvTd3pwQl7-_GyGoAvrkgXW5puoFCn1Ck_kxYwNi2Yswz3Z2NaTSkfpP5LpQDYVCu6Yw43oYwBkc0:MGUCMCIe9f1tl2C3QXQda5IdFeBpJPHcReZIr3Zt8bms2hLijzdTtVfzsKQT7a0q9Sq/JAIxAMAmLFxl6kOqqPNfbWVy5dunHn+9+K3O/OXT/lk+1LjOWT1fNkY/+thpKYHM16bVzw",
|
| 4 |
-
"clientId": "1LFoSWRMsTHl8MdMQKrMM3VzLWVhc3QtMQ",
|
| 5 |
-
"clientSecret": "eyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0.eyJzZXJpYWxpemVkIjoie1wiY2xpZW50SWRcIjp7XCJ2YWx1ZVwiOlwiMUxGb1NXUk1zVEhsOE1kTVFLck1NM1Z6TFdWaGMzUXRNUVwifSxcImlkZW1wb3RlbnRLZXlcIjpudWxsLFwidGVuYW50SWRcIjpudWxsLFwiY2xpZW50TmFtZVwiOlwiQW1hem9uIFEgRGV2ZWxvcGVyIGZvciBjb21tYW5kIGxpbmVcIixcImJhY2tmaWxsVmVyc2lvblwiOm51bGwsXCJjbGllbnRUeXBlXCI6XCJQVUJMSUNcIixcInRlbXBsYXRlQXJuXCI6bnVsbCxcInRlbXBsYXRlQ29udGV4dFwiOm51bGwsXCJleHBpcmF0aW9uVGltZXN0YW1wXCI6MTc4MTc1ODMxMS4zNjI2MDY5NTMsXCJjcmVhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMS4zNjI2MDY5NTMsXCJ1cGRhdGVkVGltZXN0YW1wXCI6MTc3Mzk4MjMxMS4zNjI2MDY5NTMsXCJjcmVhdGVkQnlcIjpudWxsLFwidXBkYXRlZEJ5XCI6bnVsbCxcInN0YXR1c1wiOm51bGwsXCJpbml0aWF0ZUxvZ2luVXJpXCI6bnVsbCxcImVudGl0bGVkUmVzb3VyY2VJZFwiOm51bGwsXCJlbnRpdGxlZFJlc291cmNlQ29udGFpbmVySWRcIjpudWxsLFwiZXh0ZXJuYWxJZFwiOm51bGwsXCJzb2Z0d2FyZUlkXCI6bnVsbCxcInNjb3Blc1wiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29tcGxldGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiY29tcGxldGlvbnNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6YW5hbHlzaXNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiYW5hbHlzaXNcIixcInNjb3BlVHlwZVwiOlwiQUNDRVNTX1NDT1BFXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn1dLFwiYXV0aGVudGljYXRpb25Db25maWd1cmF0aW9uXCI6bnVsbCxcInNoYWRvd0F1dGhlbnRpY2F0aW9uQ29uZmlndXJhdGlvblwiOm51bGwsXCJlbmFibGVkR3JhbnRzXCI6bnVsbCxcImVuZm9yY2VBdXRoTkNvbmZpZ3VyYXRpb25cIjpudWxsLFwib3duZXJBY2NvdW50SWRcIjpudWxsLFwic3NvSW5zdGFuY2VBY2NvdW50SWRcIjpudWxsLFwidXNlckNvbnNlbnRcIjpudWxsLFwibm9uSW50ZXJhY3RpdmVTZXNzaW9uc0VuYWJsZWRcIjpudWxsLFwiYXNzb2NpYXRlZEluc3RhbmNlQXJuXCI6bnVsbCxcInNob3VsZEdldFZhbHVlRnJvbVRlbXBsYXRlXCI6dHJ1ZSxcImhhc1JlcXVlc3RlZFNjb3Blc1wiOmZhbHNlLFwiY29udGFpbnNPbmx5U3NvU2NvcGVzXCI6ZmFsc2UsXCJzc29TY29wZXNcIjpbXSxcImlzVjFCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc1YyQmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNWM0JhY2tmaWxsZWRcIjpmYWxzZSxcImlzVjRCYWNrZmlsbGVkXCI6ZmFsc2UsXCJncm91cFNjb3Blc0J5RnJpZW5kbHlJZFwiOntcImNvZGV3aGlzcGVyZXJcIjpbe1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmFuYWx5c2lzXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInVzZUNhc2VBY3Rpb25cIjpcImFuYWx5c2lzXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwiLFwidHlwZVwiOlwiSW1tdXRhYmxlQWNjZXNzU2NvcGVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmNvbnZlcnNhdGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwiZnJpZW5kbHlJZFwiOlwiY29kZXdoaXNwZXJlclwiLFwidXNlQ2FzZUFjdGlvblwiOlwiY29udmVyc2F0aW9uc1wiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCJ9LHtcImZ1bGxTY29wZVwiOlwiY29kZXdoaXNwZXJlcjpjb21wbGV0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb21wbGV0aW9uc1wiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCJ9XX0sXCJpc0V4cGlyZWRcIjpmYWxzZSxcImlzQmFja2ZpbGxlZFwiOmZhbHNlLFwiaGFzSW5pdGlhbFNjb3Blc1wiOnRydWUsXCJhcmVBbGxTY29wZXNDb25zZW50ZWRUb1wiOmZhbHNlfSJ9.ZFTKp-PJaA7vKXI3UiBkqbnO-CvjX2kJw75UUIraS04b8HveyUsInBDcc40yKKf1",
|
| 6 |
-
"authMethod": "builder-id",
|
| 7 |
-
"idcRegion": "us-east-1",
|
| 8 |
-
"expiresAt": "2026-03-20T06:00:00Z"
|
| 9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/kiro/fresh1_kiro-auth-token/fresh1_kiro-auth-token.json
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"accessToken": "aoaAAAAAGm8epsqQIJk5Bj1QoXwTAMGHRMumJWCVJ13MJODTLwCdljXWm-8jmbsXAghY3niBcLEuNr9xTljmhrN-oBkc0:MGUCMQCH/mlgYPdL1PfKCnU1eSRf5NEMyhPCW/2+Y8K2bFmMH2gMFAE6ftTRveCFSe+kTjQCMC/u4EdVSdTr9Bui6OsE280xUPAH7CSDCIHNTTPk2CiYbVtge/oYlRxVoNQizk1AMQ",
|
| 3 |
-
"refreshToken": "aorAAAAAGozE4YEfji2Ny6mvPuscUKbyPuUc2XD3Nf7SdFbQcE1Z2qoUOR-gZI8cUKubWvDXOivKqghUnyZOMXp7sBkc0:MGQCMCrPdIeyBVkVlB0rtjlIvHhlPURSovG8mcs9qmDBx1VYH5LPdcWvQd74O3XFYCGMfwIwIESOUIiPVFFAMKs+N1YgEoe13glxpAP7Ri0memy2/WdqY0cFEFNqMq/CndDx0dbJ",
|
| 4 |
-
"clientId": "H6JYDDvxcMALs2QKZfIKgHVzLWVhc3QtMQ",
|
| 5 |
-
"clientSecret": "eyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0.eyJzZXJpYWxpemVkIjoie1wiY2xpZW50SWRcIjp7XCJ2YWx1ZVwiOlwiSDZKWUREdnhjTUFMczJRS1pmSUtnSFZ6TFdWaGMzUXRNUVwifSxcImlkZW1wb3RlbnRLZXlcIjpudWxsLFwidGVuYW50SWRcIjpudWxsLFwiY2xpZW50TmFtZVwiOlwiQW1hem9uIFEgRGV2ZWxvcGVyIGZvciBjb21tYW5kIGxpbmVcIixcImJhY2tmaWxsVmVyc2lvblwiOm51bGwsXCJjbGllbnRUeXBlXCI6XCJQVUJMSUNcIixcInRlbXBsYXRlQXJuXCI6bnVsbCxcInRlbXBsYXRlQ29udGV4dFwiOm51bGwsXCJleHBpcmF0aW9uVGltZXN0YW1wXCI6MTc4MTczMjIzNS43OTM5MDAyNzIsXCJjcmVhdGVkVGltZXN0YW1wXCI6MTc3Mzk1NjIzNS43OTM5MDAyNzIsXCJ1cGRhdGVkVGltZXN0YW1wXCI6MTc3Mzk1NjIzNS43OTM5MDAyNzIsXCJjcmVhdGVkQnlcIjpudWxsLFwidXBkYXRlZEJ5XCI6bnVsbCxcInN0YXR1c1wiOm51bGwsXCJpbml0aWF0ZUxvZ2luVXJpXCI6bnVsbCxcImVudGl0bGVkUmVzb3VyY2VJZFwiOm51bGwsXCJlbnRpdGxlZFJlc291cmNlQ29udGFpbmVySWRcIjpudWxsLFwiZXh0ZXJuYWxJZFwiOm51bGwsXCJzb2Z0d2FyZUlkXCI6bnVsbCxcInNjb3Blc1wiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29tcGxldGlvbnNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwidXNlQ2FzZUFjdGlvblwiOlwiY29tcGxldGlvbnNcIixcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6YW5hbHlzaXNcIixcInN0YXR1c1wiOlwiSU5JVElBTFwiLFwiYXBwbGljYXRpb25Bcm5cIjpudWxsLFwidXNlQ2FzZUFjdGlvblwiOlwiYW5hbHlzaXNcIixcImZyaWVuZGx5SWRcIjpcImNvZGV3aGlzcGVyZXJcIixcInR5cGVcIjpcIkltbXV0YWJsZUFjY2Vzc1Njb3BlXCIsXCJzY29wZVR5cGVcIjpcIkFDQ0VTU19TQ09QRVwifSx7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn1dLFwiYXV0aGVudGljYXRpb25Db25maWd1cmF0aW9uXCI6bnVsbCxcInNoYWRvd0F1dGhlbnRpY2F0aW9uQ29uZmlndXJhdGlvblwiOm51bGwsXCJlbmFibGVkR3JhbnRzXCI6bnVsbCxcImVuZm9yY2VBdXRoTkNvbmZpZ3VyYXRpb25cIjpudWxsLFwib3duZXJBY2NvdW50SWRcIjpudWxsLFwic3NvSW5zdGFuY2VBY2NvdW50SWRcIjpudWxsLFwidXNlckNvbnNlbnRcIjpudWxsLFwibm9uSW50ZXJhY3RpdmVTZXNzaW9uc0VuYWJsZWRcIjpudWxsLFwiYXNzb2NpYXRlZEluc3RhbmNlQXJuXCI6bnVsbCxcImNvbnRhaW5zT25seVNzb1Njb3Blc1wiOmZhbHNlLFwic3NvU2NvcGVzXCI6W10sXCJpc1YxQmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNWMkJhY2tmaWxsZWRcIjpmYWxzZSxcImlzVjNCYWNrZmlsbGVkXCI6ZmFsc2UsXCJpc1Y0QmFja2ZpbGxlZFwiOmZhbHNlLFwiaXNFeHBpcmVkXCI6ZmFsc2UsXCJpc0JhY2tmaWxsZWRcIjpmYWxzZSxcImhhc0luaXRpYWxTY29wZXNcIjp0cnVlLFwiYXJlQWxsU2NvcGVzQ29uc2VudGVkVG9cIjpmYWxzZSxcImdyb3VwU2NvcGVzQnlGcmllbmRseUlkXCI6e1wiY29kZXdoaXNwZXJlclwiOlt7XCJmdWxsU2NvcGVcIjpcImNvZGV3aGlzcGVyZXI6Y29udmVyc2F0aW9uc1wiLFwic3RhdHVzXCI6XCJJTklUSUFMXCIsXCJhcHBsaWNhdGlvbkFyblwiOm51bGwsXCJ1c2VDYXNlQWN0aW9uXCI6XCJjb252ZXJzYXRpb25zXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmNvbXBsZXRpb25zXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcInVzZUNhc2VBY3Rpb25cIjpcImNvbXBsZXRpb25zXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn0se1wiZnVsbFNjb3BlXCI6XCJjb2Rld2hpc3BlcmVyOmFuYWx5c2lzXCIsXCJzdGF0dXNcIjpcIklOSVRJQUxcIixcImFwcGxpY2F0aW9uQXJuXCI6bnVsbCxcInVzZUNhc2VBY3Rpb25cIjpcImFuYWx5c2lzXCIsXCJmcmllbmRseUlkXCI6XCJjb2Rld2hpc3BlcmVyXCIsXCJ0eXBlXCI6XCJJbW11dGFibGVBY2Nlc3NTY29wZVwiLFwic2NvcGVUeXBlXCI6XCJBQ0NFU1NfU0NPUEVcIn1dfSxcInNob3VsZEdldFZhbHVlRnJvbVRlbXBsYXRlXCI6dHJ1ZSxcImhhc1JlcXVlc3RlZFNjb3Blc1wiOmZhbHNlfSJ9.57MdZkhyW6vz98g6JDcRSqh1c8z1Ta1eyH6d9YbsW3TUTrfJ6WxobBzkaP69aK4r",
|
| 6 |
-
"authMethod": "builder-id",
|
| 7 |
-
"idcRegion": "us-east-1",
|
| 8 |
-
"expiresAt": "2026-03-20T07:00:00Z"
|
| 9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/kiro/personal_kiro-auth-token/personal_kiro-auth-token.json
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"accessToken": "aoaAAAAAGm82R4v3zSqMlkmiPwSHdKAAQAbZ6yu4TywrlVXmFU7XMVtQQb3phAA-gOci2L0S62LjsBj-9NFLMeQPMBkc0:MGUCMQDT2c8RFXspAv/U52k+YkcOeYIge/+GA+axl/jxo48IfcXYRM2RnclaR816zx0v/rkCMGEoQS1GOAZAZN7pUJzu5xgYKo62NwzNNNoUEyyGcPwujYtWRQFbNKgAvlL0O7VFGg",
|
| 3 |
-
"refreshToken": "",
|
| 4 |
-
"authMethod": "social",
|
| 5 |
-
"region": "us-east-1",
|
| 6 |
-
"expiresAt": "2026-03-20T06:00:00Z"
|
| 7 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/plugins.json
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"plugins": {
|
| 3 |
-
"api-potluck": {"enabled": false},
|
| 4 |
-
"default-auth": {"enabled": true}
|
| 5 |
-
}
|
| 6 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/plugins.json.example
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"plugins": {
|
| 3 |
-
"api-potluck": {
|
| 4 |
-
"enabled": false,
|
| 5 |
-
"description": "API 大锅饭 - Key 管理和用量统计插件"
|
| 6 |
-
},
|
| 7 |
-
"default-auth": {
|
| 8 |
-
"enabled": true,
|
| 9 |
-
"description": "默认 API Key 认证插件(内置)"
|
| 10 |
-
}
|
| 11 |
-
}
|
| 12 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/provider_pools.json
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"claude-kiro-oauth": [
|
| 3 |
-
{
|
| 4 |
-
"customName": "Personal Account (cs.shenhao)",
|
| 5 |
-
"KIRO_OAUTH_CREDS_FILE_PATH": "configs/kiro/personal_kiro-auth-token/personal_kiro-auth-token.json",
|
| 6 |
-
"uuid": "personal-0000-4000-a000-000000000000",
|
| 7 |
-
"checkModelName": null,
|
| 8 |
-
"checkHealth": false,
|
| 9 |
-
"isHealthy": true,
|
| 10 |
-
"isDisabled": false,
|
| 11 |
-
"lastUsed": null,
|
| 12 |
-
"usageCount": 0,
|
| 13 |
-
"errorCount": 0,
|
| 14 |
-
"lastErrorTime": null
|
| 15 |
-
},
|
| 16 |
-
{
|
| 17 |
-
"customName": "ChatGPT Mail Account 1 (kr63b7278504@prestige-leadership.org)",
|
| 18 |
-
"KIRO_OAUTH_CREDS_FILE_PATH": "configs/kiro/chatgpt_account1_kiro-auth-token/chatgpt_account1_kiro-auth-token.json",
|
| 19 |
-
"uuid": "chatgpt-0001-4000-a000-000000000001",
|
| 20 |
-
"checkModelName": null,
|
| 21 |
-
"checkHealth": true,
|
| 22 |
-
"isHealthy": true,
|
| 23 |
-
"isDisabled": false,
|
| 24 |
-
"lastUsed": null,
|
| 25 |
-
"usageCount": 0,
|
| 26 |
-
"errorCount": 0,
|
| 27 |
-
"lastErrorTime": null
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"customName": "ChatGPT Mail Account 2 (kr0a92d5ad15@mailsa.biz.id)",
|
| 31 |
-
"KIRO_OAUTH_CREDS_FILE_PATH": "configs/kiro/chatgpt_account2_kiro-auth-token/chatgpt_account2_kiro-auth-token.json",
|
| 32 |
-
"uuid": "chatgpt-0002-4000-a000-000000000002",
|
| 33 |
-
"checkModelName": null,
|
| 34 |
-
"checkHealth": true,
|
| 35 |
-
"isHealthy": true,
|
| 36 |
-
"isDisabled": false,
|
| 37 |
-
"lastUsed": null,
|
| 38 |
-
"usageCount": 0,
|
| 39 |
-
"errorCount": 0,
|
| 40 |
-
"lastErrorTime": null
|
| 41 |
-
}
|
| 42 |
-
]
|
| 43 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/provider_pools.json.example
DELETED
|
@@ -1,213 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"openai-custom": [
|
| 3 |
-
{
|
| 4 |
-
"customName": "OpenAI节点1",
|
| 5 |
-
"OPENAI_API_KEY": "sk-openai-key1",
|
| 6 |
-
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
| 7 |
-
"checkModelName": null,
|
| 8 |
-
"checkHealth": false,
|
| 9 |
-
"notSupportedModels": ["gpt-4-turbo"],
|
| 10 |
-
"uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c",
|
| 11 |
-
"isHealthy": true,
|
| 12 |
-
"isDisabled": false,
|
| 13 |
-
"lastUsed": null,
|
| 14 |
-
"usageCount": 0,
|
| 15 |
-
"errorCount": 0,
|
| 16 |
-
"lastErrorTime": null
|
| 17 |
-
},
|
| 18 |
-
{
|
| 19 |
-
"customName": "OpenAI节点2",
|
| 20 |
-
"OPENAI_API_KEY": "sk-openai-key2",
|
| 21 |
-
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
| 22 |
-
"checkModelName": null,
|
| 23 |
-
"checkHealth": false,
|
| 24 |
-
"notSupportedModels": ["gpt-4-turbo", "gpt-4"],
|
| 25 |
-
"uuid": "e284628d-302f-456d-91f3-6095386fb3b8",
|
| 26 |
-
"isHealthy": true,
|
| 27 |
-
"isDisabled": true,
|
| 28 |
-
"lastUsed": null,
|
| 29 |
-
"usageCount": 0,
|
| 30 |
-
"errorCount": 0,
|
| 31 |
-
"lastErrorTime": null
|
| 32 |
-
}
|
| 33 |
-
],
|
| 34 |
-
"openaiResponses-custom": [
|
| 35 |
-
{
|
| 36 |
-
"customName": "OpenAI Responses节点",
|
| 37 |
-
"OPENAI_API_KEY": "sk-openai-key",
|
| 38 |
-
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
| 39 |
-
"checkModelName": null,
|
| 40 |
-
"checkHealth": false,
|
| 41 |
-
"uuid": "e284628d-302f-456d-91f3-609538678968",
|
| 42 |
-
"isHealthy": true,
|
| 43 |
-
"isDisabled": false,
|
| 44 |
-
"lastUsed": null,
|
| 45 |
-
"usageCount": 0,
|
| 46 |
-
"errorCount": 0,
|
| 47 |
-
"lastErrorTime": null
|
| 48 |
-
}
|
| 49 |
-
],
|
| 50 |
-
"gemini-cli-oauth": [
|
| 51 |
-
{
|
| 52 |
-
"customName": "Gemini OAuth节点1",
|
| 53 |
-
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials1.json",
|
| 54 |
-
"PROJECT_ID": "your-project-id-1",
|
| 55 |
-
"checkModelName": null,
|
| 56 |
-
"checkHealth": false,
|
| 57 |
-
"uuid": "ac200154-26b8-4f5f-8650-e8cc738b06e3",
|
| 58 |
-
"isHealthy": true,
|
| 59 |
-
"isDisabled": false,
|
| 60 |
-
"lastUsed": null,
|
| 61 |
-
"usageCount": 0,
|
| 62 |
-
"errorCount": 0,
|
| 63 |
-
"lastErrorTime": null
|
| 64 |
-
},
|
| 65 |
-
{
|
| 66 |
-
"customName": "Gemini OAuth节点2",
|
| 67 |
-
"GEMINI_OAUTH_CREDS_FILE_PATH": "./credentials2.json",
|
| 68 |
-
"PROJECT_ID": "your-project-id-2",
|
| 69 |
-
"checkModelName": null,
|
| 70 |
-
"checkHealth": false,
|
| 71 |
-
"uuid": "4f8afcc2-a9bb-4b96-bb50-3b9667a71f54",
|
| 72 |
-
"isHealthy": true,
|
| 73 |
-
"isDisabled": false,
|
| 74 |
-
"lastUsed": null,
|
| 75 |
-
"usageCount": 0,
|
| 76 |
-
"errorCount": 0,
|
| 77 |
-
"lastErrorTime": null
|
| 78 |
-
}
|
| 79 |
-
],
|
| 80 |
-
"claude-custom": [
|
| 81 |
-
{
|
| 82 |
-
"customName": "Claude节点1",
|
| 83 |
-
"CLAUDE_API_KEY": "sk-claude-key1",
|
| 84 |
-
"CLAUDE_BASE_URL": "https://api.anthropic.com",
|
| 85 |
-
"checkModelName": null,
|
| 86 |
-
"checkHealth": false,
|
| 87 |
-
"uuid": "bb87047a-3b1d-4249-adbb-1087ecd58128",
|
| 88 |
-
"isHealthy": true,
|
| 89 |
-
"isDisabled": false,
|
| 90 |
-
"lastUsed": null,
|
| 91 |
-
"usageCount": 0,
|
| 92 |
-
"errorCount": 0,
|
| 93 |
-
"lastErrorTime": null
|
| 94 |
-
},
|
| 95 |
-
{
|
| 96 |
-
"customName": "Claude节点2",
|
| 97 |
-
"CLAUDE_API_KEY": "sk-claude-key2",
|
| 98 |
-
"CLAUDE_BASE_URL": "https://api.anthropic.com",
|
| 99 |
-
"checkModelName": null,
|
| 100 |
-
"checkHealth": false,
|
| 101 |
-
"uuid": "7c2002c6-122a-4db0-af06-8a0ff433801a",
|
| 102 |
-
"isHealthy": true,
|
| 103 |
-
"isDisabled": false,
|
| 104 |
-
"lastUsed": null,
|
| 105 |
-
"usageCount": 0,
|
| 106 |
-
"errorCount": 0,
|
| 107 |
-
"lastErrorTime": null
|
| 108 |
-
}
|
| 109 |
-
],
|
| 110 |
-
"claude-kiro-oauth": [
|
| 111 |
-
{
|
| 112 |
-
"customName": "Kiro OAuth节点1",
|
| 113 |
-
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds1.json",
|
| 114 |
-
"uuid": "2c69d0ac-b86f-43d8-9d17-0d300afc5cfd",
|
| 115 |
-
"checkModelName": null,
|
| 116 |
-
"checkHealth": false,
|
| 117 |
-
"isHealthy": true,
|
| 118 |
-
"isDisabled": false,
|
| 119 |
-
"lastUsed": null,
|
| 120 |
-
"usageCount": 0,
|
| 121 |
-
"errorCount": 0,
|
| 122 |
-
"lastErrorTime": null
|
| 123 |
-
},
|
| 124 |
-
{
|
| 125 |
-
"customName": "Kiro OAuth节点2",
|
| 126 |
-
"KIRO_OAUTH_CREDS_FILE_PATH": "./kiro_creds2.json",
|
| 127 |
-
"uuid": "7482abe6-8083-4288-bb7d-d8ecb7c461e2",
|
| 128 |
-
"checkModelName": null,
|
| 129 |
-
"checkHealth": false,
|
| 130 |
-
"isHealthy": true,
|
| 131 |
-
"isDisabled": false,
|
| 132 |
-
"lastUsed": null,
|
| 133 |
-
"usageCount": 0,
|
| 134 |
-
"errorCount": 0,
|
| 135 |
-
"lastErrorTime": null
|
| 136 |
-
}
|
| 137 |
-
],
|
| 138 |
-
"openai-qwen-oauth": [
|
| 139 |
-
{
|
| 140 |
-
"customName": "Qwen OAuth节点",
|
| 141 |
-
"QWEN_OAUTH_CREDS_FILE_PATH": "./qwen_creds.json",
|
| 142 |
-
"uuid": "658a2114-c4c9-d713-b8d4-ceabf0e0bf18",
|
| 143 |
-
"checkModelName": null,
|
| 144 |
-
"checkHealth": false,
|
| 145 |
-
"isHealthy": true,
|
| 146 |
-
"isDisabled": false,
|
| 147 |
-
"lastUsed": null,
|
| 148 |
-
"usageCount": 0,
|
| 149 |
-
"errorCount": 0,
|
| 150 |
-
"lastErrorTime": null
|
| 151 |
-
}
|
| 152 |
-
],
|
| 153 |
-
"gemini-antigravity": [
|
| 154 |
-
{
|
| 155 |
-
"customName": "Antigravity节点1",
|
| 156 |
-
"ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds1.json",
|
| 157 |
-
"PROJECT_ID": "antigravity-project-1",
|
| 158 |
-
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
| 159 |
-
"checkModelName": null,
|
| 160 |
-
"checkHealth": false,
|
| 161 |
-
"isHealthy": true,
|
| 162 |
-
"isDisabled": false,
|
| 163 |
-
"lastUsed": null,
|
| 164 |
-
"usageCount": 0,
|
| 165 |
-
"errorCount": 0,
|
| 166 |
-
"lastErrorTime": null
|
| 167 |
-
},
|
| 168 |
-
{
|
| 169 |
-
"customName": "Antigravity节点2",
|
| 170 |
-
"ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": "./antigravity_creds2.json",
|
| 171 |
-
"PROJECT_ID": "antigravity-project-2",
|
| 172 |
-
"uuid": "f0e9d8c7-b6a5-4321-fedc-ba9876543210",
|
| 173 |
-
"checkModelName": null,
|
| 174 |
-
"checkHealth": false,
|
| 175 |
-
"isHealthy": true,
|
| 176 |
-
"isDisabled": false,
|
| 177 |
-
"lastUsed": null,
|
| 178 |
-
"usageCount": 0,
|
| 179 |
-
"errorCount": 0,
|
| 180 |
-
"lastErrorTime": null
|
| 181 |
-
}
|
| 182 |
-
],
|
| 183 |
-
"openai-iflow": [
|
| 184 |
-
{
|
| 185 |
-
"customName": "iFlow Token节点1",
|
| 186 |
-
"IFLOW_TOKEN_FILE_PATH": "./configs/iflow/iflow_token.json",
|
| 187 |
-
"IFLOW_BASE_URL": "https://apis.iflow.cn/v1",
|
| 188 |
-
"uuid": "11223344-5566-7788-99aa-bbccddeeff00",
|
| 189 |
-
"checkModelName": "gpt-4o",
|
| 190 |
-
"checkHealth": false,
|
| 191 |
-
"isHealthy": true,
|
| 192 |
-
"isDisabled": false,
|
| 193 |
-
"lastUsed": null,
|
| 194 |
-
"usageCount": 0,
|
| 195 |
-
"errorCount": 0,
|
| 196 |
-
"lastErrorTime": null
|
| 197 |
-
},
|
| 198 |
-
{
|
| 199 |
-
"customName": "iFlow Token节点2",
|
| 200 |
-
"IFLOW_TOKEN_FILE_PATH": "./configs/iflow/iflow_token2.json",
|
| 201 |
-
"IFLOW_BASE_URL": "https://apis.iflow.cn/v1",
|
| 202 |
-
"uuid": "aabbccdd-eeff-0011-2233-445566778899",
|
| 203 |
-
"checkModelName": "gpt-4o",
|
| 204 |
-
"checkHealth": false,
|
| 205 |
-
"isHealthy": true,
|
| 206 |
-
"isDisabled": false,
|
| 207 |
-
"lastUsed": null,
|
| 208 |
-
"usageCount": 0,
|
| 209 |
-
"errorCount": 0,
|
| 210 |
-
"lastErrorTime": null
|
| 211 |
-
}
|
| 212 |
-
]
|
| 213 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configs/pwd
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
kiro2024
|
|
|
|
|
|
entrypoint.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
CONFIG_DIR="/app/config"
|
| 5 |
+
mkdir -p "${CONFIG_DIR}"
|
| 6 |
+
|
| 7 |
+
API_KEY="${API_KEY:-sk-kiro2api}"
|
| 8 |
+
ADMIN_API_KEY="${ADMIN_API_KEY:-sk-admin-kiro2api}"
|
| 9 |
+
PROXY="${HTTPS_PROXY:-${ALL_PROXY:-}}"
|
| 10 |
+
|
| 11 |
+
cat > "${CONFIG_DIR}/config.json" <<EOF
|
| 12 |
+
{
|
| 13 |
+
"host": "0.0.0.0",
|
| 14 |
+
"port": 7860,
|
| 15 |
+
"apiKey": "${API_KEY}",
|
| 16 |
+
"adminApiKey": "${ADMIN_API_KEY}",
|
| 17 |
+
"region": "us-east-1",
|
| 18 |
+
"tlsBackend": "rustls",
|
| 19 |
+
"proxyUrl": "${PROXY}",
|
| 20 |
+
"loadBalancingMode": "balanced"
|
| 21 |
+
}
|
| 22 |
+
EOF
|
| 23 |
+
|
| 24 |
+
# Empty credentials array — populated via admin API sync
|
| 25 |
+
echo "[]" > "${CONFIG_DIR}/credentials.json"
|
| 26 |
+
|
| 27 |
+
echo "Config generated (port=7860, proxy=${PROXY:+set}${PROXY:-none})"
|
| 28 |
+
exec ./kiro-rs -c "${CONFIG_DIR}/config.json" --credentials "${CONFIG_DIR}/credentials.json"
|
healthcheck.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Docker健康检查脚本
|
| 3 |
-
* 用于检查API服务器是否正常运行
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import http from 'http';
|
| 7 |
-
|
| 8 |
-
// 从环境变量获取主机和端口,如果没有设置则使用默认值
|
| 9 |
-
const HOST = process.env.HOST || 'localhost';
|
| 10 |
-
const PORT = process.env.SERVER_PORT || 3000;
|
| 11 |
-
|
| 12 |
-
// 发送HTTP请求到健康检查端点
|
| 13 |
-
const options = {
|
| 14 |
-
hostname: HOST,
|
| 15 |
-
port: PORT,
|
| 16 |
-
path: '/health',
|
| 17 |
-
method: 'GET',
|
| 18 |
-
timeout: 2000 // 2秒超时
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
const req = http.request(options, (res) => {
|
| 22 |
-
// 如果状态码是200,表示服务健康
|
| 23 |
-
if (res.statusCode === 200) {
|
| 24 |
-
console.log('Health check passed');
|
| 25 |
-
process.exit(0);
|
| 26 |
-
} else {
|
| 27 |
-
console.log(`Health check failed with status code: ${res.statusCode}`);
|
| 28 |
-
process.exit(1);
|
| 29 |
-
}
|
| 30 |
-
});
|
| 31 |
-
|
| 32 |
-
// 处理请求错误
|
| 33 |
-
req.on('error', (e) => {
|
| 34 |
-
console.error(`Health check failed: ${e.message}`);
|
| 35 |
-
process.exit(1);
|
| 36 |
-
});
|
| 37 |
-
|
| 38 |
-
// 设置超时处理
|
| 39 |
-
req.on('timeout', () => {
|
| 40 |
-
console.error('Health check timed out');
|
| 41 |
-
req.destroy();
|
| 42 |
-
process.exit(1);
|
| 43 |
-
});
|
| 44 |
-
|
| 45 |
-
// 结束请求
|
| 46 |
-
req.end();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package-lock.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"type": "module",
|
| 3 |
-
"dependencies": {
|
| 4 |
-
"@anthropic-ai/tokenizer": "^0.0.4",
|
| 5 |
-
"adm-zip": "^0.5.16",
|
| 6 |
-
"axios": "^1.10.0",
|
| 7 |
-
"deepmerge": "^4.3.1",
|
| 8 |
-
"dotenv": "^16.4.5",
|
| 9 |
-
"google-auth-library": "^10.1.0",
|
| 10 |
-
"http-proxy-agent": "^7.0.2",
|
| 11 |
-
"https-proxy-agent": "^7.0.6",
|
| 12 |
-
"lodash": "^4.17.21",
|
| 13 |
-
"multer": "^2.0.2",
|
| 14 |
-
"open": "^10.2.0",
|
| 15 |
-
"socks-proxy-agent": "^8.0.5",
|
| 16 |
-
"undici": "^7.12.0",
|
| 17 |
-
"uuid": "^11.1.0",
|
| 18 |
-
"ws": "^8.19.0"
|
| 19 |
-
},
|
| 20 |
-
"devDependencies": {
|
| 21 |
-
"@babel/preset-env": "^7.28.0",
|
| 22 |
-
"@jest/globals": "^29.7.0",
|
| 23 |
-
"babel-jest": "^30.0.5",
|
| 24 |
-
"babel-plugin-transform-import-meta": "^2.3.3",
|
| 25 |
-
"jest": "^29.7.0",
|
| 26 |
-
"jest-environment-node": "^29.7.0",
|
| 27 |
-
"supertest": "^6.3.3"
|
| 28 |
-
},
|
| 29 |
-
"scripts": {
|
| 30 |
-
"start": "node src/core/master.js",
|
| 31 |
-
"start:standalone": "node src/services/api-server.js",
|
| 32 |
-
"start:dev": "node src/core/master.js --dev",
|
| 33 |
-
"test": "jest",
|
| 34 |
-
"test:watch": "jest --watch",
|
| 35 |
-
"test:coverage": "jest --coverage",
|
| 36 |
-
"test:verbose": "jest --verbose",
|
| 37 |
-
"test:silent": "jest --silent",
|
| 38 |
-
"test:unit": "node run-tests.js --unit",
|
| 39 |
-
"test:integration": "node run-tests.js --integration",
|
| 40 |
-
"test:summary": "node test-summary.js"
|
| 41 |
-
}
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/codex-oauth.js
DELETED
|
@@ -1,1056 +0,0 @@
|
|
| 1 |
-
import http from 'http';
|
| 2 |
-
import logger from '../utils/logger.js';
|
| 3 |
-
import fs from 'fs';
|
| 4 |
-
import path from 'path';
|
| 5 |
-
import crypto from 'crypto';
|
| 6 |
-
import open from 'open';
|
| 7 |
-
import axios from 'axios';
|
| 8 |
-
import { broadcastEvent } from '../services/ui-manager.js';
|
| 9 |
-
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
| 10 |
-
import { CONFIG } from '../core/config-manager.js';
|
| 11 |
-
import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
|
| 12 |
-
|
| 13 |
-
/**
|
| 14 |
-
* Codex OAuth 配置
|
| 15 |
-
*/
|
| 16 |
-
const CODEX_OAUTH_CONFIG = {
|
| 17 |
-
clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
| 18 |
-
authUrl: 'https://auth.openai.com/oauth/authorize',
|
| 19 |
-
tokenUrl: 'https://auth.openai.com/oauth/token',
|
| 20 |
-
redirectUri: 'http://localhost:1455/auth/callback',
|
| 21 |
-
port: 1455,
|
| 22 |
-
scopes: 'openid email profile offline_access',
|
| 23 |
-
logPrefix: '[Codex Auth]'
|
| 24 |
-
};
|
| 25 |
-
|
| 26 |
-
/**
|
| 27 |
-
* 活动的服务器实例管理(与 gemini-oauth 一致)
|
| 28 |
-
*/
|
| 29 |
-
const activeServers = new Map();
|
| 30 |
-
|
| 31 |
-
/**
|
| 32 |
-
* 关闭指定端口的活动服务器
|
| 33 |
-
*/
|
| 34 |
-
async function closeActiveServer(provider, port = null) {
|
| 35 |
-
const existing = activeServers.get(provider);
|
| 36 |
-
|
| 37 |
-
if (existing) {
|
| 38 |
-
try {
|
| 39 |
-
// 1. 使用 Promise.race() 添加 2 秒超时
|
| 40 |
-
const closePromise = new Promise((resolve, reject) => {
|
| 41 |
-
existing.server.close((err) => {
|
| 42 |
-
if (err) reject(err);
|
| 43 |
-
else resolve();
|
| 44 |
-
});
|
| 45 |
-
});
|
| 46 |
-
|
| 47 |
-
const timeoutPromise = new Promise((_, reject) => {
|
| 48 |
-
setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
|
| 49 |
-
});
|
| 50 |
-
|
| 51 |
-
await Promise.race([closePromise, timeoutPromise]);
|
| 52 |
-
logger.info(`[Codex Auth] ${provider} server closed successfully`);
|
| 53 |
-
} catch (error) {
|
| 54 |
-
// 2. try-catch 捕获错误
|
| 55 |
-
logger.warn(`[Codex Auth] Server close failed or timed out: ${error.message}`);
|
| 56 |
-
} finally {
|
| 57 |
-
// 3. finally 块强制清理,防止阻塞
|
| 58 |
-
activeServers.delete(provider);
|
| 59 |
-
}
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
if (port) {
|
| 63 |
-
for (const [p, info] of activeServers.entries()) {
|
| 64 |
-
if (info.port === port) {
|
| 65 |
-
// 递归调用处理端口冲突的情况
|
| 66 |
-
await closeActiveServer(p);
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
/**
|
| 73 |
-
* Codex OAuth 认证类
|
| 74 |
-
* 实现 OAuth2 + PKCE 流程
|
| 75 |
-
*/
|
| 76 |
-
class CodexAuth {
|
| 77 |
-
constructor(config) {
|
| 78 |
-
this.config = config;
|
| 79 |
-
|
| 80 |
-
// 配置代理支持
|
| 81 |
-
const axiosConfig = { timeout: 30000 };
|
| 82 |
-
const proxyConfig = getProxyConfigForProvider(config, 'openai-codex-oauth');
|
| 83 |
-
if (proxyConfig) {
|
| 84 |
-
axiosConfig.httpAgent = proxyConfig.httpAgent;
|
| 85 |
-
axiosConfig.httpsAgent = proxyConfig.httpsAgent;
|
| 86 |
-
logger.info('[Codex Auth] Proxy enabled for OAuth requests');
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
this.httpClient = axios.create(axiosConfig);
|
| 90 |
-
this.server = null; // 存储服务器实例
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
/**
|
| 94 |
-
* 生成 PKCE 代码
|
| 95 |
-
* @returns {{verifier: string, challenge: string}}
|
| 96 |
-
*/
|
| 97 |
-
generatePKCECodes() {
|
| 98 |
-
// 生成 code verifier (96 随机字节 → 128 base64url 字符)
|
| 99 |
-
const verifier = crypto.randomBytes(96)
|
| 100 |
-
.toString('base64url');
|
| 101 |
-
|
| 102 |
-
// 生成 code challenge (SHA256 of verifier)
|
| 103 |
-
const challenge = crypto.createHash('sha256')
|
| 104 |
-
.update(verifier)
|
| 105 |
-
.digest('base64url');
|
| 106 |
-
|
| 107 |
-
return { verifier, challenge };
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
/**
|
| 111 |
-
* 生成授权 URL(不启动完整流程)
|
| 112 |
-
* @returns {{authUrl: string, state: string, pkce: Object, server: Object}}
|
| 113 |
-
*/
|
| 114 |
-
async generateAuthUrl() {
|
| 115 |
-
const pkce = this.generatePKCECodes();
|
| 116 |
-
const state = crypto.randomBytes(16).toString('hex');
|
| 117 |
-
|
| 118 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Generating auth URL...`);
|
| 119 |
-
|
| 120 |
-
// 启动本地回调服务器
|
| 121 |
-
const server = await this.startCallbackServer();
|
| 122 |
-
this.server = server;
|
| 123 |
-
|
| 124 |
-
// 构建授权 URL
|
| 125 |
-
const authUrl = new URL(CODEX_OAUTH_CONFIG.authUrl);
|
| 126 |
-
authUrl.searchParams.set('client_id', CODEX_OAUTH_CONFIG.clientId);
|
| 127 |
-
authUrl.searchParams.set('response_type', 'code');
|
| 128 |
-
authUrl.searchParams.set('redirect_uri', CODEX_OAUTH_CONFIG.redirectUri);
|
| 129 |
-
authUrl.searchParams.set('scope', CODEX_OAUTH_CONFIG.scopes);
|
| 130 |
-
authUrl.searchParams.set('state', state);
|
| 131 |
-
authUrl.searchParams.set('code_challenge', pkce.challenge);
|
| 132 |
-
authUrl.searchParams.set('code_challenge_method', 'S256');
|
| 133 |
-
authUrl.searchParams.set('prompt', 'login');
|
| 134 |
-
authUrl.searchParams.set('id_token_add_organizations', 'true');
|
| 135 |
-
authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
|
| 136 |
-
|
| 137 |
-
return {
|
| 138 |
-
authUrl: authUrl.toString(),
|
| 139 |
-
state,
|
| 140 |
-
pkce,
|
| 141 |
-
server
|
| 142 |
-
};
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
/**
|
| 146 |
-
* 完成 OAuth 流程(在收到回调后调用)
|
| 147 |
-
* @param {string} code - 授权码
|
| 148 |
-
* @param {string} state - 状态参数
|
| 149 |
-
* @param {string} expectedState - 期望的状态参数
|
| 150 |
-
* @param {Object} pkce - PKCE 代码
|
| 151 |
-
* @returns {Promise<Object>} tokens 和凭据路径
|
| 152 |
-
*/
|
| 153 |
-
async completeOAuthFlow(code, state, expectedState, pkce) {
|
| 154 |
-
// 验证 state
|
| 155 |
-
if (state !== expectedState) {
|
| 156 |
-
throw new Error('State mismatch - possible CSRF attack');
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
// 用 code 换取 tokens
|
| 160 |
-
const tokens = await this.exchangeCodeForTokens(code, pkce.verifier);
|
| 161 |
-
|
| 162 |
-
// 解析 JWT 提取账户信息
|
| 163 |
-
const claims = this.parseJWT(tokens.id_token);
|
| 164 |
-
|
| 165 |
-
// 保存凭据(遵循 CLIProxyAPI 格式)
|
| 166 |
-
const credentials = {
|
| 167 |
-
id_token: tokens.id_token,
|
| 168 |
-
access_token: tokens.access_token,
|
| 169 |
-
refresh_token: tokens.refresh_token,
|
| 170 |
-
account_id: claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub,
|
| 171 |
-
last_refresh: new Date().toISOString(),
|
| 172 |
-
email: claims.email,
|
| 173 |
-
type: 'codex',
|
| 174 |
-
expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString()
|
| 175 |
-
};
|
| 176 |
-
|
| 177 |
-
// 保存凭据并获取路径
|
| 178 |
-
const saveResult = await this.saveCredentials(credentials);
|
| 179 |
-
const credPath = saveResult.credsPath;
|
| 180 |
-
const relativePath = saveResult.relativePath;
|
| 181 |
-
|
| 182 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`);
|
| 183 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`);
|
| 184 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`);
|
| 185 |
-
|
| 186 |
-
// 关闭服务器
|
| 187 |
-
if (this.server) {
|
| 188 |
-
this.server.close();
|
| 189 |
-
this.server = null;
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
return {
|
| 193 |
-
...credentials,
|
| 194 |
-
credPath,
|
| 195 |
-
relativePath
|
| 196 |
-
};
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
/**
|
| 200 |
-
* 启动 OAuth 流程
|
| 201 |
-
* @returns {Promise<Object>} 返回 tokens
|
| 202 |
-
*/
|
| 203 |
-
async startOAuthFlow() {
|
| 204 |
-
const pkce = this.generatePKCECodes();
|
| 205 |
-
const state = crypto.randomBytes(16).toString('hex');
|
| 206 |
-
|
| 207 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Starting OAuth flow...`);
|
| 208 |
-
|
| 209 |
-
// 启动本地回调服务器
|
| 210 |
-
const server = await this.startCallbackServer();
|
| 211 |
-
|
| 212 |
-
// 构建授权 URL
|
| 213 |
-
const authUrl = new URL(CODEX_OAUTH_CONFIG.authUrl);
|
| 214 |
-
authUrl.searchParams.set('client_id', CODEX_OAUTH_CONFIG.clientId);
|
| 215 |
-
authUrl.searchParams.set('response_type', 'code');
|
| 216 |
-
authUrl.searchParams.set('redirect_uri', CODEX_OAUTH_CONFIG.redirectUri);
|
| 217 |
-
authUrl.searchParams.set('scope', CODEX_OAUTH_CONFIG.scopes);
|
| 218 |
-
authUrl.searchParams.set('state', state);
|
| 219 |
-
authUrl.searchParams.set('code_challenge', pkce.challenge);
|
| 220 |
-
authUrl.searchParams.set('code_challenge_method', 'S256');
|
| 221 |
-
authUrl.searchParams.set('prompt', 'login');
|
| 222 |
-
authUrl.searchParams.set('id_token_add_organizations', 'true');
|
| 223 |
-
authUrl.searchParams.set('codex_cli_simplified_flow', 'true');
|
| 224 |
-
|
| 225 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Opening browser for authentication...`);
|
| 226 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} If browser doesn't open, visit: ${authUrl.toString()}`);
|
| 227 |
-
|
| 228 |
-
try {
|
| 229 |
-
await open(authUrl.toString());
|
| 230 |
-
} catch (error) {
|
| 231 |
-
logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to open browser automatically:`, error.message);
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
// 等待回调
|
| 235 |
-
const result = await this.waitForCallback(server, state);
|
| 236 |
-
|
| 237 |
-
// 用 code 换取 tokens
|
| 238 |
-
const tokens = await this.exchangeCodeForTokens(result.code, pkce.verifier);
|
| 239 |
-
|
| 240 |
-
// 解析 JWT 提取账户信息
|
| 241 |
-
const claims = this.parseJWT(tokens.id_token);
|
| 242 |
-
|
| 243 |
-
// 保存凭据(遵循 CLIProxyAPI 格式)
|
| 244 |
-
const credentials = {
|
| 245 |
-
id_token: tokens.id_token,
|
| 246 |
-
access_token: tokens.access_token,
|
| 247 |
-
refresh_token: tokens.refresh_token,
|
| 248 |
-
account_id: claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub,
|
| 249 |
-
last_refresh: new Date().toISOString(),
|
| 250 |
-
email: claims.email,
|
| 251 |
-
type: 'codex',
|
| 252 |
-
expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString()
|
| 253 |
-
};
|
| 254 |
-
|
| 255 |
-
await this.saveCredentials(credentials);
|
| 256 |
-
|
| 257 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Authentication successful!`);
|
| 258 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Email: ${credentials.email}`);
|
| 259 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Account ID: ${credentials.account_id}`);
|
| 260 |
-
|
| 261 |
-
return credentials;
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
/**
|
| 265 |
-
* 启动回调服务器
|
| 266 |
-
* @returns {Promise<http.Server>}
|
| 267 |
-
*/
|
| 268 |
-
async startCallbackServer() {
|
| 269 |
-
// 先清理该提供商或该端口的旧服务器
|
| 270 |
-
await closeActiveServer('openai-codex-oauth', CODEX_OAUTH_CONFIG.port);
|
| 271 |
-
|
| 272 |
-
return new Promise((resolve, reject) => {
|
| 273 |
-
const server = http.createServer();
|
| 274 |
-
|
| 275 |
-
server.on('request', (req, res) => {
|
| 276 |
-
if (req.url.startsWith('/auth/callback')) {
|
| 277 |
-
const url = new URL(req.url, `http://localhost:${CODEX_OAUTH_CONFIG.port}`);
|
| 278 |
-
const code = url.searchParams.get('code');
|
| 279 |
-
const state = url.searchParams.get('state');
|
| 280 |
-
const error = url.searchParams.get('error');
|
| 281 |
-
const errorDescription = url.searchParams.get('error_description');
|
| 282 |
-
|
| 283 |
-
if (error) {
|
| 284 |
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 285 |
-
res.end(`
|
| 286 |
-
<!DOCTYPE html>
|
| 287 |
-
<html>
|
| 288 |
-
<head>
|
| 289 |
-
<title>Authentication Failed</title>
|
| 290 |
-
<style>
|
| 291 |
-
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
| 292 |
-
h1 { color: #d32f2f; }
|
| 293 |
-
p { color: #666; }
|
| 294 |
-
</style>
|
| 295 |
-
</head>
|
| 296 |
-
<body>
|
| 297 |
-
<h1>❌ Authentication Failed</h1>
|
| 298 |
-
<p>${errorDescription || error}</p>
|
| 299 |
-
<p>You can close this window and try again.</p>
|
| 300 |
-
</body>
|
| 301 |
-
</html>
|
| 302 |
-
`);
|
| 303 |
-
server.emit('auth-error', new Error(errorDescription || error));
|
| 304 |
-
} else if (code && state) {
|
| 305 |
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 306 |
-
res.end(`
|
| 307 |
-
<!DOCTYPE html>
|
| 308 |
-
<html>
|
| 309 |
-
<head>
|
| 310 |
-
<title>Authentication Successful</title>
|
| 311 |
-
<style>
|
| 312 |
-
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
| 313 |
-
h1 { color: #4caf50; }
|
| 314 |
-
p { color: #666; }
|
| 315 |
-
.countdown { font-size: 24px; font-weight: bold; color: #2196f3; }
|
| 316 |
-
</style>
|
| 317 |
-
<script>
|
| 318 |
-
let countdown = 10;
|
| 319 |
-
setInterval(() => {
|
| 320 |
-
countdown--;
|
| 321 |
-
document.getElementById('countdown').textContent = countdown;
|
| 322 |
-
if (countdown <= 0) {
|
| 323 |
-
window.close();
|
| 324 |
-
}
|
| 325 |
-
}, 1000);
|
| 326 |
-
</script>
|
| 327 |
-
</head>
|
| 328 |
-
<body>
|
| 329 |
-
<h1>✅ Authentication Successful!</h1>
|
| 330 |
-
<p>You can now close this window and return to the application.</p>
|
| 331 |
-
<p>This window will close automatically in <span id="countdown" class="countdown">10</span> seconds.</p>
|
| 332 |
-
</body>
|
| 333 |
-
</html>
|
| 334 |
-
`);
|
| 335 |
-
server.emit('auth-success', { code, state });
|
| 336 |
-
}
|
| 337 |
-
} else if (req.url === '/success') {
|
| 338 |
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 339 |
-
res.end('<h1>Success!</h1>');
|
| 340 |
-
}
|
| 341 |
-
});
|
| 342 |
-
|
| 343 |
-
server.listen(CODEX_OAUTH_CONFIG.port, () => {
|
| 344 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Callback server listening on port ${CODEX_OAUTH_CONFIG.port}`);
|
| 345 |
-
activeServers.set('openai-codex-oauth', { server, port: CODEX_OAUTH_CONFIG.port });
|
| 346 |
-
resolve(server);
|
| 347 |
-
});
|
| 348 |
-
|
| 349 |
-
server.on('error', (error) => {
|
| 350 |
-
if (error.code === 'EADDRINUSE') {
|
| 351 |
-
reject(new Error(`Port ${CODEX_OAUTH_CONFIG.port} is already in use. Please close other applications using this port.`));
|
| 352 |
-
} else {
|
| 353 |
-
reject(error);
|
| 354 |
-
}
|
| 355 |
-
});
|
| 356 |
-
});
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
/**
|
| 360 |
-
* 等待 OAuth 回调
|
| 361 |
-
* @param {http.Server} server
|
| 362 |
-
* @param {string} expectedState
|
| 363 |
-
* @returns {Promise<{code: string, state: string}>}
|
| 364 |
-
*/
|
| 365 |
-
async waitForCallback(server, expectedState) {
|
| 366 |
-
return new Promise((resolve, reject) => {
|
| 367 |
-
const timeout = setTimeout(() => {
|
| 368 |
-
server.close();
|
| 369 |
-
reject(new Error('Authentication timeout (10 minutes)'));
|
| 370 |
-
}, 10 * 60 * 1000); // 10 分钟
|
| 371 |
-
|
| 372 |
-
server.once('auth-success', (result) => {
|
| 373 |
-
clearTimeout(timeout);
|
| 374 |
-
server.close();
|
| 375 |
-
|
| 376 |
-
if (result.state !== expectedState) {
|
| 377 |
-
reject(new Error('State mismatch - possible CSRF attack'));
|
| 378 |
-
} else {
|
| 379 |
-
resolve(result);
|
| 380 |
-
}
|
| 381 |
-
});
|
| 382 |
-
|
| 383 |
-
server.once('auth-error', (error) => {
|
| 384 |
-
clearTimeout(timeout);
|
| 385 |
-
server.close();
|
| 386 |
-
reject(error);
|
| 387 |
-
});
|
| 388 |
-
});
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
/**
|
| 392 |
-
* 用授权码换取 tokens
|
| 393 |
-
* @param {string} code
|
| 394 |
-
* @param {string} codeVerifier
|
| 395 |
-
* @returns {Promise<Object>}
|
| 396 |
-
*/
|
| 397 |
-
async exchangeCodeForTokens(code, codeVerifier) {
|
| 398 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Exchanging authorization code for tokens...`);
|
| 399 |
-
|
| 400 |
-
try {
|
| 401 |
-
const response = await this.httpClient.post(
|
| 402 |
-
CODEX_OAUTH_CONFIG.tokenUrl,
|
| 403 |
-
new URLSearchParams({
|
| 404 |
-
grant_type: 'authorization_code',
|
| 405 |
-
client_id: CODEX_OAUTH_CONFIG.clientId,
|
| 406 |
-
code: code,
|
| 407 |
-
redirect_uri: CODEX_OAUTH_CONFIG.redirectUri,
|
| 408 |
-
code_verifier: codeVerifier
|
| 409 |
-
}).toString(),
|
| 410 |
-
{
|
| 411 |
-
headers: {
|
| 412 |
-
'Content-Type': 'application/x-www-form-urlencoded',
|
| 413 |
-
'Accept': 'application/json'
|
| 414 |
-
}
|
| 415 |
-
}
|
| 416 |
-
);
|
| 417 |
-
|
| 418 |
-
return response.data;
|
| 419 |
-
} catch (error) {
|
| 420 |
-
logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token exchange failed:`, error.response?.data || error.message);
|
| 421 |
-
throw new Error(`Failed to exchange code for tokens: ${error.response?.data?.error_description || error.message}`);
|
| 422 |
-
}
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
/**
|
| 426 |
-
* 刷新 tokens
|
| 427 |
-
* @param {string} refreshToken
|
| 428 |
-
* @returns {Promise<Object>}
|
| 429 |
-
*/
|
| 430 |
-
async refreshTokens(refreshToken) {
|
| 431 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Refreshing access token...`);
|
| 432 |
-
|
| 433 |
-
try {
|
| 434 |
-
const response = await this.httpClient.post(
|
| 435 |
-
CODEX_OAUTH_CONFIG.tokenUrl,
|
| 436 |
-
new URLSearchParams({
|
| 437 |
-
grant_type: 'refresh_token',
|
| 438 |
-
client_id: CODEX_OAUTH_CONFIG.clientId,
|
| 439 |
-
refresh_token: refreshToken
|
| 440 |
-
}).toString(),
|
| 441 |
-
{
|
| 442 |
-
headers: {
|
| 443 |
-
'Content-Type': 'application/x-www-form-urlencoded',
|
| 444 |
-
'Accept': 'application/json'
|
| 445 |
-
}
|
| 446 |
-
}
|
| 447 |
-
);
|
| 448 |
-
|
| 449 |
-
const tokens = response.data;
|
| 450 |
-
const claims = this.parseJWT(tokens.id_token);
|
| 451 |
-
|
| 452 |
-
return {
|
| 453 |
-
id_token: tokens.id_token,
|
| 454 |
-
access_token: tokens.access_token,
|
| 455 |
-
refresh_token: tokens.refresh_token || refreshToken,
|
| 456 |
-
account_id: claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub,
|
| 457 |
-
last_refresh: new Date().toISOString(),
|
| 458 |
-
email: claims.email,
|
| 459 |
-
type: 'codex',
|
| 460 |
-
expired: new Date(Date.now() + (tokens.expires_in || 3600) * 1000).toISOString()
|
| 461 |
-
};
|
| 462 |
-
} catch (error) {
|
| 463 |
-
logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token refresh failed:`, error.response?.data || error.message);
|
| 464 |
-
throw new Error(`Failed to refresh tokens: ${error.response?.data?.error_description || error.message}`);
|
| 465 |
-
}
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
/**
|
| 469 |
-
* 解析 JWT token
|
| 470 |
-
* @param {string} token
|
| 471 |
-
* @returns {Object}
|
| 472 |
-
*/
|
| 473 |
-
parseJWT(token) {
|
| 474 |
-
try {
|
| 475 |
-
const parts = token.split('.');
|
| 476 |
-
if (parts.length !== 3) {
|
| 477 |
-
throw new Error('Invalid JWT token format');
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
// 解码 payload (base64url)
|
| 481 |
-
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
| 482 |
-
return JSON.parse(payload);
|
| 483 |
-
} catch (error) {
|
| 484 |
-
logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to parse JWT:`, error.message);
|
| 485 |
-
throw new Error(`Failed to parse JWT token: ${error.message}`);
|
| 486 |
-
}
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
-
/**
|
| 490 |
-
* 保存凭据到文件
|
| 491 |
-
* @param {Object} creds
|
| 492 |
-
* @returns {Promise<Object>}
|
| 493 |
-
*/
|
| 494 |
-
async saveCredentials(creds) {
|
| 495 |
-
const email = creds.email || this.config.CODEX_EMAIL || 'default';
|
| 496 |
-
|
| 497 |
-
// 优先使用配置中指定的路径,否则保存到 configs/codex 目录
|
| 498 |
-
let credsPath;
|
| 499 |
-
if (this.config.CODEX_OAUTH_CREDS_FILE_PATH) {
|
| 500 |
-
credsPath = this.config.CODEX_OAUTH_CREDS_FILE_PATH;
|
| 501 |
-
} else {
|
| 502 |
-
// 保存到 configs/codex 目录(与其他供应商一致)
|
| 503 |
-
const projectDir = process.cwd();
|
| 504 |
-
const targetDir = path.join(projectDir, 'configs', 'codex');
|
| 505 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 506 |
-
const timestamp = Date.now();
|
| 507 |
-
const filename = `${timestamp}_codex-${email}.json`;
|
| 508 |
-
credsPath = path.join(targetDir, filename);
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
try {
|
| 512 |
-
const credsDir = path.dirname(credsPath);
|
| 513 |
-
await fs.promises.mkdir(credsDir, { recursive: true });
|
| 514 |
-
await fs.promises.writeFile(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
| 515 |
-
|
| 516 |
-
const relativePath = path.relative(process.cwd(), credsPath);
|
| 517 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Credentials saved to ${relativePath}`);
|
| 518 |
-
|
| 519 |
-
// 返回保存路径供后续使用
|
| 520 |
-
return { credsPath, relativePath };
|
| 521 |
-
} catch (error) {
|
| 522 |
-
logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Failed to save credentials:`, error.message);
|
| 523 |
-
throw new Error(`Failed to save credentials: ${error.message}`);
|
| 524 |
-
}
|
| 525 |
-
}
|
| 526 |
-
|
| 527 |
-
/**
|
| 528 |
-
* 加载凭据
|
| 529 |
-
* @param {string} email
|
| 530 |
-
* @returns {Promise<Object|null>}
|
| 531 |
-
*/
|
| 532 |
-
async loadCredentials(email) {
|
| 533 |
-
// 优先使用配置中指定的路径,否则从 configs/codex 目录加载
|
| 534 |
-
let credsPath;
|
| 535 |
-
if (this.config.CODEX_OAUTH_CREDS_FILE_PATH) {
|
| 536 |
-
credsPath = this.config.CODEX_OAUTH_CREDS_FILE_PATH;
|
| 537 |
-
} else {
|
| 538 |
-
// 从 configs/codex 目录加载(与其他供应商一致)
|
| 539 |
-
const projectDir = process.cwd();
|
| 540 |
-
const targetDir = path.join(projectDir, 'configs', 'codex');
|
| 541 |
-
|
| 542 |
-
// 扫描目录找到匹配的凭据文件
|
| 543 |
-
try {
|
| 544 |
-
const files = await fs.promises.readdir(targetDir);
|
| 545 |
-
const emailPattern = email || 'default';
|
| 546 |
-
const matchingFile = files
|
| 547 |
-
.filter(f => f.includes(`codex-${emailPattern}`) && f.endsWith('.json'))
|
| 548 |
-
.sort()
|
| 549 |
-
.pop(); // 获取最新的文件
|
| 550 |
-
|
| 551 |
-
if (matchingFile) {
|
| 552 |
-
credsPath = path.join(targetDir, matchingFile);
|
| 553 |
-
} else {
|
| 554 |
-
return null;
|
| 555 |
-
}
|
| 556 |
-
} catch (error) {
|
| 557 |
-
if (error.code === 'ENOENT') {
|
| 558 |
-
return null;
|
| 559 |
-
}
|
| 560 |
-
throw error;
|
| 561 |
-
}
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
try {
|
| 565 |
-
const data = await fs.promises.readFile(credsPath, 'utf8');
|
| 566 |
-
return JSON.parse(data);
|
| 567 |
-
} catch (error) {
|
| 568 |
-
if (error.code === 'ENOENT') {
|
| 569 |
-
return null; // 文件不存在
|
| 570 |
-
}
|
| 571 |
-
throw error;
|
| 572 |
-
}
|
| 573 |
-
}
|
| 574 |
-
|
| 575 |
-
/**
|
| 576 |
-
* 检查凭据文件是否存在
|
| 577 |
-
* @param {string} email
|
| 578 |
-
* @returns {Promise<boolean>}
|
| 579 |
-
*/
|
| 580 |
-
async credentialsExist(email) {
|
| 581 |
-
// 优先使用配置中指定的路径,否则从 configs/codex 目录检查
|
| 582 |
-
let credsPath;
|
| 583 |
-
if (this.config.CODEX_OAUTH_CREDS_FILE_PATH) {
|
| 584 |
-
credsPath = this.config.CODEX_OAUTH_CREDS_FILE_PATH;
|
| 585 |
-
} else {
|
| 586 |
-
const projectDir = process.cwd();
|
| 587 |
-
const targetDir = path.join(projectDir, 'configs', 'codex');
|
| 588 |
-
|
| 589 |
-
try {
|
| 590 |
-
const files = await fs.promises.readdir(targetDir);
|
| 591 |
-
const emailPattern = email || 'default';
|
| 592 |
-
const hasMatch = files.some(f =>
|
| 593 |
-
f.includes(`codex-${emailPattern}`) && f.endsWith('.json')
|
| 594 |
-
);
|
| 595 |
-
return hasMatch;
|
| 596 |
-
} catch (error) {
|
| 597 |
-
return false;
|
| 598 |
-
}
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
try {
|
| 602 |
-
await fs.promises.access(credsPath);
|
| 603 |
-
return true;
|
| 604 |
-
} catch {
|
| 605 |
-
return false;
|
| 606 |
-
}
|
| 607 |
-
}
|
| 608 |
-
|
| 609 |
-
/**
|
| 610 |
-
* 检查凭据是否已存在(基于 account_id 或 refresh_token)
|
| 611 |
-
* @param {string} accountId
|
| 612 |
-
* @param {string} refreshToken
|
| 613 |
-
* @returns {Promise<{isDuplicate: boolean, existingPath?: string}>}
|
| 614 |
-
*/
|
| 615 |
-
async checkDuplicate(accountId, refreshToken) {
|
| 616 |
-
const projectDir = process.cwd();
|
| 617 |
-
const targetDir = path.join(projectDir, 'configs', 'codex');
|
| 618 |
-
|
| 619 |
-
try {
|
| 620 |
-
if (!fs.existsSync(targetDir)) {
|
| 621 |
-
return { isDuplicate: false };
|
| 622 |
-
}
|
| 623 |
-
|
| 624 |
-
const files = await fs.promises.readdir(targetDir);
|
| 625 |
-
for (const file of files) {
|
| 626 |
-
if (file.endsWith('.json')) {
|
| 627 |
-
try {
|
| 628 |
-
const fullPath = path.join(targetDir, file);
|
| 629 |
-
const content = await fs.promises.readFile(fullPath, 'utf8');
|
| 630 |
-
const credentials = JSON.parse(content);
|
| 631 |
-
|
| 632 |
-
if ((accountId && credentials.account_id === accountId) || (refreshToken && credentials.refresh_token === refreshToken)) {
|
| 633 |
-
const relativePath = path.relative(process.cwd(), fullPath);
|
| 634 |
-
return {
|
| 635 |
-
isDuplicate: true,
|
| 636 |
-
existingPath: relativePath
|
| 637 |
-
};
|
| 638 |
-
}
|
| 639 |
-
} catch (e) {
|
| 640 |
-
// 忽略解析错误
|
| 641 |
-
}
|
| 642 |
-
}
|
| 643 |
-
}
|
| 644 |
-
return { isDuplicate: false };
|
| 645 |
-
} catch (error) {
|
| 646 |
-
logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message);
|
| 647 |
-
return { isDuplicate: false };
|
| 648 |
-
}
|
| 649 |
-
}
|
| 650 |
-
}
|
| 651 |
-
|
| 652 |
-
/**
|
| 653 |
-
* 批量导入 Codex Token 并生成凭据文件(流式版本)
|
| 654 |
-
* @param {Object[]} tokens - Token 对象数组
|
| 655 |
-
* @param {Function} onProgress - 进度回调函数
|
| 656 |
-
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查
|
| 657 |
-
* @returns {Promise<Object>} 批量处理结果
|
| 658 |
-
*/
|
| 659 |
-
export async function batchImportCodexTokensStream(tokens, onProgress = null, skipDuplicateCheck = false) {
|
| 660 |
-
const auth = new CodexAuth({});
|
| 661 |
-
const results = {
|
| 662 |
-
total: tokens.length,
|
| 663 |
-
success: 0,
|
| 664 |
-
failed: 0,
|
| 665 |
-
details: []
|
| 666 |
-
};
|
| 667 |
-
|
| 668 |
-
for (let i = 0; i < tokens.length; i++) {
|
| 669 |
-
const tokenData = tokens[i];
|
| 670 |
-
const progressData = {
|
| 671 |
-
index: i + 1,
|
| 672 |
-
total: tokens.length,
|
| 673 |
-
current: null
|
| 674 |
-
};
|
| 675 |
-
|
| 676 |
-
try {
|
| 677 |
-
// 验证 token 数据
|
| 678 |
-
if (!tokenData.access_token || !tokenData.id_token) {
|
| 679 |
-
throw new Error('Token 缺少必需字段 (access_token 或 id_token)');
|
| 680 |
-
}
|
| 681 |
-
|
| 682 |
-
// 解析 JWT 提取账户信息
|
| 683 |
-
const claims = auth.parseJWT(tokenData.id_token);
|
| 684 |
-
const accountId = claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.sub;
|
| 685 |
-
const email = claims.email;
|
| 686 |
-
|
| 687 |
-
// 检查重复
|
| 688 |
-
if (!skipDuplicateCheck) {
|
| 689 |
-
const duplicateCheck = await auth.checkDuplicate(accountId, tokenData.refresh_token);
|
| 690 |
-
if (duplicateCheck.isDuplicate) {
|
| 691 |
-
progressData.current = {
|
| 692 |
-
index: i + 1,
|
| 693 |
-
success: false,
|
| 694 |
-
error: 'duplicate',
|
| 695 |
-
existingPath: duplicateCheck.existingPath
|
| 696 |
-
};
|
| 697 |
-
results.failed++;
|
| 698 |
-
results.details.push(progressData.current);
|
| 699 |
-
if (onProgress) {
|
| 700 |
-
onProgress({
|
| 701 |
-
...progressData,
|
| 702 |
-
successCount: results.success,
|
| 703 |
-
failedCount: results.failed
|
| 704 |
-
});
|
| 705 |
-
}
|
| 706 |
-
continue;
|
| 707 |
-
}
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
// 构建凭据对象
|
| 711 |
-
const credentials = {
|
| 712 |
-
id_token: tokenData.id_token,
|
| 713 |
-
access_token: tokenData.access_token,
|
| 714 |
-
refresh_token: tokenData.refresh_token,
|
| 715 |
-
account_id: accountId,
|
| 716 |
-
last_refresh: new Date().toISOString(),
|
| 717 |
-
email: email,
|
| 718 |
-
type: 'codex',
|
| 719 |
-
expired: new Date(Date.now() + (tokenData.expires_in || 3600) * 1000).toISOString()
|
| 720 |
-
};
|
| 721 |
-
|
| 722 |
-
// 保存凭据
|
| 723 |
-
const saveResult = await auth.saveCredentials(credentials);
|
| 724 |
-
const relativePath = saveResult.relativePath;
|
| 725 |
-
|
| 726 |
-
logger.info(`${CODEX_OAUTH_CONFIG.logPrefix} Token ${i + 1} imported: ${relativePath}`);
|
| 727 |
-
|
| 728 |
-
progressData.current = {
|
| 729 |
-
index: i + 1,
|
| 730 |
-
success: true,
|
| 731 |
-
path: relativePath
|
| 732 |
-
};
|
| 733 |
-
results.success++;
|
| 734 |
-
|
| 735 |
-
// 自动关联到 Pools
|
| 736 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 737 |
-
onlyCurrentCred: true,
|
| 738 |
-
credPath: relativePath
|
| 739 |
-
});
|
| 740 |
-
|
| 741 |
-
} catch (error) {
|
| 742 |
-
logger.error(`${CODEX_OAUTH_CONFIG.logPrefix} Token ${i + 1} import failed:`, error.message);
|
| 743 |
-
|
| 744 |
-
progressData.current = {
|
| 745 |
-
index: i + 1,
|
| 746 |
-
success: false,
|
| 747 |
-
error: error.message
|
| 748 |
-
};
|
| 749 |
-
results.failed++;
|
| 750 |
-
}
|
| 751 |
-
|
| 752 |
-
results.details.push(progressData.current);
|
| 753 |
-
|
| 754 |
-
if (onProgress) {
|
| 755 |
-
onProgress({
|
| 756 |
-
...progressData,
|
| 757 |
-
successCount: results.success,
|
| 758 |
-
failedCount: results.failed
|
| 759 |
-
});
|
| 760 |
-
}
|
| 761 |
-
}
|
| 762 |
-
|
| 763 |
-
if (results.success > 0) {
|
| 764 |
-
broadcastEvent('oauth_batch_success', {
|
| 765 |
-
provider: 'openai-codex-oauth',
|
| 766 |
-
count: results.success,
|
| 767 |
-
timestamp: new Date().toISOString()
|
| 768 |
-
});
|
| 769 |
-
}
|
| 770 |
-
|
| 771 |
-
return results;
|
| 772 |
-
}
|
| 773 |
-
|
| 774 |
-
/**
|
| 775 |
-
* 带重试的 Codex token 刷新
|
| 776 |
-
* @param {string} refreshToken
|
| 777 |
-
* @param {Object} config
|
| 778 |
-
* @param {number} maxRetries
|
| 779 |
-
* @returns {Promise<Object>}
|
| 780 |
-
*/
|
| 781 |
-
export async function refreshCodexTokensWithRetry(refreshToken, config = {}, maxRetries = 3) {
|
| 782 |
-
const auth = new CodexAuth(config);
|
| 783 |
-
let lastError;
|
| 784 |
-
|
| 785 |
-
for (let i = 0; i < maxRetries; i++) {
|
| 786 |
-
try {
|
| 787 |
-
return await auth.refreshTokens(refreshToken);
|
| 788 |
-
} catch (error) {
|
| 789 |
-
lastError = error;
|
| 790 |
-
logger.warn(`${CODEX_OAUTH_CONFIG.logPrefix} Retry ${i + 1}/${maxRetries} failed:`, error.message);
|
| 791 |
-
|
| 792 |
-
if (i < maxRetries - 1) {
|
| 793 |
-
// 指数退避
|
| 794 |
-
const delay = Math.min(1000 * Math.pow(2, i), 10000);
|
| 795 |
-
await new Promise(resolve => setTimeout(resolve, delay));
|
| 796 |
-
}
|
| 797 |
-
}
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
throw lastError;
|
| 801 |
-
}
|
| 802 |
-
|
| 803 |
-
/**
|
| 804 |
-
* 处理 Codex OAuth 认证
|
| 805 |
-
* @param {Object} currentConfig - 当前配置
|
| 806 |
-
* @param {Object} options - 选项
|
| 807 |
-
* @returns {Promise<Object>} 返回认证结果
|
| 808 |
-
*/
|
| 809 |
-
export async function handleCodexOAuth(currentConfig, options = {}) {
|
| 810 |
-
const auth = new CodexAuth(currentConfig);
|
| 811 |
-
|
| 812 |
-
try {
|
| 813 |
-
logger.info('[Codex Auth] Generating OAuth URL...');
|
| 814 |
-
|
| 815 |
-
// 清理所有旧的会话和服务器
|
| 816 |
-
if (global.codexOAuthSessions && global.codexOAuthSessions.size > 0) {
|
| 817 |
-
logger.info('[Codex Auth] Cleaning up old OAuth sessions...');
|
| 818 |
-
for (const [sessionId, session] of global.codexOAuthSessions.entries()) {
|
| 819 |
-
try {
|
| 820 |
-
// 清理定时器
|
| 821 |
-
if (session.pollTimer) {
|
| 822 |
-
clearInterval(session.pollTimer);
|
| 823 |
-
}
|
| 824 |
-
// 不在这里显式关闭 server,由 startCallbackServer 中的 closeActiveServer 处理
|
| 825 |
-
global.codexOAuthSessions.delete(sessionId);
|
| 826 |
-
} catch (error) {
|
| 827 |
-
logger.warn(`[Codex Auth] Failed to clean up session ${sessionId}:`, error.message);
|
| 828 |
-
}
|
| 829 |
-
}
|
| 830 |
-
}
|
| 831 |
-
|
| 832 |
-
// 生成授权 URL 和启动回调服务器
|
| 833 |
-
const { authUrl, state, pkce, server } = await auth.generateAuthUrl();
|
| 834 |
-
|
| 835 |
-
logger.info('[Codex Auth] OAuth URL generated successfully');
|
| 836 |
-
|
| 837 |
-
// 存储 OAuth 会话信息,供后续回调使用
|
| 838 |
-
if (!global.codexOAuthSessions) {
|
| 839 |
-
global.codexOAuthSessions = new Map();
|
| 840 |
-
}
|
| 841 |
-
|
| 842 |
-
const sessionId = state; // 使用 state 作为 session ID
|
| 843 |
-
|
| 844 |
-
// 轮询计数器
|
| 845 |
-
let pollCount = 0;
|
| 846 |
-
const maxPollCount = 100; // 增加到约 5 分钟 (100 * 3s = 300s)
|
| 847 |
-
const pollInterval = 3000; // 轮询间隔(毫秒)
|
| 848 |
-
let pollTimer = null;
|
| 849 |
-
let isCompleted = false;
|
| 850 |
-
|
| 851 |
-
// 创建会话对象
|
| 852 |
-
const session = {
|
| 853 |
-
auth,
|
| 854 |
-
state,
|
| 855 |
-
pkce,
|
| 856 |
-
server,
|
| 857 |
-
pollTimer: null,
|
| 858 |
-
createdAt: Date.now()
|
| 859 |
-
};
|
| 860 |
-
|
| 861 |
-
global.codexOAuthSessions.set(sessionId, session);
|
| 862 |
-
|
| 863 |
-
// 启动轮询日志
|
| 864 |
-
pollTimer = setInterval(() => {
|
| 865 |
-
pollCount++;
|
| 866 |
-
if (pollCount <= maxPollCount && !isCompleted) {
|
| 867 |
-
logger.info(`[Codex Auth] Waiting for callback... (${pollCount}/${maxPollCount})`);
|
| 868 |
-
}
|
| 869 |
-
|
| 870 |
-
if (pollCount >= maxPollCount && !isCompleted) {
|
| 871 |
-
clearInterval(pollTimer);
|
| 872 |
-
const totalSeconds = (maxPollCount * pollInterval) / 1000;
|
| 873 |
-
logger.info(`[Codex Auth] Polling timeout (${totalSeconds}s), releasing session for next authorization`);
|
| 874 |
-
|
| 875 |
-
// 清理会话
|
| 876 |
-
if (global.codexOAuthSessions.has(sessionId)) {
|
| 877 |
-
global.codexOAuthSessions.delete(sessionId);
|
| 878 |
-
}
|
| 879 |
-
}
|
| 880 |
-
}, pollInterval);
|
| 881 |
-
|
| 882 |
-
// 将 pollTimer 存储到会话中
|
| 883 |
-
session.pollTimer = pollTimer;
|
| 884 |
-
|
| 885 |
-
// 监听回调服务器的 auth-success 事件,自动完成 OAuth 流程
|
| 886 |
-
server.once('auth-success', async (result) => {
|
| 887 |
-
isCompleted = true;
|
| 888 |
-
if (pollTimer) {
|
| 889 |
-
clearInterval(pollTimer);
|
| 890 |
-
}
|
| 891 |
-
|
| 892 |
-
try {
|
| 893 |
-
logger.info('[Codex Auth] Received auth callback, completing OAuth flow...');
|
| 894 |
-
|
| 895 |
-
const session = global.codexOAuthSessions.get(sessionId);
|
| 896 |
-
if (!session) {
|
| 897 |
-
logger.error('[Codex Auth] Session not found');
|
| 898 |
-
return;
|
| 899 |
-
}
|
| 900 |
-
|
| 901 |
-
// 完成 OAuth 流程
|
| 902 |
-
const credentials = await auth.completeOAuthFlow(result.code, result.state, session.state, session.pkce);
|
| 903 |
-
|
| 904 |
-
// 清理会话
|
| 905 |
-
global.codexOAuthSessions.delete(sessionId);
|
| 906 |
-
|
| 907 |
-
// 广播认证成功事件
|
| 908 |
-
broadcastEvent('oauth_success', {
|
| 909 |
-
provider: 'openai-codex-oauth',
|
| 910 |
-
credPath: credentials.credPath,
|
| 911 |
-
relativePath: credentials.relativePath,
|
| 912 |
-
timestamp: new Date().toISOString(),
|
| 913 |
-
email: credentials.email,
|
| 914 |
-
accountId: credentials.account_id
|
| 915 |
-
});
|
| 916 |
-
|
| 917 |
-
// 自动关联新生成的凭据到 Pools
|
| 918 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 919 |
-
onlyCurrentCred: true,
|
| 920 |
-
credPath: credentials.relativePath
|
| 921 |
-
});
|
| 922 |
-
|
| 923 |
-
logger.info('[Codex Auth] OAuth flow completed successfully');
|
| 924 |
-
} catch (error) {
|
| 925 |
-
logger.error('[Codex Auth] Failed to complete OAuth flow:', error.message);
|
| 926 |
-
|
| 927 |
-
// 广播认证失败事件
|
| 928 |
-
broadcastEvent('oauth_error', {
|
| 929 |
-
provider: 'openai-codex-oauth',
|
| 930 |
-
error: error.message,
|
| 931 |
-
timestamp: new Date().toISOString()
|
| 932 |
-
});
|
| 933 |
-
}
|
| 934 |
-
});
|
| 935 |
-
|
| 936 |
-
// 监听 auth-error 事件
|
| 937 |
-
server.once('auth-error', (error) => {
|
| 938 |
-
isCompleted = true;
|
| 939 |
-
if (pollTimer) {
|
| 940 |
-
clearInterval(pollTimer);
|
| 941 |
-
}
|
| 942 |
-
|
| 943 |
-
logger.error('[Codex Auth] Auth error:', error.message);
|
| 944 |
-
global.codexOAuthSessions.delete(sessionId);
|
| 945 |
-
|
| 946 |
-
broadcastEvent('oauth_error', {
|
| 947 |
-
provider: 'openai-codex-oauth',
|
| 948 |
-
error: error.message,
|
| 949 |
-
timestamp: new Date().toISOString()
|
| 950 |
-
});
|
| 951 |
-
});
|
| 952 |
-
|
| 953 |
-
return {
|
| 954 |
-
success: true,
|
| 955 |
-
authUrl: authUrl,
|
| 956 |
-
authInfo: {
|
| 957 |
-
provider: 'openai-codex-oauth',
|
| 958 |
-
method: 'oauth2-pkce',
|
| 959 |
-
sessionId: sessionId,
|
| 960 |
-
redirectUri: CODEX_OAUTH_CONFIG.redirectUri,
|
| 961 |
-
port: CODEX_OAUTH_CONFIG.port,
|
| 962 |
-
instructions: [
|
| 963 |
-
'1. 点击下方按钮在浏览器中打开授权链接',
|
| 964 |
-
'2. 使用您的 OpenAI 账户登录',
|
| 965 |
-
'3. 授权应用访问您的 Codex API',
|
| 966 |
-
'4. 授权成功后会自动保存凭据',
|
| 967 |
-
'5. 如果浏览器未自动跳转,请手动复制回调 URL'
|
| 968 |
-
]
|
| 969 |
-
}
|
| 970 |
-
};
|
| 971 |
-
} catch (error) {
|
| 972 |
-
logger.error('[Codex Auth] Failed to generate OAuth URL:', error.message);
|
| 973 |
-
|
| 974 |
-
return {
|
| 975 |
-
success: false,
|
| 976 |
-
error: error.message,
|
| 977 |
-
authInfo: {
|
| 978 |
-
provider: 'openai-codex-oauth',
|
| 979 |
-
method: 'oauth2-pkce',
|
| 980 |
-
instructions: [
|
| 981 |
-
`1. 确保端口 ${CODEX_OAUTH_CONFIG.port} 未被占用`,
|
| 982 |
-
'2. 确保可以访问 auth.openai.com',
|
| 983 |
-
'3. 确保浏览器可以正常打开',
|
| 984 |
-
'4. 如果问题持续,请检查网络连接'
|
| 985 |
-
]
|
| 986 |
-
}
|
| 987 |
-
};
|
| 988 |
-
}
|
| 989 |
-
}
|
| 990 |
-
|
| 991 |
-
/**
|
| 992 |
-
* 处理 Codex OAuth 回调
|
| 993 |
-
* @param {string} code - 授权码
|
| 994 |
-
* @param {string} state - 状态参数
|
| 995 |
-
* @returns {Promise<Object>} 返回认证结果
|
| 996 |
-
*/
|
| 997 |
-
export async function handleCodexOAuthCallback(code, state) {
|
| 998 |
-
try {
|
| 999 |
-
if (!global.codexOAuthSessions || !global.codexOAuthSessions.has(state)) {
|
| 1000 |
-
throw new Error('Invalid or expired OAuth session');
|
| 1001 |
-
}
|
| 1002 |
-
|
| 1003 |
-
const session = global.codexOAuthSessions.get(state);
|
| 1004 |
-
const { auth, state: expectedState, pkce } = session;
|
| 1005 |
-
|
| 1006 |
-
logger.info('[Codex Auth] Processing OAuth callback...');
|
| 1007 |
-
|
| 1008 |
-
// 完成 OAuth 流程
|
| 1009 |
-
const result = await auth.completeOAuthFlow(code, state, expectedState, pkce);
|
| 1010 |
-
|
| 1011 |
-
// 清理会话
|
| 1012 |
-
global.codexOAuthSessions.delete(state);
|
| 1013 |
-
|
| 1014 |
-
// 广播认证成功事件(与 gemini 格式一致)
|
| 1015 |
-
broadcastEvent('oauth_success', {
|
| 1016 |
-
provider: 'openai-codex-oauth',
|
| 1017 |
-
credPath: result.credPath,
|
| 1018 |
-
relativePath: result.relativePath,
|
| 1019 |
-
timestamp: new Date().toISOString(),
|
| 1020 |
-
email: result.email,
|
| 1021 |
-
accountId: result.account_id
|
| 1022 |
-
});
|
| 1023 |
-
|
| 1024 |
-
// 自动关联新生成的凭据到 Pools
|
| 1025 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 1026 |
-
onlyCurrentCred: true,
|
| 1027 |
-
credPath: result.relativePath
|
| 1028 |
-
});
|
| 1029 |
-
|
| 1030 |
-
logger.info('[Codex Auth] OAuth callback processed successfully');
|
| 1031 |
-
|
| 1032 |
-
return {
|
| 1033 |
-
success: true,
|
| 1034 |
-
message: 'Codex authentication successful',
|
| 1035 |
-
credentials: result,
|
| 1036 |
-
email: result.email,
|
| 1037 |
-
accountId: result.account_id,
|
| 1038 |
-
credPath: result.credPath,
|
| 1039 |
-
relativePath: result.relativePath
|
| 1040 |
-
};
|
| 1041 |
-
} catch (error) {
|
| 1042 |
-
logger.error('[Codex Auth] OAuth callback failed:', error.message);
|
| 1043 |
-
|
| 1044 |
-
// 广播认证失败事件
|
| 1045 |
-
broadcastEvent('oauth_error', {
|
| 1046 |
-
provider: 'openai-codex-oauth',
|
| 1047 |
-
error: error.message,
|
| 1048 |
-
timestamp: new Date().toISOString()
|
| 1049 |
-
});
|
| 1050 |
-
|
| 1051 |
-
return {
|
| 1052 |
-
success: false,
|
| 1053 |
-
error: error.message
|
| 1054 |
-
};
|
| 1055 |
-
}
|
| 1056 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/gemini-oauth.js
DELETED
|
@@ -1,504 +0,0 @@
|
|
| 1 |
-
import { OAuth2Client } from 'google-auth-library';
|
| 2 |
-
import logger from '../utils/logger.js';
|
| 3 |
-
import http from 'http';
|
| 4 |
-
import fs from 'fs';
|
| 5 |
-
import path from 'path';
|
| 6 |
-
import os from 'os';
|
| 7 |
-
import { broadcastEvent } from '../services/ui-manager.js';
|
| 8 |
-
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
| 9 |
-
import { CONFIG } from '../core/config-manager.js';
|
| 10 |
-
import { getGoogleAuthProxyConfig } from '../utils/proxy-utils.js';
|
| 11 |
-
|
| 12 |
-
/**
|
| 13 |
-
* OAuth 提供商配置
|
| 14 |
-
*/
|
| 15 |
-
const OAUTH_PROVIDERS = {
|
| 16 |
-
'gemini-cli-oauth': {
|
| 17 |
-
clientId: '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com',
|
| 18 |
-
clientSecret: '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',
|
| 19 |
-
port: 8085,
|
| 20 |
-
credentialsDir: '.gemini',
|
| 21 |
-
credentialsFile: 'oauth_creds.json',
|
| 22 |
-
scope: ['https://www.googleapis.com/auth/cloud-platform'],
|
| 23 |
-
logPrefix: '[Gemini Auth]'
|
| 24 |
-
},
|
| 25 |
-
'gemini-antigravity': {
|
| 26 |
-
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
|
| 27 |
-
clientSecret: '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',
|
| 28 |
-
port: 8086,
|
| 29 |
-
credentialsDir: '.antigravity',
|
| 30 |
-
credentialsFile: 'oauth_creds.json',
|
| 31 |
-
scope: ['https://www.googleapis.com/auth/cloud-platform'],
|
| 32 |
-
logPrefix: '[Antigravity Auth]'
|
| 33 |
-
}
|
| 34 |
-
};
|
| 35 |
-
|
| 36 |
-
/**
|
| 37 |
-
* 活动的服务器实例管理
|
| 38 |
-
*/
|
| 39 |
-
const activeServers = new Map();
|
| 40 |
-
|
| 41 |
-
/**
|
| 42 |
-
* 生成 HTML 响应页面
|
| 43 |
-
* @param {boolean} isSuccess - 是否成功
|
| 44 |
-
* @param {string} message - 显示消息
|
| 45 |
-
* @returns {string} HTML 内容
|
| 46 |
-
*/
|
| 47 |
-
function generateResponsePage(isSuccess, message) {
|
| 48 |
-
const title = isSuccess ? '授权成功!' : '授权失败';
|
| 49 |
-
|
| 50 |
-
return `<!DOCTYPE html>
|
| 51 |
-
<html lang="zh-CN">
|
| 52 |
-
<head>
|
| 53 |
-
<meta charset="utf-8">
|
| 54 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 55 |
-
<title>${title}</title>
|
| 56 |
-
</head>
|
| 57 |
-
<body>
|
| 58 |
-
<div class="container">
|
| 59 |
-
<h1>${title}</h1>
|
| 60 |
-
<p>${message}</p>
|
| 61 |
-
</div>
|
| 62 |
-
</body>
|
| 63 |
-
</html>`;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
/**
|
| 67 |
-
* 关闭指定端口的活动服务器
|
| 68 |
-
* @param {number} port - 端口号
|
| 69 |
-
* @returns {Promise<void>}
|
| 70 |
-
*/
|
| 71 |
-
async function closeActiveServer(provider, port = null) {
|
| 72 |
-
// 1. 关闭该提供商之前的所有服务器
|
| 73 |
-
const existing = activeServers.get(provider);
|
| 74 |
-
if (existing) {
|
| 75 |
-
// 清理轮询定时器
|
| 76 |
-
if (existing.pollTimer) {
|
| 77 |
-
clearInterval(existing.pollTimer);
|
| 78 |
-
existing.pollTimer = null;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
try {
|
| 82 |
-
const closePromise = new Promise((resolve, reject) => {
|
| 83 |
-
existing.server.close((err) => {
|
| 84 |
-
if (err) reject(err);
|
| 85 |
-
else resolve();
|
| 86 |
-
});
|
| 87 |
-
});
|
| 88 |
-
|
| 89 |
-
const timeoutPromise = new Promise((_, reject) => {
|
| 90 |
-
setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
|
| 91 |
-
});
|
| 92 |
-
|
| 93 |
-
await Promise.race([closePromise, timeoutPromise]);
|
| 94 |
-
logger.info(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
|
| 95 |
-
} catch (error) {
|
| 96 |
-
logger.warn(`[OAuth] 关闭提供商 ${provider} 服务器失败或超时: ${error.message}`);
|
| 97 |
-
} finally {
|
| 98 |
-
activeServers.delete(provider);
|
| 99 |
-
}
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
// 2. 如果指定了端口,检查是否有其他提供商占用了该端口
|
| 103 |
-
if (port) {
|
| 104 |
-
for (const [p, info] of activeServers.entries()) {
|
| 105 |
-
if (info.port === port) {
|
| 106 |
-
await closeActiveServer(p);
|
| 107 |
-
}
|
| 108 |
-
}
|
| 109 |
-
}
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
/**
|
| 113 |
-
* 创建 OAuth 回调服务器
|
| 114 |
-
* @param {Object} config - OAuth 提供商配置
|
| 115 |
-
* @param {string} redirectUri - 重定向 URI
|
| 116 |
-
* @param {OAuth2Client} authClient - OAuth2 客户端
|
| 117 |
-
* @param {string} credPath - 凭据保存路径
|
| 118 |
-
* @param {string} provider - 提供商标识
|
| 119 |
-
* @returns {Promise<http.Server>} HTTP 服务器实例
|
| 120 |
-
*/
|
| 121 |
-
async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) {
|
| 122 |
-
const port = parseInt(options.port) || config.port;
|
| 123 |
-
// 先关闭该提供商之前可能运行的所有服务器,或该端口上的旧服务器
|
| 124 |
-
await closeActiveServer(provider, port);
|
| 125 |
-
|
| 126 |
-
return new Promise((resolve, reject) => {
|
| 127 |
-
let pollCount = 0;
|
| 128 |
-
const maxPollCount = 100; // 约 5 分钟 (100 * 3s = 300s)
|
| 129 |
-
const pollInterval = 3000;
|
| 130 |
-
let pollTimer = null;
|
| 131 |
-
|
| 132 |
-
const clearPollTimer = () => {
|
| 133 |
-
if (pollTimer) {
|
| 134 |
-
clearInterval(pollTimer);
|
| 135 |
-
pollTimer = null;
|
| 136 |
-
}
|
| 137 |
-
};
|
| 138 |
-
|
| 139 |
-
const server = http.createServer(async (req, res) => {
|
| 140 |
-
try {
|
| 141 |
-
const url = new URL(req.url, redirectUri);
|
| 142 |
-
const code = url.searchParams.get('code');
|
| 143 |
-
const errorParam = url.searchParams.get('error');
|
| 144 |
-
|
| 145 |
-
if (code) {
|
| 146 |
-
clearPollTimer();
|
| 147 |
-
logger.info(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`);
|
| 148 |
-
|
| 149 |
-
try {
|
| 150 |
-
const { tokens } = await authClient.getToken(code);
|
| 151 |
-
let finalCredPath = credPath;
|
| 152 |
-
|
| 153 |
-
// 如果指定了保存到 configs 目录
|
| 154 |
-
if (options.saveToConfigs) {
|
| 155 |
-
const providerDir = options.providerDir;
|
| 156 |
-
const targetDir = path.join(process.cwd(), 'configs', providerDir);
|
| 157 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 158 |
-
const timestamp = Date.now();
|
| 159 |
-
const filename = `${timestamp}_oauth_creds.json`;
|
| 160 |
-
finalCredPath = path.join(targetDir, filename);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
await fs.promises.mkdir(path.dirname(finalCredPath), { recursive: true });
|
| 164 |
-
await fs.promises.writeFile(finalCredPath, JSON.stringify(tokens, null, 2));
|
| 165 |
-
logger.info(`${config.logPrefix} 新令牌已接收并保存到文件: ${finalCredPath}`);
|
| 166 |
-
|
| 167 |
-
const relativePath = path.relative(process.cwd(), finalCredPath);
|
| 168 |
-
|
| 169 |
-
// 广播授权成功事件
|
| 170 |
-
broadcastEvent('oauth_success', {
|
| 171 |
-
provider: provider,
|
| 172 |
-
credPath: finalCredPath,
|
| 173 |
-
relativePath: relativePath,
|
| 174 |
-
timestamp: new Date().toISOString()
|
| 175 |
-
});
|
| 176 |
-
|
| 177 |
-
// 自动关联新生成的凭据到 Pools
|
| 178 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 179 |
-
onlyCurrentCred: true,
|
| 180 |
-
credPath: relativePath
|
| 181 |
-
});
|
| 182 |
-
|
| 183 |
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 184 |
-
res.end(generateResponsePage(true, '您可以关闭此页面'));
|
| 185 |
-
} catch (tokenError) {
|
| 186 |
-
logger.error(`${config.logPrefix} 获取令牌失败:`, tokenError);
|
| 187 |
-
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 188 |
-
res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`));
|
| 189 |
-
} finally {
|
| 190 |
-
server.close(() => {
|
| 191 |
-
activeServers.delete(provider);
|
| 192 |
-
});
|
| 193 |
-
}
|
| 194 |
-
} else if (errorParam) {
|
| 195 |
-
clearPollTimer();
|
| 196 |
-
const errorMessage = `授权失败。Google 返回错误: ${errorParam}`;
|
| 197 |
-
logger.error(`${config.logPrefix}`, errorMessage);
|
| 198 |
-
|
| 199 |
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 200 |
-
res.end(generateResponsePage(false, errorMessage));
|
| 201 |
-
server.close(() => {
|
| 202 |
-
activeServers.delete(provider);
|
| 203 |
-
});
|
| 204 |
-
} else {
|
| 205 |
-
logger.info(`${config.logPrefix} 忽略无关请求: ${req.url}`);
|
| 206 |
-
res.writeHead(204);
|
| 207 |
-
res.end();
|
| 208 |
-
}
|
| 209 |
-
} catch (error) {
|
| 210 |
-
clearPollTimer();
|
| 211 |
-
logger.error(`${config.logPrefix} 处理回调时出错:`, error);
|
| 212 |
-
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 213 |
-
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
|
| 214 |
-
|
| 215 |
-
if (server.listening) {
|
| 216 |
-
server.close(() => {
|
| 217 |
-
activeServers.delete(provider);
|
| 218 |
-
});
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
});
|
| 222 |
-
|
| 223 |
-
server.on('error', (err) => {
|
| 224 |
-
clearPollTimer();
|
| 225 |
-
if (err.code === 'EADDRINUSE') {
|
| 226 |
-
logger.error(`${config.logPrefix} 端口 ${port} 已被占用`);
|
| 227 |
-
reject(new Error(`端口 ${port} 已被占用`));
|
| 228 |
-
} else {
|
| 229 |
-
logger.error(`${config.logPrefix} 服务器错误:`, err);
|
| 230 |
-
reject(err);
|
| 231 |
-
}
|
| 232 |
-
});
|
| 233 |
-
|
| 234 |
-
const host = '0.0.0.0';
|
| 235 |
-
server.listen(port, host, () => {
|
| 236 |
-
logger.info(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
|
| 237 |
-
|
| 238 |
-
// 启动轮询日志
|
| 239 |
-
pollTimer = setInterval(() => {
|
| 240 |
-
pollCount++;
|
| 241 |
-
if (pollCount <= maxPollCount) {
|
| 242 |
-
logger.info(`${config.logPrefix} Waiting for callback... (${pollCount}/${maxPollCount})`);
|
| 243 |
-
} else {
|
| 244 |
-
clearPollTimer();
|
| 245 |
-
logger.warn(`${config.logPrefix} Polling timeout, closing server...`);
|
| 246 |
-
if (server.listening) {
|
| 247 |
-
server.close(() => {
|
| 248 |
-
activeServers.delete(provider);
|
| 249 |
-
});
|
| 250 |
-
}
|
| 251 |
-
}
|
| 252 |
-
}, pollInterval);
|
| 253 |
-
|
| 254 |
-
activeServers.set(provider, { server, port, pollTimer });
|
| 255 |
-
resolve(server);
|
| 256 |
-
});
|
| 257 |
-
});
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
/**
|
| 261 |
-
* 处理 Google OAuth 授权(通用函数)
|
| 262 |
-
* @param {string} providerKey - 提供商键名
|
| 263 |
-
* @param {Object} currentConfig - 当前配置对象
|
| 264 |
-
* @param {Object} options - 额外选项
|
| 265 |
-
* @returns {Promise<Object>} 返回授权URL和相关信息
|
| 266 |
-
*/
|
| 267 |
-
async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
|
| 268 |
-
const config = OAUTH_PROVIDERS[providerKey];
|
| 269 |
-
if (!config) {
|
| 270 |
-
throw new Error(`未知的提供商: ${providerKey}`);
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
const port = parseInt(options.port) || config.port;
|
| 274 |
-
const host = 'localhost';
|
| 275 |
-
const redirectUri = `http://${host}:${port}`;
|
| 276 |
-
|
| 277 |
-
// 获取代理配置
|
| 278 |
-
const proxyConfig = getGoogleAuthProxyConfig(currentConfig, providerKey);
|
| 279 |
-
|
| 280 |
-
// 构建 OAuth2Client 选项
|
| 281 |
-
const oauth2Options = {
|
| 282 |
-
clientId: config.clientId,
|
| 283 |
-
clientSecret: config.clientSecret,
|
| 284 |
-
};
|
| 285 |
-
|
| 286 |
-
if (proxyConfig) {
|
| 287 |
-
oauth2Options.transporterOptions = proxyConfig;
|
| 288 |
-
logger.info(`${config.logPrefix} Using proxy for OAuth token exchange`);
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
const authClient = new OAuth2Client(oauth2Options);
|
| 292 |
-
authClient.redirectUri = redirectUri;
|
| 293 |
-
|
| 294 |
-
const authUrl = authClient.generateAuthUrl({
|
| 295 |
-
access_type: 'offline',
|
| 296 |
-
prompt: 'select_account',
|
| 297 |
-
scope: config.scope
|
| 298 |
-
});
|
| 299 |
-
|
| 300 |
-
// 启动回调服务器
|
| 301 |
-
const credPath = path.join(os.homedir(), config.credentialsDir, config.credentialsFile);
|
| 302 |
-
|
| 303 |
-
try {
|
| 304 |
-
await createOAuthCallbackServer(config, redirectUri, authClient, credPath, providerKey, options);
|
| 305 |
-
} catch (error) {
|
| 306 |
-
throw new Error(`启动回调服务器失败: ${error.message}`);
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
return {
|
| 310 |
-
authUrl,
|
| 311 |
-
authInfo: {
|
| 312 |
-
provider: providerKey,
|
| 313 |
-
redirectUri: redirectUri,
|
| 314 |
-
port: port,
|
| 315 |
-
...options
|
| 316 |
-
}
|
| 317 |
-
};
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
/**
|
| 321 |
-
* 处理 Gemini CLI OAuth 授权
|
| 322 |
-
* @param {Object} currentConfig - 当前配置对象
|
| 323 |
-
* @param {Object} options - 额外选项
|
| 324 |
-
* @returns {Promise<Object>} 返回授权URL和相关信息
|
| 325 |
-
*/
|
| 326 |
-
export async function handleGeminiCliOAuth(currentConfig, options = {}) {
|
| 327 |
-
return handleGoogleOAuth('gemini-cli-oauth', currentConfig, options);
|
| 328 |
-
}
|
| 329 |
-
|
| 330 |
-
/**
|
| 331 |
-
* 处理 Gemini Antigravity OAuth 授权
|
| 332 |
-
* @param {Object} currentConfig - 当前配置对象
|
| 333 |
-
* @param {Object} options - 额外选项
|
| 334 |
-
* @returns {Promise<Object>} 返回授权URL和相关信息
|
| 335 |
-
*/
|
| 336 |
-
export async function handleGeminiAntigravityOAuth(currentConfig, options = {}) {
|
| 337 |
-
return handleGoogleOAuth('gemini-antigravity', currentConfig, options);
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
/**
|
| 341 |
-
* 检查 Gemini 凭据是否已存在(基于 refresh_token)
|
| 342 |
-
* @param {string} providerType - 提供商类型
|
| 343 |
-
* @param {string} refreshToken - 要检查的 refreshToken
|
| 344 |
-
* @returns {Promise<{isDuplicate: boolean, existingPath?: string}>} 检查结果
|
| 345 |
-
*/
|
| 346 |
-
export async function checkGeminiCredentialsDuplicate(providerType, refreshToken) {
|
| 347 |
-
const config = OAUTH_PROVIDERS[providerType];
|
| 348 |
-
if (!config) return { isDuplicate: false };
|
| 349 |
-
|
| 350 |
-
const providerDir = config.credentialsDir.replace('.', '');
|
| 351 |
-
const targetDir = path.join(process.cwd(), 'configs', providerDir);
|
| 352 |
-
|
| 353 |
-
try {
|
| 354 |
-
if (!fs.existsSync(targetDir)) {
|
| 355 |
-
return { isDuplicate: false };
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
const files = await fs.promises.readdir(targetDir);
|
| 359 |
-
for (const file of files) {
|
| 360 |
-
if (file.endsWith('.json')) {
|
| 361 |
-
try {
|
| 362 |
-
const fullPath = path.join(targetDir, file);
|
| 363 |
-
const content = await fs.promises.readFile(fullPath, 'utf8');
|
| 364 |
-
const credentials = JSON.parse(content);
|
| 365 |
-
|
| 366 |
-
if (credentials.refresh_token === refreshToken) {
|
| 367 |
-
const relativePath = path.relative(process.cwd(), fullPath);
|
| 368 |
-
return {
|
| 369 |
-
isDuplicate: true,
|
| 370 |
-
existingPath: relativePath
|
| 371 |
-
};
|
| 372 |
-
}
|
| 373 |
-
} catch (e) {
|
| 374 |
-
// 忽略解析错误
|
| 375 |
-
}
|
| 376 |
-
}
|
| 377 |
-
}
|
| 378 |
-
return { isDuplicate: false };
|
| 379 |
-
} catch (error) {
|
| 380 |
-
logger.warn(`[Gemini Auth] Error checking duplicates for ${providerType}:`, error.message);
|
| 381 |
-
return { isDuplicate: false };
|
| 382 |
-
}
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
/**
|
| 386 |
-
* 批量导入 Gemini Token 并生成凭据文件(流式版本,支持实时进度回调)
|
| 387 |
-
* @param {string} providerType - 提供商类型 ('gemini-cli-oauth' 或 'gemini-antigravity')
|
| 388 |
-
* @param {Object[]} tokens - Token 对象数组
|
| 389 |
-
* @param {Function} onProgress - 进度回调函数
|
| 390 |
-
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
|
| 391 |
-
* @returns {Promise<Object>} 批量处理结果
|
| 392 |
-
*/
|
| 393 |
-
export async function batchImportGeminiTokensStream(providerType, tokens, onProgress = null, skipDuplicateCheck = false) {
|
| 394 |
-
const config = OAUTH_PROVIDERS[providerType];
|
| 395 |
-
if (!config) {
|
| 396 |
-
throw new Error(`未知的提供商: ${providerType}`);
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
const results = {
|
| 400 |
-
total: tokens.length,
|
| 401 |
-
success: 0,
|
| 402 |
-
failed: 0,
|
| 403 |
-
details: []
|
| 404 |
-
};
|
| 405 |
-
|
| 406 |
-
for (let i = 0; i < tokens.length; i++) {
|
| 407 |
-
const token = tokens[i];
|
| 408 |
-
const progressData = {
|
| 409 |
-
index: i + 1,
|
| 410 |
-
total: tokens.length,
|
| 411 |
-
current: null
|
| 412 |
-
};
|
| 413 |
-
|
| 414 |
-
try {
|
| 415 |
-
// 验证 token 是否包含必需字段 (通常是 access_token 和 refresh_token)
|
| 416 |
-
if (!token.access_token || !token.refresh_token) {
|
| 417 |
-
throw new Error('Token 缺少必需字段 (access_token 或 refresh_token)');
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
// 检查重复
|
| 421 |
-
if (!skipDuplicateCheck) {
|
| 422 |
-
const duplicateCheck = await checkGeminiCredentialsDuplicate(providerType, token.refresh_token);
|
| 423 |
-
if (duplicateCheck.isDuplicate) {
|
| 424 |
-
progressData.current = {
|
| 425 |
-
index: i + 1,
|
| 426 |
-
success: false,
|
| 427 |
-
error: 'duplicate',
|
| 428 |
-
existingPath: duplicateCheck.existingPath
|
| 429 |
-
};
|
| 430 |
-
results.failed++;
|
| 431 |
-
results.details.push(progressData.current);
|
| 432 |
-
if (onProgress) {
|
| 433 |
-
onProgress({
|
| 434 |
-
...progressData,
|
| 435 |
-
successCount: results.success,
|
| 436 |
-
failedCount: results.failed
|
| 437 |
-
});
|
| 438 |
-
}
|
| 439 |
-
continue;
|
| 440 |
-
}
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
// 生成文件路径
|
| 444 |
-
const timestamp = Date.now();
|
| 445 |
-
const providerDir = config.credentialsDir.replace('.', ''); // 去掉开头的点
|
| 446 |
-
const targetDir = path.join(process.cwd(), 'configs', providerDir);
|
| 447 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 448 |
-
|
| 449 |
-
const filename = `${timestamp}_${i}_oauth_creds.json`;
|
| 450 |
-
const credPath = path.join(targetDir, filename);
|
| 451 |
-
|
| 452 |
-
await fs.promises.writeFile(credPath, JSON.stringify(token, null, 2));
|
| 453 |
-
|
| 454 |
-
const relativePath = path.relative(process.cwd(), credPath);
|
| 455 |
-
|
| 456 |
-
logger.info(`${config.logPrefix} Token ${i + 1} 已导入并保存: ${relativePath}`);
|
| 457 |
-
|
| 458 |
-
progressData.current = {
|
| 459 |
-
index: i + 1,
|
| 460 |
-
success: true,
|
| 461 |
-
path: relativePath
|
| 462 |
-
};
|
| 463 |
-
results.success++;
|
| 464 |
-
|
| 465 |
-
// 自动关联新生成的凭据到 Pools
|
| 466 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 467 |
-
onlyCurrentCred: true,
|
| 468 |
-
credPath: relativePath
|
| 469 |
-
});
|
| 470 |
-
|
| 471 |
-
} catch (error) {
|
| 472 |
-
logger.error(`${config.logPrefix} Token ${i + 1} 导入失败:`, error.message);
|
| 473 |
-
|
| 474 |
-
progressData.current = {
|
| 475 |
-
index: i + 1,
|
| 476 |
-
success: false,
|
| 477 |
-
error: error.message
|
| 478 |
-
};
|
| 479 |
-
results.failed++;
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
results.details.push(progressData.current);
|
| 483 |
-
|
| 484 |
-
// 发送进度更新
|
| 485 |
-
if (onProgress) {
|
| 486 |
-
onProgress({
|
| 487 |
-
...progressData,
|
| 488 |
-
successCount: results.success,
|
| 489 |
-
failedCount: results.failed
|
| 490 |
-
});
|
| 491 |
-
}
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
// 如果有成功的,广播事件
|
| 495 |
-
if (results.success > 0) {
|
| 496 |
-
broadcastEvent('oauth_batch_success', {
|
| 497 |
-
provider: providerType,
|
| 498 |
-
count: results.success,
|
| 499 |
-
timestamp: new Date().toISOString()
|
| 500 |
-
});
|
| 501 |
-
}
|
| 502 |
-
|
| 503 |
-
return results;
|
| 504 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/iflow-oauth.js
DELETED
|
@@ -1,539 +0,0 @@
|
|
| 1 |
-
import http from 'http';
|
| 2 |
-
import logger from '../utils/logger.js';
|
| 3 |
-
import fs from 'fs';
|
| 4 |
-
import path from 'path';
|
| 5 |
-
import os from 'os';
|
| 6 |
-
import crypto from 'crypto';
|
| 7 |
-
import { broadcastEvent } from '../services/ui-manager.js';
|
| 8 |
-
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
| 9 |
-
import { CONFIG } from '../core/config-manager.js';
|
| 10 |
-
import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
|
| 11 |
-
|
| 12 |
-
/**
|
| 13 |
-
* iFlow OAuth 配置
|
| 14 |
-
*/
|
| 15 |
-
const IFLOW_OAUTH_CONFIG = {
|
| 16 |
-
// OAuth 端点
|
| 17 |
-
tokenEndpoint: 'https://iflow.cn/oauth/token',
|
| 18 |
-
authorizeEndpoint: 'https://iflow.cn/oauth',
|
| 19 |
-
userInfoEndpoint: 'https://iflow.cn/api/oauth/getUserInfo',
|
| 20 |
-
successRedirectURL: 'https://iflow.cn/oauth/success',
|
| 21 |
-
|
| 22 |
-
// 客户端凭据
|
| 23 |
-
clientId: '10009311001',
|
| 24 |
-
clientSecret: '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW',
|
| 25 |
-
|
| 26 |
-
// 本地回调端口
|
| 27 |
-
callbackPort: 8087,
|
| 28 |
-
|
| 29 |
-
// 凭据存储
|
| 30 |
-
credentialsDir: '.iflow',
|
| 31 |
-
credentialsFile: 'oauth_creds.json',
|
| 32 |
-
|
| 33 |
-
// 日志前缀
|
| 34 |
-
logPrefix: '[iFlow Auth]'
|
| 35 |
-
};
|
| 36 |
-
|
| 37 |
-
/**
|
| 38 |
-
* 活动的 iFlow 回调服务器管理
|
| 39 |
-
*/
|
| 40 |
-
const activeIFlowServers = new Map();
|
| 41 |
-
|
| 42 |
-
/**
|
| 43 |
-
* 创建带代理支持的 fetch 请求
|
| 44 |
-
* 使用 axios 替代原生 fetch,以正确支持代理配置
|
| 45 |
-
* @param {string} url - 请求 URL
|
| 46 |
-
* @param {Object} options - fetch 选项(兼容 fetch API 格式)
|
| 47 |
-
* @param {string} providerType - 提供商类型,用于获取代理配置
|
| 48 |
-
* @returns {Promise<Object>} 返回类似 fetch Response 的对象
|
| 49 |
-
*/
|
| 50 |
-
async function fetchWithProxy(url, options = {}, providerType) {
|
| 51 |
-
const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
|
| 52 |
-
|
| 53 |
-
// 构建 axios 配置
|
| 54 |
-
const axiosConfig = {
|
| 55 |
-
url,
|
| 56 |
-
method: options.method || 'GET',
|
| 57 |
-
headers: options.headers || {},
|
| 58 |
-
timeout: 30000, // 30 秒超时
|
| 59 |
-
};
|
| 60 |
-
|
| 61 |
-
// 处理请求体
|
| 62 |
-
if (options.body) {
|
| 63 |
-
axiosConfig.data = options.body;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
// 配置代理
|
| 67 |
-
if (proxyConfig) {
|
| 68 |
-
axiosConfig.httpAgent = proxyConfig.httpAgent;
|
| 69 |
-
axiosConfig.httpsAgent = proxyConfig.httpsAgent;
|
| 70 |
-
axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent
|
| 71 |
-
logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
try {
|
| 75 |
-
const axios = (await import('axios')).default;
|
| 76 |
-
const response = await axios(axiosConfig);
|
| 77 |
-
|
| 78 |
-
// 返回类似 fetch Response 的对象
|
| 79 |
-
return {
|
| 80 |
-
ok: response.status >= 200 && response.status < 300,
|
| 81 |
-
status: response.status,
|
| 82 |
-
statusText: response.statusText,
|
| 83 |
-
headers: response.headers,
|
| 84 |
-
json: async () => response.data,
|
| 85 |
-
text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
|
| 86 |
-
};
|
| 87 |
-
} catch (error) {
|
| 88 |
-
// 处理 axios 错误,转换为类似 fetch 的响应格式
|
| 89 |
-
if (error.response) {
|
| 90 |
-
// 服务器返回了错误状态码
|
| 91 |
-
return {
|
| 92 |
-
ok: false,
|
| 93 |
-
status: error.response.status,
|
| 94 |
-
statusText: error.response.statusText,
|
| 95 |
-
headers: error.response.headers,
|
| 96 |
-
json: async () => error.response.data,
|
| 97 |
-
text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data),
|
| 98 |
-
};
|
| 99 |
-
}
|
| 100 |
-
// 网络错误或其他错误
|
| 101 |
-
throw error;
|
| 102 |
-
}
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
/**
|
| 106 |
-
* 生成 HTML 响应页面
|
| 107 |
-
* @param {boolean} isSuccess - 是否成功
|
| 108 |
-
* @param {string} message - 显示消息
|
| 109 |
-
* @returns {string} HTML 内容
|
| 110 |
-
*/
|
| 111 |
-
function generateResponsePage(isSuccess, message) {
|
| 112 |
-
const title = isSuccess ? '授权成功!' : '授权失败';
|
| 113 |
-
|
| 114 |
-
return `<!DOCTYPE html>
|
| 115 |
-
<html lang="zh-CN">
|
| 116 |
-
<head>
|
| 117 |
-
<meta charset="utf-8">
|
| 118 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 119 |
-
<title>${title}</title>
|
| 120 |
-
</head>
|
| 121 |
-
<body>
|
| 122 |
-
<div class="container">
|
| 123 |
-
<h1>${title}</h1>
|
| 124 |
-
<p>${message}</p>
|
| 125 |
-
</div>
|
| 126 |
-
</body>
|
| 127 |
-
</html>`;
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
/**
|
| 131 |
-
* 生成 iFlow 授权链接
|
| 132 |
-
* @param {string} state - 状态参数
|
| 133 |
-
* @param {number} port - 回调端口
|
| 134 |
-
* @returns {Object} 包含 authUrl 和 redirectUri
|
| 135 |
-
*/
|
| 136 |
-
function generateIFlowAuthorizationURL(state, port) {
|
| 137 |
-
const redirectUri = `http://localhost:${port}/oauth2callback`;
|
| 138 |
-
const params = new URLSearchParams({
|
| 139 |
-
loginMethod: 'phone',
|
| 140 |
-
type: 'phone',
|
| 141 |
-
redirect: redirectUri,
|
| 142 |
-
state: state,
|
| 143 |
-
client_id: IFLOW_OAUTH_CONFIG.clientId
|
| 144 |
-
});
|
| 145 |
-
const authUrl = `${IFLOW_OAUTH_CONFIG.authorizeEndpoint}?${params.toString()}`;
|
| 146 |
-
return { authUrl, redirectUri };
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
/**
|
| 150 |
-
* 交换授权码获取 iFlow 令牌
|
| 151 |
-
* @param {string} code - 授权码
|
| 152 |
-
* @param {string} redirectUri - 重定向 URI
|
| 153 |
-
* @returns {Promise<Object>} 令牌数据
|
| 154 |
-
*/
|
| 155 |
-
async function exchangeIFlowCodeForTokens(code, redirectUri) {
|
| 156 |
-
const form = new URLSearchParams({
|
| 157 |
-
grant_type: 'authorization_code',
|
| 158 |
-
code: code,
|
| 159 |
-
redirect_uri: redirectUri,
|
| 160 |
-
client_id: IFLOW_OAUTH_CONFIG.clientId,
|
| 161 |
-
client_secret: IFLOW_OAUTH_CONFIG.clientSecret
|
| 162 |
-
});
|
| 163 |
-
|
| 164 |
-
// 生成 Basic Auth 头
|
| 165 |
-
const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64');
|
| 166 |
-
|
| 167 |
-
const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, {
|
| 168 |
-
method: 'POST',
|
| 169 |
-
headers: {
|
| 170 |
-
'Content-Type': 'application/x-www-form-urlencoded',
|
| 171 |
-
'Accept': 'application/json',
|
| 172 |
-
'Authorization': `Basic ${basicAuth}`
|
| 173 |
-
},
|
| 174 |
-
body: form.toString()
|
| 175 |
-
}, 'openai-iflow');
|
| 176 |
-
|
| 177 |
-
if (!response.ok) {
|
| 178 |
-
const errorText = await response.text();
|
| 179 |
-
throw new Error(`iFlow token exchange failed: ${response.status} ${errorText}`);
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
const tokenData = await response.json();
|
| 183 |
-
|
| 184 |
-
if (!tokenData.access_token) {
|
| 185 |
-
throw new Error('iFlow token: missing access token in response');
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
return {
|
| 189 |
-
accessToken: tokenData.access_token,
|
| 190 |
-
refreshToken: tokenData.refresh_token,
|
| 191 |
-
tokenType: tokenData.token_type,
|
| 192 |
-
scope: tokenData.scope,
|
| 193 |
-
expiresIn: tokenData.expires_in,
|
| 194 |
-
expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString()
|
| 195 |
-
};
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
/**
|
| 199 |
-
* 获取 iFlow 用户信息(包含 API Key)
|
| 200 |
-
* @param {string} accessToken - 访问令牌
|
| 201 |
-
* @returns {Promise<Object>} 用户信息
|
| 202 |
-
*/
|
| 203 |
-
async function fetchIFlowUserInfo(accessToken) {
|
| 204 |
-
if (!accessToken || accessToken.trim() === '') {
|
| 205 |
-
throw new Error('iFlow api key: access token is empty');
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
const endpoint = `${IFLOW_OAUTH_CONFIG.userInfoEndpoint}?accessToken=${encodeURIComponent(accessToken)}`;
|
| 209 |
-
|
| 210 |
-
const response = await fetchWithProxy(endpoint, {
|
| 211 |
-
method: 'GET',
|
| 212 |
-
headers: {
|
| 213 |
-
'Accept': 'application/json'
|
| 214 |
-
}
|
| 215 |
-
}, 'openai-iflow');
|
| 216 |
-
|
| 217 |
-
if (!response.ok) {
|
| 218 |
-
const errorText = await response.text();
|
| 219 |
-
throw new Error(`iFlow user info failed: ${response.status} ${errorText}`);
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
const result = await response.json();
|
| 223 |
-
|
| 224 |
-
if (!result.success) {
|
| 225 |
-
throw new Error('iFlow api key: request not successful');
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
if (!result.data || !result.data.apiKey) {
|
| 229 |
-
throw new Error('iFlow api key: missing api key in response');
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
// 获取邮箱或手机号作为账户标识
|
| 233 |
-
let email = (result.data.email || '').trim();
|
| 234 |
-
if (!email) {
|
| 235 |
-
email = (result.data.phone || '').trim();
|
| 236 |
-
}
|
| 237 |
-
if (!email) {
|
| 238 |
-
throw new Error('iFlow token: missing account email/phone in user info');
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
return {
|
| 242 |
-
apiKey: result.data.apiKey,
|
| 243 |
-
email: email,
|
| 244 |
-
phone: result.data.phone || ''
|
| 245 |
-
};
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
/**
|
| 249 |
-
* 关闭 iFlow 服务器
|
| 250 |
-
* @param {string} provider - 提供商标识
|
| 251 |
-
* @param {number} port - 端口号(可选)
|
| 252 |
-
*/
|
| 253 |
-
async function closeIFlowServer(provider, port = null) {
|
| 254 |
-
const existing = activeIFlowServers.get(provider);
|
| 255 |
-
if (existing) {
|
| 256 |
-
try {
|
| 257 |
-
const closePromise = new Promise((resolve, reject) => {
|
| 258 |
-
existing.server.close((err) => {
|
| 259 |
-
if (err) reject(err);
|
| 260 |
-
else resolve();
|
| 261 |
-
});
|
| 262 |
-
});
|
| 263 |
-
|
| 264 |
-
const timeoutPromise = new Promise((_, reject) => {
|
| 265 |
-
setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
|
| 266 |
-
});
|
| 267 |
-
|
| 268 |
-
await Promise.race([closePromise, timeoutPromise]);
|
| 269 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
|
| 270 |
-
} catch (error) {
|
| 271 |
-
logger.warn(`${IFLOW_OAUTH_CONFIG.logPrefix} 关闭提供商 ${provider} 服务器失败或超时: ${error.message}`);
|
| 272 |
-
} finally {
|
| 273 |
-
activeIFlowServers.delete(provider);
|
| 274 |
-
}
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
if (port) {
|
| 278 |
-
for (const [p, info] of activeIFlowServers.entries()) {
|
| 279 |
-
if (info.port === port) {
|
| 280 |
-
await closeIFlowServer(p);
|
| 281 |
-
}
|
| 282 |
-
}
|
| 283 |
-
}
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
/**
|
| 287 |
-
* 创建 iFlow OAuth 回调服务器
|
| 288 |
-
* @param {number} port - 端口号
|
| 289 |
-
* @param {string} redirectUri - 重定向 URI
|
| 290 |
-
* @param {string} expectedState - 预期的 state 参数
|
| 291 |
-
* @param {Object} options - 额外选项
|
| 292 |
-
* @returns {Promise<http.Server>} HTTP 服务器实例
|
| 293 |
-
*/
|
| 294 |
-
function createIFlowCallbackServer(port, redirectUri, expectedState, options = {}) {
|
| 295 |
-
return new Promise((resolve, reject) => {
|
| 296 |
-
const server = http.createServer(async (req, res) => {
|
| 297 |
-
try {
|
| 298 |
-
const url = new URL(req.url, `http://localhost:${port}`);
|
| 299 |
-
|
| 300 |
-
if (url.pathname === '/oauth2callback') {
|
| 301 |
-
const code = url.searchParams.get('code');
|
| 302 |
-
const state = url.searchParams.get('state');
|
| 303 |
-
const errorParam = url.searchParams.get('error');
|
| 304 |
-
|
| 305 |
-
if (errorParam) {
|
| 306 |
-
logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 授权失败: ${errorParam}`);
|
| 307 |
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 308 |
-
res.end(generateResponsePage(false, `授权失败: ${errorParam}`));
|
| 309 |
-
server.close(() => {
|
| 310 |
-
activeIFlowServers.delete('openai-iflow');
|
| 311 |
-
});
|
| 312 |
-
return;
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
if (state !== expectedState) {
|
| 316 |
-
logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} State 验证失败`);
|
| 317 |
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 318 |
-
res.end(generateResponsePage(false, 'State 验证失败'));
|
| 319 |
-
server.close(() => {
|
| 320 |
-
activeIFlowServers.delete('openai-iflow');
|
| 321 |
-
});
|
| 322 |
-
return;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
if (!code) {
|
| 326 |
-
logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 缺少授权码`);
|
| 327 |
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 328 |
-
res.end(generateResponsePage(false, '缺少授权码'));
|
| 329 |
-
server.close(() => {
|
| 330 |
-
activeIFlowServers.delete('openai-iflow');
|
| 331 |
-
});
|
| 332 |
-
return;
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`);
|
| 336 |
-
|
| 337 |
-
try {
|
| 338 |
-
// 1. 交换授权码获取令牌
|
| 339 |
-
const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri);
|
| 340 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`);
|
| 341 |
-
|
| 342 |
-
// 2. 获取用户信息(包含 API Key)
|
| 343 |
-
const userInfo = await fetchIFlowUserInfo(tokenData.accessToken);
|
| 344 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`);
|
| 345 |
-
|
| 346 |
-
// 3. 组合完整的凭据数据
|
| 347 |
-
const credentialsData = {
|
| 348 |
-
access_token: tokenData.accessToken,
|
| 349 |
-
refresh_token: tokenData.refreshToken,
|
| 350 |
-
expiry_date: new Date(tokenData.expiresAt).getTime(),
|
| 351 |
-
token_type: tokenData.tokenType,
|
| 352 |
-
scope: tokenData.scope,
|
| 353 |
-
apiKey: userInfo.apiKey
|
| 354 |
-
};
|
| 355 |
-
|
| 356 |
-
// 4. 保存凭据
|
| 357 |
-
let credPath = path.join(os.homedir(), IFLOW_OAUTH_CONFIG.credentialsDir, IFLOW_OAUTH_CONFIG.credentialsFile);
|
| 358 |
-
|
| 359 |
-
if (options.saveToConfigs) {
|
| 360 |
-
const providerDir = options.providerDir || 'iflow';
|
| 361 |
-
const targetDir = path.join(process.cwd(), 'configs', providerDir);
|
| 362 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 363 |
-
const timestamp = Date.now();
|
| 364 |
-
const filename = `${timestamp}_oauth_creds.json`;
|
| 365 |
-
credPath = path.join(targetDir, filename);
|
| 366 |
-
}
|
| 367 |
-
|
| 368 |
-
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
| 369 |
-
await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
|
| 370 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 凭据已保存: ${credPath}`);
|
| 371 |
-
|
| 372 |
-
const relativePath = path.relative(process.cwd(), credPath);
|
| 373 |
-
|
| 374 |
-
// 5. 广播授权成功事件
|
| 375 |
-
broadcastEvent('oauth_success', {
|
| 376 |
-
provider: 'openai-iflow',
|
| 377 |
-
credPath: credPath,
|
| 378 |
-
relativePath: relativePath,
|
| 379 |
-
email: userInfo.email,
|
| 380 |
-
timestamp: new Date().toISOString()
|
| 381 |
-
});
|
| 382 |
-
|
| 383 |
-
// 6. 自动关联新生成的凭据到 Pools
|
| 384 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 385 |
-
onlyCurrentCred: true,
|
| 386 |
-
credPath: relativePath
|
| 387 |
-
});
|
| 388 |
-
|
| 389 |
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 390 |
-
res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可���关闭此页面`));
|
| 391 |
-
|
| 392 |
-
} catch (tokenError) {
|
| 393 |
-
logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌处理失败:`, tokenError);
|
| 394 |
-
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 395 |
-
res.end(generateResponsePage(false, `令牌处理失败: ${tokenError.message}`));
|
| 396 |
-
} finally {
|
| 397 |
-
server.close(() => {
|
| 398 |
-
activeIFlowServers.delete('openai-iflow');
|
| 399 |
-
});
|
| 400 |
-
}
|
| 401 |
-
} else {
|
| 402 |
-
// 忽略其他请求
|
| 403 |
-
res.writeHead(204);
|
| 404 |
-
res.end();
|
| 405 |
-
}
|
| 406 |
-
} catch (error) {
|
| 407 |
-
logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error);
|
| 408 |
-
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 409 |
-
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
|
| 410 |
-
|
| 411 |
-
if (server.listening) {
|
| 412 |
-
server.close(() => {
|
| 413 |
-
activeIFlowServers.delete('openai-iflow');
|
| 414 |
-
});
|
| 415 |
-
}
|
| 416 |
-
}
|
| 417 |
-
});
|
| 418 |
-
|
| 419 |
-
server.on('error', (err) => {
|
| 420 |
-
if (err.code === 'EADDRINUSE') {
|
| 421 |
-
logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`);
|
| 422 |
-
reject(new Error(`端口 ${port} 已被占用`));
|
| 423 |
-
} else {
|
| 424 |
-
logger.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err);
|
| 425 |
-
reject(err);
|
| 426 |
-
}
|
| 427 |
-
});
|
| 428 |
-
|
| 429 |
-
const host = '0.0.0.0';
|
| 430 |
-
server.listen(port, host, () => {
|
| 431 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
|
| 432 |
-
resolve(server);
|
| 433 |
-
});
|
| 434 |
-
|
| 435 |
-
// 10 分钟超时自动关闭
|
| 436 |
-
setTimeout(() => {
|
| 437 |
-
if (server.listening) {
|
| 438 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`);
|
| 439 |
-
server.close(() => {
|
| 440 |
-
activeIFlowServers.delete('openai-iflow');
|
| 441 |
-
});
|
| 442 |
-
}
|
| 443 |
-
}, 10 * 60 * 1000);
|
| 444 |
-
});
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
/**
|
| 448 |
-
* 处理 iFlow OAuth 授权
|
| 449 |
-
* @param {Object} currentConfig - 当前配置对象
|
| 450 |
-
* @param {Object} options - 额外选项
|
| 451 |
-
* - port: 自定义端口号
|
| 452 |
-
* - saveToConfigs: 是否保存到 configs 目录
|
| 453 |
-
* - providerDir: 提供商目录名
|
| 454 |
-
* @returns {Promise<Object>} 返回授权URL和相关信息
|
| 455 |
-
*/
|
| 456 |
-
export async function handleIFlowOAuth(currentConfig, options = {}) {
|
| 457 |
-
const port = parseInt(options.port) || IFLOW_OAUTH_CONFIG.callbackPort;
|
| 458 |
-
const providerKey = 'openai-iflow';
|
| 459 |
-
|
| 460 |
-
// 生成 state 参数
|
| 461 |
-
const state = crypto.randomBytes(16).toString('base64url');
|
| 462 |
-
|
| 463 |
-
// 生成授权链接
|
| 464 |
-
const { authUrl, redirectUri } = generateIFlowAuthorizationURL(state, port);
|
| 465 |
-
|
| 466 |
-
logger.info(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`);
|
| 467 |
-
|
| 468 |
-
// 关闭之前可能存在的服务器
|
| 469 |
-
await closeIFlowServer(providerKey, port);
|
| 470 |
-
|
| 471 |
-
// 启动回调服务器
|
| 472 |
-
try {
|
| 473 |
-
const server = await createIFlowCallbackServer(port, redirectUri, state, options);
|
| 474 |
-
activeIFlowServers.set(providerKey, { server, port });
|
| 475 |
-
} catch (error) {
|
| 476 |
-
throw new Error(`启动 iFlow 回调服务器失败: ${error.message}`);
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
return {
|
| 480 |
-
authUrl,
|
| 481 |
-
authInfo: {
|
| 482 |
-
provider: 'openai-iflow',
|
| 483 |
-
redirectUri: redirectUri,
|
| 484 |
-
callbackPort: port,
|
| 485 |
-
state: state,
|
| 486 |
-
...options
|
| 487 |
-
}
|
| 488 |
-
};
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
/**
|
| 492 |
-
* 使用 refresh_token 刷新 iFlow 令牌
|
| 493 |
-
* @param {string} refreshToken - 刷新令牌
|
| 494 |
-
* @returns {Promise<Object>} 新的令牌数据
|
| 495 |
-
*/
|
| 496 |
-
export async function refreshIFlowTokens(refreshToken) {
|
| 497 |
-
const form = new URLSearchParams({
|
| 498 |
-
grant_type: 'refresh_token',
|
| 499 |
-
refresh_token: refreshToken,
|
| 500 |
-
client_id: IFLOW_OAUTH_CONFIG.clientId,
|
| 501 |
-
client_secret: IFLOW_OAUTH_CONFIG.clientSecret
|
| 502 |
-
});
|
| 503 |
-
|
| 504 |
-
// 生成 Basic Auth 头
|
| 505 |
-
const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64');
|
| 506 |
-
|
| 507 |
-
const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, {
|
| 508 |
-
method: 'POST',
|
| 509 |
-
headers: {
|
| 510 |
-
'Content-Type': 'application/x-www-form-urlencoded',
|
| 511 |
-
'Accept': 'application/json',
|
| 512 |
-
'Authorization': `Basic ${basicAuth}`
|
| 513 |
-
},
|
| 514 |
-
body: form.toString()
|
| 515 |
-
}, 'openai-iflow');
|
| 516 |
-
|
| 517 |
-
if (!response.ok) {
|
| 518 |
-
const errorText = await response.text();
|
| 519 |
-
throw new Error(`iFlow token refresh failed: ${response.status} ${errorText}`);
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
const tokenData = await response.json();
|
| 523 |
-
|
| 524 |
-
if (!tokenData.access_token) {
|
| 525 |
-
throw new Error('iFlow token refresh: missing access token in response');
|
| 526 |
-
}
|
| 527 |
-
|
| 528 |
-
// 获取用户信息以更新 API Key
|
| 529 |
-
const userInfo = await fetchIFlowUserInfo(tokenData.access_token);
|
| 530 |
-
|
| 531 |
-
return {
|
| 532 |
-
access_token: tokenData.access_token,
|
| 533 |
-
refresh_token: tokenData.refresh_token,
|
| 534 |
-
expiry_date: Date.now() + tokenData.expires_in * 1000,
|
| 535 |
-
token_type: tokenData.token_type,
|
| 536 |
-
scope: tokenData.scope,
|
| 537 |
-
apiKey: userInfo.apiKey
|
| 538 |
-
};
|
| 539 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/index.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
// Codex OAuth
|
| 2 |
-
export {
|
| 3 |
-
refreshCodexTokensWithRetry,
|
| 4 |
-
handleCodexOAuth,
|
| 5 |
-
handleCodexOAuthCallback,
|
| 6 |
-
batchImportCodexTokensStream
|
| 7 |
-
} from './codex-oauth.js';
|
| 8 |
-
|
| 9 |
-
// Gemini OAuth
|
| 10 |
-
export {
|
| 11 |
-
handleGeminiCliOAuth,
|
| 12 |
-
handleGeminiAntigravityOAuth,
|
| 13 |
-
batchImportGeminiTokensStream,
|
| 14 |
-
checkGeminiCredentialsDuplicate
|
| 15 |
-
} from './gemini-oauth.js';
|
| 16 |
-
|
| 17 |
-
// Qwen OAuth
|
| 18 |
-
export {
|
| 19 |
-
handleQwenOAuth
|
| 20 |
-
} from './qwen-oauth.js';
|
| 21 |
-
|
| 22 |
-
// Kiro OAuth
|
| 23 |
-
export {
|
| 24 |
-
handleKiroOAuth,
|
| 25 |
-
checkKiroCredentialsDuplicate,
|
| 26 |
-
batchImportKiroRefreshTokens,
|
| 27 |
-
batchImportKiroRefreshTokensStream,
|
| 28 |
-
importAwsCredentials
|
| 29 |
-
} from './kiro-oauth.js';
|
| 30 |
-
|
| 31 |
-
// iFlow OAuth
|
| 32 |
-
export {
|
| 33 |
-
handleIFlowOAuth,
|
| 34 |
-
refreshIFlowTokens
|
| 35 |
-
} from './iflow-oauth.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/kiro-oauth.js
DELETED
|
@@ -1,1149 +0,0 @@
|
|
| 1 |
-
import http from 'http';
|
| 2 |
-
import logger from '../utils/logger.js';
|
| 3 |
-
import fs from 'fs';
|
| 4 |
-
import path from 'path';
|
| 5 |
-
import crypto from 'crypto';
|
| 6 |
-
import os from 'os';
|
| 7 |
-
import { broadcastEvent } from '../services/ui-manager.js';
|
| 8 |
-
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
| 9 |
-
import { CONFIG } from '../core/config-manager.js';
|
| 10 |
-
import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
|
| 11 |
-
|
| 12 |
-
/**
|
| 13 |
-
* Kiro OAuth 配置(支持多种认证方式)
|
| 14 |
-
*/
|
| 15 |
-
const KIRO_OAUTH_CONFIG = {
|
| 16 |
-
// Kiro Auth Service 端点 (用于 Social Auth)
|
| 17 |
-
authServiceEndpoint: 'https://prod.us-east-1.auth.desktop.kiro.dev',
|
| 18 |
-
|
| 19 |
-
// AWS SSO OIDC 端点 (用于 Builder ID)
|
| 20 |
-
ssoOIDCEndpoint: 'https://oidc.{{region}}.amazonaws.com',
|
| 21 |
-
|
| 22 |
-
// AWS Builder ID 起始 URL
|
| 23 |
-
builderIDStartURL: 'https://view.awsapps.com/start',
|
| 24 |
-
|
| 25 |
-
// 本地回调端口范围(用于 Social Auth HTTP 回调)
|
| 26 |
-
callbackPortStart: 19876,
|
| 27 |
-
callbackPortEnd: 19880,
|
| 28 |
-
|
| 29 |
-
// 超时配置
|
| 30 |
-
authTimeout: 10 * 60 * 1000, // 10 分钟
|
| 31 |
-
pollInterval: 5000, // 5 秒
|
| 32 |
-
|
| 33 |
-
// CodeWhisperer Scopes
|
| 34 |
-
scopes: [
|
| 35 |
-
'codewhisperer:completions',
|
| 36 |
-
'codewhisperer:analysis',
|
| 37 |
-
'codewhisperer:conversations',
|
| 38 |
-
// 'codewhisperer:transformations',
|
| 39 |
-
// 'codewhisperer:taskassist'
|
| 40 |
-
],
|
| 41 |
-
|
| 42 |
-
// 凭据存储(符合现有规范)
|
| 43 |
-
credentialsDir: '.kiro',
|
| 44 |
-
credentialsFile: 'oauth_creds.json',
|
| 45 |
-
|
| 46 |
-
// 日志前缀
|
| 47 |
-
logPrefix: '[Kiro Auth]'
|
| 48 |
-
};
|
| 49 |
-
|
| 50 |
-
/**
|
| 51 |
-
* 活动的 Kiro 回调服务器管理
|
| 52 |
-
*/
|
| 53 |
-
const activeKiroServers = new Map();
|
| 54 |
-
|
| 55 |
-
/**
|
| 56 |
-
* 活动的 Kiro 轮询任务管理(用于 Builder ID Device Code)
|
| 57 |
-
*/
|
| 58 |
-
const activeKiroPollingTasks = new Map();
|
| 59 |
-
|
| 60 |
-
/**
|
| 61 |
-
* 创建带代理支持的 fetch 请求
|
| 62 |
-
* 使用 axios 替代原生 fetch,以正确支持代理配置
|
| 63 |
-
* @param {string} url - 请求 URL
|
| 64 |
-
* @param {Object} options - fetch 选项(兼容 fetch API 格式)
|
| 65 |
-
* @param {string} providerType - 提供商类型,用于获取代理配置
|
| 66 |
-
* @returns {Promise<Object>} 返回类似 fetch Response 的对象
|
| 67 |
-
*/
|
| 68 |
-
async function fetchWithProxy(url, options = {}, providerType) {
|
| 69 |
-
const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
|
| 70 |
-
|
| 71 |
-
// 构建 axios 配置
|
| 72 |
-
const axiosConfig = {
|
| 73 |
-
url,
|
| 74 |
-
method: options.method || 'GET',
|
| 75 |
-
headers: options.headers || {},
|
| 76 |
-
timeout: 30000, // 30 秒超时
|
| 77 |
-
};
|
| 78 |
-
|
| 79 |
-
// 处理请求体
|
| 80 |
-
if (options.body) {
|
| 81 |
-
axiosConfig.data = options.body;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
// 配置代理
|
| 85 |
-
if (proxyConfig) {
|
| 86 |
-
axiosConfig.httpAgent = proxyConfig.httpAgent;
|
| 87 |
-
axiosConfig.httpsAgent = proxyConfig.httpsAgent;
|
| 88 |
-
axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent
|
| 89 |
-
logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`);
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
try {
|
| 93 |
-
const axios = (await import('axios')).default;
|
| 94 |
-
const response = await axios(axiosConfig);
|
| 95 |
-
|
| 96 |
-
// 返回类似 fetch Response 的对象
|
| 97 |
-
return {
|
| 98 |
-
ok: response.status >= 200 && response.status < 300,
|
| 99 |
-
status: response.status,
|
| 100 |
-
statusText: response.statusText,
|
| 101 |
-
headers: response.headers,
|
| 102 |
-
json: async () => response.data,
|
| 103 |
-
text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
|
| 104 |
-
};
|
| 105 |
-
} catch (error) {
|
| 106 |
-
// 处理 axios 错误,转换为类似 fetch 的响应格式
|
| 107 |
-
if (error.response) {
|
| 108 |
-
// 服务器返回了错误状态码
|
| 109 |
-
return {
|
| 110 |
-
ok: false,
|
| 111 |
-
status: error.response.status,
|
| 112 |
-
statusText: error.response.statusText,
|
| 113 |
-
headers: error.response.headers,
|
| 114 |
-
json: async () => error.response.data,
|
| 115 |
-
text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data),
|
| 116 |
-
};
|
| 117 |
-
}
|
| 118 |
-
// 网络错误或其他错误
|
| 119 |
-
throw error;
|
| 120 |
-
}
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
/**
|
| 124 |
-
* 生成 HTML 响应页面
|
| 125 |
-
* @param {boolean} isSuccess - 是否成功
|
| 126 |
-
* @param {string} message - 显示消息
|
| 127 |
-
* @returns {string} HTML 内容
|
| 128 |
-
*/
|
| 129 |
-
function generateResponsePage(isSuccess, message) {
|
| 130 |
-
const title = isSuccess ? '授权成功!' : '授权失败';
|
| 131 |
-
|
| 132 |
-
return `<!DOCTYPE html>
|
| 133 |
-
<html lang="zh-CN">
|
| 134 |
-
<head>
|
| 135 |
-
<meta charset="utf-8">
|
| 136 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 137 |
-
<title>${title}</title>
|
| 138 |
-
</head>
|
| 139 |
-
<body>
|
| 140 |
-
<div class="container">
|
| 141 |
-
<h1>${title}</h1>
|
| 142 |
-
<p>${message}</p>
|
| 143 |
-
</div>
|
| 144 |
-
</body>
|
| 145 |
-
</html>`;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
/**
|
| 149 |
-
* 生成 PKCE 代码验证器
|
| 150 |
-
* @returns {string} Base64URL 编码的随机字符串
|
| 151 |
-
*/
|
| 152 |
-
function generateCodeVerifier() {
|
| 153 |
-
return crypto.randomBytes(32).toString('base64url');
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
/**
|
| 157 |
-
* 生成 PKCE 代码挑战
|
| 158 |
-
* @param {string} codeVerifier - 代码验证器
|
| 159 |
-
* @returns {string} Base64URL 编码的 SHA256 哈希
|
| 160 |
-
*/
|
| 161 |
-
function generateCodeChallenge(codeVerifier) {
|
| 162 |
-
const hash = crypto.createHash('sha256');
|
| 163 |
-
hash.update(codeVerifier);
|
| 164 |
-
return hash.digest('base64url');
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
/**
|
| 168 |
-
* 处理 Kiro OAuth 授权(统一入口)
|
| 169 |
-
* @param {Object} currentConfig - 当前配置对象
|
| 170 |
-
* @param {Object} options - 额外选项
|
| 171 |
-
* - method: 'google' | 'github' | 'builder-id'
|
| 172 |
-
* - saveToConfigs: boolean
|
| 173 |
-
* @returns {Promise<Object>} 返回授权URL和相关信息
|
| 174 |
-
*/
|
| 175 |
-
export async function handleKiroOAuth(currentConfig, options = {}) {
|
| 176 |
-
const method = options.method || options.authMethod || 'google'; // 默认使用 Google,同时支持 authMethod 参数
|
| 177 |
-
|
| 178 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`);
|
| 179 |
-
|
| 180 |
-
switch (method) {
|
| 181 |
-
case 'google':
|
| 182 |
-
return handleKiroSocialAuth('Google', currentConfig, options);
|
| 183 |
-
case 'github':
|
| 184 |
-
return handleKiroSocialAuth('Github', currentConfig, options);
|
| 185 |
-
case 'builder-id':
|
| 186 |
-
return handleKiroBuilderIDDeviceCode(currentConfig, options);
|
| 187 |
-
default:
|
| 188 |
-
throw new Error(`不支持的认证方式: ${method}`);
|
| 189 |
-
}
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
/**
|
| 193 |
-
* Kiro Social Auth (Google/GitHub) - 使用 HTTP localhost 回调
|
| 194 |
-
*/
|
| 195 |
-
async function handleKiroSocialAuth(provider, currentConfig, options = {}) {
|
| 196 |
-
// 生成 PKCE 参数
|
| 197 |
-
const codeVerifier = generateCodeVerifier();
|
| 198 |
-
const codeChallenge = generateCodeChallenge(codeVerifier);
|
| 199 |
-
const state = crypto.randomBytes(16).toString('base64url');
|
| 200 |
-
|
| 201 |
-
// 启动本地回调服务器并获取端口
|
| 202 |
-
let handlerPort;
|
| 203 |
-
const providerKey = 'claude-kiro-oauth';
|
| 204 |
-
if (options.port) {
|
| 205 |
-
const port = parseInt(options.port);
|
| 206 |
-
await closeKiroServer(providerKey, port);
|
| 207 |
-
const server = await createKiroHttpCallbackServer(port, codeVerifier, state, options);
|
| 208 |
-
activeKiroServers.set(providerKey, { server, port });
|
| 209 |
-
handlerPort = port;
|
| 210 |
-
} else {
|
| 211 |
-
handlerPort = await startKiroCallbackServer(codeVerifier, state, options);
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
// 使用 HTTP localhost 作为 redirect_uri
|
| 215 |
-
const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`;
|
| 216 |
-
|
| 217 |
-
// 构建授权 URL
|
| 218 |
-
const authUrl = `${KIRO_OAUTH_CONFIG.authServiceEndpoint}/login?` +
|
| 219 |
-
`idp=${provider}&` +
|
| 220 |
-
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
|
| 221 |
-
`code_challenge=${codeChallenge}&` +
|
| 222 |
-
`code_challenge_method=S256&` +
|
| 223 |
-
`state=${state}&` +
|
| 224 |
-
`prompt=select_account`;
|
| 225 |
-
|
| 226 |
-
return {
|
| 227 |
-
authUrl,
|
| 228 |
-
authInfo: {
|
| 229 |
-
provider: 'claude-kiro-oauth',
|
| 230 |
-
authMethod: 'social',
|
| 231 |
-
socialProvider: provider,
|
| 232 |
-
port: handlerPort,
|
| 233 |
-
redirectUri: redirectUri,
|
| 234 |
-
state: state,
|
| 235 |
-
...options
|
| 236 |
-
}
|
| 237 |
-
};
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
/**
|
| 241 |
-
* Kiro Builder ID - Device Code Flow(类似 Qwen OAuth 模式)
|
| 242 |
-
*/
|
| 243 |
-
async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
|
| 244 |
-
// 停止之前的轮询任务
|
| 245 |
-
for (const [existingTaskId] of activeKiroPollingTasks.entries()) {
|
| 246 |
-
if (existingTaskId.startsWith('kiro-')) {
|
| 247 |
-
stopKiroPollingTask(existingTaskId);
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
// 获取 Builder ID Start URL(优先使用前端传入的值,否则使用默认值)
|
| 252 |
-
const builderIDStartURL = options.builderIDStartURL || KIRO_OAUTH_CONFIG.builderIDStartURL;
|
| 253 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Using Builder ID Start URL: ${builderIDStartURL}`);
|
| 254 |
-
|
| 255 |
-
// 1. 注册 OIDC 客户端
|
| 256 |
-
const region = options.region || 'us-east-1';
|
| 257 |
-
const ssoOIDCEndpoint = KIRO_OAUTH_CONFIG.ssoOIDCEndpoint.replace('{{region}}', region);
|
| 258 |
-
|
| 259 |
-
const regResponse = await fetchWithProxy(`${ssoOIDCEndpoint}/client/register`, {
|
| 260 |
-
method: 'POST',
|
| 261 |
-
headers: {
|
| 262 |
-
'Content-Type': 'application/json',
|
| 263 |
-
'User-Agent': 'KiroIDE'
|
| 264 |
-
},
|
| 265 |
-
body: JSON.stringify({
|
| 266 |
-
clientName: 'Kiro IDE',
|
| 267 |
-
clientType: 'public',
|
| 268 |
-
scopes: KIRO_OAUTH_CONFIG.scopes,
|
| 269 |
-
// grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
|
| 270 |
-
})
|
| 271 |
-
}, 'claude-kiro-oauth');
|
| 272 |
-
|
| 273 |
-
if (!regResponse.ok) {
|
| 274 |
-
throw new Error(`Kiro OAuth 客户端注册失败: ${regResponse.status}`);
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
const regData = await regResponse.json();
|
| 278 |
-
|
| 279 |
-
// 2. 启动设备授权
|
| 280 |
-
const authResponse = await fetchWithProxy(`${ssoOIDCEndpoint}/device_authorization`, {
|
| 281 |
-
method: 'POST',
|
| 282 |
-
headers: {
|
| 283 |
-
'Content-Type': 'application/json'
|
| 284 |
-
},
|
| 285 |
-
body: JSON.stringify({
|
| 286 |
-
clientId: regData.clientId,
|
| 287 |
-
clientSecret: regData.clientSecret,
|
| 288 |
-
startUrl: builderIDStartURL
|
| 289 |
-
})
|
| 290 |
-
}, 'claude-kiro-oauth');
|
| 291 |
-
|
| 292 |
-
if (!authResponse.ok) {
|
| 293 |
-
throw new Error(`Kiro OAuth 设备授权失败: ${authResponse.status}`);
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
const deviceAuth = await authResponse.json();
|
| 297 |
-
|
| 298 |
-
// 3. 启动后台轮询(类似 Qwen OAuth 的模式)
|
| 299 |
-
const taskId = `kiro-${deviceAuth.deviceCode.substring(0, 8)}-${Date.now()}`;
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
// 异步轮询
|
| 303 |
-
pollKiroBuilderIDToken(
|
| 304 |
-
regData.clientId,
|
| 305 |
-
regData.clientSecret,
|
| 306 |
-
deviceAuth.deviceCode,
|
| 307 |
-
5,
|
| 308 |
-
300,
|
| 309 |
-
taskId,
|
| 310 |
-
{ ...options, region }
|
| 311 |
-
).catch(error => {
|
| 312 |
-
logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
|
| 313 |
-
broadcastEvent('oauth_error', {
|
| 314 |
-
provider: 'claude-kiro-oauth',
|
| 315 |
-
error: error.message,
|
| 316 |
-
timestamp: new Date().toISOString()
|
| 317 |
-
});
|
| 318 |
-
});
|
| 319 |
-
|
| 320 |
-
return {
|
| 321 |
-
authUrl: deviceAuth.verificationUriComplete,
|
| 322 |
-
authInfo: {
|
| 323 |
-
provider: 'claude-kiro-oauth',
|
| 324 |
-
authMethod: 'builder-id',
|
| 325 |
-
deviceCode: deviceAuth.deviceCode,
|
| 326 |
-
userCode: deviceAuth.userCode,
|
| 327 |
-
verificationUri: deviceAuth.verificationUri,
|
| 328 |
-
verificationUriComplete: deviceAuth.verificationUriComplete,
|
| 329 |
-
expiresIn: deviceAuth.expiresIn,
|
| 330 |
-
interval: deviceAuth.interval,
|
| 331 |
-
...options
|
| 332 |
-
}
|
| 333 |
-
};
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
/**
|
| 337 |
-
* 轮询获取 Kiro Builder ID Token
|
| 338 |
-
*/
|
| 339 |
-
async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interval, expiresIn, taskId, options = {}) {
|
| 340 |
-
let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile);
|
| 341 |
-
const maxAttempts = Math.floor(expiresIn / interval);
|
| 342 |
-
let attempts = 0;
|
| 343 |
-
|
| 344 |
-
const taskControl = { shouldStop: false };
|
| 345 |
-
activeKiroPollingTasks.set(taskId, taskControl);
|
| 346 |
-
|
| 347 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`);
|
| 348 |
-
|
| 349 |
-
const poll = async () => {
|
| 350 |
-
if (taskControl.shouldStop) {
|
| 351 |
-
throw new Error('轮询任务已被取消');
|
| 352 |
-
}
|
| 353 |
-
|
| 354 |
-
if (attempts >= maxAttempts) {
|
| 355 |
-
activeKiroPollingTasks.delete(taskId);
|
| 356 |
-
throw new Error('授权超时');
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
attempts++;
|
| 360 |
-
|
| 361 |
-
try {
|
| 362 |
-
const region = options.region || 'us-east-1';
|
| 363 |
-
const ssoOIDCEndpoint = KIRO_OAUTH_CONFIG.ssoOIDCEndpoint.replace('{{region}}', region);
|
| 364 |
-
const response = await fetchWithProxy(`${ssoOIDCEndpoint}/token`, {
|
| 365 |
-
method: 'POST',
|
| 366 |
-
headers: {
|
| 367 |
-
'Content-Type': 'application/json',
|
| 368 |
-
'User-Agent': 'KiroIDE'
|
| 369 |
-
},
|
| 370 |
-
body: JSON.stringify({
|
| 371 |
-
clientId,
|
| 372 |
-
clientSecret,
|
| 373 |
-
deviceCode,
|
| 374 |
-
grantType: 'urn:ietf:params:oauth:grant-type:device_code'
|
| 375 |
-
})
|
| 376 |
-
}, 'claude-kiro-oauth');
|
| 377 |
-
|
| 378 |
-
const data = await response.json();
|
| 379 |
-
|
| 380 |
-
if (response.ok && data.accessToken) {
|
| 381 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
|
| 382 |
-
|
| 383 |
-
// 保存令牌(符合现有规范)
|
| 384 |
-
if (options.saveToConfigs) {
|
| 385 |
-
const timestamp = Date.now();
|
| 386 |
-
const folderName = `${timestamp}_kiro-auth-token`;
|
| 387 |
-
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
|
| 388 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 389 |
-
credPath = path.join(targetDir, `${folderName}.json`);
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
const tokenData = {
|
| 393 |
-
accessToken: data.accessToken,
|
| 394 |
-
refreshToken: data.refreshToken,
|
| 395 |
-
expiresAt: new Date(Date.now() + data.expiresIn * 1000).toISOString(),
|
| 396 |
-
authMethod: 'builder-id',
|
| 397 |
-
clientId,
|
| 398 |
-
clientSecret,
|
| 399 |
-
idcRegion: options.region || 'us-east-1'
|
| 400 |
-
};
|
| 401 |
-
|
| 402 |
-
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
| 403 |
-
await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
|
| 404 |
-
|
| 405 |
-
activeKiroPollingTasks.delete(taskId);
|
| 406 |
-
|
| 407 |
-
// 广播成功事件(符合现有规范)
|
| 408 |
-
broadcastEvent('oauth_success', {
|
| 409 |
-
provider: 'claude-kiro-oauth',
|
| 410 |
-
credPath,
|
| 411 |
-
relativePath: path.relative(process.cwd(), credPath),
|
| 412 |
-
timestamp: new Date().toISOString()
|
| 413 |
-
});
|
| 414 |
-
|
| 415 |
-
// 自动关联新生成的凭据到 Pools
|
| 416 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 417 |
-
onlyCurrentCred: true,
|
| 418 |
-
credPath: path.relative(process.cwd(), credPath)
|
| 419 |
-
});
|
| 420 |
-
|
| 421 |
-
return tokenData;
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
// 检查错误类型
|
| 425 |
-
if (data.error === 'authorization_pending') {
|
| 426 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (${attempts}/${maxAttempts})`);
|
| 427 |
-
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
| 428 |
-
return poll();
|
| 429 |
-
} else if (data.error === 'slow_down') {
|
| 430 |
-
await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000));
|
| 431 |
-
return poll();
|
| 432 |
-
} else {
|
| 433 |
-
activeKiroPollingTasks.delete(taskId);
|
| 434 |
-
throw new Error(`授权失败: ${data.error || '未知错误'}`);
|
| 435 |
-
}
|
| 436 |
-
} catch (error) {
|
| 437 |
-
if (error.message.includes('授权') || error.message.includes('取消')) {
|
| 438 |
-
throw error;
|
| 439 |
-
}
|
| 440 |
-
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
| 441 |
-
return poll();
|
| 442 |
-
}
|
| 443 |
-
};
|
| 444 |
-
|
| 445 |
-
return poll();
|
| 446 |
-
}
|
| 447 |
-
|
| 448 |
-
/**
|
| 449 |
-
* 停止 Kiro 轮询任务
|
| 450 |
-
*/
|
| 451 |
-
function stopKiroPollingTask(taskId) {
|
| 452 |
-
const task = activeKiroPollingTasks.get(taskId);
|
| 453 |
-
if (task) {
|
| 454 |
-
task.shouldStop = true;
|
| 455 |
-
activeKiroPollingTasks.delete(taskId);
|
| 456 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`);
|
| 457 |
-
}
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
/**
|
| 461 |
-
* 启动 Kiro 回调服务器(用于 Social Auth HTTP 回调)
|
| 462 |
-
*/
|
| 463 |
-
async function startKiroCallbackServer(codeVerifier, expectedState, options = {}) {
|
| 464 |
-
const portStart = KIRO_OAUTH_CONFIG.callbackPortStart;
|
| 465 |
-
const portEnd = KIRO_OAUTH_CONFIG.callbackPortEnd;
|
| 466 |
-
|
| 467 |
-
for (let port = portStart; port <= portEnd; port++) {
|
| 468 |
-
// 关闭已存在的服务器
|
| 469 |
-
await closeKiroServer(port);
|
| 470 |
-
|
| 471 |
-
try {
|
| 472 |
-
const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options);
|
| 473 |
-
activeKiroServers.set('claude-kiro-oauth', { server, port });
|
| 474 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`);
|
| 475 |
-
return port;
|
| 476 |
-
} catch (err) {
|
| 477 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`);
|
| 478 |
-
}
|
| 479 |
-
}
|
| 480 |
-
|
| 481 |
-
throw new Error('所有端口都被占用');
|
| 482 |
-
}
|
| 483 |
-
|
| 484 |
-
/**
|
| 485 |
-
* 关闭 Kiro 服务器
|
| 486 |
-
*/
|
| 487 |
-
async function closeKiroServer(provider, port = null) {
|
| 488 |
-
const existing = activeKiroServers.get(provider);
|
| 489 |
-
if (existing) {
|
| 490 |
-
try {
|
| 491 |
-
const closePromise = new Promise((resolve, reject) => {
|
| 492 |
-
existing.server.close((err) => {
|
| 493 |
-
if (err) reject(err);
|
| 494 |
-
else resolve();
|
| 495 |
-
});
|
| 496 |
-
});
|
| 497 |
-
|
| 498 |
-
const timeoutPromise = new Promise((_, reject) => {
|
| 499 |
-
setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000);
|
| 500 |
-
});
|
| 501 |
-
|
| 502 |
-
await Promise.race([closePromise, timeoutPromise]);
|
| 503 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
|
| 504 |
-
} catch (error) {
|
| 505 |
-
logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} 关闭提供商 ${provider} 服务器失败或超时: ${error.message}`);
|
| 506 |
-
} finally {
|
| 507 |
-
activeKiroServers.delete(provider);
|
| 508 |
-
}
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
if (port) {
|
| 512 |
-
for (const [p, info] of activeKiroServers.entries()) {
|
| 513 |
-
if (info.port === port) {
|
| 514 |
-
await closeKiroServer(p);
|
| 515 |
-
}
|
| 516 |
-
}
|
| 517 |
-
}
|
| 518 |
-
}
|
| 519 |
-
|
| 520 |
-
/**
|
| 521 |
-
* 创建 Kiro HTTP 回调服务器
|
| 522 |
-
*/
|
| 523 |
-
function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options = {}) {
|
| 524 |
-
const redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
|
| 525 |
-
|
| 526 |
-
return new Promise((resolve, reject) => {
|
| 527 |
-
const server = http.createServer(async (req, res) => {
|
| 528 |
-
try {
|
| 529 |
-
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
| 530 |
-
|
| 531 |
-
if (url.pathname === '/oauth/callback') {
|
| 532 |
-
const code = url.searchParams.get('code');
|
| 533 |
-
const state = url.searchParams.get('state');
|
| 534 |
-
const errorParam = url.searchParams.get('error');
|
| 535 |
-
|
| 536 |
-
if (errorParam) {
|
| 537 |
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 538 |
-
res.end(generateResponsePage(false, `授权失败: ${errorParam}`));
|
| 539 |
-
return;
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
if (state !== expectedState) {
|
| 543 |
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 544 |
-
res.end(generateResponsePage(false, 'State 验证失败'));
|
| 545 |
-
return;
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
// 交换 Code 获取 Token(使用动态的 redirect_uri)
|
| 549 |
-
const tokenResponse = await fetchWithProxy(`${KIRO_OAUTH_CONFIG.authServiceEndpoint}/oauth/token`, {
|
| 550 |
-
method: 'POST',
|
| 551 |
-
headers: {
|
| 552 |
-
'Content-Type': 'application/json',
|
| 553 |
-
'User-Agent': 'AIClient-2-API/1.0.0'
|
| 554 |
-
},
|
| 555 |
-
body: JSON.stringify({
|
| 556 |
-
code,
|
| 557 |
-
code_verifier: codeVerifier,
|
| 558 |
-
redirect_uri: redirectUri
|
| 559 |
-
})
|
| 560 |
-
}, 'claude-kiro-oauth');
|
| 561 |
-
|
| 562 |
-
if (!tokenResponse.ok) {
|
| 563 |
-
const errorText = await tokenResponse.text();
|
| 564 |
-
logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token exchange failed:`, errorText);
|
| 565 |
-
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 566 |
-
res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`));
|
| 567 |
-
return;
|
| 568 |
-
}
|
| 569 |
-
|
| 570 |
-
const tokenData = await tokenResponse.json();
|
| 571 |
-
|
| 572 |
-
// 保存令牌
|
| 573 |
-
let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile);
|
| 574 |
-
|
| 575 |
-
if (options.saveToConfigs) {
|
| 576 |
-
const timestamp = Date.now();
|
| 577 |
-
const folderName = `${timestamp}_kiro-auth-token`;
|
| 578 |
-
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
|
| 579 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 580 |
-
credPath = path.join(targetDir, `${folderName}.json`);
|
| 581 |
-
}
|
| 582 |
-
|
| 583 |
-
const saveData = {
|
| 584 |
-
accessToken: tokenData.accessToken,
|
| 585 |
-
refreshToken: tokenData.refreshToken,
|
| 586 |
-
profileArn: tokenData.profileArn,
|
| 587 |
-
expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(),
|
| 588 |
-
authMethod: 'social',
|
| 589 |
-
region: 'us-east-1'
|
| 590 |
-
};
|
| 591 |
-
|
| 592 |
-
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
| 593 |
-
await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2));
|
| 594 |
-
|
| 595 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 令牌已保存: ${credPath}`);
|
| 596 |
-
|
| 597 |
-
// 广播成功事件
|
| 598 |
-
broadcastEvent('oauth_success', {
|
| 599 |
-
provider: 'claude-kiro-oauth',
|
| 600 |
-
credPath,
|
| 601 |
-
relativePath: path.relative(process.cwd(), credPath),
|
| 602 |
-
timestamp: new Date().toISOString()
|
| 603 |
-
});
|
| 604 |
-
|
| 605 |
-
// 自动关联新生成的凭据到 Pools
|
| 606 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 607 |
-
onlyCurrentCred: true,
|
| 608 |
-
credPath: path.relative(process.cwd(), credPath)
|
| 609 |
-
});
|
| 610 |
-
|
| 611 |
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 612 |
-
res.end(generateResponsePage(true, '授权成功!您可以关闭此页面'));
|
| 613 |
-
|
| 614 |
-
// 关闭服务器
|
| 615 |
-
server.close(() => {
|
| 616 |
-
activeKiroServers.delete('claude-kiro-oauth');
|
| 617 |
-
});
|
| 618 |
-
|
| 619 |
-
} else {
|
| 620 |
-
res.writeHead(204);
|
| 621 |
-
res.end();
|
| 622 |
-
}
|
| 623 |
-
} catch (error) {
|
| 624 |
-
logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error);
|
| 625 |
-
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
| 626 |
-
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
|
| 627 |
-
}
|
| 628 |
-
});
|
| 629 |
-
|
| 630 |
-
server.on('error', reject);
|
| 631 |
-
server.listen(port, '127.0.0.1', () => resolve(server));
|
| 632 |
-
|
| 633 |
-
// 超时自动关闭
|
| 634 |
-
setTimeout(() => {
|
| 635 |
-
if (server.listening) {
|
| 636 |
-
server.close(() => {
|
| 637 |
-
activeKiroServers.delete('claude-kiro-oauth');
|
| 638 |
-
});
|
| 639 |
-
}
|
| 640 |
-
}, KIRO_OAUTH_CONFIG.authTimeout);
|
| 641 |
-
});
|
| 642 |
-
}
|
| 643 |
-
|
| 644 |
-
/**
|
| 645 |
-
* Kiro Token 刷新常量
|
| 646 |
-
*/
|
| 647 |
-
const KIRO_REFRESH_CONSTANTS = {
|
| 648 |
-
REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
|
| 649 |
-
REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token',
|
| 650 |
-
CONTENT_TYPE_JSON: 'application/json',
|
| 651 |
-
AUTH_METHOD_SOCIAL: 'social',
|
| 652 |
-
DEFAULT_PROVIDER: 'Google',
|
| 653 |
-
REQUEST_TIMEOUT: 30000,
|
| 654 |
-
DEFAULT_REGION: 'us-east-1',
|
| 655 |
-
IDC_REGION: 'us-east-1' // 用于 REFRESH_IDC_URL 的区域配置
|
| 656 |
-
};
|
| 657 |
-
|
| 658 |
-
/**
|
| 659 |
-
* 通过 refreshToken 获取 accessToken
|
| 660 |
-
* @param {string} refreshToken - Kiro 的 refresh token
|
| 661 |
-
* @param {string} region - AWS 区域 (默认: us-east-1)
|
| 662 |
-
* @returns {Promise<Object>} 包含 accessToken 等信息的对象
|
| 663 |
-
*/
|
| 664 |
-
async function refreshKiroToken(refreshToken, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) {
|
| 665 |
-
const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_URL.replace('{{region}}', region);
|
| 666 |
-
|
| 667 |
-
const controller = new AbortController();
|
| 668 |
-
const timeoutId = setTimeout(() => controller.abort(), KIRO_REFRESH_CONSTANTS.REQUEST_TIMEOUT);
|
| 669 |
-
|
| 670 |
-
try {
|
| 671 |
-
const response = await fetchWithProxy(refreshUrl, {
|
| 672 |
-
method: 'POST',
|
| 673 |
-
headers: {
|
| 674 |
-
'Content-Type': KIRO_REFRESH_CONSTANTS.CONTENT_TYPE_JSON
|
| 675 |
-
},
|
| 676 |
-
body: JSON.stringify({ refreshToken }),
|
| 677 |
-
signal: controller.signal
|
| 678 |
-
}, 'claude-kiro-oauth');
|
| 679 |
-
|
| 680 |
-
clearTimeout(timeoutId);
|
| 681 |
-
|
| 682 |
-
if (!response.ok) {
|
| 683 |
-
const errorText = await response.text();
|
| 684 |
-
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
| 685 |
-
}
|
| 686 |
-
|
| 687 |
-
const data = await response.json();
|
| 688 |
-
|
| 689 |
-
if (!data.accessToken) {
|
| 690 |
-
throw new Error('Invalid refresh response: Missing accessToken');
|
| 691 |
-
}
|
| 692 |
-
|
| 693 |
-
const expiresIn = data.expiresIn || 3600;
|
| 694 |
-
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
| 695 |
-
|
| 696 |
-
return {
|
| 697 |
-
accessToken: data.accessToken,
|
| 698 |
-
refreshToken: data.refreshToken || refreshToken,
|
| 699 |
-
profileArn: data.profileArn || '',
|
| 700 |
-
expiresAt: expiresAt,
|
| 701 |
-
authMethod: KIRO_REFRESH_CONSTANTS.AUTH_METHOD_SOCIAL,
|
| 702 |
-
provider: KIRO_REFRESH_CONSTANTS.DEFAULT_PROVIDER,
|
| 703 |
-
region: region
|
| 704 |
-
};
|
| 705 |
-
} catch (error) {
|
| 706 |
-
clearTimeout(timeoutId);
|
| 707 |
-
if (error.name === 'AbortError') {
|
| 708 |
-
throw new Error('Request timeout');
|
| 709 |
-
}
|
| 710 |
-
throw error;
|
| 711 |
-
}
|
| 712 |
-
}
|
| 713 |
-
|
| 714 |
-
/**
|
| 715 |
-
* 检查 Kiro 凭据是否已存在(基于 refreshToken + provider 组合)
|
| 716 |
-
* @param {string} refreshToken - 要检查的 refreshToken
|
| 717 |
-
* @param {string} provider - 提供商名称 (默认: 'claude-kiro-oauth')
|
| 718 |
-
* @returns {Promise<{isDuplicate: boolean, existingPath?: string}>} 检查结果
|
| 719 |
-
*/
|
| 720 |
-
export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'claude-kiro-oauth') {
|
| 721 |
-
const kiroDir = path.join(process.cwd(), 'configs', 'kiro');
|
| 722 |
-
|
| 723 |
-
try {
|
| 724 |
-
// 检查 configs/kiro 目录是否存在
|
| 725 |
-
if (!fs.existsSync(kiroDir)) {
|
| 726 |
-
return { isDuplicate: false };
|
| 727 |
-
}
|
| 728 |
-
|
| 729 |
-
// 递归扫描所有 JSON 文件
|
| 730 |
-
const scanDirectory = async (dirPath) => {
|
| 731 |
-
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
| 732 |
-
|
| 733 |
-
for (const entry of entries) {
|
| 734 |
-
const fullPath = path.join(dirPath, entry.name);
|
| 735 |
-
|
| 736 |
-
if (entry.isDirectory()) {
|
| 737 |
-
const result = await scanDirectory(fullPath);
|
| 738 |
-
if (result.isDuplicate) {
|
| 739 |
-
return result;
|
| 740 |
-
}
|
| 741 |
-
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
| 742 |
-
try {
|
| 743 |
-
const content = await fs.promises.readFile(fullPath, 'utf8');
|
| 744 |
-
const credentials = JSON.parse(content);
|
| 745 |
-
|
| 746 |
-
// 检查 refreshToken 是否匹配
|
| 747 |
-
if (credentials.refreshToken && credentials.refreshToken === refreshToken) {
|
| 748 |
-
const relativePath = path.relative(process.cwd(), fullPath);
|
| 749 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Found duplicate refreshToken in: ${relativePath}`);
|
| 750 |
-
return {
|
| 751 |
-
isDuplicate: true,
|
| 752 |
-
existingPath: relativePath
|
| 753 |
-
};
|
| 754 |
-
}
|
| 755 |
-
} catch (parseError) {
|
| 756 |
-
// 忽略解析错误的文件
|
| 757 |
-
}
|
| 758 |
-
}
|
| 759 |
-
}
|
| 760 |
-
|
| 761 |
-
return { isDuplicate: false };
|
| 762 |
-
};
|
| 763 |
-
|
| 764 |
-
return await scanDirectory(kiroDir);
|
| 765 |
-
|
| 766 |
-
} catch (error) {
|
| 767 |
-
logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message);
|
| 768 |
-
return { isDuplicate: false };
|
| 769 |
-
}
|
| 770 |
-
}
|
| 771 |
-
|
| 772 |
-
/**
|
| 773 |
-
* 批量导入 Kiro refreshToken 并生成凭据文件
|
| 774 |
-
* @param {string[]} refreshTokens - refreshToken 数组
|
| 775 |
-
* @param {string} region - AWS 区域 (默认: us-east-1)
|
| 776 |
-
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
|
| 777 |
-
* @returns {Promise<Object>} 批量处理结果
|
| 778 |
-
*/
|
| 779 |
-
export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, skipDuplicateCheck = false) {
|
| 780 |
-
const results = {
|
| 781 |
-
total: refreshTokens.length,
|
| 782 |
-
success: 0,
|
| 783 |
-
failed: 0,
|
| 784 |
-
details: []
|
| 785 |
-
};
|
| 786 |
-
|
| 787 |
-
for (let i = 0; i < refreshTokens.length; i++) {
|
| 788 |
-
const refreshToken = refreshTokens[i].trim();
|
| 789 |
-
|
| 790 |
-
if (!refreshToken) {
|
| 791 |
-
results.details.push({
|
| 792 |
-
index: i + 1,
|
| 793 |
-
success: false,
|
| 794 |
-
error: 'Empty token'
|
| 795 |
-
});
|
| 796 |
-
results.failed++;
|
| 797 |
-
continue;
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
// 检查重复
|
| 801 |
-
if (!skipDuplicateCheck) {
|
| 802 |
-
const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken);
|
| 803 |
-
if (duplicateCheck.isDuplicate) {
|
| 804 |
-
results.details.push({
|
| 805 |
-
index: i + 1,
|
| 806 |
-
success: false,
|
| 807 |
-
error: 'duplicate',
|
| 808 |
-
existingPath: duplicateCheck.existingPath
|
| 809 |
-
});
|
| 810 |
-
results.failed++;
|
| 811 |
-
continue;
|
| 812 |
-
}
|
| 813 |
-
}
|
| 814 |
-
|
| 815 |
-
try {
|
| 816 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`);
|
| 817 |
-
|
| 818 |
-
const tokenData = await refreshKiroToken(refreshToken, region);
|
| 819 |
-
|
| 820 |
-
// 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
|
| 821 |
-
const timestamp = Date.now();
|
| 822 |
-
const folderName = `${timestamp}_kiro-auth-token`;
|
| 823 |
-
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
|
| 824 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 825 |
-
|
| 826 |
-
const credPath = path.join(targetDir, `${folderName}.json`);
|
| 827 |
-
await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
|
| 828 |
-
|
| 829 |
-
const relativePath = path.relative(process.cwd(), credPath);
|
| 830 |
-
|
| 831 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`);
|
| 832 |
-
|
| 833 |
-
results.details.push({
|
| 834 |
-
index: i + 1,
|
| 835 |
-
success: true,
|
| 836 |
-
path: relativePath,
|
| 837 |
-
expiresAt: tokenData.expiresAt
|
| 838 |
-
});
|
| 839 |
-
results.success++;
|
| 840 |
-
|
| 841 |
-
} catch (error) {
|
| 842 |
-
logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message);
|
| 843 |
-
|
| 844 |
-
results.details.push({
|
| 845 |
-
index: i + 1,
|
| 846 |
-
success: false,
|
| 847 |
-
error: error.message
|
| 848 |
-
});
|
| 849 |
-
results.failed++;
|
| 850 |
-
}
|
| 851 |
-
}
|
| 852 |
-
|
| 853 |
-
// 如果有成功的,广播事件并自动关联
|
| 854 |
-
if (results.success > 0) {
|
| 855 |
-
broadcastEvent('oauth_batch_success', {
|
| 856 |
-
provider: 'claude-kiro-oauth',
|
| 857 |
-
count: results.success,
|
| 858 |
-
timestamp: new Date().toISOString()
|
| 859 |
-
});
|
| 860 |
-
|
| 861 |
-
// 自动关联新生成的凭据到 Pools
|
| 862 |
-
for (const detail of results.details) {
|
| 863 |
-
if (detail.success && detail.path) {
|
| 864 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 865 |
-
onlyCurrentCred: true,
|
| 866 |
-
credPath: detail.path
|
| 867 |
-
});
|
| 868 |
-
}
|
| 869 |
-
}
|
| 870 |
-
}
|
| 871 |
-
|
| 872 |
-
return results;
|
| 873 |
-
}
|
| 874 |
-
|
| 875 |
-
/**
|
| 876 |
-
* 批量导入 Kiro refreshToken 并生成凭据文件(流式版本,支持实时进度回调)
|
| 877 |
-
* @param {string[]} refreshTokens - refreshToken 数组
|
| 878 |
-
* @param {string} region - AWS 区域 (默认: us-east-1)
|
| 879 |
-
* @param {Function} onProgress - 进度回调函数,每处理完一个 token 调用
|
| 880 |
-
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
|
| 881 |
-
* @returns {Promise<Object>} 批量处理结果
|
| 882 |
-
*/
|
| 883 |
-
export async function batchImportKiroRefreshTokensStream(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, onProgress = null, skipDuplicateCheck = false) {
|
| 884 |
-
const results = {
|
| 885 |
-
total: refreshTokens.length,
|
| 886 |
-
success: 0,
|
| 887 |
-
failed: 0,
|
| 888 |
-
details: []
|
| 889 |
-
};
|
| 890 |
-
|
| 891 |
-
for (let i = 0; i < refreshTokens.length; i++) {
|
| 892 |
-
const refreshToken = refreshTokens[i].trim();
|
| 893 |
-
const progressData = {
|
| 894 |
-
index: i + 1,
|
| 895 |
-
total: refreshTokens.length,
|
| 896 |
-
current: null
|
| 897 |
-
};
|
| 898 |
-
|
| 899 |
-
if (!refreshToken) {
|
| 900 |
-
progressData.current = {
|
| 901 |
-
index: i + 1,
|
| 902 |
-
success: false,
|
| 903 |
-
error: 'Empty token'
|
| 904 |
-
};
|
| 905 |
-
results.details.push(progressData.current);
|
| 906 |
-
results.failed++;
|
| 907 |
-
|
| 908 |
-
// 发送进度更新
|
| 909 |
-
if (onProgress) {
|
| 910 |
-
onProgress({
|
| 911 |
-
...progressData,
|
| 912 |
-
successCount: results.success,
|
| 913 |
-
failedCount: results.failed
|
| 914 |
-
});
|
| 915 |
-
}
|
| 916 |
-
continue;
|
| 917 |
-
}
|
| 918 |
-
|
| 919 |
-
// 检查重复
|
| 920 |
-
if (!skipDuplicateCheck) {
|
| 921 |
-
const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken);
|
| 922 |
-
if (duplicateCheck.isDuplicate) {
|
| 923 |
-
progressData.current = {
|
| 924 |
-
index: i + 1,
|
| 925 |
-
success: false,
|
| 926 |
-
error: 'duplicate',
|
| 927 |
-
existingPath: duplicateCheck.existingPath
|
| 928 |
-
};
|
| 929 |
-
results.details.push(progressData.current);
|
| 930 |
-
results.failed++;
|
| 931 |
-
|
| 932 |
-
// 发送进度更新
|
| 933 |
-
if (onProgress) {
|
| 934 |
-
onProgress({
|
| 935 |
-
...progressData,
|
| 936 |
-
successCount: results.success,
|
| 937 |
-
failedCount: results.failed
|
| 938 |
-
});
|
| 939 |
-
}
|
| 940 |
-
continue;
|
| 941 |
-
}
|
| 942 |
-
}
|
| 943 |
-
|
| 944 |
-
try {
|
| 945 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`);
|
| 946 |
-
|
| 947 |
-
const tokenData = await refreshKiroToken(refreshToken, region);
|
| 948 |
-
|
| 949 |
-
// 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
|
| 950 |
-
const timestamp = Date.now();
|
| 951 |
-
const folderName = `${timestamp}_kiro-auth-token`;
|
| 952 |
-
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
|
| 953 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 954 |
-
|
| 955 |
-
const credPath = path.join(targetDir, `${folderName}.json`);
|
| 956 |
-
await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
|
| 957 |
-
|
| 958 |
-
const relativePath = path.relative(process.cwd(), credPath);
|
| 959 |
-
|
| 960 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`);
|
| 961 |
-
|
| 962 |
-
progressData.current = {
|
| 963 |
-
index: i + 1,
|
| 964 |
-
success: true,
|
| 965 |
-
path: relativePath,
|
| 966 |
-
expiresAt: tokenData.expiresAt
|
| 967 |
-
};
|
| 968 |
-
results.details.push(progressData.current);
|
| 969 |
-
results.success++;
|
| 970 |
-
|
| 971 |
-
} catch (error) {
|
| 972 |
-
logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message);
|
| 973 |
-
|
| 974 |
-
progressData.current = {
|
| 975 |
-
index: i + 1,
|
| 976 |
-
success: false,
|
| 977 |
-
error: error.message
|
| 978 |
-
};
|
| 979 |
-
results.details.push(progressData.current);
|
| 980 |
-
results.failed++;
|
| 981 |
-
}
|
| 982 |
-
|
| 983 |
-
// 发送进度更新
|
| 984 |
-
if (onProgress) {
|
| 985 |
-
onProgress({
|
| 986 |
-
...progressData,
|
| 987 |
-
successCount: results.success,
|
| 988 |
-
failedCount: results.failed
|
| 989 |
-
});
|
| 990 |
-
}
|
| 991 |
-
}
|
| 992 |
-
|
| 993 |
-
// 如果有成功的,广播事件并自动关联
|
| 994 |
-
if (results.success > 0) {
|
| 995 |
-
broadcastEvent('oauth_batch_success', {
|
| 996 |
-
provider: 'claude-kiro-oauth',
|
| 997 |
-
count: results.success,
|
| 998 |
-
timestamp: new Date().toISOString()
|
| 999 |
-
});
|
| 1000 |
-
|
| 1001 |
-
// 自动关联新生成的凭据到 Pools
|
| 1002 |
-
for (const detail of results.details) {
|
| 1003 |
-
if (detail.success && detail.path) {
|
| 1004 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 1005 |
-
onlyCurrentCred: true,
|
| 1006 |
-
credPath: detail.path
|
| 1007 |
-
});
|
| 1008 |
-
}
|
| 1009 |
-
}
|
| 1010 |
-
}
|
| 1011 |
-
|
| 1012 |
-
return results;
|
| 1013 |
-
}
|
| 1014 |
-
|
| 1015 |
-
/**
|
| 1016 |
-
* 导入 AWS SSO 凭据用于 Kiro (Builder ID 模式)
|
| 1017 |
-
* 从用户上传的 AWS SSO cache 文件中导入凭据
|
| 1018 |
-
* @param {Object} credentials - 合并后的凭据对象,需包含 clientId 和 clientSecret
|
| 1019 |
-
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
|
| 1020 |
-
* @returns {Promise<Object>} 导入结果
|
| 1021 |
-
*/
|
| 1022 |
-
export async function importAwsCredentials(credentials, skipDuplicateCheck = false) {
|
| 1023 |
-
try {
|
| 1024 |
-
// 验证必需字段 - 需要四个字段都存在
|
| 1025 |
-
const missingFields = [];
|
| 1026 |
-
if (!credentials.clientId) missingFields.push('clientId');
|
| 1027 |
-
if (!credentials.clientSecret) missingFields.push('clientSecret');
|
| 1028 |
-
if (!credentials.accessToken) missingFields.push('accessToken');
|
| 1029 |
-
if (!credentials.refreshToken) missingFields.push('refreshToken');
|
| 1030 |
-
|
| 1031 |
-
if (missingFields.length > 0) {
|
| 1032 |
-
return {
|
| 1033 |
-
success: false,
|
| 1034 |
-
error: `Missing required fields: ${missingFields.join(', ')}`
|
| 1035 |
-
};
|
| 1036 |
-
}
|
| 1037 |
-
|
| 1038 |
-
// 检查重复凭据
|
| 1039 |
-
if (!skipDuplicateCheck) {
|
| 1040 |
-
const duplicateCheck = await checkKiroCredentialsDuplicate(credentials.refreshToken);
|
| 1041 |
-
if (duplicateCheck.isDuplicate) {
|
| 1042 |
-
return {
|
| 1043 |
-
success: false,
|
| 1044 |
-
error: 'duplicate',
|
| 1045 |
-
existingPath: duplicateCheck.existingPath
|
| 1046 |
-
};
|
| 1047 |
-
}
|
| 1048 |
-
}
|
| 1049 |
-
|
| 1050 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Importing AWS credentials...`);
|
| 1051 |
-
|
| 1052 |
-
// 准备凭据数据 - 四个字段都是必需的
|
| 1053 |
-
const credentialsData = {
|
| 1054 |
-
clientId: credentials.clientId,
|
| 1055 |
-
clientSecret: credentials.clientSecret,
|
| 1056 |
-
accessToken: credentials.accessToken,
|
| 1057 |
-
refreshToken: credentials.refreshToken,
|
| 1058 |
-
authMethod: credentials.authMethod || 'builder-id',
|
| 1059 |
-
// region: credentials.region || KIRO_REFRESH_CONSTANTS.DEFAULT_REGION,
|
| 1060 |
-
idcRegion: credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION
|
| 1061 |
-
};
|
| 1062 |
-
|
| 1063 |
-
// 可选字段
|
| 1064 |
-
if (credentials.expiresAt) {
|
| 1065 |
-
credentialsData.expiresAt = credentials.expiresAt;
|
| 1066 |
-
}
|
| 1067 |
-
if (credentials.startUrl) {
|
| 1068 |
-
credentialsData.startUrl = credentials.startUrl;
|
| 1069 |
-
}
|
| 1070 |
-
if (credentials.registrationExpiresAt) {
|
| 1071 |
-
credentialsData.registrationExpiresAt = credentials.registrationExpiresAt;
|
| 1072 |
-
}
|
| 1073 |
-
|
| 1074 |
-
// 尝试刷新获取最新的 accessToken
|
| 1075 |
-
try {
|
| 1076 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Attempting to refresh token with provided credentials...`);
|
| 1077 |
-
|
| 1078 |
-
const refreshRegion = credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION;
|
| 1079 |
-
const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', refreshRegion);
|
| 1080 |
-
|
| 1081 |
-
const refreshResponse = await fetchWithProxy(refreshUrl, {
|
| 1082 |
-
method: 'POST',
|
| 1083 |
-
headers: {
|
| 1084 |
-
'Content-Type': 'application/json'
|
| 1085 |
-
},
|
| 1086 |
-
body: JSON.stringify({
|
| 1087 |
-
refreshToken: credentials.refreshToken,
|
| 1088 |
-
clientId: credentials.clientId,
|
| 1089 |
-
clientSecret: credentials.clientSecret,
|
| 1090 |
-
grantType: 'refresh_token'
|
| 1091 |
-
})
|
| 1092 |
-
}, 'claude-kiro-oauth');
|
| 1093 |
-
|
| 1094 |
-
if (refreshResponse.ok) {
|
| 1095 |
-
const tokenData = await refreshResponse.json();
|
| 1096 |
-
credentialsData.accessToken = tokenData.accessToken;
|
| 1097 |
-
credentialsData.refreshToken = tokenData.refreshToken;
|
| 1098 |
-
const expiresIn = tokenData.expiresIn || 3600;
|
| 1099 |
-
credentialsData.expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
| 1100 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`);
|
| 1101 |
-
} else {
|
| 1102 |
-
logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`);
|
| 1103 |
-
}
|
| 1104 |
-
} catch (refreshError) {
|
| 1105 |
-
logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh error:`, refreshError.message);
|
| 1106 |
-
// 继续保存原始凭据
|
| 1107 |
-
}
|
| 1108 |
-
|
| 1109 |
-
// 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
|
| 1110 |
-
const timestamp = Date.now();
|
| 1111 |
-
const folderName = `${timestamp}_kiro-auth-token`;
|
| 1112 |
-
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
|
| 1113 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 1114 |
-
|
| 1115 |
-
const credPath = path.join(targetDir, `${folderName}.json`);
|
| 1116 |
-
await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
|
| 1117 |
-
|
| 1118 |
-
const relativePath = path.relative(process.cwd(), credPath);
|
| 1119 |
-
|
| 1120 |
-
logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials saved to: ${relativePath}`);
|
| 1121 |
-
|
| 1122 |
-
// 广播事件
|
| 1123 |
-
broadcastEvent('oauth_success', {
|
| 1124 |
-
provider: 'claude-kiro-oauth',
|
| 1125 |
-
relativePath: relativePath,
|
| 1126 |
-
timestamp: new Date().toISOString()
|
| 1127 |
-
});
|
| 1128 |
-
|
| 1129 |
-
// 自动关联新生成的凭据到 Pools
|
| 1130 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 1131 |
-
onlyCurrentCred: true,
|
| 1132 |
-
credPath: relativePath
|
| 1133 |
-
});
|
| 1134 |
-
|
| 1135 |
-
return {
|
| 1136 |
-
success: true,
|
| 1137 |
-
path: relativePath
|
| 1138 |
-
};
|
| 1139 |
-
|
| 1140 |
-
} catch (error) {
|
| 1141 |
-
logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error);
|
| 1142 |
-
return {
|
| 1143 |
-
success: false,
|
| 1144 |
-
error: error.message
|
| 1145 |
-
};
|
| 1146 |
-
}
|
| 1147 |
-
}
|
| 1148 |
-
|
| 1149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/oauth-handlers.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
// OAuth 处理器统一导出文件
|
| 2 |
-
// 此文件已按提供商拆分为多个独立文件,请从 index.js 导入
|
| 3 |
-
|
| 4 |
-
// 重新导出所有 OAuth 处理函数以保持向后兼容
|
| 5 |
-
export {
|
| 6 |
-
// Codex OAuth
|
| 7 |
-
refreshCodexTokensWithRetry,
|
| 8 |
-
handleCodexOAuth,
|
| 9 |
-
handleCodexOAuthCallback,
|
| 10 |
-
batchImportCodexTokensStream,
|
| 11 |
-
// Gemini OAuth
|
| 12 |
-
handleGeminiCliOAuth,
|
| 13 |
-
handleGeminiAntigravityOAuth,
|
| 14 |
-
batchImportGeminiTokensStream,
|
| 15 |
-
checkGeminiCredentialsDuplicate,
|
| 16 |
-
// Qwen OAuth
|
| 17 |
-
handleQwenOAuth,
|
| 18 |
-
// Kiro OAuth
|
| 19 |
-
handleKiroOAuth,
|
| 20 |
-
checkKiroCredentialsDuplicate,
|
| 21 |
-
batchImportKiroRefreshTokens,
|
| 22 |
-
batchImportKiroRefreshTokensStream,
|
| 23 |
-
importAwsCredentials,
|
| 24 |
-
// iFlow OAuth
|
| 25 |
-
handleIFlowOAuth,
|
| 26 |
-
refreshIFlowTokens,
|
| 27 |
-
} from './index.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/auth/qwen-oauth.js
DELETED
|
@@ -1,343 +0,0 @@
|
|
| 1 |
-
import fs from 'fs';
|
| 2 |
-
import logger from '../utils/logger.js';
|
| 3 |
-
import path from 'path';
|
| 4 |
-
import os from 'os';
|
| 5 |
-
import crypto from 'crypto';
|
| 6 |
-
import { broadcastEvent } from '../services/ui-manager.js';
|
| 7 |
-
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
| 8 |
-
import { CONFIG } from '../core/config-manager.js';
|
| 9 |
-
import { getProxyConfigForProvider } from '../utils/proxy-utils.js';
|
| 10 |
-
|
| 11 |
-
/**
|
| 12 |
-
* Qwen OAuth 配置
|
| 13 |
-
*/
|
| 14 |
-
const QWEN_OAUTH_CONFIG = {
|
| 15 |
-
clientId: 'f0304373b74a44d2b584a3fb70ca9e56',
|
| 16 |
-
scope: 'openid profile email model.completion',
|
| 17 |
-
deviceCodeEndpoint: 'https://chat.qwen.ai/api/v1/oauth2/device/code',
|
| 18 |
-
tokenEndpoint: 'https://chat.qwen.ai/api/v1/oauth2/token',
|
| 19 |
-
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
| 20 |
-
credentialsDir: '.qwen',
|
| 21 |
-
credentialsFile: 'oauth_creds.json',
|
| 22 |
-
logPrefix: '[Qwen Auth]'
|
| 23 |
-
};
|
| 24 |
-
|
| 25 |
-
/**
|
| 26 |
-
* 活动的轮询任务管理
|
| 27 |
-
*/
|
| 28 |
-
const activePollingTasks = new Map();
|
| 29 |
-
|
| 30 |
-
/**
|
| 31 |
-
* 创建带代理支持的 fetch 请求
|
| 32 |
-
* 使用 axios 替代原生 fetch,以正确支持代理配置
|
| 33 |
-
* @param {string} url - 请求 URL
|
| 34 |
-
* @param {Object} options - fetch 选项(兼容 fetch API 格式)
|
| 35 |
-
* @param {string} providerType - 提供商类型,用于获取代理配置
|
| 36 |
-
* @returns {Promise<Object>} 返回类似 fetch Response 的对象
|
| 37 |
-
*/
|
| 38 |
-
async function fetchWithProxy(url, options = {}, providerType) {
|
| 39 |
-
const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
|
| 40 |
-
|
| 41 |
-
// 构建 axios 配置
|
| 42 |
-
const axiosConfig = {
|
| 43 |
-
url,
|
| 44 |
-
method: options.method || 'GET',
|
| 45 |
-
headers: options.headers || {},
|
| 46 |
-
timeout: 30000, // 30 秒超时
|
| 47 |
-
};
|
| 48 |
-
|
| 49 |
-
// 处理请求体
|
| 50 |
-
if (options.body) {
|
| 51 |
-
axiosConfig.data = options.body;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
// 配置代理
|
| 55 |
-
if (proxyConfig) {
|
| 56 |
-
axiosConfig.httpAgent = proxyConfig.httpAgent;
|
| 57 |
-
axiosConfig.httpsAgent = proxyConfig.httpsAgent;
|
| 58 |
-
axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent
|
| 59 |
-
logger.info(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
try {
|
| 63 |
-
const axios = (await import('axios')).default;
|
| 64 |
-
const response = await axios(axiosConfig);
|
| 65 |
-
|
| 66 |
-
// 返回类似 fetch Response 的对象
|
| 67 |
-
return {
|
| 68 |
-
ok: response.status >= 200 && response.status < 300,
|
| 69 |
-
status: response.status,
|
| 70 |
-
statusText: response.statusText,
|
| 71 |
-
headers: response.headers,
|
| 72 |
-
json: async () => response.data,
|
| 73 |
-
text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
|
| 74 |
-
};
|
| 75 |
-
} catch (error) {
|
| 76 |
-
// 处理 axios 错误,转换为类似 fetch 的响应格式
|
| 77 |
-
if (error.response) {
|
| 78 |
-
// 服务器返回了错误状态码
|
| 79 |
-
return {
|
| 80 |
-
ok: false,
|
| 81 |
-
status: error.response.status,
|
| 82 |
-
statusText: error.response.statusText,
|
| 83 |
-
headers: error.response.headers,
|
| 84 |
-
json: async () => error.response.data,
|
| 85 |
-
text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data),
|
| 86 |
-
};
|
| 87 |
-
}
|
| 88 |
-
// 网络错误或其他错误
|
| 89 |
-
throw error;
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
/**
|
| 94 |
-
* 生成 PKCE 代码验证器
|
| 95 |
-
* @returns {string} Base64URL 编码的随机字符串
|
| 96 |
-
*/
|
| 97 |
-
function generateCodeVerifier() {
|
| 98 |
-
return crypto.randomBytes(32).toString('base64url');
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
/**
|
| 102 |
-
* 生成 PKCE 代码挑战
|
| 103 |
-
* @param {string} codeVerifier - 代码验证器
|
| 104 |
-
* @returns {string} Base64URL 编码的 SHA256 哈希
|
| 105 |
-
*/
|
| 106 |
-
function generateCodeChallenge(codeVerifier) {
|
| 107 |
-
const hash = crypto.createHash('sha256');
|
| 108 |
-
hash.update(codeVerifier);
|
| 109 |
-
return hash.digest('base64url');
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
/**
|
| 113 |
-
* 停止活动的轮询任务
|
| 114 |
-
* @param {string} taskId - 任务标识符
|
| 115 |
-
*/
|
| 116 |
-
function stopPollingTask(taskId) {
|
| 117 |
-
const task = activePollingTasks.get(taskId);
|
| 118 |
-
if (task) {
|
| 119 |
-
task.shouldStop = true;
|
| 120 |
-
activePollingTasks.delete(taskId);
|
| 121 |
-
logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`);
|
| 122 |
-
}
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
/**
|
| 126 |
-
* 轮询获取 Qwen OAuth 令牌
|
| 127 |
-
* @param {string} deviceCode - 设备代码
|
| 128 |
-
* @param {string} codeVerifier - PKCE 代码验证器
|
| 129 |
-
* @param {number} interval - 轮询间隔(秒)
|
| 130 |
-
* @param {number} expiresIn - 过期时间(秒)
|
| 131 |
-
* @param {string} taskId - 任务标识符
|
| 132 |
-
* @param {Object} options - 额外选项
|
| 133 |
-
* @returns {Promise<Object>} 返回令牌信息
|
| 134 |
-
*/
|
| 135 |
-
async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = 300, taskId = 'default', options = {}) {
|
| 136 |
-
let credPath = path.join(os.homedir(), QWEN_OAUTH_CONFIG.credentialsDir, QWEN_OAUTH_CONFIG.credentialsFile);
|
| 137 |
-
const maxAttempts = Math.floor(expiresIn / interval);
|
| 138 |
-
let attempts = 0;
|
| 139 |
-
|
| 140 |
-
// 创建任务控制对象
|
| 141 |
-
const taskControl = { shouldStop: false };
|
| 142 |
-
activePollingTasks.set(taskId, taskControl);
|
| 143 |
-
|
| 144 |
-
logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}],间隔 ${interval} 秒,最多尝试 ${maxAttempts} 次`);
|
| 145 |
-
|
| 146 |
-
const poll = async () => {
|
| 147 |
-
// 检查是否需要停止
|
| 148 |
-
if (taskControl.shouldStop) {
|
| 149 |
-
logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询任务 [${taskId}] 已被停止`);
|
| 150 |
-
throw new Error('轮询任务已被取消');
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
if (attempts >= maxAttempts) {
|
| 154 |
-
activePollingTasks.delete(taskId);
|
| 155 |
-
throw new Error('授权超时,请重新开始授权流程');
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
attempts++;
|
| 159 |
-
|
| 160 |
-
const bodyData = {
|
| 161 |
-
client_id: QWEN_OAUTH_CONFIG.clientId,
|
| 162 |
-
device_code: deviceCode,
|
| 163 |
-
grant_type: QWEN_OAUTH_CONFIG.grantType,
|
| 164 |
-
code_verifier: codeVerifier
|
| 165 |
-
};
|
| 166 |
-
|
| 167 |
-
const formBody = Object.entries(bodyData)
|
| 168 |
-
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
| 169 |
-
.join('&');
|
| 170 |
-
|
| 171 |
-
try {
|
| 172 |
-
const response = await fetchWithProxy(QWEN_OAUTH_CONFIG.tokenEndpoint, {
|
| 173 |
-
method: 'POST',
|
| 174 |
-
headers: {
|
| 175 |
-
'Content-Type': 'application/x-www-form-urlencoded',
|
| 176 |
-
'Accept': 'application/json'
|
| 177 |
-
},
|
| 178 |
-
body: formBody
|
| 179 |
-
}, 'openai-qwen-oauth');
|
| 180 |
-
|
| 181 |
-
const data = await response.json();
|
| 182 |
-
|
| 183 |
-
if (response.ok && data.access_token) {
|
| 184 |
-
// 成功获取令牌
|
| 185 |
-
logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
|
| 186 |
-
|
| 187 |
-
// 如果指定了保存到 configs 目录
|
| 188 |
-
if (options.saveToConfigs) {
|
| 189 |
-
const targetDir = path.join(process.cwd(), 'configs', options.providerDir);
|
| 190 |
-
await fs.promises.mkdir(targetDir, { recursive: true });
|
| 191 |
-
const timestamp = Date.now();
|
| 192 |
-
const filename = `${timestamp}_oauth_creds.json`;
|
| 193 |
-
credPath = path.join(targetDir, filename);
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
// 保存令牌到文件
|
| 197 |
-
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
| 198 |
-
await fs.promises.writeFile(credPath, JSON.stringify(data, null, 2));
|
| 199 |
-
logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 令牌已保存到 ${credPath}`);
|
| 200 |
-
|
| 201 |
-
const relativePath = path.relative(process.cwd(), credPath);
|
| 202 |
-
|
| 203 |
-
// 清理任务
|
| 204 |
-
activePollingTasks.delete(taskId);
|
| 205 |
-
|
| 206 |
-
// 广播授权成功事件
|
| 207 |
-
broadcastEvent('oauth_success', {
|
| 208 |
-
provider: 'openai-qwen-oauth',
|
| 209 |
-
credPath: credPath,
|
| 210 |
-
relativePath: relativePath,
|
| 211 |
-
timestamp: new Date().toISOString()
|
| 212 |
-
});
|
| 213 |
-
|
| 214 |
-
// 自动关联新生成的凭据到 Pools
|
| 215 |
-
await autoLinkProviderConfigs(CONFIG, {
|
| 216 |
-
onlyCurrentCred: true,
|
| 217 |
-
credPath: relativePath
|
| 218 |
-
});
|
| 219 |
-
|
| 220 |
-
return data;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
// 检查错误类型
|
| 224 |
-
if (data.error === 'authorization_pending') {
|
| 225 |
-
// 用户尚未完成授权,继续轮询
|
| 226 |
-
logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (第 ${attempts}/${maxAttempts} 次尝试)`);
|
| 227 |
-
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
| 228 |
-
return poll();
|
| 229 |
-
} else if (data.error === 'slow_down') {
|
| 230 |
-
// 需要降低轮询频率
|
| 231 |
-
logger.info(`${QWEN_OAUTH_CONFIG.logPrefix} 降低轮询频率`);
|
| 232 |
-
await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000));
|
| 233 |
-
return poll();
|
| 234 |
-
} else if (data.error === 'expired_token') {
|
| 235 |
-
activePollingTasks.delete(taskId);
|
| 236 |
-
throw new Error('设备代码已过期,请重新开始授权流程');
|
| 237 |
-
} else if (data.error === 'access_denied') {
|
| 238 |
-
activePollingTasks.delete(taskId);
|
| 239 |
-
throw new Error('用户拒绝了授权请求');
|
| 240 |
-
} else {
|
| 241 |
-
activePollingTasks.delete(taskId);
|
| 242 |
-
throw new Error(`授权失败: ${data.error || '未知错误'}`);
|
| 243 |
-
}
|
| 244 |
-
} catch (error) {
|
| 245 |
-
if (error.message.includes('授权') || error.message.includes('过期') || error.message.includes('拒绝')) {
|
| 246 |
-
throw error;
|
| 247 |
-
}
|
| 248 |
-
logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询出错:`, error);
|
| 249 |
-
// 网络错误,继续重试
|
| 250 |
-
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
| 251 |
-
return poll();
|
| 252 |
-
}
|
| 253 |
-
};
|
| 254 |
-
|
| 255 |
-
return poll();
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
/**
|
| 259 |
-
* 处理 Qwen OAuth 授权(设备授权流程)
|
| 260 |
-
* @param {Object} currentConfig - 当前配置对象
|
| 261 |
-
* @param {Object} options - 额外选项
|
| 262 |
-
* @returns {Promise<Object>} 返回授权URL和相关信息
|
| 263 |
-
*/
|
| 264 |
-
export async function handleQwenOAuth(currentConfig, options = {}) {
|
| 265 |
-
const codeVerifier = generateCodeVerifier();
|
| 266 |
-
const codeChallenge = generateCodeChallenge(codeVerifier);
|
| 267 |
-
|
| 268 |
-
const bodyData = {
|
| 269 |
-
client_id: QWEN_OAUTH_CONFIG.clientId,
|
| 270 |
-
scope: QWEN_OAUTH_CONFIG.scope,
|
| 271 |
-
code_challenge: codeChallenge,
|
| 272 |
-
code_challenge_method: 'S256'
|
| 273 |
-
};
|
| 274 |
-
|
| 275 |
-
const formBody = Object.entries(bodyData)
|
| 276 |
-
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
| 277 |
-
.join('&');
|
| 278 |
-
|
| 279 |
-
try {
|
| 280 |
-
const response = await fetchWithProxy(QWEN_OAUTH_CONFIG.deviceCodeEndpoint, {
|
| 281 |
-
method: 'POST',
|
| 282 |
-
headers: {
|
| 283 |
-
'Content-Type': 'application/x-www-form-urlencoded',
|
| 284 |
-
'Accept': 'application/json'
|
| 285 |
-
},
|
| 286 |
-
body: formBody
|
| 287 |
-
}, 'openai-qwen-oauth');
|
| 288 |
-
|
| 289 |
-
if (!response.ok) {
|
| 290 |
-
throw new Error(`Qwen OAuth请求失败: ${response.status} ${response.statusText}`);
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
const deviceAuth = await response.json();
|
| 294 |
-
|
| 295 |
-
if (!deviceAuth.device_code || !deviceAuth.verification_uri_complete) {
|
| 296 |
-
throw new Error('Qwen OAuth响应格式错误,缺少必要字段');
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
// 启动后台轮询获取令牌
|
| 300 |
-
const interval = 5;
|
| 301 |
-
// const expiresIn = deviceAuth.expires_in || 1800;
|
| 302 |
-
const expiresIn = 300;
|
| 303 |
-
|
| 304 |
-
// 生成唯一的任务ID
|
| 305 |
-
const taskId = `qwen-${deviceAuth.device_code.substring(0, 8)}-${Date.now()}`;
|
| 306 |
-
|
| 307 |
-
// 先停止之前可能存在的所有 Qwen 轮询任务
|
| 308 |
-
for (const [existingTaskId] of activePollingTasks.entries()) {
|
| 309 |
-
if (existingTaskId.startsWith('qwen-')) {
|
| 310 |
-
stopPollingTask(existingTaskId);
|
| 311 |
-
}
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
// 不等待轮询完成,立即返回授权信息
|
| 315 |
-
pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId, options)
|
| 316 |
-
.catch(error => {
|
| 317 |
-
logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
|
| 318 |
-
// 广播授权失败事件
|
| 319 |
-
broadcastEvent('oauth_error', {
|
| 320 |
-
provider: 'openai-qwen-oauth',
|
| 321 |
-
error: error.message,
|
| 322 |
-
timestamp: new Date().toISOString()
|
| 323 |
-
});
|
| 324 |
-
});
|
| 325 |
-
|
| 326 |
-
return {
|
| 327 |
-
authUrl: deviceAuth.verification_uri_complete,
|
| 328 |
-
authInfo: {
|
| 329 |
-
provider: 'openai-qwen-oauth',
|
| 330 |
-
deviceCode: deviceAuth.device_code,
|
| 331 |
-
userCode: deviceAuth.user_code,
|
| 332 |
-
verificationUri: deviceAuth.verification_uri,
|
| 333 |
-
verificationUriComplete: deviceAuth.verification_uri_complete,
|
| 334 |
-
expiresIn: expiresIn,
|
| 335 |
-
interval: interval,
|
| 336 |
-
codeVerifier: codeVerifier
|
| 337 |
-
}
|
| 338 |
-
};
|
| 339 |
-
} catch (error) {
|
| 340 |
-
logger.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error);
|
| 341 |
-
throw new Error(`Qwen OAuth 授权失败: ${error.message}`);
|
| 342 |
-
}
|
| 343 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/convert/convert-old.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/convert/convert.js
DELETED
|
@@ -1,392 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 协议转换模块 - 新架构版本
|
| 3 |
-
* 使用重构后的转换器架构
|
| 4 |
-
*
|
| 5 |
-
* 这个文件展示了如何使用新的转换器架构
|
| 6 |
-
* 可以逐步替换原有的 convert.js
|
| 7 |
-
*/
|
| 8 |
-
|
| 9 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 10 |
-
import logger from '../utils/logger.js';
|
| 11 |
-
import { MODEL_PROTOCOL_PREFIX, getProtocolPrefix } from '../utils/common.js';
|
| 12 |
-
import { ConverterFactory } from '../converters/ConverterFactory.js';
|
| 13 |
-
import {
|
| 14 |
-
generateResponseCreated,
|
| 15 |
-
generateResponseInProgress,
|
| 16 |
-
generateOutputItemAdded,
|
| 17 |
-
generateContentPartAdded,
|
| 18 |
-
generateOutputTextDone,
|
| 19 |
-
generateContentPartDone,
|
| 20 |
-
generateOutputItemDone,
|
| 21 |
-
generateResponseCompleted
|
| 22 |
-
} from '../providers/openai/openai-responses-core.mjs';
|
| 23 |
-
|
| 24 |
-
// =============================================================================
|
| 25 |
-
// 初始化:注册所有转换器
|
| 26 |
-
// =============================================================================
|
| 27 |
-
|
| 28 |
-
// =============================================================================
|
| 29 |
-
// 主转换函数
|
| 30 |
-
// =============================================================================
|
| 31 |
-
|
| 32 |
-
/**
|
| 33 |
-
* 通用数据转换函数(新架构版本)
|
| 34 |
-
* @param {object} data - 要转换的数据(请求体或响应)
|
| 35 |
-
* @param {string} type - 转换类型:'request', 'response', 'streamChunk', 'modelList'
|
| 36 |
-
* @param {string} fromProvider - 源模型提供商
|
| 37 |
-
* @param {string} toProvider - 目标模型提供商
|
| 38 |
-
* @param {string} [model] - 可选的模型名称(用于响应转换)
|
| 39 |
-
* @returns {object} 转换后的数据
|
| 40 |
-
* @throws {Error} 如果找不到合适的转换函数
|
| 41 |
-
*/
|
| 42 |
-
export function convertData(data, type, fromProvider, toProvider, model, requestId) {
|
| 43 |
-
try {
|
| 44 |
-
// 获取协议前缀
|
| 45 |
-
const fromProtocol = getProtocolPrefix(fromProvider);
|
| 46 |
-
const toProtocol = getProtocolPrefix(toProvider);
|
| 47 |
-
|
| 48 |
-
// 如果目标协议为 forward,直接返回原始数据,无需转换
|
| 49 |
-
if (toProtocol === MODEL_PROTOCOL_PREFIX.FORWARD || fromProtocol === MODEL_PROTOCOL_PREFIX.FORWARD) {
|
| 50 |
-
logger.info(`[Convert] Target protocol is forward, skipping conversion`);
|
| 51 |
-
return data;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
// 从工厂获取转换器
|
| 55 |
-
const converter = ConverterFactory.getConverter(fromProtocol);
|
| 56 |
-
|
| 57 |
-
if (!converter) {
|
| 58 |
-
throw new Error(`No converter found for protocol: ${fromProtocol}`);
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
// 根据类型调用相应的转换方法
|
| 62 |
-
switch (type) {
|
| 63 |
-
case 'request':
|
| 64 |
-
return converter.convertRequest(data, toProtocol);
|
| 65 |
-
|
| 66 |
-
case 'response':
|
| 67 |
-
return converter.convertResponse(data, toProtocol, model);
|
| 68 |
-
|
| 69 |
-
case 'streamChunk':
|
| 70 |
-
return converter.convertStreamChunk(data, toProtocol, model, requestId);
|
| 71 |
-
|
| 72 |
-
case 'modelList':
|
| 73 |
-
return converter.convertModelList(data, toProtocol);
|
| 74 |
-
|
| 75 |
-
default:
|
| 76 |
-
throw new Error(`Unsupported conversion type: ${type}`);
|
| 77 |
-
}
|
| 78 |
-
} catch (error) {
|
| 79 |
-
logger.error(`Conversion error: ${error.message}`);
|
| 80 |
-
throw error;
|
| 81 |
-
}
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
// =============================================================================
|
| 85 |
-
// 向后兼容的导出函数
|
| 86 |
-
// =============================================================================
|
| 87 |
-
|
| 88 |
-
/**
|
| 89 |
-
* 以下函数保持与原有API的兼容性
|
| 90 |
-
* 内部使用新的转换器架构
|
| 91 |
-
*/
|
| 92 |
-
|
| 93 |
-
// OpenAI 相关转换
|
| 94 |
-
export function toOpenAIRequestFromGemini(geminiRequest) {
|
| 95 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 96 |
-
return converter.toOpenAIRequest(geminiRequest);
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
export function toOpenAIRequestFromClaude(claudeRequest) {
|
| 100 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
|
| 101 |
-
return converter.toOpenAIRequest(claudeRequest);
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
export function toOpenAIChatCompletionFromGemini(geminiResponse, model) {
|
| 105 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 106 |
-
return converter.toOpenAIResponse(geminiResponse, model);
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
export function toOpenAIChatCompletionFromClaude(claudeResponse, model) {
|
| 110 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
|
| 111 |
-
return converter.toOpenAIResponse(claudeResponse, model);
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
export function toOpenAIStreamChunkFromGemini(geminiChunk, model) {
|
| 115 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 116 |
-
return converter.toOpenAIStreamChunk(geminiChunk, model);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
export function toOpenAIStreamChunkFromClaude(claudeChunk, model) {
|
| 120 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
|
| 121 |
-
return converter.toOpenAIStreamChunk(claudeChunk, model);
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
export function toOpenAIModelListFromGemini(geminiModels) {
|
| 125 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 126 |
-
return converter.toOpenAIModelList(geminiModels);
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
export function toOpenAIModelListFromClaude(claudeModels) {
|
| 130 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
|
| 131 |
-
return converter.toOpenAIModelList(claudeModels);
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
// Claude 相关转换
|
| 135 |
-
export function toClaudeRequestFromOpenAI(openaiRequest) {
|
| 136 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
| 137 |
-
return converter.toClaudeRequest(openaiRequest);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
export function toClaudeRequestFromOpenAIResponses(responsesRequest) {
|
| 141 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
| 142 |
-
return converter.toClaudeRequest(responsesRequest);
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
export function toClaudeChatCompletionFromOpenAI(openaiResponse, model) {
|
| 146 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
| 147 |
-
return converter.toClaudeResponse(openaiResponse, model);
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
export function toClaudeChatCompletionFromGemini(geminiResponse, model) {
|
| 151 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 152 |
-
return converter.toClaudeResponse(geminiResponse, model);
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
export function toClaudeStreamChunkFromOpenAI(openaiChunk, model) {
|
| 156 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
| 157 |
-
return converter.toClaudeStreamChunk(openaiChunk, model);
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
export function toClaudeStreamChunkFromGemini(geminiChunk, model) {
|
| 161 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 162 |
-
return converter.toClaudeStreamChunk(geminiChunk, model);
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
export function toClaudeModelListFromOpenAI(openaiModels) {
|
| 166 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
| 167 |
-
return converter.toClaudeModelList(openaiModels);
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
export function toClaudeModelListFromGemini(geminiModels) {
|
| 171 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 172 |
-
return converter.toClaudeModelList(geminiModels);
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
// Gemini 相关转换
|
| 176 |
-
export function toGeminiRequestFromOpenAI(openaiRequest) {
|
| 177 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
| 178 |
-
return converter.toGeminiRequest(openaiRequest);
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
export function toGeminiRequestFromClaude(claudeRequest) {
|
| 182 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
|
| 183 |
-
return converter.toGeminiRequest(claudeRequest);
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
export function toGeminiRequestFromOpenAIResponses(responsesRequest) {
|
| 187 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
| 188 |
-
return converter.toGeminiRequest(responsesRequest);
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
// OpenAI Responses 相关转换
|
| 192 |
-
export function toOpenAIResponsesFromOpenAI(openaiResponse, model) {
|
| 193 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
| 194 |
-
return converter.toOpenAIResponsesResponse(openaiResponse, model);
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
export function toOpenAIResponsesFromClaude(claudeResponse, model) {
|
| 198 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
|
| 199 |
-
return converter.toOpenAIResponsesResponse(claudeResponse, model);
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
export function toOpenAIResponsesFromGemini(geminiResponse, model) {
|
| 203 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 204 |
-
return converter.toOpenAIResponsesResponse(geminiResponse, model);
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
export function toOpenAIResponsesStreamChunkFromOpenAI(openaiChunk, model, requestId) {
|
| 208 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
| 209 |
-
return converter.toOpenAIResponsesStreamChunk(openaiChunk, model, requestId);
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
export function toOpenAIResponsesStreamChunkFromClaude(claudeChunk, model, requestId) {
|
| 213 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.CLAUDE);
|
| 214 |
-
return converter.toOpenAIResponsesStreamChunk(claudeChunk, model, requestId);
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
export function toOpenAIResponsesStreamChunkFromGemini(geminiChunk, model, requestId) {
|
| 218 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GEMINI);
|
| 219 |
-
return converter.toOpenAIResponsesStreamChunk(geminiChunk, model, requestId);
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
// 从 OpenAI Responses 转换到其他格式
|
| 223 |
-
export function toOpenAIRequestFromOpenAIResponses(responsesRequest) {
|
| 224 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
| 225 |
-
return converter.toOpenAIRequest(responsesRequest);
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
export function toOpenAIChatCompletionFromOpenAIResponses(responsesResponse, model) {
|
| 229 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
| 230 |
-
return converter.toOpenAIResponse(responsesResponse, model);
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
export function toOpenAIStreamChunkFromOpenAIResponses(responsesChunk, model) {
|
| 234 |
-
const converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
| 235 |
-
return converter.toOpenAIStreamChunk(responsesChunk, model);
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
// 辅助函数导出
|
| 239 |
-
export async function extractAndProcessSystemMessages(messages) {
|
| 240 |
-
const { Utils } = await import('../converters/utils.js');
|
| 241 |
-
return Utils.extractSystemMessages(messages);
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
export async function extractTextFromMessageContent(content) {
|
| 245 |
-
const { Utils } = await import('../converters/utils.js');
|
| 246 |
-
return Utils.extractText(content);
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
// =============================================================================
|
| 250 |
-
// 工具函数
|
| 251 |
-
// =============================================================================
|
| 252 |
-
|
| 253 |
-
/**
|
| 254 |
-
* 获取所有已注册的协议
|
| 255 |
-
* @returns {Array<string>} 协议前缀数组
|
| 256 |
-
*/
|
| 257 |
-
export function getRegisteredProtocols() {
|
| 258 |
-
return ConverterFactory.getRegisteredProtocols();
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
/**
|
| 262 |
-
* 检查协议是否已注册
|
| 263 |
-
* @param {string} protocol - 协议前缀
|
| 264 |
-
* @returns {boolean} 是否已注册
|
| 265 |
-
*/
|
| 266 |
-
export function isProtocolRegistered(protocol) {
|
| 267 |
-
return ConverterFactory.isProtocolRegistered(protocol);
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
/**
|
| 271 |
-
* 清除所有转换器缓存
|
| 272 |
-
*/
|
| 273 |
-
export function clearConverterCache() {
|
| 274 |
-
ConverterFactory.clearCache();
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
/**
|
| 278 |
-
* 获取转换器实例(用于高级用法)
|
| 279 |
-
* @param {string} protocol - 协议前缀
|
| 280 |
-
* @returns {BaseConverter} 转换器实例
|
| 281 |
-
*/
|
| 282 |
-
export function getConverter(protocol) {
|
| 283 |
-
return ConverterFactory.getConverter(protocol);
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
// =============================================================================
|
| 287 |
-
// 辅助函数 - 从原 convert.js 迁移
|
| 288 |
-
// =============================================================================
|
| 289 |
-
|
| 290 |
-
/**
|
| 291 |
-
* 生成 OpenAI 流式响应的停止块
|
| 292 |
-
* @param {string} model - 模型名称
|
| 293 |
-
* @returns {Object} OpenAI 流式停止块
|
| 294 |
-
*/
|
| 295 |
-
export function getOpenAIStreamChunkStop(model) {
|
| 296 |
-
return {
|
| 297 |
-
id: `chatcmpl-${uuidv4()}`,
|
| 298 |
-
object: "chat.completion.chunk",
|
| 299 |
-
created: Math.floor(Date.now() / 1000),
|
| 300 |
-
model: model,
|
| 301 |
-
system_fingerprint: "",
|
| 302 |
-
choices: [{
|
| 303 |
-
index: 0,
|
| 304 |
-
delta: {
|
| 305 |
-
content: "",
|
| 306 |
-
reasoning_content: ""
|
| 307 |
-
},
|
| 308 |
-
finish_reason: 'stop',
|
| 309 |
-
message: {
|
| 310 |
-
content: "",
|
| 311 |
-
reasoning_content: ""
|
| 312 |
-
}
|
| 313 |
-
}],
|
| 314 |
-
usage:{
|
| 315 |
-
prompt_tokens: 0,
|
| 316 |
-
completion_tokens: 0,
|
| 317 |
-
total_tokens: 0,
|
| 318 |
-
},
|
| 319 |
-
};
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
/**
|
| 323 |
-
* 生成 OpenAI Responses 流式响应的开始事件
|
| 324 |
-
* @param {string} id - 响应 ID
|
| 325 |
-
* @param {string} model - 模型名称
|
| 326 |
-
* @returns {Array} 开始事件数组
|
| 327 |
-
*/
|
| 328 |
-
export function getOpenAIResponsesStreamChunkBegin(id, model) {
|
| 329 |
-
return [
|
| 330 |
-
generateResponseCreated(id, model),
|
| 331 |
-
generateResponseInProgress(id),
|
| 332 |
-
generateOutputItemAdded(id),
|
| 333 |
-
generateContentPartAdded(id)
|
| 334 |
-
];
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
/**
|
| 338 |
-
* 生成 OpenAI Responses 流式响应的结束事件
|
| 339 |
-
* @param {string} id - 响应 ID
|
| 340 |
-
* @returns {Array} 结束事件数组
|
| 341 |
-
*/
|
| 342 |
-
export function getOpenAIResponsesStreamChunkEnd(id) {
|
| 343 |
-
return [
|
| 344 |
-
generateOutputTextDone(id),
|
| 345 |
-
generateContentPartDone(id),
|
| 346 |
-
generateOutputItemDone(id),
|
| 347 |
-
generateResponseCompleted(id)
|
| 348 |
-
];
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
// =============================================================================
|
| 352 |
-
// 默认导出
|
| 353 |
-
// =============================================================================
|
| 354 |
-
|
| 355 |
-
export default {
|
| 356 |
-
convertData,
|
| 357 |
-
getRegisteredProtocols,
|
| 358 |
-
isProtocolRegistered,
|
| 359 |
-
clearConverterCache,
|
| 360 |
-
getConverter,
|
| 361 |
-
// 向后兼容的函数
|
| 362 |
-
toOpenAIRequestFromGemini,
|
| 363 |
-
toOpenAIRequestFromClaude,
|
| 364 |
-
toOpenAIChatCompletionFromGemini,
|
| 365 |
-
toOpenAIChatCompletionFromClaude,
|
| 366 |
-
toOpenAIStreamChunkFromGemini,
|
| 367 |
-
toOpenAIStreamChunkFromClaude,
|
| 368 |
-
toOpenAIModelListFromGemini,
|
| 369 |
-
toOpenAIModelListFromClaude,
|
| 370 |
-
toClaudeRequestFromOpenAI,
|
| 371 |
-
toClaudeChatCompletionFromOpenAI,
|
| 372 |
-
toClaudeChatCompletionFromGemini,
|
| 373 |
-
toClaudeStreamChunkFromOpenAI,
|
| 374 |
-
toClaudeStreamChunkFromGemini,
|
| 375 |
-
toClaudeModelListFromOpenAI,
|
| 376 |
-
toClaudeModelListFromGemini,
|
| 377 |
-
toGeminiRequestFromOpenAI,
|
| 378 |
-
toGeminiRequestFromClaude,
|
| 379 |
-
toOpenAIResponsesFromOpenAI,
|
| 380 |
-
toOpenAIResponsesFromClaude,
|
| 381 |
-
toOpenAIResponsesFromGemini,
|
| 382 |
-
toOpenAIResponsesStreamChunkFromOpenAI,
|
| 383 |
-
toOpenAIResponsesStreamChunkFromClaude,
|
| 384 |
-
toOpenAIResponsesStreamChunkFromGemini,
|
| 385 |
-
toOpenAIRequestFromOpenAIResponses,
|
| 386 |
-
toOpenAIChatCompletionFromOpenAIResponses,
|
| 387 |
-
toOpenAIStreamChunkFromOpenAIResponses,
|
| 388 |
-
toClaudeRequestFromOpenAIResponses,
|
| 389 |
-
toGeminiRequestFromOpenAIResponses,
|
| 390 |
-
};
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/BaseConverter.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 转换器基类
|
| 3 |
-
* 使用策略模式定义转换器的通用接口
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
/**
|
| 7 |
-
* 抽象转换器基类
|
| 8 |
-
* 所有具体的协议转换器都应继承此类
|
| 9 |
-
*/
|
| 10 |
-
export class BaseConverter {
|
| 11 |
-
constructor(protocolName) {
|
| 12 |
-
if (new.target === BaseConverter) {
|
| 13 |
-
throw new Error('BaseConverter是抽象类,不能直接实例化');
|
| 14 |
-
}
|
| 15 |
-
this.protocolName = protocolName;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
/**
|
| 19 |
-
* 转换请求
|
| 20 |
-
* @param {Object} data - 请求数据
|
| 21 |
-
* @param {string} targetProtocol - 目标协议
|
| 22 |
-
* @returns {Object} 转换后的请求
|
| 23 |
-
*/
|
| 24 |
-
convertRequest(data, targetProtocol) {
|
| 25 |
-
throw new Error('convertRequest方法必须被子类实现');
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/**
|
| 29 |
-
* 转换响应
|
| 30 |
-
* @param {Object} data - 响应数据
|
| 31 |
-
* @param {string} targetProtocol - 目标协议
|
| 32 |
-
* @param {string} model - 模型名称
|
| 33 |
-
* @returns {Object} 转换后的响应
|
| 34 |
-
*/
|
| 35 |
-
convertResponse(data, targetProtocol, model) {
|
| 36 |
-
throw new Error('convertResponse方法必须被子类实现');
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
/**
|
| 40 |
-
* 转换流式响应块
|
| 41 |
-
* @param {Object} chunk - 流式响应块
|
| 42 |
-
* @param {string} targetProtocol - 目标协议
|
| 43 |
-
* @param {string} model - 模型名称
|
| 44 |
-
* @returns {Object} 转换后的流式响应块
|
| 45 |
-
*/
|
| 46 |
-
convertStreamChunk(chunk, targetProtocol, model) {
|
| 47 |
-
throw new Error('convertStreamChunk方法必须被子类实现');
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
/**
|
| 51 |
-
* 转换模型列表
|
| 52 |
-
* @param {Object} data - 模型列表数据
|
| 53 |
-
* @param {string} targetProtocol - 目标协议
|
| 54 |
-
* @returns {Object} 转换后的模型列表
|
| 55 |
-
*/
|
| 56 |
-
convertModelList(data, targetProtocol) {
|
| 57 |
-
throw new Error('convertModelList方法必须被子类实现');
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
/**
|
| 61 |
-
* 获取协议名称
|
| 62 |
-
* @returns {string} 协议名称
|
| 63 |
-
*/
|
| 64 |
-
getProtocolName() {
|
| 65 |
-
return this.protocolName;
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
/**
|
| 70 |
-
* 内容处理器接口
|
| 71 |
-
* 用于处理不同类型的内容(文本、图片、音频等)
|
| 72 |
-
*/
|
| 73 |
-
export class ContentProcessor {
|
| 74 |
-
/**
|
| 75 |
-
* 处理内容
|
| 76 |
-
* @param {*} content - 内容数据
|
| 77 |
-
* @returns {*} 处理后的内容
|
| 78 |
-
*/
|
| 79 |
-
process(content) {
|
| 80 |
-
throw new Error('process方法必须被子类实现');
|
| 81 |
-
}
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
/**
|
| 85 |
-
* 工具处理器接口
|
| 86 |
-
* 用于处理工具调用相关的转换
|
| 87 |
-
*/
|
| 88 |
-
export class ToolProcessor {
|
| 89 |
-
/**
|
| 90 |
-
* 处理工具定义
|
| 91 |
-
* @param {Array} tools - 工具定义数组
|
| 92 |
-
* @returns {Array} 处理后的工具定义
|
| 93 |
-
*/
|
| 94 |
-
processToolDefinitions(tools) {
|
| 95 |
-
throw new Error('processToolDefinitions方法必须被子类实现');
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
/**
|
| 99 |
-
* 处理工具调用
|
| 100 |
-
* @param {Object} toolCall - 工具调用数据
|
| 101 |
-
* @returns {Object} 处理后的工具调用
|
| 102 |
-
*/
|
| 103 |
-
processToolCall(toolCall) {
|
| 104 |
-
throw new Error('processToolCall方法必须被子类实现');
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
/**
|
| 108 |
-
* 处理工具结果
|
| 109 |
-
* @param {Object} toolResult - 工具结果数据
|
| 110 |
-
* @returns {Object} 处理后的工具结果
|
| 111 |
-
*/
|
| 112 |
-
processToolResult(toolResult) {
|
| 113 |
-
throw new Error('processToolResult方法必须被子类实现');
|
| 114 |
-
}
|
| 115 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/ConverterFactory.js
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 转换器工厂类
|
| 3 |
-
* 使用工厂模式管理转换器实例的创建和缓存
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js';
|
| 7 |
-
import logger from '../utils/logger.js';
|
| 8 |
-
|
| 9 |
-
/**
|
| 10 |
-
* 转换器工厂(单例模式 + 工厂模式)
|
| 11 |
-
*/
|
| 12 |
-
export class ConverterFactory {
|
| 13 |
-
// 私有静态属性:存储转换器实例
|
| 14 |
-
static #converters = new Map();
|
| 15 |
-
|
| 16 |
-
// 私有静态属性:存储转换器类
|
| 17 |
-
static #converterClasses = new Map();
|
| 18 |
-
|
| 19 |
-
/**
|
| 20 |
-
* 注册转换器类
|
| 21 |
-
* @param {string} protocolPrefix - 协议前缀
|
| 22 |
-
* @param {Class} ConverterClass - 转换器类
|
| 23 |
-
*/
|
| 24 |
-
static registerConverter(protocolPrefix, ConverterClass) {
|
| 25 |
-
this.#converterClasses.set(protocolPrefix, ConverterClass);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/**
|
| 29 |
-
* 获取转换器实例(带缓存)
|
| 30 |
-
* @param {string} protocolPrefix - 协议前缀
|
| 31 |
-
* @returns {BaseConverter} 转换器实例
|
| 32 |
-
*/
|
| 33 |
-
static getConverter(protocolPrefix) {
|
| 34 |
-
// 检查缓存
|
| 35 |
-
if (this.#converters.has(protocolPrefix)) {
|
| 36 |
-
return this.#converters.get(protocolPrefix);
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
// 创建新实例
|
| 40 |
-
const converter = this.createConverter(protocolPrefix);
|
| 41 |
-
|
| 42 |
-
// 缓存实例
|
| 43 |
-
if (converter) {
|
| 44 |
-
this.#converters.set(protocolPrefix, converter);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
return converter;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
/**
|
| 51 |
-
* 创建转换器实例
|
| 52 |
-
* @param {string} protocolPrefix - 协议前缀
|
| 53 |
-
* @returns {BaseConverter} 转换器实例
|
| 54 |
-
*/
|
| 55 |
-
static createConverter(protocolPrefix) {
|
| 56 |
-
const ConverterClass = this.#converterClasses.get(protocolPrefix);
|
| 57 |
-
|
| 58 |
-
if (!ConverterClass) {
|
| 59 |
-
throw new Error(`No converter registered for protocol: ${protocolPrefix}`);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
return new ConverterClass();
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
/**
|
| 66 |
-
* 清除所有缓存的转换器
|
| 67 |
-
*/
|
| 68 |
-
static clearCache() {
|
| 69 |
-
this.#converters.clear();
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
/**
|
| 73 |
-
* 清除特定协议的转换器缓存
|
| 74 |
-
* @param {string} protocolPrefix - 协议前缀
|
| 75 |
-
*/
|
| 76 |
-
static clearConverterCache(protocolPrefix) {
|
| 77 |
-
this.#converters.delete(protocolPrefix);
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
/**
|
| 81 |
-
* 获取所有已注册的协议
|
| 82 |
-
* @returns {Array<string>} 协议前缀数组
|
| 83 |
-
*/
|
| 84 |
-
static getRegisteredProtocols() {
|
| 85 |
-
return Array.from(this.#converterClasses.keys());
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
/**
|
| 89 |
-
* 检查协议是否已注册
|
| 90 |
-
* @param {string} protocolPrefix - 协议前缀
|
| 91 |
-
* @returns {boolean} 是否已注册
|
| 92 |
-
*/
|
| 93 |
-
static isProtocolRegistered(protocolPrefix) {
|
| 94 |
-
return this.#converterClasses.has(protocolPrefix);
|
| 95 |
-
}
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
/**
|
| 99 |
-
* 内容处理器工厂
|
| 100 |
-
*/
|
| 101 |
-
export class ContentProcessorFactory {
|
| 102 |
-
static #processors = new Map();
|
| 103 |
-
|
| 104 |
-
/**
|
| 105 |
-
* 获取内容处理器
|
| 106 |
-
* @param {string} sourceFormat - 源格式
|
| 107 |
-
* @param {string} targetFormat - 目标格式
|
| 108 |
-
* @returns {ContentProcessor} 内容处理器实例
|
| 109 |
-
*/
|
| 110 |
-
static getProcessor(sourceFormat, targetFormat) {
|
| 111 |
-
const key = `${sourceFormat}_to_${targetFormat}`;
|
| 112 |
-
|
| 113 |
-
if (!this.#processors.has(key)) {
|
| 114 |
-
this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat));
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
return this.#processors.get(key);
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
/**
|
| 121 |
-
* 创建内容处理器
|
| 122 |
-
* @param {string} sourceFormat - 源格式
|
| 123 |
-
* @param {string} targetFormat - 目标格式
|
| 124 |
-
* @returns {ContentProcessor} 内容处理器实例
|
| 125 |
-
*/
|
| 126 |
-
static createProcessor(sourceFormat, targetFormat) {
|
| 127 |
-
// 这里返回null,实际使用时需要导入具体的处理器类
|
| 128 |
-
// 为了避免循环依赖,处理器类应该在使用时动态导入
|
| 129 |
-
logger.warn(`Content processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
|
| 130 |
-
return null;
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
/**
|
| 134 |
-
* 清除所有缓存的处理器
|
| 135 |
-
*/
|
| 136 |
-
static clearCache() {
|
| 137 |
-
this.#processors.clear();
|
| 138 |
-
}
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
/**
|
| 142 |
-
* 工具处理器工厂
|
| 143 |
-
*/
|
| 144 |
-
export class ToolProcessorFactory {
|
| 145 |
-
static #processors = new Map();
|
| 146 |
-
|
| 147 |
-
/**
|
| 148 |
-
* 获取工具处理器
|
| 149 |
-
* @param {string} sourceFormat - 源格式
|
| 150 |
-
* @param {string} targetFormat - 目标格式
|
| 151 |
-
* @returns {ToolProcessor} 工具处理器实例
|
| 152 |
-
*/
|
| 153 |
-
static getProcessor(sourceFormat, targetFormat) {
|
| 154 |
-
const key = `${sourceFormat}_to_${targetFormat}`;
|
| 155 |
-
|
| 156 |
-
if (!this.#processors.has(key)) {
|
| 157 |
-
this.#processors.set(key, this.createProcessor(sourceFormat, targetFormat));
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
return this.#processors.get(key);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
/**
|
| 164 |
-
* 创建工具处理器
|
| 165 |
-
* @param {string} sourceFormat - 源格式
|
| 166 |
-
* @param {string} targetFormat - 目标格式
|
| 167 |
-
* @returns {ToolProcessor} 工具处理器实例
|
| 168 |
-
*/
|
| 169 |
-
static createProcessor(sourceFormat, targetFormat) {
|
| 170 |
-
logger.warn(`Tool processor for ${sourceFormat} to ${targetFormat} not yet implemented`);
|
| 171 |
-
return null;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
/**
|
| 175 |
-
* 清除所有缓存的处理器
|
| 176 |
-
*/
|
| 177 |
-
static clearCache() {
|
| 178 |
-
this.#processors.clear();
|
| 179 |
-
}
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
// 导出工厂类
|
| 183 |
-
export default ConverterFactory;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/register-converters.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 转换器注册模块
|
| 3 |
-
* 用于注册所有转换器到工厂,避免循环依赖问题
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { MODEL_PROTOCOL_PREFIX } from '../utils/common.js';
|
| 7 |
-
import { ConverterFactory } from './ConverterFactory.js';
|
| 8 |
-
import { OpenAIConverter } from './strategies/OpenAIConverter.js';
|
| 9 |
-
import { OpenAIResponsesConverter } from './strategies/OpenAIResponsesConverter.js';
|
| 10 |
-
import { ClaudeConverter } from './strategies/ClaudeConverter.js';
|
| 11 |
-
import { GeminiConverter } from './strategies/GeminiConverter.js';
|
| 12 |
-
import { CodexConverter } from './strategies/CodexConverter.js';
|
| 13 |
-
import { GrokConverter } from './strategies/GrokConverter.js';
|
| 14 |
-
|
| 15 |
-
/**
|
| 16 |
-
* 注册所有转换器到工厂
|
| 17 |
-
* 此函数应在应用启动时调用一次
|
| 18 |
-
*/
|
| 19 |
-
export function registerAllConverters() {
|
| 20 |
-
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI, OpenAIConverter);
|
| 21 |
-
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES, OpenAIResponsesConverter);
|
| 22 |
-
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.CLAUDE, ClaudeConverter);
|
| 23 |
-
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.GEMINI, GeminiConverter);
|
| 24 |
-
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.CODEX, CodexConverter);
|
| 25 |
-
ConverterFactory.registerConverter(MODEL_PROTOCOL_PREFIX.GROK, GrokConverter);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
// 自动注册所有转换器
|
| 29 |
-
registerAllConverters();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/strategies/ClaudeConverter.js
DELETED
|
@@ -1,2234 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Claude转换器
|
| 3 |
-
* 处理Claude(Anthropic)协议与其他协议之间的转换
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
-
import logger from '../../utils/logger.js';
|
| 8 |
-
import { BaseConverter } from '../BaseConverter.js';
|
| 9 |
-
import {
|
| 10 |
-
checkAndAssignOrDefault,
|
| 11 |
-
cleanJsonSchemaProperties as cleanJsonSchema,
|
| 12 |
-
determineReasoningEffortFromBudget,
|
| 13 |
-
OPENAI_DEFAULT_MAX_TOKENS,
|
| 14 |
-
OPENAI_DEFAULT_TEMPERATURE,
|
| 15 |
-
OPENAI_DEFAULT_TOP_P,
|
| 16 |
-
GEMINI_DEFAULT_MAX_TOKENS,
|
| 17 |
-
GEMINI_DEFAULT_TEMPERATURE,
|
| 18 |
-
GEMINI_DEFAULT_TOP_P,
|
| 19 |
-
GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
|
| 20 |
-
GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT
|
| 21 |
-
} from '../utils.js';
|
| 22 |
-
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
| 23 |
-
import {
|
| 24 |
-
generateResponseCreated,
|
| 25 |
-
generateResponseInProgress,
|
| 26 |
-
generateOutputItemAdded,
|
| 27 |
-
generateContentPartAdded,
|
| 28 |
-
generateOutputTextDone,
|
| 29 |
-
generateContentPartDone,
|
| 30 |
-
generateOutputItemDone,
|
| 31 |
-
generateResponseCompleted
|
| 32 |
-
} from '../../providers/openai/openai-responses-core.mjs';
|
| 33 |
-
|
| 34 |
-
/**
|
| 35 |
-
* Claude转换器类
|
| 36 |
-
* 实现Claude协议到其他协议的转换
|
| 37 |
-
*/
|
| 38 |
-
export class ClaudeConverter extends BaseConverter {
|
| 39 |
-
constructor() {
|
| 40 |
-
super('claude');
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
/**
|
| 44 |
-
* 转换请求
|
| 45 |
-
*/
|
| 46 |
-
convertRequest(data, targetProtocol) {
|
| 47 |
-
switch (targetProtocol) {
|
| 48 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 49 |
-
return this.toOpenAIRequest(data);
|
| 50 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 51 |
-
return this.toGeminiRequest(data);
|
| 52 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 53 |
-
return this.toOpenAIResponsesRequest(data);
|
| 54 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 55 |
-
return this.toCodexRequest(data);
|
| 56 |
-
case MODEL_PROTOCOL_PREFIX.GROK:
|
| 57 |
-
return this.toGrokRequest(data);
|
| 58 |
-
default:
|
| 59 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
/**
|
| 64 |
-
* 转换响应
|
| 65 |
-
*/
|
| 66 |
-
convertResponse(data, targetProtocol, model) {
|
| 67 |
-
switch (targetProtocol) {
|
| 68 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 69 |
-
return this.toOpenAIResponse(data, model);
|
| 70 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 71 |
-
return this.toGeminiResponse(data, model);
|
| 72 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 73 |
-
return this.toOpenAIResponsesResponse(data, model);
|
| 74 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 75 |
-
return this.toCodexResponse(data, model);
|
| 76 |
-
default:
|
| 77 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
/**
|
| 82 |
-
* 转换流式响应块
|
| 83 |
-
*/
|
| 84 |
-
convertStreamChunk(chunk, targetProtocol, model) {
|
| 85 |
-
switch (targetProtocol) {
|
| 86 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 87 |
-
return this.toOpenAIStreamChunk(chunk, model);
|
| 88 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 89 |
-
return this.toGeminiStreamChunk(chunk, model);
|
| 90 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 91 |
-
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
| 92 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 93 |
-
return this.toCodexStreamChunk(chunk, model);
|
| 94 |
-
default:
|
| 95 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 96 |
-
}
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
/**
|
| 100 |
-
* 转换模型列表
|
| 101 |
-
*/
|
| 102 |
-
convertModelList(data, targetProtocol) {
|
| 103 |
-
switch (targetProtocol) {
|
| 104 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 105 |
-
return this.toOpenAIModelList(data);
|
| 106 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 107 |
-
return this.toGeminiModelList(data);
|
| 108 |
-
default:
|
| 109 |
-
return data;
|
| 110 |
-
}
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
// =========================================================================
|
| 114 |
-
// Claude -> OpenAI 转换
|
| 115 |
-
// =========================================================================
|
| 116 |
-
|
| 117 |
-
/**
|
| 118 |
-
* Claude请求 -> OpenAI请求
|
| 119 |
-
*/
|
| 120 |
-
toOpenAIRequest(claudeRequest) {
|
| 121 |
-
const openaiMessages = [];
|
| 122 |
-
let systemMessageContent = '';
|
| 123 |
-
|
| 124 |
-
// 添加系统消息
|
| 125 |
-
if (claudeRequest.system) {
|
| 126 |
-
systemMessageContent = claudeRequest.system;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
// 处理消息
|
| 130 |
-
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
|
| 131 |
-
const tempOpenAIMessages = [];
|
| 132 |
-
for (const msg of claudeRequest.messages) {
|
| 133 |
-
const role = msg.role;
|
| 134 |
-
|
| 135 |
-
// 处理用户的工具结果消息
|
| 136 |
-
if (role === "user" && Array.isArray(msg.content)) {
|
| 137 |
-
const hasToolResult = msg.content.some(
|
| 138 |
-
item => item && typeof item === 'object' && item.type === "tool_result"
|
| 139 |
-
);
|
| 140 |
-
|
| 141 |
-
if (hasToolResult) {
|
| 142 |
-
for (const item of msg.content) {
|
| 143 |
-
if (item && typeof item === 'object' && item.type === "tool_result") {
|
| 144 |
-
const toolUseId = item.tool_use_id || item.id || "";
|
| 145 |
-
let contentStr = item.content || "";
|
| 146 |
-
if (typeof contentStr === 'object') {
|
| 147 |
-
contentStr = JSON.stringify(contentStr);
|
| 148 |
-
} else {
|
| 149 |
-
contentStr = String(contentStr);
|
| 150 |
-
}
|
| 151 |
-
tempOpenAIMessages.push({
|
| 152 |
-
role: "tool",
|
| 153 |
-
tool_call_id: toolUseId,
|
| 154 |
-
content: contentStr,
|
| 155 |
-
});
|
| 156 |
-
}
|
| 157 |
-
}
|
| 158 |
-
continue;
|
| 159 |
-
}
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
// 处理assistant消息中的工具调用
|
| 163 |
-
if (role === "assistant" && Array.isArray(msg.content) && msg.content.length > 0) {
|
| 164 |
-
const firstPart = msg.content[0];
|
| 165 |
-
if (firstPart.type === "tool_use") {
|
| 166 |
-
const funcName = firstPart.name || "";
|
| 167 |
-
const funcArgs = firstPart.input || {};
|
| 168 |
-
tempOpenAIMessages.push({
|
| 169 |
-
role: "assistant",
|
| 170 |
-
content: '',
|
| 171 |
-
tool_calls: [
|
| 172 |
-
{
|
| 173 |
-
id: firstPart.id || `call_${funcName}_1`,
|
| 174 |
-
type: "function",
|
| 175 |
-
function: {
|
| 176 |
-
name: funcName,
|
| 177 |
-
arguments: JSON.stringify(funcArgs)
|
| 178 |
-
},
|
| 179 |
-
index: firstPart.index || 0
|
| 180 |
-
}
|
| 181 |
-
]
|
| 182 |
-
});
|
| 183 |
-
continue;
|
| 184 |
-
}
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
// 普通文本消息
|
| 188 |
-
const contentConverted = this.processClaudeContentToOpenAIContent(msg.content || "");
|
| 189 |
-
if (contentConverted && (Array.isArray(contentConverted) ? contentConverted.length > 0 : contentConverted.trim().length > 0)) {
|
| 190 |
-
tempOpenAIMessages.push({
|
| 191 |
-
role: role,
|
| 192 |
-
content: contentConverted
|
| 193 |
-
});
|
| 194 |
-
}
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
// OpenAI兼容性校验
|
| 198 |
-
const validatedMessages = [];
|
| 199 |
-
for (let idx = 0; idx < tempOpenAIMessages.length; idx++) {
|
| 200 |
-
const m = tempOpenAIMessages[idx];
|
| 201 |
-
if (m.role === "assistant" && m.tool_calls) {
|
| 202 |
-
const callIds = m.tool_calls.map(tc => tc.id).filter(id => id);
|
| 203 |
-
let unmatched = new Set(callIds);
|
| 204 |
-
for (let laterIdx = idx + 1; laterIdx < tempOpenAIMessages.length; laterIdx++) {
|
| 205 |
-
const later = tempOpenAIMessages[laterIdx];
|
| 206 |
-
if (later.role === "tool" && unmatched.has(later.tool_call_id)) {
|
| 207 |
-
unmatched.delete(later.tool_call_id);
|
| 208 |
-
}
|
| 209 |
-
if (unmatched.size === 0) break;
|
| 210 |
-
}
|
| 211 |
-
if (unmatched.size > 0) {
|
| 212 |
-
m.tool_calls = m.tool_calls.filter(tc => !unmatched.has(tc.id));
|
| 213 |
-
if (m.tool_calls.length === 0) {
|
| 214 |
-
delete m.tool_calls;
|
| 215 |
-
if (m.content === null) m.content = "";
|
| 216 |
-
}
|
| 217 |
-
}
|
| 218 |
-
}
|
| 219 |
-
validatedMessages.push(m);
|
| 220 |
-
}
|
| 221 |
-
openaiMessages.push(...validatedMessages);
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
const openaiRequest = {
|
| 225 |
-
model: claudeRequest.model,
|
| 226 |
-
messages: openaiMessages,
|
| 227 |
-
max_tokens: checkAndAssignOrDefault(claudeRequest.max_tokens, OPENAI_DEFAULT_MAX_TOKENS),
|
| 228 |
-
temperature: checkAndAssignOrDefault(claudeRequest.temperature, OPENAI_DEFAULT_TEMPERATURE),
|
| 229 |
-
top_p: checkAndAssignOrDefault(claudeRequest.top_p, OPENAI_DEFAULT_TOP_P),
|
| 230 |
-
stream: claudeRequest.stream,
|
| 231 |
-
};
|
| 232 |
-
|
| 233 |
-
// 处理工具
|
| 234 |
-
if (claudeRequest.tools) {
|
| 235 |
-
const openaiTools = [];
|
| 236 |
-
for (const tool of claudeRequest.tools) {
|
| 237 |
-
openaiTools.push({
|
| 238 |
-
type: "function",
|
| 239 |
-
function: {
|
| 240 |
-
name: tool.name || "",
|
| 241 |
-
description: tool.description || "",
|
| 242 |
-
parameters: cleanJsonSchema(tool.input_schema || {})
|
| 243 |
-
}
|
| 244 |
-
});
|
| 245 |
-
}
|
| 246 |
-
openaiRequest.tools = openaiTools;
|
| 247 |
-
openaiRequest.tool_choice = "auto";
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
// 处理thinking转换
|
| 251 |
-
if (claudeRequest.thinking && claudeRequest.thinking.type === "enabled") {
|
| 252 |
-
const budgetTokens = claudeRequest.thinking.budget_tokens;
|
| 253 |
-
const reasoningEffort = determineReasoningEffortFromBudget(budgetTokens);
|
| 254 |
-
openaiRequest.reasoning_effort = reasoningEffort;
|
| 255 |
-
|
| 256 |
-
let maxCompletionTokens = null;
|
| 257 |
-
if (claudeRequest.max_tokens !== undefined) {
|
| 258 |
-
maxCompletionTokens = claudeRequest.max_tokens;
|
| 259 |
-
delete openaiRequest.max_tokens;
|
| 260 |
-
} else {
|
| 261 |
-
const envMaxTokens = process.env.OPENAI_REASONING_MAX_TOKENS;
|
| 262 |
-
if (envMaxTokens) {
|
| 263 |
-
try {
|
| 264 |
-
maxCompletionTokens = parseInt(envMaxTokens, 10);
|
| 265 |
-
} catch (e) {
|
| 266 |
-
logger.warn(`Invalid OPENAI_REASONING_MAX_TOKENS value '${envMaxTokens}'`);
|
| 267 |
-
}
|
| 268 |
-
}
|
| 269 |
-
if (!envMaxTokens) {
|
| 270 |
-
throw new Error("For OpenAI reasoning models, max_completion_tokens is required.");
|
| 271 |
-
}
|
| 272 |
-
}
|
| 273 |
-
openaiRequest.max_completion_tokens = maxCompletionTokens;
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
// 添加系统消息
|
| 277 |
-
if (systemMessageContent) {
|
| 278 |
-
let stringifiedSystemMessageContent = systemMessageContent;
|
| 279 |
-
if (Array.isArray(systemMessageContent)) {
|
| 280 |
-
stringifiedSystemMessageContent = systemMessageContent.map(item =>
|
| 281 |
-
typeof item === 'string' ? item : item.text).join('\n');
|
| 282 |
-
}
|
| 283 |
-
openaiRequest.messages.unshift({ role: 'system', content: stringifiedSystemMessageContent });
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
return openaiRequest;
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
/**
|
| 290 |
-
* Claude响应 -> OpenAI响应
|
| 291 |
-
*/
|
| 292 |
-
toOpenAIResponse(claudeResponse, model) {
|
| 293 |
-
if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) {
|
| 294 |
-
return {
|
| 295 |
-
id: `chatcmpl-${uuidv4()}`,
|
| 296 |
-
object: "chat.completion",
|
| 297 |
-
created: Math.floor(Date.now() / 1000),
|
| 298 |
-
model: model,
|
| 299 |
-
choices: [{
|
| 300 |
-
index: 0,
|
| 301 |
-
message: {
|
| 302 |
-
role: "assistant",
|
| 303 |
-
content: "",
|
| 304 |
-
},
|
| 305 |
-
finish_reason: "stop",
|
| 306 |
-
}],
|
| 307 |
-
usage: {
|
| 308 |
-
prompt_tokens: claudeResponse.usage?.input_tokens || 0,
|
| 309 |
-
completion_tokens: claudeResponse.usage?.output_tokens || 0,
|
| 310 |
-
total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0),
|
| 311 |
-
},
|
| 312 |
-
};
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
// Extract thinking blocks into OpenAI-style `reasoning_content`.
|
| 316 |
-
let reasoningContent = '';
|
| 317 |
-
if (Array.isArray(claudeResponse.content)) {
|
| 318 |
-
for (const block of claudeResponse.content) {
|
| 319 |
-
if (!block || typeof block !== 'object') continue;
|
| 320 |
-
if (block.type === 'thinking') {
|
| 321 |
-
reasoningContent += (block.thinking ?? block.text ?? '');
|
| 322 |
-
}
|
| 323 |
-
}
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
// 检查是否包含 tool_use
|
| 327 |
-
const hasToolUse = claudeResponse.content.some(block => block && block.type === 'tool_use');
|
| 328 |
-
|
| 329 |
-
let message = {
|
| 330 |
-
role: "assistant",
|
| 331 |
-
content: null
|
| 332 |
-
};
|
| 333 |
-
|
| 334 |
-
if (hasToolUse) {
|
| 335 |
-
// 处理包含工具调用的响应
|
| 336 |
-
const toolCalls = [];
|
| 337 |
-
let textContent = '';
|
| 338 |
-
|
| 339 |
-
for (const block of claudeResponse.content) {
|
| 340 |
-
if (!block) continue;
|
| 341 |
-
|
| 342 |
-
if (block.type === 'text') {
|
| 343 |
-
textContent += block.text || '';
|
| 344 |
-
} else if (block.type === 'tool_use') {
|
| 345 |
-
toolCalls.push({
|
| 346 |
-
id: block.id || `call_${block.name}_${Date.now()}`,
|
| 347 |
-
type: "function",
|
| 348 |
-
function: {
|
| 349 |
-
name: block.name || '',
|
| 350 |
-
arguments: JSON.stringify(block.input || {})
|
| 351 |
-
}
|
| 352 |
-
});
|
| 353 |
-
}
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
message.content = textContent || null;
|
| 357 |
-
if (toolCalls.length > 0) {
|
| 358 |
-
message.tool_calls = toolCalls;
|
| 359 |
-
}
|
| 360 |
-
} else {
|
| 361 |
-
// 处理普通文本响应
|
| 362 |
-
message.content = this.processClaudeResponseContent(claudeResponse.content);
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
if (reasoningContent) {
|
| 366 |
-
message.reasoning_content = reasoningContent;
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
// 处理 finish_reason
|
| 370 |
-
let finishReason = 'stop';
|
| 371 |
-
if (claudeResponse.stop_reason === 'end_turn') {
|
| 372 |
-
finishReason = 'stop';
|
| 373 |
-
} else if (claudeResponse.stop_reason === 'max_tokens') {
|
| 374 |
-
finishReason = 'length';
|
| 375 |
-
} else if (claudeResponse.stop_reason === 'tool_use') {
|
| 376 |
-
finishReason = 'tool_calls';
|
| 377 |
-
} else if (claudeResponse.stop_reason) {
|
| 378 |
-
finishReason = claudeResponse.stop_reason;
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
return {
|
| 382 |
-
id: `chatcmpl-${uuidv4()}`,
|
| 383 |
-
object: "chat.completion",
|
| 384 |
-
created: Math.floor(Date.now() / 1000),
|
| 385 |
-
model: model,
|
| 386 |
-
choices: [{
|
| 387 |
-
index: 0,
|
| 388 |
-
message: message,
|
| 389 |
-
finish_reason: finishReason,
|
| 390 |
-
}],
|
| 391 |
-
usage: {
|
| 392 |
-
prompt_tokens: claudeResponse.usage?.input_tokens || 0,
|
| 393 |
-
completion_tokens: claudeResponse.usage?.output_tokens || 0,
|
| 394 |
-
total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0),
|
| 395 |
-
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0,
|
| 396 |
-
prompt_tokens_details: {
|
| 397 |
-
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0
|
| 398 |
-
}
|
| 399 |
-
},
|
| 400 |
-
};
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
/**
|
| 404 |
-
* Claude流式响应 -> OpenAI流式响应
|
| 405 |
-
*/
|
| 406 |
-
toOpenAIStreamChunk(claudeChunk, model) {
|
| 407 |
-
if (!claudeChunk) return null;
|
| 408 |
-
|
| 409 |
-
// 处理 Claude 流式事件
|
| 410 |
-
const chunkId = `chatcmpl-${uuidv4()}`;
|
| 411 |
-
const timestamp = Math.floor(Date.now() / 1000);
|
| 412 |
-
|
| 413 |
-
// message_start 事件
|
| 414 |
-
if (claudeChunk.type === 'message_start') {
|
| 415 |
-
return {
|
| 416 |
-
id: chunkId,
|
| 417 |
-
object: "chat.completion.chunk",
|
| 418 |
-
created: timestamp,
|
| 419 |
-
model: model,
|
| 420 |
-
system_fingerprint: "",
|
| 421 |
-
choices: [{
|
| 422 |
-
index: 0,
|
| 423 |
-
delta: {
|
| 424 |
-
role: "assistant",
|
| 425 |
-
content: ""
|
| 426 |
-
},
|
| 427 |
-
finish_reason: null
|
| 428 |
-
}],
|
| 429 |
-
usage: {
|
| 430 |
-
prompt_tokens: claudeChunk.message?.usage?.input_tokens || 0,
|
| 431 |
-
completion_tokens: 0,
|
| 432 |
-
total_tokens: claudeChunk.message?.usage?.input_tokens || 0,
|
| 433 |
-
cached_tokens: claudeChunk.message?.usage?.cache_read_input_tokens || 0
|
| 434 |
-
}
|
| 435 |
-
};
|
| 436 |
-
}
|
| 437 |
-
|
| 438 |
-
// content_block_start 事件
|
| 439 |
-
if (claudeChunk.type === 'content_block_start') {
|
| 440 |
-
const contentBlock = claudeChunk.content_block;
|
| 441 |
-
|
| 442 |
-
// 处理 tool_use 类型
|
| 443 |
-
if (contentBlock && contentBlock.type === 'tool_use') {
|
| 444 |
-
return {
|
| 445 |
-
id: chunkId,
|
| 446 |
-
object: "chat.completion.chunk",
|
| 447 |
-
created: timestamp,
|
| 448 |
-
model: model,
|
| 449 |
-
system_fingerprint: "",
|
| 450 |
-
choices: [{
|
| 451 |
-
index: 0,
|
| 452 |
-
delta: {
|
| 453 |
-
tool_calls: [{
|
| 454 |
-
index: claudeChunk.index || 0,
|
| 455 |
-
id: contentBlock.id,
|
| 456 |
-
type: "function",
|
| 457 |
-
function: {
|
| 458 |
-
name: contentBlock.name,
|
| 459 |
-
arguments: ""
|
| 460 |
-
}
|
| 461 |
-
}]
|
| 462 |
-
},
|
| 463 |
-
finish_reason: null
|
| 464 |
-
}]
|
| 465 |
-
};
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
// 处理 text 类型
|
| 469 |
-
return {
|
| 470 |
-
id: chunkId,
|
| 471 |
-
object: "chat.completion.chunk",
|
| 472 |
-
created: timestamp,
|
| 473 |
-
model: model,
|
| 474 |
-
system_fingerprint: "",
|
| 475 |
-
choices: [{
|
| 476 |
-
index: 0,
|
| 477 |
-
delta: {
|
| 478 |
-
content: ""
|
| 479 |
-
},
|
| 480 |
-
finish_reason: null
|
| 481 |
-
}]
|
| 482 |
-
};
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
// content_block_delta 事件
|
| 486 |
-
if (claudeChunk.type === 'content_block_delta') {
|
| 487 |
-
const delta = claudeChunk.delta;
|
| 488 |
-
|
| 489 |
-
// 处理 text_delta
|
| 490 |
-
if (delta && delta.type === 'text_delta') {
|
| 491 |
-
return {
|
| 492 |
-
id: chunkId,
|
| 493 |
-
object: "chat.completion.chunk",
|
| 494 |
-
created: timestamp,
|
| 495 |
-
model: model,
|
| 496 |
-
system_fingerprint: "",
|
| 497 |
-
choices: [{
|
| 498 |
-
index: 0,
|
| 499 |
-
delta: {
|
| 500 |
-
content: delta.text || ""
|
| 501 |
-
},
|
| 502 |
-
finish_reason: null
|
| 503 |
-
}]
|
| 504 |
-
};
|
| 505 |
-
}
|
| 506 |
-
|
| 507 |
-
// 处理 thinking_delta (推理内容)
|
| 508 |
-
if (delta && delta.type === 'thinking_delta') {
|
| 509 |
-
return {
|
| 510 |
-
id: chunkId,
|
| 511 |
-
object: "chat.completion.chunk",
|
| 512 |
-
created: timestamp,
|
| 513 |
-
model: model,
|
| 514 |
-
system_fingerprint: "",
|
| 515 |
-
choices: [{
|
| 516 |
-
index: 0,
|
| 517 |
-
delta: {
|
| 518 |
-
reasoning_content: delta.thinking || ""
|
| 519 |
-
},
|
| 520 |
-
finish_reason: null
|
| 521 |
-
}]
|
| 522 |
-
};
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
// 处理 input_json_delta (tool arguments)
|
| 526 |
-
if (delta && delta.type === 'input_json_delta') {
|
| 527 |
-
return {
|
| 528 |
-
id: chunkId,
|
| 529 |
-
object: "chat.completion.chunk",
|
| 530 |
-
created: timestamp,
|
| 531 |
-
model: model,
|
| 532 |
-
system_fingerprint: "",
|
| 533 |
-
choices: [{
|
| 534 |
-
index: 0,
|
| 535 |
-
delta: {
|
| 536 |
-
tool_calls: [{
|
| 537 |
-
index: claudeChunk.index || 0,
|
| 538 |
-
function: {
|
| 539 |
-
arguments: delta.partial_json || ""
|
| 540 |
-
}
|
| 541 |
-
}]
|
| 542 |
-
},
|
| 543 |
-
finish_reason: null
|
| 544 |
-
}]
|
| 545 |
-
};
|
| 546 |
-
}
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
// content_block_stop 事件
|
| 550 |
-
if (claudeChunk.type === 'content_block_stop') {
|
| 551 |
-
return {
|
| 552 |
-
id: chunkId,
|
| 553 |
-
object: "chat.completion.chunk",
|
| 554 |
-
created: timestamp,
|
| 555 |
-
model: model,
|
| 556 |
-
system_fingerprint: "",
|
| 557 |
-
choices: [{
|
| 558 |
-
index: 0,
|
| 559 |
-
delta: {},
|
| 560 |
-
finish_reason: null
|
| 561 |
-
}]
|
| 562 |
-
};
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
// message_delta 事件
|
| 566 |
-
if (claudeChunk.type === 'message_delta') {
|
| 567 |
-
const stopReason = claudeChunk.delta?.stop_reason;
|
| 568 |
-
const finishReason = stopReason === 'end_turn' ? 'stop' :
|
| 569 |
-
stopReason === 'max_tokens' ? 'length' :
|
| 570 |
-
stopReason === 'tool_use' ? 'tool_calls' :
|
| 571 |
-
stopReason || 'stop';
|
| 572 |
-
|
| 573 |
-
const chunk = {
|
| 574 |
-
id: chunkId,
|
| 575 |
-
object: "chat.completion.chunk",
|
| 576 |
-
created: timestamp,
|
| 577 |
-
model: model,
|
| 578 |
-
system_fingerprint: "",
|
| 579 |
-
choices: [{
|
| 580 |
-
index: 0,
|
| 581 |
-
delta: {},
|
| 582 |
-
finish_reason: finishReason
|
| 583 |
-
}]
|
| 584 |
-
};
|
| 585 |
-
|
| 586 |
-
if(claudeChunk.usage){
|
| 587 |
-
chunk.usage = {
|
| 588 |
-
prompt_tokens: claudeChunk.usage.input_tokens || 0,
|
| 589 |
-
completion_tokens: claudeChunk.usage.output_tokens || 0,
|
| 590 |
-
total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0),
|
| 591 |
-
cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0,
|
| 592 |
-
prompt_tokens_details: {
|
| 593 |
-
cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0
|
| 594 |
-
}
|
| 595 |
-
};
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
return chunk;
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
// message_stop 事件
|
| 602 |
-
if (claudeChunk.type === 'message_stop') {
|
| 603 |
-
return null;
|
| 604 |
-
// const chunk = {
|
| 605 |
-
// id: chunkId,
|
| 606 |
-
// object: "chat.completion.chunk",
|
| 607 |
-
// created: timestamp,
|
| 608 |
-
// model: model,
|
| 609 |
-
// system_fingerprint: "",
|
| 610 |
-
// choices: [{
|
| 611 |
-
// index: 0,
|
| 612 |
-
// delta: {},
|
| 613 |
-
// finish_reason: 'stop'
|
| 614 |
-
// }]
|
| 615 |
-
// };
|
| 616 |
-
// return chunk;
|
| 617 |
-
}
|
| 618 |
-
|
| 619 |
-
// 兼容旧格式:如果是字符串,直接作为文本内容
|
| 620 |
-
if (typeof claudeChunk === 'string') {
|
| 621 |
-
return {
|
| 622 |
-
id: chunkId,
|
| 623 |
-
object: "chat.completion.chunk",
|
| 624 |
-
created: timestamp,
|
| 625 |
-
model: model,
|
| 626 |
-
system_fingerprint: "",
|
| 627 |
-
choices: [{
|
| 628 |
-
index: 0,
|
| 629 |
-
delta: {
|
| 630 |
-
content: claudeChunk
|
| 631 |
-
},
|
| 632 |
-
finish_reason: null
|
| 633 |
-
}]
|
| 634 |
-
};
|
| 635 |
-
}
|
| 636 |
-
|
| 637 |
-
return null;
|
| 638 |
-
}
|
| 639 |
-
|
| 640 |
-
/**
|
| 641 |
-
* Claude模型列表 -> OpenAI模型列表
|
| 642 |
-
*/
|
| 643 |
-
toOpenAIModelList(claudeModels) {
|
| 644 |
-
return {
|
| 645 |
-
object: "list",
|
| 646 |
-
data: claudeModels.models.map(m => {
|
| 647 |
-
const modelId = m.id || m.name;
|
| 648 |
-
return {
|
| 649 |
-
id: modelId,
|
| 650 |
-
object: "model",
|
| 651 |
-
created: Math.floor(Date.now() / 1000),
|
| 652 |
-
owned_by: "anthropic",
|
| 653 |
-
display_name: modelId,
|
| 654 |
-
};
|
| 655 |
-
}),
|
| 656 |
-
};
|
| 657 |
-
}
|
| 658 |
-
|
| 659 |
-
/**
|
| 660 |
-
* 将 Claude 模型列表转换为 Gemini 模型列表
|
| 661 |
-
*/
|
| 662 |
-
toGeminiModelList(claudeModels) {
|
| 663 |
-
const models = claudeModels.models || [];
|
| 664 |
-
return {
|
| 665 |
-
models: models.map(m => ({
|
| 666 |
-
name: `models/${m.id || m.name}`,
|
| 667 |
-
version: m.version || "1.0.0",
|
| 668 |
-
displayName: m.displayName || m.id || m.name,
|
| 669 |
-
description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`,
|
| 670 |
-
inputTokenLimit: m.inputTokenLimit || GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
|
| 671 |
-
outputTokenLimit: m.outputTokenLimit || GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT,
|
| 672 |
-
supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
|
| 673 |
-
}))
|
| 674 |
-
};
|
| 675 |
-
}
|
| 676 |
-
|
| 677 |
-
/**
|
| 678 |
-
* 处理Claude内容到OpenAI格式
|
| 679 |
-
*/
|
| 680 |
-
processClaudeContentToOpenAIContent(content) {
|
| 681 |
-
if (!content) return [];
|
| 682 |
-
|
| 683 |
-
// 如果是字符串,直接转换为 OpenAI 的文本块格式
|
| 684 |
-
if (typeof content === 'string') {
|
| 685 |
-
return [{
|
| 686 |
-
type: 'text',
|
| 687 |
-
text: content
|
| 688 |
-
}];
|
| 689 |
-
}
|
| 690 |
-
|
| 691 |
-
if (!Array.isArray(content)) return [];
|
| 692 |
-
|
| 693 |
-
const contentArray = [];
|
| 694 |
-
|
| 695 |
-
content.forEach(block => {
|
| 696 |
-
if (!block) return;
|
| 697 |
-
|
| 698 |
-
switch (block.type) {
|
| 699 |
-
case 'text':
|
| 700 |
-
if (block.text) {
|
| 701 |
-
contentArray.push({
|
| 702 |
-
type: 'text',
|
| 703 |
-
text: block.text
|
| 704 |
-
});
|
| 705 |
-
}
|
| 706 |
-
break;
|
| 707 |
-
|
| 708 |
-
case 'image':
|
| 709 |
-
if (block.source && block.source.type === 'base64') {
|
| 710 |
-
contentArray.push({
|
| 711 |
-
type: 'image_url',
|
| 712 |
-
image_url: {
|
| 713 |
-
url: `data:${block.source.media_type};base64,${block.source.data}`
|
| 714 |
-
}
|
| 715 |
-
});
|
| 716 |
-
}
|
| 717 |
-
break;
|
| 718 |
-
|
| 719 |
-
case 'tool_use':
|
| 720 |
-
contentArray.push({
|
| 721 |
-
type: 'text',
|
| 722 |
-
text: `[Tool use: ${block.name}]`
|
| 723 |
-
});
|
| 724 |
-
break;
|
| 725 |
-
|
| 726 |
-
case 'tool_result':
|
| 727 |
-
contentArray.push({
|
| 728 |
-
type: 'text',
|
| 729 |
-
text: typeof block.content === 'string' ? block.content : JSON.stringify(block.content)
|
| 730 |
-
});
|
| 731 |
-
break;
|
| 732 |
-
|
| 733 |
-
default:
|
| 734 |
-
if (block.text) {
|
| 735 |
-
contentArray.push({
|
| 736 |
-
type: 'text',
|
| 737 |
-
text: block.text
|
| 738 |
-
});
|
| 739 |
-
}
|
| 740 |
-
}
|
| 741 |
-
});
|
| 742 |
-
|
| 743 |
-
return contentArray;
|
| 744 |
-
}
|
| 745 |
-
|
| 746 |
-
/**
|
| 747 |
-
* 处理Claude响应内容
|
| 748 |
-
*/
|
| 749 |
-
processClaudeResponseContent(content) {
|
| 750 |
-
if (!content) return '';
|
| 751 |
-
|
| 752 |
-
if (typeof content === 'string') return content;
|
| 753 |
-
|
| 754 |
-
if (!Array.isArray(content)) return '';
|
| 755 |
-
|
| 756 |
-
const contentArray = [];
|
| 757 |
-
|
| 758 |
-
content.forEach(block => {
|
| 759 |
-
if (!block) return;
|
| 760 |
-
|
| 761 |
-
switch (block.type) {
|
| 762 |
-
case 'text':
|
| 763 |
-
contentArray.push({
|
| 764 |
-
type: 'text',
|
| 765 |
-
text: block.text || ''
|
| 766 |
-
});
|
| 767 |
-
break;
|
| 768 |
-
|
| 769 |
-
case 'image':
|
| 770 |
-
if (block.source && block.source.type === 'base64') {
|
| 771 |
-
contentArray.push({
|
| 772 |
-
type: 'image_url',
|
| 773 |
-
image_url: {
|
| 774 |
-
url: `data:${block.source.media_type};base64,${block.source.data}`
|
| 775 |
-
}
|
| 776 |
-
});
|
| 777 |
-
}
|
| 778 |
-
break;
|
| 779 |
-
|
| 780 |
-
default:
|
| 781 |
-
if (block.text) {
|
| 782 |
-
contentArray.push({
|
| 783 |
-
type: 'text',
|
| 784 |
-
text: block.text
|
| 785 |
-
});
|
| 786 |
-
}
|
| 787 |
-
}
|
| 788 |
-
});
|
| 789 |
-
|
| 790 |
-
return contentArray.length === 1 && contentArray[0].type === 'text'
|
| 791 |
-
? contentArray[0].text
|
| 792 |
-
: contentArray;
|
| 793 |
-
}
|
| 794 |
-
|
| 795 |
-
// =========================================================================
|
| 796 |
-
// Claude -> Gemini 转换
|
| 797 |
-
// =========================================================================
|
| 798 |
-
|
| 799 |
-
// Gemini Claude thought signature constant
|
| 800 |
-
static GEMINI_CLAUDE_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
|
| 801 |
-
|
| 802 |
-
/**
|
| 803 |
-
* Claude请求 -> Gemini请求
|
| 804 |
-
*/
|
| 805 |
-
toGeminiRequest(claudeRequest) {
|
| 806 |
-
if (!claudeRequest || typeof claudeRequest !== 'object') {
|
| 807 |
-
logger.warn("Invalid claudeRequest provided to toGeminiRequest.");
|
| 808 |
-
return { contents: [] };
|
| 809 |
-
}
|
| 810 |
-
|
| 811 |
-
const geminiRequest = {
|
| 812 |
-
contents: []
|
| 813 |
-
};
|
| 814 |
-
|
| 815 |
-
// 处理系统指令 - 支持数组和字符串格式
|
| 816 |
-
if (claudeRequest.system) {
|
| 817 |
-
if (Array.isArray(claudeRequest.system)) {
|
| 818 |
-
// 数组格式的系统指令
|
| 819 |
-
const systemParts = [];
|
| 820 |
-
claudeRequest.system.forEach(systemPrompt => {
|
| 821 |
-
if (systemPrompt && systemPrompt.type === 'text' && typeof systemPrompt.text === 'string') {
|
| 822 |
-
systemParts.push({ text: systemPrompt.text });
|
| 823 |
-
}
|
| 824 |
-
});
|
| 825 |
-
if (systemParts.length > 0) {
|
| 826 |
-
geminiRequest.systemInstruction = {
|
| 827 |
-
role: 'user',
|
| 828 |
-
parts: systemParts
|
| 829 |
-
};
|
| 830 |
-
}
|
| 831 |
-
} else if (typeof claudeRequest.system === 'string') {
|
| 832 |
-
// 字符串格式的系统指令
|
| 833 |
-
geminiRequest.systemInstruction = {
|
| 834 |
-
parts: [{ text: claudeRequest.system }]
|
| 835 |
-
};
|
| 836 |
-
} else if (typeof claudeRequest.system === 'object') {
|
| 837 |
-
// 对象格式的系统指令
|
| 838 |
-
geminiRequest.systemInstruction = {
|
| 839 |
-
parts: [{ text: JSON.stringify(claudeRequest.system) }]
|
| 840 |
-
};
|
| 841 |
-
}
|
| 842 |
-
}
|
| 843 |
-
|
| 844 |
-
// 处理消息
|
| 845 |
-
if (Array.isArray(claudeRequest.messages)) {
|
| 846 |
-
claudeRequest.messages.forEach(message => {
|
| 847 |
-
if (!message || typeof message !== 'object' || !message.role) {
|
| 848 |
-
logger.warn("Skipping invalid message in claudeRequest.messages.");
|
| 849 |
-
return;
|
| 850 |
-
}
|
| 851 |
-
|
| 852 |
-
const geminiRole = message.role === 'assistant' ? 'model' : 'user';
|
| 853 |
-
const content = message.content;
|
| 854 |
-
|
| 855 |
-
// 处理内容
|
| 856 |
-
if (Array.isArray(content)) {
|
| 857 |
-
const parts = [];
|
| 858 |
-
|
| 859 |
-
content.forEach(block => {
|
| 860 |
-
if (!block || typeof block !== 'object') return;
|
| 861 |
-
|
| 862 |
-
switch (block.type) {
|
| 863 |
-
case 'text':
|
| 864 |
-
if (typeof block.text === 'string') {
|
| 865 |
-
parts.push({ text: block.text });
|
| 866 |
-
}
|
| 867 |
-
break;
|
| 868 |
-
|
| 869 |
-
// 添加 thinking 块处理
|
| 870 |
-
case 'thinking':
|
| 871 |
-
if (typeof block.thinking === 'string' && block.thinking.length > 0) {
|
| 872 |
-
const thinkingPart = {
|
| 873 |
-
text: block.thinking,
|
| 874 |
-
thought: true
|
| 875 |
-
};
|
| 876 |
-
// 如果有签名,添加 thoughtSignature
|
| 877 |
-
if (block.signature && block.signature.length >= 50) {
|
| 878 |
-
thinkingPart.thoughtSignature = block.signature;
|
| 879 |
-
}
|
| 880 |
-
parts.push(thinkingPart);
|
| 881 |
-
}
|
| 882 |
-
break;
|
| 883 |
-
|
| 884 |
-
// [FIX] 处理 redacted_thinking 块
|
| 885 |
-
case 'redacted_thinking':
|
| 886 |
-
// 将 redacted_thinking 转换为普通文本
|
| 887 |
-
if (block.data) {
|
| 888 |
-
parts.push({
|
| 889 |
-
text: `[Redacted Thinking: ${block.data}]`
|
| 890 |
-
});
|
| 891 |
-
}
|
| 892 |
-
break;
|
| 893 |
-
|
| 894 |
-
case 'tool_use':
|
| 895 |
-
// 转换为 Gemini functionCall 格式
|
| 896 |
-
if (block.name && block.input) {
|
| 897 |
-
const args = typeof block.input === 'string'
|
| 898 |
-
? block.input
|
| 899 |
-
: JSON.stringify(block.input);
|
| 900 |
-
|
| 901 |
-
// 验证 args 是有效的 JSON 对象
|
| 902 |
-
try {
|
| 903 |
-
const parsedArgs = JSON.parse(args);
|
| 904 |
-
if (parsedArgs && typeof parsedArgs === 'object') {
|
| 905 |
-
parts.push({
|
| 906 |
-
thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
|
| 907 |
-
functionCall: {
|
| 908 |
-
name: block.name,
|
| 909 |
-
args: parsedArgs
|
| 910 |
-
}
|
| 911 |
-
});
|
| 912 |
-
}
|
| 913 |
-
} catch (e) {
|
| 914 |
-
// 如果解析失败,尝试直接使用 input
|
| 915 |
-
if (block.input && typeof block.input === 'object') {
|
| 916 |
-
parts.push({
|
| 917 |
-
thoughtSignature: ClaudeConverter.GEMINI_CLAUDE_THOUGHT_SIGNATURE,
|
| 918 |
-
functionCall: {
|
| 919 |
-
name: block.name,
|
| 920 |
-
args: block.input
|
| 921 |
-
}
|
| 922 |
-
});
|
| 923 |
-
}
|
| 924 |
-
}
|
| 925 |
-
}
|
| 926 |
-
break;
|
| 927 |
-
|
| 928 |
-
case 'tool_result':
|
| 929 |
-
// 转换为 Gemini functionResponse 格式
|
| 930 |
-
// 的实现,正确处理 tool_use_id 到函数名的映射
|
| 931 |
-
const toolCallId = block.tool_use_id;
|
| 932 |
-
if (toolCallId) {
|
| 933 |
-
// 尝试从之前的 tool_use 块中查找对应的函数名
|
| 934 |
-
// 如果找不到,则从 tool_use_id 中提取
|
| 935 |
-
let funcName = toolCallId;
|
| 936 |
-
|
| 937 |
-
// 检查是否有缓存的 tool_id -> name 映射
|
| 938 |
-
// 格式通常是 "funcName-uuid" 或 "toolu_xxx"
|
| 939 |
-
if (toolCallId.startsWith('toolu_')) {
|
| 940 |
-
// Claude 格式的 tool_use_id,需要从上下文中查找函数名
|
| 941 |
-
// 这里我们保留原始 ID 作为 name(Gemini 会处理)
|
| 942 |
-
funcName = toolCallId;
|
| 943 |
-
} else {
|
| 944 |
-
const toolCallIdParts = toolCallId.split('-');
|
| 945 |
-
if (toolCallIdParts.length > 1) {
|
| 946 |
-
// 移除最后一个部分(UUID),保留函数名
|
| 947 |
-
funcName = toolCallIdParts.slice(0, -1).join('-');
|
| 948 |
-
}
|
| 949 |
-
}
|
| 950 |
-
|
| 951 |
-
// 获取响应数据
|
| 952 |
-
let responseData = block.content;
|
| 953 |
-
|
| 954 |
-
// 的 tool_result_compressor 逻辑
|
| 955 |
-
// 处理嵌套的 content 数组(如图片等)
|
| 956 |
-
if (Array.isArray(responseData)) {
|
| 957 |
-
// 提取文本内容
|
| 958 |
-
const textParts = responseData
|
| 959 |
-
.filter(item => item && item.type === 'text')
|
| 960 |
-
.map(item => item.text)
|
| 961 |
-
.join('\n');
|
| 962 |
-
responseData = textParts || JSON.stringify(responseData);
|
| 963 |
-
} else if (typeof responseData !== 'string') {
|
| 964 |
-
responseData = JSON.stringify(responseData);
|
| 965 |
-
}
|
| 966 |
-
|
| 967 |
-
parts.push({
|
| 968 |
-
functionResponse: {
|
| 969 |
-
name: funcName,
|
| 970 |
-
response: {
|
| 971 |
-
result: responseData
|
| 972 |
-
}
|
| 973 |
-
}
|
| 974 |
-
});
|
| 975 |
-
}
|
| 976 |
-
break;
|
| 977 |
-
|
| 978 |
-
case 'image':
|
| 979 |
-
if (block.source && block.source.type === 'base64') {
|
| 980 |
-
parts.push({
|
| 981 |
-
inlineData: {
|
| 982 |
-
mimeType: block.source.media_type,
|
| 983 |
-
data: block.source.data
|
| 984 |
-
}
|
| 985 |
-
});
|
| 986 |
-
}
|
| 987 |
-
break;
|
| 988 |
-
}
|
| 989 |
-
});
|
| 990 |
-
|
| 991 |
-
if (parts.length > 0) {
|
| 992 |
-
geminiRequest.contents.push({
|
| 993 |
-
role: geminiRole,
|
| 994 |
-
parts: parts
|
| 995 |
-
});
|
| 996 |
-
}
|
| 997 |
-
} else if (typeof content === 'string') {
|
| 998 |
-
// 字符串内容
|
| 999 |
-
geminiRequest.contents.push({
|
| 1000 |
-
role: geminiRole,
|
| 1001 |
-
parts: [{ text: content }]
|
| 1002 |
-
});
|
| 1003 |
-
}
|
| 1004 |
-
});
|
| 1005 |
-
}
|
| 1006 |
-
|
| 1007 |
-
// 添加生成配置
|
| 1008 |
-
const generationConfig = {};
|
| 1009 |
-
|
| 1010 |
-
if (claudeRequest.max_tokens !== undefined) {
|
| 1011 |
-
generationConfig.maxOutputTokens = claudeRequest.max_tokens;
|
| 1012 |
-
}
|
| 1013 |
-
if (claudeRequest.temperature !== undefined) {
|
| 1014 |
-
generationConfig.temperature = claudeRequest.temperature;
|
| 1015 |
-
}
|
| 1016 |
-
if (claudeRequest.top_p !== undefined) {
|
| 1017 |
-
generationConfig.topP = claudeRequest.top_p;
|
| 1018 |
-
}
|
| 1019 |
-
if (claudeRequest.top_k !== undefined) {
|
| 1020 |
-
generationConfig.topK = claudeRequest.top_k;
|
| 1021 |
-
}
|
| 1022 |
-
|
| 1023 |
-
// 处理 thinking 配置 - 转换为 Gemini thinkingBudget
|
| 1024 |
-
if (claudeRequest.thinking && claudeRequest.thinking.type === 'enabled') {
|
| 1025 |
-
if (claudeRequest.thinking.budget_tokens !== undefined) {
|
| 1026 |
-
const budget = claudeRequest.thinking.budget_tokens;
|
| 1027 |
-
if (!generationConfig.thinkingConfig) {
|
| 1028 |
-
generationConfig.thinkingConfig = {};
|
| 1029 |
-
}
|
| 1030 |
-
generationConfig.thinkingConfig.thinkingBudget = budget;
|
| 1031 |
-
generationConfig.thinkingConfig.include_thoughts = true;
|
| 1032 |
-
}
|
| 1033 |
-
}
|
| 1034 |
-
|
| 1035 |
-
if (Object.keys(generationConfig).length > 0) {
|
| 1036 |
-
geminiRequest.generationConfig = generationConfig;
|
| 1037 |
-
}
|
| 1038 |
-
|
| 1039 |
-
// 处理工具 - 使用 parametersJsonSchema 格式
|
| 1040 |
-
if (Array.isArray(claudeRequest.tools) && claudeRequest.tools.length > 0) {
|
| 1041 |
-
const functionDeclarations = [];
|
| 1042 |
-
|
| 1043 |
-
claudeRequest.tools.forEach(tool => {
|
| 1044 |
-
if (!tool || typeof tool !== 'object' || !tool.name) {
|
| 1045 |
-
logger.warn("Skipping invalid tool declaration in claudeRequest.tools.");
|
| 1046 |
-
return;
|
| 1047 |
-
}
|
| 1048 |
-
|
| 1049 |
-
// 清理 input_schema
|
| 1050 |
-
let inputSchema = tool.input_schema;
|
| 1051 |
-
if (inputSchema && typeof inputSchema === 'object') {
|
| 1052 |
-
// 创建副本以避免修改原始对象
|
| 1053 |
-
inputSchema = JSON.parse(JSON.stringify(inputSchema));
|
| 1054 |
-
// 清理不需要的字段
|
| 1055 |
-
delete inputSchema.$schema;
|
| 1056 |
-
// 清理 URL 格式(Gemini 不支持)
|
| 1057 |
-
this.cleanUrlFormatFromSchema(inputSchema);
|
| 1058 |
-
}
|
| 1059 |
-
|
| 1060 |
-
const funcDecl = {
|
| 1061 |
-
name: String(tool.name),
|
| 1062 |
-
description: String(tool.description || '')
|
| 1063 |
-
};
|
| 1064 |
-
|
| 1065 |
-
// 使用 parametersJsonSchema 而不是 parameters
|
| 1066 |
-
if (inputSchema) {
|
| 1067 |
-
funcDecl.parametersJsonSchema = inputSchema;
|
| 1068 |
-
}
|
| 1069 |
-
|
| 1070 |
-
functionDeclarations.push(funcDecl);
|
| 1071 |
-
});
|
| 1072 |
-
|
| 1073 |
-
if (functionDeclarations.length > 0) {
|
| 1074 |
-
geminiRequest.tools = [{
|
| 1075 |
-
functionDeclarations: functionDeclarations
|
| 1076 |
-
}];
|
| 1077 |
-
}
|
| 1078 |
-
}
|
| 1079 |
-
|
| 1080 |
-
// 处理tool_choice
|
| 1081 |
-
if (claudeRequest.tool_choice) {
|
| 1082 |
-
geminiRequest.toolConfig = this.buildGeminiToolConfigFromClaude(claudeRequest.tool_choice);
|
| 1083 |
-
}
|
| 1084 |
-
|
| 1085 |
-
// 添加默认安全设置
|
| 1086 |
-
geminiRequest.safetySettings = this.getDefaultSafetySettings();
|
| 1087 |
-
|
| 1088 |
-
return geminiRequest;
|
| 1089 |
-
}
|
| 1090 |
-
|
| 1091 |
-
/**
|
| 1092 |
-
* 清理 JSON Schema 中的 URL 格式
|
| 1093 |
-
* Gemini 不支持 "format": "uri"
|
| 1094 |
-
*/
|
| 1095 |
-
cleanUrlFormatFromSchema(schema) {
|
| 1096 |
-
if (!schema || typeof schema !== 'object') return;
|
| 1097 |
-
|
| 1098 |
-
// 如果是属性对象,检查并清理 format
|
| 1099 |
-
if (schema.type === 'string' && schema.format === 'uri') {
|
| 1100 |
-
delete schema.format;
|
| 1101 |
-
}
|
| 1102 |
-
|
| 1103 |
-
// 递归处理 properties
|
| 1104 |
-
if (schema.properties && typeof schema.properties === 'object') {
|
| 1105 |
-
Object.values(schema.properties).forEach(prop => {
|
| 1106 |
-
this.cleanUrlFormatFromSchema(prop);
|
| 1107 |
-
});
|
| 1108 |
-
}
|
| 1109 |
-
|
| 1110 |
-
// 递归处理 items(数组类型)
|
| 1111 |
-
if (schema.items) {
|
| 1112 |
-
this.cleanUrlFormatFromSchema(schema.items);
|
| 1113 |
-
}
|
| 1114 |
-
|
| 1115 |
-
// 递归处理 additionalProperties
|
| 1116 |
-
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
| 1117 |
-
this.cleanUrlFormatFromSchema(schema.additionalProperties);
|
| 1118 |
-
}
|
| 1119 |
-
}
|
| 1120 |
-
|
| 1121 |
-
/**
|
| 1122 |
-
* 获取默认的 Gemini 安全设置
|
| 1123 |
-
*/
|
| 1124 |
-
getDefaultSafetySettings() {
|
| 1125 |
-
return [
|
| 1126 |
-
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
|
| 1127 |
-
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
|
| 1128 |
-
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
|
| 1129 |
-
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
|
| 1130 |
-
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
|
| 1131 |
-
];
|
| 1132 |
-
}
|
| 1133 |
-
|
| 1134 |
-
/**
|
| 1135 |
-
* Claude响应 -> Gemini响应
|
| 1136 |
-
*/
|
| 1137 |
-
toGeminiResponse(claudeResponse, model) {
|
| 1138 |
-
if (!claudeResponse || !claudeResponse.content || claudeResponse.content.length === 0) {
|
| 1139 |
-
return { candidates: [], usageMetadata: {} };
|
| 1140 |
-
}
|
| 1141 |
-
|
| 1142 |
-
const parts = [];
|
| 1143 |
-
|
| 1144 |
-
// 处理内容块
|
| 1145 |
-
for (const block of claudeResponse.content) {
|
| 1146 |
-
if (!block) continue;
|
| 1147 |
-
|
| 1148 |
-
switch (block.type) {
|
| 1149 |
-
case 'text':
|
| 1150 |
-
if (block.text) {
|
| 1151 |
-
parts.push({ text: block.text });
|
| 1152 |
-
}
|
| 1153 |
-
break;
|
| 1154 |
-
|
| 1155 |
-
// 添加 thinking 块处理
|
| 1156 |
-
case 'thinking':
|
| 1157 |
-
if (block.thinking) {
|
| 1158 |
-
const thinkingPart = {
|
| 1159 |
-
text: block.thinking,
|
| 1160 |
-
thought: true
|
| 1161 |
-
};
|
| 1162 |
-
// 如果有签名,添加 thoughtSignature
|
| 1163 |
-
if (block.signature && block.signature.length >= 50) {
|
| 1164 |
-
thinkingPart.thoughtSignature = block.signature;
|
| 1165 |
-
}
|
| 1166 |
-
parts.push(thinkingPart);
|
| 1167 |
-
}
|
| 1168 |
-
break;
|
| 1169 |
-
|
| 1170 |
-
case 'tool_use':
|
| 1171 |
-
// [FIX] 添加 id 和 thoughtSignature 支持
|
| 1172 |
-
const functionCallPart = {
|
| 1173 |
-
functionCall: {
|
| 1174 |
-
name: block.name,
|
| 1175 |
-
args: block.input || {}
|
| 1176 |
-
}
|
| 1177 |
-
};
|
| 1178 |
-
// 添加 id(如果存在)
|
| 1179 |
-
if (block.id) {
|
| 1180 |
-
functionCallPart.functionCall.id = block.id;
|
| 1181 |
-
}
|
| 1182 |
-
// 添加签名(如果存在)
|
| 1183 |
-
if (block.signature && block.signature.length >= 50) {
|
| 1184 |
-
functionCallPart.thoughtSignature = block.signature;
|
| 1185 |
-
}
|
| 1186 |
-
parts.push(functionCallPart);
|
| 1187 |
-
break;
|
| 1188 |
-
|
| 1189 |
-
case 'image':
|
| 1190 |
-
if (block.source && block.source.type === 'base64') {
|
| 1191 |
-
parts.push({
|
| 1192 |
-
inlineData: {
|
| 1193 |
-
mimeType: block.source.media_type,
|
| 1194 |
-
data: block.source.data
|
| 1195 |
-
}
|
| 1196 |
-
});
|
| 1197 |
-
}
|
| 1198 |
-
break;
|
| 1199 |
-
|
| 1200 |
-
default:
|
| 1201 |
-
if (block.text) {
|
| 1202 |
-
parts.push({ text: block.text });
|
| 1203 |
-
}
|
| 1204 |
-
}
|
| 1205 |
-
}
|
| 1206 |
-
|
| 1207 |
-
// 映射finish_reason
|
| 1208 |
-
const finishReasonMap = {
|
| 1209 |
-
'end_turn': 'STOP',
|
| 1210 |
-
'max_tokens': 'MAX_TOKENS',
|
| 1211 |
-
'tool_use': 'STOP',
|
| 1212 |
-
'stop_sequence': 'STOP'
|
| 1213 |
-
};
|
| 1214 |
-
|
| 1215 |
-
return {
|
| 1216 |
-
candidates: [{
|
| 1217 |
-
content: {
|
| 1218 |
-
role: 'model',
|
| 1219 |
-
parts: parts
|
| 1220 |
-
},
|
| 1221 |
-
finishReason: finishReasonMap[claudeResponse.stop_reason] || 'STOP'
|
| 1222 |
-
}],
|
| 1223 |
-
usageMetadata: claudeResponse.usage ? {
|
| 1224 |
-
promptTokenCount: claudeResponse.usage.input_tokens || 0,
|
| 1225 |
-
candidatesTokenCount: claudeResponse.usage.output_tokens || 0,
|
| 1226 |
-
totalTokenCount: (claudeResponse.usage.input_tokens || 0) + (claudeResponse.usage.output_tokens || 0),
|
| 1227 |
-
cachedContentTokenCount: claudeResponse.usage.cache_read_input_tokens || 0,
|
| 1228 |
-
promptTokensDetails: [{
|
| 1229 |
-
modality: "TEXT",
|
| 1230 |
-
tokenCount: claudeResponse.usage.input_tokens || 0
|
| 1231 |
-
}],
|
| 1232 |
-
candidatesTokensDetails: [{
|
| 1233 |
-
modality: "TEXT",
|
| 1234 |
-
tokenCount: claudeResponse.usage.output_tokens || 0
|
| 1235 |
-
}]
|
| 1236 |
-
} : {}
|
| 1237 |
-
};
|
| 1238 |
-
}
|
| 1239 |
-
|
| 1240 |
-
/**
|
| 1241 |
-
* Claude流式响应 -> Gemini流式响应
|
| 1242 |
-
*/
|
| 1243 |
-
toGeminiStreamChunk(claudeChunk, model) {
|
| 1244 |
-
if (!claudeChunk) return null;
|
| 1245 |
-
|
| 1246 |
-
// 处理Claude流式事件
|
| 1247 |
-
if (typeof claudeChunk === 'object' && !Array.isArray(claudeChunk)) {
|
| 1248 |
-
// content_block_start 事件 - 处理 thinking 块开始
|
| 1249 |
-
if (claudeChunk.type === 'content_block_start') {
|
| 1250 |
-
const contentBlock = claudeChunk.content_block;
|
| 1251 |
-
if (contentBlock && contentBlock.type === 'thinking') {
|
| 1252 |
-
// thinking 块开始,返回空(等待 delta)
|
| 1253 |
-
return null;
|
| 1254 |
-
}
|
| 1255 |
-
if (contentBlock && contentBlock.type === 'tool_use') {
|
| 1256 |
-
// tool_use 块开始
|
| 1257 |
-
return {
|
| 1258 |
-
candidates: [{
|
| 1259 |
-
content: {
|
| 1260 |
-
role: "model",
|
| 1261 |
-
parts: [{
|
| 1262 |
-
functionCall: {
|
| 1263 |
-
name: contentBlock.name,
|
| 1264 |
-
args: {},
|
| 1265 |
-
id: contentBlock.id
|
| 1266 |
-
}
|
| 1267 |
-
}]
|
| 1268 |
-
}
|
| 1269 |
-
}]
|
| 1270 |
-
};
|
| 1271 |
-
}
|
| 1272 |
-
}
|
| 1273 |
-
|
| 1274 |
-
// content_block_delta 事件
|
| 1275 |
-
if (claudeChunk.type === 'content_block_delta') {
|
| 1276 |
-
const delta = claudeChunk.delta;
|
| 1277 |
-
|
| 1278 |
-
// 处理 text_delta
|
| 1279 |
-
if (delta && delta.type === 'text_delta') {
|
| 1280 |
-
return {
|
| 1281 |
-
candidates: [{
|
| 1282 |
-
content: {
|
| 1283 |
-
role: "model",
|
| 1284 |
-
parts: [{
|
| 1285 |
-
text: delta.text || ""
|
| 1286 |
-
}]
|
| 1287 |
-
}
|
| 1288 |
-
}]
|
| 1289 |
-
};
|
| 1290 |
-
}
|
| 1291 |
-
|
| 1292 |
-
// [FIX] 处理 thinking_delta - 转换为 Gemini 的 thought 格式
|
| 1293 |
-
if (delta && delta.type === 'thinking_delta') {
|
| 1294 |
-
return {
|
| 1295 |
-
candidates: [{
|
| 1296 |
-
content: {
|
| 1297 |
-
role: "model",
|
| 1298 |
-
parts: [{
|
| 1299 |
-
text: delta.thinking || "",
|
| 1300 |
-
thought: true
|
| 1301 |
-
}]
|
| 1302 |
-
}
|
| 1303 |
-
}]
|
| 1304 |
-
};
|
| 1305 |
-
}
|
| 1306 |
-
|
| 1307 |
-
// [FIX] 处理 signature_delta
|
| 1308 |
-
if (delta && delta.type === 'signature_delta') {
|
| 1309 |
-
// 签名通常与前一个 thinking 块关联
|
| 1310 |
-
// 在流式场景中,我们可以忽略或记录
|
| 1311 |
-
return null;
|
| 1312 |
-
}
|
| 1313 |
-
|
| 1314 |
-
// [FIX] 处理 input_json_delta (tool arguments)
|
| 1315 |
-
if (delta && delta.type === 'input_json_delta') {
|
| 1316 |
-
// 工具参数增量,Gemini 不支持增量参数,忽略
|
| 1317 |
-
return null;
|
| 1318 |
-
}
|
| 1319 |
-
}
|
| 1320 |
-
|
| 1321 |
-
// message_delta 事件 - 流结束
|
| 1322 |
-
if (claudeChunk.type === 'message_delta') {
|
| 1323 |
-
const stopReason = claudeChunk.delta?.stop_reason;
|
| 1324 |
-
const result = {
|
| 1325 |
-
candidates: [{
|
| 1326 |
-
finishReason: stopReason === 'end_turn' ? 'STOP' :
|
| 1327 |
-
stopReason === 'max_tokens' ? 'MAX_TOKENS' :
|
| 1328 |
-
stopReason === 'tool_use' ? 'STOP' :
|
| 1329 |
-
'OTHER'
|
| 1330 |
-
}]
|
| 1331 |
-
};
|
| 1332 |
-
|
| 1333 |
-
// 添加 usage 信息
|
| 1334 |
-
if (claudeChunk.usage) {
|
| 1335 |
-
result.usageMetadata = {
|
| 1336 |
-
promptTokenCount: claudeChunk.usage.input_tokens || 0,
|
| 1337 |
-
candidatesTokenCount: claudeChunk.usage.output_tokens || 0,
|
| 1338 |
-
totalTokenCount: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0),
|
| 1339 |
-
cachedContentTokenCount: claudeChunk.usage.cache_read_input_tokens || 0,
|
| 1340 |
-
promptTokensDetails: [{
|
| 1341 |
-
modality: "TEXT",
|
| 1342 |
-
tokenCount: claudeChunk.usage.input_tokens || 0
|
| 1343 |
-
}],
|
| 1344 |
-
candidatesTokensDetails: [{
|
| 1345 |
-
modality: "TEXT",
|
| 1346 |
-
tokenCount: claudeChunk.usage.output_tokens || 0
|
| 1347 |
-
}]
|
| 1348 |
-
};
|
| 1349 |
-
}
|
| 1350 |
-
|
| 1351 |
-
return result;
|
| 1352 |
-
}
|
| 1353 |
-
}
|
| 1354 |
-
|
| 1355 |
-
// 向后兼容:处理字符串格式
|
| 1356 |
-
if (typeof claudeChunk === 'string') {
|
| 1357 |
-
return {
|
| 1358 |
-
candidates: [{
|
| 1359 |
-
content: {
|
| 1360 |
-
role: "model",
|
| 1361 |
-
parts: [{
|
| 1362 |
-
text: claudeChunk
|
| 1363 |
-
}]
|
| 1364 |
-
}
|
| 1365 |
-
}]
|
| 1366 |
-
};
|
| 1367 |
-
}
|
| 1368 |
-
|
| 1369 |
-
return null;
|
| 1370 |
-
}
|
| 1371 |
-
|
| 1372 |
-
/**
|
| 1373 |
-
* 处理Claude内容到Gemini parts
|
| 1374 |
-
*/
|
| 1375 |
-
processClaudeContentToGeminiParts(content) {
|
| 1376 |
-
if (!content) return [];
|
| 1377 |
-
|
| 1378 |
-
if (typeof content === 'string') {
|
| 1379 |
-
return [{ text: content }];
|
| 1380 |
-
}
|
| 1381 |
-
|
| 1382 |
-
if (Array.isArray(content)) {
|
| 1383 |
-
const parts = [];
|
| 1384 |
-
|
| 1385 |
-
content.forEach(block => {
|
| 1386 |
-
if (!block || typeof block !== 'object' || !block.type) {
|
| 1387 |
-
logger.warn("Skipping invalid content block.");
|
| 1388 |
-
return;
|
| 1389 |
-
}
|
| 1390 |
-
|
| 1391 |
-
switch (block.type) {
|
| 1392 |
-
case 'text':
|
| 1393 |
-
if (typeof block.text === 'string') {
|
| 1394 |
-
parts.push({ text: block.text });
|
| 1395 |
-
}
|
| 1396 |
-
break;
|
| 1397 |
-
|
| 1398 |
-
case 'image':
|
| 1399 |
-
if (block.source && typeof block.source === 'object' &&
|
| 1400 |
-
block.source.type === 'base64' &&
|
| 1401 |
-
typeof block.source.media_type === 'string' &&
|
| 1402 |
-
typeof block.source.data === 'string') {
|
| 1403 |
-
parts.push({
|
| 1404 |
-
inlineData: {
|
| 1405 |
-
mimeType: block.source.media_type,
|
| 1406 |
-
data: block.source.data
|
| 1407 |
-
}
|
| 1408 |
-
});
|
| 1409 |
-
}
|
| 1410 |
-
break;
|
| 1411 |
-
|
| 1412 |
-
case 'tool_use':
|
| 1413 |
-
if (typeof block.name === 'string' &&
|
| 1414 |
-
block.input && typeof block.input === 'object') {
|
| 1415 |
-
parts.push({
|
| 1416 |
-
functionCall: {
|
| 1417 |
-
name: block.name,
|
| 1418 |
-
args: block.input
|
| 1419 |
-
}
|
| 1420 |
-
});
|
| 1421 |
-
}
|
| 1422 |
-
break;
|
| 1423 |
-
|
| 1424 |
-
case 'tool_result':
|
| 1425 |
-
if (typeof block.tool_use_id === 'string') {
|
| 1426 |
-
parts.push({
|
| 1427 |
-
functionResponse: {
|
| 1428 |
-
name: block.tool_use_id,
|
| 1429 |
-
response: { content: block.content }
|
| 1430 |
-
}
|
| 1431 |
-
});
|
| 1432 |
-
}
|
| 1433 |
-
break;
|
| 1434 |
-
|
| 1435 |
-
default:
|
| 1436 |
-
if (typeof block.text === 'string') {
|
| 1437 |
-
parts.push({ text: block.text });
|
| 1438 |
-
}
|
| 1439 |
-
}
|
| 1440 |
-
});
|
| 1441 |
-
|
| 1442 |
-
return parts;
|
| 1443 |
-
}
|
| 1444 |
-
|
| 1445 |
-
return [];
|
| 1446 |
-
}
|
| 1447 |
-
|
| 1448 |
-
/**
|
| 1449 |
-
* 构建Gemini工具配置
|
| 1450 |
-
*/
|
| 1451 |
-
buildGeminiToolConfigFromClaude(claudeToolChoice) {
|
| 1452 |
-
if (!claudeToolChoice || typeof claudeToolChoice !== 'object' || !claudeToolChoice.type) {
|
| 1453 |
-
logger.warn("Invalid claudeToolChoice provided.");
|
| 1454 |
-
return undefined;
|
| 1455 |
-
}
|
| 1456 |
-
|
| 1457 |
-
switch (claudeToolChoice.type) {
|
| 1458 |
-
case 'auto':
|
| 1459 |
-
return { functionCallingConfig: { mode: 'AUTO' } };
|
| 1460 |
-
case 'none':
|
| 1461 |
-
return { functionCallingConfig: { mode: 'NONE' } };
|
| 1462 |
-
case 'tool':
|
| 1463 |
-
if (claudeToolChoice.name && typeof claudeToolChoice.name === 'string') {
|
| 1464 |
-
return {
|
| 1465 |
-
functionCallingConfig: {
|
| 1466 |
-
mode: 'ANY',
|
| 1467 |
-
allowedFunctionNames: [claudeToolChoice.name]
|
| 1468 |
-
}
|
| 1469 |
-
};
|
| 1470 |
-
}
|
| 1471 |
-
logger.warn("Invalid tool name in claudeToolChoice of type 'tool'.");
|
| 1472 |
-
return undefined;
|
| 1473 |
-
default:
|
| 1474 |
-
logger.warn(`Unsupported claudeToolChoice type: ${claudeToolChoice.type}`);
|
| 1475 |
-
return undefined;
|
| 1476 |
-
}
|
| 1477 |
-
}
|
| 1478 |
-
|
| 1479 |
-
// =========================================================================
|
| 1480 |
-
// Claude -> OpenAI Responses 转换
|
| 1481 |
-
// =========================================================================
|
| 1482 |
-
|
| 1483 |
-
/**
|
| 1484 |
-
* Claude请求 -> OpenAI Responses请求
|
| 1485 |
-
*/
|
| 1486 |
-
toOpenAIResponsesRequest(claudeRequest) {
|
| 1487 |
-
const responsesRequest = {
|
| 1488 |
-
model: claudeRequest.model,
|
| 1489 |
-
instructions: '',
|
| 1490 |
-
input: [],
|
| 1491 |
-
stream: claudeRequest.stream || false,
|
| 1492 |
-
max_output_tokens: claudeRequest.max_tokens,
|
| 1493 |
-
temperature: claudeRequest.temperature,
|
| 1494 |
-
top_p: claudeRequest.top_p
|
| 1495 |
-
};
|
| 1496 |
-
|
| 1497 |
-
// 处理系统指令
|
| 1498 |
-
if (claudeRequest.system) {
|
| 1499 |
-
if (Array.isArray(claudeRequest.system)) {
|
| 1500 |
-
responsesRequest.instructions = claudeRequest.system.map(s => typeof s === 'string' ? s : s.text).join('\n');
|
| 1501 |
-
} else {
|
| 1502 |
-
responsesRequest.instructions = claudeRequest.system;
|
| 1503 |
-
}
|
| 1504 |
-
}
|
| 1505 |
-
|
| 1506 |
-
// 处理 thinking 配置
|
| 1507 |
-
if (claudeRequest.thinking && claudeRequest.thinking.type === 'enabled') {
|
| 1508 |
-
responsesRequest.reasoning = {
|
| 1509 |
-
effort: determineReasoningEffortFromBudget(claudeRequest.thinking.budget_tokens)
|
| 1510 |
-
};
|
| 1511 |
-
}
|
| 1512 |
-
|
| 1513 |
-
// 处理消息
|
| 1514 |
-
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
|
| 1515 |
-
claudeRequest.messages.forEach(msg => {
|
| 1516 |
-
const role = msg.role;
|
| 1517 |
-
const content = msg.content;
|
| 1518 |
-
|
| 1519 |
-
if (Array.isArray(content)) {
|
| 1520 |
-
// 检查是否包含 tool_result
|
| 1521 |
-
const toolResult = content.find(c => c.type === 'tool_result');
|
| 1522 |
-
if (toolResult) {
|
| 1523 |
-
responsesRequest.input.push({
|
| 1524 |
-
type: 'function_call_output',
|
| 1525 |
-
call_id: toolResult.tool_use_id,
|
| 1526 |
-
output: typeof toolResult.content === 'string' ? toolResult.content : JSON.stringify(toolResult.content)
|
| 1527 |
-
});
|
| 1528 |
-
return;
|
| 1529 |
-
}
|
| 1530 |
-
|
| 1531 |
-
// 检查是否包含 tool_use
|
| 1532 |
-
const toolUse = content.find(c => c.type === 'tool_use');
|
| 1533 |
-
if (toolUse) {
|
| 1534 |
-
responsesRequest.input.push({
|
| 1535 |
-
type: 'function_call',
|
| 1536 |
-
call_id: toolUse.id,
|
| 1537 |
-
name: toolUse.name,
|
| 1538 |
-
arguments: typeof toolUse.input === 'string' ? toolUse.input : JSON.stringify(toolUse.input)
|
| 1539 |
-
});
|
| 1540 |
-
return;
|
| 1541 |
-
}
|
| 1542 |
-
|
| 1543 |
-
const responsesContent = content.map(c => {
|
| 1544 |
-
if (c.type === 'text') {
|
| 1545 |
-
return {
|
| 1546 |
-
type: role === 'assistant' ? 'output_text' : 'input_text',
|
| 1547 |
-
text: c.text
|
| 1548 |
-
};
|
| 1549 |
-
} else if (c.type === 'image') {
|
| 1550 |
-
return {
|
| 1551 |
-
type: 'input_image',
|
| 1552 |
-
image_url: {
|
| 1553 |
-
url: `data:${c.source.media_type};base64,${c.source.data}`
|
| 1554 |
-
}
|
| 1555 |
-
};
|
| 1556 |
-
}
|
| 1557 |
-
return null;
|
| 1558 |
-
}).filter(Boolean);
|
| 1559 |
-
|
| 1560 |
-
if (responsesContent.length > 0) {
|
| 1561 |
-
responsesRequest.input.push({
|
| 1562 |
-
type: 'message',
|
| 1563 |
-
role: role,
|
| 1564 |
-
content: responsesContent
|
| 1565 |
-
});
|
| 1566 |
-
}
|
| 1567 |
-
} else if (typeof content === 'string') {
|
| 1568 |
-
responsesRequest.input.push({
|
| 1569 |
-
type: 'message',
|
| 1570 |
-
role: role,
|
| 1571 |
-
content: [{
|
| 1572 |
-
type: role === 'assistant' ? 'output_text' : 'input_text',
|
| 1573 |
-
text: content
|
| 1574 |
-
}]
|
| 1575 |
-
});
|
| 1576 |
-
}
|
| 1577 |
-
});
|
| 1578 |
-
}
|
| 1579 |
-
|
| 1580 |
-
// 处理工具
|
| 1581 |
-
if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) {
|
| 1582 |
-
responsesRequest.tools = claudeRequest.tools.map(tool => ({
|
| 1583 |
-
type: 'function',
|
| 1584 |
-
name: tool.name,
|
| 1585 |
-
description: tool.description,
|
| 1586 |
-
parameters: tool.input_schema || { type: 'object', properties: {} }
|
| 1587 |
-
}));
|
| 1588 |
-
}
|
| 1589 |
-
|
| 1590 |
-
if (claudeRequest.tool_choice) {
|
| 1591 |
-
if (claudeRequest.tool_choice.type === 'auto') {
|
| 1592 |
-
responsesRequest.tool_choice = 'auto';
|
| 1593 |
-
} else if (claudeRequest.tool_choice.type === 'any') {
|
| 1594 |
-
responsesRequest.tool_choice = 'required';
|
| 1595 |
-
} else if (claudeRequest.tool_choice.type === 'tool') {
|
| 1596 |
-
responsesRequest.tool_choice = {
|
| 1597 |
-
type: 'function',
|
| 1598 |
-
function: { name: claudeRequest.tool_choice.name }
|
| 1599 |
-
};
|
| 1600 |
-
}
|
| 1601 |
-
}
|
| 1602 |
-
|
| 1603 |
-
return responsesRequest;
|
| 1604 |
-
}
|
| 1605 |
-
|
| 1606 |
-
/**
|
| 1607 |
-
* Claude响应 -> OpenAI Responses响应
|
| 1608 |
-
*/
|
| 1609 |
-
toOpenAIResponsesResponse(claudeResponse, model) {
|
| 1610 |
-
const content = this.processClaudeResponseContent(claudeResponse.content);
|
| 1611 |
-
const textContent = typeof content === 'string' ? content : JSON.stringify(content);
|
| 1612 |
-
|
| 1613 |
-
let output = [];
|
| 1614 |
-
output.push({
|
| 1615 |
-
type: "message",
|
| 1616 |
-
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 1617 |
-
summary: [],
|
| 1618 |
-
role: "assistant",
|
| 1619 |
-
status: "completed",
|
| 1620 |
-
content: [{
|
| 1621 |
-
annotations: [],
|
| 1622 |
-
logprobs: [],
|
| 1623 |
-
text: textContent,
|
| 1624 |
-
type: "output_text"
|
| 1625 |
-
}]
|
| 1626 |
-
});
|
| 1627 |
-
|
| 1628 |
-
return {
|
| 1629 |
-
background: false,
|
| 1630 |
-
created_at: Math.floor(Date.now() / 1000),
|
| 1631 |
-
error: null,
|
| 1632 |
-
id: `resp_${uuidv4().replace(/-/g, '')}`,
|
| 1633 |
-
incomplete_details: null,
|
| 1634 |
-
max_output_tokens: null,
|
| 1635 |
-
max_tool_calls: null,
|
| 1636 |
-
metadata: {},
|
| 1637 |
-
model: model || claudeResponse.model,
|
| 1638 |
-
object: "response",
|
| 1639 |
-
output: output,
|
| 1640 |
-
parallel_tool_calls: true,
|
| 1641 |
-
previous_response_id: null,
|
| 1642 |
-
prompt_cache_key: null,
|
| 1643 |
-
reasoning: {},
|
| 1644 |
-
safety_identifier: "user-" + uuidv4().replace(/-/g, ''),
|
| 1645 |
-
service_tier: "default",
|
| 1646 |
-
status: "completed",
|
| 1647 |
-
store: false,
|
| 1648 |
-
temperature: 1,
|
| 1649 |
-
text: {
|
| 1650 |
-
format: { type: "text" },
|
| 1651 |
-
},
|
| 1652 |
-
tool_choice: "auto",
|
| 1653 |
-
tools: [],
|
| 1654 |
-
top_logprobs: 0,
|
| 1655 |
-
top_p: 1,
|
| 1656 |
-
truncation: "disabled",
|
| 1657 |
-
usage: {
|
| 1658 |
-
input_tokens: claudeResponse.usage?.input_tokens || 0,
|
| 1659 |
-
input_tokens_details: {
|
| 1660 |
-
cached_tokens: claudeResponse.usage?.cache_read_input_tokens || 0
|
| 1661 |
-
},
|
| 1662 |
-
output_tokens: claudeResponse.usage?.output_tokens || 0,
|
| 1663 |
-
output_tokens_details: {
|
| 1664 |
-
reasoning_tokens: 0
|
| 1665 |
-
},
|
| 1666 |
-
total_tokens: (claudeResponse.usage?.input_tokens || 0) + (claudeResponse.usage?.output_tokens || 0)
|
| 1667 |
-
},
|
| 1668 |
-
user: null
|
| 1669 |
-
};
|
| 1670 |
-
}
|
| 1671 |
-
|
| 1672 |
-
/**
|
| 1673 |
-
* Claude流式响应 -> OpenAI Responses流式响应
|
| 1674 |
-
*/
|
| 1675 |
-
toOpenAIResponsesStreamChunk(claudeChunk, model, requestId = null) {
|
| 1676 |
-
if (!claudeChunk) return [];
|
| 1677 |
-
|
| 1678 |
-
const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
|
| 1679 |
-
const events = [];
|
| 1680 |
-
|
| 1681 |
-
// message_start 事件 - 流开始
|
| 1682 |
-
if (claudeChunk.type === 'message_start') {
|
| 1683 |
-
events.push(
|
| 1684 |
-
generateResponseCreated(responseId, model || 'unknown'),
|
| 1685 |
-
generateResponseInProgress(responseId),
|
| 1686 |
-
generateOutputItemAdded(responseId),
|
| 1687 |
-
generateContentPartAdded(responseId)
|
| 1688 |
-
);
|
| 1689 |
-
}
|
| 1690 |
-
|
| 1691 |
-
// content_block_start 事件
|
| 1692 |
-
if (claudeChunk.type === 'content_block_start') {
|
| 1693 |
-
const contentBlock = claudeChunk.content_block;
|
| 1694 |
-
|
| 1695 |
-
// 对于 tool_use 类型,添加工具调用项
|
| 1696 |
-
if (contentBlock && contentBlock.type === 'tool_use') {
|
| 1697 |
-
events.push({
|
| 1698 |
-
item: {
|
| 1699 |
-
id: contentBlock.id,
|
| 1700 |
-
type: "function_call",
|
| 1701 |
-
name: contentBlock.name,
|
| 1702 |
-
arguments: "",
|
| 1703 |
-
status: "in_progress"
|
| 1704 |
-
},
|
| 1705 |
-
output_index: claudeChunk.index || 0,
|
| 1706 |
-
sequence_number: 2,
|
| 1707 |
-
type: "response.output_item.added"
|
| 1708 |
-
});
|
| 1709 |
-
}
|
| 1710 |
-
}
|
| 1711 |
-
|
| 1712 |
-
// content_block_delta 事件
|
| 1713 |
-
if (claudeChunk.type === 'content_block_delta') {
|
| 1714 |
-
const delta = claudeChunk.delta;
|
| 1715 |
-
|
| 1716 |
-
// 处理文本增量
|
| 1717 |
-
if (delta && delta.type === 'text_delta') {
|
| 1718 |
-
events.push({
|
| 1719 |
-
delta: delta.text || "",
|
| 1720 |
-
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 1721 |
-
output_index: claudeChunk.index || 0,
|
| 1722 |
-
sequence_number: 3,
|
| 1723 |
-
type: "response.output_text.delta"
|
| 1724 |
-
});
|
| 1725 |
-
}
|
| 1726 |
-
// 处理推理内容增量
|
| 1727 |
-
else if (delta && delta.type === 'thinking_delta') {
|
| 1728 |
-
events.push({
|
| 1729 |
-
delta: delta.thinking || "",
|
| 1730 |
-
item_id: `thinking_${uuidv4().replace(/-/g, '')}`,
|
| 1731 |
-
output_index: claudeChunk.index || 0,
|
| 1732 |
-
sequence_number: 3,
|
| 1733 |
-
type: "response.reasoning_summary_text.delta"
|
| 1734 |
-
});
|
| 1735 |
-
}
|
| 1736 |
-
// 处理工具调用参数增量
|
| 1737 |
-
else if (delta && delta.type === 'input_json_delta') {
|
| 1738 |
-
events.push({
|
| 1739 |
-
delta: delta.partial_json || "",
|
| 1740 |
-
item_id: `call_${uuidv4().replace(/-/g, '')}`,
|
| 1741 |
-
output_index: claudeChunk.index || 0,
|
| 1742 |
-
sequence_number: 3,
|
| 1743 |
-
type: "response.custom_tool_call_input.delta"
|
| 1744 |
-
});
|
| 1745 |
-
}
|
| 1746 |
-
}
|
| 1747 |
-
|
| 1748 |
-
// content_block_stop 事件
|
| 1749 |
-
if (claudeChunk.type === 'content_block_stop') {
|
| 1750 |
-
events.push({
|
| 1751 |
-
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 1752 |
-
output_index: claudeChunk.index || 0,
|
| 1753 |
-
sequence_number: 4,
|
| 1754 |
-
type: "response.output_item.done"
|
| 1755 |
-
});
|
| 1756 |
-
}
|
| 1757 |
-
|
| 1758 |
-
// message_delta 事件 - 流结束
|
| 1759 |
-
if (claudeChunk.type === 'message_delta') {
|
| 1760 |
-
// events.push(
|
| 1761 |
-
// generateOutputTextDone(responseId),
|
| 1762 |
-
// generateContentPartDone(responseId),
|
| 1763 |
-
// generateOutputItemDone(responseId),
|
| 1764 |
-
// generateResponseCompleted(responseId)
|
| 1765 |
-
// );
|
| 1766 |
-
|
| 1767 |
-
// 如果有 usage 信息,更新最后一个事件
|
| 1768 |
-
if (claudeChunk.usage && events.length > 0) {
|
| 1769 |
-
const lastEvent = events[events.length - 1];
|
| 1770 |
-
if (lastEvent.response) {
|
| 1771 |
-
lastEvent.response.usage = {
|
| 1772 |
-
input_tokens: claudeChunk.usage.input_tokens || 0,
|
| 1773 |
-
input_tokens_details: {
|
| 1774 |
-
cached_tokens: claudeChunk.usage.cache_read_input_tokens || 0
|
| 1775 |
-
},
|
| 1776 |
-
output_tokens: claudeChunk.usage.output_tokens || 0,
|
| 1777 |
-
output_tokens_details: {
|
| 1778 |
-
reasoning_tokens: 0
|
| 1779 |
-
},
|
| 1780 |
-
total_tokens: (claudeChunk.usage.input_tokens || 0) + (claudeChunk.usage.output_tokens || 0)
|
| 1781 |
-
};
|
| 1782 |
-
}
|
| 1783 |
-
}
|
| 1784 |
-
}
|
| 1785 |
-
|
| 1786 |
-
// message_stop 事件
|
| 1787 |
-
if (claudeChunk.type === 'message_stop') {
|
| 1788 |
-
events.push(
|
| 1789 |
-
generateOutputTextDone(responseId),
|
| 1790 |
-
generateContentPartDone(responseId),
|
| 1791 |
-
generateOutputItemDone(responseId),
|
| 1792 |
-
generateResponseCompleted(responseId)
|
| 1793 |
-
);
|
| 1794 |
-
}
|
| 1795 |
-
|
| 1796 |
-
return events;
|
| 1797 |
-
}
|
| 1798 |
-
|
| 1799 |
-
// =========================================================================
|
| 1800 |
-
// Claude -> Codex 转换
|
| 1801 |
-
// =========================================================================
|
| 1802 |
-
|
| 1803 |
-
/**
|
| 1804 |
-
* 应用简单缩短规则缩短工具名称
|
| 1805 |
-
*/
|
| 1806 |
-
_shortenNameIfNeeded(name) {
|
| 1807 |
-
const limit = 64;
|
| 1808 |
-
if (name.length <= limit) {
|
| 1809 |
-
return name;
|
| 1810 |
-
}
|
| 1811 |
-
if (name.startsWith("mcp__")) {
|
| 1812 |
-
const idx = name.lastIndexOf("__");
|
| 1813 |
-
if (idx > 0) {
|
| 1814 |
-
const cand = "mcp__" + name.substring(idx + 2);
|
| 1815 |
-
if (cand.length > limit) {
|
| 1816 |
-
return cand.substring(0, limit);
|
| 1817 |
-
}
|
| 1818 |
-
return cand;
|
| 1819 |
-
}
|
| 1820 |
-
}
|
| 1821 |
-
return name.substring(0, limit);
|
| 1822 |
-
}
|
| 1823 |
-
|
| 1824 |
-
/**
|
| 1825 |
-
* 构建短名称映射以确保请求内唯一性
|
| 1826 |
-
*/
|
| 1827 |
-
_buildShortNameMap(names) {
|
| 1828 |
-
const limit = 64;
|
| 1829 |
-
const used = new Set();
|
| 1830 |
-
const m = {};
|
| 1831 |
-
|
| 1832 |
-
const baseCandidate = (n) => {
|
| 1833 |
-
if (n.length <= limit) {
|
| 1834 |
-
return n;
|
| 1835 |
-
}
|
| 1836 |
-
if (n.startsWith("mcp__")) {
|
| 1837 |
-
const idx = n.lastIndexOf("__");
|
| 1838 |
-
if (idx > 0) {
|
| 1839 |
-
let cand = "mcp__" + n.substring(idx + 2);
|
| 1840 |
-
if (cand.length > limit) {
|
| 1841 |
-
cand = cand.substring(0, limit);
|
| 1842 |
-
}
|
| 1843 |
-
return cand;
|
| 1844 |
-
}
|
| 1845 |
-
}
|
| 1846 |
-
return n.substring(0, limit);
|
| 1847 |
-
};
|
| 1848 |
-
|
| 1849 |
-
const makeUnique = (cand) => {
|
| 1850 |
-
if (!used.has(cand)) {
|
| 1851 |
-
return cand;
|
| 1852 |
-
}
|
| 1853 |
-
const base = cand;
|
| 1854 |
-
for (let i = 1; ; i++) {
|
| 1855 |
-
const suffix = "_" + i;
|
| 1856 |
-
const allowed = limit - suffix.length;
|
| 1857 |
-
let tmp = base;
|
| 1858 |
-
if (tmp.length > (allowed < 0 ? 0 : allowed)) {
|
| 1859 |
-
tmp = tmp.substring(0, allowed < 0 ? 0 : allowed);
|
| 1860 |
-
}
|
| 1861 |
-
tmp = tmp + suffix;
|
| 1862 |
-
if (!used.has(tmp)) {
|
| 1863 |
-
return tmp;
|
| 1864 |
-
}
|
| 1865 |
-
}
|
| 1866 |
-
};
|
| 1867 |
-
|
| 1868 |
-
for (const n of names) {
|
| 1869 |
-
const cand = baseCandidate(n);
|
| 1870 |
-
const uniq = makeUnique(cand);
|
| 1871 |
-
used.add(uniq);
|
| 1872 |
-
m[n] = uniq;
|
| 1873 |
-
}
|
| 1874 |
-
return m;
|
| 1875 |
-
}
|
| 1876 |
-
|
| 1877 |
-
/**
|
| 1878 |
-
* 标准化工具参数,确保对象 Schema 包含 properties
|
| 1879 |
-
*/
|
| 1880 |
-
_normalizeToolParameters(schema) {
|
| 1881 |
-
if (!schema || typeof schema !== 'object') {
|
| 1882 |
-
return { type: 'object', properties: {} };
|
| 1883 |
-
}
|
| 1884 |
-
const result = { ...schema };
|
| 1885 |
-
if (!result.type) {
|
| 1886 |
-
result.type = 'object';
|
| 1887 |
-
}
|
| 1888 |
-
if (result.type === 'object' && !result.properties) {
|
| 1889 |
-
result.properties = {};
|
| 1890 |
-
}
|
| 1891 |
-
return result;
|
| 1892 |
-
}
|
| 1893 |
-
|
| 1894 |
-
/**
|
| 1895 |
-
* Claude请求 -> Codex请求
|
| 1896 |
-
*/
|
| 1897 |
-
toCodexRequest(claudeRequest) {
|
| 1898 |
-
const codexRequest = {
|
| 1899 |
-
model: claudeRequest.model,
|
| 1900 |
-
instructions: '',
|
| 1901 |
-
input: [],
|
| 1902 |
-
stream: true,
|
| 1903 |
-
store: false,
|
| 1904 |
-
parallel_tool_calls: true,
|
| 1905 |
-
metadata: claudeRequest.metadata || {},
|
| 1906 |
-
reasoning: {
|
| 1907 |
-
effort: claudeRequest.reasoning?.effort || 'medium',
|
| 1908 |
-
summary: 'auto'
|
| 1909 |
-
},
|
| 1910 |
-
include: ['reasoning.encrypted_content']
|
| 1911 |
-
};
|
| 1912 |
-
|
| 1913 |
-
// 处理系统指令
|
| 1914 |
-
if (claudeRequest.system) {
|
| 1915 |
-
let instructions = '';
|
| 1916 |
-
if (Array.isArray(claudeRequest.system)) {
|
| 1917 |
-
instructions = claudeRequest.system.map(s => typeof s === 'string' ? s : s.text).join('\n');
|
| 1918 |
-
} else {
|
| 1919 |
-
instructions = claudeRequest.system;
|
| 1920 |
-
}
|
| 1921 |
-
codexRequest.instructions = instructions;
|
| 1922 |
-
|
| 1923 |
-
// 处理 Codex 中的系统消息(作为 developer 角色添加到 input)
|
| 1924 |
-
const systemParts = Array.isArray(claudeRequest.system) ? claudeRequest.system : [{ type: 'text', text: claudeRequest.system }];
|
| 1925 |
-
const developerMessage = {
|
| 1926 |
-
type: 'message',
|
| 1927 |
-
role: 'developer',
|
| 1928 |
-
content: []
|
| 1929 |
-
};
|
| 1930 |
-
|
| 1931 |
-
systemParts.forEach(part => {
|
| 1932 |
-
if (part.type === 'text') {
|
| 1933 |
-
developerMessage.content.push({
|
| 1934 |
-
type: 'input_text',
|
| 1935 |
-
text: part.text
|
| 1936 |
-
});
|
| 1937 |
-
} else if (typeof part === 'string') {
|
| 1938 |
-
developerMessage.content.push({
|
| 1939 |
-
type: 'input_text',
|
| 1940 |
-
text: part
|
| 1941 |
-
});
|
| 1942 |
-
}
|
| 1943 |
-
});
|
| 1944 |
-
|
| 1945 |
-
if (developerMessage.content.length > 0) {
|
| 1946 |
-
codexRequest.input.push(developerMessage);
|
| 1947 |
-
}
|
| 1948 |
-
}
|
| 1949 |
-
|
| 1950 |
-
// 处理工具并构建短名称映射
|
| 1951 |
-
let shortMap = {};
|
| 1952 |
-
if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) {
|
| 1953 |
-
const toolNames = claudeRequest.tools.map(t => t.name).filter(Boolean);
|
| 1954 |
-
shortMap = this._buildShortNameMap(toolNames);
|
| 1955 |
-
|
| 1956 |
-
codexRequest.tools = claudeRequest.tools.map(tool => {
|
| 1957 |
-
// 特殊处理:将 Claude Web Search 工具映射到 Codex web_search
|
| 1958 |
-
if (tool.type === "web_search_20250305") {
|
| 1959 |
-
return { type: "web_search" };
|
| 1960 |
-
}
|
| 1961 |
-
|
| 1962 |
-
let name = tool.name;
|
| 1963 |
-
if (shortMap[name]) {
|
| 1964 |
-
name = shortMap[name];
|
| 1965 |
-
} else {
|
| 1966 |
-
name = this._shortenNameIfNeeded(name);
|
| 1967 |
-
}
|
| 1968 |
-
|
| 1969 |
-
const convertedTool = {
|
| 1970 |
-
type: 'function',
|
| 1971 |
-
name: name,
|
| 1972 |
-
description: tool.description || '',
|
| 1973 |
-
parameters: this._normalizeToolParameters(tool.input_schema),
|
| 1974 |
-
strict: false
|
| 1975 |
-
};
|
| 1976 |
-
|
| 1977 |
-
// 移除 parameters.$schema
|
| 1978 |
-
if (convertedTool.parameters && convertedTool.parameters.$schema) {
|
| 1979 |
-
delete convertedTool.parameters.$schema;
|
| 1980 |
-
}
|
| 1981 |
-
|
| 1982 |
-
return convertedTool;
|
| 1983 |
-
});
|
| 1984 |
-
codexRequest.tool_choice = "auto";
|
| 1985 |
-
}
|
| 1986 |
-
|
| 1987 |
-
// 处理消息
|
| 1988 |
-
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
|
| 1989 |
-
for (const msg of claudeRequest.messages) {
|
| 1990 |
-
const role = msg.role;
|
| 1991 |
-
const content = msg.content;
|
| 1992 |
-
|
| 1993 |
-
let currentMessage = {
|
| 1994 |
-
type: 'message',
|
| 1995 |
-
role: role,
|
| 1996 |
-
content: []
|
| 1997 |
-
};
|
| 1998 |
-
|
| 1999 |
-
const flushMessage = () => {
|
| 2000 |
-
if (currentMessage.content.length > 0) {
|
| 2001 |
-
codexRequest.input.push({ ...currentMessage });
|
| 2002 |
-
currentMessage.content = [];
|
| 2003 |
-
}
|
| 2004 |
-
};
|
| 2005 |
-
|
| 2006 |
-
const appendTextContent = (text) => {
|
| 2007 |
-
const partType = role === 'assistant' ? 'output_text' : 'input_text';
|
| 2008 |
-
currentMessage.content.push({
|
| 2009 |
-
type: partType,
|
| 2010 |
-
text: text
|
| 2011 |
-
});
|
| 2012 |
-
};
|
| 2013 |
-
|
| 2014 |
-
const appendImageContent = (data, mediaType) => {
|
| 2015 |
-
currentMessage.content.push({
|
| 2016 |
-
type: 'input_image',
|
| 2017 |
-
image_url: `data:${mediaType};base64,${data}`
|
| 2018 |
-
});
|
| 2019 |
-
};
|
| 2020 |
-
|
| 2021 |
-
if (Array.isArray(content)) {
|
| 2022 |
-
for (const block of content) {
|
| 2023 |
-
switch (block.type) {
|
| 2024 |
-
case 'text':
|
| 2025 |
-
appendTextContent(block.text);
|
| 2026 |
-
break;
|
| 2027 |
-
case 'image':
|
| 2028 |
-
if (block.source) {
|
| 2029 |
-
const data = block.source.data || block.source.base64 || '';
|
| 2030 |
-
const mediaType = block.source.media_type || block.source.mime_type || 'application/octet-stream';
|
| 2031 |
-
if (data) {
|
| 2032 |
-
appendImageContent(data, mediaType);
|
| 2033 |
-
}
|
| 2034 |
-
}
|
| 2035 |
-
break;
|
| 2036 |
-
case 'tool_use':
|
| 2037 |
-
flushMessage();
|
| 2038 |
-
let toolName = block.name;
|
| 2039 |
-
if (shortMap[toolName]) {
|
| 2040 |
-
toolName = shortMap[toolName];
|
| 2041 |
-
} else {
|
| 2042 |
-
toolName = this._shortenNameIfNeeded(toolName);
|
| 2043 |
-
}
|
| 2044 |
-
codexRequest.input.push({
|
| 2045 |
-
type: 'function_call',
|
| 2046 |
-
call_id: block.id,
|
| 2047 |
-
name: toolName,
|
| 2048 |
-
arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input || {})
|
| 2049 |
-
});
|
| 2050 |
-
break;
|
| 2051 |
-
case 'tool_result':
|
| 2052 |
-
flushMessage();
|
| 2053 |
-
codexRequest.input.push({
|
| 2054 |
-
type: 'function_call_output',
|
| 2055 |
-
call_id: block.tool_use_id,
|
| 2056 |
-
output: typeof block.content === 'string' ? block.content : JSON.stringify(block.content || "")
|
| 2057 |
-
});
|
| 2058 |
-
break;
|
| 2059 |
-
}
|
| 2060 |
-
}
|
| 2061 |
-
} else if (typeof content === 'string') {
|
| 2062 |
-
appendTextContent(content);
|
| 2063 |
-
}
|
| 2064 |
-
flushMessage();
|
| 2065 |
-
}
|
| 2066 |
-
}
|
| 2067 |
-
|
| 2068 |
-
// 处理 thinking 转换
|
| 2069 |
-
if (claudeRequest.thinking && claudeRequest.thinking.type === "enabled") {
|
| 2070 |
-
const budgetTokens = claudeRequest.thinking.budget_tokens;
|
| 2071 |
-
codexRequest.reasoning.effort = determineReasoningEffortFromBudget(budgetTokens);
|
| 2072 |
-
} else if (claudeRequest.thinking && claudeRequest.thinking.type === "disabled") {
|
| 2073 |
-
codexRequest.reasoning.effort = determineReasoningEffortFromBudget(0);
|
| 2074 |
-
}
|
| 2075 |
-
|
| 2076 |
-
// 注入 Codex 指令 (对应 末尾的特殊逻辑)
|
| 2077 |
-
// 注意:这里需要检查是否需要注入 "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!"
|
| 2078 |
-
// 通过 misc.GetCodexInstructionsEnabled() 判断,这里我们参考其逻辑
|
| 2079 |
-
const shouldInjectInstructions = process.env.CODEX_INSTRUCTIONS_ENABLED === 'true'; // 假设环境变量控制
|
| 2080 |
-
if (shouldInjectInstructions && codexRequest.input.length > 0) {
|
| 2081 |
-
const firstInput = codexRequest.input[0];
|
| 2082 |
-
const firstText = firstInput.content && firstInput.content[0] && firstInput.content[0].text;
|
| 2083 |
-
const instructions = "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!";
|
| 2084 |
-
if (firstText !== instructions) {
|
| 2085 |
-
codexRequest.input.unshift({
|
| 2086 |
-
type: 'message',
|
| 2087 |
-
role: 'user',
|
| 2088 |
-
content: [{
|
| 2089 |
-
type: 'input_text',
|
| 2090 |
-
text: instructions
|
| 2091 |
-
}]
|
| 2092 |
-
});
|
| 2093 |
-
}
|
| 2094 |
-
}
|
| 2095 |
-
|
| 2096 |
-
return codexRequest;
|
| 2097 |
-
}
|
| 2098 |
-
|
| 2099 |
-
/**
|
| 2100 |
-
* Claude请求 -> Grok请求
|
| 2101 |
-
*/
|
| 2102 |
-
toGrokRequest(claudeRequest) {
|
| 2103 |
-
// 先转换为 OpenAI 格式,因为 Grok 兼容 OpenAI 格式
|
| 2104 |
-
const openaiRequest = this.toOpenAIRequest(claudeRequest);
|
| 2105 |
-
return {
|
| 2106 |
-
...openaiRequest,
|
| 2107 |
-
_isConverted: true
|
| 2108 |
-
};
|
| 2109 |
-
}
|
| 2110 |
-
|
| 2111 |
-
/**
|
| 2112 |
-
* Claude响应 -> Codex响应 (实际上是 Codex 转 Claude)
|
| 2113 |
-
*/
|
| 2114 |
-
toCodexResponse(codexResponse, model) {
|
| 2115 |
-
const content = [];
|
| 2116 |
-
let stopReason = "end_turn";
|
| 2117 |
-
|
| 2118 |
-
if (codexResponse.response?.output) {
|
| 2119 |
-
codexResponse.response.output.forEach(item => {
|
| 2120 |
-
if (item.type === 'message' && item.content) {
|
| 2121 |
-
const textPart = item.content.find(c => c.type === 'output_text');
|
| 2122 |
-
if (textPart) content.push({ type: 'text', text: textPart.text });
|
| 2123 |
-
} else if (item.type === 'reasoning' && item.summary) {
|
| 2124 |
-
const textPart = item.summary.find(c => c.type === 'summary_text');
|
| 2125 |
-
if (textPart) content.push({ type: 'thinking', thinking: textPart.text });
|
| 2126 |
-
} else if (item.type === 'function_call') {
|
| 2127 |
-
stopReason = "tool_use";
|
| 2128 |
-
content.push({
|
| 2129 |
-
type: 'tool_use',
|
| 2130 |
-
id: item.call_id,
|
| 2131 |
-
name: item.name,
|
| 2132 |
-
input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 2133 |
-
});
|
| 2134 |
-
}
|
| 2135 |
-
});
|
| 2136 |
-
}
|
| 2137 |
-
|
| 2138 |
-
return {
|
| 2139 |
-
id: codexResponse.response?.id || `msg_${uuidv4().replace(/-/g, '')}`,
|
| 2140 |
-
type: "message",
|
| 2141 |
-
role: "assistant",
|
| 2142 |
-
model: model,
|
| 2143 |
-
content: content,
|
| 2144 |
-
stop_reason: stopReason,
|
| 2145 |
-
usage: {
|
| 2146 |
-
input_tokens: codexResponse.response?.usage?.input_tokens || 0,
|
| 2147 |
-
output_tokens: codexResponse.response?.usage?.output_tokens || 0
|
| 2148 |
-
}
|
| 2149 |
-
};
|
| 2150 |
-
}
|
| 2151 |
-
|
| 2152 |
-
/**
|
| 2153 |
-
* Claude流式响应 -> Codex流式响应 (实际上是 Codex 转 Claude)
|
| 2154 |
-
*/
|
| 2155 |
-
toCodexStreamChunk(codexChunk, model) {
|
| 2156 |
-
const type = codexChunk.type;
|
| 2157 |
-
const resId = codexChunk.response?.id || 'default';
|
| 2158 |
-
|
| 2159 |
-
if (type === 'response.created') {
|
| 2160 |
-
return {
|
| 2161 |
-
type: "message_start",
|
| 2162 |
-
message: {
|
| 2163 |
-
id: codexChunk.response.id,
|
| 2164 |
-
type: "message",
|
| 2165 |
-
role: "assistant",
|
| 2166 |
-
content: [],
|
| 2167 |
-
model: model,
|
| 2168 |
-
usage: { input_tokens: 0, output_tokens: 0 }
|
| 2169 |
-
}
|
| 2170 |
-
};
|
| 2171 |
-
}
|
| 2172 |
-
|
| 2173 |
-
if (type === 'response.reasoning_summary_text.delta') {
|
| 2174 |
-
return {
|
| 2175 |
-
type: "content_block_delta",
|
| 2176 |
-
index: 0,
|
| 2177 |
-
delta: { type: "thinking_delta", thinking: codexChunk.delta }
|
| 2178 |
-
};
|
| 2179 |
-
}
|
| 2180 |
-
|
| 2181 |
-
if (type === 'response.output_text.delta') {
|
| 2182 |
-
return {
|
| 2183 |
-
type: "content_block_delta",
|
| 2184 |
-
index: 0,
|
| 2185 |
-
delta: { type: "text_delta", text: codexChunk.delta }
|
| 2186 |
-
};
|
| 2187 |
-
}
|
| 2188 |
-
|
| 2189 |
-
if (type === 'response.output_item.done' && codexChunk.item?.type === 'function_call') {
|
| 2190 |
-
return [
|
| 2191 |
-
{
|
| 2192 |
-
type: "content_block_start",
|
| 2193 |
-
index: 0,
|
| 2194 |
-
content_block: {
|
| 2195 |
-
type: "tool_use",
|
| 2196 |
-
id: codexChunk.item.call_id,
|
| 2197 |
-
name: codexChunk.item.name,
|
| 2198 |
-
input: {}
|
| 2199 |
-
}
|
| 2200 |
-
},
|
| 2201 |
-
{
|
| 2202 |
-
type: "content_block_delta",
|
| 2203 |
-
index: 0,
|
| 2204 |
-
delta: {
|
| 2205 |
-
type: "input_json_delta",
|
| 2206 |
-
partial_json: typeof codexChunk.item.arguments === 'string' ? codexChunk.item.arguments : JSON.stringify(codexChunk.item.arguments)
|
| 2207 |
-
}
|
| 2208 |
-
},
|
| 2209 |
-
{
|
| 2210 |
-
type: "content_block_stop",
|
| 2211 |
-
index: 0
|
| 2212 |
-
}
|
| 2213 |
-
];
|
| 2214 |
-
}
|
| 2215 |
-
|
| 2216 |
-
if (type === 'response.completed') {
|
| 2217 |
-
return [
|
| 2218 |
-
{
|
| 2219 |
-
type: "message_delta",
|
| 2220 |
-
delta: { stop_reason: "end_turn" },
|
| 2221 |
-
usage: {
|
| 2222 |
-
input_tokens: codexChunk.response.usage?.input_tokens || 0,
|
| 2223 |
-
output_tokens: codexChunk.response.usage?.output_tokens || 0
|
| 2224 |
-
}
|
| 2225 |
-
},
|
| 2226 |
-
{ type: "message_stop" }
|
| 2227 |
-
];
|
| 2228 |
-
}
|
| 2229 |
-
|
| 2230 |
-
return null;
|
| 2231 |
-
}
|
| 2232 |
-
}
|
| 2233 |
-
|
| 2234 |
-
export default ClaudeConverter;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/strategies/CodexConverter.js
DELETED
|
@@ -1,1327 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Codex 转换器
|
| 3 |
-
* 处理 OpenAI 协议与 Codex 协议之间的转换
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
-
import { BaseConverter } from '../BaseConverter.js';
|
| 8 |
-
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
| 9 |
-
import {
|
| 10 |
-
generateResponseCreated,
|
| 11 |
-
generateResponseInProgress,
|
| 12 |
-
generateOutputItemAdded,
|
| 13 |
-
generateContentPartAdded,
|
| 14 |
-
generateOutputTextDone,
|
| 15 |
-
generateContentPartDone,
|
| 16 |
-
generateOutputItemDone,
|
| 17 |
-
generateResponseCompleted
|
| 18 |
-
} from '../../providers/openai/openai-responses-core.mjs';
|
| 19 |
-
|
| 20 |
-
export class CodexConverter extends BaseConverter {
|
| 21 |
-
constructor() {
|
| 22 |
-
super('codex');
|
| 23 |
-
this.toolNameMap = new Map(); // 工具名称缩短映射: original -> short
|
| 24 |
-
this.reverseToolNameMap = new Map(); // 反向映射: short -> original
|
| 25 |
-
this.streamParams = new Map(); // 用于存储流式状态,key 为响应 ID 或临时标识
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/**
|
| 29 |
-
* 转换请求
|
| 30 |
-
*/
|
| 31 |
-
convertRequest(data, targetProtocol) {
|
| 32 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
/**
|
| 36 |
-
* 转换响应
|
| 37 |
-
*/
|
| 38 |
-
convertResponse(data, targetProtocol, model) {
|
| 39 |
-
switch (targetProtocol) {
|
| 40 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 41 |
-
return this.toOpenAIResponse(data, model);
|
| 42 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 43 |
-
return this.toOpenAIResponsesResponse(data, model);
|
| 44 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 45 |
-
return this.toGeminiResponse(data, model);
|
| 46 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 47 |
-
return this.toClaudeResponse(data, model);
|
| 48 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 49 |
-
return data; // Codex to Codex
|
| 50 |
-
default:
|
| 51 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
/**
|
| 56 |
-
* 转换流式响应块
|
| 57 |
-
*/
|
| 58 |
-
convertStreamChunk(chunk, targetProtocol, model, requestId) {
|
| 59 |
-
switch (targetProtocol) {
|
| 60 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 61 |
-
return this.toOpenAIStreamChunk(chunk, model);
|
| 62 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 63 |
-
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
| 64 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 65 |
-
return this.toGeminiStreamChunk(chunk, model);
|
| 66 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 67 |
-
return this.toClaudeStreamChunk(chunk, model, requestId);
|
| 68 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 69 |
-
return chunk; // Codex to Codex
|
| 70 |
-
default:
|
| 71 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 72 |
-
}
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
/**
|
| 76 |
-
* 转换模型列表
|
| 77 |
-
*/
|
| 78 |
-
convertModelList(data, targetProtocol) {
|
| 79 |
-
return data;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
/**
|
| 83 |
-
* OpenAI Responses → Codex 请求转换
|
| 84 |
-
*/
|
| 85 |
-
toOpenAIResponsesToCodexRequest(responsesRequest) {
|
| 86 |
-
let codexRequest = { ...responsesRequest };
|
| 87 |
-
|
| 88 |
-
// 保留监控相关字段
|
| 89 |
-
if (responsesRequest._monitorRequestId) {
|
| 90 |
-
codexRequest._monitorRequestId = responsesRequest._monitorRequestId;
|
| 91 |
-
}
|
| 92 |
-
if (responsesRequest._requestBaseUrl) {
|
| 93 |
-
codexRequest._requestBaseUrl = responsesRequest._requestBaseUrl;
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
// 处理 input 字段,如果它是字符串,则转换为消息数组
|
| 97 |
-
if (codexRequest.input && typeof codexRequest.input === 'string') {
|
| 98 |
-
const inputText = codexRequest.input;
|
| 99 |
-
codexRequest.input = [{
|
| 100 |
-
type: "message",
|
| 101 |
-
role: "user",
|
| 102 |
-
content: [{
|
| 103 |
-
type: "input_text",
|
| 104 |
-
text: inputText
|
| 105 |
-
}]
|
| 106 |
-
}];
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
// 设置Codex特定的字段
|
| 110 |
-
codexRequest.stream = true;
|
| 111 |
-
codexRequest.store = false;
|
| 112 |
-
codexRequest.parallel_tool_calls = true;
|
| 113 |
-
codexRequest.include = ['reasoning.encrypted_content'];
|
| 114 |
-
codexRequest.service_tier = responsesRequest.service_tier || 'default';
|
| 115 |
-
if (codexRequest.service_tier !== 'priority') {
|
| 116 |
-
delete codexRequest.service_tier;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
// 删除Codex不支持的字段
|
| 120 |
-
delete codexRequest.max_output_tokens;
|
| 121 |
-
delete codexRequest.max_completion_tokens;
|
| 122 |
-
delete codexRequest.temperature;
|
| 123 |
-
delete codexRequest.top_p;
|
| 124 |
-
delete codexRequest.user;
|
| 125 |
-
|
| 126 |
-
// 添加 reasoning 配置
|
| 127 |
-
codexRequest.reasoning = {
|
| 128 |
-
"effort": responsesRequest.reasoning_effort || responsesRequest.reasoning?.effort || "medium",
|
| 129 |
-
"summary": responsesRequest.reasoning?.summary || "auto"
|
| 130 |
-
};
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
// 确保 input 数组中的每个项都有 type: "message",并将系统角色转换为开发者角色
|
| 134 |
-
if (codexRequest.input && Array.isArray(codexRequest.input)) {
|
| 135 |
-
codexRequest.input = codexRequest.input.filter(item => {
|
| 136 |
-
// 如��� instructions 已存在,过滤掉 input 中的 system/developer 消息以避免重复
|
| 137 |
-
if (codexRequest.instructions && (item.role === 'system' || item.role === 'developer')) {
|
| 138 |
-
return false;
|
| 139 |
-
}
|
| 140 |
-
return true;
|
| 141 |
-
}).map(item => {
|
| 142 |
-
// 如果没有 type 或者 type 不是 message,则添加 type: "message"
|
| 143 |
-
if (!item.type || item.type !== 'message') {
|
| 144 |
-
item = { type: "message", ...item };
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
// 将系统角色转换为开发者角色
|
| 148 |
-
if (item.role === 'system') {
|
| 149 |
-
item = { ...item, role: 'developer' };
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
return item;
|
| 153 |
-
});
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
return codexRequest;
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
/**
|
| 160 |
-
* OpenAI → Codex 请求转换
|
| 161 |
-
*/
|
| 162 |
-
toOpenAIRequestToCodexRequest(data) {
|
| 163 |
-
// 构建工具名称映射
|
| 164 |
-
this.buildToolNameMap(data.tools || []);
|
| 165 |
-
|
| 166 |
-
const codexRequest = {
|
| 167 |
-
model: data.model,
|
| 168 |
-
instructions: this.buildInstructions(data),
|
| 169 |
-
input: this.convertMessages((data.messages || []).filter(m => m.role !== 'system' && m.role !== 'developer')),
|
| 170 |
-
stream: true,
|
| 171 |
-
store: false,
|
| 172 |
-
metadata: data.metadata || {},
|
| 173 |
-
reasoning: {
|
| 174 |
-
effort: data.reasoning_effort || data.reasoning?.effort || 'medium',
|
| 175 |
-
summary: data.reasoning?.summary || 'auto'
|
| 176 |
-
},
|
| 177 |
-
parallel_tool_calls: true,
|
| 178 |
-
include: ['reasoning.encrypted_content']
|
| 179 |
-
};
|
| 180 |
-
|
| 181 |
-
// 保留监控相关字段
|
| 182 |
-
if (data._monitorRequestId) {
|
| 183 |
-
codexRequest._monitorRequestId = data._monitorRequestId;
|
| 184 |
-
}
|
| 185 |
-
if (data._requestBaseUrl) {
|
| 186 |
-
codexRequest._requestBaseUrl = data._requestBaseUrl;
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
codexRequest.service_tier = data.service_tier || 'default';
|
| 190 |
-
if (codexRequest.service_tier !== 'priority') {
|
| 191 |
-
delete codexRequest.service_tier;
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
// 处理 OpenAI Responses 特有的 instructions 和 input 字段(如果存在)
|
| 195 |
-
if (data.instructions && !codexRequest.instructions) {
|
| 196 |
-
codexRequest.instructions = data.instructions;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
if (data.input && Array.isArray(data.input) && codexRequest.input.length === 0) {
|
| 200 |
-
// 如果是 OpenAI Responses 格式的 input
|
| 201 |
-
for (const item of data.input) {
|
| 202 |
-
if (item.type === 'message' && item.role !== 'system' && item.role !== 'developer') {
|
| 203 |
-
codexRequest.input.push({
|
| 204 |
-
type: 'message',
|
| 205 |
-
role: item.role === 'system' ? 'developer' : item.role,
|
| 206 |
-
content: Array.isArray(item.content) ? item.content.map(c => ({
|
| 207 |
-
type: item.role === 'assistant' ? 'output_text' : 'input_text',
|
| 208 |
-
text: c.text
|
| 209 |
-
})) : [{
|
| 210 |
-
type: item.role === 'assistant' ? 'output_text' : 'input_text',
|
| 211 |
-
text: item.content
|
| 212 |
-
}]
|
| 213 |
-
});
|
| 214 |
-
}
|
| 215 |
-
}
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
if (data.tools && data.tools.length > 0) {
|
| 219 |
-
codexRequest.tools = this.convertTools(data.tools);
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
if (data.tool_choice) {
|
| 223 |
-
codexRequest.tool_choice = this.convertToolChoice(data.tool_choice);
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
if (data.response_format || data.text?.verbosity) {
|
| 227 |
-
const textObj = {};
|
| 228 |
-
if (data.response_format) {
|
| 229 |
-
textObj.format = this.convertResponseFormat(data.response_format);
|
| 230 |
-
}
|
| 231 |
-
if (data.text?.verbosity) {
|
| 232 |
-
textObj.verbosity = data.text.verbosity;
|
| 233 |
-
}
|
| 234 |
-
codexRequest.text = textObj;
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
// 在 input 开头注入特殊指令(如果配置允许)
|
| 238 |
-
// 这里我们默认开启,因为这是为了确保 Codex 遵循指令
|
| 239 |
-
if (codexRequest.input.length > 0 && codexRequest.instructions) {
|
| 240 |
-
const firstMsg = codexRequest.input[0];
|
| 241 |
-
const specialInstruction = "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!";
|
| 242 |
-
const firstText = firstMsg.content?.[0]?.text;
|
| 243 |
-
|
| 244 |
-
if (firstMsg.role === 'user' && firstText !== specialInstruction) {
|
| 245 |
-
codexRequest.input.unshift({
|
| 246 |
-
type: "message",
|
| 247 |
-
role: "user",
|
| 248 |
-
content: [{
|
| 249 |
-
type: "input_text",
|
| 250 |
-
text: specialInstruction
|
| 251 |
-
}]
|
| 252 |
-
});
|
| 253 |
-
}
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
return codexRequest;
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
/**
|
| 260 |
-
* 构建指令
|
| 261 |
-
*/
|
| 262 |
-
buildInstructions(data) {
|
| 263 |
-
// 首先检查显式的 instructions 字段 (OpenAI Responses)
|
| 264 |
-
if (data.instructions) return data.instructions;
|
| 265 |
-
|
| 266 |
-
const systemMessages = (data.messages || []).filter(m => m.role === 'system');
|
| 267 |
-
if (systemMessages.length > 0) {
|
| 268 |
-
return systemMessages.map(m => {
|
| 269 |
-
if (typeof m.content === 'string') {
|
| 270 |
-
return m.content;
|
| 271 |
-
} else if (Array.isArray(m.content)) {
|
| 272 |
-
const textPart = m.content.find(part => part.type === 'text');
|
| 273 |
-
return textPart ? textPart.text : '';
|
| 274 |
-
}
|
| 275 |
-
return '';
|
| 276 |
-
}).join('\n').trim();
|
| 277 |
-
}
|
| 278 |
-
return '';
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
/**
|
| 282 |
-
* 转换消息
|
| 283 |
-
*/
|
| 284 |
-
convertMessages(messages) {
|
| 285 |
-
const input = [];
|
| 286 |
-
|
| 287 |
-
for (const msg of messages) {
|
| 288 |
-
const role = msg.role;
|
| 289 |
-
|
| 290 |
-
if (role === 'tool' || role === 'tool_result') {
|
| 291 |
-
input.push({
|
| 292 |
-
type: 'function_call_output',
|
| 293 |
-
call_id: msg.tool_call_id || msg.tool_use_id,
|
| 294 |
-
output: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
|
| 295 |
-
});
|
| 296 |
-
} else {
|
| 297 |
-
const codexMsg = {
|
| 298 |
-
type: 'message',
|
| 299 |
-
role: role === 'system' ? 'developer' : (role === 'model' ? 'assistant' : role),
|
| 300 |
-
content: this.convertMessageContent(msg.content, role)
|
| 301 |
-
};
|
| 302 |
-
|
| 303 |
-
if (codexMsg.content.length > 0) {
|
| 304 |
-
input.push(codexMsg);
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
if ((role === 'assistant' || role === 'model') && msg.tool_calls) {
|
| 308 |
-
for (const toolCall of msg.tool_calls) {
|
| 309 |
-
if (toolCall.type === 'function' || toolCall.function) {
|
| 310 |
-
const func = toolCall.function || toolCall;
|
| 311 |
-
const originalName = func.name;
|
| 312 |
-
const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
|
| 313 |
-
input.push({
|
| 314 |
-
type: 'function_call',
|
| 315 |
-
call_id: toolCall.id,
|
| 316 |
-
name: shortName,
|
| 317 |
-
arguments: typeof func.arguments === 'string' ? func.arguments : JSON.stringify(func.arguments)
|
| 318 |
-
});
|
| 319 |
-
}
|
| 320 |
-
}
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
// 处理 Claude 格式的 tool_use
|
| 324 |
-
if (role === 'assistant' && Array.isArray(msg.content)) {
|
| 325 |
-
for (const part of msg.content) {
|
| 326 |
-
if (part.type === 'tool_use') {
|
| 327 |
-
const originalName = part.name;
|
| 328 |
-
const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
|
| 329 |
-
input.push({
|
| 330 |
-
type: 'function_call',
|
| 331 |
-
call_id: part.id,
|
| 332 |
-
name: shortName,
|
| 333 |
-
arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input)
|
| 334 |
-
});
|
| 335 |
-
}
|
| 336 |
-
}
|
| 337 |
-
}
|
| 338 |
-
}
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
return input;
|
| 342 |
-
}
|
| 343 |
-
|
| 344 |
-
/**
|
| 345 |
-
* 转换消息内容
|
| 346 |
-
*/
|
| 347 |
-
convertMessageContent(content, role) {
|
| 348 |
-
if (!content) return [];
|
| 349 |
-
|
| 350 |
-
const isAssistant = role === 'assistant' || role === 'model';
|
| 351 |
-
|
| 352 |
-
if (typeof content === 'string') {
|
| 353 |
-
return [{
|
| 354 |
-
type: isAssistant ? 'output_text' : 'input_text',
|
| 355 |
-
text: content
|
| 356 |
-
}];
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
if (Array.isArray(content)) {
|
| 360 |
-
return content.map(part => {
|
| 361 |
-
if (typeof part === 'string') {
|
| 362 |
-
return {
|
| 363 |
-
type: isAssistant ? 'output_text' : 'input_text',
|
| 364 |
-
text: part
|
| 365 |
-
};
|
| 366 |
-
}
|
| 367 |
-
if (part.type === 'text') {
|
| 368 |
-
return {
|
| 369 |
-
type: isAssistant ? 'output_text' : 'input_text',
|
| 370 |
-
text: part.text
|
| 371 |
-
};
|
| 372 |
-
} else if ((part.type === 'image_url' || part.type === 'image') && !isAssistant) {
|
| 373 |
-
let url = '';
|
| 374 |
-
if (part.image_url) {
|
| 375 |
-
url = typeof part.image_url === 'string' ? part.image_url : part.image_url.url;
|
| 376 |
-
} else if (part.source && part.source.type === 'base64') {
|
| 377 |
-
url = `data:${part.source.media_type};base64,${part.source.data}`;
|
| 378 |
-
}
|
| 379 |
-
return url ? {
|
| 380 |
-
type: 'input_image',
|
| 381 |
-
image_url: url
|
| 382 |
-
} : null;
|
| 383 |
-
}
|
| 384 |
-
return null;
|
| 385 |
-
}).filter(Boolean);
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
return [];
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
/**
|
| 392 |
-
* 构建工具名称映射
|
| 393 |
-
*/
|
| 394 |
-
buildToolNameMap(tools) {
|
| 395 |
-
this.toolNameMap.clear();
|
| 396 |
-
this.reverseToolNameMap.clear();
|
| 397 |
-
|
| 398 |
-
const names = [];
|
| 399 |
-
for (const t of tools) {
|
| 400 |
-
if (t.type === 'function' && t.function?.name) {
|
| 401 |
-
names.push(t.function.name);
|
| 402 |
-
} else if (t.name) {
|
| 403 |
-
names.push(t.name);
|
| 404 |
-
}
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
if (names.length === 0) return;
|
| 408 |
-
|
| 409 |
-
const limit = 64;
|
| 410 |
-
const used = new Set();
|
| 411 |
-
|
| 412 |
-
const baseCandidate = (n) => {
|
| 413 |
-
if (n.length <= limit) return n;
|
| 414 |
-
if (n.startsWith('mcp__')) {
|
| 415 |
-
const idx = n.lastIndexOf('__');
|
| 416 |
-
if (idx > 0) {
|
| 417 |
-
let cand = 'mcp__' + n.slice(idx + 2);
|
| 418 |
-
return cand.length > limit ? cand.slice(0, limit) : cand;
|
| 419 |
-
}
|
| 420 |
-
}
|
| 421 |
-
return n.slice(0, limit);
|
| 422 |
-
};
|
| 423 |
-
|
| 424 |
-
for (const n of names) {
|
| 425 |
-
let cand = baseCandidate(n);
|
| 426 |
-
let uniq = cand;
|
| 427 |
-
if (used.has(uniq)) {
|
| 428 |
-
for (let i = 1; ; i++) {
|
| 429 |
-
const suffix = '_' + i;
|
| 430 |
-
const allowed = limit - suffix.length;
|
| 431 |
-
const base = cand.slice(0, Math.max(0, allowed));
|
| 432 |
-
const tmp = base + suffix;
|
| 433 |
-
if (!used.has(tmp)) {
|
| 434 |
-
uniq = tmp;
|
| 435 |
-
break;
|
| 436 |
-
}
|
| 437 |
-
}
|
| 438 |
-
}
|
| 439 |
-
used.add(uniq);
|
| 440 |
-
this.toolNameMap.set(n, uniq);
|
| 441 |
-
this.reverseToolNameMap.set(uniq, n);
|
| 442 |
-
}
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
/**
|
| 446 |
-
* 转换工具
|
| 447 |
-
*/
|
| 448 |
-
convertTools(tools) {
|
| 449 |
-
return tools.map(tool => {
|
| 450 |
-
// 处理 Claude 的 web_search
|
| 451 |
-
if (tool.type === "web_search_20250305") {
|
| 452 |
-
return { type: "web_search" };
|
| 453 |
-
}
|
| 454 |
-
|
| 455 |
-
if (tool.type !== 'function' && !tool.name) {
|
| 456 |
-
return tool;
|
| 457 |
-
}
|
| 458 |
-
|
| 459 |
-
const func = tool.function || tool;
|
| 460 |
-
const originalName = func.name;
|
| 461 |
-
const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
|
| 462 |
-
|
| 463 |
-
const result = {
|
| 464 |
-
type: 'function',
|
| 465 |
-
name: shortName,
|
| 466 |
-
description: func.description,
|
| 467 |
-
parameters: func.parameters || func.input_schema || { type: 'object', properties: {} },
|
| 468 |
-
strict: func.strict !== undefined ? func.strict : false
|
| 469 |
-
};
|
| 470 |
-
|
| 471 |
-
// 清理参数
|
| 472 |
-
if (result.parameters && result.parameters.$schema) {
|
| 473 |
-
delete result.parameters.$schema;
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
return result;
|
| 477 |
-
});
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
/**
|
| 481 |
-
* 转换 tool_choice
|
| 482 |
-
*/
|
| 483 |
-
convertToolChoice(toolChoice) {
|
| 484 |
-
if (typeof toolChoice === 'string') {
|
| 485 |
-
return toolChoice;
|
| 486 |
-
}
|
| 487 |
-
|
| 488 |
-
if (toolChoice.type === 'function') {
|
| 489 |
-
const name = toolChoice.function?.name;
|
| 490 |
-
const shortName = name ? (this.toolNameMap.get(name) || this.shortenToolName(name)) : '';
|
| 491 |
-
return {
|
| 492 |
-
type: 'function',
|
| 493 |
-
name: shortName
|
| 494 |
-
};
|
| 495 |
-
}
|
| 496 |
-
|
| 497 |
-
return toolChoice;
|
| 498 |
-
}
|
| 499 |
-
|
| 500 |
-
/**
|
| 501 |
-
* 缩短工具名称
|
| 502 |
-
*/
|
| 503 |
-
shortenToolName(name) {
|
| 504 |
-
const limit = 64;
|
| 505 |
-
if (name.length <= limit) return name;
|
| 506 |
-
if (name.startsWith('mcp__')) {
|
| 507 |
-
const idx = name.lastIndexOf('__');
|
| 508 |
-
if (idx > 0) {
|
| 509 |
-
let cand = 'mcp__' + name.slice(idx + 2);
|
| 510 |
-
return cand.length > limit ? cand.slice(0, limit) : cand;
|
| 511 |
-
}
|
| 512 |
-
}
|
| 513 |
-
return name.slice(0, limit);
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
/**
|
| 517 |
-
* 获取原始工具名称
|
| 518 |
-
*/
|
| 519 |
-
getOriginalToolName(shortName) {
|
| 520 |
-
return this.reverseToolNameMap.get(shortName) || shortName;
|
| 521 |
-
}
|
| 522 |
-
|
| 523 |
-
/**
|
| 524 |
-
* 转换响应格式
|
| 525 |
-
*/
|
| 526 |
-
convertResponseFormat(responseFormat) {
|
| 527 |
-
if (responseFormat.type === 'json_schema') {
|
| 528 |
-
return {
|
| 529 |
-
type: 'json_schema',
|
| 530 |
-
name: responseFormat.json_schema?.name || 'response',
|
| 531 |
-
schema: responseFormat.json_schema?.schema || {}
|
| 532 |
-
};
|
| 533 |
-
} else if (responseFormat.type === 'json_object') {
|
| 534 |
-
return {
|
| 535 |
-
type: 'json_object'
|
| 536 |
-
};
|
| 537 |
-
}
|
| 538 |
-
return responseFormat;
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
/**
|
| 542 |
-
* Codex → OpenAI 响应转换(非流式)
|
| 543 |
-
*/
|
| 544 |
-
toOpenAIResponse(rawJSON, model) {
|
| 545 |
-
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
| 546 |
-
if (root.type !== 'response.completed') {
|
| 547 |
-
return null;
|
| 548 |
-
}
|
| 549 |
-
|
| 550 |
-
const response = root.response;
|
| 551 |
-
const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
|
| 552 |
-
|
| 553 |
-
const openaiResponse = {
|
| 554 |
-
id: response.id || `chatcmpl-${Date.now()}`,
|
| 555 |
-
object: 'chat.completion',
|
| 556 |
-
created: unixTimestamp,
|
| 557 |
-
model: response.model || model,
|
| 558 |
-
choices: [{
|
| 559 |
-
index: 0,
|
| 560 |
-
message: {
|
| 561 |
-
role: 'assistant',
|
| 562 |
-
content: null,
|
| 563 |
-
reasoning_content: null,
|
| 564 |
-
tool_calls: null
|
| 565 |
-
},
|
| 566 |
-
finish_reason: null,
|
| 567 |
-
native_finish_reason: null
|
| 568 |
-
}],
|
| 569 |
-
usage: {
|
| 570 |
-
prompt_tokens: response.usage?.input_tokens || 0,
|
| 571 |
-
completion_tokens: response.usage?.output_tokens || 0,
|
| 572 |
-
total_tokens: response.usage?.total_tokens || 0
|
| 573 |
-
}
|
| 574 |
-
};
|
| 575 |
-
|
| 576 |
-
if (response.usage?.output_tokens_details?.reasoning_tokens) {
|
| 577 |
-
openaiResponse.usage.completion_tokens_details = {
|
| 578 |
-
reasoning_tokens: response.usage.output_tokens_details.reasoning_tokens
|
| 579 |
-
};
|
| 580 |
-
}
|
| 581 |
-
|
| 582 |
-
const output = response.output || [];
|
| 583 |
-
let contentText = '';
|
| 584 |
-
let reasoningText = '';
|
| 585 |
-
const toolCalls = [];
|
| 586 |
-
|
| 587 |
-
for (const item of output) {
|
| 588 |
-
switch (item.type) {
|
| 589 |
-
case 'reasoning':
|
| 590 |
-
if (Array.isArray(item.summary)) {
|
| 591 |
-
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
| 592 |
-
if (summaryItem) reasoningText = summaryItem.text;
|
| 593 |
-
}
|
| 594 |
-
break;
|
| 595 |
-
case 'message':
|
| 596 |
-
if (Array.isArray(item.content)) {
|
| 597 |
-
const contentItem = item.content.find(c => c.type === 'output_text');
|
| 598 |
-
if (contentItem) contentText = contentItem.text;
|
| 599 |
-
}
|
| 600 |
-
break;
|
| 601 |
-
case 'function_call':
|
| 602 |
-
toolCalls.push({
|
| 603 |
-
id: item.call_id || `call_${Date.now()}_${toolCalls.length}`,
|
| 604 |
-
type: 'function',
|
| 605 |
-
function: {
|
| 606 |
-
name: this.getOriginalToolName(item.name),
|
| 607 |
-
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments)
|
| 608 |
-
}
|
| 609 |
-
});
|
| 610 |
-
break;
|
| 611 |
-
}
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
if (contentText) openaiResponse.choices[0].message.content = contentText;
|
| 615 |
-
if (reasoningText) openaiResponse.choices[0].message.reasoning_content = reasoningText;
|
| 616 |
-
if (toolCalls.length > 0) openaiResponse.choices[0].message.tool_calls = toolCalls;
|
| 617 |
-
|
| 618 |
-
if (response.status === 'completed') {
|
| 619 |
-
openaiResponse.choices[0].finish_reason = toolCalls.length > 0 ? 'tool_calls' : 'stop';
|
| 620 |
-
openaiResponse.choices[0].native_finish_reason = 'stop';
|
| 621 |
-
}
|
| 622 |
-
|
| 623 |
-
return openaiResponse;
|
| 624 |
-
}
|
| 625 |
-
|
| 626 |
-
/**
|
| 627 |
-
* Codex → OpenAI Responses 响应转换
|
| 628 |
-
*/
|
| 629 |
-
toOpenAIResponsesResponse(rawJSON, model) {
|
| 630 |
-
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
| 631 |
-
if (root.type !== 'response.completed') {
|
| 632 |
-
return null;
|
| 633 |
-
}
|
| 634 |
-
|
| 635 |
-
const response = root.response;
|
| 636 |
-
const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
|
| 637 |
-
|
| 638 |
-
const output = [];
|
| 639 |
-
|
| 640 |
-
if (response.output && Array.isArray(response.output)) {
|
| 641 |
-
for (const item of response.output) {
|
| 642 |
-
if (item.type === 'reasoning') {
|
| 643 |
-
let reasoningText = '';
|
| 644 |
-
if (Array.isArray(item.summary)) {
|
| 645 |
-
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
| 646 |
-
if (summaryItem) reasoningText = summaryItem.text;
|
| 647 |
-
}
|
| 648 |
-
if (reasoningText) {
|
| 649 |
-
output.push({
|
| 650 |
-
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 651 |
-
type: "message",
|
| 652 |
-
role: "assistant",
|
| 653 |
-
status: "completed",
|
| 654 |
-
content: [{
|
| 655 |
-
type: "reasoning",
|
| 656 |
-
text: reasoningText
|
| 657 |
-
}]
|
| 658 |
-
});
|
| 659 |
-
}
|
| 660 |
-
} else if (item.type === 'message') {
|
| 661 |
-
let contentText = '';
|
| 662 |
-
if (Array.isArray(item.content)) {
|
| 663 |
-
const contentItem = item.content.find(c => c.type === 'output_text');
|
| 664 |
-
if (contentItem) contentText = contentItem.text;
|
| 665 |
-
}
|
| 666 |
-
if (contentText) {
|
| 667 |
-
output.push({
|
| 668 |
-
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 669 |
-
type: "message",
|
| 670 |
-
role: "assistant",
|
| 671 |
-
status: "completed",
|
| 672 |
-
content: [{
|
| 673 |
-
type: "output_text",
|
| 674 |
-
text: contentText,
|
| 675 |
-
annotations: []
|
| 676 |
-
}]
|
| 677 |
-
});
|
| 678 |
-
}
|
| 679 |
-
} else if (item.type === 'function_call') {
|
| 680 |
-
output.push({
|
| 681 |
-
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
|
| 682 |
-
type: "function_call",
|
| 683 |
-
name: this.getOriginalToolName(item.name),
|
| 684 |
-
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
|
| 685 |
-
status: "completed"
|
| 686 |
-
});
|
| 687 |
-
}
|
| 688 |
-
}
|
| 689 |
-
}
|
| 690 |
-
|
| 691 |
-
return {
|
| 692 |
-
id: response.id || `resp_${uuidv4().replace(/-/g, '')}`,
|
| 693 |
-
object: "response",
|
| 694 |
-
created_at: unixTimestamp,
|
| 695 |
-
model: response.model || model,
|
| 696 |
-
status: "completed",
|
| 697 |
-
output: output,
|
| 698 |
-
incomplete_details: response.incomplete_details || null,
|
| 699 |
-
usage: {
|
| 700 |
-
input_tokens: response.usage?.input_tokens || 0,
|
| 701 |
-
output_tokens: response.usage?.output_tokens || 0,
|
| 702 |
-
total_tokens: response.usage?.total_tokens || 0,
|
| 703 |
-
output_tokens_details: {
|
| 704 |
-
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0
|
| 705 |
-
}
|
| 706 |
-
}
|
| 707 |
-
};
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
/**
|
| 711 |
-
* Codex → Gemini 响应转换
|
| 712 |
-
*/
|
| 713 |
-
toGeminiResponse(rawJSON, model) {
|
| 714 |
-
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
| 715 |
-
if (root.type !== 'response.completed') {
|
| 716 |
-
return null;
|
| 717 |
-
}
|
| 718 |
-
|
| 719 |
-
const response = root.response;
|
| 720 |
-
const parts = [];
|
| 721 |
-
|
| 722 |
-
if (response.output && Array.isArray(response.output)) {
|
| 723 |
-
for (const item of response.output) {
|
| 724 |
-
if (item.type === 'reasoning') {
|
| 725 |
-
let reasoningText = '';
|
| 726 |
-
if (Array.isArray(item.summary)) {
|
| 727 |
-
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
| 728 |
-
if (summaryItem) reasoningText = summaryItem.text;
|
| 729 |
-
}
|
| 730 |
-
if (reasoningText) {
|
| 731 |
-
parts.push({ text: reasoningText, thought: true });
|
| 732 |
-
}
|
| 733 |
-
} else if (item.type === 'message') {
|
| 734 |
-
let contentText = '';
|
| 735 |
-
if (Array.isArray(item.content)) {
|
| 736 |
-
const contentItem = item.content.find(c => c.type === 'output_text');
|
| 737 |
-
if (contentItem) contentText = contentItem.text;
|
| 738 |
-
}
|
| 739 |
-
if (contentText) {
|
| 740 |
-
parts.push({ text: contentText });
|
| 741 |
-
}
|
| 742 |
-
} else if (item.type === 'function_call') {
|
| 743 |
-
parts.push({
|
| 744 |
-
functionCall: {
|
| 745 |
-
name: this.getOriginalToolName(item.name),
|
| 746 |
-
args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 747 |
-
}
|
| 748 |
-
});
|
| 749 |
-
}
|
| 750 |
-
}
|
| 751 |
-
}
|
| 752 |
-
|
| 753 |
-
return {
|
| 754 |
-
candidates: [{
|
| 755 |
-
content: {
|
| 756 |
-
role: "model",
|
| 757 |
-
parts: parts
|
| 758 |
-
},
|
| 759 |
-
finishReason: "STOP"
|
| 760 |
-
}],
|
| 761 |
-
usageMetadata: {
|
| 762 |
-
promptTokenCount: response.usage?.input_tokens || 0,
|
| 763 |
-
candidatesTokenCount: response.usage?.output_tokens || 0,
|
| 764 |
-
totalTokenCount: response.usage?.total_tokens || 0
|
| 765 |
-
},
|
| 766 |
-
modelVersion: response.model || model,
|
| 767 |
-
responseId: response.id
|
| 768 |
-
};
|
| 769 |
-
}
|
| 770 |
-
|
| 771 |
-
/**
|
| 772 |
-
* Codex → Claude 响应转换
|
| 773 |
-
*/
|
| 774 |
-
toClaudeResponse(rawJSON, model) {
|
| 775 |
-
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
|
| 776 |
-
if (root.type !== 'response.completed') {
|
| 777 |
-
return null;
|
| 778 |
-
}
|
| 779 |
-
|
| 780 |
-
const response = root.response;
|
| 781 |
-
const content = [];
|
| 782 |
-
let stopReason = "end_turn";
|
| 783 |
-
|
| 784 |
-
if (response.output && Array.isArray(response.output)) {
|
| 785 |
-
for (const item of response.output) {
|
| 786 |
-
if (item.type === 'reasoning') {
|
| 787 |
-
let reasoningText = '';
|
| 788 |
-
if (Array.isArray(item.summary)) {
|
| 789 |
-
const summaryItem = item.summary.find(s => s.type === 'summary_text');
|
| 790 |
-
if (summaryItem) reasoningText = summaryItem.text;
|
| 791 |
-
}
|
| 792 |
-
if (reasoningText) {
|
| 793 |
-
content.push({ type: "thinking", thinking: reasoningText });
|
| 794 |
-
}
|
| 795 |
-
} else if (item.type === 'message') {
|
| 796 |
-
let contentText = '';
|
| 797 |
-
if (Array.isArray(item.content)) {
|
| 798 |
-
const contentItem = item.content.find(c => c.type === 'output_text');
|
| 799 |
-
if (contentItem) contentText = contentItem.text;
|
| 800 |
-
}
|
| 801 |
-
if (contentText) {
|
| 802 |
-
content.push({ type: "text", text: contentText });
|
| 803 |
-
}
|
| 804 |
-
} else if (item.type === 'function_call') {
|
| 805 |
-
stopReason = "tool_use";
|
| 806 |
-
content.push({
|
| 807 |
-
type: "tool_use",
|
| 808 |
-
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
|
| 809 |
-
name: this.getOriginalToolName(item.name),
|
| 810 |
-
input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 811 |
-
});
|
| 812 |
-
}
|
| 813 |
-
}
|
| 814 |
-
}
|
| 815 |
-
|
| 816 |
-
return {
|
| 817 |
-
id: response.id || `msg_${uuidv4().replace(/-/g, '')}`,
|
| 818 |
-
type: "message",
|
| 819 |
-
role: "assistant",
|
| 820 |
-
model: response.model || model,
|
| 821 |
-
content: content,
|
| 822 |
-
stop_reason: stopReason,
|
| 823 |
-
usage: {
|
| 824 |
-
input_tokens: response.usage?.input_tokens || 0,
|
| 825 |
-
output_tokens: response.usage?.output_tokens || 0
|
| 826 |
-
}
|
| 827 |
-
};
|
| 828 |
-
}
|
| 829 |
-
|
| 830 |
-
/**
|
| 831 |
-
* Codex → OpenAI 流式响应块转换
|
| 832 |
-
*/
|
| 833 |
-
toOpenAIStreamChunk(chunk, model) {
|
| 834 |
-
const type = chunk.type;
|
| 835 |
-
// 使用固定的 key 来存储当前流的状态
|
| 836 |
-
const stateKey = 'openai_stream_current';
|
| 837 |
-
|
| 838 |
-
if (!this.streamParams.has(stateKey)) {
|
| 839 |
-
this.streamParams.set(stateKey, {
|
| 840 |
-
model: model,
|
| 841 |
-
createdAt: Math.floor(Date.now() / 1000),
|
| 842 |
-
responseID: chunk.response?.id || `chatcmpl-${Date.now()}`,
|
| 843 |
-
functionCallIndex: 0, // 初始值为 0,第一个 function_call 的 index 为 0
|
| 844 |
-
isFirstChunk: true // 标记是否是第一个内容 chunk
|
| 845 |
-
});
|
| 846 |
-
}
|
| 847 |
-
const state = this.streamParams.get(stateKey);
|
| 848 |
-
|
| 849 |
-
// 构建模板时使用当前状态中的值
|
| 850 |
-
const buildTemplate = () => ({
|
| 851 |
-
id: state.responseID,
|
| 852 |
-
object: 'chat.completion.chunk',
|
| 853 |
-
created: state.createdAt,
|
| 854 |
-
model: state.model,
|
| 855 |
-
choices: [{
|
| 856 |
-
index: 0,
|
| 857 |
-
delta: {
|
| 858 |
-
role: 'assistant',
|
| 859 |
-
content: null,
|
| 860 |
-
reasoning_content: null,
|
| 861 |
-
tool_calls: null
|
| 862 |
-
},
|
| 863 |
-
finish_reason: null,
|
| 864 |
-
native_finish_reason: null
|
| 865 |
-
}]
|
| 866 |
-
});
|
| 867 |
-
|
| 868 |
-
if (type === 'response.created') {
|
| 869 |
-
// 更新状态中的 responseID
|
| 870 |
-
state.responseID = chunk.response.id;
|
| 871 |
-
state.createdAt = chunk.response.created_at || state.createdAt;
|
| 872 |
-
state.model = chunk.response.model || state.model;
|
| 873 |
-
// 重置 functionCallIndex,确保每个新请求从 0 开始
|
| 874 |
-
state.functionCallIndex = 0;
|
| 875 |
-
state.isFirstChunk = true;
|
| 876 |
-
// response.created 不发送 chunk,等待第一个内容 chunk
|
| 877 |
-
return null;
|
| 878 |
-
}
|
| 879 |
-
|
| 880 |
-
if (type === 'response.reasoning_summary_text.delta') {
|
| 881 |
-
const results = [];
|
| 882 |
-
// 如果是第一个内容 chunk,先发送带 role 的 chunk
|
| 883 |
-
if (state.isFirstChunk) {
|
| 884 |
-
const firstTemplate = buildTemplate();
|
| 885 |
-
firstTemplate.choices[0].delta = {
|
| 886 |
-
role: 'assistant',
|
| 887 |
-
content: null,
|
| 888 |
-
reasoning_content: chunk.delta,
|
| 889 |
-
tool_calls: null
|
| 890 |
-
};
|
| 891 |
-
results.push(firstTemplate);
|
| 892 |
-
state.isFirstChunk = false;
|
| 893 |
-
} else {
|
| 894 |
-
const template = buildTemplate();
|
| 895 |
-
template.choices[0].delta = {
|
| 896 |
-
role: 'assistant',
|
| 897 |
-
content: null,
|
| 898 |
-
reasoning_content: chunk.delta,
|
| 899 |
-
tool_calls: null
|
| 900 |
-
};
|
| 901 |
-
results.push(template);
|
| 902 |
-
}
|
| 903 |
-
return results.length === 1 ? results[0] : results;
|
| 904 |
-
}
|
| 905 |
-
|
| 906 |
-
if (type === 'response.reasoning_summary_text.done') {
|
| 907 |
-
const template = buildTemplate();
|
| 908 |
-
template.choices[0].delta = {
|
| 909 |
-
role: 'assistant',
|
| 910 |
-
content: null,
|
| 911 |
-
reasoning_content: '\n\n',
|
| 912 |
-
tool_calls: null
|
| 913 |
-
};
|
| 914 |
-
return template;
|
| 915 |
-
}
|
| 916 |
-
|
| 917 |
-
if (type === 'response.output_text.delta') {
|
| 918 |
-
const results = [];
|
| 919 |
-
// 如果是第一个内容 chunk,先发送带 role 的 chunk
|
| 920 |
-
if (state.isFirstChunk) {
|
| 921 |
-
const firstTemplate = buildTemplate();
|
| 922 |
-
firstTemplate.choices[0].delta = {
|
| 923 |
-
role: 'assistant',
|
| 924 |
-
content: chunk.delta,
|
| 925 |
-
reasoning_content: null,
|
| 926 |
-
tool_calls: null
|
| 927 |
-
};
|
| 928 |
-
results.push(firstTemplate);
|
| 929 |
-
state.isFirstChunk = false;
|
| 930 |
-
} else {
|
| 931 |
-
const template = buildTemplate();
|
| 932 |
-
template.choices[0].delta = {
|
| 933 |
-
role: 'assistant',
|
| 934 |
-
content: chunk.delta,
|
| 935 |
-
reasoning_content: null,
|
| 936 |
-
tool_calls: null
|
| 937 |
-
};
|
| 938 |
-
results.push(template);
|
| 939 |
-
}
|
| 940 |
-
return results.length === 1 ? results[0] : results;
|
| 941 |
-
}
|
| 942 |
-
|
| 943 |
-
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
|
| 944 |
-
const currentIndex = state.functionCallIndex;
|
| 945 |
-
state.functionCallIndex++; // 递增,为下一个 function_call 准备
|
| 946 |
-
const template = buildTemplate();
|
| 947 |
-
template.choices[0].delta = {
|
| 948 |
-
role: 'assistant',
|
| 949 |
-
content: null,
|
| 950 |
-
reasoning_content: null,
|
| 951 |
-
tool_calls: [{
|
| 952 |
-
index: currentIndex,
|
| 953 |
-
id: chunk.item.call_id,
|
| 954 |
-
type: 'function',
|
| 955 |
-
function: {
|
| 956 |
-
name: this.getOriginalToolName(chunk.item.name),
|
| 957 |
-
arguments: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments)
|
| 958 |
-
}
|
| 959 |
-
}]
|
| 960 |
-
};
|
| 961 |
-
return template;
|
| 962 |
-
}
|
| 963 |
-
|
| 964 |
-
if (type === 'response.completed') {
|
| 965 |
-
const template = buildTemplate();
|
| 966 |
-
const finishReason = state.functionCallIndex > 0 ? 'tool_calls' : 'stop';
|
| 967 |
-
template.choices[0].delta = {
|
| 968 |
-
role: null,
|
| 969 |
-
content: null,
|
| 970 |
-
reasoning_content: null,
|
| 971 |
-
tool_calls: null
|
| 972 |
-
};
|
| 973 |
-
template.choices[0].finish_reason = finishReason;
|
| 974 |
-
template.choices[0].native_finish_reason = finishReason;
|
| 975 |
-
template.usage = {
|
| 976 |
-
prompt_tokens: chunk.response.usage?.input_tokens || 0,
|
| 977 |
-
completion_tokens: chunk.response.usage?.output_tokens || 0,
|
| 978 |
-
total_tokens: chunk.response.usage?.total_tokens || 0
|
| 979 |
-
};
|
| 980 |
-
if (chunk.response.usage?.output_tokens_details?.reasoning_tokens) {
|
| 981 |
-
template.usage.completion_tokens_details = {
|
| 982 |
-
reasoning_tokens: chunk.response.usage.output_tokens_details.reasoning_tokens
|
| 983 |
-
};
|
| 984 |
-
}
|
| 985 |
-
// 完成后清理状态
|
| 986 |
-
this.streamParams.delete(stateKey);
|
| 987 |
-
return template;
|
| 988 |
-
}
|
| 989 |
-
|
| 990 |
-
return null;
|
| 991 |
-
}
|
| 992 |
-
|
| 993 |
-
/**
|
| 994 |
-
* Codex → OpenAI Responses 流式响应转换
|
| 995 |
-
*/
|
| 996 |
-
toOpenAIResponsesStreamChunk(chunk, model) {
|
| 997 |
-
if(true){
|
| 998 |
-
return chunk;
|
| 999 |
-
}
|
| 1000 |
-
|
| 1001 |
-
const type = chunk.type;
|
| 1002 |
-
const resId = chunk.response?.id || 'default';
|
| 1003 |
-
|
| 1004 |
-
if (!this.streamParams.has(resId)) {
|
| 1005 |
-
this.streamParams.set(resId, {
|
| 1006 |
-
model: model,
|
| 1007 |
-
createdAt: Math.floor(Date.now() / 1000),
|
| 1008 |
-
responseID: resId,
|
| 1009 |
-
functionCallIndex: -1,
|
| 1010 |
-
eventsSent: new Set()
|
| 1011 |
-
});
|
| 1012 |
-
}
|
| 1013 |
-
const state = this.streamParams.get(resId);
|
| 1014 |
-
const events = [];
|
| 1015 |
-
|
| 1016 |
-
if (type === 'response.created') {
|
| 1017 |
-
state.responseID = chunk.response.id;
|
| 1018 |
-
state.model = chunk.response.model || state.model;
|
| 1019 |
-
events.push(
|
| 1020 |
-
generateResponseCreated(state.responseID, state.model),
|
| 1021 |
-
generateResponseInProgress(state.responseID)
|
| 1022 |
-
);
|
| 1023 |
-
return events;
|
| 1024 |
-
}
|
| 1025 |
-
|
| 1026 |
-
if (type === 'response.reasoning_summary_text.delta') {
|
| 1027 |
-
events.push({
|
| 1028 |
-
type: "response.reasoning_summary_text.delta",
|
| 1029 |
-
response_id: state.responseID,
|
| 1030 |
-
delta: chunk.delta
|
| 1031 |
-
});
|
| 1032 |
-
return events;
|
| 1033 |
-
}
|
| 1034 |
-
|
| 1035 |
-
if (type === 'response.output_text.delta') {
|
| 1036 |
-
if (!state.eventsSent.has('output_item_added')) {
|
| 1037 |
-
events.push(generateOutputItemAdded(state.responseID));
|
| 1038 |
-
state.eventsSent.add('output_item_added');
|
| 1039 |
-
}
|
| 1040 |
-
if (!state.eventsSent.has('content_part_added')) {
|
| 1041 |
-
events.push(generateContentPartAdded(state.responseID));
|
| 1042 |
-
state.eventsSent.add('content_part_added');
|
| 1043 |
-
}
|
| 1044 |
-
events.push({
|
| 1045 |
-
type: "response.output_text.delta",
|
| 1046 |
-
response_id: state.responseID,
|
| 1047 |
-
delta: chunk.delta
|
| 1048 |
-
});
|
| 1049 |
-
return events;
|
| 1050 |
-
}
|
| 1051 |
-
|
| 1052 |
-
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
|
| 1053 |
-
events.push({
|
| 1054 |
-
type: "response.output_item.added",
|
| 1055 |
-
response_id: state.responseID,
|
| 1056 |
-
item: {
|
| 1057 |
-
id: chunk.item.call_id,
|
| 1058 |
-
type: "function_call",
|
| 1059 |
-
name: this.getOriginalToolName(chunk.item.name),
|
| 1060 |
-
arguments: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments),
|
| 1061 |
-
status: "completed"
|
| 1062 |
-
}
|
| 1063 |
-
});
|
| 1064 |
-
events.push({
|
| 1065 |
-
type: "response.output_item.done",
|
| 1066 |
-
response_id: state.responseID,
|
| 1067 |
-
item_id: chunk.item.call_id
|
| 1068 |
-
});
|
| 1069 |
-
return events;
|
| 1070 |
-
}
|
| 1071 |
-
|
| 1072 |
-
if (type === 'response.completed') {
|
| 1073 |
-
events.push(
|
| 1074 |
-
generateOutputTextDone(state.responseID),
|
| 1075 |
-
generateContentPartDone(state.responseID),
|
| 1076 |
-
generateOutputItemDone(state.responseID)
|
| 1077 |
-
);
|
| 1078 |
-
const completedEvent = generateResponseCompleted(state.responseID);
|
| 1079 |
-
completedEvent.response.usage = {
|
| 1080 |
-
input_tokens: chunk.response.usage?.input_tokens || 0,
|
| 1081 |
-
output_tokens: chunk.response.usage?.output_tokens || 0,
|
| 1082 |
-
total_tokens: chunk.response.usage?.total_tokens || 0
|
| 1083 |
-
};
|
| 1084 |
-
events.push(completedEvent);
|
| 1085 |
-
this.streamParams.delete(resId);
|
| 1086 |
-
return events;
|
| 1087 |
-
}
|
| 1088 |
-
|
| 1089 |
-
return null;
|
| 1090 |
-
}
|
| 1091 |
-
|
| 1092 |
-
/**
|
| 1093 |
-
* Codex → Gemini 流式响应转换
|
| 1094 |
-
*/
|
| 1095 |
-
toGeminiStreamChunk(chunk, model) {
|
| 1096 |
-
const type = chunk.type;
|
| 1097 |
-
const resId = chunk.response?.id || 'default';
|
| 1098 |
-
|
| 1099 |
-
if (!this.streamParams.has(resId)) {
|
| 1100 |
-
this.streamParams.set(resId, {
|
| 1101 |
-
model: model,
|
| 1102 |
-
createdAt: Math.floor(Date.now() / 1000),
|
| 1103 |
-
responseID: resId
|
| 1104 |
-
});
|
| 1105 |
-
}
|
| 1106 |
-
const state = this.streamParams.get(resId);
|
| 1107 |
-
|
| 1108 |
-
const template = {
|
| 1109 |
-
candidates: [{
|
| 1110 |
-
content: {
|
| 1111 |
-
role: "model",
|
| 1112 |
-
parts: []
|
| 1113 |
-
}
|
| 1114 |
-
}],
|
| 1115 |
-
modelVersion: state.model,
|
| 1116 |
-
responseId: state.responseID
|
| 1117 |
-
};
|
| 1118 |
-
|
| 1119 |
-
if (type === 'response.reasoning_summary_text.delta') {
|
| 1120 |
-
template.candidates[0].content.parts.push({ text: chunk.delta, thought: true });
|
| 1121 |
-
return template;
|
| 1122 |
-
}
|
| 1123 |
-
|
| 1124 |
-
if (type === 'response.output_text.delta') {
|
| 1125 |
-
template.candidates[0].content.parts.push({ text: chunk.delta });
|
| 1126 |
-
return template;
|
| 1127 |
-
}
|
| 1128 |
-
|
| 1129 |
-
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
|
| 1130 |
-
template.candidates[0].content.parts.push({
|
| 1131 |
-
functionCall: {
|
| 1132 |
-
name: this.getOriginalToolName(chunk.item.name),
|
| 1133 |
-
args: typeof chunk.item.arguments === 'string' ? JSON.parse(chunk.item.arguments) : chunk.item.arguments
|
| 1134 |
-
}
|
| 1135 |
-
});
|
| 1136 |
-
return template;
|
| 1137 |
-
}
|
| 1138 |
-
|
| 1139 |
-
if (type === 'response.completed') {
|
| 1140 |
-
template.candidates[0].finishReason = "STOP";
|
| 1141 |
-
template.usageMetadata = {
|
| 1142 |
-
promptTokenCount: chunk.response.usage?.input_tokens || 0,
|
| 1143 |
-
candidatesTokenCount: chunk.response.usage?.output_tokens || 0,
|
| 1144 |
-
totalTokenCount: chunk.response.usage?.total_tokens || 0
|
| 1145 |
-
};
|
| 1146 |
-
this.streamParams.delete(resId);
|
| 1147 |
-
return template;
|
| 1148 |
-
}
|
| 1149 |
-
|
| 1150 |
-
return null;
|
| 1151 |
-
}
|
| 1152 |
-
|
| 1153 |
-
/**
|
| 1154 |
-
* Codex → Claude 流式响应转换
|
| 1155 |
-
*/
|
| 1156 |
-
toClaudeStreamChunk(chunk, model, requestId) {
|
| 1157 |
-
const type = chunk.type;
|
| 1158 |
-
|
| 1159 |
-
// 使用 requestId 作为流状态的隔离 key(并发安全)。
|
| 1160 |
-
// 每个请求在 handleStreamRequest 中生成唯一 requestId,
|
| 1161 |
-
// 确保同一单例 converter 上的并发流状态完全独立。
|
| 1162 |
-
const stateKey = requestId || chunk.response?.id || 'default';
|
| 1163 |
-
|
| 1164 |
-
// response.created 携带 response.id,用它来初始化该请求的流状态
|
| 1165 |
-
if (type === 'response.created') {
|
| 1166 |
-
const resId = chunk.response.id;
|
| 1167 |
-
this.streamParams.set(stateKey, {
|
| 1168 |
-
model: model,
|
| 1169 |
-
createdAt: Math.floor(Date.now() / 1000),
|
| 1170 |
-
responseID: resId,
|
| 1171 |
-
blockIndex: 0,
|
| 1172 |
-
blockStarted: false,
|
| 1173 |
-
currentBlockType: null,
|
| 1174 |
-
});
|
| 1175 |
-
const state = this.streamParams.get(stateKey);
|
| 1176 |
-
return {
|
| 1177 |
-
type: "message_start",
|
| 1178 |
-
message: {
|
| 1179 |
-
id: state.responseID,
|
| 1180 |
-
type: "message",
|
| 1181 |
-
role: "assistant",
|
| 1182 |
-
content: [],
|
| 1183 |
-
model: state.model,
|
| 1184 |
-
usage: { input_tokens: 0, output_tokens: 0 }
|
| 1185 |
-
}
|
| 1186 |
-
};
|
| 1187 |
-
}
|
| 1188 |
-
|
| 1189 |
-
if (!this.streamParams.has(stateKey)) {
|
| 1190 |
-
// 如果还没有状态(比如没有收到 response.created 就收到了其他事件),
|
| 1191 |
-
// 用 chunk 中能拿到的信息初始化
|
| 1192 |
-
this.streamParams.set(stateKey, {
|
| 1193 |
-
model: model,
|
| 1194 |
-
createdAt: Math.floor(Date.now() / 1000),
|
| 1195 |
-
responseID: chunk.response?.id || stateKey,
|
| 1196 |
-
blockIndex: 0,
|
| 1197 |
-
blockStarted: false,
|
| 1198 |
-
currentBlockType: null,
|
| 1199 |
-
});
|
| 1200 |
-
}
|
| 1201 |
-
const state = this.streamParams.get(stateKey);
|
| 1202 |
-
|
| 1203 |
-
// response.output_item.added 不产生 Claude 输出
|
| 1204 |
-
if (type === 'response.output_item.added') {
|
| 1205 |
-
return null;
|
| 1206 |
-
}
|
| 1207 |
-
|
| 1208 |
-
if (type === 'response.created') {
|
| 1209 |
-
// 已在上方处理,不应到达此处
|
| 1210 |
-
return null;
|
| 1211 |
-
}
|
| 1212 |
-
|
| 1213 |
-
if (type === 'response.reasoning_summary_text.delta') {
|
| 1214 |
-
const events = [];
|
| 1215 |
-
// If switching from a different block type, close the previous block first
|
| 1216 |
-
if (state.blockStarted && state.currentBlockType !== 'thinking') {
|
| 1217 |
-
events.push({ type: "content_block_stop", index: state.blockIndex });
|
| 1218 |
-
state.blockIndex++;
|
| 1219 |
-
state.blockStarted = false;
|
| 1220 |
-
}
|
| 1221 |
-
// Emit content_block_start on first delta for this thinking block
|
| 1222 |
-
if (!state.blockStarted) {
|
| 1223 |
-
events.push({
|
| 1224 |
-
type: "content_block_start",
|
| 1225 |
-
index: state.blockIndex,
|
| 1226 |
-
content_block: { type: "thinking", thinking: "" }
|
| 1227 |
-
});
|
| 1228 |
-
state.blockStarted = true;
|
| 1229 |
-
state.currentBlockType = 'thinking';
|
| 1230 |
-
}
|
| 1231 |
-
events.push({
|
| 1232 |
-
type: "content_block_delta",
|
| 1233 |
-
index: state.blockIndex,
|
| 1234 |
-
delta: { type: "thinking_delta", thinking: chunk.delta }
|
| 1235 |
-
});
|
| 1236 |
-
return events;
|
| 1237 |
-
}
|
| 1238 |
-
|
| 1239 |
-
if (type === 'response.output_text.delta') {
|
| 1240 |
-
const events = [];
|
| 1241 |
-
// If switching from a different block type, close the previous block first
|
| 1242 |
-
if (state.blockStarted && state.currentBlockType !== 'text') {
|
| 1243 |
-
events.push({ type: "content_block_stop", index: state.blockIndex });
|
| 1244 |
-
state.blockIndex++;
|
| 1245 |
-
state.blockStarted = false;
|
| 1246 |
-
}
|
| 1247 |
-
// Emit content_block_start on first delta for this text block
|
| 1248 |
-
if (!state.blockStarted) {
|
| 1249 |
-
events.push({
|
| 1250 |
-
type: "content_block_start",
|
| 1251 |
-
index: state.blockIndex,
|
| 1252 |
-
content_block: { type: "text", text: "" }
|
| 1253 |
-
});
|
| 1254 |
-
state.blockStarted = true;
|
| 1255 |
-
state.currentBlockType = 'text';
|
| 1256 |
-
}
|
| 1257 |
-
events.push({
|
| 1258 |
-
type: "content_block_delta",
|
| 1259 |
-
index: state.blockIndex,
|
| 1260 |
-
delta: { type: "text_delta", text: chunk.delta }
|
| 1261 |
-
});
|
| 1262 |
-
return events;
|
| 1263 |
-
}
|
| 1264 |
-
|
| 1265 |
-
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
|
| 1266 |
-
const events = [];
|
| 1267 |
-
// Close any open text/thinking block before tool_use
|
| 1268 |
-
if (state.blockStarted) {
|
| 1269 |
-
events.push({ type: "content_block_stop", index: state.blockIndex });
|
| 1270 |
-
state.blockIndex++;
|
| 1271 |
-
state.blockStarted = false;
|
| 1272 |
-
state.currentBlockType = null;
|
| 1273 |
-
}
|
| 1274 |
-
events.push(
|
| 1275 |
-
{
|
| 1276 |
-
type: "content_block_start",
|
| 1277 |
-
index: state.blockIndex,
|
| 1278 |
-
content_block: {
|
| 1279 |
-
type: "tool_use",
|
| 1280 |
-
id: chunk.item.call_id,
|
| 1281 |
-
name: this.getOriginalToolName(chunk.item.name),
|
| 1282 |
-
input: {}
|
| 1283 |
-
}
|
| 1284 |
-
},
|
| 1285 |
-
{
|
| 1286 |
-
type: "content_block_delta",
|
| 1287 |
-
index: state.blockIndex,
|
| 1288 |
-
delta: {
|
| 1289 |
-
type: "input_json_delta",
|
| 1290 |
-
partial_json: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments)
|
| 1291 |
-
}
|
| 1292 |
-
},
|
| 1293 |
-
{
|
| 1294 |
-
type: "content_block_stop",
|
| 1295 |
-
index: state.blockIndex
|
| 1296 |
-
}
|
| 1297 |
-
);
|
| 1298 |
-
state.blockIndex++;
|
| 1299 |
-
return events;
|
| 1300 |
-
}
|
| 1301 |
-
|
| 1302 |
-
if (type === 'response.completed') {
|
| 1303 |
-
const events = [];
|
| 1304 |
-
// Close any open content block before ending the message
|
| 1305 |
-
if (state.blockStarted) {
|
| 1306 |
-
events.push({ type: "content_block_stop", index: state.blockIndex });
|
| 1307 |
-
}
|
| 1308 |
-
events.push(
|
| 1309 |
-
{
|
| 1310 |
-
type: "message_delta",
|
| 1311 |
-
delta: { stop_reason: "end_turn" },
|
| 1312 |
-
usage: {
|
| 1313 |
-
input_tokens: chunk.response.usage?.input_tokens || 0,
|
| 1314 |
-
output_tokens: chunk.response.usage?.output_tokens || 0
|
| 1315 |
-
}
|
| 1316 |
-
},
|
| 1317 |
-
{ type: "message_stop" }
|
| 1318 |
-
);
|
| 1319 |
-
// 清理该请求的流状态
|
| 1320 |
-
this.streamParams.delete(stateKey);
|
| 1321 |
-
return events;
|
| 1322 |
-
}
|
| 1323 |
-
|
| 1324 |
-
return null;
|
| 1325 |
-
}
|
| 1326 |
-
|
| 1327 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/strategies/GeminiConverter.js
DELETED
|
@@ -1,1529 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Gemini转换器
|
| 3 |
-
* 处理Gemini(Google)协议与其他协议之间的转换
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
-
import { BaseConverter } from '../BaseConverter.js';
|
| 8 |
-
import {
|
| 9 |
-
checkAndAssignOrDefault,
|
| 10 |
-
OPENAI_DEFAULT_MAX_TOKENS,
|
| 11 |
-
OPENAI_DEFAULT_TEMPERATURE,
|
| 12 |
-
OPENAI_DEFAULT_TOP_P,
|
| 13 |
-
CLAUDE_DEFAULT_MAX_TOKENS,
|
| 14 |
-
CLAUDE_DEFAULT_TEMPERATURE,
|
| 15 |
-
CLAUDE_DEFAULT_TOP_P
|
| 16 |
-
} from '../utils.js';
|
| 17 |
-
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
| 18 |
-
import {
|
| 19 |
-
generateResponseCreated,
|
| 20 |
-
generateResponseInProgress,
|
| 21 |
-
generateOutputItemAdded,
|
| 22 |
-
generateContentPartAdded,
|
| 23 |
-
generateOutputTextDone,
|
| 24 |
-
generateContentPartDone,
|
| 25 |
-
generateOutputItemDone,
|
| 26 |
-
generateResponseCompleted
|
| 27 |
-
} from '../../providers/openai/openai-responses-core.mjs';
|
| 28 |
-
|
| 29 |
-
/**
|
| 30 |
-
* 修复 Gemini 返回的工具参数名称问题
|
| 31 |
-
* Gemini 有时会使用不同的参数名称,需要映射到 Claude Code 期望的格式
|
| 32 |
-
*/
|
| 33 |
-
function remapFunctionCallArgs(toolName, args) {
|
| 34 |
-
if (!args || typeof args !== 'object') return args;
|
| 35 |
-
|
| 36 |
-
const remappedArgs = { ...args };
|
| 37 |
-
const toolNameLower = toolName.toLowerCase();
|
| 38 |
-
|
| 39 |
-
// [IMPORTANT] Claude Code CLI 的 EnterPlanMode 工具禁止携带任何参数
|
| 40 |
-
if (toolName === 'EnterPlanMode') {
|
| 41 |
-
return {};
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
switch (toolNameLower) {
|
| 45 |
-
case 'grep':
|
| 46 |
-
case 'search':
|
| 47 |
-
case 'search_code_definitions':
|
| 48 |
-
case 'search_code_snippets':
|
| 49 |
-
// [FIX] Gemini hallucination: maps parameter description to "description" field
|
| 50 |
-
if (remappedArgs.description && !remappedArgs.pattern) {
|
| 51 |
-
remappedArgs.pattern = remappedArgs.description;
|
| 52 |
-
delete remappedArgs.description;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
// Gemini uses "query", Claude Code expects "pattern"
|
| 56 |
-
if (remappedArgs.query && !remappedArgs.pattern) {
|
| 57 |
-
remappedArgs.pattern = remappedArgs.query;
|
| 58 |
-
delete remappedArgs.query;
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
// [CRITICAL FIX] Claude Code uses "path" (string), NOT "paths" (array)!
|
| 62 |
-
if (!remappedArgs.path) {
|
| 63 |
-
if (remappedArgs.paths) {
|
| 64 |
-
if (Array.isArray(remappedArgs.paths)) {
|
| 65 |
-
remappedArgs.path = remappedArgs.paths[0] || '.';
|
| 66 |
-
} else if (typeof remappedArgs.paths === 'string') {
|
| 67 |
-
remappedArgs.path = remappedArgs.paths;
|
| 68 |
-
} else {
|
| 69 |
-
remappedArgs.path = '.';
|
| 70 |
-
}
|
| 71 |
-
delete remappedArgs.paths;
|
| 72 |
-
} else {
|
| 73 |
-
// Default to current directory if missing
|
| 74 |
-
remappedArgs.path = '.';
|
| 75 |
-
}
|
| 76 |
-
}
|
| 77 |
-
// Note: We keep "-n" and "output_mode" if present as they are valid in Grep schema
|
| 78 |
-
break;
|
| 79 |
-
|
| 80 |
-
case 'glob':
|
| 81 |
-
// [FIX] Gemini hallucination: maps parameter description to "description" field
|
| 82 |
-
if (remappedArgs.description && !remappedArgs.pattern) {
|
| 83 |
-
remappedArgs.pattern = remappedArgs.description;
|
| 84 |
-
delete remappedArgs.description;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
// Gemini uses "query", Claude Code expects "pattern"
|
| 88 |
-
if (remappedArgs.query && !remappedArgs.pattern) {
|
| 89 |
-
remappedArgs.pattern = remappedArgs.query;
|
| 90 |
-
delete remappedArgs.query;
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
// [CRITICAL FIX] Claude Code uses "path" (string), NOT "paths" (array)!
|
| 94 |
-
// [NOTE] 与 grep 不同,glob 不添加默认 path(参考 Rust 代码)
|
| 95 |
-
if (!remappedArgs.path) {
|
| 96 |
-
if (remappedArgs.paths) {
|
| 97 |
-
if (Array.isArray(remappedArgs.paths)) {
|
| 98 |
-
remappedArgs.path = remappedArgs.paths[0] || '.';
|
| 99 |
-
} else if (typeof remappedArgs.paths === 'string') {
|
| 100 |
-
remappedArgs.path = remappedArgs.paths;
|
| 101 |
-
} else {
|
| 102 |
-
remappedArgs.path = '.';
|
| 103 |
-
}
|
| 104 |
-
delete remappedArgs.paths;
|
| 105 |
-
}
|
| 106 |
-
// [FIX] glob 不添加默认 path,与 Rust 代码保持一致
|
| 107 |
-
}
|
| 108 |
-
break;
|
| 109 |
-
|
| 110 |
-
case 'read':
|
| 111 |
-
// Gemini might use "path" vs "file_path"
|
| 112 |
-
if (remappedArgs.path && !remappedArgs.file_path) {
|
| 113 |
-
remappedArgs.file_path = remappedArgs.path;
|
| 114 |
-
delete remappedArgs.path;
|
| 115 |
-
}
|
| 116 |
-
break;
|
| 117 |
-
|
| 118 |
-
case 'ls':
|
| 119 |
-
// LS tool: ensure "path" parameter exists
|
| 120 |
-
if (!remappedArgs.path) {
|
| 121 |
-
remappedArgs.path = '.';
|
| 122 |
-
}
|
| 123 |
-
break;
|
| 124 |
-
|
| 125 |
-
default:
|
| 126 |
-
// [NEW] [Issue #785] Generic Property Mapping for all tools
|
| 127 |
-
// If a tool has "paths" (array of 1) but no "path", convert it.
|
| 128 |
-
// [FIX] 与 Rust 代码保持一致:只在 paths.length === 1 时转换,不删除原始 paths
|
| 129 |
-
if (!remappedArgs.path && remappedArgs.paths) {
|
| 130 |
-
if (Array.isArray(remappedArgs.paths) && remappedArgs.paths.length === 1) {
|
| 131 |
-
const pathValue = remappedArgs.paths[0];
|
| 132 |
-
if (typeof pathValue === 'string') {
|
| 133 |
-
remappedArgs.path = pathValue;
|
| 134 |
-
// [FIX] Rust 代码中不删除 paths,这里也不删除
|
| 135 |
-
}
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
break;
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
return remappedArgs;
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
/**
|
| 145 |
-
* [FIX] 规范化工具名称
|
| 146 |
-
* Gemini 有时会返回 "search" 而不是 "Grep"
|
| 147 |
-
*/
|
| 148 |
-
function normalizeToolName(name) {
|
| 149 |
-
if (!name) return name;
|
| 150 |
-
|
| 151 |
-
const nameLower = name.toLowerCase();
|
| 152 |
-
if (nameLower === 'search') {
|
| 153 |
-
return 'Grep';
|
| 154 |
-
}
|
| 155 |
-
return name;
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
/**
|
| 159 |
-
* Gemini转换器类
|
| 160 |
-
* 实现Gemini协议到其他协议的转换
|
| 161 |
-
*/
|
| 162 |
-
export class GeminiConverter extends BaseConverter {
|
| 163 |
-
constructor() {
|
| 164 |
-
super('gemini');
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
/**
|
| 168 |
-
* 转换请求
|
| 169 |
-
*/
|
| 170 |
-
convertRequest(data, targetProtocol) {
|
| 171 |
-
switch (targetProtocol) {
|
| 172 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 173 |
-
return this.toOpenAIRequest(data);
|
| 174 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 175 |
-
return this.toClaudeRequest(data);
|
| 176 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 177 |
-
return this.toOpenAIResponsesRequest(data);
|
| 178 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 179 |
-
return this.toCodexRequest(data);
|
| 180 |
-
case MODEL_PROTOCOL_PREFIX.GROK:
|
| 181 |
-
return this.toGrokRequest(data);
|
| 182 |
-
default:
|
| 183 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 184 |
-
}
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
/**
|
| 188 |
-
* 转换响应
|
| 189 |
-
*/
|
| 190 |
-
convertResponse(data, targetProtocol, model) {
|
| 191 |
-
switch (targetProtocol) {
|
| 192 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 193 |
-
return this.toOpenAIResponse(data, model);
|
| 194 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 195 |
-
return this.toClaudeResponse(data, model);
|
| 196 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 197 |
-
return this.toOpenAIResponsesResponse(data, model);
|
| 198 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 199 |
-
return this.toCodexResponse(data, model);
|
| 200 |
-
default:
|
| 201 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 202 |
-
}
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
/**
|
| 206 |
-
* 转换流式响应块
|
| 207 |
-
*/
|
| 208 |
-
convertStreamChunk(chunk, targetProtocol, model) {
|
| 209 |
-
switch (targetProtocol) {
|
| 210 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 211 |
-
return this.toOpenAIStreamChunk(chunk, model);
|
| 212 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 213 |
-
return this.toClaudeStreamChunk(chunk, model);
|
| 214 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 215 |
-
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
| 216 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 217 |
-
return this.toCodexStreamChunk(chunk, model);
|
| 218 |
-
default:
|
| 219 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 220 |
-
}
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
/**
|
| 224 |
-
* 转换模型列表
|
| 225 |
-
*/
|
| 226 |
-
convertModelList(data, targetProtocol) {
|
| 227 |
-
switch (targetProtocol) {
|
| 228 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 229 |
-
return this.toOpenAIModelList(data);
|
| 230 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 231 |
-
return this.toClaudeModelList(data);
|
| 232 |
-
default:
|
| 233 |
-
return data;
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
// =========================================================================
|
| 238 |
-
// Gemini -> OpenAI 转换
|
| 239 |
-
// =========================================================================
|
| 240 |
-
|
| 241 |
-
/**
|
| 242 |
-
* Gemini请求 -> OpenAI请求
|
| 243 |
-
*/
|
| 244 |
-
toOpenAIRequest(geminiRequest) {
|
| 245 |
-
const openaiRequest = {
|
| 246 |
-
messages: [],
|
| 247 |
-
model: geminiRequest.model,
|
| 248 |
-
max_tokens: checkAndAssignOrDefault(geminiRequest.max_tokens, OPENAI_DEFAULT_MAX_TOKENS),
|
| 249 |
-
temperature: checkAndAssignOrDefault(geminiRequest.temperature, OPENAI_DEFAULT_TEMPERATURE),
|
| 250 |
-
top_p: checkAndAssignOrDefault(geminiRequest.top_p, OPENAI_DEFAULT_TOP_P),
|
| 251 |
-
};
|
| 252 |
-
|
| 253 |
-
// 处理系统指令
|
| 254 |
-
if (geminiRequest.systemInstruction && Array.isArray(geminiRequest.systemInstruction.parts)) {
|
| 255 |
-
const systemContent = this.processGeminiPartsToOpenAIContent(geminiRequest.systemInstruction.parts);
|
| 256 |
-
if (systemContent) {
|
| 257 |
-
openaiRequest.messages.push({
|
| 258 |
-
role: 'system',
|
| 259 |
-
content: systemContent
|
| 260 |
-
});
|
| 261 |
-
}
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
// 处理内容
|
| 265 |
-
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
|
| 266 |
-
geminiRequest.contents.forEach(content => {
|
| 267 |
-
if (content && Array.isArray(content.parts)) {
|
| 268 |
-
const openaiContent = this.processGeminiPartsToOpenAIContent(content.parts);
|
| 269 |
-
if (openaiContent && openaiContent.length > 0) {
|
| 270 |
-
const openaiRole = content.role === 'model' ? 'assistant' : content.role;
|
| 271 |
-
openaiRequest.messages.push({
|
| 272 |
-
role: openaiRole,
|
| 273 |
-
content: openaiContent
|
| 274 |
-
});
|
| 275 |
-
}
|
| 276 |
-
}
|
| 277 |
-
});
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
return openaiRequest;
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
-
/**
|
| 284 |
-
* Gemini响应 -> OpenAI响应
|
| 285 |
-
*/
|
| 286 |
-
toOpenAIResponse(geminiResponse, model) {
|
| 287 |
-
const content = this.processGeminiResponseContent(geminiResponse);
|
| 288 |
-
|
| 289 |
-
// 提取 tool_calls
|
| 290 |
-
const toolCalls = [];
|
| 291 |
-
let finishReason = "stop";
|
| 292 |
-
|
| 293 |
-
if (geminiResponse && geminiResponse.candidates) {
|
| 294 |
-
for (const candidate of geminiResponse.candidates) {
|
| 295 |
-
if (candidate.content && candidate.content.parts) {
|
| 296 |
-
for (const part of candidate.content.parts) {
|
| 297 |
-
if (part.functionCall) {
|
| 298 |
-
toolCalls.push({
|
| 299 |
-
id: part.functionCall.id || `call_${uuidv4()}`,
|
| 300 |
-
type: 'function',
|
| 301 |
-
function: {
|
| 302 |
-
name: part.functionCall.name,
|
| 303 |
-
arguments: typeof part.functionCall.args === 'string'
|
| 304 |
-
? part.functionCall.args
|
| 305 |
-
: JSON.stringify(part.functionCall.args)
|
| 306 |
-
}
|
| 307 |
-
});
|
| 308 |
-
}
|
| 309 |
-
}
|
| 310 |
-
}
|
| 311 |
-
}
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
// 如果有工具调用,设置 finish_reason 为 tool_calls
|
| 315 |
-
if (toolCalls.length > 0) {
|
| 316 |
-
finishReason = "tool_calls";
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
const message = {
|
| 320 |
-
role: "assistant",
|
| 321 |
-
content: content
|
| 322 |
-
};
|
| 323 |
-
|
| 324 |
-
// 只有在有 tool_calls 时才添加该字段
|
| 325 |
-
if (toolCalls.length > 0) {
|
| 326 |
-
message.tool_calls = toolCalls;
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
return {
|
| 330 |
-
id: `chatcmpl-${uuidv4()}`,
|
| 331 |
-
object: "chat.completion",
|
| 332 |
-
created: Math.floor(Date.now() / 1000),
|
| 333 |
-
model: model,
|
| 334 |
-
choices: [{
|
| 335 |
-
index: 0,
|
| 336 |
-
message: message,
|
| 337 |
-
finish_reason: finishReason,
|
| 338 |
-
}],
|
| 339 |
-
usage: geminiResponse.usageMetadata ? {
|
| 340 |
-
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
|
| 341 |
-
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
|
| 342 |
-
total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0,
|
| 343 |
-
cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0,
|
| 344 |
-
prompt_tokens_details: {
|
| 345 |
-
cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0
|
| 346 |
-
},
|
| 347 |
-
completion_tokens_details: {
|
| 348 |
-
reasoning_tokens: geminiResponse.usageMetadata.thoughtsTokenCount || 0
|
| 349 |
-
}
|
| 350 |
-
} : {
|
| 351 |
-
prompt_tokens: 0,
|
| 352 |
-
completion_tokens: 0,
|
| 353 |
-
total_tokens: 0,
|
| 354 |
-
cached_tokens: 0,
|
| 355 |
-
prompt_tokens_details: {
|
| 356 |
-
cached_tokens: 0
|
| 357 |
-
},
|
| 358 |
-
completion_tokens_details: {
|
| 359 |
-
reasoning_tokens: 0
|
| 360 |
-
}
|
| 361 |
-
},
|
| 362 |
-
};
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
/**
|
| 366 |
-
* Gemini流式响应 -> OpenAI流式响应
|
| 367 |
-
*/
|
| 368 |
-
toOpenAIStreamChunk(geminiChunk, model) {
|
| 369 |
-
if (!geminiChunk) return null;
|
| 370 |
-
|
| 371 |
-
const candidate = geminiChunk.candidates?.[0];
|
| 372 |
-
if (!candidate) return null;
|
| 373 |
-
|
| 374 |
-
let content = '';
|
| 375 |
-
const toolCalls = [];
|
| 376 |
-
|
| 377 |
-
// 从parts中提取文本和tool calls
|
| 378 |
-
const parts = candidate.content?.parts;
|
| 379 |
-
if (parts && Array.isArray(parts)) {
|
| 380 |
-
for (const part of parts) {
|
| 381 |
-
if (part.text) {
|
| 382 |
-
content += part.text;
|
| 383 |
-
}
|
| 384 |
-
if (part.functionCall) {
|
| 385 |
-
toolCalls.push({
|
| 386 |
-
index: toolCalls.length,
|
| 387 |
-
id: part.functionCall.id || `call_${uuidv4()}`,
|
| 388 |
-
type: 'function',
|
| 389 |
-
function: {
|
| 390 |
-
name: part.functionCall.name,
|
| 391 |
-
arguments: typeof part.functionCall.args === 'string'
|
| 392 |
-
? part.functionCall.args
|
| 393 |
-
: JSON.stringify(part.functionCall.args)
|
| 394 |
-
}
|
| 395 |
-
});
|
| 396 |
-
}
|
| 397 |
-
// thoughtSignature is ignored (internal Gemini data)
|
| 398 |
-
}
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
// 处理finishReason
|
| 402 |
-
let finishReason = null;
|
| 403 |
-
if (candidate.finishReason) {
|
| 404 |
-
const finishReasonMap = {
|
| 405 |
-
'FINISH_REASON_UNSPECIFIED': 'stop',
|
| 406 |
-
'STOP': 'stop',
|
| 407 |
-
'MAX_TOKENS': 'length',
|
| 408 |
-
'SAFETY': 'content_filter',
|
| 409 |
-
'RECITATION': 'content_filter',
|
| 410 |
-
'OTHER': 'stop',
|
| 411 |
-
'BLOCKLIST': 'content_filter',
|
| 412 |
-
'PROHIBITED_CONTENT': 'content_filter',
|
| 413 |
-
'SPII': 'content_filter',
|
| 414 |
-
'MALFORMED_FUNCTION_CALL': 'stop',
|
| 415 |
-
'MODEL_ARMOR': 'content_filter',
|
| 416 |
-
};
|
| 417 |
-
finishReason = finishReasonMap[candidate.finishReason] || 'stop';
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
// [FIX] 适配 Gemini 流式:Gemini 的最后一条流式消息通常不带 functionCall
|
| 421 |
-
// 如果当前 chunk 包含工具调用,直接将其标记为 tool_calls
|
| 422 |
-
if (toolCalls.length > 0) {
|
| 423 |
-
finishReason = 'tool_calls';
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
// 构建delta对象
|
| 427 |
-
const delta = {};
|
| 428 |
-
if (content) delta.content = content;
|
| 429 |
-
if (toolCalls.length > 0) delta.tool_calls = toolCalls;
|
| 430 |
-
|
| 431 |
-
// Don't return empty delta chunks
|
| 432 |
-
if (Object.keys(delta).length === 0 && !finishReason) {
|
| 433 |
-
return null;
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
-
const chunk = {
|
| 437 |
-
id: `chatcmpl-${uuidv4()}`,
|
| 438 |
-
object: "chat.completion.chunk",
|
| 439 |
-
created: Math.floor(Date.now() / 1000),
|
| 440 |
-
model: model,
|
| 441 |
-
choices: [{
|
| 442 |
-
index: 0,
|
| 443 |
-
delta: delta,
|
| 444 |
-
finish_reason: finishReason,
|
| 445 |
-
}],
|
| 446 |
-
};
|
| 447 |
-
|
| 448 |
-
if(geminiChunk.usageMetadata){
|
| 449 |
-
chunk.usage = {
|
| 450 |
-
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
| 451 |
-
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
| 452 |
-
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
|
| 453 |
-
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
|
| 454 |
-
prompt_tokens_details: {
|
| 455 |
-
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
|
| 456 |
-
},
|
| 457 |
-
completion_tokens_details: {
|
| 458 |
-
reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
|
| 459 |
-
}
|
| 460 |
-
};
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
return chunk;
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
/**
|
| 467 |
-
* Gemini模型列表 -> OpenAI模型列表
|
| 468 |
-
*/
|
| 469 |
-
toOpenAIModelList(geminiModels) {
|
| 470 |
-
return {
|
| 471 |
-
object: "list",
|
| 472 |
-
data: geminiModels.models.map(m => {
|
| 473 |
-
const modelId = m.name.startsWith('models/') ? m.name.substring(7) : m.name;
|
| 474 |
-
return {
|
| 475 |
-
id: modelId,
|
| 476 |
-
object: "model",
|
| 477 |
-
created: Math.floor(Date.now() / 1000),
|
| 478 |
-
owned_by: "google",
|
| 479 |
-
display_name: m.displayName || modelId,
|
| 480 |
-
};
|
| 481 |
-
}),
|
| 482 |
-
};
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
/**
|
| 486 |
-
* 处理Gemini parts到OpenAI内容
|
| 487 |
-
*/
|
| 488 |
-
processGeminiPartsToOpenAIContent(parts) {
|
| 489 |
-
if (!parts || !Array.isArray(parts)) return '';
|
| 490 |
-
|
| 491 |
-
const contentArray = [];
|
| 492 |
-
|
| 493 |
-
parts.forEach(part => {
|
| 494 |
-
if (!part) return;
|
| 495 |
-
|
| 496 |
-
if (typeof part.text === 'string') {
|
| 497 |
-
contentArray.push({
|
| 498 |
-
type: 'text',
|
| 499 |
-
text: part.text
|
| 500 |
-
});
|
| 501 |
-
}
|
| 502 |
-
|
| 503 |
-
if (part.inlineData) {
|
| 504 |
-
const { mimeType, data } = part.inlineData;
|
| 505 |
-
if (mimeType && data) {
|
| 506 |
-
contentArray.push({
|
| 507 |
-
type: 'image_url',
|
| 508 |
-
image_url: {
|
| 509 |
-
url: `data:${mimeType};base64,${data}`
|
| 510 |
-
}
|
| 511 |
-
});
|
| 512 |
-
}
|
| 513 |
-
}
|
| 514 |
-
|
| 515 |
-
if (part.fileData) {
|
| 516 |
-
const { mimeType, fileUri } = part.fileData;
|
| 517 |
-
if (mimeType && fileUri) {
|
| 518 |
-
if (mimeType.startsWith('image/')) {
|
| 519 |
-
contentArray.push({
|
| 520 |
-
type: 'image_url',
|
| 521 |
-
image_url: {
|
| 522 |
-
url: fileUri
|
| 523 |
-
}
|
| 524 |
-
});
|
| 525 |
-
} else if (mimeType.startsWith('audio/')) {
|
| 526 |
-
contentArray.push({
|
| 527 |
-
type: 'text',
|
| 528 |
-
text: `[Audio file: ${fileUri}]`
|
| 529 |
-
});
|
| 530 |
-
}
|
| 531 |
-
}
|
| 532 |
-
}
|
| 533 |
-
});
|
| 534 |
-
|
| 535 |
-
return contentArray.length === 1 && contentArray[0].type === 'text'
|
| 536 |
-
? contentArray[0].text
|
| 537 |
-
: contentArray;
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
/**
|
| 541 |
-
* 处理Gemini响应内容
|
| 542 |
-
*/
|
| 543 |
-
processGeminiResponseContent(geminiResponse) {
|
| 544 |
-
if (!geminiResponse || !geminiResponse.candidates) return '';
|
| 545 |
-
|
| 546 |
-
const contents = [];
|
| 547 |
-
|
| 548 |
-
geminiResponse.candidates.forEach(candidate => {
|
| 549 |
-
if (candidate.content && candidate.content.parts) {
|
| 550 |
-
candidate.content.parts.forEach(part => {
|
| 551 |
-
if (part.text) {
|
| 552 |
-
contents.push(part.text);
|
| 553 |
-
}
|
| 554 |
-
});
|
| 555 |
-
}
|
| 556 |
-
});
|
| 557 |
-
|
| 558 |
-
return contents.join('\n');
|
| 559 |
-
}
|
| 560 |
-
|
| 561 |
-
// =========================================================================
|
| 562 |
-
// Gemini -> Claude 转换
|
| 563 |
-
// =========================================================================
|
| 564 |
-
|
| 565 |
-
/**
|
| 566 |
-
* Gemini请求 -> Claude请求
|
| 567 |
-
*/
|
| 568 |
-
toClaudeRequest(geminiRequest) {
|
| 569 |
-
const claudeRequest = {
|
| 570 |
-
model: geminiRequest.model || 'claude-3-opus',
|
| 571 |
-
messages: [],
|
| 572 |
-
max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, CLAUDE_DEFAULT_MAX_TOKENS),
|
| 573 |
-
temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, CLAUDE_DEFAULT_TEMPERATURE),
|
| 574 |
-
top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, CLAUDE_DEFAULT_TOP_P),
|
| 575 |
-
};
|
| 576 |
-
|
| 577 |
-
// 处理系统指令
|
| 578 |
-
if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
|
| 579 |
-
const systemText = geminiRequest.systemInstruction.parts
|
| 580 |
-
.filter(p => p.text)
|
| 581 |
-
.map(p => p.text)
|
| 582 |
-
.join('\n');
|
| 583 |
-
if (systemText) {
|
| 584 |
-
claudeRequest.system = systemText;
|
| 585 |
-
}
|
| 586 |
-
}
|
| 587 |
-
|
| 588 |
-
// 处理内容
|
| 589 |
-
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
|
| 590 |
-
geminiRequest.contents.forEach(content => {
|
| 591 |
-
if (!content || !content.parts) return;
|
| 592 |
-
|
| 593 |
-
const role = content.role === 'model' ? 'assistant' : 'user';
|
| 594 |
-
const claudeContent = this.processGeminiPartsToClaudeContent(content.parts);
|
| 595 |
-
|
| 596 |
-
if (claudeContent.length > 0) {
|
| 597 |
-
claudeRequest.messages.push({
|
| 598 |
-
role: role,
|
| 599 |
-
content: claudeContent
|
| 600 |
-
});
|
| 601 |
-
}
|
| 602 |
-
});
|
| 603 |
-
}
|
| 604 |
-
|
| 605 |
-
// 处理工具
|
| 606 |
-
if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
|
| 607 |
-
claudeRequest.tools = geminiRequest.tools[0].functionDeclarations.map(func => ({
|
| 608 |
-
name: func.name,
|
| 609 |
-
description: func.description || '',
|
| 610 |
-
input_schema: func.parameters || { type: 'object', properties: {} }
|
| 611 |
-
}));
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
return claudeRequest;
|
| 615 |
-
}
|
| 616 |
-
|
| 617 |
-
/**
|
| 618 |
-
* Gemini响应 -> Claude响应
|
| 619 |
-
*/
|
| 620 |
-
toClaudeResponse(geminiResponse, model) {
|
| 621 |
-
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
|
| 622 |
-
return {
|
| 623 |
-
id: `msg_${uuidv4()}`,
|
| 624 |
-
type: "message",
|
| 625 |
-
role: "assistant",
|
| 626 |
-
content: [],
|
| 627 |
-
model: model,
|
| 628 |
-
stop_reason: "end_turn",
|
| 629 |
-
stop_sequence: null,
|
| 630 |
-
usage: {
|
| 631 |
-
input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0,
|
| 632 |
-
output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0
|
| 633 |
-
}
|
| 634 |
-
};
|
| 635 |
-
}
|
| 636 |
-
|
| 637 |
-
const candidate = geminiResponse.candidates[0];
|
| 638 |
-
const { content, hasToolUse } = this.processGeminiResponseToClaudeContent(geminiResponse);
|
| 639 |
-
const finishReason = candidate.finishReason;
|
| 640 |
-
let stopReason = "end_turn";
|
| 641 |
-
|
| 642 |
-
// - 如果有工具调用,stop_reason 应该是 "tool_use"
|
| 643 |
-
if (hasToolUse) {
|
| 644 |
-
stopReason = 'tool_use';
|
| 645 |
-
} else if (finishReason) {
|
| 646 |
-
switch (finishReason) {
|
| 647 |
-
case 'STOP':
|
| 648 |
-
stopReason = 'end_turn';
|
| 649 |
-
break;
|
| 650 |
-
case 'MAX_TOKENS':
|
| 651 |
-
stopReason = 'max_tokens';
|
| 652 |
-
break;
|
| 653 |
-
case 'SAFETY':
|
| 654 |
-
stopReason = 'safety';
|
| 655 |
-
break;
|
| 656 |
-
case 'RECITATION':
|
| 657 |
-
stopReason = 'recitation';
|
| 658 |
-
break;
|
| 659 |
-
case 'OTHER':
|
| 660 |
-
stopReason = 'other';
|
| 661 |
-
break;
|
| 662 |
-
default:
|
| 663 |
-
stopReason = 'end_turn';
|
| 664 |
-
}
|
| 665 |
-
}
|
| 666 |
-
|
| 667 |
-
return {
|
| 668 |
-
id: `msg_${uuidv4()}`,
|
| 669 |
-
type: "message",
|
| 670 |
-
role: "assistant",
|
| 671 |
-
content: content,
|
| 672 |
-
model: model,
|
| 673 |
-
stop_reason: stopReason,
|
| 674 |
-
stop_sequence: null,
|
| 675 |
-
usage: {
|
| 676 |
-
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
| 677 |
-
cache_creation_input_tokens: 0,
|
| 678 |
-
cache_read_input_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0,
|
| 679 |
-
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0
|
| 680 |
-
}
|
| 681 |
-
};
|
| 682 |
-
}
|
| 683 |
-
|
| 684 |
-
/**
|
| 685 |
-
* Gemini流式响应 -> Claude流式响应
|
| 686 |
-
*/
|
| 687 |
-
toClaudeStreamChunk(geminiChunk, model) {
|
| 688 |
-
if (!geminiChunk) return null;
|
| 689 |
-
|
| 690 |
-
// 处理完整的Gemini chunk对象
|
| 691 |
-
if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
|
| 692 |
-
const candidate = geminiChunk.candidates?.[0];
|
| 693 |
-
|
| 694 |
-
if (candidate) {
|
| 695 |
-
const parts = candidate.content?.parts;
|
| 696 |
-
|
| 697 |
-
// thinking 和 text 块
|
| 698 |
-
if (parts && Array.isArray(parts)) {
|
| 699 |
-
const results = [];
|
| 700 |
-
let hasToolUse = false;
|
| 701 |
-
|
| 702 |
-
for (const part of parts) {
|
| 703 |
-
if (!part) continue;
|
| 704 |
-
|
| 705 |
-
if (typeof part.text === 'string') {
|
| 706 |
-
if (part.thought === true) {
|
| 707 |
-
// [FIX] 这是一个 thinking 块
|
| 708 |
-
const thinkingResult = {
|
| 709 |
-
type: "content_block_delta",
|
| 710 |
-
index: 0,
|
| 711 |
-
delta: {
|
| 712 |
-
type: "thinking_delta",
|
| 713 |
-
thinking: part.text
|
| 714 |
-
}
|
| 715 |
-
};
|
| 716 |
-
results.push(thinkingResult);
|
| 717 |
-
|
| 718 |
-
// 如果有签名,发送 signature_delta
|
| 719 |
-
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
| 720 |
-
const rawSignature = part.thoughtSignature || part.thought_signature;
|
| 721 |
-
if (rawSignature) {
|
| 722 |
-
let signature = rawSignature;
|
| 723 |
-
try {
|
| 724 |
-
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
| 725 |
-
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
| 726 |
-
signature = decoded;
|
| 727 |
-
}
|
| 728 |
-
} catch (e) {
|
| 729 |
-
// 解码失败,保持原样
|
| 730 |
-
}
|
| 731 |
-
results.push({
|
| 732 |
-
type: "content_block_delta",
|
| 733 |
-
index: 0,
|
| 734 |
-
delta: {
|
| 735 |
-
type: "signature_delta",
|
| 736 |
-
signature: signature
|
| 737 |
-
}
|
| 738 |
-
});
|
| 739 |
-
}
|
| 740 |
-
} else {
|
| 741 |
-
// 普通文本
|
| 742 |
-
results.push({
|
| 743 |
-
type: "content_block_delta",
|
| 744 |
-
index: 0,
|
| 745 |
-
delta: {
|
| 746 |
-
type: "text_delta",
|
| 747 |
-
text: part.text
|
| 748 |
-
}
|
| 749 |
-
});
|
| 750 |
-
}
|
| 751 |
-
}
|
| 752 |
-
|
| 753 |
-
// [FIX] 处理 functionCall
|
| 754 |
-
if (part.functionCall) {
|
| 755 |
-
hasToolUse = true;
|
| 756 |
-
// [FIX] 规范化工具名称和参数映射
|
| 757 |
-
const toolName = normalizeToolName(part.functionCall.name);
|
| 758 |
-
const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {});
|
| 759 |
-
|
| 760 |
-
// 发送 tool_use 开始
|
| 761 |
-
const toolId = part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`;
|
| 762 |
-
results.push({
|
| 763 |
-
type: "content_block_start",
|
| 764 |
-
index: 0,
|
| 765 |
-
content_block: {
|
| 766 |
-
type: "tool_use",
|
| 767 |
-
id: toolId,
|
| 768 |
-
name: toolName,
|
| 769 |
-
input: {}
|
| 770 |
-
}
|
| 771 |
-
});
|
| 772 |
-
// 发送参数
|
| 773 |
-
results.push({
|
| 774 |
-
type: "content_block_delta",
|
| 775 |
-
index: 0,
|
| 776 |
-
delta: {
|
| 777 |
-
type: "input_json_delta",
|
| 778 |
-
partial_json: JSON.stringify(remappedArgs)
|
| 779 |
-
}
|
| 780 |
-
});
|
| 781 |
-
}
|
| 782 |
-
}
|
| 783 |
-
|
| 784 |
-
// [FIX] 如果有工具���用,添加 message_delta 事件设置 stop_reason 为 tool_use
|
| 785 |
-
if (hasToolUse && candidate.finishReason) {
|
| 786 |
-
const messageDelta = {
|
| 787 |
-
type: "message_delta",
|
| 788 |
-
delta: {
|
| 789 |
-
stop_reason: 'tool_use'
|
| 790 |
-
}
|
| 791 |
-
};
|
| 792 |
-
if (geminiChunk.usageMetadata) {
|
| 793 |
-
messageDelta.usage = {
|
| 794 |
-
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
| 795 |
-
cache_creation_input_tokens: 0,
|
| 796 |
-
cache_read_input_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
|
| 797 |
-
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0
|
| 798 |
-
};
|
| 799 |
-
}
|
| 800 |
-
results.push(messageDelta);
|
| 801 |
-
}
|
| 802 |
-
|
| 803 |
-
// 如果有多个结果,返回数组;否则返回单个或 null
|
| 804 |
-
if (results.length > 1) {
|
| 805 |
-
return results;
|
| 806 |
-
} else if (results.length === 1) {
|
| 807 |
-
return results[0];
|
| 808 |
-
}
|
| 809 |
-
}
|
| 810 |
-
|
| 811 |
-
// 处理finishReason
|
| 812 |
-
if (candidate.finishReason) {
|
| 813 |
-
const result = {
|
| 814 |
-
type: "message_delta",
|
| 815 |
-
delta: {
|
| 816 |
-
stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' :
|
| 817 |
-
candidate.finishReason === 'MAX_TOKENS' ? 'max_tokens' :
|
| 818 |
-
candidate.finishReason.toLowerCase()
|
| 819 |
-
}
|
| 820 |
-
};
|
| 821 |
-
|
| 822 |
-
// 添加 usage 信息
|
| 823 |
-
if (geminiChunk.usageMetadata) {
|
| 824 |
-
result.usage = {
|
| 825 |
-
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
| 826 |
-
cache_creation_input_tokens: 0,
|
| 827 |
-
cache_read_input_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
|
| 828 |
-
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
| 829 |
-
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
| 830 |
-
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
| 831 |
-
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
|
| 832 |
-
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
|
| 833 |
-
};
|
| 834 |
-
}
|
| 835 |
-
|
| 836 |
-
return result;
|
| 837 |
-
}
|
| 838 |
-
}
|
| 839 |
-
}
|
| 840 |
-
|
| 841 |
-
// 向后兼容:处理字符串格式
|
| 842 |
-
if (typeof geminiChunk === 'string') {
|
| 843 |
-
return {
|
| 844 |
-
type: "content_block_delta",
|
| 845 |
-
index: 0,
|
| 846 |
-
delta: {
|
| 847 |
-
type: "text_delta",
|
| 848 |
-
text: geminiChunk
|
| 849 |
-
}
|
| 850 |
-
};
|
| 851 |
-
}
|
| 852 |
-
|
| 853 |
-
return null;
|
| 854 |
-
}
|
| 855 |
-
|
| 856 |
-
/**
|
| 857 |
-
* Gemini模型列表 -> Claude模型列表
|
| 858 |
-
*/
|
| 859 |
-
toClaudeModelList(geminiModels) {
|
| 860 |
-
return {
|
| 861 |
-
models: geminiModels.models.map(m => ({
|
| 862 |
-
name: m.name.startsWith('models/') ? m.name.substring(7) : m.name,
|
| 863 |
-
description: "",
|
| 864 |
-
})),
|
| 865 |
-
};
|
| 866 |
-
}
|
| 867 |
-
|
| 868 |
-
/**
|
| 869 |
-
* 处理Gemini parts到Claude内容
|
| 870 |
-
*/
|
| 871 |
-
processGeminiPartsToClaudeContent(parts) {
|
| 872 |
-
if (!parts || !Array.isArray(parts)) return [];
|
| 873 |
-
|
| 874 |
-
const content = [];
|
| 875 |
-
|
| 876 |
-
parts.forEach(part => {
|
| 877 |
-
if (!part) return;
|
| 878 |
-
|
| 879 |
-
// 处理 thinking 块
|
| 880 |
-
// Gemini 使用 thought: true 和 thoughtSignature 表示思考内容
|
| 881 |
-
// [FIX] 同时支持 thoughtSignature 和 thought_signature(Gemini CLI 可能使用下划线格式)
|
| 882 |
-
if (part.text) {
|
| 883 |
-
if (part.thought === true) {
|
| 884 |
-
// 这是一个 thinking 块
|
| 885 |
-
const thinkingBlock = {
|
| 886 |
-
type: 'thinking',
|
| 887 |
-
thinking: part.text
|
| 888 |
-
};
|
| 889 |
-
// 处理签名 - 可能是 Base64 编码的
|
| 890 |
-
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
| 891 |
-
const rawSignature = part.thoughtSignature || part.thought_signature;
|
| 892 |
-
if (rawSignature) {
|
| 893 |
-
let signature = rawSignature;
|
| 894 |
-
// 尝试 Base64 解码
|
| 895 |
-
try {
|
| 896 |
-
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
| 897 |
-
// 检查解码后是否是有效的 UTF-8 字符串
|
| 898 |
-
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
| 899 |
-
signature = decoded;
|
| 900 |
-
}
|
| 901 |
-
} catch (e) {
|
| 902 |
-
// 解码失败,保持原样
|
| 903 |
-
}
|
| 904 |
-
thinkingBlock.signature = signature;
|
| 905 |
-
}
|
| 906 |
-
content.push(thinkingBlock);
|
| 907 |
-
} else {
|
| 908 |
-
// 普通文本
|
| 909 |
-
content.push({
|
| 910 |
-
type: 'text',
|
| 911 |
-
text: part.text
|
| 912 |
-
});
|
| 913 |
-
}
|
| 914 |
-
}
|
| 915 |
-
|
| 916 |
-
if (part.inlineData) {
|
| 917 |
-
content.push({
|
| 918 |
-
type: 'image',
|
| 919 |
-
source: {
|
| 920 |
-
type: 'base64',
|
| 921 |
-
media_type: part.inlineData.mimeType,
|
| 922 |
-
data: part.inlineData.data
|
| 923 |
-
}
|
| 924 |
-
});
|
| 925 |
-
}
|
| 926 |
-
|
| 927 |
-
if (part.functionCall) {
|
| 928 |
-
// [FIX] 规范化工具名称和参数映射
|
| 929 |
-
const toolName = normalizeToolName(part.functionCall.name);
|
| 930 |
-
const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {});
|
| 931 |
-
|
| 932 |
-
// [FIX] 使用 Gemini 提供的 id,如果没有则生成
|
| 933 |
-
const toolUseBlock = {
|
| 934 |
-
type: 'tool_use',
|
| 935 |
-
id: part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`,
|
| 936 |
-
name: toolName,
|
| 937 |
-
input: remappedArgs
|
| 938 |
-
};
|
| 939 |
-
// [FIX] 如果有签名,添加到 tool_use 块
|
| 940 |
-
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
| 941 |
-
const rawSignature = part.thoughtSignature || part.thought_signature;
|
| 942 |
-
if (rawSignature) {
|
| 943 |
-
let signature = rawSignature;
|
| 944 |
-
try {
|
| 945 |
-
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
| 946 |
-
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
| 947 |
-
signature = decoded;
|
| 948 |
-
}
|
| 949 |
-
} catch (e) {
|
| 950 |
-
// 解码失败,保持原样
|
| 951 |
-
}
|
| 952 |
-
toolUseBlock.signature = signature;
|
| 953 |
-
}
|
| 954 |
-
content.push(toolUseBlock);
|
| 955 |
-
}
|
| 956 |
-
|
| 957 |
-
if (part.functionResponse) {
|
| 958 |
-
// [FIX] 正确处理 functionResponse
|
| 959 |
-
let responseContent = part.functionResponse.response;
|
| 960 |
-
// 如果 response 是对象且有 result 字段,提取它
|
| 961 |
-
if (responseContent && typeof responseContent === 'object' && responseContent.result !== undefined) {
|
| 962 |
-
responseContent = responseContent.result;
|
| 963 |
-
}
|
| 964 |
-
content.push({
|
| 965 |
-
type: 'tool_result',
|
| 966 |
-
tool_use_id: part.functionResponse.name,
|
| 967 |
-
content: typeof responseContent === 'string' ? responseContent : JSON.stringify(responseContent)
|
| 968 |
-
});
|
| 969 |
-
}
|
| 970 |
-
});
|
| 971 |
-
|
| 972 |
-
return content;
|
| 973 |
-
}
|
| 974 |
-
|
| 975 |
-
/**
|
| 976 |
-
* 处理Gemini响应到Claude内容
|
| 977 |
-
* @returns {{ content: Array, hasToolUse: boolean }}
|
| 978 |
-
*/
|
| 979 |
-
processGeminiResponseToClaudeContent(geminiResponse) {
|
| 980 |
-
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
|
| 981 |
-
return { content: [], hasToolUse: false };
|
| 982 |
-
}
|
| 983 |
-
|
| 984 |
-
const content = [];
|
| 985 |
-
let hasToolUse = false;
|
| 986 |
-
|
| 987 |
-
for (const candidate of geminiResponse.candidates) {
|
| 988 |
-
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
|
| 989 |
-
if (candidate.finishMessage) {
|
| 990 |
-
content.push({
|
| 991 |
-
type: 'text',
|
| 992 |
-
text: `Error: ${candidate.finishMessage}`
|
| 993 |
-
});
|
| 994 |
-
}
|
| 995 |
-
continue;
|
| 996 |
-
}
|
| 997 |
-
|
| 998 |
-
if (candidate.content && candidate.content.parts) {
|
| 999 |
-
for (const part of candidate.content.parts) {
|
| 1000 |
-
// 处理 thinking 块
|
| 1001 |
-
if (part.text) {
|
| 1002 |
-
if (part.thought === true) {
|
| 1003 |
-
// 这是一个 thinking 块
|
| 1004 |
-
const thinkingBlock = {
|
| 1005 |
-
type: 'thinking',
|
| 1006 |
-
thinking: part.text
|
| 1007 |
-
};
|
| 1008 |
-
// 处理签名
|
| 1009 |
-
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
| 1010 |
-
const rawSignature = part.thoughtSignature || part.thought_signature;
|
| 1011 |
-
if (rawSignature) {
|
| 1012 |
-
let signature = rawSignature;
|
| 1013 |
-
try {
|
| 1014 |
-
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
| 1015 |
-
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
| 1016 |
-
signature = decoded;
|
| 1017 |
-
}
|
| 1018 |
-
} catch (e) {
|
| 1019 |
-
// 解码失败,保持原样
|
| 1020 |
-
}
|
| 1021 |
-
thinkingBlock.signature = signature;
|
| 1022 |
-
}
|
| 1023 |
-
content.push(thinkingBlock);
|
| 1024 |
-
} else {
|
| 1025 |
-
// 普通文本
|
| 1026 |
-
content.push({
|
| 1027 |
-
type: 'text',
|
| 1028 |
-
text: part.text
|
| 1029 |
-
});
|
| 1030 |
-
}
|
| 1031 |
-
} else if (part.inlineData) {
|
| 1032 |
-
content.push({
|
| 1033 |
-
type: 'image',
|
| 1034 |
-
source: {
|
| 1035 |
-
type: 'base64',
|
| 1036 |
-
media_type: part.inlineData.mimeType,
|
| 1037 |
-
data: part.inlineData.data
|
| 1038 |
-
}
|
| 1039 |
-
});
|
| 1040 |
-
} else if (part.functionCall) {
|
| 1041 |
-
hasToolUse = true;
|
| 1042 |
-
// [FIX] 规范化工具名称和参数映射
|
| 1043 |
-
const toolName = normalizeToolName(part.functionCall.name);
|
| 1044 |
-
const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {});
|
| 1045 |
-
|
| 1046 |
-
// [FIX] 使用 Gemini 提供的 id
|
| 1047 |
-
const toolUseBlock = {
|
| 1048 |
-
type: 'tool_use',
|
| 1049 |
-
id: part.functionCall.id || `${toolName}-${uuidv4().split('-')[0]}`,
|
| 1050 |
-
name: toolName,
|
| 1051 |
-
input: remappedArgs
|
| 1052 |
-
};
|
| 1053 |
-
// 添加签名(如果存在)
|
| 1054 |
-
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
| 1055 |
-
const rawSignature = part.thoughtSignature || part.thought_signature;
|
| 1056 |
-
if (rawSignature) {
|
| 1057 |
-
let signature = rawSignature;
|
| 1058 |
-
try {
|
| 1059 |
-
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
| 1060 |
-
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
| 1061 |
-
signature = decoded;
|
| 1062 |
-
}
|
| 1063 |
-
} catch (e) {
|
| 1064 |
-
// 解码失败,保持原样
|
| 1065 |
-
}
|
| 1066 |
-
toolUseBlock.signature = signature;
|
| 1067 |
-
}
|
| 1068 |
-
content.push(toolUseBlock);
|
| 1069 |
-
}
|
| 1070 |
-
}
|
| 1071 |
-
}
|
| 1072 |
-
}
|
| 1073 |
-
|
| 1074 |
-
return { content, hasToolUse };
|
| 1075 |
-
}
|
| 1076 |
-
|
| 1077 |
-
// =========================================================================
|
| 1078 |
-
// Gemini -> OpenAI Responses 转换
|
| 1079 |
-
// =========================================================================
|
| 1080 |
-
|
| 1081 |
-
/**
|
| 1082 |
-
* Gemini请求 -> OpenAI Responses请求
|
| 1083 |
-
*/
|
| 1084 |
-
toOpenAIResponsesRequest(geminiRequest) {
|
| 1085 |
-
const responsesRequest = {
|
| 1086 |
-
model: geminiRequest.model,
|
| 1087 |
-
instructions: '',
|
| 1088 |
-
input: [],
|
| 1089 |
-
stream: geminiRequest.stream || false,
|
| 1090 |
-
max_output_tokens: geminiRequest.generationConfig?.maxOutputTokens,
|
| 1091 |
-
temperature: geminiRequest.generationConfig?.temperature,
|
| 1092 |
-
top_p: geminiRequest.generationConfig?.topP
|
| 1093 |
-
};
|
| 1094 |
-
|
| 1095 |
-
// 处理系统指令
|
| 1096 |
-
if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
|
| 1097 |
-
responsesRequest.instructions = geminiRequest.systemInstruction.parts
|
| 1098 |
-
.filter(p => p.text)
|
| 1099 |
-
.map(p => p.text)
|
| 1100 |
-
.join('\n');
|
| 1101 |
-
}
|
| 1102 |
-
|
| 1103 |
-
// 处理内容
|
| 1104 |
-
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
|
| 1105 |
-
geminiRequest.contents.forEach(content => {
|
| 1106 |
-
const role = content.role === 'model' ? 'assistant' : 'user';
|
| 1107 |
-
const parts = content.parts || [];
|
| 1108 |
-
|
| 1109 |
-
parts.forEach(part => {
|
| 1110 |
-
if (part.text) {
|
| 1111 |
-
responsesRequest.input.push({
|
| 1112 |
-
type: 'message',
|
| 1113 |
-
role: role,
|
| 1114 |
-
content: [{
|
| 1115 |
-
type: role === 'assistant' ? 'output_text' : 'input_text',
|
| 1116 |
-
text: part.text
|
| 1117 |
-
}]
|
| 1118 |
-
});
|
| 1119 |
-
}
|
| 1120 |
-
|
| 1121 |
-
if (part.functionCall) {
|
| 1122 |
-
responsesRequest.input.push({
|
| 1123 |
-
type: 'function_call',
|
| 1124 |
-
call_id: part.functionCall.id || `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`,
|
| 1125 |
-
name: part.functionCall.name,
|
| 1126 |
-
arguments: typeof part.functionCall.args === 'string'
|
| 1127 |
-
? part.functionCall.args
|
| 1128 |
-
: JSON.stringify(part.functionCall.args)
|
| 1129 |
-
});
|
| 1130 |
-
}
|
| 1131 |
-
|
| 1132 |
-
if (part.functionResponse) {
|
| 1133 |
-
responsesRequest.input.push({
|
| 1134 |
-
type: 'function_call_output',
|
| 1135 |
-
call_id: part.functionResponse.name, // Gemini 通常使用 name 作为关联
|
| 1136 |
-
output: typeof part.functionResponse.response?.result === 'string'
|
| 1137 |
-
? part.functionResponse.response.result
|
| 1138 |
-
: JSON.stringify(part.functionResponse.response || {})
|
| 1139 |
-
});
|
| 1140 |
-
}
|
| 1141 |
-
|
| 1142 |
-
if (part.inlineData) {
|
| 1143 |
-
responsesRequest.input.push({
|
| 1144 |
-
type: 'message',
|
| 1145 |
-
role: role,
|
| 1146 |
-
content: [{
|
| 1147 |
-
type: 'input_image',
|
| 1148 |
-
image_url: {
|
| 1149 |
-
url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
|
| 1150 |
-
}
|
| 1151 |
-
}]
|
| 1152 |
-
});
|
| 1153 |
-
}
|
| 1154 |
-
});
|
| 1155 |
-
});
|
| 1156 |
-
}
|
| 1157 |
-
|
| 1158 |
-
// 处理工具
|
| 1159 |
-
if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
|
| 1160 |
-
responsesRequest.tools = geminiRequest.tools[0].functionDeclarations.map(fn => ({
|
| 1161 |
-
type: 'function',
|
| 1162 |
-
name: fn.name,
|
| 1163 |
-
description: fn.description,
|
| 1164 |
-
parameters: fn.parameters || fn.parametersJsonSchema || { type: 'object', properties: {} }
|
| 1165 |
-
}));
|
| 1166 |
-
}
|
| 1167 |
-
|
| 1168 |
-
return responsesRequest;
|
| 1169 |
-
}
|
| 1170 |
-
|
| 1171 |
-
/**
|
| 1172 |
-
* Gemini响应 -> OpenAI Responses响应
|
| 1173 |
-
*/
|
| 1174 |
-
toOpenAIResponsesResponse(geminiResponse, model) {
|
| 1175 |
-
const content = this.processGeminiResponseContent(geminiResponse);
|
| 1176 |
-
const textContent = typeof content === 'string' ? content : JSON.stringify(content);
|
| 1177 |
-
|
| 1178 |
-
let output = [];
|
| 1179 |
-
output.push({
|
| 1180 |
-
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 1181 |
-
summary: [],
|
| 1182 |
-
type: "message",
|
| 1183 |
-
role: "assistant",
|
| 1184 |
-
status: "completed",
|
| 1185 |
-
content: [{
|
| 1186 |
-
annotations: [],
|
| 1187 |
-
logprobs: [],
|
| 1188 |
-
text: textContent,
|
| 1189 |
-
type: "output_text"
|
| 1190 |
-
}]
|
| 1191 |
-
});
|
| 1192 |
-
|
| 1193 |
-
return {
|
| 1194 |
-
background: false,
|
| 1195 |
-
created_at: Math.floor(Date.now() / 1000),
|
| 1196 |
-
error: null,
|
| 1197 |
-
id: `resp_${uuidv4().replace(/-/g, '')}`,
|
| 1198 |
-
incomplete_details: null,
|
| 1199 |
-
max_output_tokens: null,
|
| 1200 |
-
max_tool_calls: null,
|
| 1201 |
-
metadata: {},
|
| 1202 |
-
model: model,
|
| 1203 |
-
object: "response",
|
| 1204 |
-
output: output,
|
| 1205 |
-
parallel_tool_calls: true,
|
| 1206 |
-
previous_response_id: null,
|
| 1207 |
-
prompt_cache_key: null,
|
| 1208 |
-
reasoning: {},
|
| 1209 |
-
safety_identifier: "user-" + uuidv4().replace(/-/g, ''),
|
| 1210 |
-
service_tier: "default",
|
| 1211 |
-
status: "completed",
|
| 1212 |
-
store: false,
|
| 1213 |
-
temperature: 1,
|
| 1214 |
-
text: {
|
| 1215 |
-
format: { type: "text" },
|
| 1216 |
-
},
|
| 1217 |
-
tool_choice: "auto",
|
| 1218 |
-
tools: [],
|
| 1219 |
-
top_logprobs: 0,
|
| 1220 |
-
top_p: 1,
|
| 1221 |
-
truncation: "disabled",
|
| 1222 |
-
usage: {
|
| 1223 |
-
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
| 1224 |
-
input_tokens_details: {
|
| 1225 |
-
cached_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0
|
| 1226 |
-
},
|
| 1227 |
-
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
| 1228 |
-
output_tokens_details: {
|
| 1229 |
-
reasoning_tokens: geminiResponse.usageMetadata?.thoughtsTokenCount || 0
|
| 1230 |
-
},
|
| 1231 |
-
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0
|
| 1232 |
-
},
|
| 1233 |
-
user: null
|
| 1234 |
-
};
|
| 1235 |
-
}
|
| 1236 |
-
|
| 1237 |
-
/**
|
| 1238 |
-
* Gemini流式响应 -> OpenAI Responses流式响应
|
| 1239 |
-
*/
|
| 1240 |
-
toOpenAIResponsesStreamChunk(geminiChunk, model, requestId = null) {
|
| 1241 |
-
if (!geminiChunk) return [];
|
| 1242 |
-
|
| 1243 |
-
const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
|
| 1244 |
-
const events = [];
|
| 1245 |
-
|
| 1246 |
-
// 处理完整的Gemini chunk对象
|
| 1247 |
-
if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
|
| 1248 |
-
const candidate = geminiChunk.candidates?.[0];
|
| 1249 |
-
|
| 1250 |
-
if (candidate) {
|
| 1251 |
-
const parts = candidate.content?.parts;
|
| 1252 |
-
|
| 1253 |
-
// 第一个chunk - 检测是否是开始(有role)
|
| 1254 |
-
if (candidate.content?.role === 'model' && parts && parts.length > 0) {
|
| 1255 |
-
// 只在第一次有内容时发送开始事件
|
| 1256 |
-
const hasContent = parts.some(part => part && typeof part.text === 'string' && part.text.length > 0);
|
| 1257 |
-
if (hasContent) {
|
| 1258 |
-
events.push(
|
| 1259 |
-
generateResponseCreated(responseId, model || 'unknown'),
|
| 1260 |
-
generateResponseInProgress(responseId),
|
| 1261 |
-
generateOutputItemAdded(responseId),
|
| 1262 |
-
generateContentPartAdded(responseId)
|
| 1263 |
-
);
|
| 1264 |
-
}
|
| 1265 |
-
}
|
| 1266 |
-
|
| 1267 |
-
// 提取文本内容
|
| 1268 |
-
if (parts && Array.isArray(parts)) {
|
| 1269 |
-
const textParts = parts.filter(part => part && typeof part.text === 'string');
|
| 1270 |
-
if (textParts.length > 0) {
|
| 1271 |
-
const text = textParts.map(part => part.text).join('');
|
| 1272 |
-
events.push({
|
| 1273 |
-
delta: text,
|
| 1274 |
-
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 1275 |
-
output_index: 0,
|
| 1276 |
-
sequence_number: 3,
|
| 1277 |
-
type: "response.output_text.delta"
|
| 1278 |
-
});
|
| 1279 |
-
}
|
| 1280 |
-
}
|
| 1281 |
-
|
| 1282 |
-
// 处理finishReason
|
| 1283 |
-
if (candidate.finishReason) {
|
| 1284 |
-
events.push(
|
| 1285 |
-
generateOutputTextDone(responseId),
|
| 1286 |
-
generateContentPartDone(responseId),
|
| 1287 |
-
generateOutputItemDone(responseId),
|
| 1288 |
-
generateResponseCompleted(responseId)
|
| 1289 |
-
);
|
| 1290 |
-
|
| 1291 |
-
// 如果有 usage 信息,更新最后一个事件
|
| 1292 |
-
if (geminiChunk.usageMetadata && events.length > 0) {
|
| 1293 |
-
const lastEvent = events[events.length - 1];
|
| 1294 |
-
if (lastEvent.response) {
|
| 1295 |
-
lastEvent.response.usage = {
|
| 1296 |
-
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
| 1297 |
-
input_tokens_details: {
|
| 1298 |
-
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
|
| 1299 |
-
},
|
| 1300 |
-
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
|
| 1301 |
-
output_tokens_details: {
|
| 1302 |
-
reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
|
| 1303 |
-
},
|
| 1304 |
-
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0
|
| 1305 |
-
};
|
| 1306 |
-
}
|
| 1307 |
-
}
|
| 1308 |
-
}
|
| 1309 |
-
}
|
| 1310 |
-
}
|
| 1311 |
-
|
| 1312 |
-
// 向后兼容:处理字符串格式
|
| 1313 |
-
if (typeof geminiChunk === 'string') {
|
| 1314 |
-
events.push({
|
| 1315 |
-
delta: geminiChunk,
|
| 1316 |
-
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 1317 |
-
output_index: 0,
|
| 1318 |
-
sequence_number: 3,
|
| 1319 |
-
type: "response.output_text.delta"
|
| 1320 |
-
});
|
| 1321 |
-
}
|
| 1322 |
-
|
| 1323 |
-
return events;
|
| 1324 |
-
}
|
| 1325 |
-
|
| 1326 |
-
// =========================================================================
|
| 1327 |
-
// Gemini -> Codex 转换
|
| 1328 |
-
// =========================================================================
|
| 1329 |
-
|
| 1330 |
-
/**
|
| 1331 |
-
* Gemini请求 -> Codex请求
|
| 1332 |
-
*/
|
| 1333 |
-
toCodexRequest(geminiRequest) {
|
| 1334 |
-
// 使用 CodexConverter 进行转换,因为 CodexConverter.js 中已经实现了 OpenAI -> Codex 的逻辑
|
| 1335 |
-
// 我们需要先将 Gemini 转为 OpenAI 格式,再转为 Codex 格式
|
| 1336 |
-
const openaiRequest = this.toOpenAIRequest(geminiRequest);
|
| 1337 |
-
|
| 1338 |
-
// 注意:这里我们直接在 GeminiConverter 中实现逻辑,避免循环依赖
|
| 1339 |
-
const codexRequest = {
|
| 1340 |
-
model: openaiRequest.model,
|
| 1341 |
-
instructions: '',
|
| 1342 |
-
input: [],
|
| 1343 |
-
stream: geminiRequest.stream || false,
|
| 1344 |
-
store: false,
|
| 1345 |
-
reasoning: {
|
| 1346 |
-
effort: 'medium',
|
| 1347 |
-
summary: 'auto'
|
| 1348 |
-
},
|
| 1349 |
-
parallel_tool_calls: true,
|
| 1350 |
-
include: ['reasoning.encrypted_content']
|
| 1351 |
-
};
|
| 1352 |
-
|
| 1353 |
-
// 处理系统指令
|
| 1354 |
-
if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
|
| 1355 |
-
codexRequest.instructions = geminiRequest.systemInstruction.parts
|
| 1356 |
-
.filter(p => p.text)
|
| 1357 |
-
.map(p => p.text)
|
| 1358 |
-
.join('\n');
|
| 1359 |
-
}
|
| 1360 |
-
|
| 1361 |
-
// 处理内容
|
| 1362 |
-
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
|
| 1363 |
-
const pendingCallIDs = [];
|
| 1364 |
-
|
| 1365 |
-
geminiRequest.contents.forEach(content => {
|
| 1366 |
-
const role = content.role === 'model' ? 'assistant' : 'user';
|
| 1367 |
-
const parts = content.parts || [];
|
| 1368 |
-
|
| 1369 |
-
parts.forEach(part => {
|
| 1370 |
-
if (part.text) {
|
| 1371 |
-
codexRequest.input.push({
|
| 1372 |
-
type: 'message',
|
| 1373 |
-
role: role,
|
| 1374 |
-
content: [{
|
| 1375 |
-
type: role === 'assistant' ? 'output_text' : 'input_text',
|
| 1376 |
-
text: part.text
|
| 1377 |
-
}]
|
| 1378 |
-
});
|
| 1379 |
-
}
|
| 1380 |
-
|
| 1381 |
-
if (part.functionCall) {
|
| 1382 |
-
const callId = `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`;
|
| 1383 |
-
pendingCallIDs.push(callId);
|
| 1384 |
-
codexRequest.input.push({
|
| 1385 |
-
type: 'function_call',
|
| 1386 |
-
call_id: callId,
|
| 1387 |
-
name: part.functionCall.name,
|
| 1388 |
-
arguments: typeof part.functionCall.args === 'string'
|
| 1389 |
-
? part.functionCall.args
|
| 1390 |
-
: JSON.stringify(part.functionCall.args)
|
| 1391 |
-
});
|
| 1392 |
-
}
|
| 1393 |
-
|
| 1394 |
-
if (part.functionResponse) {
|
| 1395 |
-
const callId = pendingCallIDs.shift() || `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`;
|
| 1396 |
-
codexRequest.input.push({
|
| 1397 |
-
type: 'function_call_output',
|
| 1398 |
-
call_id: callId,
|
| 1399 |
-
output: typeof part.functionResponse.response?.result === 'string'
|
| 1400 |
-
? part.functionResponse.response.result
|
| 1401 |
-
: JSON.stringify(part.functionResponse.response || {})
|
| 1402 |
-
});
|
| 1403 |
-
}
|
| 1404 |
-
});
|
| 1405 |
-
});
|
| 1406 |
-
}
|
| 1407 |
-
|
| 1408 |
-
// 处理工具
|
| 1409 |
-
if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
|
| 1410 |
-
codexRequest.tools = geminiRequest.tools[0].functionDeclarations.map(fn => ({
|
| 1411 |
-
type: 'function',
|
| 1412 |
-
name: fn.name,
|
| 1413 |
-
description: fn.description,
|
| 1414 |
-
parameters: fn.parameters || { type: 'object', properties: {} }
|
| 1415 |
-
}));
|
| 1416 |
-
}
|
| 1417 |
-
|
| 1418 |
-
return codexRequest;
|
| 1419 |
-
}
|
| 1420 |
-
|
| 1421 |
-
/**
|
| 1422 |
-
* Gemini请求 -> Grok请求
|
| 1423 |
-
*/
|
| 1424 |
-
toGrokRequest(geminiRequest) {
|
| 1425 |
-
// 先转换为 OpenAI 格式
|
| 1426 |
-
const openaiRequest = this.toOpenAIRequest(geminiRequest);
|
| 1427 |
-
return {
|
| 1428 |
-
...openaiRequest,
|
| 1429 |
-
_isConverted: true
|
| 1430 |
-
};
|
| 1431 |
-
}
|
| 1432 |
-
|
| 1433 |
-
/**
|
| 1434 |
-
* Gemini响应 -> Codex响应 (实际上是 Codex 转 Gemini)
|
| 1435 |
-
*/
|
| 1436 |
-
toCodexResponse(geminiResponse, model) {
|
| 1437 |
-
// 这里实际上是实现 Codex -> Gemini 的非流式转换
|
| 1438 |
-
// 为了保持接口一致,我们按照其他 Converter 的命名习惯
|
| 1439 |
-
const parts = [];
|
| 1440 |
-
if (geminiResponse.response?.output) {
|
| 1441 |
-
geminiResponse.response.output.forEach(item => {
|
| 1442 |
-
if (item.type === 'message' && item.content) {
|
| 1443 |
-
const textPart = item.content.find(c => c.type === 'output_text');
|
| 1444 |
-
if (textPart) parts.push({ text: textPart.text });
|
| 1445 |
-
} else if (item.type === 'reasoning' && item.summary) {
|
| 1446 |
-
const textPart = item.summary.find(c => c.type === 'summary_text');
|
| 1447 |
-
if (textPart) parts.push({ text: textPart.text, thought: true });
|
| 1448 |
-
} else if (item.type === 'function_call') {
|
| 1449 |
-
parts.push({
|
| 1450 |
-
functionCall: {
|
| 1451 |
-
name: item.name,
|
| 1452 |
-
args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 1453 |
-
}
|
| 1454 |
-
});
|
| 1455 |
-
}
|
| 1456 |
-
});
|
| 1457 |
-
}
|
| 1458 |
-
|
| 1459 |
-
return {
|
| 1460 |
-
candidates: [{
|
| 1461 |
-
content: {
|
| 1462 |
-
role: 'model',
|
| 1463 |
-
parts: parts
|
| 1464 |
-
},
|
| 1465 |
-
finishReason: 'STOP'
|
| 1466 |
-
}],
|
| 1467 |
-
usageMetadata: {
|
| 1468 |
-
promptTokenCount: geminiResponse.response?.usage?.input_tokens || 0,
|
| 1469 |
-
candidatesTokenCount: geminiResponse.response?.usage?.output_tokens || 0,
|
| 1470 |
-
totalTokenCount: geminiResponse.response?.usage?.total_tokens || 0
|
| 1471 |
-
},
|
| 1472 |
-
modelVersion: model,
|
| 1473 |
-
responseId: geminiResponse.response?.id
|
| 1474 |
-
};
|
| 1475 |
-
}
|
| 1476 |
-
|
| 1477 |
-
/**
|
| 1478 |
-
* Gemini流式响应 -> Codex流式响应 (实际上是 Codex 转 Gemini)
|
| 1479 |
-
*/
|
| 1480 |
-
toCodexStreamChunk(codexChunk, model) {
|
| 1481 |
-
const type = codexChunk.type;
|
| 1482 |
-
const resId = codexChunk.response?.id || 'default';
|
| 1483 |
-
|
| 1484 |
-
const template = {
|
| 1485 |
-
candidates: [{
|
| 1486 |
-
content: {
|
| 1487 |
-
role: "model",
|
| 1488 |
-
parts: []
|
| 1489 |
-
}
|
| 1490 |
-
}],
|
| 1491 |
-
modelVersion: model,
|
| 1492 |
-
responseId: resId
|
| 1493 |
-
};
|
| 1494 |
-
|
| 1495 |
-
if (type === 'response.reasoning_summary_text.delta') {
|
| 1496 |
-
template.candidates[0].content.parts.push({ text: codexChunk.delta, thought: true });
|
| 1497 |
-
return template;
|
| 1498 |
-
}
|
| 1499 |
-
|
| 1500 |
-
if (type === 'response.output_text.delta') {
|
| 1501 |
-
template.candidates[0].content.parts.push({ text: codexChunk.delta });
|
| 1502 |
-
return template;
|
| 1503 |
-
}
|
| 1504 |
-
|
| 1505 |
-
if (type === 'response.output_item.done' && codexChunk.item?.type === 'function_call') {
|
| 1506 |
-
template.candidates[0].content.parts.push({
|
| 1507 |
-
functionCall: {
|
| 1508 |
-
name: codexChunk.item.name,
|
| 1509 |
-
args: typeof codexChunk.item.arguments === 'string' ? JSON.parse(codexChunk.item.arguments) : codexChunk.item.arguments
|
| 1510 |
-
}
|
| 1511 |
-
});
|
| 1512 |
-
return template;
|
| 1513 |
-
}
|
| 1514 |
-
|
| 1515 |
-
if (type === 'response.completed') {
|
| 1516 |
-
template.candidates[0].finishReason = "STOP";
|
| 1517 |
-
template.usageMetadata = {
|
| 1518 |
-
promptTokenCount: codexChunk.response.usage?.input_tokens || 0,
|
| 1519 |
-
candidatesTokenCount: codexChunk.response.usage?.output_tokens || 0,
|
| 1520 |
-
totalTokenCount: codexChunk.response.usage?.total_tokens || 0
|
| 1521 |
-
};
|
| 1522 |
-
return template;
|
| 1523 |
-
}
|
| 1524 |
-
|
| 1525 |
-
return null;
|
| 1526 |
-
}
|
| 1527 |
-
}
|
| 1528 |
-
|
| 1529 |
-
export default GeminiConverter;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/strategies/GrokConverter.js
DELETED
|
@@ -1,1153 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Grok转换器
|
| 3 |
-
* 处理Grok协议与其他协议之间的转换
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
-
import logger from '../../utils/logger.js';
|
| 8 |
-
import { BaseConverter } from '../BaseConverter.js';
|
| 9 |
-
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
| 10 |
-
|
| 11 |
-
/**
|
| 12 |
-
* Grok转换器类
|
| 13 |
-
* 实现Grok协议到其他协议的转换
|
| 14 |
-
*/
|
| 15 |
-
export class GrokConverter extends BaseConverter {
|
| 16 |
-
// 静态属性,确保所有实例共享最新的基础 URL 和 UUID 配置
|
| 17 |
-
static sharedRequestBaseUrl = "";
|
| 18 |
-
static sharedUuid = null;
|
| 19 |
-
|
| 20 |
-
constructor() {
|
| 21 |
-
super('grok');
|
| 22 |
-
// 用于跟踪每个请求的状态
|
| 23 |
-
this.requestStates = new Map();
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
/**
|
| 27 |
-
* 设置请求的基础 URL
|
| 28 |
-
*/
|
| 29 |
-
setRequestBaseUrl(baseUrl) {
|
| 30 |
-
if (baseUrl) {
|
| 31 |
-
GrokConverter.sharedRequestBaseUrl = baseUrl;
|
| 32 |
-
}
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
/**
|
| 36 |
-
* 设置账号的 UUID
|
| 37 |
-
*/
|
| 38 |
-
setUuid(uuid) {
|
| 39 |
-
if (uuid) {
|
| 40 |
-
GrokConverter.sharedUuid = uuid;
|
| 41 |
-
}
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
/**
|
| 45 |
-
* 为 assets.grok.com 域名的资源 URL 添加 uuid 参数,并转换为本地代理 URL
|
| 46 |
-
*/
|
| 47 |
-
_appendSsoToken(url, state = null) {
|
| 48 |
-
const requestBaseUrl = state?.requestBaseUrl || GrokConverter.sharedRequestBaseUrl;
|
| 49 |
-
const uuid = state?.uuid || GrokConverter.sharedUuid;
|
| 50 |
-
|
| 51 |
-
if (!url || !uuid) return url;
|
| 52 |
-
|
| 53 |
-
// 检查是否为 assets.grok.com 域名或相对路径
|
| 54 |
-
const isGrokAsset = url.includes('assets.grok.com') || (!url.startsWith('http') && !url.startsWith('data:'));
|
| 55 |
-
|
| 56 |
-
if (!isGrokAsset) return url;
|
| 57 |
-
|
| 58 |
-
// 构造完整的原始 URL
|
| 59 |
-
let originalUrl = url;
|
| 60 |
-
if (!url.startsWith('http')) {
|
| 61 |
-
originalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
// 返回本地代理接口 URL
|
| 65 |
-
// 使用 uuid 以提高安全性,防止 token 泄露在链接中
|
| 66 |
-
const authParam = `uuid=${encodeURIComponent(uuid)}`;
|
| 67 |
-
|
| 68 |
-
const proxyPath = `/api/grok/assets?url=${encodeURIComponent(originalUrl)}&${authParam}`;
|
| 69 |
-
if (requestBaseUrl) {
|
| 70 |
-
return `${requestBaseUrl}${proxyPath}`;
|
| 71 |
-
}
|
| 72 |
-
return proxyPath;
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
/**
|
| 76 |
-
* 在文本中查找并替换所有 assets.grok.com 的资源链接为绝对代理链接
|
| 77 |
-
*/
|
| 78 |
-
_processGrokAssetsInText(text, state = null) {
|
| 79 |
-
const uuid = state?.uuid || GrokConverter.sharedUuid;
|
| 80 |
-
if (!text || !uuid) return text;
|
| 81 |
-
|
| 82 |
-
// 更宽松的正则匹配 assets.grok.com 的 URL
|
| 83 |
-
const grokUrlRegex = /https?:\/\/assets\.grok\.com\/[^\s\)\"\'\>]+/g;
|
| 84 |
-
|
| 85 |
-
return text.replace(grokUrlRegex, (url) => {
|
| 86 |
-
return this._appendSsoToken(url, state);
|
| 87 |
-
});
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
/**
|
| 91 |
-
* 获取或初始化请求状态
|
| 92 |
-
*/
|
| 93 |
-
_getState(requestId) {
|
| 94 |
-
if (!this.requestStates.has(requestId)) {
|
| 95 |
-
this.requestStates.set(requestId, {
|
| 96 |
-
think_opened: false,
|
| 97 |
-
image_think_active: false,
|
| 98 |
-
video_think_active: false,
|
| 99 |
-
role_sent: false,
|
| 100 |
-
tool_buffer: "",
|
| 101 |
-
last_is_thinking: false,
|
| 102 |
-
fingerprint: "",
|
| 103 |
-
content_buffer: "", // 用于缓存内容以解析工具调用
|
| 104 |
-
has_tool_call: false,
|
| 105 |
-
rollout_id: "",
|
| 106 |
-
in_tool_call: false, // 是否处于 <tool_call> 块内
|
| 107 |
-
requestBaseUrl: "",
|
| 108 |
-
uuid: null,
|
| 109 |
-
pending_text_buffer: "" // 用于处理流式输出中被截断的 URL
|
| 110 |
-
});
|
| 111 |
-
}
|
| 112 |
-
return this.requestStates.get(requestId);
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
/**
|
| 116 |
-
* 构建工具系统提示词 (build_tool_prompt)
|
| 117 |
-
*/
|
| 118 |
-
buildToolPrompt(tools, toolChoice = "auto", parallelToolCalls = true) {
|
| 119 |
-
if (!tools || tools.length === 0 || toolChoice === "none") {
|
| 120 |
-
return "";
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
const lines = [
|
| 124 |
-
"# Available Tools",
|
| 125 |
-
"",
|
| 126 |
-
"You have access to the following tools. To call a tool, output a <tool_call> block with a JSON object containing \"name\" and \"arguments\".",
|
| 127 |
-
"",
|
| 128 |
-
"Format:",
|
| 129 |
-
"<tool_call>",
|
| 130 |
-
'{"name": "function_name", "arguments": {"param": "value"}}',
|
| 131 |
-
"</tool_call>",
|
| 132 |
-
"",
|
| 133 |
-
];
|
| 134 |
-
|
| 135 |
-
if (parallelToolCalls) {
|
| 136 |
-
lines.push("You may make multiple tool calls in a single response by using multiple <tool_call> blocks.");
|
| 137 |
-
lines.push("");
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
lines.push("## Tool Definitions");
|
| 141 |
-
lines.push("");
|
| 142 |
-
for (const tool of tools) {
|
| 143 |
-
if (tool.type !== "function") continue;
|
| 144 |
-
const func = tool.function || {};
|
| 145 |
-
lines.push(`### ${func.name}`);
|
| 146 |
-
if (func.description) lines.push(func.description);
|
| 147 |
-
if (func.parameters) lines.push(`Parameters: ${JSON.stringify(func.parameters)}`);
|
| 148 |
-
lines.push("");
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
if (toolChoice === "required") {
|
| 152 |
-
lines.push("IMPORTANT: You MUST call at least one tool in your response. Do not respond with only text.");
|
| 153 |
-
} else if (typeof toolChoice === 'object' && toolChoice.function?.name) {
|
| 154 |
-
lines.push(`IMPORTANT: You MUST call the tool "${toolChoice.function.name}" in your response.`);
|
| 155 |
-
} else {
|
| 156 |
-
lines.push("Decide whether to call a tool based on the user's request. If you don't need a tool, respond normally with text only.");
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
lines.push("");
|
| 160 |
-
lines.push("When you call a tool, you may include text before or after the <tool_call> blocks, but the tool call blocks must be valid JSON.");
|
| 161 |
-
|
| 162 |
-
return lines.join("\n");
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
/**
|
| 166 |
-
* 格式化工具历史 (format_tool_history)
|
| 167 |
-
*/
|
| 168 |
-
formatToolHistory(messages) {
|
| 169 |
-
const result = [];
|
| 170 |
-
for (const msg of messages) {
|
| 171 |
-
const role = msg.role;
|
| 172 |
-
const content = msg.content;
|
| 173 |
-
const toolCalls = msg.tool_calls;
|
| 174 |
-
|
| 175 |
-
if (role === "assistant" && toolCalls && toolCalls.length > 0) {
|
| 176 |
-
const parts = [];
|
| 177 |
-
if (content) parts.push(typeof content === 'string' ? content : JSON.stringify(content));
|
| 178 |
-
for (const tc of toolCalls) {
|
| 179 |
-
const func = tc.function || {};
|
| 180 |
-
parts.push(`<tool_call>{"name":"${func.name}","arguments":${func.arguments || "{}"}}</tool_call>`);
|
| 181 |
-
}
|
| 182 |
-
result.push({ role: "assistant", content: parts.join("\n") });
|
| 183 |
-
} else if (role === "tool") {
|
| 184 |
-
const toolName = msg.name || "unknown";
|
| 185 |
-
const callId = msg.tool_call_id || "";
|
| 186 |
-
const contentStr = typeof content === 'string' ? content : JSON.stringify(content);
|
| 187 |
-
result.push({
|
| 188 |
-
role: "user",
|
| 189 |
-
content: `tool (${toolName}, ${callId}): ${contentStr}`
|
| 190 |
-
});
|
| 191 |
-
} else {
|
| 192 |
-
result.push(msg);
|
| 193 |
-
}
|
| 194 |
-
}
|
| 195 |
-
return result;
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
/**
|
| 199 |
-
* 解析工具调用 (parse_tool_calls)
|
| 200 |
-
*/
|
| 201 |
-
parseToolCalls(content) {
|
| 202 |
-
if (!content) return { text: content, toolCalls: null };
|
| 203 |
-
|
| 204 |
-
const toolCallRegex = /<tool_call>\s*(.*?)\s*<\/tool_call>/gs;
|
| 205 |
-
const matches = [...content.matchAll(toolCallRegex)];
|
| 206 |
-
|
| 207 |
-
if (matches.length === 0) return { text: content, toolCalls: null };
|
| 208 |
-
|
| 209 |
-
const toolCalls = [];
|
| 210 |
-
for (const match of matches) {
|
| 211 |
-
try {
|
| 212 |
-
const parsed = JSON.parse(match[1].trim());
|
| 213 |
-
if (parsed.name) {
|
| 214 |
-
let args = parsed.arguments || {};
|
| 215 |
-
const argumentsStr = typeof args === 'string' ? args : JSON.stringify(args);
|
| 216 |
-
|
| 217 |
-
toolCalls.push({
|
| 218 |
-
id: `call_${uuidv4().replace(/-/g, '').slice(0, 24)}`,
|
| 219 |
-
type: "function",
|
| 220 |
-
function: {
|
| 221 |
-
name: parsed.name,
|
| 222 |
-
arguments: argumentsStr
|
| 223 |
-
}
|
| 224 |
-
});
|
| 225 |
-
}
|
| 226 |
-
} catch (e) {
|
| 227 |
-
// 忽略解析失败的块
|
| 228 |
-
}
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
if (toolCalls.length === 0) return { text: content, toolCalls: null };
|
| 232 |
-
|
| 233 |
-
// 提取文本内容
|
| 234 |
-
let text = content;
|
| 235 |
-
for (const match of matches) {
|
| 236 |
-
text = text.replace(match[0], "");
|
| 237 |
-
}
|
| 238 |
-
text = text.trim() || null;
|
| 239 |
-
|
| 240 |
-
return { text, toolCalls };
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
/**
|
| 244 |
-
* 转换请求
|
| 245 |
-
*/
|
| 246 |
-
convertRequest(data, targetProtocol) {
|
| 247 |
-
switch (targetProtocol) {
|
| 248 |
-
default:
|
| 249 |
-
return data;
|
| 250 |
-
}
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
/**
|
| 254 |
-
* 转换响应
|
| 255 |
-
*/
|
| 256 |
-
convertResponse(data, targetProtocol, model) {
|
| 257 |
-
switch (targetProtocol) {
|
| 258 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 259 |
-
return this.toOpenAIResponse(data, model);
|
| 260 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 261 |
-
return this.toGeminiResponse(data, model);
|
| 262 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 263 |
-
return this.toOpenAIResponsesResponse(data, model);
|
| 264 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 265 |
-
return this.toCodexResponse(data, model);
|
| 266 |
-
default:
|
| 267 |
-
return data;
|
| 268 |
-
}
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
/**
|
| 272 |
-
* 转换流式响应块
|
| 273 |
-
*/
|
| 274 |
-
convertStreamChunk(chunk, targetProtocol, model) {
|
| 275 |
-
switch (targetProtocol) {
|
| 276 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 277 |
-
return this.toOpenAIStreamChunk(chunk, model);
|
| 278 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 279 |
-
return this.toGeminiStreamChunk(chunk, model);
|
| 280 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 281 |
-
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
| 282 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 283 |
-
return this.toCodexStreamChunk(chunk, model);
|
| 284 |
-
default:
|
| 285 |
-
return chunk;
|
| 286 |
-
}
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
/**
|
| 290 |
-
* 转换模型列表
|
| 291 |
-
*/
|
| 292 |
-
convertModelList(data, targetProtocol) {
|
| 293 |
-
switch (targetProtocol) {
|
| 294 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 295 |
-
return this.toOpenAIModelList(data);
|
| 296 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 297 |
-
return this.toGeminiModelList(data);
|
| 298 |
-
default:
|
| 299 |
-
return data;
|
| 300 |
-
}
|
| 301 |
-
}
|
| 302 |
-
|
| 303 |
-
/**
|
| 304 |
-
* 构建工具覆盖配置 (build_tool_overrides)
|
| 305 |
-
*/
|
| 306 |
-
buildToolOverrides(tools) {
|
| 307 |
-
if (!tools || !Array.isArray(tools)) {
|
| 308 |
-
return {};
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
const toolOverrides = {};
|
| 312 |
-
for (const tool of tools) {
|
| 313 |
-
if (tool.type !== "function") continue;
|
| 314 |
-
const func = tool.function || {};
|
| 315 |
-
const name = func.name;
|
| 316 |
-
if (!name) continue;
|
| 317 |
-
|
| 318 |
-
toolOverrides[name] = {
|
| 319 |
-
"enabled": true,
|
| 320 |
-
"description": func.description || "",
|
| 321 |
-
"parameters": func.parameters || {}
|
| 322 |
-
};
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
return toolOverrides;
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
/**
|
| 329 |
-
* 递归收集响应中的图片 URL
|
| 330 |
-
*/
|
| 331 |
-
_collectImages(obj) {
|
| 332 |
-
const urls = [];
|
| 333 |
-
const seen = new Set();
|
| 334 |
-
|
| 335 |
-
const add = (url) => {
|
| 336 |
-
if (!url || seen.has(url)) return;
|
| 337 |
-
seen.add(url);
|
| 338 |
-
urls.push(url);
|
| 339 |
-
};
|
| 340 |
-
|
| 341 |
-
const walk = (value) => {
|
| 342 |
-
if (value && typeof value === 'object') {
|
| 343 |
-
if (Array.isArray(value)) {
|
| 344 |
-
value.forEach(walk);
|
| 345 |
-
} else {
|
| 346 |
-
for (const [key, item] of Object.entries(value)) {
|
| 347 |
-
if (key === "generatedImageUrls" || key === "imageUrls" || key === "imageURLs") {
|
| 348 |
-
if (Array.isArray(item)) {
|
| 349 |
-
item.forEach(url => typeof url === 'string' && add(url));
|
| 350 |
-
} else if (typeof item === 'string') {
|
| 351 |
-
add(item);
|
| 352 |
-
}
|
| 353 |
-
continue;
|
| 354 |
-
}
|
| 355 |
-
walk(item);
|
| 356 |
-
}
|
| 357 |
-
}
|
| 358 |
-
}
|
| 359 |
-
};
|
| 360 |
-
|
| 361 |
-
walk(obj);
|
| 362 |
-
return urls;
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
/**
|
| 366 |
-
* 渲染图片为 Markdown
|
| 367 |
-
*/
|
| 368 |
-
_renderImage(url, imageId = "image", state = null) {
|
| 369 |
-
let finalUrl = url;
|
| 370 |
-
if (!url.startsWith('http')) {
|
| 371 |
-
finalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
|
| 372 |
-
}
|
| 373 |
-
finalUrl = this._appendSsoToken(finalUrl, state);
|
| 374 |
-
return ``;
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
/**
|
| 378 |
-
* 渲染视频为 Markdown/HTML (render_video)
|
| 379 |
-
*/
|
| 380 |
-
_renderVideo(videoUrl, thumbnailImageUrl = "", state = null) {
|
| 381 |
-
let finalVideoUrl = videoUrl;
|
| 382 |
-
if (!videoUrl.startsWith('http')) {
|
| 383 |
-
finalVideoUrl = `https://assets.grok.com${videoUrl.startsWith('/') ? '' : '/'}${videoUrl}`;
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
let finalThumbUrl = thumbnailImageUrl;
|
| 387 |
-
if (thumbnailImageUrl && !thumbnailImageUrl.startsWith('http')) {
|
| 388 |
-
finalThumbUrl = `https://assets.grok.com${thumbnailImageUrl.startsWith('/') ? '' : '/'}${thumbnailImageUrl}`;
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
const defaultThumb = 'https://assets.grok.com/favicon.ico';
|
| 392 |
-
return `\n[](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`;
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
/**
|
| 396 |
-
* 提取工具卡片文本 (extract_tool_text)
|
| 397 |
-
*/
|
| 398 |
-
_extractToolText(raw, rolloutId = "") {
|
| 399 |
-
if (!raw) return "";
|
| 400 |
-
|
| 401 |
-
const nameMatch = raw.match(/<xai:tool_name>(.*?)<\/xai:tool_name>/s);
|
| 402 |
-
const argsMatch = raw.match(/<xai:tool_args>(.*?)<\/xai:tool_args>/s);
|
| 403 |
-
|
| 404 |
-
let name = nameMatch ? nameMatch[1].replace(/<!\[CDATA\[(.*?)\]\]>/gs, "$1").trim() : "";
|
| 405 |
-
let args = argsMatch ? argsMatch[1].replace(/<!\[CDATA\[(.*?)\]\]>/gs, "$1").trim() : "";
|
| 406 |
-
|
| 407 |
-
let payload = null;
|
| 408 |
-
if (args) {
|
| 409 |
-
try {
|
| 410 |
-
payload = JSON.parse(args);
|
| 411 |
-
} catch (e) {
|
| 412 |
-
payload = null;
|
| 413 |
-
}
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
let label = name;
|
| 417 |
-
let text = args;
|
| 418 |
-
const prefix = rolloutId ? `[${rolloutId}]` : "";
|
| 419 |
-
|
| 420 |
-
if (name === "web_search") {
|
| 421 |
-
label = `${prefix}[WebSearch]`;
|
| 422 |
-
if (payload && typeof payload === 'object') {
|
| 423 |
-
text = payload.query || payload.q || "";
|
| 424 |
-
}
|
| 425 |
-
} else if (name === "search_images") {
|
| 426 |
-
label = `${prefix}[SearchImage]`;
|
| 427 |
-
if (payload && typeof payload === 'object') {
|
| 428 |
-
text = payload.image_description || payload.description || payload.query || "";
|
| 429 |
-
}
|
| 430 |
-
} else if (name === "chatroom_send") {
|
| 431 |
-
label = `${prefix}[AgentThink]`;
|
| 432 |
-
if (payload && typeof payload === 'object') {
|
| 433 |
-
text = payload.message || "";
|
| 434 |
-
}
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
if (label && text) return `${label} ${text}`.trim();
|
| 438 |
-
if (label) return label;
|
| 439 |
-
if (text) return text;
|
| 440 |
-
return raw.replace(/<[^>]+>/g, "").trim();
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
/**
|
| 444 |
-
* 过滤特殊标签
|
| 445 |
-
*/
|
| 446 |
-
_filterToken(token, requestId = "") {
|
| 447 |
-
if (!token) return token;
|
| 448 |
-
|
| 449 |
-
let filtered = token;
|
| 450 |
-
|
| 451 |
-
// 移除 xai:tool_usage_card 及其内容,不显示工具调用的过程输出
|
| 452 |
-
filtered = filtered.replace(/<xai:tool_usage_card[^>]*>.*?<\/xai:tool_usage_card>/gs, "");
|
| 453 |
-
filtered = filtered.replace(/<xai:tool_usage_card[^>]*\/>/gs, "");
|
| 454 |
-
|
| 455 |
-
// 移除其他内部标签
|
| 456 |
-
const tagsToFilter = ["rolloutId", "responseId", "isThinking"];
|
| 457 |
-
for (const tag of tagsToFilter) {
|
| 458 |
-
const pattern = new RegExp(`<${tag}[^>]*>.*?<\\/${tag}>|<${tag}[^>]*\\/>`, 'gs');
|
| 459 |
-
filtered = filtered.replace(pattern, "");
|
| 460 |
-
}
|
| 461 |
-
|
| 462 |
-
return filtered;
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
/**
|
| 466 |
-
* Grok响应 -> OpenAI响应
|
| 467 |
-
*/
|
| 468 |
-
toOpenAIResponse(grokResponse, model) {
|
| 469 |
-
if (!grokResponse) return null;
|
| 470 |
-
|
| 471 |
-
const responseId = grokResponse.responseId || `chatcmpl-${uuidv4()}`;
|
| 472 |
-
let content = grokResponse.message || "";
|
| 473 |
-
const modelHash = grokResponse.llmInfo?.modelHash || "";
|
| 474 |
-
|
| 475 |
-
const state = this._getState(this._formatResponseId(responseId));
|
| 476 |
-
if (grokResponse._requestBaseUrl) {
|
| 477 |
-
state.requestBaseUrl = grokResponse._requestBaseUrl;
|
| 478 |
-
}
|
| 479 |
-
if (grokResponse._uuid) {
|
| 480 |
-
state.uuid = grokResponse._uuid;
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
// 过滤内容并处理其中的 Grok 资源链接
|
| 484 |
-
content = this._filterToken(content, responseId);
|
| 485 |
-
content = this._processGrokAssetsInText(content, state);
|
| 486 |
-
|
| 487 |
-
// 收集图片并追加
|
| 488 |
-
const imageUrls = this._collectImages(grokResponse);
|
| 489 |
-
if (imageUrls.length > 0) {
|
| 490 |
-
content += "\n";
|
| 491 |
-
for (const url of imageUrls) {
|
| 492 |
-
content += this._renderImage(url, "image", state) + "\n";
|
| 493 |
-
}
|
| 494 |
-
}
|
| 495 |
-
|
| 496 |
-
// 处理视频 (非流式模式)
|
| 497 |
-
if (grokResponse.finalVideoUrl) {
|
| 498 |
-
content += this._renderVideo(grokResponse.finalVideoUrl, grokResponse.finalThumbnailUrl, state);
|
| 499 |
-
}
|
| 500 |
-
|
| 501 |
-
// 解析工具调用
|
| 502 |
-
const { text, toolCalls } = this.parseToolCalls(content);
|
| 503 |
-
|
| 504 |
-
const result = {
|
| 505 |
-
id: responseId,
|
| 506 |
-
object: "chat.completion",
|
| 507 |
-
created: Math.floor(Date.now() / 1000),
|
| 508 |
-
model: model,
|
| 509 |
-
system_fingerprint: modelHash,
|
| 510 |
-
choices: [{
|
| 511 |
-
index: 0,
|
| 512 |
-
message: {
|
| 513 |
-
role: "assistant",
|
| 514 |
-
content: text,
|
| 515 |
-
},
|
| 516 |
-
finish_reason: toolCalls ? "tool_calls" : "stop",
|
| 517 |
-
}],
|
| 518 |
-
usage: {
|
| 519 |
-
prompt_tokens: 0,
|
| 520 |
-
completion_tokens: 0,
|
| 521 |
-
total_tokens: 0,
|
| 522 |
-
},
|
| 523 |
-
};
|
| 524 |
-
|
| 525 |
-
if (toolCalls) {
|
| 526 |
-
result.choices[0].message.tool_calls = toolCalls;
|
| 527 |
-
}
|
| 528 |
-
|
| 529 |
-
return result;
|
| 530 |
-
}
|
| 531 |
-
|
| 532 |
-
_formatResponseId(id) {
|
| 533 |
-
if (!id) return `chatcmpl-${uuidv4()}`;
|
| 534 |
-
if (id.startsWith('chatcmpl-')) return id;
|
| 535 |
-
return `chatcmpl-${id}`;
|
| 536 |
-
}
|
| 537 |
-
|
| 538 |
-
/**
|
| 539 |
-
* Grok流式响应块 -> OpenAI流式响应块
|
| 540 |
-
*/
|
| 541 |
-
toOpenAIStreamChunk(grokChunk, model) {
|
| 542 |
-
if (!grokChunk || !grokChunk.result || !grokChunk.result.response) {
|
| 543 |
-
return null;
|
| 544 |
-
}
|
| 545 |
-
|
| 546 |
-
const resp = grokChunk.result.response;
|
| 547 |
-
const rawResponseId = resp.responseId || "";
|
| 548 |
-
const responseId = this._formatResponseId(rawResponseId);
|
| 549 |
-
const state = this._getState(responseId);
|
| 550 |
-
|
| 551 |
-
// 从响应块中同步 uuid 和基础 URL
|
| 552 |
-
if (resp._requestBaseUrl) {
|
| 553 |
-
state.requestBaseUrl = resp._requestBaseUrl;
|
| 554 |
-
}
|
| 555 |
-
if (resp._uuid) {
|
| 556 |
-
state.uuid = resp._uuid;
|
| 557 |
-
}
|
| 558 |
-
|
| 559 |
-
if (resp.llmInfo?.modelHash && !state.fingerprint) {
|
| 560 |
-
state.fingerprint = resp.llmInfo.modelHash;
|
| 561 |
-
}
|
| 562 |
-
if (resp.rolloutId) {
|
| 563 |
-
state.rollout_id = String(resp.rolloutId);
|
| 564 |
-
}
|
| 565 |
-
|
| 566 |
-
const chunks = [];
|
| 567 |
-
|
| 568 |
-
// 0. 发送角色信息(仅第一次)
|
| 569 |
-
if (!state.role_sent) {
|
| 570 |
-
chunks.push({
|
| 571 |
-
id: responseId,
|
| 572 |
-
object: "chat.completion.chunk",
|
| 573 |
-
created: Math.floor(Date.now() / 1000),
|
| 574 |
-
model: model,
|
| 575 |
-
system_fingerprint: state.fingerprint,
|
| 576 |
-
choices: [{
|
| 577 |
-
index: 0,
|
| 578 |
-
delta: { role: "assistant", content: "" },
|
| 579 |
-
finish_reason: null
|
| 580 |
-
}]
|
| 581 |
-
});
|
| 582 |
-
state.role_sent = true;
|
| 583 |
-
}
|
| 584 |
-
|
| 585 |
-
// 处理结束标志
|
| 586 |
-
if (resp.isDone) {
|
| 587 |
-
let finalContent = "";
|
| 588 |
-
// 处理剩余的缓冲区
|
| 589 |
-
if (state.pending_text_buffer) {
|
| 590 |
-
finalContent += this._processGrokAssetsInText(state.pending_text_buffer, state);
|
| 591 |
-
state.pending_text_buffer = "";
|
| 592 |
-
}
|
| 593 |
-
|
| 594 |
-
// 处理 buffer 中的工具调用
|
| 595 |
-
const { text, toolCalls } = this.parseToolCalls(state.content_buffer);
|
| 596 |
-
|
| 597 |
-
if (toolCalls) {
|
| 598 |
-
chunks.push({
|
| 599 |
-
id: responseId,
|
| 600 |
-
object: "chat.completion.chunk",
|
| 601 |
-
created: Math.floor(Date.now() / 1000),
|
| 602 |
-
model: model,
|
| 603 |
-
system_fingerprint: state.fingerprint,
|
| 604 |
-
choices: [{
|
| 605 |
-
index: 0,
|
| 606 |
-
delta: {
|
| 607 |
-
content: (finalContent + (text || "")).trim() || null,
|
| 608 |
-
tool_calls: toolCalls
|
| 609 |
-
},
|
| 610 |
-
finish_reason: "tool_calls"
|
| 611 |
-
}]
|
| 612 |
-
});
|
| 613 |
-
} else {
|
| 614 |
-
chunks.push({
|
| 615 |
-
id: responseId,
|
| 616 |
-
object: "chat.completion.chunk",
|
| 617 |
-
created: Math.floor(Date.now() / 1000),
|
| 618 |
-
model: model,
|
| 619 |
-
system_fingerprint: state.fingerprint,
|
| 620 |
-
choices: [{
|
| 621 |
-
index: 0,
|
| 622 |
-
delta: { content: finalContent || null },
|
| 623 |
-
finish_reason: "stop"
|
| 624 |
-
}]
|
| 625 |
-
});
|
| 626 |
-
}
|
| 627 |
-
|
| 628 |
-
// 清理状态
|
| 629 |
-
this.requestStates.delete(responseId);
|
| 630 |
-
return chunks;
|
| 631 |
-
}
|
| 632 |
-
|
| 633 |
-
let deltaContent = "";
|
| 634 |
-
let deltaReasoning = "";
|
| 635 |
-
|
| 636 |
-
// 1. 处理图片生成进度
|
| 637 |
-
if (resp.streamingImageGenerationResponse) {
|
| 638 |
-
const img = resp.streamingImageGenerationResponse;
|
| 639 |
-
state.image_think_active = true;
|
| 640 |
-
/*
|
| 641 |
-
if (!state.think_opened) {
|
| 642 |
-
deltaReasoning += "<think>\n";
|
| 643 |
-
state.think_opened = true;
|
| 644 |
-
}
|
| 645 |
-
*/
|
| 646 |
-
const idx = (img.imageIndex || 0) + 1;
|
| 647 |
-
const progress = img.progress || 0;
|
| 648 |
-
deltaReasoning += `正在生成第${idx}张图片中,当前进度${progress}%\n`;
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
// 2. 处理视频生成进度 (VideoStreamProcessor)
|
| 652 |
-
if (resp.streamingVideoGenerationResponse) {
|
| 653 |
-
const vid = resp.streamingVideoGenerationResponse;
|
| 654 |
-
state.video_think_active = true;
|
| 655 |
-
/*
|
| 656 |
-
if (!state.think_opened) {
|
| 657 |
-
deltaReasoning += "<think>\n";
|
| 658 |
-
state.think_opened = true;
|
| 659 |
-
}
|
| 660 |
-
*/
|
| 661 |
-
const progress = vid.progress || 0;
|
| 662 |
-
deltaReasoning += `正在生成视频中,当前进度${progress}%\n`;
|
| 663 |
-
|
| 664 |
-
if (progress === 100 && vid.videoUrl) {
|
| 665 |
-
/*
|
| 666 |
-
if (state.think_opened) {
|
| 667 |
-
deltaContent += "\n</think>\n";
|
| 668 |
-
state.think_opened = false;
|
| 669 |
-
}
|
| 670 |
-
*/
|
| 671 |
-
state.video_think_active = false;
|
| 672 |
-
deltaContent += this._renderVideo(vid.videoUrl, vid.thumbnailImageUrl, state);
|
| 673 |
-
}
|
| 674 |
-
}
|
| 675 |
-
|
| 676 |
-
// 3. 处理模型响应(通常包含完整消息或图片)
|
| 677 |
-
if (resp.modelResponse) {
|
| 678 |
-
const mr = resp.modelResponse;
|
| 679 |
-
/*
|
| 680 |
-
if ((state.image_think_active || state.video_think_active) && state.think_opened) {
|
| 681 |
-
deltaContent += "\n</think>\n";
|
| 682 |
-
state.think_opened = false;
|
| 683 |
-
}
|
| 684 |
-
*/
|
| 685 |
-
state.image_think_active = false;
|
| 686 |
-
state.video_think_active = false;
|
| 687 |
-
|
| 688 |
-
const imageUrls = this._collectImages(mr);
|
| 689 |
-
for (const url of imageUrls) {
|
| 690 |
-
deltaContent += this._renderImage(url, "image", state) + "\n";
|
| 691 |
-
}
|
| 692 |
-
|
| 693 |
-
if (mr.metadata?.llm_info?.modelHash) {
|
| 694 |
-
state.fingerprint = mr.metadata.llm_info.modelHash;
|
| 695 |
-
}
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
-
// 4. 处理卡片附件
|
| 699 |
-
if (resp.cardAttachment) {
|
| 700 |
-
const card = resp.cardAttachment;
|
| 701 |
-
if (card.jsonData) {
|
| 702 |
-
try {
|
| 703 |
-
const cardData = JSON.parse(card.jsonData);
|
| 704 |
-
let original = cardData.image?.original;
|
| 705 |
-
const title = cardData.image?.title || "image";
|
| 706 |
-
if (original) {
|
| 707 |
-
// 确保是绝对路径
|
| 708 |
-
if (!original.startsWith('http')) {
|
| 709 |
-
original = `https://assets.grok.com${original.startsWith('/') ? '' : '/'}${original}`;
|
| 710 |
-
}
|
| 711 |
-
original = this._appendSsoToken(original, state);
|
| 712 |
-
deltaContent += `\n`;
|
| 713 |
-
}
|
| 714 |
-
} catch (e) {
|
| 715 |
-
// 忽略 JSON 解析错误
|
| 716 |
-
}
|
| 717 |
-
}
|
| 718 |
-
}
|
| 719 |
-
|
| 720 |
-
// 5. 处理普通 Token 和 思考状态
|
| 721 |
-
if (resp.token !== undefined && resp.token !== null) {
|
| 722 |
-
const token = resp.token;
|
| 723 |
-
const filtered = this._filterToken(token, responseId);
|
| 724 |
-
const isThinking = !!resp.isThinking;
|
| 725 |
-
const inThink = isThinking || state.image_think_active || state.video_think_active;
|
| 726 |
-
|
| 727 |
-
if (inThink) {
|
| 728 |
-
deltaReasoning += filtered;
|
| 729 |
-
} else {
|
| 730 |
-
// 将新 token 加入待处理缓冲区,解决 URL 被截断的问题
|
| 731 |
-
state.pending_text_buffer += filtered;
|
| 732 |
-
|
| 733 |
-
let outputFromBuffer = "";
|
| 734 |
-
|
| 735 |
-
// 启发式逻辑:检查缓冲区是否包含完整的 URL
|
| 736 |
-
if (state.pending_text_buffer.includes("https://assets.grok.com")) {
|
| 737 |
-
const lastUrlIndex = state.pending_text_buffer.lastIndexOf("https://assets.grok.com");
|
| 738 |
-
const textAfterUrl = state.pending_text_buffer.slice(lastUrlIndex);
|
| 739 |
-
|
| 740 |
-
// 检查 URL 是否结束(空格、右括号、引号、换行、大于号等)
|
| 741 |
-
const terminatorMatch = textAfterUrl.match(/[\s\)\"\'\>\n]/);
|
| 742 |
-
if (terminatorMatch) {
|
| 743 |
-
// URL 已结束,可以安全地处理并输出缓冲区
|
| 744 |
-
outputFromBuffer = this._processGrokAssetsInText(state.pending_text_buffer, state);
|
| 745 |
-
state.pending_text_buffer = "";
|
| 746 |
-
} else if (state.pending_text_buffer.length > 1000) {
|
| 747 |
-
// 缓冲区过长,强制处理输出,避免过度延迟
|
| 748 |
-
outputFromBuffer = this._processGrokAssetsInText(state.pending_text_buffer, state);
|
| 749 |
-
state.pending_text_buffer = "";
|
| 750 |
-
}
|
| 751 |
-
} else {
|
| 752 |
-
// 不包含 Grok URL,直接输出
|
| 753 |
-
outputFromBuffer = state.pending_text_buffer;
|
| 754 |
-
state.pending_text_buffer = "";
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
if (outputFromBuffer) {
|
| 758 |
-
// 工具调用抑制逻辑:不向客户端输出 <tool_call> 块及其内容
|
| 759 |
-
let outputToken = outputFromBuffer;
|
| 760 |
-
|
| 761 |
-
// 简单的状态切换检测
|
| 762 |
-
if (outputToken.includes('<tool_call>')) {
|
| 763 |
-
state.in_tool_call = true;
|
| 764 |
-
state.has_tool_call = true;
|
| 765 |
-
// 移除标签之后的部分(如果有)
|
| 766 |
-
outputToken = outputToken.split('<tool_call>')[0];
|
| 767 |
-
} else if (state.in_tool_call && outputToken.includes('</tool_call>')) {
|
| 768 |
-
state.in_tool_call = false;
|
| 769 |
-
// 只保留标签之后的部分
|
| 770 |
-
outputToken = outputToken.split('</tool_call>')[1] || "";
|
| 771 |
-
} else if (state.in_tool_call) {
|
| 772 |
-
// 处于块内,完全抑制
|
| 773 |
-
outputToken = "";
|
| 774 |
-
}
|
| 775 |
-
|
| 776 |
-
deltaContent += outputToken;
|
| 777 |
-
}
|
| 778 |
-
|
| 779 |
-
// 将内容加入 buffer 用于最终解析工具调用
|
| 780 |
-
state.content_buffer += filtered;
|
| 781 |
-
}
|
| 782 |
-
state.last_is_thinking = isThinking;
|
| 783 |
-
}
|
| 784 |
-
|
| 785 |
-
if (deltaContent || deltaReasoning) {
|
| 786 |
-
const delta = {};
|
| 787 |
-
if (deltaContent) delta.content = deltaContent;
|
| 788 |
-
if (deltaReasoning) delta.reasoning_content = deltaReasoning;
|
| 789 |
-
|
| 790 |
-
chunks.push({
|
| 791 |
-
id: responseId,
|
| 792 |
-
object: "chat.completion.chunk",
|
| 793 |
-
created: Math.floor(Date.now() / 1000),
|
| 794 |
-
model: model,
|
| 795 |
-
system_fingerprint: state.fingerprint,
|
| 796 |
-
choices: [{
|
| 797 |
-
index: 0,
|
| 798 |
-
delta: delta,
|
| 799 |
-
finish_reason: null
|
| 800 |
-
}]
|
| 801 |
-
});
|
| 802 |
-
}
|
| 803 |
-
|
| 804 |
-
return chunks.length > 0 ? chunks : null;
|
| 805 |
-
}
|
| 806 |
-
|
| 807 |
-
/**
|
| 808 |
-
* Grok响应 -> Gemini响应
|
| 809 |
-
*/
|
| 810 |
-
toGeminiResponse(grokResponse, model) {
|
| 811 |
-
const openaiRes = this.toOpenAIResponse(grokResponse, model);
|
| 812 |
-
if (!openaiRes) return null;
|
| 813 |
-
|
| 814 |
-
const choice = openaiRes.choices[0];
|
| 815 |
-
const message = choice.message;
|
| 816 |
-
const parts = [];
|
| 817 |
-
|
| 818 |
-
if (message.reasoning_content) {
|
| 819 |
-
parts.push({ text: message.reasoning_content, thought: true });
|
| 820 |
-
}
|
| 821 |
-
|
| 822 |
-
if (message.content) {
|
| 823 |
-
parts.push({ text: message.content });
|
| 824 |
-
}
|
| 825 |
-
|
| 826 |
-
if (message.tool_calls) {
|
| 827 |
-
for (const tc of message.tool_calls) {
|
| 828 |
-
parts.push({
|
| 829 |
-
functionCall: {
|
| 830 |
-
name: tc.function.name,
|
| 831 |
-
args: typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments
|
| 832 |
-
}
|
| 833 |
-
});
|
| 834 |
-
}
|
| 835 |
-
}
|
| 836 |
-
|
| 837 |
-
return {
|
| 838 |
-
candidates: [{
|
| 839 |
-
content: {
|
| 840 |
-
role: 'model',
|
| 841 |
-
parts: parts
|
| 842 |
-
},
|
| 843 |
-
finishReason: choice.finish_reason === 'tool_calls' ? 'STOP' : (choice.finish_reason === 'length' ? 'MAX_TOKENS' : 'STOP')
|
| 844 |
-
}],
|
| 845 |
-
usageMetadata: {
|
| 846 |
-
promptTokenCount: openaiRes.usage.prompt_tokens,
|
| 847 |
-
candidatesTokenCount: openaiRes.usage.completion_tokens,
|
| 848 |
-
totalTokenCount: openaiRes.usage.total_tokens
|
| 849 |
-
}
|
| 850 |
-
};
|
| 851 |
-
}
|
| 852 |
-
|
| 853 |
-
/**
|
| 854 |
-
* Grok流式响应块 -> Gemini流式响应块
|
| 855 |
-
*/
|
| 856 |
-
toGeminiStreamChunk(grokChunk, model) {
|
| 857 |
-
const openaiChunks = this.toOpenAIStreamChunk(grokChunk, model);
|
| 858 |
-
if (!openaiChunks) return null;
|
| 859 |
-
|
| 860 |
-
const geminiChunks = [];
|
| 861 |
-
for (const oachunk of openaiChunks) {
|
| 862 |
-
const choice = oachunk.choices[0];
|
| 863 |
-
const delta = choice.delta;
|
| 864 |
-
const parts = [];
|
| 865 |
-
|
| 866 |
-
if (delta.reasoning_content) {
|
| 867 |
-
parts.push({ text: delta.reasoning_content, thought: true });
|
| 868 |
-
}
|
| 869 |
-
if (delta.content) {
|
| 870 |
-
parts.push({ text: delta.content });
|
| 871 |
-
}
|
| 872 |
-
if (delta.tool_calls) {
|
| 873 |
-
for (const tc of delta.tool_calls) {
|
| 874 |
-
parts.push({
|
| 875 |
-
functionCall: {
|
| 876 |
-
name: tc.function.name,
|
| 877 |
-
args: typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments
|
| 878 |
-
}
|
| 879 |
-
});
|
| 880 |
-
}
|
| 881 |
-
}
|
| 882 |
-
|
| 883 |
-
if (parts.length > 0 || choice.finish_reason) {
|
| 884 |
-
const gchunk = {
|
| 885 |
-
candidates: [{
|
| 886 |
-
content: {
|
| 887 |
-
role: 'model',
|
| 888 |
-
parts: parts
|
| 889 |
-
}
|
| 890 |
-
}]
|
| 891 |
-
};
|
| 892 |
-
if (choice.finish_reason) {
|
| 893 |
-
gchunk.candidates[0].finishReason = choice.finish_reason === 'length' ? 'MAX_TOKENS' : 'STOP';
|
| 894 |
-
}
|
| 895 |
-
geminiChunks.push(gchunk);
|
| 896 |
-
}
|
| 897 |
-
}
|
| 898 |
-
|
| 899 |
-
return geminiChunks.length > 0 ? geminiChunks : null;
|
| 900 |
-
}
|
| 901 |
-
|
| 902 |
-
/**
|
| 903 |
-
* Grok响应 -> OpenAI Responses响应
|
| 904 |
-
*/
|
| 905 |
-
toOpenAIResponsesResponse(grokResponse, model) {
|
| 906 |
-
const openaiRes = this.toOpenAIResponse(grokResponse, model);
|
| 907 |
-
if (!openaiRes) return null;
|
| 908 |
-
|
| 909 |
-
const choice = openaiRes.choices[0];
|
| 910 |
-
const message = choice.message;
|
| 911 |
-
const output = [];
|
| 912 |
-
|
| 913 |
-
const content = [];
|
| 914 |
-
if (message.content) {
|
| 915 |
-
content.push({
|
| 916 |
-
type: "output_text",
|
| 917 |
-
text: message.content
|
| 918 |
-
});
|
| 919 |
-
}
|
| 920 |
-
|
| 921 |
-
output.push({
|
| 922 |
-
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 923 |
-
type: "message",
|
| 924 |
-
role: "assistant",
|
| 925 |
-
status: "completed",
|
| 926 |
-
content: content
|
| 927 |
-
});
|
| 928 |
-
|
| 929 |
-
if (message.tool_calls) {
|
| 930 |
-
for (const tc of message.tool_calls) {
|
| 931 |
-
output.push({
|
| 932 |
-
id: tc.id,
|
| 933 |
-
type: "function_call",
|
| 934 |
-
name: tc.function.name,
|
| 935 |
-
arguments: tc.function.arguments,
|
| 936 |
-
status: "completed"
|
| 937 |
-
});
|
| 938 |
-
}
|
| 939 |
-
}
|
| 940 |
-
|
| 941 |
-
return {
|
| 942 |
-
id: `resp_${uuidv4().replace(/-/g, '')}`,
|
| 943 |
-
object: "response",
|
| 944 |
-
created_at: Math.floor(Date.now() / 1000),
|
| 945 |
-
status: "completed",
|
| 946 |
-
model: model,
|
| 947 |
-
output: output,
|
| 948 |
-
usage: {
|
| 949 |
-
input_tokens: openaiRes.usage.prompt_tokens,
|
| 950 |
-
output_tokens: openaiRes.usage.completion_tokens,
|
| 951 |
-
total_tokens: openaiRes.usage.total_tokens
|
| 952 |
-
}
|
| 953 |
-
};
|
| 954 |
-
}
|
| 955 |
-
|
| 956 |
-
/**
|
| 957 |
-
* Grok流式响应块 -> OpenAI Responses流式响应块
|
| 958 |
-
*/
|
| 959 |
-
toOpenAIResponsesStreamChunk(grokChunk, model) {
|
| 960 |
-
const openaiChunks = this.toOpenAIStreamChunk(grokChunk, model);
|
| 961 |
-
if (!openaiChunks) return null;
|
| 962 |
-
|
| 963 |
-
const events = [];
|
| 964 |
-
for (const oachunk of openaiChunks) {
|
| 965 |
-
const choice = oachunk.choices[0];
|
| 966 |
-
const delta = choice.delta;
|
| 967 |
-
|
| 968 |
-
if (delta.role === 'assistant') {
|
| 969 |
-
events.push({ type: "response.created", response: { id: oachunk.id, model: model } });
|
| 970 |
-
}
|
| 971 |
-
|
| 972 |
-
if (delta.reasoning_content) {
|
| 973 |
-
events.push({
|
| 974 |
-
type: "response.reasoning_summary_text.delta",
|
| 975 |
-
delta: delta.reasoning_content,
|
| 976 |
-
response_id: oachunk.id
|
| 977 |
-
});
|
| 978 |
-
}
|
| 979 |
-
|
| 980 |
-
if (delta.content) {
|
| 981 |
-
events.push({
|
| 982 |
-
type: "response.output_text.delta",
|
| 983 |
-
delta: delta.content,
|
| 984 |
-
response_id: oachunk.id
|
| 985 |
-
});
|
| 986 |
-
}
|
| 987 |
-
|
| 988 |
-
if (delta.tool_calls) {
|
| 989 |
-
for (const tc of delta.tool_calls) {
|
| 990 |
-
if (tc.function?.name) {
|
| 991 |
-
events.push({
|
| 992 |
-
type: "response.output_item.added",
|
| 993 |
-
item: { id: tc.id, type: "function_call", name: tc.function.name, arguments: "" },
|
| 994 |
-
response_id: oachunk.id
|
| 995 |
-
});
|
| 996 |
-
}
|
| 997 |
-
if (tc.function?.arguments) {
|
| 998 |
-
events.push({
|
| 999 |
-
type: "response.custom_tool_call_input.delta",
|
| 1000 |
-
delta: tc.function.arguments,
|
| 1001 |
-
item_id: tc.id,
|
| 1002 |
-
response_id: oachunk.id
|
| 1003 |
-
});
|
| 1004 |
-
}
|
| 1005 |
-
}
|
| 1006 |
-
}
|
| 1007 |
-
|
| 1008 |
-
if (choice.finish_reason) {
|
| 1009 |
-
events.push({ type: "response.completed", response: { id: oachunk.id, status: "completed" } });
|
| 1010 |
-
}
|
| 1011 |
-
}
|
| 1012 |
-
|
| 1013 |
-
return events;
|
| 1014 |
-
}
|
| 1015 |
-
|
| 1016 |
-
/**
|
| 1017 |
-
* Grok响应 -> Codex响应
|
| 1018 |
-
*/
|
| 1019 |
-
toCodexResponse(grokResponse, model) {
|
| 1020 |
-
const openaiRes = this.toOpenAIResponse(grokResponse, model);
|
| 1021 |
-
if (!openaiRes) return null;
|
| 1022 |
-
|
| 1023 |
-
const choice = openaiRes.choices[0];
|
| 1024 |
-
const message = choice.message;
|
| 1025 |
-
const output = [];
|
| 1026 |
-
|
| 1027 |
-
if (message.content) {
|
| 1028 |
-
output.push({
|
| 1029 |
-
type: "message",
|
| 1030 |
-
role: "assistant",
|
| 1031 |
-
content: [{ type: "output_text", text: message.content }]
|
| 1032 |
-
});
|
| 1033 |
-
}
|
| 1034 |
-
|
| 1035 |
-
if (message.reasoning_content) {
|
| 1036 |
-
output.push({
|
| 1037 |
-
type: "reasoning",
|
| 1038 |
-
summary: [{ type: "summary_text", text: message.reasoning_content }]
|
| 1039 |
-
});
|
| 1040 |
-
}
|
| 1041 |
-
|
| 1042 |
-
if (message.tool_calls) {
|
| 1043 |
-
for (const tc of message.tool_calls) {
|
| 1044 |
-
output.push({
|
| 1045 |
-
type: "function_call",
|
| 1046 |
-
call_id: tc.id,
|
| 1047 |
-
name: tc.function.name,
|
| 1048 |
-
arguments: tc.function.arguments
|
| 1049 |
-
});
|
| 1050 |
-
}
|
| 1051 |
-
}
|
| 1052 |
-
|
| 1053 |
-
return {
|
| 1054 |
-
response: {
|
| 1055 |
-
id: openaiRes.id,
|
| 1056 |
-
output: output,
|
| 1057 |
-
usage: {
|
| 1058 |
-
input_tokens: openaiRes.usage.prompt_tokens,
|
| 1059 |
-
output_tokens: openaiRes.usage.completion_tokens,
|
| 1060 |
-
total_tokens: openaiRes.usage.total_tokens
|
| 1061 |
-
}
|
| 1062 |
-
}
|
| 1063 |
-
};
|
| 1064 |
-
}
|
| 1065 |
-
|
| 1066 |
-
/**
|
| 1067 |
-
* Grok流式响应块 -> Codex流式响应块
|
| 1068 |
-
*/
|
| 1069 |
-
toCodexStreamChunk(grokChunk, model) {
|
| 1070 |
-
const openaiChunks = this.toOpenAIStreamChunk(grokChunk, model);
|
| 1071 |
-
if (!openaiChunks) return null;
|
| 1072 |
-
|
| 1073 |
-
const codexChunks = [];
|
| 1074 |
-
for (const oachunk of openaiChunks) {
|
| 1075 |
-
const choice = oachunk.choices[0];
|
| 1076 |
-
const delta = choice.delta;
|
| 1077 |
-
|
| 1078 |
-
if (delta.role === 'assistant') {
|
| 1079 |
-
codexChunks.push({ type: "response.created", response: { id: oachunk.id } });
|
| 1080 |
-
}
|
| 1081 |
-
|
| 1082 |
-
if (delta.reasoning_content) {
|
| 1083 |
-
codexChunks.push({
|
| 1084 |
-
type: "response.reasoning_summary_text.delta",
|
| 1085 |
-
delta: delta.reasoning_content,
|
| 1086 |
-
response: { id: oachunk.id }
|
| 1087 |
-
});
|
| 1088 |
-
}
|
| 1089 |
-
|
| 1090 |
-
if (delta.content) {
|
| 1091 |
-
codexChunks.push({
|
| 1092 |
-
type: "response.output_text.delta",
|
| 1093 |
-
delta: delta.content,
|
| 1094 |
-
response: { id: oachunk.id }
|
| 1095 |
-
});
|
| 1096 |
-
}
|
| 1097 |
-
|
| 1098 |
-
if (delta.tool_calls) {
|
| 1099 |
-
for (const tc of delta.tool_calls) {
|
| 1100 |
-
if (tc.function?.arguments) {
|
| 1101 |
-
codexChunks.push({
|
| 1102 |
-
type: "response.custom_tool_call_input.delta",
|
| 1103 |
-
delta: tc.function.arguments,
|
| 1104 |
-
item_id: tc.id,
|
| 1105 |
-
response: { id: oachunk.id }
|
| 1106 |
-
});
|
| 1107 |
-
}
|
| 1108 |
-
}
|
| 1109 |
-
}
|
| 1110 |
-
|
| 1111 |
-
if (choice.finish_reason) {
|
| 1112 |
-
codexChunks.push({ type: "response.completed", response: { id: oachunk.id, usage: oachunk.usage } });
|
| 1113 |
-
}
|
| 1114 |
-
}
|
| 1115 |
-
|
| 1116 |
-
return codexChunks.length > 0 ? codexChunks : null;
|
| 1117 |
-
}
|
| 1118 |
-
|
| 1119 |
-
/**
|
| 1120 |
-
* Grok模型列表 -> OpenAI模型列表
|
| 1121 |
-
*/
|
| 1122 |
-
toOpenAIModelList(grokModels) {
|
| 1123 |
-
const models = Array.isArray(grokModels) ? grokModels : (grokModels?.models || grokModels?.data || []);
|
| 1124 |
-
return {
|
| 1125 |
-
object: "list",
|
| 1126 |
-
data: models.map(m => ({
|
| 1127 |
-
id: m.id || m.name || (typeof m === 'string' ? m : ''),
|
| 1128 |
-
object: "model",
|
| 1129 |
-
created: Math.floor(Date.now() / 1000),
|
| 1130 |
-
owned_by: "xai",
|
| 1131 |
-
display_name: m.display_name || m.name || m.id || (typeof m === 'string' ? m : ''),
|
| 1132 |
-
})),
|
| 1133 |
-
};
|
| 1134 |
-
}
|
| 1135 |
-
|
| 1136 |
-
/**
|
| 1137 |
-
* Grok模���列表 -> Gemini模型列表
|
| 1138 |
-
*/
|
| 1139 |
-
toGeminiModelList(grokModels) {
|
| 1140 |
-
const models = Array.isArray(grokModels) ? grokModels : (grokModels?.models || grokModels?.data || []);
|
| 1141 |
-
return {
|
| 1142 |
-
models: models.map(m => ({
|
| 1143 |
-
name: `models/${m.id || m.name || (typeof m === 'string' ? m : '')}`,
|
| 1144 |
-
version: "1.0",
|
| 1145 |
-
displayName: m.display_name || m.name || m.id || (typeof m === 'string' ? m : ''),
|
| 1146 |
-
description: m.description || `Grok model: ${m.name || m.id || (typeof m === 'string' ? m : '')}`,
|
| 1147 |
-
inputTokenLimit: 131072,
|
| 1148 |
-
outputTokenLimit: 8192,
|
| 1149 |
-
supportedGenerationMethods: ["generateContent", "streamGenerateContent"]
|
| 1150 |
-
}))
|
| 1151 |
-
};
|
| 1152 |
-
}
|
| 1153 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/strategies/OpenAIConverter.js
DELETED
|
@@ -1,1769 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* OpenAI转换器
|
| 3 |
-
* 处理OpenAI协议与其他协议之间的转换
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
-
import logger from '../../utils/logger.js';
|
| 8 |
-
import { BaseConverter } from '../BaseConverter.js';
|
| 9 |
-
import { CodexConverter } from './CodexConverter.js';
|
| 10 |
-
import {
|
| 11 |
-
extractAndProcessSystemMessages as extractSystemMessages,
|
| 12 |
-
extractTextFromMessageContent as extractText,
|
| 13 |
-
safeParseJSON,
|
| 14 |
-
checkAndAssignOrDefault,
|
| 15 |
-
extractThinkingFromOpenAIText,
|
| 16 |
-
mapFinishReason,
|
| 17 |
-
cleanJsonSchemaProperties as cleanJsonSchema,
|
| 18 |
-
CLAUDE_DEFAULT_MAX_TOKENS,
|
| 19 |
-
CLAUDE_DEFAULT_TEMPERATURE,
|
| 20 |
-
CLAUDE_DEFAULT_TOP_P,
|
| 21 |
-
GEMINI_DEFAULT_MAX_TOKENS,
|
| 22 |
-
GEMINI_DEFAULT_TEMPERATURE,
|
| 23 |
-
GEMINI_DEFAULT_TOP_P,
|
| 24 |
-
OPENAI_DEFAULT_INPUT_TOKEN_LIMIT,
|
| 25 |
-
OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT
|
| 26 |
-
} from '../utils.js';
|
| 27 |
-
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
| 28 |
-
import {
|
| 29 |
-
generateResponseCreated,
|
| 30 |
-
generateResponseInProgress,
|
| 31 |
-
generateOutputItemAdded,
|
| 32 |
-
generateContentPartAdded,
|
| 33 |
-
generateOutputTextDone,
|
| 34 |
-
generateContentPartDone,
|
| 35 |
-
generateOutputItemDone,
|
| 36 |
-
generateResponseCompleted
|
| 37 |
-
} from '../../providers/openai/openai-responses-core.mjs';
|
| 38 |
-
|
| 39 |
-
/**
|
| 40 |
-
* OpenAI转换器类
|
| 41 |
-
* 实现OpenAI协议到其他协议的转换
|
| 42 |
-
*/
|
| 43 |
-
export class OpenAIConverter extends BaseConverter {
|
| 44 |
-
constructor() {
|
| 45 |
-
super('openai');
|
| 46 |
-
// 创建 CodexConverter 实例用于委托
|
| 47 |
-
this.codexConverter = new CodexConverter();
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
/**
|
| 51 |
-
* 转换请求
|
| 52 |
-
*/
|
| 53 |
-
convertRequest(data, targetProtocol) {
|
| 54 |
-
switch (targetProtocol) {
|
| 55 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 56 |
-
return this.toClaudeRequest(data);
|
| 57 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 58 |
-
return this.toGeminiRequest(data);
|
| 59 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 60 |
-
return this.toOpenAIResponsesRequest(data);
|
| 61 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 62 |
-
return this.toCodexRequest(data);
|
| 63 |
-
case MODEL_PROTOCOL_PREFIX.GROK:
|
| 64 |
-
return this.toGrokRequest(data);
|
| 65 |
-
default:
|
| 66 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
/**
|
| 71 |
-
* 转换响应
|
| 72 |
-
*/
|
| 73 |
-
convertResponse(data, targetProtocol, model) {
|
| 74 |
-
// OpenAI作为源格式时,通常不需要转换响应
|
| 75 |
-
// 因为其他协议会转换到OpenAI格式
|
| 76 |
-
switch (targetProtocol) {
|
| 77 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 78 |
-
return this.toClaudeResponse(data, model);
|
| 79 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 80 |
-
return this.toGeminiResponse(data, model);
|
| 81 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 82 |
-
return this.toOpenAIResponsesResponse(data, model);
|
| 83 |
-
case MODEL_PROTOCOL_PREFIX.GROK:
|
| 84 |
-
return this.toGrokResponse(data, model);
|
| 85 |
-
default:
|
| 86 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 87 |
-
}
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
/**
|
| 91 |
-
* 转换流式响应块
|
| 92 |
-
*/
|
| 93 |
-
convertStreamChunk(chunk, targetProtocol, model) {
|
| 94 |
-
switch (targetProtocol) {
|
| 95 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 96 |
-
return this.toClaudeStreamChunk(chunk, model);
|
| 97 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 98 |
-
return this.toGeminiStreamChunk(chunk, model);
|
| 99 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
|
| 100 |
-
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
| 101 |
-
case MODEL_PROTOCOL_PREFIX.GROK:
|
| 102 |
-
return this.toGrokStreamChunk(chunk, model);
|
| 103 |
-
default:
|
| 104 |
-
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
|
| 105 |
-
}
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
/**
|
| 109 |
-
* 转换模型列表
|
| 110 |
-
*/
|
| 111 |
-
convertModelList(data, targetProtocol) {
|
| 112 |
-
switch (targetProtocol) {
|
| 113 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 114 |
-
return this.toClaudeModelList(data);
|
| 115 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 116 |
-
return this.toGeminiModelList(data);
|
| 117 |
-
default:
|
| 118 |
-
return this.ensureDisplayName(data);
|
| 119 |
-
}
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
/**
|
| 123 |
-
* Ensure display_name field exists in OpenAI model list
|
| 124 |
-
*/
|
| 125 |
-
ensureDisplayName(openaiModels) {
|
| 126 |
-
if (!openaiModels || !openaiModels.data) {
|
| 127 |
-
return openaiModels;
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
return {
|
| 131 |
-
...openaiModels,
|
| 132 |
-
data: openaiModels.data.map(model => ({
|
| 133 |
-
...model,
|
| 134 |
-
display_name: model.display_name || model.id,
|
| 135 |
-
})),
|
| 136 |
-
};
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
// =========================================================================
|
| 140 |
-
// OpenAI -> Claude 转换
|
| 141 |
-
// =========================================================================
|
| 142 |
-
|
| 143 |
-
/**
|
| 144 |
-
* OpenAI请求 -> Claude请求
|
| 145 |
-
*/
|
| 146 |
-
toClaudeRequest(openaiRequest) {
|
| 147 |
-
const messages = openaiRequest.messages || [];
|
| 148 |
-
const { systemInstruction, nonSystemMessages } = extractSystemMessages(messages);
|
| 149 |
-
|
| 150 |
-
const claudeMessages = [];
|
| 151 |
-
|
| 152 |
-
for (const message of nonSystemMessages) {
|
| 153 |
-
const role = message.role === 'assistant' ? 'assistant' : 'user';
|
| 154 |
-
let content = [];
|
| 155 |
-
|
| 156 |
-
if (message.role === 'tool') {
|
| 157 |
-
// 工具结果消息
|
| 158 |
-
let toolContent = message.content;
|
| 159 |
-
if (typeof toolContent === 'object' && toolContent !== null) {
|
| 160 |
-
toolContent = JSON.stringify(toolContent);
|
| 161 |
-
}
|
| 162 |
-
content.push({
|
| 163 |
-
type: 'tool_result',
|
| 164 |
-
tool_use_id: message.tool_call_id || message.tool_use_id,
|
| 165 |
-
content: toolContent
|
| 166 |
-
});
|
| 167 |
-
claudeMessages.push({ role: 'user', content: content });
|
| 168 |
-
} else if (message.role === 'assistant' && (message.tool_calls?.length || message.function_calls?.length)) {
|
| 169 |
-
// 助手工具调用消息 - 支持tool_calls和function_calls
|
| 170 |
-
const calls = message.tool_calls || message.function_calls || [];
|
| 171 |
-
const toolUseBlocks = calls.map(tc => ({
|
| 172 |
-
type: 'tool_use',
|
| 173 |
-
id: tc.id,
|
| 174 |
-
name: tc.function.name,
|
| 175 |
-
input: safeParseJSON(tc.function.arguments)
|
| 176 |
-
}));
|
| 177 |
-
claudeMessages.push({ role: 'assistant', content: toolUseBlocks });
|
| 178 |
-
} else {
|
| 179 |
-
// 普通消息
|
| 180 |
-
if (typeof message.content === 'string') {
|
| 181 |
-
if (message.content) {
|
| 182 |
-
content.push({ type: 'text', text: message.content.trim() });
|
| 183 |
-
}
|
| 184 |
-
} else if (Array.isArray(message.content)) {
|
| 185 |
-
message.content.forEach(item => {
|
| 186 |
-
if (!item) return;
|
| 187 |
-
switch (item.type) {
|
| 188 |
-
case 'text':
|
| 189 |
-
if (item.text) {
|
| 190 |
-
content.push({ type: 'text', text: item.text.trim() });
|
| 191 |
-
}
|
| 192 |
-
break;
|
| 193 |
-
case 'image_url':
|
| 194 |
-
if (item.image_url) {
|
| 195 |
-
const imageUrl = typeof item.image_url === 'string'
|
| 196 |
-
? item.image_url
|
| 197 |
-
: item.image_url.url;
|
| 198 |
-
if (imageUrl.startsWith('data:')) {
|
| 199 |
-
const [header, data] = imageUrl.split(',');
|
| 200 |
-
const mediaType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg';
|
| 201 |
-
content.push({
|
| 202 |
-
type: 'image',
|
| 203 |
-
source: {
|
| 204 |
-
type: 'base64',
|
| 205 |
-
media_type: mediaType,
|
| 206 |
-
data: data
|
| 207 |
-
}
|
| 208 |
-
});
|
| 209 |
-
} else {
|
| 210 |
-
content.push({ type: 'text', text: `[Image: ${imageUrl}]` });
|
| 211 |
-
}
|
| 212 |
-
}
|
| 213 |
-
break;
|
| 214 |
-
case 'audio':
|
| 215 |
-
if (item.audio_url) {
|
| 216 |
-
const audioUrl = typeof item.audio_url === 'string'
|
| 217 |
-
? item.audio_url
|
| 218 |
-
: item.audio_url.url;
|
| 219 |
-
content.push({ type: 'text', text: `[Audio: ${audioUrl}]` });
|
| 220 |
-
}
|
| 221 |
-
break;
|
| 222 |
-
case 'input_audio':
|
| 223 |
-
// OpenAI 官方 input_audio 格式
|
| 224 |
-
if (item.input_audio) {
|
| 225 |
-
// Claude 不直接支持音频输入,转换为文本描述
|
| 226 |
-
content.push({ type: 'text', text: `[Audio Input: ${item.input_audio.format || 'audio'}]` });
|
| 227 |
-
}
|
| 228 |
-
break;
|
| 229 |
-
case 'tool_use':
|
| 230 |
-
content.push({
|
| 231 |
-
type: 'tool_use',
|
| 232 |
-
id: item.id,
|
| 233 |
-
name: item.name,
|
| 234 |
-
input: typeof item.input === 'string' ? safeParseJSON(item.input) : (item.input || {})
|
| 235 |
-
});
|
| 236 |
-
break;
|
| 237 |
-
case 'tool_result': {
|
| 238 |
-
let resultContent = item.content;
|
| 239 |
-
if (typeof resultContent === 'object' && resultContent !== null) {
|
| 240 |
-
resultContent = JSON.stringify(resultContent);
|
| 241 |
-
}
|
| 242 |
-
content.push({
|
| 243 |
-
type: 'tool_result',
|
| 244 |
-
tool_use_id: item.tool_use_id || item.id,
|
| 245 |
-
content: resultContent
|
| 246 |
-
});
|
| 247 |
-
break;
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
});
|
| 251 |
-
}
|
| 252 |
-
if (content.length > 0) {
|
| 253 |
-
claudeMessages.push({ role: role, content: content });
|
| 254 |
-
}
|
| 255 |
-
}
|
| 256 |
-
}
|
| 257 |
-
// 合并相邻相同 role 的消息
|
| 258 |
-
const mergedClaudeMessages = [];
|
| 259 |
-
for (let i = 0; i < claudeMessages.length; i++) {
|
| 260 |
-
const currentMessage = claudeMessages[i];
|
| 261 |
-
|
| 262 |
-
if (mergedClaudeMessages.length === 0) {
|
| 263 |
-
mergedClaudeMessages.push(currentMessage);
|
| 264 |
-
} else {
|
| 265 |
-
const lastMessage = mergedClaudeMessages[mergedClaudeMessages.length - 1];
|
| 266 |
-
|
| 267 |
-
// 如果当前消息的 role 与上一条消息的 role 相同,则合并 content 数组
|
| 268 |
-
if (lastMessage.role === currentMessage.role) {
|
| 269 |
-
lastMessage.content = lastMessage.content.concat(currentMessage.content);
|
| 270 |
-
} else {
|
| 271 |
-
mergedClaudeMessages.push(currentMessage);
|
| 272 |
-
}
|
| 273 |
-
}
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
// 清理最后一条 assistant 消息的尾部空白
|
| 277 |
-
if (mergedClaudeMessages.length > 0) {
|
| 278 |
-
const lastMessage = mergedClaudeMessages[mergedClaudeMessages.length - 1];
|
| 279 |
-
if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
|
| 280 |
-
// 从后往前找到最后一个 text 类型的内容块
|
| 281 |
-
for (let i = lastMessage.content.length - 1; i >= 0; i--) {
|
| 282 |
-
const contentBlock = lastMessage.content[i];
|
| 283 |
-
if (contentBlock.type === 'text' && contentBlock.text) {
|
| 284 |
-
// 移除尾部空白字符
|
| 285 |
-
contentBlock.text = contentBlock.text.trimEnd();
|
| 286 |
-
break;
|
| 287 |
-
}
|
| 288 |
-
}
|
| 289 |
-
}
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
const claudeRequest = {
|
| 294 |
-
model: openaiRequest.model,
|
| 295 |
-
messages: mergedClaudeMessages,
|
| 296 |
-
max_tokens: checkAndAssignOrDefault(openaiRequest.max_tokens, CLAUDE_DEFAULT_MAX_TOKENS),
|
| 297 |
-
temperature: checkAndAssignOrDefault(openaiRequest.temperature, CLAUDE_DEFAULT_TEMPERATURE),
|
| 298 |
-
top_p: checkAndAssignOrDefault(openaiRequest.top_p, CLAUDE_DEFAULT_TOP_P),
|
| 299 |
-
};
|
| 300 |
-
|
| 301 |
-
if (systemInstruction) {
|
| 302 |
-
claudeRequest.system = extractText(systemInstruction.parts[0].text);
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
if (openaiRequest.tools?.length) {
|
| 306 |
-
claudeRequest.tools = openaiRequest.tools
|
| 307 |
-
.filter(t => t && ((t.function && t.function.name) || t.name))
|
| 308 |
-
.map(t => {
|
| 309 |
-
if (t.function) {
|
| 310 |
-
return {
|
| 311 |
-
name: t.function.name,
|
| 312 |
-
description: t.function.description || '',
|
| 313 |
-
input_schema: t.function.parameters || { type: 'object', properties: {} }
|
| 314 |
-
};
|
| 315 |
-
}
|
| 316 |
-
return {
|
| 317 |
-
name: t.name,
|
| 318 |
-
description: t.description || '',
|
| 319 |
-
input_schema: t.input_schema || { type: 'object', properties: {} }
|
| 320 |
-
};
|
| 321 |
-
});
|
| 322 |
-
if (claudeRequest.tools.length > 0) {
|
| 323 |
-
claudeRequest.tool_choice = this.buildClaudeToolChoice(openaiRequest.tool_choice);
|
| 324 |
-
}
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
-
// Optional passthrough: request-side "thinking" controls for Claude/Kiro.
|
| 328 |
-
// OpenAI-compatible clients can provide these via `extra_body.anthropic.thinking`.
|
| 329 |
-
// We intentionally keep normalization minimal here; provider implementations
|
| 330 |
-
// (e.g. Kiro) clamp budgets and apply defaults.
|
| 331 |
-
const extThinking = openaiRequest?.extra_body?.anthropic?.thinking;
|
| 332 |
-
if (extThinking && typeof extThinking === 'object' && !Array.isArray(extThinking)) {
|
| 333 |
-
const type = String(extThinking.type || '').toLowerCase().trim();
|
| 334 |
-
if (type === 'enabled') {
|
| 335 |
-
const thinkingCfg = { type: 'enabled' };
|
| 336 |
-
if (extThinking.budget_tokens !== undefined) {
|
| 337 |
-
const n = parseInt(extThinking.budget_tokens, 10);
|
| 338 |
-
if (Number.isFinite(n)) {
|
| 339 |
-
thinkingCfg.budget_tokens = n;
|
| 340 |
-
}
|
| 341 |
-
}
|
| 342 |
-
claudeRequest.thinking = thinkingCfg;
|
| 343 |
-
} else if (type === 'adaptive') {
|
| 344 |
-
const effortRaw = typeof extThinking.effort === 'string' ? extThinking.effort : '';
|
| 345 |
-
const effort = effortRaw.toLowerCase().trim();
|
| 346 |
-
const normalizedEffort = (effort === 'low' || effort === 'medium' || effort === 'high') ? effort : 'high';
|
| 347 |
-
claudeRequest.thinking = { type: 'adaptive', effort: normalizedEffort };
|
| 348 |
-
} else if (type === 'disabled') {
|
| 349 |
-
// Explicitly disabled: omit thinking config.
|
| 350 |
-
}
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
return claudeRequest;
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
/**
|
| 357 |
-
* OpenAI响应 -> Claude响应
|
| 358 |
-
*/
|
| 359 |
-
toClaudeResponse(openaiResponse, model) {
|
| 360 |
-
if (!openaiResponse || !openaiResponse.choices || openaiResponse.choices.length === 0) {
|
| 361 |
-
return {
|
| 362 |
-
id: `msg_${uuidv4()}`,
|
| 363 |
-
type: "message",
|
| 364 |
-
role: "assistant",
|
| 365 |
-
content: [],
|
| 366 |
-
model: model,
|
| 367 |
-
stop_reason: "end_turn",
|
| 368 |
-
stop_sequence: null,
|
| 369 |
-
usage: {
|
| 370 |
-
input_tokens: openaiResponse?.usage?.prompt_tokens || 0,
|
| 371 |
-
output_tokens: openaiResponse?.usage?.completion_tokens || 0
|
| 372 |
-
}
|
| 373 |
-
};
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
const choice = openaiResponse.choices[0];
|
| 377 |
-
const contentList = [];
|
| 378 |
-
|
| 379 |
-
// 处理工具调用 - 支持tool_calls和function_calls
|
| 380 |
-
const toolCalls = choice.message?.tool_calls || choice.message?.function_calls || [];
|
| 381 |
-
for (const toolCall of toolCalls.filter(tc => tc && typeof tc === 'object')) {
|
| 382 |
-
if (toolCall.function) {
|
| 383 |
-
const func = toolCall.function;
|
| 384 |
-
const argStr = func.arguments || "{}";
|
| 385 |
-
let argObj;
|
| 386 |
-
try {
|
| 387 |
-
argObj = typeof argStr === 'string' ? JSON.parse(argStr) : argStr;
|
| 388 |
-
} catch (e) {
|
| 389 |
-
argObj = {};
|
| 390 |
-
}
|
| 391 |
-
contentList.push({
|
| 392 |
-
type: "tool_use",
|
| 393 |
-
id: toolCall.id || "",
|
| 394 |
-
name: func.name || "",
|
| 395 |
-
input: argObj,
|
| 396 |
-
});
|
| 397 |
-
}
|
| 398 |
-
}
|
| 399 |
-
|
| 400 |
-
// 处理reasoning_content(推理内容)
|
| 401 |
-
const reasoningContent = choice.message?.reasoning_content || "";
|
| 402 |
-
if (reasoningContent) {
|
| 403 |
-
contentList.push({
|
| 404 |
-
type: "thinking",
|
| 405 |
-
thinking: reasoningContent
|
| 406 |
-
});
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
// 处理文本内容
|
| 410 |
-
const contentText = choice.message?.content || "";
|
| 411 |
-
if (contentText) {
|
| 412 |
-
const extractedContent = extractThinkingFromOpenAIText(contentText);
|
| 413 |
-
if (Array.isArray(extractedContent)) {
|
| 414 |
-
contentList.push(...extractedContent);
|
| 415 |
-
} else {
|
| 416 |
-
contentList.push({ type: "text", text: extractedContent });
|
| 417 |
-
}
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
// 映射结束原因
|
| 421 |
-
const stopReason = mapFinishReason(
|
| 422 |
-
choice.finish_reason || "stop",
|
| 423 |
-
"openai",
|
| 424 |
-
"anthropic"
|
| 425 |
-
);
|
| 426 |
-
|
| 427 |
-
return {
|
| 428 |
-
id: `msg_${uuidv4()}`,
|
| 429 |
-
type: "message",
|
| 430 |
-
role: "assistant",
|
| 431 |
-
content: contentList,
|
| 432 |
-
model: model,
|
| 433 |
-
stop_reason: stopReason,
|
| 434 |
-
stop_sequence: null,
|
| 435 |
-
usage: {
|
| 436 |
-
input_tokens: openaiResponse.usage?.prompt_tokens || 0,
|
| 437 |
-
cache_creation_input_tokens: 0,
|
| 438 |
-
cache_read_input_tokens: openaiResponse.usage?.prompt_tokens_details?.cached_tokens || 0,
|
| 439 |
-
output_tokens: openaiResponse.usage?.completion_tokens || 0
|
| 440 |
-
}
|
| 441 |
-
};
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
-
/**
|
| 445 |
-
* OpenAI流式响应 -> Claude流式响应
|
| 446 |
-
*
|
| 447 |
-
* 这个方法实现了与 ClaudeConverter.toOpenAIStreamChunk 相反的转换逻辑
|
| 448 |
-
* 将 OpenAI 的流式 chunk 转换为 Claude 的流式事件
|
| 449 |
-
*/
|
| 450 |
-
toClaudeStreamChunk(openaiChunk, model) {
|
| 451 |
-
if (!openaiChunk) return null;
|
| 452 |
-
|
| 453 |
-
// 处理 OpenAI chunk 对象
|
| 454 |
-
if (typeof openaiChunk === 'object' && !Array.isArray(openaiChunk)) {
|
| 455 |
-
const choice = openaiChunk.choices?.[0];
|
| 456 |
-
if (!choice) {
|
| 457 |
-
return null;
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
const delta = choice.delta;
|
| 461 |
-
const finishReason = choice.finish_reason;
|
| 462 |
-
const events = [];
|
| 463 |
-
|
| 464 |
-
// 注释部分是为了兼容claude code,但是不兼容cherry studio
|
| 465 |
-
// 1. 处理 role (对应 message_start)
|
| 466 |
-
// if (delta?.role === "assistant") {
|
| 467 |
-
// events.push({
|
| 468 |
-
// type: "message_start",
|
| 469 |
-
// message: {
|
| 470 |
-
// id: openaiChunk.id || `msg_${uuidv4()}`,
|
| 471 |
-
// type: "message",
|
| 472 |
-
// role: "assistant",
|
| 473 |
-
// content: [],
|
| 474 |
-
// model: model || openaiChunk.model || "unknown",
|
| 475 |
-
// stop_reason: null,
|
| 476 |
-
// stop_sequence: null,
|
| 477 |
-
// usage: {
|
| 478 |
-
// input_tokens: openaiChunk.usage?.prompt_tokens || 0,
|
| 479 |
-
// output_tokens: 0
|
| 480 |
-
// }
|
| 481 |
-
// }
|
| 482 |
-
// });
|
| 483 |
-
// events.push({
|
| 484 |
-
// type: "content_block_start",
|
| 485 |
-
// index: 0,
|
| 486 |
-
// content_block: {
|
| 487 |
-
// type: "text",
|
| 488 |
-
// text: ""
|
| 489 |
-
// }
|
| 490 |
-
// });
|
| 491 |
-
// }
|
| 492 |
-
|
| 493 |
-
// 2. 处理 tool_calls (对应 content_block_start 和 content_block_delta)
|
| 494 |
-
// if (delta?.tool_calls) {
|
| 495 |
-
// const toolCalls = delta.tool_calls;
|
| 496 |
-
// for (const toolCall of toolCalls) {
|
| 497 |
-
// // 如果有 function.name,说明是工具调用开始
|
| 498 |
-
// if (toolCall.function?.name) {
|
| 499 |
-
// events.push({
|
| 500 |
-
// type: "content_block_start",
|
| 501 |
-
// index: toolCall.index || 0,
|
| 502 |
-
// content_block: {
|
| 503 |
-
// type: "tool_use",
|
| 504 |
-
// id: toolCall.id || `tool_${uuidv4()}`,
|
| 505 |
-
// name: toolCall.function.name,
|
| 506 |
-
// input: {}
|
| 507 |
-
// }
|
| 508 |
-
// });
|
| 509 |
-
// }
|
| 510 |
-
|
| 511 |
-
// // 如果有 function.arguments,说明是参数增量
|
| 512 |
-
// if (toolCall.function?.arguments) {
|
| 513 |
-
// events.push({
|
| 514 |
-
// type: "content_block_delta",
|
| 515 |
-
// index: toolCall.index || 0,
|
| 516 |
-
// delta: {
|
| 517 |
-
// type: "input_json_delta",
|
| 518 |
-
// partial_json: toolCall.function.arguments
|
| 519 |
-
// }
|
| 520 |
-
// });
|
| 521 |
-
// }
|
| 522 |
-
// }
|
| 523 |
-
// }
|
| 524 |
-
|
| 525 |
-
// 3. 处理 reasoning_content (对应 thinking 类型的 content_block)
|
| 526 |
-
if (delta?.reasoning_content) {
|
| 527 |
-
// 注意:这里可能需要先发送 content_block_start,但由于状态管理复杂,
|
| 528 |
-
// 我们假设调用方会处理这个逻辑
|
| 529 |
-
events.push({
|
| 530 |
-
type: "content_block_delta",
|
| 531 |
-
index: 0,
|
| 532 |
-
delta: {
|
| 533 |
-
type: "thinking_delta",
|
| 534 |
-
thinking: delta.reasoning_content
|
| 535 |
-
}
|
| 536 |
-
});
|
| 537 |
-
}
|
| 538 |
-
|
| 539 |
-
// 4. 处理普通文本 content (对应 text 类型的 content_block)
|
| 540 |
-
if (delta?.content) {
|
| 541 |
-
events.push({
|
| 542 |
-
type: "content_block_delta",
|
| 543 |
-
index: 0,
|
| 544 |
-
delta: {
|
| 545 |
-
type: "text_delta",
|
| 546 |
-
text: delta.content
|
| 547 |
-
}
|
| 548 |
-
});
|
| 549 |
-
}
|
| 550 |
-
|
| 551 |
-
// 5. 处理 finish_reason (对应 message_delta 和 message_stop)
|
| 552 |
-
if (finishReason) {
|
| 553 |
-
// 映射 finish_reason
|
| 554 |
-
const stopReason = finishReason === "stop" ? "end_turn" :
|
| 555 |
-
finishReason === "length" ? "max_tokens" :
|
| 556 |
-
"end_turn";
|
| 557 |
-
|
| 558 |
-
events.push({
|
| 559 |
-
type: "content_block_stop",
|
| 560 |
-
index: 0
|
| 561 |
-
});
|
| 562 |
-
// 发送 message_delta
|
| 563 |
-
events.push({
|
| 564 |
-
type: "message_delta",
|
| 565 |
-
delta: {
|
| 566 |
-
stop_reason: stopReason,
|
| 567 |
-
stop_sequence: null
|
| 568 |
-
},
|
| 569 |
-
usage: {
|
| 570 |
-
input_tokens: openaiChunk.usage?.prompt_tokens || 0,
|
| 571 |
-
cache_creation_input_tokens: 0,
|
| 572 |
-
cache_read_input_tokens: openaiChunk.usage?.prompt_tokens_details?.cached_tokens || 0,
|
| 573 |
-
output_tokens: openaiChunk.usage?.completion_tokens || 0
|
| 574 |
-
}
|
| 575 |
-
});
|
| 576 |
-
|
| 577 |
-
// 发送 message_stop
|
| 578 |
-
events.push({
|
| 579 |
-
type: "message_stop"
|
| 580 |
-
});
|
| 581 |
-
}
|
| 582 |
-
|
| 583 |
-
return events.length > 0 ? events : null;
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
// 向后兼容:处理字符串格式
|
| 587 |
-
if (typeof openaiChunk === 'string') {
|
| 588 |
-
return {
|
| 589 |
-
type: "content_block_delta",
|
| 590 |
-
index: 0,
|
| 591 |
-
delta: {
|
| 592 |
-
type: "text_delta",
|
| 593 |
-
text: openaiChunk
|
| 594 |
-
}
|
| 595 |
-
};
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
return null;
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
/**
|
| 602 |
-
* OpenAI模型列表 -> Claude模型列表
|
| 603 |
-
*/
|
| 604 |
-
toClaudeModelList(openaiModels) {
|
| 605 |
-
return {
|
| 606 |
-
models: openaiModels.data.map(m => ({
|
| 607 |
-
name: m.id,
|
| 608 |
-
description: "",
|
| 609 |
-
})),
|
| 610 |
-
};
|
| 611 |
-
}
|
| 612 |
-
|
| 613 |
-
/**
|
| 614 |
-
* 将 OpenAI 模型列表转换为 Gemini 模型列表
|
| 615 |
-
*/
|
| 616 |
-
toGeminiModelList(openaiModels) {
|
| 617 |
-
const models = openaiModels.data || [];
|
| 618 |
-
return {
|
| 619 |
-
models: models.map(m => ({
|
| 620 |
-
name: `models/${m.id}`,
|
| 621 |
-
version: m.version || "1.0.0",
|
| 622 |
-
displayName: m.displayName || m.id,
|
| 623 |
-
description: m.description || `A generative model for text and chat generation. ID: ${m.id}`,
|
| 624 |
-
inputTokenLimit: m.inputTokenLimit || OPENAI_DEFAULT_INPUT_TOKEN_LIMIT,
|
| 625 |
-
outputTokenLimit: m.outputTokenLimit || OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT,
|
| 626 |
-
supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
|
| 627 |
-
}))
|
| 628 |
-
};
|
| 629 |
-
}
|
| 630 |
-
|
| 631 |
-
/**
|
| 632 |
-
* 构建Claude工具选择
|
| 633 |
-
*/
|
| 634 |
-
buildClaudeToolChoice(toolChoice) {
|
| 635 |
-
if (typeof toolChoice === 'string') {
|
| 636 |
-
const mapping = { auto: 'auto', none: 'none', required: 'any' };
|
| 637 |
-
return { type: mapping[toolChoice] };
|
| 638 |
-
}
|
| 639 |
-
if (typeof toolChoice === 'object') {
|
| 640 |
-
// Claude 原生格式:{ type, name }
|
| 641 |
-
if (toolChoice.type && toolChoice.name) {
|
| 642 |
-
return { type: toolChoice.type, name: toolChoice.name };
|
| 643 |
-
}
|
| 644 |
-
// OpenAI 格式:{ function: { name } }
|
| 645 |
-
if (toolChoice.function) {
|
| 646 |
-
return { type: 'tool', name: toolChoice.function.name };
|
| 647 |
-
}
|
| 648 |
-
}
|
| 649 |
-
return undefined;
|
| 650 |
-
}
|
| 651 |
-
|
| 652 |
-
// =========================================================================
|
| 653 |
-
// OpenAI -> Gemini 转换
|
| 654 |
-
// =========================================================================
|
| 655 |
-
|
| 656 |
-
// Gemini Openai thought signature constant
|
| 657 |
-
static GEMINI_OPENAI_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
|
| 658 |
-
/**
|
| 659 |
-
* OpenAI请求 -> Gemini请求
|
| 660 |
-
*/
|
| 661 |
-
toGeminiRequest(openaiRequest) {
|
| 662 |
-
const messages = openaiRequest.messages || [];
|
| 663 |
-
const model = openaiRequest.model || '';
|
| 664 |
-
|
| 665 |
-
// 构建 tool_call_id -> function_name 映射
|
| 666 |
-
const tcID2Name = {};
|
| 667 |
-
for (const message of messages) {
|
| 668 |
-
if (message.role === 'assistant' && message.tool_calls) {
|
| 669 |
-
for (const tc of message.tool_calls) {
|
| 670 |
-
if (tc.type === 'function' && tc.id && tc.function?.name) {
|
| 671 |
-
tcID2Name[tc.id] = tc.function.name;
|
| 672 |
-
}
|
| 673 |
-
}
|
| 674 |
-
}
|
| 675 |
-
// Claude 格式:content 数组中的 tool_use
|
| 676 |
-
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
| 677 |
-
for (const item of message.content) {
|
| 678 |
-
if (item && item.type === 'tool_use' && item.id && item.name) {
|
| 679 |
-
tcID2Name[item.id] = item.name;
|
| 680 |
-
}
|
| 681 |
-
}
|
| 682 |
-
}
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
// 构建 tool_call_id -> response 映射
|
| 686 |
-
const toolResponses = {};
|
| 687 |
-
for (const message of messages) {
|
| 688 |
-
if (message.role === 'tool' && message.tool_call_id) {
|
| 689 |
-
toolResponses[message.tool_call_id] = message.content;
|
| 690 |
-
}
|
| 691 |
-
// Claude 格式:user content 数组中的 tool_result
|
| 692 |
-
if (message.role === 'user' && Array.isArray(message.content)) {
|
| 693 |
-
for (const item of message.content) {
|
| 694 |
-
if (item && item.type === 'tool_result' && item.tool_use_id) {
|
| 695 |
-
toolResponses[item.tool_use_id] = item.content;
|
| 696 |
-
}
|
| 697 |
-
}
|
| 698 |
-
}
|
| 699 |
-
}
|
| 700 |
-
|
| 701 |
-
const processedMessages = [];
|
| 702 |
-
let systemInstruction = null;
|
| 703 |
-
|
| 704 |
-
for (let i = 0; i < messages.length; i++) {
|
| 705 |
-
const message = messages[i];
|
| 706 |
-
const role = message.role;
|
| 707 |
-
const content = message.content;
|
| 708 |
-
|
| 709 |
-
if (role === 'system' || role === 'developer') {
|
| 710 |
-
// system -> system_instruction
|
| 711 |
-
if (messages.length > 1) {
|
| 712 |
-
if (typeof content === 'string') {
|
| 713 |
-
systemInstruction = {
|
| 714 |
-
role: 'user',
|
| 715 |
-
parts: [{ text: content }]
|
| 716 |
-
};
|
| 717 |
-
} else if (Array.isArray(content)) {
|
| 718 |
-
const parts = content
|
| 719 |
-
.filter(item => item.type === 'text' && item.text)
|
| 720 |
-
.map(item => ({ text: item.text }));
|
| 721 |
-
if (parts.length > 0) {
|
| 722 |
-
systemInstruction = {
|
| 723 |
-
role: 'user',
|
| 724 |
-
parts: parts
|
| 725 |
-
};
|
| 726 |
-
}
|
| 727 |
-
} else if (typeof content === 'object' && content.type === 'text') {
|
| 728 |
-
systemInstruction = {
|
| 729 |
-
role: 'user',
|
| 730 |
-
parts: [{ text: content.text }]
|
| 731 |
-
};
|
| 732 |
-
}
|
| 733 |
-
} else {
|
| 734 |
-
// 只有一条 system 消息时,作为 user 消息处理
|
| 735 |
-
const node = { role: 'user', parts: [] };
|
| 736 |
-
if (typeof content === 'string') {
|
| 737 |
-
node.parts.push({ text: content });
|
| 738 |
-
} else if (Array.isArray(content)) {
|
| 739 |
-
for (const item of content) {
|
| 740 |
-
if (item.type === 'text' && item.text) {
|
| 741 |
-
node.parts.push({ text: item.text });
|
| 742 |
-
}
|
| 743 |
-
}
|
| 744 |
-
}
|
| 745 |
-
if (node.parts.length > 0) {
|
| 746 |
-
processedMessages.push(node);
|
| 747 |
-
}
|
| 748 |
-
}
|
| 749 |
-
} else if (role === 'user') {
|
| 750 |
-
// user -> user content
|
| 751 |
-
const node = { role: 'user', parts: [] };
|
| 752 |
-
if (typeof content === 'string') {
|
| 753 |
-
node.parts.push({ text: content });
|
| 754 |
-
} else if (Array.isArray(content)) {
|
| 755 |
-
for (const item of content) {
|
| 756 |
-
if (!item) continue;
|
| 757 |
-
switch (item.type) {
|
| 758 |
-
case 'text':
|
| 759 |
-
if (item.text) {
|
| 760 |
-
node.parts.push({ text: item.text });
|
| 761 |
-
}
|
| 762 |
-
break;
|
| 763 |
-
case 'image_url':
|
| 764 |
-
if (item.image_url) {
|
| 765 |
-
const imageUrl = typeof item.image_url === 'string'
|
| 766 |
-
? item.image_url
|
| 767 |
-
: item.image_url.url;
|
| 768 |
-
if (imageUrl && imageUrl.startsWith('data:')) {
|
| 769 |
-
const commaIndex = imageUrl.indexOf(',');
|
| 770 |
-
if (commaIndex > 5) {
|
| 771 |
-
const header = imageUrl.substring(5, commaIndex);
|
| 772 |
-
const semicolonIndex = header.indexOf(';');
|
| 773 |
-
if (semicolonIndex > 0) {
|
| 774 |
-
const mimeType = header.substring(0, semicolonIndex);
|
| 775 |
-
const data = imageUrl.substring(commaIndex + 1);
|
| 776 |
-
node.parts.push({
|
| 777 |
-
inlineData: {
|
| 778 |
-
mimeType: mimeType,
|
| 779 |
-
data: data
|
| 780 |
-
},
|
| 781 |
-
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
|
| 782 |
-
});
|
| 783 |
-
}
|
| 784 |
-
}
|
| 785 |
-
} else if (imageUrl) {
|
| 786 |
-
node.parts.push({
|
| 787 |
-
fileData: {
|
| 788 |
-
mimeType: 'image/jpeg',
|
| 789 |
-
fileUri: imageUrl
|
| 790 |
-
}
|
| 791 |
-
});
|
| 792 |
-
}
|
| 793 |
-
}
|
| 794 |
-
break;
|
| 795 |
-
case 'file':
|
| 796 |
-
if (item.file) {
|
| 797 |
-
const filename = item.file.filename || '';
|
| 798 |
-
const fileData = item.file.file_data || '';
|
| 799 |
-
const ext = filename.includes('.')
|
| 800 |
-
? filename.split('.').pop().toLowerCase()
|
| 801 |
-
: '';
|
| 802 |
-
const mimeTypes = {
|
| 803 |
-
'pdf': 'application/pdf',
|
| 804 |
-
'txt': 'text/plain',
|
| 805 |
-
'html': 'text/html',
|
| 806 |
-
'css': 'text/css',
|
| 807 |
-
'js': 'application/javascript',
|
| 808 |
-
'json': 'application/json',
|
| 809 |
-
'xml': 'application/xml',
|
| 810 |
-
'csv': 'text/csv',
|
| 811 |
-
'md': 'text/markdown',
|
| 812 |
-
'py': 'text/x-python',
|
| 813 |
-
'java': 'text/x-java',
|
| 814 |
-
'c': 'text/x-c',
|
| 815 |
-
'cpp': 'text/x-c++',
|
| 816 |
-
'h': 'text/x-c',
|
| 817 |
-
'hpp': 'text/x-c++',
|
| 818 |
-
'go': 'text/x-go',
|
| 819 |
-
'rs': 'text/x-rust',
|
| 820 |
-
'ts': 'text/typescript',
|
| 821 |
-
'tsx': 'text/typescript',
|
| 822 |
-
'jsx': 'text/javascript',
|
| 823 |
-
'png': 'image/png',
|
| 824 |
-
'jpg': 'image/jpeg',
|
| 825 |
-
'jpeg': 'image/jpeg',
|
| 826 |
-
'gif': 'image/gif',
|
| 827 |
-
'webp': 'image/webp',
|
| 828 |
-
'svg': 'image/svg+xml',
|
| 829 |
-
'mp3': 'audio/mpeg',
|
| 830 |
-
'wav': 'audio/wav',
|
| 831 |
-
'mp4': 'video/mp4',
|
| 832 |
-
'webm': 'video/webm'
|
| 833 |
-
};
|
| 834 |
-
const mimeType = mimeTypes[ext];
|
| 835 |
-
if (mimeType && fileData) {
|
| 836 |
-
node.parts.push({
|
| 837 |
-
inlineData: {
|
| 838 |
-
mimeType: mimeType,
|
| 839 |
-
data: fileData
|
| 840 |
-
}
|
| 841 |
-
});
|
| 842 |
-
}
|
| 843 |
-
}
|
| 844 |
-
break;
|
| 845 |
-
}
|
| 846 |
-
}
|
| 847 |
-
}
|
| 848 |
-
if (node.parts.length > 0) {
|
| 849 |
-
processedMessages.push(node);
|
| 850 |
-
}
|
| 851 |
-
} else if (role === 'assistant') {
|
| 852 |
-
// assistant -> model content
|
| 853 |
-
const node = { role: 'model', parts: [] };
|
| 854 |
-
|
| 855 |
-
// 处理文本内容
|
| 856 |
-
const functionCallIds = [];
|
| 857 |
-
if (typeof content === 'string' && content) {
|
| 858 |
-
node.parts.push({ text: content });
|
| 859 |
-
} else if (Array.isArray(content)) {
|
| 860 |
-
for (const item of content) {
|
| 861 |
-
if (!item) continue;
|
| 862 |
-
if (item.type === 'text' && item.text) {
|
| 863 |
-
node.parts.push({ text: item.text });
|
| 864 |
-
} else if (item.type === 'tool_use') {
|
| 865 |
-
// Claude 格式 tool_use -> Gemini functionCall
|
| 866 |
-
const fid = item.id || '';
|
| 867 |
-
const fname = item.name || '';
|
| 868 |
-
const argsObj = typeof item.input === 'string' ? (() => { try { return JSON.parse(item.input); } catch(e) { return {}; } })() : (item.input || {});
|
| 869 |
-
node.parts.push({
|
| 870 |
-
functionCall: {
|
| 871 |
-
name: fname,
|
| 872 |
-
args: argsObj
|
| 873 |
-
},
|
| 874 |
-
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
|
| 875 |
-
});
|
| 876 |
-
if (fid) functionCallIds.push(fid);
|
| 877 |
-
} else if (item.type === 'image_url' && item.image_url) {
|
| 878 |
-
const imageUrl = typeof item.image_url === 'string'
|
| 879 |
-
? item.image_url
|
| 880 |
-
: item.image_url.url;
|
| 881 |
-
if (imageUrl && imageUrl.startsWith('data:')) {
|
| 882 |
-
const commaIndex = imageUrl.indexOf(',');
|
| 883 |
-
if (commaIndex > 5) {
|
| 884 |
-
const header = imageUrl.substring(5, commaIndex);
|
| 885 |
-
const semicolonIndex = header.indexOf(';');
|
| 886 |
-
if (semicolonIndex > 0) {
|
| 887 |
-
const mimeType = header.substring(0, semicolonIndex);
|
| 888 |
-
const data = imageUrl.substring(commaIndex + 1);
|
| 889 |
-
node.parts.push({
|
| 890 |
-
inlineData: {
|
| 891 |
-
mimeType: mimeType,
|
| 892 |
-
data: data
|
| 893 |
-
},
|
| 894 |
-
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
|
| 895 |
-
});
|
| 896 |
-
}
|
| 897 |
-
}
|
| 898 |
-
}
|
| 899 |
-
}
|
| 900 |
-
}
|
| 901 |
-
}
|
| 902 |
-
|
| 903 |
-
// 处理 OpenAI 格式 tool_calls -> functionCall
|
| 904 |
-
if (message.tool_calls && Array.isArray(message.tool_calls)) {
|
| 905 |
-
for (const tc of message.tool_calls) {
|
| 906 |
-
if (tc.type !== 'function') continue;
|
| 907 |
-
const fid = tc.id || '';
|
| 908 |
-
const fname = tc.function?.name || '';
|
| 909 |
-
const fargs = tc.function?.arguments || '{}';
|
| 910 |
-
|
| 911 |
-
let argsObj;
|
| 912 |
-
try {
|
| 913 |
-
argsObj = typeof fargs === 'string' ? JSON.parse(fargs) : fargs;
|
| 914 |
-
} catch (e) {
|
| 915 |
-
argsObj = {};
|
| 916 |
-
}
|
| 917 |
-
|
| 918 |
-
node.parts.push({
|
| 919 |
-
functionCall: {
|
| 920 |
-
name: fname,
|
| 921 |
-
args: argsObj
|
| 922 |
-
},
|
| 923 |
-
thoughtSignature: OpenAIConverter.GEMINI_OPENAI_THOUGHT_SIGNATURE
|
| 924 |
-
});
|
| 925 |
-
|
| 926 |
-
if (fid) {
|
| 927 |
-
functionCallIds.push(fid);
|
| 928 |
-
}
|
| 929 |
-
}
|
| 930 |
-
}
|
| 931 |
-
|
| 932 |
-
// 添加 model 消息
|
| 933 |
-
if (node.parts.length > 0) {
|
| 934 |
-
processedMessages.push(node);
|
| 935 |
-
}
|
| 936 |
-
|
| 937 |
-
// 添加对应的 functionResponse(作为 user 消息)
|
| 938 |
-
if (functionCallIds.length > 0) {
|
| 939 |
-
const toolNode = { role: 'user', parts: [] };
|
| 940 |
-
for (const fid of functionCallIds) {
|
| 941 |
-
const name = tcID2Name[fid];
|
| 942 |
-
if (name) {
|
| 943 |
-
let resp = toolResponses[fid] || '{}';
|
| 944 |
-
if (typeof resp !== 'string') {
|
| 945 |
-
resp = JSON.stringify(resp);
|
| 946 |
-
}
|
| 947 |
-
toolNode.parts.push({
|
| 948 |
-
functionResponse: {
|
| 949 |
-
name: name,
|
| 950 |
-
response: {
|
| 951 |
-
result: resp
|
| 952 |
-
}
|
| 953 |
-
}
|
| 954 |
-
});
|
| 955 |
-
}
|
| 956 |
-
}
|
| 957 |
-
if (toolNode.parts.length > 0) {
|
| 958 |
-
processedMessages.push(toolNode);
|
| 959 |
-
}
|
| 960 |
-
}
|
| 961 |
-
} else if (role === 'tool') {
|
| 962 |
-
// 处理独立的 tool role 消息(OpenAI 格式)
|
| 963 |
-
// 转换为 Gemini 的 functionResponse 格式
|
| 964 |
-
const toolNode = { role: 'user', parts: [] };
|
| 965 |
-
|
| 966 |
-
// 从 tool_call_id 查找对应的函数名
|
| 967 |
-
const toolCallId = message.tool_call_id;
|
| 968 |
-
const functionName = tcID2Name[toolCallId];
|
| 969 |
-
|
| 970 |
-
if (functionName) {
|
| 971 |
-
let responseContent = message.content;
|
| 972 |
-
if (typeof responseContent !== 'string') {
|
| 973 |
-
responseContent = JSON.stringify(responseContent);
|
| 974 |
-
}
|
| 975 |
-
|
| 976 |
-
toolNode.parts.push({
|
| 977 |
-
functionResponse: {
|
| 978 |
-
name: functionName,
|
| 979 |
-
response: {
|
| 980 |
-
result: responseContent
|
| 981 |
-
}
|
| 982 |
-
}
|
| 983 |
-
});
|
| 984 |
-
|
| 985 |
-
if (toolNode.parts.length > 0) {
|
| 986 |
-
processedMessages.push(toolNode);
|
| 987 |
-
}
|
| 988 |
-
}
|
| 989 |
-
}
|
| 990 |
-
// 其他 role 类型跳过
|
| 991 |
-
}
|
| 992 |
-
|
| 993 |
-
// 构建 Gemini 请求
|
| 994 |
-
const geminiRequest = {
|
| 995 |
-
contents: processedMessages.filter(item => item.parts && item.parts.length > 0)
|
| 996 |
-
};
|
| 997 |
-
|
| 998 |
-
// 添加 model
|
| 999 |
-
if (model) {
|
| 1000 |
-
geminiRequest.model = model;
|
| 1001 |
-
}
|
| 1002 |
-
|
| 1003 |
-
// 添加 system_instruction
|
| 1004 |
-
if (systemInstruction) {
|
| 1005 |
-
geminiRequest.system_instruction = systemInstruction;
|
| 1006 |
-
}
|
| 1007 |
-
|
| 1008 |
-
// 处理 reasoning_effort -> thinkingConfig
|
| 1009 |
-
if (openaiRequest.reasoning_effort) {
|
| 1010 |
-
const effort = String(openaiRequest.reasoning_effort).toLowerCase().trim();
|
| 1011 |
-
if (this.modelSupportsThinking(model)) {
|
| 1012 |
-
if (this.isGemini3Model(model)) {
|
| 1013 |
-
// Gemini 3 模型使用 thinkingLevel
|
| 1014 |
-
if (effort === 'none') {
|
| 1015 |
-
// 不添加 thinkingConfig
|
| 1016 |
-
} else if (effort === 'auto') {
|
| 1017 |
-
geminiRequest.generationConfig = geminiRequest.generationConfig || {};
|
| 1018 |
-
geminiRequest.generationConfig.thinkingConfig = {
|
| 1019 |
-
includeThoughts: true
|
| 1020 |
-
};
|
| 1021 |
-
} else {
|
| 1022 |
-
const level = this.validateGemini3ThinkingLevel(model, effort);
|
| 1023 |
-
if (level) {
|
| 1024 |
-
geminiRequest.generationConfig = geminiRequest.generationConfig || {};
|
| 1025 |
-
geminiRequest.generationConfig.thinkingConfig = {
|
| 1026 |
-
thinkingLevel: level
|
| 1027 |
-
};
|
| 1028 |
-
}
|
| 1029 |
-
}
|
| 1030 |
-
} else if (!this.modelUsesThinkingLevels(model)) {
|
| 1031 |
-
// 使用 thinkingBudget 的模型
|
| 1032 |
-
geminiRequest.generationConfig = geminiRequest.generationConfig || {};
|
| 1033 |
-
geminiRequest.generationConfig.thinkingConfig = this.applyReasoningEffortToGemini(effort);
|
| 1034 |
-
}
|
| 1035 |
-
}
|
| 1036 |
-
}
|
| 1037 |
-
|
| 1038 |
-
// 处理 extra_body.google.thinking_config(Cherry Studio 扩展)
|
| 1039 |
-
if (!openaiRequest.reasoning_effort && openaiRequest.extra_body?.google?.thinking_config) {
|
| 1040 |
-
const tc = openaiRequest.extra_body.google.thinking_config;
|
| 1041 |
-
if (this.modelSupportsThinking(model) && !this.modelUsesThinkingLevels(model)) {
|
| 1042 |
-
geminiRequest.generationConfig = geminiRequest.generationConfig || {};
|
| 1043 |
-
geminiRequest.generationConfig.thinkingConfig = geminiRequest.generationConfig.thinkingConfig || {};
|
| 1044 |
-
|
| 1045 |
-
let setBudget = false;
|
| 1046 |
-
let budget = 0;
|
| 1047 |
-
|
| 1048 |
-
if (tc.thinkingBudget !== undefined) {
|
| 1049 |
-
budget = parseInt(tc.thinkingBudget, 10);
|
| 1050 |
-
geminiRequest.generationConfig.thinkingConfig.thinkingBudget = budget;
|
| 1051 |
-
setBudget = true;
|
| 1052 |
-
} else if (tc.thinking_budget !== undefined) {
|
| 1053 |
-
budget = parseInt(tc.thinking_budget, 10);
|
| 1054 |
-
geminiRequest.generationConfig.thinkingConfig.thinkingBudget = budget;
|
| 1055 |
-
setBudget = true;
|
| 1056 |
-
}
|
| 1057 |
-
|
| 1058 |
-
if (tc.includeThoughts !== undefined) {
|
| 1059 |
-
geminiRequest.generationConfig.thinkingConfig.includeThoughts = tc.includeThoughts;
|
| 1060 |
-
} else if (tc.include_thoughts !== undefined) {
|
| 1061 |
-
geminiRequest.generationConfig.thinkingConfig.includeThoughts = tc.include_thoughts;
|
| 1062 |
-
} else if (setBudget && budget !== 0) {
|
| 1063 |
-
geminiRequest.generationConfig.thinkingConfig.includeThoughts = true;
|
| 1064 |
-
}
|
| 1065 |
-
}
|
| 1066 |
-
}
|
| 1067 |
-
|
| 1068 |
-
// 处理 modalities -> responseModalities
|
| 1069 |
-
if (openaiRequest.modalities && Array.isArray(openaiRequest.modalities)) {
|
| 1070 |
-
const responseMods = [];
|
| 1071 |
-
for (const m of openaiRequest.modalities) {
|
| 1072 |
-
const mod = String(m).toLowerCase();
|
| 1073 |
-
if (mod === 'text') {
|
| 1074 |
-
responseMods.push('TEXT');
|
| 1075 |
-
} else if (mod === 'image') {
|
| 1076 |
-
responseMods.push('IMAGE');
|
| 1077 |
-
}
|
| 1078 |
-
}
|
| 1079 |
-
if (responseMods.length > 0) {
|
| 1080 |
-
geminiRequest.generationConfig = geminiRequest.generationConfig || {};
|
| 1081 |
-
geminiRequest.generationConfig.responseModalities = responseMods;
|
| 1082 |
-
}
|
| 1083 |
-
}
|
| 1084 |
-
|
| 1085 |
-
// 处理 image_config(OpenRouter 风格)
|
| 1086 |
-
if (openaiRequest.image_config) {
|
| 1087 |
-
const imgCfg = openaiRequest.image_config;
|
| 1088 |
-
if (imgCfg.aspect_ratio) {
|
| 1089 |
-
geminiRequest.generationConfig = geminiRequest.generationConfig || {};
|
| 1090 |
-
geminiRequest.generationConfig.imageConfig = geminiRequest.generationConfig.imageConfig || {};
|
| 1091 |
-
geminiRequest.generationConfig.imageConfig.aspectRatio = imgCfg.aspect_ratio;
|
| 1092 |
-
}
|
| 1093 |
-
if (imgCfg.image_size) {
|
| 1094 |
-
geminiRequest.generationConfig = geminiRequest.generationConfig || {};
|
| 1095 |
-
geminiRequest.generationConfig.imageConfig = geminiRequest.generationConfig.imageConfig || {};
|
| 1096 |
-
geminiRequest.generationConfig.imageConfig.imageSize = imgCfg.image_size;
|
| 1097 |
-
}
|
| 1098 |
-
}
|
| 1099 |
-
|
| 1100 |
-
// 处理 tools -> functionDeclarations
|
| 1101 |
-
if (openaiRequest.tools?.length) {
|
| 1102 |
-
const functionDeclarations = [];
|
| 1103 |
-
let hasGoogleSearch = false;
|
| 1104 |
-
|
| 1105 |
-
for (const t of openaiRequest.tools) {
|
| 1106 |
-
if (!t || typeof t !== 'object') continue;
|
| 1107 |
-
|
| 1108 |
-
if (t.type === 'function' && t.function) {
|
| 1109 |
-
const func = t.function;
|
| 1110 |
-
let fnDecl = {
|
| 1111 |
-
name: String(func.name || ''),
|
| 1112 |
-
description: String(func.description || '')
|
| 1113 |
-
};
|
| 1114 |
-
|
| 1115 |
-
// 处理 parameters -> parametersJsonSchema
|
| 1116 |
-
if (func.parameters) {
|
| 1117 |
-
fnDecl.parametersJsonSchema = cleanJsonSchema(func.parameters);
|
| 1118 |
-
} else {
|
| 1119 |
-
fnDecl.parametersJsonSchema = {
|
| 1120 |
-
type: 'object',
|
| 1121 |
-
properties: {}
|
| 1122 |
-
};
|
| 1123 |
-
}
|
| 1124 |
-
|
| 1125 |
-
functionDeclarations.push(fnDecl);
|
| 1126 |
-
} else if (t.name) {
|
| 1127 |
-
functionDeclarations.push({
|
| 1128 |
-
name: String(t.name),
|
| 1129 |
-
description: String(t.description || ''),
|
| 1130 |
-
parametersJsonSchema: cleanJsonSchema(t.input_schema || { type: 'object', properties: {} })
|
| 1131 |
-
});
|
| 1132 |
-
}
|
| 1133 |
-
|
| 1134 |
-
// 处�� google_search 工具
|
| 1135 |
-
if (t.google_search) {
|
| 1136 |
-
hasGoogleSearch = true;
|
| 1137 |
-
}
|
| 1138 |
-
}
|
| 1139 |
-
|
| 1140 |
-
if (functionDeclarations.length > 0 || hasGoogleSearch) {
|
| 1141 |
-
geminiRequest.tools = [{}];
|
| 1142 |
-
if (functionDeclarations.length > 0) {
|
| 1143 |
-
geminiRequest.tools[0].functionDeclarations = functionDeclarations;
|
| 1144 |
-
}
|
| 1145 |
-
if (hasGoogleSearch) {
|
| 1146 |
-
const googleSearchTool = openaiRequest.tools.find(t => t.google_search);
|
| 1147 |
-
geminiRequest.tools[0].googleSearch = googleSearchTool.google_search;
|
| 1148 |
-
}
|
| 1149 |
-
}
|
| 1150 |
-
}
|
| 1151 |
-
|
| 1152 |
-
// 处理 tool_choice
|
| 1153 |
-
if (openaiRequest.tool_choice) {
|
| 1154 |
-
geminiRequest.toolConfig = this.buildGeminiToolConfig(openaiRequest.tool_choice);
|
| 1155 |
-
}
|
| 1156 |
-
|
| 1157 |
-
// 构建 generationConfig
|
| 1158 |
-
const config = this.buildGeminiGenerationConfig(openaiRequest, model);
|
| 1159 |
-
if (Object.keys(config).length) {
|
| 1160 |
-
geminiRequest.generationConfig = {
|
| 1161 |
-
...config,
|
| 1162 |
-
...(geminiRequest.generationConfig || {})
|
| 1163 |
-
};
|
| 1164 |
-
}
|
| 1165 |
-
|
| 1166 |
-
// 添加默认安全设置
|
| 1167 |
-
geminiRequest.safetySettings = this.getDefaultSafetySettings();
|
| 1168 |
-
|
| 1169 |
-
return geminiRequest;
|
| 1170 |
-
}
|
| 1171 |
-
|
| 1172 |
-
/**
|
| 1173 |
-
* 检查模型是否支持 thinking
|
| 1174 |
-
*/
|
| 1175 |
-
modelSupportsThinking(model) {
|
| 1176 |
-
if (!model) return false;
|
| 1177 |
-
const m = model.toLowerCase();
|
| 1178 |
-
return m.includes('2.5') || m.includes('thinking') || m.includes('2.0-flash-thinking');
|
| 1179 |
-
}
|
| 1180 |
-
|
| 1181 |
-
/**
|
| 1182 |
-
* 检查是否是 Gemini 3 模型
|
| 1183 |
-
*/
|
| 1184 |
-
isGemini3Model(model) {
|
| 1185 |
-
if (!model) return false;
|
| 1186 |
-
const m = model.toLowerCase();
|
| 1187 |
-
return m.includes('gemini-3') || m.includes('gemini3');
|
| 1188 |
-
}
|
| 1189 |
-
|
| 1190 |
-
/**
|
| 1191 |
-
* 检查模型是否使用 thinking levels(而不是 budget)
|
| 1192 |
-
*/
|
| 1193 |
-
modelUsesThinkingLevels(model) {
|
| 1194 |
-
if (!model) return false;
|
| 1195 |
-
// Gemini 3 模型使用 levels,其他使用 budget
|
| 1196 |
-
return this.isGemini3Model(model);
|
| 1197 |
-
}
|
| 1198 |
-
|
| 1199 |
-
/**
|
| 1200 |
-
* 验证 Gemini 3 thinking level
|
| 1201 |
-
*/
|
| 1202 |
-
validateGemini3ThinkingLevel(model, effort) {
|
| 1203 |
-
const validLevels = ['low', 'medium', 'high'];
|
| 1204 |
-
if (validLevels.includes(effort)) {
|
| 1205 |
-
return effort.toUpperCase();
|
| 1206 |
-
}
|
| 1207 |
-
return null;
|
| 1208 |
-
}
|
| 1209 |
-
|
| 1210 |
-
/**
|
| 1211 |
-
* 将 reasoning_effort 转换为 Gemini thinkingConfig
|
| 1212 |
-
*/
|
| 1213 |
-
applyReasoningEffortToGemini(effort) {
|
| 1214 |
-
const effortToBudget = {
|
| 1215 |
-
'low': 1024,
|
| 1216 |
-
'medium': 8192,
|
| 1217 |
-
'high': 24576
|
| 1218 |
-
};
|
| 1219 |
-
const budget = effortToBudget[effort] || effortToBudget['medium'];
|
| 1220 |
-
return {
|
| 1221 |
-
thinkingBudget: budget,
|
| 1222 |
-
includeThoughts: true
|
| 1223 |
-
};
|
| 1224 |
-
}
|
| 1225 |
-
|
| 1226 |
-
/**
|
| 1227 |
-
* 获取默认安全设置
|
| 1228 |
-
*/
|
| 1229 |
-
getDefaultSafetySettings() {
|
| 1230 |
-
return [
|
| 1231 |
-
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
|
| 1232 |
-
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
|
| 1233 |
-
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
|
| 1234 |
-
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
|
| 1235 |
-
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
|
| 1236 |
-
];
|
| 1237 |
-
}
|
| 1238 |
-
|
| 1239 |
-
/**
|
| 1240 |
-
* 处理OpenAI内容到Gemini parts
|
| 1241 |
-
*/
|
| 1242 |
-
processOpenAIContentToGeminiParts(content) {
|
| 1243 |
-
if (!content) return [];
|
| 1244 |
-
if (typeof content === 'string') return [{ text: content }];
|
| 1245 |
-
|
| 1246 |
-
if (Array.isArray(content)) {
|
| 1247 |
-
const parts = [];
|
| 1248 |
-
|
| 1249 |
-
for (const item of content) {
|
| 1250 |
-
if (!item) continue;
|
| 1251 |
-
|
| 1252 |
-
if (item.type === 'text' && item.text) {
|
| 1253 |
-
parts.push({ text: item.text });
|
| 1254 |
-
} else if (item.type === 'image_url' && item.image_url) {
|
| 1255 |
-
const imageUrl = typeof item.image_url === 'string'
|
| 1256 |
-
? item.image_url
|
| 1257 |
-
: item.image_url.url;
|
| 1258 |
-
|
| 1259 |
-
if (imageUrl.startsWith('data:')) {
|
| 1260 |
-
const [header, data] = imageUrl.split(',');
|
| 1261 |
-
const mimeType = header.match(/data:([^;]+)/)?.[1] || 'image/jpeg';
|
| 1262 |
-
parts.push({ inlineData: { mimeType, data } });
|
| 1263 |
-
} else {
|
| 1264 |
-
parts.push({
|
| 1265 |
-
fileData: { mimeType: 'image/jpeg', fileUri: imageUrl }
|
| 1266 |
-
});
|
| 1267 |
-
}
|
| 1268 |
-
}
|
| 1269 |
-
}
|
| 1270 |
-
|
| 1271 |
-
return parts;
|
| 1272 |
-
}
|
| 1273 |
-
|
| 1274 |
-
return [];
|
| 1275 |
-
}
|
| 1276 |
-
|
| 1277 |
-
/**
|
| 1278 |
-
* 构建Gemini工具配置
|
| 1279 |
-
*/
|
| 1280 |
-
buildGeminiToolConfig(toolChoice) {
|
| 1281 |
-
if (typeof toolChoice === 'string' && ['none', 'auto'].includes(toolChoice)) {
|
| 1282 |
-
return { functionCallingConfig: { mode: toolChoice.toUpperCase() } };
|
| 1283 |
-
}
|
| 1284 |
-
if (typeof toolChoice === 'object' && toolChoice.function) {
|
| 1285 |
-
return { functionCallingConfig: { mode: 'ANY', allowedFunctionNames: [toolChoice.function.name] } };
|
| 1286 |
-
}
|
| 1287 |
-
return null;
|
| 1288 |
-
}
|
| 1289 |
-
|
| 1290 |
-
/**
|
| 1291 |
-
* 构建Gemini生成配置
|
| 1292 |
-
*/
|
| 1293 |
-
buildGeminiGenerationConfig({ temperature, max_tokens, top_p, stop, tools, response_format }, model) {
|
| 1294 |
-
const config = {};
|
| 1295 |
-
config.temperature = checkAndAssignOrDefault(temperature, GEMINI_DEFAULT_TEMPERATURE);
|
| 1296 |
-
config.maxOutputTokens = checkAndAssignOrDefault(max_tokens, GEMINI_DEFAULT_MAX_TOKENS);
|
| 1297 |
-
config.topP = checkAndAssignOrDefault(top_p, GEMINI_DEFAULT_TOP_P);
|
| 1298 |
-
if (stop !== undefined) config.stopSequences = Array.isArray(stop) ? stop : [stop];
|
| 1299 |
-
|
| 1300 |
-
// Handle response_format
|
| 1301 |
-
if (response_format) {
|
| 1302 |
-
if (response_format.type === 'json_object') {
|
| 1303 |
-
config.responseMimeType = 'application/json';
|
| 1304 |
-
} else if (response_format.type === 'json_schema' && response_format.json_schema) {
|
| 1305 |
-
config.responseMimeType = 'application/json';
|
| 1306 |
-
if (response_format.json_schema.schema) {
|
| 1307 |
-
config.responseSchema = response_format.json_schema.schema;
|
| 1308 |
-
}
|
| 1309 |
-
}
|
| 1310 |
-
}
|
| 1311 |
-
|
| 1312 |
-
// Gemini 2.5 and thinking models require responseModalities: ["TEXT"]
|
| 1313 |
-
// But this parameter cannot be added when using tools (causes 400 error)
|
| 1314 |
-
const hasTools = tools && Array.isArray(tools) && tools.length > 0;
|
| 1315 |
-
if (!hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) {
|
| 1316 |
-
logger.info(`[OpenAI->Gemini] Adding responseModalities: ["TEXT"] for model: ${model}`);
|
| 1317 |
-
config.responseModalities = ["TEXT"];
|
| 1318 |
-
} else if (hasTools && model && (model.includes('2.5') || model.includes('thinking') || model.includes('2.0-flash-thinking'))) {
|
| 1319 |
-
logger.info(`[OpenAI->Gemini] Skipping responseModalities for model ${model} because tools are present`);
|
| 1320 |
-
}
|
| 1321 |
-
|
| 1322 |
-
return config;
|
| 1323 |
-
}
|
| 1324 |
-
/**
|
| 1325 |
-
* 将OpenAI响应转换为Gemini响应格式
|
| 1326 |
-
*/
|
| 1327 |
-
toGeminiResponse(openaiResponse, model) {
|
| 1328 |
-
if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) {
|
| 1329 |
-
return { candidates: [], usageMetadata: {} };
|
| 1330 |
-
}
|
| 1331 |
-
|
| 1332 |
-
const choice = openaiResponse.choices[0];
|
| 1333 |
-
const message = choice.message || {};
|
| 1334 |
-
const parts = [];
|
| 1335 |
-
|
| 1336 |
-
// 处理文本内容
|
| 1337 |
-
if (message.content) {
|
| 1338 |
-
parts.push({ text: message.content });
|
| 1339 |
-
}
|
| 1340 |
-
|
| 1341 |
-
// 处理工具调用
|
| 1342 |
-
if (message.tool_calls && message.tool_calls.length > 0) {
|
| 1343 |
-
for (const toolCall of message.tool_calls) {
|
| 1344 |
-
if (toolCall.type === 'function') {
|
| 1345 |
-
parts.push({
|
| 1346 |
-
functionCall: {
|
| 1347 |
-
name: toolCall.function.name,
|
| 1348 |
-
args: typeof toolCall.function.arguments === 'string'
|
| 1349 |
-
? JSON.parse(toolCall.function.arguments)
|
| 1350 |
-
: toolCall.function.arguments
|
| 1351 |
-
}
|
| 1352 |
-
});
|
| 1353 |
-
}
|
| 1354 |
-
}
|
| 1355 |
-
}
|
| 1356 |
-
|
| 1357 |
-
// 映射finish_reason
|
| 1358 |
-
const finishReasonMap = {
|
| 1359 |
-
'stop': 'STOP',
|
| 1360 |
-
'length': 'MAX_TOKENS',
|
| 1361 |
-
'tool_calls': 'STOP',
|
| 1362 |
-
'content_filter': 'SAFETY'
|
| 1363 |
-
};
|
| 1364 |
-
|
| 1365 |
-
return {
|
| 1366 |
-
candidates: [{
|
| 1367 |
-
content: {
|
| 1368 |
-
role: 'model',
|
| 1369 |
-
parts: parts
|
| 1370 |
-
},
|
| 1371 |
-
finishReason: finishReasonMap[choice.finish_reason] || 'STOP'
|
| 1372 |
-
}],
|
| 1373 |
-
usageMetadata: openaiResponse.usage ? {
|
| 1374 |
-
promptTokenCount: openaiResponse.usage.prompt_tokens || 0,
|
| 1375 |
-
candidatesTokenCount: openaiResponse.usage.completion_tokens || 0,
|
| 1376 |
-
totalTokenCount: openaiResponse.usage.total_tokens || 0,
|
| 1377 |
-
cachedContentTokenCount: openaiResponse.usage.prompt_tokens_details?.cached_tokens || 0,
|
| 1378 |
-
promptTokensDetails: [{
|
| 1379 |
-
modality: "TEXT",
|
| 1380 |
-
tokenCount: openaiResponse.usage.prompt_tokens || 0
|
| 1381 |
-
}],
|
| 1382 |
-
candidatesTokensDetails: [{
|
| 1383 |
-
modality: "TEXT",
|
| 1384 |
-
tokenCount: openaiResponse.usage.completion_tokens || 0
|
| 1385 |
-
}],
|
| 1386 |
-
thoughtsTokenCount: openaiResponse.usage.completion_tokens_details?.reasoning_tokens || 0
|
| 1387 |
-
} : {}
|
| 1388 |
-
};
|
| 1389 |
-
}
|
| 1390 |
-
|
| 1391 |
-
/**
|
| 1392 |
-
* 将OpenAI流式响应块转换为Gemini流式响应格式
|
| 1393 |
-
*/
|
| 1394 |
-
toGeminiStreamChunk(openaiChunk, model) {
|
| 1395 |
-
if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) {
|
| 1396 |
-
return null;
|
| 1397 |
-
}
|
| 1398 |
-
|
| 1399 |
-
const choice = openaiChunk.choices[0];
|
| 1400 |
-
const delta = choice.delta || {};
|
| 1401 |
-
const parts = [];
|
| 1402 |
-
|
| 1403 |
-
// 处理文本内容
|
| 1404 |
-
if (delta.content) {
|
| 1405 |
-
parts.push({ text: delta.content });
|
| 1406 |
-
}
|
| 1407 |
-
|
| 1408 |
-
// 处理工具调用
|
| 1409 |
-
if (delta.tool_calls && delta.tool_calls.length > 0) {
|
| 1410 |
-
for (const toolCall of delta.tool_calls) {
|
| 1411 |
-
if (toolCall.function) {
|
| 1412 |
-
const functionCall = {
|
| 1413 |
-
name: toolCall.function.name || '',
|
| 1414 |
-
args: {}
|
| 1415 |
-
};
|
| 1416 |
-
|
| 1417 |
-
if (toolCall.function.arguments) {
|
| 1418 |
-
try {
|
| 1419 |
-
functionCall.args = typeof toolCall.function.arguments === 'string'
|
| 1420 |
-
? JSON.parse(toolCall.function.arguments)
|
| 1421 |
-
: toolCall.function.arguments;
|
| 1422 |
-
} catch (e) {
|
| 1423 |
-
// 部分参数,保持为字符串
|
| 1424 |
-
functionCall.args = { partial: toolCall.function.arguments };
|
| 1425 |
-
}
|
| 1426 |
-
}
|
| 1427 |
-
|
| 1428 |
-
parts.push({ functionCall });
|
| 1429 |
-
}
|
| 1430 |
-
}
|
| 1431 |
-
}
|
| 1432 |
-
|
| 1433 |
-
const result = {
|
| 1434 |
-
candidates: [{
|
| 1435 |
-
content: {
|
| 1436 |
-
role: 'model',
|
| 1437 |
-
parts: parts
|
| 1438 |
-
}
|
| 1439 |
-
}]
|
| 1440 |
-
};
|
| 1441 |
-
|
| 1442 |
-
return result;
|
| 1443 |
-
}
|
| 1444 |
-
|
| 1445 |
-
// =========================================================================
|
| 1446 |
-
// OpenAI -> Grok 转换
|
| 1447 |
-
// =========================================================================
|
| 1448 |
-
|
| 1449 |
-
/**
|
| 1450 |
-
* OpenAI请求 -> Grok请求
|
| 1451 |
-
*/
|
| 1452 |
-
toGrokRequest(openaiRequest) {
|
| 1453 |
-
// 我们需要 GrokConverter 来处理复杂的仿真逻辑
|
| 1454 |
-
const { ConverterFactory } = (import.meta.url ? { ConverterFactory: null } : { ConverterFactory: null }); // 这是一个占位,实际会从全局获取
|
| 1455 |
-
|
| 1456 |
-
// 直接返回结构化数据,由 GrokApiService.buildPayload 最终处理
|
| 1457 |
-
// 这样可以保留原始的 messages, tools, tool_choice 以进行高质量仿真
|
| 1458 |
-
return {
|
| 1459 |
-
...openaiRequest,
|
| 1460 |
-
// 保持原始结构以便 GrokApiService 处理
|
| 1461 |
-
_isConverted: true
|
| 1462 |
-
};
|
| 1463 |
-
}
|
| 1464 |
-
|
| 1465 |
-
/**
|
| 1466 |
-
* OpenAI响应 -> Grok响应(通常不使用)
|
| 1467 |
-
*/
|
| 1468 |
-
toGrokResponse(openaiResponse, model) {
|
| 1469 |
-
return openaiResponse;
|
| 1470 |
-
}
|
| 1471 |
-
|
| 1472 |
-
/**
|
| 1473 |
-
* OpenAI流式响应 -> Grok流式响应(通常不使用)
|
| 1474 |
-
*/
|
| 1475 |
-
toGrokStreamChunk(openaiChunk, model) {
|
| 1476 |
-
return openaiChunk;
|
| 1477 |
-
}
|
| 1478 |
-
|
| 1479 |
-
/**
|
| 1480 |
-
* OpenAI模型列表 -> Grok模型列表(通常不使用)
|
| 1481 |
-
*/
|
| 1482 |
-
toGrokModelList(openaiModels) {
|
| 1483 |
-
return openaiModels;
|
| 1484 |
-
}
|
| 1485 |
-
|
| 1486 |
-
/**
|
| 1487 |
-
* 将 OpenAI 模型列表转换为 Gemini 模型列表
|
| 1488 |
-
*/
|
| 1489 |
-
toCodexRequest(openaiRequest) {
|
| 1490 |
-
return this.codexConverter.toOpenAIRequestToCodexRequest(openaiRequest);
|
| 1491 |
-
}
|
| 1492 |
-
|
| 1493 |
-
/**
|
| 1494 |
-
* 将OpenAI请求转换为OpenAI Responses格式
|
| 1495 |
-
*/
|
| 1496 |
-
toOpenAIResponsesRequest(openaiRequest) {
|
| 1497 |
-
const responsesRequest = {
|
| 1498 |
-
model: openaiRequest.model,
|
| 1499 |
-
instructions: '',
|
| 1500 |
-
input: [],
|
| 1501 |
-
stream: openaiRequest.stream || false,
|
| 1502 |
-
max_output_tokens: openaiRequest.max_tokens,
|
| 1503 |
-
temperature: openaiRequest.temperature,
|
| 1504 |
-
top_p: openaiRequest.top_p,
|
| 1505 |
-
parallel_tool_calls: openaiRequest.parallel_tool_calls,
|
| 1506 |
-
tool_choice: openaiRequest.tool_choice
|
| 1507 |
-
};
|
| 1508 |
-
|
| 1509 |
-
const { systemInstruction, nonSystemMessages } = extractSystemMessages(openaiRequest.messages || []);
|
| 1510 |
-
|
| 1511 |
-
if (systemInstruction) {
|
| 1512 |
-
responsesRequest.instructions = extractText(systemInstruction.parts[0].text);
|
| 1513 |
-
}
|
| 1514 |
-
|
| 1515 |
-
if (openaiRequest.reasoning_effort) {
|
| 1516 |
-
responsesRequest.reasoning = {
|
| 1517 |
-
effort: openaiRequest.reasoning_effort
|
| 1518 |
-
};
|
| 1519 |
-
}
|
| 1520 |
-
|
| 1521 |
-
// 转换messages到input
|
| 1522 |
-
for (const msg of nonSystemMessages) {
|
| 1523 |
-
if (msg.role === 'tool') {
|
| 1524 |
-
responsesRequest.input.push({
|
| 1525 |
-
type: 'function_call_output',
|
| 1526 |
-
call_id: msg.tool_call_id,
|
| 1527 |
-
output: msg.content
|
| 1528 |
-
});
|
| 1529 |
-
} else if (msg.role === 'assistant' && msg.tool_calls?.length) {
|
| 1530 |
-
for (const tc of msg.tool_calls) {
|
| 1531 |
-
responsesRequest.input.push({
|
| 1532 |
-
type: 'function_call',
|
| 1533 |
-
call_id: tc.id,
|
| 1534 |
-
name: tc.function.name,
|
| 1535 |
-
arguments: tc.function.arguments
|
| 1536 |
-
});
|
| 1537 |
-
}
|
| 1538 |
-
} else {
|
| 1539 |
-
let content = [];
|
| 1540 |
-
if (typeof msg.content === 'string') {
|
| 1541 |
-
content.push({
|
| 1542 |
-
type: msg.role === 'assistant' ? 'output_text' : 'input_text',
|
| 1543 |
-
text: msg.content
|
| 1544 |
-
});
|
| 1545 |
-
} else if (Array.isArray(msg.content)) {
|
| 1546 |
-
msg.content.forEach(c => {
|
| 1547 |
-
if (c.type === 'text') {
|
| 1548 |
-
content.push({
|
| 1549 |
-
type: msg.role === 'assistant' ? 'output_text' : 'input_text',
|
| 1550 |
-
text: c.text
|
| 1551 |
-
});
|
| 1552 |
-
} else if (c.type === 'image_url') {
|
| 1553 |
-
content.push({
|
| 1554 |
-
type: 'input_image',
|
| 1555 |
-
image_url: c.image_url
|
| 1556 |
-
});
|
| 1557 |
-
}
|
| 1558 |
-
});
|
| 1559 |
-
}
|
| 1560 |
-
|
| 1561 |
-
if (content.length > 0) {
|
| 1562 |
-
responsesRequest.input.push({
|
| 1563 |
-
type: 'message',
|
| 1564 |
-
role: msg.role,
|
| 1565 |
-
content: content
|
| 1566 |
-
});
|
| 1567 |
-
}
|
| 1568 |
-
}
|
| 1569 |
-
}
|
| 1570 |
-
|
| 1571 |
-
// 处理工具
|
| 1572 |
-
if (openaiRequest.tools) {
|
| 1573 |
-
responsesRequest.tools = openaiRequest.tools.map(t => ({
|
| 1574 |
-
type: t.type || 'function',
|
| 1575 |
-
name: t.function?.name,
|
| 1576 |
-
description: t.function?.description,
|
| 1577 |
-
parameters: t.function?.parameters
|
| 1578 |
-
}));
|
| 1579 |
-
}
|
| 1580 |
-
|
| 1581 |
-
return responsesRequest;
|
| 1582 |
-
}
|
| 1583 |
-
|
| 1584 |
-
/**
|
| 1585 |
-
* 将OpenAI响应转换为OpenAI Responses格式
|
| 1586 |
-
*/
|
| 1587 |
-
toOpenAIResponsesResponse(openaiResponse, model) {
|
| 1588 |
-
if (!openaiResponse || !openaiResponse.choices || !openaiResponse.choices[0]) {
|
| 1589 |
-
return {
|
| 1590 |
-
id: `resp_${Date.now()}`,
|
| 1591 |
-
object: 'response',
|
| 1592 |
-
created_at: Math.floor(Date.now() / 1000),
|
| 1593 |
-
status: 'completed',
|
| 1594 |
-
model: model || 'unknown',
|
| 1595 |
-
output: [],
|
| 1596 |
-
usage: {
|
| 1597 |
-
input_tokens: 0,
|
| 1598 |
-
output_tokens: 0,
|
| 1599 |
-
total_tokens: 0
|
| 1600 |
-
}
|
| 1601 |
-
};
|
| 1602 |
-
}
|
| 1603 |
-
|
| 1604 |
-
const choice = openaiResponse.choices[0];
|
| 1605 |
-
const message = choice.message || {};
|
| 1606 |
-
const output = [];
|
| 1607 |
-
|
| 1608 |
-
// 构建message输出
|
| 1609 |
-
const messageContent = [];
|
| 1610 |
-
if (message.content) {
|
| 1611 |
-
messageContent.push({
|
| 1612 |
-
type: 'output_text',
|
| 1613 |
-
text: message.content
|
| 1614 |
-
});
|
| 1615 |
-
}
|
| 1616 |
-
|
| 1617 |
-
output.push({
|
| 1618 |
-
type: 'message',
|
| 1619 |
-
id: `msg_${Date.now()}`,
|
| 1620 |
-
status: 'completed',
|
| 1621 |
-
role: 'assistant',
|
| 1622 |
-
content: messageContent
|
| 1623 |
-
});
|
| 1624 |
-
|
| 1625 |
-
return {
|
| 1626 |
-
id: openaiResponse.id || `resp_${Date.now()}`,
|
| 1627 |
-
object: 'response',
|
| 1628 |
-
created_at: openaiResponse.created || Math.floor(Date.now() / 1000),
|
| 1629 |
-
status: choice.finish_reason === 'stop' ? 'completed' : 'in_progress',
|
| 1630 |
-
model: model || openaiResponse.model || 'unknown',
|
| 1631 |
-
output: output,
|
| 1632 |
-
usage: openaiResponse.usage ? {
|
| 1633 |
-
input_tokens: openaiResponse.usage.prompt_tokens || 0,
|
| 1634 |
-
input_tokens_details: {
|
| 1635 |
-
cached_tokens: openaiResponse.usage.prompt_tokens_details?.cached_tokens || 0
|
| 1636 |
-
},
|
| 1637 |
-
output_tokens: openaiResponse.usage.completion_tokens || 0,
|
| 1638 |
-
output_tokens_details: {
|
| 1639 |
-
reasoning_tokens: openaiResponse.usage.completion_tokens_details?.reasoning_tokens || 0
|
| 1640 |
-
},
|
| 1641 |
-
total_tokens: openaiResponse.usage.total_tokens || 0
|
| 1642 |
-
} : {
|
| 1643 |
-
input_tokens: 0,
|
| 1644 |
-
input_tokens_details: {
|
| 1645 |
-
cached_tokens: 0
|
| 1646 |
-
},
|
| 1647 |
-
output_tokens: 0,
|
| 1648 |
-
output_tokens_details: {
|
| 1649 |
-
reasoning_tokens: 0
|
| 1650 |
-
},
|
| 1651 |
-
total_tokens: 0
|
| 1652 |
-
}
|
| 1653 |
-
};
|
| 1654 |
-
}
|
| 1655 |
-
|
| 1656 |
-
/**
|
| 1657 |
-
* 将OpenAI流式响应转换为OpenAI Responses流式格式
|
| 1658 |
-
* 参考 ClaudeConverter.toOpenAIResponsesStreamChunk 的实现逻辑
|
| 1659 |
-
*/
|
| 1660 |
-
toOpenAIResponsesStreamChunk(openaiChunk, model, requestId = null) {
|
| 1661 |
-
if (!openaiChunk || !openaiChunk.choices || !openaiChunk.choices[0]) {
|
| 1662 |
-
return [];
|
| 1663 |
-
}
|
| 1664 |
-
|
| 1665 |
-
const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
|
| 1666 |
-
const choice = openaiChunk.choices[0];
|
| 1667 |
-
const delta = choice.delta || {};
|
| 1668 |
-
const events = [];
|
| 1669 |
-
|
| 1670 |
-
// 第一个chunk - role为assistant时调用 getOpenAIResponsesStreamChunkBegin
|
| 1671 |
-
if (delta.role === 'assistant') {
|
| 1672 |
-
events.push(
|
| 1673 |
-
generateResponseCreated(responseId, model || openaiChunk.model || 'unknown'),
|
| 1674 |
-
generateResponseInProgress(responseId),
|
| 1675 |
-
generateOutputItemAdded(responseId),
|
| 1676 |
-
generateContentPartAdded(responseId)
|
| 1677 |
-
);
|
| 1678 |
-
}
|
| 1679 |
-
|
| 1680 |
-
// 处理 reasoning_content(推理内容)
|
| 1681 |
-
if (delta.reasoning_content) {
|
| 1682 |
-
events.push({
|
| 1683 |
-
delta: delta.reasoning_content,
|
| 1684 |
-
item_id: `thinking_${uuidv4().replace(/-/g, '')}`,
|
| 1685 |
-
output_index: 0,
|
| 1686 |
-
sequence_number: 3,
|
| 1687 |
-
type: "response.reasoning_summary_text.delta"
|
| 1688 |
-
});
|
| 1689 |
-
}
|
| 1690 |
-
|
| 1691 |
-
// 处理 tool_calls(工具调用)
|
| 1692 |
-
if (delta.tool_calls && delta.tool_calls.length > 0) {
|
| 1693 |
-
for (const toolCall of delta.tool_calls) {
|
| 1694 |
-
const outputIndex = toolCall.index || 0;
|
| 1695 |
-
|
| 1696 |
-
// 如果有 function.name,说明是工具调用开始
|
| 1697 |
-
if (toolCall.function && toolCall.function.name) {
|
| 1698 |
-
events.push({
|
| 1699 |
-
item: {
|
| 1700 |
-
id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`,
|
| 1701 |
-
type: "function_call",
|
| 1702 |
-
name: toolCall.function.name,
|
| 1703 |
-
arguments: "",
|
| 1704 |
-
status: "in_progress"
|
| 1705 |
-
},
|
| 1706 |
-
output_index: outputIndex,
|
| 1707 |
-
sequence_number: 2,
|
| 1708 |
-
type: "response.output_item.added"
|
| 1709 |
-
});
|
| 1710 |
-
}
|
| 1711 |
-
|
| 1712 |
-
// 如果有 function.arguments,说明是参数增量
|
| 1713 |
-
if (toolCall.function && toolCall.function.arguments) {
|
| 1714 |
-
events.push({
|
| 1715 |
-
delta: toolCall.function.arguments,
|
| 1716 |
-
item_id: toolCall.id || `call_${uuidv4().replace(/-/g, '')}`,
|
| 1717 |
-
output_index: outputIndex,
|
| 1718 |
-
sequence_number: 3,
|
| 1719 |
-
type: "response.custom_tool_call_input.delta"
|
| 1720 |
-
});
|
| 1721 |
-
}
|
| 1722 |
-
}
|
| 1723 |
-
}
|
| 1724 |
-
|
| 1725 |
-
// 处理普通文本内容
|
| 1726 |
-
if (delta.content) {
|
| 1727 |
-
events.push({
|
| 1728 |
-
delta: delta.content,
|
| 1729 |
-
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
|
| 1730 |
-
output_index: 0,
|
| 1731 |
-
sequence_number: 3,
|
| 1732 |
-
type: "response.output_text.delta"
|
| 1733 |
-
});
|
| 1734 |
-
}
|
| 1735 |
-
|
| 1736 |
-
// 处理完成状态 - 调用 getOpenAIResponsesStreamChunkEnd
|
| 1737 |
-
if (choice.finish_reason) {
|
| 1738 |
-
events.push(
|
| 1739 |
-
generateOutputTextDone(responseId),
|
| 1740 |
-
generateContentPartDone(responseId),
|
| 1741 |
-
generateOutputItemDone(responseId),
|
| 1742 |
-
generateResponseCompleted(responseId)
|
| 1743 |
-
);
|
| 1744 |
-
|
| 1745 |
-
// 如果有 usage 信息,更新最后一个事件
|
| 1746 |
-
if (openaiChunk.usage && events.length > 0) {
|
| 1747 |
-
const lastEvent = events[events.length - 1];
|
| 1748 |
-
if (lastEvent.response) {
|
| 1749 |
-
lastEvent.response.usage = {
|
| 1750 |
-
input_tokens: openaiChunk.usage.prompt_tokens || 0,
|
| 1751 |
-
input_tokens_details: {
|
| 1752 |
-
cached_tokens: openaiChunk.usage.prompt_tokens_details?.cached_tokens || 0
|
| 1753 |
-
},
|
| 1754 |
-
output_tokens: openaiChunk.usage.completion_tokens || 0,
|
| 1755 |
-
output_tokens_details: {
|
| 1756 |
-
reasoning_tokens: openaiChunk.usage.completion_tokens_details?.reasoning_tokens || 0
|
| 1757 |
-
},
|
| 1758 |
-
total_tokens: openaiChunk.usage.total_tokens || 0
|
| 1759 |
-
};
|
| 1760 |
-
}
|
| 1761 |
-
}
|
| 1762 |
-
}
|
| 1763 |
-
|
| 1764 |
-
return events;
|
| 1765 |
-
}
|
| 1766 |
-
|
| 1767 |
-
}
|
| 1768 |
-
|
| 1769 |
-
export default OpenAIConverter;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/strategies/OpenAIResponsesConverter.js
DELETED
|
@@ -1,1032 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* OpenAI Responses API 转换器
|
| 3 |
-
* 处理 OpenAI Responses API 格式与其他协议之间的转换
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
-
import { BaseConverter } from '../BaseConverter.js';
|
| 8 |
-
import { CodexConverter } from './CodexConverter.js';
|
| 9 |
-
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
| 10 |
-
import {
|
| 11 |
-
extractAndProcessSystemMessages as extractSystemMessages,
|
| 12 |
-
extractTextFromMessageContent as extractText,
|
| 13 |
-
CLAUDE_DEFAULT_MAX_TOKENS,
|
| 14 |
-
GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
|
| 15 |
-
GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT
|
| 16 |
-
} from '../utils.js';
|
| 17 |
-
import {
|
| 18 |
-
generateResponseCreated,
|
| 19 |
-
generateResponseInProgress,
|
| 20 |
-
generateOutputItemAdded,
|
| 21 |
-
generateContentPartAdded,
|
| 22 |
-
generateOutputTextDone,
|
| 23 |
-
generateContentPartDone,
|
| 24 |
-
generateOutputItemDone,
|
| 25 |
-
generateResponseCompleted
|
| 26 |
-
} from '../../providers/openai/openai-responses-core.mjs';
|
| 27 |
-
|
| 28 |
-
/**
|
| 29 |
-
* OpenAI Responses API 转换器类
|
| 30 |
-
* 支持 OpenAI Responses 格式与 OpenAI、Claude、Gemini 之间的转换
|
| 31 |
-
*/
|
| 32 |
-
export class OpenAIResponsesConverter extends BaseConverter {
|
| 33 |
-
constructor() {
|
| 34 |
-
super(MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES);
|
| 35 |
-
this.codexConverter = new CodexConverter();
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
// =============================================================================
|
| 39 |
-
// 请求转换
|
| 40 |
-
// =============================================================================
|
| 41 |
-
|
| 42 |
-
/**
|
| 43 |
-
* 转换请求到目标协议
|
| 44 |
-
*/
|
| 45 |
-
convertRequest(data, toProtocol) {
|
| 46 |
-
switch (toProtocol) {
|
| 47 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 48 |
-
return this.toOpenAIRequest(data);
|
| 49 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 50 |
-
return this.toClaudeRequest(data);
|
| 51 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 52 |
-
return this.toGeminiRequest(data);
|
| 53 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 54 |
-
return this.toCodexRequest(data);
|
| 55 |
-
case MODEL_PROTOCOL_PREFIX.GROK:
|
| 56 |
-
return this.toGrokRequest(data);
|
| 57 |
-
default:
|
| 58 |
-
throw new Error(`Unsupported target protocol: ${toProtocol}`);
|
| 59 |
-
}
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
/**
|
| 63 |
-
* 转换响应到目标协议
|
| 64 |
-
*/
|
| 65 |
-
convertResponse(data, toProtocol, model) {
|
| 66 |
-
switch (toProtocol) {
|
| 67 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 68 |
-
return this.toOpenAIResponse(data, model);
|
| 69 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 70 |
-
return this.toClaudeResponse(data, model);
|
| 71 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 72 |
-
return this.toGeminiResponse(data, model);
|
| 73 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 74 |
-
return this.toCodexResponse(data, model);
|
| 75 |
-
default:
|
| 76 |
-
throw new Error(`Unsupported target protocol: ${toProtocol}`);
|
| 77 |
-
}
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
/**
|
| 81 |
-
* 转换流式响应块到目标协议
|
| 82 |
-
*/
|
| 83 |
-
convertStreamChunk(chunk, toProtocol, model) {
|
| 84 |
-
switch (toProtocol) {
|
| 85 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 86 |
-
return this.toOpenAIStreamChunk(chunk, model);
|
| 87 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 88 |
-
return this.toClaudeStreamChunk(chunk, model);
|
| 89 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 90 |
-
return this.toGeminiStreamChunk(chunk, model);
|
| 91 |
-
case MODEL_PROTOCOL_PREFIX.CODEX:
|
| 92 |
-
return this.toCodexStreamChunk(chunk, model);
|
| 93 |
-
default:
|
| 94 |
-
throw new Error(`Unsupported target protocol: ${toProtocol}`);
|
| 95 |
-
}
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
/**
|
| 99 |
-
* 转换模型列表到目标协议
|
| 100 |
-
*/
|
| 101 |
-
convertModelList(data, targetProtocol) {
|
| 102 |
-
switch (targetProtocol) {
|
| 103 |
-
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
| 104 |
-
return this.toOpenAIModelList(data);
|
| 105 |
-
case MODEL_PROTOCOL_PREFIX.CLAUDE:
|
| 106 |
-
return this.toClaudeModelList(data);
|
| 107 |
-
case MODEL_PROTOCOL_PREFIX.GEMINI:
|
| 108 |
-
return this.toGeminiModelList(data);
|
| 109 |
-
default:
|
| 110 |
-
return data;
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
// =============================================================================
|
| 115 |
-
// 转换到 OpenAI 格式
|
| 116 |
-
// =============================================================================
|
| 117 |
-
|
| 118 |
-
/**
|
| 119 |
-
* 将 OpenAI Responses 请求转换为标准 OpenAI 请求
|
| 120 |
-
*/
|
| 121 |
-
toOpenAIRequest(responsesRequest) {
|
| 122 |
-
const openaiRequest = {
|
| 123 |
-
model: responsesRequest.model,
|
| 124 |
-
messages: [],
|
| 125 |
-
stream: responsesRequest.stream || false
|
| 126 |
-
};
|
| 127 |
-
|
| 128 |
-
// 复制其他参数
|
| 129 |
-
if (responsesRequest.temperature !== undefined) {
|
| 130 |
-
openaiRequest.temperature = responsesRequest.temperature;
|
| 131 |
-
}
|
| 132 |
-
if (responsesRequest.max_output_tokens !== undefined) {
|
| 133 |
-
openaiRequest.max_tokens = responsesRequest.max_output_tokens;
|
| 134 |
-
} else if (responsesRequest.max_tokens !== undefined) {
|
| 135 |
-
openaiRequest.max_tokens = responsesRequest.max_tokens;
|
| 136 |
-
}
|
| 137 |
-
if (responsesRequest.top_p !== undefined) {
|
| 138 |
-
openaiRequest.top_p = responsesRequest.top_p;
|
| 139 |
-
}
|
| 140 |
-
if (responsesRequest.parallel_tool_calls !== undefined) {
|
| 141 |
-
openaiRequest.parallel_tool_calls = responsesRequest.parallel_tool_calls;
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
// OpenAI Responses API 使用 instructions 和 input 字段
|
| 145 |
-
// 需要转换为标准的 messages 格式
|
| 146 |
-
if (responsesRequest.instructions) {
|
| 147 |
-
// instructions 作为系统消息
|
| 148 |
-
openaiRequest.messages.push({
|
| 149 |
-
role: 'system',
|
| 150 |
-
content: responsesRequest.instructions
|
| 151 |
-
});
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
// input 包含用户消息和历史对话
|
| 155 |
-
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
|
| 156 |
-
responsesRequest.input.forEach(item => {
|
| 157 |
-
const itemType = item.type || (item.role ? 'message' : '');
|
| 158 |
-
|
| 159 |
-
switch (itemType) {
|
| 160 |
-
case 'message':
|
| 161 |
-
// 提取消息内容
|
| 162 |
-
let content = '';
|
| 163 |
-
if (Array.isArray(item.content)) {
|
| 164 |
-
content = item.content
|
| 165 |
-
.filter(c => c.type === 'input_text' || c.type === 'output_text')
|
| 166 |
-
.map(c => c.text)
|
| 167 |
-
.join('\n');
|
| 168 |
-
} else if (typeof item.content === 'string') {
|
| 169 |
-
content = item.content;
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
if (content || (item.role === 'assistant' || item.role === 'developer')) {
|
| 173 |
-
openaiRequest.messages.push({
|
| 174 |
-
role: item.role === 'developer' ? 'assistant' : item.role,
|
| 175 |
-
content: content
|
| 176 |
-
});
|
| 177 |
-
}
|
| 178 |
-
break;
|
| 179 |
-
|
| 180 |
-
case 'function_call':
|
| 181 |
-
openaiRequest.messages.push({
|
| 182 |
-
role: 'assistant',
|
| 183 |
-
tool_calls: [{
|
| 184 |
-
id: item.call_id,
|
| 185 |
-
type: 'function',
|
| 186 |
-
function: {
|
| 187 |
-
name: item.name,
|
| 188 |
-
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments)
|
| 189 |
-
}
|
| 190 |
-
}]
|
| 191 |
-
});
|
| 192 |
-
break;
|
| 193 |
-
|
| 194 |
-
case 'function_call_output':
|
| 195 |
-
openaiRequest.messages.push({
|
| 196 |
-
role: 'tool',
|
| 197 |
-
tool_call_id: item.call_id,
|
| 198 |
-
content: item.output
|
| 199 |
-
});
|
| 200 |
-
break;
|
| 201 |
-
}
|
| 202 |
-
});
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
// 如果有标准的 messages 字段,也支持
|
| 206 |
-
if (responsesRequest.messages && Array.isArray(responsesRequest.messages)) {
|
| 207 |
-
responsesRequest.messages.forEach(msg => {
|
| 208 |
-
openaiRequest.messages.push({
|
| 209 |
-
role: msg.role,
|
| 210 |
-
content: msg.content
|
| 211 |
-
});
|
| 212 |
-
});
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
// 处理工具
|
| 216 |
-
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
| 217 |
-
openaiRequest.tools = responsesRequest.tools
|
| 218 |
-
.map(tool => {
|
| 219 |
-
if (tool.type && tool.type !== 'function') {
|
| 220 |
-
return null;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
const name = tool.name || (tool.function && tool.function.name);
|
| 224 |
-
const description = tool.description || (tool.function && tool.function.description);
|
| 225 |
-
const parameters = tool.parameters || (tool.function && tool.function.parameters) || tool.parametersJsonSchema || { type: 'object', properties: {} };
|
| 226 |
-
|
| 227 |
-
// 如果没有名称,则该工具无效,稍后过滤掉
|
| 228 |
-
if (!name) {
|
| 229 |
-
return null;
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
return {
|
| 233 |
-
type: 'function',
|
| 234 |
-
function: {
|
| 235 |
-
name: name,
|
| 236 |
-
description: description,
|
| 237 |
-
parameters: parameters
|
| 238 |
-
}
|
| 239 |
-
};
|
| 240 |
-
})
|
| 241 |
-
.filter(tool => tool !== null);
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
if (responsesRequest.tool_choice) {
|
| 245 |
-
openaiRequest.tool_choice = responsesRequest.tool_choice;
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
return openaiRequest;
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
/**
|
| 252 |
-
* 将 OpenAI Responses 响应转换为标准 OpenAI 响应
|
| 253 |
-
*/
|
| 254 |
-
toOpenAIResponse(responsesResponse, model) {
|
| 255 |
-
const choices = [];
|
| 256 |
-
let usage = {
|
| 257 |
-
prompt_tokens: 0,
|
| 258 |
-
completion_tokens: 0,
|
| 259 |
-
total_tokens: 0,
|
| 260 |
-
prompt_tokens_details: { cached_tokens: 0 },
|
| 261 |
-
completion_tokens_details: { reasoning_tokens: 0 }
|
| 262 |
-
};
|
| 263 |
-
|
| 264 |
-
if (responsesResponse.output && Array.isArray(responsesResponse.output)) {
|
| 265 |
-
responsesResponse.output.forEach((item, index) => {
|
| 266 |
-
if (item.type === 'message') {
|
| 267 |
-
const content = item.content
|
| 268 |
-
?.filter(c => c.type === 'output_text')
|
| 269 |
-
.map(c => c.text)
|
| 270 |
-
.join('') || '';
|
| 271 |
-
|
| 272 |
-
choices.push({
|
| 273 |
-
index: index,
|
| 274 |
-
message: {
|
| 275 |
-
role: 'assistant',
|
| 276 |
-
content: content
|
| 277 |
-
},
|
| 278 |
-
finish_reason: responsesResponse.status === 'completed' ? 'stop' : null
|
| 279 |
-
});
|
| 280 |
-
} else if (item.type === 'function_call') {
|
| 281 |
-
choices.push({
|
| 282 |
-
index: index,
|
| 283 |
-
message: {
|
| 284 |
-
role: 'assistant',
|
| 285 |
-
tool_calls: [{
|
| 286 |
-
id: item.call_id,
|
| 287 |
-
type: 'function',
|
| 288 |
-
function: {
|
| 289 |
-
name: item.name,
|
| 290 |
-
arguments: item.arguments
|
| 291 |
-
}
|
| 292 |
-
}]
|
| 293 |
-
},
|
| 294 |
-
finish_reason: 'tool_calls'
|
| 295 |
-
});
|
| 296 |
-
}
|
| 297 |
-
});
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
if (responsesResponse.usage) {
|
| 301 |
-
usage = {
|
| 302 |
-
prompt_tokens: responsesResponse.usage.input_tokens || 0,
|
| 303 |
-
completion_tokens: responsesResponse.usage.output_tokens || 0,
|
| 304 |
-
total_tokens: responsesResponse.usage.total_tokens || 0,
|
| 305 |
-
prompt_tokens_details: {
|
| 306 |
-
cached_tokens: responsesResponse.usage.input_tokens_details?.cached_tokens || 0
|
| 307 |
-
},
|
| 308 |
-
completion_tokens_details: {
|
| 309 |
-
reasoning_tokens: responsesResponse.usage.output_tokens_details?.reasoning_tokens || 0
|
| 310 |
-
}
|
| 311 |
-
};
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
return {
|
| 315 |
-
id: responsesResponse.id || `chatcmpl-${Date.now()}`,
|
| 316 |
-
object: 'chat.completion',
|
| 317 |
-
created: responsesResponse.created_at || Math.floor(Date.now() / 1000),
|
| 318 |
-
model: model || responsesResponse.model,
|
| 319 |
-
choices: choices.length > 0 ? choices : [{
|
| 320 |
-
index: 0,
|
| 321 |
-
message: {
|
| 322 |
-
role: 'assistant',
|
| 323 |
-
content: ''
|
| 324 |
-
},
|
| 325 |
-
finish_reason: 'stop'
|
| 326 |
-
}],
|
| 327 |
-
usage: usage
|
| 328 |
-
};
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
/**
|
| 332 |
-
* 将 OpenAI Responses 流式块转换为标准 OpenAI 流式块
|
| 333 |
-
*/
|
| 334 |
-
toOpenAIStreamChunk(responsesChunk, model) {
|
| 335 |
-
const resId = responsesChunk.response?.id || responsesChunk.id || `chatcmpl-${Date.now()}`;
|
| 336 |
-
const created = responsesChunk.response?.created_at || responsesChunk.created || Math.floor(Date.now() / 1000);
|
| 337 |
-
|
| 338 |
-
const delta = {};
|
| 339 |
-
let finish_reason = null;
|
| 340 |
-
|
| 341 |
-
if (responsesChunk.type === 'response.output_text.delta') {
|
| 342 |
-
delta.content = responsesChunk.delta;
|
| 343 |
-
} else if (responsesChunk.type === 'response.function_call_arguments.delta') {
|
| 344 |
-
delta.tool_calls = [{
|
| 345 |
-
index: responsesChunk.output_index || 0,
|
| 346 |
-
function: {
|
| 347 |
-
arguments: responsesChunk.delta
|
| 348 |
-
}
|
| 349 |
-
}];
|
| 350 |
-
} else if (responsesChunk.type === 'response.output_item.added' && responsesChunk.item?.type === 'function_call') {
|
| 351 |
-
delta.tool_calls = [{
|
| 352 |
-
index: responsesChunk.output_index || 0,
|
| 353 |
-
id: responsesChunk.item.call_id,
|
| 354 |
-
type: 'function',
|
| 355 |
-
function: {
|
| 356 |
-
name: responsesChunk.item.name,
|
| 357 |
-
arguments: ''
|
| 358 |
-
}
|
| 359 |
-
}];
|
| 360 |
-
} else if (responsesChunk.type === 'response.completed') {
|
| 361 |
-
finish_reason = 'stop';
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
return {
|
| 365 |
-
id: resId,
|
| 366 |
-
object: 'chat.completion.chunk',
|
| 367 |
-
created: created,
|
| 368 |
-
model: model || responsesChunk.response?.model || responsesChunk.model,
|
| 369 |
-
choices: [{
|
| 370 |
-
index: 0,
|
| 371 |
-
delta: delta,
|
| 372 |
-
finish_reason: finish_reason
|
| 373 |
-
}]
|
| 374 |
-
};
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
// =============================================================================
|
| 378 |
-
// 转换到 Claude 格式
|
| 379 |
-
// =============================================================================
|
| 380 |
-
|
| 381 |
-
/**
|
| 382 |
-
* 将 OpenAI Responses 请求转换为 Claude 请求
|
| 383 |
-
*/
|
| 384 |
-
toClaudeRequest(responsesRequest) {
|
| 385 |
-
const claudeRequest = {
|
| 386 |
-
model: responsesRequest.model,
|
| 387 |
-
messages: [],
|
| 388 |
-
max_tokens: responsesRequest.max_output_tokens || responsesRequest.max_tokens || CLAUDE_DEFAULT_MAX_TOKENS,
|
| 389 |
-
stream: responsesRequest.stream || false
|
| 390 |
-
};
|
| 391 |
-
|
| 392 |
-
// 处理 instructions 作为系统消息
|
| 393 |
-
if (responsesRequest.instructions) {
|
| 394 |
-
claudeRequest.system = responsesRequest.instructions;
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
// 处理 reasoning effort
|
| 398 |
-
if (responsesRequest.reasoning?.effort) {
|
| 399 |
-
const effort = String(responsesRequest.reasoning.effort || '').toLowerCase().trim();
|
| 400 |
-
let budgetTokens = 20000;
|
| 401 |
-
if (effort === 'low') budgetTokens = 2048;
|
| 402 |
-
else if (effort === 'medium') budgetTokens = 8192;
|
| 403 |
-
else if (effort === 'high') budgetTokens = 20000;
|
| 404 |
-
claudeRequest.thinking = {
|
| 405 |
-
type: 'enabled',
|
| 406 |
-
budget_tokens: budgetTokens
|
| 407 |
-
};
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
// 处理 input 数组中的消息
|
| 411 |
-
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
|
| 412 |
-
responsesRequest.input.forEach(item => {
|
| 413 |
-
const itemType = item.type || (item.role ? 'message' : '');
|
| 414 |
-
|
| 415 |
-
switch (itemType) {
|
| 416 |
-
case 'message':
|
| 417 |
-
const content = [];
|
| 418 |
-
if (Array.isArray(item.content)) {
|
| 419 |
-
item.content.forEach(c => {
|
| 420 |
-
if (c.type === 'input_text' || c.type === 'output_text') {
|
| 421 |
-
content.push({ type: 'text', text: c.text });
|
| 422 |
-
} else if (c.type === 'input_image') {
|
| 423 |
-
const url = c.image_url?.url || c.url;
|
| 424 |
-
if (url && url.startsWith('data:')) {
|
| 425 |
-
const [mediaInfo, data] = url.split(';base64,');
|
| 426 |
-
const mediaType = mediaInfo.replace('data:', '');
|
| 427 |
-
content.push({
|
| 428 |
-
type: 'image',
|
| 429 |
-
source: {
|
| 430 |
-
type: 'base64',
|
| 431 |
-
media_type: mediaType,
|
| 432 |
-
data: data
|
| 433 |
-
}
|
| 434 |
-
});
|
| 435 |
-
}
|
| 436 |
-
}
|
| 437 |
-
});
|
| 438 |
-
} else if (typeof item.content === 'string') {
|
| 439 |
-
content.push({ type: 'text', text: item.content });
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
if (content.length > 0) {
|
| 443 |
-
claudeRequest.messages.push({
|
| 444 |
-
role: item.role === 'assistant' ? 'assistant' : 'user',
|
| 445 |
-
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
|
| 446 |
-
});
|
| 447 |
-
}
|
| 448 |
-
break;
|
| 449 |
-
|
| 450 |
-
case 'function_call':
|
| 451 |
-
claudeRequest.messages.push({
|
| 452 |
-
role: 'assistant',
|
| 453 |
-
content: [{
|
| 454 |
-
type: 'tool_use',
|
| 455 |
-
id: item.call_id || `toolu_${uuidv4().replace(/-/g, '')}`,
|
| 456 |
-
name: item.name,
|
| 457 |
-
input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 458 |
-
}]
|
| 459 |
-
});
|
| 460 |
-
break;
|
| 461 |
-
|
| 462 |
-
case 'function_call_output':
|
| 463 |
-
claudeRequest.messages.push({
|
| 464 |
-
role: 'user',
|
| 465 |
-
content: [{
|
| 466 |
-
type: 'tool_result',
|
| 467 |
-
tool_use_id: item.call_id,
|
| 468 |
-
content: item.output
|
| 469 |
-
}]
|
| 470 |
-
});
|
| 471 |
-
break;
|
| 472 |
-
}
|
| 473 |
-
});
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
// 处理工具
|
| 477 |
-
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
| 478 |
-
claudeRequest.tools = responsesRequest.tools.map(tool => ({
|
| 479 |
-
name: tool.name,
|
| 480 |
-
description: tool.description,
|
| 481 |
-
input_schema: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
| 482 |
-
}));
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
if (responsesRequest.tool_choice) {
|
| 486 |
-
if (typeof responsesRequest.tool_choice === 'string') {
|
| 487 |
-
if (responsesRequest.tool_choice === 'auto') {
|
| 488 |
-
claudeRequest.tool_choice = { type: 'auto' };
|
| 489 |
-
} else if (responsesRequest.tool_choice === 'required') {
|
| 490 |
-
claudeRequest.tool_choice = { type: 'any' };
|
| 491 |
-
}
|
| 492 |
-
} else if (responsesRequest.tool_choice.type === 'function') {
|
| 493 |
-
claudeRequest.tool_choice = {
|
| 494 |
-
type: 'tool',
|
| 495 |
-
name: responsesRequest.tool_choice.function.name
|
| 496 |
-
};
|
| 497 |
-
}
|
| 498 |
-
}
|
| 499 |
-
|
| 500 |
-
return claudeRequest;
|
| 501 |
-
}
|
| 502 |
-
|
| 503 |
-
/**
|
| 504 |
-
* 将 OpenAI Responses 响应转换为 Claude 响应
|
| 505 |
-
*/
|
| 506 |
-
toClaudeResponse(responsesResponse, model) {
|
| 507 |
-
const content = [];
|
| 508 |
-
let stop_reason = 'end_turn';
|
| 509 |
-
|
| 510 |
-
if (responsesResponse.output && Array.isArray(responsesResponse.output)) {
|
| 511 |
-
responsesResponse.output.forEach(item => {
|
| 512 |
-
if (item.type === 'message') {
|
| 513 |
-
const text = item.content
|
| 514 |
-
?.filter(c => c.type === 'output_text')
|
| 515 |
-
.map(c => c.text)
|
| 516 |
-
.join('') || '';
|
| 517 |
-
if (text) {
|
| 518 |
-
content.push({ type: 'text', text: text });
|
| 519 |
-
}
|
| 520 |
-
} else if (item.type === 'function_call') {
|
| 521 |
-
content.push({
|
| 522 |
-
type: 'tool_use',
|
| 523 |
-
id: item.call_id,
|
| 524 |
-
name: item.name,
|
| 525 |
-
input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 526 |
-
});
|
| 527 |
-
stop_reason = 'tool_use';
|
| 528 |
-
}
|
| 529 |
-
});
|
| 530 |
-
}
|
| 531 |
-
|
| 532 |
-
return {
|
| 533 |
-
id: responsesResponse.id || `msg_${Date.now()}`,
|
| 534 |
-
type: 'message',
|
| 535 |
-
role: 'assistant',
|
| 536 |
-
content: content,
|
| 537 |
-
model: model || responsesResponse.model,
|
| 538 |
-
stop_reason: stop_reason,
|
| 539 |
-
usage: {
|
| 540 |
-
input_tokens: responsesResponse.usage?.input_tokens || 0,
|
| 541 |
-
output_tokens: responsesResponse.usage?.output_tokens || 0,
|
| 542 |
-
total_tokens: responsesResponse.usage?.total_tokens || 0
|
| 543 |
-
}
|
| 544 |
-
};
|
| 545 |
-
}
|
| 546 |
-
|
| 547 |
-
/**
|
| 548 |
-
* 将 OpenAI Responses 流式块转换为 Claude 流式块
|
| 549 |
-
*/
|
| 550 |
-
toClaudeStreamChunk(responsesChunk, model) {
|
| 551 |
-
if (responsesChunk.type === 'response.created') {
|
| 552 |
-
return {
|
| 553 |
-
type: 'message_start',
|
| 554 |
-
message: {
|
| 555 |
-
id: responsesChunk.response.id,
|
| 556 |
-
type: 'message',
|
| 557 |
-
role: 'assistant',
|
| 558 |
-
content: [],
|
| 559 |
-
model: model || responsesChunk.response.model
|
| 560 |
-
}
|
| 561 |
-
};
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
if (responsesChunk.type === 'response.output_text.delta') {
|
| 565 |
-
return {
|
| 566 |
-
type: 'content_block_delta',
|
| 567 |
-
index: 0,
|
| 568 |
-
delta: {
|
| 569 |
-
type: 'text_delta',
|
| 570 |
-
text: responsesChunk.delta
|
| 571 |
-
}
|
| 572 |
-
};
|
| 573 |
-
}
|
| 574 |
-
|
| 575 |
-
if (responsesChunk.type === 'response.function_call_arguments.delta') {
|
| 576 |
-
return {
|
| 577 |
-
type: 'content_block_delta',
|
| 578 |
-
index: responsesChunk.output_index || 0,
|
| 579 |
-
delta: {
|
| 580 |
-
type: 'input_json_delta',
|
| 581 |
-
partial_json: responsesChunk.delta
|
| 582 |
-
}
|
| 583 |
-
};
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
if (responsesChunk.type === 'response.output_item.added' && responsesChunk.item?.type === 'function_call') {
|
| 587 |
-
return {
|
| 588 |
-
type: 'content_block_start',
|
| 589 |
-
index: responsesChunk.output_index || 0,
|
| 590 |
-
content_block: {
|
| 591 |
-
type: 'tool_use',
|
| 592 |
-
id: responsesChunk.item.call_id,
|
| 593 |
-
name: responsesChunk.item.name,
|
| 594 |
-
input: {}
|
| 595 |
-
}
|
| 596 |
-
};
|
| 597 |
-
}
|
| 598 |
-
|
| 599 |
-
if (responsesChunk.type === 'response.completed') {
|
| 600 |
-
return {
|
| 601 |
-
type: 'message_stop'
|
| 602 |
-
};
|
| 603 |
-
}
|
| 604 |
-
|
| 605 |
-
return null;
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
// =============================================================================
|
| 609 |
-
// 转换到 Gemini 格式
|
| 610 |
-
// =============================================================================
|
| 611 |
-
|
| 612 |
-
/**
|
| 613 |
-
* 将 OpenAI Responses 请求转换为 Gemini 请求
|
| 614 |
-
*/
|
| 615 |
-
toGeminiRequest(responsesRequest) {
|
| 616 |
-
const geminiRequest = {
|
| 617 |
-
contents: [],
|
| 618 |
-
generationConfig: {}
|
| 619 |
-
};
|
| 620 |
-
|
| 621 |
-
// 处理 instructions 作为系统指令
|
| 622 |
-
if (responsesRequest.instructions) {
|
| 623 |
-
geminiRequest.systemInstruction = {
|
| 624 |
-
parts: [{
|
| 625 |
-
text: responsesRequest.instructions
|
| 626 |
-
}]
|
| 627 |
-
};
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
// 处理 input 数组中的消息
|
| 631 |
-
if (responsesRequest.input && Array.isArray(responsesRequest.input)) {
|
| 632 |
-
responsesRequest.input.forEach(item => {
|
| 633 |
-
const itemType = item.type || (item.role ? 'message' : '');
|
| 634 |
-
|
| 635 |
-
switch (itemType) {
|
| 636 |
-
case 'message':
|
| 637 |
-
const parts = [];
|
| 638 |
-
if (Array.isArray(item.content)) {
|
| 639 |
-
item.content.forEach(c => {
|
| 640 |
-
if (c.type === 'input_text' || c.type === 'output_text') {
|
| 641 |
-
parts.push({ text: c.text });
|
| 642 |
-
} else if (c.type === 'input_image') {
|
| 643 |
-
const url = c.image_url?.url || c.url;
|
| 644 |
-
if (url && url.startsWith('data:')) {
|
| 645 |
-
const [mediaInfo, data] = url.split(';base64,');
|
| 646 |
-
const mimeType = mediaInfo.replace('data:', '');
|
| 647 |
-
parts.push({
|
| 648 |
-
inlineData: {
|
| 649 |
-
mimeType: mimeType,
|
| 650 |
-
data: data
|
| 651 |
-
}
|
| 652 |
-
});
|
| 653 |
-
}
|
| 654 |
-
}
|
| 655 |
-
});
|
| 656 |
-
} else if (typeof item.content === 'string') {
|
| 657 |
-
parts.push({ text: item.content });
|
| 658 |
-
}
|
| 659 |
-
|
| 660 |
-
if (parts.length > 0) {
|
| 661 |
-
geminiRequest.contents.push({
|
| 662 |
-
role: item.role === 'assistant' ? 'model' : 'user',
|
| 663 |
-
parts: parts
|
| 664 |
-
});
|
| 665 |
-
}
|
| 666 |
-
break;
|
| 667 |
-
|
| 668 |
-
case 'function_call':
|
| 669 |
-
geminiRequest.contents.push({
|
| 670 |
-
role: 'model',
|
| 671 |
-
parts: [{
|
| 672 |
-
functionCall: {
|
| 673 |
-
name: item.name,
|
| 674 |
-
args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 675 |
-
}
|
| 676 |
-
}]
|
| 677 |
-
});
|
| 678 |
-
break;
|
| 679 |
-
|
| 680 |
-
case 'function_call_output':
|
| 681 |
-
geminiRequest.contents.push({
|
| 682 |
-
role: 'user', // Gemini function response role is user or tool? usually user/model
|
| 683 |
-
parts: [{
|
| 684 |
-
functionResponse: {
|
| 685 |
-
name: item.name,
|
| 686 |
-
response: { content: item.output }
|
| 687 |
-
}
|
| 688 |
-
}]
|
| 689 |
-
});
|
| 690 |
-
break;
|
| 691 |
-
}
|
| 692 |
-
});
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
// 设置生成配置
|
| 696 |
-
if (responsesRequest.temperature !== undefined) {
|
| 697 |
-
geminiRequest.generationConfig.temperature = responsesRequest.temperature;
|
| 698 |
-
}
|
| 699 |
-
if (responsesRequest.max_output_tokens !== undefined) {
|
| 700 |
-
geminiRequest.generationConfig.maxOutputTokens = responsesRequest.max_output_tokens;
|
| 701 |
-
} else if (responsesRequest.max_tokens !== undefined) {
|
| 702 |
-
geminiRequest.generationConfig.maxOutputTokens = responsesRequest.max_tokens;
|
| 703 |
-
}
|
| 704 |
-
if (responsesRequest.top_p !== undefined) {
|
| 705 |
-
geminiRequest.generationConfig.topP = responsesRequest.top_p;
|
| 706 |
-
}
|
| 707 |
-
|
| 708 |
-
// 处理工具
|
| 709 |
-
if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
|
| 710 |
-
geminiRequest.tools = [{
|
| 711 |
-
functionDeclarations: responsesRequest.tools
|
| 712 |
-
.filter(tool => !tool.type || tool.type === 'function')
|
| 713 |
-
.map(tool => ({
|
| 714 |
-
name: tool.name,
|
| 715 |
-
description: tool.description,
|
| 716 |
-
parameters: tool.parameters || tool.parametersJsonSchema || { type: 'object', properties: {} }
|
| 717 |
-
}))
|
| 718 |
-
}];
|
| 719 |
-
}
|
| 720 |
-
|
| 721 |
-
return geminiRequest;
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
/**
|
| 725 |
-
* 将 OpenAI Responses 响应转换为 Gemini 响应
|
| 726 |
-
*/
|
| 727 |
-
toGeminiResponse(responsesResponse, model) {
|
| 728 |
-
const parts = [];
|
| 729 |
-
let finishReason = 'STOP';
|
| 730 |
-
|
| 731 |
-
if (responsesResponse.output && Array.isArray(responsesResponse.output)) {
|
| 732 |
-
responsesResponse.output.forEach(item => {
|
| 733 |
-
if (item.type === 'message') {
|
| 734 |
-
const text = item.content
|
| 735 |
-
?.filter(c => c.type === 'output_text')
|
| 736 |
-
.map(c => c.text)
|
| 737 |
-
.join('') || '';
|
| 738 |
-
if (text) {
|
| 739 |
-
parts.push({ text: text });
|
| 740 |
-
}
|
| 741 |
-
} else if (item.type === 'function_call') {
|
| 742 |
-
parts.push({
|
| 743 |
-
functionCall: {
|
| 744 |
-
name: item.name,
|
| 745 |
-
args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
|
| 746 |
-
}
|
| 747 |
-
});
|
| 748 |
-
}
|
| 749 |
-
});
|
| 750 |
-
}
|
| 751 |
-
|
| 752 |
-
return {
|
| 753 |
-
candidates: [{
|
| 754 |
-
content: {
|
| 755 |
-
parts: parts,
|
| 756 |
-
role: 'model'
|
| 757 |
-
},
|
| 758 |
-
finishReason: finishReason,
|
| 759 |
-
index: 0
|
| 760 |
-
}],
|
| 761 |
-
usageMetadata: {
|
| 762 |
-
promptTokenCount: responsesResponse.usage?.input_tokens || 0,
|
| 763 |
-
candidatesTokenCount: responsesResponse.usage?.output_tokens || 0,
|
| 764 |
-
totalTokenCount: responsesResponse.usage?.total_tokens || 0
|
| 765 |
-
}
|
| 766 |
-
};
|
| 767 |
-
}
|
| 768 |
-
|
| 769 |
-
/**
|
| 770 |
-
* 将 OpenAI Responses 流式块转换为 Gemini 流式块
|
| 771 |
-
*/
|
| 772 |
-
toGeminiStreamChunk(responsesChunk, model) {
|
| 773 |
-
if (responsesChunk.type === 'response.output_text.delta') {
|
| 774 |
-
return {
|
| 775 |
-
candidates: [{
|
| 776 |
-
content: {
|
| 777 |
-
parts: [{ text: responsesChunk.delta }],
|
| 778 |
-
role: 'model'
|
| 779 |
-
},
|
| 780 |
-
index: 0
|
| 781 |
-
}]
|
| 782 |
-
};
|
| 783 |
-
}
|
| 784 |
-
|
| 785 |
-
if (responsesChunk.type === 'response.function_call_arguments.delta') {
|
| 786 |
-
// Gemini 不太支持流式 functionCall 参数,这里只能简单映射
|
| 787 |
-
return {
|
| 788 |
-
candidates: [{
|
| 789 |
-
content: {
|
| 790 |
-
parts: [{
|
| 791 |
-
functionCall: {
|
| 792 |
-
name: '', // 无法在 delta 中获取名称
|
| 793 |
-
args: responsesChunk.delta
|
| 794 |
-
}
|
| 795 |
-
}],
|
| 796 |
-
role: 'model'
|
| 797 |
-
},
|
| 798 |
-
index: 0
|
| 799 |
-
}]
|
| 800 |
-
};
|
| 801 |
-
}
|
| 802 |
-
|
| 803 |
-
return null;
|
| 804 |
-
}
|
| 805 |
-
|
| 806 |
-
/**
|
| 807 |
-
* OpenAI Responses → Codex 请求转换
|
| 808 |
-
*/
|
| 809 |
-
toCodexRequest(responsesRequest) {
|
| 810 |
-
return this.codexConverter.toOpenAIResponsesToCodexRequest(responsesRequest);
|
| 811 |
-
}
|
| 812 |
-
|
| 813 |
-
/**
|
| 814 |
-
* OpenAI Responses → Grok 请求转换
|
| 815 |
-
*/
|
| 816 |
-
toGrokRequest(responsesRequest) {
|
| 817 |
-
// 先转换为 OpenAI 格式
|
| 818 |
-
const openaiRequest = this.toOpenAIRequest(responsesRequest);
|
| 819 |
-
return {
|
| 820 |
-
...openaiRequest,
|
| 821 |
-
_isConverted: true
|
| 822 |
-
};
|
| 823 |
-
}
|
| 824 |
-
|
| 825 |
-
// =============================================================================
|
| 826 |
-
// 辅助方法
|
| 827 |
-
// =============================================================================
|
| 828 |
-
|
| 829 |
-
/**
|
| 830 |
-
* 映射完成原因
|
| 831 |
-
*/
|
| 832 |
-
mapFinishReason(reason) {
|
| 833 |
-
const reasonMap = {
|
| 834 |
-
'stop': 'STOP',
|
| 835 |
-
'length': 'MAX_TOKENS',
|
| 836 |
-
'content_filter': 'SAFETY',
|
| 837 |
-
'end_turn': 'STOP'
|
| 838 |
-
};
|
| 839 |
-
return reasonMap[reason] || 'STOP';
|
| 840 |
-
}
|
| 841 |
-
|
| 842 |
-
/**
|
| 843 |
-
* 将 OpenAI Responses 模型列表转换为标准 OpenAI 模型列表
|
| 844 |
-
*/
|
| 845 |
-
toOpenAIModelList(responsesModels) {
|
| 846 |
-
// OpenAI Responses 格式的模型列表已经是标准 OpenAI 格式
|
| 847 |
-
// 如果输入已经是标准格式,直接返回
|
| 848 |
-
if (responsesModels.object === 'list' && responsesModels.data) {
|
| 849 |
-
return responsesModels;
|
| 850 |
-
}
|
| 851 |
-
|
| 852 |
-
// 如果是其他格式,转换为标准格式
|
| 853 |
-
return {
|
| 854 |
-
object: "list",
|
| 855 |
-
data: (responsesModels.models || responsesModels.data || []).map(m => ({
|
| 856 |
-
id: m.id || m.name,
|
| 857 |
-
object: "model",
|
| 858 |
-
created: m.created || Math.floor(Date.now() / 1000),
|
| 859 |
-
owned_by: m.owned_by || "openai",
|
| 860 |
-
})),
|
| 861 |
-
};
|
| 862 |
-
}
|
| 863 |
-
|
| 864 |
-
/**
|
| 865 |
-
* 将 OpenAI Responses 模型列表转换为 Claude 模型列表
|
| 866 |
-
*/
|
| 867 |
-
toClaudeModelList(responsesModels) {
|
| 868 |
-
const models = responsesModels.data || responsesModels.models || [];
|
| 869 |
-
return {
|
| 870 |
-
models: models.map(m => ({
|
| 871 |
-
name: m.id || m.name,
|
| 872 |
-
description: m.description || "",
|
| 873 |
-
})),
|
| 874 |
-
};
|
| 875 |
-
}
|
| 876 |
-
|
| 877 |
-
/**
|
| 878 |
-
* 将 OpenAI Responses 模型列表转换为 Gemini 模型列表
|
| 879 |
-
*/
|
| 880 |
-
toGeminiModelList(responsesModels) {
|
| 881 |
-
const models = responsesModels.data || responsesModels.models || [];
|
| 882 |
-
return {
|
| 883 |
-
models: models.map(m => ({
|
| 884 |
-
name: `models/${m.id || m.name}`,
|
| 885 |
-
version: m.version || "1.0.0",
|
| 886 |
-
displayName: m.displayName || m.id || m.name,
|
| 887 |
-
description: m.description || `A generative model for text and chat generation. ID: ${m.id || m.name}`,
|
| 888 |
-
inputTokenLimit: m.inputTokenLimit || GEMINI_DEFAULT_INPUT_TOKEN_LIMIT,
|
| 889 |
-
outputTokenLimit: m.outputTokenLimit || GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT,
|
| 890 |
-
supportedGenerationMethods: m.supportedGenerationMethods || ["generateContent", "streamGenerateContent"]
|
| 891 |
-
}))
|
| 892 |
-
};
|
| 893 |
-
}
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
/**
|
| 897 |
-
* OpenAI Responses → Codex 响应转换 (实际上是 Codex 转 OpenAI Responses)
|
| 898 |
-
*/
|
| 899 |
-
toCodexResponse(codexResponse, model) {
|
| 900 |
-
const output = [];
|
| 901 |
-
const responseData = codexResponse.response || codexResponse;
|
| 902 |
-
|
| 903 |
-
if (responseData.output && Array.isArray(responseData.output)) {
|
| 904 |
-
responseData.output.forEach(item => {
|
| 905 |
-
if (item.type === 'message' && item.content) {
|
| 906 |
-
const content = item.content.map(c => ({
|
| 907 |
-
type: c.type === 'output_text' ? 'output_text' : 'input_text',
|
| 908 |
-
text: c.text,
|
| 909 |
-
annotations: []
|
| 910 |
-
}));
|
| 911 |
-
output.push({
|
| 912 |
-
id: item.id || `msg_${uuidv4().replace(/-/g, '')}`,
|
| 913 |
-
type: "message",
|
| 914 |
-
role: item.role || "assistant",
|
| 915 |
-
status: item.status || "completed",
|
| 916 |
-
content: content
|
| 917 |
-
});
|
| 918 |
-
} else if (item.type === 'reasoning') {
|
| 919 |
-
output.push({
|
| 920 |
-
id: item.id || `rs_${uuidv4().replace(/-/g, '')}`,
|
| 921 |
-
type: "reasoning",
|
| 922 |
-
status: item.status || "completed",
|
| 923 |
-
summary: item.summary || []
|
| 924 |
-
});
|
| 925 |
-
} else if (item.type === 'function_call') {
|
| 926 |
-
output.push({
|
| 927 |
-
id: item.id || `fc_${uuidv4().replace(/-/g, '')}`,
|
| 928 |
-
call_id: item.call_id,
|
| 929 |
-
type: "function_call",
|
| 930 |
-
name: item.name,
|
| 931 |
-
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
|
| 932 |
-
status: item.status || "completed"
|
| 933 |
-
});
|
| 934 |
-
}
|
| 935 |
-
});
|
| 936 |
-
}
|
| 937 |
-
|
| 938 |
-
return {
|
| 939 |
-
id: responseData.id || `resp_${uuidv4().replace(/-/g, '')}`,
|
| 940 |
-
object: "response",
|
| 941 |
-
created_at: responseData.created_at || Math.floor(Date.now() / 1000),
|
| 942 |
-
model: model || responseData.model,
|
| 943 |
-
status: responseData.status || "completed",
|
| 944 |
-
output: output,
|
| 945 |
-
usage: {
|
| 946 |
-
input_tokens: responseData.usage?.input_tokens || 0,
|
| 947 |
-
output_tokens: responseData.usage?.output_tokens || 0,
|
| 948 |
-
total_tokens: responseData.usage?.total_tokens || 0
|
| 949 |
-
}
|
| 950 |
-
};
|
| 951 |
-
}
|
| 952 |
-
|
| 953 |
-
/**
|
| 954 |
-
* OpenAI Responses → Codex 流式响应转换 (实际上是 Codex 转 OpenAI Responses)
|
| 955 |
-
*/
|
| 956 |
-
toCodexStreamChunk(codexChunk, model) {
|
| 957 |
-
const type = codexChunk.type;
|
| 958 |
-
const resId = codexChunk.response?.id || 'default';
|
| 959 |
-
const events = [];
|
| 960 |
-
|
| 961 |
-
if (type === 'response.created') {
|
| 962 |
-
events.push(
|
| 963 |
-
generateResponseCreated(resId, model || codexChunk.response?.model),
|
| 964 |
-
generateResponseInProgress(resId)
|
| 965 |
-
);
|
| 966 |
-
return events;
|
| 967 |
-
}
|
| 968 |
-
|
| 969 |
-
if (type === 'response.reasoning_summary_text.delta') {
|
| 970 |
-
events.push({
|
| 971 |
-
type: "response.reasoning_summary_text.delta",
|
| 972 |
-
response_id: resId,
|
| 973 |
-
item_id: codexChunk.item_id,
|
| 974 |
-
output_index: codexChunk.output_index,
|
| 975 |
-
summary_index: codexChunk.summary_index,
|
| 976 |
-
delta: codexChunk.delta
|
| 977 |
-
});
|
| 978 |
-
return events;
|
| 979 |
-
}
|
| 980 |
-
|
| 981 |
-
if (type === 'response.output_text.delta') {
|
| 982 |
-
events.push({
|
| 983 |
-
type: "response.output_text.delta",
|
| 984 |
-
response_id: resId,
|
| 985 |
-
item_id: codexChunk.item_id,
|
| 986 |
-
output_index: codexChunk.output_index,
|
| 987 |
-
content_index: codexChunk.content_index,
|
| 988 |
-
delta: codexChunk.delta
|
| 989 |
-
});
|
| 990 |
-
return events;
|
| 991 |
-
}
|
| 992 |
-
|
| 993 |
-
if (type === 'response.function_call_arguments.delta') {
|
| 994 |
-
events.push({
|
| 995 |
-
type: "response.function_call_arguments.delta",
|
| 996 |
-
response_id: resId,
|
| 997 |
-
item_id: codexChunk.item_id,
|
| 998 |
-
output_index: codexChunk.output_index,
|
| 999 |
-
delta: codexChunk.delta
|
| 1000 |
-
});
|
| 1001 |
-
return events;
|
| 1002 |
-
}
|
| 1003 |
-
|
| 1004 |
-
if (type === 'response.output_item.added') {
|
| 1005 |
-
events.push({
|
| 1006 |
-
type: "response.output_item.added",
|
| 1007 |
-
response_id: resId,
|
| 1008 |
-
output_index: codexChunk.output_index,
|
| 1009 |
-
item: codexChunk.item
|
| 1010 |
-
});
|
| 1011 |
-
return events;
|
| 1012 |
-
}
|
| 1013 |
-
|
| 1014 |
-
if (type === 'response.completed') {
|
| 1015 |
-
const completedEvent = generateResponseCompleted(resId);
|
| 1016 |
-
completedEvent.response = {
|
| 1017 |
-
...completedEvent.response,
|
| 1018 |
-
...codexChunk.response
|
| 1019 |
-
};
|
| 1020 |
-
events.push(completedEvent);
|
| 1021 |
-
return events;
|
| 1022 |
-
}
|
| 1023 |
-
|
| 1024 |
-
// 透传其他 response.* 事件
|
| 1025 |
-
if (type && type.startsWith('response.')) {
|
| 1026 |
-
return [codexChunk];
|
| 1027 |
-
}
|
| 1028 |
-
|
| 1029 |
-
return null;
|
| 1030 |
-
}
|
| 1031 |
-
|
| 1032 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/converters/utils.js
DELETED
|
@@ -1,369 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 转换器公共工具函数模块
|
| 3 |
-
* 提供各种协议转换所需的通用辅助函数
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
-
import logger from '../utils/logger.js';
|
| 8 |
-
|
| 9 |
-
// =============================================================================
|
| 10 |
-
// 常量定义
|
| 11 |
-
// =============================================================================
|
| 12 |
-
|
| 13 |
-
// 通用默认值
|
| 14 |
-
export const DEFAULT_MAX_TOKENS = 8192;
|
| 15 |
-
export const DEFAULT_TEMPERATURE = 1;
|
| 16 |
-
export const DEFAULT_TOP_P = 0.95;
|
| 17 |
-
|
| 18 |
-
// =============================================================================
|
| 19 |
-
// OpenAI 相关常量
|
| 20 |
-
// =============================================================================
|
| 21 |
-
export const OPENAI_DEFAULT_MAX_TOKENS = 128000;
|
| 22 |
-
export const OPENAI_DEFAULT_TEMPERATURE = 1;
|
| 23 |
-
export const OPENAI_DEFAULT_TOP_P = 0.95;
|
| 24 |
-
export const OPENAI_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
|
| 25 |
-
export const OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT = 128000;
|
| 26 |
-
|
| 27 |
-
// =============================================================================
|
| 28 |
-
// Claude 相关常量
|
| 29 |
-
// =============================================================================
|
| 30 |
-
export const CLAUDE_DEFAULT_MAX_TOKENS = 200000;
|
| 31 |
-
export const CLAUDE_DEFAULT_TEMPERATURE = 1;
|
| 32 |
-
export const CLAUDE_DEFAULT_TOP_P = 0.95;
|
| 33 |
-
|
| 34 |
-
// =============================================================================
|
| 35 |
-
// Gemini 相关常量
|
| 36 |
-
// =============================================================================
|
| 37 |
-
export const GEMINI_DEFAULT_MAX_TOKENS = 65534;
|
| 38 |
-
export const GEMINI_DEFAULT_TEMPERATURE = 1;
|
| 39 |
-
export const GEMINI_DEFAULT_TOP_P = 0.95;
|
| 40 |
-
export const GEMINI_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
|
| 41 |
-
export const GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT = 65534;
|
| 42 |
-
|
| 43 |
-
// =============================================================================
|
| 44 |
-
// OpenAI Responses 相关常量
|
| 45 |
-
// =============================================================================
|
| 46 |
-
export const OPENAI_RESPONSES_DEFAULT_MAX_TOKENS = 128000;
|
| 47 |
-
export const OPENAI_RESPONSES_DEFAULT_TEMPERATURE = 1;
|
| 48 |
-
export const OPENAI_RESPONSES_DEFAULT_TOP_P = 0.95;
|
| 49 |
-
export const OPENAI_RESPONSES_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
|
| 50 |
-
export const OPENAI_RESPONSES_DEFAULT_OUTPUT_TOKEN_LIMIT = 128000;
|
| 51 |
-
|
| 52 |
-
// =============================================================================
|
| 53 |
-
// 通用辅助函数
|
| 54 |
-
// =============================================================================
|
| 55 |
-
|
| 56 |
-
/**
|
| 57 |
-
* 判断值是否为 undefined 或 0,并返回默认值
|
| 58 |
-
* @param {*} value - 要检查的值
|
| 59 |
-
* @param {*} defaultValue - 默认值
|
| 60 |
-
* @returns {*} 处理后的值
|
| 61 |
-
*/
|
| 62 |
-
export function checkAndAssignOrDefault(value, defaultValue) {
|
| 63 |
-
if (value !== undefined && value !== 0) {
|
| 64 |
-
return value;
|
| 65 |
-
}
|
| 66 |
-
return defaultValue;
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
/**
|
| 70 |
-
* 生成唯一ID
|
| 71 |
-
* @param {string} prefix - ID前缀
|
| 72 |
-
* @returns {string} 生成的ID
|
| 73 |
-
*/
|
| 74 |
-
export function generateId(prefix = '') {
|
| 75 |
-
return prefix ? `${prefix}_${uuidv4()}` : uuidv4();
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
/**
|
| 79 |
-
* 安全解析JSON字符串
|
| 80 |
-
* @param {string} str - JSON字符串
|
| 81 |
-
* @returns {*} 解析后的对象或原始字符串
|
| 82 |
-
*/
|
| 83 |
-
export function safeParseJSON(str) {
|
| 84 |
-
if (!str) {
|
| 85 |
-
return str;
|
| 86 |
-
}
|
| 87 |
-
let cleanedStr = str;
|
| 88 |
-
|
| 89 |
-
// 处理可能被截断的转义序列
|
| 90 |
-
if (cleanedStr.endsWith('\\') && !cleanedStr.endsWith('\\\\')) {
|
| 91 |
-
cleanedStr = cleanedStr.substring(0, cleanedStr.length - 1);
|
| 92 |
-
} else if (cleanedStr.endsWith('\\u') || cleanedStr.endsWith('\\u0') || cleanedStr.endsWith('\\u00')) {
|
| 93 |
-
const idx = cleanedStr.lastIndexOf('\\u');
|
| 94 |
-
cleanedStr = cleanedStr.substring(0, idx);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
try {
|
| 98 |
-
return JSON.parse(cleanedStr || '{}');
|
| 99 |
-
} catch (e) {
|
| 100 |
-
return str;
|
| 101 |
-
}
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
/**
|
| 105 |
-
* 提取消息内容中的文本
|
| 106 |
-
* @param {string|Array} content - 消息内容
|
| 107 |
-
* @returns {string} 提取的文本
|
| 108 |
-
*/
|
| 109 |
-
export function extractTextFromMessageContent(content) {
|
| 110 |
-
if (typeof content === 'string') {
|
| 111 |
-
return content;
|
| 112 |
-
}
|
| 113 |
-
if (Array.isArray(content)) {
|
| 114 |
-
return content
|
| 115 |
-
.filter(part => part.type === 'text' && part.text)
|
| 116 |
-
.map(part => part.text)
|
| 117 |
-
.join('\n');
|
| 118 |
-
}
|
| 119 |
-
return '';
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
/**
|
| 123 |
-
* 提取并处理系统消息
|
| 124 |
-
* @param {Array} messages - 消息数组
|
| 125 |
-
* @returns {{systemInstruction: Object|null, nonSystemMessages: Array}}
|
| 126 |
-
*/
|
| 127 |
-
export function extractAndProcessSystemMessages(messages) {
|
| 128 |
-
const systemContents = [];
|
| 129 |
-
const nonSystemMessages = [];
|
| 130 |
-
|
| 131 |
-
for (const message of messages) {
|
| 132 |
-
if (message.role === 'system') {
|
| 133 |
-
systemContents.push(extractTextFromMessageContent(message.content));
|
| 134 |
-
} else {
|
| 135 |
-
nonSystemMessages.push(message);
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
let systemInstruction = null;
|
| 140 |
-
if (systemContents.length > 0) {
|
| 141 |
-
systemInstruction = {
|
| 142 |
-
parts: [{
|
| 143 |
-
text: systemContents.join('\n')
|
| 144 |
-
}]
|
| 145 |
-
};
|
| 146 |
-
}
|
| 147 |
-
return { systemInstruction, nonSystemMessages };
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
/**
|
| 151 |
-
* 清理JSON Schema属性(移除Gemini不支持的属性)
|
| 152 |
-
* Google Gemini API 只支持有限的 JSON Schema 属性,不支持以下属性:
|
| 153 |
-
* - exclusiveMinimum, exclusiveMaximum, minimum, maximum
|
| 154 |
-
* - minLength, maxLength, minItems, maxItems
|
| 155 |
-
* - pattern, format, default, const
|
| 156 |
-
* - additionalProperties, $schema, $ref, $id
|
| 157 |
-
* - allOf, anyOf, oneOf, not
|
| 158 |
-
* @param {Object} schema - JSON Schema
|
| 159 |
-
* @returns {Object} 清理后的JSON Schema
|
| 160 |
-
*/
|
| 161 |
-
export function cleanJsonSchemaProperties(schema) {
|
| 162 |
-
if (!schema || typeof schema !== 'object') {
|
| 163 |
-
return schema;
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
// 如果是数组,递归处理每个元素
|
| 167 |
-
if (Array.isArray(schema)) {
|
| 168 |
-
return schema.map(item => cleanJsonSchemaProperties(item));
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
// Gemini 支持的 JSON Schema 属性白名单
|
| 172 |
-
const allowedKeys = [
|
| 173 |
-
"type",
|
| 174 |
-
"description",
|
| 175 |
-
"properties",
|
| 176 |
-
"required",
|
| 177 |
-
"enum",
|
| 178 |
-
"items",
|
| 179 |
-
"nullable"
|
| 180 |
-
];
|
| 181 |
-
|
| 182 |
-
const sanitized = {};
|
| 183 |
-
for (const [key, value] of Object.entries(schema)) {
|
| 184 |
-
if (allowedKeys.includes(key)) {
|
| 185 |
-
// 对于需要递归处理的属性
|
| 186 |
-
if (key === 'properties' && typeof value === 'object' && value !== null) {
|
| 187 |
-
const cleanProperties = {};
|
| 188 |
-
for (const [propName, propSchema] of Object.entries(value)) {
|
| 189 |
-
cleanProperties[propName] = cleanJsonSchemaProperties(propSchema);
|
| 190 |
-
}
|
| 191 |
-
sanitized[key] = cleanProperties;
|
| 192 |
-
} else if (key === 'items') {
|
| 193 |
-
sanitized[key] = cleanJsonSchemaProperties(value);
|
| 194 |
-
} else if (key === 'type') {
|
| 195 |
-
// Google Gemini API 不支持数组形式的 type (如 ["string", "null"])
|
| 196 |
-
// 必须是单个字符串,且通常需要大写 (STRING, NUMBER, OBJECT, ARRAY, BOOLEAN, INTEGER)
|
| 197 |
-
if (Array.isArray(value)) {
|
| 198 |
-
// 如果包含 null,设置 nullable 为 true
|
| 199 |
-
if (value.includes('null')) {
|
| 200 |
-
sanitized.nullable = true;
|
| 201 |
-
}
|
| 202 |
-
// 取第一个非 null 类型
|
| 203 |
-
const actualType = value.find(t => t !== 'null');
|
| 204 |
-
if (actualType) {
|
| 205 |
-
sanitized[key] = actualType.toUpperCase();
|
| 206 |
-
}
|
| 207 |
-
} else if (typeof value === 'string') {
|
| 208 |
-
sanitized[key] = value.toUpperCase();
|
| 209 |
-
}
|
| 210 |
-
} else {
|
| 211 |
-
sanitized[key] = value;
|
| 212 |
-
}
|
| 213 |
-
}
|
| 214 |
-
// 其他属性(如 exclusiveMinimum, minimum, maximum, pattern 等)被忽略
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
return sanitized;
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
/**
|
| 221 |
-
* 映射结束原因
|
| 222 |
-
* @param {string} reason - 结束原因
|
| 223 |
-
* @param {string} sourceFormat - 源格式
|
| 224 |
-
* @param {string} targetFormat - 目标格式
|
| 225 |
-
* @returns {string} 映射后的结束原因
|
| 226 |
-
*/
|
| 227 |
-
export function mapFinishReason(reason, sourceFormat, targetFormat) {
|
| 228 |
-
const reasonMappings = {
|
| 229 |
-
openai: {
|
| 230 |
-
anthropic: {
|
| 231 |
-
stop: "end_turn",
|
| 232 |
-
length: "max_tokens",
|
| 233 |
-
content_filter: "stop_sequence",
|
| 234 |
-
tool_calls: "tool_use"
|
| 235 |
-
}
|
| 236 |
-
},
|
| 237 |
-
gemini: {
|
| 238 |
-
anthropic: {
|
| 239 |
-
STOP: "end_turn",
|
| 240 |
-
MAX_TOKENS: "max_tokens",
|
| 241 |
-
SAFETY: "stop_sequence",
|
| 242 |
-
RECITATION: "stop_sequence",
|
| 243 |
-
stop: "end_turn",
|
| 244 |
-
length: "max_tokens",
|
| 245 |
-
safety: "stop_sequence",
|
| 246 |
-
recitation: "stop_sequence",
|
| 247 |
-
other: "end_turn"
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
};
|
| 251 |
-
|
| 252 |
-
try {
|
| 253 |
-
return reasonMappings[sourceFormat][targetFormat][reason] || "end_turn";
|
| 254 |
-
} catch (e) {
|
| 255 |
-
return "end_turn";
|
| 256 |
-
}
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
/**
|
| 260 |
-
* 根据budget_tokens智能判断OpenAI reasoning_effort等级
|
| 261 |
-
* @param {number|null} budgetTokens - Anthropic thinking的budget_tokens值
|
| 262 |
-
* @returns {string} OpenAI reasoning_effort等级
|
| 263 |
-
*/
|
| 264 |
-
export function determineReasoningEffortFromBudget(budgetTokens) {
|
| 265 |
-
if (budgetTokens === null || budgetTokens === undefined) {
|
| 266 |
-
logger.info("No budget_tokens provided, defaulting to reasoning_effort='high'");
|
| 267 |
-
return "high";
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
const LOW_THRESHOLD = 50;
|
| 271 |
-
const HIGH_THRESHOLD = 200;
|
| 272 |
-
|
| 273 |
-
logger.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`);
|
| 274 |
-
|
| 275 |
-
let effort;
|
| 276 |
-
if (budgetTokens <= LOW_THRESHOLD) {
|
| 277 |
-
effort = "low";
|
| 278 |
-
} else if (budgetTokens <= HIGH_THRESHOLD) {
|
| 279 |
-
effort = "medium";
|
| 280 |
-
} else {
|
| 281 |
-
effort = "high";
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
logger.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`);
|
| 285 |
-
return effort;
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
/**
|
| 289 |
-
* 从OpenAI文本中提取thinking内容
|
| 290 |
-
* @param {string} text - 文本内容
|
| 291 |
-
* @returns {string|Array} 提取后的内容
|
| 292 |
-
*/
|
| 293 |
-
export function extractThinkingFromOpenAIText(text) {
|
| 294 |
-
const thinkingPattern = /<thinking>\s*(.*?)\s*<\/thinking>/gs;
|
| 295 |
-
const matches = [...text.matchAll(thinkingPattern)];
|
| 296 |
-
|
| 297 |
-
const contentBlocks = [];
|
| 298 |
-
let lastEnd = 0;
|
| 299 |
-
|
| 300 |
-
for (const match of matches) {
|
| 301 |
-
const beforeText = text.substring(lastEnd, match.index).trim();
|
| 302 |
-
if (beforeText) {
|
| 303 |
-
contentBlocks.push({
|
| 304 |
-
type: "text",
|
| 305 |
-
text: beforeText
|
| 306 |
-
});
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
const thinkingText = match[1].trim();
|
| 310 |
-
if (thinkingText) {
|
| 311 |
-
contentBlocks.push({
|
| 312 |
-
type: "thinking",
|
| 313 |
-
thinking: thinkingText
|
| 314 |
-
});
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
lastEnd = match.index + match[0].length;
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
const afterText = text.substring(lastEnd).trim();
|
| 321 |
-
if (afterText) {
|
| 322 |
-
contentBlocks.push({
|
| 323 |
-
type: "text",
|
| 324 |
-
text: afterText
|
| 325 |
-
});
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
if (contentBlocks.length === 0) {
|
| 329 |
-
return text;
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
if (contentBlocks.length === 1 && contentBlocks[0].type === "text") {
|
| 333 |
-
return contentBlocks[0].text;
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
return contentBlocks;
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
// =============================================================================
|
| 340 |
-
// 工具状态管理器(单例模式)
|
| 341 |
-
// =============================================================================
|
| 342 |
-
|
| 343 |
-
/**
|
| 344 |
-
* 全局工具状态管理器
|
| 345 |
-
*/
|
| 346 |
-
class ToolStateManager {
|
| 347 |
-
constructor() {
|
| 348 |
-
if (ToolStateManager.instance) {
|
| 349 |
-
return ToolStateManager.instance;
|
| 350 |
-
}
|
| 351 |
-
ToolStateManager.instance = this;
|
| 352 |
-
this._toolMappings = {};
|
| 353 |
-
return this;
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
storeToolMapping(funcName, toolId) {
|
| 357 |
-
this._toolMappings[funcName] = toolId;
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
getToolId(funcName) {
|
| 361 |
-
return this._toolMappings[funcName] || null;
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
clearMappings() {
|
| 365 |
-
this._toolMappings = {};
|
| 366 |
-
}
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
export const toolStateManager = new ToolStateManager();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/config-manager.js
DELETED
|
@@ -1,249 +0,0 @@
|
|
| 1 |
-
import * as fs from 'fs';
|
| 2 |
-
import { promises as pfs } from 'fs';
|
| 3 |
-
import { INPUT_SYSTEM_PROMPT_FILE, MODEL_PROVIDER } from '../utils/common.js';
|
| 4 |
-
import logger from '../utils/logger.js';
|
| 5 |
-
|
| 6 |
-
export let CONFIG = {}; // Make CONFIG exportable
|
| 7 |
-
export let PROMPT_LOG_FILENAME = ''; // Make PROMPT_LOG_FILENAME exportable
|
| 8 |
-
|
| 9 |
-
const ALL_MODEL_PROVIDERS = Object.values(MODEL_PROVIDER);
|
| 10 |
-
|
| 11 |
-
function normalizeConfiguredProviders(config) {
|
| 12 |
-
const fallbackProvider = MODEL_PROVIDER.GEMINI_CLI;
|
| 13 |
-
const dedupedProviders = [];
|
| 14 |
-
|
| 15 |
-
const addProvider = (value) => {
|
| 16 |
-
if (typeof value !== 'string') {
|
| 17 |
-
return;
|
| 18 |
-
}
|
| 19 |
-
const trimmed = value.trim();
|
| 20 |
-
if (!trimmed) {
|
| 21 |
-
return;
|
| 22 |
-
}
|
| 23 |
-
const matched = ALL_MODEL_PROVIDERS.find((provider) => provider.toLowerCase() === trimmed.toLowerCase());
|
| 24 |
-
if (!matched) {
|
| 25 |
-
logger.warn(`[Config Warning] Unknown model provider '${trimmed}'. This entry will be ignored.`);
|
| 26 |
-
return;
|
| 27 |
-
}
|
| 28 |
-
if (!dedupedProviders.includes(matched)) {
|
| 29 |
-
dedupedProviders.push(matched);
|
| 30 |
-
}
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
const rawValue = config.MODEL_PROVIDER;
|
| 34 |
-
if (Array.isArray(rawValue)) {
|
| 35 |
-
rawValue.forEach((entry) => addProvider(typeof entry === 'string' ? entry : String(entry)));
|
| 36 |
-
} else if (typeof rawValue === 'string') {
|
| 37 |
-
rawValue.split(',').forEach(addProvider);
|
| 38 |
-
} else if (rawValue != null) {
|
| 39 |
-
addProvider(String(rawValue));
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
if (dedupedProviders.length === 0) {
|
| 43 |
-
dedupedProviders.push(fallbackProvider);
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
config.DEFAULT_MODEL_PROVIDERS = dedupedProviders;
|
| 47 |
-
config.MODEL_PROVIDER = dedupedProviders[0];
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
/**
|
| 51 |
-
* Initializes the server configuration from config.json and command-line arguments.
|
| 52 |
-
* @param {string[]} args - Command-line arguments.
|
| 53 |
-
* @param {string} [configFilePath='configs/config.json'] - Path to the configuration file.
|
| 54 |
-
* @returns {Object} The initialized configuration object.
|
| 55 |
-
*/
|
| 56 |
-
export async function initializeConfig(args = process.argv.slice(2), configFilePath = 'configs/config.json') {
|
| 57 |
-
const defaultConfig = {
|
| 58 |
-
REQUIRED_API_KEY: "123456",
|
| 59 |
-
SERVER_PORT: 3000,
|
| 60 |
-
HOST: '0.0.0.0',
|
| 61 |
-
MODEL_PROVIDER: MODEL_PROVIDER.GEMINI_CLI,
|
| 62 |
-
SYSTEM_PROMPT_FILE_PATH: INPUT_SYSTEM_PROMPT_FILE, // Default value
|
| 63 |
-
SYSTEM_PROMPT_MODE: 'append',
|
| 64 |
-
PROXY_URL: null, // HTTP/HTTPS/SOCKS5 代理地址,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
|
| 65 |
-
PROXY_ENABLED_PROVIDERS: [], // 启用代理的提供商列表,如 ['gemini-cli-oauth', 'claude-kiro-oauth']
|
| 66 |
-
PROMPT_LOG_BASE_NAME: "prompt_log",
|
| 67 |
-
PROMPT_LOG_MODE: "none",
|
| 68 |
-
REQUEST_MAX_RETRIES: 3,
|
| 69 |
-
REQUEST_BASE_DELAY: 1000,
|
| 70 |
-
CREDENTIAL_SWITCH_MAX_RETRIES: 5, // 坏凭证切换最大重试次数(用于认证错误后切换凭证)
|
| 71 |
-
CRON_NEAR_MINUTES: 15,
|
| 72 |
-
CRON_REFRESH_TOKEN: false,
|
| 73 |
-
LOGIN_EXPIRY: 3600, // 登录过期时间(秒),默认1小时
|
| 74 |
-
LOGIN_MAX_ATTEMPTS: 5, // 最大失败重试次数
|
| 75 |
-
LOGIN_LOCKOUT_DURATION: 1800, // 锁定持续时间(秒),默认30分钟
|
| 76 |
-
LOGIN_MIN_INTERVAL: 5000, // 两次尝试之间的最小间隔(毫秒),默认1秒
|
| 77 |
-
PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径
|
| 78 |
-
MAX_ERROR_COUNT: 10, // 提供商最大错误次数
|
| 79 |
-
providerFallbackChain: {}, // 跨类型 Fallback 链配置
|
| 80 |
-
LOG_ENABLED: true,
|
| 81 |
-
LOG_OUTPUT_MODE: "all",
|
| 82 |
-
LOG_LEVEL: "info",
|
| 83 |
-
LOG_DIR: "logs",
|
| 84 |
-
LOG_INCLUDE_REQUEST_ID: true,
|
| 85 |
-
LOG_INCLUDE_TIMESTAMP: true,
|
| 86 |
-
LOG_MAX_FILE_SIZE: 10485760,
|
| 87 |
-
LOG_MAX_FILES: 10,
|
| 88 |
-
TLS_SIDECAR_ENABLED: false, // 启用 Go uTLS sidecar(需要编译 tls-sidecar 二进制)
|
| 89 |
-
TLS_SIDECAR_ENABLED_PROVIDERS: [], // 启用 TLS Sidecar 的提供商列表
|
| 90 |
-
TLS_SIDECAR_PORT: 9090, // sidecar 监听端口
|
| 91 |
-
TLS_SIDECAR_BINARY_PATH: null, // 自定义二进制路径(默认自动搜索)
|
| 92 |
-
TLS_SIDECAR_PROXY_URL: null // TLS Sidecar 专用的上游代理地址
|
| 93 |
-
};
|
| 94 |
-
|
| 95 |
-
let currentConfig = { ...defaultConfig };
|
| 96 |
-
|
| 97 |
-
try {
|
| 98 |
-
const configData = fs.readFileSync(configFilePath, 'utf8');
|
| 99 |
-
const loadedConfig = JSON.parse(configData);
|
| 100 |
-
Object.assign(currentConfig, loadedConfig);
|
| 101 |
-
logger.info('[Config] Loaded configuration from configs/config.json');
|
| 102 |
-
} catch (error) {
|
| 103 |
-
if (error.code !== 'ENOENT') {
|
| 104 |
-
logger.error('[Config Error] Failed to load configs/config.json:', error.message);
|
| 105 |
-
} else {
|
| 106 |
-
logger.info('[Config] configs/config.json not found, using default configuration.');
|
| 107 |
-
}
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
// CLI argument definitions: { flag, configKey, type, validValues? }
|
| 112 |
-
// type: 'string' | 'int' | 'bool' | 'enum'
|
| 113 |
-
const cliArgDefs = [
|
| 114 |
-
{ flag: '--api-key', configKey: 'REQUIRED_API_KEY', type: 'string' },
|
| 115 |
-
{ flag: '--log-prompts', configKey: 'PROMPT_LOG_MODE', type: 'enum', validValues: ['console', 'file'] },
|
| 116 |
-
{ flag: '--port', configKey: 'SERVER_PORT', type: 'int' },
|
| 117 |
-
{ flag: '--model-provider', configKey: 'MODEL_PROVIDER', type: 'string' },
|
| 118 |
-
{ flag: '--system-prompt-file', configKey: 'SYSTEM_PROMPT_FILE_PATH', type: 'string' },
|
| 119 |
-
{ flag: '--system-prompt-mode', configKey: 'SYSTEM_PROMPT_MODE', type: 'enum', validValues: ['overwrite', 'append'] },
|
| 120 |
-
{ flag: '--host', configKey: 'HOST', type: 'string' },
|
| 121 |
-
{ flag: '--prompt-log-base-name', configKey: 'PROMPT_LOG_BASE_NAME', type: 'string' },
|
| 122 |
-
{ flag: '--cron-near-minutes', configKey: 'CRON_NEAR_MINUTES', type: 'int' },
|
| 123 |
-
{ flag: '--cron-refresh-token', configKey: 'CRON_REFRESH_TOKEN', type: 'bool' },
|
| 124 |
-
{ flag: '--provider-pools-file', configKey: 'PROVIDER_POOLS_FILE_PATH', type: 'string' },
|
| 125 |
-
{ flag: '--max-error-count', configKey: 'MAX_ERROR_COUNT', type: 'int' },
|
| 126 |
-
{ flag: '--login-max-attempts', configKey: 'LOGIN_MAX_ATTEMPTS', type: 'int' },
|
| 127 |
-
{ flag: '--login-lockout-duration', configKey: 'LOGIN_LOCKOUT_DURATION', type: 'int' },
|
| 128 |
-
{ flag: '--login-min-interval', configKey: 'LOGIN_MIN_INTERVAL', type: 'int' },
|
| 129 |
-
];
|
| 130 |
-
|
| 131 |
-
// Parse command-line arguments using definitions
|
| 132 |
-
const flagMap = new Map(cliArgDefs.map(def => [def.flag, def]));
|
| 133 |
-
for (let i = 0; i < args.length; i++) {
|
| 134 |
-
const def = flagMap.get(args[i]);
|
| 135 |
-
if (!def) continue;
|
| 136 |
-
|
| 137 |
-
if (i + 1 >= args.length) {
|
| 138 |
-
logger.warn(`[Config Warning] ${def.flag} flag requires a value.`);
|
| 139 |
-
continue;
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
const rawValue = args[++i];
|
| 143 |
-
switch (def.type) {
|
| 144 |
-
case 'string':
|
| 145 |
-
currentConfig[def.configKey] = rawValue;
|
| 146 |
-
break;
|
| 147 |
-
case 'int':
|
| 148 |
-
currentConfig[def.configKey] = parseInt(rawValue, 10);
|
| 149 |
-
break;
|
| 150 |
-
case 'bool':
|
| 151 |
-
currentConfig[def.configKey] = rawValue.toLowerCase() === 'true';
|
| 152 |
-
break;
|
| 153 |
-
case 'enum':
|
| 154 |
-
if (def.validValues.includes(rawValue)) {
|
| 155 |
-
currentConfig[def.configKey] = rawValue;
|
| 156 |
-
} else {
|
| 157 |
-
logger.warn(`[Config Warning] Invalid value for ${def.flag}. Expected one of: ${def.validValues.join(', ')}.`);
|
| 158 |
-
}
|
| 159 |
-
break;
|
| 160 |
-
}
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
normalizeConfiguredProviders(currentConfig);
|
| 164 |
-
|
| 165 |
-
if (!currentConfig.SYSTEM_PROMPT_FILE_PATH) {
|
| 166 |
-
currentConfig.SYSTEM_PROMPT_FILE_PATH = INPUT_SYSTEM_PROMPT_FILE;
|
| 167 |
-
}
|
| 168 |
-
currentConfig.SYSTEM_PROMPT_CONTENT = await getSystemPromptFileContent(currentConfig.SYSTEM_PROMPT_FILE_PATH);
|
| 169 |
-
|
| 170 |
-
// 加载号池配置
|
| 171 |
-
if (!currentConfig.PROVIDER_POOLS_FILE_PATH) {
|
| 172 |
-
currentConfig.PROVIDER_POOLS_FILE_PATH = 'configs/provider_pools.json';
|
| 173 |
-
}
|
| 174 |
-
if (currentConfig.PROVIDER_POOLS_FILE_PATH) {
|
| 175 |
-
try {
|
| 176 |
-
const poolsData = await pfs.readFile(currentConfig.PROVIDER_POOLS_FILE_PATH, 'utf8');
|
| 177 |
-
currentConfig.providerPools = JSON.parse(poolsData);
|
| 178 |
-
logger.info(`[Config] Loaded provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}`);
|
| 179 |
-
} catch (error) {
|
| 180 |
-
logger.error(`[Config Error] Failed to load provider pools from ${currentConfig.PROVIDER_POOLS_FILE_PATH}: ${error.message}`);
|
| 181 |
-
currentConfig.providerPools = {};
|
| 182 |
-
}
|
| 183 |
-
} else {
|
| 184 |
-
currentConfig.providerPools = {};
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
// Set PROMPT_LOG_FILENAME based on the determined config
|
| 188 |
-
if (currentConfig.PROMPT_LOG_MODE === 'file') {
|
| 189 |
-
const now = new Date();
|
| 190 |
-
const pad = (num) => String(num).padStart(2, '0');
|
| 191 |
-
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
| 192 |
-
PROMPT_LOG_FILENAME = `${currentConfig.PROMPT_LOG_BASE_NAME}-${timestamp}.log`;
|
| 193 |
-
} else {
|
| 194 |
-
PROMPT_LOG_FILENAME = ''; // Clear if not logging to file
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
// Assign to the exported CONFIG
|
| 198 |
-
Object.assign(CONFIG, currentConfig);
|
| 199 |
-
|
| 200 |
-
// Initialize logger
|
| 201 |
-
logger.initialize({
|
| 202 |
-
enabled: CONFIG.LOG_ENABLED ?? true,
|
| 203 |
-
outputMode: CONFIG.LOG_OUTPUT_MODE || "all",
|
| 204 |
-
logLevel: CONFIG.LOG_LEVEL || "info",
|
| 205 |
-
logDir: CONFIG.LOG_DIR || "logs",
|
| 206 |
-
includeRequestId: CONFIG.LOG_INCLUDE_REQUEST_ID ?? true,
|
| 207 |
-
includeTimestamp: CONFIG.LOG_INCLUDE_TIMESTAMP ?? true,
|
| 208 |
-
maxFileSize: CONFIG.LOG_MAX_FILE_SIZE || 10485760,
|
| 209 |
-
maxFiles: CONFIG.LOG_MAX_FILES || 10
|
| 210 |
-
});
|
| 211 |
-
|
| 212 |
-
// Cleanup old logs periodically
|
| 213 |
-
logger.cleanupOldLogs();
|
| 214 |
-
|
| 215 |
-
return CONFIG;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
/**
|
| 219 |
-
* Gets system prompt content from the specified file path.
|
| 220 |
-
* @param {string} filePath - Path to the system prompt file.
|
| 221 |
-
* @returns {Promise<string|null>} File content, or null if the file does not exist, is empty, or an error occurs.
|
| 222 |
-
*/
|
| 223 |
-
export async function getSystemPromptFileContent(filePath) {
|
| 224 |
-
try {
|
| 225 |
-
await pfs.access(filePath, pfs.constants.F_OK);
|
| 226 |
-
} catch (error) {
|
| 227 |
-
if (error.code === 'ENOENT') {
|
| 228 |
-
logger.warn(`[System Prompt] Specified system prompt file not found: ${filePath}`);
|
| 229 |
-
} else {
|
| 230 |
-
logger.error(`[System Prompt] Error accessing system prompt file ${filePath}: ${error.message}`);
|
| 231 |
-
}
|
| 232 |
-
return null;
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
try {
|
| 236 |
-
const content = await pfs.readFile(filePath, 'utf8');
|
| 237 |
-
if (!content.trim()) {
|
| 238 |
-
return null;
|
| 239 |
-
}
|
| 240 |
-
logger.info(`[System Prompt] Loaded system prompt from ${filePath}`);
|
| 241 |
-
return content;
|
| 242 |
-
} catch (error) {
|
| 243 |
-
logger.error(`[System Prompt] Error reading system prompt file ${filePath}: ${error.message}`);
|
| 244 |
-
return null;
|
| 245 |
-
}
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
export { ALL_MODEL_PROVIDERS };
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/master.js
DELETED
|
@@ -1,395 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 主进程 (Master Process)
|
| 3 |
-
*
|
| 4 |
-
* 负责管理子进程的生命周期,包括:
|
| 5 |
-
* - 启动子进程
|
| 6 |
-
* - 监控子进程状态
|
| 7 |
-
* - 处理子进程重启请求
|
| 8 |
-
* - 提供 IPC 通信
|
| 9 |
-
*
|
| 10 |
-
* 使用方式:
|
| 11 |
-
* node src/core/master.js [原有的命令行参数]
|
| 12 |
-
*/
|
| 13 |
-
|
| 14 |
-
import { fork } from 'child_process';
|
| 15 |
-
import logger from '../utils/logger.js';
|
| 16 |
-
import * as http from 'http';
|
| 17 |
-
import * as path from 'path';
|
| 18 |
-
import { fileURLToPath } from 'url';
|
| 19 |
-
import { isRetryableNetworkError } from '../utils/common.js';
|
| 20 |
-
|
| 21 |
-
const __filename = fileURLToPath(import.meta.url);
|
| 22 |
-
const __dirname = path.dirname(__filename);
|
| 23 |
-
|
| 24 |
-
// 子进程实例
|
| 25 |
-
let workerProcess = null;
|
| 26 |
-
|
| 27 |
-
// 子进程状态
|
| 28 |
-
let workerStatus = {
|
| 29 |
-
pid: null,
|
| 30 |
-
startTime: null,
|
| 31 |
-
restartCount: 0,
|
| 32 |
-
lastRestartTime: null,
|
| 33 |
-
isRestarting: false
|
| 34 |
-
};
|
| 35 |
-
|
| 36 |
-
// 配置
|
| 37 |
-
const config = {
|
| 38 |
-
workerScript: path.join(__dirname, '../services/api-server.js'),
|
| 39 |
-
maxRestartAttempts: 10,
|
| 40 |
-
restartDelay: 1000, // 重启延迟(毫秒)
|
| 41 |
-
masterPort: parseInt(process.env.MASTER_PORT) || 3100, // 主进程管理端口
|
| 42 |
-
args: process.argv.slice(2) // 传递给子进程的参数
|
| 43 |
-
};
|
| 44 |
-
|
| 45 |
-
/**
|
| 46 |
-
* 启动子进程
|
| 47 |
-
*/
|
| 48 |
-
function startWorker() {
|
| 49 |
-
if (workerProcess) {
|
| 50 |
-
logger.info('[Master] Worker process already running, PID:', workerProcess.pid);
|
| 51 |
-
return;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
logger.info('[Master] Starting worker process...');
|
| 55 |
-
logger.info('[Master] Worker script:', config.workerScript);
|
| 56 |
-
logger.info('[Master] Worker args:', config.args.join(' '));
|
| 57 |
-
|
| 58 |
-
workerProcess = fork(config.workerScript, config.args, {
|
| 59 |
-
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
| 60 |
-
env: {
|
| 61 |
-
...process.env,
|
| 62 |
-
IS_WORKER_PROCESS: 'true'
|
| 63 |
-
}
|
| 64 |
-
});
|
| 65 |
-
|
| 66 |
-
workerStatus.pid = workerProcess.pid;
|
| 67 |
-
workerStatus.startTime = new Date().toISOString();
|
| 68 |
-
|
| 69 |
-
logger.info('[Master] Worker process started, PID:', workerProcess.pid);
|
| 70 |
-
|
| 71 |
-
// 监听子进程消息
|
| 72 |
-
workerProcess.on('message', (message) => {
|
| 73 |
-
logger.info('[Master] Received message from worker:', message);
|
| 74 |
-
handleWorkerMessage(message);
|
| 75 |
-
});
|
| 76 |
-
|
| 77 |
-
// 监听子进程退出
|
| 78 |
-
workerProcess.on('exit', (code, signal) => {
|
| 79 |
-
logger.info(`[Master] Worker process exited with code ${code}, signal ${signal}`);
|
| 80 |
-
workerProcess = null;
|
| 81 |
-
workerStatus.pid = null;
|
| 82 |
-
|
| 83 |
-
// 如果不是主动重启导致的退出,尝试自动重启
|
| 84 |
-
if (!workerStatus.isRestarting && code !== 0) {
|
| 85 |
-
logger.info('[Master] Worker crashed, attempting auto-restart...');
|
| 86 |
-
scheduleRestart();
|
| 87 |
-
}
|
| 88 |
-
});
|
| 89 |
-
|
| 90 |
-
// 监听子进程错误
|
| 91 |
-
workerProcess.on('error', (error) => {
|
| 92 |
-
logger.error('[Master] Worker process error:', error.message);
|
| 93 |
-
});
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
/**
|
| 97 |
-
* 停止子进程
|
| 98 |
-
* @param {boolean} graceful - 是否优雅关闭
|
| 99 |
-
* @returns {Promise<void>}
|
| 100 |
-
*/
|
| 101 |
-
function stopWorker(graceful = true) {
|
| 102 |
-
return new Promise((resolve) => {
|
| 103 |
-
if (!workerProcess) {
|
| 104 |
-
logger.info('[Master] No worker process to stop');
|
| 105 |
-
resolve();
|
| 106 |
-
return;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
logger.info('[Master] Stopping worker process, PID:', workerProcess.pid);
|
| 110 |
-
|
| 111 |
-
const timeout = setTimeout(() => {
|
| 112 |
-
if (workerProcess) {
|
| 113 |
-
logger.info('[Master] Force killing worker process...');
|
| 114 |
-
workerProcess.kill('SIGKILL');
|
| 115 |
-
}
|
| 116 |
-
resolve();
|
| 117 |
-
}, 5000); // 5秒超时后强制杀死
|
| 118 |
-
|
| 119 |
-
workerProcess.once('exit', () => {
|
| 120 |
-
clearTimeout(timeout);
|
| 121 |
-
workerProcess = null;
|
| 122 |
-
workerStatus.pid = null;
|
| 123 |
-
logger.info('[Master] Worker process stopped');
|
| 124 |
-
resolve();
|
| 125 |
-
});
|
| 126 |
-
|
| 127 |
-
if (graceful) {
|
| 128 |
-
// 发送优雅关闭信号
|
| 129 |
-
workerProcess.send({ type: 'shutdown' });
|
| 130 |
-
workerProcess.kill('SIGTERM');
|
| 131 |
-
} else {
|
| 132 |
-
workerProcess.kill('SIGKILL');
|
| 133 |
-
}
|
| 134 |
-
});
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
/**
|
| 138 |
-
* 重启子进程
|
| 139 |
-
* @returns {Promise<Object>}
|
| 140 |
-
*/
|
| 141 |
-
async function restartWorker() {
|
| 142 |
-
if (workerStatus.isRestarting) {
|
| 143 |
-
logger.info('[Master] Restart already in progress');
|
| 144 |
-
return { success: false, message: 'Restart already in progress' };
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
workerStatus.isRestarting = true;
|
| 148 |
-
workerStatus.restartCount++;
|
| 149 |
-
workerStatus.lastRestartTime = new Date().toISOString();
|
| 150 |
-
|
| 151 |
-
logger.info('[Master] Restarting worker process...');
|
| 152 |
-
|
| 153 |
-
try {
|
| 154 |
-
await stopWorker(true);
|
| 155 |
-
|
| 156 |
-
// 等待一小段时间确保端口释放
|
| 157 |
-
await new Promise(resolve => setTimeout(resolve, config.restartDelay));
|
| 158 |
-
|
| 159 |
-
startWorker();
|
| 160 |
-
workerStatus.isRestarting = false;
|
| 161 |
-
|
| 162 |
-
return {
|
| 163 |
-
success: true,
|
| 164 |
-
message: 'Worker restarted successfully',
|
| 165 |
-
pid: workerStatus.pid,
|
| 166 |
-
restartCount: workerStatus.restartCount
|
| 167 |
-
};
|
| 168 |
-
} catch (error) {
|
| 169 |
-
workerStatus.isRestarting = false;
|
| 170 |
-
logger.error('[Master] Failed to restart worker:', error.message);
|
| 171 |
-
return {
|
| 172 |
-
success: false,
|
| 173 |
-
message: 'Failed to restart worker: ' + error.message
|
| 174 |
-
};
|
| 175 |
-
}
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
/**
|
| 179 |
-
* 计划重启(用于崩溃后自动重启)
|
| 180 |
-
*/
|
| 181 |
-
function scheduleRestart() {
|
| 182 |
-
if (workerStatus.restartCount >= config.maxRestartAttempts) {
|
| 183 |
-
logger.error('[Master] Max restart attempts reached, giving up');
|
| 184 |
-
return;
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
const delay = Math.min(config.restartDelay * Math.pow(2, workerStatus.restartCount), 30000);
|
| 188 |
-
logger.info(`[Master] Scheduling restart in ${delay}ms...`);
|
| 189 |
-
|
| 190 |
-
setTimeout(() => {
|
| 191 |
-
restartWorker();
|
| 192 |
-
}, delay);
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
/**
|
| 196 |
-
* 处理来自子进程的消息
|
| 197 |
-
* @param {Object} message - 消息对象
|
| 198 |
-
*/
|
| 199 |
-
function handleWorkerMessage(message) {
|
| 200 |
-
if (!message || !message.type) return;
|
| 201 |
-
|
| 202 |
-
switch (message.type) {
|
| 203 |
-
case 'ready':
|
| 204 |
-
logger.info('[Master] Worker is ready');
|
| 205 |
-
break;
|
| 206 |
-
case 'restart_request':
|
| 207 |
-
logger.info('[Master] Worker requested restart');
|
| 208 |
-
restartWorker();
|
| 209 |
-
break;
|
| 210 |
-
case 'status':
|
| 211 |
-
logger.info('[Master] Worker status:', message.data);
|
| 212 |
-
break;
|
| 213 |
-
default:
|
| 214 |
-
logger.info('[Master] Unknown message type:', message.type);
|
| 215 |
-
}
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
/**
|
| 219 |
-
* 获取状态信息
|
| 220 |
-
* @returns {Object}
|
| 221 |
-
*/
|
| 222 |
-
function getStatus() {
|
| 223 |
-
return {
|
| 224 |
-
master: {
|
| 225 |
-
pid: process.pid,
|
| 226 |
-
uptime: process.uptime(),
|
| 227 |
-
memoryUsage: process.memoryUsage()
|
| 228 |
-
},
|
| 229 |
-
worker: {
|
| 230 |
-
pid: workerStatus.pid,
|
| 231 |
-
startTime: workerStatus.startTime,
|
| 232 |
-
restartCount: workerStatus.restartCount,
|
| 233 |
-
lastRestartTime: workerStatus.lastRestartTime,
|
| 234 |
-
isRestarting: workerStatus.isRestarting,
|
| 235 |
-
isRunning: workerProcess !== null
|
| 236 |
-
}
|
| 237 |
-
};
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
/**
|
| 241 |
-
* 创建主进程管理 HTTP 服务器
|
| 242 |
-
*/
|
| 243 |
-
function createMasterServer() {
|
| 244 |
-
const server = http.createServer(async (req, res) => {
|
| 245 |
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
| 246 |
-
const path = url.pathname;
|
| 247 |
-
const method = req.method;
|
| 248 |
-
|
| 249 |
-
// 设置 CORS 头
|
| 250 |
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 251 |
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
| 252 |
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
| 253 |
-
|
| 254 |
-
if (method === 'OPTIONS') {
|
| 255 |
-
res.writeHead(204);
|
| 256 |
-
res.end();
|
| 257 |
-
return;
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
// 状态端点
|
| 261 |
-
if (method === 'GET' && path === '/master/status') {
|
| 262 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 263 |
-
res.end(JSON.stringify(getStatus()));
|
| 264 |
-
return;
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
// 重启端点
|
| 268 |
-
if (method === 'POST' && path === '/master/restart') {
|
| 269 |
-
logger.info('[Master] Restart requested via API');
|
| 270 |
-
const result = await restartWorker();
|
| 271 |
-
res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' });
|
| 272 |
-
res.end(JSON.stringify(result));
|
| 273 |
-
return;
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
// 停止端点
|
| 277 |
-
if (method === 'POST' && path === '/master/stop') {
|
| 278 |
-
logger.info('[Master] Stop requested via API');
|
| 279 |
-
await stopWorker(true);
|
| 280 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 281 |
-
res.end(JSON.stringify({ success: true, message: 'Worker stopped' }));
|
| 282 |
-
return;
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
// 启动端点
|
| 286 |
-
if (method === 'POST' && path === '/master/start') {
|
| 287 |
-
logger.info('[Master] Start requested via API');
|
| 288 |
-
if (workerProcess) {
|
| 289 |
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
| 290 |
-
res.end(JSON.stringify({ success: false, message: 'Worker already running' }));
|
| 291 |
-
return;
|
| 292 |
-
}
|
| 293 |
-
startWorker();
|
| 294 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 295 |
-
res.end(JSON.stringify({ success: true, message: 'Worker started', pid: workerStatus.pid }));
|
| 296 |
-
return;
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
// 健康检查
|
| 300 |
-
if (method === 'GET' && path === '/master/health') {
|
| 301 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 302 |
-
res.end(JSON.stringify({
|
| 303 |
-
status: 'healthy',
|
| 304 |
-
workerRunning: workerProcess !== null,
|
| 305 |
-
timestamp: new Date().toISOString()
|
| 306 |
-
}));
|
| 307 |
-
return;
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
-
// 404
|
| 311 |
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
| 312 |
-
res.end(JSON.stringify({ error: 'Not Found' }));
|
| 313 |
-
});
|
| 314 |
-
|
| 315 |
-
server.listen(config.masterPort, () => {
|
| 316 |
-
logger.info(`[Master] Management server listening on port ${config.masterPort}`);
|
| 317 |
-
logger.info(`[Master] Available endpoints:`);
|
| 318 |
-
logger.info(` GET /master/status - Get master and worker status`);
|
| 319 |
-
logger.info(` GET /master/health - Health check`);
|
| 320 |
-
logger.info(` POST /master/restart - Restart worker process`);
|
| 321 |
-
logger.info(` POST /master/stop - Stop worker process`);
|
| 322 |
-
logger.info(` POST /master/start - Start worker process`);
|
| 323 |
-
});
|
| 324 |
-
|
| 325 |
-
return server;
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
/**
|
| 329 |
-
* 处理进程信号
|
| 330 |
-
*/
|
| 331 |
-
function setupSignalHandlers() {
|
| 332 |
-
// 优雅关闭
|
| 333 |
-
process.on('SIGTERM', async () => {
|
| 334 |
-
logger.info('[Master] Received SIGTERM, shutting down...');
|
| 335 |
-
await stopWorker(true);
|
| 336 |
-
process.exit(0);
|
| 337 |
-
});
|
| 338 |
-
|
| 339 |
-
process.on('SIGINT', async () => {
|
| 340 |
-
logger.info('[Master] Received SIGINT, shutting down...');
|
| 341 |
-
await stopWorker(true);
|
| 342 |
-
process.exit(0);
|
| 343 |
-
});
|
| 344 |
-
|
| 345 |
-
// 未捕获的异常
|
| 346 |
-
process.on('uncaughtException', (error) => {
|
| 347 |
-
logger.error('[Master] Uncaught exception:', error);
|
| 348 |
-
|
| 349 |
-
// 检查是否为可重试的网络错误
|
| 350 |
-
if (isRetryableNetworkError(error)) {
|
| 351 |
-
logger.warn('[Master] Network error detected, continuing operation...');
|
| 352 |
-
return; // 不退出程序,继续运行
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
// 对于其他严重错误,记录但不退出(由主进程管理子进程)
|
| 356 |
-
logger.error('[Master] Fatal error detected in master process');
|
| 357 |
-
});
|
| 358 |
-
|
| 359 |
-
process.on('unhandledRejection', (reason, promise) => {
|
| 360 |
-
logger.error('[Master] Unhandled rejection at:', promise, 'reason:', reason);
|
| 361 |
-
|
| 362 |
-
// 检查是否为可重试的网络错误
|
| 363 |
-
if (reason && isRetryableNetworkError(reason)) {
|
| 364 |
-
logger.warn('[Master] Network error in promise rejection, continuing operation...');
|
| 365 |
-
return; // 不退出程序,继续运行
|
| 366 |
-
}
|
| 367 |
-
});
|
| 368 |
-
}
|
| 369 |
-
|
| 370 |
-
/**
|
| 371 |
-
* 主函数
|
| 372 |
-
*/
|
| 373 |
-
async function main() {
|
| 374 |
-
logger.info('='.repeat(50));
|
| 375 |
-
logger.info('[Master] AIClient2API Master Process');
|
| 376 |
-
logger.info('[Master] PID:', process.pid);
|
| 377 |
-
logger.info('[Master] Node version:', process.version);
|
| 378 |
-
logger.info('[Master] Working directory:', process.cwd());
|
| 379 |
-
logger.info('='.repeat(50));
|
| 380 |
-
|
| 381 |
-
// 设置信号处理
|
| 382 |
-
setupSignalHandlers();
|
| 383 |
-
|
| 384 |
-
// 创建管理服务器
|
| 385 |
-
createMasterServer();
|
| 386 |
-
|
| 387 |
-
// 启动子进程
|
| 388 |
-
startWorker();
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
// 启动主进程
|
| 392 |
-
main().catch(error => {
|
| 393 |
-
logger.error('[Master] Failed to start:', error);
|
| 394 |
-
process.exit(1);
|
| 395 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/core/plugin-manager.js
DELETED
|
@@ -1,549 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 插件管理器 - 可插拔插件系统核心
|
| 3 |
-
*
|
| 4 |
-
* 功能:
|
| 5 |
-
* 1. 插件注册与加载
|
| 6 |
-
* 2. 生命周期管理(init/destroy)
|
| 7 |
-
* 3. 扩展点管理(中间件、路由、钩子)
|
| 8 |
-
* 4. 插件配置管理
|
| 9 |
-
*/
|
| 10 |
-
|
| 11 |
-
import { promises as fs } from 'fs';
|
| 12 |
-
import logger from '../utils/logger.js';
|
| 13 |
-
import { existsSync } from 'fs';
|
| 14 |
-
import path from 'path';
|
| 15 |
-
|
| 16 |
-
// 插件配置文件路径
|
| 17 |
-
const PLUGINS_CONFIG_FILE = path.join(process.cwd(), 'configs', 'plugins.json');
|
| 18 |
-
|
| 19 |
-
// 默认禁用的插件列表
|
| 20 |
-
const DEFAULT_DISABLED_PLUGINS = ['api-potluck', 'ai-monitor'];
|
| 21 |
-
|
| 22 |
-
/**
|
| 23 |
-
* 插件类型常量
|
| 24 |
-
*/
|
| 25 |
-
export const PLUGIN_TYPE = {
|
| 26 |
-
AUTH: 'auth', // 认证插件,参与认证流程
|
| 27 |
-
MIDDLEWARE: 'middleware' // 普通中间件,不参与认证
|
| 28 |
-
};
|
| 29 |
-
|
| 30 |
-
/**
|
| 31 |
-
* 插件接口定义(JSDoc 类型)
|
| 32 |
-
* @typedef {Object} Plugin
|
| 33 |
-
* @property {string} name - 插件名称(唯一标识)
|
| 34 |
-
* @property {string} version - 插件版本
|
| 35 |
-
* @property {string} [description] - 插件描述
|
| 36 |
-
* @property {string} [type] - 插件类型:'auth'(认证插件)或 'middleware'(普通中间件,默认)
|
| 37 |
-
* @property {boolean} [enabled] - 是否启用(默认 true)
|
| 38 |
-
* @property {number} [_priority] - 优先级,数字越小越先执行(默认 100)
|
| 39 |
-
* @property {boolean} [_builtin] - 是否为内置插件(内置插件最后执行)
|
| 40 |
-
* @property {Function} [init] - 初始化钩子 (config) => Promise<void>
|
| 41 |
-
* @property {Function} [destroy] - 销毁钩子 () => Promise<void>
|
| 42 |
-
* @property {Function} [middleware] - 请求中间件 (req, res, requestUrl, config) => Promise<{handled: boolean, data?: Object}>
|
| 43 |
-
* @property {Function} [authenticate] - 认证方法(仅 type='auth' 时有效)(req, res, requestUrl, config) => Promise<{handled: boolean, authorized: boolean|null, error?: Object, data?: Object}>
|
| 44 |
-
* @property {Array<{method: string, path: string|RegExp, handler: Function}>} [routes] - 路由定义
|
| 45 |
-
* @property {string[]} [staticPaths] - 静态文件路径(相对于 static 目录)
|
| 46 |
-
* @property {Object} [hooks] - 钩子函数
|
| 47 |
-
* @property {Function} [hooks.onBeforeRequest] - 请求前钩子 (req, config) => Promise<void>
|
| 48 |
-
* @property {Function} [hooks.onAfterResponse] - 响应后钩子 (req, res, config) => Promise<void>
|
| 49 |
-
* @property {Function} [hooks.onContentGenerated] - 内容生成后钩子 (config) => Promise<void>
|
| 50 |
-
*/
|
| 51 |
-
|
| 52 |
-
/**
|
| 53 |
-
* 插件管理器类
|
| 54 |
-
*/
|
| 55 |
-
class PluginManager {
|
| 56 |
-
constructor() {
|
| 57 |
-
/** @type {Map<string, Plugin>} */
|
| 58 |
-
this.plugins = new Map();
|
| 59 |
-
/** @type {Object} */
|
| 60 |
-
this.pluginsConfig = { plugins: {} };
|
| 61 |
-
/** @type {boolean} */
|
| 62 |
-
this.initialized = false;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
/**
|
| 66 |
-
* 加载插件配置文件
|
| 67 |
-
* 永远生成默认配置,如果本地文件存在则合并,但不覆盖 enabled 字段
|
| 68 |
-
*/
|
| 69 |
-
async loadConfig() {
|
| 70 |
-
try {
|
| 71 |
-
// 1. 永远生成默认配置
|
| 72 |
-
const defaultConfig = await this.generateDefaultConfig();
|
| 73 |
-
|
| 74 |
-
// 2. 如果本地文件存在,读取并合并
|
| 75 |
-
if (existsSync(PLUGINS_CONFIG_FILE)) {
|
| 76 |
-
const content = await fs.readFile(PLUGINS_CONFIG_FILE, 'utf8');
|
| 77 |
-
const localConfig = JSON.parse(content);
|
| 78 |
-
|
| 79 |
-
// 3. 合并配置:遍历默认配置中的所有插件
|
| 80 |
-
for (const [pluginName, defaultPluginConfig] of Object.entries(defaultConfig.plugins)) {
|
| 81 |
-
const localPluginConfig = localConfig.plugins?.[pluginName];
|
| 82 |
-
|
| 83 |
-
if (localPluginConfig) {
|
| 84 |
-
// 本地配置存在,合并但保留本地的 enabled 字段
|
| 85 |
-
defaultConfig.plugins[pluginName] = {
|
| 86 |
-
...defaultPluginConfig,
|
| 87 |
-
...localPluginConfig,
|
| 88 |
-
enabled: localPluginConfig.enabled // 保留本地的 enabled 字段
|
| 89 |
-
};
|
| 90 |
-
}
|
| 91 |
-
// 如果本地配置不存在该插件,使用默认配置
|
| 92 |
-
}
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
this.pluginsConfig = defaultConfig;
|
| 96 |
-
await this.saveConfig();
|
| 97 |
-
} catch (error) {
|
| 98 |
-
logger.error('[PluginManager] Failed to load config:', error.message);
|
| 99 |
-
this.pluginsConfig = { plugins: {} };
|
| 100 |
-
}
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
/**
|
| 104 |
-
* 扫描 plugins 目录生成默认配置
|
| 105 |
-
* @returns {Promise<Object>} 默认插件配置
|
| 106 |
-
*/
|
| 107 |
-
async generateDefaultConfig() {
|
| 108 |
-
const defaultConfig = { plugins: {} };
|
| 109 |
-
const pluginsDir = path.join(process.cwd(), 'src', 'plugins');
|
| 110 |
-
|
| 111 |
-
try {
|
| 112 |
-
if (!existsSync(pluginsDir)) {
|
| 113 |
-
return defaultConfig;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
|
| 117 |
-
|
| 118 |
-
for (const entry of entries) {
|
| 119 |
-
if (!entry.isDirectory()) continue;
|
| 120 |
-
|
| 121 |
-
const pluginPath = path.join(pluginsDir, entry.name, 'index.js');
|
| 122 |
-
if (!existsSync(pluginPath)) continue;
|
| 123 |
-
|
| 124 |
-
try {
|
| 125 |
-
// 动态导入插件以获取其元信息
|
| 126 |
-
const pluginModule = await import(`file://${pluginPath}`);
|
| 127 |
-
const plugin = pluginModule.default || pluginModule;
|
| 128 |
-
|
| 129 |
-
if (plugin && plugin.name) {
|
| 130 |
-
// 检查是否在默认禁用列表中
|
| 131 |
-
const enabled = !DEFAULT_DISABLED_PLUGINS.includes(plugin.name);
|
| 132 |
-
defaultConfig.plugins[plugin.name] = {
|
| 133 |
-
enabled: enabled,
|
| 134 |
-
description: plugin.description || ''
|
| 135 |
-
};
|
| 136 |
-
logger.info(`[PluginManager] Found plugin for default config: ${plugin.name}`);
|
| 137 |
-
}
|
| 138 |
-
} catch (importError) {
|
| 139 |
-
// 如果导入失败,使用目录名作为插件名
|
| 140 |
-
// 检查是否在默认禁用列表中
|
| 141 |
-
const enabled = !DEFAULT_DISABLED_PLUGINS.includes(entry.name);
|
| 142 |
-
defaultConfig.plugins[entry.name] = {
|
| 143 |
-
enabled: enabled,
|
| 144 |
-
description: ''
|
| 145 |
-
};
|
| 146 |
-
logger.warn(`[PluginManager] Could not import plugin ${entry.name}, using directory name:`, importError.message);
|
| 147 |
-
}
|
| 148 |
-
}
|
| 149 |
-
} catch (error) {
|
| 150 |
-
logger.error('[PluginManager] Failed to scan plugins directory:', error.message);
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
return defaultConfig;
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
/**
|
| 157 |
-
* 保存插件配置文件
|
| 158 |
-
*/
|
| 159 |
-
async saveConfig() {
|
| 160 |
-
try {
|
| 161 |
-
const dir = path.dirname(PLUGINS_CONFIG_FILE);
|
| 162 |
-
if (!existsSync(dir)) {
|
| 163 |
-
await fs.mkdir(dir, { recursive: true });
|
| 164 |
-
}
|
| 165 |
-
await fs.writeFile(PLUGINS_CONFIG_FILE, JSON.stringify(this.pluginsConfig, null, 2), 'utf8');
|
| 166 |
-
} catch (error) {
|
| 167 |
-
logger.error('[PluginManager] Failed to save config:', error.message);
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
/**
|
| 172 |
-
* 注册插件
|
| 173 |
-
* @param {Plugin} plugin - 插件对象
|
| 174 |
-
*/
|
| 175 |
-
register(plugin) {
|
| 176 |
-
if (!plugin.name) {
|
| 177 |
-
throw new Error('Plugin must have a name');
|
| 178 |
-
}
|
| 179 |
-
if (this.plugins.has(plugin.name)) {
|
| 180 |
-
logger.warn(`[PluginManager] Plugin "${plugin.name}" is already registered, skipping`);
|
| 181 |
-
return;
|
| 182 |
-
}
|
| 183 |
-
this.plugins.set(plugin.name, plugin);
|
| 184 |
-
logger.info(`[PluginManager] Registered plugin: ${plugin.name} v${plugin.version || '1.0.0'}`);
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
/**
|
| 188 |
-
* 初始化所有已启用的插件
|
| 189 |
-
* @param {Object} config - 服务器配置
|
| 190 |
-
*/
|
| 191 |
-
async initAll(config) {
|
| 192 |
-
await this.loadConfig();
|
| 193 |
-
|
| 194 |
-
for (const [name, plugin] of this.plugins) {
|
| 195 |
-
const pluginConfig = this.pluginsConfig.plugins[name] || {};
|
| 196 |
-
const enabled = pluginConfig.enabled !== false; // 默认启用
|
| 197 |
-
|
| 198 |
-
if (!enabled) {
|
| 199 |
-
logger.info(`[PluginManager] Plugin "${name}" is disabled, skipping init`);
|
| 200 |
-
continue;
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
try {
|
| 204 |
-
if (typeof plugin.init === 'function') {
|
| 205 |
-
await plugin.init(config);
|
| 206 |
-
logger.info(`[PluginManager] Initialized plugin: ${name}`);
|
| 207 |
-
}
|
| 208 |
-
plugin._enabled = true;
|
| 209 |
-
} catch (error) {
|
| 210 |
-
logger.error(`[PluginManager] Failed to init plugin "${name}":`, error.message);
|
| 211 |
-
plugin._enabled = false;
|
| 212 |
-
}
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
this.initialized = true;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
/**
|
| 219 |
-
* 销毁所有插件
|
| 220 |
-
*/
|
| 221 |
-
async destroyAll() {
|
| 222 |
-
for (const [name, plugin] of this.plugins) {
|
| 223 |
-
if (!plugin._enabled) continue;
|
| 224 |
-
|
| 225 |
-
try {
|
| 226 |
-
if (typeof plugin.destroy === 'function') {
|
| 227 |
-
await plugin.destroy();
|
| 228 |
-
logger.info(`[PluginManager] Destroyed plugin: ${name}`);
|
| 229 |
-
}
|
| 230 |
-
} catch (error) {
|
| 231 |
-
logger.error(`[PluginManager] Failed to destroy plugin "${name}":`, error.message);
|
| 232 |
-
}
|
| 233 |
-
}
|
| 234 |
-
this.initialized = false;
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
/**
|
| 238 |
-
* 检查插件是否启用
|
| 239 |
-
* @param {string} name - 插件名称
|
| 240 |
-
* @returns {boolean}
|
| 241 |
-
*/
|
| 242 |
-
isEnabled(name) {
|
| 243 |
-
const plugin = this.plugins.get(name);
|
| 244 |
-
return plugin && plugin._enabled === true;
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
/**
|
| 248 |
-
* 获取所有启用的插件(按优先级排序)
|
| 249 |
-
* 优先级数字越小越先执行,内置插件(_builtin: true)最后执行
|
| 250 |
-
* @returns {Plugin[]}
|
| 251 |
-
*/
|
| 252 |
-
getEnabledPlugins() {
|
| 253 |
-
return Array.from(this.plugins.values())
|
| 254 |
-
.filter(p => p._enabled)
|
| 255 |
-
.sort((a, b) => {
|
| 256 |
-
// 内置插件排在最后
|
| 257 |
-
const aBuiltin = a._builtin ? 1 : 0;
|
| 258 |
-
const bBuiltin = b._builtin ? 1 : 0;
|
| 259 |
-
if (aBuiltin !== bBuiltin) return aBuiltin - bBuiltin;
|
| 260 |
-
|
| 261 |
-
// 按优先级排序(数字越小越先执行)
|
| 262 |
-
const aPriority = a._priority || 100;
|
| 263 |
-
const bPriority = b._priority || 100;
|
| 264 |
-
return aPriority - bPriority;
|
| 265 |
-
});
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
/**
|
| 269 |
-
* 获取所有认证插件(按优先级排序)
|
| 270 |
-
* @returns {Plugin[]}
|
| 271 |
-
*/
|
| 272 |
-
getAuthPlugins() {
|
| 273 |
-
return this.getEnabledPlugins().filter(p =>
|
| 274 |
-
p.type === PLUGIN_TYPE.AUTH && typeof p.authenticate === 'function'
|
| 275 |
-
);
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
/**
|
| 279 |
-
* 获取所有普通中间件插件(按优先级排序)
|
| 280 |
-
* @returns {Plugin[]}
|
| 281 |
-
*/
|
| 282 |
-
getMiddlewarePlugins() {
|
| 283 |
-
return this.getEnabledPlugins().filter(p =>
|
| 284 |
-
p.type !== PLUGIN_TYPE.AUTH && typeof p.middleware === 'function'
|
| 285 |
-
);
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
/**
|
| 289 |
-
* 执行认证流程
|
| 290 |
-
* 只有 type='auth' 的插件会参与认证
|
| 291 |
-
*
|
| 292 |
-
* 认证插件返回值说明:
|
| 293 |
-
* - { handled: true } - 请求已被处理(如发送了错误响应),停止后续处理
|
| 294 |
-
* - { authorized: true, data: {...} } - 认证成功,可附带数据
|
| 295 |
-
* - { authorized: false } - 认证失败,已发送错误响应
|
| 296 |
-
* - { authorized: null } - 此插件不处理该请求,继续下一个认证插件
|
| 297 |
-
*
|
| 298 |
-
* @param {http.IncomingMessage} req - HTTP 请求
|
| 299 |
-
* @param {http.ServerResponse} res - HTTP 响应
|
| 300 |
-
* @param {URL} requestUrl - 解析后的 URL
|
| 301 |
-
* @param {Object} config - 服务器配置
|
| 302 |
-
* @returns {Promise<{handled: boolean, authorized: boolean}>}
|
| 303 |
-
*/
|
| 304 |
-
async executeAuth(req, res, requestUrl, config) {
|
| 305 |
-
const authPlugins = this.getAuthPlugins();
|
| 306 |
-
|
| 307 |
-
for (const plugin of authPlugins) {
|
| 308 |
-
try {
|
| 309 |
-
const result = await plugin.authenticate(req, res, requestUrl, config);
|
| 310 |
-
|
| 311 |
-
if (!result) continue;
|
| 312 |
-
|
| 313 |
-
// 如果请求已被处理(如发送了错误响应),停止执行
|
| 314 |
-
if (result.handled) {
|
| 315 |
-
return { handled: true, authorized: false };
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
-
// 如果认证失败,停止执行
|
| 319 |
-
if (result.authorized === false) {
|
| 320 |
-
return { handled: true, authorized: false };
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
// 如果认证成功,合并数据并返回
|
| 324 |
-
if (result.authorized === true) {
|
| 325 |
-
if (result.data) {
|
| 326 |
-
Object.assign(config, result.data);
|
| 327 |
-
}
|
| 328 |
-
return { handled: false, authorized: true };
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
// authorized === null 表示此插件不处理,继续下一个
|
| 332 |
-
} catch (error) {
|
| 333 |
-
logger.error(`[PluginManager] Auth error in plugin "${plugin.name}":`, error.message);
|
| 334 |
-
}
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
// 没有任何认证插件处理,返回未授权
|
| 338 |
-
return { handled: false, authorized: false };
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
/**
|
| 342 |
-
* 执行普通中间件
|
| 343 |
-
* 只有 type!='auth' 的插件会执行
|
| 344 |
-
*
|
| 345 |
-
* 中间件返回值说明:
|
| 346 |
-
* - { handled: true } - 请求已被处理,停止后续处理
|
| 347 |
-
* - { handled: false, data: {...} } - 继续处理,可附带数据
|
| 348 |
-
* - null/undefined - 继续执行下一个中间件
|
| 349 |
-
*
|
| 350 |
-
* @param {http.IncomingMessage} req - HTTP 请求
|
| 351 |
-
* @param {http.ServerResponse} res - HTTP 响应
|
| 352 |
-
* @param {URL} requestUrl - 解析后的 URL
|
| 353 |
-
* @param {Object} config - 服务器配置
|
| 354 |
-
* @returns {Promise<{handled: boolean}>}
|
| 355 |
-
*/
|
| 356 |
-
async executeMiddleware(req, res, requestUrl, config) {
|
| 357 |
-
const middlewarePlugins = this.getMiddlewarePlugins();
|
| 358 |
-
|
| 359 |
-
for (const plugin of middlewarePlugins) {
|
| 360 |
-
try {
|
| 361 |
-
const result = await plugin.middleware(req, res, requestUrl, config);
|
| 362 |
-
|
| 363 |
-
if (!result) continue;
|
| 364 |
-
|
| 365 |
-
// 如果请求已被处理,停止执行
|
| 366 |
-
if (result.handled) {
|
| 367 |
-
return { handled: true };
|
| 368 |
-
}
|
| 369 |
-
|
| 370 |
-
// 合并数据
|
| 371 |
-
if (result.data) {
|
| 372 |
-
Object.assign(config, result.data);
|
| 373 |
-
}
|
| 374 |
-
} catch (error) {
|
| 375 |
-
logger.error(`[PluginManager] Middleware error in plugin "${plugin.name}":`, error.message);
|
| 376 |
-
}
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
return { handled: false };
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
/**
|
| 383 |
-
* 执行所有插件的路由处理
|
| 384 |
-
* @param {string} method - HTTP 方法
|
| 385 |
-
* @param {string} path - 请求路径
|
| 386 |
-
* @param {http.IncomingMessage} req - HTTP 请求
|
| 387 |
-
* @param {http.ServerResponse} res - HTTP 响应
|
| 388 |
-
* @returns {Promise<boolean>} - 是否已处理
|
| 389 |
-
*/
|
| 390 |
-
async executeRoutes(method, path, req, res) {
|
| 391 |
-
for (const plugin of this.getEnabledPlugins()) {
|
| 392 |
-
if (!Array.isArray(plugin.routes)) continue;
|
| 393 |
-
|
| 394 |
-
for (const route of plugin.routes) {
|
| 395 |
-
const methodMatch = route.method === '*' || route.method.toUpperCase() === method;
|
| 396 |
-
if (!methodMatch) continue;
|
| 397 |
-
|
| 398 |
-
let pathMatch = false;
|
| 399 |
-
if (route.path instanceof RegExp) {
|
| 400 |
-
pathMatch = route.path.test(path);
|
| 401 |
-
} else if (typeof route.path === 'string') {
|
| 402 |
-
pathMatch = path === route.path || path.startsWith(route.path + '/');
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
if (pathMatch) {
|
| 406 |
-
try {
|
| 407 |
-
const handled = await route.handler(method, path, req, res);
|
| 408 |
-
if (handled) return true;
|
| 409 |
-
} catch (error) {
|
| 410 |
-
logger.error(`[PluginManager] Route error in plugin "${plugin.name}":`, error.message);
|
| 411 |
-
}
|
| 412 |
-
}
|
| 413 |
-
}
|
| 414 |
-
}
|
| 415 |
-
return false;
|
| 416 |
-
}
|
| 417 |
-
|
| 418 |
-
/**
|
| 419 |
-
* 获取所有插件的静态文件路径
|
| 420 |
-
* @returns {string[]}
|
| 421 |
-
*/
|
| 422 |
-
getStaticPaths() {
|
| 423 |
-
const paths = [];
|
| 424 |
-
for (const plugin of this.getEnabledPlugins()) {
|
| 425 |
-
if (Array.isArray(plugin.staticPaths)) {
|
| 426 |
-
paths.push(...plugin.staticPaths);
|
| 427 |
-
}
|
| 428 |
-
}
|
| 429 |
-
return paths;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
/**
|
| 433 |
-
* 检查路径是否是插件静态文件
|
| 434 |
-
* @param {string} path - 请求路径
|
| 435 |
-
* @returns {boolean}
|
| 436 |
-
*/
|
| 437 |
-
isPluginStaticPath(path) {
|
| 438 |
-
const staticPaths = this.getStaticPaths();
|
| 439 |
-
return staticPaths.some(sp => path === sp || path === '/' + sp);
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
/**
|
| 443 |
-
* 执行钩子函数
|
| 444 |
-
* @param {string} hookName - 钩子名称
|
| 445 |
-
* @param {...any} args - 钩子参数
|
| 446 |
-
*/
|
| 447 |
-
async executeHook(hookName, ...args) {
|
| 448 |
-
for (const plugin of this.getEnabledPlugins()) {
|
| 449 |
-
if (!plugin.hooks || typeof plugin.hooks[hookName] !== 'function') continue;
|
| 450 |
-
|
| 451 |
-
try {
|
| 452 |
-
await plugin.hooks[hookName](...args);
|
| 453 |
-
} catch (error) {
|
| 454 |
-
logger.error(`[PluginManager] Hook "${hookName}" error in plugin "${plugin.name}":`, error.message);
|
| 455 |
-
}
|
| 456 |
-
}
|
| 457 |
-
}
|
| 458 |
-
|
| 459 |
-
/**
|
| 460 |
-
* 获取插件列表(用于 API)
|
| 461 |
-
* @returns {Object[]}
|
| 462 |
-
*/
|
| 463 |
-
getPluginList() {
|
| 464 |
-
const list = [];
|
| 465 |
-
for (const [name, plugin] of this.plugins) {
|
| 466 |
-
const pluginConfig = this.pluginsConfig.plugins[name] || {};
|
| 467 |
-
list.push({
|
| 468 |
-
name: plugin.name,
|
| 469 |
-
version: plugin.version || '1.0.0',
|
| 470 |
-
description: plugin.description || pluginConfig.description || '',
|
| 471 |
-
enabled: plugin._enabled === true,
|
| 472 |
-
hasMiddleware: typeof plugin.middleware === 'function',
|
| 473 |
-
hasRoutes: Array.isArray(plugin.routes) && plugin.routes.length > 0,
|
| 474 |
-
hasHooks: plugin.hooks && Object.keys(plugin.hooks).length > 0
|
| 475 |
-
});
|
| 476 |
-
}
|
| 477 |
-
return list;
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
/**
|
| 481 |
-
* 启用/禁用插件
|
| 482 |
-
* @param {string} name - 插件名称
|
| 483 |
-
* @param {boolean} enabled - 是否启用
|
| 484 |
-
*/
|
| 485 |
-
async setPluginEnabled(name, enabled) {
|
| 486 |
-
if (!this.pluginsConfig.plugins[name]) {
|
| 487 |
-
this.pluginsConfig.plugins[name] = {};
|
| 488 |
-
}
|
| 489 |
-
this.pluginsConfig.plugins[name].enabled = enabled;
|
| 490 |
-
await this.saveConfig();
|
| 491 |
-
|
| 492 |
-
const plugin = this.plugins.get(name);
|
| 493 |
-
if (plugin) {
|
| 494 |
-
plugin._enabled = enabled;
|
| 495 |
-
}
|
| 496 |
-
}
|
| 497 |
-
}
|
| 498 |
-
|
| 499 |
-
// 单例实例
|
| 500 |
-
const pluginManager = new PluginManager();
|
| 501 |
-
|
| 502 |
-
/**
|
| 503 |
-
* 自动发现并加载插件
|
| 504 |
-
* 扫描 src/plugins/ 目录下的所有插件
|
| 505 |
-
*/
|
| 506 |
-
export async function discoverPlugins() {
|
| 507 |
-
const pluginsDir = path.join(process.cwd(), 'src', 'plugins');
|
| 508 |
-
|
| 509 |
-
try {
|
| 510 |
-
if (!existsSync(pluginsDir)) {
|
| 511 |
-
await fs.mkdir(pluginsDir, { recursive: true });
|
| 512 |
-
logger.info('[PluginManager] Created plugins directory');
|
| 513 |
-
}
|
| 514 |
-
|
| 515 |
-
const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
|
| 516 |
-
|
| 517 |
-
for (const entry of entries) {
|
| 518 |
-
if (!entry.isDirectory()) continue;
|
| 519 |
-
|
| 520 |
-
const pluginPath = path.join(pluginsDir, entry.name, 'index.js');
|
| 521 |
-
if (!existsSync(pluginPath)) continue;
|
| 522 |
-
|
| 523 |
-
try {
|
| 524 |
-
// 动态导入插件
|
| 525 |
-
const pluginModule = await import(`file://${pluginPath}`);
|
| 526 |
-
const plugin = pluginModule.default || pluginModule;
|
| 527 |
-
|
| 528 |
-
if (plugin && plugin.name) {
|
| 529 |
-
pluginManager.register(plugin);
|
| 530 |
-
}
|
| 531 |
-
} catch (error) {
|
| 532 |
-
logger.error(`[PluginManager] Failed to load plugin from ${entry.name}:`, error.message);
|
| 533 |
-
}
|
| 534 |
-
}
|
| 535 |
-
} catch (error) {
|
| 536 |
-
logger.error('[PluginManager] Failed to discover plugins:', error.message);
|
| 537 |
-
}
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
/**
|
| 541 |
-
* 获取插件管理器实例
|
| 542 |
-
* @returns {PluginManager}
|
| 543 |
-
*/
|
| 544 |
-
export function getPluginManager() {
|
| 545 |
-
return pluginManager;
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
// 导出类和实例
|
| 549 |
-
export { PluginManager, pluginManager };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/handlers/request-handler.js
DELETED
|
@@ -1,271 +0,0 @@
|
|
| 1 |
-
import deepmerge from 'deepmerge';
|
| 2 |
-
import logger from '../utils/logger.js';
|
| 3 |
-
import { handleError, getClientIp } from '../utils/common.js';
|
| 4 |
-
import { handleUIApiRequests, serveStaticFiles } from '../services/ui-manager.js';
|
| 5 |
-
import { handleAPIRequests } from '../services/api-manager.js';
|
| 6 |
-
import { getApiService, getProviderStatus } from '../services/service-manager.js';
|
| 7 |
-
import { getProviderPoolManager } from '../services/service-manager.js';
|
| 8 |
-
import { MODEL_PROVIDER } from '../utils/common.js';
|
| 9 |
-
import { getRegisteredProviders } from '../providers/adapter.js';
|
| 10 |
-
import { countTokensAnthropic } from '../utils/token-utils.js';
|
| 11 |
-
import { PROMPT_LOG_FILENAME } from '../core/config-manager.js';
|
| 12 |
-
import { getPluginManager } from '../core/plugin-manager.js';
|
| 13 |
-
import { randomUUID } from 'crypto';
|
| 14 |
-
import { handleGrokAssetsProxy } from '../utils/grok-assets-proxy.js';
|
| 15 |
-
|
| 16 |
-
/**
|
| 17 |
-
* Generate a short unique request ID (8 characters)
|
| 18 |
-
*/
|
| 19 |
-
function generateRequestId() {
|
| 20 |
-
return randomUUID().slice(0, 8);
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
/**
|
| 24 |
-
* Parse request body as JSON
|
| 25 |
-
*/
|
| 26 |
-
function parseRequestBody(req) {
|
| 27 |
-
return new Promise((resolve, reject) => {
|
| 28 |
-
let body = '';
|
| 29 |
-
req.on('data', chunk => { body += chunk.toString(); });
|
| 30 |
-
req.on('end', () => {
|
| 31 |
-
try {
|
| 32 |
-
resolve(body ? JSON.parse(body) : {});
|
| 33 |
-
} catch (e) {
|
| 34 |
-
reject(new Error('Invalid JSON in request body'));
|
| 35 |
-
}
|
| 36 |
-
});
|
| 37 |
-
req.on('error', reject);
|
| 38 |
-
});
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
/**
|
| 42 |
-
* Main request handler. It authenticates the request, determines the endpoint type,
|
| 43 |
-
* and delegates to the appropriate specialized handler function.
|
| 44 |
-
* @param {Object} config - The server configuration
|
| 45 |
-
* @param {Object} providerPoolManager - The provider pool manager instance
|
| 46 |
-
* @returns {Function} - The request handler function
|
| 47 |
-
*/
|
| 48 |
-
export function createRequestHandler(config, providerPoolManager) {
|
| 49 |
-
return async function requestHandler(req, res) {
|
| 50 |
-
// Generate unique request ID and set it in logger context
|
| 51 |
-
const clientIp = getClientIp(req);
|
| 52 |
-
const requestId = `${clientIp}:${generateRequestId()}`;
|
| 53 |
-
|
| 54 |
-
return logger.runWithContext(requestId, async () => {
|
| 55 |
-
// Deep copy the config for each request to allow dynamic modification
|
| 56 |
-
const currentConfig = deepmerge({}, config);
|
| 57 |
-
|
| 58 |
-
// 计算当前请求的基础 URL
|
| 59 |
-
const protocol = req.socket.encrypted || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
|
| 60 |
-
const host = req.headers.host;
|
| 61 |
-
currentConfig.requestBaseUrl = `${protocol}://${host}`;
|
| 62 |
-
|
| 63 |
-
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
|
| 64 |
-
let path = requestUrl.pathname;
|
| 65 |
-
const method = req.method;
|
| 66 |
-
|
| 67 |
-
try {
|
| 68 |
-
// Set CORS headers for all requests
|
| 69 |
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
| 70 |
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
|
| 71 |
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-goog-api-key, Model-Provider, X-Requested-With, Accept, Origin');
|
| 72 |
-
res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours cache for preflight
|
| 73 |
-
|
| 74 |
-
// Handle CORS preflight requests
|
| 75 |
-
if (method === 'OPTIONS') {
|
| 76 |
-
res.writeHead(204);
|
| 77 |
-
res.end();
|
| 78 |
-
return;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
// Serve static files for UI (除了登录页面需要认证)
|
| 82 |
-
// 检查是否是插件静态文件
|
| 83 |
-
const pluginManager = getPluginManager();
|
| 84 |
-
const isPluginStatic = pluginManager.isPluginStaticPath(path);
|
| 85 |
-
if (path.startsWith('/static/') || path === '/' || path === '/favicon.ico' || path === '/index.html' || path.startsWith('/app/') || path.startsWith('/components/') || path === '/login.html' || isPluginStatic) {
|
| 86 |
-
const served = await serveStaticFiles(path, res);
|
| 87 |
-
if (served) return;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
// 执行插件路由
|
| 91 |
-
const pluginRouteHandled = await pluginManager.executeRoutes(method, path, req, res);
|
| 92 |
-
if (pluginRouteHandled) return;
|
| 93 |
-
|
| 94 |
-
const uiHandled = await handleUIApiRequests(method, path, req, res, currentConfig, providerPoolManager);
|
| 95 |
-
if (uiHandled) return;
|
| 96 |
-
|
| 97 |
-
// logger.info(`\n${new Date().toLocaleString()}`);
|
| 98 |
-
logger.info(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`);
|
| 99 |
-
|
| 100 |
-
// Health check endpoint
|
| 101 |
-
if (method === 'GET' && path === '/health') {
|
| 102 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 103 |
-
res.end(JSON.stringify({
|
| 104 |
-
status: 'healthy',
|
| 105 |
-
timestamp: new Date().toISOString(),
|
| 106 |
-
provider: currentConfig.MODEL_PROVIDER
|
| 107 |
-
}));
|
| 108 |
-
return true;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
// Grok assets proxy endpoint
|
| 112 |
-
if (method === 'GET' && path === '/api/grok/assets') {
|
| 113 |
-
await handleGrokAssetsProxy(req, res, currentConfig, providerPoolManager);
|
| 114 |
-
return true;
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
// providers health endpoint
|
| 118 |
-
// url params: provider[string], customName[string], unhealthRatioThreshold[float]
|
| 119 |
-
// 支持provider, customName过滤记录
|
| 120 |
-
// 支持unhealthRatioThreshold控制不健康比例的阈值, 当unhealthyRatio超过阈值返回summaryHealthy: false
|
| 121 |
-
if (method === 'GET' && path === '/provider_health') {
|
| 122 |
-
try {
|
| 123 |
-
const provider = requestUrl.searchParams.get('provider');
|
| 124 |
-
const customName = requestUrl.searchParams.get('customName');
|
| 125 |
-
let unhealthRatioThreshold = requestUrl.searchParams.get('unhealthRatioThreshold');
|
| 126 |
-
unhealthRatioThreshold = unhealthRatioThreshold === null ? 0.0001 : parseFloat(unhealthRatioThreshold);
|
| 127 |
-
let provideStatus = await getProviderStatus(currentConfig, { provider, customName });
|
| 128 |
-
let summaryHealth = true;
|
| 129 |
-
if (!isNaN(unhealthRatioThreshold)) {
|
| 130 |
-
summaryHealth = provideStatus.unhealthyRatio <= unhealthRatioThreshold;
|
| 131 |
-
}
|
| 132 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 133 |
-
res.end(JSON.stringify({
|
| 134 |
-
timestamp: new Date().toISOString(),
|
| 135 |
-
items: provideStatus.providerPoolsSlim,
|
| 136 |
-
count: provideStatus.count,
|
| 137 |
-
unhealthyCount: provideStatus.unhealthyCount,
|
| 138 |
-
unhealthyRatio: provideStatus.unhealthyRatio,
|
| 139 |
-
unhealthySummeryMessage: provideStatus.unhealthySummeryMessage,
|
| 140 |
-
summaryHealth
|
| 141 |
-
}));
|
| 142 |
-
return true;
|
| 143 |
-
} catch (error) {
|
| 144 |
-
logger.info(`[Server] req provider_health error: ${error.message}`);
|
| 145 |
-
handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER);
|
| 146 |
-
return;
|
| 147 |
-
}
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
// Handle API requests
|
| 152 |
-
// Allow overriding MODEL_PROVIDER via request header
|
| 153 |
-
const modelProviderHeader = req.headers['model-provider'];
|
| 154 |
-
if (modelProviderHeader) {
|
| 155 |
-
const registeredProviders = getRegisteredProviders();
|
| 156 |
-
if (registeredProviders.includes(modelProviderHeader)) {
|
| 157 |
-
currentConfig.MODEL_PROVIDER = modelProviderHeader;
|
| 158 |
-
logger.info(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`);
|
| 159 |
-
} else {
|
| 160 |
-
logger.warn(`[Config] Provider ${modelProviderHeader} in header is not available.`);
|
| 161 |
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
| 162 |
-
res.end(JSON.stringify({ error: { message: `Provider ${modelProviderHeader} is not available.` } }));
|
| 163 |
-
return;
|
| 164 |
-
}
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
// Check if the first path segment matches a MODEL_PROVIDER and switch if it does
|
| 168 |
-
const pathSegments = path.split('/').filter(segment => segment.length > 0);
|
| 169 |
-
|
| 170 |
-
if (pathSegments.length > 0) {
|
| 171 |
-
const firstSegment = pathSegments[0];
|
| 172 |
-
const registeredProviders = getRegisteredProviders();
|
| 173 |
-
const isValidProvider = registeredProviders.includes(firstSegment);
|
| 174 |
-
const isAutoMode = firstSegment === MODEL_PROVIDER.AUTO;
|
| 175 |
-
|
| 176 |
-
if (firstSegment && (isValidProvider || isAutoMode)) {
|
| 177 |
-
currentConfig.MODEL_PROVIDER = firstSegment;
|
| 178 |
-
logger.info(`[Config] MODEL_PROVIDER overridden by path segment to: ${currentConfig.MODEL_PROVIDER}`);
|
| 179 |
-
pathSegments.shift();
|
| 180 |
-
path = '/' + pathSegments.join('/');
|
| 181 |
-
requestUrl.pathname = path;
|
| 182 |
-
} else if (firstSegment && Object.values(MODEL_PROVIDER).includes(firstSegment)) {
|
| 183 |
-
// 如果在 MODEL_PROVIDER 中但没注册适配器,拦截并报错
|
| 184 |
-
logger.warn(`[Config] Provider ${firstSegment} is recognized but no adapter is registered.`);
|
| 185 |
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
| 186 |
-
res.end(JSON.stringify({ error: { message: `Provider ${firstSegment} is not available.` } }));
|
| 187 |
-
return;
|
| 188 |
-
} else if (firstSegment && !isValidProvider) {
|
| 189 |
-
logger.info(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`);
|
| 190 |
-
}
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
// 1. 执行认证流程(只有 type='auth' 的插件参与)
|
| 194 |
-
const authResult = await pluginManager.executeAuth(req, res, requestUrl, currentConfig);
|
| 195 |
-
if (authResult.handled) {
|
| 196 |
-
// 认证插件已处理请求(如发送了错误响应)
|
| 197 |
-
return;
|
| 198 |
-
}
|
| 199 |
-
if (!authResult.authorized) {
|
| 200 |
-
// 没有认证插件授权,返回 401
|
| 201 |
-
res.writeHead(401, { 'Content-Type': 'application/json' });
|
| 202 |
-
res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing.' } }));
|
| 203 |
-
return;
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
// 2. 执行普通中间件(type!='auth' 的插件)
|
| 207 |
-
const middlewareResult = await pluginManager.executeMiddleware(req, res, requestUrl, currentConfig);
|
| 208 |
-
if (middlewareResult.handled) {
|
| 209 |
-
// 中间件已处理请求
|
| 210 |
-
return;
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
// Handle count_tokens requests (Anthropic API compatible)
|
| 214 |
-
if (path.includes('/count_tokens') && method === 'POST') {
|
| 215 |
-
try {
|
| 216 |
-
const body = await parseRequestBody(req);
|
| 217 |
-
logger.info(`[Server] Handling count_tokens request for model: ${body.model}`);
|
| 218 |
-
|
| 219 |
-
// Use common utility method directly
|
| 220 |
-
try {
|
| 221 |
-
const result = countTokensAnthropic(body);
|
| 222 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 223 |
-
res.end(JSON.stringify(result));
|
| 224 |
-
} catch (tokenError) {
|
| 225 |
-
logger.warn(`[Server] Common countTokens failed, falling back: ${tokenError.message}`);
|
| 226 |
-
// Last resort: return 0
|
| 227 |
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
| 228 |
-
res.end(JSON.stringify({ input_tokens: 0 }));
|
| 229 |
-
}
|
| 230 |
-
return true;
|
| 231 |
-
} catch (error) {
|
| 232 |
-
logger.error(`[Server] count_tokens error: ${error.message}`);
|
| 233 |
-
handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER);
|
| 234 |
-
return;
|
| 235 |
-
}
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
// 获取或选择 API Service 实例
|
| 239 |
-
let apiService;
|
| 240 |
-
// try {
|
| 241 |
-
// apiService = await getApiService(currentConfig);
|
| 242 |
-
// } catch (error) {
|
| 243 |
-
// handleError(res, { statusCode: 500, message: `Failed to get API service: ${error.message}` }, currentConfig.MODEL_PROVIDER);
|
| 244 |
-
// const poolManager = getProviderPoolManager();
|
| 245 |
-
// if (poolManager) {
|
| 246 |
-
// poolManager.markProviderUnhealthy(currentConfig.MODEL_PROVIDER, {
|
| 247 |
-
// uuid: currentConfig.uuid
|
| 248 |
-
// });
|
| 249 |
-
// }
|
| 250 |
-
// return;
|
| 251 |
-
// }
|
| 252 |
-
|
| 253 |
-
try {
|
| 254 |
-
// Handle API requests
|
| 255 |
-
const apiHandled = await handleAPIRequests(method, path, req, res, currentConfig, apiService, providerPoolManager, PROMPT_LOG_FILENAME);
|
| 256 |
-
if (apiHandled) return;
|
| 257 |
-
|
| 258 |
-
// Fallback for unmatched routes
|
| 259 |
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
| 260 |
-
res.end(JSON.stringify({ error: { message: 'Not Found' } }));
|
| 261 |
-
} catch (error) {
|
| 262 |
-
handleError(res, error, currentConfig.MODEL_PROVIDER);
|
| 263 |
-
}
|
| 264 |
-
} finally {
|
| 265 |
-
// Clear request context after request is complete
|
| 266 |
-
logger.clearRequestContext(requestId);
|
| 267 |
-
}
|
| 268 |
-
});
|
| 269 |
-
};
|
| 270 |
-
|
| 271 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/plugins/ai-monitor/index.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
| 1 |
-
import logger from '../../utils/logger.js';
|
| 2 |
-
|
| 3 |
-
/**
|
| 4 |
-
* AI 接口监控插件
|
| 5 |
-
* 功能:
|
| 6 |
-
* 1. 捕获 AI 接口的请求参数(转换前和转换后)
|
| 7 |
-
* 2. 捕获 AI 接口的响应结果(转换前和转换后,流式响应聚合输出)
|
| 8 |
-
*/
|
| 9 |
-
const aiMonitorPlugin = {
|
| 10 |
-
name: 'ai-monitor',
|
| 11 |
-
version: '1.0.0',
|
| 12 |
-
description: 'AI 接口监控插件 - 捕获请求和响应参数(全链路协议转换监控,流式聚合输出,用于调试和分析)',
|
| 13 |
-
type: 'middleware',
|
| 14 |
-
_priority: 100,
|
| 15 |
-
|
| 16 |
-
// 用于存储流式响应的中间状态
|
| 17 |
-
streamCache: new Map(),
|
| 18 |
-
|
| 19 |
-
async init(config) {
|
| 20 |
-
logger.info('[AI Monitor Plugin] Initialized');
|
| 21 |
-
},
|
| 22 |
-
|
| 23 |
-
/**
|
| 24 |
-
* 中间件:初始化请求上下文
|
| 25 |
-
*/
|
| 26 |
-
async middleware(req, res, requestUrl, config) {
|
| 27 |
-
const aiPaths = ['/v1/chat/completions', '/v1/responses', '/v1/messages', '/v1beta/models'];
|
| 28 |
-
const isAiPath = aiPaths.some(path => requestUrl.pathname.includes(path));
|
| 29 |
-
|
| 30 |
-
if (isAiPath && req.method === 'POST') {
|
| 31 |
-
// 在监控插件中生成请求标识,并存入 config 以供全链路追踪
|
| 32 |
-
const requestId = Date.now() + Math.random().toString(36).substring(2, 10);
|
| 33 |
-
config._monitorRequestId = requestId;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
return { handled: false };
|
| 37 |
-
},
|
| 38 |
-
|
| 39 |
-
hooks: {
|
| 40 |
-
/**
|
| 41 |
-
* 请求转换后的钩子
|
| 42 |
-
*/
|
| 43 |
-
async onContentGenerated(config) {
|
| 44 |
-
const { originalRequestBody, processedRequestBody, fromProvider, toProvider, model, _monitorRequestId, isStream } = config;
|
| 45 |
-
if (!originalRequestBody) return;
|
| 46 |
-
|
| 47 |
-
setImmediate(() => {
|
| 48 |
-
const hasConversion = JSON.stringify(originalRequestBody) !== JSON.stringify(processedRequestBody);
|
| 49 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] >>> Req Protocol: ${fromProvider}${hasConversion ? ' -> ' + toProvider : ''} | Model: ${model}`);
|
| 50 |
-
|
| 51 |
-
if (hasConversion) {
|
| 52 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] [Req Original]: ${JSON.stringify(originalRequestBody)}`);
|
| 53 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] [Req Processed]: ${JSON.stringify(processedRequestBody)}`);
|
| 54 |
-
} else {
|
| 55 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] [Req]: ${JSON.stringify(originalRequestBody)}`);
|
| 56 |
-
}
|
| 57 |
-
});
|
| 58 |
-
|
| 59 |
-
// 处理流式响应的聚合输出
|
| 60 |
-
if (isStream && _monitorRequestId) {
|
| 61 |
-
setTimeout(() => {
|
| 62 |
-
const cache = aiMonitorPlugin.streamCache.get(_monitorRequestId);
|
| 63 |
-
if (cache) {
|
| 64 |
-
const hasConversion = JSON.stringify(cache.nativeChunks) !== JSON.stringify(cache.convertedChunks);
|
| 65 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] <<< Stream Response Aggregated: ${hasConversion ? cache.toProvider + ' -> ' : ''}${cache.fromProvider}`);
|
| 66 |
-
|
| 67 |
-
if (hasConversion) {
|
| 68 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] [Res Native Full]: ${JSON.stringify(cache.nativeChunks)}`);
|
| 69 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] [Res Converted Full]: ${JSON.stringify(cache.convertedChunks)}`);
|
| 70 |
-
} else {
|
| 71 |
-
logger.info(`[AI Monitor][${_monitorRequestId}] [Res Full]: ${JSON.stringify(cache.nativeChunks)}`);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
aiMonitorPlugin.streamCache.delete(_monitorRequestId);
|
| 75 |
-
}
|
| 76 |
-
}, 2000); // 等待流传输完成
|
| 77 |
-
}
|
| 78 |
-
},
|
| 79 |
-
|
| 80 |
-
/**
|
| 81 |
-
* 非流式响应转换监控
|
| 82 |
-
*/
|
| 83 |
-
async onUnaryResponse({ nativeResponse, clientResponse, fromProvider, toProvider, requestId }) {
|
| 84 |
-
setImmediate(() => {
|
| 85 |
-
const reqId = requestId || 'N/A';
|
| 86 |
-
const hasConversion = JSON.stringify(nativeResponse) !== JSON.stringify(clientResponse);
|
| 87 |
-
logger.info(`[AI Monitor][${reqId}] <<< Res Protocol: ${hasConversion ? toProvider + ' -> ' : ''}${fromProvider} (Unary)`);
|
| 88 |
-
|
| 89 |
-
if (hasConversion) {
|
| 90 |
-
logger.info(`[AI Monitor][${reqId}] [Res Native]: ${JSON.stringify(nativeResponse)}`);
|
| 91 |
-
logger.info(`[AI Monitor][${reqId}] [Res Converted]: ${JSON.stringify(clientResponse)}`);
|
| 92 |
-
} else {
|
| 93 |
-
logger.info(`[AI Monitor][${reqId}] [Res]: ${JSON.stringify(nativeResponse)}`);
|
| 94 |
-
}
|
| 95 |
-
});
|
| 96 |
-
},
|
| 97 |
-
|
| 98 |
-
/**
|
| 99 |
-
* 流式响应分块转换监控 - 聚合数据
|
| 100 |
-
*/
|
| 101 |
-
async onStreamChunk({ nativeChunk, chunkToSend, fromProvider, toProvider, requestId }) {
|
| 102 |
-
if (!requestId) return;
|
| 103 |
-
|
| 104 |
-
if (!aiMonitorPlugin.streamCache.has(requestId)) {
|
| 105 |
-
aiMonitorPlugin.streamCache.set(requestId, {
|
| 106 |
-
nativeChunks: [],
|
| 107 |
-
convertedChunks: [],
|
| 108 |
-
fromProvider,
|
| 109 |
-
toProvider
|
| 110 |
-
});
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
const cache = aiMonitorPlugin.streamCache.get(requestId);
|
| 114 |
-
|
| 115 |
-
// 过滤 null 值,并判断是否为数组类型
|
| 116 |
-
if (nativeChunk != null) {
|
| 117 |
-
if (Array.isArray(nativeChunk)) {
|
| 118 |
-
cache.nativeChunks.push(...nativeChunk.filter(item => item != null));
|
| 119 |
-
} else {
|
| 120 |
-
cache.nativeChunks.push(nativeChunk);
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
if (chunkToSend != null) {
|
| 125 |
-
if (Array.isArray(chunkToSend)) {
|
| 126 |
-
cache.convertedChunks.push(...chunkToSend.filter(item => item != null));
|
| 127 |
-
} else {
|
| 128 |
-
cache.convertedChunks.push(chunkToSend);
|
| 129 |
-
}
|
| 130 |
-
}
|
| 131 |
-
},
|
| 132 |
-
|
| 133 |
-
/**
|
| 134 |
-
* 内部请求转换监控
|
| 135 |
-
*/
|
| 136 |
-
async onInternalRequestConverted({ requestId, internalRequest, converterName }) {
|
| 137 |
-
setImmediate(() => {
|
| 138 |
-
const reqId = requestId || 'N/A';
|
| 139 |
-
logger.info(`[AI Monitor][${reqId}] >>> Internal Req Converted [${converterName}]: ${JSON.stringify(internalRequest)}`);
|
| 140 |
-
});
|
| 141 |
-
}
|
| 142 |
-
}
|
| 143 |
-
};
|
| 144 |
-
|
| 145 |
-
export default aiMonitorPlugin;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/plugins/api-potluck/api-routes.js
DELETED
|
@@ -1,452 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* API 大锅饭 - 管理 API 路由
|
| 3 |
-
* 提供 Key 管理的 RESTful API
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import {
|
| 7 |
-
createKey,
|
| 8 |
-
listKeys,
|
| 9 |
-
getKey,
|
| 10 |
-
deleteKey,
|
| 11 |
-
updateKeyLimit,
|
| 12 |
-
resetKeyUsage,
|
| 13 |
-
toggleKey,
|
| 14 |
-
updateKeyName,
|
| 15 |
-
regenerateKey,
|
| 16 |
-
getStats,
|
| 17 |
-
validateKey,
|
| 18 |
-
KEY_PREFIX,
|
| 19 |
-
applyDailyLimitToAllKeys,
|
| 20 |
-
getAllKeyIds
|
| 21 |
-
} from './key-manager.js';
|
| 22 |
-
import logger from '../../utils/logger.js';
|
| 23 |
-
|
| 24 |
-
/**
|
| 25 |
-
* 解析请求体
|
| 26 |
-
* @param {http.IncomingMessage} req
|
| 27 |
-
* @returns {Promise<Object>}
|
| 28 |
-
*/
|
| 29 |
-
function parseRequestBody(req) {
|
| 30 |
-
return new Promise((resolve, reject) => {
|
| 31 |
-
let body = '';
|
| 32 |
-
req.on('data', chunk => {
|
| 33 |
-
body += chunk.toString();
|
| 34 |
-
});
|
| 35 |
-
req.on('end', () => {
|
| 36 |
-
try {
|
| 37 |
-
resolve(body ? JSON.parse(body) : {});
|
| 38 |
-
} catch (error) {
|
| 39 |
-
reject(new Error('JSON 格式无效'));
|
| 40 |
-
}
|
| 41 |
-
});
|
| 42 |
-
req.on('error', reject);
|
| 43 |
-
});
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
/**
|
| 47 |
-
* 发送 JSON 响应
|
| 48 |
-
* @param {http.ServerResponse} res
|
| 49 |
-
* @param {number} statusCode
|
| 50 |
-
* @param {Object} data
|
| 51 |
-
*/
|
| 52 |
-
function sendJson(res, statusCode, data) {
|
| 53 |
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
| 54 |
-
res.end(JSON.stringify(data));
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
/**
|
| 58 |
-
* 验证管理员 Token
|
| 59 |
-
* @param {http.IncomingMessage} req
|
| 60 |
-
* @returns {Promise<boolean>}
|
| 61 |
-
*/
|
| 62 |
-
async function checkAdminAuth(req) {
|
| 63 |
-
const authHeader = req.headers.authorization;
|
| 64 |
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
| 65 |
-
return false;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
// 动态导入 ui-manager 中的 token 验证逻辑
|
| 69 |
-
try {
|
| 70 |
-
const { existsSync, readFileSync } = await import('fs');
|
| 71 |
-
const { promises: fs } = await import('fs');
|
| 72 |
-
const path = await import('path');
|
| 73 |
-
|
| 74 |
-
const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json');
|
| 75 |
-
|
| 76 |
-
if (!existsSync(TOKEN_STORE_FILE)) {
|
| 77 |
-
return false;
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
const content = readFileSync(TOKEN_STORE_FILE, 'utf8');
|
| 81 |
-
const tokenStore = JSON.parse(content);
|
| 82 |
-
const token = authHeader.substring(7);
|
| 83 |
-
const tokenInfo = tokenStore.tokens[token];
|
| 84 |
-
|
| 85 |
-
if (!tokenInfo) {
|
| 86 |
-
return false;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
// 检查是否过期
|
| 90 |
-
if (Date.now() > tokenInfo.expiryTime) {
|
| 91 |
-
return false;
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
return true;
|
| 95 |
-
} catch (error) {
|
| 96 |
-
logger.error('[API Potluck] Auth check error:', error.message);
|
| 97 |
-
return false;
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
/**
|
| 102 |
-
* 处理 Potluck 管理 API 请求
|
| 103 |
-
* @param {string} method - HTTP 方法
|
| 104 |
-
* @param {string} path - 请求路径
|
| 105 |
-
* @param {http.IncomingMessage} req - HTTP 请求对象
|
| 106 |
-
* @param {http.ServerResponse} res - HTTP 响应对象
|
| 107 |
-
* @returns {Promise<boolean>} - 是否处理了请求
|
| 108 |
-
*/
|
| 109 |
-
export async function handlePotluckApiRoutes(method, path, req, res) {
|
| 110 |
-
// 只处理 /api/potluck 开头的请求
|
| 111 |
-
if (!path.startsWith('/api/potluck')) {
|
| 112 |
-
return false;
|
| 113 |
-
}
|
| 114 |
-
logger.info('[API Potluck] Handling request:', method, path);
|
| 115 |
-
|
| 116 |
-
// 验证管理员权限
|
| 117 |
-
const isAuthed = await checkAdminAuth(req);
|
| 118 |
-
if (!isAuthed) {
|
| 119 |
-
sendJson(res, 401, {
|
| 120 |
-
success: false,
|
| 121 |
-
error: { message: '未授权:请先登录', code: 'UNAUTHORIZED' }
|
| 122 |
-
});
|
| 123 |
-
return true;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
try {
|
| 127 |
-
// GET /api/potluck/stats - 获取统计信息
|
| 128 |
-
if (method === 'GET' && path === '/api/potluck/stats') {
|
| 129 |
-
const stats = await getStats();
|
| 130 |
-
sendJson(res, 200, { success: true, data: stats });
|
| 131 |
-
return true;
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
// GET /api/potluck/keys - 获取所有 Key 列表
|
| 135 |
-
if (method === 'GET' && path === '/api/potluck/keys') {
|
| 136 |
-
const keys = await listKeys();
|
| 137 |
-
const stats = await getStats();
|
| 138 |
-
sendJson(res, 200, {
|
| 139 |
-
success: true,
|
| 140 |
-
data: { keys, stats }
|
| 141 |
-
});
|
| 142 |
-
return true;
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
// POST /api/potluck/keys/apply-limit - 批量应用每日限额到所有 Key
|
| 146 |
-
if (method === 'POST' && path === '/api/potluck/keys/apply-limit') {
|
| 147 |
-
const body = await parseRequestBody(req);
|
| 148 |
-
const { dailyLimit } = body;
|
| 149 |
-
|
| 150 |
-
if (dailyLimit === undefined || typeof dailyLimit !== 'number' || dailyLimit < 1) {
|
| 151 |
-
sendJson(res, 400, { success: false, error: { message: 'dailyLimit 必须是一个正数' } });
|
| 152 |
-
return true;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
const result = await applyDailyLimitToAllKeys(dailyLimit);
|
| 156 |
-
sendJson(res, 200, {
|
| 157 |
-
success: true,
|
| 158 |
-
message: `已将每日限额 ${dailyLimit} 应用到 ${result.updated}/${result.total} 个 Key`,
|
| 159 |
-
data: result
|
| 160 |
-
});
|
| 161 |
-
return true;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
// POST /api/potluck/keys - 创建新 Key
|
| 165 |
-
if (method === 'POST' && path === '/api/potluck/keys') {
|
| 166 |
-
const body = await parseRequestBody(req);
|
| 167 |
-
const { name, dailyLimit } = body;
|
| 168 |
-
const keyData = await createKey(name, dailyLimit);
|
| 169 |
-
sendJson(res, 201, {
|
| 170 |
-
success: true,
|
| 171 |
-
message: 'API Key 创建成功',
|
| 172 |
-
data: keyData
|
| 173 |
-
});
|
| 174 |
-
return true;
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
// 处理带 keyId 的路由
|
| 178 |
-
const keyIdMatch = path.match(/^\/api\/potluck\/keys\/([^\/]+)(\/.*)?$/);
|
| 179 |
-
if (keyIdMatch) {
|
| 180 |
-
const keyId = decodeURIComponent(keyIdMatch[1]);
|
| 181 |
-
const subPath = keyIdMatch[2] || '';
|
| 182 |
-
|
| 183 |
-
// GET /api/potluck/keys/:keyId - 获取单个 Key 详情
|
| 184 |
-
if (method === 'GET' && !subPath) {
|
| 185 |
-
const keyData = await getKey(keyId);
|
| 186 |
-
if (!keyData) {
|
| 187 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
|
| 188 |
-
return true;
|
| 189 |
-
}
|
| 190 |
-
sendJson(res, 200, { success: true, data: keyData });
|
| 191 |
-
return true;
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
// DELETE /api/potluck/keys/:keyId - 删除 Key
|
| 195 |
-
if (method === 'DELETE' && !subPath) {
|
| 196 |
-
const deleted = await deleteKey(keyId);
|
| 197 |
-
if (!deleted) {
|
| 198 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
|
| 199 |
-
return true;
|
| 200 |
-
}
|
| 201 |
-
sendJson(res, 200, { success: true, message: 'Key 删除成功' });
|
| 202 |
-
return true;
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
// PUT /api/potluck/keys/:keyId/limit - 更新每日限额
|
| 206 |
-
if (method === 'PUT' && subPath === '/limit') {
|
| 207 |
-
const body = await parseRequestBody(req);
|
| 208 |
-
const { dailyLimit } = body;
|
| 209 |
-
|
| 210 |
-
if (typeof dailyLimit !== 'number' || dailyLimit < 0) {
|
| 211 |
-
sendJson(res, 400, {
|
| 212 |
-
success: false,
|
| 213 |
-
error: { message: '无效的每日限额值' }
|
| 214 |
-
});
|
| 215 |
-
return true;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
const keyData = await updateKeyLimit(keyId, dailyLimit);
|
| 219 |
-
if (!keyData) {
|
| 220 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
|
| 221 |
-
return true;
|
| 222 |
-
}
|
| 223 |
-
sendJson(res, 200, {
|
| 224 |
-
success: true,
|
| 225 |
-
message: '每日限额更新成功',
|
| 226 |
-
data: keyData
|
| 227 |
-
});
|
| 228 |
-
return true;
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
// POST /api/potluck/keys/:keyId/reset - 重置当天调用次数
|
| 232 |
-
if (method === 'POST' && subPath === '/reset') {
|
| 233 |
-
const keyData = await resetKeyUsage(keyId);
|
| 234 |
-
if (!keyData) {
|
| 235 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
|
| 236 |
-
return true;
|
| 237 |
-
}
|
| 238 |
-
sendJson(res, 200, {
|
| 239 |
-
success: true,
|
| 240 |
-
message: '使用量重置成功',
|
| 241 |
-
data: keyData
|
| 242 |
-
});
|
| 243 |
-
return true;
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
// POST /api/potluck/keys/:keyId/toggle - 切换启用/禁用状态
|
| 247 |
-
if (method === 'POST' && subPath === '/toggle') {
|
| 248 |
-
const keyData = await toggleKey(keyId);
|
| 249 |
-
if (!keyData) {
|
| 250 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
|
| 251 |
-
return true;
|
| 252 |
-
}
|
| 253 |
-
sendJson(res, 200, {
|
| 254 |
-
success: true,
|
| 255 |
-
message: `Key 已成功${keyData.enabled ? '启用' : '禁用'}`,
|
| 256 |
-
data: keyData
|
| 257 |
-
});
|
| 258 |
-
return true;
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
// PUT /api/potluck/keys/:keyId/name - 更新 Key 名称
|
| 262 |
-
if (method === 'PUT' && subPath === '/name') {
|
| 263 |
-
const body = await parseRequestBody(req);
|
| 264 |
-
const { name } = body;
|
| 265 |
-
|
| 266 |
-
if (!name || typeof name !== 'string') {
|
| 267 |
-
sendJson(res, 400, {
|
| 268 |
-
success: false,
|
| 269 |
-
error: { message: '无效的名称值' }
|
| 270 |
-
});
|
| 271 |
-
return true;
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
const keyData = await updateKeyName(keyId, name);
|
| 275 |
-
if (!keyData) {
|
| 276 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
|
| 277 |
-
return true;
|
| 278 |
-
}
|
| 279 |
-
sendJson(res, 200, {
|
| 280 |
-
success: true,
|
| 281 |
-
message: '名称更新成功',
|
| 282 |
-
data: keyData
|
| 283 |
-
});
|
| 284 |
-
return true;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
// POST /api/potluck/keys/:keyId/regenerate - 重新生成 Key
|
| 288 |
-
if (method === 'POST' && subPath === '/regenerate') {
|
| 289 |
-
const result = await regenerateKey(keyId);
|
| 290 |
-
if (!result) {
|
| 291 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Key' } });
|
| 292 |
-
return true;
|
| 293 |
-
}
|
| 294 |
-
sendJson(res, 200, {
|
| 295 |
-
success: true,
|
| 296 |
-
message: 'Key 重新生成成功',
|
| 297 |
-
data: {
|
| 298 |
-
oldKey: result.oldKey,
|
| 299 |
-
newKey: result.newKey,
|
| 300 |
-
keyData: result.keyData
|
| 301 |
-
}
|
| 302 |
-
});
|
| 303 |
-
return true;
|
| 304 |
-
}
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
// 未匹配的 potluck 路由
|
| 308 |
-
sendJson(res, 404, { success: false, error: { message: '未找到 Potluck API 端点' } });
|
| 309 |
-
return true;
|
| 310 |
-
|
| 311 |
-
} catch (error) {
|
| 312 |
-
logger.error('[API Potluck] API error:', error);
|
| 313 |
-
sendJson(res, 500, {
|
| 314 |
-
success: false,
|
| 315 |
-
error: { message: error.message || '内部服务器错误' }
|
| 316 |
-
});
|
| 317 |
-
return true;
|
| 318 |
-
}
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
/**
|
| 322 |
-
* 从请求中提取 Potluck API Key
|
| 323 |
-
* @param {http.IncomingMessage} req - HTTP 请求对象
|
| 324 |
-
* @returns {string|null}
|
| 325 |
-
*/
|
| 326 |
-
function extractApiKeyFromRequest(req) {
|
| 327 |
-
// 1. 检查 Authorization header
|
| 328 |
-
const authHeader = req.headers['authorization'];
|
| 329 |
-
if (authHeader && authHeader.startsWith('Bearer ')) {
|
| 330 |
-
const token = authHeader.substring(7);
|
| 331 |
-
if (token.startsWith(KEY_PREFIX)) {
|
| 332 |
-
return token;
|
| 333 |
-
}
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
// 2. 检查 x-api-key header
|
| 337 |
-
const xApiKey = req.headers['x-api-key'];
|
| 338 |
-
if (xApiKey && xApiKey.startsWith(KEY_PREFIX)) {
|
| 339 |
-
return xApiKey;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
return null;
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
/**
|
| 346 |
-
* 处理用户端 API 请求 - 用户通过自己的 API Key 查询使用量
|
| 347 |
-
* @param {string} method - HTTP 方法
|
| 348 |
-
* @param {string} path - 请求路径
|
| 349 |
-
* @param {http.IncomingMessage} req - HTTP 请求对象
|
| 350 |
-
* @param {http.ServerResponse} res - HTTP 响应对象
|
| 351 |
-
* @returns {Promise<boolean>} - 是否处理了请求
|
| 352 |
-
*/
|
| 353 |
-
export async function handlePotluckUserApiRoutes(method, path, req, res) {
|
| 354 |
-
// 只处理 /api/potluckuser 开头的请求
|
| 355 |
-
if (!path.startsWith('/api/potluckuser')) {
|
| 356 |
-
return false;
|
| 357 |
-
}
|
| 358 |
-
logger.info('[API Potluck User] Handling request:', method, path);
|
| 359 |
-
|
| 360 |
-
try {
|
| 361 |
-
// 从请求中提取 API Key
|
| 362 |
-
const apiKey = extractApiKeyFromRequest(req);
|
| 363 |
-
|
| 364 |
-
if (!apiKey) {
|
| 365 |
-
sendJson(res, 401, {
|
| 366 |
-
success: false,
|
| 367 |
-
error: {
|
| 368 |
-
message: '需要 API Key。请在 Authorization 标头 (Bearer maki_xxx) 或 x-api-key 标头中提供您的 API Key。',
|
| 369 |
-
code: 'API_KEY_REQUIRED'
|
| 370 |
-
}
|
| 371 |
-
});
|
| 372 |
-
return true;
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
// 验证 API Key
|
| 376 |
-
const validation = await validateKey(apiKey);
|
| 377 |
-
|
| 378 |
-
if (!validation.valid && validation.reason !== 'quota_exceeded') {
|
| 379 |
-
const errorMessages = {
|
| 380 |
-
'invalid_format': 'API Key 格式无效',
|
| 381 |
-
'not_found': '未找到 API Key',
|
| 382 |
-
'disabled': 'API Key 已禁用'
|
| 383 |
-
};
|
| 384 |
-
|
| 385 |
-
sendJson(res, 401, {
|
| 386 |
-
success: false,
|
| 387 |
-
error: {
|
| 388 |
-
message: errorMessages[validation.reason] || '无效的 API Key',
|
| 389 |
-
code: validation.reason
|
| 390 |
-
}
|
| 391 |
-
});
|
| 392 |
-
return true;
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
// GET /api/potluckuser/usage - 获取当前用户的使用量信息
|
| 396 |
-
if (method === 'GET' && path === '/api/potluckuser/usage') {
|
| 397 |
-
const keyData = await getKey(apiKey);
|
| 398 |
-
|
| 399 |
-
if (!keyData) {
|
| 400 |
-
sendJson(res, 404, {
|
| 401 |
-
success: false,
|
| 402 |
-
error: { message: '未找到 Key', code: 'KEY_NOT_FOUND' }
|
| 403 |
-
});
|
| 404 |
-
return true;
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
// 计算使用百分比
|
| 408 |
-
const usagePercent = keyData.dailyLimit > 0
|
| 409 |
-
? Math.round((keyData.todayUsage / keyData.dailyLimit) * 100)
|
| 410 |
-
: 0;
|
| 411 |
-
|
| 412 |
-
// 返回用户友好的使用量信息(隐藏敏感信息)
|
| 413 |
-
sendJson(res, 200, {
|
| 414 |
-
success: true,
|
| 415 |
-
data: {
|
| 416 |
-
name: keyData.name,
|
| 417 |
-
enabled: keyData.enabled,
|
| 418 |
-
usage: {
|
| 419 |
-
today: keyData.todayUsage,
|
| 420 |
-
limit: keyData.dailyLimit,
|
| 421 |
-
remaining: Math.max(0, keyData.dailyLimit - keyData.todayUsage),
|
| 422 |
-
percent: usagePercent,
|
| 423 |
-
resetDate: keyData.lastResetDate
|
| 424 |
-
},
|
| 425 |
-
total: keyData.totalUsage,
|
| 426 |
-
lastUsedAt: keyData.lastUsedAt,
|
| 427 |
-
createdAt: keyData.createdAt,
|
| 428 |
-
usageHistory: keyData.usageHistory || {},
|
| 429 |
-
// 显示部分遮蔽的 Key ID
|
| 430 |
-
|
| 431 |
-
maskedKey: `${apiKey.substring(0, 12)}...${apiKey.substring(apiKey.length - 4)}`
|
| 432 |
-
}
|
| 433 |
-
});
|
| 434 |
-
return true;
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
// 未匹配的用户端路由
|
| 438 |
-
sendJson(res, 404, {
|
| 439 |
-
success: false,
|
| 440 |
-
error: { message: '未找到用户 API 端点' }
|
| 441 |
-
});
|
| 442 |
-
return true;
|
| 443 |
-
|
| 444 |
-
} catch (error) {
|
| 445 |
-
logger.error('[API Potluck] User API error:', error);
|
| 446 |
-
sendJson(res, 500, {
|
| 447 |
-
success: false,
|
| 448 |
-
error: { message: error.message || '内部服务器错误' }
|
| 449 |
-
});
|
| 450 |
-
return true;
|
| 451 |
-
}
|
| 452 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/plugins/api-potluck/index.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* API 大锅饭插件 - 标准插件格式
|
| 3 |
-
*
|
| 4 |
-
* 功能:
|
| 5 |
-
* 1. API Key 管理(创建、删除、启用/禁用)
|
| 6 |
-
* 2. 每日配额限制
|
| 7 |
-
* 3. 用量统计
|
| 8 |
-
* 4. 管理 API 接口
|
| 9 |
-
*/
|
| 10 |
-
|
| 11 |
-
import {
|
| 12 |
-
createKey,
|
| 13 |
-
listKeys,
|
| 14 |
-
getKey,
|
| 15 |
-
deleteKey,
|
| 16 |
-
updateKeyLimit,
|
| 17 |
-
resetKeyUsage,
|
| 18 |
-
toggleKey,
|
| 19 |
-
updateKeyName,
|
| 20 |
-
validateKey,
|
| 21 |
-
incrementUsage,
|
| 22 |
-
getStats,
|
| 23 |
-
KEY_PREFIX,
|
| 24 |
-
setConfigGetter
|
| 25 |
-
} from './key-manager.js';
|
| 26 |
-
|
| 27 |
-
import {
|
| 28 |
-
extractPotluckKey,
|
| 29 |
-
isPotluckRequest,
|
| 30 |
-
sendPotluckError
|
| 31 |
-
} from './middleware.js';
|
| 32 |
-
|
| 33 |
-
import logger from '../../utils/logger.js';
|
| 34 |
-
|
| 35 |
-
import { handlePotluckApiRoutes, handlePotluckUserApiRoutes } from './api-routes.js';
|
| 36 |
-
|
| 37 |
-
/**
|
| 38 |
-
* 插件定义
|
| 39 |
-
*/
|
| 40 |
-
const apiPotluckPlugin = {
|
| 41 |
-
name: 'api-potluck',
|
| 42 |
-
version: '1.0.2',
|
| 43 |
-
description: 'API 大锅饭 - Key 管理和用量统计插件<br>管理端:<a href="potluck.html" target="_blank">potluck.html</a><br>用户端:<a href="potluck-user.html" target="_blank">potluck-user.html</a>',
|
| 44 |
-
|
| 45 |
-
// 插件类型:认证插件
|
| 46 |
-
type: 'auth',
|
| 47 |
-
|
| 48 |
-
// 优先级:数字越小越先执行,默认认证插件优先级为 9999
|
| 49 |
-
_priority: 10,
|
| 50 |
-
|
| 51 |
-
/**
|
| 52 |
-
* 初始化钩子
|
| 53 |
-
* @param {Object} config - 服务器配置
|
| 54 |
-
*/
|
| 55 |
-
async init(config) {
|
| 56 |
-
logger.info('[API Potluck Plugin] Initializing...');
|
| 57 |
-
},
|
| 58 |
-
|
| 59 |
-
/**
|
| 60 |
-
* 销毁钩子
|
| 61 |
-
*/
|
| 62 |
-
async destroy() {
|
| 63 |
-
logger.info('[API Potluck Plugin] Destroying...');
|
| 64 |
-
},
|
| 65 |
-
|
| 66 |
-
/**
|
| 67 |
-
* 静态文件路径
|
| 68 |
-
*/
|
| 69 |
-
staticPaths: ['potluck.html', 'potluck-user.html'],
|
| 70 |
-
|
| 71 |
-
/**
|
| 72 |
-
* 路由定义
|
| 73 |
-
*/
|
| 74 |
-
routes: [
|
| 75 |
-
{
|
| 76 |
-
method: '*',
|
| 77 |
-
path: '/api/potluckuser',
|
| 78 |
-
handler: handlePotluckUserApiRoutes
|
| 79 |
-
},
|
| 80 |
-
{
|
| 81 |
-
method: '*',
|
| 82 |
-
path: '/api/potluck',
|
| 83 |
-
handler: handlePotluckApiRoutes
|
| 84 |
-
}
|
| 85 |
-
],
|
| 86 |
-
|
| 87 |
-
/**
|
| 88 |
-
* 认证方法 - 处理 Potluck Key 认证
|
| 89 |
-
* @param {http.IncomingMessage} req - HTTP 请求
|
| 90 |
-
* @param {http.ServerResponse} res - HTTP 响应
|
| 91 |
-
* @param {URL} requestUrl - 解析后的 URL
|
| 92 |
-
* @param {Object} config - 服务器配置
|
| 93 |
-
* @returns {Promise<{handled: boolean, authorized: boolean|null, error?: Object, data?: Object}>}
|
| 94 |
-
*/
|
| 95 |
-
async authenticate(req, res, requestUrl, config) {
|
| 96 |
-
const apiKey = extractPotluckKey(req, requestUrl);
|
| 97 |
-
|
| 98 |
-
if (!apiKey) {
|
| 99 |
-
// 不是 potluck 请求,返回 null 让其他认证插件处理
|
| 100 |
-
return { handled: false, authorized: null };
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
// 验证 Key
|
| 104 |
-
const validation = await validateKey(apiKey);
|
| 105 |
-
|
| 106 |
-
if (!validation.valid) {
|
| 107 |
-
const errorMessages = {
|
| 108 |
-
'invalid_format': 'Invalid API key format',
|
| 109 |
-
'not_found': 'API key not found',
|
| 110 |
-
'disabled': 'API key has been disabled',
|
| 111 |
-
'quota_exceeded': 'Quota exceeded for this API key'
|
| 112 |
-
};
|
| 113 |
-
|
| 114 |
-
const statusCodes = {
|
| 115 |
-
'invalid_format': 401,
|
| 116 |
-
'not_found': 401,
|
| 117 |
-
'disabled': 403,
|
| 118 |
-
'quota_exceeded': 429
|
| 119 |
-
};
|
| 120 |
-
|
| 121 |
-
const error = {
|
| 122 |
-
statusCode: statusCodes[validation.reason] || 401,
|
| 123 |
-
message: errorMessages[validation.reason] || 'Authentication failed',
|
| 124 |
-
code: validation.reason,
|
| 125 |
-
keyData: validation.keyData
|
| 126 |
-
};
|
| 127 |
-
|
| 128 |
-
// 发送错误响应
|
| 129 |
-
sendPotluckError(res, error);
|
| 130 |
-
return { handled: true, authorized: false, error };
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
// 认证成功,返回数据供后续使用
|
| 134 |
-
logger.info(`[API Potluck Plugin] Authorized with key: ${apiKey.substring(0, 12)}...`);
|
| 135 |
-
return {
|
| 136 |
-
handled: false,
|
| 137 |
-
authorized: true,
|
| 138 |
-
data: {
|
| 139 |
-
potluckApiKey: apiKey,
|
| 140 |
-
potluckKeyData: validation.keyData
|
| 141 |
-
}
|
| 142 |
-
};
|
| 143 |
-
},
|
| 144 |
-
|
| 145 |
-
/**
|
| 146 |
-
* 钩子函数
|
| 147 |
-
*/
|
| 148 |
-
hooks: {
|
| 149 |
-
/**
|
| 150 |
-
* 内容生成后钩子 - 记录用量
|
| 151 |
-
* @param {Object} hookContext - 钩子上下文,包含请求和模型信息
|
| 152 |
-
*/
|
| 153 |
-
async onContentGenerated(hookContext) {
|
| 154 |
-
if (hookContext.potluckApiKey) {
|
| 155 |
-
try {
|
| 156 |
-
// 传入提供商和模型信息
|
| 157 |
-
await incrementUsage(
|
| 158 |
-
hookContext.potluckApiKey,
|
| 159 |
-
hookContext.toProvider,
|
| 160 |
-
hookContext.model
|
| 161 |
-
);
|
| 162 |
-
} catch (e) {
|
| 163 |
-
// 静默失败,不影响主流程
|
| 164 |
-
logger.error('[API Potluck Plugin] Failed to record usage:', e.message);
|
| 165 |
-
}
|
| 166 |
-
}
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
},
|
| 170 |
-
|
| 171 |
-
// 导出内部函数供外部使用(可选)
|
| 172 |
-
exports: {
|
| 173 |
-
createKey,
|
| 174 |
-
listKeys,
|
| 175 |
-
getKey,
|
| 176 |
-
deleteKey,
|
| 177 |
-
updateKeyLimit,
|
| 178 |
-
resetKeyUsage,
|
| 179 |
-
toggleKey,
|
| 180 |
-
updateKeyName,
|
| 181 |
-
validateKey,
|
| 182 |
-
incrementUsage,
|
| 183 |
-
getStats,
|
| 184 |
-
KEY_PREFIX,
|
| 185 |
-
extractPotluckKey,
|
| 186 |
-
isPotluckRequest
|
| 187 |
-
}
|
| 188 |
-
};
|
| 189 |
-
|
| 190 |
-
export default apiPotluckPlugin;
|
| 191 |
-
|
| 192 |
-
// 也导出命名导出,方便直接引用
|
| 193 |
-
export {
|
| 194 |
-
createKey,
|
| 195 |
-
listKeys,
|
| 196 |
-
getKey,
|
| 197 |
-
deleteKey,
|
| 198 |
-
updateKeyLimit,
|
| 199 |
-
resetKeyUsage,
|
| 200 |
-
toggleKey,
|
| 201 |
-
updateKeyName,
|
| 202 |
-
validateKey,
|
| 203 |
-
incrementUsage,
|
| 204 |
-
getStats,
|
| 205 |
-
KEY_PREFIX,
|
| 206 |
-
extractPotluckKey,
|
| 207 |
-
isPotluckRequest
|
| 208 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/plugins/api-potluck/key-manager.js
DELETED
|
@@ -1,486 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* API 大锅饭 - Key 管理模块
|
| 3 |
-
* 使用内存缓存 + 写锁 + 定期持久化,解决并发安全问题
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { promises as fs } from 'fs';
|
| 7 |
-
import logger from '../../utils/logger.js';
|
| 8 |
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
| 9 |
-
import path from 'path';
|
| 10 |
-
import crypto from 'crypto';
|
| 11 |
-
|
| 12 |
-
// 配置文件路径
|
| 13 |
-
const KEYS_STORE_FILE = path.join(process.cwd(), 'configs', 'api-potluck-keys.json');
|
| 14 |
-
const KEY_PREFIX = 'maki_';
|
| 15 |
-
|
| 16 |
-
// 默认配置
|
| 17 |
-
const DEFAULT_CONFIG = {
|
| 18 |
-
defaultDailyLimit: 500,
|
| 19 |
-
persistInterval: 5000
|
| 20 |
-
};
|
| 21 |
-
|
| 22 |
-
// 配置获取函数(由外部注入)
|
| 23 |
-
let configGetter = null;
|
| 24 |
-
|
| 25 |
-
/**
|
| 26 |
-
* 设置配置获取函数
|
| 27 |
-
* @param {Function} getter - 返回配置对象的函数
|
| 28 |
-
*/
|
| 29 |
-
export function setConfigGetter(getter) {
|
| 30 |
-
configGetter = getter;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
/**
|
| 34 |
-
* 获取当前配置
|
| 35 |
-
*/
|
| 36 |
-
function getConfig() {
|
| 37 |
-
if (configGetter) {
|
| 38 |
-
return configGetter();
|
| 39 |
-
}
|
| 40 |
-
return DEFAULT_CONFIG;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
// 内存缓存
|
| 44 |
-
let keyStore = null;
|
| 45 |
-
let isDirty = false;
|
| 46 |
-
let isWriting = false;
|
| 47 |
-
let persistTimer = null;
|
| 48 |
-
let currentPersistInterval = DEFAULT_CONFIG.persistInterval;
|
| 49 |
-
|
| 50 |
-
/**
|
| 51 |
-
* 初始化:从文件加载数据到内存
|
| 52 |
-
*/
|
| 53 |
-
function ensureLoaded() {
|
| 54 |
-
if (keyStore !== null) return;
|
| 55 |
-
try {
|
| 56 |
-
if (existsSync(KEYS_STORE_FILE)) {
|
| 57 |
-
const content = readFileSync(KEYS_STORE_FILE, 'utf8');
|
| 58 |
-
keyStore = JSON.parse(content);
|
| 59 |
-
} else {
|
| 60 |
-
keyStore = { keys: {} };
|
| 61 |
-
syncWriteToFile();
|
| 62 |
-
}
|
| 63 |
-
} catch (error) {
|
| 64 |
-
logger.error('[API Potluck] Failed to load key store:', error.message);
|
| 65 |
-
keyStore = { keys: {} };
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
// 获取配置的持久化间隔
|
| 69 |
-
const config = getConfig();
|
| 70 |
-
currentPersistInterval = config.persistInterval || DEFAULT_CONFIG.persistInterval;
|
| 71 |
-
|
| 72 |
-
// 启动定期持久化
|
| 73 |
-
if (!persistTimer) {
|
| 74 |
-
persistTimer = setInterval(persistIfDirty, currentPersistInterval);
|
| 75 |
-
// 进程退出时保存
|
| 76 |
-
process.on('beforeExit', () => persistIfDirty());
|
| 77 |
-
process.on('SIGINT', () => { persistIfDirty(); process.exit(0); });
|
| 78 |
-
process.on('SIGTERM', () => { persistIfDirty(); process.exit(0); });
|
| 79 |
-
}
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
/**
|
| 83 |
-
* 同步写入文件(仅初始化时使用)
|
| 84 |
-
*/
|
| 85 |
-
function syncWriteToFile() {
|
| 86 |
-
try {
|
| 87 |
-
const dir = path.dirname(KEYS_STORE_FILE);
|
| 88 |
-
if (!existsSync(dir)) {
|
| 89 |
-
require('fs').mkdirSync(dir, { recursive: true });
|
| 90 |
-
}
|
| 91 |
-
writeFileSync(KEYS_STORE_FILE, JSON.stringify(keyStore, null, 2), 'utf8');
|
| 92 |
-
} catch (error) {
|
| 93 |
-
logger.error('[API Potluck] Sync write failed:', error.message);
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
/**
|
| 98 |
-
* 异步持久化(带写锁)
|
| 99 |
-
*/
|
| 100 |
-
async function persistIfDirty() {
|
| 101 |
-
if (!isDirty || isWriting || keyStore === null) return;
|
| 102 |
-
isWriting = true;
|
| 103 |
-
try {
|
| 104 |
-
const dir = path.dirname(KEYS_STORE_FILE);
|
| 105 |
-
if (!existsSync(dir)) {
|
| 106 |
-
await fs.mkdir(dir, { recursive: true });
|
| 107 |
-
}
|
| 108 |
-
// 写入临时文件再重命名,防止写入中断导致文件损坏
|
| 109 |
-
const tempFile = KEYS_STORE_FILE + '.tmp';
|
| 110 |
-
await fs.writeFile(tempFile, JSON.stringify(keyStore, null, 2), 'utf8');
|
| 111 |
-
await fs.rename(tempFile, KEYS_STORE_FILE);
|
| 112 |
-
isDirty = false;
|
| 113 |
-
} catch (error) {
|
| 114 |
-
logger.error('[API Potluck] Persist failed:', error.message);
|
| 115 |
-
} finally {
|
| 116 |
-
isWriting = false;
|
| 117 |
-
}
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
/**
|
| 121 |
-
* 标记数据已修改
|
| 122 |
-
*/
|
| 123 |
-
function markDirty() {
|
| 124 |
-
isDirty = true;
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
/**
|
| 128 |
-
* 生成随机 API Key(确保不重复)
|
| 129 |
-
*/
|
| 130 |
-
function generateApiKey() {
|
| 131 |
-
ensureLoaded();
|
| 132 |
-
let apiKey;
|
| 133 |
-
let attempts = 0;
|
| 134 |
-
const maxAttempts = 10;
|
| 135 |
-
|
| 136 |
-
do {
|
| 137 |
-
apiKey = `${KEY_PREFIX}${crypto.randomBytes(16).toString('hex')}`;
|
| 138 |
-
attempts++;
|
| 139 |
-
if (attempts >= maxAttempts) {
|
| 140 |
-
throw new Error('Failed to generate unique API key after multiple attempts');
|
| 141 |
-
}
|
| 142 |
-
} while (keyStore.keys[apiKey]);
|
| 143 |
-
|
| 144 |
-
return apiKey;
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
/**
|
| 148 |
-
* 获取今天的日期字符串 (YYYY-MM-DD)
|
| 149 |
-
*/
|
| 150 |
-
function getTodayDateString() {
|
| 151 |
-
const now = new Date();
|
| 152 |
-
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
/**
|
| 156 |
-
* 检查并重置过期的每日计数
|
| 157 |
-
*/
|
| 158 |
-
function checkAndResetDailyCount(keyData) {
|
| 159 |
-
const today = getTodayDateString();
|
| 160 |
-
if (keyData.lastResetDate !== today) {
|
| 161 |
-
keyData.todayUsage = 0;
|
| 162 |
-
keyData.lastResetDate = today;
|
| 163 |
-
}
|
| 164 |
-
return keyData;
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
/**
|
| 168 |
-
* 创建新的 API Key
|
| 169 |
-
|
| 170 |
-
* @param {string} name - Key 名称
|
| 171 |
-
* @param {number} [dailyLimit] - 每日限额,不传则使用配置的默认值
|
| 172 |
-
*/
|
| 173 |
-
export async function createKey(name = '', dailyLimit = null) {
|
| 174 |
-
ensureLoaded();
|
| 175 |
-
const config = getConfig();
|
| 176 |
-
const actualDailyLimit = dailyLimit ?? config.defaultDailyLimit ?? DEFAULT_CONFIG.defaultDailyLimit;
|
| 177 |
-
|
| 178 |
-
const apiKey = generateApiKey();
|
| 179 |
-
const now = new Date().toISOString();
|
| 180 |
-
const today = getTodayDateString();
|
| 181 |
-
|
| 182 |
-
const keyData = {
|
| 183 |
-
id: apiKey,
|
| 184 |
-
name: name || `Key-${Object.keys(keyStore.keys).length + 1}`,
|
| 185 |
-
createdAt: now,
|
| 186 |
-
dailyLimit: actualDailyLimit,
|
| 187 |
-
todayUsage: 0,
|
| 188 |
-
totalUsage: 0,
|
| 189 |
-
lastResetDate: today,
|
| 190 |
-
lastUsedAt: null,
|
| 191 |
-
enabled: true
|
| 192 |
-
};
|
| 193 |
-
|
| 194 |
-
keyStore.keys[apiKey] = keyData;
|
| 195 |
-
markDirty();
|
| 196 |
-
await persistIfDirty(); // 创建操作立即持久化
|
| 197 |
-
|
| 198 |
-
logger.info(`[API Potluck] Created key: ${apiKey.substring(0, 12)}...`);
|
| 199 |
-
return keyData;
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
/**
|
| 203 |
-
* 获取所有 Key 列表
|
| 204 |
-
*/
|
| 205 |
-
export async function listKeys() {
|
| 206 |
-
ensureLoaded();
|
| 207 |
-
const keys = [];
|
| 208 |
-
for (const [keyId, keyData] of Object.entries(keyStore.keys)) {
|
| 209 |
-
const updated = checkAndResetDailyCount({ ...keyData });
|
| 210 |
-
keys.push({
|
| 211 |
-
...updated,
|
| 212 |
-
maskedKey: `${keyId.substring(0, 12)}...${keyId.substring(keyId.length - 4)}`
|
| 213 |
-
});
|
| 214 |
-
}
|
| 215 |
-
return keys;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
/**
|
| 219 |
-
* 获取单个 Key 详情
|
| 220 |
-
*/
|
| 221 |
-
export async function getKey(keyId) {
|
| 222 |
-
ensureLoaded();
|
| 223 |
-
const keyData = keyStore.keys[keyId];
|
| 224 |
-
if (!keyData) return null;
|
| 225 |
-
return checkAndResetDailyCount({ ...keyData });
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
/**
|
| 229 |
-
* 删除 Key
|
| 230 |
-
*/
|
| 231 |
-
export async function deleteKey(keyId) {
|
| 232 |
-
ensureLoaded();
|
| 233 |
-
if (!keyStore.keys[keyId]) return false;
|
| 234 |
-
delete keyStore.keys[keyId];
|
| 235 |
-
markDirty();
|
| 236 |
-
await persistIfDirty(); // 删除操作立即持久化
|
| 237 |
-
logger.info(`[API Potluck] Deleted key: ${keyId.substring(0, 12)}...`);
|
| 238 |
-
return true;
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
/**
|
| 242 |
-
* 更新 Key 的每日限额
|
| 243 |
-
*/
|
| 244 |
-
export async function updateKeyLimit(keyId, newLimit) {
|
| 245 |
-
ensureLoaded();
|
| 246 |
-
if (!keyStore.keys[keyId]) return null;
|
| 247 |
-
keyStore.keys[keyId].dailyLimit = newLimit;
|
| 248 |
-
markDirty();
|
| 249 |
-
return keyStore.keys[keyId];
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
/**
|
| 253 |
-
* 重置 Key 的当天调用次数
|
| 254 |
-
*/
|
| 255 |
-
export async function resetKeyUsage(keyId) {
|
| 256 |
-
ensureLoaded();
|
| 257 |
-
if (!keyStore.keys[keyId]) return null;
|
| 258 |
-
keyStore.keys[keyId].todayUsage = 0;
|
| 259 |
-
keyStore.keys[keyId].lastResetDate = getTodayDateString();
|
| 260 |
-
markDirty();
|
| 261 |
-
return keyStore.keys[keyId];
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
/**
|
| 265 |
-
* 切换 Key 的启用/禁用状态
|
| 266 |
-
*/
|
| 267 |
-
export async function toggleKey(keyId) {
|
| 268 |
-
ensureLoaded();
|
| 269 |
-
if (!keyStore.keys[keyId]) return null;
|
| 270 |
-
keyStore.keys[keyId].enabled = !keyStore.keys[keyId].enabled;
|
| 271 |
-
markDirty();
|
| 272 |
-
return keyStore.keys[keyId];
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
/**
|
| 276 |
-
* 更新 Key 名称
|
| 277 |
-
*/
|
| 278 |
-
export async function updateKeyName(keyId, newName) {
|
| 279 |
-
ensureLoaded();
|
| 280 |
-
if (!keyStore.keys[keyId]) return null;
|
| 281 |
-
keyStore.keys[keyId].name = newName;
|
| 282 |
-
markDirty();
|
| 283 |
-
return keyStore.keys[keyId];
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
/**
|
| 287 |
-
* 重新生成 API Key(保留原有数据,更换 Key ID)
|
| 288 |
-
* @param {string} oldKeyId - 原 Key ID
|
| 289 |
-
* @returns {Promise<{oldKey: string, newKey: string, keyData: Object}|null>}
|
| 290 |
-
*/
|
| 291 |
-
export async function regenerateKey(oldKeyId) {
|
| 292 |
-
ensureLoaded();
|
| 293 |
-
const oldKeyData = keyStore.keys[oldKeyId];
|
| 294 |
-
if (!oldKeyData) return null;
|
| 295 |
-
|
| 296 |
-
// 生成新的唯一 Key
|
| 297 |
-
const newKeyId = generateApiKey();
|
| 298 |
-
|
| 299 |
-
// 复制数据到新 Key
|
| 300 |
-
const newKeyData = {
|
| 301 |
-
...oldKeyData,
|
| 302 |
-
id: newKeyId,
|
| 303 |
-
regeneratedAt: new Date().toISOString(),
|
| 304 |
-
regeneratedFrom: oldKeyId.substring(0, 12) + '...'
|
| 305 |
-
};
|
| 306 |
-
|
| 307 |
-
// 删除旧 Key,添加新 Key
|
| 308 |
-
delete keyStore.keys[oldKeyId];
|
| 309 |
-
keyStore.keys[newKeyId] = newKeyData;
|
| 310 |
-
|
| 311 |
-
markDirty();
|
| 312 |
-
await persistIfDirty(); // 立即持久化
|
| 313 |
-
|
| 314 |
-
logger.info(`[API Potluck] Regenerated key: ${oldKeyId.substring(0, 12)}... -> ${newKeyId.substring(0, 12)}...`);
|
| 315 |
-
|
| 316 |
-
return {
|
| 317 |
-
oldKey: oldKeyId,
|
| 318 |
-
newKey: newKeyId,
|
| 319 |
-
keyData: newKeyData
|
| 320 |
-
};
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
/**
|
| 324 |
-
* 验证 API Key 是否有效且有配额
|
| 325 |
-
*/
|
| 326 |
-
export async function validateKey(apiKey) {
|
| 327 |
-
ensureLoaded();
|
| 328 |
-
if (!apiKey || !apiKey.startsWith(KEY_PREFIX)) {
|
| 329 |
-
return { valid: false, reason: 'invalid_format' };
|
| 330 |
-
}
|
| 331 |
-
const keyData = keyStore.keys[apiKey];
|
| 332 |
-
if (!keyData) return { valid: false, reason: 'not_found' };
|
| 333 |
-
if (!keyData.enabled) return { valid: false, reason: 'disabled' };
|
| 334 |
-
|
| 335 |
-
// 直接在内存中检查和重置
|
| 336 |
-
checkAndResetDailyCount(keyData);
|
| 337 |
-
|
| 338 |
-
// 检查每日限额
|
| 339 |
-
if (keyData.todayUsage < keyData.dailyLimit) {
|
| 340 |
-
return { valid: true, keyData };
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
return { valid: false, reason: 'quota_exceeded', keyData };
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
/**
|
| 347 |
-
* 增加 Key 的使用次数(原子操作,直接修改内存)
|
| 348 |
-
* @param {string} apiKey - API Key
|
| 349 |
-
* @param {string} provider - 使用的提供商
|
| 350 |
-
* @param {string} model - 使用的模型
|
| 351 |
-
*/
|
| 352 |
-
export async function incrementUsage(apiKey, provider = 'unknown', model = 'unknown') {
|
| 353 |
-
ensureLoaded();
|
| 354 |
-
const keyData = keyStore.keys[apiKey];
|
| 355 |
-
if (!keyData) return null;
|
| 356 |
-
|
| 357 |
-
checkAndResetDailyCount(keyData);
|
| 358 |
-
|
| 359 |
-
// 消耗每日限额
|
| 360 |
-
if (keyData.todayUsage < keyData.dailyLimit) {
|
| 361 |
-
keyData.todayUsage += 1;
|
| 362 |
-
} else {
|
| 363 |
-
// 每日限额用尽
|
| 364 |
-
return null;
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
keyData.totalUsage += 1;
|
| 368 |
-
keyData.lastUsedAt = new Date().toISOString();
|
| 369 |
-
|
| 370 |
-
// 记录个人按天统计 (每个 Key 独立)
|
| 371 |
-
const today = getTodayDateString();
|
| 372 |
-
if (!keyData.usageHistory) keyData.usageHistory = {};
|
| 373 |
-
if (!keyData.usageHistory[today]) {
|
| 374 |
-
keyData.usageHistory[today] = { providers: {}, models: {} };
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
// 确保 provider 和 model 是字符串
|
| 378 |
-
const pName = String(provider || 'unknown');
|
| 379 |
-
const mName = String(model || 'unknown');
|
| 380 |
-
|
| 381 |
-
const userHistory = keyData.usageHistory[today];
|
| 382 |
-
userHistory.providers[pName] = (userHistory.providers[pName] || 0) + 1;
|
| 383 |
-
userHistory.models[mName] = (userHistory.models[mName] || 0) + 1;
|
| 384 |
-
|
| 385 |
-
// 清理该 Key 的过期历史 (保留 7 天)
|
| 386 |
-
const userDates = Object.keys(keyData.usageHistory).sort();
|
| 387 |
-
if (userDates.length > 7) {
|
| 388 |
-
const dropDates = userDates.slice(0, userDates.length - 7);
|
| 389 |
-
dropDates.forEach(d => delete keyData.usageHistory[d]);
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
markDirty();
|
| 393 |
-
|
| 394 |
-
return {
|
| 395 |
-
...keyData,
|
| 396 |
-
usedBonus: false
|
| 397 |
-
};
|
| 398 |
-
}
|
| 399 |
-
|
| 400 |
-
/**
|
| 401 |
-
* 获取统计信息
|
| 402 |
-
*/
|
| 403 |
-
export async function getStats() {
|
| 404 |
-
ensureLoaded();
|
| 405 |
-
const keys = Object.values(keyStore.keys);
|
| 406 |
-
let enabledKeys = 0, todayTotalUsage = 0, totalUsage = 0;
|
| 407 |
-
const aggregatedHistory = {};
|
| 408 |
-
|
| 409 |
-
for (const key of keys) {
|
| 410 |
-
checkAndResetDailyCount(key);
|
| 411 |
-
if (key.enabled) enabledKeys++;
|
| 412 |
-
todayTotalUsage += key.todayUsage;
|
| 413 |
-
totalUsage += key.totalUsage;
|
| 414 |
-
|
| 415 |
-
// 汇总每个 Key 的历史数据
|
| 416 |
-
if (key.usageHistory) {
|
| 417 |
-
Object.entries(key.usageHistory).forEach(([date, history]) => {
|
| 418 |
-
if (!aggregatedHistory[date]) {
|
| 419 |
-
aggregatedHistory[date] = { providers: {}, models: {} };
|
| 420 |
-
}
|
| 421 |
-
|
| 422 |
-
// 汇总提供商
|
| 423 |
-
if (history.providers) {
|
| 424 |
-
Object.entries(history.providers).forEach(([p, count]) => {
|
| 425 |
-
aggregatedHistory[date].providers[p] = (aggregatedHistory[date].providers[p] || 0) + count;
|
| 426 |
-
});
|
| 427 |
-
}
|
| 428 |
-
|
| 429 |
-
// 汇总模型
|
| 430 |
-
if (history.models) {
|
| 431 |
-
Object.entries(history.models).forEach(([m, count]) => {
|
| 432 |
-
aggregatedHistory[date].models[m] = (aggregatedHistory[date].models[m] || 0) + count;
|
| 433 |
-
});
|
| 434 |
-
}
|
| 435 |
-
});
|
| 436 |
-
}
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
return {
|
| 440 |
-
totalKeys: keys.length,
|
| 441 |
-
enabledKeys,
|
| 442 |
-
disabledKeys: keys.length - enabledKeys,
|
| 443 |
-
todayTotalUsage,
|
| 444 |
-
totalUsage,
|
| 445 |
-
usageHistory: aggregatedHistory
|
| 446 |
-
};
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
/**
|
| 451 |
-
* 批量更新所有 Key 的每日限额
|
| 452 |
-
* @param {number} newLimit - 新的每日限额
|
| 453 |
-
* @returns {Promise<{total: number, updated: number}>}
|
| 454 |
-
*/
|
| 455 |
-
export async function applyDailyLimitToAllKeys(newLimit) {
|
| 456 |
-
ensureLoaded();
|
| 457 |
-
const keys = Object.values(keyStore.keys);
|
| 458 |
-
let updated = 0;
|
| 459 |
-
|
| 460 |
-
for (const keyData of keys) {
|
| 461 |
-
if (keyData.dailyLimit !== newLimit) {
|
| 462 |
-
keyData.dailyLimit = newLimit;
|
| 463 |
-
updated++;
|
| 464 |
-
}
|
| 465 |
-
}
|
| 466 |
-
|
| 467 |
-
if (updated > 0) {
|
| 468 |
-
markDirty();
|
| 469 |
-
await persistIfDirty();
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
logger.info(`[API Potluck] Applied daily limit ${newLimit} to ${updated}/${keys.length} keys`);
|
| 473 |
-
return { total: keys.length, updated };
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
/**
|
| 477 |
-
* 获取所有 Key ID 列表
|
| 478 |
-
* @returns {string[]}
|
| 479 |
-
*/
|
| 480 |
-
export function getAllKeyIds() {
|
| 481 |
-
ensureLoaded();
|
| 482 |
-
return Object.keys(keyStore.keys);
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
// 导出常量
|
| 486 |
-
export { KEY_PREFIX };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/plugins/api-potluck/middleware.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* API 大锅饭 - 中间件模块
|
| 3 |
-
* 负责请求拦截和配额检查
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
import { validateKey, incrementUsage, KEY_PREFIX } from './key-manager.js';
|
| 7 |
-
import logger from '../../utils/logger.js';
|
| 8 |
-
|
| 9 |
-
/**
|
| 10 |
-
* 从请求中提取 Potluck API Key
|
| 11 |
-
* 支持多种认证方式:
|
| 12 |
-
* 1. Authorization: Bearer maki_xxx
|
| 13 |
-
* 2. x-api-key: maki_xxx
|
| 14 |
-
* 3. x-goog-api-key: maki_xxx
|
| 15 |
-
* 4. URL query: ?key=maki_xxx
|
| 16 |
-
*
|
| 17 |
-
* @param {http.IncomingMessage} req - HTTP 请求对象
|
| 18 |
-
* @param {URL} requestUrl - 解析后的 URL 对象
|
| 19 |
-
* @returns {string|null} 提取到的 API Key,如果不是 potluck key 则返回 null
|
| 20 |
-
*/
|
| 21 |
-
export function extractPotluckKey(req, requestUrl) {
|
| 22 |
-
// 1. 检查 Authorization header
|
| 23 |
-
const authHeader = req.headers['authorization'];
|
| 24 |
-
if (authHeader && authHeader.startsWith('Bearer ')) {
|
| 25 |
-
const token = authHeader.substring(7);
|
| 26 |
-
if (token.startsWith(KEY_PREFIX)) {
|
| 27 |
-
return token;
|
| 28 |
-
}
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
// 2. 检查 x-api-key header (Claude style)
|
| 32 |
-
const xApiKey = req.headers['x-api-key'];
|
| 33 |
-
if (xApiKey && xApiKey.startsWith(KEY_PREFIX)) {
|
| 34 |
-
return xApiKey;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
// 3. 检查 x-goog-api-key header (Gemini style)
|
| 38 |
-
const googApiKey = req.headers['x-goog-api-key'];
|
| 39 |
-
if (googApiKey && googApiKey.startsWith(KEY_PREFIX)) {
|
| 40 |
-
return googApiKey;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
// 4. 检查 URL query parameter
|
| 44 |
-
const queryKey = requestUrl.searchParams.get('key');
|
| 45 |
-
if (queryKey && queryKey.startsWith(KEY_PREFIX)) {
|
| 46 |
-
return queryKey;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
return null;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
/**
|
| 53 |
-
* 检查请求是否使用 Potluck Key
|
| 54 |
-
* @param {http.IncomingMessage} req - HTTP 请求对象
|
| 55 |
-
* @param {URL} requestUrl - 解析后的 URL 对象
|
| 56 |
-
* @returns {boolean}
|
| 57 |
-
*/
|
| 58 |
-
export function isPotluckRequest(req, requestUrl) {
|
| 59 |
-
return extractPotluckKey(req, requestUrl) !== null;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
/**
|
| 63 |
-
* Potluck 认证中间件
|
| 64 |
-
* 验证 Potluck API Key 并检查配额
|
| 65 |
-
*
|
| 66 |
-
* @param {http.IncomingMessage} req - HTTP 请求对象
|
| 67 |
-
* @param {URL} requestUrl - 解析后的 URL 对象
|
| 68 |
-
* @returns {Promise<{authorized: boolean, error?: Object, keyData?: Object, apiKey?: string}>}
|
| 69 |
-
*/
|
| 70 |
-
export async function potluckAuthMiddleware(req, requestUrl) {
|
| 71 |
-
const apiKey = extractPotluckKey(req, requestUrl);
|
| 72 |
-
|
| 73 |
-
if (!apiKey) {
|
| 74 |
-
// 不是 potluck 请求,返回 null 让原有逻辑处理
|
| 75 |
-
return { authorized: null };
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
// 验证 Key
|
| 79 |
-
const validation = await validateKey(apiKey);
|
| 80 |
-
|
| 81 |
-
if (!validation.valid) {
|
| 82 |
-
const errorMessages = {
|
| 83 |
-
'invalid_format': 'Invalid API key format',
|
| 84 |
-
'not_found': 'API key not found',
|
| 85 |
-
'disabled': 'API key has been disabled',
|
| 86 |
-
'quota_exceeded': 'Quota exceeded for this API key'
|
| 87 |
-
};
|
| 88 |
-
|
| 89 |
-
const statusCodes = {
|
| 90 |
-
'invalid_format': 401,
|
| 91 |
-
'not_found': 401,
|
| 92 |
-
'disabled': 403,
|
| 93 |
-
'quota_exceeded': 429
|
| 94 |
-
};
|
| 95 |
-
|
| 96 |
-
return {
|
| 97 |
-
authorized: false,
|
| 98 |
-
error: {
|
| 99 |
-
statusCode: statusCodes[validation.reason] || 401,
|
| 100 |
-
message: errorMessages[validation.reason] || 'Authentication failed',
|
| 101 |
-
code: validation.reason,
|
| 102 |
-
keyData: validation.keyData
|
| 103 |
-
}
|
| 104 |
-
};
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
return {
|
| 108 |
-
authorized: true,
|
| 109 |
-
keyData: validation.keyData,
|
| 110 |
-
apiKey: apiKey
|
| 111 |
-
};
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
/**
|
| 115 |
-
* 记录 Potluck 请求使用
|
| 116 |
-
* 在请求成功处理后调用
|
| 117 |
-
*
|
| 118 |
-
* @param {string} apiKey - API Key
|
| 119 |
-
* @returns {Promise<Object|null>}
|
| 120 |
-
*/
|
| 121 |
-
export async function recordPotluckUsage(apiKey) {
|
| 122 |
-
if (!apiKey || !apiKey.startsWith(KEY_PREFIX)) {
|
| 123 |
-
return null;
|
| 124 |
-
}
|
| 125 |
-
return incrementUsage(apiKey);
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
/**
|
| 129 |
-
* 创建 Potluck 错误响应
|
| 130 |
-
* @param {http.ServerResponse} res - HTTP 响应对象
|
| 131 |
-
* @param {Object} error - 错误信息
|
| 132 |
-
*/
|
| 133 |
-
export function sendPotluckError(res, error) {
|
| 134 |
-
const response = {
|
| 135 |
-
error: {
|
| 136 |
-
message: error.message,
|
| 137 |
-
code: error.code,
|
| 138 |
-
type: 'potluck_error'
|
| 139 |
-
}
|
| 140 |
-
};
|
| 141 |
-
|
| 142 |
-
// 如果是配额超限,添加额外信息
|
| 143 |
-
if (error.code === 'quota_exceeded' && error.keyData) {
|
| 144 |
-
response.error.quota = {
|
| 145 |
-
used: error.keyData.todayUsage,
|
| 146 |
-
limit: error.keyData.dailyLimit,
|
| 147 |
-
resetDate: error.keyData.lastResetDate
|
| 148 |
-
};
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
// 检查响应流是否已关闭
|
| 152 |
-
if (res.writableEnded || res.destroyed) {
|
| 153 |
-
logger.warn('[API Potluck] Response already ended, skipping error response');
|
| 154 |
-
return;
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
if (!res.headersSent) {
|
| 158 |
-
res.writeHead(error.statusCode, { 'Content-Type': 'application/json' });
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
try {
|
| 162 |
-
res.end(JSON.stringify(response));
|
| 163 |
-
} catch (writeError) {
|
| 164 |
-
logger.error('[API Potluck] Failed to write error response:', writeError.message);
|
| 165 |
-
}
|
| 166 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|