ai-zero-token 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +146 -395
- package/README.zh-CN.md +258 -0
- package/dist/cli/commands/serve.js +1 -0
- package/dist/core/providers/http-client.js +1 -1
- package/dist/core/services/auth-service.js +98 -7
- package/dist/core/services/config-service.js +15 -3
- package/dist/core/services/image-service.js +2 -7
- package/dist/core/services/version-service.js +18 -13
- package/dist/core/store/settings-store.js +6 -0
- package/dist/server/admin-page.js +1018 -103
- package/dist/server/app.js +68 -2
- package/dist/server/index.js +17 -2
- package/package.json +2 -1
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# AI Zero Token
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
AI Zero Token 是一个本地优先的 OpenAI 兼容网关,用于把 ChatGPT/Codex OAuth 账号能力包装成本地 CLI、管理页和 HTTP API。
|
|
6
|
+
|
|
7
|
+
它适合本地单用户场景、原型验证、自动化脚本和 OpenAI-compatible 客户端接入。
|
|
8
|
+
|
|
9
|
+
> 这是实验性质的本地工具,不是 OpenAI 官方产品,也不建议作为生产级多租户网关使用。
|
|
10
|
+
|
|
11
|
+
## 功能
|
|
12
|
+
|
|
13
|
+
- OpenAI 风格本地接口:
|
|
14
|
+
- `GET /v1/models`
|
|
15
|
+
- `POST /v1/responses`
|
|
16
|
+
- `POST /v1/chat/completions`
|
|
17
|
+
- `POST /v1/images/generations`
|
|
18
|
+
- `POST /v1/images/edits`
|
|
19
|
+
- 支持 ChatGPT/Codex OAuth 登录和本地 token 自动刷新。
|
|
20
|
+
- 管理页支持多账号保存、切换和删除。
|
|
21
|
+
- 支持账号 JSON 导入/导出,以及复选框选择后的批量导出。
|
|
22
|
+
- 支持把已保存账号应用到本机 Codex,并自动备份 `~/.codex/auth.json`。
|
|
23
|
+
- 支持 `gpt-image-2` 文生图和 JSON 图生图。
|
|
24
|
+
- 支持 API 账号额度耗尽后自动切换到下一个仍有额度的账号。
|
|
25
|
+
- 支持上游代理配置,覆盖 OAuth、模型刷新和接口转发。
|
|
26
|
+
- 模型列表优先读取本机 Codex 模型缓存,并支持手动刷新。
|
|
27
|
+
|
|
28
|
+
## 技术架构
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
AI Zero Token 会把账号 token、网关配置和运行状态保存在本地,通过 OpenAI 兼容接口转发到当前选中的 ChatGPT/Codex OAuth 账号。管理页读取同一份本地状态,因此账号切换、应用到 Codex、代理配置和额度耗尽自动切换都可以在本地统一管理。
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g ai-zero-token
|
|
38
|
+
azt start
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
启动后会打开本地管理页,并暴露本地网关:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
http://127.0.0.1:8787
|
|
45
|
+
http://127.0.0.1:8787/v1
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
如果客户端必须填写 API Key,可以填任意非空占位值;真正起作用的是本地网关里的账号授权。
|
|
49
|
+
|
|
50
|
+
## 管理页
|
|
51
|
+
|
|
52
|
+
管理页是推荐入口,可以完成:
|
|
53
|
+
|
|
54
|
+
- OpenAI Codex OAuth 登录。
|
|
55
|
+
- 导入一个或多个账号 JSON。
|
|
56
|
+
- 切换当前账号。
|
|
57
|
+
- 导出单个账号或勾选导出多个账号。
|
|
58
|
+
- 将已保存账号应用到本机 Codex。
|
|
59
|
+
- 配置默认文本模型和上游代理。
|
|
60
|
+
- 开启当前 API 账号额度耗尽后的自动切换。
|
|
61
|
+
- 测试 `models`、`responses`、`chat.completions`、`images.generations`、`images.edits`。
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
## API 使用
|
|
66
|
+
|
|
67
|
+
### 模型列表
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl http://127.0.0.1:8787/v1/models
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Responses
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
curl http://127.0.0.1:8787/v1/responses \
|
|
77
|
+
-H "content-type: application/json" \
|
|
78
|
+
-d '{"model":"gpt-5.4","input":"请只回复 OK"}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Chat Completions
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
curl http://127.0.0.1:8787/v1/chat/completions \
|
|
85
|
+
-H "content-type: application/json" \
|
|
86
|
+
-d '{
|
|
87
|
+
"model": "gpt-5.4",
|
|
88
|
+
"messages": [
|
|
89
|
+
{
|
|
90
|
+
"role": "user",
|
|
91
|
+
"content": "请只回复 OK"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 文生图
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
curl http://127.0.0.1:8787/v1/images/generations \
|
|
101
|
+
-H "content-type: application/json" \
|
|
102
|
+
-d '{
|
|
103
|
+
"model": "gpt-image-2",
|
|
104
|
+
"prompt": "生成一张白底红苹果商品图,构图简洁,光线干净。",
|
|
105
|
+
"size": "1024x1024",
|
|
106
|
+
"quality": "low",
|
|
107
|
+
"response_format": "b64_json"
|
|
108
|
+
}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
响应会返回 OpenAI 风格的 `data[].b64_json`。
|
|
112
|
+
|
|
113
|
+

|
|
114
|
+
|
|
115
|
+
### 图生图
|
|
116
|
+
|
|
117
|
+
`/v1/images/edits` 当前支持 JSON 请求,图片可以使用 URL、data URL 或裸 base64:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
curl http://127.0.0.1:8787/v1/images/edits \
|
|
121
|
+
-H "content-type: application/json" \
|
|
122
|
+
-d '{
|
|
123
|
+
"model": "gpt-image-2",
|
|
124
|
+
"prompt": "参考这张图,生成一张更适合科技产品广告的版本。",
|
|
125
|
+
"images": [
|
|
126
|
+
{
|
|
127
|
+
"image_url": "data:image/png;base64,替换为你的图片base64"
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"size": "1024x1024",
|
|
131
|
+
"quality": "low",
|
|
132
|
+
"response_format": "b64_json"
|
|
133
|
+
}'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
更多示例见 [docs/API_USAGE.md](docs/API_USAGE.md)。
|
|
137
|
+
|
|
138
|
+
## 账号管理
|
|
139
|
+
|
|
140
|
+
AI Zero Token 的账号状态默认保存在:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
~/.ai-zero-token/.state
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
管理页支持:
|
|
147
|
+
|
|
148
|
+
- OAuth 登录。
|
|
149
|
+
- 从单个 profile、数组或 `profiles` bundle 导入。
|
|
150
|
+
- 导出单个账号。
|
|
151
|
+
- 使用复选框批量导出已选择账号。
|
|
152
|
+
- 删除账号和切换当前账号。
|
|
153
|
+
|
|
154
|
+
导出的账号 JSON 包含认证 token,等同于登录凭据,只应在可信环境中使用。
|
|
155
|
+
|
|
156
|
+
### 应用到 Codex
|
|
157
|
+
|
|
158
|
+
`应用到 Codex` 会把选中的账号写入:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
~/.codex/auth.json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
写入前会把原文件备份为 `auth.json.azt-backup-*`。新的 Codex 会话会使用该账号。
|
|
165
|
+
|
|
166
|
+
## 配置
|
|
167
|
+
|
|
168
|
+
默认监听:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
0.0.0.0:8787
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
本地网关 Base URL:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
http://127.0.0.1:8787/v1
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
限制浏览器 CORS 来源:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
AZT_CORS_ORIGIN=http://127.0.0.1:8124 azt start
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
多个来源用英文逗号分隔:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
AZT_CORS_ORIGIN=http://127.0.0.1:8124,http://localhost:3000 azt start
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
覆盖持久化状态目录:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
AI_ZERO_TOKEN_HOME=/path/to/home azt start
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
管理页里的配置会保存在同一个本地状态目录。额度耗尽自动切换会保存为 `autoSwitch.enabled`;开启后,网关会根据最近一次保存的额度快照判断当前 API 账号是否耗尽,并把 API 流量切到下一个仍有额度的账号。
|
|
199
|
+
|
|
200
|
+
默认请求体上限是 `32 MiB`,用于让 JSON base64 图片在本地图片编辑场景里更实用。可以用下面的环境变量覆盖:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
AZT_BODY_LIMIT_MB=64 azt start
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 生图额度
|
|
207
|
+
|
|
208
|
+
ChatGPT Images 的可用性和额度由上游账号决定。Free 账号可以尝试生图,但限制比付费账号更严格,官方没有公开固定张数。网关不会本地硬拦截 Free 账号,而是展示上游真实返回,例如 `usage_limit_reached` 和重置时间。
|
|
209
|
+
|
|
210
|
+
图片请求内部使用 `gpt-5.4-mini` 作为编排模型,并把请求里的图片模型(例如 `gpt-image-2`)传给 `image_generation` tool。
|
|
211
|
+
|
|
212
|
+
对于 JSON 图生图,base64 通常比原始图片大约 33%。在默认 `32 MiB` 请求体上限下,原始图片约 `24 MiB` 是比较实际的上限,再大就容易被 JSON 开销和本地内存影响。大图或批量场景建议优先使用可访问的图片 URL。
|
|
213
|
+
|
|
214
|
+
## 当前限制
|
|
215
|
+
|
|
216
|
+
- 项目默认面向本地单用户使用。
|
|
217
|
+
- `stream=true` 目前只识别,并未对所有接口实现完整流式兼容。
|
|
218
|
+
- `/v1/images/generations` 当前返回 `b64_json`,暂不支持托管图片 URL。
|
|
219
|
+
- `/v1/images/generations` 暂不支持 `n > 1`。
|
|
220
|
+
- `/v1/images/edits` 当前只支持 JSON,暂不支持 `multipart/form-data`、`mask` 和 `file_id`。
|
|
221
|
+
- 超大的 base64 JSON 请求受 `AZT_BODY_LIMIT_MB` 和本地内存限制。
|
|
222
|
+
- OpenAI Responses API 兼容范围是常见本地客户端工作流,不是完整实现。
|
|
223
|
+
|
|
224
|
+
## 安全
|
|
225
|
+
|
|
226
|
+
- `access_token`、`refresh_token`、`id_token` 都等同于登录凭据。
|
|
227
|
+
- 不要把本地网关暴露给不可信网络。
|
|
228
|
+
- 不要在不可信环境中传递导出的账号 JSON。
|
|
229
|
+
- 复制或发布本地文件前,请检查 `~/.ai-zero-token/.state` 和 `~/.codex/auth.json`。
|
|
230
|
+
|
|
231
|
+
## 开发
|
|
232
|
+
|
|
233
|
+
安装依赖:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm install
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
源码运行:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
bun src/cli.ts start
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
验证:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npm run typecheck
|
|
249
|
+
npm run build
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
CLI 构建和发布说明见 [BUILD_CLI.md](BUILD_CLI.md)。用户可见变更见 [CHANGELOG.md](CHANGELOG.md)。
|
|
253
|
+
|
|
254
|
+
## 项目状态
|
|
255
|
+
|
|
256
|
+
AI Zero Token 仍在快速迭代。当前重点是稳定本地网关、账号迁移工作流,以及 OpenAI 风格客户端的图片生成/编辑兼容性。
|
|
257
|
+
|
|
258
|
+
反馈和问题:[GitHub Issues](https://github.com/fchangjun/AI-Zero-Token/issues)
|
|
@@ -44,6 +44,7 @@ async function runServeCommand(args, options) {
|
|
|
44
44
|
console.log(`admin: ${adminUrl}`);
|
|
45
45
|
console.log(`apiBase: ${adminUrl}/v1`);
|
|
46
46
|
console.log(`corsOrigin: ${server.corsOrigin}`);
|
|
47
|
+
console.log(`bodyLimitMB: ${(server.bodyLimit / 1024 / 1024).toFixed(1)}`);
|
|
47
48
|
console.log(`activeProvider: ${status.activeProvider ?? "none"}`);
|
|
48
49
|
console.log(`defaultModel: ${status.defaultModel}`);
|
|
49
50
|
if (shouldOpenBrowser) {
|
|
@@ -168,7 +168,7 @@ async function loadNetworkProxySettings() {
|
|
|
168
168
|
}
|
|
169
169
|
async function requestText(init) {
|
|
170
170
|
const requestId = nextRequestId();
|
|
171
|
-
const proxy = await loadNetworkProxySettings();
|
|
171
|
+
const proxy = init.ignoreProxy ? void 0 : init.proxyOverride ?? await loadNetworkProxySettings();
|
|
172
172
|
const useCurlOnly = process.env.OAUTH_DEMO_USE_CURL === "1";
|
|
173
173
|
const useConfiguredProxy = !!proxy?.enabled && !!proxy.url.trim();
|
|
174
174
|
const timeoutMs = init.timeoutMs;
|
|
@@ -53,6 +53,83 @@ class AuthService {
|
|
|
53
53
|
isActive: profile.profileId === activeProfileId
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
getQuotaPercents(profile) {
|
|
57
|
+
const quota = profile.quota;
|
|
58
|
+
if (!quota) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return [quota.primaryUsedPercent, quota.secondaryUsedPercent].filter(
|
|
62
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
isQuotaExhausted(profile) {
|
|
66
|
+
const percents = this.getQuotaPercents(profile);
|
|
67
|
+
return percents.length > 0 && percents.some((value) => value >= 100);
|
|
68
|
+
}
|
|
69
|
+
hasKnownAvailableQuota(profile) {
|
|
70
|
+
const percents = this.getQuotaPercents(profile);
|
|
71
|
+
return percents.length > 0 && percents.every((value) => value < 100);
|
|
72
|
+
}
|
|
73
|
+
getQuotaUsageScore(profile) {
|
|
74
|
+
const percents = this.getQuotaPercents(profile);
|
|
75
|
+
if (percents.length === 0) {
|
|
76
|
+
return 100;
|
|
77
|
+
}
|
|
78
|
+
return Math.max(...percents);
|
|
79
|
+
}
|
|
80
|
+
async maybeAutoSwitchProfile(profile, provider) {
|
|
81
|
+
const settings = await this.configService.getSettings();
|
|
82
|
+
if (!settings.autoSwitch.enabled || !this.isQuotaExhausted(profile)) {
|
|
83
|
+
return profile;
|
|
84
|
+
}
|
|
85
|
+
const [profiles, codexStatus] = await Promise.all([
|
|
86
|
+
listProfiles(),
|
|
87
|
+
getCodexAuthStatus()
|
|
88
|
+
]);
|
|
89
|
+
const currentIndex = profiles.findIndex((item) => item.profileId === profile.profileId);
|
|
90
|
+
const codexAccountId = codexStatus.accountId;
|
|
91
|
+
const candidates = profiles.map((item, index) => ({
|
|
92
|
+
profile: item,
|
|
93
|
+
index,
|
|
94
|
+
distance: currentIndex >= 0 ? (index - currentIndex + profiles.length) % profiles.length : index + 1
|
|
95
|
+
})).filter((item) => item.profile.provider === provider && item.profile.profileId !== profile.profileId).filter((item) => this.hasKnownAvailableQuota(item.profile)).sort((left, right) => {
|
|
96
|
+
const leftCodexConflict = codexAccountId && left.profile.accountId === codexAccountId ? 1 : 0;
|
|
97
|
+
const rightCodexConflict = codexAccountId && right.profile.accountId === codexAccountId ? 1 : 0;
|
|
98
|
+
const codexDiff = leftCodexConflict - rightCodexConflict;
|
|
99
|
+
if (codexDiff !== 0) {
|
|
100
|
+
return codexDiff;
|
|
101
|
+
}
|
|
102
|
+
const distanceDiff = left.distance - right.distance;
|
|
103
|
+
if (distanceDiff !== 0) {
|
|
104
|
+
return distanceDiff;
|
|
105
|
+
}
|
|
106
|
+
const usageDiff = this.getQuotaUsageScore(left.profile) - this.getQuotaUsageScore(right.profile);
|
|
107
|
+
if (usageDiff !== 0) {
|
|
108
|
+
return usageDiff;
|
|
109
|
+
}
|
|
110
|
+
const leftCapturedAt = left.profile.quota?.capturedAt ?? 0;
|
|
111
|
+
const rightCapturedAt = right.profile.quota?.capturedAt ?? 0;
|
|
112
|
+
if (leftCapturedAt !== rightCapturedAt) {
|
|
113
|
+
return leftCapturedAt - rightCapturedAt;
|
|
114
|
+
}
|
|
115
|
+
return right.profile.expires - left.profile.expires;
|
|
116
|
+
}).map((item) => item.profile);
|
|
117
|
+
const nextProfile = candidates[0];
|
|
118
|
+
if (!nextProfile) {
|
|
119
|
+
return profile;
|
|
120
|
+
}
|
|
121
|
+
const activated = await setActiveProfile(nextProfile.profileId);
|
|
122
|
+
if (!activated) {
|
|
123
|
+
return profile;
|
|
124
|
+
}
|
|
125
|
+
console.info("[auth] auto switched active profile after quota exhaustion", {
|
|
126
|
+
provider,
|
|
127
|
+
fromProfileId: profile.profileId,
|
|
128
|
+
toProfileId: activated.profileId,
|
|
129
|
+
avoidedCodexAccount: Boolean(codexAccountId && activated.accountId !== codexAccountId)
|
|
130
|
+
});
|
|
131
|
+
return this.toManagedProfile(activated);
|
|
132
|
+
}
|
|
56
133
|
async login(provider) {
|
|
57
134
|
if (provider !== "openai-codex") {
|
|
58
135
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
@@ -151,8 +228,9 @@ class AuthService {
|
|
|
151
228
|
}
|
|
152
229
|
await removeProfile(profileId);
|
|
153
230
|
}
|
|
154
|
-
async requireUsableProfile(provider = "openai-codex") {
|
|
155
|
-
const
|
|
231
|
+
async requireUsableProfile(provider = "openai-codex", options) {
|
|
232
|
+
const activeProfile = await this.getActiveProfile(provider);
|
|
233
|
+
const profile = activeProfile ? options?.skipAutoSwitch ? activeProfile : await this.maybeAutoSwitchProfile(activeProfile, provider) : null;
|
|
156
234
|
if (!profile) {
|
|
157
235
|
throw new Error(`\u8FD8\u6CA1\u6709\u767B\u5F55 ${provider}\u3002\u5148\u8FD0\u884C azt login`);
|
|
158
236
|
}
|
|
@@ -185,7 +263,9 @@ class AuthService {
|
|
|
185
263
|
async syncActiveProfileQuota(provider = "openai-codex", options) {
|
|
186
264
|
let profile;
|
|
187
265
|
try {
|
|
188
|
-
profile = await this.requireUsableProfile(provider
|
|
266
|
+
profile = await this.requireUsableProfile(provider, {
|
|
267
|
+
skipAutoSwitch: options?.skipAutoSwitch
|
|
268
|
+
});
|
|
189
269
|
} catch (error) {
|
|
190
270
|
if (options?.suppressErrors) {
|
|
191
271
|
return;
|
|
@@ -203,10 +283,14 @@ class AuthService {
|
|
|
203
283
|
text: { verbosity: "low" }
|
|
204
284
|
}
|
|
205
285
|
});
|
|
206
|
-
await this.updateProfileQuota(profile.profileId, result.quota, provider
|
|
286
|
+
await this.updateProfileQuota(profile.profileId, result.quota, provider, {
|
|
287
|
+
skipAutoSwitch: options?.skipAutoSwitch
|
|
288
|
+
});
|
|
207
289
|
} catch (error) {
|
|
208
290
|
const quota = error.quota;
|
|
209
|
-
await this.updateProfileQuota(profile.profileId, quota, provider
|
|
291
|
+
await this.updateProfileQuota(profile.profileId, quota, provider, {
|
|
292
|
+
skipAutoSwitch: options?.skipAutoSwitch
|
|
293
|
+
});
|
|
210
294
|
if (!options?.suppressErrors) {
|
|
211
295
|
throw error;
|
|
212
296
|
}
|
|
@@ -217,11 +301,11 @@ class AuthService {
|
|
|
217
301
|
});
|
|
218
302
|
}
|
|
219
303
|
}
|
|
220
|
-
async updateProfileQuota(profileId, quota, provider = "openai-codex") {
|
|
304
|
+
async updateProfileQuota(profileId, quota, provider = "openai-codex", options) {
|
|
221
305
|
if (!quota) {
|
|
222
306
|
return;
|
|
223
307
|
}
|
|
224
|
-
await updateProfile(profileId, (profile) => {
|
|
308
|
+
const updated = await updateProfile(profileId, (profile) => {
|
|
225
309
|
if (profile.provider !== provider) {
|
|
226
310
|
return profile;
|
|
227
311
|
}
|
|
@@ -230,6 +314,13 @@ class AuthService {
|
|
|
230
314
|
quota
|
|
231
315
|
};
|
|
232
316
|
});
|
|
317
|
+
if (options?.skipAutoSwitch || !updated || updated.provider !== provider || !this.isQuotaExhausted(updated)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const activeProfile = await this.getActiveProfile(provider);
|
|
321
|
+
if (activeProfile?.profileId === updated.profileId) {
|
|
322
|
+
await this.maybeAutoSwitchProfile(updated, provider);
|
|
323
|
+
}
|
|
233
324
|
}
|
|
234
325
|
async getStatus() {
|
|
235
326
|
const [profile, profiles] = await Promise.all([
|
|
@@ -42,8 +42,10 @@ class ConfigService {
|
|
|
42
42
|
return next;
|
|
43
43
|
}
|
|
44
44
|
async setNetworkProxy(params) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
45
|
+
const settings = await this.getSettings();
|
|
46
|
+
const requestedUrl = params.url?.trim() ?? "";
|
|
47
|
+
const url = requestedUrl || (!params.enabled ? settings.networkProxy.url : "");
|
|
48
|
+
const noProxy = params.noProxy?.trim() || settings.networkProxy.noProxy || "localhost,127.0.0.1,::1";
|
|
47
49
|
if (params.enabled) {
|
|
48
50
|
if (!url) {
|
|
49
51
|
throw new Error("\u542F\u7528\u4EE3\u7406\u65F6\u5FC5\u987B\u586B\u5199\u4EE3\u7406\u5730\u5740\u3002");
|
|
@@ -59,7 +61,6 @@ class ConfigService {
|
|
|
59
61
|
throw new Error("\u4EE3\u7406\u5730\u5740\u4EC5\u652F\u6301 http\u3001https\u3001socks4\u3001socks4a\u3001socks5 \u6216 socks5h\u3002");
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
|
-
const settings = await this.getSettings();
|
|
63
64
|
const next = {
|
|
64
65
|
...settings,
|
|
65
66
|
networkProxy: {
|
|
@@ -71,6 +72,17 @@ class ConfigService {
|
|
|
71
72
|
await saveSettings(next);
|
|
72
73
|
return next;
|
|
73
74
|
}
|
|
75
|
+
async setAutoSwitch(params) {
|
|
76
|
+
const settings = await this.getSettings();
|
|
77
|
+
const next = {
|
|
78
|
+
...settings,
|
|
79
|
+
autoSwitch: {
|
|
80
|
+
enabled: params.enabled
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
await saveSettings(next);
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
74
86
|
async getServerConfig() {
|
|
75
87
|
const settings = await this.getSettings();
|
|
76
88
|
return settings.server;
|
|
@@ -6,6 +6,7 @@ const SUPPORTED_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
6
6
|
"gpt-image-1.5",
|
|
7
7
|
"gpt-image-2"
|
|
8
8
|
]);
|
|
9
|
+
const IMAGE_ORCHESTRATOR_MODEL = "gpt-5.4-mini";
|
|
9
10
|
const SUPPORTED_IMAGE_QUALITIES = /* @__PURE__ */ new Set([
|
|
10
11
|
"low",
|
|
11
12
|
"medium",
|
|
@@ -260,15 +261,9 @@ class ImageService {
|
|
|
260
261
|
}
|
|
261
262
|
return model;
|
|
262
263
|
}
|
|
263
|
-
isFreePlan(profile) {
|
|
264
|
-
return profile.quota?.planType === "free";
|
|
265
|
-
}
|
|
266
264
|
async generate(request) {
|
|
267
265
|
const profile = await this.deps.authService.requireUsableProfile("openai-codex");
|
|
268
|
-
|
|
269
|
-
throw new Error("\u5F53\u524D\u8D26\u53F7\u4E3A free \u5957\u9910\uFF0C\u4E0D\u652F\u6301\u56FE\u7247\u751F\u6210\u3002\u8BF7\u5207\u6362\u5230 Plus \u6216\u66F4\u9AD8\u5957\u9910\u8D26\u53F7\u3002");
|
|
270
|
-
}
|
|
271
|
-
const orchestratorModel = await this.deps.configService.getDefaultModel();
|
|
266
|
+
const orchestratorModel = IMAGE_ORCHESTRATOR_MODEL;
|
|
272
267
|
const requestedImageModel = this.resolveRequestedImageModel(request.model);
|
|
273
268
|
const requestSummary = {
|
|
274
269
|
requestedImageModel,
|
|
@@ -56,19 +56,7 @@ class VersionService {
|
|
|
56
56
|
const manifest = await readPackageManifest();
|
|
57
57
|
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(manifest.name)}/latest`;
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
60
|
-
method: "GET",
|
|
61
|
-
url: registryUrl,
|
|
62
|
-
timeoutMs: 5e3
|
|
63
|
-
});
|
|
64
|
-
if (response.status < 200 || response.status >= 300) {
|
|
65
|
-
throw new Error(`npm registry returned ${response.status}`);
|
|
66
|
-
}
|
|
67
|
-
const parsed = JSON.parse(response.body);
|
|
68
|
-
const latestVersion = typeof parsed.version === "string" && parsed.version ? parsed.version : void 0;
|
|
69
|
-
if (!latestVersion) {
|
|
70
|
-
throw new Error("npm registry did not return a version");
|
|
71
|
-
}
|
|
59
|
+
const latestVersion = await this.fetchNpmLatestVersion(registryUrl);
|
|
72
60
|
const needsUpdate = compareSemver(manifest.version, latestVersion) < 0;
|
|
73
61
|
return {
|
|
74
62
|
packageName: manifest.name,
|
|
@@ -91,6 +79,23 @@ class VersionService {
|
|
|
91
79
|
};
|
|
92
80
|
}
|
|
93
81
|
}
|
|
82
|
+
async fetchNpmLatestVersion(registryUrl) {
|
|
83
|
+
const response = await requestText({
|
|
84
|
+
method: "GET",
|
|
85
|
+
url: registryUrl,
|
|
86
|
+
timeoutMs: 5e3,
|
|
87
|
+
ignoreProxy: true
|
|
88
|
+
});
|
|
89
|
+
if (response.status < 200 || response.status >= 300) {
|
|
90
|
+
throw new Error(`npm registry returned ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
const parsed = JSON.parse(response.body);
|
|
93
|
+
const latestVersion = typeof parsed.version === "string" && parsed.version ? parsed.version : void 0;
|
|
94
|
+
if (!latestVersion) {
|
|
95
|
+
throw new Error("npm registry did not return a version");
|
|
96
|
+
}
|
|
97
|
+
return latestVersion;
|
|
98
|
+
}
|
|
94
99
|
}
|
|
95
100
|
export {
|
|
96
101
|
VersionService
|
|
@@ -15,6 +15,9 @@ function createDefaultSettings() {
|
|
|
15
15
|
url: "",
|
|
16
16
|
noProxy: "localhost,127.0.0.1,::1"
|
|
17
17
|
},
|
|
18
|
+
autoSwitch: {
|
|
19
|
+
enabled: false
|
|
20
|
+
},
|
|
18
21
|
server: {
|
|
19
22
|
host: "0.0.0.0",
|
|
20
23
|
port: 8787
|
|
@@ -36,6 +39,9 @@ async function loadSettings() {
|
|
|
36
39
|
url: parsed.networkProxy?.url ?? defaults.networkProxy.url,
|
|
37
40
|
noProxy: parsed.networkProxy?.noProxy ?? defaults.networkProxy.noProxy
|
|
38
41
|
},
|
|
42
|
+
autoSwitch: {
|
|
43
|
+
enabled: parsed.autoSwitch?.enabled ?? defaults.autoSwitch.enabled
|
|
44
|
+
},
|
|
39
45
|
server: {
|
|
40
46
|
host: parsed.server?.host ?? defaults.server.host,
|
|
41
47
|
port: parsed.server?.port ?? defaults.server.port
|