dsh-image-gen 0.9.2 → 0.9.3
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 +2 -0
- package/README.md +2 -0
- package/lib/client.js +64 -17
- package/lib/index.js +238 -141
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/config.d.ts +4 -2
- package/lib/types/image-tool-context.d.ts +3 -0
- package/lib/types/openai-compatible.d.ts +1 -1
- package/lib/types/shared.d.ts +4 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -339,6 +339,8 @@ Bring private image generation on your local GPU directly into Agent conversatio
|
|
|
339
339
|
> Official OpenAI size presets can be customized with `openaiSizes`, shaped as ratio → tier → pixels, e.g. `{ "16:9": { "2K": "2048x1152", "4K": "3840x2160" } }`. For the gpt-image-2 family, dimensions must be multiples of 16, the longest edge at most 3840, aspect ratio within 1:3–3:1, and total pixels within 655360–8294400. Outputs above 3686400 pixels have experimental upstream support. See the [official API reference](https://developers.openai.com/api/reference/resources/images/methods/generate). Older GPT Image models retain their three standard sizes.
|
|
340
340
|
> Compatible providers use `openaiCompatSizes` in the same format; an empty table retains the legacy sizes. `openaiCompatQuality` sets the default generation quality (empty means omitted), and `openaiCompatQualityOptions` sets Studio choices. Choosing “Provider default (omit)” omits quality for that request. Conversation tools also accept independent `size` and `quality` parameters.
|
|
341
341
|
> “Generation extra fields” maps to `openaiCompatExtraBody`, e.g. `{ "watermark": false }`. These fields apply only to generation, merge after standard fields, and can override matching keys; `false` and nested objects are preserved. Editing continues to use `openaiCompatEditExtra`. For JSON image URL array edits, `openaiCompatEditSizeMode` defaults to `auto`; set it to `selected` to send the selected dimensions. EditExtra can still override size.
|
|
342
|
+
> Compatible channels can set a separate editing model with `openaiCompatEditModel`; empty reuses the generation model, and an explicit conversation-tool `model` still overrides one edit. Official SiliconFlow `/v1` endpoints automatically use `siliconflowJson`; relays implementing the same contract can select it manually. Edits post an `image` string to `/images/generations`. `Qwen/Qwen-Image-Edit` supports one reference; `Qwen/Qwen-Image-Edit-2509` supports up to three using `image`, `image2`, and `image3` in order. Generic size and quality fields are omitted; `size`, `image_size`, or `quality` in editing extras produce an actionable error, and the plugin fills reference fields. See the [SiliconFlow API reference](https://docs.siliconflow.cn/docs/api/images-generations-post).
|
|
343
|
+
> Ordinary conversation requests should omit provider and model to use the saved default. The model receives live default-provider and parameter instructions. Omit unused optional fields; blank dimensions are treated as absent, while two nonempty `size` / `aspect_ratio` values still conflict. ChatGPT subscription does not accept `image_size`; use `size` or `aspect_ratio` rather than a tier such as `1K`.
|
|
342
344
|
> The workbench shows “Visible watermark: Channel default / Off / On” beside ratio and resolution, checking generation and editing separately. Seedream, native DashScope Qwen Image, and recognized official SenseNova and Qwen compatible endpoints enable it automatically; recognized compatible endpoints also select their JSON edit contracts automatically. SenseNova matches `https://token.sensenova.cn/v1` and `sensenova-u1.5-lite`; Qwen matches official workspace hosts under `/compatible-mode/v1` with Qwen Image 3.0/3.0 Pro or 2.1 Pro/Turbo. Other compatible channels can bind a boolean `watermark`, `watermark_enabled`, or `parameters.watermark` through the matching operation's extra fields; editing requires a JSON format. Unknown channels and Zhipu display a disabled “Support unconfirmed” control; interfaces without a confirmed switch also show an explanation. A model name alone does not identify an unknown relay. Channel default preserves configured fields or omits the parameter; an explicit choice overrides only the watermark field and is retained for batches, comparison models supporting the operation, and regeneration.
|
|
343
345
|
|
|
344
346
|
</details>
|
package/README.md
CHANGED
|
@@ -339,6 +339,8 @@ pnpm dsh plugin --profile web add ./dsh-image-gen
|
|
|
339
339
|
> OpenAI 官方可通过设置中的 `openaiSizes` 自定义尺寸表,格式为「比例 → 档位 → 像素」,例如 `{ "16:9": { "2K": "2048x1152", "4K": "3840x2160" } }`。gpt-image-2 系列按官方限制校验:边长为 16 的倍数、最大 3840、比例在 1:3 到 3:1、总像素 655360–8294400;超过 3686400 像素的输出属于上游实验支持。[官方说明](https://developers.openai.com/api/reference/resources/images/methods/generate)。旧版 GPT Image 模型仍只使用三种标准尺寸。
|
|
340
340
|
> OpenAI Compatible 的尺寸表为 `openaiCompatSizes`,格式相同;留空时保持原来的标准尺寸。`openaiCompatQuality` 设置默认生成质量(留空不发送),`openaiCompatQualityOptions` 设置 Studio 可选值。选择「渠道默认(不发送)」可在单次请求中省略 quality;对话工具也支持独立的 `size` 和 `quality` 参数。
|
|
341
341
|
> 兼容渠道的「文生图额外字段」对应 `openaiCompatExtraBody`,例如 `{ "watermark": false }`。它仅用于文生图,在常规字段之后合并,可覆盖同名参数;`false` 和嵌套对象都会保留。图生图继续使用 `openaiCompatEditExtra`;JSON 图片 URL 数组格式的 `openaiCompatEditSizeMode` 默认 `auto`,改为 `selected` 才发送所选尺寸,EditExtra 仍可覆盖该尺寸。
|
|
342
|
+
> 兼容渠道可设置独立的「图生图模型」`openaiCompatEditModel`,留空沿用文生图模型;对话中的显式 `model` 仍可覆盖单次编辑。硅基流动官方 `/v1` 地址自动使用 `siliconflowJson` 编辑格式,其他采用同一协议的中转可手动选择:通过 `/images/generations` 发送 `image` 字符串。`Qwen/Qwen-Image-Edit` 支持一张参考图,`Qwen/Qwen-Image-Edit-2509` 最多三张,依次使用 `image`、`image2`、`image3`。编辑不发送通用尺寸和质量字段;图生图额外字段中的 `size`、`image_size`、`quality` 会给出明确错误,图片字段由插件填入。协议以[硅基流动官方文档](https://docs.siliconflow.cn/docs/api/images-generations-post)为准。
|
|
343
|
+
> 普通对话生图无需指定 provider 或 model,会使用已保存的默认渠道;模型可读取随配置更新的默认渠道及参数提示。无关可选字段应省略,空白尺寸会按未提供处理;两个有效的 `size` / `aspect_ratio` 仍互斥。ChatGPT 订阅不支持 `image_size`,请使用 `size` 或 `aspect_ratio`,不要填入 `1K` 等档位。
|
|
342
344
|
> 工作台在比例、分辨率旁提供「可见水印:渠道默认 / 关闭 / 开启」,分别判断文生图和图生图支持情况。Seedream、通义原生 Qwen Image,以及已确认的商汤和通义官方兼容接口自动启用;官方兼容接口同时自动使用对应的 JSON 编辑格式。商汤匹配 `https://token.sensenova.cn/v1` 与 `sensenova-u1.5-lite`;通义匹配官方工作空间域名的 `/compatible-mode/v1` 与 Qwen Image 3.0/3.0 Pro、2.1 Pro/Turbo。其他兼容渠道可通过对应操作的额外字段绑定布尔值 `watermark`、`watermark_enabled` 或 `parameters.watermark`;图生图绑定仅用于 JSON 编辑格式。未知渠道和智谱显示禁用的「支持未确认」,无已确认开关的接口也显示说明。不会仅凭模型名识别未知中转。渠道默认保留原配置或省略参数,单次选择只覆盖水印字段;批量生成、支持该操作的对比模型及再次生成会保留选择。
|
|
343
345
|
|
|
344
346
|
</details>
|
package/lib/client.js
CHANGED
|
@@ -75,7 +75,7 @@ window.__ModuleLoader__.load({
|
|
|
75
75
|
//#region lib/types/watermark.js
|
|
76
76
|
/** Model names alone do not establish the protocol of an unknown relay. */
|
|
77
77
|
function officialContract(config, mode) {
|
|
78
|
-
const model = (mode === "generate" ? config.openaiCompatExtraBody : config.openaiCompatEditExtra)?.model ?? config.openaiCompatModel;
|
|
78
|
+
const model = (mode === "generate" ? config.openaiCompatExtraBody : config.openaiCompatEditExtra)?.model ?? (mode === "edit" ? config.openaiCompatEditModel?.trim() || config.openaiCompatModel : config.openaiCompatModel);
|
|
79
79
|
if (typeof model !== "string") return void 0;
|
|
80
80
|
try {
|
|
81
81
|
const url = new URL(config.openaiCompatBaseURL ?? "");
|
|
@@ -87,6 +87,10 @@ window.__ModuleLoader__.load({
|
|
|
87
87
|
}
|
|
88
88
|
/** Known official edit contracts take precedence over relay-specific format settings. */
|
|
89
89
|
function compatEditFormat(config) {
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(config.openaiCompatBaseURL ?? "");
|
|
92
|
+
if (url.protocol === "https:" && !url.port && !url.search && !url.hash && (url.hostname === "api.siliconflow.cn" || url.hostname === "api.siliconflow.com") && url.pathname.replace(/\/+$/, "") === "/v1") return "siliconflowJson";
|
|
93
|
+
} catch {}
|
|
90
94
|
const contract = officialContract(config, "edit");
|
|
91
95
|
if (contract === "sensenova") return "jsonImageUrlArray";
|
|
92
96
|
if (contract === "qwen") return "qwenJson";
|
|
@@ -181,6 +185,10 @@ window.__ModuleLoader__.load({
|
|
|
181
185
|
"off",
|
|
182
186
|
"on"
|
|
183
187
|
];
|
|
188
|
+
/** A model picker/request must use the same operation-specific default. */
|
|
189
|
+
function studioModelForMode(profile, mode) {
|
|
190
|
+
return mode === "edit" ? profile.editModel ?? profile.model : profile.model;
|
|
191
|
+
}
|
|
184
192
|
function studioWatermarkCapability(profile, mode) {
|
|
185
193
|
return profile?.watermarkCapabilities?.[mode] ?? (mode === "generate" && profile?.watermarkOptions?.length ? { status: "supported" } : {
|
|
186
194
|
status: "unknown",
|
|
@@ -117762,6 +117770,7 @@ img.ProseMirror-separator {
|
|
|
117762
117770
|
};
|
|
117763
117771
|
}, []);
|
|
117764
117772
|
const activeProfile = (0, react.useMemo)(() => config?.providers.find((item) => item.provider === provider), [config, provider]);
|
|
117773
|
+
const requestModel = mode === "edit" && activeProfile?.editModel ? studioModelForMode(activeProfile, mode) : model;
|
|
117765
117774
|
const configuredCount = config?.providers.filter((item) => item.configured).length ?? 0;
|
|
117766
117775
|
const apiConfiguredCount = config?.providers.filter((item) => item.configured && !isSubscriptionProvider(item.provider)).length ?? 0;
|
|
117767
117776
|
const subscriptionSignedInCount = config?.providers.filter((item) => item.configured && isSubscriptionProvider(item.provider)).length ?? 0;
|
|
@@ -118105,7 +118114,7 @@ img.ProseMirror-separator {
|
|
|
118105
118114
|
body: JSON.stringify({
|
|
118106
118115
|
mode,
|
|
118107
118116
|
provider: target.profile.provider,
|
|
118108
|
-
model: target.profile
|
|
118117
|
+
model: studioModelForMode(target.profile, mode),
|
|
118109
118118
|
prompt: prompt.trim(),
|
|
118110
118119
|
ratio: target.ratio,
|
|
118111
118120
|
quality: target.quality,
|
|
@@ -118174,7 +118183,7 @@ img.ProseMirror-separator {
|
|
|
118174
118183
|
body: JSON.stringify({
|
|
118175
118184
|
mode,
|
|
118176
118185
|
provider,
|
|
118177
|
-
model,
|
|
118186
|
+
model: requestModel,
|
|
118178
118187
|
prompt: prompt.trim(),
|
|
118179
118188
|
ratio,
|
|
118180
118189
|
quality: selectedQuality,
|
|
@@ -118759,7 +118768,7 @@ img.ProseMirror-separator {
|
|
|
118759
118768
|
className: p.configured ? "is-configured" : "is-missing",
|
|
118760
118769
|
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
118761
118770
|
className: "dsh-ig-status-item-name",
|
|
118762
|
-
children: [(0, react_jsx_runtime.jsx)("strong", { children: p.label }), (0, react_jsx_runtime.jsx)("small", { children: p
|
|
118771
|
+
children: [(0, react_jsx_runtime.jsx)("strong", { children: p.label }), (0, react_jsx_runtime.jsx)("small", { children: studioModelForMode(p, mode) })]
|
|
118763
118772
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
118764
118773
|
className: "dsh-ig-status-item-tag",
|
|
118765
118774
|
children: isSubscriptionProvider(p.provider) ? p.configured ? t("subSignedIn") : t("subSignedOut") : p.configured ? t("configured") : t("unconfigured")
|
|
@@ -118883,7 +118892,7 @@ img.ProseMirror-separator {
|
|
|
118883
118892
|
children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 28 })
|
|
118884
118893
|
}),
|
|
118885
118894
|
(0, react_jsx_runtime.jsx)("strong", { children: comparisonEnabled ? t("comparing", { count: String(comparisonTargets.length) }) : count > 1 ? t("generatingCount", { count: String(count) }) : t("generating") }),
|
|
118886
|
-
(0, react_jsx_runtime.jsx)("span", { children: comparisonEnabled ? comparisonTargets.map((target) => target.profile.label).join(" · ") : `${activeProfile?.label ?? ""} · ${
|
|
118895
|
+
(0, react_jsx_runtime.jsx)("span", { children: comparisonEnabled ? comparisonTargets.map((target) => target.profile.label).join(" · ") : `${activeProfile?.label ?? ""} · ${requestModel}` })
|
|
118887
118896
|
]
|
|
118888
118897
|
}) : currentBatch && currentBatch.length > 1 ? (0, react_jsx_runtime.jsx)("div", {
|
|
118889
118898
|
className: "dsh-ig-canvas-cluster",
|
|
@@ -119197,8 +119206,8 @@ img.ProseMirror-separator {
|
|
|
119197
119206
|
(0, react_jsx_runtime.jsxs)("span", {
|
|
119198
119207
|
className: "dsh-ig-model-copy",
|
|
119199
119208
|
children: [(0, react_jsx_runtime.jsx)("strong", { children: item.label }), (0, react_jsx_runtime.jsx)("small", {
|
|
119200
|
-
title: item
|
|
119201
|
-
children: item
|
|
119209
|
+
title: studioModelForMode(item, mode),
|
|
119210
|
+
children: studioModelForMode(item, mode)
|
|
119202
119211
|
})]
|
|
119203
119212
|
}),
|
|
119204
119213
|
target && (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -119267,11 +119276,11 @@ img.ProseMirror-separator {
|
|
|
119267
119276
|
}))
|
|
119268
119277
|
}), (0, react_jsx_runtime.jsx)(FieldSelect, {
|
|
119269
119278
|
label: t("model"),
|
|
119270
|
-
value:
|
|
119279
|
+
value: requestModel,
|
|
119271
119280
|
onChange: setModel,
|
|
119272
119281
|
options: activeProfile === void 0 ? [] : [{
|
|
119273
|
-
value: activeProfile
|
|
119274
|
-
label: activeProfile
|
|
119282
|
+
value: studioModelForMode(activeProfile, mode),
|
|
119283
|
+
label: studioModelForMode(activeProfile, mode)
|
|
119275
119284
|
}]
|
|
119276
119285
|
})]
|
|
119277
119286
|
}),
|
|
@@ -125436,7 +125445,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
125436
125445
|
`;
|
|
125437
125446
|
//#endregion
|
|
125438
125447
|
//#region lib/types/client/index.js
|
|
125439
|
-
console.info(`[dsh-image-gen] canvas bundle 2026-10-
|
|
125448
|
+
console.info(`[dsh-image-gen] canvas bundle 2026-10-26T00:24:12.240Z`);
|
|
125440
125449
|
/** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
|
|
125441
125450
|
const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
|
|
125442
125451
|
/** The tab kind: a page type opened by kind, recognizing no resource address. */
|
|
@@ -125519,11 +125528,15 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
125519
125528
|
endpointHintOpenAI: "官方 api.openai.com 的 /v1 地址;中转站请使用下方「OpenAI 兼容」行。",
|
|
125520
125529
|
endpointHintOpenAICompat: "中转站/自建服务的 OpenAI 兼容 /v1 地址(必填),例如 https://your-relay.example.com/v1。",
|
|
125521
125530
|
editFormat: "图生图请求形态",
|
|
125531
|
+
editModel: "图生图模型(可选)",
|
|
125532
|
+
editModelHint: "留空沿用上方模型。硅基流动可分别填写 Qwen/Qwen-Image(文生图)和 Qwen/Qwen-Image-Edit(图生图)。",
|
|
125533
|
+
editFormatSiliconFlow: "硅基流动 JSON(image 字符串)",
|
|
125534
|
+
editFormatSiliconFlowHint: "编辑尺寸由参考图决定,不发送尺寸和质量字段。Qwen-Image-Edit 支持一张图,Edit-2509 最多三张;参考图字段由插件填入。",
|
|
125522
125535
|
editFormatMultipart: "标准 multipart(OpenAI 官方)",
|
|
125523
125536
|
editFormatAutomatic: "自动:官方接口 JSON 格式",
|
|
125524
125537
|
editFormatJsonImageUrlArray: "JSON images 数组(商汤等中转)",
|
|
125525
125538
|
editFormatFormReferenceImages: "multipart + reference_images JSON 数组(base64)",
|
|
125526
|
-
editFormatHint: "
|
|
125539
|
+
editFormatHint: "商汤、通义、硅基流动已确认的官方接口自动使用对应 JSON 格式;其他中转按接口文档选择。文生图不受影响。",
|
|
125527
125540
|
editExtra: "附加 JSON 字段",
|
|
125528
125541
|
editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
|
|
125529
125542
|
editExtraHint: "仅 JSON 形态生效;会合并到请求体末尾,可覆盖默认字段。留空表示不附加。",
|
|
@@ -125689,11 +125702,15 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
125689
125702
|
endpointHintOpenAI: "Official api.openai.com /v1 base URL; for relays use the \"OpenAI-compatible\" row below.",
|
|
125690
125703
|
endpointHintOpenAICompat: "OpenAI-compatible /v1 base URL of your relay or self-hosted service (required), e.g. https://your-relay.example.com/v1.",
|
|
125691
125704
|
editFormat: "Edit request format",
|
|
125705
|
+
editModel: "Editing model (optional)",
|
|
125706
|
+
editModelHint: "Leave empty to reuse the model above. SiliconFlow can use Qwen/Qwen-Image for generation and Qwen/Qwen-Image-Edit for editing.",
|
|
125707
|
+
editFormatSiliconFlow: "SiliconFlow JSON (image string)",
|
|
125708
|
+
editFormatSiliconFlowHint: "Edit dimensions follow the reference; size and quality fields are omitted. Qwen-Image-Edit supports one reference, Edit-2509 up to three. The plugin fills the image fields.",
|
|
125692
125709
|
editFormatMultipart: "Standard multipart (official OpenAI)",
|
|
125693
125710
|
editFormatAutomatic: "Automatic: official JSON contract",
|
|
125694
125711
|
editFormatJsonImageUrlArray: "JSON images array (SenseNova etc.)",
|
|
125695
125712
|
editFormatFormReferenceImages: "Multipart with a reference_images JSON array (base64)",
|
|
125696
|
-
editFormatHint: "Recognized official SenseNova and
|
|
125713
|
+
editFormatHint: "Recognized official SenseNova, Qwen and SiliconFlow endpoints automatically use their JSON edit contracts. Choose other relay formats according to their documentation. Text-to-image is unaffected.",
|
|
125697
125714
|
editExtra: "Extra JSON fields",
|
|
125698
125715
|
editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
|
|
125699
125716
|
editExtraHint: "Only used with the JSON format; merged into the request body last and may override defaults. Leave empty for none.",
|
|
@@ -126328,6 +126345,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
126328
126345
|
return {
|
|
126329
126346
|
expanded: false,
|
|
126330
126347
|
model: "",
|
|
126348
|
+
editModel: "",
|
|
126331
126349
|
baseURL: "",
|
|
126332
126350
|
googleNative: false,
|
|
126333
126351
|
keyInput: "",
|
|
@@ -126410,7 +126428,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
126410
126428
|
}
|
|
126411
126429
|
/** Persisted or selected edit-format value → row state; unknown values fall back to standard multipart. */
|
|
126412
126430
|
function parseEditFormat(value) {
|
|
126413
|
-
return value === "jsonImageUrlArray" || value === "formReferenceImages" ? value : "multipart";
|
|
126431
|
+
return value === "jsonImageUrlArray" || value === "formReferenceImages" || value === "siliconflowJson" ? value : "multipart";
|
|
126414
126432
|
}
|
|
126415
126433
|
/** Build one row per provider from persisted settings, including ComfyUI extras. */
|
|
126416
126434
|
function rowsFromSettings(value) {
|
|
@@ -126427,6 +126445,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
126427
126445
|
timeoutSeconds: Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3))
|
|
126428
126446
|
} : {},
|
|
126429
126447
|
...provider === "openai-compat" ? {
|
|
126448
|
+
editModel: value?.openaiCompatEditModel ?? "",
|
|
126430
126449
|
editFormat: parseEditFormat(value?.openaiCompatEditFormat),
|
|
126431
126450
|
editExtraText: editExtraTextOf(value?.openaiCompatEditExtra),
|
|
126432
126451
|
sizeTableText: editExtraTextOf(value?.openaiCompatSizes),
|
|
@@ -126679,6 +126698,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
126679
126698
|
for (const provider of IMAGE_PROVIDERS) next[provider] = {
|
|
126680
126699
|
...current[provider],
|
|
126681
126700
|
model: modelOf(provider, value),
|
|
126701
|
+
...provider === "openai-compat" ? { editModel: value?.openaiCompatEditModel ?? "" } : {},
|
|
126682
126702
|
baseURL: baseURLOf(provider, value),
|
|
126683
126703
|
...provider === "comfyui" ? {
|
|
126684
126704
|
workflows: resolveComfyUIWorkflows(value ?? {}),
|
|
@@ -126809,6 +126829,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
126809
126829
|
if (provider === "google") await saveSetting("googleNative", row.googleNative);
|
|
126810
126830
|
if (provider === "openai") await saveSetting("openaiSizes", sizeTable);
|
|
126811
126831
|
if (provider === "openai-compat") {
|
|
126832
|
+
await saveSetting("openaiCompatEditModel", row.editModel.trim());
|
|
126812
126833
|
await saveSetting("openaiCompatEditFormat", row.editFormat);
|
|
126813
126834
|
await saveSetting("openaiCompatEditExtra", editExtra);
|
|
126814
126835
|
await saveSetting("openaiCompatSizes", sizeTable);
|
|
@@ -127160,7 +127181,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
127160
127181
|
const row = rows[provider];
|
|
127161
127182
|
const officialEditFormat = provider === "openai-compat" ? compatEditFormat({
|
|
127162
127183
|
openaiCompatBaseURL: row.baseURL,
|
|
127163
|
-
openaiCompatModel: row.model
|
|
127184
|
+
openaiCompatModel: row.editModel.trim() || row.model
|
|
127164
127185
|
}) : "multipart";
|
|
127165
127186
|
const automaticEditFormat = officialEditFormat !== "multipart";
|
|
127166
127187
|
const editFormat = automaticEditFormat ? officialEditFormat : row.editFormat;
|
|
@@ -127308,6 +127329,28 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
127308
127329
|
}) : null
|
|
127309
127330
|
]
|
|
127310
127331
|
}),
|
|
127332
|
+
provider === "openai-compat" ? (0, react_jsx_runtime.jsxs)("label", {
|
|
127333
|
+
className: "dsh-ig-field",
|
|
127334
|
+
children: [
|
|
127335
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
127336
|
+
className: "dsh-ig-label",
|
|
127337
|
+
children: t("editModel")
|
|
127338
|
+
}),
|
|
127339
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
127340
|
+
className: "dsh-ig-input",
|
|
127341
|
+
value: row.editModel,
|
|
127342
|
+
onChange: (event) => {
|
|
127343
|
+
updateRow(provider, { editModel: event.target.value });
|
|
127344
|
+
},
|
|
127345
|
+
list: "dsh-ig-openai-compat-model-options",
|
|
127346
|
+
disabled: !settingsWritable
|
|
127347
|
+
}),
|
|
127348
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
127349
|
+
className: "dsh-ig-hint",
|
|
127350
|
+
children: t("editModelHint")
|
|
127351
|
+
})
|
|
127352
|
+
]
|
|
127353
|
+
}) : null,
|
|
127311
127354
|
provider === "openai-compat" ? (0, react_jsx_runtime.jsxs)("label", {
|
|
127312
127355
|
className: "dsh-ig-field",
|
|
127313
127356
|
children: [
|
|
@@ -127338,12 +127381,16 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
127338
127381
|
(0, react_jsx_runtime.jsx)("option", {
|
|
127339
127382
|
value: "formReferenceImages",
|
|
127340
127383
|
children: t("editFormatFormReferenceImages")
|
|
127384
|
+
}),
|
|
127385
|
+
(0, react_jsx_runtime.jsx)("option", {
|
|
127386
|
+
value: "siliconflowJson",
|
|
127387
|
+
children: t("editFormatSiliconFlow")
|
|
127341
127388
|
})
|
|
127342
127389
|
]
|
|
127343
127390
|
}),
|
|
127344
127391
|
(0, react_jsx_runtime.jsx)("span", {
|
|
127345
127392
|
className: "dsh-ig-hint",
|
|
127346
|
-
children: t("editFormatHint")
|
|
127393
|
+
children: t(editFormat === "siliconflowJson" ? "editFormatSiliconFlowHint" : "editFormatHint")
|
|
127347
127394
|
})
|
|
127348
127395
|
]
|
|
127349
127396
|
}) : null,
|
|
@@ -127516,7 +127563,7 @@ body[data-ds-dark-theme] .dsh-ig-workbench{--ig-danger:#f08c8c;--ig-warn:#e5b76a
|
|
|
127516
127563
|
]
|
|
127517
127564
|
})
|
|
127518
127565
|
] }) : null,
|
|
127519
|
-
provider === "openai-compat" && (editFormat === "jsonImageUrlArray" || editFormat === "qwenJson") ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [editFormat === "jsonImageUrlArray" ? (0, react_jsx_runtime.jsxs)("label", {
|
|
127566
|
+
provider === "openai-compat" && (editFormat === "jsonImageUrlArray" || editFormat === "qwenJson" || editFormat === "siliconflowJson") ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [editFormat === "jsonImageUrlArray" ? (0, react_jsx_runtime.jsxs)("label", {
|
|
127520
127567
|
className: "dsh-ig-field",
|
|
127521
127568
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
127522
127569
|
className: "dsh-ig-label",
|
package/lib/index.js
CHANGED
|
@@ -5604,7 +5604,7 @@ function configuredField(extra) {
|
|
|
5604
5604
|
}
|
|
5605
5605
|
/** Model names alone do not establish the protocol of an unknown relay. */
|
|
5606
5606
|
function officialContract(config, mode) {
|
|
5607
|
-
const model = (mode === "generate" ? config.openaiCompatExtraBody : config.openaiCompatEditExtra)?.model ?? config.openaiCompatModel;
|
|
5607
|
+
const model = (mode === "generate" ? config.openaiCompatExtraBody : config.openaiCompatEditExtra)?.model ?? (mode === "edit" ? config.openaiCompatEditModel?.trim() || config.openaiCompatModel : config.openaiCompatModel);
|
|
5608
5608
|
if (typeof model !== "string") return void 0;
|
|
5609
5609
|
try {
|
|
5610
5610
|
const url = new URL(config.openaiCompatBaseURL ?? "");
|
|
@@ -5616,6 +5616,10 @@ function officialContract(config, mode) {
|
|
|
5616
5616
|
}
|
|
5617
5617
|
/** Known official edit contracts take precedence over relay-specific format settings. */
|
|
5618
5618
|
function compatEditFormat(config) {
|
|
5619
|
+
try {
|
|
5620
|
+
const url = new URL(config.openaiCompatBaseURL ?? "");
|
|
5621
|
+
if (url.protocol === "https:" && !url.port && !url.search && !url.hash && (url.hostname === "api.siliconflow.cn" || url.hostname === "api.siliconflow.com") && url.pathname.replace(/\/+$/, "") === "/v1") return "siliconflowJson";
|
|
5622
|
+
} catch {}
|
|
5619
5623
|
const contract = officialContract(config, "edit");
|
|
5620
5624
|
if (contract === "sensenova") return "jsonImageUrlArray";
|
|
5621
5625
|
if (contract === "qwen") return "qwenJson";
|
|
@@ -5827,6 +5831,10 @@ const STUDIO_WATERMARK_VALUES = [
|
|
|
5827
5831
|
"off",
|
|
5828
5832
|
"on"
|
|
5829
5833
|
];
|
|
5834
|
+
/** A model picker/request must use the same operation-specific default. */
|
|
5835
|
+
function studioModelForMode(profile, mode) {
|
|
5836
|
+
return mode === "edit" ? profile.editModel ?? profile.model : profile.model;
|
|
5837
|
+
}
|
|
5830
5838
|
function studioWatermarkCapability(profile, mode) {
|
|
5831
5839
|
return profile?.watermarkCapabilities?.[mode] ?? (mode === "generate" && profile?.watermarkOptions?.length ? { status: "supported" } : {
|
|
5832
5840
|
status: "unknown",
|
|
@@ -5961,6 +5969,7 @@ const Config = z.object({
|
|
|
5961
5969
|
openaiSizes: volatile(z.dict(z.dict(z.string())).default({})),
|
|
5962
5970
|
openaiCompatBaseURL: volatile(z.string().default("")),
|
|
5963
5971
|
openaiCompatModel: volatile(z.string().default("")),
|
|
5972
|
+
openaiCompatEditModel: volatile(z.string().default("")),
|
|
5964
5973
|
openaiCompatQuality: volatile(z.string().default("")),
|
|
5965
5974
|
openaiCompatQualityOptions: volatile(z.array(z.string()).default([
|
|
5966
5975
|
"auto",
|
|
@@ -5973,7 +5982,8 @@ const Config = z.object({
|
|
|
5973
5982
|
openaiCompatEditFormat: volatile(z.union([
|
|
5974
5983
|
z.const("multipart"),
|
|
5975
5984
|
z.const("jsonImageUrlArray"),
|
|
5976
|
-
z.const("formReferenceImages")
|
|
5985
|
+
z.const("formReferenceImages"),
|
|
5986
|
+
z.const("siliconflowJson")
|
|
5977
5987
|
]).default("multipart")),
|
|
5978
5988
|
openaiCompatEditExtra: volatile(z.dict(z.any()).default({})),
|
|
5979
5989
|
openaiCompatSizes: volatile(z.dict(z.dict(z.string())).default({})),
|
|
@@ -6101,13 +6111,12 @@ function resolveProvider(config) {
|
|
|
6101
6111
|
* Apply a per-call provider and/or model override on top of the saved config.
|
|
6102
6112
|
* `model` is ignored for ComfyUI, whose per-call equivalent is `workflow`.
|
|
6103
6113
|
*/
|
|
6104
|
-
function withProviderOverrides(config, provider, model) {
|
|
6114
|
+
function withProviderOverrides(config, provider, model, mode = "generate") {
|
|
6105
6115
|
const base = provider === void 0 ? { ...config } : {
|
|
6106
6116
|
...config,
|
|
6107
6117
|
provider
|
|
6108
6118
|
};
|
|
6109
|
-
|
|
6110
|
-
const trimmed = model.trim();
|
|
6119
|
+
const trimmed = model?.trim() || (mode === "edit" && base.provider === "openai-compat" ? base.openaiCompatEditModel?.trim() : "") || "";
|
|
6111
6120
|
if (trimmed.length === 0) return base;
|
|
6112
6121
|
switch (base.provider ?? "google") {
|
|
6113
6122
|
case "google": return {
|
|
@@ -6120,7 +6129,8 @@ function withProviderOverrides(config, provider, model) {
|
|
|
6120
6129
|
};
|
|
6121
6130
|
case "openai-compat": return {
|
|
6122
6131
|
...base,
|
|
6123
|
-
openaiCompatModel: trimmed
|
|
6132
|
+
openaiCompatModel: trimmed,
|
|
6133
|
+
...mode === "edit" ? { openaiCompatEditModel: trimmed } : {}
|
|
6124
6134
|
};
|
|
6125
6135
|
case "seedream": return {
|
|
6126
6136
|
...base,
|
|
@@ -8263,6 +8273,39 @@ async function generateOpenAICompatibleImage(input) {
|
|
|
8263
8273
|
}), input.apiKey), input.provider, input);
|
|
8264
8274
|
}
|
|
8265
8275
|
async function editOpenAICompatibleImage(input) {
|
|
8276
|
+
if (input.editFormat === "siliconflowJson") {
|
|
8277
|
+
const body = {
|
|
8278
|
+
model: input.model,
|
|
8279
|
+
prompt: input.prompt,
|
|
8280
|
+
...input.editExtra ?? {},
|
|
8281
|
+
...input.extraBody ?? {}
|
|
8282
|
+
};
|
|
8283
|
+
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
8284
|
+
if (!model) throw new Error("硅基流动图生图需要编辑模型");
|
|
8285
|
+
body.model = model;
|
|
8286
|
+
if (model === "Qwen/Qwen-Image") throw new Error("Qwen/Qwen-Image 是文生图模型;请配置图生图模型,例如 Qwen/Qwen-Image-Edit");
|
|
8287
|
+
const maxReferences = model === "Qwen/Qwen-Image-Edit-2509" ? 3 : 1;
|
|
8288
|
+
if (input.sourceImages.length < 1 || input.sourceImages.length > maxReferences) throw new Error(`${model} 图生图支持 1–${String(maxReferences)} 张参考图,当前 ${String(input.sourceImages.length)} 张;请精简参考图或选择支持多图的编辑模型`);
|
|
8289
|
+
if ([
|
|
8290
|
+
"size",
|
|
8291
|
+
"image_size",
|
|
8292
|
+
"quality"
|
|
8293
|
+
].some((key) => key in body)) throw new Error("此硅基流动图生图格式不发送 size、image_size 或 quality;请从图生图额外字段中移除这些字段");
|
|
8294
|
+
delete body.image;
|
|
8295
|
+
delete body.image2;
|
|
8296
|
+
delete body.image3;
|
|
8297
|
+
for (const [index, source] of input.sourceImages.entries()) body[index === 0 ? "image" : `image${String(index + 1)}`] = toDataUrl$2(source);
|
|
8298
|
+
return parseImageResponse(await guardedFetch("siliconflow image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
|
|
8299
|
+
method: "POST",
|
|
8300
|
+
redirect: "error",
|
|
8301
|
+
signal: input.signal,
|
|
8302
|
+
headers: {
|
|
8303
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
8304
|
+
"content-type": "application/json"
|
|
8305
|
+
},
|
|
8306
|
+
body: JSON.stringify(body)
|
|
8307
|
+
}), input.apiKey), "siliconflow", input);
|
|
8308
|
+
}
|
|
8266
8309
|
if (input.editFormat === "qwenJson") return parseImageResponse(await guardedFetch("openai-compat image edit request", input.signal, () => fetch(imageEndpoint$1(input.baseURL, "generations"), {
|
|
8267
8310
|
method: "POST",
|
|
8268
8311
|
redirect: "error",
|
|
@@ -10158,16 +10201,18 @@ function jsonError$3(res, status, code) {
|
|
|
10158
10201
|
//#region lib/types/subscription.js
|
|
10159
10202
|
/** Translate image tool options into each subscription channel's wire inputs. */
|
|
10160
10203
|
function subscriptionToolParameters(provider, options) {
|
|
10161
|
-
|
|
10162
|
-
const ratio = options.aspectRatio;
|
|
10204
|
+
const requestedSize = options.size?.trim() || void 0;
|
|
10205
|
+
const ratio = options.aspectRatio?.trim() || void 0;
|
|
10206
|
+
const imageSize = options.imageSize?.trim() || void 0;
|
|
10207
|
+
if (requestedSize !== void 0 && ratio !== void 0) throw new Error("size 与 aspect_ratio 请只选一个;无关可选字段请省略,不要填写空字符串");
|
|
10163
10208
|
if (ratio !== void 0 && !SUBSCRIPTION_CAPABILITIES[provider].ratioOptions.some((option) => option.value === ratio)) throw new Error(`${provider} 不支持比例 ${ratio}`);
|
|
10164
|
-
const size =
|
|
10209
|
+
const size = requestedSize ?? (ratio === void 0 ? void 0 : provider === "chatgpt-sub" ? SUBSCRIPTION_CAPABILITIES[provider].sizeFor?.(ratio, "auto") : ratio);
|
|
10165
10210
|
let quality;
|
|
10166
|
-
if (
|
|
10167
|
-
if (provider === "grok-sub" && (
|
|
10168
|
-
else if (provider === "google-sub" &&
|
|
10169
|
-
else if (provider === "google-sub" &&
|
|
10170
|
-
else throw new Error(`${provider} 不支持清晰度 ${
|
|
10211
|
+
if (imageSize !== void 0) {
|
|
10212
|
+
if (provider === "grok-sub" && (imageSize === "1K" || imageSize === "2K")) quality = imageSize.toLowerCase();
|
|
10213
|
+
else if (provider === "google-sub" && imageSize === "4K") quality = "hd";
|
|
10214
|
+
else if (provider === "google-sub" && imageSize === "1K") quality = void 0;
|
|
10215
|
+
else throw new Error(`${provider} 不支持清晰度 ${imageSize};${provider === "chatgpt-sub" ? "请省略 image_size,使用 size 或 aspect_ratio 指定尺寸" : `image_size 仅支持 ${provider === "grok-sub" ? "1K、2K" : "1K、4K"}`}`);
|
|
10171
10216
|
}
|
|
10172
10217
|
return {
|
|
10173
10218
|
...size === void 0 ? {} : { size },
|
|
@@ -28122,8 +28167,10 @@ function subscriptionResponse(input, startedAt, requestedCount, failedCount, ite
|
|
|
28122
28167
|
}
|
|
28123
28168
|
function studioProfile(config, provider, configured) {
|
|
28124
28169
|
const result = cloudStudioProfile(config, provider, configured);
|
|
28170
|
+
const editModel = provider === "openai-compat" ? config.openaiCompatEditModel?.trim() : void 0;
|
|
28125
28171
|
return {
|
|
28126
28172
|
...result,
|
|
28173
|
+
...editModel ? { editModel } : {},
|
|
28127
28174
|
...watermarkProfile(config, provider, result.model)
|
|
28128
28175
|
};
|
|
28129
28176
|
}
|
|
@@ -28221,7 +28268,7 @@ function option(value) {
|
|
|
28221
28268
|
};
|
|
28222
28269
|
}
|
|
28223
28270
|
function assertAllowed(profile, input) {
|
|
28224
|
-
if (input.model !== profile.
|
|
28271
|
+
if (input.model !== studioModelForMode(profile, input.mode)) throw new Error("模型配置已变化,请刷新工作台后重试");
|
|
28225
28272
|
if (!profile.ratioOptions.some((option) => option.value === input.ratio)) throw new Error("该 Provider 不支持所选比例");
|
|
28226
28273
|
const selectedQuality = input.provider === "openai" ? input.generationQuality ?? input.quality : input.resolution ?? input.quality;
|
|
28227
28274
|
if (!profile.qualityOptions.some((option) => option.value === selectedQuality)) throw new Error("该 Provider 不支持所选清晰度");
|
|
@@ -28791,14 +28838,53 @@ function serveSettingsHealth(req, res, health) {
|
|
|
28791
28838
|
res.end(JSON.stringify(health));
|
|
28792
28839
|
}
|
|
28793
28840
|
//#endregion
|
|
28841
|
+
//#region lib/types/image-tool-context.js
|
|
28842
|
+
/** Live, credential-free instructions for the conversation model's image tools. */
|
|
28843
|
+
function imageToolContext(config) {
|
|
28844
|
+
const provider = config.provider ?? "google";
|
|
28845
|
+
let model = "not configured";
|
|
28846
|
+
let editModel = model;
|
|
28847
|
+
try {
|
|
28848
|
+
const active = resolveProvider(config);
|
|
28849
|
+
model = active.provider === "comfyui" ? active.workflow?.name ?? "workflow selected in settings" : active.model;
|
|
28850
|
+
const edit = resolveProvider(withProviderOverrides(config, void 0, void 0, "edit"));
|
|
28851
|
+
editModel = edit.provider === "comfyui" ? edit.workflow?.name ?? model : edit.model;
|
|
28852
|
+
} catch {}
|
|
28853
|
+
return [
|
|
28854
|
+
`Image tools current default provider: ${provider}; generation model: ${JSON.stringify(model)}; editing model: ${JSON.stringify(editModel)}.`,
|
|
28855
|
+
"Unless the user explicitly requests a different image provider or model, OMIT provider and model from generate_image, generate_images and edit_image. Do not guess a channel or switch channels after an error.",
|
|
28856
|
+
"Omit unused optional arguments entirely; never fill them with empty strings. size and aspect_ratio are alternatives: provide at most one.",
|
|
28857
|
+
"Subscription parameters: chatgpt-sub accepts size OR aspect_ratio, but NOT image_size. grok-sub image_size: 1K or 2K. google-sub image_size: 1K or 4K. Do not copy parameters from another provider.",
|
|
28858
|
+
"Minimal calls: generate_image({\"prompt\":\"a sailboat\"}); edit_image({\"prompt\":\"make the sail blue\"}). A valid ChatGPT subscription ratio example is {\"prompt\":\"a sailboat\",\"aspect_ratio\":\"1:1\"} with image_size and size omitted."
|
|
28859
|
+
].join("\n");
|
|
28860
|
+
}
|
|
28861
|
+
//#endregion
|
|
28794
28862
|
//#region lib/types/index.js
|
|
28795
28863
|
const name = "dsh-image-gen";
|
|
28796
28864
|
const inject = [
|
|
28797
28865
|
"tools",
|
|
28798
28866
|
"attachments",
|
|
28799
|
-
"credentials"
|
|
28800
|
-
"webServer"
|
|
28867
|
+
"credentials"
|
|
28801
28868
|
];
|
|
28869
|
+
/** Optional strings from tool callers may contain empty placeholders (#86). */
|
|
28870
|
+
function cleanImageToolArgs(args) {
|
|
28871
|
+
const clean = { ...args };
|
|
28872
|
+
const fields = clean;
|
|
28873
|
+
for (const key of [
|
|
28874
|
+
"model",
|
|
28875
|
+
"size",
|
|
28876
|
+
"aspect_ratio",
|
|
28877
|
+
"image_size",
|
|
28878
|
+
"quality",
|
|
28879
|
+
"workflow"
|
|
28880
|
+
]) {
|
|
28881
|
+
const value = fields[key];
|
|
28882
|
+
if (typeof value !== "string") continue;
|
|
28883
|
+
if (value.trim().length === 0) delete fields[key];
|
|
28884
|
+
else fields[key] = value.trim();
|
|
28885
|
+
}
|
|
28886
|
+
return clean;
|
|
28887
|
+
}
|
|
28802
28888
|
/** Validate the untrusted per-call provider override from tool arguments. */
|
|
28803
28889
|
function providerOverrideOf(value) {
|
|
28804
28890
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
@@ -28815,142 +28901,152 @@ function apply(ctx, config = {}) {
|
|
|
28815
28901
|
const knownWorkspaceRoots = /* @__PURE__ */ new Set();
|
|
28816
28902
|
const canvasMirror = new CanvasMirror();
|
|
28817
28903
|
const subscriptionManager = new SubscriptionManager(ctx);
|
|
28818
|
-
registerSubscriptionRoutes(ctx, subscriptionManager);
|
|
28819
28904
|
const settingsHealth = installImageSettings(ctx, current(), {
|
|
28820
28905
|
setSource: (source) => {
|
|
28821
28906
|
current = () => migrateOpenAICompatConfig(plainConfig(source()));
|
|
28822
28907
|
},
|
|
28823
28908
|
onChange: () => {}
|
|
28824
28909
|
});
|
|
28825
|
-
ctx.
|
|
28826
|
-
|
|
28827
|
-
|
|
28828
|
-
|
|
28829
|
-
|
|
28830
|
-
|
|
28831
|
-
|
|
28832
|
-
|
|
28833
|
-
|
|
28834
|
-
|
|
28835
|
-
|
|
28836
|
-
|
|
28837
|
-
|
|
28838
|
-
|
|
28839
|
-
|
|
28840
|
-
|
|
28841
|
-
|
|
28842
|
-
|
|
28843
|
-
|
|
28844
|
-
|
|
28845
|
-
|
|
28846
|
-
|
|
28847
|
-
|
|
28848
|
-
|
|
28849
|
-
|
|
28850
|
-
...knownWorkspaceRoots,
|
|
28851
|
-
...discovered,
|
|
28852
|
-
process.cwd()
|
|
28853
|
-
]));
|
|
28854
|
-
} })
|
|
28855
|
-
}), "dsh-image-gen: delete route");
|
|
28856
|
-
ctx.effect(() => ctx.webServer.register({
|
|
28857
|
-
kind: "exact",
|
|
28858
|
-
path: SAVE_WORKSPACE_ROUTE,
|
|
28859
|
-
handler: (req, res) => serveSaveWorkspace(req, res, {
|
|
28860
|
-
readImage: (ref) => ctx.attachments.readImage(ref),
|
|
28861
|
-
saveToWorkspace: (options) => {
|
|
28862
|
-
if (options.workspaceRoot) knownWorkspaceRoots.add(options.workspaceRoot);
|
|
28863
|
-
return saveImageToWorkspace({
|
|
28864
|
-
workspaceRoot: options.workspaceRoot,
|
|
28865
|
-
folder: current().workspaceFolder,
|
|
28866
|
-
attachmentId: options.attachmentId,
|
|
28867
|
-
mediaType: options.mediaType,
|
|
28868
|
-
data: options.data
|
|
28869
|
-
});
|
|
28870
|
-
},
|
|
28871
|
-
getActiveWorkspaceRoot: () => Array.from(knownWorkspaceRoots)[0] || process.cwd(),
|
|
28872
|
-
getAllowedWorkspaceRoots: async () => {
|
|
28910
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
28911
|
+
registerSubscriptionRoutes(webCtx, subscriptionManager);
|
|
28912
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28913
|
+
kind: "exact",
|
|
28914
|
+
path: SETTINGS_HEALTH_ROUTE,
|
|
28915
|
+
handler: (req, res) => serveSettingsHealth(req, res, settingsHealth())
|
|
28916
|
+
}), "dsh-image-gen: settings health");
|
|
28917
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28918
|
+
kind: "exact",
|
|
28919
|
+
path: IMAGE_ROUTE,
|
|
28920
|
+
handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
|
|
28921
|
+
}), "dsh-image-gen: image route");
|
|
28922
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28923
|
+
kind: "exact",
|
|
28924
|
+
path: IMPORT_ROUTE,
|
|
28925
|
+
handler: (req, res) => serveImport(req, res, {
|
|
28926
|
+
saveImage: (image) => ctx.attachments.saveImage(image),
|
|
28927
|
+
maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
28928
|
+
mediaTypes: ctx.attachments.imageLimits.mediaTypes
|
|
28929
|
+
})
|
|
28930
|
+
}), "dsh-image-gen: import route");
|
|
28931
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28932
|
+
kind: "exact",
|
|
28933
|
+
path: DELETE_ROUTE,
|
|
28934
|
+
handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: async (filePath) => {
|
|
28873
28935
|
const discovered = await getDshWorkspaceRoots().catch(() => []);
|
|
28874
|
-
return /* @__PURE__ */ new Set([
|
|
28936
|
+
return deleteImageFromWorkspace(filePath, /* @__PURE__ */ new Set([
|
|
28875
28937
|
...knownWorkspaceRoots,
|
|
28876
28938
|
...discovered,
|
|
28877
28939
|
process.cwd()
|
|
28878
|
-
]);
|
|
28879
|
-
}
|
|
28880
|
-
|
|
28881
|
-
|
|
28882
|
-
|
|
28883
|
-
|
|
28884
|
-
|
|
28885
|
-
|
|
28886
|
-
|
|
28887
|
-
|
|
28888
|
-
|
|
28889
|
-
|
|
28890
|
-
|
|
28891
|
-
|
|
28892
|
-
|
|
28893
|
-
|
|
28894
|
-
|
|
28895
|
-
|
|
28896
|
-
|
|
28897
|
-
|
|
28898
|
-
|
|
28899
|
-
|
|
28900
|
-
|
|
28901
|
-
|
|
28902
|
-
|
|
28903
|
-
|
|
28904
|
-
|
|
28905
|
-
|
|
28906
|
-
|
|
28907
|
-
})
|
|
28908
|
-
|
|
28940
|
+
]));
|
|
28941
|
+
} })
|
|
28942
|
+
}), "dsh-image-gen: delete route");
|
|
28943
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28944
|
+
kind: "exact",
|
|
28945
|
+
path: SAVE_WORKSPACE_ROUTE,
|
|
28946
|
+
handler: (req, res) => serveSaveWorkspace(req, res, {
|
|
28947
|
+
readImage: (ref) => ctx.attachments.readImage(ref),
|
|
28948
|
+
saveToWorkspace: (options) => {
|
|
28949
|
+
if (options.workspaceRoot) knownWorkspaceRoots.add(options.workspaceRoot);
|
|
28950
|
+
return saveImageToWorkspace({
|
|
28951
|
+
workspaceRoot: options.workspaceRoot,
|
|
28952
|
+
folder: current().workspaceFolder,
|
|
28953
|
+
attachmentId: options.attachmentId,
|
|
28954
|
+
mediaType: options.mediaType,
|
|
28955
|
+
data: options.data
|
|
28956
|
+
});
|
|
28957
|
+
},
|
|
28958
|
+
getActiveWorkspaceRoot: () => Array.from(knownWorkspaceRoots)[0] || process.cwd(),
|
|
28959
|
+
getAllowedWorkspaceRoots: async () => {
|
|
28960
|
+
const discovered = await getDshWorkspaceRoots().catch(() => []);
|
|
28961
|
+
return /* @__PURE__ */ new Set([
|
|
28962
|
+
...knownWorkspaceRoots,
|
|
28963
|
+
...discovered,
|
|
28964
|
+
process.cwd()
|
|
28965
|
+
]);
|
|
28966
|
+
},
|
|
28967
|
+
isSaveEnabled: () => current().saveToWorkspace !== false
|
|
28968
|
+
})
|
|
28969
|
+
}), "dsh-image-gen: save workspace route");
|
|
28970
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28971
|
+
kind: "exact",
|
|
28972
|
+
path: TEST_CONNECTION_ROUTE,
|
|
28973
|
+
handler: (req, res) => serveTestConnection(req, res, {
|
|
28974
|
+
resolveKey: (provider) => resolveApiKey(ctx, provider),
|
|
28975
|
+
config: () => current(),
|
|
28976
|
+
subscriptionManager
|
|
28977
|
+
})
|
|
28978
|
+
}), "dsh-image-gen: test connection route");
|
|
28979
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28980
|
+
kind: "exact",
|
|
28981
|
+
path: CANVAS_STATE_ROUTE,
|
|
28982
|
+
handler: (req, res) => serveCanvasState(req, res, {
|
|
28983
|
+
mirror: canvasMirror,
|
|
28984
|
+
maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4) + 262144,
|
|
28985
|
+
maxImageBytes: ctx.attachments.imageLimits.maxImageBytes
|
|
28986
|
+
})
|
|
28987
|
+
}), "dsh-image-gen: canvas state route");
|
|
28988
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
28989
|
+
kind: "exact",
|
|
28990
|
+
path: CANVAS_ASSET_ROUTE,
|
|
28991
|
+
handler: (req, res) => serveCanvasAsset(req, res, {
|
|
28992
|
+
maxImageBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
28993
|
+
saveImage: (image) => ctx.attachments.saveImage(image)
|
|
28994
|
+
})
|
|
28995
|
+
}), "dsh-image-gen: canvas asset route");
|
|
28996
|
+
});
|
|
28909
28997
|
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
28910
|
-
promptCtx.systemPrompt.context({
|
|
28998
|
+
promptCtx.effect(() => promptCtx.systemPrompt.context({
|
|
28999
|
+
name: "dsh-image-gen:image-defaults",
|
|
29000
|
+
order: 59,
|
|
29001
|
+
text: () => imageToolContext(current())
|
|
29002
|
+
}), "dsh-image-gen: image tool context");
|
|
29003
|
+
promptCtx.effect(() => promptCtx.systemPrompt.context({
|
|
28911
29004
|
name: "dsh-image-gen:canvas",
|
|
28912
29005
|
order: 60,
|
|
28913
29006
|
text: () => canvasMirror.digest()
|
|
28914
|
-
});
|
|
29007
|
+
}), "dsh-image-gen: canvas context");
|
|
28915
29008
|
});
|
|
28916
29009
|
registerCanvasTools(ctx, canvasMirror, { persistSelectionImage: (image) => ctx.attachments.saveImage({
|
|
28917
29010
|
data: image.data,
|
|
28918
29011
|
mediaType: image.mediaType,
|
|
28919
29012
|
name: "canvas-selection"
|
|
28920
29013
|
}) });
|
|
28921
|
-
ctx.
|
|
28922
|
-
|
|
28923
|
-
|
|
28924
|
-
|
|
28925
|
-
|
|
28926
|
-
|
|
28927
|
-
|
|
28928
|
-
|
|
28929
|
-
|
|
28930
|
-
|
|
28931
|
-
|
|
28932
|
-
|
|
28933
|
-
|
|
28934
|
-
|
|
28935
|
-
|
|
28936
|
-
|
|
28937
|
-
|
|
28938
|
-
|
|
28939
|
-
|
|
28940
|
-
|
|
28941
|
-
|
|
28942
|
-
|
|
28943
|
-
|
|
28944
|
-
|
|
28945
|
-
|
|
28946
|
-
|
|
28947
|
-
|
|
28948
|
-
|
|
28949
|
-
|
|
28950
|
-
|
|
28951
|
-
|
|
29014
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
29015
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
29016
|
+
kind: "exact",
|
|
29017
|
+
path: STUDIO_ROUTE,
|
|
29018
|
+
handler: (req, res) => serveStudio(req, res, {
|
|
29019
|
+
describe: async () => {
|
|
29020
|
+
const base = await describeStudio(ctx, current(), subscriptionManager);
|
|
29021
|
+
const workspaces = await getDshWorkspacesFull().catch(() => []);
|
|
29022
|
+
const activeRoot = Array.from(knownWorkspaceRoots)[0] || workspaces[0]?.path || process.cwd();
|
|
29023
|
+
return {
|
|
29024
|
+
...base,
|
|
29025
|
+
workspaceRoot: activeRoot,
|
|
29026
|
+
workspaces
|
|
29027
|
+
};
|
|
29028
|
+
},
|
|
29029
|
+
generate: (input, signal) => {
|
|
29030
|
+
const fallbackRoot = Array.from(knownWorkspaceRoots)[0] || process.cwd();
|
|
29031
|
+
return generateFromStudio(ctx, current(), input, signal, fallbackRoot, subscriptionManager);
|
|
29032
|
+
},
|
|
29033
|
+
maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4 * 5) + 262144
|
|
29034
|
+
})
|
|
29035
|
+
}), "dsh-image-gen: studio route");
|
|
29036
|
+
const serveInspiration = createInspirationRoute();
|
|
29037
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
29038
|
+
kind: "prefix",
|
|
29039
|
+
path: INSPIRATION_ROUTE,
|
|
29040
|
+
handler: (req, res) => {
|
|
29041
|
+
const originalUrl = req.url ?? "/";
|
|
29042
|
+
req.url = originalUrl.startsWith("/plugins/dsh-image-gen/inspiration") ? originalUrl.slice(34) || "/" : originalUrl;
|
|
29043
|
+
return serveInspiration(req, res);
|
|
29044
|
+
}
|
|
29045
|
+
}), "dsh-image-gen: inspiration route");
|
|
29046
|
+
});
|
|
28952
29047
|
/** Per-item generation request shared by generate_image and generate_images. */
|
|
28953
|
-
const generateSingle = async (
|
|
29048
|
+
const generateSingle = async (rawArgs, exec) => {
|
|
29049
|
+
const args = cleanImageToolArgs(rawArgs);
|
|
28954
29050
|
const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
|
|
28955
29051
|
if (active.provider === "comfyui") {
|
|
28956
29052
|
const workflow = selectComfyUIWorkflow(active, args.workflow);
|
|
@@ -29066,7 +29162,7 @@ function apply(ctx, config = {}) {
|
|
|
29066
29162
|
"grok-sub",
|
|
29067
29163
|
"google-sub"
|
|
29068
29164
|
],
|
|
29069
|
-
description: "
|
|
29165
|
+
description: "OMIT unless the user explicitly requests a specific image provider. Omission uses the saved default; never guess a channel or fill this with an empty string. chatgpt-sub, grok-sub, and google-sub use logged-in subscription accounts instead of API keys."
|
|
29070
29166
|
},
|
|
29071
29167
|
model: {
|
|
29072
29168
|
type: "string",
|
|
@@ -29095,11 +29191,11 @@ function apply(ctx, config = {}) {
|
|
|
29095
29191
|
"2K",
|
|
29096
29192
|
"4K"
|
|
29097
29193
|
],
|
|
29098
|
-
description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
|
|
29194
|
+
description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K). NEVER send this for chatgpt-sub; omit unused fields entirely."
|
|
29099
29195
|
},
|
|
29100
29196
|
size: {
|
|
29101
29197
|
type: "string",
|
|
29102
|
-
description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope; WIDTHxHEIGHT maps to aspect_ratio for xAI Grok."
|
|
29198
|
+
description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope; WIDTHxHEIGHT maps to aspect_ratio for xAI Grok. Do not combine with aspect_ratio; omit instead of using an empty string."
|
|
29103
29199
|
},
|
|
29104
29200
|
quality: {
|
|
29105
29201
|
type: "string",
|
|
@@ -29141,7 +29237,7 @@ function apply(ctx, config = {}) {
|
|
|
29141
29237
|
"grok-sub",
|
|
29142
29238
|
"google-sub"
|
|
29143
29239
|
],
|
|
29144
|
-
description: "
|
|
29240
|
+
description: "OMIT unless the user explicitly requests a specific image provider. Uses the saved default for all items; never guess a channel or fill unused fields with empty strings."
|
|
29145
29241
|
},
|
|
29146
29242
|
model: {
|
|
29147
29243
|
type: "string",
|
|
@@ -29170,7 +29266,7 @@ function apply(ctx, config = {}) {
|
|
|
29170
29266
|
"2K",
|
|
29171
29267
|
"4K"
|
|
29172
29268
|
],
|
|
29173
|
-
description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
|
|
29269
|
+
description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K). NEVER send this for chatgpt-sub; omit unused fields entirely."
|
|
29174
29270
|
},
|
|
29175
29271
|
size: {
|
|
29176
29272
|
type: "string",
|
|
@@ -29363,7 +29459,7 @@ function apply(ctx, config = {}) {
|
|
|
29363
29459
|
"grok-sub",
|
|
29364
29460
|
"google-sub"
|
|
29365
29461
|
],
|
|
29366
|
-
description: "
|
|
29462
|
+
description: "OMIT unless the user explicitly requests a specific image provider. Omission uses the saved default; never guess a channel or fill this with an empty string. Subscription providers edit through their logged-in accounts."
|
|
29367
29463
|
},
|
|
29368
29464
|
model: {
|
|
29369
29465
|
type: "string",
|
|
@@ -29415,7 +29511,7 @@ function apply(ctx, config = {}) {
|
|
|
29415
29511
|
"2K",
|
|
29416
29512
|
"4K"
|
|
29417
29513
|
],
|
|
29418
|
-
description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K)."
|
|
29514
|
+
description: "Optional output resolution for Google Gemini, xAI Grok, Grok subscription (1K/2K), or Google subscription (1K/4K). NEVER send this for chatgpt-sub; omit unused fields entirely."
|
|
29419
29515
|
},
|
|
29420
29516
|
size: {
|
|
29421
29517
|
type: "string",
|
|
@@ -29431,8 +29527,9 @@ function apply(ctx, config = {}) {
|
|
|
29431
29527
|
}
|
|
29432
29528
|
},
|
|
29433
29529
|
output: imageOutput("Edited"),
|
|
29434
|
-
async execute(
|
|
29435
|
-
const
|
|
29530
|
+
async execute(rawArgs, exec) {
|
|
29531
|
+
const args = cleanImageToolArgs(rawArgs);
|
|
29532
|
+
const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model, "edit"));
|
|
29436
29533
|
const canvasSelection = args.source === "canvas_selection";
|
|
29437
29534
|
if (canvasSelection && (args.source_attachment_id !== void 0 || Array.isArray(args.source_attachment_ids) || args.source_path !== void 0 || Array.isArray(args.source_paths))) throw new Error("edit_image source=canvas_selection cannot be combined with source_attachment_id, source_attachment_ids, source_path, or source_paths; provide exactly one selector");
|
|
29438
29535
|
const sourceImages = canvasSelection ? await resolveCanvasSelectionReferences({
|
|
@@ -23,13 +23,14 @@ interface ImageSettings {
|
|
|
23
23
|
openaiSizes?: Record<string, Record<string, string>>;
|
|
24
24
|
openaiCompatBaseURL?: string;
|
|
25
25
|
openaiCompatModel?: string;
|
|
26
|
+
openaiCompatEditModel?: string;
|
|
26
27
|
openaiCompatSizes?: Record<string, Record<string, string>>;
|
|
27
28
|
openaiCompatQuality?: string;
|
|
28
29
|
openaiCompatQualityOptions?: string[];
|
|
29
30
|
openaiCompatExtraBody?: Record<string, unknown>;
|
|
30
31
|
openaiCompatEditSizeMode?: 'auto' | 'selected';
|
|
31
32
|
/** Edit-request shape for the openai-compat relay (#41); see src/config.ts. */
|
|
32
|
-
openaiCompatEditFormat?: 'multipart' | 'jsonImageUrlArray' | 'formReferenceImages';
|
|
33
|
+
openaiCompatEditFormat?: 'multipart' | 'jsonImageUrlArray' | 'formReferenceImages' | 'siliconflowJson';
|
|
33
34
|
/** Extra JSON fields merged into the JSON edit body; ignored in multipart mode. */
|
|
34
35
|
openaiCompatEditExtra?: Record<string, unknown>;
|
|
35
36
|
seedreamBaseURL?: string;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface Config {
|
|
|
24
24
|
/** OpenAI-compatible relay settings; independent from the official OpenAI row. */
|
|
25
25
|
openaiCompatBaseURL?: string;
|
|
26
26
|
openaiCompatModel?: string;
|
|
27
|
+
/** Editing model; empty reuses the generation model. */
|
|
28
|
+
openaiCompatEditModel?: string;
|
|
27
29
|
/** Default quality; empty preserves the upstream default by omitting the field. */
|
|
28
30
|
openaiCompatQuality?: string;
|
|
29
31
|
/** Quality values accepted by this relay; independent of resolution tiers. */
|
|
@@ -40,7 +42,7 @@ export interface Config {
|
|
|
40
42
|
* `reference_images` field is a JSON array of base64 strings. Defaults to
|
|
41
43
|
* `multipart`.
|
|
42
44
|
*/
|
|
43
|
-
openaiCompatEditFormat?: 'multipart' | 'jsonImageUrlArray' | 'formReferenceImages';
|
|
45
|
+
openaiCompatEditFormat?: 'multipart' | 'jsonImageUrlArray' | 'formReferenceImages' | 'siliconflowJson';
|
|
44
46
|
/**
|
|
45
47
|
* Extra JSON fields merged into the JSON edit body last (can override the
|
|
46
48
|
* built-in defaults), e.g. SenseNova's `watermark`/`prompt_extend`.
|
|
@@ -173,7 +175,7 @@ export declare function resolveProvider(config: Config): {
|
|
|
173
175
|
* Apply a per-call provider and/or model override on top of the saved config.
|
|
174
176
|
* `model` is ignored for ComfyUI, whose per-call equivalent is `workflow`.
|
|
175
177
|
*/
|
|
176
|
-
export declare function withProviderOverrides(config: Config, provider?: ImageProvider, model?: string): Config;
|
|
178
|
+
export declare function withProviderOverrides(config: Config, provider?: ImageProvider, model?: string, mode?: 'generate' | 'edit'): Config;
|
|
177
179
|
/**
|
|
178
180
|
* One-time migration: when the legacy single OpenAI slot points at a relay
|
|
179
181
|
* (non-official base URL) and the compat row is empty, move the relay config
|
|
@@ -32,7 +32,7 @@ export declare function generateOpenAICompatibleImage(input: {
|
|
|
32
32
|
arkOptions?: ArkOutputOptions;
|
|
33
33
|
}): Promise<GeneratedCompatibleImage>;
|
|
34
34
|
/** How the edits endpoint expects its request body (#41). */
|
|
35
|
-
export type CompatEditFormat = 'multipart' | 'jsonImageUrlArray' | 'formReferenceImages' | 'xaiJson' | 'qwenJson';
|
|
35
|
+
export type CompatEditFormat = 'multipart' | 'jsonImageUrlArray' | 'formReferenceImages' | 'siliconflowJson' | 'xaiJson' | 'qwenJson';
|
|
36
36
|
export declare function editOpenAICompatibleImage(input: {
|
|
37
37
|
apiKey: string;
|
|
38
38
|
baseURL: string;
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -200,6 +200,8 @@ export interface StudioProviderProfile {
|
|
|
200
200
|
provider: StudioProvider;
|
|
201
201
|
label: string;
|
|
202
202
|
model: string;
|
|
203
|
+
/** Separate configured edit model; absent preserves the generation model. */
|
|
204
|
+
editModel?: string;
|
|
203
205
|
configured: boolean;
|
|
204
206
|
supportsEditing: boolean;
|
|
205
207
|
ratioOptions: StudioOption[];
|
|
@@ -216,6 +218,8 @@ export interface StudioProviderProfile {
|
|
|
216
218
|
watermarkOptions?: StudioOption[];
|
|
217
219
|
watermarkCapabilities?: Record<'generate' | 'edit', StudioWatermarkCapability>;
|
|
218
220
|
}
|
|
221
|
+
/** A model picker/request must use the same operation-specific default. */
|
|
222
|
+
export declare function studioModelForMode(profile: StudioProviderProfile, mode: 'generate' | 'edit'): string;
|
|
219
223
|
export declare function studioWatermarkCapability(profile: StudioProviderProfile | undefined, mode: 'generate' | 'edit'): StudioWatermarkCapability;
|
|
220
224
|
/** Lightweight workspace description exposed to the client for scoping. */
|
|
221
225
|
export interface StudioWorkspaceInfo {
|
package/package.json
CHANGED