@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.1

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.
Files changed (234) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/README.en.md +109 -311
  3. package/README.md +106 -309
  4. package/bin/dsh-iris.js +452 -92
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +2 -0
  7. package/docs/ARCHITECTURE.md +13 -3
  8. package/docs/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +87 -0
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +62 -0
  12. package/docs/CORE_CONNECTIONS.md +101 -0
  13. package/docs/CORE_QUICKSTART.md +110 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  15. package/docs/CORE_SERVICE.md +101 -0
  16. package/docs/CORE_SKILLS.md +35 -3
  17. package/docs/DSH_COMPATIBILITY.md +21 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +6 -2
  21. package/docs/HEADLESS_CLI.md +30 -5
  22. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  23. package/docs/IMAGE_FLOW.md +4 -0
  24. package/docs/IMAGE_GENERATION.md +127 -0
  25. package/docs/LOCAL_API.md +4 -2
  26. package/docs/LOCAL_API_CONTRACT.md +42 -15
  27. package/docs/LOCAL_CREATION.md +7 -1
  28. package/docs/LOCAL_OPERATIONS.md +46 -0
  29. package/docs/LOCAL_SELECTION.md +57 -0
  30. package/docs/LOCAL_WEB.md +207 -0
  31. package/docs/MCP.md +212 -0
  32. package/docs/MEDIA_SCHEDULING.md +72 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/NETWORK_RESOURCES.md +106 -0
  35. package/docs/NETWORK_ROUTING.md +103 -0
  36. package/docs/NETWORK_WORKBENCH.md +65 -0
  37. package/docs/PANEL_EXTENSIONS.md +64 -0
  38. package/docs/PLUGINS.md +193 -0
  39. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  40. package/docs/PLUGIN_STARTUP.md +113 -0
  41. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  42. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  43. package/docs/PROVIDER_EXTENSIONS.md +82 -0
  44. package/docs/PROVIDER_HEALTH.md +5 -1
  45. package/docs/PROVIDER_NETWORK.md +45 -0
  46. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  47. package/docs/PROVIDER_SETTINGS.md +91 -0
  48. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
  49. package/docs/ROADMAP.md +18 -8
  50. package/docs/SECURITY.md +1 -1
  51. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  52. package/docs/VIDEO_FLOWS.md +89 -0
  53. package/docs/VISION_CLI.md +1 -1
  54. package/docs/VISION_MODEL.md +2 -0
  55. package/docs/WEB_PROTOTYPE.md +21 -6
  56. package/docs/WEB_RECOVERY.md +46 -0
  57. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  58. package/docs/WORKBENCH_LOGIN.md +63 -0
  59. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  60. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  61. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  62. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  63. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  64. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  65. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  66. package/docs/releases/0.3.0-alpha.1.md +51 -0
  67. package/docs/releases/0.3.0-beta.1.md +34 -0
  68. package/docs/screenshots.md +41 -1
  69. package/lib/actions.js +56 -4
  70. package/lib/adapters.js +75 -49
  71. package/lib/api.js +23 -13
  72. package/lib/application-runtime.js +73 -17
  73. package/lib/application-writer.js +193 -0
  74. package/lib/audio-flow-input.js +37 -0
  75. package/lib/client.d.ts +3 -3
  76. package/lib/client.js +402 -72
  77. package/lib/comfyui-adapter.js +95 -0
  78. package/lib/comfyui-client.js +60 -0
  79. package/lib/comfyui-inspection.js +73 -0
  80. package/lib/comfyui-workflow.js +94 -0
  81. package/lib/command-service.js +18 -5
  82. package/lib/config.js +49 -10
  83. package/lib/configured-image-runtime.js +69 -0
  84. package/lib/connection-skill.js +98 -0
  85. package/lib/connection-template.js +23 -0
  86. package/lib/core-artifact-store.js +84 -28
  87. package/lib/core-bridge-selection.js +31 -0
  88. package/lib/core-bridge-tools.js +85 -0
  89. package/lib/core-capability-catalog.js +192 -0
  90. package/lib/core-connection-diagnostics.js +49 -0
  91. package/lib/core-connection-store.js +277 -0
  92. package/lib/core-connections.js +225 -0
  93. package/lib/core-delivery-cache.js +8 -3
  94. package/lib/core-maintenance.js +9 -3
  95. package/lib/core-runtime.js +2 -1
  96. package/lib/core-service-client.js +174 -0
  97. package/lib/core-tasks.js +113 -21
  98. package/lib/core-writer-lease.js +16 -0
  99. package/lib/creation-request-store.js +41 -64
  100. package/lib/doctor.js +2 -2
  101. package/lib/dsh-core-adapter.js +61 -9
  102. package/lib/dsh-host-adapter.js +4 -1
  103. package/lib/dsh-provider-config.js +1 -1
  104. package/lib/dsh-service-bridge.js +67 -0
  105. package/lib/execution-ownership.js +43 -0
  106. package/lib/flow-store.js +156 -14
  107. package/lib/generation-input.js +10 -1
  108. package/lib/headless-vision.js +7 -7
  109. package/lib/image-edit-input.js +1 -1
  110. package/lib/image-flow.js +97 -31
  111. package/lib/index.js +47 -22
  112. package/lib/local-api-projection.js +15 -6
  113. package/lib/local-api-token-directory.js +31 -0
  114. package/lib/local-api.js +861 -71
  115. package/lib/local-creation.js +96 -24
  116. package/lib/local-operations.js +36 -0
  117. package/lib/local-request-root.js +86 -0
  118. package/lib/local-web-assets.js +39 -0
  119. package/lib/mcp-bridge.js +58 -0
  120. package/lib/mcp-check.js +26 -0
  121. package/lib/mcp-config.js +42 -0
  122. package/lib/mcp-delivery.js +46 -0
  123. package/lib/mcp-http.js +174 -0
  124. package/lib/mcp-server.js +69 -0
  125. package/lib/media-scheduler.js +194 -0
  126. package/lib/model-directory-service.js +37 -10
  127. package/lib/model-directory.js +3 -3
  128. package/lib/models.js +2 -2
  129. package/lib/music-transport.js +6 -3
  130. package/lib/network-config-service.js +123 -0
  131. package/lib/network-config.js +127 -0
  132. package/lib/network-doctor.js +94 -0
  133. package/lib/network-management.js +51 -0
  134. package/lib/network-policy.js +19 -0
  135. package/lib/network-route-evidence.js +38 -0
  136. package/lib/network-routing.js +283 -0
  137. package/lib/operation-runner.js +281 -0
  138. package/lib/operation-store.js +216 -0
  139. package/lib/panel-extension.js +36 -0
  140. package/lib/plugin-contract.js +194 -0
  141. package/lib/plugin-execution.js +51 -0
  142. package/lib/plugin-launch.js +86 -0
  143. package/lib/plugin-operation-store.js +101 -0
  144. package/lib/plugin-operations.js +28 -0
  145. package/lib/plugin-registry.js +109 -0
  146. package/lib/plugin-selection.js +15 -0
  147. package/lib/plugin-source-image.js +5 -0
  148. package/lib/plugin-web-assets.js +54 -0
  149. package/lib/plugins.d.ts +307 -0
  150. package/lib/plugins.js +59 -0
  151. package/lib/provider-adapter.js +7 -3
  152. package/lib/provider-adapters.js +92 -22
  153. package/lib/provider-catalog.js +64 -36
  154. package/lib/provider-config-format.js +51 -0
  155. package/lib/provider-config-service.js +143 -31
  156. package/lib/provider-context.d.ts +90 -0
  157. package/lib/provider-context.js +42 -0
  158. package/lib/provider-credentials.js +97 -0
  159. package/lib/provider-diagnostics.js +56 -0
  160. package/lib/provider-extensions.js +29 -0
  161. package/lib/provider-health.js +18 -6
  162. package/lib/provider-monitor.js +81 -0
  163. package/lib/provider-network.js +52 -0
  164. package/lib/provider-protocol.js +73 -22
  165. package/lib/provider-registry.js +128 -0
  166. package/lib/provider-response.js +38 -0
  167. package/lib/provider-settings.js +136 -0
  168. package/lib/provider-task-runner.js +132 -43
  169. package/lib/provider-transport.js +16 -0
  170. package/lib/render.js +11 -1
  171. package/lib/scheduling-policy.js +66 -0
  172. package/lib/skill-installation.js +23 -11
  173. package/lib/tasks.js +3 -2
  174. package/lib/video-flow-input.js +16 -0
  175. package/lib/video-input.js +12 -5
  176. package/lib/vision-model-routing.js +5 -0
  177. package/lib/web/app.js +703 -0
  178. package/lib/web/appearance.js +111 -0
  179. package/lib/web/comfyui.js +146 -0
  180. package/lib/web/connections.js +516 -0
  181. package/lib/web/creation.js +359 -0
  182. package/lib/web/flow-view.js +34 -0
  183. package/lib/web/gallery.js +133 -0
  184. package/lib/web/index.html +344 -0
  185. package/lib/web/media-client.js +218 -0
  186. package/lib/web/network.js +232 -0
  187. package/lib/web/plugin-panel.js +223 -0
  188. package/lib/web/plugin-status.js +140 -0
  189. package/lib/web/plugin-surface.css +17 -0
  190. package/lib/web/providers.js +513 -0
  191. package/lib/web/recovery.js +105 -0
  192. package/lib/web/startup.js +18 -0
  193. package/lib/web/styles.css +571 -0
  194. package/lib/web/transport.js +101 -0
  195. package/lib/web/workbench-login.js +165 -0
  196. package/lib/workbench-artifacts.js +3 -2
  197. package/lib/workbench-auth-cli.js +47 -0
  198. package/lib/workbench-auth.js +200 -0
  199. package/package.json +24 -6
  200. package/plugins/contact-sheet/index.d.mts +4 -0
  201. package/plugins/contact-sheet/index.mjs +33 -0
  202. package/plugins/contact-sheet/web/board.css +14 -0
  203. package/plugins/contact-sheet/web/board.mjs +93 -0
  204. package/skills/iris-core/SKILL.md +8 -4
  205. package/skills/iris-core/references/cli.md +2 -2
  206. package/skills/iris-core/references/comfyui.md +69 -0
  207. package/skills/iris-core/references/tasks.md +2 -0
  208. package/templates/plugin-starter/LICENSE +21 -0
  209. package/templates/plugin-starter/README.md +74 -0
  210. package/templates/plugin-starter/assets/peach.png +0 -0
  211. package/templates/plugin-starter/assets/violet.png +0 -0
  212. package/templates/plugin-starter/index.mjs +27 -0
  213. package/templates/plugin-starter/package.json +18 -0
  214. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  215. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  216. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  217. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  218. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  219. package/templates/plugin-starter/web/board.css +27 -0
  220. package/templates/plugin-starter/web/board.mjs +160 -0
  221. package/templates/plugin-starter/web/png.mjs +23 -0
  222. package/templates/provider-starter/LICENSE +21 -0
  223. package/templates/provider-starter/README.md +50 -0
  224. package/templates/provider-starter/index.d.mts +5 -0
  225. package/templates/provider-starter/index.mjs +165 -0
  226. package/templates/provider-starter/package.json +13 -0
  227. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  228. package/templates/provider-starter/scripts/environment.mjs +122 -0
  229. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  230. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  231. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  232. package/templates/provider-starter/scripts/setup.mjs +44 -0
  233. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  234. package/user_guide.md +5 -1
