@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-alpha.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 (160) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.en.md +103 -312
  3. package/README.md +100 -310
  4. package/bin/dsh-iris.js +333 -65
  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/AUDIO_FLOWS.md +71 -0
  9. package/docs/CONTACT_SHEET.md +62 -0
  10. package/docs/CORE_CONNECTIONS.md +99 -0
  11. package/docs/CORE_QUICKSTART.md +108 -0
  12. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  13. package/docs/CORE_SERVICE.md +97 -0
  14. package/docs/CORE_SKILLS.md +35 -3
  15. package/docs/DSH_COMPATIBILITY.md +10 -0
  16. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  17. package/docs/DSH_RUNTIME.md +6 -2
  18. package/docs/HEADLESS_CLI.md +2 -2
  19. package/docs/IMAGE_FLOW.md +4 -0
  20. package/docs/IMAGE_GENERATION.md +127 -0
  21. package/docs/LOCAL_API.md +4 -2
  22. package/docs/LOCAL_API_CONTRACT.md +39 -14
  23. package/docs/LOCAL_CREATION.md +7 -1
  24. package/docs/LOCAL_OPERATIONS.md +46 -0
  25. package/docs/LOCAL_SELECTION.md +57 -0
  26. package/docs/LOCAL_WEB.md +201 -0
  27. package/docs/MCP.md +191 -0
  28. package/docs/MUSIC.md +2 -0
  29. package/docs/PANEL_EXTENSIONS.md +64 -0
  30. package/docs/PLUGINS.md +189 -0
  31. package/docs/PLUGIN_STARTUP.md +100 -0
  32. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  33. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  34. package/docs/PROVIDER_HEALTH.md +3 -1
  35. package/docs/PROVIDER_SETTINGS.md +85 -0
  36. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  37. package/docs/ROADMAP.md +17 -7
  38. package/docs/VISION_CLI.md +1 -1
  39. package/docs/WEB_PROTOTYPE.md +21 -6
  40. package/docs/WEB_RECOVERY.md +46 -0
  41. package/docs/WORKBENCH_LOGIN.md +63 -0
  42. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  43. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  44. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  45. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  46. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  47. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  48. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  49. package/docs/releases/0.3.0-alpha.1.md +51 -0
  50. package/docs/screenshots.md +41 -1
  51. package/lib/actions.js +53 -2
  52. package/lib/adapters.js +20 -11
  53. package/lib/application-runtime.js +34 -7
  54. package/lib/audio-flow-input.js +37 -0
  55. package/lib/client.js +346 -53
  56. package/lib/comfyui-adapter.js +94 -0
  57. package/lib/comfyui-client.js +51 -0
  58. package/lib/comfyui-inspection.js +73 -0
  59. package/lib/comfyui-workflow.js +94 -0
  60. package/lib/config.js +36 -1
  61. package/lib/configured-image-runtime.js +50 -0
  62. package/lib/connection-skill.js +89 -0
  63. package/lib/connection-template.js +23 -0
  64. package/lib/core-artifact-store.js +16 -2
  65. package/lib/core-bridge-selection.js +31 -0
  66. package/lib/core-bridge-tools.js +83 -0
  67. package/lib/core-capability-catalog.js +182 -0
  68. package/lib/core-connection-diagnostics.js +49 -0
  69. package/lib/core-connection-store.js +277 -0
  70. package/lib/core-connections.js +225 -0
  71. package/lib/core-maintenance.js +9 -3
  72. package/lib/core-runtime.js +2 -1
  73. package/lib/core-service-client.js +170 -0
  74. package/lib/core-tasks.js +29 -12
  75. package/lib/core-writer-lease.js +16 -0
  76. package/lib/creation-request-store.js +38 -64
  77. package/lib/dsh-host-adapter.js +4 -1
  78. package/lib/dsh-service-bridge.js +66 -0
  79. package/lib/execution-ownership.js +43 -0
  80. package/lib/flow-store.js +105 -10
  81. package/lib/generation-input.js +10 -1
  82. package/lib/headless-vision.js +7 -7
  83. package/lib/image-edit-input.js +1 -1
  84. package/lib/image-flow.js +73 -29
  85. package/lib/index.js +37 -17
  86. package/lib/local-api-projection.js +10 -5
  87. package/lib/local-api-token-directory.js +31 -0
  88. package/lib/local-api.js +769 -67
  89. package/lib/local-creation.js +89 -24
  90. package/lib/local-operations.js +36 -0
  91. package/lib/local-request-root.js +86 -0
  92. package/lib/local-web-assets.js +37 -0
  93. package/lib/mcp-bridge.js +58 -0
  94. package/lib/mcp-check.js +26 -0
  95. package/lib/mcp-config.js +42 -0
  96. package/lib/mcp-delivery.js +46 -0
  97. package/lib/mcp-http.js +101 -0
  98. package/lib/mcp-server.js +47 -0
  99. package/lib/model-directory-service.js +16 -6
  100. package/lib/model-directory.js +3 -3
  101. package/lib/models.js +2 -2
  102. package/lib/music-transport.js +2 -1
  103. package/lib/operation-runner.js +236 -0
  104. package/lib/operation-store.js +216 -0
  105. package/lib/panel-extension.js +36 -0
  106. package/lib/plugin-contract.js +194 -0
  107. package/lib/plugin-execution.js +51 -0
  108. package/lib/plugin-launch.js +86 -0
  109. package/lib/plugin-operation-store.js +101 -0
  110. package/lib/plugin-operations.js +28 -0
  111. package/lib/plugin-registry.js +109 -0
  112. package/lib/plugin-selection.js +15 -0
  113. package/lib/plugin-source-image.js +5 -0
  114. package/lib/plugin-web-assets.js +54 -0
  115. package/lib/plugins.d.ts +288 -0
  116. package/lib/plugins.js +58 -0
  117. package/lib/provider-adapter.js +5 -1
  118. package/lib/provider-adapters.js +56 -18
  119. package/lib/provider-catalog.js +40 -29
  120. package/lib/provider-config-service.js +105 -23
  121. package/lib/provider-diagnostics.js +56 -0
  122. package/lib/provider-extensions.js +29 -0
  123. package/lib/provider-health.js +8 -1
  124. package/lib/provider-monitor.js +79 -0
  125. package/lib/provider-protocol.js +70 -20
  126. package/lib/provider-registry.js +118 -0
  127. package/lib/provider-response.js +28 -0
  128. package/lib/provider-settings.js +134 -0
  129. package/lib/provider-task-runner.js +22 -9
  130. package/lib/skill-installation.js +23 -11
  131. package/lib/vision-model-routing.js +3 -0
  132. package/lib/web/app.js +642 -0
  133. package/lib/web/appearance.js +111 -0
  134. package/lib/web/comfyui.js +146 -0
  135. package/lib/web/connections.js +377 -0
  136. package/lib/web/creation.js +355 -0
  137. package/lib/web/flow-view.js +33 -0
  138. package/lib/web/gallery.js +132 -0
  139. package/lib/web/index.html +291 -0
  140. package/lib/web/media-client.js +204 -0
  141. package/lib/web/plugin-panel.js +221 -0
  142. package/lib/web/plugin-surface.css +17 -0
  143. package/lib/web/providers.js +492 -0
  144. package/lib/web/recovery.js +105 -0
  145. package/lib/web/startup.js +18 -0
  146. package/lib/web/styles.css +518 -0
  147. package/lib/web/transport.js +96 -0
  148. package/lib/web/workbench-login.js +165 -0
  149. package/lib/workbench-auth-cli.js +47 -0
  150. package/lib/workbench-auth.js +200 -0
  151. package/package.json +20 -5
  152. package/plugins/contact-sheet/index.d.mts +4 -0
  153. package/plugins/contact-sheet/index.mjs +33 -0
  154. package/plugins/contact-sheet/web/board.css +14 -0
  155. package/plugins/contact-sheet/web/board.mjs +93 -0
  156. package/skills/iris-core/SKILL.md +8 -4
  157. package/skills/iris-core/references/cli.md +2 -2
  158. package/skills/iris-core/references/comfyui.md +69 -0
  159. package/skills/iris-core/references/tasks.md +2 -0
  160. package/user_guide.md +5 -1
