ai-zero-token 1.0.6 → 1.0.8
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 +16 -0
- package/README.md +141 -356
- package/README.zh-CN.md +248 -0
- package/dist/cli/commands/serve.js +1 -0
- package/dist/core/providers/openai-codex/oauth.js +6 -3
- package/dist/core/services/auth-service.js +27 -0
- package/dist/core/services/image-service.js +12 -9
- package/dist/core/store/codex-auth-store.js +94 -0
- package/dist/core/store/profile-transfer.js +4 -0
- package/dist/server/admin-page.js +79 -15
- package/dist/server/app.js +207 -3
- package/dist/server/index.js +17 -2
- package/docs/API_USAGE.md +19 -1
- package/package.json +3 -2
|
@@ -1498,6 +1498,7 @@ function renderAdminPage() {
|
|
|
1498
1498
|
<div class="tester-tabs" id="testerTabs">
|
|
1499
1499
|
<button class="tab-btn is-active" type="button" data-endpoint="/v1/chat/completions">Chat</button>
|
|
1500
1500
|
<button class="tab-btn" type="button" data-endpoint="/v1/images/generations">Images</button>
|
|
1501
|
+
<button class="tab-btn" type="button" data-endpoint="/v1/images/edits">Edits</button>
|
|
1501
1502
|
<button class="tab-btn" type="button" data-endpoint="/v1/responses">Responses</button>
|
|
1502
1503
|
<button class="tab-btn" type="button" data-endpoint="/v1/models">Models</button>
|
|
1503
1504
|
</div>
|
|
@@ -1545,6 +1546,7 @@ function renderAdminPage() {
|
|
|
1545
1546
|
<button class="btn-secondary" type="button" data-example="/v1/responses">\u793A\u4F8B Responses</button>
|
|
1546
1547
|
<button class="btn-secondary" type="button" data-example="/v1/chat/completions">\u793A\u4F8B Chat</button>
|
|
1547
1548
|
<button class="btn-secondary" type="button" data-example="/v1/images/generations">\u793A\u4F8B Images</button>
|
|
1549
|
+
<button class="btn-secondary" type="button" data-example="/v1/images/edits">\u793A\u4F8B Edits</button>
|
|
1548
1550
|
<button class="btn-primary" id="runTestBtn" type="button">\u53D1\u9001\u8BF7\u6C42</button>
|
|
1549
1551
|
</div>
|
|
1550
1552
|
|
|
@@ -1720,6 +1722,11 @@ function renderAdminPage() {
|
|
|
1720
1722
|
tab: "Images",
|
|
1721
1723
|
description: "\u517C\u5BB9 OpenAI images.generations \u63A5\u53E3\u3002",
|
|
1722
1724
|
},
|
|
1725
|
+
"/v1/images/edits": {
|
|
1726
|
+
method: "POST",
|
|
1727
|
+
tab: "Edits",
|
|
1728
|
+
description: "\u517C\u5BB9 OpenAI images.edits JSON \u63A5\u53E3\u3002",
|
|
1729
|
+
},
|
|
1723
1730
|
};
|
|
1724
1731
|
|
|
1725
1732
|
const endpointSelect = document.getElementById("endpointSelect");
|
|
@@ -1926,12 +1933,12 @@ function renderAdminPage() {
|
|
|
1926
1933
|
title.textContent = "\u53D1\u73B0\u65B0\u7248\u672C\u53EF\u66F4\u65B0";
|
|
1927
1934
|
detail.textContent = "\u5F53\u524D\u7248\u672C " + versionStatus.currentVersion + "\uFF0C\u6700\u65B0\u7248\u672C "
|
|
1928
1935
|
+ versionStatus.latestVersion + "\u3002\u66F4\u65B0\u540E\u53EF\u83B7\u5F97\u6700\u65B0\u6A21\u578B\u5217\u8868\u903B\u8F91\u3001\u7BA1\u7406\u9875\u4F53\u9A8C\u548C\u63A5\u53E3\u4FEE\u590D\u3002";
|
|
1929
|
-
command.textContent = "npm install -g " + versionStatus.packageName
|
|
1936
|
+
command.textContent = "npm install -g " + versionStatus.packageName;
|
|
1930
1937
|
panel.classList.add("is-visible");
|
|
1931
1938
|
}
|
|
1932
1939
|
|
|
1933
1940
|
function supportsImageGeneration(profile) {
|
|
1934
|
-
return
|
|
1941
|
+
return Boolean(profile);
|
|
1935
1942
|
}
|
|
1936
1943
|
|
|
1937
1944
|
function getImageCapability(profile) {
|
|
@@ -1947,10 +1954,10 @@ function renderAdminPage() {
|
|
|
1947
1954
|
const planType = getPlanType(profile);
|
|
1948
1955
|
if (planType === "free") {
|
|
1949
1956
|
return {
|
|
1950
|
-
supported:
|
|
1951
|
-
label: "\u751F\u56FE
|
|
1952
|
-
detail: "free \
|
|
1953
|
-
badgeClass: "
|
|
1957
|
+
supported: true,
|
|
1958
|
+
label: "\u53EF\u5C1D\u8BD5\u751F\u56FE",
|
|
1959
|
+
detail: "free \u8D26\u53F7\u53EF\u5C1D\u8BD5\u56FE\u7247\u751F\u6210\uFF0C\u989D\u5EA6\u548C\u53EF\u7528\u6027\u4EE5\u4E0A\u6E38\u8FD4\u56DE\u4E3A\u51C6\u3002",
|
|
1960
|
+
badgeClass: "orange",
|
|
1954
1961
|
};
|
|
1955
1962
|
}
|
|
1956
1963
|
|
|
@@ -2325,6 +2332,21 @@ function renderAdminPage() {
|
|
|
2325
2332
|
});
|
|
2326
2333
|
}
|
|
2327
2334
|
|
|
2335
|
+
if (endpoint === "/v1/images/edits") {
|
|
2336
|
+
return formatJson({
|
|
2337
|
+
model: "gpt-image-2",
|
|
2338
|
+
prompt: "\u53C2\u8003\u8FD9\u5F20\u56FE\u7247\uFF0C\u751F\u6210\u4E00\u5F20\u66F4\u9002\u5408\u79D1\u6280\u4EA7\u54C1\u5E7F\u544A\u7684\u7248\u672C\uFF0C\u4FDD\u7559\u4E3B\u4F53\u6784\u56FE\uFF0C\u589E\u5F3A\u5149\u7EBF\u548C\u8D28\u611F\u3002",
|
|
2339
|
+
images: [
|
|
2340
|
+
{
|
|
2341
|
+
image_url: "data:image/png;base64,\u66FF\u6362\u4E3A\u4F60\u7684\u56FE\u7247base64",
|
|
2342
|
+
},
|
|
2343
|
+
],
|
|
2344
|
+
size: "1024x1024",
|
|
2345
|
+
quality: "low",
|
|
2346
|
+
response_format: "b64_json",
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2328
2350
|
return formatJson({
|
|
2329
2351
|
model: model,
|
|
2330
2352
|
input: "\u8BF7\u53EA\u56DE\u590D OK",
|
|
@@ -2336,6 +2358,10 @@ function renderAdminPage() {
|
|
|
2336
2358
|
const avg = requests.length
|
|
2337
2359
|
? requests.reduce(function (sum, item) { return sum + (item.durationMs || 0); }, 0) / requests.length
|
|
2338
2360
|
: 0;
|
|
2361
|
+
const codexAccountId = config.codex && config.codex.accountId ? config.codex.accountId : "";
|
|
2362
|
+
const codexProfile = codexAccountId && Array.isArray(config.profiles)
|
|
2363
|
+
? config.profiles.find(function (profile) { return profile.accountId === codexAccountId; })
|
|
2364
|
+
: null;
|
|
2339
2365
|
|
|
2340
2366
|
return [
|
|
2341
2367
|
{
|
|
@@ -2357,6 +2383,12 @@ function renderAdminPage() {
|
|
|
2357
2383
|
detail: "\u672A\u663E\u5F0F\u6307\u5B9A model \u65F6\u751F\u6548",
|
|
2358
2384
|
compact: true,
|
|
2359
2385
|
},
|
|
2386
|
+
{
|
|
2387
|
+
label: "Codex \u5F53\u524D\u8D26\u53F7",
|
|
2388
|
+
value: codexProfile ? getProfileDisplayLabel(codexProfile) : (codexAccountId ? maskIdentifier(codexAccountId) : "\u672A\u68C0\u6D4B\u5230"),
|
|
2389
|
+
detail: config.codex && config.codex.exists ? "\u6765\u81EA ~/.codex/auth.json" : "\u5C1A\u672A\u5E94\u7528\u5230 Codex",
|
|
2390
|
+
compact: true,
|
|
2391
|
+
},
|
|
2360
2392
|
{
|
|
2361
2393
|
label: "\u5F53\u524D\u7248\u672C",
|
|
2362
2394
|
value: getVersionValue(config),
|
|
@@ -2550,6 +2582,7 @@ function renderAdminPage() {
|
|
|
2550
2582
|
+ "</div>"
|
|
2551
2583
|
+ '<div class="account-actions">'
|
|
2552
2584
|
+ actionButton
|
|
2585
|
+
+ '<button class="btn-secondary" type="button" data-profile-action="apply-codex" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528\u5230 Codex</button>'
|
|
2553
2586
|
+ '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
|
|
2554
2587
|
+ '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
|
|
2555
2588
|
+ "</div>"
|
|
@@ -2561,13 +2594,15 @@ function renderAdminPage() {
|
|
|
2561
2594
|
const profiles = Array.isArray(config.profiles) ? config.profiles : [];
|
|
2562
2595
|
const now = Date.now();
|
|
2563
2596
|
const seeds = profiles.slice(0, 5).map(function (profile, index) {
|
|
2564
|
-
const endpoint = index %
|
|
2597
|
+
const endpoint = index % 5 === 0
|
|
2565
2598
|
? "/v1/chat/completions"
|
|
2566
|
-
: index %
|
|
2599
|
+
: index % 5 === 1
|
|
2567
2600
|
? "/v1/responses"
|
|
2568
|
-
: index %
|
|
2601
|
+
: index % 5 === 2
|
|
2569
2602
|
? "/v1/models"
|
|
2570
|
-
:
|
|
2603
|
+
: index % 5 === 3
|
|
2604
|
+
? "/v1/images/generations"
|
|
2605
|
+
: "/v1/images/edits";
|
|
2571
2606
|
const method = endpointMeta[endpoint].method;
|
|
2572
2607
|
return {
|
|
2573
2608
|
time: now - index * 15 * 60 * 1000,
|
|
@@ -2575,7 +2610,7 @@ function renderAdminPage() {
|
|
|
2575
2610
|
endpoint: endpoint,
|
|
2576
2611
|
accountEmail: profile.email || "",
|
|
2577
2612
|
accountFallback: profile.accountId || profile.profileId || "\u672A\u547D\u540D\u8D26\u53F7",
|
|
2578
|
-
model: endpoint
|
|
2613
|
+
model: endpoint.indexOf("/v1/images/") === 0 ? "gpt-image-2" : config.settings.defaultModel,
|
|
2579
2614
|
statusCode: 200,
|
|
2580
2615
|
durationMs: 860 + index * 230 + getPrimaryUsage(profile) * 8,
|
|
2581
2616
|
source: index % 2 === 0 ? "\u7BA1\u7406\u9875" : "CLI",
|
|
@@ -2806,13 +2841,13 @@ function renderAdminPage() {
|
|
|
2806
2841
|
|
|
2807
2842
|
function syncImageCapabilityHint(config) {
|
|
2808
2843
|
const capability = getImageCapability(config ? config.profile : null);
|
|
2809
|
-
const isImageEndpoint = endpointSelect.value === "/v1/images/generations";
|
|
2844
|
+
const isImageEndpoint = endpointSelect.value === "/v1/images/generations" || endpointSelect.value === "/v1/images/edits";
|
|
2810
2845
|
imageCapabilityHint.textContent = capability.detail;
|
|
2811
2846
|
imageCapabilityHint.className = capability.supported && !isImageEndpoint ? "hint" : "hint warn";
|
|
2812
|
-
runTestBtn.disabled = isImageEndpoint && !
|
|
2847
|
+
runTestBtn.disabled = isImageEndpoint && !config.profile;
|
|
2813
2848
|
if (isImageEndpoint && !capability.supported) {
|
|
2814
2849
|
testerMeta.textContent = capability.label;
|
|
2815
|
-
} else if (testerMeta.textContent === capability.label || testerMeta.textContent === "\u751F\u56FE\u53D7\u9650") {
|
|
2850
|
+
} else if (testerMeta.textContent === capability.label || testerMeta.textContent === "\u751F\u56FE\u53D7\u9650" || testerMeta.textContent === "\u53EF\u5C1D\u8BD5\u751F\u56FE") {
|
|
2816
2851
|
testerMeta.textContent = "\u51C6\u5907\u5C31\u7EEA";
|
|
2817
2852
|
}
|
|
2818
2853
|
}
|
|
@@ -2843,7 +2878,7 @@ function renderAdminPage() {
|
|
|
2843
2878
|
authStatus.textContent = config.status.loggedIn
|
|
2844
2879
|
? (supportsImageGeneration(config.profile)
|
|
2845
2880
|
? "\u7F51\u5173\u5DF2\u53EF\u76F4\u63A5\u8F6C\u53D1\u8BF7\u6C42\uFF0C\u53EF\u4EE5\u5728\u4E0B\u65B9\u5207\u6362\u9ED8\u8BA4\u6A21\u578B\u5E76\u53D1\u9001\u6D4B\u8BD5\u8BF7\u6C42\u3002"
|
|
2846
|
-
: "\u5F53\u524D\u8D26\u53F7\
|
|
2881
|
+
: "\u5F53\u524D\u8D26\u53F7\u672A\u5C31\u7EEA\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u6D4B\u8BD5\u63A5\u53E3\u3002")
|
|
2847
2882
|
: "\u8BF7\u5148\u70B9\u51FB\u201C\u65B0\u589E\u8D26\u53F7\u201D\uFF0C\u5B8C\u6210 OAuth \u540E\u518D\u6D4B\u8BD5\u63A5\u53E3\u3002";
|
|
2848
2883
|
if (!requestBody.value) {
|
|
2849
2884
|
requestBody.value = buildExample(endpointSelect.value || "/v1/chat/completions");
|
|
@@ -2987,6 +3022,10 @@ function renderAdminPage() {
|
|
|
2987
3022
|
await exportProfile(profileId, button);
|
|
2988
3023
|
return;
|
|
2989
3024
|
}
|
|
3025
|
+
if (action === "apply-codex") {
|
|
3026
|
+
await applyProfileToCodex(profileId, button);
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
2990
3029
|
|
|
2991
3030
|
setBusy(button, true);
|
|
2992
3031
|
authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
|
|
@@ -3020,6 +3059,31 @@ function renderAdminPage() {
|
|
|
3020
3059
|
}
|
|
3021
3060
|
}
|
|
3022
3061
|
|
|
3062
|
+
async function applyProfileToCodex(profileId, button) {
|
|
3063
|
+
setBusy(button, true);
|
|
3064
|
+
authStatus.textContent = "\u6B63\u5728\u5E94\u7528\u8D26\u53F7\u5230 Codex...";
|
|
3065
|
+
try {
|
|
3066
|
+
const result = await fetchJson("/_gateway/admin/codex/apply", {
|
|
3067
|
+
method: "POST",
|
|
3068
|
+
headers: {
|
|
3069
|
+
"Content-Type": "application/json",
|
|
3070
|
+
},
|
|
3071
|
+
body: formatJson({
|
|
3072
|
+
profileId: profileId,
|
|
3073
|
+
}),
|
|
3074
|
+
});
|
|
3075
|
+
const config = result.config || await fetchJson("/_gateway/admin/config");
|
|
3076
|
+
renderConfig(config);
|
|
3077
|
+
const codex = result.codex || config.codex || {};
|
|
3078
|
+
authStatus.textContent = "\u5DF2\u5E94\u7528\u5230 Codex\u3002\u65B0\u5F00\u7684 Codex \u4F1A\u8BDD\u5C06\u4F7F\u7528\u8BE5\u8D26\u53F7\u3002"
|
|
3079
|
+
+ (codex.backupPath ? " \u5DF2\u5907\u4EFD\u539F auth.json\u3002" : "");
|
|
3080
|
+
} catch (error) {
|
|
3081
|
+
authStatus.textContent = error.message;
|
|
3082
|
+
} finally {
|
|
3083
|
+
setBusy(button, false);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3023
3087
|
function downloadJsonFile(fileName, value) {
|
|
3024
3088
|
const blob = new Blob([formatJson(value) + "\\n"], { type: "application/json" });
|
|
3025
3089
|
const url = URL.createObjectURL(blob);
|
package/dist/server/app.js
CHANGED
|
@@ -85,6 +85,9 @@ const profileExportSchema = z.object({
|
|
|
85
85
|
profileIds: z.array(z.string().min(1)).optional(),
|
|
86
86
|
all: z.boolean().optional()
|
|
87
87
|
});
|
|
88
|
+
const codexApplySchema = z.object({
|
|
89
|
+
profileId: z.string().min(1)
|
|
90
|
+
});
|
|
88
91
|
const imageGenerationsBodySchema = z.object({
|
|
89
92
|
prompt: z.string().min(1),
|
|
90
93
|
model: z.string().optional(),
|
|
@@ -98,6 +101,29 @@ const imageGenerationsBodySchema = z.object({
|
|
|
98
101
|
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
99
102
|
user: z.string().optional()
|
|
100
103
|
}).passthrough();
|
|
104
|
+
const imageReferenceSchema = z.union([
|
|
105
|
+
z.string().min(1),
|
|
106
|
+
z.object({
|
|
107
|
+
image_url: z.string().min(1).optional(),
|
|
108
|
+
file_id: z.string().min(1).optional()
|
|
109
|
+
}).passthrough()
|
|
110
|
+
]);
|
|
111
|
+
const imageEditsBodySchema = z.object({
|
|
112
|
+
prompt: z.string().min(1),
|
|
113
|
+
images: z.array(imageReferenceSchema).min(1).max(16).optional(),
|
|
114
|
+
image: z.union([imageReferenceSchema, z.array(imageReferenceSchema).min(1).max(16)]).optional(),
|
|
115
|
+
mask: imageReferenceSchema.optional(),
|
|
116
|
+
model: z.string().optional(),
|
|
117
|
+
n: z.number().int().positive().optional(),
|
|
118
|
+
quality: z.enum(["low", "medium", "high", "auto"]).optional(),
|
|
119
|
+
size: z.string().min(1).optional(),
|
|
120
|
+
background: z.enum(["transparent", "opaque", "auto"]).optional(),
|
|
121
|
+
output_format: z.enum(["png", "webp", "jpeg"]).optional(),
|
|
122
|
+
output_compression: z.number().int().min(0).max(100).optional(),
|
|
123
|
+
moderation: z.enum(["auto", "low"]).optional(),
|
|
124
|
+
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
125
|
+
user: z.string().optional()
|
|
126
|
+
}).passthrough();
|
|
101
127
|
const chatCompletionExcludedKeys = /* @__PURE__ */ new Set([
|
|
102
128
|
"messages",
|
|
103
129
|
"n",
|
|
@@ -205,6 +231,46 @@ function summarizeImageRequestForLog(body) {
|
|
|
205
231
|
user: body.user ?? void 0
|
|
206
232
|
};
|
|
207
233
|
}
|
|
234
|
+
function getImageEditReferences(data) {
|
|
235
|
+
if (Array.isArray(data.images)) {
|
|
236
|
+
return data.images;
|
|
237
|
+
}
|
|
238
|
+
if (Array.isArray(data.image)) {
|
|
239
|
+
return data.image;
|
|
240
|
+
}
|
|
241
|
+
if (data.image) {
|
|
242
|
+
return [data.image];
|
|
243
|
+
}
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
function normalizeJsonImageReference(reference) {
|
|
247
|
+
if (typeof reference === "string") {
|
|
248
|
+
return {
|
|
249
|
+
imageUrl: normalizeJsonImageUrl(reference)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
imageUrl: reference.image_url ? normalizeJsonImageUrl(reference.image_url) : void 0,
|
|
254
|
+
fileId: reference.file_id
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function normalizeJsonImageUrl(value) {
|
|
258
|
+
const trimmed = value.trim();
|
|
259
|
+
if (/^https?:\/\//i.test(trimmed) || /^data:image\//i.test(trimmed)) {
|
|
260
|
+
return trimmed;
|
|
261
|
+
}
|
|
262
|
+
if (/^[A-Za-z0-9+/=_-]+$/.test(trimmed) && trimmed.length > 80) {
|
|
263
|
+
return `data:image/png;base64,${trimmed}`;
|
|
264
|
+
}
|
|
265
|
+
return trimmed;
|
|
266
|
+
}
|
|
267
|
+
function summarizeImageEditRequestForLog(body) {
|
|
268
|
+
return {
|
|
269
|
+
...summarizeImageRequestForLog(body),
|
|
270
|
+
imageCount: getImageEditReferences(body).length,
|
|
271
|
+
hasMask: Boolean(body.mask)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
208
274
|
function buildResponseApiBody(result, includeRaw) {
|
|
209
275
|
const responseBody = {
|
|
210
276
|
object: "response",
|
|
@@ -266,6 +332,30 @@ function validateImageRequest(data) {
|
|
|
266
332
|
}
|
|
267
333
|
return null;
|
|
268
334
|
}
|
|
335
|
+
function validateImageEditRequest(data) {
|
|
336
|
+
const generationValidationError = validateImageRequest(data);
|
|
337
|
+
if (generationValidationError) {
|
|
338
|
+
return generationValidationError;
|
|
339
|
+
}
|
|
340
|
+
if (data.mask) {
|
|
341
|
+
return "\u5F53\u524D\u7F51\u5173\u7684 JSON \u7248 images.edits \u6682\u4E0D\u652F\u6301 mask\uFF1B\u8BF7\u5148\u4F7F\u7528\u53C2\u8003\u56FE\u7F16\u8F91\u3002";
|
|
342
|
+
}
|
|
343
|
+
const references = getImageEditReferences(data);
|
|
344
|
+
if (references.length === 0) {
|
|
345
|
+
return "images.edits \u8BF7\u6C42\u7F3A\u5C11 images \u6216 image\u3002";
|
|
346
|
+
}
|
|
347
|
+
const normalized = references.map((reference) => normalizeJsonImageReference(reference));
|
|
348
|
+
if (normalized.some((reference) => reference.fileId)) {
|
|
349
|
+
return "\u5F53\u524D\u7F51\u5173\u7684 JSON \u7248 images.edits \u6682\u4E0D\u652F\u6301 file_id\uFF0C\u8BF7\u4F7F\u7528 image_url URL \u6216 base64 data URL\u3002";
|
|
350
|
+
}
|
|
351
|
+
if (normalized.some((reference) => !reference.imageUrl)) {
|
|
352
|
+
return "images.edits \u7684\u6BCF\u4E2A\u56FE\u7247\u5F15\u7528\u90FD\u9700\u8981\u63D0\u4F9B image_url\u3002";
|
|
353
|
+
}
|
|
354
|
+
if (normalized.some((reference) => reference.imageUrl && !/^https?:\/\//i.test(reference.imageUrl) && !/^data:image\//i.test(reference.imageUrl))) {
|
|
355
|
+
return "images.edits \u7684 image_url \u9700\u8981\u662F http(s) URL\u3001data:image/...;base64,...\uFF0C\u6216\u88F8 base64 \u5B57\u7B26\u4E32\u3002";
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
269
359
|
function maskSecret(value) {
|
|
270
360
|
if (value.length <= 12) {
|
|
271
361
|
return value;
|
|
@@ -326,7 +416,8 @@ function getErrorStatusCode(error) {
|
|
|
326
416
|
}
|
|
327
417
|
function createApp(params) {
|
|
328
418
|
const app = Fastify({
|
|
329
|
-
logger: false
|
|
419
|
+
logger: false,
|
|
420
|
+
bodyLimit: params?.bodyLimit
|
|
330
421
|
});
|
|
331
422
|
const ctx = createGatewayContext();
|
|
332
423
|
void app.register(cors, {
|
|
@@ -352,14 +443,15 @@ function createApp(params) {
|
|
|
352
443
|
};
|
|
353
444
|
});
|
|
354
445
|
async function buildAdminConfig(request) {
|
|
355
|
-
const [status, models, modelCatalog, versionStatus, settings, profile, profiles] = await Promise.all([
|
|
446
|
+
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus] = await Promise.all([
|
|
356
447
|
ctx.authService.getStatus(),
|
|
357
448
|
ctx.modelService.listModels(),
|
|
358
449
|
ctx.modelService.getCatalog(),
|
|
359
450
|
ctx.versionService.getVersionStatus(),
|
|
360
451
|
ctx.configService.getSettings(),
|
|
361
452
|
ctx.authService.getActiveProfile(),
|
|
362
|
-
ctx.authService.listProfiles()
|
|
453
|
+
ctx.authService.listProfiles(),
|
|
454
|
+
ctx.authService.getCodexStatus()
|
|
363
455
|
]);
|
|
364
456
|
const origin = resolveOrigin(request);
|
|
365
457
|
return {
|
|
@@ -370,6 +462,7 @@ function createApp(params) {
|
|
|
370
462
|
versionStatus,
|
|
371
463
|
profile: serializeProfile(profile),
|
|
372
464
|
profiles: profiles.map((item) => serializeManagedProfile(item)),
|
|
465
|
+
codex: codexStatus,
|
|
373
466
|
adminUrl: `${origin}/`,
|
|
374
467
|
baseUrl: `${origin}/v1`,
|
|
375
468
|
supportedEndpoints: [
|
|
@@ -392,6 +485,11 @@ function createApp(params) {
|
|
|
392
485
|
method: "POST",
|
|
393
486
|
path: "/v1/images/generations",
|
|
394
487
|
description: "OpenAI images.generations \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
method: "POST",
|
|
491
|
+
path: "/v1/images/edits",
|
|
492
|
+
description: "OpenAI images.edits JSON \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
395
493
|
}
|
|
396
494
|
]
|
|
397
495
|
};
|
|
@@ -518,6 +616,22 @@ function createApp(params) {
|
|
|
518
616
|
profile: await ctx.authService.exportProfile(parsed.data.profileId)
|
|
519
617
|
};
|
|
520
618
|
});
|
|
619
|
+
app.post("/_gateway/admin/codex/apply", async (request, reply) => {
|
|
620
|
+
const parsed = codexApplySchema.safeParse(request.body);
|
|
621
|
+
if (!parsed.success) {
|
|
622
|
+
reply.code(400);
|
|
623
|
+
return {
|
|
624
|
+
error: {
|
|
625
|
+
type: "validation_error",
|
|
626
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
codex: await ctx.authService.applyProfileToCodex(parsed.data.profileId),
|
|
632
|
+
config: await buildAdminConfig(request)
|
|
633
|
+
};
|
|
634
|
+
});
|
|
521
635
|
app.put("/_gateway/admin/settings", async (request, reply) => {
|
|
522
636
|
const parsed = settingsUpdateSchema.safeParse(request.body);
|
|
523
637
|
if (!parsed.success) {
|
|
@@ -733,6 +847,96 @@ function createApp(params) {
|
|
|
733
847
|
});
|
|
734
848
|
return response;
|
|
735
849
|
});
|
|
850
|
+
app.post("/v1/images/edits", async (request, reply) => {
|
|
851
|
+
const contentType = request.headers["content-type"] ?? "";
|
|
852
|
+
if (!String(contentType).toLowerCase().includes("application/json")) {
|
|
853
|
+
reply.code(415);
|
|
854
|
+
return {
|
|
855
|
+
error: {
|
|
856
|
+
type: "unsupported_media_type",
|
|
857
|
+
message: "\u5F53\u524D\u7F51\u5173\u4EC5\u652F\u6301 JSON \u7248 images.edits\uFF1B\u8BF7\u4F7F\u7528 application/json\uFF0C\u5E76\u901A\u8FC7 images[].image_url \u4F20 URL \u6216 base64 data URL\u3002"
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const parsed = imageEditsBodySchema.safeParse(request.body);
|
|
862
|
+
if (!parsed.success) {
|
|
863
|
+
console.error("[gateway:image:edit] validation failure", {
|
|
864
|
+
method: request.method,
|
|
865
|
+
url: request.url,
|
|
866
|
+
issue: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
867
|
+
});
|
|
868
|
+
reply.code(400);
|
|
869
|
+
return {
|
|
870
|
+
error: {
|
|
871
|
+
type: "validation_error",
|
|
872
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
const validationError = validateImageEditRequest(parsed.data);
|
|
877
|
+
if (validationError) {
|
|
878
|
+
console.error("[gateway:image:edit] validation failure", {
|
|
879
|
+
method: request.method,
|
|
880
|
+
url: request.url,
|
|
881
|
+
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
882
|
+
issue: validationError
|
|
883
|
+
});
|
|
884
|
+
reply.code(400);
|
|
885
|
+
return {
|
|
886
|
+
error: {
|
|
887
|
+
type: "validation_error",
|
|
888
|
+
message: validationError
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (typeof parsed.data.n === "number" && parsed.data.n > 1) {
|
|
893
|
+
console.error("[gateway:image:edit] not supported", {
|
|
894
|
+
method: request.method,
|
|
895
|
+
url: request.url,
|
|
896
|
+
summary: summarizeImageEditRequestForLog(parsed.data),
|
|
897
|
+
issue: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
898
|
+
});
|
|
899
|
+
reply.code(501);
|
|
900
|
+
return {
|
|
901
|
+
error: {
|
|
902
|
+
type: "not_supported",
|
|
903
|
+
message: "\u5F53\u524D\u7F51\u5173\u6682\u4E0D\u652F\u6301 images.edits \u4E00\u6B21\u8FD4\u56DE\u591A\u5F20\u56FE\uFF08n > 1\uFF09"
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
const imageReferences = getImageEditReferences(parsed.data).map((reference) => normalizeJsonImageReference(reference)).map((reference) => ({
|
|
908
|
+
imageUrl: reference.imageUrl ?? ""
|
|
909
|
+
}));
|
|
910
|
+
const requestSummary = summarizeImageEditRequestForLog(parsed.data);
|
|
911
|
+
console.info("[gateway:image:edit] request accepted", {
|
|
912
|
+
method: request.method,
|
|
913
|
+
url: request.url,
|
|
914
|
+
summary: requestSummary
|
|
915
|
+
});
|
|
916
|
+
const response = await ctx.imageService.generate({
|
|
917
|
+
prompt: parsed.data.prompt,
|
|
918
|
+
inputImages: imageReferences,
|
|
919
|
+
model: parsed.data.model,
|
|
920
|
+
n: parsed.data.n,
|
|
921
|
+
size: parsed.data.size,
|
|
922
|
+
quality: parsed.data.quality,
|
|
923
|
+
background: parsed.data.background,
|
|
924
|
+
outputFormat: parsed.data.output_format,
|
|
925
|
+
outputCompression: parsed.data.output_compression,
|
|
926
|
+
moderation: parsed.data.moderation
|
|
927
|
+
});
|
|
928
|
+
console.info("[gateway:image:edit] response ready", {
|
|
929
|
+
method: request.method,
|
|
930
|
+
url: request.url,
|
|
931
|
+
summary: requestSummary,
|
|
932
|
+
created: response.created,
|
|
933
|
+
imageCount: response.data.length,
|
|
934
|
+
output_format: response.output_format,
|
|
935
|
+
quality: response.quality,
|
|
936
|
+
size: response.size
|
|
937
|
+
});
|
|
938
|
+
return response;
|
|
939
|
+
});
|
|
736
940
|
return app;
|
|
737
941
|
}
|
|
738
942
|
export {
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ConfigService } from "../core/services/config-service.js";
|
|
3
3
|
import { createApp } from "./app.js";
|
|
4
|
+
const DEFAULT_BODY_LIMIT_MB = 32;
|
|
4
5
|
function resolveCorsOrigin() {
|
|
5
6
|
const raw = process.env.AZT_CORS_ORIGIN?.trim();
|
|
6
7
|
if (!raw || raw === "*") {
|
|
@@ -9,9 +10,22 @@ function resolveCorsOrigin() {
|
|
|
9
10
|
const values = raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
10
11
|
return values.length <= 1 ? values[0] ?? true : values;
|
|
11
12
|
}
|
|
13
|
+
function resolveBodyLimitBytes() {
|
|
14
|
+
const raw = process.env.AZT_BODY_LIMIT_MB?.trim();
|
|
15
|
+
if (!raw) {
|
|
16
|
+
return DEFAULT_BODY_LIMIT_MB * 1024 * 1024;
|
|
17
|
+
}
|
|
18
|
+
const value = Number(raw);
|
|
19
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
20
|
+
return DEFAULT_BODY_LIMIT_MB * 1024 * 1024;
|
|
21
|
+
}
|
|
22
|
+
return Math.floor(value * 1024 * 1024);
|
|
23
|
+
}
|
|
12
24
|
async function startServer(params) {
|
|
25
|
+
const bodyLimit = resolveBodyLimitBytes();
|
|
13
26
|
const app = createApp({
|
|
14
|
-
corsOrigin: resolveCorsOrigin()
|
|
27
|
+
corsOrigin: resolveCorsOrigin(),
|
|
28
|
+
bodyLimit
|
|
15
29
|
});
|
|
16
30
|
const configService = new ConfigService();
|
|
17
31
|
const defaults = await configService.getServerConfig();
|
|
@@ -25,7 +39,8 @@ async function startServer(params) {
|
|
|
25
39
|
app,
|
|
26
40
|
host,
|
|
27
41
|
port,
|
|
28
|
-
corsOrigin: process.env.AZT_CORS_ORIGIN?.trim() || "*"
|
|
42
|
+
corsOrigin: process.env.AZT_CORS_ORIGIN?.trim() || "*",
|
|
43
|
+
bodyLimit
|
|
29
44
|
};
|
|
30
45
|
}
|
|
31
46
|
export {
|
package/docs/API_USAGE.md
CHANGED
|
@@ -91,6 +91,25 @@ curl http://127.0.0.1:8787/v1/images/generations \
|
|
|
91
91
|
}'
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
JSON image edit with a reference image URL or base64 data URL:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
curl http://127.0.0.1:8787/v1/images/edits \
|
|
98
|
+
-H "Content-Type: application/json" \
|
|
99
|
+
-d '{
|
|
100
|
+
"model": "gpt-image-2",
|
|
101
|
+
"prompt": "Use this reference image and make it look like a clean product advertisement.",
|
|
102
|
+
"images": [
|
|
103
|
+
{
|
|
104
|
+
"image_url": "data:image/png;base64,REPLACE_WITH_IMAGE_BASE64"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"size": "1024x1024",
|
|
108
|
+
"quality": "low",
|
|
109
|
+
"response_format": "b64_json"
|
|
110
|
+
}'
|
|
111
|
+
```
|
|
112
|
+
|
|
94
113
|
## JavaScript SDK Example
|
|
95
114
|
|
|
96
115
|
```ts
|
|
@@ -117,4 +136,3 @@ console.log(response.choices[0]?.message?.content);
|
|
|
117
136
|
- A model appearing in `/v1/models` means the local Codex cache lists it. Final availability still depends on the active account.
|
|
118
137
|
- `stream=true` is not supported yet.
|
|
119
138
|
- The default listener is `0.0.0.0:8787`, so local-network clients can call the gateway by using the machine IP.
|
|
120
|
-
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-zero-token",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Local-first OpenAI-compatible AI CLI and gateway with Codex OAuth, multi-account management, and gpt-image-2 image generation.",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Local-first OpenAI-compatible AI CLI and gateway with Codex OAuth, multi-account management, and gpt-image-2 image generation/editing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"CHANGELOG.md",
|
|
61
61
|
"docs/API_USAGE.md",
|
|
62
62
|
"README.md",
|
|
63
|
+
"README.zh-CN.md",
|
|
63
64
|
"package.json"
|
|
64
65
|
]
|
|
65
66
|
}
|