@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
@@ -1,11 +1,13 @@
1
1
  # 独立工作台 Demo(面向 0.3.0,未发布)
2
2
 
3
- 这是面向 **0.3.0** 的交互预览,用来展示“选图与填写要求 → 查看状态 → 比较 → 选择与下载”的操作方向。**当前 Demo 保留作为参考,界面、文案和交互细节将在 0.3.0 阶段统一润色,尚未定稿。**
3
+ 这是面向 **0.3.0** 的交互预览,包含创作、作品和连接草稿三个页面。首轮布局已围绕大图预览、紧凑参数面板和默认折叠的详情整理,仍用于验证操作方向,尚未定稿。
4
4
 
5
- Demo 可在源码树中独立预览,无需 DSH;使用内存演示与内置样图,不连接模型、不计费,也不写入 Core 数据。真实生成目前使用现有 DSH 工作台或 CLI;npm 0.2.7 不携带源码树的 prototypes/ 和预览脚本。
5
+ Demo 可在源码树中独立预览,无需 DSH;使用内存演示与内置样图,不连接模型、不计费,也不写入 Core 数据。0.3 alpha源码的真实工作台已接入固定图片编辑及[独立文生图](IMAGE_GENERATION.md);npm 0.2.7 不含该页面,也不携带 prototypes/ 和预览脚本。
6
6
 
7
7
  ## 启动预览
8
8
 
9
+ 源码另有 [真实 Core 工作台](LOCAL_WEB.md),已采用此 Demo 的布局方向,接入明确改图、选择、下载、恢复及真实连接管理,权限由独立开关控制,默认只读。本页仅描述独立 Demo;演示数据和连接草稿不会转为真实记录,不能以演示检查替代真实服务验收。
10
+
9
11
  在 Git 源码树中,使用现有 Node.js ≥22,无须安装新依赖:
10
12
 
11
13
  ```bash
@@ -18,24 +20,35 @@ node scripts/preview-workbench.mjs
18
20
 
19
21
  ## 可体验的流程
20
22
 
23
+ 桌面左侧为导航和最近流程,中间为图片画布,右侧为输入面板;手机上导航保留在顶部,输入和画布纵向排列。
24
+
21
25
  1. 使用内置鸢尾花样图,或选择/拖放本地 PNG、JPEG、WebP;本地文件只生成标签页内的 Blob 预览,不上传。
22
26
  2. 选择演示模型、填写修改要求,点击“开始演示”。本次输入随后固定;修改输入需明确新建流程。
23
27
  3. 查看原图与示例结果,切换并排/叠图,明确选择原图或结果,再下载。
24
28
  4. 展开“运行详情与演示场景”,体验原任务进行中、受理未知、生成成功但交付中断、等待本地比较等状态。场景对新流程生效。
29
+ 5. 到“作品”页按全部、已选择或待选择筛选本标签页产生的流程;点击卡片或最近流程可只读切回原记录。首次打开为空,不预填虚构作品。
30
+
31
+ 提示词快捷按钮只填写要求,放大按钮只预览原图;均不会开始生成。“演示说明”介绍样图与记录的保留范围。
25
32
 
26
33
  **示例结果始终是内置样图,不是对上传图片或提示词的真实修改**。原图和示例结果可能相同;原型不伪造像素差异、模型质量分数、计费价格或进度百分比。下载原图取本地原字节,下载结果取内置样图;“已选择”和“下载请求已交给浏览器”分别显示,不声称文件已保存。
27
34
 
28
- 状态名称和白名单 DTO 依据真实 [固定流程](IMAGE_FLOW.md);投影有实际 Core/受控 Provider 测试。演示传输不读写磁盘 Run,不保证服务重启恢复。新建后可从详情中的流程列表只读切回本标签页旧记录;整页刷新清空演示状态,并且不会自动开始。未将文件、提示词或主题写入 localStorage。
35
+ 状态名称和白名单 DTO 依据真实 [固定流程](IMAGE_FLOW.md);投影有实际 Core/受控 Provider 测试。演示传输不读写磁盘 Run,不保证服务重启恢复。新建后也可从详情中的流程列表只读切回本标签页旧记录;整页刷新清空演示状态,并且不会自动开始。文件、提示词、草稿与主题均不写入 localStorage 或 sessionStorage。
29
36
 
30
37
  “停止本次等待”只中止当前演示操作,原任务事实保留;真实 API 的远端取消须有独立能力与确认,不能把停止等待写成取消成功。展开详情、重新读取状态、切换历史与比较方式均不驱动生成。
31
38
 
39
+ ## 连接草稿
40
+
41
+ “连接”页可自由填写名称,选择图片功能及读取、创建、执行、停止权限,并展开查看相应工具名称。草稿沿用当前 Core 图片流程的工具分组;其他媒体功能灰显,等待逐步接入。关闭图片功能后,只保留连接自检和连接清单工具。
42
+
43
+ “保存演示草稿”只保存到本标签页内存,重复保存更新同一草稿。此页不连接客户端、不创建真实授权,不生成密钥、MCP 配置或 Skill 安装;整页刷新后草稿清空。需要真实配置时使用 [Core 连接中心](CORE_CONNECTIONS.md)。
44
+
32
45
  ## 实现和下一步
33
46
 
34
- 原型位于 `prototypes/workbench/`,使用原生 HTML/CSS/JavaScript,复用现有图标和示例图片,没有新框架或生产依赖。运行详情默认折叠,关闭显示“展开”、打开显示“收起”;提供浅/深色、键盘焦点与 320/390px 布局。
47
+ 原型位于 `prototypes/workbench/`,使用原生 HTML/CSS/JavaScript,复用 Iris 图标和示例图片,没有新框架或生产依赖。运行详情和工具预览默认折叠,关闭显示“展开”、打开显示“收起”;提供浅/深色、键盘焦点与 320/390px 布局。
35
48
 
36
- 0.2.7 已有 [默认只读 API](LOCAL_API.md)及显式开启的 [本地上传/Run 创建](LOCAL_CREATION.md),带持久请求意图和身份接回;当前 Demo 保持原样,尚未连接它们。后续补执行/取消、Operation 与恢复边界,再在 0.3.0 阶段结合实际接口统一完善页面。Demo 的当前布局、提示词和样例效果仅用于说明方向,不代表最终界面或模型能力。
49
+ 0.2.7 已有 [默认只读 API](LOCAL_API.md)及显式开启的 [本地上传/Run 创建](LOCAL_CREATION.md),带持久请求意图和身份接回;0.3 alpha源码另有 [Operation 与有限执行服务](CORE_SERVICE.md)、[版本约束选择](LOCAL_SELECTION.md)及真实创作页面。此 Demo 仍独立使用内存样例,其布局、提示词和样例效果用于说明方向,不代表模型能力;真实页面的恢复、故障与发布门禁继续独立验收。
37
50
 
38
- [本地 API 契约](LOCAL_API_CONTRACT.md)区分已实现的只读/本地创建切片与待实现的执行、选择和取消。相同 Run 的 Task 复用与 HTTP 创建幂等分别验证;执行接口安全和中断测试通过后,再替换演示传输并完成无 DSH 的实际端到端验收。
51
+ [本地 API 契约](LOCAL_API_CONTRACT.md)区分已发布的只读/本地创建,以及未发布的有限执行、本地停止和 owner 持久选择。相同 Run 的 Task 复用、HTTP 创建幂等与选择的版本门禁分别验证;替换演示传输时仍须单独验证浏览器写入、原身份恢复与无 DSH 的完整流程。
39
52
 
40
53
  源码树可运行下面的检查;浏览器检查需显式提供已经安装的 Chromium,不下载或安装浏览器。无沙箱仅在受控测试环境确有需要时显式追加 `--no-sandbox`,不改变生产浏览器策略。
41
54
 
@@ -45,3 +58,5 @@ node scripts/verify-workbench-prototype.mjs --browser-executable /absolute/path/
45
58
  ```