@@ -0,0 +1,291 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN" data-auth="token" data-startup="pending">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <title>Iris · Core 工作台</title>
8
+ <link rel="icon" type="image/svg+xml" href="/iris-mark.svg">
9
+ <script src="/startup.js"></script>
10
+ <link rel="stylesheet" href="/styles.css">
11
+ <script type="module" src="/app.js"></script>
12
+ </head>
13
+ <body>
14
+ <div class="startup-screen" role="status" aria-live="polite">
15
+ <div class="startup-brand" aria-hidden="true">Iris<span>.</span></div>
16
+ <p class="startup-message">正在打开工作区…</p>
17
+ <div class="startup-failure"><p>工作台未能载入,请重新加载。</p><a href="/">重新加载</a></div>
18
+ <noscript><p>请启用 JavaScript 后重新加载工作台。</p></noscript>
19
+ </div>
20
+ <a class="skip-link" href="#main-content">跳到工作区</a>
21
+ <div class="app-shell">
22
+ <aside id="app-sidebar" class="app-sidebar" hidden>
23
+ <a class="brand" href="#overview" aria-label="Iris 工作台总览"><img src="/iris-mark.svg" alt="" width="32" height="32"><span>Iris<span class="brand-dot">.</span></span></a>
24
+ <p class="sidebar-label">你的工作区</p>
25
+ <nav id="workspace-nav" class="workspace-nav" aria-label="工作台" hidden>
26
+ <button id="overview-tab" class="nav-item" type="button" aria-current="false"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="2"/><rect x="14" y="3" width="7" height="7" rx="2"/><rect x="3" y="14" width="7" height="7" rx="2"/><rect x="14" y="14" width="7" height="7" rx="2"/></svg> 总览</button>
27
+ <button id="studio-tab" class="nav-item" type="button" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 4 5 5M4 20l4-1L20 7a2.8 2.8 0 0 0-4-4L4 15z"/></svg> 创作</button>
28
+ <button id="media-tab" class="nav-item" type="button" aria-current="false"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="m3 16 5-5 4 4 3-3 6 6"/><circle cx="16" cy="8" r="1"/></svg> 作品</button>
29
+ <button id="connections-tab" class="nav-item" type="button" aria-current="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 21h8m-4-5v5m-4-13 2 2-2 2m5 0h3"/></svg> 客户端 <span id="connections-count" class="count">0</span></button>
30
+ <button id="providers-tab" class="nav-item" type="button" aria-current="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="3" width="16" height="7" rx="2"/><rect x="4" y="14" width="16" height="7" rx="2"/><path d="M8 6.5h.01M8 17.5h.01M12 6.5h4m-4 11h4"/></svg> 模型服务</button>
31
+ <div id="plugin-navigation" class="plugin-navigation" role="group" aria-label="已启用扩展" hidden></div>
32
+ </nav>
33
+ <details id="recent-panel" class="recent-panel" open><summary><span>最近流程 <span id="history-count" class="count">0</span></span><span class="disclosure">收起</span></summary>
34
+ <p id="history-empty" class="muted" hidden>还没有流程。你的第一次创作会保存在这里。</p>
35
+ <nav id="history-list" aria-label="已保存的创作流程"></nav>
36
+ <button id="load-more" class="quiet full" type="button" hidden>加载更多</button>
37
+ </details>
38
+ <div class="sidebar-bottom"><span class="workspace-avatar" aria-hidden="true">i</span><span>本地工作区<small>作品与任务保存在 Core</small></span></div>
39
+ </aside>
40
+ <div class="main-area">
41
+ <header class="site-header">
42
+ <span class="breadcrumb">工作区 <span aria-hidden="true">/</span> <strong id="page-label">Iris Core</strong></span>
43
+ <div class="header-actions"><span id="mode-badge" class="badge">只读预览</span><button id="appearance-open" class="quiet" type="button">外观</button><button id="disconnect" class="quiet" type="button" hidden>退出登录</button></div>
44
+ </header>
45
+ <main id="main-content" tabindex="-1">
46
+ <section id="connection" class="connection card" aria-labelledby="connection-title">
47
+ <p class="eyebrow">YOUR LOCAL WORKSPACE</p>
48
+ <h1 id="connection-title">接回你的创作</h1>
49
+ <p class="intro">让想法成为作品。连接本地工作区,继续创作或接回已保存的作品。</p>
50
+ <div id="password-login" hidden>
51
+ <p id="password-hint" class="muted">正在读取工作台登录状态…</p>
52
+ <form id="password-form" autocomplete="on">
53
+ <input name="username" value="Iris" autocomplete="username" hidden>
54
+ <label for="password">访问口令</label>
55
+ <div class="password-row"><input id="password" name="password" type="password" autocomplete="current-password" minlength="8" maxlength="128" required spellcheck="false" autocapitalize="none"><button id="password-show" class="quiet" type="button" aria-pressed="false" aria-controls="password">显示</button></div>
56
+ <div id="password-confirm-row" hidden><label for="password-confirm">再次输入口令</label><input id="password-confirm" type="password" autocomplete="new-password" minlength="8" maxlength="128" spellcheck="false" autocapitalize="none"></div>
57
+ <label class="password-remember" for="password-remember"><input id="password-remember" type="checkbox" checked><span>记住此设备 <small>30 天</small></span></label>
58
+ <button id="password-submit" class="full" type="submit" disabled>进入工作台</button>
59
+ </form>
60
+ <button id="password-retry" class="quiet" type="button" hidden>重新读取登录状态</button>
61
+ <details class="password-help"><summary><span>忘记口令</span><span class="disclosure">展开</span></summary><p>可在运行 Iris 的终端重置工作台口令。重置会注销工作台登录,作品、任务和 MCP 连接会保留。</p><code>dsh-iris web password reset --data-root 你的工作区路径</code></details>
62
+ </div>
63
+ <p id="login-help" class="muted">点击启动终端给出的登录链接,即可进入。登录后刷新可继续;链接失效时,可在终端重新获取。</p>
64
+ <details id="paste-panel"><summary><span>备用登录</span><span class="disclosure">展开</span></summary>
65
+ <label class="file-button" for="token-file">选择 Token 文件<input id="token-file" type="file" aria-describedby="token-help"></label>
66
+ <p id="token-help" class="muted">使用本次启动生成的文件,或粘贴其中的 Token。退出会清除当前标签页的登录,服务重启后需要重新登录。</p>
67
+ <form id="connect-form">
68
+ <label for="token">会话 Token</label>
69
+ <div class="token-row"><input id="token" type="password" autocomplete="off" spellcheck="false" maxlength="128" placeholder="粘贴启动时生成的 Token"><button id="connect" type="submit">连接</button></div>
70
+ </form>
71
+ </details>
72
+ <p class="scope-note">连接与浏览不会提交媒体任务。创作和作品选择按服务已开启的权限提供。</p>
73
+ </section>
74
+ <p id="feedback" role="status" aria-live="polite" hidden></p>
75
+ <p id="session-notice" class="field-note" role="status" aria-live="polite" hidden></p>
76
+ <section id="overview-panel" class="overview-panel" aria-labelledby="overview-title" hidden>
77
+ <div class="overview-welcome card">
78
+ <div><p class="eyebrow">IRIS WORKSPACE</p><h1 id="overview-title">你的多模态工作区</h1><p class="muted">从一个想法开始,让作品在不同工具之间接续。</p></div>
79
+ <div class="overview-welcome-actions"><button id="overview-create" type="button">开始创作</button><button id="overview-gallery" class="secondary" type="button">浏览作品</button></div>
80
+ </div>
81
+ <dl id="overview-stats" class="overview-stats" aria-label="当前工作区"></dl>
82
+ <div class="overview-section-heading"><div><h2>创作能力</h2><p class="muted">入口、配置与实测记录分别呈现。</p></div><button id="overview-refresh" class="quiet" type="button">刷新状态</button></div>
83
+ <p id="overview-feedback" class="field-note" role="status" hidden></p>
84
+ <div id="overview-capabilities" class="overview-capabilities"></div>
85
+ <details id="overview-tools" class="overview-tools card"><summary><span>更多 Core 工具 <small>现有入口与覆盖范围</small></span><span class="disclosure">展开</span></summary><div class="overview-tool-grid">
86
+ <article><h3>裁剪与图片差异</h3><p>CLI 可裁剪图片、保存差异热力图;创作页已有图片比较,独立工具面板待接入。</p><code>run crop · media diff</code></article>
87
+ <article><h3>视频抽帧</h3><p>CLI 可提取视频帧并保存为作品,需系统 ffmpeg / ffprobe;面板与 MCP 待接入。</p><code>media frames</code></article>
88
+ <article><h3>HTML 截图</h3><p>CLI 使用显式指定的 Chromium,DSH 使用宿主浏览器;Core 面板与 MCP 待接入。</p><code>media html</code></article>
89
+ <article><h3>提示词优化与组装</h3><p>共享核心与 DSH 泡泡已有实现,需对应文本模型服务;独立 Core 面板与 MCP 待接入。</p></article>
90
+ </div></details>
91
+ <details id="overview-notes" class="overview-notes"><summary><span>这些状态如何判断</span><span class="disclosure">展开</span></summary><p>模型候选来自当前工作区配置,标注用途不等于接口可调用。历史实测仅表示记录时的结果;打开或刷新总览不会测试模型、同步远端目录或提交媒体任务。</p><p>作品页展示当前工作区的流程及其关联作品,不会自动汇入其他数据根或旧 DSH 工作区。</p></details>
92
+ </section>
93
+ <details id="recovery-panel" class="card compact-details" hidden>
94
+ <summary><span>操作核对 <small id="recovery-summary"></small></span><span class="disclosure">展开</span></summary>
95
+ <p id="recovery-note" class="muted" role="status" aria-live="polite"></p>
96
+ <ul id="recovery-list"></ul>
97
+ <button id="recovery-check" class="quiet" type="button">重新核对</button>
98
+ <button id="recovery-clear" class="quiet" type="button" hidden>处理本地标识</button>
99
+ <div id="recovery-clear-confirm" hidden><p class="field-note">清除后,本标签页将不再按这些标识核对未知操作。原流程和任务仍保留,也不会停止执行。请先核对原结果;确定清除本地标识吗?</p><button id="recovery-clear-yes" class="secondary danger" type="button">确认清除本地标识</button><button id="recovery-clear-no" class="quiet" type="button">返回</button></div>
100
+ </details>
101
+ <p id="plugin-feedback" class="field-note" role="status" aria-live="polite" hidden></p>
102
+ <section id="plugin-page-panel" hidden aria-labelledby="plugin-page-title">
103
+ <div class="page-heading"><div><p class="eyebrow">WORKSPACE EXTENSION</p><h1 id="plugin-page-title"></h1><p id="plugin-page-identity" class="muted"></p></div></div>
104
+ <div id="plugin-page-slot" class="card plugin-slot"></div>
105
+ </section>
106
+ <section id="plugin-history-panel" hidden aria-labelledby="plugin-history-title">
107
+ <div class="page-heading"><div><p class="eyebrow">SAVED OPERATIONS</p><h1 id="plugin-history-title">业务记录</h1><p class="muted">记录与作品由 Core 保存,插件移除后仍可查看。</p></div><button id="plugin-history-refresh" class="quiet" type="button">重新读取</button></div>
108
+ <p id="plugin-history-feedback" class="field-note" role="status"></p><div id="plugin-history-list"></div>
109
+ </section>
110
+ <section id="plugin-settings-panel" hidden aria-labelledby="plugin-settings-title">
111
+ <div class="page-heading"><div><p class="eyebrow">EXTENSION SETTINGS</p><h1 id="plugin-settings-title">扩展设置</h1><p class="muted">展开需要的表单,保存前请核对修改。</p></div></div>
112
+ <div id="plugin-settings-list"></div>
113
+ </section>
114
+ <section id="gallery-panel" hidden aria-labelledby="gallery-title">
115
+ <div class="page-heading"><div><p class="eyebrow">YOUR COLLECTION</p><h1 id="gallery-title">灵感,留在这里。</h1><p class="muted">每一次选择,都可以回到原来的创作。</p></div><button id="gallery-new" class="secondary" type="button">+ 新建流程</button></div>
116
+ <div id="gallery-types" class="view-switch gallery-types" aria-label="作品类别" hidden><button type="button" data-gallery-kind="image" aria-pressed="true">图片</button><button type="button" data-gallery-kind="speech" aria-pressed="false">语音</button><button type="button" data-gallery-kind="transcribe" aria-pressed="false">转写</button></div>
117
+ <div class="gallery-toolbar"><div id="gallery-filters" class="view-switch" aria-label="作品筛选"><button type="button" data-filter="all" aria-pressed="true">全部 <span>0</span></button><button type="button" data-filter="selected" aria-pressed="false">已选择 <span>0</span></button><button type="button" data-filter="unselected" aria-pressed="false">未选择 <span>0</span></button></div><button id="gallery-refresh" class="quiet" type="button">刷新</button></div>
118
+ <p id="gallery-feedback" class="muted" role="status" aria-live="polite"></p>
119
+ <div id="gallery-grid" class="gallery-grid" aria-busy="false"></div>
120
+ <div id="gallery-empty" class="empty-collection" hidden><span aria-hidden="true">✧</span><h2>留一点空间给下一次灵感</h2><p class="muted">这里还没有符合筛选的流程。已有作品和选择仍保留在原工作区。</p></div>
121
+ <button id="gallery-more" class="secondary" type="button" hidden>加载更多作品</button>
122
+ </section>
123
+ <section id="connections-panel" hidden aria-label="连接中心">
124
+ <div class="page-heading management-heading"><div><h1>客户端连接</h1><p class="muted">让你的编程助手,通过 MCP 使用 Iris。</p></div><div class="page-actions"><button id="connections-refresh" class="quiet" type="button">刷新</button><button id="connection-new" class="secondary" type="button">+ 新建连接</button></div></div>
125
+ <p id="connections-feedback" role="status" aria-live="polite" hidden></p>
126
+ <div class="connections-workspace">
127
+ <div id="connections-overview"><div id="connections-list" class="service-grid"></div><div id="connections-empty" class="management-empty"><span class="empty-mark" aria-hidden="true">MCP</span><h2>连接你的第一个客户端</h2><p>自由命名,选择要开放的能力,再把配置交给客户端。</p></div>
128
+ <details id="connection-help" class="management-help"><summary><span>如何接入 MCP?</span><span class="disclosure">展开</span></summary><div class="connection-guide" aria-label="MCP 接入步骤"><ol><li><strong>1 · 创建连接</strong><span>命名并选择功能、权限</span></li><li><strong>2 · 生成配置</strong><span>选择客户端支持的接入方式</span></li><li><strong>3 · 客户端启用</strong><span>导入 MCP 配置,连接后刷新这里</span></li></ol><p id="connection-service-note" class="field-note"></p></div></details>
129
+ <details id="connection-contributions" class="management-help" hidden><summary><span>已加载的插件贡献</span><span class="disclosure">展开</span></summary><p class="field-note">加载与查看不会授予权限。以下列出声明与接入状态,客户端仍只能使用你明确选择的工具。</p><ul id="connection-contribution-list"></ul></details>
130
+ </div>
131
+ <section id="connection-editor" class="card connection-editor management-editor" hidden aria-labelledby="connection-editor-title">
132
+ <div class="editor-heading"><button id="connection-back" class="quiet back-button" type="button">← 返回列表</button><div class="section-heading"><h2 id="connection-editor-title">新建连接</h2><span id="connection-state" class="badge">尚未保存</span></div></div>
133
+ <div id="connection-leave-confirm" class="inline-confirm" hidden><p>放弃未保存的连接修改并返回列表?</p><button id="connection-leave-discard" type="button" class="secondary">放弃修改</button><button id="connection-leave-keep" type="button" class="quiet">继续编辑</button></div>
134
+ <nav class="editor-tabs" aria-label="连接设置分组"><button type="button" data-connection-view="settings" aria-pressed="true">功能与权限</button><button type="button" data-connection-view="delivery" aria-pressed="false">接入配置</button><button type="button" data-connection-view="details" aria-pressed="false">详情与管理</button></nav>
135
+ <div id="connection-view-settings">
136
+ <form id="connection-form">
137
+ <fieldset id="connection-fields">
138
+ <label for="connection-name">连接名称</label><input id="connection-name" maxlength="80" required placeholder="例如:我的编程助手 · 工作项目" autocomplete="off">
139
+ <h3>媒体功能</h3><div id="connection-media" class="media-options"></div><div id="connection-image-options" class="scope-options"></div><details id="connection-future" class="tool-details"><summary><span>更多功能 <small>尚未接入</small></span><span class="disclosure">展开</span></summary><div id="connection-media-unavailable" class="media-options"></div></details>
140
+ <div id="connection-plugin-section" hidden><h3>插件业务</h3><div id="connection-plugin-options" class="scope-options"></div><p class="field-note">默认不开放。选择具体业务后,再选择允许的操作权限;安装插件不会自动授权。</p></div>
141
+ <h3>操作权限</h3><div id="connection-scopes" class="scope-options"></div><p class="field-note">只读可浏览记录;插件业务还需允许执行。绘图还需开放“导入与创建”和“执行流程”。执行可能产生供应商费用;停止只结束本地等待。</p>
142
+ <details id="connection-tool-details" class="tool-details"><summary><span>工具细选 <small id="connection-tool-count"></small></span><span class="disclosure">展开</span></summary><div id="connection-tools"></div></details>
143
+ <div class="connection-form-actions"><button id="connection-save" type="submit">创建连接</button><span id="connection-save-note" class="muted">创建只保存配置,不调用模型。</span></div>
144
+ </fieldset>
145
+ </form>
146
+ </div>
147
+ <div id="connection-view-delivery" hidden>
148
+ <section id="connection-export" class="connection-section" hidden aria-labelledby="export-title">
149
+ <h3 id="export-title">接入配置</h3><p class="field-note">按已保存的授权生成。把配置加入客户端,重新连接后在这里刷新。</p>
150
+ <label class="template-choice" for="connection-template">接入模板<select id="connection-template"><option value="">手动选择</option></select></label>
151
+ <details class="tool-details"><summary><span>导入模板</span><span class="disclosure">展开</span></summary><p class="field-note">模板只选择传输与配置格式,不增加权限、不执行命令,也不查找或改写客户端文件。其他客户端可手动映射单条 entry。</p><div class="export-actions"><button id="connection-template-example" class="quiet" type="button">下载示例</button><button id="connection-template-import" class="quiet" type="button">导入 JSON</button><input id="connection-template-file" type="file" accept="application/json,.json" hidden></div></details>
152
+ <p id="connection-template-note" class="field-note" hidden></p>
153
+ <div class="export-options"><label>传输<select id="connection-transport"><option value="stdio">stdio · 本机</option><option value="http">HTTP · 本机</option></select></label><label>格式<select id="connection-format"><option value="mcpServers">mcpServers</option><option value="entry">单条 entry</option><option value="claude-json">Claude Code · JSON</option><option value="codex-toml">Codex · TOML</option></select></label><button id="connection-export-button" class="secondary" type="button">生成配置</button></div>
154
+ <p id="connection-transport-note" class="field-note"></p>
155
+ <p class="field-note">按所选客户端合并 JSON 或 TOML,保留已有配置,然后在客户端启用。其他客户端也可手动填写单条 entry。</p>
156
+ <div id="connection-export-result" hidden><p id="connection-export-note" class="field-note"></p><textarea id="connection-configuration" readonly spellcheck="false" rows="7" aria-label="MCP 接入配置"></textarea><div class="export-actions"><button id="connection-copy" class="quiet" type="button">复制</button><button id="connection-download" class="quiet" type="button">下载 JSON</button><button id="connection-clear-export" class="quiet" type="button">隐藏配置</button></div></div>
157
+ <details id="connection-skill-details" class="tool-details"><summary><span>连接专属 Skill</span><span class="disclosure">展开</span></summary><p class="field-note">英文说明附中文标题,仅包含当前有效工具。下载文件不含密钥;权限改变后请重新生成并安装。</p><button id="connection-skill-generate" class="secondary" type="button">生成说明</button><div id="connection-skill-result" hidden><textarea id="connection-skill-preview" readonly spellcheck="false" rows="8" aria-label="Skill 说明预览"></textarea><p class="field-note">下载后,在本机明确指定客户端的 Skills 目录安装:</p><code class="skill-install-command">dsh-iris skills install --bundle /absolute/path/iris-skill.json --directory /absolute/path/skills</code><div class="export-actions"><button id="connection-skill-download" class="quiet" type="button">下载 Skill 文件</button><button id="connection-skill-clear" class="quiet" type="button">隐藏说明</button></div></div></details>
158
+ </section>
159
+ </div>
160
+ <div id="connection-view-details" hidden><dl id="connection-facts" hidden></dl>
161
+ <section id="connection-revoke-section" class="connection-section" hidden>
162
+ <div class="export-actions"><button id="connection-revoke" class="quiet danger" type="button">撤销连接</button><button id="connection-delete" class="quiet danger" type="button">删除连接</button></div>
163
+ <div id="connection-revoke-confirm" hidden><p>撤销后,已导出的配置立即失效。已接收的操作继续保留;不会取消供应商任务或保证停止计费。</p><div class="export-actions"><button id="connection-revoke-yes" class="secondary danger" type="button">确认撤销</button><button id="connection-revoke-no" class="quiet" type="button">返回</button></div></div>
164
+ <div id="connection-delete-confirm" hidden><p>删除后,连接将从列表移除,旧配置失效。作品、任务和已接收的操作保留;不会取消供应商任务。Iris 保留私有撤销档案,不会修改客户端的 MCP 配置。</p><div class="export-actions"><button id="connection-delete-yes" class="secondary danger" type="button">确认删除</button><button id="connection-delete-no" class="quiet" type="button">返回</button></div></div>
165
+ </section>
166
+ </div>
167
+ </section>
168
+ </div>
169
+ </section>
170
+ <section id="providers-panel" hidden aria-labelledby="providers-title">
171
+ <div class="page-heading management-heading"><div><h1 id="providers-title">模型服务 <span id="providers-count" class="count">0</span></h1><p class="muted">连接 API 或 ComfyUI,为创作准备好模型。</p></div><div class="page-actions"><button id="providers-refresh" class="quiet" type="button">重新读取</button><button id="provider-new" class="secondary" type="button">+ 添加服务</button></div></div>
172
+ <p id="providers-feedback" role="status" aria-live="polite" hidden></p>
173
+ <nav id="provider-sections" class="editor-tabs provider-sections" aria-label="模型服务分组"><button type="button" data-provider-section="suppliers" aria-pressed="true">供应商</button><button type="button" data-provider-section="pools" aria-pressed="false">功能模型池</button><button type="button" data-provider-section="health" aria-pressed="false">健康检测</button></nav>
174
+ <div id="provider-close-confirm" class="inline-confirm" hidden><p>放弃未保存修改并离开?</p><div class="export-actions"><button id="provider-close-discard" class="secondary" type="button">放弃修改</button><button id="provider-close-cancel" class="quiet" type="button">继续编辑</button></div></div>
175
+ <div id="providers-reload-confirm" class="card reload-confirm" hidden><p>重新读取将放弃当前未保存编辑,并载入文件中的最新设置。</p><button id="providers-reload-yes" class="secondary" type="button">确认重新读取</button><button id="providers-reload-no" class="quiet" type="button">保留草稿</button></div>
176
+ <section id="provider-pool-panel" class="card provider-pool-panel" hidden aria-label="功能模型池"><p class="field-note">自动按已确认用途选择;手动按优先顺序;停用停止新调用。池变化只影响新请求。</p><form id="provider-pool-form"><fieldset id="provider-pool-fields"><div id="provider-assignments"></div><div class="provider-footer"><button id="provider-pool-save" type="submit">保存模型池</button><button id="provider-pool-cancel" class="quiet" type="button">取消更改</button><p id="provider-pool-note" class="field-note"></p></div></fieldset></form></section>
177
+ <section id="provider-health-panel" hidden aria-label="能力健康与连接检查"><div class="section-heading"><h2>能力光谱</h2><button id="provider-health-refresh" class="quiet" type="button">刷新状态</button></div><p class="field-note">候选、近期成功与当前受阻分别呈现。刷新只读本地记录。</p><div id="provider-spectrum" class="provider-spectrum"></div><details id="provider-health-details" class="card provider-health-details"><summary><span id="provider-health-title">能力详情</span><span class="disclosure">展开</span></summary><div id="provider-health-models"></div></details><h2 class="provider-check-heading">连接检查与保活</h2><p class="field-note">只读取目录或 ComfyUI 服务状态,不生成媒体;连接可达不等于模型可用。保活默认关闭,仅本次 Core 服务生效。</p><div id="provider-monitors" class="service-grid"></div></section>
178
+ <div class="providers-workspace">
179
+ <div id="providers-overview"><div id="providers-list" class="service-grid"></div><div id="providers-empty" class="management-empty"><span class="empty-mark" aria-hidden="true">API</span><h2>从一个模型服务开始</h2><p>使用自己的 API,或连接已经运行的 ComfyUI。</p></div></div>
180
+ <section id="provider-editor" class="card provider-editor management-editor" hidden aria-labelledby="provider-editor-title">
181
+ <div class="editor-heading"><button id="provider-back" class="quiet back-button" type="button">← 返回列表</button><div class="section-heading"><div class="provider-heading"><h2 id="provider-editor-title">添加供应商</h2><span id="provider-comfy-mode" class="provider-mode-pill" hidden><span aria-hidden="true">✦</span> ComfyUI Studio</span></div><span id="provider-state" class="badge">未保存</span></div></div>
182
+ <nav class="editor-tabs" aria-label="模型服务设置分组"><button type="button" data-provider-view="connection" aria-pressed="true">连接</button><button type="button" data-provider-view="models" aria-pressed="false">模型与用途</button><button type="button" data-provider-view="workflow" aria-pressed="false" hidden>ComfyUI 工作流</button></nav>
183
+ <form id="provider-form"><fieldset id="provider-fields">
184
+ <section id="provider-view-connection">
185
+ <label class="field-label" for="provider-preset">快速填写</label><select id="provider-preset"><option value="custom">自定义兼容服务</option><option value="openai">OpenAI · Images</option><option value="dashscope">百炼 · DashScope</option><option value="comfyui">ComfyUI · 已有服务</option></select><p id="provider-preset-note" class="field-note">按服务文档填写;编程订阅或聊天登录不等于媒体 API 认证。</p>
186
+ <div class="provider-two-fields"><div><label class="field-label" for="provider-name">名称</label><input id="provider-name" required maxlength="80" placeholder="例如:我的媒体服务" autocomplete="off"></div><div><label class="field-label" for="provider-auth">认证方式</label><select id="provider-auth"><option value="bearer">API Key</option><option value="none">无需认证</option></select></div></div>
187
+ <label class="field-label" for="provider-url">Base URL</label><input id="provider-url" type="url" required maxlength="2048" placeholder="https://example.com/v1" autocomplete="off">
188
+ <div class="provider-two-fields"><div><label class="field-label" for="provider-key-action">密钥操作 <small id="provider-key-state"></small></label><select id="provider-key-action"><option value="keep">保留已有密钥</option><option value="replace">填写 / 替换密钥</option><option value="clear">清除密钥</option></select></div><div id="provider-key-field" hidden><label class="field-label" for="provider-key">新 API Key</label><input id="provider-key" type="password" autocomplete="new-password" maxlength="8192" spellcheck="false" placeholder="仅填写新密钥"></div></div>
189
+ <label class="provider-enabled"><input id="provider-enabled" type="checkbox" checked> 启用此供应商</label>
190
+ <details id="provider-advanced" class="tool-details"><summary><span>高级连接设置</span><span class="disclosure">展开</span></summary><label class="field-label" for="provider-protocol">默认媒体协议</label><select id="provider-protocol"></select><label class="field-label" for="provider-media-url">媒体专用 Base URL(可选)</label><input id="provider-media-url" type="url" maxlength="2048" placeholder="留空沿用 Base URL"><p id="provider-id-note" class="field-note"></p></details>
191
+ </section>
192
+ <section id="provider-view-workflow" hidden>
193
+ <details id="comfyui-tools" class="tool-details" hidden>
194
+ <summary><span>ComfyUI 工作流 <small>文生图</small></span><span class="disclosure">展开</span></summary>
195
+ <p class="field-note">导入 → 确认节点 → 开放参数 → 预览。连接已有服务,一次接收一张图片。</p>
196
+ <div class="export-actions"><a id="comfyui-open" class="quiet" target="_blank" rel="noopener noreferrer" hidden>打开 ComfyUI ↗</a><label class="file-button secondary" for="comfyui-file">导入 API 工作流<input id="comfyui-file" type="file" accept="application/json,.json"></label></div>
197
+ <p id="comfyui-note" class="field-note" role="status">最多 40 KiB。支持 API 图和完整 Iris v0/v1 模板,导入不执行生成。</p>
198
+ <div id="comfyui-bindings" hidden>
199
+ <label class="field-label" for="comfyui-model">工作流名称 / 模型 ID</label><input id="comfyui-model" maxlength="120" placeholder="例如:my-sdxl">
200
+ <details class="comfy-group"><summary><span>输入与输出节点</span><span class="disclosure">展开</span></summary>
201
+ <label class="field-label" for="comfyui-prompt-node">正向提示词节点</label><select id="comfyui-prompt-node"></select>
202
+ <label class="field-label" for="comfyui-output-node">图片输出节点</label><select id="comfyui-output-node"></select>
203
+ <label class="field-label" for="comfyui-size-node">尺寸节点(可选)</label><select id="comfyui-size-node"></select>
204
+ </details>
205
+ <details id="comfyui-exposed" class="comfy-group"><summary><span>开放参数 <small id="comfyui-exposed-count">0 / 32</small></span><span class="disclosure">展开</span></summary><p class="field-note">勾选已有输入并命名。LoRA 模型与 CLIP 权重分别开放;不添加节点或安装模型。</p><div id="comfyui-slots"></div></details>
206
+ <section class="comfy-preview" aria-label="工作流表单预览"><div class="section-heading"><h3>使用时的表单</h3><span id="comfyui-preview-count" class="badge">固定工作流</span></div><div id="comfyui-preview"></div></section>
207
+ <p class="field-note">只读检查使用已保存连接;检查不保存、不入队。未开放的输入沿用模板,输出节点须恰好产生一张图。</p>
208
+ <div class="export-actions"><button id="comfyui-check" class="secondary" type="button">只读检查</button><button id="comfyui-add" class="secondary" type="button">加入模型草稿</button><button id="comfyui-discard" class="quiet" type="button">放弃导入</button></div>
209
+ <div id="comfyui-replace" class="comfy-confirm" role="group" aria-label="确认替换工作流" hidden><p>同名工作流已存在。替换后新调用使用新模板,旧任务保持原绑定。</p><div class="export-actions"><button id="comfyui-replace-yes" type="button">确认替换草稿</button><button id="comfyui-replace-no" class="quiet" type="button">取消</button></div></div>
210
+ </div>
211
+ </details>
212
+ </section>
213
+ <section id="provider-view-models" hidden>
214
+ <div class="section-heading provider-section"><h3>模型与用途</h3><button id="provider-discover" class="quiet" type="button">读取模型目录</button></div><p id="provider-directory-note" class="field-note">先保存连接,再读取目录;目录成功不代表媒体实测通过。</p>
215
+ <div class="provider-model-add"><input id="provider-model-id" maxlength="512" placeholder="手填模型 ID" aria-label="手填模型 ID" autocomplete="off"><button id="provider-model-add" class="secondary" type="button">添加</button></div>
216
+ <div id="provider-directory" hidden><input id="provider-directory-search" type="search" placeholder="搜索目录" aria-label="搜索模型目录"><div id="provider-directory-list"></div><p class="field-note">添加后请明确勾选用途。目录只提供参考,不自动批准调用。</p></div>
217
+ <p class="field-note">确认用途后可加入自动池;在“功能模型池”统一调整策略。</p><div id="provider-models"></div><p id="provider-models-empty" class="field-note">也可以先仅保存连接,稍后添加模型。</p>
218
+ </section>
219
+ <div class="provider-save-area"><p id="provider-save-summary" class="field-note"></p><div class="provider-footer"><button id="provider-save" type="submit">保存设置</button><button id="provider-cancel" class="quiet" type="button">取消编辑</button></div></div>
220
+ </fieldset></form>
221
+ <details id="provider-management-tools" class="tool-details"><summary><span>供应商管理</span><span class="disclosure">展开</span></summary><p id="provider-binding-warning" class="field-note">停用可恢复;修改连接或删除可能影响旧任务恢复,不会取消远端任务或退款。删除仅检查当前 Core 工作区的依赖。</p><div class="export-actions"><button id="provider-toggle" class="secondary" type="button">停用供应商</button><button id="provider-delete" class="quiet danger" type="button">删除供应商</button></div><div id="provider-delete-confirm" class="inline-confirm" hidden><p id="provider-delete-impact"></p><button id="provider-delete-yes" class="secondary danger" type="button">确认删除</button><button id="provider-delete-no" class="quiet" type="button">取消</button></div></details>
222
+ </section>
223
+ </div>
224
+ </section>
225
+ <section id="workspace" hidden aria-labelledby="studio-title">
226
+ <div class="page-heading"><div><p class="eyebrow">A LITTLE ROOM TO CREATE</p><h1 id="studio-title">让想法,有画面。</h1><p id="studio-note" class="muted">保留喜欢的部分,试一点新的可能。</p></div><button id="new-run" class="secondary" type="button">+ 新建流程</button></div>
227
+ <div class="studio-grid">
228
+ <details id="creation-settings" class="card composer" open>
229
+ <summary><span>创作设置 <small id="input-summary">从一张图片开始</small></span><span class="disclosure">收起</span></summary>
230
+ <form id="edit-form">
231
+ <fieldset id="draft-fields">
232
+ <div id="creation-modes" class="view-switch creation-modes" aria-label="创作方式"><button type="button" data-creation-mode="generate" aria-pressed="false">生成图片</button><button type="button" data-creation-mode="edit" aria-pressed="true">修改图片</button><button type="button" data-creation-mode="tts" aria-pressed="false" hidden>语音合成</button><button type="button" data-creation-mode="transcribe" aria-pressed="false" hidden>音频转写</button></div>
233
+ <div id="source-input">
234
+ <label for="image-file" class="field-label">原图</label>
235
+ <label id="image-drop" class="image-drop" for="image-file"><img id="input-preview" alt="待创作的原图" hidden><span><strong id="file-name">选择一张图片</strong><small>点击或拖入 PNG / JPEG / WebP</small></span><span class="upload-arrow" aria-hidden="true">↗</span><input id="image-file" type="file" accept="image/png,image/jpeg,image/webp" aria-describedby="image-help"></label>
236
+ <p id="image-help" class="field-note">仅支持静态图片,最大 20 MiB。</p>
237
+ </div>
238
+ <div id="audio-source-input" hidden><label for="audio-file" class="field-label">原音频</label><label class="file-button secondary" for="audio-file">选择音频<input id="audio-file" type="file" accept="audio/wav,audio/x-wav,audio/mpeg,audio/ogg,audio/flac,.wav,.mp3,.ogg,.flac"></label><p id="audio-file-name" class="field-note">WAV / MP3 / OGG / FLAC,最大20 MiB。</p><audio id="audio-input-preview" controls preload="metadata" hidden></audio></div>
239
+ <label id="model-label" for="model" class="field-label">图片模型 <small>配置候选</small></label><select id="model" required><option value="">读取模型…</option></select>
240
+ <label id="prompt-label" for="edit-prompt" class="field-label">想改些什么?</label><textarea id="edit-prompt" rows="4" maxlength="12000" required placeholder="描述想保留的细节,以及想尝试的变化…"></textarea>
241
+ <details id="speech-options" class="tool-details" hidden><summary><span>音色(可选)</span><span class="disclosure">展开</span></summary><label for="speech-voice" class="field-label">模型支持的音色标识</label><input id="speech-voice" maxlength="64" placeholder="留空沿用协议默认值" autocomplete="off"><p class="field-note">按所选模型的要求填写,不同模型的音色名称可能不同。</p></details>
242
+ <section id="comfy-creation" class="comfy-studio" aria-label="ComfyUI 工作流参数" hidden><div class="section-heading"><h3>工作流参数</h3><span class="badge">ComfyUI</span></div><p id="comfy-creation-summary" class="field-note"></p><div id="comfy-creation-fields"></div><button id="comfy-creation-check" type="button" class="quiet inline-button">检查参数并读取选项</button><p id="comfy-creation-note" class="field-note" role="status">尚未检查,不会自动生成。</p></section>
243
+ <details id="advanced-settings" class="tool-details"><summary><span>高级参数</span><span class="disclosure">展开</span></summary><label for="edit-size" class="field-label">输出尺寸(可选)</label><input id="edit-size" maxlength="32" placeholder="使用模型默认值" autocomplete="off"><p id="edit-size-note" class="field-note">填写模型支持的尺寸;留空使用默认值。</p></details>
244
+ </fieldset>
245
+ <div class="model-management"><button id="manage-models" class="quiet inline-button" type="button">管理模型 ↗</button></div>
246
+ <button id="start-edit" class="full" type="submit" disabled>开始改图</button>
247
+ <p id="creation-help" class="field-note">读取权限中…</p>
248
+ <p id="creation-feedback" role="status" aria-live="polite" hidden></p>
249
+ </form>
250
+ <details id="model-panel" class="compact-details"><summary><span>模型与配置 <small id="model-summary">读取中</small></span><span class="disclosure">展开</span></summary><p id="model-note" class="muted"></p><ul id="model-list"></ul></details>
251
+ <p id="locked-note" class="composer-note" hidden>此流程的输入已固定。想换个要求,可以新建流程。</p>
252
+ </details>
253
+ <section id="viewer" class="card viewer" aria-label="流程与作品" aria-busy="false">
254
+ <div class="canvas-toolbar"><span class="canvas-label">创作画布</span><div id="view-modes" class="view-switch" aria-label="比较方式"><button type="button" data-view="pair" aria-pressed="true">并排</button><button type="button" data-view="overlay" aria-pressed="false" disabled>叠图</button></div><button id="refresh" class="quiet" type="button">刷新</button></div>
255
+ <div id="viewer-empty" class="viewer-empty"><img id="draft-preview" alt="待改图的本地预览" hidden><span class="empty-mark" aria-hidden="true">✧</span><h2>从一张图片开始</h2><p class="muted">选好原图,写下想法。作品会保存在当前工作区。</p></div>
256
+ <div id="run-view" hidden>
257
+ <section id="audio-result" class="audio-result" hidden aria-label="音频与转写结果"><div class="audio-result-header"><span class="audio-result-icon" aria-hidden="true">♪</span><div><p class="eyebrow" id="audio-result-kind">AUDIO</p><h3 id="audio-result-title">原任务的作品</h3></div></div><div id="audio-source-result" hidden><p class="field-label">原音频</p><audio id="source-audio" controls preload="metadata"></audio></div><div id="audio-output-result" hidden><p class="field-label">合成语音</p><audio id="output-audio" controls preload="metadata"></audio></div><pre id="transcript-result" class="transcript-result" hidden></pre><p id="audio-result-note" class="field-note" role="status"></p></section>
258
+ <div id="image-pair" class="image-pair">
259
+ <figure id="source-card" class="image-card"><figcaption><span>原图</span><small id="source-selected" class="selected-label" hidden>已选择</small></figcaption><button id="source-preview" class="image-stage" type="button" aria-label="放大原图"><span id="source-placeholder" class="muted">读取中…</span><img id="source-image" alt="此流程保存的原图" hidden></button><p id="source-dimensions" class="image-dimensions"></p><button id="select-source" class="choice-button" type="button" aria-pressed="false" disabled>选择原图</button></figure>
260
+ <figure id="output-card" class="image-card"><figcaption><span>生成结果</span><small id="output-selected" class="selected-label" hidden>已选择</small></figcaption><button id="output-preview" class="image-stage" type="button" aria-label="放大生成结果"><span id="output-placeholder" class="muted">尚无已保存的结果</span><img id="output-image" alt="此流程保存的生成结果" hidden></button><p id="output-dimensions" class="image-dimensions"></p><button id="select-output" class="choice-button" type="button" aria-pressed="false" disabled>选择结果</button></figure>
261
+ </div>
262
+ <div id="overlay-view" class="overlay-view" hidden><div class="overlay-stage"><img id="overlay-source" alt="叠图原图"><img id="overlay-output" alt="叠图结果"></div><label for="compare-split">原图 / 结果分界<input id="compare-split" type="range" min="0" max="100" value="50"></label></div>
263
+ <details id="plugin-preview" class="plugin-preview compact-details" hidden>
264
+ <summary><span>扩展预览</span><span class="disclosure">展开</span></summary>
265
+ <div class="plugin-preview-options"><label>作品<select id="plugin-artifact" aria-label="扩展预览作品"></select></label><label>查看方式<select id="plugin-renderer" aria-label="作品预览方式"></select></label></div>
266
+ <div id="plugin-preview-slot" class="plugin-slot"></div>
267
+ </details>
268
+ <div class="run-heading"><div><h2 id="status-title"></h2><p id="status-message" class="muted"></p></div><button id="stop-operation" class="secondary" type="button" hidden>停止等待</button></div>
269
+ <p id="operation-note" class="field-note" hidden></p>
270
+ <div class="result-actions"><p id="selection-feedback" role="status" aria-live="polite">选择喜欢的版本,再保存到设备。</p><button id="download" type="button" disabled>下载已选作品</button></div>
271
+ <div class="reuse-actions"><button id="reuse-selected" class="quiet" type="button" disabled>用已选作品新建</button><span id="run-meta" class="meta"></span></div>
272
+ <details id="run-details" class="run-details"><summary><span>流程详情</span><span class="disclosure">展开</span></summary><section class="prompt-section" aria-labelledby="prompt-title"><h3 id="prompt-title">修改要求</h3><p id="prompt"></p></section><section id="run-parameters" class="comfy-record" hidden><h3>本次记录的工作流参数</h3><dl id="run-parameter-values"></dl><p class="field-note">来自此流程的输入快照;未记录的输入沿用当时模板。</p></section><dl id="run-facts"></dl><p class="muted">像素差异只描述图片变化,不代表质量评分。</p></details>
273
+ </div>
274
+ </section>
275
+ </div>
276
+ </section>
277
+ <footer>Iris · 本地创作,随时接续<span>面向 0.3 的 Core 工作台</span></footer>
278
+ </main>
279
+ </div></div>
280
+ <dialog id="image-dialog" aria-labelledby="image-dialog-title"><div class="section-heading"><h2 id="image-dialog-title">图片预览</h2><button id="close-image-dialog" class="quiet" type="button" aria-label="关闭图片预览">关闭</button></div><img id="expanded-image" alt="放大的作品"><p class="field-note">显示原始图片,不改变作品像素。</p></dialog>
281
+ <dialog id="appearance-dialog" class="appearance-dialog" aria-labelledby="appearance-title">
282
+ <div class="section-heading"><h2 id="appearance-title">外观</h2><button id="appearance-close" class="quiet" type="button">关闭</button></div>
283
+ <div class="iris-swatch" aria-label="鸢尾六色渐变"></div><p class="field-note">一点鸢尾色,留白给创作。设置仅保存在此浏览器。</p>
284
+ <label class="field-label" for="appearance-theme">颜色模式</label><select id="appearance-theme"><option value="light">浅色</option><option value="dark">深色</option><option value="system">跟随系统</option></select>
285
+ <label class="field-label" for="appearance-density">界面密度</label><select id="appearance-density"><option value="comfortable">舒适</option><option value="compact">紧凑</option></select>
286
+ <details id="appearance-background" class="tool-details"><summary><span>自选背景</span><span class="disclosure">展开</span></summary><label class="file-button secondary" for="appearance-image">选择本地图片<input id="appearance-image" type="file" accept="image/png,image/jpeg,image/webp" disabled></label><p id="appearance-image-note" class="field-note">没有自选背景。图片不会上传到 Core。</p><p class="field-note">PNG / JPEG / WebP,最大 5 MiB;图片比较区保持中性底色。</p><label class="field-label" for="appearance-position">位置</label><select id="appearance-position"><option value="center">居中</option><option value="top">顶部</option><option value="bottom">底部</option></select><label class="field-label" for="appearance-overlay">遮罩 <output id="appearance-overlay-value">86%</output></label><input id="appearance-overlay" type="range" min="50" max="100" value="86"><label class="field-label" for="appearance-blur">模糊 <output id="appearance-blur-value">0 px</output></label><input id="appearance-blur" type="range" min="0" max="20" value="0"><button id="appearance-remove" class="quiet" type="button" disabled>移除背景</button></details>
287
+ <details class="tool-details"><summary><span>分享外观设置</span><span class="disclosure">展开</span></summary><p class="field-note">JSON 只包含配色与显示设置,不包含背景图片、连接或密钥。</p><div class="export-actions"><button id="appearance-export" class="quiet" type="button">导出设置</button><label class="file-button secondary" for="appearance-import">导入设置<input id="appearance-import" type="file" accept="application/json,.json"></label></div></details>
288
+ <div class="export-actions"><button id="appearance-reset" class="quiet" type="button">恢复默认配色与布局</button></div><p id="appearance-note" class="field-note" role="status" aria-live="polite">更改即时生效。</p>
289
+ </dialog>
290
+ </body>
291
+ </html>
@@ -0,0 +1,204 @@
1
+ import { createRecoveryJournal, WorkbenchMediaError } from './recovery.js';
2
+ import { normalizeWorkflowValues } from './comfyui.js';
3
+
4
+ const CAPABILITY = { 'image-import': 'images.create', 'flow-create': 'runs.create', 'generation-create': 'generations.create', continue: 'runs.continue',
5
+ 'audio-import': 'audio.create', 'speech-create': 'speeches.create', 'transcription-create': 'transcriptions.create', selection: 'runs.select', stop: 'operations.stop' };
6
+ const TERMINAL = new Set(['succeeded', 'failed', 'canceled', 'interrupted']);
7
+ const RUN = /^run_[a-f0-9]{24}$/, ARTIFACT = /^artifact_[a-f0-9]{24}$/, OPERATION = /^op_[a-f0-9]{64}$/;
8
+ const fail = (code, message) => { throw new WorkbenchMediaError(code, message); };
9
+ export const CREATIONS = ['image-import', 'flow-create', 'generation-create', 'audio-import', 'speech-create', 'transcription-create'];
10
+ export const IMPORTS = ['image-import', 'audio-import'];
11
+ const AUDIO_TYPES = ['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac'];
12
+ function audioInput(value, kind) {
13
+ const required = kind === 'speech-create' ? ['modelRef', 'text'] : ['modelRef', 'sourceArtifactId'];
14
+ if (!value || typeof value !== 'object' || Array.isArray(value) || required.some(key => typeof value[key] !== 'string' || !value[key].trim())
15
+ || Object.keys(value).some(key => ![...required, ...(kind === 'speech-create' ? ['voice'] : [])].includes(key))
16
+ || value.voice !== undefined && typeof value.voice !== 'string') throw new TypeError('音频流程输入无效');
17
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, item.trim()]));
18
+ }
19
+ function flowInput(value, generate = false) {
20
+ const required = generate ? ['modelRef', 'prompt'] : ['sourceArtifactId', 'modelRef', 'prompt'];
21
+ if (!value || typeof value !== 'object' || Array.isArray(value)
22
+ || Object.keys(value).some(key => ![...required, 'size', ...(generate ? ['workflowParameters'] : [])].includes(key))
23
+ || !required.every(key => typeof value[key] === 'string')
24
+ || value.size !== undefined && typeof value.size !== 'string') throw new TypeError('流程输入无效');
25
+ return { ...(!generate ? { sourceArtifactId: value.sourceArtifactId } : {}), modelRef: value.modelRef, prompt: value.prompt,
26
+ ...(value.size === undefined ? {} : { size: value.size }),
27
+ ...(value.workflowParameters === undefined ? {} : { workflowParameters: normalizeWorkflowValues(value.workflowParameters) }) };
28
+ }
29
+ function operation(value, runId) {
30
+ const item = value?.operation;
31
+ if (value?.contractVersion !== 0 || typeof item?.id !== 'string' || !OPERATION.test(item.id)
32
+ || typeof item.runId !== 'string' || !RUN.test(item.runId) || runId && item.runId !== runId
33
+ || !['queued', 'running', ...TERMINAL].includes(item.state)) throw new TypeError('不支持的操作响应');
34
+ return item;
35
+ }
36
+ function flow(value, runId) {
37
+ if (value?.contractVersion !== 0 || value.id !== runId || !Number.isSafeInteger(value.revision) || value.revision < 1
38
+ || value.selection !== null && (typeof value.selection?.artifactId !== 'string' || !ARTIFACT.test(value.selection.artifactId))) {
39
+ throw new TypeError('不支持的流程响应');
40
+ }
41
+ return value;
42
+ }
43
+
44
+ /** Explicit actions only. Constructing, reconnecting and recovering never send POST. */
45
+ export function createMediaClient({ client, session, origin, getStorage }) {
46
+ if (session?.contractVersion !== 0 || !Array.isArray(session.operations) || !session.operations.every(item => typeof item === 'string')) {
47
+ throw new TypeError('不支持的工作台会话');
48
+ }
49
+ const journal = createRecoveryJournal({ origin, rootId: session.rootId, getStorage });
50
+ const capabilities = new Set(session.operations), lifetime = new AbortController(), stops = new Map();
51
+ let active = null, recovering = null;
52
+ const open = () => lifetime.signal.throwIfAborted();
53
+ const requireCapability = name => { if (!capabilities.has(name)) fail('IRIS_WEB_CAPABILITY_UNAVAILABLE', '当前服务未开启这项操作'); };
54
+ const snapshot = () => ({ ...journal.snapshot(), busy: Boolean(active || stops.size || recovering) });
55
+ function settle(record, response) {
56
+ open();
57
+ if (response?.contractVersion !== 0) throw new TypeError('不支持的媒体响应');
58
+ if (IMPORTS.includes(record.kind)) {
59
+ const id = response.artifact?.artifactId;
60
+ if (typeof id !== 'string' || !ARTIFACT.test(id)) throw new TypeError('不支持的素材响应');
61
+ journal.remove(record.id);
62
+ } else if (CREATIONS.includes(record.kind)) {
63
+ const result = flow(response.flow, response.flow?.id);
64
+ const recipes = { 'flow-create': 'image-edit/v0', 'generation-create': 'image-generate/v0', 'speech-create': 'speech/v0', 'transcription-create': 'transcribe/v0' };
65
+ if (typeof result.id !== 'string' || !RUN.test(result.id) || result.recipe !== recipes[record.kind]
66
+ || (record.artifactId ? result.source?.artifactId !== record.artifactId : result.source !== null)) throw new TypeError('流程来源或类型与本次创建不一致');
67
+ journal.remove(record.id);
68
+ } else if (record.kind === 'selection') {
69
+ const result = flow(response, record.runId);
70
+ if (result.selection?.artifactId !== record.artifactId || result.revision < record.expectedRevision) throw new TypeError('选择结果与本次操作不一致');
71
+ journal.remove(record.id);
72
+ } else {
73
+ const item = operation(response, record.kind === 'continue' ? record.runId : undefined);
74
+ if (record.kind === 'stop' && item.id !== record.operationId) throw new TypeError('停止响应与原操作不一致');
75
+ if (TERMINAL.has(item.state)) {
76
+ journal.remove(record.id);
77
+ if (record.kind === 'stop') for (const other of journal.snapshot().actions) {
78
+ if (other.kind === 'continue' && other.operationId === item.id) journal.remove(other.id);
79
+ }
80
+ } else journal.update(record.id, { operationId: item.id });
81
+ }
82
+ return response;
83
+ }
84
+ function send(record, input) {
85
+ const options = { key: record.key, signal: lifetime.signal };
86
+ if (record.kind === 'image-import') return client.uploadImage(input, options);
87
+ if (record.kind === 'flow-create') return client.createRun(input, options);
88
+ if (record.kind === 'generation-create') return client.createGeneration(input, options);
89
+ if (record.kind === 'audio-import') return client.uploadAudio(input, options);
90
+ if (record.kind === 'speech-create') return client.createSpeech(input, options);
91
+ if (record.kind === 'transcription-create') return client.createTranscription(input, options);
92
+ if (record.kind === 'continue') return client.continueRun(record.runId, record.expectedRevision, options);
93
+ if (record.kind === 'selection') return client.selectRun(record.runId, record.artifactId, record.expectedRevision, options);
94
+ return client.stopOperation(record.operationId, options);
95
+ }
96
+ function perform(kind, input, original) {
97
+ open(); requireCapability(CAPABILITY[kind]);
98
+ if (journal.snapshot().blocked) fail('IRIS_WEB_RECOVERY_CORRUPT', '待核对记录损坏,请先明确处理原记录');
99
+ if (recovering) fail('IRIS_WEB_BUSY', '正在核对原操作,请等待读取完成');
100
+ const signature = IMPORTS.includes(kind) ? input : JSON.stringify([kind, input]);
101
+ const running = kind === 'stop' ? stops.get(input.operationId) : active;
102
+ if (running) {
103
+ if (running.kind === kind && running.signature === signature && running.originalId === original?.id) return running.promise;
104
+ fail('IRIS_WEB_BUSY', '本次操作尚未返回,请等待或读取原结果');
105
+ }
106
+ if (!original && journal.snapshot().actions.some(item => kind === 'stop'
107
+ ? item.kind === 'stop' && item.operationId === input.operationId
108
+ : CREATIONS.includes(kind) ? item.kind === kind : item.runId === input.runId)) {
109
+ fail('IRIS_WEB_RECOVERY_REQUIRED', '原操作结果尚待核对,请先读取原结果或明确重试原动作');
110
+ }
111
+ const id = original?.id || crypto.randomUUID();
112
+ const record = original || { id, kind,
113
+ ...([...CREATIONS, 'continue'].includes(kind) ? { key: id } : {}),
114
+ ...(['flow-create', 'transcription-create'].includes(kind) ? { artifactId: input.sourceArtifactId } : {}),
115
+ ...(['continue', 'selection'].includes(kind) ? { runId: input.runId, expectedRevision: input.expectedRevision } : {}),
116
+ ...(kind === 'selection' ? { artifactId: input.artifactId } : {}), ...(kind === 'stop' ? { operationId: input.operationId } : {}) };
117
+ if (!original) journal.add(record);
118
+ const promise = Promise.resolve().then(() => { open(); return send(record, input); }).then(response => settle(record, response)).finally(() => {
119
+ if (kind === 'stop') { if (stops.get(input.operationId)?.promise === promise) stops.delete(input.operationId); }
120
+ else if (active?.promise === promise) active = null;
121
+ });
122
+ const job = { kind, signature, originalId: original?.id, promise };
123
+ if (kind === 'stop') stops.set(input.operationId, job); else active = job;
124
+ return promise;
125
+ }
126
+ async function readAction(action) {
127
+ const capability = CREATIONS.includes(action.kind) ? 'creation.lookup'
128
+ : action.kind === 'selection' ? 'runs.inspect' : action.operationId ? 'operations.inspect' : 'operations.lookup';
129
+ if (!capabilities.has(capability)) return { action, status: 'capability_unavailable' };
130
+ try {
131
+ let result;
132
+ if (CREATIONS.includes(action.kind)) {
133
+ result = await client.lookupCreation(action.kind, action.key, { signal: lifetime.signal }); open();
134
+ if (result.contractVersion !== 0 || result.kind !== action.kind || !['prepared', 'succeeded'].includes(result.state)
135
+ || result.state === 'succeeded' && !result.result) throw new TypeError('不支持的创建查询响应');
136
+ if (result.result) {
137
+ const field = IMPORTS.includes(action.kind) ? 'artifactId' : 'runId';
138
+ journal.update(action.id, { [field]: result.result[field] });
139
+ }
140
+ if (result.state === 'succeeded') journal.remove(action.id);
141
+ return { action, status: result.state === 'succeeded' ? 'confirmed' : 'prepared', result };
142
+ }
143
+ if (action.kind === 'selection') {
144
+ result = flow(await client.getRun(action.runId, { signal: lifetime.signal }), action.runId); open();
145
+ journal.remove(action.id);
146
+ return { action, status: 'current', matches: result.selection?.artifactId === action.artifactId, result };
147
+ }
148
+ result = await (action.operationId ? client.getOperation(action.operationId, { signal: lifetime.signal })
149
+ : client.lookupOperation(action.key, { signal: lifetime.signal })); open();
150
+ const item = operation(result, action.runId);
151
+ if (action.operationId && item.id !== action.operationId) throw new TypeError('查询响应与原操作不一致');
152
+ if (TERMINAL.has(item.state)) journal.remove(action.id);
153
+ else journal.update(action.id, { operationId: item.id });
154
+ return { action, status: TERMINAL.has(item.state) ? 'confirmed' : 'active', result };
155
+ } catch (error) {
156
+ open();
157
+ return { action, status: error.status === 404 ? 'not_found' : error.status === 409 ? 'conflict' : 'unavailable', error };
158
+ }
159
+ }
160
+ return Object.freeze({
161
+ snapshot,
162
+ uploadImage(image) {
163
+ if (!(image instanceof Blob) || !['image/png', 'image/jpeg', 'image/webp'].includes(image.type)) throw new TypeError('需要静态图片文件');
164
+ return perform('image-import', image);
165
+ },
166
+ createRun(value) { return perform('flow-create', flowInput(value)); },
167
+ createGeneration(value) { return perform('generation-create', flowInput(value, true)); },
168
+ uploadAudio(audio) {
169
+ if (!(audio instanceof Blob) || !AUDIO_TYPES.includes(audio.type)) throw new TypeError('需要支持的音频文件');
170
+ return perform('audio-import', audio);
171
+ },
172
+ createSpeech(value) { return perform('speech-create', audioInput(value, 'speech-create')); },
173
+ createTranscription(value) { return perform('transcription-create', audioInput(value, 'transcription-create')); },
174
+ continueRun(runId, expectedRevision) { return perform('continue', { runId, expectedRevision }); },
175
+ selectRun(runId, artifactId, expectedRevision) { return perform('selection', { runId, artifactId, expectedRevision }); },
176
+ stopOperation(operationId) { return perform('stop', { operationId }); },
177
+ retry(id, input) {
178
+ const action = journal.snapshot().actions.find(item => item.id === id);
179
+ if (!action) throw new TypeError('原操作身份不存在');
180
+ if (IMPORTS.includes(action.kind)) {
181
+ if (!(input instanceof Blob) || !(action.kind === 'audio-import' ? AUDIO_TYPES : ['image/png', 'image/jpeg', 'image/webp']).includes(input.type)) throw new TypeError('请重新提供原素材');
182
+ } else if (['flow-create', 'generation-create'].includes(action.kind)) {
183
+ input = flowInput(input, action.kind === 'generation-create');
184
+ if (action.kind === 'flow-create' && input.sourceArtifactId !== action.artifactId) throw new TypeError('请提供原流程的来源图片与输入');
185
+ } else if (['speech-create', 'transcription-create'].includes(action.kind)) {
186
+ input = audioInput(input, action.kind);
187
+ if (action.kind === 'transcription-create' && input.sourceArtifactId !== action.artifactId) throw new TypeError('请提供原转写音频');
188
+ } else input = action;
189
+ return perform(action.kind, input, action);
190
+ },
191
+ recover() {
192
+ open();
193
+ if (active || stops.size) fail('IRIS_WEB_BUSY', '本次写入尚未返回,请等待后核对');
194
+ if (recovering) return recovering;
195
+ if (journal.snapshot().blocked) return Promise.resolve([]);
196
+ recovering = (async () => { const results = []; for (const item of journal.snapshot().actions) results.push(await readAction(item)); return results; })()
197
+ .finally(() => { recovering = null; });
198
+ return recovering;
199
+ },
200
+ forget(id) { open(); if (active || stops.size || recovering) fail('IRIS_WEB_BUSY', '请等待当前操作结束'); journal.remove(id); },
201
+ clearRecovery() { open(); if (active || stops.size || recovering) fail('IRIS_WEB_BUSY', '请等待当前操作结束'); journal.clear(); },
202
+ close() { lifetime.abort(); journal.close(); }
203
+ });
204
+ }