dsh-image-gen 0.2.2 → 0.2.4

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
@@ -52,7 +52,7 @@ window.__ModuleLoader__.load({
52
52
  }
53
53
  if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
54
54
  if (!Object.values(value).some((node) => {
55
- const inputs = record(node)?.inputs;
55
+ const inputs = record$1(node)?.inputs;
56
56
  return inputs !== void 0 && containsPromptPlaceholder(inputs);
57
57
  })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
58
58
  return value;
@@ -65,7 +65,7 @@ window.__ModuleLoader__.load({
65
65
  function isRecord(value) {
66
66
  return typeof value === "object" && value !== null && !Array.isArray(value);
67
67
  }
68
- function record(value) {
68
+ function record$1(value) {
69
69
  return isRecord(value) ? value : void 0;
70
70
  }
71
71
  //#endregion
@@ -928,8 +928,146 @@ window.__ModuleLoader__.load({
928
928
  }
929
929
  }
930
930
  //#endregion
931
+ //#region lib/types/client/image-ref.js
932
+ /**
933
+ * Extract the verified durable image attachment from a completed tool result block.
934
+ * Supports legacy rc.2 `resultView.content` and modern DSH `block.content`.
935
+ */
936
+ function imageRef(block) {
937
+ if (!("kind" in block)) return void 0;
938
+ const fromResultView = block.resultView?.card === "generic" ? block.resultView.content?.find((item) => item.type === "image") : void 0;
939
+ if (fromResultView?.type === "image") return fromResultView.attachment;
940
+ const fromBlockContent = Array.isArray(block.content) ? block.content.find((item) => item.type === "image") : void 0;
941
+ if (fromBlockContent?.type === "image") return fromBlockContent.attachment;
942
+ }
943
+ //#endregion
944
+ //#region lib/types/client/image-result-node.js
945
+ const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
946
+ /** One image result row per Turn, re-anchored to Turn end after the final answer settles. */
947
+ const imageResultDefinition = {
948
+ kind: IMAGE_RESULT_NODE_KIND,
949
+ target: "chat",
950
+ match: (event) => {
951
+ const turn = eventTurn(event);
952
+ if (turn === void 0) return null;
953
+ if (event.type === "turn/start") return {
954
+ id: String(turn),
955
+ role: "start"
956
+ };
957
+ if (event.type === "turn/end") return {
958
+ id: String(turn),
959
+ role: "update"
960
+ };
961
+ if (event.type === "assistant/message") return {
962
+ id: String(turn),
963
+ role: "update"
964
+ };
965
+ if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
966
+ id: String(turn),
967
+ role: "update"
968
+ };
969
+ return null;
970
+ },
971
+ start: (_context, match) => {
972
+ const turn = eventTurn(match.event);
973
+ if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
974
+ return {
975
+ turn,
976
+ results: []
977
+ };
978
+ },
979
+ update: (context, match) => {
980
+ if (match.event.type === "turn/end") return {
981
+ ...context.state,
982
+ endSeq: match.event.seq
983
+ };
984
+ if (match.event.type === "assistant/message") return {
985
+ ...context.state,
986
+ answerSeq: match.event.seq
987
+ };
988
+ if (match.event.type !== "tool/result") return context.state;
989
+ const result = imageResultFromMeta(match.event.data.meta);
990
+ if (result === void 0) return context.state;
991
+ if (context.state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return context.state;
992
+ return {
993
+ ...context.state,
994
+ results: [...context.state.results, {
995
+ ...result,
996
+ seq: match.event.seq
997
+ }]
998
+ };
999
+ },
1000
+ buildViewNode: (context) => {
1001
+ const state = context.state;
1002
+ const last = state?.results.at(-1);
1003
+ if (state === void 0 || last === void 0) return null;
1004
+ const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
1005
+ return {
1006
+ key: context.key,
1007
+ kind: IMAGE_RESULT_NODE_KIND,
1008
+ id: context.id,
1009
+ target: "chat",
1010
+ anchorSeq: state.answerSeq ?? state.endSeq ?? last.seq,
1011
+ location,
1012
+ visibility: "visible",
1013
+ data: {
1014
+ turn: state.turn,
1015
+ results: state.results.map(({ seq: _seq, ...result }) => result)
1016
+ }
1017
+ };
1018
+ }
1019
+ };
1020
+ /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
1021
+ function imageResultFromMeta(value) {
1022
+ const meta = record(value);
1023
+ if (meta?.kind !== "dsh-image-gen") return void 0;
1024
+ const attachment = imageAttachment(meta.attachment);
1025
+ if (attachment === void 0) return void 0;
1026
+ return {
1027
+ attachment,
1028
+ prompt: stringValue(meta.prompt, "Generated Image"),
1029
+ provider: stringValue(meta.provider, "google"),
1030
+ model: stringValue(meta.model, ""),
1031
+ output: stringValue(meta.output, ""),
1032
+ ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {}
1033
+ };
1034
+ }
1035
+ function eventTurn(event) {
1036
+ const turn = event.data.turn;
1037
+ return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 0 ? turn : void 0;
1038
+ }
1039
+ function imageAttachment(value) {
1040
+ const candidate = record(value);
1041
+ if (candidate === void 0 || typeof candidate.attachmentId !== "string" || !isImageMediaType(candidate.mediaType) || !positiveInteger(candidate.bytes) || !positiveInteger(candidate.width) || !positiveInteger(candidate.height)) return void 0;
1042
+ const original = record(candidate.originalDimensions);
1043
+ if (candidate.originalDimensions !== void 0 && (original === void 0 || !positiveInteger(original.width) || !positiveInteger(original.height))) return void 0;
1044
+ return {
1045
+ attachmentId: candidate.attachmentId,
1046
+ mediaType: candidate.mediaType,
1047
+ bytes: candidate.bytes,
1048
+ width: candidate.width,
1049
+ height: candidate.height,
1050
+ ...typeof candidate.name === "string" ? { name: candidate.name } : {},
1051
+ ...original === void 0 ? {} : { originalDimensions: {
1052
+ width: original.width,
1053
+ height: original.height
1054
+ } }
1055
+ };
1056
+ }
1057
+ function isImageMediaType(value) {
1058
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
1059
+ }
1060
+ function positiveInteger(value) {
1061
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1062
+ }
1063
+ function stringValue(value, fallback) {
1064
+ return typeof value === "string" ? value : fallback;
1065
+ }
1066
+ function record(value) {
1067
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1068
+ }
1069
+ //#endregion
931
1070
  //#region lib/types/client/index.js
932
- /** Web settings and generated-image cards contributed by the Bundle. */
933
1071
  const KEY_REF = {
934
1072
  google: "GEMINI_API_KEY",
935
1073
  openai: "OPENAI_API_KEY",
@@ -982,6 +1120,7 @@ window.__ModuleLoader__.load({
982
1120
  loading: "正在加载图片…",
983
1121
  loadFailed: "图片读取失败 ({status})",
984
1122
  generatedTitle: "已生成图片",
1123
+ resultShown: "图片结果已显示在对话中",
985
1124
  copyImg: "复制图片",
986
1125
  download: "下载图片",
987
1126
  openNewTab: "新标签页打开",
@@ -1033,6 +1172,7 @@ window.__ModuleLoader__.load({
1033
1172
  loading: "Loading image…",
1034
1173
  loadFailed: "Failed to load image ({status})",
1035
1174
  generatedTitle: "Generated image",
1175
+ resultShown: "Image result is shown in the conversation",
1036
1176
  copyImg: "Copy Image",
1037
1177
  download: "Download Image",
1038
1178
  openNewTab: "Open in new tab",
@@ -1074,6 +1214,7 @@ window.__ModuleLoader__.load({
1074
1214
  .dsh-ig-save:disabled{opacity:.4;cursor:default}
1075
1215
 
1076
1216
  .dsh-ig-result{display:grid;gap:10px;max-width:520px}
1217
+ .dsh-ig-promoted-results{display:grid;gap:16px}
1077
1218
  .dsh-ig-result-title{font-size:14px;font-weight:600}
1078
1219
  .dsh-ig-container{position:relative;display:inline-block;width:fit-content;max-width:100%;justify-self:start;border-radius:12px;overflow:hidden;line-height:0}
1079
1220
  .dsh-ig-container:hover .dsh-ig-toolbar{opacity:1;pointer-events:auto}
@@ -1145,8 +1286,8 @@ window.__ModuleLoader__.load({
1145
1286
  /** Required browser services. */
1146
1287
  const inject = [
1147
1288
  "slots",
1289
+ "connection",
1148
1290
  "remote",
1149
- "remote.credentials",
1150
1291
  "settingsScope",
1151
1292
  "locale"
1152
1293
  ];
@@ -1154,7 +1295,7 @@ window.__ModuleLoader__.load({
1154
1295
  function apply(ctx) {
1155
1296
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
1156
1297
  const locale = ctx.get("locale");
1157
- const credentials = ctx.remote.credentials;
1298
+ const promotion = { enabled: false };
1158
1299
  ctx.effect(() => {
1159
1300
  const style = document.createElement("style");
1160
1301
  style.dataset.plugin = "dsh-image-gen";
@@ -1165,24 +1306,54 @@ window.__ModuleLoader__.load({
1165
1306
  };
1166
1307
  }, "dsh-image-gen: styles");
1167
1308
  const register = ctx.slots.register.bind(ctx.slots);
1168
- ctx.slots.inject("settings.plugin.item", () => register({
1169
- name: "settings.plugin.item",
1170
- key: IMAGE_GENERATION_NAMESPACE,
1171
- inject: () => ({
1172
- scope,
1173
- credentials,
1174
- locale
1175
- })
1176
- }, ImageGenerationSettingsCard));
1309
+ ctx.inject(["uiConversation"], (owner) => {
1310
+ const uiConversation = asModernUiConversation(owner.get("uiConversation"));
1311
+ if (uiConversation === void 0) throw new Error("dsh-image-gen: uiConversation has an incompatible interface");
1312
+ promotion.enabled = true;
1313
+ const ownerRegister = owner.slots.register.bind(owner.slots);
1314
+ owner.effect(() => uiConversation.events.register(imageResultDefinition), "dsh-image-gen: promoted image result node");
1315
+ owner.slots.inject("conversation.chat.node", () => ownerRegister({
1316
+ name: "conversation.chat.node",
1317
+ key: IMAGE_RESULT_NODE_KIND,
1318
+ inject: () => ({ locale })
1319
+ }, PromotedImageResultNode));
1320
+ });
1321
+ const injectSettingsItem = (owner, credentials) => {
1322
+ const ownerRegister = owner.slots.register.bind(owner.slots);
1323
+ owner.slots.inject("settings.plugin.item", () => ownerRegister({
1324
+ name: "settings.plugin.item",
1325
+ key: IMAGE_GENERATION_NAMESPACE,
1326
+ inject: () => ({
1327
+ scope,
1328
+ credentials,
1329
+ locale
1330
+ })
1331
+ }, ImageGenerationSettingsCard));
1332
+ };
1333
+ const remoteCredentials = asCredentialsRemote(ctx.get("remote.credentials"));
1334
+ const legacyCredentials = credentialsFromLegacyConnection(ctx.get("connection"));
1335
+ if (remoteCredentials !== void 0) injectSettingsItem(ctx, remoteCredentials);
1336
+ else if (legacyCredentials !== void 0) injectSettingsItem(ctx, legacyCredentials);
1337
+ else ctx.inject(["remote.credentials"], (remoteCtx) => {
1338
+ const credentials = asCredentialsRemote(remoteCtx.get("remote.credentials"));
1339
+ if (credentials === void 0) throw new Error("dsh-image-gen: remote.credentials has an incompatible interface");
1340
+ injectSettingsItem(remoteCtx, credentials);
1341
+ });
1177
1342
  ctx.slots.inject("tool.call.toolview", () => register({
1178
1343
  name: "tool.call.toolview",
1179
1344
  key: "generate_image",
1180
- inject: () => ({ locale })
1345
+ inject: () => ({
1346
+ locale,
1347
+ promoted: promotion.enabled
1348
+ })
1181
1349
  }, GeneratedImageCard));
1182
1350
  ctx.slots.inject("tool.call.toolview", () => register({
1183
1351
  name: "tool.call.toolview",
1184
1352
  key: "edit_image",
1185
- inject: () => ({ locale })
1353
+ inject: () => ({
1354
+ locale,
1355
+ promoted: promotion.enabled
1356
+ })
1186
1357
  }, GeneratedImageCard));
1187
1358
  ctx.slots.inject("conversation.view", () => register({
1188
1359
  name: "conversation.view",
@@ -1194,6 +1365,42 @@ window.__ModuleLoader__.load({
1194
1365
  inject: () => ({ locale })
1195
1366
  }, GalleryViewTab));
1196
1367
  }
1368
+ function asModernUiConversation(value) {
1369
+ if (value === null || typeof value !== "object") return void 0;
1370
+ const events = value.events;
1371
+ if (events === null || typeof events !== "object") return void 0;
1372
+ return typeof events.register === "function" ? value : void 0;
1373
+ }
1374
+ function asCredentialsRemote(value) {
1375
+ if (value === null || typeof value !== "object") return void 0;
1376
+ const candidate = value;
1377
+ return typeof candidate.describe === "function" && typeof candidate.set === "function" ? candidate : void 0;
1378
+ }
1379
+ function credentialsFromLegacyConnection(value) {
1380
+ if (value === null || typeof value !== "object") return void 0;
1381
+ const credentials = value.api?.credentials;
1382
+ if (credentials === void 0 || typeof credentials.describe !== "function" || typeof credentials.set !== "function") return void 0;
1383
+ const legacy = credentials;
1384
+ return {
1385
+ async describe(refs) {
1386
+ const response = await legacy.describe({ refs });
1387
+ return response.result.ok ? {
1388
+ ok: true,
1389
+ value: response.result.value.credentials
1390
+ } : { ok: false };
1391
+ },
1392
+ async set(ref, credentialValue) {
1393
+ const response = await legacy.set({
1394
+ ref,
1395
+ value: credentialValue
1396
+ });
1397
+ return response.result.ok ? { ok: true } : {
1398
+ ok: false,
1399
+ error: response.result.error
1400
+ };
1401
+ }
1402
+ };
1403
+ }
1197
1404
  /** Edit provider settings and its write-only API credential. */
1198
1405
  function ImageGenerationSettingsCard(props) {
1199
1406
  const [open, setOpen] = (0, react.useState)(false);
@@ -1576,43 +1783,57 @@ window.__ModuleLoader__.load({
1576
1783
  }) : null]
1577
1784
  });
1578
1785
  }
1579
- /** Render the durable attachment referenced by a completed image tool call. */
1786
+ /** Keep the legacy Tool row for old DSH and hand modern results to the independent Chat node. */
1580
1787
  function GeneratedImageCard(props) {
1581
- const attachment = imageRef(props.block);
1582
- const savedTo = imageSavedTo(props.block);
1788
+ const result = imageResultFromBlock(props.block);
1789
+ if (props.promoted && result !== void 0) return (0, react_jsx_runtime.jsx)(PromotedResultNotice, { locale: props.locale });
1790
+ return (0, react_jsx_runtime.jsx)(ImageResultCard, {
1791
+ result,
1792
+ locale: props.locale
1793
+ });
1794
+ }
1795
+ /** Render modern image artifacts as final conversation output instead of Tool process content. */
1796
+ function PromotedImageResultNode(props) {
1797
+ return (0, react_jsx_runtime.jsx)("div", {
1798
+ className: "dsh-ig-promoted-results",
1799
+ children: props.node.data.results.map((result) => (0, react_jsx_runtime.jsx)(ImageResultCard, {
1800
+ result,
1801
+ locale: props.locale
1802
+ }, result.attachment.attachmentId))
1803
+ });
1804
+ }
1805
+ function PromotedResultNotice({ locale }) {
1806
+ const lang = usePluginLanguage(locale);
1807
+ return (0, react_jsx_runtime.jsx)("div", {
1808
+ className: "dsh-ig-loading",
1809
+ children: DICT[lang].resultShown
1810
+ });
1811
+ }
1812
+ function ImageResultCard({ result, locale }) {
1813
+ const attachment = result?.attachment;
1814
+ const savedTo = result?.savedTo;
1583
1815
  const [url, setUrl] = (0, react.useState)();
1584
1816
  const [blob, setBlob] = (0, react.useState)();
1585
1817
  const [error, setError] = (0, react.useState)();
1586
1818
  const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
1587
1819
  const [toast, setToast] = (0, react.useState)();
1588
- const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1589
- (0, react.useEffect)(() => {
1590
- return props.locale?.subscribe?.(() => {
1591
- setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1592
- });
1593
- }, [props.locale]);
1820
+ const lang = usePluginLanguage(locale);
1594
1821
  const t = (keyName, params) => {
1595
1822
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
1596
1823
  if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
1597
1824
  return text;
1598
1825
  };
1599
1826
  (0, react.useEffect)(() => {
1600
- if (attachment === void 0) return;
1601
- const blockAny = props.block;
1602
- const meta = blockAny.meta ?? blockAny.resultView?.meta;
1603
- const prompt = typeof meta?.prompt === "string" ? meta.prompt : blockAny.call?.args?.prompt ?? "Generated Image";
1604
- const provider = typeof meta?.provider === "string" ? meta.provider : "google";
1605
- const model = typeof meta?.model === "string" ? meta.model : "";
1606
- const output = typeof meta?.output === "string" ? meta.output : "";
1827
+ if (result === void 0) return;
1607
1828
  saveGalleryItem({
1608
- id: attachment.attachmentId,
1609
- attachment,
1610
- prompt,
1611
- provider,
1612
- model,
1613
- output
1829
+ id: result.attachment.attachmentId,
1830
+ attachment: result.attachment,
1831
+ prompt: result.prompt,
1832
+ provider: result.provider,
1833
+ model: result.model,
1834
+ output: result.output
1614
1835
  });
1615
- }, [attachment?.attachmentId]);
1836
+ }, [result?.attachment.attachmentId]);
1616
1837
  (0, react.useEffect)(() => {
1617
1838
  if (!previewOpen) return;
1618
1839
  const onKeyDown = (e) => {
@@ -1816,6 +2037,27 @@ window.__ModuleLoader__.load({
1816
2037
  ]
1817
2038
  });
1818
2039
  }
2040
+ function usePluginLanguage(locale) {
2041
+ const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
2042
+ (0, react.useEffect)(() => locale?.subscribe?.(() => {
2043
+ setLang(locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
2044
+ }), [locale]);
2045
+ return lang;
2046
+ }
2047
+ function imageResultFromBlock(block) {
2048
+ const attachment = imageRef(block);
2049
+ if (attachment === void 0) return void 0;
2050
+ const blockValue = block;
2051
+ const meta = blockValue.meta ?? blockValue.resultView?.meta;
2052
+ return {
2053
+ attachment,
2054
+ prompt: typeof meta?.prompt === "string" ? meta.prompt : blockValue.call?.args?.prompt ?? "Generated Image",
2055
+ provider: typeof meta?.provider === "string" ? meta.provider : "google",
2056
+ model: typeof meta?.model === "string" ? meta.model : "",
2057
+ output: typeof meta?.output === "string" ? meta.output : "",
2058
+ ...typeof meta?.savedTo === "string" ? { savedTo: meta.savedTo } : {}
2059
+ };
2060
+ }
1819
2061
  function modelOf(provider, value) {
1820
2062
  const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : value?.comfyuiWorkflowName;
1821
2063
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
@@ -1824,20 +2066,10 @@ window.__ModuleLoader__.load({
1824
2066
  const stored = provider === "google" ? value?.googleEndpoint : provider === "openai" ? value?.openaiBaseURL : provider === "seedream" ? value?.seedreamBaseURL : provider === "dashscope" ? value?.dashscopeEndpoint : value?.comfyuiBaseURL;
1825
2067
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
1826
2068
  }
1827
- function imageRef(block) {
1828
- if (!("kind" in block) || block.resultView?.card !== "generic") return void 0;
1829
- const image = block.resultView.content?.find((item) => item.type === "image");
1830
- return image?.type === "image" ? image.attachment : void 0;
1831
- }
1832
- /** The workspace file path a completed image call saved, when the result meta carries one. */
1833
- function imageSavedTo(block) {
1834
- if (!("kind" in block)) return void 0;
1835
- const meta = block.meta ?? block.resultView?.meta;
1836
- return typeof meta?.savedTo === "string" ? meta.savedTo : void 0;
1837
- }
1838
2069
  //#endregion
1839
2070
  exports.GeneratedImageCard = GeneratedImageCard;
1840
2071
  exports.ImageGenerationSettingsCard = ImageGenerationSettingsCard;
2072
+ exports.PromotedImageResultNode = PromotedImageResultNode;
1841
2073
  exports.apply = apply;
1842
2074
  exports.inject = inject;
1843
2075
  return module.exports;