dsh-image-gen 0.6.1 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -87,6 +87,8 @@ window.__ModuleLoader__.load({
87
87
  const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
88
88
  /** Browser route the workbench infinite canvas pushes live state through. */
89
89
  const CANVAS_STATE_ROUTE = "/plugins/dsh-image-gen/canvas-state";
90
+ const SUBSCRIPTION_LOGIN_ROUTE = "/plugins/dsh-image-gen/subscription-login";
91
+ const SUBSCRIPTION_STATUS_ROUTE = "/plugins/dsh-image-gen/subscription-status";
90
92
  /** Namespace persisted through DSH Settings. */
91
93
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
92
94
  /** Supported providers. */
@@ -98,8 +100,25 @@ window.__ModuleLoader__.load({
98
100
  "dashscope",
99
101
  "xai",
100
102
  "zhipu",
101
- "comfyui"
103
+ "comfyui",
104
+ "chatgpt-sub",
105
+ "grok-sub",
106
+ "google-sub"
102
107
  ];
108
+ /**
109
+ * Providers that generate through a logged-in subscription account instead
110
+ * of an API key. They use no credential reference and never join the
111
+ * cloud/BYOK sets.
112
+ */
113
+ const SUBSCRIPTION_PROVIDERS = [
114
+ "chatgpt-sub",
115
+ "grok-sub",
116
+ "google-sub"
117
+ ];
118
+ /** True when the provider generates through a logged-in subscription account. */
119
+ function isSubscriptionProvider(provider) {
120
+ return SUBSCRIPTION_PROVIDERS.includes(provider);
121
+ }
103
122
  /** Providers supported by the first browser workbench release. */
104
123
  const CLOUD_IMAGE_PROVIDERS = [
105
124
  "google",
@@ -184,6 +203,12 @@ window.__ModuleLoader__.load({
184
203
  if (!existing.includes(candidate)) return candidate;
185
204
  }
186
205
  }
206
+ /** Default models for the subscription channels; fixed by the bridge protocol. */
207
+ const DEFAULT_SUBSCRIPTION_MODELS = {
208
+ "chatgpt-sub": "gpt-image-2.5-flare",
209
+ "grok-sub": "grok-imagine-image-2.0",
210
+ "google-sub": "gemini-3-pro-image"
211
+ };
187
212
  const DEFAULT_MODELS = {
188
213
  google: DEFAULT_GOOGLE_MODEL,
189
214
  openai: DEFAULT_OPENAI_MODEL,
@@ -192,7 +217,10 @@ window.__ModuleLoader__.load({
192
217
  dashscope: DEFAULT_DASHSCOPE_MODEL,
193
218
  xai: DEFAULT_XAI_MODEL,
194
219
  zhipu: DEFAULT_ZHIPU_MODEL,
195
- comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL
220
+ comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL,
221
+ "chatgpt-sub": DEFAULT_SUBSCRIPTION_MODELS["chatgpt-sub"],
222
+ "grok-sub": DEFAULT_SUBSCRIPTION_MODELS["grok-sub"],
223
+ "google-sub": DEFAULT_SUBSCRIPTION_MODELS["google-sub"]
196
224
  };
197
225
  const DEFAULT_BASE_URLS = {
198
226
  google: DEFAULT_GOOGLE_ENDPOINT,
@@ -202,7 +230,10 @@ window.__ModuleLoader__.load({
202
230
  dashscope: DEFAULT_DASHSCOPE_ENDPOINT,
203
231
  xai: DEFAULT_XAI_BASE_URL,
204
232
  zhipu: DEFAULT_ZHIPU_BASE_URL,
205
- comfyui: DEFAULT_COMFYUI_BASE_URL
233
+ comfyui: DEFAULT_COMFYUI_BASE_URL,
234
+ "chatgpt-sub": "",
235
+ "grok-sub": "",
236
+ "google-sub": ""
206
237
  };
207
238
  //#endregion
208
239
  //#region lib/types/comfyui-workflow.js
@@ -121322,6 +121353,10 @@ img.ProseMirror-separator {
121322
121353
  keyMissing: "未配置 Key",
121323
121354
  noKeyNeeded: "无需 Key",
121324
121355
  noWorkflow: "未导入工作流",
121356
+ subNoKey: "订阅生图",
121357
+ subSignedIn: "已登录",
121358
+ subSignedOut: "未登录",
121359
+ subStatusUnknown: "状态未知",
121325
121360
  readOnly: "图像设置为只读,无法在此切换",
121326
121361
  switchFailed: "切换失败"
121327
121362
  },
@@ -121332,6 +121367,10 @@ img.ProseMirror-separator {
121332
121367
  keyMissing: "No key",
121333
121368
  noKeyNeeded: "No key needed",
121334
121369
  noWorkflow: "No workflow",
121370
+ subNoKey: "Subscription",
121371
+ subSignedIn: "Signed in",
121372
+ subSignedOut: "Signed out",
121373
+ subStatusUnknown: "Unknown",
121335
121374
  readOnly: "Image settings are read-only; switch them in the config source",
121336
121375
  switchFailed: "Switch failed"
121337
121376
  }
@@ -121345,8 +121384,25 @@ img.ProseMirror-separator {
121345
121384
  dashscope: "DashScope",
121346
121385
  xai: "Grok",
121347
121386
  zhipu: "智谱 GLM",
121348
- comfyui: "ComfyUI"
121349
- };
121387
+ comfyui: "ComfyUI",
121388
+ "chatgpt-sub": "ChatGPT 订阅",
121389
+ "grok-sub": "Grok 订阅",
121390
+ "google-sub": "Google 订阅"
121391
+ };
121392
+ /** Compute the fixed-position anchor from the pill button: open upward when
121393
+ * the chat pane leaves room above the composer, drop down otherwise. */
121394
+ function anchorOf(button) {
121395
+ const rect = button?.getBoundingClientRect();
121396
+ if (rect === void 0) return void 0;
121397
+ if (rect.top >= 332) return {
121398
+ left: rect.left,
121399
+ bottom: window.innerHeight - rect.top + 6
121400
+ };
121401
+ return {
121402
+ left: rect.left,
121403
+ top: rect.bottom + 6
121404
+ };
121405
+ }
121350
121406
  /** Whether the user opted into the composer pill; hidden (not merely inert) when off. */
121351
121407
  function pillVisible(value) {
121352
121408
  return value?.showProviderPill === true;
@@ -121357,6 +121413,7 @@ img.ProseMirror-separator {
121357
121413
  const workflow = activeComfyUIWorkflow(value ?? {});
121358
121414
  return workflow === void 0 ? "" : workflow.name;
121359
121415
  }
121416
+ if (SUBSCRIPTION_PROVIDERS.includes(provider)) return DEFAULT_MODELS[provider];
121360
121417
  const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "openai-compat" ? value?.openaiCompatModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : provider === "xai" ? value?.xaiModel : value?.zhipuModel;
121361
121418
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
121362
121419
  }
@@ -121377,6 +121434,11 @@ img.ProseMirror-separator {
121377
121434
  zhipu: "unknown"
121378
121435
  });
121379
121436
  const [keyTick, setKeyTick] = (0, react.useState)(0);
121437
+ const [subDots, setSubDots] = (0, react.useState)(() => ({
121438
+ "chatgpt-sub": "unknown",
121439
+ "grok-sub": "unknown",
121440
+ "google-sub": "unknown"
121441
+ }));
121380
121442
  const buttonRef = (0, react.useRef)(null);
121381
121443
  const [menuStyle, setMenuStyle] = (0, react.useState)(null);
121382
121444
  (0, react.useEffect)(() => props.scope.subscribe(() => {
@@ -121419,6 +121481,31 @@ img.ProseMirror-separator {
121419
121481
  active = false;
121420
121482
  };
121421
121483
  }, [props.credentials, keyTick]);
121484
+ (0, react.useEffect)(() => {
121485
+ if (!open) return;
121486
+ let active = true;
121487
+ const probe = async () => {
121488
+ try {
121489
+ const response = await fetch(SUBSCRIPTION_STATUS_ROUTE, {
121490
+ method: "POST",
121491
+ cache: "no-store"
121492
+ });
121493
+ if (!active || !response.ok) return;
121494
+ const payload = await response.json();
121495
+ if (!active || payload.statuses === void 0) return;
121496
+ const next = {};
121497
+ for (const provider of SUBSCRIPTION_PROVIDERS) {
121498
+ const row = payload.statuses[provider];
121499
+ next[provider] = row?.state === "logged-in" ? "logged-in" : row?.state === "logged-out" ? "logged-out" : "unknown";
121500
+ }
121501
+ setSubDots(next);
121502
+ } catch {}
121503
+ };
121504
+ probe();
121505
+ return () => {
121506
+ active = false;
121507
+ };
121508
+ }, [open, keyTick]);
121422
121509
  const t = (keyName) => (lang === "en" ? PILL_DICT.en : PILL_DICT.zh)[keyName];
121423
121510
  const current = snapshot.value?.provider ?? "google";
121424
121511
  const writable = snapshot.writable;
@@ -121427,11 +121514,15 @@ img.ProseMirror-separator {
121427
121514
  setMenuStyle(null);
121428
121515
  return;
121429
121516
  }
121430
- const rect = buttonRef.current?.getBoundingClientRect();
121431
- if (rect !== void 0) setMenuStyle({
121432
- left: rect.left,
121433
- bottom: window.innerHeight - rect.top + 6
121434
- });
121517
+ const anchor = () => {
121518
+ const next = anchorOf(buttonRef.current);
121519
+ if (next !== void 0) setMenuStyle(next);
121520
+ };
121521
+ anchor();
121522
+ window.addEventListener("resize", anchor);
121523
+ return () => {
121524
+ window.removeEventListener("resize", anchor);
121525
+ };
121435
121526
  }, [open]);
121436
121527
  (0, react.useEffect)(() => {
121437
121528
  if (!open) return;
@@ -121442,8 +121533,15 @@ img.ProseMirror-separator {
121442
121533
  const onKeyDown = (event) => {
121443
121534
  if (event.key === "Escape") setOpen(false);
121444
121535
  };
121445
- const onReflow = () => {
121446
- setOpen(false);
121536
+ const onReflow = (event) => {
121537
+ if (event.target instanceof Element && event.target.closest(".dsh-ig-pill-menu") !== null) return;
121538
+ const rect = buttonRef.current?.getBoundingClientRect();
121539
+ if (rect !== void 0 && (rect.bottom < 0 || rect.top > window.innerHeight)) {
121540
+ setOpen(false);
121541
+ return;
121542
+ }
121543
+ const next = anchorOf(buttonRef.current);
121544
+ if (next !== void 0) setMenuStyle(next);
121447
121545
  };
121448
121546
  document.addEventListener("mousedown", onPointerDown, true);
121449
121547
  document.addEventListener("keydown", onKeyDown, true);
@@ -121473,8 +121571,12 @@ img.ProseMirror-separator {
121473
121571
  setPending(void 0);
121474
121572
  });
121475
121573
  };
121574
+ /** Plan A dots: ComfyUI is always fixed blue (theme-independent); each
121575
+ * subscription reflects its login state (green in, gray out); cloud rows
121576
+ * reflect their API-key state as before. */
121476
121577
  const dotClassOf = (provider) => {
121477
121578
  if (provider === "comfyui") return "dsh-ig-pill-dot-neutral";
121579
+ if (isSubscriptionProvider(provider)) return subDots[provider] === "logged-in" ? "dsh-ig-pill-dot-ok" : "dsh-ig-pill-dot-missing";
121478
121580
  const dot = dots[provider];
121479
121581
  return dot === "configured" ? "dsh-ig-pill-dot-ok" : dot === "missing" ? "dsh-ig-pill-dot-missing" : "dsh-ig-pill-dot-neutral";
121480
121582
  };
@@ -121483,6 +121585,11 @@ img.ProseMirror-separator {
121483
121585
  const workflow = pillModelOf(provider, snapshot.value);
121484
121586
  return workflow.length > 0 ? `${t("noKeyNeeded")} · ${workflow}` : `${t("noKeyNeeded")} · ${t("noWorkflow")}`;
121485
121587
  }
121588
+ if (isSubscriptionProvider(provider)) {
121589
+ const state = subDots[provider];
121590
+ const stateText = state === "logged-in" ? t("subSignedIn") : state === "logged-out" ? t("subSignedOut") : t("subStatusUnknown");
121591
+ return `${t("subNoKey")} · ${stateText} · ${pillModelOf(provider, snapshot.value)}`;
121592
+ }
121486
121593
  const dot = dots[provider];
121487
121594
  const model = pillModelOf(provider, snapshot.value);
121488
121595
  if (dot === "configured") return `${t("keyConfigured")} · ${model}`;
@@ -121635,8 +121742,8 @@ img.ProseMirror-separator {
121635
121742
  .dsh-ig-pill-option-active{background:var(--dsw-alias-brand-primary-soft,rgba(76,120,255,.10))}
121636
121743
  .dsh-ig-pill-dot{flex:none;width:7px;height:7px;border-radius:50%}
121637
121744
  .dsh-ig-pill-dot-ok{background:#22c55e}
121638
- .dsh-ig-pill-dot-missing{background:var(--dsw-alias-border-l2,#d1d5db)}
121639
- .dsh-ig-pill-dot-neutral{background:var(--dsw-alias-brand-primary,#4c78ff)}
121745
+ .dsh-ig-pill-dot-missing{background:#9ca3af}
121746
+ .dsh-ig-pill-dot-neutral{background:#4c78ff}
121640
121747
  .dsh-ig-pill-option-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
121641
121748
  .dsh-ig-pill-option-name{font-size:12.5px;font-weight:500;color:var(--dsw-alias-label-primary,#111827);line-height:1.2}
121642
121749
  .dsh-ig-pill-option-sub{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -121645,7 +121752,7 @@ img.ProseMirror-separator {
121645
121752
  `;
121646
121753
  //#endregion
121647
121754
  //#region lib/types/client/index.js
121648
- console.info(`[dsh-image-gen] canvas bundle 2026-09-12T16:41:18.261Z`);
121755
+ console.info(`[dsh-image-gen] canvas bundle 2026-09-14T00:33:35.649Z`);
121649
121756
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
121650
121757
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
121651
121758
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -121665,6 +121772,27 @@ img.ProseMirror-separator {
121665
121772
  providerXAI: "xAI Grok Imagine",
121666
121773
  providerZhipu: "智谱 GLM-Image",
121667
121774
  providerComfyUI: "本地 ComfyUI",
121775
+ providerChatGPTSub: "ChatGPT 订阅",
121776
+ providerGrokSub: "Grok 订阅",
121777
+ providerGoogleSub: "Google 订阅",
121778
+ subBadgeLoggedIn: "已登录",
121779
+ subBadgeLoggedOut: "未登录",
121780
+ subBadgeUnknown: "状态未知",
121781
+ subSectionTitle: "订阅生图",
121782
+ subHint: "通过已登录的订阅账号生图,无需 API Key。登录在你的授权下进行,不会修改任何 API Key 或默认 Provider。",
121783
+ subAccountLabel: "账号",
121784
+ subAccountEmail: "已登录:{email}",
121785
+ subAccountNone: "未登录",
121786
+ subLogin: "登录",
121787
+ subLoggingIn: "正在登录…",
121788
+ subLogout: "退出登录",
121789
+ subLoggingOut: "正在退出…",
121790
+ subWaitingLogin: "已打开授权页面,请在浏览器中完成登录…",
121791
+ subLoginOk: "登录成功",
121792
+ subModelLabel: "订阅模型",
121793
+ subModelHint: "由订阅通道固定,不可更改。",
121794
+ subEditUnsupported: "订阅通道暂不支持图生图(编辑图片),仅支持文字生图。",
121795
+ subDefaultHint: "订阅 Provider 只能生成,不能编辑图片;需要编辑时请切换到 API Key Provider。",
121668
121796
  apiKeyLabel: "{provider} API Key",
121669
121797
  apiKeyPlaceholder: "留空即可保留已配置的 Key",
121670
121798
  apiKeyHint: "安全保存为 {key};页面不会读回明文。",
@@ -121692,6 +121820,14 @@ img.ProseMirror-separator {
121692
121820
  endpointHintGoogle: "Google 官方地址或反代端点(全路径)。",
121693
121821
  endpointHintOpenAI: "官方 api.openai.com 的 /v1 地址;中转站请使用下方「OpenAI 兼容」行。",
121694
121822
  endpointHintOpenAICompat: "中转站/自建服务的 OpenAI 兼容 /v1 地址(必填),例如 https://your-relay.example.com/v1。",
121823
+ editFormat: "图生图请求形态",
121824
+ editFormatMultipart: "标准 multipart(OpenAI 官方)",
121825
+ editFormatJsonImageUrlArray: "JSON images 数组(商汤等中转)",
121826
+ editFormatHint: "文生图不受影响。仅当中转站的图生图接口使用自有 JSON 契约(如商汤 SenseNova)时才需要切换。",
121827
+ editExtra: "附加 JSON 字段",
121828
+ editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
121829
+ editExtraHint: "仅 JSON 形态生效;会合并到请求体末尾,可覆盖默认字段。留空表示不附加。",
121830
+ editExtraInvalid: "附加 JSON 字段必须是合法的 JSON 对象。",
121695
121831
  endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
121696
121832
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
121697
121833
  endpointHintXAI: "xAI 官方 api.x.ai 的 /v1 地址。",
@@ -121765,6 +121901,27 @@ img.ProseMirror-separator {
121765
121901
  providerXAI: "xAI Grok Imagine",
121766
121902
  providerZhipu: "Zhipu GLM-Image",
121767
121903
  providerComfyUI: "Local ComfyUI",
121904
+ providerChatGPTSub: "ChatGPT Subscription",
121905
+ providerGrokSub: "Grok Subscription",
121906
+ providerGoogleSub: "Google Subscription",
121907
+ subBadgeLoggedIn: "Signed in",
121908
+ subBadgeLoggedOut: "Signed out",
121909
+ subBadgeUnknown: "Unknown",
121910
+ subSectionTitle: "Subscription generation",
121911
+ subHint: "Generates through a logged-in subscription account; no API key needed. Signing in never changes any API key or the default provider.",
121912
+ subAccountLabel: "Account",
121913
+ subAccountEmail: "Signed in: {email}",
121914
+ subAccountNone: "Not signed in",
121915
+ subLogin: "Sign in",
121916
+ subLoggingIn: "Signing in…",
121917
+ subLogout: "Sign out",
121918
+ subLoggingOut: "Signing out…",
121919
+ subWaitingLogin: "Authorization page opened; complete sign-in in your browser…",
121920
+ subLoginOk: "Signed in",
121921
+ subModelLabel: "Subscription model",
121922
+ subModelHint: "Fixed by the subscription channel; not changeable.",
121923
+ subEditUnsupported: "The subscription channel does not support image editing yet; text-to-image only.",
121924
+ subDefaultHint: "Subscription providers can only generate; switch to an API-key provider for editing.",
121768
121925
  apiKeyLabel: "{provider} API Key",
121769
121926
  apiKeyPlaceholder: "Leave empty to keep configured key",
121770
121927
  apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
@@ -121792,6 +121949,14 @@ img.ProseMirror-separator {
121792
121949
  endpointHintGoogle: "Official Google endpoint or reverse proxy (full path).",
121793
121950
  endpointHintOpenAI: "Official api.openai.com /v1 base URL; for relays use the \"OpenAI-compatible\" row below.",
121794
121951
  endpointHintOpenAICompat: "OpenAI-compatible /v1 base URL of your relay or self-hosted service (required), e.g. https://your-relay.example.com/v1.",
121952
+ editFormat: "Edit request format",
121953
+ editFormatMultipart: "Standard multipart (official OpenAI)",
121954
+ editFormatJsonImageUrlArray: "JSON images array (SenseNova etc.)",
121955
+ editFormatHint: "Text-to-image is unaffected. Only switch when the relay runs image edits on its own JSON contract (e.g. SenseNova); size is pinned to \"auto\" in this format (the only value SenseNova's edits endpoint accepts) and can be overridden via extra fields.",
121956
+ editExtra: "Extra JSON fields",
121957
+ editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
121958
+ editExtraHint: "Only used with the JSON format; merged into the request body last and may override defaults. Leave empty for none.",
121959
+ editExtraInvalid: "Extra JSON fields must be a valid JSON object.",
121795
121960
  endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
121796
121961
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
121797
121962
  endpointHintXAI: "Official xAI api.x.ai /v1 base URL.",
@@ -121868,6 +122033,7 @@ img.ProseMirror-separator {
121868
122033
  .dsh-ig-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}
121869
122034
  .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}
121870
122035
  .dsh-ig-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff)}
122036
+ .dsh-ig-textarea{resize:vertical;min-height:56px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.5}
121871
122037
  .dsh-ig-input-group{display:flex;gap:8px;align-items:center}
121872
122038
  .dsh-ig-file-row{display:flex;align-items:center;gap:10px;min-width:0}
121873
122039
  .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%)}
@@ -122363,7 +122529,9 @@ img.ProseMirror-separator {
122363
122529
  messageIsError: false,
122364
122530
  workflows: [],
122365
122531
  activeWorkflow: "",
122366
- timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3
122532
+ timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3,
122533
+ editFormat: "multipart",
122534
+ editExtraText: ""
122367
122535
  };
122368
122536
  }
122369
122537
  /** Settings field each cloud provider persists its model under. */
@@ -122408,6 +122576,15 @@ img.ProseMirror-separator {
122408
122576
  function modelPullSupported(_provider) {
122409
122577
  return true;
122410
122578
  }
122579
+ /** Serialize the extra-fields record into editor text; empty record becomes ''. */
122580
+ function editExtraTextOf(value) {
122581
+ if (value === void 0 || Object.keys(value).length === 0) return "";
122582
+ try {
122583
+ return JSON.stringify(value);
122584
+ } catch {
122585
+ return "";
122586
+ }
122587
+ }
122411
122588
  /** Build one row per provider from persisted settings, including ComfyUI extras. */
122412
122589
  function rowsFromSettings(value) {
122413
122590
  const rows = {};
@@ -122419,6 +122596,10 @@ img.ProseMirror-separator {
122419
122596
  workflows: resolveComfyUIWorkflows(value ?? {}),
122420
122597
  activeWorkflow: activeComfyUIWorkflow(value ?? {})?.name ?? "",
122421
122598
  timeoutSeconds: Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3))
122599
+ } : {},
122600
+ ...provider === "openai-compat" ? {
122601
+ editFormat: value?.openaiCompatEditFormat === "jsonImageUrlArray" ? "jsonImageUrlArray" : "multipart",
122602
+ editExtraText: editExtraTextOf(value?.openaiCompatEditExtra)
122422
122603
  } : {}
122423
122604
  };
122424
122605
  return rows;
@@ -122440,6 +122621,14 @@ img.ProseMirror-separator {
122440
122621
  const [workspaceMessageIsError, setWorkspaceMessageIsError] = (0, react.useState)(false);
122441
122622
  const [rows, setRows] = (0, react.useState)(() => rowsFromSettings(props.scope.getSnapshot().value));
122442
122623
  const [keyTick, setKeyTick] = (0, react.useState)(0);
122624
+ const [subStatus, setSubStatus] = (0, react.useState)(() => ({
122625
+ "chatgpt-sub": { state: "unknown" },
122626
+ "grok-sub": { state: "unknown" },
122627
+ "google-sub": { state: "unknown" }
122628
+ }));
122629
+ const [subTick, setSubTick] = (0, react.useState)(0);
122630
+ const [subBusy, setSubBusy] = (0, react.useState)({});
122631
+ const [subPending, setSubPending] = (0, react.useState)([]);
122443
122632
  (0, react.useEffect)(() => props.scope.subscribe(() => {
122444
122633
  setSnapshot(props.scope.getSnapshot());
122445
122634
  }), [props.scope]);
@@ -122452,6 +122641,143 @@ img.ProseMirror-separator {
122452
122641
  (0, react.useEffect)(() => credentialEvents?.listen(() => {
122453
122642
  setKeyTick((tick) => tick + 1);
122454
122643
  }), [credentialEvents]);
122644
+ const pending = subPending;
122645
+ (0, react.useEffect)(() => {
122646
+ if (!open) return void 0;
122647
+ let active = true;
122648
+ const probe = async () => {
122649
+ try {
122650
+ const response = await fetch(SUBSCRIPTION_STATUS_ROUTE, {
122651
+ method: "POST",
122652
+ cache: "no-store"
122653
+ });
122654
+ if (!response.ok) return;
122655
+ const payload = await response.json();
122656
+ if (!active || payload.statuses === void 0) return;
122657
+ const next = {
122658
+ "chatgpt-sub": { state: "unknown" },
122659
+ "grok-sub": { state: "unknown" },
122660
+ "google-sub": { state: "unknown" }
122661
+ };
122662
+ for (const provider of SUBSCRIPTION_PROVIDERS) {
122663
+ const row = payload.statuses[provider];
122664
+ if (row?.state === "logged-in") next[provider] = {
122665
+ state: "logged-in",
122666
+ ...typeof row.email === "string" ? { email: row.email } : {}
122667
+ };
122668
+ else if (row?.state === "logged-out") next[provider] = { state: "logged-out" };
122669
+ }
122670
+ setSubStatus(next);
122671
+ const landed = pending.filter((entry) => next[entry.provider].state === "logged-in");
122672
+ if (landed.length > 0) {
122673
+ setSubPending((current) => current.filter((entry) => next[entry.provider].state !== "logged-in"));
122674
+ for (const entry of landed) updateRow(entry.provider, {
122675
+ message: t("subLoginOk"),
122676
+ messageIsError: false
122677
+ });
122678
+ }
122679
+ const WINDOW_MS = 63e4;
122680
+ if (pending.filter((entry) => Date.now() - entry.startedAt > WINDOW_MS).length > 0) setSubPending((current) => current.filter((entry) => Date.now() - entry.startedAt <= WINDOW_MS));
122681
+ } catch {}
122682
+ };
122683
+ probe();
122684
+ let timer;
122685
+ if (pending.length > 0) timer = setInterval(() => {
122686
+ probe();
122687
+ }, 2e3);
122688
+ const onFocus = () => {
122689
+ probe();
122690
+ };
122691
+ window.addEventListener("focus", onFocus);
122692
+ return () => {
122693
+ active = false;
122694
+ if (timer !== void 0) clearInterval(timer);
122695
+ window.removeEventListener("focus", onFocus);
122696
+ };
122697
+ }, [
122698
+ open,
122699
+ subTick,
122700
+ pending
122701
+ ]);
122702
+ /** Start a subscription login: the host registers PKCE and the loopback
122703
+ * catch server, then the browser opens the vendor's authorize URL. The
122704
+ * provider joins the pending set so the badge polls until login lands. */
122705
+ const subscriptionLogin = async (provider) => {
122706
+ updateRow(provider, {
122707
+ message: "",
122708
+ messageIsError: false
122709
+ });
122710
+ setSubBusy((current) => ({
122711
+ ...current,
122712
+ [provider]: "login"
122713
+ }));
122714
+ try {
122715
+ const response = await fetch(SUBSCRIPTION_LOGIN_ROUTE, {
122716
+ method: "POST",
122717
+ headers: { "content-type": "application/json" },
122718
+ body: JSON.stringify({ provider })
122719
+ });
122720
+ const payload = await response.json().catch(() => ({}));
122721
+ if (!response.ok || payload.ok !== true || typeof payload.url !== "string") {
122722
+ const detail = typeof payload.error === "string" ? payload.error : payload.error?.message;
122723
+ throw new Error(detail ?? `HTTP ${String(response.status)}`);
122724
+ }
122725
+ setSubPending((current) => [...current.filter((entry) => entry.provider !== provider), {
122726
+ provider,
122727
+ startedAt: Date.now()
122728
+ }]);
122729
+ window.open(payload.url, "_blank", "noopener");
122730
+ updateRow(provider, {
122731
+ message: t("subWaitingLogin"),
122732
+ messageIsError: false
122733
+ });
122734
+ } catch (cause) {
122735
+ updateRow(provider, {
122736
+ message: cause instanceof Error ? cause.message : String(cause),
122737
+ messageIsError: true
122738
+ });
122739
+ } finally {
122740
+ setSubBusy((current) => {
122741
+ const next = { ...current };
122742
+ delete next[provider];
122743
+ return next;
122744
+ });
122745
+ }
122746
+ };
122747
+ /** Sign out one subscription account; nothing else changes. */
122748
+ const subscriptionLogout = async (provider) => {
122749
+ setSubBusy((current) => ({
122750
+ ...current,
122751
+ [provider]: "logout"
122752
+ }));
122753
+ try {
122754
+ const response = await fetch(SUBSCRIPTION_LOGIN_ROUTE, {
122755
+ method: "POST",
122756
+ headers: { "content-type": "application/json" },
122757
+ body: JSON.stringify({
122758
+ provider,
122759
+ action: "logout"
122760
+ })
122761
+ });
122762
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
122763
+ updateRow(provider, {
122764
+ message: "",
122765
+ messageIsError: false
122766
+ });
122767
+ } catch (cause) {
122768
+ updateRow(provider, {
122769
+ message: cause instanceof Error ? cause.message : String(cause),
122770
+ messageIsError: true
122771
+ });
122772
+ } finally {
122773
+ setSubBusy((current) => {
122774
+ const next = { ...current };
122775
+ delete next[provider];
122776
+ return next;
122777
+ });
122778
+ setSubTick((tick) => tick + 1);
122779
+ }
122780
+ };
122455
122781
  const t = (keyName, params) => {
122456
122782
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
122457
122783
  if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
@@ -122465,7 +122791,10 @@ img.ProseMirror-separator {
122465
122791
  dashscope: t("providerDashScope"),
122466
122792
  xai: t("providerXAI"),
122467
122793
  zhipu: t("providerZhipu"),
122468
- comfyui: t("providerComfyUI")
122794
+ comfyui: t("providerComfyUI"),
122795
+ "chatgpt-sub": t("providerChatGPTSub"),
122796
+ "grok-sub": t("providerGrokSub"),
122797
+ "google-sub": t("providerGoogleSub")
122469
122798
  };
122470
122799
  (0, react.useEffect)(() => {
122471
122800
  const value = snapshot.value;
@@ -122592,9 +122921,28 @@ img.ProseMirror-separator {
122592
122921
  await props.scope.set("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
122593
122922
  await props.scope.set("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
122594
122923
  await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(row.timeoutSeconds)) * 1e3);
122595
- } else {
122924
+ } else if (isSubscriptionProvider(provider)) await Promise.resolve();
122925
+ else {
122926
+ let editExtra = {};
122927
+ if (provider === "openai-compat") {
122928
+ const extraText = row.editExtraText.trim();
122929
+ if (extraText.length > 0) {
122930
+ let parsed;
122931
+ try {
122932
+ parsed = JSON.parse(extraText);
122933
+ } catch {
122934
+ throw new Error(t("editExtraInvalid"));
122935
+ }
122936
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(t("editExtraInvalid"));
122937
+ editExtra = parsed;
122938
+ }
122939
+ }
122596
122940
  await props.scope.set(modelFieldOf(provider), row.model);
122597
122941
  await props.scope.set(baseURLFieldOf(provider), row.baseURL);
122942
+ if (provider === "openai-compat") {
122943
+ await props.scope.set("openaiCompatEditFormat", row.editFormat);
122944
+ await props.scope.set("openaiCompatEditExtra", editExtra);
122945
+ }
122598
122946
  if (row.keyInput.trim().length > 0) {
122599
122947
  const keyRef = cloudCredentialRef(provider);
122600
122948
  if (keyRef === void 0) throw new Error(t("comfyuiNoKey"));
@@ -122738,6 +123086,21 @@ img.ProseMirror-separator {
122738
123086
  text: t("comfyuiNoKey"),
122739
123087
  className: "dsh-ig-badge dsh-ig-badge-neutral"
122740
123088
  };
123089
+ if (isSubscriptionProvider(provider)) {
123090
+ const status = subStatus[provider].state;
123091
+ if (status === "logged-in") return {
123092
+ text: t("subBadgeLoggedIn"),
123093
+ className: "dsh-ig-badge dsh-ig-badge-ok"
123094
+ };
123095
+ if (status === "logged-out") return {
123096
+ text: t("subBadgeLoggedOut"),
123097
+ className: "dsh-ig-badge dsh-ig-badge-missing"
123098
+ };
123099
+ return {
123100
+ text: t("subBadgeUnknown"),
123101
+ className: "dsh-ig-badge dsh-ig-badge-neutral"
123102
+ };
123103
+ }
122741
123104
  const status = rows[provider].keyStatus;
122742
123105
  if (status === "checking") return {
122743
123106
  text: t("badgeChecking"),
@@ -123007,6 +123370,57 @@ img.ProseMirror-separator {
123007
123370
  }) : null
123008
123371
  ]
123009
123372
  }),
123373
+ provider === "openai-compat" ? (0, react_jsx_runtime.jsxs)("label", {
123374
+ className: "dsh-ig-field",
123375
+ children: [
123376
+ (0, react_jsx_runtime.jsx)("span", {
123377
+ className: "dsh-ig-label",
123378
+ children: t("editFormat")
123379
+ }),
123380
+ (0, react_jsx_runtime.jsxs)("select", {
123381
+ className: "dsh-ig-input",
123382
+ value: row.editFormat,
123383
+ onChange: (event) => {
123384
+ updateRow(provider, { editFormat: event.target.value === "jsonImageUrlArray" ? "jsonImageUrlArray" : "multipart" });
123385
+ },
123386
+ disabled: !snapshot.writable,
123387
+ children: [(0, react_jsx_runtime.jsx)("option", {
123388
+ value: "multipart",
123389
+ children: t("editFormatMultipart")
123390
+ }), (0, react_jsx_runtime.jsx)("option", {
123391
+ value: "jsonImageUrlArray",
123392
+ children: t("editFormatJsonImageUrlArray")
123393
+ })]
123394
+ }),
123395
+ (0, react_jsx_runtime.jsx)("span", {
123396
+ className: "dsh-ig-hint",
123397
+ children: t("editFormatHint")
123398
+ })
123399
+ ]
123400
+ }) : null,
123401
+ provider === "openai-compat" && row.editFormat === "jsonImageUrlArray" ? (0, react_jsx_runtime.jsxs)("label", {
123402
+ className: "dsh-ig-field",
123403
+ children: [
123404
+ (0, react_jsx_runtime.jsx)("span", {
123405
+ className: "dsh-ig-label",
123406
+ children: t("editExtra")
123407
+ }),
123408
+ (0, react_jsx_runtime.jsx)("textarea", {
123409
+ className: "dsh-ig-input dsh-ig-textarea",
123410
+ value: row.editExtraText,
123411
+ onChange: (event) => {
123412
+ updateRow(provider, { editExtraText: event.target.value });
123413
+ },
123414
+ placeholder: t("editExtraPlaceholder"),
123415
+ rows: 2,
123416
+ disabled: !snapshot.writable
123417
+ }),
123418
+ (0, react_jsx_runtime.jsx)("span", {
123419
+ className: "dsh-ig-hint",
123420
+ children: t("editExtraHint")
123421
+ })
123422
+ ]
123423
+ }) : null,
123010
123424
  (0, react_jsx_runtime.jsxs)("div", {
123011
123425
  className: "dsh-ig-row-actions",
123012
123426
  children: [(0, react_jsx_runtime.jsx)("p", {
@@ -123224,6 +123638,119 @@ img.ProseMirror-separator {
123224
123638
  })
123225
123639
  });
123226
123640
  };
123641
+ /** Subscription rows: account login/logout, fixed model, edit limitation. */
123642
+ const renderSubscriptionBody = (provider) => {
123643
+ const row = rows[provider];
123644
+ const status = subStatus[provider];
123645
+ const busy = subBusy[provider];
123646
+ return (0, react_jsx_runtime.jsx)("div", {
123647
+ className: "dsh-ig-provider-body",
123648
+ children: (0, react_jsx_runtime.jsxs)("form", {
123649
+ onSubmit: (event) => {
123650
+ event.preventDefault();
123651
+ saveProviderRow(provider);
123652
+ },
123653
+ children: [
123654
+ (0, react_jsx_runtime.jsxs)("div", {
123655
+ className: "dsh-ig-field",
123656
+ children: [(0, react_jsx_runtime.jsx)("span", {
123657
+ className: "dsh-ig-label",
123658
+ children: t("subSectionTitle")
123659
+ }), (0, react_jsx_runtime.jsx)("p", {
123660
+ className: "dsh-ig-hint",
123661
+ children: t("subHint")
123662
+ })]
123663
+ }),
123664
+ (0, react_jsx_runtime.jsxs)("div", {
123665
+ className: "dsh-ig-field",
123666
+ children: [
123667
+ (0, react_jsx_runtime.jsx)("span", {
123668
+ className: "dsh-ig-label",
123669
+ children: t("subAccountLabel")
123670
+ }),
123671
+ (0, react_jsx_runtime.jsx)("p", {
123672
+ className: "dsh-ig-hint",
123673
+ children: status.state === "logged-in" ? t("subAccountEmail", { email: status.email ?? "" }) : status.state === "logged-out" ? t("subAccountNone") : t("subBadgeUnknown")
123674
+ }),
123675
+ (0, react_jsx_runtime.jsx)("span", {
123676
+ className: "dsh-ig-row-buttons",
123677
+ children: status.state === "logged-in" ? (0, react_jsx_runtime.jsx)("button", {
123678
+ type: "button",
123679
+ className: "dsh-ig-btn-secondary",
123680
+ disabled: busy !== void 0,
123681
+ onClick: () => {
123682
+ subscriptionLogout(provider);
123683
+ },
123684
+ children: busy === "logout" ? t("subLoggingOut") : t("subLogout")
123685
+ }) : (0, react_jsx_runtime.jsx)("button", {
123686
+ type: "button",
123687
+ className: "dsh-ig-save",
123688
+ disabled: busy !== void 0,
123689
+ onClick: () => {
123690
+ subscriptionLogin(provider);
123691
+ setSubTick((tick) => tick + 1);
123692
+ },
123693
+ children: busy === "login" ? t("subLoggingIn") : t("subLogin")
123694
+ })
123695
+ })
123696
+ ]
123697
+ }),
123698
+ (0, react_jsx_runtime.jsxs)("label", {
123699
+ className: "dsh-ig-field",
123700
+ children: [
123701
+ (0, react_jsx_runtime.jsx)("span", {
123702
+ className: "dsh-ig-label",
123703
+ children: t("subModelLabel")
123704
+ }),
123705
+ (0, react_jsx_runtime.jsx)("input", {
123706
+ className: "dsh-ig-input",
123707
+ type: "text",
123708
+ value: row.model,
123709
+ readOnly: true,
123710
+ disabled: true
123711
+ }),
123712
+ (0, react_jsx_runtime.jsx)("span", {
123713
+ className: "dsh-ig-hint",
123714
+ children: t("subModelHint")
123715
+ })
123716
+ ]
123717
+ }),
123718
+ (0, react_jsx_runtime.jsx)("div", {
123719
+ className: "dsh-ig-field",
123720
+ children: (0, react_jsx_runtime.jsx)("span", {
123721
+ className: "dsh-ig-label",
123722
+ children: t("subEditUnsupported")
123723
+ })
123724
+ }),
123725
+ (0, react_jsx_runtime.jsxs)("div", {
123726
+ className: "dsh-ig-row-actions",
123727
+ children: [(0, react_jsx_runtime.jsx)("p", {
123728
+ className: `dsh-ig-status${row.messageIsError ? " dsh-ig-status-error" : ""}`,
123729
+ role: "status",
123730
+ children: row.message || testResultText(row.testResult)
123731
+ }), (0, react_jsx_runtime.jsxs)("span", {
123732
+ className: "dsh-ig-row-buttons",
123733
+ children: [(0, react_jsx_runtime.jsx)("button", {
123734
+ type: "button",
123735
+ className: "dsh-ig-btn-secondary",
123736
+ disabled: row.testing,
123737
+ onClick: () => {
123738
+ testConnection(provider);
123739
+ setSubTick((tick) => tick + 1);
123740
+ },
123741
+ children: row.testing ? t("testing") : t("testConnection")
123742
+ }), (0, react_jsx_runtime.jsx)("button", {
123743
+ className: "dsh-ig-save",
123744
+ type: "submit",
123745
+ disabled: row.saving || !snapshot.writable,
123746
+ children: row.saving ? t("saving") : t("save")
123747
+ })]
123748
+ })]
123749
+ })
123750
+ ]
123751
+ })
123752
+ });
123753
+ };
123227
123754
  const renderProviderRow = (provider) => {
123228
123755
  const row = rows[provider];
123229
123756
  const badge = badgeOf(provider);
@@ -123265,7 +123792,7 @@ img.ProseMirror-separator {
123265
123792
  })
123266
123793
  })
123267
123794
  ]
123268
- }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : renderCloudBody(provider) : null]
123795
+ }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : isSubscriptionProvider(provider) ? renderSubscriptionBody(provider) : renderCloudBody(provider) : null]
123269
123796
  }, provider);
123270
123797
  };
123271
123798
  return (0, react_jsx_runtime.jsxs)("li", {
@@ -124004,11 +124531,11 @@ img.ProseMirror-separator {
124004
124531
  };
124005
124532
  }
124006
124533
  function modelOf(provider, value) {
124007
- const stored = provider === "comfyui" ? activeComfyUIWorkflow(value ?? {})?.name : value?.[CLOUD_MODEL_FIELDS[provider]];
124534
+ const stored = provider === "comfyui" ? activeComfyUIWorkflow(value ?? {})?.name : isSubscriptionProvider(provider) ? DEFAULT_MODELS[provider] : value?.[CLOUD_MODEL_FIELDS[provider]];
124008
124535
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
124009
124536
  }
124010
124537
  function baseURLOf(provider, value) {
124011
- const stored = provider === "comfyui" ? value?.comfyuiBaseURL : value?.[CLOUD_URL_FIELDS[provider]];
124538
+ const stored = provider === "comfyui" ? value?.comfyuiBaseURL : isSubscriptionProvider(provider) ? "" : value?.[CLOUD_URL_FIELDS[provider]];
124012
124539
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
124013
124540
  }
124014
124541
  //#endregion