dsh-image-gen 0.4.1 → 0.5.0
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 +344 -338
- package/README.md +344 -338
- package/README.zh-CN.md +3 -3
- package/lib/client.js +1514 -388
- package/lib/client.js.map +1 -1
- package/lib/index.js +761 -214
- package/lib/types/client/conversation-image-revisions.d.ts +1 -1
- package/lib/types/client/gallery-view.d.ts +8 -2
- package/lib/types/client/index.d.ts +17 -1
- package/lib/types/client/inspiration-api.d.ts +10 -0
- package/lib/types/client/inspiration-detail.d.ts +14 -0
- package/lib/types/client/provider-pill.d.ts +49 -0
- package/lib/types/config.d.ts +39 -10
- package/lib/types/credentials.d.ts +15 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/inspiration/cache.d.ts +20 -0
- package/lib/types/inspiration/downloader.d.ts +27 -0
- package/lib/types/inspiration/routes.d.ts +13 -0
- package/lib/types/inspiration/sources.d.ts +33 -0
- package/lib/types/inspiration/types.d.ts +65 -0
- package/lib/types/inspiration/updater.d.ts +7 -0
- package/lib/types/openai-compatible.d.ts +1 -1
- package/lib/types/redact.d.ts +2 -0
- package/lib/types/shared.d.ts +37 -2
- package/lib/types/test-route.d.ts +73 -0
- package/package.json +1 -1
- package/skills/install-dsh-image-gen/SKILL.md +22 -22
package/lib/index.js
CHANGED
|
@@ -16,39 +16,6 @@ var __exportAll = (all, no_symbols) => {
|
|
|
16
16
|
return target;
|
|
17
17
|
};
|
|
18
18
|
//#endregion
|
|
19
|
-
//#region node_modules/.pnpm/@deepseek-ai+dsh-credential_07859f6f9ce036fd17600077e6a695b1/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
|
|
20
|
-
/**
|
|
21
|
-
* Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
|
|
22
|
-
* *references* to secrets — environment-variable names — while providers own
|
|
23
|
-
* the actual values and their storage. Consumers resolve a reference once per
|
|
24
|
-
* operation, so a changed credential reaches the next operation without any
|
|
25
|
-
* plugin restart, and configuration surfaces describe a reference without
|
|
26
|
-
* ever seeing its value.
|
|
27
|
-
* @module @deepseek-ai/dsh-credentials
|
|
28
|
-
*/
|
|
29
|
-
const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
30
|
-
/**
|
|
31
|
-
* Brand a raw string as a {@link CredentialRef}.
|
|
32
|
-
* @param value - candidate reference; a POSIX shell identifier such as `DEEPSEEK_API_KEY`.
|
|
33
|
-
* @returns the branded reference.
|
|
34
|
-
*/
|
|
35
|
-
function credentialRef(value) {
|
|
36
|
-
if (!isCredentialRefName(value)) throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`);
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Whether a raw string could name a reference at all. Consumers that receive
|
|
41
|
-
* environment-variable names from somewhere else — a provider library's own
|
|
42
|
-
* ambient discovery, a hook payload — ask this before resolving, because a name
|
|
43
|
-
* outside the grammar has no reference to miss and should read as "not set"
|
|
44
|
-
* rather than as a thrown error.
|
|
45
|
-
* @param value - candidate reference.
|
|
46
|
-
* @returns true when {@link credentialRef} would accept it.
|
|
47
|
-
*/
|
|
48
|
-
function isCredentialRefName(value) {
|
|
49
|
-
return REF_PATTERN.test(value);
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
19
|
//#region node_modules/.pnpm/@deepseek-ai+dsh-util-value_e176751d487506fbf1ea91463d745d9b/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
|
|
53
20
|
/**
|
|
54
21
|
* Compare JSON-compatible values structurally.
|
|
@@ -117,7 +84,7 @@ var lib_exports = /* @__PURE__ */ __exportAll({
|
|
|
117
84
|
SettingsConflictError: () => SettingsConflictError,
|
|
118
85
|
SettingsProvider: () => SettingsProvider,
|
|
119
86
|
default: () => SettingsProvider,
|
|
120
|
-
redactSecrets: () => redactSecrets
|
|
87
|
+
redactSecrets: () => redactSecrets$1
|
|
121
88
|
});
|
|
122
89
|
/**
|
|
123
90
|
* Structural secret redaction for settings values. `role('secret')` fields are
|
|
@@ -181,7 +148,7 @@ function walk(node, value, path, secrets) {
|
|
|
181
148
|
* object-property secret slots still enumerated.
|
|
182
149
|
* @returns the stripped detached value and the ordered secret positions.
|
|
183
150
|
*/
|
|
184
|
-
function redactSecrets(schema, value) {
|
|
151
|
+
function redactSecrets$1(schema, value) {
|
|
185
152
|
const secrets = [];
|
|
186
153
|
return {
|
|
187
154
|
value: walk(schema, value, [], secrets),
|
|
@@ -485,12 +452,12 @@ var SettingsProvider = class extends Service {
|
|
|
485
452
|
};
|
|
486
453
|
if (options?.redactSecrets !== true) return descriptor;
|
|
487
454
|
const schema = registration.schema;
|
|
488
|
-
const redacted = redactSecrets(schema, registration.resolved);
|
|
455
|
+
const redacted = redactSecrets$1(schema, registration.resolved);
|
|
489
456
|
return {
|
|
490
457
|
...descriptor,
|
|
491
458
|
value: redacted.value,
|
|
492
|
-
...base === void 0 ? {} : { base: redactSecrets(schema, base).value },
|
|
493
|
-
...detachedUser === void 0 ? {} : { user: redactSecrets(schema, detachedUser).value },
|
|
459
|
+
...base === void 0 ? {} : { base: redactSecrets$1(schema, base).value },
|
|
460
|
+
...detachedUser === void 0 ? {} : { user: redactSecrets$1(schema, detachedUser).value },
|
|
494
461
|
secrets: redacted.secrets
|
|
495
462
|
};
|
|
496
463
|
});
|
|
@@ -4872,28 +4839,82 @@ const STUDIO_ROUTE = "/plugins/dsh-image-gen/studio";
|
|
|
4872
4839
|
const INSPIRATION_ROUTE = "/plugins/dsh-image-gen/inspiration";
|
|
4873
4840
|
/** Browser route used for saving generated images to workspace on demand. */
|
|
4874
4841
|
const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
|
|
4842
|
+
/** Browser route the settings card probes provider connectivity through. */
|
|
4843
|
+
const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
|
|
4875
4844
|
/** Namespace persisted through DSH Settings. */
|
|
4876
4845
|
const IMAGE_GENERATION_NAMESPACE = "image-generation";
|
|
4877
4846
|
/** Supported providers. */
|
|
4878
4847
|
const IMAGE_PROVIDERS = [
|
|
4879
4848
|
"google",
|
|
4880
4849
|
"openai",
|
|
4850
|
+
"openai-compat",
|
|
4881
4851
|
"seedream",
|
|
4882
4852
|
"dashscope",
|
|
4853
|
+
"xai",
|
|
4854
|
+
"zhipu",
|
|
4883
4855
|
"comfyui"
|
|
4884
4856
|
];
|
|
4885
4857
|
/** Providers supported by the first browser workbench release. */
|
|
4886
4858
|
const CLOUD_IMAGE_PROVIDERS = [
|
|
4887
4859
|
"google",
|
|
4888
4860
|
"openai",
|
|
4861
|
+
"openai-compat",
|
|
4889
4862
|
"seedream",
|
|
4890
|
-
"dashscope"
|
|
4863
|
+
"dashscope",
|
|
4864
|
+
"xai",
|
|
4865
|
+
"zhipu"
|
|
4891
4866
|
];
|
|
4867
|
+
/**
|
|
4868
|
+
* Credential references resolved through the DSH Credentials service (BYOK).
|
|
4869
|
+
* These are POSIX-style reference names, not environment variables: the host
|
|
4870
|
+
* layers the process environment and its managed store behind them, so a name
|
|
4871
|
+
* like OPENAI_API_KEY is shared with any other plugin resolving the same ref.
|
|
4872
|
+
*/
|
|
4873
|
+
const GOOGLE_API_KEY_ENV = "GEMINI_API_KEY";
|
|
4874
|
+
/** OpenAI Platform credential reference. */
|
|
4875
|
+
const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
4876
|
+
/**
|
|
4877
|
+
* OpenAI-compatible relay credential reference. Deliberately distinct from
|
|
4878
|
+
* OPENAI_API_KEY so an official key and a relay key can coexist without
|
|
4879
|
+
* overwriting each other.
|
|
4880
|
+
*/
|
|
4881
|
+
const OPENAI_COMPAT_API_KEY_ENV = "DSH_IMAGE_GEN_OPENAI_COMPAT_KEY";
|
|
4882
|
+
/** Volcengine Ark credential reference. */
|
|
4883
|
+
const SEEDREAM_API_KEY_ENV = "ARK_API_KEY";
|
|
4884
|
+
/** DashScope credential reference. */
|
|
4885
|
+
const DASHSCOPE_API_KEY_ENV = "DASHSCOPE_API_KEY";
|
|
4886
|
+
/** xAI credential reference; matches the official xAI SDK environment name. */
|
|
4887
|
+
const XAI_API_KEY_ENV = "XAI_API_KEY";
|
|
4888
|
+
/** Zhipu credential reference; matches the official Zhipu SDK environment name. */
|
|
4889
|
+
const ZHIPU_API_KEY_ENV = "ZHIPUAI_API_KEY";
|
|
4890
|
+
/** The credential reference each cloud provider's API key is stored under. */
|
|
4891
|
+
const CLOUD_CREDENTIAL_REFS = {
|
|
4892
|
+
google: GOOGLE_API_KEY_ENV,
|
|
4893
|
+
openai: OPENAI_API_KEY_ENV,
|
|
4894
|
+
"openai-compat": OPENAI_COMPAT_API_KEY_ENV,
|
|
4895
|
+
seedream: SEEDREAM_API_KEY_ENV,
|
|
4896
|
+
dashscope: DASHSCOPE_API_KEY_ENV,
|
|
4897
|
+
xai: XAI_API_KEY_ENV,
|
|
4898
|
+
zhipu: ZHIPU_API_KEY_ENV
|
|
4899
|
+
};
|
|
4900
|
+
/** Locale-neutral provider names for user-facing errors and status lines. */
|
|
4901
|
+
const PROVIDER_DISPLAY_NAMES = {
|
|
4902
|
+
google: "Google Gemini",
|
|
4903
|
+
openai: "OpenAI",
|
|
4904
|
+
"openai-compat": "OpenAI 兼容",
|
|
4905
|
+
seedream: "Seedream",
|
|
4906
|
+
dashscope: "DashScope",
|
|
4907
|
+
xai: "xAI Grok",
|
|
4908
|
+
zhipu: "智谱 GLM",
|
|
4909
|
+
comfyui: "ComfyUI"
|
|
4910
|
+
};
|
|
4892
4911
|
/** Default endpoints and base URLs. */
|
|
4893
4912
|
const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
|
|
4894
4913
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
4895
4914
|
const DEFAULT_SEEDREAM_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
|
|
4896
4915
|
const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
|
|
4916
|
+
const DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1";
|
|
4917
|
+
const DEFAULT_ZHIPU_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
|
|
4897
4918
|
const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
|
|
4898
4919
|
const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
|
|
4899
4920
|
const DEFAULT_COMFYUI_WORKFLOW_LABEL = "API workflow";
|
|
@@ -4902,6 +4923,8 @@ const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
|
|
|
4902
4923
|
const DEFAULT_OPENAI_MODEL = "gpt-image-2";
|
|
4903
4924
|
const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
|
|
4904
4925
|
const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
|
|
4926
|
+
const DEFAULT_XAI_MODEL = "grok-imagine-image";
|
|
4927
|
+
const DEFAULT_ZHIPU_MODEL = "glm-image";
|
|
4905
4928
|
/** Named workflows, falling back to the legacy single-workflow fields when the list is empty. */
|
|
4906
4929
|
function resolveComfyUIWorkflows(source) {
|
|
4907
4930
|
const named = [];
|
|
@@ -4953,14 +4976,6 @@ function mergeComfyUIPrompt(preset, user) {
|
|
|
4953
4976
|
/** User-facing configuration for supported image providers. */
|
|
4954
4977
|
/** Default workspace subfolder that receives generated image files. */
|
|
4955
4978
|
const DEFAULT_WORKSPACE_FOLDER = "dsh-image-gen";
|
|
4956
|
-
/** Google API credential reference. */
|
|
4957
|
-
const GOOGLE_API_KEY_ENV = "GEMINI_API_KEY";
|
|
4958
|
-
/** OpenAI Platform or compatible relay credential reference. */
|
|
4959
|
-
const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
4960
|
-
/** Volcengine Ark credential reference. */
|
|
4961
|
-
const SEEDREAM_API_KEY_ENV = "ARK_API_KEY";
|
|
4962
|
-
/** DashScope credential reference. */
|
|
4963
|
-
const DASHSCOPE_API_KEY_ENV = "DASHSCOPE_API_KEY";
|
|
4964
4979
|
/** Google tool-level controls. */
|
|
4965
4980
|
const ASPECT_RATIOS = [
|
|
4966
4981
|
"1:1",
|
|
@@ -4983,10 +4998,16 @@ const Config = z.object({
|
|
|
4983
4998
|
googleEndpoint: z.string().default(DEFAULT_GOOGLE_ENDPOINT),
|
|
4984
4999
|
openaiBaseURL: z.string().default(DEFAULT_OPENAI_BASE_URL),
|
|
4985
5000
|
openaiModel: z.string().default(DEFAULT_OPENAI_MODEL),
|
|
5001
|
+
openaiCompatBaseURL: z.string().default(""),
|
|
5002
|
+
openaiCompatModel: z.string().default(""),
|
|
4986
5003
|
seedreamBaseURL: z.string().default(DEFAULT_SEEDREAM_BASE_URL),
|
|
4987
5004
|
seedreamModel: z.string().default(DEFAULT_SEEDREAM_MODEL),
|
|
4988
5005
|
dashscopeEndpoint: z.string().default(DEFAULT_DASHSCOPE_ENDPOINT),
|
|
4989
5006
|
dashscopeModel: z.string().default(DEFAULT_DASHSCOPE_MODEL),
|
|
5007
|
+
xaiBaseURL: z.string().default(DEFAULT_XAI_BASE_URL),
|
|
5008
|
+
xaiModel: z.string().default(DEFAULT_XAI_MODEL),
|
|
5009
|
+
zhipuBaseURL: z.string().default(DEFAULT_ZHIPU_BASE_URL),
|
|
5010
|
+
zhipuModel: z.string().default(DEFAULT_ZHIPU_MODEL),
|
|
4990
5011
|
comfyuiBaseURL: z.string().default(DEFAULT_COMFYUI_BASE_URL),
|
|
4991
5012
|
comfyuiWorkflows: z.array(z.object({
|
|
4992
5013
|
name: z.string(),
|
|
@@ -5010,6 +5031,19 @@ function resolveProvider(config) {
|
|
|
5010
5031
|
baseURL: config.openaiBaseURL ?? "https://api.openai.com/v1",
|
|
5011
5032
|
imageSize: "1024x1024"
|
|
5012
5033
|
};
|
|
5034
|
+
case "openai-compat": {
|
|
5035
|
+
const baseURL = config.openaiCompatBaseURL?.trim();
|
|
5036
|
+
if (baseURL === void 0 || baseURL.length === 0) throw new Error("OpenAI 兼容 provider requires a base URL; set it in Settings > Plugins > Image generation.");
|
|
5037
|
+
const model = config.openaiCompatModel?.trim();
|
|
5038
|
+
if (model === void 0 || model.length === 0) throw new Error("OpenAI 兼容 provider requires a model name; set it in Settings > Plugins > Image generation.");
|
|
5039
|
+
return {
|
|
5040
|
+
provider: "openai-compat",
|
|
5041
|
+
apiKeyEnv: OPENAI_COMPAT_API_KEY_ENV,
|
|
5042
|
+
model,
|
|
5043
|
+
baseURL,
|
|
5044
|
+
imageSize: "1024x1024"
|
|
5045
|
+
};
|
|
5046
|
+
}
|
|
5013
5047
|
case "seedream": return {
|
|
5014
5048
|
provider: "seedream",
|
|
5015
5049
|
apiKeyEnv: SEEDREAM_API_KEY_ENV,
|
|
@@ -5024,6 +5058,20 @@ function resolveProvider(config) {
|
|
|
5024
5058
|
endpoint: config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1",
|
|
5025
5059
|
imageSize: "1024*1024"
|
|
5026
5060
|
};
|
|
5061
|
+
case "xai": return {
|
|
5062
|
+
provider: "xai",
|
|
5063
|
+
apiKeyEnv: XAI_API_KEY_ENV,
|
|
5064
|
+
model: config.xaiModel ?? "grok-imagine-image",
|
|
5065
|
+
baseURL: config.xaiBaseURL ?? "https://api.x.ai/v1",
|
|
5066
|
+
imageSize: "1024x1024"
|
|
5067
|
+
};
|
|
5068
|
+
case "zhipu": return {
|
|
5069
|
+
provider: "zhipu",
|
|
5070
|
+
apiKeyEnv: ZHIPU_API_KEY_ENV,
|
|
5071
|
+
model: config.zhipuModel ?? "glm-image",
|
|
5072
|
+
baseURL: config.zhipuBaseURL ?? "https://open.bigmodel.cn/api/paas/v4",
|
|
5073
|
+
imageSize: "1024x1024"
|
|
5074
|
+
};
|
|
5027
5075
|
case "comfyui": {
|
|
5028
5076
|
const workflows = resolveComfyUIWorkflows(config);
|
|
5029
5077
|
const workflow = activeComfyUIWorkflow(config);
|
|
@@ -5045,6 +5093,78 @@ function resolveProvider(config) {
|
|
|
5045
5093
|
};
|
|
5046
5094
|
}
|
|
5047
5095
|
}
|
|
5096
|
+
/**
|
|
5097
|
+
* Apply a per-call provider and/or model override on top of the saved config.
|
|
5098
|
+
* `model` is ignored for ComfyUI, whose per-call equivalent is `workflow`.
|
|
5099
|
+
*/
|
|
5100
|
+
function withProviderOverrides(config, provider, model) {
|
|
5101
|
+
const base = provider === void 0 ? { ...config } : {
|
|
5102
|
+
...config,
|
|
5103
|
+
provider
|
|
5104
|
+
};
|
|
5105
|
+
if (model === void 0) return base;
|
|
5106
|
+
const trimmed = model.trim();
|
|
5107
|
+
if (trimmed.length === 0) return base;
|
|
5108
|
+
switch (base.provider ?? "google") {
|
|
5109
|
+
case "google": return {
|
|
5110
|
+
...base,
|
|
5111
|
+
googleModel: trimmed
|
|
5112
|
+
};
|
|
5113
|
+
case "openai": return {
|
|
5114
|
+
...base,
|
|
5115
|
+
openaiModel: trimmed
|
|
5116
|
+
};
|
|
5117
|
+
case "openai-compat": return {
|
|
5118
|
+
...base,
|
|
5119
|
+
openaiCompatModel: trimmed
|
|
5120
|
+
};
|
|
5121
|
+
case "seedream": return {
|
|
5122
|
+
...base,
|
|
5123
|
+
seedreamModel: trimmed
|
|
5124
|
+
};
|
|
5125
|
+
case "dashscope": return {
|
|
5126
|
+
...base,
|
|
5127
|
+
dashscopeModel: trimmed
|
|
5128
|
+
};
|
|
5129
|
+
case "xai": return {
|
|
5130
|
+
...base,
|
|
5131
|
+
xaiModel: trimmed
|
|
5132
|
+
};
|
|
5133
|
+
case "zhipu": return {
|
|
5134
|
+
...base,
|
|
5135
|
+
zhipuModel: trimmed
|
|
5136
|
+
};
|
|
5137
|
+
case "comfyui": return base;
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
/**
|
|
5141
|
+
* One-time migration: when the legacy single OpenAI slot points at a relay
|
|
5142
|
+
* (non-official base URL) and the compat row is empty, move the relay config
|
|
5143
|
+
* to the compat row so both can coexist. Returns the input untouched when
|
|
5144
|
+
* nothing needs moving.
|
|
5145
|
+
*/
|
|
5146
|
+
function migrateOpenAICompatConfig(config) {
|
|
5147
|
+
const legacyBase = config.openaiBaseURL?.trim() ?? "";
|
|
5148
|
+
if (legacyBase.length === 0 || legacyBase === "https://api.openai.com/v1") return config;
|
|
5149
|
+
if (hostOf(legacyBase) === "api.openai.com") return config;
|
|
5150
|
+
if ((config.openaiCompatBaseURL?.trim() ?? "").length > 0) return config;
|
|
5151
|
+
const compatModel = config.openaiCompatModel?.trim() ?? "";
|
|
5152
|
+
return {
|
|
5153
|
+
...config,
|
|
5154
|
+
openaiCompatBaseURL: legacyBase,
|
|
5155
|
+
...compatModel.length > 0 ? {} : { openaiCompatModel: config.openaiModel },
|
|
5156
|
+
openaiBaseURL: DEFAULT_OPENAI_BASE_URL,
|
|
5157
|
+
...config.provider === "openai" ? { provider: "openai-compat" } : {}
|
|
5158
|
+
};
|
|
5159
|
+
}
|
|
5160
|
+
/** Host part of a URL, or null when it cannot be parsed. */
|
|
5161
|
+
function hostOf(raw) {
|
|
5162
|
+
try {
|
|
5163
|
+
return new URL(raw).host;
|
|
5164
|
+
} catch {
|
|
5165
|
+
return null;
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5048
5168
|
/** The workflow a ComfyUI call runs: the requested name when given, else the active one. */
|
|
5049
5169
|
function selectComfyUIWorkflow(active, requested) {
|
|
5050
5170
|
if (active.workflow === void 0) throw new Error("ComfyUI image generation requires an imported workflow; import one in Settings > Plugins > Image generation.");
|
|
@@ -5055,6 +5175,62 @@ function selectComfyUIWorkflow(active, requested) {
|
|
|
5055
5175
|
return workflow;
|
|
5056
5176
|
}
|
|
5057
5177
|
//#endregion
|
|
5178
|
+
//#region node_modules/.pnpm/@deepseek-ai+dsh-credential_07859f6f9ce036fd17600077e6a695b1/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
|
|
5179
|
+
/**
|
|
5180
|
+
* Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
|
|
5181
|
+
* *references* to secrets — environment-variable names — while providers own
|
|
5182
|
+
* the actual values and their storage. Consumers resolve a reference once per
|
|
5183
|
+
* operation, so a changed credential reaches the next operation without any
|
|
5184
|
+
* plugin restart, and configuration surfaces describe a reference without
|
|
5185
|
+
* ever seeing its value.
|
|
5186
|
+
* @module @deepseek-ai/dsh-credentials
|
|
5187
|
+
*/
|
|
5188
|
+
const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5189
|
+
/**
|
|
5190
|
+
* Brand a raw string as a {@link CredentialRef}.
|
|
5191
|
+
* @param value - candidate reference; a POSIX shell identifier such as `DEEPSEEK_API_KEY`.
|
|
5192
|
+
* @returns the branded reference.
|
|
5193
|
+
*/
|
|
5194
|
+
function credentialRef(value) {
|
|
5195
|
+
if (!isCredentialRefName(value)) throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`);
|
|
5196
|
+
return value;
|
|
5197
|
+
}
|
|
5198
|
+
/**
|
|
5199
|
+
* Whether a raw string could name a reference at all. Consumers that receive
|
|
5200
|
+
* environment-variable names from somewhere else — a provider library's own
|
|
5201
|
+
* ambient discovery, a hook payload — ask this before resolving, because a name
|
|
5202
|
+
* outside the grammar has no reference to miss and should read as "not set"
|
|
5203
|
+
* rather than as a thrown error.
|
|
5204
|
+
* @param value - candidate reference.
|
|
5205
|
+
* @returns true when {@link credentialRef} would accept it.
|
|
5206
|
+
*/
|
|
5207
|
+
function isCredentialRefName(value) {
|
|
5208
|
+
return REF_PATTERN.test(value);
|
|
5209
|
+
}
|
|
5210
|
+
//#endregion
|
|
5211
|
+
//#region lib/types/credentials.js
|
|
5212
|
+
/** Shared BYOK credential resolution for the Agent tools and the Studio route. */
|
|
5213
|
+
/**
|
|
5214
|
+
* Resolve one provider's stored API key to a non-blank value. Blank values
|
|
5215
|
+
* never count as configured: the host treats an empty stored value as absent,
|
|
5216
|
+
* and a whitespace-only key is never sent to a provider.
|
|
5217
|
+
*/
|
|
5218
|
+
async function resolveApiKey(ctx, provider) {
|
|
5219
|
+
const value = (await ctx.credentials.resolve(credentialRef(CLOUD_CREDENTIAL_REFS[provider])))?.value.trim() ?? "";
|
|
5220
|
+
return value.length > 0 ? value : void 0;
|
|
5221
|
+
}
|
|
5222
|
+
/**
|
|
5223
|
+
* Resolve one provider's API key or fail with a user-facing message that says
|
|
5224
|
+
* where to configure it. Passing `tool` selects the Agent-facing English
|
|
5225
|
+
* wording; without it the message is the browser-facing Chinese one used by
|
|
5226
|
+
* the Studio route.
|
|
5227
|
+
*/
|
|
5228
|
+
async function requireApiKey(ctx, provider, tool) {
|
|
5229
|
+
const value = await resolveApiKey(ctx, provider);
|
|
5230
|
+
if (value !== void 0) return value;
|
|
5231
|
+
throw new Error(tool === void 0 ? `${PROVIDER_DISPLAY_NAMES[provider]} 尚未配置 API Key,请先到 设置 > 插件 > 图像生成 配置` : `${tool} requires the ${PROVIDER_DISPLAY_NAMES[provider]} API key; configure it in Settings > Plugins > Image generation.`);
|
|
5232
|
+
}
|
|
5233
|
+
//#endregion
|
|
5058
5234
|
//#region lib/types/comfyui-workflow.js
|
|
5059
5235
|
/** Pure ComfyUI API-workflow validation and placeholder injection. */
|
|
5060
5236
|
const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
|
|
@@ -5095,8 +5271,8 @@ function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed(), image
|
|
|
5095
5271
|
};
|
|
5096
5272
|
let imageInputs = 0;
|
|
5097
5273
|
const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
|
|
5098
|
-
const node = record$
|
|
5099
|
-
const inputs = node === void 0 ? void 0 : record$
|
|
5274
|
+
const node = record$7(value);
|
|
5275
|
+
const inputs = node === void 0 ? void 0 : record$7(node.inputs);
|
|
5100
5276
|
if (inputs === void 0) return [nodeId, value];
|
|
5101
5277
|
const nextInputs = inject(inputs);
|
|
5102
5278
|
if (isImagePlaceholder(nextInputs[IMAGE_INPUT_KEY])) {
|
|
@@ -5132,7 +5308,7 @@ function parseWorkflow(workflowJson) {
|
|
|
5132
5308
|
}
|
|
5133
5309
|
if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
|
|
5134
5310
|
if (!Object.values(value).some((node) => {
|
|
5135
|
-
const inputs = record$
|
|
5311
|
+
const inputs = record$7(node)?.inputs;
|
|
5136
5312
|
return inputs !== void 0 && containsPromptPlaceholder(inputs);
|
|
5137
5313
|
})) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
|
|
5138
5314
|
return value;
|
|
@@ -5148,10 +5324,36 @@ function containsPromptPlaceholder(value) {
|
|
|
5148
5324
|
function isRecord(value) {
|
|
5149
5325
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5150
5326
|
}
|
|
5151
|
-
function record$
|
|
5327
|
+
function record$7(value) {
|
|
5152
5328
|
return isRecord(value) ? value : void 0;
|
|
5153
5329
|
}
|
|
5154
5330
|
//#endregion
|
|
5331
|
+
//#region lib/types/redact.js
|
|
5332
|
+
/**
|
|
5333
|
+
* Redact secret-shaped content from provider error messages.
|
|
5334
|
+
*
|
|
5335
|
+
* Error bodies from providers and relays surface in the conversation and in
|
|
5336
|
+
* the settings UI. A relay may echo request headers — including the API key —
|
|
5337
|
+
* inside its error body, so every adapter passes response text through here
|
|
5338
|
+
* before embedding it in a thrown error, and also passes the live key so its
|
|
5339
|
+
* exact value cannot survive even in non-standard formats.
|
|
5340
|
+
*/
|
|
5341
|
+
const REDACTED = "[REDACTED]";
|
|
5342
|
+
const KEY_SHAPED_PATTERNS = [
|
|
5343
|
+
/\bsk-[A-Za-z0-9_-]{8,}/g,
|
|
5344
|
+
/\bAIza[A-Za-z0-9_-]{10,}/g,
|
|
5345
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
5346
|
+
/\b(?:api[_-]?key|apikey|token|secret)["']?\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
5347
|
+
/\b(?:api[_-]?key|apikey|token|secret)\s*["'][A-Za-z0-9._~+/=-]{8,}["']/gi
|
|
5348
|
+
];
|
|
5349
|
+
/** Replace every occurrence of a known secret plus key-shaped values. */
|
|
5350
|
+
function redactSecrets(text, ...secrets) {
|
|
5351
|
+
let redacted = text;
|
|
5352
|
+
for (const secret of secrets) if (secret !== void 0 && secret.length >= 8) redacted = redacted.split(secret).join(REDACTED);
|
|
5353
|
+
for (const pattern of KEY_SHAPED_PATTERNS) redacted = redacted.replace(pattern, REDACTED);
|
|
5354
|
+
return redacted;
|
|
5355
|
+
}
|
|
5356
|
+
//#endregion
|
|
5155
5357
|
//#region lib/types/comfyui.js
|
|
5156
5358
|
const ERROR_LIMIT$3 = 4096;
|
|
5157
5359
|
const POLL_INTERVAL_MS = 500;
|
|
@@ -5217,9 +5419,9 @@ async function uploadSourceImage(baseURL, image, signal) {
|
|
|
5217
5419
|
body: form
|
|
5218
5420
|
});
|
|
5219
5421
|
const text = await readBoundedText$3(response, ERROR_LIMIT$3);
|
|
5220
|
-
if (!response.ok) throw new Error(`ComfyUI image upload failed (${response.status}): ${text}`);
|
|
5422
|
+
if (!response.ok) throw new Error(`ComfyUI image upload failed (${response.status}): ${redactSecrets(text)}`);
|
|
5221
5423
|
const payload = parseJsonRecord(text, "ComfyUI /upload/image returned invalid JSON");
|
|
5222
|
-
if (typeof payload.name !== "string" || payload.name.length === 0) throw new Error(`ComfyUI /upload/image returned no name: ${text}`);
|
|
5424
|
+
if (typeof payload.name !== "string" || payload.name.length === 0) throw new Error(`ComfyUI /upload/image returned no name: ${redactSecrets(text)}`);
|
|
5223
5425
|
const subfolder = typeof payload.subfolder === "string" ? payload.subfolder : "";
|
|
5224
5426
|
return subfolder.length > 0 ? `${subfolder}/${payload.name}` : payload.name;
|
|
5225
5427
|
}
|
|
@@ -5241,9 +5443,9 @@ async function submitWorkflow(baseURL, workflow, signal) {
|
|
|
5241
5443
|
body: JSON.stringify({ prompt: workflow })
|
|
5242
5444
|
});
|
|
5243
5445
|
const text = await readBoundedText$3(response, ERROR_LIMIT$3);
|
|
5244
|
-
if (!response.ok) throw new Error(`ComfyUI rejected the workflow (${response.status}): ${text}`);
|
|
5446
|
+
if (!response.ok) throw new Error(`ComfyUI rejected the workflow (${response.status}): ${redactSecrets(text)}`);
|
|
5245
5447
|
const payload = parseJsonRecord(text, "ComfyUI /prompt returned invalid JSON");
|
|
5246
|
-
if (typeof payload.prompt_id !== "string" || payload.prompt_id.length === 0) throw new Error(`ComfyUI /prompt returned no prompt_id: ${text}`);
|
|
5448
|
+
if (typeof payload.prompt_id !== "string" || payload.prompt_id.length === 0) throw new Error(`ComfyUI /prompt returned no prompt_id: ${redactSecrets(text)}`);
|
|
5247
5449
|
return payload.prompt_id;
|
|
5248
5450
|
}
|
|
5249
5451
|
async function waitForOutput(baseURL, promptId, signal) {
|
|
@@ -5255,11 +5457,11 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
5255
5457
|
headers: { accept: "application/json" }
|
|
5256
5458
|
});
|
|
5257
5459
|
const text = await readBoundedText$3(response, MAX_HISTORY_BYTES);
|
|
5258
|
-
if (!response.ok) throw new Error(`ComfyUI history request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$3)}`);
|
|
5259
|
-
const entry = record$
|
|
5460
|
+
if (!response.ok) throw new Error(`ComfyUI history request failed (${response.status}): ${redactSecrets(text).slice(0, ERROR_LIMIT$3)}`);
|
|
5461
|
+
const entry = record$6(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
|
|
5260
5462
|
if (entry !== void 0) {
|
|
5261
|
-
const status = record$
|
|
5262
|
-
if (status?.status_str === "error") throw new Error(`ComfyUI workflow failed: ${JSON.stringify(status.messages ?? status).slice(0, ERROR_LIMIT$3)}`);
|
|
5463
|
+
const status = record$6(entry.status);
|
|
5464
|
+
if (status?.status_str === "error") throw new Error(`ComfyUI workflow failed: ${redactSecrets(JSON.stringify(status.messages ?? status)).slice(0, ERROR_LIMIT$3)}`);
|
|
5263
5465
|
const output = firstOutputImage(entry.outputs);
|
|
5264
5466
|
if (output !== void 0) return output;
|
|
5265
5467
|
if (status?.completed === true) throw new Error("ComfyUI workflow completed without an output image");
|
|
@@ -5268,13 +5470,13 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
5268
5470
|
}
|
|
5269
5471
|
}
|
|
5270
5472
|
function firstOutputImage(value) {
|
|
5271
|
-
const outputs = record$
|
|
5473
|
+
const outputs = record$6(value);
|
|
5272
5474
|
if (outputs === void 0) return void 0;
|
|
5273
5475
|
for (const nodeOutput of Object.values(outputs)) {
|
|
5274
|
-
const images = record$
|
|
5476
|
+
const images = record$6(nodeOutput)?.images;
|
|
5275
5477
|
if (!Array.isArray(images)) continue;
|
|
5276
5478
|
for (const image of images) {
|
|
5277
|
-
const item = record$
|
|
5479
|
+
const item = record$6(image);
|
|
5278
5480
|
if (typeof item?.filename !== "string" || item.filename.length === 0) continue;
|
|
5279
5481
|
const output = {
|
|
5280
5482
|
filename: item.filename,
|
|
@@ -5315,7 +5517,7 @@ function comfyUIBaseURL(value) {
|
|
|
5315
5517
|
function endpoint(baseURL, path) {
|
|
5316
5518
|
return new URL(path, baseURL);
|
|
5317
5519
|
}
|
|
5318
|
-
function record$
|
|
5520
|
+
function record$6(value) {
|
|
5319
5521
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
5320
5522
|
}
|
|
5321
5523
|
function parseJsonRecord(text, message) {
|
|
@@ -5325,7 +5527,7 @@ function parseJsonRecord(text, message) {
|
|
|
5325
5527
|
} catch {
|
|
5326
5528
|
throw new Error(message);
|
|
5327
5529
|
}
|
|
5328
|
-
const parsed = record$
|
|
5530
|
+
const parsed = record$6(value);
|
|
5329
5531
|
if (parsed === void 0) throw new Error(message);
|
|
5330
5532
|
return parsed;
|
|
5331
5533
|
}
|
|
@@ -5441,12 +5643,12 @@ async function requestQwenImage(options) {
|
|
|
5441
5643
|
body: JSON.stringify(options.requestBody)
|
|
5442
5644
|
});
|
|
5443
5645
|
if (!response.ok) {
|
|
5444
|
-
const errorText = await response.text();
|
|
5646
|
+
const errorText = redactSecrets(await response.text(), options.apiKey);
|
|
5445
5647
|
throw new Error(`DashScope image ${options.operation} failed (${String(response.status)}): ${errorText}`);
|
|
5446
5648
|
}
|
|
5447
5649
|
const payload = await response.json();
|
|
5448
5650
|
const imageUrl = extractImageUrl(payload);
|
|
5449
|
-
if (imageUrl === void 0) throw new Error(`DashScope image ${options.operation} returned no image URL: ${payload.message ?? JSON.stringify(payload)}`);
|
|
5651
|
+
if (imageUrl === void 0) throw new Error(`DashScope image ${options.operation} returned no image URL: ${redactSecrets(payload.message ?? JSON.stringify(payload), options.apiKey)}`);
|
|
5450
5652
|
return downloadImageBlob(imageUrl, options);
|
|
5451
5653
|
}
|
|
5452
5654
|
function extractImageUrl(response) {
|
|
@@ -5531,7 +5733,7 @@ async function requestGoogleImage(input) {
|
|
|
5531
5733
|
})
|
|
5532
5734
|
});
|
|
5533
5735
|
const text = await readBoundedText$2(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$2, label);
|
|
5534
|
-
if (!response.ok) throw new Error(`${label} failed (${response.status}): ${text.slice(0, ERROR_LIMIT$2)}`);
|
|
5736
|
+
if (!response.ok) throw new Error(`${label} failed (${response.status}): ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT$2)}`);
|
|
5535
5737
|
let payload;
|
|
5536
5738
|
try {
|
|
5537
5739
|
payload = JSON.parse(text);
|
|
@@ -5539,7 +5741,7 @@ async function requestGoogleImage(input) {
|
|
|
5539
5741
|
throw new Error(`${label} returned invalid JSON`);
|
|
5540
5742
|
}
|
|
5541
5743
|
const image = outputImage(payload);
|
|
5542
|
-
if (image === void 0) throw new Error(`${label} returned no image: ${text.slice(0, ERROR_LIMIT$2)}`);
|
|
5744
|
+
if (image === void 0) throw new Error(`${label} returned no image: ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT$2)}`);
|
|
5543
5745
|
const mediaType = mediaTypeOf(image.mime_type ?? REQUESTED_MEDIA_TYPE);
|
|
5544
5746
|
if (mediaType === void 0) throw new Error(`${label} returned unsupported media type ${JSON.stringify(image.mime_type)}`);
|
|
5545
5747
|
const data = decodeBase64$2(image.data, label);
|
|
@@ -5550,13 +5752,13 @@ async function requestGoogleImage(input) {
|
|
|
5550
5752
|
};
|
|
5551
5753
|
}
|
|
5552
5754
|
function outputImage(value) {
|
|
5553
|
-
const interaction = record$
|
|
5755
|
+
const interaction = record$5(value);
|
|
5554
5756
|
if (interaction === void 0) return void 0;
|
|
5555
5757
|
const direct = imageContent(interaction.output_image, false);
|
|
5556
5758
|
if (direct !== void 0) return direct;
|
|
5557
5759
|
if (!Array.isArray(interaction.steps)) return void 0;
|
|
5558
5760
|
for (const step of interaction.steps) {
|
|
5559
|
-
const modelOutput = record$
|
|
5761
|
+
const modelOutput = record$5(step);
|
|
5560
5762
|
if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
|
|
5561
5763
|
for (const content of modelOutput.content) {
|
|
5562
5764
|
const image = imageContent(content, true);
|
|
@@ -5565,14 +5767,14 @@ function outputImage(value) {
|
|
|
5565
5767
|
}
|
|
5566
5768
|
}
|
|
5567
5769
|
function imageContent(value, requiresImageType) {
|
|
5568
|
-
const image = record$
|
|
5770
|
+
const image = record$5(value);
|
|
5569
5771
|
if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
|
|
5570
5772
|
return {
|
|
5571
5773
|
data: image.data,
|
|
5572
5774
|
mime_type: image.mime_type
|
|
5573
5775
|
};
|
|
5574
5776
|
}
|
|
5575
|
-
function record$
|
|
5777
|
+
function record$5(value) {
|
|
5576
5778
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
5577
5779
|
}
|
|
5578
5780
|
function mediaTypeOf(value) {
|
|
@@ -5716,7 +5918,7 @@ function findReferenceImage(messages, sourceAttachmentId) {
|
|
|
5716
5918
|
}
|
|
5717
5919
|
/** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
|
|
5718
5920
|
function parseImageAttachmentRef(value) {
|
|
5719
|
-
const ref = record$
|
|
5921
|
+
const ref = record$4(value);
|
|
5720
5922
|
if (ref === void 0) return void 0;
|
|
5721
5923
|
if (typeof ref.attachmentId !== "string" || !imageMediaType$3(ref.mediaType)) return void 0;
|
|
5722
5924
|
if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
|
|
@@ -5748,7 +5950,7 @@ function findInBlocks(blocks, sourceAttachmentId) {
|
|
|
5748
5950
|
}
|
|
5749
5951
|
}
|
|
5750
5952
|
function parseDimensions(value) {
|
|
5751
|
-
const dimensions = record$
|
|
5953
|
+
const dimensions = record$4(value);
|
|
5752
5954
|
if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
|
|
5753
5955
|
return {
|
|
5754
5956
|
width: dimensions.width,
|
|
@@ -5820,7 +6022,7 @@ function positiveInteger(value) {
|
|
|
5820
6022
|
function nonNegativeInteger(value) {
|
|
5821
6023
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
5822
6024
|
}
|
|
5823
|
-
function record$
|
|
6025
|
+
function record$4(value) {
|
|
5824
6026
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
5825
6027
|
}
|
|
5826
6028
|
//#endregion
|
|
@@ -6049,22 +6251,22 @@ async function deleteImageFromWorkspace(filePath, allowedWorkspaceRoots) {
|
|
|
6049
6251
|
}
|
|
6050
6252
|
//#endregion
|
|
6051
6253
|
//#region lib/types/image-route.js
|
|
6052
|
-
const MAX_BODY_BYTES = 65536;
|
|
6254
|
+
const MAX_BODY_BYTES$1 = 65536;
|
|
6053
6255
|
/** Serve one verified durable image reference to a same-origin browser request. */
|
|
6054
6256
|
async function serveImage(req, res, deps) {
|
|
6055
|
-
if (req.method !== "POST") return jsonError$
|
|
6056
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$
|
|
6257
|
+
if (req.method !== "POST") return jsonError$3(res, 405, "method-not-allowed");
|
|
6258
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$3(res, 415, "json-required");
|
|
6057
6259
|
const origin = req.headers.origin;
|
|
6058
6260
|
const host = req.headers.host;
|
|
6059
|
-
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$
|
|
6261
|
+
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$3(res, 403, "origin-rejected");
|
|
6060
6262
|
let body;
|
|
6061
6263
|
try {
|
|
6062
|
-
body = JSON.parse(await readBody$
|
|
6264
|
+
body = JSON.parse(await readBody$2(req));
|
|
6063
6265
|
} catch {
|
|
6064
|
-
return jsonError$
|
|
6266
|
+
return jsonError$3(res, 400, "invalid-request");
|
|
6065
6267
|
}
|
|
6066
6268
|
const attachment = attachmentFromRequest(body);
|
|
6067
|
-
if (attachment === void 0) return jsonError$
|
|
6269
|
+
if (attachment === void 0) return jsonError$3(res, 400, "invalid-attachment");
|
|
6068
6270
|
try {
|
|
6069
6271
|
const stored = await deps.readImage(attachment);
|
|
6070
6272
|
res.writeHead(200, {
|
|
@@ -6075,10 +6277,10 @@ async function serveImage(req, res, deps) {
|
|
|
6075
6277
|
});
|
|
6076
6278
|
res.end(stored.data);
|
|
6077
6279
|
} catch {
|
|
6078
|
-
jsonError$
|
|
6280
|
+
jsonError$3(res, 404, "image-unavailable");
|
|
6079
6281
|
}
|
|
6080
6282
|
}
|
|
6081
|
-
function sameOrigin$
|
|
6283
|
+
function sameOrigin$3(req) {
|
|
6082
6284
|
const origin = req.headers.origin;
|
|
6083
6285
|
const host = req.headers.host;
|
|
6084
6286
|
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return false;
|
|
@@ -6086,14 +6288,14 @@ function sameOrigin$2(req) {
|
|
|
6086
6288
|
}
|
|
6087
6289
|
/** Safely delete one or more generated image files from the workspace disk. */
|
|
6088
6290
|
async function serveDelete(req, res, deps) {
|
|
6089
|
-
if (req.method !== "POST") return jsonError$
|
|
6090
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$
|
|
6091
|
-
if (!sameOrigin$
|
|
6291
|
+
if (req.method !== "POST") return jsonError$3(res, 405, "method-not-allowed");
|
|
6292
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$3(res, 415, "json-required");
|
|
6293
|
+
if (!sameOrigin$3(req)) return jsonError$3(res, 403, "origin-rejected");
|
|
6092
6294
|
let body;
|
|
6093
6295
|
try {
|
|
6094
|
-
body = JSON.parse(await readBody$
|
|
6296
|
+
body = JSON.parse(await readBody$2(req));
|
|
6095
6297
|
} catch {
|
|
6096
|
-
return jsonError$
|
|
6298
|
+
return jsonError$3(res, 400, "invalid-request");
|
|
6097
6299
|
}
|
|
6098
6300
|
const rawPaths = typeof body === "object" && body !== null && "paths" in body && Array.isArray(body.paths) ? body.paths : [];
|
|
6099
6301
|
const deletedFiles = [];
|
|
@@ -6123,13 +6325,13 @@ async function serveDelete(req, res, deps) {
|
|
|
6123
6325
|
}
|
|
6124
6326
|
/** Serve on-demand image persistence into the workspace. */
|
|
6125
6327
|
async function serveSaveWorkspace(req, res, deps) {
|
|
6126
|
-
if (!sameOrigin$
|
|
6127
|
-
if (req.method !== "POST") return jsonError$
|
|
6128
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$
|
|
6328
|
+
if (!sameOrigin$3(req)) return jsonError$3(res, 403, "origin-rejected");
|
|
6329
|
+
if (req.method !== "POST") return jsonError$3(res, 405, "method-not-allowed");
|
|
6330
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$3(res, 415, "json-required");
|
|
6129
6331
|
try {
|
|
6130
|
-
const raw = JSON.parse(await readBody$
|
|
6332
|
+
const raw = JSON.parse(await readBody$2(req));
|
|
6131
6333
|
const attachment = parseImageAttachmentRef(raw.attachment);
|
|
6132
|
-
if (!attachment) return jsonError$
|
|
6334
|
+
if (!attachment) return jsonError$3(res, 400, "invalid-attachment");
|
|
6133
6335
|
if (!deps.isSaveEnabled()) {
|
|
6134
6336
|
res.writeHead(200, {
|
|
6135
6337
|
"content-type": "application/json",
|
|
@@ -6147,10 +6349,10 @@ async function serveSaveWorkspace(req, res, deps) {
|
|
|
6147
6349
|
try {
|
|
6148
6350
|
targetRoot = await assertWorkspaceAllowed(rawTarget, allowedRoots);
|
|
6149
6351
|
} catch {
|
|
6150
|
-
return jsonError$
|
|
6352
|
+
return jsonError$3(res, 403, "workspace-root-not-allowed");
|
|
6151
6353
|
}
|
|
6152
6354
|
const stored = await deps.readImage(attachment);
|
|
6153
|
-
if (!stored || !stored.data) return jsonError$
|
|
6355
|
+
if (!stored || !stored.data) return jsonError$3(res, 404, "image-not-found");
|
|
6154
6356
|
const savedTo = await deps.saveToWorkspace({
|
|
6155
6357
|
workspaceRoot: targetRoot,
|
|
6156
6358
|
attachmentId: String(attachment.attachmentId),
|
|
@@ -6178,28 +6380,28 @@ async function serveSaveWorkspace(req, res, deps) {
|
|
|
6178
6380
|
}
|
|
6179
6381
|
/** Validate the persisted reference carried by a tool presentation. */
|
|
6180
6382
|
function imageAttachmentFromMeta(meta) {
|
|
6181
|
-
const value = record$
|
|
6383
|
+
const value = record$3(meta);
|
|
6182
6384
|
if (value?.kind !== "dsh-image-gen") return void 0;
|
|
6183
6385
|
return parseImageAttachmentRef(value.attachment);
|
|
6184
6386
|
}
|
|
6185
6387
|
function attachmentFromRequest(value) {
|
|
6186
|
-
return parseImageAttachmentRef(record$
|
|
6388
|
+
return parseImageAttachmentRef(record$3(value)?.attachment);
|
|
6187
6389
|
}
|
|
6188
|
-
function record$
|
|
6390
|
+
function record$3(value) {
|
|
6189
6391
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
6190
6392
|
}
|
|
6191
|
-
async function readBody$
|
|
6393
|
+
async function readBody$2(req) {
|
|
6192
6394
|
const chunks = [];
|
|
6193
6395
|
let bytes = 0;
|
|
6194
6396
|
for await (const chunk of req) {
|
|
6195
6397
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
6196
6398
|
bytes += buffer.byteLength;
|
|
6197
|
-
if (bytes > MAX_BODY_BYTES) throw new Error("request too large");
|
|
6399
|
+
if (bytes > MAX_BODY_BYTES$1) throw new Error("request too large");
|
|
6198
6400
|
chunks.push(buffer);
|
|
6199
6401
|
}
|
|
6200
6402
|
return Buffer.concat(chunks).toString("utf8");
|
|
6201
6403
|
}
|
|
6202
|
-
function jsonError$
|
|
6404
|
+
function jsonError$3(res, status, code) {
|
|
6203
6405
|
res.writeHead(status, {
|
|
6204
6406
|
"content-type": "application/json",
|
|
6205
6407
|
"cache-control": "no-store"
|
|
@@ -6248,7 +6450,7 @@ async function editOpenAICompatibleImage(input) {
|
|
|
6248
6450
|
}
|
|
6249
6451
|
async function parseImageResponse(response, provider, input) {
|
|
6250
6452
|
const text = await readBoundedText$1(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$1);
|
|
6251
|
-
if (!response.ok) throw new Error(`${provider} image request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$1)}`);
|
|
6453
|
+
if (!response.ok) throw new Error(`${provider} image request failed (${response.status}): ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT$1)}`);
|
|
6252
6454
|
let payload;
|
|
6253
6455
|
try {
|
|
6254
6456
|
payload = JSON.parse(text);
|
|
@@ -6256,7 +6458,7 @@ async function parseImageResponse(response, provider, input) {
|
|
|
6256
6458
|
throw new Error(`${provider} image request returned invalid JSON`);
|
|
6257
6459
|
}
|
|
6258
6460
|
const image = firstImage$1(payload);
|
|
6259
|
-
if (image === void 0) throw new Error(`${provider} image request returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
|
|
6461
|
+
if (image === void 0) throw new Error(`${provider} image request returned no image: ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT$1)}`);
|
|
6260
6462
|
if (image.b64_json !== void 0) return {
|
|
6261
6463
|
data: decodeBase64$1(image.b64_json, provider),
|
|
6262
6464
|
mediaType: imageMediaType$2(image.mime_type) ?? "image/png"
|
|
@@ -6385,7 +6587,7 @@ async function editSeedreamImage(input) {
|
|
|
6385
6587
|
})
|
|
6386
6588
|
});
|
|
6387
6589
|
const text = await readBoundedText(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT);
|
|
6388
|
-
if (!response.ok) throw new Error(`seedream image editing failed (${response.status}): ${text.slice(0, ERROR_LIMIT)}`);
|
|
6590
|
+
if (!response.ok) throw new Error(`seedream image editing failed (${response.status}): ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT)}`);
|
|
6389
6591
|
let payload;
|
|
6390
6592
|
try {
|
|
6391
6593
|
payload = JSON.parse(text);
|
|
@@ -6393,7 +6595,7 @@ async function editSeedreamImage(input) {
|
|
|
6393
6595
|
throw new Error("seedream image editing returned invalid JSON");
|
|
6394
6596
|
}
|
|
6395
6597
|
const image = firstImage(payload);
|
|
6396
|
-
if (image === void 0) throw new Error(`seedream image editing returned no image: ${text.slice(0, ERROR_LIMIT)}`);
|
|
6598
|
+
if (image === void 0) throw new Error(`seedream image editing returned no image: ${redactSecrets(text, input.apiKey).slice(0, ERROR_LIMIT)}`);
|
|
6397
6599
|
if (image.b64_json !== void 0) return {
|
|
6398
6600
|
data: decodeBase64(image.b64_json),
|
|
6399
6601
|
mediaType: imageMediaType$1(image.mime_type) ?? "image/png"
|
|
@@ -16978,13 +17180,13 @@ const INSPIRATION_SOURCE_UPDATED_AT = "2026-08-28T10:24:55Z";
|
|
|
16978
17180
|
const INSPIRATION_SOURCE_IMAGE_MIRROR = "https://gpt-image2.canghe.ai";
|
|
16979
17181
|
/** Parse the upstream snapshot defensively before it becomes application data. */
|
|
16980
17182
|
function parseInspirationSnapshot(value, version = INSPIRATION_SOURCE_VERSION, updatedAt = INSPIRATION_SOURCE_UPDATED_AT) {
|
|
16981
|
-
const document = record$
|
|
17183
|
+
const document = record$2(value);
|
|
16982
17184
|
const cases = Array.isArray(document?.cases) ? document.cases : [];
|
|
16983
17185
|
if (cases.length === 0 || cases.length > 2e3) throw new Error("素材索引案例数量无效");
|
|
16984
17186
|
const seen = /* @__PURE__ */ new Set();
|
|
16985
17187
|
const parsedCases = [];
|
|
16986
17188
|
for (const candidate of cases) {
|
|
16987
|
-
const entry = record$
|
|
17189
|
+
const entry = record$2(candidate);
|
|
16988
17190
|
const id = String(entry?.id ?? "").trim();
|
|
16989
17191
|
const title = text(entry?.title, 240);
|
|
16990
17192
|
const prompt = text(entry?.prompt, 8e3);
|
|
@@ -17037,7 +17239,7 @@ function findInspirationCase(catalog, sourceId, caseId) {
|
|
|
17037
17239
|
return catalog.sources.find((candidate) => candidate.id === sourceId)?.cases.find((candidate) => candidate.id === caseId);
|
|
17038
17240
|
}
|
|
17039
17241
|
const BUNDLED_INSPIRATION_CATALOG = parseInspirationSnapshot(awesome_gpt_image_2_default);
|
|
17040
|
-
function record$
|
|
17242
|
+
function record$2(value) {
|
|
17041
17243
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
17042
17244
|
}
|
|
17043
17245
|
function text(value, maxLength) {
|
|
@@ -17094,24 +17296,24 @@ function createInspirationRoute(deps = {}) {
|
|
|
17094
17296
|
let activeCatalog = BUNDLED_INSPIRATION_CATALOG;
|
|
17095
17297
|
let triedRefreshForUnknownCase = false;
|
|
17096
17298
|
return async function serveInspiration(req, res) {
|
|
17097
|
-
if (!sameOrigin$
|
|
17299
|
+
if (!sameOrigin$2(req)) return jsonError$2(res, 403, "origin-rejected");
|
|
17098
17300
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
17099
|
-
if (req.method === "GET" && url.pathname === "/catalog") return json$
|
|
17301
|
+
if (req.method === "GET" && url.pathname === "/catalog") return json$2(res, 200, publicInspirationCatalog(activeCatalog));
|
|
17100
17302
|
if (req.method === "POST" && url.pathname === "/refresh") try {
|
|
17101
17303
|
activeCatalog = await fetchSnapshot(request);
|
|
17102
|
-
return json$
|
|
17304
|
+
return json$2(res, 200, publicInspirationCatalog(activeCatalog));
|
|
17103
17305
|
} catch (error) {
|
|
17104
|
-
return jsonError$
|
|
17306
|
+
return jsonError$2(res, 502, message(error, "refresh-failed"));
|
|
17105
17307
|
}
|
|
17106
17308
|
if (req.method === "POST" && url.pathname === "/cache/clear") {
|
|
17107
17309
|
diskCacheEpoch += 1;
|
|
17108
17310
|
await clearDiskCache();
|
|
17109
|
-
return json$
|
|
17311
|
+
return json$2(res, 200, { ok: true });
|
|
17110
17312
|
}
|
|
17111
17313
|
const match = /^\/image\/([a-z0-9-]{1,80})\/([a-zA-Z0-9_-]{1,120})$/u.exec(url.pathname);
|
|
17112
|
-
if (req.method !== "GET" || match === null) return jsonError$
|
|
17314
|
+
if (req.method !== "GET" || match === null) return jsonError$2(res, 404, "not-found");
|
|
17113
17315
|
const [, sourceId, caseId] = match;
|
|
17114
|
-
if (sourceId !== "awesome-gpt-image-2" || caseId === void 0) return jsonError$
|
|
17316
|
+
if (sourceId !== "awesome-gpt-image-2" || caseId === void 0) return jsonError$2(res, 404, "source-not-found");
|
|
17115
17317
|
let item = findInspirationCase(activeCatalog, sourceId, caseId);
|
|
17116
17318
|
if (item === void 0 && !triedRefreshForUnknownCase) {
|
|
17117
17319
|
triedRefreshForUnknownCase = true;
|
|
@@ -17120,7 +17322,7 @@ function createInspirationRoute(deps = {}) {
|
|
|
17120
17322
|
item = findInspirationCase(activeCatalog, sourceId, caseId);
|
|
17121
17323
|
} catch {}
|
|
17122
17324
|
}
|
|
17123
|
-
if (item === void 0) return jsonError$
|
|
17325
|
+
if (item === void 0) return jsonError$2(res, 404, "case-not-found");
|
|
17124
17326
|
const cacheKey = `${sourceId}_${caseId}`;
|
|
17125
17327
|
const requestEpoch = diskCacheEpoch;
|
|
17126
17328
|
const cached = await readImageCache(cacheKey);
|
|
@@ -17162,7 +17364,7 @@ function createInspirationRoute(deps = {}) {
|
|
|
17162
17364
|
res.end(body);
|
|
17163
17365
|
return;
|
|
17164
17366
|
} catch {}
|
|
17165
|
-
jsonError$
|
|
17367
|
+
jsonError$2(res, 502, "source-image-unavailable");
|
|
17166
17368
|
};
|
|
17167
17369
|
}
|
|
17168
17370
|
/**
|
|
@@ -17237,7 +17439,7 @@ function imageUrls(version, imagePath) {
|
|
|
17237
17439
|
`${REMOTE_RAW}/${commit}/data${imagePath}`
|
|
17238
17440
|
];
|
|
17239
17441
|
}
|
|
17240
|
-
function sameOrigin$
|
|
17442
|
+
function sameOrigin$2(req) {
|
|
17241
17443
|
const origin = req.headers.origin;
|
|
17242
17444
|
const host = req.headers.host;
|
|
17243
17445
|
return origin === void 0 || host === void 0 || origin === `http://${host}` || origin === `https://${host}`;
|
|
@@ -17245,7 +17447,7 @@ function sameOrigin$1(req) {
|
|
|
17245
17447
|
function message(error, fallback) {
|
|
17246
17448
|
return error instanceof Error && error.message.trim() ? error.message : fallback;
|
|
17247
17449
|
}
|
|
17248
|
-
function json$
|
|
17450
|
+
function json$2(res, status, value) {
|
|
17249
17451
|
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
17250
17452
|
res.writeHead(status, {
|
|
17251
17453
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -17254,8 +17456,8 @@ function json$1(res, status, value) {
|
|
|
17254
17456
|
});
|
|
17255
17457
|
res.end(JSON.stringify(value));
|
|
17256
17458
|
}
|
|
17257
|
-
function jsonError$
|
|
17258
|
-
json$
|
|
17459
|
+
function jsonError$2(res, status, error) {
|
|
17460
|
+
json$2(res, status, { error });
|
|
17259
17461
|
}
|
|
17260
17462
|
const MIME_TO_EXT = {
|
|
17261
17463
|
"image/jpeg": ".jpg",
|
|
@@ -17353,18 +17555,6 @@ async function clearDiskCache() {
|
|
|
17353
17555
|
}
|
|
17354
17556
|
//#endregion
|
|
17355
17557
|
//#region lib/types/studio.js
|
|
17356
|
-
const PROVIDER_LABELS = {
|
|
17357
|
-
google: "Google",
|
|
17358
|
-
openai: "OpenAI",
|
|
17359
|
-
seedream: "Seedream",
|
|
17360
|
-
dashscope: "DashScope"
|
|
17361
|
-
};
|
|
17362
|
-
const CREDENTIALS = {
|
|
17363
|
-
google: GOOGLE_API_KEY_ENV,
|
|
17364
|
-
openai: OPENAI_API_KEY_ENV,
|
|
17365
|
-
seedream: SEEDREAM_API_KEY_ENV,
|
|
17366
|
-
dashscope: DASHSCOPE_API_KEY_ENV
|
|
17367
|
-
};
|
|
17368
17558
|
const RATIO_LABELS = {
|
|
17369
17559
|
auto: "自动",
|
|
17370
17560
|
"1:1": "1:1 方形",
|
|
@@ -17378,8 +17568,7 @@ const RATIO_LABELS = {
|
|
|
17378
17568
|
/** Return only browser-safe capability data. */
|
|
17379
17569
|
async function describeStudio(ctx, config) {
|
|
17380
17570
|
const configuredEntries = await Promise.all(CLOUD_IMAGE_PROVIDERS.map(async (provider) => {
|
|
17381
|
-
|
|
17382
|
-
return [provider, credential !== void 0 && credential.value.trim().length > 0];
|
|
17571
|
+
return [provider, await resolveApiKey(ctx, provider) !== void 0];
|
|
17383
17572
|
}));
|
|
17384
17573
|
const configured = Object.fromEntries(configuredEntries);
|
|
17385
17574
|
const profiles = CLOUD_IMAGE_PROVIDERS.map((provider) => studioProfile(config, provider, configured[provider]));
|
|
@@ -17392,10 +17581,9 @@ async function describeStudio(ctx, config) {
|
|
|
17392
17581
|
/** Execute one validated browser workbench request using the existing provider adapters. */
|
|
17393
17582
|
async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceRoot) {
|
|
17394
17583
|
assertAllowed(studioProfile(config, input.provider, true), input);
|
|
17395
|
-
const active = resolveProvider(
|
|
17584
|
+
const active = resolveProvider(withProviderOverrides(config, input.provider, input.model));
|
|
17396
17585
|
if (active.provider === "comfyui") throw new Error("ComfyUI 暂未接入工作台");
|
|
17397
|
-
const credential = await ctx.
|
|
17398
|
-
if (credential === void 0 || credential.value.trim().length === 0) throw new Error(`${PROVIDER_LABELS[input.provider]} 尚未配置 API Key,请先到设置中配置`);
|
|
17586
|
+
const credential = await requireApiKey(ctx, input.provider);
|
|
17399
17587
|
const rawRefs = input.references ?? (input.reference ? [input.reference] : []);
|
|
17400
17588
|
if (input.mode === "edit" && rawRefs.length === 0) throw new Error("图生图需要至少一张参考图");
|
|
17401
17589
|
const sourceImages = input.mode === "edit" ? await Promise.all(rawRefs.map((ref) => readStudioReference(ctx, ref, signal))) : [];
|
|
@@ -17408,7 +17596,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17408
17596
|
const aspectRatio = input.ratio;
|
|
17409
17597
|
const imageSize = input.quality;
|
|
17410
17598
|
generated = input.mode === "edit" ? await editGoogleImage({
|
|
17411
|
-
apiKey: credential
|
|
17599
|
+
apiKey: credential,
|
|
17412
17600
|
endpoint: active.endpoint,
|
|
17413
17601
|
model: active.model,
|
|
17414
17602
|
prompt: input.prompt,
|
|
@@ -17418,7 +17606,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17418
17606
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
17419
17607
|
signal
|
|
17420
17608
|
}) : await generateGoogleImage({
|
|
17421
|
-
apiKey: credential
|
|
17609
|
+
apiKey: credential,
|
|
17422
17610
|
endpoint: active.endpoint,
|
|
17423
17611
|
model: active.model,
|
|
17424
17612
|
prompt: input.prompt,
|
|
@@ -17428,10 +17616,10 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17428
17616
|
signal
|
|
17429
17617
|
});
|
|
17430
17618
|
output = `${aspectRatio}, ${imageSize}`;
|
|
17431
|
-
} else if (active.provider === "openai") {
|
|
17619
|
+
} else if (active.provider === "openai" || active.provider === "openai-compat" || active.provider === "xai" || active.provider === "zhipu") {
|
|
17432
17620
|
const size = openAISize(input.ratio);
|
|
17433
17621
|
generated = input.mode === "edit" ? await editOpenAICompatibleImage({
|
|
17434
|
-
apiKey: credential
|
|
17622
|
+
apiKey: credential,
|
|
17435
17623
|
baseURL: active.baseURL,
|
|
17436
17624
|
model: active.model,
|
|
17437
17625
|
prompt: input.prompt,
|
|
@@ -17440,8 +17628,8 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17440
17628
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
17441
17629
|
signal
|
|
17442
17630
|
}) : await generateOpenAICompatibleImage({
|
|
17443
|
-
provider:
|
|
17444
|
-
apiKey: credential
|
|
17631
|
+
provider: active.provider,
|
|
17632
|
+
apiKey: credential,
|
|
17445
17633
|
baseURL: active.baseURL,
|
|
17446
17634
|
model: active.model,
|
|
17447
17635
|
prompt: input.prompt,
|
|
@@ -17453,7 +17641,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17453
17641
|
} else if (active.provider === "seedream") {
|
|
17454
17642
|
const size = input.quality;
|
|
17455
17643
|
generated = input.mode === "edit" ? await editSeedreamImage({
|
|
17456
|
-
apiKey: credential
|
|
17644
|
+
apiKey: credential,
|
|
17457
17645
|
baseURL: active.baseURL,
|
|
17458
17646
|
model: active.model,
|
|
17459
17647
|
prompt: input.prompt,
|
|
@@ -17463,7 +17651,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17463
17651
|
signal
|
|
17464
17652
|
}) : await generateOpenAICompatibleImage({
|
|
17465
17653
|
provider: "seedream",
|
|
17466
|
-
apiKey: credential
|
|
17654
|
+
apiKey: credential,
|
|
17467
17655
|
baseURL: active.baseURL,
|
|
17468
17656
|
model: active.model,
|
|
17469
17657
|
prompt: input.prompt,
|
|
@@ -17476,7 +17664,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17476
17664
|
if (input.mode === "edit" && sourceImages.length > 3) throw new Error("DashScope (通义万相) 图生图目前最多支持 3 张参考图,请精简后重试");
|
|
17477
17665
|
const size = dashScopeSize(input.ratio);
|
|
17478
17666
|
generated = input.mode === "edit" ? await editDashScopeImage({
|
|
17479
|
-
apiKey: credential
|
|
17667
|
+
apiKey: credential,
|
|
17480
17668
|
endpoint: active.endpoint,
|
|
17481
17669
|
model: active.model,
|
|
17482
17670
|
prompt: input.prompt,
|
|
@@ -17485,7 +17673,7 @@ async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceR
|
|
|
17485
17673
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
17486
17674
|
signal
|
|
17487
17675
|
}) : await generateDashScopeImage({
|
|
17488
|
-
apiKey: credential
|
|
17676
|
+
apiKey: credential,
|
|
17489
17677
|
endpoint: active.endpoint,
|
|
17490
17678
|
model: active.model,
|
|
17491
17679
|
prompt: input.prompt,
|
|
@@ -17578,14 +17766,26 @@ async function runPool(tasks, concurrency = 2) {
|
|
|
17578
17766
|
return results;
|
|
17579
17767
|
}
|
|
17580
17768
|
function studioProfile(config, provider, configured) {
|
|
17581
|
-
|
|
17769
|
+
let active;
|
|
17770
|
+
try {
|
|
17771
|
+
active = resolveProvider(withProviderOverrides(config, provider));
|
|
17772
|
+
} catch {
|
|
17773
|
+
return profile(provider, "", configured, [
|
|
17774
|
+
"1:1",
|
|
17775
|
+
"3:2",
|
|
17776
|
+
"2:3"
|
|
17777
|
+
].map(option), [{
|
|
17778
|
+
value: "standard",
|
|
17779
|
+
label: "标准(推荐)"
|
|
17780
|
+
}], "1:1", "standard");
|
|
17781
|
+
}
|
|
17582
17782
|
if (active.provider === "comfyui") throw new Error("Invalid cloud provider profile");
|
|
17583
17783
|
const model = active.model;
|
|
17584
17784
|
if (provider === "google") return profile(provider, model, configured, ASPECT_RATIOS.map(option), IMAGE_SIZES.map((value) => ({
|
|
17585
17785
|
value,
|
|
17586
17786
|
label: value
|
|
17587
17787
|
})), "1:1", "1K");
|
|
17588
|
-
if (provider === "openai") return profile(provider, model, configured, [
|
|
17788
|
+
if (provider === "openai" || provider === "openai-compat" || provider === "xai" || provider === "zhipu") return profile(provider, model, configured, [
|
|
17589
17789
|
"1:1",
|
|
17590
17790
|
"3:2",
|
|
17591
17791
|
"2:3"
|
|
@@ -17618,7 +17818,7 @@ function studioProfile(config, provider, configured) {
|
|
|
17618
17818
|
function profile(provider, model, configured, ratioOptions, qualityOptions, defaultRatio, defaultQuality) {
|
|
17619
17819
|
return {
|
|
17620
17820
|
provider,
|
|
17621
|
-
label:
|
|
17821
|
+
label: PROVIDER_DISPLAY_NAMES[provider],
|
|
17622
17822
|
model,
|
|
17623
17823
|
configured,
|
|
17624
17824
|
supportsEditing: true,
|
|
@@ -17634,34 +17834,6 @@ function option(value) {
|
|
|
17634
17834
|
label: RATIO_LABELS[value] ?? value
|
|
17635
17835
|
};
|
|
17636
17836
|
}
|
|
17637
|
-
function providerConfig(config, provider, model) {
|
|
17638
|
-
if (model === void 0) return {
|
|
17639
|
-
...config,
|
|
17640
|
-
provider
|
|
17641
|
-
};
|
|
17642
|
-
switch (provider) {
|
|
17643
|
-
case "google": return {
|
|
17644
|
-
...config,
|
|
17645
|
-
provider,
|
|
17646
|
-
googleModel: model
|
|
17647
|
-
};
|
|
17648
|
-
case "openai": return {
|
|
17649
|
-
...config,
|
|
17650
|
-
provider,
|
|
17651
|
-
openaiModel: model
|
|
17652
|
-
};
|
|
17653
|
-
case "seedream": return {
|
|
17654
|
-
...config,
|
|
17655
|
-
provider,
|
|
17656
|
-
seedreamModel: model
|
|
17657
|
-
};
|
|
17658
|
-
case "dashscope": return {
|
|
17659
|
-
...config,
|
|
17660
|
-
provider,
|
|
17661
|
-
dashscopeModel: model
|
|
17662
|
-
};
|
|
17663
|
-
}
|
|
17664
|
-
}
|
|
17665
17837
|
function assertAllowed(profile, input) {
|
|
17666
17838
|
if (input.model !== profile.model) throw new Error("模型配置已变化,请刷新工作台后重试");
|
|
17667
17839
|
if (!profile.ratioOptions.some((option) => option.value === input.ratio)) throw new Error("该 Provider 不支持所选比例");
|
|
@@ -17716,14 +17888,14 @@ function cloudProvider$1(value) {
|
|
|
17716
17888
|
//#region lib/types/studio-route.js
|
|
17717
17889
|
/** Serve workbench capabilities and generation requests without exposing provider credentials. */
|
|
17718
17890
|
async function serveStudio(req, res, deps) {
|
|
17719
|
-
if (!sameOrigin(req)) return jsonError(res, 403, "origin-rejected");
|
|
17891
|
+
if (!sameOrigin$1(req)) return jsonError$1(res, 403, "origin-rejected");
|
|
17720
17892
|
if (req.method === "GET") try {
|
|
17721
|
-
return json(res, 200, await deps.describe());
|
|
17893
|
+
return json$1(res, 200, await deps.describe());
|
|
17722
17894
|
} catch (error) {
|
|
17723
|
-
return jsonError(res, 500, errorMessage(error, "studio-unavailable"));
|
|
17895
|
+
return jsonError$1(res, 500, errorMessage(error, "studio-unavailable"));
|
|
17724
17896
|
}
|
|
17725
|
-
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
17726
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError(res, 415, "json-required");
|
|
17897
|
+
if (req.method !== "POST") return jsonError$1(res, 405, "method-not-allowed");
|
|
17898
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$1(res, 415, "json-required");
|
|
17727
17899
|
const controller = new AbortController();
|
|
17728
17900
|
const onConnectionClose = () => {
|
|
17729
17901
|
if (!res.writableEnded) controller.abort(/* @__PURE__ */ new Error("The browser closed the image generation request."));
|
|
@@ -17736,15 +17908,15 @@ async function serveStudio(req, res, deps) {
|
|
|
17736
17908
|
try {
|
|
17737
17909
|
let input;
|
|
17738
17910
|
try {
|
|
17739
|
-
input = parseStudioGenerateRequest(JSON.parse(await readBody(req, deps.maxBodyBytes)));
|
|
17911
|
+
input = parseStudioGenerateRequest(JSON.parse(await readBody$1(req, deps.maxBodyBytes)));
|
|
17740
17912
|
} catch (error) {
|
|
17741
|
-
return jsonError(res, 400, errorMessage(error, "invalid-request"));
|
|
17913
|
+
return jsonError$1(res, 400, errorMessage(error, "invalid-request"));
|
|
17742
17914
|
}
|
|
17743
17915
|
const output = await deps.generate(input, controller.signal);
|
|
17744
|
-
if (!res.headersSent && !res.writableEnded && !res.destroyed) json(res, 200, output);
|
|
17916
|
+
if (!res.headersSent && !res.writableEnded && !res.destroyed) json$1(res, 200, output);
|
|
17745
17917
|
} catch (error) {
|
|
17746
17918
|
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
17747
|
-
jsonError(res, controller.signal.aborted ? 499 : 502, errorMessage(error, "generation-failed"));
|
|
17919
|
+
jsonError$1(res, controller.signal.aborted ? 499 : 502, errorMessage(error, "generation-failed"));
|
|
17748
17920
|
} finally {
|
|
17749
17921
|
res.off("close", onConnectionClose);
|
|
17750
17922
|
req.off("aborted", onReqAborted);
|
|
@@ -17752,7 +17924,7 @@ async function serveStudio(req, res, deps) {
|
|
|
17752
17924
|
}
|
|
17753
17925
|
/** Strictly validate the small untrusted workbench wire contract. */
|
|
17754
17926
|
function parseStudioGenerateRequest(value) {
|
|
17755
|
-
const input = record(value);
|
|
17927
|
+
const input = record$1(value);
|
|
17756
17928
|
if (input === void 0) throw new Error("请求格式无效");
|
|
17757
17929
|
if (input.mode !== "generate" && input.mode !== "edit") throw new Error("请选择生成类型");
|
|
17758
17930
|
if (!cloudProvider(input.provider)) throw new Error("不支持该图像 Provider");
|
|
@@ -17785,7 +17957,7 @@ function parseStudioGenerateRequest(value) {
|
|
|
17785
17957
|
};
|
|
17786
17958
|
}
|
|
17787
17959
|
function parseReference(value) {
|
|
17788
|
-
const reference = record(value);
|
|
17960
|
+
const reference = record$1(value);
|
|
17789
17961
|
if (reference === void 0) throw new Error("参考图格式无效");
|
|
17790
17962
|
if ("attachment" in reference) {
|
|
17791
17963
|
const attachment = parseImageAttachmentRef(reference.attachment);
|
|
@@ -17813,15 +17985,15 @@ function cloudProvider(value) {
|
|
|
17813
17985
|
function imageMediaType(value) {
|
|
17814
17986
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
17815
17987
|
}
|
|
17816
|
-
function sameOrigin(req) {
|
|
17988
|
+
function sameOrigin$1(req) {
|
|
17817
17989
|
const origin = req.headers.origin;
|
|
17818
17990
|
const host = req.headers.host;
|
|
17819
17991
|
return origin === void 0 || host === void 0 || origin === `http://${host}` || origin === `https://${host}`;
|
|
17820
17992
|
}
|
|
17821
|
-
function record(value) {
|
|
17993
|
+
function record$1(value) {
|
|
17822
17994
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
17823
17995
|
}
|
|
17824
|
-
async function readBody(req, maxBytes) {
|
|
17996
|
+
async function readBody$1(req, maxBytes) {
|
|
17825
17997
|
const chunks = [];
|
|
17826
17998
|
let bytes = 0;
|
|
17827
17999
|
for await (const chunk of req) {
|
|
@@ -17835,6 +18007,333 @@ async function readBody(req, maxBytes) {
|
|
|
17835
18007
|
function errorMessage(error, fallback) {
|
|
17836
18008
|
return error instanceof Error && error.message.trim().length > 0 ? error.message : fallback;
|
|
17837
18009
|
}
|
|
18010
|
+
function json$1(res, status, value) {
|
|
18011
|
+
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
18012
|
+
res.writeHead(status, {
|
|
18013
|
+
"content-type": "application/json; charset=utf-8",
|
|
18014
|
+
"cache-control": "no-store"
|
|
18015
|
+
});
|
|
18016
|
+
res.end(JSON.stringify(value));
|
|
18017
|
+
}
|
|
18018
|
+
function jsonError$1(res, status, message) {
|
|
18019
|
+
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
18020
|
+
json$1(res, status, { error: message });
|
|
18021
|
+
}
|
|
18022
|
+
//#endregion
|
|
18023
|
+
//#region lib/types/test-route.js
|
|
18024
|
+
const MAX_BODY_BYTES = 16384;
|
|
18025
|
+
const PROBE_TIMEOUT_MS = 1e4;
|
|
18026
|
+
/**
|
|
18027
|
+
* The endpoint each provider is probed on. All four cloud providers expose a
|
|
18028
|
+
* model-list GET that authenticates with the same credential image generation
|
|
18029
|
+
* uses; ComfyUI exposes its standard `system_stats` health endpoint.
|
|
18030
|
+
*/
|
|
18031
|
+
function probeTarget(provider, config, apiKey) {
|
|
18032
|
+
if (provider === "comfyui") return {
|
|
18033
|
+
url: joinUrl(config.comfyuiBaseURL ?? "http://127.0.0.1:8188", "system_stats"),
|
|
18034
|
+
headers: {}
|
|
18035
|
+
};
|
|
18036
|
+
if (provider === "google") return {
|
|
18037
|
+
url: googleModelsUrl(config),
|
|
18038
|
+
headers: { "x-goog-api-key": apiKey ?? "" }
|
|
18039
|
+
};
|
|
18040
|
+
const headers = { authorization: `Bearer ${apiKey ?? ""}` };
|
|
18041
|
+
if (provider === "dashscope") return {
|
|
18042
|
+
url: joinUrl(config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1", "models"),
|
|
18043
|
+
headers
|
|
18044
|
+
};
|
|
18045
|
+
if (provider === "seedream") return {
|
|
18046
|
+
url: joinUrl(config.seedreamBaseURL ?? "https://ark.cn-beijing.volces.com/api/v3", "models"),
|
|
18047
|
+
headers
|
|
18048
|
+
};
|
|
18049
|
+
if (provider === "openai-compat") {
|
|
18050
|
+
const base = config.openaiCompatBaseURL?.trim() ?? "";
|
|
18051
|
+
if (base.length === 0) throw new Error("OpenAI-compatible base URL is not configured");
|
|
18052
|
+
return {
|
|
18053
|
+
url: joinUrl(base, "models"),
|
|
18054
|
+
headers
|
|
18055
|
+
};
|
|
18056
|
+
}
|
|
18057
|
+
if (provider === "xai") return {
|
|
18058
|
+
url: joinUrl(config.xaiBaseURL ?? "https://api.x.ai/v1", "models"),
|
|
18059
|
+
headers
|
|
18060
|
+
};
|
|
18061
|
+
if (provider === "zhipu") return {
|
|
18062
|
+
url: joinUrl(config.zhipuBaseURL ?? "https://open.bigmodel.cn/api/paas/v4", "models"),
|
|
18063
|
+
headers
|
|
18064
|
+
};
|
|
18065
|
+
return {
|
|
18066
|
+
url: joinUrl(config.openaiBaseURL ?? "https://api.openai.com/v1", "models"),
|
|
18067
|
+
headers
|
|
18068
|
+
};
|
|
18069
|
+
}
|
|
18070
|
+
/** `…/v1beta/interactions` (or any trailing path) becomes `…/v1beta/models`. */
|
|
18071
|
+
function googleModelsUrl(config) {
|
|
18072
|
+
const endpoint = config.googleEndpoint;
|
|
18073
|
+
const fallback = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
18074
|
+
if (typeof endpoint !== "string" || endpoint.trim().length === 0) return fallback;
|
|
18075
|
+
try {
|
|
18076
|
+
const url = new URL(endpoint);
|
|
18077
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
18078
|
+
segments[segments.length - 1] = "models";
|
|
18079
|
+
url.pathname = `/${segments.join("/")}`;
|
|
18080
|
+
return url.toString();
|
|
18081
|
+
} catch {
|
|
18082
|
+
throw new Error("Google endpoint must be an absolute URL");
|
|
18083
|
+
}
|
|
18084
|
+
}
|
|
18085
|
+
function joinUrl(base, path) {
|
|
18086
|
+
return new URL(path, base.endsWith("/") ? base : `${base}/`).toString();
|
|
18087
|
+
}
|
|
18088
|
+
/** Extract bare model ids from a Google `list models` payload (`models/{id}`). */
|
|
18089
|
+
function parseGoogleModelIds(payload) {
|
|
18090
|
+
if (typeof payload !== "object" || payload === null) return [];
|
|
18091
|
+
const models = payload.models;
|
|
18092
|
+
if (!Array.isArray(models)) return [];
|
|
18093
|
+
const ids = [];
|
|
18094
|
+
for (const entry of models) {
|
|
18095
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
18096
|
+
const name = entry.name;
|
|
18097
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
18098
|
+
ids.push(name.startsWith("models/") ? name.slice(7) : name);
|
|
18099
|
+
}
|
|
18100
|
+
return ids;
|
|
18101
|
+
}
|
|
18102
|
+
/** Heuristic filter: Gemini image models carry `image`/`imagen` in the id. */
|
|
18103
|
+
function filterImageModelIds(ids) {
|
|
18104
|
+
return ids.filter((id) => /image|imagen/i.test(id));
|
|
18105
|
+
}
|
|
18106
|
+
/** Narrow filter for the official OpenAI catalog: gpt-image and DALL·E families. */
|
|
18107
|
+
function filterOpenAIImageModelIds(ids) {
|
|
18108
|
+
return ids.filter((id) => /^(gpt-image|dall-e)/i.test(id));
|
|
18109
|
+
}
|
|
18110
|
+
/** Broad filter for relays: common image-model id fragments across vendors. */
|
|
18111
|
+
function filterRelayImageModelIds(ids) {
|
|
18112
|
+
return ids.filter((id) => /(^|[-_.])(image|imagen|flux|seedream|seededit|sd|sdxl|sd3|cogview|janus|wan|kolors|hunyuan-image)/i.test(id) || /^(dall-e|gpt-image)/i.test(id));
|
|
18113
|
+
}
|
|
18114
|
+
/** Ark catalog filter: the Seedream (generation) and SeedEdit (editing) families. */
|
|
18115
|
+
function filterSeedreamImageModelIds(ids) {
|
|
18116
|
+
return ids.filter((id) => /seedream|seededit/i.test(id));
|
|
18117
|
+
}
|
|
18118
|
+
/** Zhipu catalog filter: GLM-Image and CogView families. */
|
|
18119
|
+
function filterZhipuImageModelIds(ids) {
|
|
18120
|
+
return ids.filter((id) => /glm-image|cogview/i.test(id));
|
|
18121
|
+
}
|
|
18122
|
+
/** Extract model ids from an OpenAI-style `{data:[{id}]}` or xAI-style `{models:[{id}]}` payload. */
|
|
18123
|
+
function parseOpenAIModelIds(payload) {
|
|
18124
|
+
if (typeof payload !== "object" || payload === null) return [];
|
|
18125
|
+
const data = payload.data;
|
|
18126
|
+
const models = payload.models;
|
|
18127
|
+
const entries = Array.isArray(data) ? data : Array.isArray(models) ? models : void 0;
|
|
18128
|
+
if (entries === void 0) return [];
|
|
18129
|
+
const ids = [];
|
|
18130
|
+
for (const entry of entries) {
|
|
18131
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
18132
|
+
const id = entry.id;
|
|
18133
|
+
if (typeof id === "string" && id.length > 0) ids.push(id);
|
|
18134
|
+
}
|
|
18135
|
+
return ids;
|
|
18136
|
+
}
|
|
18137
|
+
/**
|
|
18138
|
+
* Extract image-capable model ids from a DashScope native
|
|
18139
|
+
* `{output:{models:[{model,capabilities}]}}` payload. The `capabilities`
|
|
18140
|
+
* array marks image generation as `IG`; name fragments are a fallback for
|
|
18141
|
+
* payloads that omit the field.
|
|
18142
|
+
*/
|
|
18143
|
+
function parseDashScopeImageModelIds(payload) {
|
|
18144
|
+
if (typeof payload !== "object" || payload === null) return [];
|
|
18145
|
+
const output = payload.output;
|
|
18146
|
+
if (typeof output !== "object" || output === null) return [];
|
|
18147
|
+
const models = output.models;
|
|
18148
|
+
if (!Array.isArray(models)) return [];
|
|
18149
|
+
const ids = [];
|
|
18150
|
+
for (const entry of models) {
|
|
18151
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
18152
|
+
const model = entry.model;
|
|
18153
|
+
if (typeof model !== "string" || model.length === 0) continue;
|
|
18154
|
+
const capabilities = entry.capabilities;
|
|
18155
|
+
if (Array.isArray(capabilities) && capabilities.includes("IG") || /wanx|qwen-image|wan2|image/i.test(model)) ids.push(model);
|
|
18156
|
+
}
|
|
18157
|
+
return ids;
|
|
18158
|
+
}
|
|
18159
|
+
/** Gemini list-models pagination cap; one page comfortably covers the catalog. */
|
|
18160
|
+
const GOOGLE_MODELS_PAGE_SIZE = 1e3;
|
|
18161
|
+
/** Fetch with the shared timeout and classify the outcome; secrets never leave redacted. */
|
|
18162
|
+
async function fetchClassifiedJson(url, headers, apiKey, signal) {
|
|
18163
|
+
const response = await fetch(url, {
|
|
18164
|
+
method: "GET",
|
|
18165
|
+
headers,
|
|
18166
|
+
signal: signal ?? AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
18167
|
+
redirect: "follow"
|
|
18168
|
+
}).catch((error) => {
|
|
18169
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
18170
|
+
});
|
|
18171
|
+
if (response.ok) return {
|
|
18172
|
+
ok: true,
|
|
18173
|
+
payload: await response.json().catch(() => null)
|
|
18174
|
+
};
|
|
18175
|
+
if (response.status === 401 || response.status === 403) return {
|
|
18176
|
+
ok: false,
|
|
18177
|
+
reason: "unauthorized"
|
|
18178
|
+
};
|
|
18179
|
+
const text = await response.text().catch(() => "");
|
|
18180
|
+
return {
|
|
18181
|
+
ok: false,
|
|
18182
|
+
reason: "error",
|
|
18183
|
+
message: `HTTP ${String(response.status)}${text.length > 0 ? `: ${redactSecrets(text, apiKey).slice(0, 300)}` : ""}`
|
|
18184
|
+
};
|
|
18185
|
+
}
|
|
18186
|
+
/** Pull and filter the image-capable models from the Google endpoint. */
|
|
18187
|
+
async function fetchGoogleImageModels(config, apiKey, signal) {
|
|
18188
|
+
if (apiKey === void 0) return {
|
|
18189
|
+
ok: false,
|
|
18190
|
+
reason: "missing-key"
|
|
18191
|
+
};
|
|
18192
|
+
const url = new URL(googleModelsUrl(config));
|
|
18193
|
+
url.searchParams.set("pageSize", String(GOOGLE_MODELS_PAGE_SIZE));
|
|
18194
|
+
const result = await fetchClassifiedJson(url, { "x-goog-api-key": apiKey }, apiKey, signal);
|
|
18195
|
+
if (!result.ok) return result;
|
|
18196
|
+
return {
|
|
18197
|
+
ok: true,
|
|
18198
|
+
models: filterImageModelIds(parseGoogleModelIds(result.payload))
|
|
18199
|
+
};
|
|
18200
|
+
}
|
|
18201
|
+
/** Pull and filter image-capable models from an OpenAI-compatible endpoint (official, relay, Ark, xAI, Zhipu). */
|
|
18202
|
+
async function fetchOpenAIImageModels(provider, config, apiKey, signal) {
|
|
18203
|
+
if (apiKey === void 0) return {
|
|
18204
|
+
ok: false,
|
|
18205
|
+
reason: "missing-key"
|
|
18206
|
+
};
|
|
18207
|
+
const configured = provider === "openai" ? config.openaiBaseURL?.trim() ?? "" : provider === "openai-compat" ? config.openaiCompatBaseURL?.trim() ?? "" : provider === "seedream" ? config.seedreamBaseURL?.trim() ?? "" : provider === "xai" ? config.xaiBaseURL?.trim() ?? "" : config.zhipuBaseURL?.trim() ?? "";
|
|
18208
|
+
const fallback = provider === "openai" ? DEFAULT_OPENAI_BASE_URL : provider === "seedream" ? DEFAULT_SEEDREAM_BASE_URL : provider === "xai" ? DEFAULT_XAI_BASE_URL : provider === "zhipu" ? DEFAULT_ZHIPU_BASE_URL : "";
|
|
18209
|
+
const base = configured.length > 0 ? configured : fallback;
|
|
18210
|
+
if (base.length === 0) return {
|
|
18211
|
+
ok: false,
|
|
18212
|
+
reason: "error",
|
|
18213
|
+
message: "Base URL is not configured"
|
|
18214
|
+
};
|
|
18215
|
+
const result = await fetchClassifiedJson(joinUrl(base, provider === "xai" ? "image-generation-models" : "models"), { authorization: `Bearer ${apiKey}` }, apiKey, signal);
|
|
18216
|
+
if (!result.ok) return result;
|
|
18217
|
+
const ids = parseOpenAIModelIds(result.payload);
|
|
18218
|
+
return {
|
|
18219
|
+
ok: true,
|
|
18220
|
+
models: provider === "openai" ? filterOpenAIImageModelIds(ids) : provider === "openai-compat" ? filterRelayImageModelIds(ids) : provider === "seedream" ? filterSeedreamImageModelIds(ids) : provider === "xai" ? ids : filterZhipuImageModelIds(ids)
|
|
18221
|
+
};
|
|
18222
|
+
}
|
|
18223
|
+
/** Pull and filter image-capable models from the DashScope native model list (`capabilities=IG`). */
|
|
18224
|
+
async function fetchDashScopeImageModels(config, apiKey, signal) {
|
|
18225
|
+
if (apiKey === void 0) return {
|
|
18226
|
+
ok: false,
|
|
18227
|
+
reason: "missing-key"
|
|
18228
|
+
};
|
|
18229
|
+
const configured = config.dashscopeEndpoint?.trim() ?? "";
|
|
18230
|
+
const base = configured.length > 0 ? configured : DEFAULT_DASHSCOPE_ENDPOINT;
|
|
18231
|
+
const url = new URL(joinUrl(base, "models"));
|
|
18232
|
+
url.searchParams.set("capabilities", "IG");
|
|
18233
|
+
url.searchParams.set("page_no", "1");
|
|
18234
|
+
url.searchParams.set("page_size", "100");
|
|
18235
|
+
const result = await fetchClassifiedJson(url, { authorization: `Bearer ${apiKey}` }, apiKey, signal);
|
|
18236
|
+
if (!result.ok) return result;
|
|
18237
|
+
return {
|
|
18238
|
+
ok: true,
|
|
18239
|
+
models: parseDashScopeImageModelIds(result.payload)
|
|
18240
|
+
};
|
|
18241
|
+
}
|
|
18242
|
+
/** Run one provider probe and classify the outcome; secrets never leave redacted. */
|
|
18243
|
+
async function probeProviderConnection(provider, config, apiKey, signal) {
|
|
18244
|
+
if (apiKey === void 0) return {
|
|
18245
|
+
ok: false,
|
|
18246
|
+
reason: "missing-key"
|
|
18247
|
+
};
|
|
18248
|
+
if (provider === "openai-compat" && (config.openaiCompatBaseURL?.trim() ?? "").length === 0) return {
|
|
18249
|
+
ok: false,
|
|
18250
|
+
reason: "error",
|
|
18251
|
+
message: "Base URL is not configured"
|
|
18252
|
+
};
|
|
18253
|
+
const target = probeTarget(provider, config, apiKey);
|
|
18254
|
+
const result = await fetchClassifiedJson(target.url, target.headers, apiKey, signal);
|
|
18255
|
+
return result.ok ? { ok: true } : result;
|
|
18256
|
+
}
|
|
18257
|
+
/** Probe the local ComfyUI service without any credential. */
|
|
18258
|
+
async function probeComfyUIConnection(config, signal) {
|
|
18259
|
+
const target = probeTarget("comfyui", config);
|
|
18260
|
+
const response = await fetch(target.url, {
|
|
18261
|
+
method: "GET",
|
|
18262
|
+
signal: signal ?? AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
18263
|
+
redirect: "follow"
|
|
18264
|
+
}).catch((error) => {
|
|
18265
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
18266
|
+
});
|
|
18267
|
+
if (response.ok) return { ok: true };
|
|
18268
|
+
return {
|
|
18269
|
+
ok: false,
|
|
18270
|
+
reason: "error",
|
|
18271
|
+
message: `HTTP ${String(response.status)}`
|
|
18272
|
+
};
|
|
18273
|
+
}
|
|
18274
|
+
/** Serve the settings card's per-provider connectivity probe. */
|
|
18275
|
+
async function serveTestConnection(req, res, deps) {
|
|
18276
|
+
if (!sameOrigin(req)) return jsonError(res, 403, "origin-rejected");
|
|
18277
|
+
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
18278
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError(res, 415, "json-required");
|
|
18279
|
+
let body;
|
|
18280
|
+
try {
|
|
18281
|
+
body = JSON.parse(await readBody(req));
|
|
18282
|
+
} catch {
|
|
18283
|
+
return jsonError(res, 400, "invalid-request");
|
|
18284
|
+
}
|
|
18285
|
+
const provider = record(body)?.provider;
|
|
18286
|
+
if (typeof provider !== "string" || !IMAGE_PROVIDERS.includes(provider)) return jsonError(res, 400, "invalid-provider");
|
|
18287
|
+
if (record(body)?.action === "models") {
|
|
18288
|
+
if (provider === "comfyui") return jsonError(res, 400, "models-unsupported");
|
|
18289
|
+
let result;
|
|
18290
|
+
try {
|
|
18291
|
+
const apiKey = await deps.resolveKey(provider);
|
|
18292
|
+
result = provider === "google" ? await fetchGoogleImageModels(deps.config(), apiKey) : provider === "dashscope" ? await fetchDashScopeImageModels(deps.config(), apiKey) : await fetchOpenAIImageModels(provider, deps.config(), apiKey);
|
|
18293
|
+
} catch (error) {
|
|
18294
|
+
result = {
|
|
18295
|
+
ok: false,
|
|
18296
|
+
reason: "error",
|
|
18297
|
+
message: error instanceof Error ? error.message : String(error)
|
|
18298
|
+
};
|
|
18299
|
+
}
|
|
18300
|
+
return json(res, 200, result);
|
|
18301
|
+
}
|
|
18302
|
+
let result;
|
|
18303
|
+
try {
|
|
18304
|
+
if (provider === "comfyui") result = await probeComfyUIConnection(deps.config());
|
|
18305
|
+
else {
|
|
18306
|
+
const apiKey = await deps.resolveKey(provider);
|
|
18307
|
+
result = await probeProviderConnection(provider, deps.config(), apiKey);
|
|
18308
|
+
}
|
|
18309
|
+
} catch (error) {
|
|
18310
|
+
result = {
|
|
18311
|
+
ok: false,
|
|
18312
|
+
reason: "error",
|
|
18313
|
+
message: error instanceof Error ? error.message : String(error)
|
|
18314
|
+
};
|
|
18315
|
+
}
|
|
18316
|
+
json(res, 200, result);
|
|
18317
|
+
}
|
|
18318
|
+
function sameOrigin(req) {
|
|
18319
|
+
const origin = req.headers.origin;
|
|
18320
|
+
const host = req.headers.host;
|
|
18321
|
+
return origin === void 0 || host === void 0 || origin === `http://${host}` || origin === `https://${host}`;
|
|
18322
|
+
}
|
|
18323
|
+
function record(value) {
|
|
18324
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
18325
|
+
}
|
|
18326
|
+
async function readBody(req) {
|
|
18327
|
+
const chunks = [];
|
|
18328
|
+
let bytes = 0;
|
|
18329
|
+
for await (const chunk of req) {
|
|
18330
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
18331
|
+
bytes += buffer.byteLength;
|
|
18332
|
+
if (bytes > MAX_BODY_BYTES) throw new Error("request too large");
|
|
18333
|
+
chunks.push(buffer);
|
|
18334
|
+
}
|
|
18335
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
18336
|
+
}
|
|
17838
18337
|
function json(res, status, value) {
|
|
17839
18338
|
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
17840
18339
|
res.writeHead(status, {
|
|
@@ -17843,9 +18342,9 @@ function json(res, status, value) {
|
|
|
17843
18342
|
});
|
|
17844
18343
|
res.end(JSON.stringify(value));
|
|
17845
18344
|
}
|
|
17846
|
-
function jsonError(res, status,
|
|
18345
|
+
function jsonError(res, status, code) {
|
|
17847
18346
|
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
17848
|
-
json(res, status, { error:
|
|
18347
|
+
json(res, status, { error: code });
|
|
17849
18348
|
}
|
|
17850
18349
|
//#endregion
|
|
17851
18350
|
//#region lib/types/index.js
|
|
@@ -17856,12 +18355,18 @@ const inject = [
|
|
|
17856
18355
|
"credentials",
|
|
17857
18356
|
"webServer"
|
|
17858
18357
|
];
|
|
18358
|
+
/** Validate the untrusted per-call provider override from tool arguments. */
|
|
18359
|
+
function providerOverrideOf(value) {
|
|
18360
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
18361
|
+
if (typeof value !== "string" || !IMAGE_PROVIDERS.includes(value)) throw new Error(`Unsupported provider ${JSON.stringify(value)}. Supported providers: ${IMAGE_PROVIDERS.join(", ")}.`);
|
|
18362
|
+
return value;
|
|
18363
|
+
}
|
|
17859
18364
|
function apply(ctx, config = {}) {
|
|
17860
|
-
let current = () => config;
|
|
18365
|
+
let current = () => migrateOpenAICompatConfig(config);
|
|
17861
18366
|
const knownWorkspaceRoots = /* @__PURE__ */ new Set();
|
|
17862
18367
|
installImageSettings(ctx, config, {
|
|
17863
18368
|
setSource: (source) => {
|
|
17864
|
-
current = source;
|
|
18369
|
+
current = () => migrateOpenAICompatConfig(source());
|
|
17865
18370
|
},
|
|
17866
18371
|
onChange: () => {}
|
|
17867
18372
|
});
|
|
@@ -17909,6 +18414,14 @@ function apply(ctx, config = {}) {
|
|
|
17909
18414
|
isSaveEnabled: () => current().saveToWorkspace !== false
|
|
17910
18415
|
})
|
|
17911
18416
|
}), "dsh-image-gen: save workspace route");
|
|
18417
|
+
ctx.effect(() => ctx.webServer.register({
|
|
18418
|
+
kind: "exact",
|
|
18419
|
+
path: TEST_CONNECTION_ROUTE,
|
|
18420
|
+
handler: (req, res) => serveTestConnection(req, res, {
|
|
18421
|
+
resolveKey: (provider) => resolveApiKey(ctx, provider),
|
|
18422
|
+
config: () => current()
|
|
18423
|
+
})
|
|
18424
|
+
}), "dsh-image-gen: test connection route");
|
|
17912
18425
|
ctx.effect(() => ctx.webServer.register({
|
|
17913
18426
|
kind: "exact",
|
|
17914
18427
|
path: STUDIO_ROUTE,
|
|
@@ -17942,13 +18455,31 @@ function apply(ctx, config = {}) {
|
|
|
17942
18455
|
}), "dsh-image-gen: inspiration route");
|
|
17943
18456
|
ctx.tools.register(defineTool({
|
|
17944
18457
|
name: "generate_image",
|
|
17945
|
-
description: "Generate a new image with the configured provider. Use when the user asks to create or draw a new image; use edit_image instead when they want to change an existing image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is attached directly to the conversation and may also be saved under the session workspace. Do not call read, glob, or other tools to locate or verify the image.",
|
|
18458
|
+
description: "Generate a new image with the configured provider. Use when the user asks to create or draw a new image; use edit_image instead when they want to change an existing image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. The optional provider/model arguments switch provider or model for this call only when the user asks for a specific one. A successful image is attached directly to the conversation and may also be saved under the session workspace. Do not call read, glob, or other tools to locate or verify the image.",
|
|
17946
18459
|
parameters: {
|
|
17947
18460
|
prompt: {
|
|
17948
18461
|
type: "string",
|
|
17949
18462
|
required: true,
|
|
17950
18463
|
description: "Complete description of the image to generate."
|
|
17951
18464
|
},
|
|
18465
|
+
provider: {
|
|
18466
|
+
type: "string",
|
|
18467
|
+
enum: [
|
|
18468
|
+
"google",
|
|
18469
|
+
"openai",
|
|
18470
|
+
"openai-compat",
|
|
18471
|
+
"seedream",
|
|
18472
|
+
"dashscope",
|
|
18473
|
+
"xai",
|
|
18474
|
+
"zhipu",
|
|
18475
|
+
"comfyui"
|
|
18476
|
+
],
|
|
18477
|
+
description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default."
|
|
18478
|
+
},
|
|
18479
|
+
model: {
|
|
18480
|
+
type: "string",
|
|
18481
|
+
description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead)."
|
|
18482
|
+
},
|
|
17952
18483
|
aspect_ratio: {
|
|
17953
18484
|
type: "string",
|
|
17954
18485
|
enum: [
|
|
@@ -17982,7 +18513,7 @@ function apply(ctx, config = {}) {
|
|
|
17982
18513
|
},
|
|
17983
18514
|
output: imageOutput("Generated"),
|
|
17984
18515
|
async execute(args, exec) {
|
|
17985
|
-
const active = resolveProvider(current());
|
|
18516
|
+
const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
|
|
17986
18517
|
if (active.provider === "comfyui") {
|
|
17987
18518
|
const workflow = selectComfyUIWorkflow(active, args.workflow);
|
|
17988
18519
|
return saveGenerated(ctx, await generateComfyUIImage({
|
|
@@ -17994,13 +18525,12 @@ function apply(ctx, config = {}) {
|
|
|
17994
18525
|
signal: exec.signal
|
|
17995
18526
|
}), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
|
|
17996
18527
|
}
|
|
17997
|
-
const credential = await ctx
|
|
17998
|
-
if (credential === void 0 || credential.value.length === 0) throw new Error(`generate_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
|
|
18528
|
+
const credential = await requireApiKey(ctx, active.provider, "generate_image");
|
|
17999
18529
|
if (active.provider === "google") {
|
|
18000
18530
|
const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
|
|
18001
18531
|
const imageSize = args.image_size ?? active.imageSize;
|
|
18002
18532
|
return saveGenerated(ctx, await generateGoogleImage({
|
|
18003
|
-
apiKey: credential
|
|
18533
|
+
apiKey: credential,
|
|
18004
18534
|
endpoint: active.endpoint,
|
|
18005
18535
|
model: active.model,
|
|
18006
18536
|
prompt: args.prompt,
|
|
@@ -18013,7 +18543,7 @@ function apply(ctx, config = {}) {
|
|
|
18013
18543
|
if (active.provider === "dashscope") {
|
|
18014
18544
|
const size = args.size ?? active.imageSize;
|
|
18015
18545
|
return saveGenerated(ctx, await generateDashScopeImage({
|
|
18016
|
-
apiKey: credential
|
|
18546
|
+
apiKey: credential,
|
|
18017
18547
|
endpoint: active.endpoint,
|
|
18018
18548
|
model: active.model,
|
|
18019
18549
|
prompt: args.prompt,
|
|
@@ -18025,7 +18555,7 @@ function apply(ctx, config = {}) {
|
|
|
18025
18555
|
const size = args.size ?? active.imageSize;
|
|
18026
18556
|
return saveGenerated(ctx, await generateOpenAICompatibleImage({
|
|
18027
18557
|
provider: active.provider,
|
|
18028
|
-
apiKey: credential
|
|
18558
|
+
apiKey: credential,
|
|
18029
18559
|
baseURL: active.baseURL,
|
|
18030
18560
|
model: active.model,
|
|
18031
18561
|
prompt: args.prompt,
|
|
@@ -18045,6 +18575,24 @@ function apply(ctx, config = {}) {
|
|
|
18045
18575
|
required: true,
|
|
18046
18576
|
description: "Describe the changes to make while preserving everything else that should remain."
|
|
18047
18577
|
},
|
|
18578
|
+
provider: {
|
|
18579
|
+
type: "string",
|
|
18580
|
+
enum: [
|
|
18581
|
+
"google",
|
|
18582
|
+
"openai",
|
|
18583
|
+
"openai-compat",
|
|
18584
|
+
"seedream",
|
|
18585
|
+
"dashscope",
|
|
18586
|
+
"xai",
|
|
18587
|
+
"zhipu",
|
|
18588
|
+
"comfyui"
|
|
18589
|
+
],
|
|
18590
|
+
description: "Optional provider for this call only (for example when the user asks to use a specific provider); omit to use the configured default."
|
|
18591
|
+
},
|
|
18592
|
+
model: {
|
|
18593
|
+
type: "string",
|
|
18594
|
+
description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead)."
|
|
18595
|
+
},
|
|
18048
18596
|
source_attachment_id: {
|
|
18049
18597
|
type: "string",
|
|
18050
18598
|
description: "Optional attachment id of a specific image already present in the current conversation."
|
|
@@ -18096,7 +18644,7 @@ function apply(ctx, config = {}) {
|
|
|
18096
18644
|
},
|
|
18097
18645
|
output: imageOutput("Edited"),
|
|
18098
18646
|
async execute(args, exec) {
|
|
18099
|
-
const active = resolveProvider(current());
|
|
18647
|
+
const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
|
|
18100
18648
|
const sourceImages = await resolveReferenceImages({
|
|
18101
18649
|
...exec.agent === void 0 ? {} : { agent: exec.agent },
|
|
18102
18650
|
attachments: ctx.attachments,
|
|
@@ -18125,13 +18673,12 @@ function apply(ctx, config = {}) {
|
|
|
18125
18673
|
signal: exec.signal
|
|
18126
18674
|
}), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
|
|
18127
18675
|
}
|
|
18128
|
-
const credential = await ctx
|
|
18129
|
-
if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
|
|
18676
|
+
const credential = await requireApiKey(ctx, active.provider, "edit_image");
|
|
18130
18677
|
if (active.provider === "google") {
|
|
18131
18678
|
const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
|
|
18132
18679
|
const imageSize = args.image_size ?? active.imageSize;
|
|
18133
18680
|
return saveGenerated(ctx, await editGoogleImage({
|
|
18134
|
-
apiKey: credential
|
|
18681
|
+
apiKey: credential,
|
|
18135
18682
|
endpoint: active.endpoint,
|
|
18136
18683
|
model: active.model,
|
|
18137
18684
|
prompt: args.prompt,
|
|
@@ -18143,8 +18690,8 @@ function apply(ctx, config = {}) {
|
|
|
18143
18690
|
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
|
|
18144
18691
|
}
|
|
18145
18692
|
const size = args.size ?? active.imageSize;
|
|
18146
|
-
if (active.provider === "openai") return saveGenerated(ctx, await editOpenAICompatibleImage({
|
|
18147
|
-
apiKey: credential
|
|
18693
|
+
if (active.provider === "openai" || active.provider === "openai-compat" || active.provider === "xai" || active.provider === "zhipu") return saveGenerated(ctx, await editOpenAICompatibleImage({
|
|
18694
|
+
apiKey: credential,
|
|
18148
18695
|
baseURL: active.baseURL,
|
|
18149
18696
|
model: active.model,
|
|
18150
18697
|
prompt: args.prompt,
|
|
@@ -18154,7 +18701,7 @@ function apply(ctx, config = {}) {
|
|
|
18154
18701
|
signal: exec.signal
|
|
18155
18702
|
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
18156
18703
|
if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
|
|
18157
|
-
apiKey: credential
|
|
18704
|
+
apiKey: credential,
|
|
18158
18705
|
baseURL: active.baseURL,
|
|
18159
18706
|
model: active.model,
|
|
18160
18707
|
prompt: args.prompt,
|
|
@@ -18164,7 +18711,7 @@ function apply(ctx, config = {}) {
|
|
|
18164
18711
|
signal: exec.signal
|
|
18165
18712
|
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
18166
18713
|
return saveGenerated(ctx, await editDashScopeImage({
|
|
18167
|
-
apiKey: credential
|
|
18714
|
+
apiKey: credential,
|
|
18168
18715
|
endpoint: active.endpoint,
|
|
18169
18716
|
model: active.model,
|
|
18170
18717
|
prompt: args.prompt,
|
|
@@ -18349,4 +18896,4 @@ function installImageSettings(ctx, config, hooks) {
|
|
|
18349
18896
|
ctx.logger.warn("dsh-image-gen: this DSH exposes neither settings API generation; settings UI stays on the composition entry");
|
|
18350
18897
|
}
|
|
18351
18898
|
//#endregion
|
|
18352
|
-
export { Config, DELETE_ROUTE, IMAGE_ROUTE, INSPIRATION_ROUTE, SAVE_WORKSPACE_ROUTE, STUDIO_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|
|
18899
|
+
export { Config, DELETE_ROUTE, IMAGE_ROUTE, INSPIRATION_ROUTE, SAVE_WORKSPACE_ROUTE, STUDIO_ROUTE, TEST_CONNECTION_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|