46
59
 
47
60
  浏览器检查保存交互结果与桌面/手机截图;这是隔离 Chromium 验收,不是 Android 真机触控或日常 DSH 装载证明。
61
+
62
+ 布局参考了 [Invoke 的画布分区](https://v7.invoke.ai/users-guide/canvas/canvas-guide/)、[ComfyUI 的导航与工作区域](https://support.comfy.org/articles/7675456845-the-comfyui-interface)和 [Linear 的界面层级整理](https://linear.app/now/behind-the-latest-design-refresh),保留 Iris 自有标识、配色与素材。
@@ -0,0 +1,46 @@
1
+ # 工作台请求核对与刷新恢复(0.3 alpha)
2
+
3
+ 本页描述源码中的 W1b 恢复及 W1c 页面接入。npm 0.2.7 不含这些新增接口与浏览器客户端;[真实工作台](LOCAL_WEB.md)已接入明确创作、选择和本地停止,“操作核对”默认折叠,自动恢复始终只读。
4
+
5
+ ## 按原请求读取
6
+
7
+ owner 的 `GET /api/v0/session` 提供 `rootId`:使用现有创建请求区的稳定 UUID,服务重启、Token 轮换不改变它;没有请求区时为 `null`,读取不会初始化数据。`instanceId` 继续表示本次服务。连接客户端不获得此字段或新查询权限,MCP 工具和 scope 保持。
8
+
9
+ 有现成请求区时,owner 的 `operations` 包含 `creation.lookup`,默认只读服务也可调用:
10
+
11
+ ```text
12
+ GET /api/v0/creation-requests?kind=image-import&key=<original-lowercase-UUIDv4>
13
+ GET /api/v0/creation-requests?kind=flow-create&key=<original-lowercase-UUIDv4>
14
+
15
+ {contractVersion:0, kind, state:"prepared"|"succeeded", result}
16
+ ```
17
+
18
+ 创建类型另包括 `generation-create`;新 v3 音频工作区增加 `audio-import`、`speech-create`、`transcription-create`,返回同样的有限身份,不返回音频或朗读正文。音频恢复与大小边界见[音频流程](AUDIO_FLOWS.md)。
19
+
20
+ `result` 为 `{artifactId}`、`{runId}` 或 `null`。查询会核对原意图与已有结果;prepared 尚无目标时返回 null,已有目标核验通过后才返回引用。它不补写完成回执、不重建目标、不观察 Task 或调用 Provider。已推进或选择的 Run 不退回初始状态。没有原请求为 404;原结果不可用或证据损坏为 409,并保留原记录。
21
+
22
+ 查询仍要求 Bearer、可信 Host/Origin,不接受其他 kind、路径或重复参数。不返回原始 Key、Prompt、供应商绑定或磁盘记录。Key 是动作身份,不能替代认证。
23
+
24
+ ## 浏览器动作协调
25
+
26
+ 内部客户端提供命名的上传、创建、继续、停止与选择方法,没有任意媒体 POST/Command 执行入口。上传、创建、continue 各自使用一个 Key;同一活动动作的重复调用共享同一次请求,输入不同则明确繁忙。请求失败不自动重试。
27
+
28
+ 结果未知时先读取原 Key/Operation/Run。显式重试保留原 Key;上传需重新提供原图片或音频字节,创建需原输入。服务器继续核对输入摘要,不用新 Key 绕过冲突。查到创建成功只恢复已有 Run,不自动首次执行;读 Operation 也不调用 continue。
29
+
30
+ 选择没有持久 Key:回包丢失后读取 Run,展示“当前选择”,不声称能证明这次请求完成。陈旧 revision 不能覆盖新选择。停止复用原 Operation ID,只停止本地等待,不代表远端取消或退款。
31
+
32
+ 读取总并发仍为二;轻量 JSON 优先于排队中的媒体,不能抢占已开始的读取。写请求和停止直接发送,不进入图片读取队列。会话断开会取消客户端等待,服务已接收的事实仍保留。
33
+
34
+ ## 标签页恢复表
35
+
36
+ 使用 `sessionStorage` 的单个 `iris.workbench.recovery.v0` 键,最多八条待核对动作。只存 origin、稳定 rootId、动作类型与标识、原 Key、已知 Artifact/Run/Operation ID 和预期 revision;不存 Token、Prompt、图片或供应商配置,不使用 localStorage。
37
+
38
+ 上传/创建确认后移除记录;活动 Operation 保留到读到终态。prepared、404、冲突、网络错误或当前服务未开放查询时,保留待核对身份,不自动生成。表满先核对或明确处理已有动作,不悄悄丢弃未知请求。存储不可用时退为内存并提示刷新恢复受限;损坏表保留,暂停新动作,不把损坏当空表。
39
+
40
+ 刷新后先使用标签页保存的登录重新验证,再核对 session/rootId 和原请求,全程 GET;Token 失效时清除并要求重新连接,见 [会话边界](LOCAL_WEB.md#会话与图片边界)。登录缓存与本页的操作恢复表分开,恢复表仍不含 Token。换到不同 origin/根时只丢弃旧浏览器标识,Core 数据保持;迟到响应不能写回新根的表。无请求区时只有内存记录,不能承诺刷新定位未知上传/创建。
41
+
42
+ 面板支持只读的“重新核对”和“查看流程”。prepared/未找到的上传、创建或继续可明确“准备按原 Key 接回”,重新提供原图片/模型/要求/参数后再次确认;接回创建不会自动首次执行。损坏或无法继续处理的本地标识,可在说明影响并二次确认后清除;只移除浏览器核对标识,不删 Core、不停止原任务。关闭标签页后不保证保留 sessionStorage;恢复表也不是作品或请求记录的备份。
43
+
44
+ ## 验证
45
+
46
+ `tests/local-creation-lookup.mjs`、`tests/web-recovery.mjs`、`tests/web-media-api.mjs`覆盖只读查询、原 Key、回包丢失、重启、换根、版本冲突、存储异常及有限动作。真实 Chromium 验证可在[工作台验证命令](LOCAL_WEB.md#验证与下一阶段)后增加 `--recovery`:使用独立根和受控 Provider,由测试明确提交本地写入,再检查生产页面只读恢复。它不进行真实付费生成,也不替代 Android 真机验收。
@@ -0,0 +1,63 @@
1
+ # 工作台访问口令(0.3 alpha)
2
+
3
+ 普通 `web serve` 使用一个访问口令,不要求账号、邮箱或读取 Token 文件。**稳定 npm 0.2.7 没有 `web serve`;下文针对0.3.0-alpha.1。** 源码树可把 `dsh-iris` 替换为 `node bin/dsh-iris.js`。
4
+
5
+ ## 首次设置与日常使用
6
+
7
+ 使用已有的专用 Flow 根;不要转换已有业务数据:
8
+
9
+ ```bash
10
+ dsh-iris web serve --data-root /absolute/iris/core --port 3182
11
+ ```
12
+
13
+ 首次打开输出的 `loginUrl`,在网页中设置 8–128 个字符的访问口令。没有 10 分钟倒计时;设置成功后该设置入口失效。设置尚未完成时,服务重启会生成新的设置入口。初次链接允许设置访问口令,应私下保管。
14
+
15
+ 之后收藏普通地址,例如 `http://127.0.0.1:3182`。默认勾选“记住此设备”30天,刷新、新标签页和服务正常重启后继续使用;不勾选时仅当前标签页保存,服务端会话最长12小时。保持相同数据根、认证目录和端口。浏览器清除站点数据、隐私模式或禁止存储时,可能需要重新输入口令;页面会说明存储限制。
16
+
17
+ 默认 Panel 在首屏绘制前读取已保存的明暗主题;恢复登录期间显示同色的简洁过渡页,验证成功直接进入工作区,不先展示登录卡片。没有可用登录或验证失败时再显示表单与说明;入口资源加载失败提供重新加载。背景图继续在浏览器内异步读取,不上传到 Core。
18
+
19
+ “退出”经服务确认后撤销当前浏览器会话并清除本机保存的登录。退出后在同一地址输入原口令即可,无需找文件或向 Agent 取新链接。若退出请求尚未确认,页面保留状态并提示重试。已经接收的业务操作不因退出而取消。
20
+
21
+ ## 忘记地址或口令
22
+
23
+ 只需记住启动时的 Core 数据根,不需要查找凭据文件。在另一个本机终端执行:
24
+
25
+ ```bash
26
+ dsh-iris web login --data-root /absolute/iris/core
27
+ dsh-iris web password reset --data-root /absolute/iris/core
28
+ ```
29
+
30
+ `web login` 在已设置时返回普通登录地址;尚未设置时返回当前设置入口,不绕过已设置口令。`web password reset` 撤销全部工作台会话并返回新的设置入口;服务运行时无需重启,不修改作品、任务、供应商配置或 MCP 连接密钥/授权。服务正常停止后也可重置,下一次启动再打开输出的设置入口。不要把口令写在命令参数或聊天里。
31
+
32
+ 认证目录默认是 `$IRIS_HOME/web-auth/<物理数据根的SHA-256>`,`IRIS_HOME` 默认 `~/.iris`。用户不需要打开该目录。可在启动、取地址及重置命令中一致指定 `--auth-store /absolute/private-auth`。它必须与 Core 根相互独立,使用私有真实目录,POSIX目录0700、文件0600;Android共享存储通常不适合保存此目录。
33
+
34
+ 认证状态有独立写者租约。异常强杀后先核对原进程,再对 `<认证目录>/ownership` 使用现有 `doctor` 和明确的 `runtime recover` 流程;不会自动清锁或抢占实例。Core 执行根与连接目录的租约仍按各自契约处理。损坏、未知版本或属于其他根的认证状态明确报错,不自动覆盖。
35
+
36
+ ## 凭据边界
37
+
38
+ 口令只在同源登录请求中发送,服务端使用内置异步 scrypt、独立随机盐保存派生值,不保存明文。每次登录生成独立浏览器凭据,服务端只保存摘要和期限,最多32个有效会话。口令错误受有限速控制。
39
+
40
+ “记住此设备”将浏览器凭据保存在当前 origin 的 localStorage,普通模式使用 sessionStorage;不保存口令、owner 控制令牌或 MCP 密钥。继续使用 Bearer 而非共享本机端口的 Cookie,保留精确 Host/Origin、无 CORS 和现有 CSP。可信同源插件仍有页面脚本权限,只加载经过审查的模块;这不是不可信代码沙箱。
41
+
42
+ owner 控制记录仅在私有认证目录供本机 CLI 使用,浏览器会话和 MCP 连接均不能调用口令重置接口。MCP 使用连接中心导出的独立授权,浏览器登录不代替 MCP 配置。服务仍只绑定 `127.0.0.1`,没有新增公网监听、远程账户、SSO 或自动服务管理。
43
+
44
+ ## 旧 Token 模式
45
+
46
+ 显式提供 `--token-file` 或 `--token-dir` 时,`web serve` 保留旧的一次性链接和备用 Token 登录,服务重启后需新链接;与 `--auth-store` 互斥。`api serve` 继续要求 Token 参数,不默认开放口令登录。旧模式仍可使用 `web login --origin <实际地址> --token-file <本次文件>`。新默认方式不迁移或删除这些旧文件。
47
+
48
+ ## 接口与装配
49
+
50
+ 以下路径均位于 `/api/v0/workbench` 下,不接受查询参数;POST仅接受最多1024字节的 JSON。所有入口保留 Host/Origin 检查,不开放 CORS。
51
+
52
+ | 路径 | 方法与输入 | 身份与输出 |
53
+ |---|---|---|
54
+ | `/auth` | GET/HEAD,无正文 | 有限公开状态:`contractVersion:0, mode:'password', configured, authId, rememberDays, minPasswordLength, maxPasswordLength`;不返回私有路径或哈希 |
55
+ | `/setup` | POST `{ticket,password,remember}` | 严格同源,验证首次设置票据;返回独立会话 |
56
+ | `/password-login` | POST `{password,remember}` | 严格同源,验证访问口令;返回独立会话 |
57
+ | `/logout` | POST `{}` | 严格同源及当前浏览器 Bearer,幂等撤销;返回 `signedOut:true` |
58
+ | `/setup-link` | POST `{}` | 仅本机 owner 控制令牌;尚未设置时返回 `setupUrl` |
59
+ | `/password-reset` | POST `{}` | 仅本机 owner 控制令牌;重置后返回 `setupUrl` |
60
+
61
+ 会话响应为 `{contractVersion:0, authId, accessToken, expiresAt, remember}`;`accessToken` 是浏览器会话,不能用于 MCP 或口令重置。未知字段、跨源请求、错误口令、过期会话或版本冲突明确返回对应错误,不自动降级到其他认证方式。正常退出只撤销提供的会话;已撤销会话可重复退出,便于丢失响应后重试。
62
+
63
+ Node 宿主用公开 `createCorePluginWorkbench` 时,显式传入 `workbenchAuth: { directory: '/absolute/private-auth' }` 才启用口令模式;未传时保持旧 SDK 行为。生命周期、媒体写权限和插件选择见[工作台](LOCAL_WEB.md)与[稳定启动](PLUGIN_STARTUP.md)。
@@ -0,0 +1,51 @@
1
+ # Iris 0.3.0-alpha.1: a shared media workspace for Agents
2
+
3
+ This first 0.3 alpha adds an independent Core workbench, generic MCP access and explicitly selected plugin contributions to the existing DSH plugin and standalone CLI. Core owns execution, storage and recovery; users choose what each client can access.
4
+
5
+ The package remains `@mokuyoaxis/dsh-iris`, with Node.js 22.0.0 or later required. Install the exact prerelease or follow npm `alpha`; stable `latest` remains 0.2.7. Existing DSH host-version declarations are unchanged. New Core and plugin contracts are experimental and may change before stable 0.3.
6
+
7
+ ```bash
8
+ npm install @mokuyoaxis/dsh-iris@0.3.0-alpha.1
9
+ npx dsh-iris --help
10
+ ```
11
+
12
+ Follow the [Core quickstart](https://github.com/mokuyoaxis/dsh-iris/blob/v0.3.0-alpha.1/docs/CORE_QUICKSTART.md) for a new workspace and the [Agent guide](https://github.com/mokuyoaxis/dsh-iris/blob/v0.3.0-alpha.1/docs/AGENT_CORE_QUICKSTART.md) for setup instructions. Both have English text with Chinese headings. The detailed product guides are primarily in Chinese.
13
+
14
+ ## An independent workbench
15
+
16
+ - A real creation UI, works library, client connections and model services share the same Core records. The default Panel supports light/dark themes, local backgrounds and mobile layouts. Distributed screenshots use built-in themes, without a personal background.
17
+ - Local password login and remembered devices survive normal restarts. Logout permits signing back in with the same password; it does not require a mobile browser to read a private Termux token file.
18
+ - Model services expose providers, per-function automatic/manual/disabled pools and a health spectrum. Providers support connection checks, opt-in service-lifetime keepalive, disable/restore and protected deletion. Configuration or connectivity is separate from a successful media test.
19
+
20
+ ## Durable media and ComfyUI
21
+
22
+ - Image generation/editing and speech/transcription persist Runs, original Tasks and finite Operations. Stable request keys reconnect lost responses; refresh never silently submits media again. Execution, local stop and image selection remain explicit permissions.
23
+ - ComfyUI workflows expose declared scalar parameters, including LoRA model/CLIP weights and schema-driven choices. Both Core and DSH have dedicated controls. Iris talks to an existing ComfyUI service; it does not install ComfyUI or guess workflow/model identifiers.
24
+ - Core Skills add independent ComfyUI guidance: clarify the workflow and requested result, read actual descriptors, keep original task identities and distinguish an API model from a ComfyUI workflow. Skill installation remains explicit and protects local modifications.
25
+
26
+ ## Generic MCP and selected plugins
27
+
28
+ - Official SDK stdio and Streamable HTTP transports share Core. Users select media groups, scopes and individual tools; client presets and exports do not scan disks, install clients or grant additional access. Keep the Core service running when using either transport.
29
+ - Trusted plugins can contribute Provider adapters, durable business operations, pages, settings forms and artifact renderers. Core checks identity, compatibility, lifecycle and persistent task dependencies; loading does not authorize execution.
30
+ - The optional contact-sheet plugin demonstrates public SDK access, durable execution, a real Web page and generic MCP calls. Image-input businesses share eligible PNG/JPEG/WebP sources up to 8 MiB within one data root. Other businesses' execution, history and stop controls remain separately authorized; a handler reads only its bound sources.
31
+ - Exact dependency pins, original request keys and stored output identities survive interruption. A missing plugin does not remove saved history or outputs; restarting or upgrading does not automatically replay business work.
32
+
33
+ ## Reliability fixes
34
+
35
+ This release fixes provider-health edit conflicts, bounded JSON/media responses, interrupted writer-lease preparation, adapter cleanup and delayed metadata, delivery error categories, transient maintenance-reader errors and stale DSH refresh responses. Startup test waits use a monotonic clock; timeout evidence retains the original budgets and failures.
36
+
37
+ Windows short-path aliases now use the same authentication record identity and credential-directory boundary checks as Core's physical root, while retaining the existing default authentication directory mapping. Downloads wait for the output file to close before publication or partial-file cleanup. Windows lifecycle tests explicitly deliver the CLI's shutdown event; they do not claim that Windows `child.kill('SIGTERM')` is graceful. Use terminal Ctrl+C for normal shutdown and inspect leases after forced termination.
38
+
39
+ Validation includes controlled local HTTP services, persistent Core storage, public SDK consumers, official MCP clients over both transports, Chromium business flows and actual process-interruption recovery. These tests do not establish compatibility with every paid model, GPU deployment or client attachment renderer.
40
+
41
+ ## Alpha boundaries and upgrades
42
+
43
+ - Existing roots are not automatically migrated. Initialize new execution/audio roots explicitly; one data root has one writer. The shared-root model is not tenant isolation.
44
+ - Native DSH keeps its own UI and host services. The alpha does not require stable DSH users to move their existing workspace into Core Web.
45
+ - Video, music and visual understanding retain their existing DSH/CLI entry points. Their new Core Web/MCP creation entries are not all implemented. Real ComfyUI GPU inference remains unverified here.
46
+ - This is not a stable plugin SDK, community marketplace or hostile-code sandbox. Provider URLs, redirects and HTTP endpoints remain an explicit trust/deployment boundary.
47
+ - Historical timing anomalies, linear history-validation cost and a specific DSH/Firefox download observation remain recorded. Later successful tests do not establish the cause of an earlier timeout.
48
+
49
+ ## 中文摘要
50
+
51
+ 0.3.0-alpha.1 提供独立 Core 工作台、通用 MCP、显式插件贡献、功能模型池与健康光谱,以及 ComfyUI参数和持久媒体恢复。使用 npm `alpha` 或准确版本安装,稳定 `latest` 保留0.2.7;DSH原生入口及准确宿主范围保持。安装不授权,刷新不自动重做,旧数据根不自动迁移。当前为实验性alpha,尚未完成所有媒体新入口、真实GPU和全部部署场景验收。
@@ -1,4 +1,44 @@
1
- # Iris Android 真机截图 / Android screenshots
1
+ # Iris 界面与生成来源 / Screenshots and provenance
2
+
3
+ ## Core 工作台开发预览 / Core workbench development preview
4
+
5
+ 2026-10-29 重拍,使用当前未发布源码、独立演示数据根及 Chrome for Testing 153.0.8010.12(Linux ARM64)。全新浏览器 profile 只使用内置明亮/暗色主题;流程与作品是此前从零使用演练已完成记录的独立副本,没有读取用户背景、真实供应商凭据或日常浏览器会话。
6
+
7
+ Recaptured on 2026-10-29 from the current unreleased source in Chrome for Testing 153.0.8010.12 on Linux ARM64. A fresh browser profile uses only the built-in light/dark themes. The separate demo data root contains copies of completed records from the earlier first-use walkthrough. No personal background, real provider credentials or daily browser session is used.
8
+
9
+ | 创作与下载 / Studio and download | 完整六类总览 / All six capabilities |
10
+ |---|---|
11
+ | ![Core 创作、选择与下载](assets/screenshots/core-studio-preview.webp) | ![Core 能力总览](assets/screenshots/core-overview-preview.webp) |
12
+
13
+ | 功能模型池 / Function pools | 能力光谱 / Health spectrum |
14
+ |---|---|
15
+ | ![功能模型池的自动、手动与停用策略](assets/screenshots/core-model-pools-preview.webp) | ![Core 六类能力光谱与连接检查](assets/screenshots/core-spectrum-preview.webp) |
16
+
17
+ ![客户端选择图片生成能力和操作权限](assets/screenshots/core-connections-preview.webp)
18
+
19
+ 客户端图展示实际保存的示例授权,状态为“尚未使用”;未导出或展示连接密钥,不据此声称已连接真实客户端。模型池与光谱中的候选、近期成功来自演示配置与受控任务记录,连接检查和保活均未启用。
20
+
21
+ The client capture shows an actually saved demo grant marked unused. Its credential is not exported or shown, and the image does not claim a real client handshake. Pool candidates and recent-success facts come from demo configuration and controlled task records; no connection check or keepalive is enabled.
22
+
23
+ ### 窄屏与暗色 / Mobile layout and dark mode
24
+
25
+ 窄屏图使用桌面 Chromium 的390像素视口,展示布局适配;不是 Android 实机截图。创作设置按现有逻辑收起,完整展示图片、选择状态与下载按钮。
26
+
27
+ The mobile layout uses a 390-pixel viewport in desktop Chromium, rather than a physical Android device. Existing responsive behavior collapses creation settings and keeps the image, selection and download visible.
28
+
29
+ <p align="center"><img src="assets/screenshots/core-studio-mobile-preview.webp" width="390" alt="Core 390像素窄屏创作与下载"></p>
30
+
31
+ ![Core 内置暗色主题](assets/screenshots/core-studio-dark-preview.webp)
32
+
33
+ **媒体与截图边界 / Media and capture scope:** 图中的768×768作品来自此前本地受控 OpenAI Images 回放下方历史鸢尾花图片。本轮重拍只读取已有结果及配置,未新增生成、Task或Operation,也不代表真实付费图片、语音或ComfyUI GPU验收。画面未经DOM/CSS改写或拼接,仅通过正常登录、导航和主题操作拍摄。
34
+
35
+ The 768×768 work comes from the earlier controlled local OpenAI Images replay of the historical iris image below. This capture session reads existing works and configuration without adding a generation, Task or Operation. It does not validate paid image/speech services or a ComfyUI GPU. Captures use normal login, navigation and theme controls, without DOM/CSS rewriting or compositing.
36
+
37
+ 公开版本采用无损WebP以保留界面文字,未保留EXIF/XMP/IPTC。界面当前以中文显示,独立Core功能尚未包含在npm0.2.7中。使用方法见[Core快速开始](CORE_QUICKSTART.md)。
38
+
39
+ Public copies use lossless WebP to preserve UI text, without EXIF/XMP/IPTC. The current interface is in Chinese; these standalone Core features are outside npm 0.2.7. See the [Core quickstart](CORE_QUICKSTART.md).
40
+
41
+ ## DSH Android 历史真机截图 / Historical DSH Android captures
2
42
 
3
43
  这些画面来自 Android 16 浏览器,访问运行在 Termux/PRoot Debian ARM64 中的 DSH;界面使用 DSH 默认皮肤。截图与生成图均为 2026-09-06 的真实操作结果,公开副本已重新编码并移除 EXIF、XMP 与 IPTC 元数据。
4
44
 
package/lib/actions.js CHANGED
@@ -40,10 +40,13 @@ import { videoTaskCandidates } from './video-input.js';
40
40
  import { normalizeDshProvider, withDshProviderConfig } from './dsh-provider-config.js';
41
41
  import { modelDirectorySummary, modelUnavailableReason } from './model-directory.js';
42
42
  import { ModelPortError } from './model-port-contract.js';
43
+ import { compileComfyWorkflow, projectComfyWorkflow } from './comfyui-workflow.js';
44
+ import { createComfyUiClient } from './comfyui-client.js';
45
+ import { inspectComfyWorkflow } from './comfyui-inspection.js';
43
46
 
44
47
  const PROVIDER_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
45
48
  'locate', 'ocr', 'media_summarize', 'task_reobserve', 'task_redeliver', 'task_manual_retry',
46
- 'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model']);
49
+ 'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model', 'providers_check_comfy_workflow']);
47
50
  const AUTO_DIRECTORY_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
