dsh-image-gen 0.6.0 → 0.6.5
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/README.en.md +12 -2
- package/README.md +13 -3
- package/lib/client.js +1086 -133
- package/lib/client.js.map +1 -1
- package/lib/index.js +1425 -103
- package/lib/types/client/image-result-node.d.ts +11 -1
- package/lib/types/client/index.d.ts +5 -0
- package/lib/types/client/provider-pill.d.ts +1 -1
- package/lib/types/client/studio-style.d.ts +1 -1
- package/lib/types/client/studio-view.d.ts +6 -0
- package/lib/types/client/tl/css-supports-shim.d.ts +21 -0
- package/lib/types/client/tl/diagram-landing.d.ts +75 -0
- package/lib/types/client/tl/diagram-layout.d.ts +76 -0
- package/lib/types/client/tl/diagram-measure.d.ts +40 -0
- package/lib/types/client/tl/tl-brand-theme.d.ts +3 -0
- package/lib/types/client/tl/tl-brand-tokens.d.ts +24 -0
- package/lib/types/client/tl/tl-theme-css.d.ts +17 -0
- package/lib/types/config.d.ts +24 -0
- package/lib/types/diagram.d.ts +42 -0
- package/lib/types/openai-compatible.d.ts +15 -0
- package/lib/types/shared.d.ts +16 -1
- package/lib/types/subscription/blob.d.ts +23 -0
- package/lib/types/subscription/loopback.d.ts +11 -0
- package/lib/types/subscription/manager.d.ts +94 -0
- package/lib/types/subscription/oauth.d.ts +25 -0
- package/lib/types/subscription/subscription-route.d.ts +5 -0
- package/lib/types/subscription/vendors/antigravity.d.ts +66 -0
- package/lib/types/subscription/vendors/codex.d.ts +30 -0
- package/lib/types/subscription/vendors/grok.d.ts +32 -0
- package/lib/types/subscription.d.ts +42 -0
- package/lib/types/test-route.d.ts +9 -0
- package/package.json +10 -8
package/lib/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import { Context, Service } from "@deepseek-ai/cordis";
|
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { lstat, mkdir, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
|
+
import http from "node:http";
|
|
7
8
|
//#region \0rolldown/runtime.js
|
|
8
9
|
var __defProp = Object.defineProperty;
|
|
9
10
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -5951,6 +5952,8 @@ const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
|
|
|
5951
5952
|
const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
|
|
5952
5953
|
/** Browser route the workbench infinite canvas pushes live state through. */
|
|
5953
5954
|
const CANVAS_STATE_ROUTE = "/plugins/dsh-image-gen/canvas-state";
|
|
5955
|
+
const SUBSCRIPTION_LOGIN_ROUTE = "/plugins/dsh-image-gen/subscription-login";
|
|
5956
|
+
const SUBSCRIPTION_STATUS_ROUTE = "/plugins/dsh-image-gen/subscription-status";
|
|
5954
5957
|
/** Every accepted canvas node kind, for validating untrusted pushes. */
|
|
5955
5958
|
const CANVAS_NODE_KINDS = [
|
|
5956
5959
|
"image",
|
|
@@ -5973,8 +5976,25 @@ const IMAGE_PROVIDERS = [
|
|
|
5973
5976
|
"dashscope",
|
|
5974
5977
|
"xai",
|
|
5975
5978
|
"zhipu",
|
|
5976
|
-
"comfyui"
|
|
5979
|
+
"comfyui",
|
|
5980
|
+
"chatgpt-sub",
|
|
5981
|
+
"grok-sub",
|
|
5982
|
+
"google-sub"
|
|
5977
5983
|
];
|
|
5984
|
+
/**
|
|
5985
|
+
* Providers that generate through a logged-in subscription account instead
|
|
5986
|
+
* of an API key. They use no credential reference and never join the
|
|
5987
|
+
* cloud/BYOK sets.
|
|
5988
|
+
*/
|
|
5989
|
+
const SUBSCRIPTION_PROVIDERS = [
|
|
5990
|
+
"chatgpt-sub",
|
|
5991
|
+
"grok-sub",
|
|
5992
|
+
"google-sub"
|
|
5993
|
+
];
|
|
5994
|
+
/** True when the provider generates through a logged-in subscription account. */
|
|
5995
|
+
function isSubscriptionProvider(provider) {
|
|
5996
|
+
return SUBSCRIPTION_PROVIDERS.includes(provider);
|
|
5997
|
+
}
|
|
5978
5998
|
/** Providers supported by the first browser workbench release. */
|
|
5979
5999
|
const CLOUD_IMAGE_PROVIDERS = [
|
|
5980
6000
|
"google",
|
|
@@ -6027,7 +6047,16 @@ const PROVIDER_DISPLAY_NAMES = {
|
|
|
6027
6047
|
dashscope: "DashScope",
|
|
6028
6048
|
xai: "xAI Grok",
|
|
6029
6049
|
zhipu: "智谱 GLM",
|
|
6030
|
-
comfyui: "ComfyUI"
|
|
6050
|
+
comfyui: "ComfyUI",
|
|
6051
|
+
"chatgpt-sub": "ChatGPT 订阅",
|
|
6052
|
+
"grok-sub": "Grok 订阅",
|
|
6053
|
+
"google-sub": "Google 订阅"
|
|
6054
|
+
};
|
|
6055
|
+
/** Display names for the subscription providers, separate from the BYOK table. */
|
|
6056
|
+
const SUBSCRIPTION_PROVIDER_DISPLAY_NAMES = {
|
|
6057
|
+
"chatgpt-sub": "ChatGPT 订阅",
|
|
6058
|
+
"grok-sub": "Grok 订阅",
|
|
6059
|
+
"google-sub": "Google 订阅"
|
|
6031
6060
|
};
|
|
6032
6061
|
/** Default endpoints and base URLs. */
|
|
6033
6062
|
const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
|
|
@@ -6092,13 +6121,20 @@ function mergeComfyUIPrompt(preset, user) {
|
|
|
6092
6121
|
if (userText.length === 0) return presetText;
|
|
6093
6122
|
return `${presetText}, ${userText}`;
|
|
6094
6123
|
}
|
|
6124
|
+
/** Default models for the subscription channels; fixed by the bridge protocol. */
|
|
6125
|
+
const DEFAULT_SUBSCRIPTION_MODELS = {
|
|
6126
|
+
"chatgpt-sub": "gpt-image-2.5-flare",
|
|
6127
|
+
"grok-sub": "grok-imagine-image-2.0",
|
|
6128
|
+
"google-sub": "gemini-3-pro-image"
|
|
6129
|
+
};
|
|
6130
|
+
DEFAULT_SUBSCRIPTION_MODELS["chatgpt-sub"], DEFAULT_SUBSCRIPTION_MODELS["grok-sub"], DEFAULT_SUBSCRIPTION_MODELS["google-sub"];
|
|
6095
6131
|
//#endregion
|
|
6096
6132
|
//#region lib/types/config.js
|
|
6097
6133
|
/** User-facing configuration for supported image providers. */
|
|
6098
6134
|
/** Default workspace subfolder that receives generated image files. */
|
|
6099
6135
|
const DEFAULT_WORKSPACE_FOLDER = "dsh-image-gen";
|
|
6100
6136
|
/** Google tool-level controls. */
|
|
6101
|
-
const ASPECT_RATIOS = [
|
|
6137
|
+
const ASPECT_RATIOS$1 = [
|
|
6102
6138
|
"1:1",
|
|
6103
6139
|
"3:2",
|
|
6104
6140
|
"2:3",
|
|
@@ -6121,6 +6157,8 @@ const Config = z.object({
|
|
|
6121
6157
|
openaiModel: z.string().default(DEFAULT_OPENAI_MODEL),
|
|
6122
6158
|
openaiCompatBaseURL: z.string().default(""),
|
|
6123
6159
|
openaiCompatModel: z.string().default(""),
|
|
6160
|
+
openaiCompatEditFormat: z.union([z.const("multipart"), z.const("jsonImageUrlArray")]).default("multipart"),
|
|
6161
|
+
openaiCompatEditExtra: z.dict(z.any()).default({}),
|
|
6124
6162
|
seedreamBaseURL: z.string().default(DEFAULT_SEEDREAM_BASE_URL),
|
|
6125
6163
|
seedreamModel: z.string().default(DEFAULT_SEEDREAM_MODEL),
|
|
6126
6164
|
dashscopeEndpoint: z.string().default(DEFAULT_DASHSCOPE_ENDPOINT),
|
|
@@ -6162,7 +6200,9 @@ function resolveProvider(config) {
|
|
|
6162
6200
|
apiKeyEnv: OPENAI_COMPAT_API_KEY_ENV,
|
|
6163
6201
|
model,
|
|
6164
6202
|
baseURL,
|
|
6165
|
-
imageSize: "1024x1024"
|
|
6203
|
+
imageSize: "1024x1024",
|
|
6204
|
+
editFormat: config.openaiCompatEditFormat ?? "multipart",
|
|
6205
|
+
editExtra: config.openaiCompatEditExtra ?? {}
|
|
6166
6206
|
};
|
|
6167
6207
|
}
|
|
6168
6208
|
case "seedream": return {
|
|
@@ -6204,6 +6244,18 @@ function resolveProvider(config) {
|
|
|
6204
6244
|
timeoutMs: config.comfyuiTimeoutMs ?? 3e5
|
|
6205
6245
|
};
|
|
6206
6246
|
}
|
|
6247
|
+
case "chatgpt-sub": return {
|
|
6248
|
+
provider: "chatgpt-sub",
|
|
6249
|
+
model: DEFAULT_SUBSCRIPTION_MODELS["chatgpt-sub"]
|
|
6250
|
+
};
|
|
6251
|
+
case "grok-sub": return {
|
|
6252
|
+
provider: "grok-sub",
|
|
6253
|
+
model: DEFAULT_SUBSCRIPTION_MODELS["grok-sub"]
|
|
6254
|
+
};
|
|
6255
|
+
case "google-sub": return {
|
|
6256
|
+
provider: "google-sub",
|
|
6257
|
+
model: DEFAULT_SUBSCRIPTION_MODELS["google-sub"]
|
|
6258
|
+
};
|
|
6207
6259
|
case "google": return {
|
|
6208
6260
|
provider: "google",
|
|
6209
6261
|
apiKeyEnv: GOOGLE_API_KEY_ENV,
|
|
@@ -6255,6 +6307,9 @@ function withProviderOverrides(config, provider, model) {
|
|
|
6255
6307
|
...base,
|
|
6256
6308
|
zhipuModel: trimmed
|
|
6257
6309
|
};
|
|
6310
|
+
case "chatgpt-sub": return base;
|
|
6311
|
+
case "grok-sub": return base;
|
|
6312
|
+
case "google-sub": return base;
|
|
6258
6313
|
case "comfyui": return base;
|
|
6259
6314
|
}
|
|
6260
6315
|
}
|
|
@@ -6495,24 +6550,24 @@ const MAX_NODE_COUNT = 1e5;
|
|
|
6495
6550
|
const MAX_SELECTION_COUNT = 1e3;
|
|
6496
6551
|
/** Serve one canvas-state push. */
|
|
6497
6552
|
async function serveCanvasState(req, res, deps) {
|
|
6498
|
-
if (req.method !== "POST") return jsonError$
|
|
6499
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$
|
|
6553
|
+
if (req.method !== "POST") return jsonError$5(res, 405, "method-not-allowed");
|
|
6554
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$5(res, 415, "json-required");
|
|
6500
6555
|
const origin = req.headers.origin;
|
|
6501
6556
|
const host = req.headers.host;
|
|
6502
|
-
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$
|
|
6557
|
+
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$5(res, 403, "origin-rejected");
|
|
6503
6558
|
let body;
|
|
6504
6559
|
try {
|
|
6505
|
-
body = JSON.parse(await readBody$
|
|
6560
|
+
body = JSON.parse(await readBody$4(req, deps.maxBodyBytes));
|
|
6506
6561
|
} catch (error) {
|
|
6507
|
-
return jsonError$
|
|
6562
|
+
return jsonError$5(res, error instanceof Error && error.message === "request-too-large" ? 413 : 400, "invalid-request");
|
|
6508
6563
|
}
|
|
6509
6564
|
const push = parseCanvasStatePush(body);
|
|
6510
|
-
if (push === void 0) return jsonError$
|
|
6565
|
+
if (push === void 0) return jsonError$5(res, 400, "invalid-canvas-state");
|
|
6511
6566
|
let selectionImage;
|
|
6512
6567
|
if (push.selectionImage !== void 0) {
|
|
6513
6568
|
const decoded = decodeSelectionImage(push.selectionImage);
|
|
6514
|
-
if (decoded === void 0) return jsonError$
|
|
6515
|
-
if (decoded.data.byteLength > deps.maxImageBytes) return jsonError$
|
|
6569
|
+
if (decoded === void 0) return jsonError$5(res, 400, "invalid-selection-image");
|
|
6570
|
+
if (decoded.data.byteLength > deps.maxImageBytes) return jsonError$5(res, 413, "selection-image-too-large");
|
|
6516
6571
|
selectionImage = decoded;
|
|
6517
6572
|
}
|
|
6518
6573
|
deps.mirror.apply(push, selectionImage);
|
|
@@ -6668,7 +6723,7 @@ function optionalDimension(value) {
|
|
|
6668
6723
|
function nonNegativeInteger$1(value) {
|
|
6669
6724
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
6670
6725
|
}
|
|
6671
|
-
async function readBody$
|
|
6726
|
+
async function readBody$4(req, maxBytes) {
|
|
6672
6727
|
const chunks = [];
|
|
6673
6728
|
let bytes = 0;
|
|
6674
6729
|
for await (const chunk of req) {
|
|
@@ -6679,7 +6734,7 @@ async function readBody$3(req, maxBytes) {
|
|
|
6679
6734
|
}
|
|
6680
6735
|
return Buffer.concat(chunks).toString("utf8");
|
|
6681
6736
|
}
|
|
6682
|
-
function jsonError$
|
|
6737
|
+
function jsonError$5(res, status, code) {
|
|
6683
6738
|
res.writeHead(status, {
|
|
6684
6739
|
"content-type": "application/json",
|
|
6685
6740
|
"cache-control": "no-store"
|
|
@@ -6793,7 +6848,7 @@ function findReferenceImage(messages, sourceAttachmentId) {
|
|
|
6793
6848
|
}
|
|
6794
6849
|
/** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
|
|
6795
6850
|
function parseImageAttachmentRef(value) {
|
|
6796
|
-
const ref = record$
|
|
6851
|
+
const ref = record$8(value);
|
|
6797
6852
|
if (ref === void 0) return void 0;
|
|
6798
6853
|
if (typeof ref.attachmentId !== "string" || !imageMediaType$4(ref.mediaType)) return void 0;
|
|
6799
6854
|
if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
|
|
@@ -6825,7 +6880,7 @@ function findInBlocks(blocks, sourceAttachmentId) {
|
|
|
6825
6880
|
}
|
|
6826
6881
|
}
|
|
6827
6882
|
function parseDimensions(value) {
|
|
6828
|
-
const dimensions = record$
|
|
6883
|
+
const dimensions = record$8(value);
|
|
6829
6884
|
if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
|
|
6830
6885
|
return {
|
|
6831
6886
|
width: dimensions.width,
|
|
@@ -6897,7 +6952,7 @@ function positiveInteger(value) {
|
|
|
6897
6952
|
function nonNegativeInteger(value) {
|
|
6898
6953
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
6899
6954
|
}
|
|
6900
|
-
function record$
|
|
6955
|
+
function record$8(value) {
|
|
6901
6956
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
6902
6957
|
}
|
|
6903
6958
|
//#endregion
|
|
@@ -7115,8 +7170,8 @@ function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed(), image
|
|
|
7115
7170
|
};
|
|
7116
7171
|
let imageInputs = 0;
|
|
7117
7172
|
const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
|
|
7118
|
-
const node = record$
|
|
7119
|
-
const inputs = node === void 0 ? void 0 : record$
|
|
7173
|
+
const node = record$7(value);
|
|
7174
|
+
const inputs = node === void 0 ? void 0 : record$7(node.inputs);
|
|
7120
7175
|
if (inputs === void 0) return [nodeId, value];
|
|
7121
7176
|
const nextInputs = inject(inputs);
|
|
7122
7177
|
if (isImagePlaceholder(nextInputs[IMAGE_INPUT_KEY])) {
|
|
@@ -7152,7 +7207,7 @@ function parseWorkflow(workflowJson) {
|
|
|
7152
7207
|
}
|
|
7153
7208
|
if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
|
|
7154
7209
|
if (!Object.values(value).some((node) => {
|
|
7155
|
-
const inputs = record$
|
|
7210
|
+
const inputs = record$7(node)?.inputs;
|
|
7156
7211
|
return inputs !== void 0 && containsPromptPlaceholder(inputs);
|
|
7157
7212
|
})) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
|
|
7158
7213
|
return value;
|
|
@@ -7168,7 +7223,7 @@ function containsPromptPlaceholder(value) {
|
|
|
7168
7223
|
function isRecord(value) {
|
|
7169
7224
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7170
7225
|
}
|
|
7171
|
-
function record$
|
|
7226
|
+
function record$7(value) {
|
|
7172
7227
|
return isRecord(value) ? value : void 0;
|
|
7173
7228
|
}
|
|
7174
7229
|
//#endregion
|
|
@@ -7302,9 +7357,9 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
7302
7357
|
});
|
|
7303
7358
|
const text = await readBoundedText$3(response, MAX_HISTORY_BYTES);
|
|
7304
7359
|
if (!response.ok) throw new Error(`ComfyUI history request failed (${response.status}): ${redactSecrets(text).slice(0, ERROR_LIMIT$3)}`);
|
|
7305
|
-
const entry = record$
|
|
7360
|
+
const entry = record$6(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
|
|
7306
7361
|
if (entry !== void 0) {
|
|
7307
|
-
const status = record$
|
|
7362
|
+
const status = record$6(entry.status);
|
|
7308
7363
|
if (status?.status_str === "error") throw new Error(`ComfyUI workflow failed: ${redactSecrets(JSON.stringify(status.messages ?? status)).slice(0, ERROR_LIMIT$3)}`);
|
|
7309
7364
|
const output = firstOutputImage(entry.outputs);
|
|
7310
7365
|
if (output !== void 0) return output;
|
|
@@ -7314,13 +7369,13 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
7314
7369
|
}
|
|
7315
7370
|
}
|
|
7316
7371
|
function firstOutputImage(value) {
|
|
7317
|
-
const outputs = record$
|
|
7372
|
+
const outputs = record$6(value);
|
|
7318
7373
|
if (outputs === void 0) return void 0;
|
|
7319
7374
|
for (const nodeOutput of Object.values(outputs)) {
|
|
7320
|
-
const images = record$
|
|
7375
|
+
const images = record$6(nodeOutput)?.images;
|
|
7321
7376
|
if (!Array.isArray(images)) continue;
|
|
7322
7377
|
for (const image of images) {
|
|
7323
|
-
const item = record$
|
|
7378
|
+
const item = record$6(image);
|
|
7324
7379
|
if (typeof item?.filename !== "string" || item.filename.length === 0) continue;
|
|
7325
7380
|
const output = {
|
|
7326
7381
|
filename: item.filename,
|
|
@@ -7361,7 +7416,7 @@ function comfyUIBaseURL(value) {
|
|
|
7361
7416
|
function endpoint(baseURL, path) {
|
|
7362
7417
|
return new URL(path, baseURL);
|
|
7363
7418
|
}
|
|
7364
|
-
function record$
|
|
7419
|
+
function record$6(value) {
|
|
7365
7420
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
7366
7421
|
}
|
|
7367
7422
|
function parseJsonRecord(text, message) {
|
|
@@ -7371,7 +7426,7 @@ function parseJsonRecord(text, message) {
|
|
|
7371
7426
|
} catch {
|
|
7372
7427
|
throw new Error(message);
|
|
7373
7428
|
}
|
|
7374
|
-
const parsed = record$
|
|
7429
|
+
const parsed = record$6(value);
|
|
7375
7430
|
if (parsed === void 0) throw new Error(message);
|
|
7376
7431
|
return parsed;
|
|
7377
7432
|
}
|
|
@@ -7455,7 +7510,7 @@ async function editDashScopeImage(options) {
|
|
|
7455
7510
|
model: options.model,
|
|
7456
7511
|
input: { messages: [{
|
|
7457
7512
|
role: "user",
|
|
7458
|
-
content: [...options.sourceImages.map((sourceImage) => ({ image: toDataUrl$
|
|
7513
|
+
content: [...options.sourceImages.map((sourceImage) => ({ image: toDataUrl$2(sourceImage) })), { text: options.prompt }]
|
|
7459
7514
|
}] },
|
|
7460
7515
|
parameters: {
|
|
7461
7516
|
prompt_extend: true,
|
|
@@ -7472,7 +7527,7 @@ function formatSize(size) {
|
|
|
7472
7527
|
if (size === void 0 || size.length === 0) return void 0;
|
|
7473
7528
|
return size.replace("x", "*");
|
|
7474
7529
|
}
|
|
7475
|
-
function toDataUrl$
|
|
7530
|
+
function toDataUrl$2(image) {
|
|
7476
7531
|
return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
7477
7532
|
}
|
|
7478
7533
|
async function requestQwenImage(options) {
|
|
@@ -7596,13 +7651,13 @@ async function requestGoogleImage(input) {
|
|
|
7596
7651
|
};
|
|
7597
7652
|
}
|
|
7598
7653
|
function outputImage(value) {
|
|
7599
|
-
const interaction = record$
|
|
7654
|
+
const interaction = record$5(value);
|
|
7600
7655
|
if (interaction === void 0) return void 0;
|
|
7601
7656
|
const direct = imageContent(interaction.output_image, false);
|
|
7602
7657
|
if (direct !== void 0) return direct;
|
|
7603
7658
|
if (!Array.isArray(interaction.steps)) return void 0;
|
|
7604
7659
|
for (const step of interaction.steps) {
|
|
7605
|
-
const modelOutput = record$
|
|
7660
|
+
const modelOutput = record$5(step);
|
|
7606
7661
|
if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
|
|
7607
7662
|
for (const content of modelOutput.content) {
|
|
7608
7663
|
const image = imageContent(content, true);
|
|
@@ -7611,14 +7666,14 @@ function outputImage(value) {
|
|
|
7611
7666
|
}
|
|
7612
7667
|
}
|
|
7613
7668
|
function imageContent(value, requiresImageType) {
|
|
7614
|
-
const image = record$
|
|
7669
|
+
const image = record$5(value);
|
|
7615
7670
|
if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
|
|
7616
7671
|
return {
|
|
7617
7672
|
data: image.data,
|
|
7618
7673
|
mime_type: image.mime_type
|
|
7619
7674
|
};
|
|
7620
7675
|
}
|
|
7621
|
-
function record$
|
|
7676
|
+
function record$5(value) {
|
|
7622
7677
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
7623
7678
|
}
|
|
7624
7679
|
function mediaTypeOf(value) {
|
|
@@ -7881,22 +7936,22 @@ async function deleteImageFromWorkspace(filePath, allowedWorkspaceRoots) {
|
|
|
7881
7936
|
}
|
|
7882
7937
|
//#endregion
|
|
7883
7938
|
//#region lib/types/image-route.js
|
|
7884
|
-
const MAX_BODY_BYTES$
|
|
7939
|
+
const MAX_BODY_BYTES$2 = 65536;
|
|
7885
7940
|
/** Serve one verified durable image reference to a same-origin browser request. */
|
|
7886
7941
|
async function serveImage(req, res, deps) {
|
|
7887
|
-
if (req.method !== "POST") return jsonError$
|
|
7888
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$
|
|
7942
|
+
if (req.method !== "POST") return jsonError$4(res, 405, "method-not-allowed");
|
|
7943
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$4(res, 415, "json-required");
|
|
7889
7944
|
const origin = req.headers.origin;
|
|
7890
7945
|
const host = req.headers.host;
|
|
7891
|
-
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$
|
|
7946
|
+
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$4(res, 403, "origin-rejected");
|
|
7892
7947
|
let body;
|
|
7893
7948
|
try {
|
|
7894
|
-
body = JSON.parse(await readBody$
|
|
7949
|
+
body = JSON.parse(await readBody$3(req));
|
|
7895
7950
|
} catch {
|
|
7896
|
-
return jsonError$
|
|
7951
|
+
return jsonError$4(res, 400, "invalid-request");
|
|
7897
7952
|
}
|
|
7898
7953
|
const attachment = attachmentFromRequest(body);
|
|
7899
|
-
if (attachment === void 0) return jsonError$
|
|
7954
|
+
if (attachment === void 0) return jsonError$4(res, 400, "invalid-attachment");
|
|
7900
7955
|
try {
|
|
7901
7956
|
const stored = await deps.readImage(attachment);
|
|
7902
7957
|
res.writeHead(200, {
|
|
@@ -7907,10 +7962,10 @@ async function serveImage(req, res, deps) {
|
|
|
7907
7962
|
});
|
|
7908
7963
|
res.end(stored.data);
|
|
7909
7964
|
} catch {
|
|
7910
|
-
jsonError$
|
|
7965
|
+
jsonError$4(res, 404, "image-unavailable");
|
|
7911
7966
|
}
|
|
7912
7967
|
}
|
|
7913
|
-
function sameOrigin$
|
|
7968
|
+
function sameOrigin$4(req) {
|
|
7914
7969
|
const origin = req.headers.origin;
|
|
7915
7970
|
const host = req.headers.host;
|
|
7916
7971
|
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return false;
|
|
@@ -7918,14 +7973,14 @@ function sameOrigin$3(req) {
|
|
|
7918
7973
|
}
|
|
7919
7974
|
/** Safely delete one or more generated image files from the workspace disk. */
|
|
7920
7975
|
async function serveDelete(req, res, deps) {
|
|
7921
|
-
if (req.method !== "POST") return jsonError$
|
|
7922
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$
|
|
7923
|
-
if (!sameOrigin$
|
|
7976
|
+
if (req.method !== "POST") return jsonError$4(res, 405, "method-not-allowed");
|
|
7977
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$4(res, 415, "json-required");
|
|
7978
|
+
if (!sameOrigin$4(req)) return jsonError$4(res, 403, "origin-rejected");
|
|
7924
7979
|
let body;
|
|
7925
7980
|
try {
|
|
7926
|
-
body = JSON.parse(await readBody$
|
|
7981
|
+
body = JSON.parse(await readBody$3(req));
|
|
7927
7982
|
} catch {
|
|
7928
|
-
return jsonError$
|
|
7983
|
+
return jsonError$4(res, 400, "invalid-request");
|
|
7929
7984
|
}
|
|
7930
7985
|
const rawPaths = typeof body === "object" && body !== null && "paths" in body && Array.isArray(body.paths) ? body.paths : [];
|
|
7931
7986
|
const deletedFiles = [];
|
|
@@ -7955,13 +8010,13 @@ async function serveDelete(req, res, deps) {
|
|
|
7955
8010
|
}
|
|
7956
8011
|
/** Serve on-demand image persistence into the workspace. */
|
|
7957
8012
|
async function serveSaveWorkspace(req, res, deps) {
|
|
7958
|
-
if (!sameOrigin$
|
|
7959
|
-
if (req.method !== "POST") return jsonError$
|
|
7960
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$
|
|
8013
|
+
if (!sameOrigin$4(req)) return jsonError$4(res, 403, "origin-rejected");
|
|
8014
|
+
if (req.method !== "POST") return jsonError$4(res, 405, "method-not-allowed");
|
|
8015
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$4(res, 415, "json-required");
|
|
7961
8016
|
try {
|
|
7962
|
-
const raw = JSON.parse(await readBody$
|
|
8017
|
+
const raw = JSON.parse(await readBody$3(req));
|
|
7963
8018
|
const attachment = parseImageAttachmentRef(raw.attachment);
|
|
7964
|
-
if (!attachment) return jsonError$
|
|
8019
|
+
if (!attachment) return jsonError$4(res, 400, "invalid-attachment");
|
|
7965
8020
|
if (!deps.isSaveEnabled()) {
|
|
7966
8021
|
res.writeHead(200, {
|
|
7967
8022
|
"content-type": "application/json",
|
|
@@ -7979,10 +8034,10 @@ async function serveSaveWorkspace(req, res, deps) {
|
|
|
7979
8034
|
try {
|
|
7980
8035
|
targetRoot = await assertWorkspaceAllowed(rawTarget, allowedRoots);
|
|
7981
8036
|
} catch {
|
|
7982
|
-
return jsonError$
|
|
8037
|
+
return jsonError$4(res, 403, "workspace-root-not-allowed");
|
|
7983
8038
|
}
|
|
7984
8039
|
const stored = await deps.readImage(attachment);
|
|
7985
|
-
if (!stored || !stored.data) return jsonError$
|
|
8040
|
+
if (!stored || !stored.data) return jsonError$4(res, 404, "image-not-found");
|
|
7986
8041
|
const savedTo = await deps.saveToWorkspace({
|
|
7987
8042
|
workspaceRoot: targetRoot,
|
|
7988
8043
|
attachmentId: String(attachment.attachmentId),
|
|
@@ -8010,28 +8065,28 @@ async function serveSaveWorkspace(req, res, deps) {
|
|
|
8010
8065
|
}
|
|
8011
8066
|
/** Validate the persisted reference carried by a tool presentation. */
|
|
8012
8067
|
function imageAttachmentFromMeta(meta) {
|
|
8013
|
-
const value = record$
|
|
8068
|
+
const value = record$4(meta);
|
|
8014
8069
|
if (value?.kind !== "dsh-image-gen") return void 0;
|
|
8015
8070
|
return parseImageAttachmentRef(value.attachment);
|
|
8016
8071
|
}
|
|
8017
8072
|
function attachmentFromRequest(value) {
|
|
8018
|
-
return parseImageAttachmentRef(record$
|
|
8073
|
+
return parseImageAttachmentRef(record$4(value)?.attachment);
|
|
8019
8074
|
}
|
|
8020
|
-
function record$
|
|
8075
|
+
function record$4(value) {
|
|
8021
8076
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
8022
8077
|
}
|
|
8023
|
-
async function readBody$
|
|
8078
|
+
async function readBody$3(req) {
|
|
8024
8079
|
const chunks = [];
|
|
8025
8080
|
let bytes = 0;
|
|
8026
8081
|
for await (const chunk of req) {
|
|
8027
8082
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8028
8083
|
bytes += buffer.byteLength;
|
|
8029
|
-
if (bytes > MAX_BODY_BYTES$
|
|
8084
|
+
if (bytes > MAX_BODY_BYTES$2) throw new Error("request too large");
|
|
8030
8085
|
chunks.push(buffer);
|
|
8031
8086
|
}
|
|
8032
8087
|
return Buffer.concat(chunks).toString("utf8");
|
|
8033
8088
|
}
|
|
8034
|
-
function jsonError$
|
|
8089
|
+
function jsonError$4(res, status, code) {
|
|
8035
8090
|
res.writeHead(status, {
|
|
8036
8091
|
"content-type": "application/json",
|
|
8037
8092
|
"cache-control": "no-store"
|
|
@@ -8059,6 +8114,27 @@ async function generateOpenAICompatibleImage(input) {
|
|
|
8059
8114
|
}), input.provider, input);
|
|
8060
8115
|
}
|
|
8061
8116
|
async function editOpenAICompatibleImage(input) {
|
|
8117
|
+
if (input.editFormat === "jsonImageUrlArray") {
|
|
8118
|
+
const body = {
|
|
8119
|
+
model: input.model,
|
|
8120
|
+
images: input.sourceImages.map((sourceImage) => ({ image_url: toDataUrl$1(sourceImage) })),
|
|
8121
|
+
prompt: input.prompt,
|
|
8122
|
+
n: 1,
|
|
8123
|
+
size: "auto",
|
|
8124
|
+
response_format: "url",
|
|
8125
|
+
...input.editExtra ?? {}
|
|
8126
|
+
};
|
|
8127
|
+
return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
|
|
8128
|
+
method: "POST",
|
|
8129
|
+
redirect: "error",
|
|
8130
|
+
signal: input.signal,
|
|
8131
|
+
headers: {
|
|
8132
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
8133
|
+
"content-type": "application/json"
|
|
8134
|
+
},
|
|
8135
|
+
body: JSON.stringify(body)
|
|
8136
|
+
}), "openai", input);
|
|
8137
|
+
}
|
|
8062
8138
|
const form = new FormData();
|
|
8063
8139
|
const imageField = input.sourceImages.length > 1 ? "image[]" : "image";
|
|
8064
8140
|
input.sourceImages.forEach((sourceImage, index) => {
|
|
@@ -8102,6 +8178,9 @@ function imageEndpoint$1(baseURL, operation) {
|
|
|
8102
8178
|
throw new Error("Image endpoint must be an absolute URL");
|
|
8103
8179
|
}
|
|
8104
8180
|
}
|
|
8181
|
+
function toDataUrl$1(image) {
|
|
8182
|
+
return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
8183
|
+
}
|
|
8105
8184
|
function firstImage$1(value) {
|
|
8106
8185
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
8107
8186
|
const record = value;
|
|
@@ -8111,10 +8190,10 @@ function firstImage$1(value) {
|
|
|
8111
8190
|
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
|
|
8112
8191
|
const item = candidate;
|
|
8113
8192
|
const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
|
|
8114
|
-
return typeof item.b64_json === "string" ? {
|
|
8193
|
+
return typeof item.b64_json === "string" && item.b64_json.length > 0 ? {
|
|
8115
8194
|
b64_json: item.b64_json,
|
|
8116
8195
|
...mime === void 0 ? {} : { mime_type: mime }
|
|
8117
|
-
} : typeof item.url === "string" ? {
|
|
8196
|
+
} : typeof item.url === "string" && item.url.length > 0 ? {
|
|
8118
8197
|
url: item.url,
|
|
8119
8198
|
...mime === void 0 ? {} : { mime_type: mime }
|
|
8120
8199
|
} : void 0;
|
|
@@ -8129,11 +8208,15 @@ async function downloadImage$1(url, provider, input) {
|
|
|
8129
8208
|
mediaType: imageMediaType$2(parsed.mediaType) ?? "image/png"
|
|
8130
8209
|
};
|
|
8131
8210
|
}
|
|
8132
|
-
|
|
8211
|
+
let response = await fetch(url, {
|
|
8133
8212
|
redirect: "follow",
|
|
8134
8213
|
signal: input.signal,
|
|
8135
8214
|
...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
|
|
8136
8215
|
});
|
|
8216
|
+
if (!response.ok && input.apiKey !== void 0) response = await fetch(url, {
|
|
8217
|
+
redirect: "follow",
|
|
8218
|
+
signal: input.signal
|
|
8219
|
+
});
|
|
8137
8220
|
if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
|
|
8138
8221
|
const data = await readBoundedBytes$1(response, input.maxBytes);
|
|
8139
8222
|
const mediaType = detectImageMediaType(data) ?? imageMediaType$2(response.headers.get("content-type"));
|
|
@@ -8252,10 +8335,10 @@ function firstImage(value) {
|
|
|
8252
8335
|
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
|
|
8253
8336
|
const item = candidate;
|
|
8254
8337
|
const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
|
|
8255
|
-
return typeof item.b64_json === "string" ? {
|
|
8338
|
+
return typeof item.b64_json === "string" && item.b64_json.length > 0 ? {
|
|
8256
8339
|
b64_json: item.b64_json,
|
|
8257
8340
|
...mime === void 0 ? {} : { mime_type: mime }
|
|
8258
|
-
} : typeof item.url === "string" ? {
|
|
8341
|
+
} : typeof item.url === "string" && item.url.length > 0 ? {
|
|
8259
8342
|
url: item.url,
|
|
8260
8343
|
...mime === void 0 ? {} : { mime_type: mime }
|
|
8261
8344
|
} : void 0;
|
|
@@ -8313,6 +8396,1193 @@ async function readBoundedBytes(response, maxBytes) {
|
|
|
8313
8396
|
return joined;
|
|
8314
8397
|
}
|
|
8315
8398
|
//#endregion
|
|
8399
|
+
//#region lib/types/subscription/oauth.js
|
|
8400
|
+
/**
|
|
8401
|
+
* OAuth primitives shared by the subscription vendors.
|
|
8402
|
+
*
|
|
8403
|
+
* Adapted from @goodandready/dsh-subscriptions (MIT, (c) 2026 GooDAnDReaDY)
|
|
8404
|
+
* lib/oauth.js, lib/pkce.js, lib/jwt.js — PKCE generation, authorize-URL
|
|
8405
|
+
* building and the JWT payload reads the vendors need for identity headers.
|
|
8406
|
+
*/
|
|
8407
|
+
async function createPkce() {
|
|
8408
|
+
const verifier = Buffer.from(randomBytes(32)).toString("base64url");
|
|
8409
|
+
return {
|
|
8410
|
+
verifier,
|
|
8411
|
+
challenge: Buffer.from(createHash("sha256").update(verifier).digest()).toString("base64url"),
|
|
8412
|
+
state: Buffer.from(randomBytes(16)).toString("base64url")
|
|
8413
|
+
};
|
|
8414
|
+
}
|
|
8415
|
+
/** Build an authorization-code URL with PKCE and optional extra params. */
|
|
8416
|
+
function buildAuthorizeUrl(input) {
|
|
8417
|
+
const url = new URL(input.authUrl);
|
|
8418
|
+
url.searchParams.set("response_type", "code");
|
|
8419
|
+
url.searchParams.set("client_id", input.clientId);
|
|
8420
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
8421
|
+
url.searchParams.set("code_challenge", input.challenge);
|
|
8422
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
8423
|
+
url.searchParams.set("state", input.state);
|
|
8424
|
+
if (input.scope !== void 0 && input.scope.length > 0) url.searchParams.set("scope", input.scope);
|
|
8425
|
+
if (input.extra !== void 0) for (const [key, value] of Object.entries(input.extra)) {
|
|
8426
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
8427
|
+
url.searchParams.set(key, String(value));
|
|
8428
|
+
}
|
|
8429
|
+
return url.toString();
|
|
8430
|
+
}
|
|
8431
|
+
/** Decode a JWT payload without signature verification (identity read only). */
|
|
8432
|
+
function decodeJwtPayload(token) {
|
|
8433
|
+
const parts = String(token ?? "").split(".");
|
|
8434
|
+
if (parts.length < 2) return void 0;
|
|
8435
|
+
try {
|
|
8436
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
8437
|
+
const parsed = JSON.parse(json);
|
|
8438
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
8439
|
+
} catch {
|
|
8440
|
+
return;
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
/** The chatgpt-account-id a Codex API call must carry. */
|
|
8444
|
+
function chatgptAccountId(token) {
|
|
8445
|
+
const payload = decodeJwtPayload(token);
|
|
8446
|
+
if (payload === void 0) return "";
|
|
8447
|
+
if (typeof payload.chatgpt_account_id === "string") return payload.chatgpt_account_id;
|
|
8448
|
+
const nested = payload["https://api.openai.com/auth"];
|
|
8449
|
+
if (typeof nested === "object" && nested !== null && typeof nested.chatgpt_account_id === "string") return nested.chatgpt_account_id;
|
|
8450
|
+
const orgs = payload.organizations;
|
|
8451
|
+
if (Array.isArray(orgs) && orgs[0] !== null && typeof orgs[0] === "object" && typeof orgs[0].id === "string") return orgs[0].id;
|
|
8452
|
+
return "";
|
|
8453
|
+
}
|
|
8454
|
+
/** The account email for badges and login confirmation. */
|
|
8455
|
+
function emailFromToken(token) {
|
|
8456
|
+
const payload = decodeJwtPayload(token);
|
|
8457
|
+
if (payload === void 0) return "";
|
|
8458
|
+
const email = payload.email;
|
|
8459
|
+
const preferred = payload.preferred_username;
|
|
8460
|
+
if (typeof email === "string" && email.length > 0) return email;
|
|
8461
|
+
if (typeof preferred === "string") return preferred;
|
|
8462
|
+
return "";
|
|
8463
|
+
}
|
|
8464
|
+
/** POST an OAuth token endpoint with form encoding; 25s cap like the source. */
|
|
8465
|
+
async function formTokenRequest(url, params, fetchImpl) {
|
|
8466
|
+
const response = await fetchImpl(url, {
|
|
8467
|
+
method: "POST",
|
|
8468
|
+
headers: {
|
|
8469
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
8470
|
+
Accept: "application/json"
|
|
8471
|
+
},
|
|
8472
|
+
body: new URLSearchParams(params).toString(),
|
|
8473
|
+
signal: AbortSignal.timeout(25e3)
|
|
8474
|
+
});
|
|
8475
|
+
if (!response.ok) {
|
|
8476
|
+
const text = await response.text().catch(() => "");
|
|
8477
|
+
throw new Error(`token endpoint HTTP ${String(response.status)}${text.length > 0 ? `: ${text.slice(0, 200)}` : ""}`);
|
|
8478
|
+
}
|
|
8479
|
+
try {
|
|
8480
|
+
const parsed = await response.json();
|
|
8481
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8482
|
+
} catch {
|
|
8483
|
+
return {};
|
|
8484
|
+
}
|
|
8485
|
+
}
|
|
8486
|
+
//#endregion
|
|
8487
|
+
//#region lib/types/subscription/loopback.js
|
|
8488
|
+
/**
|
|
8489
|
+
* Temporary loopback server that catches the OAuth redirect on a vendor-fixed
|
|
8490
|
+
* local port and hands the code to a handler.
|
|
8491
|
+
*
|
|
8492
|
+
* Adapted from @goodandready/dsh-subscriptions (MIT, (c) 2026 GooDAnDReaDY)
|
|
8493
|
+
* lib/loopback.js — same protocol (first matching request wins, server shuts
|
|
8494
|
+
* down, 10-minute cap), restated in TypeScript with this bundle's style.
|
|
8495
|
+
*/
|
|
8496
|
+
const OK_HTML = "<!doctype html><meta charset=\"utf-8\"><title>dsh-image-gen</title><p>登录成功,可以关闭此页返回设置。</p>";
|
|
8497
|
+
const ERR_HTML = "<!doctype html><meta charset=\"utf-8\"><title>dsh-image-gen</title><p>登录失败,请返回设置重试。</p>";
|
|
8498
|
+
/**
|
|
8499
|
+
* Listen on the redirect_uri's port and resolve when the provider calls back.
|
|
8500
|
+
* Rejects on port conflicts, network errors, or when nothing arrives in time.
|
|
8501
|
+
*/
|
|
8502
|
+
function startLoopback(options) {
|
|
8503
|
+
const parsed = new URL(options.redirectUri);
|
|
8504
|
+
if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") return Promise.reject(/* @__PURE__ */ new Error(`loopback redirect requires localhost, got ${parsed.hostname}`));
|
|
8505
|
+
const port = Number(parsed.port) || 80;
|
|
8506
|
+
const path = parsed.pathname;
|
|
8507
|
+
const timeoutMs = options.timeoutMs ?? 6e5;
|
|
8508
|
+
const state = { done: false };
|
|
8509
|
+
return new Promise((resolve, reject) => {
|
|
8510
|
+
const server = http.createServer((req, res) => {
|
|
8511
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${String(port)}`);
|
|
8512
|
+
if (!url.pathname.startsWith(path)) {
|
|
8513
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
8514
|
+
res.end(ERR_HTML);
|
|
8515
|
+
return;
|
|
8516
|
+
}
|
|
8517
|
+
if (state.done) return;
|
|
8518
|
+
state.done = true;
|
|
8519
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8520
|
+
Promise.resolve(options.onCode(url.searchParams)).then((html) => {
|
|
8521
|
+
res.end(html.length > 0 ? html : OK_HTML);
|
|
8522
|
+
}).catch(() => {
|
|
8523
|
+
res.end(ERR_HTML);
|
|
8524
|
+
}).finally(() => {
|
|
8525
|
+
clearTimeout(timer);
|
|
8526
|
+
server.close();
|
|
8527
|
+
resolve({ ok: true });
|
|
8528
|
+
});
|
|
8529
|
+
});
|
|
8530
|
+
server.on("error", (error) => {
|
|
8531
|
+
if (state.done) return;
|
|
8532
|
+
state.done = true;
|
|
8533
|
+
clearTimeout(timer);
|
|
8534
|
+
reject(error);
|
|
8535
|
+
});
|
|
8536
|
+
const timer = setTimeout(() => {
|
|
8537
|
+
if (state.done) return;
|
|
8538
|
+
state.done = true;
|
|
8539
|
+
server.close();
|
|
8540
|
+
reject(/* @__PURE__ */ new Error("loopback timeout: no callback received"));
|
|
8541
|
+
}, timeoutMs);
|
|
8542
|
+
server.listen(port, parsed.hostname);
|
|
8543
|
+
});
|
|
8544
|
+
}
|
|
8545
|
+
//#endregion
|
|
8546
|
+
//#region lib/types/subscription/blob.js
|
|
8547
|
+
/**
|
|
8548
|
+
* OAuth token blob persistence for subscription accounts.
|
|
8549
|
+
*
|
|
8550
|
+
* Adapted from @goodandready/dsh-subscriptions (MIT, (c) 2026 GooDAnDReaDY)
|
|
8551
|
+
* lib/blob.js — trimmed to the two image vendors this plugin ships. The blob
|
|
8552
|
+
* is stored through the DSH Credentials service under a provider-scoped ref
|
|
8553
|
+
* (see refs.ts); it never reaches the browser side.
|
|
8554
|
+
*/
|
|
8555
|
+
function asString(value) {
|
|
8556
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
8557
|
+
}
|
|
8558
|
+
/** Serialize a blob for storage; at least one token must be present. */
|
|
8559
|
+
function serializeBlob(obj) {
|
|
8560
|
+
const accessToken = asString(obj.accessToken);
|
|
8561
|
+
const refreshToken = asString(obj.refreshToken);
|
|
8562
|
+
if (accessToken.length === 0 && refreshToken.length === 0) throw new Error("oauth blob needs accessToken or refreshToken");
|
|
8563
|
+
return JSON.stringify({
|
|
8564
|
+
accessToken,
|
|
8565
|
+
refreshToken,
|
|
8566
|
+
expiresAt: Number(obj.expiresAt) || 0,
|
|
8567
|
+
label: asString(obj.label),
|
|
8568
|
+
email: asString(obj.email),
|
|
8569
|
+
accountId: asString(obj.accountId)
|
|
8570
|
+
});
|
|
8571
|
+
}
|
|
8572
|
+
/** Parse a stored blob back; invalid input throws. */
|
|
8573
|
+
function parseBlob(text) {
|
|
8574
|
+
const obj = typeof text === "string" ? JSON.parse(text) : text;
|
|
8575
|
+
if (typeof obj !== "object" || obj === null) throw new Error("invalid oauth blob");
|
|
8576
|
+
const row = obj;
|
|
8577
|
+
return {
|
|
8578
|
+
accessToken: asString(row.accessToken),
|
|
8579
|
+
refreshToken: asString(row.refreshToken),
|
|
8580
|
+
expiresAt: Number(row.expiresAt) || 0,
|
|
8581
|
+
label: asString(row.label),
|
|
8582
|
+
email: asString(row.email),
|
|
8583
|
+
accountId: asString(row.accountId)
|
|
8584
|
+
};
|
|
8585
|
+
}
|
|
8586
|
+
//#endregion
|
|
8587
|
+
//#region lib/types/subscription/vendors/codex.js
|
|
8588
|
+
/**
|
|
8589
|
+
* ChatGPT (Codex) subscription image vendor.
|
|
8590
|
+
*
|
|
8591
|
+
* Adapted from @goodandready/dsh-subscriptions (MIT, (c) 2026 GooDAnDReaDY)
|
|
8592
|
+
* lib/vendors/codex.js and lib/images.js — only the pieces the image channel
|
|
8593
|
+
* needs: authorize URL, code exchange, refresh, and the generations call.
|
|
8594
|
+
* The device-code flow and chat streaming stay out of this bundle.
|
|
8595
|
+
*/
|
|
8596
|
+
const CODEX_AUTH = "https://auth.openai.com/oauth/authorize";
|
|
8597
|
+
const CODEX_TOKEN = "https://auth.openai.com/oauth/token";
|
|
8598
|
+
const CODEX_SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
|
|
8599
|
+
/** Where the ChatGPT subscription image request goes. */
|
|
8600
|
+
const CODEX_IMAGE_URL = "https://chatgpt.com/backend-api/codex/images/generations";
|
|
8601
|
+
/** The model served by this endpoint (probed live: gpt-image-2.5-flare works
|
|
8602
|
+
* on the ChatGPT internal generations route as of 2026-09-14). */
|
|
8603
|
+
const CODEX_IMAGE_MODEL = "gpt-image-2.5-flare";
|
|
8604
|
+
/** Public client id of the Codex CLI; vendor-fixed redirect on port 1455. */
|
|
8605
|
+
const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
8606
|
+
const CODEX_REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
8607
|
+
function codexConfig() {
|
|
8608
|
+
return {
|
|
8609
|
+
clientId: CODEX_CLIENT_ID,
|
|
8610
|
+
redirectUri: CODEX_REDIRECT_URI
|
|
8611
|
+
};
|
|
8612
|
+
}
|
|
8613
|
+
function codexAuthorizeUrl(cfg, pkce) {
|
|
8614
|
+
return buildAuthorizeUrl({
|
|
8615
|
+
authUrl: CODEX_AUTH,
|
|
8616
|
+
clientId: cfg.clientId,
|
|
8617
|
+
redirectUri: cfg.redirectUri,
|
|
8618
|
+
challenge: pkce.challenge,
|
|
8619
|
+
state: pkce.state,
|
|
8620
|
+
scope: CODEX_SCOPE,
|
|
8621
|
+
extra: {
|
|
8622
|
+
id_token_add_organizations: "true",
|
|
8623
|
+
codex_cli_simplified_flow: "true",
|
|
8624
|
+
originator: "codex_cli_rs"
|
|
8625
|
+
}
|
|
8626
|
+
});
|
|
8627
|
+
}
|
|
8628
|
+
/** Identity fields every Codex API call must carry. */
|
|
8629
|
+
function codexIdentityHeaders(blob) {
|
|
8630
|
+
return {
|
|
8631
|
+
authorization: `Bearer ${blob.accessToken}`,
|
|
8632
|
+
"chatgpt-account-id": blob.accountId,
|
|
8633
|
+
originator: "codex_cli_rs",
|
|
8634
|
+
"content-type": "application/json",
|
|
8635
|
+
accept: "application/json"
|
|
8636
|
+
};
|
|
8637
|
+
}
|
|
8638
|
+
function decorate(blob, json) {
|
|
8639
|
+
const idToken = typeof json.id_token === "string" ? json.id_token : "";
|
|
8640
|
+
return {
|
|
8641
|
+
...blob,
|
|
8642
|
+
accountId: blob.accountId.length > 0 ? blob.accountId : chatgptAccountId(idToken) || chatgptAccountId(blob.accessToken),
|
|
8643
|
+
email: blob.email.length > 0 ? blob.email : emailFromToken(idToken) || emailFromToken(blob.accessToken),
|
|
8644
|
+
label: blob.label.length > 0 ? blob.label : emailFromToken(idToken) || emailFromToken(blob.accessToken) || "ChatGPT"
|
|
8645
|
+
};
|
|
8646
|
+
}
|
|
8647
|
+
function tokenBlobFromOAuth$2(json) {
|
|
8648
|
+
const access = json.access_token;
|
|
8649
|
+
const refresh = json.refresh_token;
|
|
8650
|
+
return {
|
|
8651
|
+
accessToken: typeof access === "string" ? access : "",
|
|
8652
|
+
refreshToken: typeof refresh === "string" ? refresh : "",
|
|
8653
|
+
expiresAt: Date.now() + (Number(json.expires_in) || 3600) * 1e3,
|
|
8654
|
+
label: "",
|
|
8655
|
+
email: "",
|
|
8656
|
+
accountId: ""
|
|
8657
|
+
};
|
|
8658
|
+
}
|
|
8659
|
+
async function codexExchangeCode(cfg, pkce, code) {
|
|
8660
|
+
const json = await formTokenRequest(CODEX_TOKEN, {
|
|
8661
|
+
grant_type: "authorization_code",
|
|
8662
|
+
client_id: cfg.clientId,
|
|
8663
|
+
code,
|
|
8664
|
+
redirect_uri: cfg.redirectUri,
|
|
8665
|
+
code_verifier: pkce.verifier
|
|
8666
|
+
}, fetch);
|
|
8667
|
+
return decorate(tokenBlobFromOAuth$2(json), json);
|
|
8668
|
+
}
|
|
8669
|
+
async function codexRefresh(blob) {
|
|
8670
|
+
const next = tokenBlobFromOAuth$2(await formTokenRequest(CODEX_TOKEN, {
|
|
8671
|
+
grant_type: "refresh_token",
|
|
8672
|
+
client_id: CODEX_CLIENT_ID,
|
|
8673
|
+
refresh_token: blob.refreshToken
|
|
8674
|
+
}, fetch));
|
|
8675
|
+
return {
|
|
8676
|
+
...next,
|
|
8677
|
+
refreshToken: next.refreshToken.length > 0 ? next.refreshToken : blob.refreshToken,
|
|
8678
|
+
label: blob.label,
|
|
8679
|
+
email: blob.email,
|
|
8680
|
+
accountId: blob.accountId.length > 0 ? blob.accountId : next.accountId
|
|
8681
|
+
};
|
|
8682
|
+
}
|
|
8683
|
+
//#endregion
|
|
8684
|
+
//#region lib/types/subscription/vendors/grok.js
|
|
8685
|
+
/**
|
|
8686
|
+
* Grok subscription image vendor.
|
|
8687
|
+
*
|
|
8688
|
+
* Adapted from @goodandready/dsh-subscriptions (MIT, (c) 2026 GooDAnDReaDY)
|
|
8689
|
+
* lib/vendors/grok.js and lib/images.js — only the pieces the image channel
|
|
8690
|
+
* needs: authorize URL, code exchange, refresh, and the generations call
|
|
8691
|
+
* (grok-cli token, not an xAI API key).
|
|
8692
|
+
*/
|
|
8693
|
+
const GROK_AUTH = "https://auth.x.ai/oauth2/authorize";
|
|
8694
|
+
const GROK_TOKEN = "https://auth.x.ai/oauth2/token";
|
|
8695
|
+
const GROK_SCOPE = "openid profile email offline_access grok-cli:access api:access conversations:read conversations:write";
|
|
8696
|
+
/** Where the Grok subscription image request goes. */
|
|
8697
|
+
const GROK_IMAGE_URL = "https://api.x.ai/v1/images/generations";
|
|
8698
|
+
/** The model served by this endpoint. */
|
|
8699
|
+
const GROK_IMAGE_MODEL = "grok-imagine-image-2.0";
|
|
8700
|
+
/** Public client id of the grok-cli; vendor-fixed redirect on 127.0.0.1:56121. */
|
|
8701
|
+
const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
8702
|
+
const GROK_REDIRECT_URI = "http://127.0.0.1:56121/callback";
|
|
8703
|
+
function grokConfig() {
|
|
8704
|
+
return {
|
|
8705
|
+
clientId: GROK_CLIENT_ID,
|
|
8706
|
+
redirectUri: GROK_REDIRECT_URI
|
|
8707
|
+
};
|
|
8708
|
+
}
|
|
8709
|
+
function grokAuthorizeUrl(cfg, pkce) {
|
|
8710
|
+
return buildAuthorizeUrl({
|
|
8711
|
+
authUrl: GROK_AUTH,
|
|
8712
|
+
clientId: cfg.clientId,
|
|
8713
|
+
redirectUri: cfg.redirectUri,
|
|
8714
|
+
challenge: pkce.challenge,
|
|
8715
|
+
state: pkce.state,
|
|
8716
|
+
scope: GROK_SCOPE
|
|
8717
|
+
});
|
|
8718
|
+
}
|
|
8719
|
+
/** Identity fields every grok.com call must carry (the API host ignores them). */
|
|
8720
|
+
function grokIdentityHeaders(blob) {
|
|
8721
|
+
return {
|
|
8722
|
+
authorization: `Bearer ${blob.accessToken}`,
|
|
8723
|
+
"X-XAI-Token-Auth": "xai-grok-cli",
|
|
8724
|
+
"x-grok-client-identifier": "grok-shell",
|
|
8725
|
+
"x-grok-client-version": "0.2.103",
|
|
8726
|
+
"User-Agent": "xai-grok-cli",
|
|
8727
|
+
"content-type": "application/json",
|
|
8728
|
+
accept: "application/json"
|
|
8729
|
+
};
|
|
8730
|
+
}
|
|
8731
|
+
function tokenBlobFromOAuth$1(json) {
|
|
8732
|
+
const access = json.access_token;
|
|
8733
|
+
const refresh = json.refresh_token;
|
|
8734
|
+
return {
|
|
8735
|
+
accessToken: typeof access === "string" ? access : "",
|
|
8736
|
+
refreshToken: typeof refresh === "string" ? refresh : "",
|
|
8737
|
+
expiresAt: Date.now() + (Number(json.expires_in) || 3600) * 1e3,
|
|
8738
|
+
label: "",
|
|
8739
|
+
email: "",
|
|
8740
|
+
accountId: ""
|
|
8741
|
+
};
|
|
8742
|
+
}
|
|
8743
|
+
async function grokExchangeCode(cfg, pkce, code) {
|
|
8744
|
+
const blob = tokenBlobFromOAuth$1(await formTokenRequest(GROK_TOKEN, {
|
|
8745
|
+
grant_type: "authorization_code",
|
|
8746
|
+
client_id: cfg.clientId,
|
|
8747
|
+
code,
|
|
8748
|
+
redirect_uri: cfg.redirectUri,
|
|
8749
|
+
code_verifier: pkce.verifier
|
|
8750
|
+
}, fetch));
|
|
8751
|
+
return {
|
|
8752
|
+
...blob,
|
|
8753
|
+
email: blob.email.length > 0 ? blob.email : emailFromToken(blob.accessToken),
|
|
8754
|
+
label: "Grok"
|
|
8755
|
+
};
|
|
8756
|
+
}
|
|
8757
|
+
async function grokRefresh(blob) {
|
|
8758
|
+
const next = tokenBlobFromOAuth$1(await formTokenRequest(GROK_TOKEN, {
|
|
8759
|
+
grant_type: "refresh_token",
|
|
8760
|
+
client_id: GROK_CLIENT_ID,
|
|
8761
|
+
refresh_token: blob.refreshToken
|
|
8762
|
+
}, fetch));
|
|
8763
|
+
return {
|
|
8764
|
+
...next,
|
|
8765
|
+
refreshToken: next.refreshToken.length > 0 ? next.refreshToken : blob.refreshToken,
|
|
8766
|
+
label: blob.label.length > 0 ? blob.label : "Grok",
|
|
8767
|
+
email: blob.email.length > 0 ? blob.email : next.email
|
|
8768
|
+
};
|
|
8769
|
+
}
|
|
8770
|
+
//#endregion
|
|
8771
|
+
//#region lib/types/subscription/vendors/antigravity.js
|
|
8772
|
+
/**
|
|
8773
|
+
* Google Antigravity (Nano Banana Pro) subscription image vendor.
|
|
8774
|
+
*
|
|
8775
|
+
* Protocol adapted from opencode-antigravity-auth-remix (MIT,
|
|
8776
|
+
* (c) 2026 Darkstarrd-dev, based on NoeFabris/opencode-antigravity-auth):
|
|
8777
|
+
* Google OAuth with the Antigravity CLI's public client credentials, managed
|
|
8778
|
+
* project discovery via loadCodeAssist, and image generation through the
|
|
8779
|
+
* v1internal:streamGenerateContent endpoint with the gemini-3-pro-image
|
|
8780
|
+
* model. Reference: dist/src/constants.js, dist/src/plugin/image.js,
|
|
8781
|
+
* dist/src/plugin/project.js.
|
|
8782
|
+
*
|
|
8783
|
+
* The dsh-subscriptions antigravity vendor left the OAuth client empty
|
|
8784
|
+
* (Google's is confidential); the remix package ships the Antigravity CLI's
|
|
8785
|
+
* own embedded client id/secret, which the community treats as public the
|
|
8786
|
+
* same way Codex CLI's and grok-cli's are.
|
|
8787
|
+
*/
|
|
8788
|
+
const AUTH = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8789
|
+
const TOKEN = "https://oauth2.googleapis.com/token";
|
|
8790
|
+
const SCOPE = [
|
|
8791
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
8792
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
8793
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
8794
|
+
"https://www.googleapis.com/auth/cclog",
|
|
8795
|
+
"https://www.googleapis.com/auth/experimentsandconfigs"
|
|
8796
|
+
].join(" ");
|
|
8797
|
+
/** Antigravity CLI's embedded OAuth client, treated as public like Codex's. */
|
|
8798
|
+
const ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
8799
|
+
const ANTIGRAVITY_CLIENT_SECRET = "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";
|
|
8800
|
+
const ANTIGRAVITY_REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
|
8801
|
+
/**
|
|
8802
|
+
* Endpoint fallback order for generation, mirroring the reference (daily
|
|
8803
|
+
* sandbox first, then autopush, then prod).
|
|
8804
|
+
*/
|
|
8805
|
+
const ANTIGRAVITY_ENDPOINTS = [
|
|
8806
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
8807
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
|
8808
|
+
"https://cloudcode-pa.googleapis.com"
|
|
8809
|
+
];
|
|
8810
|
+
/** loadCodeAssist is best supported on prod; discovery tries prod first. */
|
|
8811
|
+
const LOAD_ENDPOINTS = [
|
|
8812
|
+
"https://cloudcode-pa.googleapis.com",
|
|
8813
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
8814
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com"
|
|
8815
|
+
];
|
|
8816
|
+
/** The image model served through this channel: Nano Banana Pro. */
|
|
8817
|
+
const ANTIGRAVITY_IMAGE_MODEL = "gemini-3-pro-image";
|
|
8818
|
+
/**
|
|
8819
|
+
* Antigravity client version carried in the browser User-Agent. Google's
|
|
8820
|
+
* backend routes by this: a non-Antigravity or outdated UA lands in the
|
|
8821
|
+
* enterprise-license check and fails with the misleading 403 #3501
|
|
8822
|
+
* "You do not have a valid license of this product". The official
|
|
8823
|
+
* auto-updater publishes the current version; changelog scrape and the
|
|
8824
|
+
* hardcoded fallback keep the header sane when the updater is unreachable
|
|
8825
|
+
* (same strategy as the reference implementation's version.js).
|
|
8826
|
+
*/
|
|
8827
|
+
const ANTIGRAVITY_VERSION_FALLBACK = "2.0.6";
|
|
8828
|
+
const VERSION_URL = "https://antigravity-auto-updater-974169037036.us-central1.run.app";
|
|
8829
|
+
const CHANGELOG_URL = "https://antigravity.google/changelog";
|
|
8830
|
+
const VERSION_FETCH_TIMEOUT_MS = 5e3;
|
|
8831
|
+
const CHANGELOG_SCAN_CHARS = 5e3;
|
|
8832
|
+
const VERSION_REGEX = /\d+\.\d+\.\d+/;
|
|
8833
|
+
let antigravityVersion;
|
|
8834
|
+
/** Current client version: the fetched one, or the fallback until fetched. */
|
|
8835
|
+
function getAntigravityVersion() {
|
|
8836
|
+
return antigravityVersion ?? ANTIGRAVITY_VERSION_FALLBACK;
|
|
8837
|
+
}
|
|
8838
|
+
/** Fetch and cache the current version once; failures keep the fallback. */
|
|
8839
|
+
async function initAntigravityVersion() {
|
|
8840
|
+
if (antigravityVersion !== void 0) return;
|
|
8841
|
+
const parsed = await fetchVersionText(VERSION_URL) ?? await fetchVersionText(CHANGELOG_URL, CHANGELOG_SCAN_CHARS);
|
|
8842
|
+
if (parsed !== void 0) antigravityVersion = parsed;
|
|
8843
|
+
}
|
|
8844
|
+
async function fetchVersionText(url, maxChars) {
|
|
8845
|
+
try {
|
|
8846
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(VERSION_FETCH_TIMEOUT_MS) });
|
|
8847
|
+
if (!response.ok) return void 0;
|
|
8848
|
+
let text = await response.text();
|
|
8849
|
+
if (maxChars !== void 0) text = text.slice(0, maxChars);
|
|
8850
|
+
const match = text.match(VERSION_REGEX);
|
|
8851
|
+
return match === null ? void 0 : match[0];
|
|
8852
|
+
} catch {
|
|
8853
|
+
return;
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
/** Hardcoded fallback project when Antigravity returns none (workspace accounts). */
|
|
8857
|
+
const DEFAULT_PROJECT_ID = "rising-fact-p41fc";
|
|
8858
|
+
/** Generation timeout mirroring the reference implementation. */
|
|
8859
|
+
const IMAGE_TIMEOUT_MS = 12e4;
|
|
8860
|
+
/** Aspect ratios the generationConfig.imageConfig accepts. */
|
|
8861
|
+
const ASPECT_RATIOS = [
|
|
8862
|
+
"1:1",
|
|
8863
|
+
"16:9",
|
|
8864
|
+
"9:16",
|
|
8865
|
+
"4:3",
|
|
8866
|
+
"3:4",
|
|
8867
|
+
"21:9"
|
|
8868
|
+
];
|
|
8869
|
+
/** Safety settings the image endpoint requires to not block benign prompts. */
|
|
8870
|
+
const SAFETY_SETTINGS_OFF = [
|
|
8871
|
+
{
|
|
8872
|
+
category: "HARM_CATEGORY_HARASSMENT",
|
|
8873
|
+
threshold: "OFF"
|
|
8874
|
+
},
|
|
8875
|
+
{
|
|
8876
|
+
category: "HARM_CATEGORY_HATE_SPEECH",
|
|
8877
|
+
threshold: "OFF"
|
|
8878
|
+
},
|
|
8879
|
+
{
|
|
8880
|
+
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
8881
|
+
threshold: "OFF"
|
|
8882
|
+
},
|
|
8883
|
+
{
|
|
8884
|
+
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
8885
|
+
threshold: "OFF"
|
|
8886
|
+
},
|
|
8887
|
+
{
|
|
8888
|
+
category: "HARM_CATEGORY_CIVIC_INTEGRITY",
|
|
8889
|
+
threshold: "OFF"
|
|
8890
|
+
}
|
|
8891
|
+
];
|
|
8892
|
+
function antigravityConfig() {
|
|
8893
|
+
return {
|
|
8894
|
+
clientId: ANTIGRAVITY_CLIENT_ID,
|
|
8895
|
+
clientSecret: ANTIGRAVITY_CLIENT_SECRET,
|
|
8896
|
+
redirectUri: ANTIGRAVITY_REDIRECT_URI
|
|
8897
|
+
};
|
|
8898
|
+
}
|
|
8899
|
+
function antigravityAuthorizeUrl(cfg, pkce) {
|
|
8900
|
+
return buildAuthorizeUrl({
|
|
8901
|
+
authUrl: AUTH,
|
|
8902
|
+
clientId: cfg.clientId,
|
|
8903
|
+
redirectUri: cfg.redirectUri,
|
|
8904
|
+
challenge: pkce.challenge,
|
|
8905
|
+
state: pkce.state,
|
|
8906
|
+
scope: SCOPE,
|
|
8907
|
+
extra: {
|
|
8908
|
+
access_type: "offline",
|
|
8909
|
+
prompt: "consent"
|
|
8910
|
+
}
|
|
8911
|
+
});
|
|
8912
|
+
}
|
|
8913
|
+
/**
|
|
8914
|
+
* Headers for project discovery (loadCodeAssist): the reference keeps the
|
|
8915
|
+
* plain SDK identity here, mirroring its project.js loadHeaders.
|
|
8916
|
+
*/
|
|
8917
|
+
function loadHeaders(projectId) {
|
|
8918
|
+
const platform = process.platform === "win32" ? "WINDOWS" : process.platform === "darwin" ? "MACOS" : "LINUX";
|
|
8919
|
+
return {
|
|
8920
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
8921
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
8922
|
+
"Client-Metadata": JSON.stringify({
|
|
8923
|
+
ideType: "ANTIGRAVITY",
|
|
8924
|
+
platform,
|
|
8925
|
+
pluginType: "GEMINI",
|
|
8926
|
+
...projectId.length > 0 ? { duetProject: projectId } : {}
|
|
8927
|
+
})
|
|
8928
|
+
};
|
|
8929
|
+
}
|
|
8930
|
+
/**
|
|
8931
|
+
* Headers for content requests (image generation): the full Electron
|
|
8932
|
+
* browser User-Agent with the Antigravity client version, matching the
|
|
8933
|
+
* official IDE. This is the identity the license check keys on — the plain
|
|
8934
|
+
* SDK UA here is what produced 403 #3501 on personal accounts.
|
|
8935
|
+
*/
|
|
8936
|
+
function contentHeaders(projectId) {
|
|
8937
|
+
const uaPlatform = process.platform === "darwin" ? "Macintosh; Intel Mac OS X 10_15_7" : "Windows NT 10.0; Win64; x64";
|
|
8938
|
+
const platform = process.platform === "win32" ? "WINDOWS" : process.platform === "darwin" ? "MACOS" : "LINUX";
|
|
8939
|
+
return {
|
|
8940
|
+
"User-Agent": `Mozilla/5.0 (${uaPlatform}) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${getAntigravityVersion()} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`,
|
|
8941
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
8942
|
+
"Client-Metadata": JSON.stringify({
|
|
8943
|
+
ideType: "ANTIGRAVITY",
|
|
8944
|
+
platform,
|
|
8945
|
+
pluginType: "GEMINI",
|
|
8946
|
+
...projectId.length > 0 ? { duetProject: projectId } : {}
|
|
8947
|
+
})
|
|
8948
|
+
};
|
|
8949
|
+
}
|
|
8950
|
+
function tokenBlobFromOAuth(json) {
|
|
8951
|
+
const access = json.access_token;
|
|
8952
|
+
const refresh = json.refresh_token;
|
|
8953
|
+
const idToken = typeof json.id_token === "string" ? json.id_token : "";
|
|
8954
|
+
return {
|
|
8955
|
+
accessToken: typeof access === "string" ? access : "",
|
|
8956
|
+
refreshToken: typeof refresh === "string" ? refresh : "",
|
|
8957
|
+
expiresAt: Date.now() + (Number(json.expires_in) || 3600) * 1e3,
|
|
8958
|
+
label: "",
|
|
8959
|
+
email: emailFromIdToken(idToken),
|
|
8960
|
+
accountId: ""
|
|
8961
|
+
};
|
|
8962
|
+
}
|
|
8963
|
+
/** Decode the id_token's JWT payload for the account email (identity only). */
|
|
8964
|
+
function emailFromIdToken(idToken) {
|
|
8965
|
+
const parts = idToken.split(".");
|
|
8966
|
+
if (parts.length < 2) return "";
|
|
8967
|
+
try {
|
|
8968
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
8969
|
+
return typeof payload.email === "string" ? payload.email : "";
|
|
8970
|
+
} catch {
|
|
8971
|
+
return "";
|
|
8972
|
+
}
|
|
8973
|
+
}
|
|
8974
|
+
async function antigravityExchangeCode(cfg, pkce, code) {
|
|
8975
|
+
const blob = tokenBlobFromOAuth(await formTokenRequest(TOKEN, {
|
|
8976
|
+
client_id: cfg.clientId,
|
|
8977
|
+
client_secret: cfg.clientSecret,
|
|
8978
|
+
grant_type: "authorization_code",
|
|
8979
|
+
code,
|
|
8980
|
+
redirect_uri: cfg.redirectUri,
|
|
8981
|
+
code_verifier: pkce.verifier
|
|
8982
|
+
}, fetch));
|
|
8983
|
+
if (blob.accessToken.length === 0) throw new Error("Antigravity token endpoint returned no access token");
|
|
8984
|
+
return blob;
|
|
8985
|
+
}
|
|
8986
|
+
async function antigravityRefresh(blob) {
|
|
8987
|
+
const next = tokenBlobFromOAuth(await formTokenRequest(TOKEN, {
|
|
8988
|
+
client_id: ANTIGRAVITY_CLIENT_ID,
|
|
8989
|
+
client_secret: ANTIGRAVITY_CLIENT_SECRET,
|
|
8990
|
+
grant_type: "refresh_token",
|
|
8991
|
+
refresh_token: blob.refreshToken
|
|
8992
|
+
}, fetch));
|
|
8993
|
+
return {
|
|
8994
|
+
...next,
|
|
8995
|
+
refreshToken: next.refreshToken.length > 0 ? next.refreshToken : blob.refreshToken,
|
|
8996
|
+
label: blob.label,
|
|
8997
|
+
email: blob.email.length > 0 ? blob.email : next.email,
|
|
8998
|
+
accountId: ""
|
|
8999
|
+
};
|
|
9000
|
+
}
|
|
9001
|
+
/**
|
|
9002
|
+
* Resolve the managed project id for the logged-in account. Most accounts
|
|
9003
|
+
* have one; when loadCodeAssist reports none, the hardcoded fallback keeps
|
|
9004
|
+
* generation working (mirrors the reference's ensureProjectContext).
|
|
9005
|
+
* Returns '' only when every endpoint fails, letting generate() throw then.
|
|
9006
|
+
*/
|
|
9007
|
+
async function antigravityResolveProject(blob) {
|
|
9008
|
+
const metadata = {
|
|
9009
|
+
ideType: "ANTIGRAVITY",
|
|
9010
|
+
platform: platformOf(),
|
|
9011
|
+
pluginType: "GEMINI"
|
|
9012
|
+
};
|
|
9013
|
+
for (const base of LOAD_ENDPOINTS) try {
|
|
9014
|
+
const response = await fetch(`${base}/v1internal:loadCodeAssist`, {
|
|
9015
|
+
method: "POST",
|
|
9016
|
+
headers: {
|
|
9017
|
+
"Content-Type": "application/json",
|
|
9018
|
+
Authorization: `Bearer ${blob.accessToken}`,
|
|
9019
|
+
...loadHeaders("")
|
|
9020
|
+
},
|
|
9021
|
+
body: JSON.stringify({ metadata }),
|
|
9022
|
+
signal: AbortSignal.timeout(25e3)
|
|
9023
|
+
});
|
|
9024
|
+
if (!response.ok) continue;
|
|
9025
|
+
const payload = await response.json().catch(() => null);
|
|
9026
|
+
if (payload === null) continue;
|
|
9027
|
+
const project = payload.cloudaicompanionProject;
|
|
9028
|
+
const id = typeof project === "string" ? project : typeof project === "object" && project !== null && typeof project.id === "string" ? project.id : "";
|
|
9029
|
+
if (id.length > 0) return id;
|
|
9030
|
+
} catch {}
|
|
9031
|
+
return DEFAULT_PROJECT_ID;
|
|
9032
|
+
}
|
|
9033
|
+
function platformOf() {
|
|
9034
|
+
if (process.platform === "win32") return "WINDOWS";
|
|
9035
|
+
if (process.platform === "darwin") return "MACOS";
|
|
9036
|
+
return "LINUX";
|
|
9037
|
+
}
|
|
9038
|
+
/**
|
|
9039
|
+
* Build the streamGenerateContent request body for one image generation.
|
|
9040
|
+
* Ported from the reference buildImageRequest + the wrapping envelope: no
|
|
9041
|
+
* tools, no systemInstruction, no thinkingConfig; responseModalities asks
|
|
9042
|
+
* for TEXT+IMAGE.
|
|
9043
|
+
*/
|
|
9044
|
+
function antigravityImageBody(options) {
|
|
9045
|
+
const imageConfig = { aspectRatio: options.aspectRatio !== void 0 && ASPECT_RATIOS.includes(options.aspectRatio) ? options.aspectRatio : "1:1" };
|
|
9046
|
+
if (options.hd === true) imageConfig.imageSize = "4K";
|
|
9047
|
+
return {
|
|
9048
|
+
contents: [{
|
|
9049
|
+
role: "user",
|
|
9050
|
+
parts: [{ text: options.prompt }]
|
|
9051
|
+
}],
|
|
9052
|
+
generationConfig: {
|
|
9053
|
+
candidateCount: 1,
|
|
9054
|
+
imageConfig,
|
|
9055
|
+
responseModalities: ["TEXT", "IMAGE"]
|
|
9056
|
+
},
|
|
9057
|
+
safetySettings: SAFETY_SETTINGS_OFF
|
|
9058
|
+
};
|
|
9059
|
+
}
|
|
9060
|
+
/** Wrap the inner request in the Antigravity agent envelope. */
|
|
9061
|
+
function antigravityEnvelope(projectId, request) {
|
|
9062
|
+
return {
|
|
9063
|
+
project: projectId,
|
|
9064
|
+
model: ANTIGRAVITY_IMAGE_MODEL,
|
|
9065
|
+
userAgent: "antigravity",
|
|
9066
|
+
requestId: `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`,
|
|
9067
|
+
requestType: "agent",
|
|
9068
|
+
request: {
|
|
9069
|
+
...request,
|
|
9070
|
+
session_id: `sess-${randomUUID()}`
|
|
9071
|
+
}
|
|
9072
|
+
};
|
|
9073
|
+
}
|
|
9074
|
+
/**
|
|
9075
|
+
* Generate one image on the logged-in Antigravity account: resolve the
|
|
9076
|
+
* project, POST the wrapped body to each endpoint in fallback order, and
|
|
9077
|
+
* parse the SSE stream for the inlineData image part.
|
|
9078
|
+
*/
|
|
9079
|
+
async function antigravityGenerateImage(options) {
|
|
9080
|
+
await initAntigravityVersion();
|
|
9081
|
+
const inner = antigravityImageBody({
|
|
9082
|
+
prompt: options.prompt,
|
|
9083
|
+
...options.aspectRatio !== void 0 ? { aspectRatio: options.aspectRatio } : {},
|
|
9084
|
+
...options.hd !== void 0 ? { hd: options.hd } : {}
|
|
9085
|
+
});
|
|
9086
|
+
const body = antigravityEnvelope(options.projectId, inner);
|
|
9087
|
+
let lastError = "no endpoint succeeded";
|
|
9088
|
+
for (const base of ANTIGRAVITY_ENDPOINTS) {
|
|
9089
|
+
const url = `${base}/v1internal:streamGenerateContent?alt=sse`;
|
|
9090
|
+
const timeoutSignal = AbortSignal.timeout(IMAGE_TIMEOUT_MS);
|
|
9091
|
+
const signal = options.signal !== void 0 ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
9092
|
+
try {
|
|
9093
|
+
const response = await fetch(url, {
|
|
9094
|
+
method: "POST",
|
|
9095
|
+
headers: {
|
|
9096
|
+
...contentHeaders(options.projectId),
|
|
9097
|
+
Authorization: `Bearer ${options.blob.accessToken}`,
|
|
9098
|
+
"Content-Type": "application/json",
|
|
9099
|
+
Accept: "text/event-stream"
|
|
9100
|
+
},
|
|
9101
|
+
body: JSON.stringify(body),
|
|
9102
|
+
signal
|
|
9103
|
+
});
|
|
9104
|
+
if (!response.ok) {
|
|
9105
|
+
const text = await response.text().catch(() => "");
|
|
9106
|
+
lastError = `HTTP ${String(response.status)}: ${text.slice(0, 200)}`;
|
|
9107
|
+
if (response.status === 403 || response.status === 404 || response.status === 429 || response.status >= 500) continue;
|
|
9108
|
+
throw new Error(`antigravity ${lastError}`);
|
|
9109
|
+
}
|
|
9110
|
+
const text = await response.text();
|
|
9111
|
+
const parsed = parseSseImage(text);
|
|
9112
|
+
if (parsed !== void 0) return parsed;
|
|
9113
|
+
const reason = parseSseError(text);
|
|
9114
|
+
lastError = reason !== void 0 ? reason : "response contained no image";
|
|
9115
|
+
continue;
|
|
9116
|
+
} catch (error) {
|
|
9117
|
+
if (options.signal?.aborted === true) throw error;
|
|
9118
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
9119
|
+
continue;
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
9122
|
+
throw new Error(`antigravity image generation failed: ${lastError}`);
|
|
9123
|
+
}
|
|
9124
|
+
/** Scan an SSE body for the first image part in any data: line. */
|
|
9125
|
+
function parseSseImage(text) {
|
|
9126
|
+
for (const line of text.split("\n")) {
|
|
9127
|
+
if (!line.startsWith("data: ")) continue;
|
|
9128
|
+
const raw = line.slice(6).trim();
|
|
9129
|
+
if (raw.length === 0 || raw === "[DONE]") continue;
|
|
9130
|
+
try {
|
|
9131
|
+
const data = JSON.parse(raw);
|
|
9132
|
+
for (const candidate of data.response?.candidates ?? []) for (const part of candidate.content?.parts ?? []) {
|
|
9133
|
+
const inline = part.inlineData;
|
|
9134
|
+
if (typeof inline?.data === "string" && inline.data.length > 0 && typeof inline.mimeType === "string" && inline.mimeType.startsWith("image/")) return {
|
|
9135
|
+
b64: inline.data,
|
|
9136
|
+
mimeType: inline.mimeType
|
|
9137
|
+
};
|
|
9138
|
+
}
|
|
9139
|
+
} catch {}
|
|
9140
|
+
}
|
|
9141
|
+
}
|
|
9142
|
+
/** Pull the first error block an SSE body reports, for better messages. */
|
|
9143
|
+
function parseSseError(text) {
|
|
9144
|
+
for (const line of text.split("\n")) {
|
|
9145
|
+
if (!line.startsWith("data: ")) continue;
|
|
9146
|
+
const raw = line.slice(6).trim();
|
|
9147
|
+
if (raw.length === 0 || raw === "[DONE]") continue;
|
|
9148
|
+
try {
|
|
9149
|
+
const data = JSON.parse(raw);
|
|
9150
|
+
if (data.error !== void 0) {
|
|
9151
|
+
const code = typeof data.error.code === "string" || typeof data.error.code === "number" ? String(data.error.code) : "";
|
|
9152
|
+
const message = typeof data.error.message === "string" ? data.error.message : "";
|
|
9153
|
+
const joined = `${code.length > 0 ? `${code}: ` : ""}${message}`.trim();
|
|
9154
|
+
if (joined.length > 0) return joined;
|
|
9155
|
+
}
|
|
9156
|
+
} catch {}
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
//#endregion
|
|
9160
|
+
//#region lib/types/subscription/manager.js
|
|
9161
|
+
/**
|
|
9162
|
+
* Subscription account manager: login flows, blob storage through the DSH
|
|
9163
|
+
* Credentials service, token refresh with per-account locking, and image
|
|
9164
|
+
* generation on the logged-in account.
|
|
9165
|
+
*
|
|
9166
|
+
* The account store pattern follows @goodandready/dsh-subscriptions (MIT,
|
|
9167
|
+
* (c) 2026 GooDAnDReaDY) lib/accounts.js, trimmed to one account per vendor
|
|
9168
|
+
* (no rotation, no cooldowns) because this bundle has exactly two vendors
|
|
9169
|
+
* and no chat adapter to protect.
|
|
9170
|
+
*
|
|
9171
|
+
* Isolation invariants (project rules):
|
|
9172
|
+
* - OAuth blobs live under SUBSCRIPTION_OAUTH_* refs, never the API-key refs.
|
|
9173
|
+
* - Login/logout never touches any other setting or credential.
|
|
9174
|
+
* - The browser side never sees a token; it sees the authorize URL and
|
|
9175
|
+
* status words only. Generation happens host-side, mirroring every other
|
|
9176
|
+
* adapter in this bundle.
|
|
9177
|
+
*/
|
|
9178
|
+
/** The vendor each dsh-image-gen subscription provider maps onto. */
|
|
9179
|
+
function vendorOf(provider) {
|
|
9180
|
+
if (provider === "chatgpt-sub") return "codex";
|
|
9181
|
+
if (provider === "google-sub") return "antigravity";
|
|
9182
|
+
return "grok";
|
|
9183
|
+
}
|
|
9184
|
+
/** Credential ref one vendor's OAuth blob is stored under (index fixed at 1). */
|
|
9185
|
+
function subscriptionOauthRef(vendor) {
|
|
9186
|
+
return credentialRef(`${vendor.toUpperCase()}_OAUTH_1`);
|
|
9187
|
+
}
|
|
9188
|
+
/**
|
|
9189
|
+
* Per-application manager instance. Constructed once in the plugin entry,
|
|
9190
|
+
* owns the pending-login map (PKCE state to login session) and the refresh
|
|
9191
|
+
* locks; nothing here is persisted except the blobs themselves.
|
|
9192
|
+
*/
|
|
9193
|
+
/** Provider names by vendor for login-result pages and errors. */
|
|
9194
|
+
function providerNameOf(vendor) {
|
|
9195
|
+
return SUBSCRIPTION_PROVIDER_DISPLAY_NAMES[vendor === "codex" ? "chatgpt-sub" : vendor === "antigravity" ? "google-sub" : "grok-sub"];
|
|
9196
|
+
}
|
|
9197
|
+
var SubscriptionManager = class {
|
|
9198
|
+
ctx;
|
|
9199
|
+
pending = /* @__PURE__ */ new Map();
|
|
9200
|
+
refreshLocks = /* @__PURE__ */ new Map();
|
|
9201
|
+
/** Cheap in-memory expiry cache so badges do not hit credentials on read. */
|
|
9202
|
+
blobCache = /* @__PURE__ */ new Map();
|
|
9203
|
+
/** Antigravity needs a per-account project id on every generation call. */
|
|
9204
|
+
projectCache = /* @__PURE__ */ new Map();
|
|
9205
|
+
constructor(ctx) {
|
|
9206
|
+
this.ctx = ctx;
|
|
9207
|
+
}
|
|
9208
|
+
/** Read a vendor's stored blob, or undefined when signed out. */
|
|
9209
|
+
async readBlob(vendor) {
|
|
9210
|
+
try {
|
|
9211
|
+
const raw = (await this.ctx.credentials.resolve(subscriptionOauthRef(vendor)))?.value ?? "";
|
|
9212
|
+
if (raw.trim().length === 0) return void 0;
|
|
9213
|
+
return parseBlob(raw);
|
|
9214
|
+
} catch {
|
|
9215
|
+
return;
|
|
9216
|
+
}
|
|
9217
|
+
}
|
|
9218
|
+
/** Login status for the settings card; never throws. */
|
|
9219
|
+
async loginStatus(vendor) {
|
|
9220
|
+
const blob = await this.readBlob(vendor);
|
|
9221
|
+
if (blob === void 0) return { state: "logged-out" };
|
|
9222
|
+
return {
|
|
9223
|
+
state: "logged-in",
|
|
9224
|
+
email: blob.email
|
|
9225
|
+
};
|
|
9226
|
+
}
|
|
9227
|
+
/**
|
|
9228
|
+
* Begin a login: registers PKCE state, starts the loopback catch server on
|
|
9229
|
+
* the vendor-fixed redirect port, and returns the authorize URL for the
|
|
9230
|
+
* browser to open. Completion lands in the loopback callback.
|
|
9231
|
+
*/
|
|
9232
|
+
async beginLogin(vendor) {
|
|
9233
|
+
const redirectUri = vendor === "codex" ? CODEX_REDIRECT_URI : vendor === "antigravity" ? ANTIGRAVITY_REDIRECT_URI : GROK_REDIRECT_URI;
|
|
9234
|
+
const pkce = await createPkce();
|
|
9235
|
+
this.pending.set(pkce.state, {
|
|
9236
|
+
vendor,
|
|
9237
|
+
pkce
|
|
9238
|
+
});
|
|
9239
|
+
const url = vendor === "codex" ? codexAuthorizeUrl(codexConfig(), pkce) : vendor === "antigravity" ? antigravityAuthorizeUrl(antigravityConfig(), pkce) : grokAuthorizeUrl(grokConfig(), pkce);
|
|
9240
|
+
startLoopback({
|
|
9241
|
+
redirectUri,
|
|
9242
|
+
onCode: async (params) => await this.completeLoginFromCallback(vendor, params.get("code") ?? "", params.get("state") ?? "")
|
|
9243
|
+
}).catch(() => {});
|
|
9244
|
+
return { url };
|
|
9245
|
+
}
|
|
9246
|
+
/** Loopback callback: validate state, exchange the code, store the blob. */
|
|
9247
|
+
async completeLoginFromCallback(vendor, code, state) {
|
|
9248
|
+
if (code.length === 0) throw new Error("callback carried no authorization code");
|
|
9249
|
+
const row = this.pending.get(state);
|
|
9250
|
+
if (row === void 0 || row.vendor !== vendor) throw new Error("login session expired; start again");
|
|
9251
|
+
this.pending.delete(state);
|
|
9252
|
+
const blob = vendor === "codex" ? await codexExchangeCode(codexConfig(), row.pkce, code) : vendor === "antigravity" ? await antigravityExchangeCode(antigravityConfig(), row.pkce, code) : await grokExchangeCode(grokConfig(), row.pkce, code);
|
|
9253
|
+
await this.saveBlob(vendor, blob);
|
|
9254
|
+
return `<!doctype html><meta charset="utf-8"><title>dsh-image-gen</title><p>${providerNameOf(vendor)} 登录成功(${escapeHtml(blob.email)}),可以关闭此页返回设置。</p>`;
|
|
9255
|
+
}
|
|
9256
|
+
/** Sign out: clear the blob and the caches. No other setting changes. */
|
|
9257
|
+
async logout(vendor) {
|
|
9258
|
+
await this.ctx.credentials.unset(subscriptionOauthRef(vendor));
|
|
9259
|
+
this.blobCache.delete(vendor);
|
|
9260
|
+
this.projectCache.clear();
|
|
9261
|
+
}
|
|
9262
|
+
async saveBlob(vendor, blob) {
|
|
9263
|
+
await this.ctx.credentials.set(subscriptionOauthRef(vendor), serializeBlob(blob));
|
|
9264
|
+
this.blobCache.set(vendor, blob);
|
|
9265
|
+
}
|
|
9266
|
+
/**
|
|
9267
|
+
* Return a blob whose access token is usable; refreshes first when the
|
|
9268
|
+
* stored one is expired (or expiring within the skew window). Single-flight
|
|
9269
|
+
* per vendor so concurrent generate calls share one refresh.
|
|
9270
|
+
*/
|
|
9271
|
+
async ensureFresh(vendor) {
|
|
9272
|
+
const blob = await this.readBlob(vendor);
|
|
9273
|
+
if (blob === void 0) throw notLoggedInError(vendor);
|
|
9274
|
+
if (blob.refreshToken.length === 0) return blob;
|
|
9275
|
+
if (blob.expiresAt !== 0 && blob.expiresAt - 6e4 > Date.now()) return blob;
|
|
9276
|
+
const inflight = this.refreshLocks.get(vendor);
|
|
9277
|
+
if (inflight !== void 0) return inflight;
|
|
9278
|
+
const refresh = (async () => {
|
|
9279
|
+
const next = vendor === "codex" ? await codexRefresh(blob) : vendor === "antigravity" ? await antigravityRefresh(blob) : await grokRefresh(blob);
|
|
9280
|
+
const merged = {
|
|
9281
|
+
...next,
|
|
9282
|
+
refreshToken: next.refreshToken.length > 0 ? next.refreshToken : blob.refreshToken,
|
|
9283
|
+
accountId: next.accountId.length > 0 ? next.accountId : blob.accountId
|
|
9284
|
+
};
|
|
9285
|
+
await this.saveBlob(vendor, merged);
|
|
9286
|
+
return merged;
|
|
9287
|
+
})().finally(() => {
|
|
9288
|
+
this.refreshLocks.delete(vendor);
|
|
9289
|
+
});
|
|
9290
|
+
this.refreshLocks.set(vendor, refresh);
|
|
9291
|
+
return refresh;
|
|
9292
|
+
}
|
|
9293
|
+
/** Generate one image through the logged-in account. b64 reply decoded host-side. */
|
|
9294
|
+
async generate(options) {
|
|
9295
|
+
const { vendor, prompt } = options;
|
|
9296
|
+
const session = await this.ensureFresh(vendor);
|
|
9297
|
+
const text = prompt.trim();
|
|
9298
|
+
if (text.length === 0) throw new Error("prompt must be a non-empty string");
|
|
9299
|
+
if (vendor === "antigravity") {
|
|
9300
|
+
const projectId = await this.ensureAntigravityProject(session);
|
|
9301
|
+
const aspectRatio = antigravityAspectRatioOf(options.size);
|
|
9302
|
+
return [{ b64_json: (await antigravityGenerateImage({
|
|
9303
|
+
blob: session,
|
|
9304
|
+
projectId,
|
|
9305
|
+
prompt: text,
|
|
9306
|
+
...aspectRatio !== void 0 ? { aspectRatio } : {},
|
|
9307
|
+
hd: options.quality === "hd" || options.quality === "high",
|
|
9308
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
9309
|
+
})).b64 }];
|
|
9310
|
+
}
|
|
9311
|
+
let url;
|
|
9312
|
+
let headers;
|
|
9313
|
+
let body;
|
|
9314
|
+
if (vendor === "codex") {
|
|
9315
|
+
url = CODEX_IMAGE_URL;
|
|
9316
|
+
headers = codexIdentityHeaders(session);
|
|
9317
|
+
body = {
|
|
9318
|
+
prompt: text,
|
|
9319
|
+
model: CODEX_IMAGE_MODEL,
|
|
9320
|
+
...options.size !== void 0 && options.size.length > 0 ? { size: options.size } : {},
|
|
9321
|
+
...options.quality !== void 0 && options.quality.length > 0 ? { quality: options.quality } : {}
|
|
9322
|
+
};
|
|
9323
|
+
} else {
|
|
9324
|
+
url = GROK_IMAGE_URL;
|
|
9325
|
+
headers = grokIdentityHeaders(session);
|
|
9326
|
+
const aspect = {
|
|
9327
|
+
"1024x1024": "1:1",
|
|
9328
|
+
"1024x1536": "2:3",
|
|
9329
|
+
"1536x1024": "3:2",
|
|
9330
|
+
auto: "auto"
|
|
9331
|
+
};
|
|
9332
|
+
const level = options.quality === "low" ? "low" : options.quality === "medium" || options.quality === "high" ? "medium" : void 0;
|
|
9333
|
+
body = {
|
|
9334
|
+
prompt: text,
|
|
9335
|
+
model: GROK_IMAGE_MODEL,
|
|
9336
|
+
response_format: "b64_json",
|
|
9337
|
+
...options.size !== void 0 && aspect[options.size] !== void 0 ? { aspect_ratio: aspect[options.size] } : {},
|
|
9338
|
+
...level !== void 0 ? { quality: level } : {}
|
|
9339
|
+
};
|
|
9340
|
+
}
|
|
9341
|
+
const response = await fetch(url, {
|
|
9342
|
+
method: "POST",
|
|
9343
|
+
headers,
|
|
9344
|
+
body: JSON.stringify(body),
|
|
9345
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
9346
|
+
});
|
|
9347
|
+
const payload = await response.json().catch(() => ({}));
|
|
9348
|
+
if (!response.ok) {
|
|
9349
|
+
const detail = extractErrorMessage(payload);
|
|
9350
|
+
throw new Error(`${vendor} HTTP ${String(response.status)}${detail !== void 0 ? `: ${detail.slice(0, 200)}` : ""}`);
|
|
9351
|
+
}
|
|
9352
|
+
return parseImages(payload);
|
|
9353
|
+
}
|
|
9354
|
+
/**
|
|
9355
|
+
* Resolve (and cache per refresh token) the managed project id the
|
|
9356
|
+
* Antigravity generation envelope needs. Re-resolved when the account
|
|
9357
|
+
* signs out or a different account logs in.
|
|
9358
|
+
*/
|
|
9359
|
+
async ensureAntigravityProject(blob) {
|
|
9360
|
+
const cacheKey = blob.refreshToken.length > 0 ? blob.refreshToken : blob.accessToken.slice(0, 32);
|
|
9361
|
+
const cached = this.projectCache.get(cacheKey);
|
|
9362
|
+
if (cached !== void 0) return cached;
|
|
9363
|
+
const projectId = await antigravityResolveProject(blob);
|
|
9364
|
+
this.projectCache.set(cacheKey, projectId);
|
|
9365
|
+
return projectId;
|
|
9366
|
+
}
|
|
9367
|
+
};
|
|
9368
|
+
/** Map an OpenAI-style size onto the aspect ratio Antigravity accepts. */
|
|
9369
|
+
function antigravityAspectRatioOf(size) {
|
|
9370
|
+
const table = {
|
|
9371
|
+
"1024x1024": "1:1",
|
|
9372
|
+
"1024x1536": "2:3",
|
|
9373
|
+
"1536x1024": "3:2",
|
|
9374
|
+
"768x1398": "9:16",
|
|
9375
|
+
"1398x768": "16:9",
|
|
9376
|
+
auto: "1:1"
|
|
9377
|
+
};
|
|
9378
|
+
if (size === void 0) return void 0;
|
|
9379
|
+
const mapped = table[size];
|
|
9380
|
+
if (mapped !== void 0) return mapped;
|
|
9381
|
+
return /^\d+:\d+$/.test(size) ? size : "1:1";
|
|
9382
|
+
}
|
|
9383
|
+
/** Response parsing: both vendors reply in the same `{data:[{b64_json}]}` shape. */
|
|
9384
|
+
function parseImages(payload) {
|
|
9385
|
+
const body = typeof payload === "object" && payload !== null ? payload : {};
|
|
9386
|
+
const rows = Array.isArray(body.data) ? body.data : [];
|
|
9387
|
+
const images = [];
|
|
9388
|
+
for (const row of rows) {
|
|
9389
|
+
if (typeof row !== "object" || row === null) continue;
|
|
9390
|
+
const b64 = row.b64_json;
|
|
9391
|
+
if (typeof b64 !== "string" || b64.length === 0) continue;
|
|
9392
|
+
const revised = row.revised_prompt;
|
|
9393
|
+
images.push({
|
|
9394
|
+
b64_json: b64,
|
|
9395
|
+
...typeof revised === "string" && revised.length > 0 ? { revisedPrompt: revised } : {}
|
|
9396
|
+
});
|
|
9397
|
+
}
|
|
9398
|
+
if (images.length === 0) throw new Error("response contains no images");
|
|
9399
|
+
return images;
|
|
9400
|
+
}
|
|
9401
|
+
function extractErrorMessage(payload) {
|
|
9402
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
9403
|
+
const error = payload.error;
|
|
9404
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
9405
|
+
const row = error;
|
|
9406
|
+
if (typeof row.message === "string") return row.message;
|
|
9407
|
+
if (typeof row.code === "string") return row.code;
|
|
9408
|
+
}
|
|
9409
|
+
function notLoggedInError(vendor) {
|
|
9410
|
+
const display = providerNameOf(vendor);
|
|
9411
|
+
return /* @__PURE__ */ new Error(`${display} 未登录:请到 设置 > 插件 > 图像生成 的订阅区完成登录`);
|
|
9412
|
+
}
|
|
9413
|
+
function escapeHtml(value) {
|
|
9414
|
+
return value.replace(/[&<>"']/g, (ch) => ({
|
|
9415
|
+
"&": "&",
|
|
9416
|
+
"<": "<",
|
|
9417
|
+
">": ">",
|
|
9418
|
+
"\"": """,
|
|
9419
|
+
"'": "'"
|
|
9420
|
+
})[ch] ?? ch);
|
|
9421
|
+
}
|
|
9422
|
+
//#endregion
|
|
9423
|
+
//#region lib/types/subscription/subscription-route.js
|
|
9424
|
+
const MAX_BODY_BYTES$1 = 16384;
|
|
9425
|
+
/** The vendor each subscription provider logs in as. */
|
|
9426
|
+
function vendorOfProvider(provider) {
|
|
9427
|
+
if (provider === "chatgpt-sub") return "codex";
|
|
9428
|
+
if (provider === "google-sub") return "antigravity";
|
|
9429
|
+
return "grok";
|
|
9430
|
+
}
|
|
9431
|
+
/** Register the login/status routes on the plugin's web server. */
|
|
9432
|
+
function registerSubscriptionRoutes(ctx, manager) {
|
|
9433
|
+
ctx.effect(() => ctx.webServer.register({
|
|
9434
|
+
kind: "exact",
|
|
9435
|
+
path: SUBSCRIPTION_LOGIN_ROUTE,
|
|
9436
|
+
handler: (req, res) => {
|
|
9437
|
+
serveLogin(req, res, manager);
|
|
9438
|
+
}
|
|
9439
|
+
}), "dsh-image-gen: subscription login route");
|
|
9440
|
+
ctx.effect(() => ctx.webServer.register({
|
|
9441
|
+
kind: "exact",
|
|
9442
|
+
path: SUBSCRIPTION_STATUS_ROUTE,
|
|
9443
|
+
handler: (req, res) => {
|
|
9444
|
+
serveStatus(req, res, manager);
|
|
9445
|
+
}
|
|
9446
|
+
}), "dsh-image-gen: subscription status route");
|
|
9447
|
+
}
|
|
9448
|
+
async function serveLogin(req, res, manager) {
|
|
9449
|
+
if (!sameOrigin$3(req)) return jsonError$3(res, 403, "origin-rejected");
|
|
9450
|
+
if (req.method !== "POST") return jsonError$3(res, 405, "method-not-allowed");
|
|
9451
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$3(res, 415, "json-required");
|
|
9452
|
+
let body;
|
|
9453
|
+
try {
|
|
9454
|
+
body = JSON.parse(await readBody$2(req));
|
|
9455
|
+
} catch {
|
|
9456
|
+
return jsonError$3(res, 400, "invalid-request");
|
|
9457
|
+
}
|
|
9458
|
+
const provider = record$3(body)?.provider;
|
|
9459
|
+
if (typeof provider !== "string" || !SUBSCRIPTION_PROVIDERS.includes(provider)) return jsonError$3(res, 400, "invalid-provider");
|
|
9460
|
+
const action = record$3(body)?.action;
|
|
9461
|
+
const vendor = vendorOfProvider(provider);
|
|
9462
|
+
try {
|
|
9463
|
+
if (action === "logout") {
|
|
9464
|
+
await manager.logout(vendor);
|
|
9465
|
+
return json$3(res, 200, { ok: true });
|
|
9466
|
+
}
|
|
9467
|
+
const { url } = await manager.beginLogin(vendor);
|
|
9468
|
+
json$3(res, 200, {
|
|
9469
|
+
ok: true,
|
|
9470
|
+
url
|
|
9471
|
+
});
|
|
9472
|
+
} catch (error) {
|
|
9473
|
+
json$3(res, 502, {
|
|
9474
|
+
ok: false,
|
|
9475
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9476
|
+
});
|
|
9477
|
+
}
|
|
9478
|
+
}
|
|
9479
|
+
async function serveStatus(req, res, manager) {
|
|
9480
|
+
if (!sameOrigin$3(req)) return jsonError$3(res, 403, "origin-rejected");
|
|
9481
|
+
if (req.method !== "POST") return jsonError$3(res, 405, "method-not-allowed");
|
|
9482
|
+
const statuses = {};
|
|
9483
|
+
for (const provider of SUBSCRIPTION_PROVIDERS) {
|
|
9484
|
+
const status = await manager.loginStatus(vendorOfProvider(provider));
|
|
9485
|
+
statuses[provider] = status.state === "logged-in" ? {
|
|
9486
|
+
state: status.state,
|
|
9487
|
+
email: status.email
|
|
9488
|
+
} : { state: status.state };
|
|
9489
|
+
}
|
|
9490
|
+
json$3(res, 200, {
|
|
9491
|
+
ok: true,
|
|
9492
|
+
statuses
|
|
9493
|
+
});
|
|
9494
|
+
}
|
|
9495
|
+
function sameOrigin$3(req) {
|
|
9496
|
+
const origin = req.headers.origin;
|
|
9497
|
+
const host = req.headers.host;
|
|
9498
|
+
return origin === void 0 || host === void 0 || origin === `http://${host}` || origin === `https://${host}`;
|
|
9499
|
+
}
|
|
9500
|
+
function record$3(value) {
|
|
9501
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
9502
|
+
}
|
|
9503
|
+
async function readBody$2(req) {
|
|
9504
|
+
const chunks = [];
|
|
9505
|
+
let bytes = 0;
|
|
9506
|
+
for await (const chunk of req) {
|
|
9507
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
9508
|
+
bytes += buffer.byteLength;
|
|
9509
|
+
if (bytes > MAX_BODY_BYTES$1) throw new Error("request too large");
|
|
9510
|
+
chunks.push(buffer);
|
|
9511
|
+
}
|
|
9512
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
9513
|
+
}
|
|
9514
|
+
function json$3(res, status, value) {
|
|
9515
|
+
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
9516
|
+
res.writeHead(status, {
|
|
9517
|
+
"content-type": "application/json; charset=utf-8",
|
|
9518
|
+
"cache-control": "no-store"
|
|
9519
|
+
});
|
|
9520
|
+
res.end(JSON.stringify(value));
|
|
9521
|
+
}
|
|
9522
|
+
function jsonError$3(res, status, code) {
|
|
9523
|
+
json$3(res, status, { error: code });
|
|
9524
|
+
}
|
|
9525
|
+
//#endregion
|
|
9526
|
+
//#region lib/types/subscription.js
|
|
9527
|
+
/** Timeout for one subscription image call, mirroring the other adapters. */
|
|
9528
|
+
const SUBSCRIPTION_TIMEOUT_MS = 3e5;
|
|
9529
|
+
/**
|
|
9530
|
+
* Generate one image through a logged-in subscription account. Returns the
|
|
9531
|
+
* same `{ data, mediaType }` contract as the API-key adapters so the caller
|
|
9532
|
+
* can feed it straight into `saveGenerated`.
|
|
9533
|
+
*/
|
|
9534
|
+
async function generateSubscriptionImage(options) {
|
|
9535
|
+
const { manager, provider, prompt, maxBytes, signal } = options;
|
|
9536
|
+
const size = options.size?.trim();
|
|
9537
|
+
const result = await withTimeout(manager.generate({
|
|
9538
|
+
vendor: vendorOf(provider),
|
|
9539
|
+
prompt,
|
|
9540
|
+
...size !== void 0 && size.length > 0 ? { size } : {},
|
|
9541
|
+
signal
|
|
9542
|
+
}), signal);
|
|
9543
|
+
const first = Array.isArray(result) ? result[0] : void 0;
|
|
9544
|
+
if (first === void 0 || typeof first.b64_json !== "string" || first.b64_json.length === 0) throw new Error("Subscription image reply contained no image payload");
|
|
9545
|
+
const bytes = new Uint8Array(Buffer.from(first.b64_json, "base64"));
|
|
9546
|
+
if (bytes.byteLength > maxBytes) throw new Error(`Subscription image is ${formatBytes(bytes.byteLength)}, above this DSH's ${formatBytes(maxBytes)} limit`);
|
|
9547
|
+
const mediaType = detectImageMediaType(bytes);
|
|
9548
|
+
if (mediaType === void 0) throw new Error("Subscription image payload has an unrecognized format");
|
|
9549
|
+
return {
|
|
9550
|
+
data: bytes,
|
|
9551
|
+
mediaType,
|
|
9552
|
+
...typeof first.revisedPrompt === "string" && first.revisedPrompt.length > 0 ? { revisedPrompt: first.revisedPrompt } : {}
|
|
9553
|
+
};
|
|
9554
|
+
}
|
|
9555
|
+
/** Subscription channels are prompt-only today; edit_image is rejected outright. */
|
|
9556
|
+
function assertSubscriptionEditUnsupported(provider) {
|
|
9557
|
+
throw new Error(`edit_image with ${SUBSCRIPTION_PROVIDER_DISPLAY_NAMES[provider]} is not supported yet: the subscription channel only accepts text prompts. Retry with an API-key provider (for example google or openai) through the provider argument.`);
|
|
9558
|
+
}
|
|
9559
|
+
/** Run one promise with the subscription timeout and the call's abort signal. */
|
|
9560
|
+
async function withTimeout(promise, signal) {
|
|
9561
|
+
let timer;
|
|
9562
|
+
let rejectCall;
|
|
9563
|
+
const onAbort = () => {
|
|
9564
|
+
rejectCall(/* @__PURE__ */ new Error("Subscription image generation aborted"));
|
|
9565
|
+
};
|
|
9566
|
+
const guarded = new Promise((resolve, reject) => {
|
|
9567
|
+
rejectCall = reject;
|
|
9568
|
+
timer = setTimeout(() => {
|
|
9569
|
+
reject(/* @__PURE__ */ new Error(`Subscription image generation timed out after ${String(Math.round(SUBSCRIPTION_TIMEOUT_MS / 1e3))}s`));
|
|
9570
|
+
}, SUBSCRIPTION_TIMEOUT_MS);
|
|
9571
|
+
promise.then(resolve, reject);
|
|
9572
|
+
});
|
|
9573
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
9574
|
+
try {
|
|
9575
|
+
return await guarded;
|
|
9576
|
+
} finally {
|
|
9577
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
9578
|
+
signal.removeEventListener("abort", onAbort);
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
function formatBytes(bytes) {
|
|
9582
|
+
if (bytes >= 1048576) return `${String(Math.round(bytes / 1048576 * 10) / 10)}MB`;
|
|
9583
|
+
return `${String(Math.round(bytes / 1024))}KB`;
|
|
9584
|
+
}
|
|
9585
|
+
//#endregion
|
|
8316
9586
|
//#region lib/types/inspiration/data/awesome-gpt-image-2.json
|
|
8317
9587
|
var awesome_gpt_image_2_default = {
|
|
8318
9588
|
repository: "https://github.com/freestylefly/awesome-gpt-image-2",
|
|
@@ -19214,6 +20484,8 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
19214
20484
|
assertAllowed(studioProfile(config, input.provider, true), input);
|
|
19215
20485
|
const active = resolveProvider(withProviderOverrides(config, input.provider, input.model));
|
|
19216
20486
|
if (active.provider === "comfyui") throw new Error("ComfyUI 暂未接入工作台");
|
|
20487
|
+
if (isSubscriptionProvider(active.provider)) throw new Error("订阅生图暂未接入工作台,请通过对话或设置切换默认 Provider 使用");
|
|
20488
|
+
const wired = active;
|
|
19217
20489
|
const credential = await requireApiKey(ctx, input.provider);
|
|
19218
20490
|
const rawRefs = input.references ?? (input.reference ? [input.reference] : []);
|
|
19219
20491
|
if (input.mode === "edit" && rawRefs.length === 0) throw new Error("图生图需要至少一张参考图");
|
|
@@ -19223,13 +20495,13 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
19223
20495
|
const generateSingle = async (index) => {
|
|
19224
20496
|
let generated;
|
|
19225
20497
|
let output;
|
|
19226
|
-
if (
|
|
20498
|
+
if (wired.provider === "google") {
|
|
19227
20499
|
const aspectRatio = input.ratio;
|
|
19228
20500
|
const imageSize = input.quality;
|
|
19229
20501
|
generated = input.mode === "edit" ? await editGoogleImage({
|
|
19230
20502
|
apiKey: credential,
|
|
19231
|
-
endpoint:
|
|
19232
|
-
model:
|
|
20503
|
+
endpoint: wired.endpoint,
|
|
20504
|
+
model: wired.model,
|
|
19233
20505
|
prompt: input.prompt,
|
|
19234
20506
|
sourceImages,
|
|
19235
20507
|
aspectRatio,
|
|
@@ -19238,8 +20510,8 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
19238
20510
|
signal
|
|
19239
20511
|
}) : await generateGoogleImage({
|
|
19240
20512
|
apiKey: credential,
|
|
19241
|
-
endpoint:
|
|
19242
|
-
model:
|
|
20513
|
+
endpoint: wired.endpoint,
|
|
20514
|
+
model: wired.model,
|
|
19243
20515
|
prompt: input.prompt,
|
|
19244
20516
|
aspectRatio,
|
|
19245
20517
|
imageSize,
|
|
@@ -19247,34 +20519,38 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
19247
20519
|
signal
|
|
19248
20520
|
});
|
|
19249
20521
|
output = `${aspectRatio}, ${imageSize}`;
|
|
19250
|
-
} else if (
|
|
20522
|
+
} else if (wired.provider === "openai" || wired.provider === "openai-compat" || wired.provider === "xai" || wired.provider === "zhipu") {
|
|
19251
20523
|
const size = openAISize(input.ratio);
|
|
19252
20524
|
generated = input.mode === "edit" ? await editOpenAICompatibleImage({
|
|
19253
20525
|
apiKey: credential,
|
|
19254
|
-
baseURL:
|
|
19255
|
-
model:
|
|
20526
|
+
baseURL: wired.baseURL,
|
|
20527
|
+
model: wired.model,
|
|
19256
20528
|
prompt: input.prompt,
|
|
19257
20529
|
sourceImages,
|
|
19258
20530
|
size,
|
|
19259
20531
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
19260
|
-
signal
|
|
20532
|
+
signal,
|
|
20533
|
+
...wired.provider === "openai-compat" ? {
|
|
20534
|
+
editFormat: wired.editFormat,
|
|
20535
|
+
editExtra: wired.editExtra
|
|
20536
|
+
} : {}
|
|
19261
20537
|
}) : await generateOpenAICompatibleImage({
|
|
19262
|
-
provider:
|
|
20538
|
+
provider: wired.provider,
|
|
19263
20539
|
apiKey: credential,
|
|
19264
|
-
baseURL:
|
|
19265
|
-
model:
|
|
20540
|
+
baseURL: wired.baseURL,
|
|
20541
|
+
model: wired.model,
|
|
19266
20542
|
prompt: input.prompt,
|
|
19267
20543
|
size,
|
|
19268
20544
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
19269
20545
|
signal
|
|
19270
20546
|
});
|
|
19271
20547
|
output = size;
|
|
19272
|
-
} else if (
|
|
20548
|
+
} else if (wired.provider === "seedream") {
|
|
19273
20549
|
const size = input.quality;
|
|
19274
20550
|
generated = input.mode === "edit" ? await editSeedreamImage({
|
|
19275
20551
|
apiKey: credential,
|
|
19276
|
-
baseURL:
|
|
19277
|
-
model:
|
|
20552
|
+
baseURL: wired.baseURL,
|
|
20553
|
+
model: wired.model,
|
|
19278
20554
|
prompt: input.prompt,
|
|
19279
20555
|
sourceImages,
|
|
19280
20556
|
size,
|
|
@@ -19283,8 +20559,8 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
19283
20559
|
}) : await generateOpenAICompatibleImage({
|
|
19284
20560
|
provider: "seedream",
|
|
19285
20561
|
apiKey: credential,
|
|
19286
|
-
baseURL:
|
|
19287
|
-
model:
|
|
20562
|
+
baseURL: wired.baseURL,
|
|
20563
|
+
model: wired.model,
|
|
19288
20564
|
prompt: input.prompt,
|
|
19289
20565
|
size,
|
|
19290
20566
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
@@ -19296,8 +20572,8 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
19296
20572
|
const size = dashScopeSize(input.ratio);
|
|
19297
20573
|
generated = input.mode === "edit" ? await editDashScopeImage({
|
|
19298
20574
|
apiKey: credential,
|
|
19299
|
-
endpoint:
|
|
19300
|
-
model:
|
|
20575
|
+
endpoint: wired.endpoint,
|
|
20576
|
+
model: wired.model,
|
|
19301
20577
|
prompt: input.prompt,
|
|
19302
20578
|
sourceImages,
|
|
19303
20579
|
size,
|
|
@@ -19305,8 +20581,8 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
19305
20581
|
signal
|
|
19306
20582
|
}) : await generateDashScopeImage({
|
|
19307
20583
|
apiKey: credential,
|
|
19308
|
-
endpoint:
|
|
19309
|
-
model:
|
|
20584
|
+
endpoint: wired.endpoint,
|
|
20585
|
+
model: wired.model,
|
|
19310
20586
|
prompt: input.prompt,
|
|
19311
20587
|
size,
|
|
19312
20588
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
@@ -19412,7 +20688,7 @@ function studioProfile(config, provider, configured) {
|
|
|
19412
20688
|
}
|
|
19413
20689
|
if (active.provider === "comfyui") throw new Error("Invalid cloud provider profile");
|
|
19414
20690
|
const model = active.model;
|
|
19415
|
-
if (provider === "google") return profile(provider, model, configured, ASPECT_RATIOS.map(option), IMAGE_SIZES.map((value) => ({
|
|
20691
|
+
if (provider === "google") return profile(provider, model, configured, ASPECT_RATIOS$1.map(option), IMAGE_SIZES.map((value) => ({
|
|
19416
20692
|
value,
|
|
19417
20693
|
label: value
|
|
19418
20694
|
})), "1:1", "1K");
|
|
@@ -19667,6 +20943,7 @@ function probeTarget(provider, config, apiKey) {
|
|
|
19667
20943
|
url: joinUrl(config.comfyuiBaseURL ?? "http://127.0.0.1:8188", "system_stats"),
|
|
19668
20944
|
headers: {}
|
|
19669
20945
|
};
|
|
20946
|
+
if (isSubscriptionProvider(provider)) throw new Error("Subscription providers are probed through the manager, not probeTarget");
|
|
19670
20947
|
if (provider === "google") return {
|
|
19671
20948
|
url: googleModelsUrl(config),
|
|
19672
20949
|
headers: { "x-goog-api-key": apiKey ?? "" }
|
|
@@ -19905,6 +21182,27 @@ async function probeComfyUIConnection(config, signal) {
|
|
|
19905
21182
|
message: `HTTP ${String(response.status)}`
|
|
19906
21183
|
};
|
|
19907
21184
|
}
|
|
21185
|
+
/**
|
|
21186
|
+
* Probe a subscription provider: the internal manager's login state for the
|
|
21187
|
+
* matching vendor. No network call and no token access; the probe reports
|
|
21188
|
+
* signed-in (with the account email) or signed-out guidance.
|
|
21189
|
+
*/
|
|
21190
|
+
async function probeSubscriptionConnection(provider, manager) {
|
|
21191
|
+
const vendor = provider === "chatgpt-sub" ? "codex" : provider === "google-sub" ? "antigravity" : "grok";
|
|
21192
|
+
const display = provider === "chatgpt-sub" ? "ChatGPT" : provider === "google-sub" ? "Google" : "Grok";
|
|
21193
|
+
const status = await manager.loginStatus(vendor);
|
|
21194
|
+
if (status.state === "logged-in") return { ok: true };
|
|
21195
|
+
if (status.state === "logged-out") return {
|
|
21196
|
+
ok: false,
|
|
21197
|
+
reason: "missing-key",
|
|
21198
|
+
message: `${display} 账号未登录:请在设置卡片中点击“登录”完成授权`
|
|
21199
|
+
};
|
|
21200
|
+
return {
|
|
21201
|
+
ok: false,
|
|
21202
|
+
reason: "error",
|
|
21203
|
+
message: "登录状态读取失败"
|
|
21204
|
+
};
|
|
21205
|
+
}
|
|
19908
21206
|
/** Serve the settings card's per-provider connectivity probe. */
|
|
19909
21207
|
async function serveTestConnection(req, res, deps) {
|
|
19910
21208
|
if (!sameOrigin(req)) return jsonError(res, 403, "origin-rejected");
|
|
@@ -19918,12 +21216,13 @@ async function serveTestConnection(req, res, deps) {
|
|
|
19918
21216
|
}
|
|
19919
21217
|
const provider = record(body)?.provider;
|
|
19920
21218
|
if (typeof provider !== "string" || !IMAGE_PROVIDERS.includes(provider)) return jsonError(res, 400, "invalid-provider");
|
|
21219
|
+
const active = provider;
|
|
19921
21220
|
if (record(body)?.action === "models") {
|
|
19922
|
-
if (
|
|
21221
|
+
if (active === "comfyui" || isSubscriptionProvider(active)) return jsonError(res, 400, "models-unsupported");
|
|
19923
21222
|
let result;
|
|
19924
21223
|
try {
|
|
19925
|
-
const apiKey = await deps.resolveKey(
|
|
19926
|
-
result =
|
|
21224
|
+
const apiKey = await deps.resolveKey(active);
|
|
21225
|
+
result = active === "google" ? await fetchGoogleImageModels(deps.config(), apiKey) : active === "dashscope" ? await fetchDashScopeImageModels(deps.config(), apiKey) : await fetchOpenAIImageModels(active, deps.config(), apiKey);
|
|
19927
21226
|
} catch (error) {
|
|
19928
21227
|
result = {
|
|
19929
21228
|
ok: false,
|
|
@@ -19935,10 +21234,11 @@ async function serveTestConnection(req, res, deps) {
|
|
|
19935
21234
|
}
|
|
19936
21235
|
let result;
|
|
19937
21236
|
try {
|
|
19938
|
-
if (
|
|
21237
|
+
if (active === "comfyui") result = await probeComfyUIConnection(deps.config());
|
|
21238
|
+
else if (isSubscriptionProvider(active)) result = await probeSubscriptionConnection(active, deps.subscriptionManager);
|
|
19939
21239
|
else {
|
|
19940
|
-
const apiKey = await deps.resolveKey(
|
|
19941
|
-
result = await probeProviderConnection(
|
|
21240
|
+
const apiKey = await deps.resolveKey(active);
|
|
21241
|
+
result = await probeProviderConnection(active, deps.config(), apiKey);
|
|
19942
21242
|
}
|
|
19943
21243
|
} catch (error) {
|
|
19944
21244
|
result = {
|
|
@@ -19999,6 +21299,8 @@ function apply(ctx, config = {}) {
|
|
|
19999
21299
|
let current = () => migrateOpenAICompatConfig(config);
|
|
20000
21300
|
const knownWorkspaceRoots = /* @__PURE__ */ new Set();
|
|
20001
21301
|
const canvasMirror = new CanvasMirror();
|
|
21302
|
+
const subscriptionManager = new SubscriptionManager(ctx);
|
|
21303
|
+
registerSubscriptionRoutes(ctx, subscriptionManager);
|
|
20002
21304
|
installImageSettings(ctx, config, {
|
|
20003
21305
|
setSource: (source) => {
|
|
20004
21306
|
current = () => migrateOpenAICompatConfig(source());
|
|
@@ -20054,7 +21356,8 @@ function apply(ctx, config = {}) {
|
|
|
20054
21356
|
path: TEST_CONNECTION_ROUTE,
|
|
20055
21357
|
handler: (req, res) => serveTestConnection(req, res, {
|
|
20056
21358
|
resolveKey: (provider) => resolveApiKey(ctx, provider),
|
|
20057
|
-
config: () => current()
|
|
21359
|
+
config: () => current(),
|
|
21360
|
+
subscriptionManager
|
|
20058
21361
|
})
|
|
20059
21362
|
}), "dsh-image-gen: test connection route");
|
|
20060
21363
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -20128,13 +21431,16 @@ function apply(ctx, config = {}) {
|
|
|
20128
21431
|
"dashscope",
|
|
20129
21432
|
"xai",
|
|
20130
21433
|
"zhipu",
|
|
20131
|
-
"comfyui"
|
|
21434
|
+
"comfyui",
|
|
21435
|
+
"chatgpt-sub",
|
|
21436
|
+
"grok-sub",
|
|
21437
|
+
"google-sub"
|
|
20132
21438
|
],
|
|
20133
|
-
description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default."
|
|
21439
|
+
description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default. chatgpt-sub, grok-sub, and google-sub generate through the logged-in subscription account instead of an API key."
|
|
20134
21440
|
},
|
|
20135
21441
|
model: {
|
|
20136
21442
|
type: "string",
|
|
20137
|
-
description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead)."
|
|
21443
|
+
description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
|
|
20138
21444
|
},
|
|
20139
21445
|
aspect_ratio: {
|
|
20140
21446
|
type: "string",
|
|
@@ -20181,6 +21487,14 @@ function apply(ctx, config = {}) {
|
|
|
20181
21487
|
signal: exec.signal
|
|
20182
21488
|
}), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
|
|
20183
21489
|
}
|
|
21490
|
+
if (active.provider === "chatgpt-sub" || active.provider === "grok-sub" || active.provider === "google-sub") return saveGenerated(ctx, await generateSubscriptionImage({
|
|
21491
|
+
manager: subscriptionManager,
|
|
21492
|
+
provider: active.provider,
|
|
21493
|
+
prompt: args.prompt,
|
|
21494
|
+
...args.size !== void 0 ? { size: args.size } : {},
|
|
21495
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
21496
|
+
signal: exec.signal
|
|
21497
|
+
}), active.provider, active.model, "subscription", current(), exec, knownWorkspaceRoots);
|
|
20184
21498
|
const credential = await requireApiKey(ctx, active.provider, "generate_image");
|
|
20185
21499
|
if (active.provider === "google") {
|
|
20186
21500
|
const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
|
|
@@ -20241,13 +21555,16 @@ function apply(ctx, config = {}) {
|
|
|
20241
21555
|
"dashscope",
|
|
20242
21556
|
"xai",
|
|
20243
21557
|
"zhipu",
|
|
20244
|
-
"comfyui"
|
|
21558
|
+
"comfyui",
|
|
21559
|
+
"chatgpt-sub",
|
|
21560
|
+
"grok-sub",
|
|
21561
|
+
"google-sub"
|
|
20245
21562
|
],
|
|
20246
|
-
description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default."
|
|
21563
|
+
description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default. chatgpt-sub, grok-sub, and google-sub cannot edit images yet; the subscription channel is text-prompt only, so retry editing with an API-key provider instead."
|
|
20247
21564
|
},
|
|
20248
21565
|
model: {
|
|
20249
21566
|
type: "string",
|
|
20250
|
-
description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead)."
|
|
21567
|
+
description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead) nor by the subscription providers (model fixed by the subscription)."
|
|
20251
21568
|
},
|
|
20252
21569
|
source: {
|
|
20253
21570
|
type: "string",
|
|
@@ -20306,6 +21623,7 @@ function apply(ctx, config = {}) {
|
|
|
20306
21623
|
output: imageOutput("Edited"),
|
|
20307
21624
|
async execute(args, exec) {
|
|
20308
21625
|
const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
|
|
21626
|
+
if (active.provider === "chatgpt-sub" || active.provider === "grok-sub" || active.provider === "google-sub") assertSubscriptionEditUnsupported(active.provider);
|
|
20309
21627
|
const canvasSelection = args.source === "canvas_selection";
|
|
20310
21628
|
if (canvasSelection && (args.source_attachment_id !== void 0 || Array.isArray(args.source_attachment_ids) || args.source_path !== void 0 || Array.isArray(args.source_paths))) throw new Error("edit_image source=canvas_selection cannot be combined with source_attachment_id, source_attachment_ids, source_path, or source_paths; provide exactly one selector");
|
|
20311
21629
|
const sourceImages = canvasSelection ? await resolveCanvasSelectionReferences({
|
|
@@ -20367,7 +21685,11 @@ function apply(ctx, config = {}) {
|
|
|
20367
21685
|
sourceImages,
|
|
20368
21686
|
size,
|
|
20369
21687
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
20370
|
-
signal: exec.signal
|
|
21688
|
+
signal: exec.signal,
|
|
21689
|
+
...active.provider === "openai-compat" ? {
|
|
21690
|
+
editFormat: active.editFormat,
|
|
21691
|
+
editExtra: active.editExtra
|
|
21692
|
+
} : {}
|
|
20371
21693
|
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
20372
21694
|
if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
|
|
20373
21695
|
apiKey: credential,
|