dsh-image-gen 0.2.1 → 0.2.2

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  # 🎨 dsh-image-gen
8
8
 
9
- **Bring ChatGPT-like conversational image generation and image editing to DeepSeek Harness, with support for text-to-image, image-to-image, continuous editing, and multiple image providers.**
9
+ **Complete conversational image capabilities for DeepSeek Harness: text-to-image, image-to-image, multi-image references, continuous editing, local ComfyUI, and gallery management.**
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/dsh-image-gen.svg?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-image-gen)
12
12
  [![DSH Plugin](https://img.shields.io/badge/Plugin%20For-DeepSeek%20Harness-6366f1?style=flat-square)](https://github.com/deepseek-ai)
@@ -27,7 +27,7 @@ Install the image generation plugin by running: pnpm dsh plugin --profile web ad
27
27
 
28
28
  <br />
29
29
 
30
- <p align="center">After installation, enter your API Key in DSH Settings, then tell the Agent:</p>
30
+ <p align="center">After installation, enter your API key or configure local ComfyUI in DSH Settings, then tell the Agent:</p>
31
31
 
32
32
  ```text
33
33
  Draw a cyberpunk cat on a neon street in a rainy night.
@@ -56,7 +56,7 @@ DeepSeek Harness empowers agents to use tools for various tasks. This project ad
56
56
  graph LR
57
57
  A[User Prompt] --> B[DeepSeek Harness Agent]
58
58
  B --> C[generate_image / edit_image]
59
- C --> D[Gemini / OpenAI / Seedream / DashScope]
59
+ C --> D[Gemini / OpenAI / Seedream / DashScope / ComfyUI]
60
60
  D --> E[Image Data]
61
61
  E --> F[In-chat Conversation Stream]
62
62
  ```
@@ -91,12 +91,12 @@ pnpm dsh plugin --profile web add ./dsh-image-gen
91
91
 
92
92
  </details>
93
93
 
94
- ### 2. Configure API Key & Workspace Settings
94
+ ### 2. Configure Provider & Workspace Settings
95
95
 
96
96
  Open DSH Web (`http://localhost:3080`):
97
97
 
98
98
  1. Navigate to **Settings → Plugins → Image generation**.
99
- 2. Select Provider, input API Key.
99
+ 2. Select a Provider. Enter an API key for cloud providers; for local ComfyUI, enter its URL and import an API Format Workflow JSON using `{{prompt}}` for the prompt and optional `{{seed}}` for the seed.
100
100
  3. Optionally enable **Save to workspace** (enabled by default) and specify subfolder, then click **Save**.
101
101
 
102
102
  <div align="center">
@@ -128,7 +128,7 @@ Click the **`[Gallery]`** Tab in the top navigation bar to browse and search all
128
128
  - 💬 **In-Chat Image Generation**: Tell your Agent what you want to draw without switching tabs or tools.
129
129
  - 🖼️ **Native Image Gallery**: Dedicated "Gallery" tab automatically collecting all generated images with keyword search, provider filtering, single-item deletion (with confirmation and persistent tombstones), and quick copy/download.
130
130
  - 🔍 **Interactive Image Tools**: High-res fullscreen preview, one-click copy to clipboard, local download, and open in new tab.
131
- - 🎨 **Multi-Provider Support**: Supports Google Gemini, OpenAI Images, OpenAI Compatible API, ByteDance Seedream / Volcengine Ark, and Aliyun DashScope (Wanx / Qwen-Image).
131
+ - 🎨 **Multi-Provider Support**: Supports Google Gemini, OpenAI Images, OpenAI Compatible API, ByteDance Seedream / Volcengine Ark, Aliyun DashScope (Wanx / Qwen-Image), and local ComfyUI workflows.
132
132
  - 🔑 **BYOK (Bring Your Own Key)**: Uses your own API keys managed securely by DSH credentials service with write-only protection.
133
133
  - 🖼️ **Durable Session Persistence**: Images integrate with DSH Attachment and conversation lifecycle, preserved across reloads.
134
134
  - 💾 **Workspace File Output**: By default each generated image is also written as a file into the current session workspace; the tool result carries the absolute file path. Can be disabled or re-pointed in settings.
@@ -145,6 +145,7 @@ Click the **`[Gallery]`** Tab in the top navigation bar to browse and search all
145
145
  | **OpenAI Compatible** | Custom | Custom Base URL |
146
146
  | **ByteDance Seedream / Volcengine Ark** | `doubao-seedream-5-0-260128` | `https://ark.cn-beijing.volces.com/api/v3` |
147
147
  | **Aliyun DashScope / Wanx** | `wanx2.1-t2i-turbo` | `https://dashscope.aliyuncs.com/api/v1` |
148
+ | **Local ComfyUI (text-to-image only)** | Imported API Format Workflow | `http://127.0.0.1:8188` |
148
149
 
149
150
  ---
150
151
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  # 🎨 dsh-image-gen
8
8
 
9
- **让 DeepSeek Harness 拥有类似 ChatGPT 的原生对话生图与图片编辑能力,支持文生图、图生图、连续编辑,并兼容多家图像模型提供商。**
9
+ **为 DeepSeek Harness 提供完整的对话图像能力:文生图、图生图、多图参考、连续编辑、本地 ComfyUI 与画廊管理。**
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/dsh-image-gen.svg?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-image-gen)
12
12
  [![DSH Plugin](https://img.shields.io/badge/Plugin%20For-DeepSeek%20Harness-6366f1?style=flat-square)](https://github.com/deepseek-ai)
@@ -28,7 +28,7 @@
28
28
 
29
29
  <br />
30
30
 
31
- <p align="center">安装完成后,在 DSH 设置中填入自己的 API Key,就可以直接对 Agent 说:</p>
31
+ <p align="center">安装完成后,在 DSH 设置中填入自己的 API Key 或配置本地 ComfyUI,就可以直接对 Agent 说:</p>
32
32
 
33
33
  ```text
34
34
  帮我画一张雨夜霓虹街头的赛博朋克猫咪。
@@ -57,7 +57,7 @@ DeepSeek Harness 已经可以让 Agent 调用不同工具完成任务,本项
57
57
  graph LR
58
58
  A[用户 Prompt] --> B[DeepSeek Harness Agent]
59
59
  B --> C[generate_image / edit_image]
60
- C --> D[Gemini / OpenAI / Seedream / DashScope]
60
+ C --> D[Gemini / OpenAI / Seedream / DashScope / ComfyUI]
61
61
  D --> E[图片数据]
62
62
  E --> F[当前 Conversation 对话流]
63
63
  ```
@@ -92,12 +92,12 @@ pnpm dsh plugin --profile web add ./dsh-image-gen
92
92
 
93
93
  </details>
94
94
 
95
- ### 2. 配置 API Key 与工作区设置
95
+ ### 2. 配置 Provider 与工作区设置
96
96
 
97
97
  打开 DSH Web 页面(默认 `http://localhost:3080`):
98
98
 
99
99
  1. 进入 **Settings → Plugins → Image generation**。
100
- 2. 选择 Provider,填写 API Key。
100
+ 2. 选择 Provider;云端 Provider 填写 API Key,本地 ComfyUI 填写地址并导入 API Format Workflow JSON(提示词位置使用 `{{prompt}}`,种子可选用 `{{seed}}`)。
101
101
  3. 可按需开启 **保存到工作区**(默认开启)并自定义子目录,点击 **保存** 即可。
102
102
 
103
103
  <div align="center">
@@ -137,7 +137,7 @@ Agent 会调用 `edit_image`,复用当前会话中的图片继续修改。
137
137
  - 💬 **原生对话生图与编辑**:直接在 DeepSeek Harness 对话中生成图片,也可以基于已有图片继续修改。
138
138
  - 🔁 **连续多轮编辑**:支持复用当前会话中的上传图片、历史生成图和上一轮编辑结果继续迭代。
139
139
  - 🖼️ **画廊与图片工具**:自动汇总历史图片,支持搜索、筛选、全屏预览、复制、下载与删除。
140
- - 🎨 **多 Provider 支持**:兼容 Google Gemini、OpenAI Images / Compatible、Seedream、DashScope Qwen Image。
140
+ - 🎨 **多 Provider 支持**:兼容 Google Gemini、OpenAI Images / Compatible、Seedream、DashScope Qwen Image,以及用户本地的 ComfyUI 工作流。
141
141
  - 🔑 **BYOK + 原生设置**:API Key、Provider、模型和 Endpoint 都可以直接在 DSH 设置中配置。
142
142
  - 💾 **会话与工作区保存**:图片接入 DSH Attachment / Conversation,并可自动保存到当前工作区。
143
143
 
@@ -152,6 +152,7 @@ Agent 会调用 `edit_image`,复用当前会话中的图片继续修改。
152
152
  | **OpenAI Compatible** | 自定义 | 自定义 Base URL |
153
153
  | **ByteDance Seedream / 火山方舟** | `doubao-seedream-5-0-260128` | `https://ark.cn-beijing.volces.com/api/v3` |
154
154
  | **Aliyun DashScope / Qwen Image** | `qwen-image-3.0` | `https://dashscope.aliyuncs.com/api/v1` |
155
+ | **Local ComfyUI(仅文生图)** | 用户导入的 API Format Workflow | `http://127.0.0.1:8188` |
155
156
 
156
157
  ---
157
158
 
package/README.zh-CN.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  # 🎨 dsh-image-gen
8
8
 
9
- **让 DeepSeek Harness 像 ChatGPT 一样在对话中直接生成图片,支持画廊汇总,全屏预览、快捷复制与一键下载。**
9
+ **为 DeepSeek Harness 提供完整的对话图像能力:文生图、图生图、多图参考、连续编辑、本地 ComfyUI 与画廊管理。**
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/dsh-image-gen.svg?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-image-gen)
12
12
  [![DSH Plugin](https://img.shields.io/badge/Plugin%20For-DeepSeek%20Harness-6366f1?style=flat-square)](https://github.com/deepseek-ai)
@@ -27,7 +27,7 @@
27
27
 
28
28
  <br />
29
29
 
30
- <p align="center">安装完成后,在 DSH 设置中填入自己的 API Key,就可以直接对 Agent 说:</p>
30
+ <p align="center">安装完成后,在 DSH 设置中填入自己的 API Key 或配置本地 ComfyUI,就可以直接对 Agent 说:</p>
31
31
 
32
32
  ```text
33
33
  帮我画一张雨夜霓虹街头的赛博朋克猫咪。
@@ -53,7 +53,7 @@ DeepSeek Harness 已经可以让 Agent 调用不同工具完成任务,本项
53
53
  graph LR
54
54
  A[用户 Prompt] --> B[DeepSeek Harness Agent]
55
55
  B --> C[generate_image 工具]
56
- C --> D[Gemini / OpenAI / Seedream / DashScope]
56
+ C --> D[Gemini / OpenAI / Seedream / DashScope / ComfyUI]
57
57
  D --> E[图片数据]
58
58
  E --> F[当前 Conversation 对话流]
59
59
  ```
@@ -88,12 +88,12 @@ pnpm dsh plugin --profile web add ./dsh-image-gen
88
88
 
89
89
  </details>
90
90
 
91
- ### 2. 配置 API Key 与工作区设置
91
+ ### 2. 配置 Provider 与工作区设置
92
92
 
93
93
  打开 DSH Web 页面(默认 `http://localhost:3080`):
94
94
 
95
95
  1. 进入 **Settings → Plugins → Image generation**。
96
- 2. 选择 Provider,填写 API Key。
96
+ 2. 选择 Provider;云端 Provider 填写 API Key,本地 ComfyUI 填写地址并导入 API Format Workflow JSON(提示词位置使用 `{{prompt}}`,种子可选用 `{{seed}}`)。
97
97
  3. 可按需开启 **保存到工作区**(默认开启)并自定义子目录,点击 **保存** 即可。
98
98
 
99
99
  <div align="center">
@@ -125,7 +125,7 @@ pnpm dsh plugin --profile web add ./dsh-image-gen
125
125
  - 💬 **对话中直接生图**:不需要切换到其他网站,也不需要手动复制 Prompt,直接告诉 Agent 你想画什么即可。
126
126
  - 🖼️ **历史生图画廊**:顶栏自带「画廊」Tab,自动汇总所有历史生成的图片,支持关键词搜索、厂商筛选、单张删除(带防误触确认与持久化墓碑)与一键复制/下载。
127
127
  - 🔍 **交互式图片工具**:支持点击全屏大图预览、一键复制图片到剪贴板、本地下载与新标签页打开。
128
- - 🎨 **多 Provider 支持**:支持 Google Gemini、OpenAI Images、OpenAI Compatible API、ByteDance Seedream / 火山方舟以及阿里云 DashScope(通义万相 / Qwen-Image)。Provider、模型和 Endpoint 均可在设置中自由定制。
128
+ - 🎨 **多 Provider 支持**:支持 Google Gemini、OpenAI Images、OpenAI Compatible API、ByteDance Seedream / 火山方舟、阿里云 DashScope(通义万相 / Qwen-Image)以及用户本地的 ComfyUI 工作流。
129
129
  - 🔑 **BYOK (自带 Key)**:插件使用你自己的 API Key。API Key 通过 DeepSeek Harness 的 `credentials` 服务管理,采用写保护隔离,不需要写进项目源码或配置文件,前端不存明文。
130
130
  - 🖼️ **图片跟随会话保存**:生成结果会接入 DeepSeek Harness 的 Attachment / Conversation 体系,重新打开历史会话后,仍然可以看到之前生成的图片。
131
131
  - 💾 **生成图片自动落盘到工作区**:默认在每次生成后把图片文件保存到当前会话工作区,工具结果会返回文件的绝对路径;可在设置中关闭或修改保存目录。
@@ -142,6 +142,7 @@ pnpm dsh plugin --profile web add ./dsh-image-gen
142
142
  | **OpenAI Compatible** | 自定义 | 自定义 Base URL |
143
143
  | **ByteDance Seedream / 火山方舟** | `doubao-seedream-5-0-260128` | `https://ark.cn-beijing.volces.com/api/v3` |
144
144
  | **Aliyun DashScope / 通义万相** | `wanx2.1-t2i-turbo` | `https://dashscope.aliyuncs.com/api/v1` |
145
+ | **Local ComfyUI(仅文生图)** | 用户导入的 API Format Workflow | `http://127.0.0.1:8188` |
145
146
 
146
147
  ---
147
148
 
package/lib/client.js CHANGED
@@ -17,19 +17,58 @@ window.__ModuleLoader__.load({
17
17
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
18
18
  const DEFAULT_SEEDREAM_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
19
19
  const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
20
+ const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
21
+ const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
20
22
  const DEFAULT_MODELS = {
21
23
  google: "gemini-3.1-flash-image",
22
24
  openai: "gpt-image-2",
23
25
  seedream: "doubao-seedream-5-0-260128",
24
- dashscope: "qwen-image-3.0"
26
+ dashscope: "qwen-image-3.0",
27
+ comfyui: "API workflow"
25
28
  };
26
29
  const DEFAULT_BASE_URLS = {
27
30
  google: DEFAULT_GOOGLE_ENDPOINT,
28
31
  openai: DEFAULT_OPENAI_BASE_URL,
29
32
  seedream: DEFAULT_SEEDREAM_BASE_URL,
30
- dashscope: DEFAULT_DASHSCOPE_ENDPOINT
33
+ dashscope: DEFAULT_DASHSCOPE_ENDPOINT,
34
+ comfyui: DEFAULT_COMFYUI_BASE_URL
31
35
  };
32
36
  //#endregion
37
+ //#region lib/types/comfyui-workflow.js
38
+ /** Pure ComfyUI API-workflow validation and placeholder injection. */
39
+ const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
40
+ /** Validate imported JSON without exposing workflow graph details to callers. */
41
+ function validateComfyUIWorkflowJson(workflowJson) {
42
+ parseWorkflow(workflowJson);
43
+ }
44
+ function parseWorkflow(workflowJson) {
45
+ if (workflowJson.trim().length === 0) throw new Error("Import a ComfyUI API workflow JSON file in Settings before generating");
46
+ if (new TextEncoder().encode(workflowJson).byteLength > 5242880) throw new Error("ComfyUI workflow file must be no larger than 5 MB");
47
+ let value;
48
+ try {
49
+ value = JSON.parse(workflowJson);
50
+ } catch {
51
+ throw new Error("ComfyUI workflow file is not valid JSON");
52
+ }
53
+ if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
54
+ if (!Object.values(value).some((node) => {
55
+ const inputs = record(node)?.inputs;
56
+ return inputs !== void 0 && containsPromptPlaceholder(inputs);
57
+ })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
58
+ return value;
59
+ }
60
+ function containsPromptPlaceholder(value) {
61
+ if (typeof value === "string") return value.includes(COMFYUI_PROMPT_PLACEHOLDER);
62
+ if (Array.isArray(value)) return value.some(containsPromptPlaceholder);
63
+ return isRecord(value) && Object.values(value).some(containsPromptPlaceholder);
64
+ }
65
+ function isRecord(value) {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
68
+ function record(value) {
69
+ return isRecord(value) ? value : void 0;
70
+ }
71
+ //#endregion
33
72
  //#region lib/types/client/gallery-store.js
34
73
  const DB_NAME = "dsh_image_gen_db";
35
74
  const DB_VERSION = 2;
@@ -906,6 +945,7 @@ window.__ModuleLoader__.load({
906
945
  providerOpenAI: "OpenAI / 中转站",
907
946
  providerSeedream: "字节 Seedream",
908
947
  providerDashScope: "阿里 DashScope (通义万相 / Qwen)",
948
+ providerComfyUI: "本地 ComfyUI",
909
949
  apiKeyLabel: "{provider} API Key",
910
950
  apiKeyPlaceholder: "留空即可保留已配置的 Key",
911
951
  apiKeyHint: "安全保存为 {key};页面不会读回明文。",
@@ -916,7 +956,17 @@ window.__ModuleLoader__.load({
916
956
  endpointHintOpenAI: "中转站请填其 OpenAI 兼容的 /v1 地址。",
917
957
  endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
918
958
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
959
+ endpointHintComfyUI: "正在运行且 DSH Host 可以访问的 ComfyUI 地址,默认使用本机 8188 端口。",
919
960
  model: "模型",
961
+ workflow: "API Workflow",
962
+ workflowImport: "导入 JSON 文件",
963
+ workflowReplace: "替换 JSON 文件",
964
+ workflowMissing: "尚未导入工作流",
965
+ workflowImported: "已导入 {name}",
966
+ workflowHint: "请从 ComfyUI 导出 API Format JSON,并在提示词输入位置写入 {{prompt}};可用 {{seed}} 作为随机种子。",
967
+ workflowTooLarge: "工作流文件不能超过 5 MB。",
968
+ timeout: "生成超时(秒)",
969
+ timeoutHint: "包括提交、等待和下载图片;默认 300 秒。",
920
970
  saveToWorkspace: "保存到工作区",
921
971
  saveToWorkspaceHint: "每次生成后,把图片文件保存到当前会话工作区。",
922
972
  folder: "工作区文件夹",
@@ -946,6 +996,7 @@ window.__ModuleLoader__.load({
946
996
  providerOpenAI: "OpenAI / Relay",
947
997
  providerSeedream: "ByteDance Seedream",
948
998
  providerDashScope: "Aliyun DashScope (Wanx / Qwen)",
999
+ providerComfyUI: "Local ComfyUI",
949
1000
  apiKeyLabel: "{provider} API Key",
950
1001
  apiKeyPlaceholder: "Leave empty to keep configured key",
951
1002
  apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
@@ -956,7 +1007,17 @@ window.__ModuleLoader__.load({
956
1007
  endpointHintOpenAI: "OpenAI-compatible /v1 base URL for relays.",
957
1008
  endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
958
1009
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
1010
+ endpointHintComfyUI: "A running ComfyUI server reachable by the DSH Host; the default points to port 8188 on this computer.",
959
1011
  model: "Model",
1012
+ workflow: "API Workflow",
1013
+ workflowImport: "Import JSON file",
1014
+ workflowReplace: "Replace JSON file",
1015
+ workflowMissing: "No workflow imported",
1016
+ workflowImported: "Imported {name}",
1017
+ workflowHint: "Export an API Format JSON from ComfyUI and place {{prompt}} in its prompt input. {{seed}} is available for a random seed.",
1018
+ workflowTooLarge: "Workflow files must be no larger than 5 MB.",
1019
+ timeout: "Generation timeout (seconds)",
1020
+ timeoutHint: "Covers submission, waiting, and image download; defaults to 300 seconds.",
960
1021
  saveToWorkspace: "Save to workspace",
961
1022
  saveToWorkspaceHint: "Write each generated image as a file into the session workspace.",
962
1023
  folder: "Workspace folder",
@@ -996,6 +1057,12 @@ window.__ModuleLoader__.load({
996
1057
  .dsh-ig-input{box-sizing:border-box;width:100%;padding:8px 12px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-3,transparent);color:inherit;outline:none;transition:border-color .15s}
997
1058
  .dsh-ig-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff)}
998
1059
  .dsh-ig-input-group{display:flex;gap:8px;align-items:center}
1060
+ .dsh-ig-file-row{display:flex;align-items:center;gap:10px;min-width:0}
1061
+ .dsh-ig-file-input{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;clip-path:inset(50%)}
1062
+ .dsh-ig-file-button{appearance:none;flex:none;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;padding:7px 12px;background:var(--dsw-alias-bg-layer-3,#f9fafb);color:var(--dsw-alias-label-secondary,inherit);font-size:13px;cursor:pointer;transition:background .15s,border-color .15s}
1063
+ .dsh-ig-file-button:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1064
+ .dsh-ig-file-button:focus-within{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:2px}
1065
+ .dsh-ig-file-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,inherit);font-size:12px}
999
1066
  .dsh-ig-btn-reset{appearance:none;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;padding:7px 12px;background:var(--dsw-alias-bg-layer-3,#f9fafb);color:var(--dsw-alias-label-secondary,inherit);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;transition:background .15s,border-color .15s}
1000
1067
  .dsh-ig-btn-reset:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1001
1068
  .dsh-ig-hint,.dsh-ig-status{margin:0;color:var(--dsw-alias-label-tertiary,#7b818b);font-size:12px;line-height:1.4}
@@ -1078,16 +1145,16 @@ window.__ModuleLoader__.load({
1078
1145
  /** Required browser services. */
1079
1146
  const inject = [
1080
1147
  "slots",
1081
- "connection",
1082
1148
  "remote",
1149
+ "remote.credentials",
1083
1150
  "settingsScope",
1084
1151
  "locale"
1085
1152
  ];
1086
1153
  /** Mount the settings card, generated-image card, and native conversation gallery view. */
1087
1154
  function apply(ctx) {
1088
- const { api } = ctx.get("connection");
1089
1155
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
1090
1156
  const locale = ctx.get("locale");
1157
+ const credentials = ctx.remote.credentials;
1091
1158
  ctx.effect(() => {
1092
1159
  const style = document.createElement("style");
1093
1160
  style.dataset.plugin = "dsh-image-gen";
@@ -1103,7 +1170,7 @@ window.__ModuleLoader__.load({
1103
1170
  key: IMAGE_GENERATION_NAMESPACE,
1104
1171
  inject: () => ({
1105
1172
  scope,
1106
- credentials: api.credentials,
1173
+ credentials,
1107
1174
  locale
1108
1175
  })
1109
1176
  }, ImageGenerationSettingsCard));
@@ -1135,6 +1202,9 @@ window.__ModuleLoader__.load({
1135
1202
  const [provider, setProvider] = (0, react.useState)("google");
1136
1203
  const [model, setModel] = (0, react.useState)("");
1137
1204
  const [baseURL, setBaseURL] = (0, react.useState)("");
1205
+ const [workflowJson, setWorkflowJson] = (0, react.useState)("");
1206
+ const [workflowName, setWorkflowName] = (0, react.useState)("");
1207
+ const [timeoutSeconds, setTimeoutSeconds] = (0, react.useState)(DEFAULT_COMFYUI_TIMEOUT_MS / 1e3);
1138
1208
  const [saveToWorkspace, setSaveToWorkspace] = (0, react.useState)(true);
1139
1209
  const [workspaceFolder, setWorkspaceFolder] = (0, react.useState)("dsh-image-gen");
1140
1210
  const [key, setKey] = (0, react.useState)("");
@@ -1158,7 +1228,8 @@ window.__ModuleLoader__.load({
1158
1228
  google: t("providerGoogle"),
1159
1229
  openai: t("providerOpenAI"),
1160
1230
  seedream: t("providerSeedream"),
1161
- dashscope: t("providerDashScope")
1231
+ dashscope: t("providerDashScope"),
1232
+ comfyui: t("providerComfyUI")
1162
1233
  };
1163
1234
  (0, react.useEffect)(() => {
1164
1235
  const value = snapshot.value;
@@ -1166,13 +1237,21 @@ window.__ModuleLoader__.load({
1166
1237
  setProvider(next);
1167
1238
  setModel(modelOf(next, value));
1168
1239
  setBaseURL(baseURLOf(next, value));
1240
+ setWorkflowJson(value?.comfyuiWorkflowJson ?? "");
1241
+ setWorkflowName(value?.comfyuiWorkflowName ?? "");
1242
+ setTimeoutSeconds(Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3)));
1169
1243
  setSaveToWorkspace(value?.saveToWorkspace ?? true);
1170
1244
  setWorkspaceFolder(value?.workspaceFolder ?? "dsh-image-gen");
1171
1245
  }, [snapshot]);
1172
1246
  (0, react.useEffect)(() => {
1247
+ const keyRef = KEY_REF[provider];
1248
+ if (keyRef === void 0) {
1249
+ setConfigured(void 0);
1250
+ return;
1251
+ }
1173
1252
  let active = true;
1174
- props.credentials.describe({ refs: [KEY_REF[provider]] }).then((response) => {
1175
- if (active) setConfigured(response.result.ok ? response.result.value.credentials[KEY_REF[provider]]?.configured ?? false : void 0);
1253
+ props.credentials.describe([keyRef]).then((response) => {
1254
+ if (active) setConfigured(response.ok ? response.value?.[keyRef]?.configured ?? false : void 0);
1176
1255
  }).catch(() => {
1177
1256
  if (active) setConfigured(void 0);
1178
1257
  });
@@ -1186,16 +1265,23 @@ window.__ModuleLoader__.load({
1186
1265
  setMessage("");
1187
1266
  try {
1188
1267
  await props.scope.set("provider", provider);
1189
- await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : provider === "seedream" ? "seedreamModel" : "dashscopeModel", model);
1190
- await props.scope.set(provider === "google" ? "googleEndpoint" : provider === "openai" ? "openaiBaseURL" : provider === "seedream" ? "seedreamBaseURL" : "dashscopeEndpoint", baseURL);
1268
+ if (provider === "comfyui") {
1269
+ validateComfyUIWorkflowJson(workflowJson);
1270
+ await props.scope.set("comfyuiBaseURL", baseURL);
1271
+ await props.scope.set("comfyuiWorkflowJson", workflowJson);
1272
+ await props.scope.set("comfyuiWorkflowName", workflowName);
1273
+ await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(timeoutSeconds)) * 1e3);
1274
+ } else {
1275
+ await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : provider === "seedream" ? "seedreamModel" : "dashscopeModel", model);
1276
+ await props.scope.set(provider === "google" ? "googleEndpoint" : provider === "openai" ? "openaiBaseURL" : provider === "seedream" ? "seedreamBaseURL" : "dashscopeEndpoint", baseURL);
1277
+ }
1191
1278
  await props.scope.set("saveToWorkspace", saveToWorkspace);
1192
1279
  await props.scope.set("workspaceFolder", workspaceFolder.trim());
1193
1280
  if (key.trim().length > 0) {
1194
- const response = await props.credentials.set({
1195
- ref: KEY_REF[provider],
1196
- value: key.trim()
1197
- });
1198
- if (!response.result.ok) throw new Error(response.result.error.message);
1281
+ const keyRef = KEY_REF[provider];
1282
+ if (keyRef === void 0) throw new Error("ComfyUI does not use an API key in this version");
1283
+ const response = await props.credentials.set(keyRef, key.trim());
1284
+ if (!response.ok) throw new Error(response.error?.message ?? "Failed to save API key");
1199
1285
  setKey("");
1200
1286
  setConfigured(true);
1201
1287
  }
@@ -1207,6 +1293,23 @@ window.__ModuleLoader__.load({
1207
1293
  }
1208
1294
  };
1209
1295
  const keyStatus = configured === void 0 ? t("checkingKey") : configured ? t("keyConfigured") : t("keyNotConfigured");
1296
+ const workflowStatus = workflowName.length > 0 ? t("workflowImported", { name: workflowName }) : t("workflowMissing");
1297
+ const importWorkflow = async (event) => {
1298
+ const file = event.target.files?.[0];
1299
+ event.target.value = "";
1300
+ if (file === void 0) return;
1301
+ setMessage("");
1302
+ try {
1303
+ if (file.size > 5242880) throw new Error(t("workflowTooLarge"));
1304
+ const json = await file.text();
1305
+ validateComfyUIWorkflowJson(json);
1306
+ setWorkflowJson(json);
1307
+ setWorkflowName(file.name);
1308
+ setMessage(t("workflowImported", { name: file.name }));
1309
+ } catch (cause) {
1310
+ setMessage(cause instanceof Error ? cause.message : String(cause));
1311
+ }
1312
+ };
1210
1313
  return (0, react_jsx_runtime.jsxs)("li", {
1211
1314
  className: `dsh-ig-card ${open ? "dsh-ig-card-open" : ""}`,
1212
1315
  children: [(0, react_jsx_runtime.jsxs)("button", {
@@ -1279,6 +1382,10 @@ window.__ModuleLoader__.load({
1279
1382
  (0, react_jsx_runtime.jsx)("option", {
1280
1383
  value: "dashscope",
1281
1384
  children: t("providerDashScope")
1385
+ }),
1386
+ (0, react_jsx_runtime.jsx)("option", {
1387
+ value: "comfyui",
1388
+ children: t("providerComfyUI")
1282
1389
  })
1283
1390
  ]
1284
1391
  }),
@@ -1288,7 +1395,7 @@ window.__ModuleLoader__.load({
1288
1395
  })
1289
1396
  ]
1290
1397
  }),
1291
- (0, react_jsx_runtime.jsxs)("label", {
1398
+ provider !== "comfyui" ? (0, react_jsx_runtime.jsxs)("label", {
1292
1399
  className: "dsh-ig-field",
1293
1400
  children: [
1294
1401
  (0, react_jsx_runtime.jsx)("span", {
@@ -1307,10 +1414,10 @@ window.__ModuleLoader__.load({
1307
1414
  }),
1308
1415
  (0, react_jsx_runtime.jsx)("span", {
1309
1416
  className: "dsh-ig-hint",
1310
- children: t("apiKeyHint", { key: KEY_REF[provider] })
1417
+ children: t("apiKeyHint", { key: KEY_REF[provider] ?? "" })
1311
1418
  })
1312
1419
  ]
1313
- }),
1420
+ }) : null,
1314
1421
  (0, react_jsx_runtime.jsxs)("label", {
1315
1422
  className: "dsh-ig-field",
1316
1423
  children: [
@@ -1340,11 +1447,11 @@ window.__ModuleLoader__.load({
1340
1447
  }),
1341
1448
  (0, react_jsx_runtime.jsx)("span", {
1342
1449
  className: "dsh-ig-hint",
1343
- children: provider === "google" ? t("endpointHintGoogle") : provider === "openai" ? t("endpointHintOpenAI") : provider === "seedream" ? t("endpointHintSeedream") : t("endpointHintDashScope")
1450
+ children: provider === "google" ? t("endpointHintGoogle") : provider === "openai" ? t("endpointHintOpenAI") : provider === "seedream" ? t("endpointHintSeedream") : provider === "dashscope" ? t("endpointHintDashScope") : t("endpointHintComfyUI")
1344
1451
  })
1345
1452
  ]
1346
1453
  }),
1347
- (0, react_jsx_runtime.jsxs)("label", {
1454
+ provider !== "comfyui" ? (0, react_jsx_runtime.jsxs)("label", {
1348
1455
  className: "dsh-ig-field",
1349
1456
  children: [(0, react_jsx_runtime.jsx)("span", {
1350
1457
  className: "dsh-ig-label",
@@ -1357,7 +1464,61 @@ window.__ModuleLoader__.load({
1357
1464
  },
1358
1465
  required: true
1359
1466
  })]
1360
- }),
1467
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
1468
+ className: "dsh-ig-field",
1469
+ children: [
1470
+ (0, react_jsx_runtime.jsx)("span", {
1471
+ className: "dsh-ig-label",
1472
+ children: t("workflow")
1473
+ }),
1474
+ (0, react_jsx_runtime.jsxs)("div", {
1475
+ className: "dsh-ig-file-row",
1476
+ children: [(0, react_jsx_runtime.jsxs)("label", {
1477
+ className: "dsh-ig-file-button",
1478
+ children: [(0, react_jsx_runtime.jsx)("input", {
1479
+ className: "dsh-ig-file-input",
1480
+ type: "file",
1481
+ accept: ".json,application/json",
1482
+ onChange: (event) => {
1483
+ importWorkflow(event);
1484
+ }
1485
+ }), workflowName.length > 0 ? t("workflowReplace") : t("workflowImport")]
1486
+ }), (0, react_jsx_runtime.jsx)("span", {
1487
+ className: "dsh-ig-file-name",
1488
+ title: workflowName,
1489
+ children: workflowName || t("workflowMissing")
1490
+ })]
1491
+ }),
1492
+ (0, react_jsx_runtime.jsx)("span", {
1493
+ className: "dsh-ig-hint",
1494
+ children: t("workflowHint")
1495
+ })
1496
+ ]
1497
+ }), (0, react_jsx_runtime.jsxs)("label", {
1498
+ className: "dsh-ig-field",
1499
+ children: [
1500
+ (0, react_jsx_runtime.jsx)("span", {
1501
+ className: "dsh-ig-label",
1502
+ children: t("timeout")
1503
+ }),
1504
+ (0, react_jsx_runtime.jsx)("input", {
1505
+ className: "dsh-ig-input",
1506
+ type: "number",
1507
+ min: "1",
1508
+ max: "3600",
1509
+ step: "1",
1510
+ value: timeoutSeconds,
1511
+ onChange: (event) => {
1512
+ setTimeoutSeconds(Number(event.target.value));
1513
+ },
1514
+ required: true
1515
+ }),
1516
+ (0, react_jsx_runtime.jsx)("span", {
1517
+ className: "dsh-ig-hint",
1518
+ children: t("timeoutHint")
1519
+ })
1520
+ ]
1521
+ })] }),
1361
1522
  (0, react_jsx_runtime.jsxs)("div", {
1362
1523
  className: "dsh-ig-field",
1363
1524
  children: [(0, react_jsx_runtime.jsxs)("label", {
@@ -1403,11 +1564,11 @@ window.__ModuleLoader__.load({
1403
1564
  children: [(0, react_jsx_runtime.jsx)("p", {
1404
1565
  className: "dsh-ig-status",
1405
1566
  role: "status",
1406
- children: message || keyStatus
1567
+ children: message || (provider === "comfyui" ? workflowStatus : keyStatus)
1407
1568
  }), (0, react_jsx_runtime.jsx)("button", {
1408
1569
  className: "dsh-ig-save",
1409
1570
  type: "submit",
1410
- disabled: saving || !snapshot.writable,
1571
+ disabled: saving || !snapshot.writable || provider === "comfyui" && workflowJson.length === 0,
1411
1572
  children: saving ? t("saving") : t("save")
1412
1573
  })]
1413
1574
  })
@@ -1656,11 +1817,11 @@ window.__ModuleLoader__.load({
1656
1817
  });
1657
1818
  }
1658
1819
  function modelOf(provider, value) {
1659
- const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : value?.dashscopeModel;
1820
+ const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : value?.comfyuiWorkflowName;
1660
1821
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
1661
1822
  }
1662
1823
  function baseURLOf(provider, value) {
1663
- const stored = provider === "google" ? value?.googleEndpoint : provider === "openai" ? value?.openaiBaseURL : provider === "seedream" ? value?.seedreamBaseURL : value?.dashscopeEndpoint;
1824
+ const stored = provider === "google" ? value?.googleEndpoint : provider === "openai" ? value?.openaiBaseURL : provider === "seedream" ? value?.seedreamBaseURL : provider === "dashscope" ? value?.dashscopeEndpoint : value?.comfyuiBaseURL;
1664
1825
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
1665
1826
  }
1666
1827
  function imageRef(block) {