48
51
  'locate', 'ocr', 'media_summarize']);
49
52
  const VISION_ACTIONS = new Set(['look', 'relook', 'locate', 'ocr', 'media_summarize']);
@@ -64,6 +67,12 @@ function imageRoutingForModel(provider, modelId) {
64
67
  return { ...routing, editSupported: supportsImageEdit(routing.mediaProtocol, modelId) };
65
68
  }
66
69
 
70
+ function comfySummaryForModel(provider, modelId) {
71
+ const route = providerForImageModel(provider, modelId);
72
+ return route.mediaProtocol === 'comfyui' && route.comfyWorkflow !== undefined
73
+ ? { comfyWorkflow: projectComfyWorkflow(route.comfyWorkflow) } : {};
74
+ }
75
+
67
76
  const EMPTY_ACTION_HOST = defineHostAdapter({
68
77
  id: 'iris-hostless',
69
78
  ports: {},
@@ -512,11 +521,22 @@ async function imageAction(host, args, { signal, core }, editing = false) {
512
521
  if (editing && !args.source_artifact_id) throw new Error('iris: 改图必须提供 source_artifact_id');
513
522
  const { providerInput, sourceArtifactId } = normalizeGenerationInput('image', {
514
523
  prompt: args.prompt, size: optionalActionText(args.size), n: args.n,
524
+ ...(args.workflow_parameters !== undefined ? { model_ref: args.model, workflow_parameters: args.workflow_parameters } : {}),
515
525
  ...(editing ? { source_artifact_id: args.source_artifact_id } : {})
516
526
  });
517
527
 
518
528
  const imageProviders = selectProviders(cap.CAPABILITIES.IMAGE, '图像', args.model);
519
529
  const routes = imageProviders.map((provider) => imageRoute(provider, args));
530
+ if (args.workflow_sha !== undefined && (typeof args.workflow_sha !== 'string' || routes.length !== 1
531
+ || projectComfyWorkflow(routes[0].provider.comfyWorkflow)?.sha256 !== args.workflow_sha)) {
532
+ throw Object.assign(new Error('工作流已变化,请重新选择并核对参数'), { code: 'IRIS_CONFIG_CHANGED' });
533
+ }
534
+ if (providerInput.workflow_parameters !== undefined) {
535
+ if (!models.parseModelRef(args.model) || routes.length !== 1 || routes[0].protocol !== 'comfyui') {
536
+ throw new TypeError('工作流参数需要明确选择一个 ComfyUI 工作流');
537
+ }
538
+ compileComfyWorkflow(routes[0].provider.comfyWorkflow, providerInput);
539
+ }
520
540
  const submitted = await submitCoreImage(core, routes, providerInput, signal, sourceArtifactId);
521
541
  const firstArtifactId = submitted.task.artifactIds[0];
522
542
  const ready = submitted.task.outcome === 'succeeded'
@@ -1091,6 +1111,7 @@ register('providers_list', async (host, args) => {
1091
1111
  ...(m.directoryOnly ? { directoryOnly: true } : {}),
1092
1112
  ...(m.catalogStatus ? { catalogStatus: m.catalogStatus } : {}),
1093
1113
  ...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
1114
+ ...comfySummaryForModel(p, m.id),
1094
1115
  ...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
1095
1116
  ...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
1096
1117
  ...(m.capabilities.includes(cap.CAPABILITIES.VISION) ? { visionInputEffective: visionInputLimits(p, m.id) } : {}),
@@ -1112,6 +1133,34 @@ register('providers_host_connections', async host => {
1112
1133
  return { ok: true, connections: await host.ports.providerConnections.list() };
1113
1134
  });
1114
1135
 
1136
+ /** Explicit template save; no provider request or model execution. */
1137
+ register('providers_save_comfy_workflow', async (_host, args) => {
1138
+ if (Object.keys(args).some(key => !['id', 'model_id', 'workflow', 'expectedSha', 'enableAutomatic'].includes(key))) throw new TypeError('工作流保存包含不支持的字段');
1139
+ const entry = store.setComfyWorkflow(args.id, args.model_id, args.workflow, {
1140
+ expectedSha: args.expectedSha, enableAutomatic: args.enableAutomatic
1141
+ });
1142
+ return { ok: true, modelId: entry.id, comfyWorkflow: projectComfyWorkflow(entry.comfyWorkflow),
1143
+ text: '工作流已保存;未执行生成' + (args.enableAutomatic ? ',图片功能已使用自动池' : ',功能池设置保持') };
1144
+ });
1145
+
1146
+ /** Uses only an already saved connection. /object_info never enqueues a prompt. */
1147
+ register('providers_check_comfy_workflow', async (_host, args, { signal }) => {
1148
+ if (Object.keys(args).some(key => !['id', 'model_id', 'workflow', 'workflow_parameters'].includes(key))
1149
+ || (args.workflow !== undefined) === (args.model_id !== undefined)) throw new TypeError('请选择已保存工作流或导入一份草稿进行检查');
1150
+ const provider = store.providerById(args.id);
1151
+ if (!provider) throw new TypeError('请先保存并启用 ComfyUI 连接');
1152
+ const route = args.workflow !== undefined ? provider : providerForImageModel(provider, args.model_id);
1153
+ if (route.mediaProtocol !== 'comfyui') throw new TypeError('此检查仅用于 ComfyUI 连接');
1154
+ const file = path.join(store.irisHome(), 'providers.json'), before = fs.readFileSync(file);
1155
+ const workflow = compileComfyWorkflow(args.workflow !== undefined ? args.workflow : route.comfyWorkflow,
1156
+ { workflow_parameters: args.workflow_parameters });
1157
+ const report = await inspectComfyWorkflow(createComfyUiClient(route), workflow, { signal });
1158
+ signal?.throwIfAborted();
1159
+ if (!fs.readFileSync(file).equals(before)) throw Object.assign(new Error('检查期间配置已变化,请重新检查'), { code: 'IRIS_CONFIG_CHANGED' });
1160
+ const key = route.apiKey;
1161
+ return { ok: true, report: key ? JSON.parse(JSON.stringify(report).split(key).join('[redacted]')) : report };
1162
+ });
1163
+
1115
1164
  /** 新增/更新供应商 */
1116
1165
  register('providers_upsert', async (host, args) => {
1117
1166
  const id = args.id || undefined;
@@ -1121,6 +1170,7 @@ register('providers_upsert', async (host, args) => {
1121
1170
  for (const key of allowed) {
1122
1171
  if (Object.prototype.hasOwnProperty.call(args, key)) patch[key] = args[key];
1123
1172
  }
1173
+ if (Object.hasOwn(patch, 'auth') && !['none', 'bearer'].includes(patch.auth)) throw new TypeError('认证方式必须为 bearer 或 none');
1124
1174
  if (Object.hasOwn(args, 'dshProvider')) {
1125
1175
  patch.dshProvider = normalizeDshProvider(args.dshProvider);
1126
1176
  const source = requireHostPort(host, 'providerConnections', '复用 DSH 供应商连接');
@@ -1235,6 +1285,7 @@ async function probeModel(host, p, modelId, capability, signal) {
1235
1285
  }
1236
1286
  if (capability === cap.CAPABILITIES.IMAGE) {
1237
1287
  p = providerForImageModel(p, modelId);
1288
+ if (p.mediaProtocol === 'comfyui') return { skipped: true, ok: false, note: 'ComfyUI 请在画图表单选择工作流验证,以持久 Task 保留受理与结果;只读工作流检查不生成图片' };
1238
1289
  const providerAdapter = createConfiguredProviderAdapter(p, { allowRateLimited: true, modelId });
1239
1290
  const submitted = await invokeProviderOperation(providerAdapter, 'submit', {
1240
1291
  capability: 'image', model: modelId,
@@ -1407,7 +1458,7 @@ register('assignments_get', async (host, args) => {
1407
1458
  ? (c === cap.CAPABILITIES.TTS && selectMediaProtocol(provider).mediaProtocol === 'dashscope'
1408
1459
  && dashscopeTtsProtocolIssue(m.id)) || '当前接口协议不支持此功能'
1409
1460
  : modelUnavailable ? modelUnavailableReason() : m.rateLimited ? '限流冷却或额度不可用,请稍后或检查账号' : m.source === 'discovered' ? '已发现,待确认模型用途' : '',
1410
- ...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id) } : {}) });
1461
+ ...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id), ...comfySummaryForModel(provider, m.id) } : {}) });
1411
1462
  }
1412
1463
  }
1413
1464
  // order:归一化的有序分配列表(GUI 直接渲染 failover 顺序,无需处理单字符串旧格式)
package/lib/adapters.js CHANGED
@@ -15,6 +15,7 @@ import { dashscopeApiBase, imageSizeViolation, isDashScopeBaseUrl, normalizeImag
15
15
  import { chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
16
16
  import { isFreeQuotaExhausted, retryAfterDelay } from './provider-health.js';
17
17
  import { ProviderContractError } from './provider-contract.js';
18
+ import { boundedProviderBody, readProviderText, PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
18
19
  export { generateMusic } from './music-transport.js';
19
20
 
20
21
  /**
@@ -42,8 +43,8 @@ function authHeaders(key) {
42
43
  };
43
44
  }
44
45
 
45
- async function readJson(res) {
46
- const text = await res.text();
46
+ async function readJson(res, maxBytes = PROVIDER_RESPONSE_LIMITS.json) {
47
+ const text = await readProviderText(res, { maxBytes });
47
48
  try {
48
49
  return text ? JSON.parse(text) : null;
49
50
  } catch (_) {
@@ -149,8 +150,9 @@ async function fetchImageJson(url, options) {
149
150
  let response;
150
151
  try {
151
152
  response = await fetch(url, options);
152
- return { res: response, body: await readJson(response) };
153
+ return { res: response, body: await readJson(response, response.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json) };
153
154
  } catch (err) {
155
+ if (err instanceof ProviderContractError) throw err;
154
156
  throw transportFetchError(err, response);
155
157
  }
156
158
  }
@@ -287,7 +289,11 @@ export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, siz
287
289
  * 返回 oss:// 形式 URL;调用模型时请求头须带 X-DashScope-OssResourceResolve: enable
288
290
  * (submitVideo 的 s2v 分支已自动处理)。
289
291
  */
290
- export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs = T_UPLOAD, signal }) {
292
+ export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, filename, timeoutMs = T_UPLOAD, signal }) {
293
+ if ((filePath === undefined) === (bytes === undefined)) throw new TypeError('上传须且只能提供本地路径或明确字节');
294
+ const name = filePath !== undefined ? path.basename(filePath) : filename;
295
+ if (typeof name !== 'string' || !name || name.length > 128 || /[\/\\\0]/.test(name)
296
+ || bytes !== undefined && (!(bytes instanceof Uint8Array) || !bytes.length)) throw new TypeError('上传字节或文件名无效');
291
297
  const apiBase = dashscopeApiBase(baseUrl);
292
298
  const u = new URL(`${apiBase}/uploads`);
293
299
  u.searchParams.set('action', 'getPolicy');
@@ -296,7 +302,6 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
296
302
  const pbody = await readJson(pres);
297
303
  if (!pres.ok || !pbody || !pbody.data) throw providerError(pres.status, pbody, '获取上传凭证失败', pres.headers);
298
304
  const pol = pbody.data;
299
- const name = path.basename(filePath);
300
305
  const keyPath = `${pol.upload_dir}/${Date.now()}-${name}`;
301
306
  const fd = new FormData();
302
307
  fd.append('OSSAccessKeyId', pol.oss_access_key_id);
@@ -306,7 +311,7 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
306
311
  fd.append('x-oss-forbid-overwrite', pol.x_oss_forbid_overwrite);
307
312
  fd.append('key', keyPath);
308
313
  fd.append('success_action_status', '200');
309
- fd.append('file', new Blob([fs.readFileSync(filePath)]), name);
314
+ fd.append('file', new Blob([bytes ?? fs.readFileSync(filePath)]), name);
310
315
  const up = await fetch(pol.upload_host, { method: 'POST', body: fd, signal: timeoutSignal(timeoutMs, signal) });
311
316
  if (up.status !== 200) throw new Error(`临时上传失败:HTTP ${up.status}`);
312
317
  return 'oss://' + keyPath;
@@ -366,7 +371,7 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
366
371
  signal: timeoutSignal(timeoutMs, signal),
367
372
  body: JSON.stringify({ model, input: { text, voice } })
368
373
  });
369
- const body = await readJson(res);
374
+ const body = await readJson(res, res.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json);
370
375
  if (!res.ok) {
371
376
  const error = providerError(res.status, body, '合成失败', res.headers);
372
377
  if (res.status === 404 && (/NotFound\.Model|model_not_found/i.test(error.providerCode)
@@ -614,7 +619,7 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
614
619
  throw networkError(err); // 网络层失败归类为 network
615
620
  }
616
621
  if (!res.ok) {
617
- const text = await res.text().catch(() => '');
622
+ const text = await readProviderText(res, { maxBytes: PROVIDER_RESPONSE_LIMITS.json, stage: 'response', acceptance: 'unknown' });
618
623
  let body = null;
619
624
  try { body = JSON.parse(text); } catch (_) { /* keep null */ }
620
625
  throw providerError(res.status, body, '视觉理解失败', res.headers);
@@ -655,15 +660,19 @@ export function extFromUrl(url, fallback = 'bin') {
655
660
  return fallback;
656
661
  }
657
662
 
658
- export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal } = {}) {
663
+ export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal, maxBytes = PROVIDER_RESPONSE_LIMITS.video } = {}) {
659
664
  const res = await fetch(url, { signal: timeoutSignal(timeoutMs, signal) });
660
- if (!res.ok) throw new Error(`下载结果失败:HTTP ${res.status}`);
665
+ if (!res.ok) {
666
+ await res.body?.cancel();
667
+ throw Object.assign(new Error(`下载结果失败:HTTP ${res.status}`), { stage: 'download', category: 'provider', httpStatus: res.status, acceptance: 'accepted' });
668
+ }
661
669
  if (!res.body) throw new Error('下载结果失败:响应没有内容');
662
670
  ensurePrivateDir(path.dirname(filePath));
663
671
  const tmp = privateSibling(filePath, 'download');
664
672
  try {
665
673
  const output = fs.createWriteStream(tmp, { flags: 'wx', mode: 0o600 });
666
- await pipeline(Readable.fromWeb(res.body), output);
674
+ const closed = new Promise(resolve => output.once('close', resolve));
675
+ await pipeline(Readable.from(boundedProviderBody(res, { maxBytes, stage: 'download', acceptance: 'accepted' })), output).finally(() => closed);
667
676
  const bytes = fs.statSync(tmp).size;
668
677
  fs.renameSync(tmp, filePath);
669
678
  chmodPrivateFile(filePath);
@@ -1,12 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  import { normalizeCoreOptions } from './core-contract.js';
4
- import { createCoreRuntime } from './core-runtime.js';
4
+ import { createCoreRuntime, inspectCoreWriterLease } from './core-runtime.js';
5
5
  import { createCommandService } from './command-service.js';
6
6
  import { normalizeGenerationInput } from './generation-input.js';
7
7
  import { createProviderTaskRunner } from './provider-task-runner.js';
8
8
  import { createImageFlowService } from './image-flow.js';
9
9
  import { createLocalCreationService } from './local-creation.js';
10
+ import { createLocalOperationService } from './local-operations.js';
11
+ import { reservePluginOperation } from './plugin-operations.js';
12
+ import { invokeReservedPlugin } from './plugin-execution.js';
10
13
 
11
14
  // Internal first-slice execution surface, not a public API or an HTTP allowlist.
12
15
  const COMMAND_MODES = Object.freeze({
@@ -15,7 +18,11 @@ const COMMAND_MODES = Object.freeze({
15
18
  'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
16
19
  'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
17
20
  'flow.inspect': 'reader', 'flow.list': 'reader',
18
- 'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer'
21
+ 'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer', 'creation.generate': 'writer',
22
+ 'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer',
23
+ 'creation.root': 'reader', 'creation.lookup': 'reader',
24
+ 'execution.init': 'writer', 'operation.accept': 'writer', 'operation.transition': 'writer',
25
+ 'operation.inspect': 'reader', 'operation.lookup': 'reader', 'operation.list': 'reader'
19
26
  });
20
27
 
21
28
  export class ApplicationRuntimeError extends Error {
@@ -31,7 +38,7 @@ function closed() {
31
38
  }
32
39
 
33
40
  /** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
34
- export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveTaskAdapter } = {}) {
41
+ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter } = {}) {
35
42
  const root = normalizeCoreOptions({ dataRoot }).dataRoot;
36
43
  if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
37
44
  throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
@@ -39,6 +46,7 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
39
46
  if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
40
47
  throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
41
48
  }
49
+ if (resolveAudioCandidates !== undefined && typeof resolveAudioCandidates !== 'function') throw new TypeError('音频候选解析须为函数');
42
50
  let state = 'open';
43
51
  const queue = [];
44
52
  const active = new Set();
@@ -80,7 +88,16 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
80
88
  try {
81
89
  runtime.start();
82
90
  result = await job.action(runtime);
83
- } catch (error) { failed = true; operationError = error; }
91
+ } catch (error) {
92
+ failed = true; operationError = error;
93
+ if (job.mode === 'reader' && error?.code === 'IRIS_ARTIFACT_INDEX_INVALID' && error.missingFile === true) {
94
+ const lease = inspectCoreWriterLease(root);
95
+ if (lease.status === 'present' && lease.ownerStatus === 'alive') {
96
+ operationError = new ApplicationRuntimeError('IRIS_CORE_DATA_ROOT_BUSY', '作品读取遇到活动写入,请在写入结束后显式重试');
97
+ Object.defineProperty(operationError, 'cause', { value: error });
98
+ }
99
+ }
100
+ }
84
101
  try { await (job.disposal || runtime.dispose()); }
85
102
  catch (error) {
86
103
  cleanupFailure ||= error;
@@ -150,9 +167,10 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
150
167
  if (runtime.signal.aborted) throw aborted();
151
168
  return adapter;
152
169
  } } : {};
153
- if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates,
170
+ if (name === 'execution.init' || name.startsWith('operation.')) return createLocalOperationService(runtime).execute(name, ownedInput);
171
+ if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates,
154
172
  onPrepared: options?.onPrepared }).execute(name, ownedInput);
155
- if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates }).execute(name, ownedInput);
173
+ if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates }).execute(name, ownedInput);
156
174
  return createCommandService(runtime, ports).execute(name, ownedInput);
157
175
  }, options);
158
176
  }
@@ -170,6 +188,15 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
170
188
  }, options);
171
189
  }
172
190
 
191
+ /** Explicit trusted assembly only. No HTTP/MCP route or persistent Operation action is added. */
192
+ async function executePluginOperation(registry, id, input, { allowedOperations = [], dependency, signal } = {}) {
193
+ const reserved = reservePluginOperation(registry, id, input, allowedOperations, dependency);
194
+ try {
195
+ return await schedule(reserved.mode, runtime => runtime.run(reserved.mode === 'reader' ? 'inspect' : 'execute',
196
+ () => invokeReservedPlugin(runtime, reserved)), { signal });
197
+ } finally { reserved.release(); }
198
+ }
199
+
173
200
  function dispose() {
174
201
  if (disposePromise) return disposePromise;
175
202
  state = 'closing';
@@ -182,5 +209,5 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
182
209
  return disposePromise;
183
210
  }
184
211
 
185
- return Object.freeze({ execute, generateImage, run, dispose, snapshot });
212
+ return Object.freeze({ execute, generateImage, executePluginOperation, run, dispose, snapshot });
186
213
  }