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/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$6(value);
5099
- const inputs = node === void 0 ? void 0 : record$6(node.inputs);
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$6(node)?.inputs;
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$6(value) {
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$5(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
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$5(entry.status);
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$5(value);
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$5(nodeOutput)?.images;
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$5(image);
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$5(value) {
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$5(value);
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$4(value);
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$4(step);
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$4(value);
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$4(value) {
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$3(value);
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$3(value);
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$3(value) {
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$2(res, 405, "method-not-allowed");
6056
- if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$2(res, 415, "json-required");
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$2(res, 403, "origin-rejected");
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$1(req));
6264
+ body = JSON.parse(await readBody$2(req));
6063
6265
  } catch {
6064
- return jsonError$2(res, 400, "invalid-request");
6266
+ return jsonError$3(res, 400, "invalid-request");
6065
6267
  }
6066
6268
  const attachment = attachmentFromRequest(body);
6067
- if (attachment === void 0) return jsonError$2(res, 400, "invalid-attachment");
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$2(res, 404, "image-unavailable");
6280
+ jsonError$3(res, 404, "image-unavailable");
6079
6281
  }
6080
6282
  }
6081
- function sameOrigin$2(req) {
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$2(res, 405, "method-not-allowed");
6090
- if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$2(res, 415, "json-required");
6091
- if (!sameOrigin$2(req)) return jsonError$2(res, 403, "origin-rejected");
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$1(req));
6296
+ body = JSON.parse(await readBody$2(req));
6095
6297
  } catch {
6096
- return jsonError$2(res, 400, "invalid-request");
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$2(req)) return jsonError$2(res, 403, "origin-rejected");
6127
- if (req.method !== "POST") return jsonError$2(res, 405, "method-not-allowed");
6128
- if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$2(res, 415, "json-required");
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$1(req));
6332
+ const raw = JSON.parse(await readBody$2(req));
6131
6333
  const attachment = parseImageAttachmentRef(raw.attachment);
6132
- if (!attachment) return jsonError$2(res, 400, "invalid-attachment");
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$2(res, 403, "workspace-root-not-allowed");
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$2(res, 404, "image-not-found");
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$2(meta);
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$2(value)?.attachment);
6388
+ return parseImageAttachmentRef(record$3(value)?.attachment);
6187
6389
  }
6188
- function record$2(value) {
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$1(req) {
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$2(res, status, code) {
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$1(value);
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$1(candidate);
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$1(value) {
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$1(req)) return jsonError$1(res, 403, "origin-rejected");
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$1(res, 200, publicInspirationCatalog(activeCatalog));
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$1(res, 200, publicInspirationCatalog(activeCatalog));
17304
+ return json$2(res, 200, publicInspirationCatalog(activeCatalog));
17103
17305
  } catch (error) {
17104
- return jsonError$1(res, 502, message(error, "refresh-failed"));
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$1(res, 200, { ok: true });
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$1(res, 404, "not-found");
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$1(res, 404, "source-not-found");
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$1(res, 404, "case-not-found");
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$1(res, 502, "source-image-unavailable");
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$1(req) {
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$1(res, status, value) {
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$1(res, status, error) {
17258
- json$1(res, status, { error });
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
- const credential = await ctx.credentials.resolve(credentialRef(CREDENTIALS[provider]));
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(providerConfig(config, input.provider, input.model));
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.credentials.resolve(credentialRef(active.apiKeyEnv));
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.value,
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.value,
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.value,
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: "openai",
17444
- apiKey: credential.value,
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.value,
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.value,
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.value,
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.value,
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
- const active = resolveProvider(providerConfig(config, provider));
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: PROVIDER_LABELS[provider],
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, message) {
18345
+ function jsonError(res, status, code) {
17847
18346
  if (res.headersSent || res.writableEnded || res.destroyed) return;
17848
- json(res, status, { error: message });
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.credentials.resolve(credentialRef(active.apiKeyEnv));
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.value,
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.value,
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.value,
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.credentials.resolve(credentialRef(active.apiKeyEnv));
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.value,
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.value,
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.value,
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.value,
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 };