@@ -0,0 +1,111 @@
1
+ export const DEFAULT_APPEARANCE = Object.freeze({ contractVersion: 0, theme: 'light', accent: 'iris', density: 'comfortable',
2
+ position: 'center', overlay: 86, blur: 0 });
3
+ const KEY = 'iris.panel.appearance.v0';
4
+ export function normalizeAppearance(value) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value) || value.contractVersion !== 0
6
+ || Object.keys(value).some(key => !Object.hasOwn(DEFAULT_APPEARANCE, key))
7
+ || !['light', 'dark', 'system'].includes(value.theme) || !['iris', 'violet', 'rose', 'apricot'].includes(value.accent)
8
+ || !['comfortable', 'compact'].includes(value.density) || !['center', 'top', 'bottom'].includes(value.position)
9
+ || !Number.isInteger(value.overlay) || value.overlay < 50 || value.overlay > 100
10
+ || !Number.isInteger(value.blur) || value.blur < 0 || value.blur > 20) throw new TypeError('外观文件格式或版本不支持');
11
+ return { ...value, accent: 'iris' };
12
+ }
13
+
14
+ export function createAppearance() {
15
+ const $ = id => document.getElementById(id), root = document.documentElement;
16
+ const system = matchMedia('(prefers-color-scheme: dark)');
17
+ let preferences = { ...DEFAULT_APPEARANCE }, backgroundUrl, pictureTicket = 0, backgroundReady = false;
18
+ const note = (text, error = false) => { $('appearance-note').textContent = text; $('appearance-note').classList.toggle('error', error); };
19
+ function apply() {
20
+ const dark = preferences.theme === 'dark' || preferences.theme === 'system' && system.matches;
21
+ root.dataset.theme = dark ? 'dark' : 'light'; root.dataset.accent = preferences.accent; root.dataset.density = preferences.density;
22
+ root.style.setProperty('--background-position', preferences.position);
23
+ root.style.setProperty('--background-opacity', String((100 - preferences.overlay) / 100));
24
+ root.style.setProperty('--background-blur', preferences.blur + 'px');
25
+ for (const key of ['theme', 'density', 'position', 'overlay', 'blur']) $('appearance-' + key).value = String(preferences[key]);
26
+ $('appearance-overlay-value').textContent = preferences.overlay + '%'; $('appearance-blur-value').textContent = preferences.blur + ' px';
27
+ }
28
+ function save() {
29
+ apply();
30
+ try { localStorage.setItem(KEY, JSON.stringify(preferences)); note('已保存到此浏览器,即时生效。'); }
31
+ catch (_) { note('此浏览器无法保存外观,本次预览仍有效。', true); }
32
+ }
33
+ async function storedPicture(action, value) {
34
+ const db = await new Promise((resolve, reject) => {
35
+ const request = indexedDB.open('iris.panel.appearance', 1);
36
+ request.onupgradeneeded = () => request.result.createObjectStore('images');
37
+ request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
38
+ });
39
+ try {
40
+ return await new Promise((resolve, reject) => {
41
+ const transaction = db.transaction('images', action === 'get' ? 'readonly' : 'readwrite');
42
+ const store = transaction.objectStore('images');
43
+ const request = action === 'get' ? store.get('background') : action === 'put' ? store.put(value, 'background') : store.delete('background');
44
+ transaction.oncomplete = () => resolve(request.result);
45
+ transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error || new Error('外观存储已中断'));
46
+ });
47
+ } finally { db.close(); }
48
+ }
49
+ function replacePicture(url) {
50
+ if (backgroundUrl) URL.revokeObjectURL(backgroundUrl);
51
+ backgroundUrl = url;
52
+ root.style.setProperty('--workspace-image', url ? 'url("' + url + '")' : 'none');
53
+ $('appearance-remove').disabled = !url;
54
+ $('appearance-image-note').textContent = url ? '已使用本地背景,仅保存在此浏览器。' : '没有自选背景。图片不会上传到 Core。';
55
+ }
56
+ async function validatePicture(file) {
57
+ if (!(file instanceof Blob) || !['image/png', 'image/jpeg', 'image/webp'].includes(file.type) || file.size > 5 * 1024 * 1024) throw new TypeError('请选择 5 MiB 以内的 PNG、JPEG 或 WebP 图片');
58
+ const url = URL.createObjectURL(file), image = new Image();
59
+ try { image.src = url; await image.decode(); if (image.naturalWidth > 16384 || image.naturalHeight > 16384) throw new TypeError('背景图片尺寸过大'); return url; }
60
+ catch (error) { URL.revokeObjectURL(url); throw error; }
61
+ }
62
+ try {
63
+ const saved = localStorage.getItem(KEY);
64
+ if (saved) preferences = normalizeAppearance(JSON.parse(saved));
65
+ } catch (_) { note('已使用默认外观;原偏好不可读取或版本不支持。', true); }
66
+ apply();
67
+ void (async () => {
68
+ const ticket = pictureTicket;
69
+ try {
70
+ const file = await storedPicture('get');
71
+ if (file) { const url = await validatePicture(file); if (ticket === pictureTicket) replacePicture(url); else URL.revokeObjectURL(url); }
72
+ } catch (_) { note('本地背景无法读取,配色仍可使用。可重新选择图片。', true); }
73
+ finally { backgroundReady = true; $('appearance-image').disabled = false; }
74
+ })();
75
+ $('appearance-open').addEventListener('click', () => $('appearance-dialog').showModal());
76
+ $('appearance-close').addEventListener('click', () => $('appearance-dialog').close());
77
+ for (const key of ['theme', 'density', 'position', 'overlay', 'blur']) $('appearance-' + key).addEventListener('input', event => {
78
+ preferences[key] = ['overlay', 'blur'].includes(key) ? Number(event.target.value) : event.target.value; save();
79
+ });
80
+ system.addEventListener('change', () => { if (preferences.theme === 'system') apply(); });
81
+ $('appearance-image').addEventListener('change', async event => {
82
+ const file = event.target.files[0]; if (!file || !backgroundReady) return;
83
+ const ticket = ++pictureTicket; $('appearance-image').disabled = true; note('正在保存本地背景…');
84
+ try {
85
+ const url = await validatePicture(file);
86
+ if (ticket !== pictureTicket) { URL.revokeObjectURL(url); return; }
87
+ replacePicture(url);
88
+ $('appearance-remove').disabled = true;
89
+ try { await storedPicture('put', file); note('背景已保存到此浏览器。'); }
90
+ catch (_) { note('背景存储不可用,本次预览仍有效;刷新后需重新选择。', true); }
91
+ } catch (error) { note(error.message, true); }
92
+ finally { event.target.value = ''; $('appearance-image').disabled = false; $('appearance-remove').disabled = !backgroundUrl; }
93
+ });
94
+ $('appearance-remove').addEventListener('click', async () => {
95
+ ++pictureTicket; replacePicture(null);
96
+ try { await storedPicture('delete'); note('已移除此浏览器的背景。'); }
97
+ catch (_) { note('背景预览已移除,但本地存储更新失败;刷新后请核对。', true); }
98
+ });
99
+ $('appearance-reset').addEventListener('click', () => { preferences = { ...DEFAULT_APPEARANCE }; save(); });
100
+ $('appearance-export').addEventListener('click', () => {
101
+ const url = URL.createObjectURL(new Blob([JSON.stringify(preferences, null, 2) + '\n'], { type: 'application/json' }));
102
+ const a = document.createElement('a'); a.href = url; a.download = 'iris-theme.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
103
+ });
104
+ $('appearance-import').addEventListener('change', async event => {
105
+ const file = event.target.files[0]; if (!file) return;
106
+ try { if (file.size > 16384) throw new TypeError('外观文件过大'); preferences = normalizeAppearance(JSON.parse(await file.text())); save(); }
107
+ catch (_) { note('外观文件格式或版本不支持;当前设置已保留。', true); }
108
+ finally { event.target.value = ''; }
109
+ });
110
+ window.addEventListener('pagehide', () => { ++pictureTicket; if (backgroundUrl) URL.revokeObjectURL(backgroundUrl); });
111
+ }
@@ -0,0 +1,146 @@
1
+ // Browser presentation for explicitly exposed workflow inputs. Server validation remains authoritative.
2
+ const object = value => value && typeof value === 'object' && !Array.isArray(value);
3
+ const scalar = value => ['string', 'boolean'].includes(typeof value) || typeof value === 'number' && Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER;
4
+ const labelByInput = { lora_name: 'LoRA', strength_model: '模型权重', strength_clip: 'CLIP 权重',
5
+ ckpt_name: 'Checkpoint', seed: '种子', steps: '步数', sampler_name: '采样器', scheduler: '调度器', cfg: 'CFG', denoise: '降噪强度' };
6
+ const element = (tag, text, className) => { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; };
7
+ export const parameterLabel = parameter => parameter.label || labelByInput[parameter.input] || (parameter.input === 'text' ? parameter.id === 'negative' ? '负面提示词' : '文本 · ' + parameter.node : parameter.id);
8
+
9
+ export function normalizeWorkflowValues(value) {
10
+ if (!object(value) || !Object.keys(value).length || Object.keys(value).length > 32
11
+ || Object.entries(value).some(([id, item]) => !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(id) || ['constructor', 'prototype'].includes(id)
12
+ || !scalar(item) || typeof item === 'string' && item.length > 20000)
13
+ || new TextEncoder().encode(JSON.stringify(value)).length > 24 * 1024) throw new TypeError('工作流参数需要 1–32 个有界标量,整数限于安全范围,正文最多 24 KiB');
14
+ return Object.fromEntries(Object.keys(value).sort().map(id => [id, value[id]]));
15
+ }
16
+
17
+ export function readWorkflowImport(text) {
18
+ if (new TextEncoder().encode(text).length > 40 * 1024) throw new TypeError('工作流超过 40 KiB,请精简后导入。');
19
+ const value = JSON.parse(text), template = object(value) && Object.hasOwn(value, 'graph');
20
+ const graph = template ? value.graph : value;
21
+ if (!object(graph) || !Object.keys(graph).length || Object.keys(graph).length > 256
22
+ || Object.entries(graph).some(([id, node]) => !/^[A-Za-z0-9_.:-]{1,100}$/.test(id) || ['__proto__', 'constructor', 'prototype'].includes(id)
23
+ || !object(node) || typeof node.class_type !== 'string' || !node.class_type || !object(node.inputs))) {
24
+ throw new TypeError('需要 Export (API) 的节点映射或完整 Iris 模板,普通画布 JSON 不能执行。');
25
+ }
26
+ for (const node of Object.values(graph)) for (const number of Object.values(node.inputs)) {
27
+ if (typeof number === 'number' && (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number))) {
28
+ throw new TypeError('图中有超出安全范围的整数,请将种子调整到 0–9007199254740991 后重新导出。');
29
+ }
30
+ }
31
+ if (template && (![0, 1].includes(value.version) || value.version === 1 && (!Array.isArray(value.parameters) || !value.parameters.length || value.parameters.length > 32))) {
32
+ throw new TypeError('不支持的 Iris 工作流模板版本或参数数量。');
33
+ }
34
+ return structuredClone(template ? value : { version: 0, graph });
35
+ }
36
+
37
+ export function workflowSlots(template, { promptNode, sizeNode } = template) {
38
+ const existing = new Map((template.parameters || []).map(parameter => [parameter.node + '\0' + parameter.input, parameter]));
39
+ const used = new Set((template.parameters || []).map(parameter => parameter.id));
40
+ return Object.entries(template.graph).flatMap(([node, value]) => Object.entries(value.inputs)
41
+ .filter(([input, item]) => scalar(item) && !(node === promptNode && input === 'text') && !(node === sizeNode && ['width', 'height'].includes(input))
42
+ && /^[A-Za-z0-9_.:-]{1,100}$/.test(input) && !['__proto__', 'constructor', 'prototype'].includes(input))
43
+ .map(([input, item]) => {
44
+ const parameter = existing.get(node + '\0' + input);
45
+ const base = 'p_' + (node + '_' + input).replace(/[^A-Za-z0-9_]/g, '_').slice(0, 48); let id = base, suffix = 0;
46
+ if (!parameter) { while (used.has(id)) id = base + '_' + ++suffix; used.add(id); }
47
+ const type = typeof item === 'number' ? Number.isSafeInteger(item) && !['strength_model', 'strength_clip', 'cfg', 'denoise'].includes(input) ? 'integer' : 'number' : typeof item;
48
+ return { ...(parameter || { id, node, input, type }),
49
+ default: item, exposed: Boolean(parameter), nodeType: value.class_type };
50
+ }));
51
+ }
52
+
53
+ export function buildWorkflow(template, bindings, slots) {
54
+ const { promptNode, outputNode, sizeNode } = bindings, graph = template.graph;
55
+ if (typeof graph[promptNode]?.inputs.text !== 'string' || !graph[outputNode]) throw new TypeError('请明确选择正向提示词和图片输出节点。');
56
+ if (sizeNode && !['width', 'height'].every(key => Number.isSafeInteger(graph[sizeNode]?.inputs[key]) && graph[sizeNode].inputs[key] > 0)) throw new TypeError('尺寸节点需要有效的 width 和 height。');
57
+ const parameters = slots.filter(slot => slot.exposed).map(({ exposed, default: value, nodeType, ...parameter }) => parameter);
58
+ const order = new Map((template.parameters || []).map((parameter, index) => [parameter.id, index]));
59
+ parameters.sort((a, b) => (order.get(a.id) ?? 32) - (order.get(b.id) ?? 32));
60
+ if (parameters.length > 32 || new Set(parameters.map(parameter => parameter.id)).size !== parameters.length) throw new TypeError('最多开放 32 个参数,参数 ID 不能重复。');
61
+ for (const parameter of parameters) {
62
+ if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(parameter.id) || ['constructor', 'prototype'].includes(parameter.id)) throw new TypeError('参数 ID 需要以英文字母开头,只含字母、数字和下划线。');
63
+ if (parameter.label !== undefined && (!parameter.label.trim() || parameter.label.length > 80)) throw new TypeError('参数名称需要 1–80 个字符。');
64
+ }
65
+ const workflow = { version: parameters.length ? 1 : 0, graph: structuredClone(graph), promptNode, outputNode,
66
+ ...(sizeNode ? { sizeNode } : {}), ...(parameters.length ? { parameters } : {}) };
67
+ if (new TextEncoder().encode(JSON.stringify(workflow)).length > 40 * 1024) throw new TypeError('加上参数声明后,模板超过 40 KiB。');
68
+ return workflow;
69
+ }
70
+
71
+ function disclosure(title, className = '') {
72
+ const details = element('details', undefined, 'comfy-group ' + className), summary = element('summary');
73
+ const state = element('span', '展开', 'disclosure'); summary.append(element('span', title), state); details.append(summary);
74
+ details.addEventListener('toggle', () => { state.textContent = details.open ? '收起' : '展开'; }); return details;
75
+ }
76
+
77
+ /** Renders only supplied descriptors; reading returns a fresh, sorted value snapshot. */
78
+ export function renderWorkflowParameters(container, parameters, { values = {}, checked = [], onChange = () => {}, disabled = false, preserveOpen = false } = {}) {
79
+ const expanded = new Set(preserveOpen ? [...container.querySelectorAll('details[open]')].map(node => node.dataset.parameterGroup) : []);
80
+ container.replaceChildren(); const controls = new Map(), loras = new Map();
81
+ const common = element('div', undefined, 'comfy-common'), advanced = disclosure('更多参数 · ' + parameters.filter(parameter => !['seed', 'text', 'lora_name', 'strength_model', 'strength_clip'].includes(parameter.input)).length);
82
+ advanced.dataset.parameterGroup = 'advanced'; advanced.open = expanded.has('advanced');
83
+ container.append(common);
84
+ for (const parameter of parameters) {
85
+ let parent = common;
86
+ if (['lora_name', 'strength_model', 'strength_clip'].includes(parameter.input)) {
87
+ if (!loras.has(parameter.node)) {
88
+ const name = parameters.find(item => item.node === parameter.node && item.input === 'lora_name');
89
+ const selected = name && (Object.hasOwn(values, name.id) ? values[name.id] : name.default);
90
+ const card = disclosure('LoRA · ' + (selected || '节点 ' + parameter.node), 'comfy-lora');
91
+ card.dataset.parameterGroup = 'lora:' + parameter.node; card.open = expanded.has(card.dataset.parameterGroup);
92
+ const body = element('div', undefined, 'comfy-parameter-grid'); card.append(body); loras.set(parameter.node, body); container.append(card);
93
+ }
94
+ parent = loras.get(parameter.node);
95
+ } else if (!['seed', 'text'].includes(parameter.input)) parent = advanced;
96
+ const wrapper = element('label', undefined, 'comfy-parameter'); wrapper.append(element('span', parameterLabel(parameter)));
97
+ const value = Object.hasOwn(values, parameter.id) ? values[parameter.id] : parameter.default;
98
+ const discovered = checked.find(item => item.id === parameter.id);
99
+ const choices = parameter.choices || discovered?.availableChoices;
100
+ let input;
101
+ if (choices && !discovered?.choicesTruncated) {
102
+ input = element('select');
103
+ for (const choice of choices) { const option = element('option', String(choice)); option.value = JSON.stringify(choice); input.append(option); }
104
+ if (!choices.includes(value)) { const option = element('option', String(value) + ' · 不在当前候选中'); option.value = JSON.stringify(value); input.append(option); }
105
+ input.value = JSON.stringify(value);
106
+ } else if (parameter.type === 'boolean') { input = element('input'); input.type = 'checkbox'; input.checked = value; }
107
+ else {
108
+ input = element(parameter.type === 'string' && parameter.input === 'text' ? 'textarea' : 'input');
109
+ if (input.tagName === 'TEXTAREA') input.rows = 2;
110
+ else input.type = parameter.type === 'string' ? 'text' : 'number';
111
+ input.value = String(value); if (parameter.type === 'string') input.maxLength = 20000;
112
+ else { input.step = parameter.type === 'integer' ? '1' : 'any'; input.required = true;
113
+ input.min = String(parameter.min ?? -Number.MAX_SAFE_INTEGER); input.max = String(parameter.max ?? Number.MAX_SAFE_INTEGER); }
114
+ }
115
+ input.dataset.workflowParameter = parameter.id; input.setAttribute('aria-label', parameterLabel(parameter)); input.disabled = disabled;
116
+ input.addEventListener(input.tagName === 'SELECT' || input.type === 'checkbox' ? 'change' : 'input', () => {
117
+ if (parameter.input === 'lora_name') parent.parentElement.querySelector('summary>span:first-child').textContent = 'LoRA · ' + (input.tagName === 'SELECT' ? JSON.parse(input.value) : input.value);
118
+ onChange();
119
+ }); wrapper.append(input);
120
+ if (discovered?.choicesTruncated && !parameter.choices) wrapper.append(element('small', '候选超过 256 项,可手填服务上已有的名称。', 'field-note'));
121
+ parent.append(wrapper); controls.set(parameter.id, { parameter, input });
122
+ }
123
+ if (advanced.children.length > 1) container.append(advanced);
124
+ if (!parameters.length) container.append(element('p', '沿用工作流中的固定参数。', 'field-note'));
125
+ function read() {
126
+ const result = {};
127
+ for (const [id, { parameter, input }] of controls) {
128
+ const value = input.tagName === 'SELECT' ? JSON.parse(input.value) : parameter.type === 'boolean' ? input.checked
129
+ : parameter.type === 'string' ? input.value : input.value === '' ? null : Number(input.value);
130
+ if (!scalar(value) || parameter.type === 'integer' && !Number.isSafeInteger(value)
131
+ || parameter.type === 'number' && typeof value !== 'number' || parameter.type === 'string' && typeof value !== 'string'
132
+ || parameter.type === 'boolean' && typeof value !== 'boolean' || parameter.min !== undefined && value < parameter.min
133
+ || parameter.max !== undefined && value > parameter.max || parameter.choices && !parameter.choices.includes(value)) {
134
+ throw new TypeError(parameterLabel(parameter) + '的类型、范围或选项不符。');
135
+ }
136
+ result[id] = value;
137
+ }
138
+ return parameters.length ? normalizeWorkflowValues(result) : {};
139
+ }
140
+ return { read, valid() { try { read(); return true; } catch { return false; } } };
141
+ }
142
+
143
+ export function inspectionMessage(report) {
144
+ return report.ok ? '静态检查通过 · ' + report.checkedClasses + ' 类节点 · 未执行生成'
145
+ : '检查未通过:' + report.issues.map(issue => issue.message).join(';');
146
+ }