@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 0.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -1
- package/README.en.md +20 -13
- package/README.md +20 -13
- package/bin/dsh-iris.js +143 -51
- package/docs/AGENT_CORE_QUICKSTART.md +2 -2
- package/docs/AGENT_QUICKSTART.md +1 -1
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +20 -4
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +1 -1
- package/docs/CORE_CONNECTIONS.md +3 -1
- package/docs/CORE_QUICKSTART.md +7 -5
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
- package/docs/CORE_SERVICE.md +9 -5
- package/docs/CORE_SKILLS.md +2 -2
- package/docs/DSH_COMPATIBILITY.md +11 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RUNTIME.md +2 -2
- package/docs/HEADLESS_CLI.md +29 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +2 -2
- package/docs/IMAGE_GENERATION.md +1 -1
- package/docs/LOCAL_API.md +2 -2
- package/docs/LOCAL_API_CONTRACT.md +17 -15
- package/docs/LOCAL_CREATION.md +2 -2
- package/docs/LOCAL_OPERATIONS.md +2 -2
- package/docs/LOCAL_SELECTION.md +1 -1
- package/docs/LOCAL_WEB.md +11 -5
- package/docs/MCP.md +30 -9
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +1 -1
- package/docs/PLUGINS.md +9 -5
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +15 -2
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +8 -4
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +13 -7
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +3 -3
- package/docs/WEB_RECOVERY.md +2 -2
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +2 -2
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +2 -2
- package/lib/actions.js +5 -4
- package/lib/adapters.js +58 -41
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +44 -15
- package/lib/application-writer.js +193 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +62 -25
- package/lib/comfyui-adapter.js +3 -2
- package/lib/comfyui-client.js +10 -1
- package/lib/command-service.js +18 -5
- package/lib/config.js +13 -9
- package/lib/configured-image-runtime.js +28 -9
- package/lib/connection-skill.js +9 -0
- package/lib/core-artifact-store.js +68 -26
- package/lib/core-bridge-tools.js +5 -3
- package/lib/core-capability-catalog.js +21 -11
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-service-client.js +6 -2
- package/lib/core-tasks.js +86 -11
- package/lib/creation-request-store.js +8 -5
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +1 -0
- package/lib/flow-store.js +64 -17
- package/lib/image-flow.js +39 -17
- package/lib/index.js +10 -5
- package/lib/local-api-projection.js +6 -2
- package/lib/local-api.js +137 -49
- package/lib/local-creation.js +12 -5
- package/lib/local-operations.js +2 -2
- package/lib/local-request-root.js +2 -2
- package/lib/local-web-assets.js +2 -0
- package/lib/mcp-http.js +82 -9
- package/lib/mcp-server.js +23 -1
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +25 -8
- package/lib/music-transport.js +4 -2
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +83 -38
- package/lib/operation-store.js +6 -6
- package/lib/plugins.d.ts +25 -6
- package/lib/plugins.js +2 -1
- package/lib/provider-adapter.js +2 -2
- package/lib/provider-adapters.js +37 -5
- package/lib/provider-catalog.js +33 -16
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +52 -22
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +5 -5
- package/lib/provider-health.js +10 -5
- package/lib/provider-monitor.js +5 -3
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +3 -2
- package/lib/provider-registry.js +15 -5
- package/lib/provider-response.js +10 -0
- package/lib/provider-settings.js +14 -12
- package/lib/provider-task-runner.js +113 -37
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +2 -0
- package/lib/web/app.js +107 -46
- package/lib/web/connections.js +151 -12
- package/lib/web/creation.js +29 -25
- package/lib/web/flow-view.js +4 -3
- package/lib/web/gallery.js +17 -16
- package/lib/web/index.html +58 -5
- package/lib/web/media-client.js +18 -4
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +4 -2
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/providers.js +34 -13
- package/lib/web/recovery.js +2 -2
- package/lib/web/styles.css +54 -1
- package/lib/web/transport.js +6 -1
- package/lib/workbench-artifacts.js +3 -2
- package/package.json +5 -2
- package/skills/iris-core/references/comfyui.md +1 -1
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +3 -3
package/lib/web/index.html
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
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
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
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
|
+
<button id="network-tab" class="nav-item" type="button" aria-current="false" hidden><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/><path d="M12 8v4M5 16v-4h14v4"/></svg> 网络出口</button>
|
|
32
|
+
<button id="plugins-tab" class="nav-item" type="button" aria-current="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3h6v5h5v7h-5v6H9v-6H4V8h5z"/></svg> 插件</button>
|
|
31
33
|
<div id="plugin-navigation" class="plugin-navigation" role="group" aria-label="已启用扩展" hidden></div>
|
|
32
34
|
</nav>
|
|
33
35
|
<details id="recent-panel" class="recent-panel" open><summary><span>最近流程 <span id="history-count" class="count">0</span></span><span class="disclosure">收起</span></summary>
|
|
@@ -99,6 +101,41 @@
|
|
|
99
101
|
<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
102
|
</details>
|
|
101
103
|
<p id="plugin-feedback" class="field-note" role="status" aria-live="polite" hidden></p>
|
|
104
|
+
<section id="network-panel" hidden aria-labelledby="network-title">
|
|
105
|
+
<div class="page-heading management-heading"><div><p class="eyebrow">CONNECTION ROUTES</p><h1 id="network-title">网络出口</h1><p class="muted">功能选择模型,供应商连接选择出口。未绑定时继承当前环境。</p></div><div class="page-actions"><button id="network-refresh" class="quiet" type="button">重新读取</button><button id="network-new" class="secondary" type="button" hidden>+ 添加节点</button></div></div>
|
|
106
|
+
<p id="network-feedback" class="field-note" role="status" aria-live="polite" hidden></p>
|
|
107
|
+
<dl id="network-stats" class="overview-stats" aria-label="出口配置摘要"></dl>
|
|
108
|
+
<div id="network-leave-confirm" class="inline-confirm" hidden><p>放弃未保存编辑并离开?保存结果待核对时,请重新读取后检查。</p><div class="export-actions"><button id="network-leave-yes" class="secondary" type="button">放弃编辑并继续</button><button id="network-leave-no" class="quiet" type="button">继续编辑</button></div></div>
|
|
109
|
+
<nav id="network-sections" class="editor-tabs provider-sections" aria-label="网络出口分组"><button type="button" data-network-section="bindings" aria-pressed="true">供应商出口</button><button type="button" data-network-section="pools" aria-pressed="false">出口池</button><button type="button" data-network-section="nodes" aria-pressed="false">节点</button><button type="button" data-network-section="doctor" aria-pressed="false">检测</button></nav>
|
|
110
|
+
<div id="network-setup" class="card network-setup" hidden><h2>准备出口配置</h2><p id="network-setup-note" class="muted"></p><button id="network-init" type="button" hidden>初始化出口配置</button><details class="compact-details"><summary><span>另存启用与兼容说明</span><span class="disclosure">展开</span></summary><p>旧供应商目录与已有任务保持原样。先在终端另存启用,再用新文件正常启动 Core;页面不会替换正在运行的配置。</p><pre>dsh-iris network routing copy --network-config /private/network-v2.json --input '{"source":"/private/network-v1.json"}'
|
|
111
|
+
dsh-iris network catalog create --network-config /private/network-v2.json --provider-config /private/providers.json --output /private/providers-routed.json</pre><p>第一行仅用于沿用已有 v1 资源,与页面初始化二选一。已初始化 v2 时,只需第二行另存供应商目录;新目标不能覆盖已有文件。</p><p>启动时显式选择新目录和同一网络文件,并开启 --allow-network-management。普通 MCP 连接不能管理出口或读取代理认证。</p></details></div>
|
|
112
|
+
<div id="network-list" class="service-grid network-grid"></div>
|
|
113
|
+
<p id="network-empty" class="management-empty" hidden></p>
|
|
114
|
+
<section id="network-editor" class="card management-editor network-editor" hidden aria-labelledby="network-editor-title">
|
|
115
|
+
<div class="editor-heading"><button id="network-back" class="quiet back-button" type="button">← 返回列表</button><div class="section-heading"><h2 id="network-editor-title">编辑出口</h2><span id="network-editor-state" class="badge">未保存</span></div></div>
|
|
116
|
+
<form id="network-form"><fieldset id="network-fields">
|
|
117
|
+
<div id="network-node-fields" hidden><div class="provider-two-fields"><label class="field-label">稳定 ID<input id="network-node-id" maxlength="64" pattern="[A-Za-z0-9][A-Za-z0-9._-]*" autocomplete="off"></label><label class="field-label">名称<input id="network-node-label" maxlength="120" autocomplete="off"></label></div><div class="network-node-address"><label class="field-label">协议<select id="network-node-protocol"><option value="http">HTTP</option><option value="https">HTTPS</option></select></label><label class="field-label">主机<input id="network-node-host" maxlength="253" placeholder="127.0.0.1" autocomplete="off"></label><label class="field-label">端口<input id="network-node-port" type="number" min="1" max="65535" placeholder="7890"></label></div><label class="mini-check"><input id="network-node-enabled" type="checkbox" checked><span>启用节点</span></label><label class="mini-check"><input id="network-node-auth" type="checkbox"><span>使用认证环境变量</span></label><div id="network-auth-fields" class="provider-two-fields" hidden><label class="field-label">用户名变量名<input id="network-node-username" maxlength="128" placeholder="IRIS_PROXY_USERNAME" autocomplete="off"></label><label class="field-label">密码变量名<input id="network-node-password" maxlength="128" placeholder="IRIS_PROXY_PASSWORD" autocomplete="off"></label></div><p class="field-note">填写已有代理的地址,不是单独的公网 IP。认证只保存变量名,值由 Core 进程解析;不会修改 Clash 或系统路由。</p></div>
|
|
118
|
+
<div id="network-pool-fields" hidden><div class="provider-two-fields"><label class="field-label">稳定 ID<input id="network-pool-id" maxlength="64" pattern="[A-Za-z0-9][A-Za-z0-9._-]*" autocomplete="off"></label><label class="field-label">名称<input id="network-pool-label" maxlength="120" autocomplete="off"></label></div><label class="field-label">选择方式<select id="network-pool-mode"><option value="ordered">有序主备</option><option value="fixed">固定出口</option></select></label><label class="mini-check"><input id="network-pool-enabled" type="checkbox" checked><span>启用出口池</span></label><h3>允许的候选</h3><div id="network-candidates" class="network-candidates"></div><div class="network-candidate-add"><select id="network-candidate-choice" aria-label="添加允许的出口候选"></select><button id="network-candidate-add" class="secondary" type="button">添加候选</button></div><p class="field-note">只在业务发送前的连接预检中选择备用。固定池最多一个候选;空池可保存草稿,绑定后会阻止新请求。直连必须明确添加。</p></div>
|
|
119
|
+
<div id="network-binding-fields" hidden><p id="network-binding-provider" class="network-binding-name"></p><label class="field-label">此连接的出口<select id="network-binding-pool"></select></label><p id="network-binding-note" class="field-note"></p></div>
|
|
120
|
+
<div class="provider-footer"><button id="network-save" type="submit">保存设置</button><button id="network-cancel" class="quiet" type="button">取消更改</button><button id="network-remove" class="quiet danger" type="button" hidden>删除</button></div>
|
|
121
|
+
</fieldset></form>
|
|
122
|
+
<div id="network-delete-confirm" class="inline-confirm" hidden><p>确认删除此配置引用?仍被引用的资源会拒绝删除,作品与任务历史保留。</p><button id="network-delete-yes" class="secondary danger" type="button">确认删除</button><button id="network-delete-no" class="quiet" type="button">返回</button></div>
|
|
123
|
+
</section>
|
|
124
|
+
<section id="network-doctor" hidden aria-labelledby="network-doctor-title"><div class="card network-doctor-card"><div class="section-heading"><div><h2 id="network-doctor-title">Core 网络检测</h2><p class="field-note">状态来自运行 Core 的机器;打开、刷新此页不会发起外部请求。</p></div><span class="badge">静态检查</span></div><dl id="network-environment" class="network-facts"></dl><details class="compact-details"><summary><span>代理并存与未知状态</span><span class="disclosure">展开</span></summary><p>显式出口池只控制 Iris 自己的请求;Clash 与 Iris 同时存在不等于冲突。系统 TUN、宿主 dispatcher 和网关后半段的路线无法仅靠配置证明,保持“未知”。</p></details></div><div class="card network-probe-card"><h2>测试这条连接</h2><p class="field-note">点击后最多一次 GET、8 秒、16 KiB 响应,不跟随重定向、不携带供应商 Key,也不会生成媒体。</p><form id="network-probe-form"><div class="provider-two-fields"><label class="field-label">供应商连接<select id="network-probe-provider"></select></label><label class="field-label">测试目标<select id="network-probe-target"><option value="provider">已保存的服务地址</option><option value="ip">IP 观察 · JSON</option><option value="custom">自定义 GET</option></select></label></div><label id="network-probe-url-field" class="field-label" hidden>测试 URL<input id="network-probe-url" type="url" maxlength="2048" placeholder="https://api.ipify.org?format=json" autocomplete="off"></label><div class="provider-footer"><button id="network-probe-start" type="submit">开始检测</button><button id="network-probe-cancel" class="quiet" type="button" hidden>取消检测</button></div></form><div id="network-probe-result" class="network-probe-result" role="status" aria-live="polite" hidden></div></div></section>
|
|
125
|
+
<details id="network-other-bindings" class="card compact-details" hidden><summary><span>其他目录与遗留绑定</span><span class="disclosure">展开</span></summary><p class="field-note">按保存的身份展示,不扫描其他供应商文件。解除后该连接恢复继承环境。</p><div id="network-other-list"></div></details>
|
|
126
|
+
</section>
|
|
127
|
+
<section id="plugins-panel" class="plugins-panel" hidden aria-labelledby="plugins-title">
|
|
128
|
+
<div class="page-heading management-heading"><div><p class="eyebrow">WORKSPACE PLUGINS</p><h1 id="plugins-title">插件,让能力接续。</h1><p class="muted">查看当前加载、启用与客户端授权。</p></div><button id="plugins-refresh" class="quiet" type="button" disabled>重新读取</button></div>
|
|
129
|
+
<dl id="plugins-stats" class="overview-stats" aria-label="插件状态"></dl>
|
|
130
|
+
<p id="plugins-feedback" class="field-note" role="status" aria-live="polite" hidden></p>
|
|
131
|
+
<p id="plugins-authorizations-note" class="field-note" role="status" hidden></p>
|
|
132
|
+
<p id="plugins-updated" class="field-note" hidden></p>
|
|
133
|
+
<div id="plugins-list" class="plugin-status-list" aria-busy="false"></div>
|
|
134
|
+
<div id="plugins-empty" class="management-empty card" hidden><span class="empty-mark" aria-hidden="true">◇</span><h2>从一个插件开始</h2><p>当前服务没有加载插件。选择可信插件后,使用启动配置明确启用。</p></div>
|
|
135
|
+
<section id="plugins-missing-section" class="card plugin-missing-section" hidden aria-labelledby="plugins-missing-title"><h2 id="plugins-missing-title">授权引用的未加载业务</h2><p class="field-note">保留原授权和历史记录;核对准确版本后再决定是否调整。</p><ul id="plugins-missing" class="plugin-grants"></ul></section>
|
|
136
|
+
<div class="plugin-status-footer"><p class="field-note">加载、启用、授权分别生效。握手记录不代表持续在线,配置满足条件不等于业务实测成功。</p><button id="plugins-connections" class="secondary" type="button" hidden>管理客户端授权</button></div>
|
|
137
|
+
<details class="card compact-details plugin-status-help"><summary><span>如何启用与定位问题</span><span class="disclosure">展开</span></summary><p>在启动选择文件中用 entries 加载插件、operations 启用业务、panel 选择页面或预览;客户端还需单独选择业务与权限。修改后正常停止并重启 Core,当前页面不安装或热加载插件。</p><p>启动失败时服务可能尚未运行,请查看启动终端的错误码、阶段和配置字段。可先检查明确的选择文件:</p><code>dsh-iris plugins inspect --plugin-config /absolute/startup.json</code><p>inspect 会执行可信插件的加载与释放;只对确认可信的插件使用。查看状态和重新读取不会调用插件业务、探测模型或自动授权。</p></details>
|
|
138
|
+
</section>
|
|
102
139
|
<section id="plugin-page-panel" hidden aria-labelledby="plugin-page-title">
|
|
103
140
|
<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
141
|
<div id="plugin-page-slot" class="card plugin-slot"></div>
|
|
@@ -113,7 +150,7 @@
|
|
|
113
150
|
</section>
|
|
114
151
|
<section id="gallery-panel" hidden aria-labelledby="gallery-title">
|
|
115
152
|
<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>
|
|
153
|
+
<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="video" aria-pressed="false" hidden>视频</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
154
|
<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
155
|
<p id="gallery-feedback" class="muted" role="status" aria-live="polite"></p>
|
|
119
156
|
<div id="gallery-grid" class="gallery-grid" aria-busy="false"></div>
|
|
@@ -131,7 +168,8 @@
|
|
|
131
168
|
<section id="connection-editor" class="card connection-editor management-editor" hidden aria-labelledby="connection-editor-title">
|
|
132
169
|
<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
170
|
<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="
|
|
171
|
+
<nav class="editor-tabs connection-steps" aria-label="接入步骤"><button type="button" data-connection-view="settings" aria-pressed="true">1 · 选择与授权</button><button type="button" data-connection-view="delivery" aria-pressed="false">2 · 获取配置</button><button type="button" data-connection-view="verification" aria-pressed="false">3 · 核对接入</button><button type="button" data-connection-view="details" aria-pressed="false">详情与管理</button></nav>
|
|
172
|
+
<p id="connection-unsaved-note" class="field-note connection-draft-note" hidden>功能或权限有未保存的修改。请先保存,再生成配置或检查接入;客户端仍按原授权访问。</p>
|
|
135
173
|
<div id="connection-view-settings">
|
|
136
174
|
<form id="connection-form">
|
|
137
175
|
<fieldset id="connection-fields">
|
|
@@ -140,6 +178,7 @@
|
|
|
140
178
|
<div id="connection-plugin-section" hidden><h3>插件业务</h3><div id="connection-plugin-options" class="scope-options"></div><p class="field-note">默认不开放。选择具体业务后,再选择允许的操作权限;安装插件不会自动授权。</p></div>
|
|
141
179
|
<h3>操作权限</h3><div id="connection-scopes" class="scope-options"></div><p class="field-note">只读可浏览记录;插件业务还需允许执行。绘图还需开放“导入与创建”和“执行流程”。执行可能产生供应商费用;停止只结束本地等待。</p>
|
|
142
180
|
<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>
|
|
181
|
+
<div class="connection-grant-review"><span class="field-note">本次授权</span><p id="connection-grant-summary"></p></div>
|
|
143
182
|
<div class="connection-form-actions"><button id="connection-save" type="submit">创建连接</button><span id="connection-save-note" class="muted">创建只保存配置,不调用模型。</span></div>
|
|
144
183
|
</fieldset>
|
|
145
184
|
</form>
|
|
@@ -147,6 +186,7 @@
|
|
|
147
186
|
<div id="connection-view-delivery" hidden>
|
|
148
187
|
<section id="connection-export" class="connection-section" hidden aria-labelledby="export-title">
|
|
149
188
|
<h3 id="export-title">接入配置</h3><p class="field-note">按已保存的授权生成。把配置加入客户端,重新连接后在这里刷新。</p>
|
|
189
|
+
<label class="template-choice" for="connection-location">Agent 是否运行在 Core 所在机器?<select id="connection-location"><option value="same">同一台机器</option><option value="other">另一台机器</option><option value="unknown">还不确定</option></select></label><p id="connection-location-note" class="field-note"></p>
|
|
150
190
|
<label class="template-choice" for="connection-template">接入模板<select id="connection-template"><option value="">手动选择</option></select></label>
|
|
151
191
|
<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
192
|
<p id="connection-template-note" class="field-note" hidden></p>
|
|
@@ -154,9 +194,20 @@
|
|
|
154
194
|
<p id="connection-transport-note" class="field-note"></p>
|
|
155
195
|
<p class="field-note">按所选客户端合并 JSON 或 TOML,保留已有配置,然后在客户端启用。其他客户端也可手动填写单条 entry。</p>
|
|
156
196
|
<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>
|
|
197
|
+
<div class="connection-next"><button id="connection-verify-next" class="secondary" type="button">下一步:核对接入 →</button><p class="field-note">将配置合并到客户端后,启动或重新连接 MCP;不覆盖已有配置。</p></div>
|
|
157
198
|
<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
199
|
</section>
|
|
159
200
|
</div>
|
|
201
|
+
<div id="connection-view-verification" hidden>
|
|
202
|
+
<div class="connection-verification-heading"><h3>接入核对</h3><p class="field-note">配置、Iris 端检查和客户端握手分别确认。这里不会安装客户端或提交媒体任务。</p></div>
|
|
203
|
+
<ol class="connection-evidence">
|
|
204
|
+
<li><span class="connection-evidence-number" aria-hidden="true">1</span><div><h3 id="connection-export-state">本页未记录当前配置导出</h3><p id="connection-export-evidence" class="field-note"></p></div></li>
|
|
205
|
+
<li><span class="connection-evidence-number" aria-hidden="true">2</span><div><h3 id="connection-check-state">尚未检查当前配置</h3><p id="connection-check-evidence" class="field-note"></p><ul id="connection-check-issues" class="connection-check-issues"></ul><details id="connection-check-tool-details" class="tool-details"><summary><span>当前允许的工具</span><span class="disclosure">展开</span></summary><p id="connection-check-tools-note" class="field-note"></p><ul id="connection-check-tools" class="connection-check-tools"></ul></details></div></li>
|
|
206
|
+
<li><span class="connection-evidence-number" aria-hidden="true">3</span><div><h3 id="connection-client-state">等待客户端握手与声明</h3><ul id="connection-client-evidence" class="connection-client-evidence"></ul><p class="field-note">名称/版本属于可选声明,未收到不等于调用失败。仅桥接或 CLI 自检握手不作为 Agent 启用证据;握手不代表持续在线、工具已被发现或媒体实测。</p></div></li>
|
|
207
|
+
</ol>
|
|
208
|
+
<div class="export-actions"><button id="connection-check" class="secondary" type="button">检查 Iris 端与接入记录</button><button id="connection-return-settings" class="quiet" type="button">返回功能与权限</button></div>
|
|
209
|
+
<details class="tool-details"><summary><span>客户端还没有出现?</span><span class="disclosure">展开</span></summary><p class="field-note">确认 Core 仍在运行,Agent 与 Core 的部署位置正确,配置已合并并在客户端启用。客户端可在已允许工具中发起一次只读调用;没有对应工具时不要擅自扩大权限。stdio 需重启桥进程加载新工具表。</p><p class="field-note">若需要从客户端所在机器验证配置、传输与临时握手,可使用既有命令:</p><code class="skill-install-command">dsh-iris mcp check --connection-file /absolute/client.json</code><p class="field-note">使用真正的 MCP client.json;这不是工作台登录 Token 或 server-grants.json。自检通过仍不代表 Agent 已启用 MCP。</p></details>
|
|
210
|
+
</div>
|
|
160
211
|
<div id="connection-view-details" hidden><dl id="connection-facts" hidden></dl>
|
|
161
212
|
<section id="connection-revoke-section" class="connection-section" hidden>
|
|
162
213
|
<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>
|
|
@@ -179,13 +230,14 @@
|
|
|
179
230
|
<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
231
|
<section id="provider-editor" class="card provider-editor management-editor" hidden aria-labelledby="provider-editor-title">
|
|
181
232
|
<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>
|
|
233
|
+
<button id="provider-network-link" class="quiet network-link" type="button" hidden>配置此连接的出口 →</button>
|
|
182
234
|
<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
235
|
<form id="provider-form"><fieldset id="provider-fields">
|
|
184
236
|
<section id="provider-view-connection">
|
|
185
237
|
<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
238
|
<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
239
|
<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"
|
|
240
|
+
<div class="provider-two-fields provider-credentials-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="env">引用环境变量</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 id="provider-key-env-field" hidden><label class="field-label" for="provider-key-env">环境变量名称</label><input id="provider-key-env" maxlength="128" pattern="[A-Za-z_][A-Za-z0-9_]{0,127}" autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="IRIS_MEDIA_KEY"><p class="field-note">只保存名称;值由 Core 进程环境提供,不读取 .env 文件。</p></div></div>
|
|
189
241
|
<label class="provider-enabled"><input id="provider-enabled" type="checkbox" checked> 启用此供应商</label>
|
|
190
242
|
<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
243
|
</section>
|
|
@@ -229,7 +281,7 @@
|
|
|
229
281
|
<summary><span>创作设置 <small id="input-summary">从一张图片开始</small></span><span class="disclosure">收起</span></summary>
|
|
230
282
|
<form id="edit-form">
|
|
231
283
|
<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>
|
|
284
|
+
<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="video" aria-pressed="false" hidden>生成视频</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
285
|
<div id="source-input">
|
|
234
286
|
<label for="image-file" class="field-label">原图</label>
|
|
235
287
|
<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>
|
|
@@ -240,7 +292,7 @@
|
|
|
240
292
|
<label id="prompt-label" for="edit-prompt" class="field-label">想改些什么?</label><textarea id="edit-prompt" rows="4" maxlength="12000" required placeholder="描述想保留的细节,以及想尝试的变化…"></textarea>
|
|
241
293
|
<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
294
|
<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="
|
|
295
|
+
<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="64" placeholder="使用模型默认值" autocomplete="off"><p id="edit-size-note" class="field-note">填写模型支持的尺寸;留空使用默认值。</p><div id="video-duration-field" hidden><label for="video-duration" class="field-label">时长(秒,可选)</label><input id="video-duration" type="number" min="1" max="60" step="any" placeholder="使用模型默认值"><p class="field-note">填写所选模型支持的时长(1–60 秒);目录不代表参数已实测。</p></div></details>
|
|
244
296
|
</fieldset>
|
|
245
297
|
<div class="model-management"><button id="manage-models" class="quiet inline-button" type="button">管理模型 ↗</button></div>
|
|
246
298
|
<button id="start-edit" class="full" type="submit" disabled>开始改图</button>
|
|
@@ -254,6 +306,7 @@
|
|
|
254
306
|
<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
307
|
<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
308
|
<div id="run-view" hidden>
|
|
309
|
+
<section id="video-result" class="video-result" hidden aria-label="视频结果"><p class="eyebrow">MOTION</p><h3>让想法动起来</h3><video id="output-video" controls playsinline preload="metadata" hidden></video><p id="video-result-note" class="field-note" role="status">原视频会保存在当前工作区。</p></section>
|
|
257
310
|
<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
311
|
<div id="image-pair" class="image-pair">
|
|
259
312
|
<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>
|
package/lib/web/media-client.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createRecoveryJournal, WorkbenchMediaError } from './recovery.js';
|
|
2
2
|
import { normalizeWorkflowValues } from './comfyui.js';
|
|
3
3
|
|
|
4
|
-
const CAPABILITY = { 'image-import': 'images.create', 'flow-create': 'runs.create', 'generation-create': 'generations.create', continue: 'runs.continue',
|
|
4
|
+
const CAPABILITY = { 'image-import': 'images.create', 'flow-create': 'runs.create', 'generation-create': 'generations.create', 'video-create': 'videos.create', continue: 'runs.continue',
|
|
5
5
|
'audio-import': 'audio.create', 'speech-create': 'speeches.create', 'transcription-create': 'transcriptions.create', selection: 'runs.select', stop: 'operations.stop' };
|
|
6
6
|
const TERMINAL = new Set(['succeeded', 'failed', 'canceled', 'interrupted']);
|
|
7
7
|
const RUN = /^run_[a-f0-9]{24}$/, ARTIFACT = /^artifact_[a-f0-9]{24}$/, OPERATION = /^op_[a-f0-9]{64}$/;
|
|
8
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'];
|
|
9
|
+
export const CREATIONS = ['image-import', 'flow-create', 'generation-create', 'video-create', 'audio-import', 'speech-create', 'transcription-create'];
|
|
10
10
|
export const IMPORTS = ['image-import', 'audio-import'];
|
|
11
11
|
const AUDIO_TYPES = ['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac'];
|
|
12
12
|
function audioInput(value, kind) {
|
|
@@ -16,6 +16,17 @@ function audioInput(value, kind) {
|
|
|
16
16
|
|| value.voice !== undefined && typeof value.voice !== 'string') throw new TypeError('音频流程输入无效');
|
|
17
17
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, item.trim()]));
|
|
18
18
|
}
|
|
19
|
+
function videoInput(value) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
21
|
+
|| !['modelRef', 'prompt'].every(key => typeof value[key] === 'string' && value[key].trim())
|
|
22
|
+
|| Object.keys(value).some(key => !['modelRef', 'prompt', 'size', 'duration'].includes(key))
|
|
23
|
+
|| value.size !== undefined && typeof value.size !== 'string'
|
|
24
|
+
|| value.duration !== undefined && (typeof value.duration !== 'number' || !Number.isFinite(value.duration) || value.duration < 1 || value.duration > 60)) {
|
|
25
|
+
throw new TypeError('视频流程需要明确模型、描述与有效参数');
|
|
26
|
+
}
|
|
27
|
+
return { modelRef: value.modelRef.trim(), prompt: value.prompt.trim(),
|
|
28
|
+
...(value.size !== undefined ? { size: value.size.trim() } : {}), ...(value.duration !== undefined ? { duration: value.duration } : {}) };
|
|
29
|
+
}
|
|
19
30
|
function flowInput(value, generate = false) {
|
|
20
31
|
const required = generate ? ['modelRef', 'prompt'] : ['sourceArtifactId', 'modelRef', 'prompt'];
|
|
21
32
|
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
@@ -61,7 +72,7 @@ export function createMediaClient({ client, session, origin, getStorage }) {
|
|
|
61
72
|
journal.remove(record.id);
|
|
62
73
|
} else if (CREATIONS.includes(record.kind)) {
|
|
63
74
|
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' };
|
|
75
|
+
const recipes = { 'flow-create': 'image-edit/v0', 'generation-create': 'image-generate/v0', 'video-create': 'video-generate/v0', 'speech-create': 'speech/v0', 'transcription-create': 'transcribe/v0' };
|
|
65
76
|
if (typeof result.id !== 'string' || !RUN.test(result.id) || result.recipe !== recipes[record.kind]
|
|
66
77
|
|| (record.artifactId ? result.source?.artifactId !== record.artifactId : result.source !== null)) throw new TypeError('流程来源或类型与本次创建不一致');
|
|
67
78
|
journal.remove(record.id);
|
|
@@ -86,6 +97,7 @@ export function createMediaClient({ client, session, origin, getStorage }) {
|
|
|
86
97
|
if (record.kind === 'image-import') return client.uploadImage(input, options);
|
|
87
98
|
if (record.kind === 'flow-create') return client.createRun(input, options);
|
|
88
99
|
if (record.kind === 'generation-create') return client.createGeneration(input, options);
|
|
100
|
+
if (record.kind === 'video-create') return client.createVideo(input, options);
|
|
89
101
|
if (record.kind === 'audio-import') return client.uploadAudio(input, options);
|
|
90
102
|
if (record.kind === 'speech-create') return client.createSpeech(input, options);
|
|
91
103
|
if (record.kind === 'transcription-create') return client.createTranscription(input, options);
|
|
@@ -165,6 +177,7 @@ export function createMediaClient({ client, session, origin, getStorage }) {
|
|
|
165
177
|
},
|
|
166
178
|
createRun(value) { return perform('flow-create', flowInput(value)); },
|
|
167
179
|
createGeneration(value) { return perform('generation-create', flowInput(value, true)); },
|
|
180
|
+
createVideo(value) { return perform('video-create', videoInput(value)); },
|
|
168
181
|
uploadAudio(audio) {
|
|
169
182
|
if (!(audio instanceof Blob) || !AUDIO_TYPES.includes(audio.type)) throw new TypeError('需要支持的音频文件');
|
|
170
183
|
return perform('audio-import', audio);
|
|
@@ -182,7 +195,8 @@ export function createMediaClient({ client, session, origin, getStorage }) {
|
|
|
182
195
|
} else if (['flow-create', 'generation-create'].includes(action.kind)) {
|
|
183
196
|
input = flowInput(input, action.kind === 'generation-create');
|
|
184
197
|
if (action.kind === 'flow-create' && input.sourceArtifactId !== action.artifactId) throw new TypeError('请提供原流程的来源图片与输入');
|
|
185
|
-
} else if (
|
|
198
|
+
} else if (action.kind === 'video-create') input = videoInput(input);
|
|
199
|
+
else if (['speech-create', 'transcription-create'].includes(action.kind)) {
|
|
186
200
|
input = audioInput(input, action.kind);
|
|
187
201
|
if (action.kind === 'transcription-create' && input.sourceArtifactId !== action.artifactId) throw new TypeError('请提供原转写音频');
|
|
188
202
|
} else input = action;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const $ = id => document.getElementById(id);
|
|
2
|
+
const el = (tag, text, className) => { const node = document.createElement(tag); if (text) node.textContent = text; if (className) node.className = className; return node; };
|
|
3
|
+
const options = (select, rows, selected) => { select.replaceChildren(...rows.map(([value, text]) => { const item = el('option', text); item.value = value; return item; })); if (rows.some(([value]) => value === selected)) select.value = selected; };
|
|
4
|
+
const statusLabel = value => ({ configured: '已配置 · 未实测', empty: '空池', disabled: '已停用', unavailable: '无可用候选' })[value] || value;
|
|
5
|
+
export function networkRouteLabel(route) {
|
|
6
|
+
return !route ? '出口未读取' : route.state === 'inherit' ? '继承环境' : route.state === 'blocked'
|
|
7
|
+
? '已阻止 · ' + (route.poolLabel || '核对网络配置') : route.poolLabel + ' · ' + (route.mode === 'fixed' ? '固定' : '有序主备');
|
|
8
|
+
}
|
|
9
|
+
export function createNetworkPanel({ describe, onChanged = () => {} }) {
|
|
10
|
+
let client, data, draft, section = 'bindings', busy = false, unknown = false, dirty = false, generation = 0, visible = false;
|
|
11
|
+
let candidates = [], pendingLeave, probeController, readController, writeController;
|
|
12
|
+
const feedback = (text, error = false) => { $('network-feedback').textContent = text; $('network-feedback').hidden = !text; $('network-feedback').classList.toggle('error', error); };
|
|
13
|
+
const active = (ticket, owner) => ticket === generation && owner === client;
|
|
14
|
+
const nodeLabel = value => value.kind === 'direct' ? '明确直连' : data.network.nodes.find(node => node.id === value.nodeId)?.label || value.nodeId;
|
|
15
|
+
function controls() {
|
|
16
|
+
$('network-fields').disabled = busy || unknown;
|
|
17
|
+
$('network-save').disabled = busy || unknown || draft?.kind === 'pool' && $('network-pool-mode').value === 'fixed' && candidates.length > 1;
|
|
18
|
+
$('network-save').textContent = busy ? '正在保存…' : '保存设置';
|
|
19
|
+
$('network-editor-state').textContent = busy ? '处理中…' : unknown ? '结果待核对' : dirty ? '未保存' : '已读取';
|
|
20
|
+
$('network-refresh').disabled = busy && !probeController;
|
|
21
|
+
$('network-new').disabled = busy || !data?.network;
|
|
22
|
+
$('network-init').disabled = busy;
|
|
23
|
+
$('network-back').disabled = busy; $('network-cancel').disabled = busy;
|
|
24
|
+
for (const button of document.querySelectorAll('#network-list button, [data-network-section], #network-other-list button')) button.disabled = busy || button.dataset.networkBlocked === 'true';
|
|
25
|
+
$('network-probe-start').disabled = busy || !data?.network || !data.providers.length;
|
|
26
|
+
for (const id of ['network-probe-provider', 'network-probe-target', 'network-probe-url']) $(id).disabled = busy;
|
|
27
|
+
$('network-probe-cancel').hidden = !probeController;
|
|
28
|
+
}
|
|
29
|
+
function changed() { dirty = true; controls(); }
|
|
30
|
+
function leave(action) { if (busy) return; if (dirty || unknown) { pendingLeave = action; $('network-leave-confirm').hidden = false; return; } action(); }
|
|
31
|
+
function closeEditor() { draft = null; candidates = []; dirty = unknown = false; $('network-editor').hidden = true; $('network-delete-confirm').hidden = true; $('network-leave-confirm').hidden = true; render(); }
|
|
32
|
+
function stats() {
|
|
33
|
+
$('network-stats').replaceChildren(...[['节点', data.network?.nodes.length || 0], ['出口池', data.network?.pools.length || 0], ['绑定', data.network?.bindings?.length || 0], ['继承环境', data.providers.filter(provider => provider.route.state === 'inherit').length]].map(([name, value]) => {
|
|
34
|
+
const row = el('div'); row.append(el('dt', name), el('dd', String(value))); return row;
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
function render() {
|
|
38
|
+
if (!data) {
|
|
39
|
+
$('network-doctor').hidden = section !== 'doctor'; $('network-list').hidden = true; $('network-new').hidden = true; $('network-empty').hidden = true;
|
|
40
|
+
$('network-setup').hidden = true; $('network-other-bindings').hidden = true; $('network-list').replaceChildren(); $('network-stats').replaceChildren();
|
|
41
|
+
for (const button of document.querySelectorAll('[data-network-section]')) button.setAttribute('aria-pressed', String(button.dataset.networkSection === section));
|
|
42
|
+
controls(); return;
|
|
43
|
+
}
|
|
44
|
+
stats(); $('network-sections').hidden = Boolean(draft);
|
|
45
|
+
$('network-list').hidden = Boolean(draft) || section === 'doctor'; $('network-doctor').hidden = Boolean(draft) || section !== 'doctor';
|
|
46
|
+
$('network-new').hidden = Boolean(draft) || !['nodes', 'pools'].includes(section) || !data.network;
|
|
47
|
+
$('network-new').textContent = section === 'nodes' ? '+ 添加节点' : '+ 添加出口池';
|
|
48
|
+
const ready = data.catalog.state === 'ready';
|
|
49
|
+
$('network-setup').hidden = Boolean(draft) || data.storage !== 'missing' && (section !== 'bindings' || ready);
|
|
50
|
+
$('network-init').hidden = data.storage !== 'missing';
|
|
51
|
+
$('network-setup-note').textContent = data.storage === 'missing' ? '本次服务指定的网络文件尚未创建。明确初始化后才能管理节点与池;不会改动供应商目录。'
|
|
52
|
+
: data.catalog.state === 'legacy' ? '当前是旧供应商目录。节点与池可以独立配置;绑定需先另存启用新目录,旧任务不会被迁移。'
|
|
53
|
+
: data.catalog.state === 'absent' ? '此服务未指定供应商目录。可以管理资源,指定目录后再配置连接出口。' : '网络文件版本或身份与供应商目录不匹配。请核对启动时指定的文件,当前绑定不会被借用。';
|
|
54
|
+
for (const button of document.querySelectorAll('[data-network-section]')) button.setAttribute('aria-pressed', String(button.dataset.networkSection === section));
|
|
55
|
+
const records = section === 'bindings' ? data.providers : data.network?.[section] || [];
|
|
56
|
+
$('network-empty').hidden = Boolean(draft) || section === 'doctor' || records.length > 0 || data.storage === 'missing';
|
|
57
|
+
$('network-empty').textContent = section === 'nodes' ? '还没有节点。添加已有 HTTP/HTTPS 代理,不会安装代理服务。' : section === 'pools' ? '还没有出口池。节点可复用;直连需要明确添加。' : '当前没有供应商连接。';
|
|
58
|
+
$('network-list').replaceChildren(...(section === 'doctor' ? [] : records).map((record, index) => {
|
|
59
|
+
const card = el('article', '', 'card network-card'); card.dataset.networkTone = String(index % 6); card.dataset.networkId = record.id;
|
|
60
|
+
const heading = el('div', '', 'section-heading');
|
|
61
|
+
heading.append(el('span', section === 'bindings' ? '连接' : section === 'nodes' ? record.protocol.toUpperCase() : record.mode === 'fixed' ? '固定' : '有序主备', 'network-kicker'),
|
|
62
|
+
el('span', section === 'bindings' ? record.route.state === 'blocked' ? '已阻止' : '配置路线' : section === 'nodes' ? !record.enabled ? '已停用' : record.credentialStatus === 'missing' ? '认证未就绪' : record.credentialStatus === 'invalid' ? '认证值无效' : '未实测' : statusLabel(record.status), 'badge'));
|
|
63
|
+
card.append(heading, el('h3', record.name || record.label));
|
|
64
|
+
card.append(el('p', section === 'bindings' ? networkRouteLabel(record.route) : section === 'nodes' ? record.host + ':' + record.port
|
|
65
|
+
: record.candidates.length ? record.candidates.map(nodeLabel).join(' → ') : '空候选 · 绑定后阻止新请求', 'network-route'));
|
|
66
|
+
card.append(el('p', section === 'bindings' ? !record.enabled ? '供应商已停用,绑定仍保留。' : '仅影响这条供应商连接。' : section === 'nodes'
|
|
67
|
+
? (record.auth ? '环境认证 · ' + record.credentialStatus : '无需代理认证') + ' · ' + record.referencedBy.length + ' 个池引用'
|
|
68
|
+
: (record.referencedBy?.length || 0) + ' 条绑定 · 连接预检后才派发业务', 'field-note'));
|
|
69
|
+
const button = el('button', section === 'bindings' ? '设置出口 →' : '编辑 →', 'quiet network-card-action'); button.type = 'button';
|
|
70
|
+
button.dataset.networkBlocked = String(section === 'bindings' && !ready); button.addEventListener('click', () => edit(section === 'bindings' ? 'binding' : section === 'nodes' ? 'node' : 'pool', record)); card.append(button);
|
|
71
|
+
return card;
|
|
72
|
+
}));
|
|
73
|
+
// Legacy/foreign references stay explicit and removable without scanning files.
|
|
74
|
+
const others = (data.network?.bindings || []).filter(item => item.catalogId !== data.catalog.catalogId || !data.providers.some(provider => provider.id === item.providerId));
|
|
75
|
+
$('network-other-bindings').hidden = Boolean(draft) || section !== 'bindings' || !others.length;
|
|
76
|
+
$('network-other-list').replaceChildren(...others.map(item => {
|
|
77
|
+
const row = el('div', '', 'network-other-row'); row.append(el('span', item.providerId + ' → ' + item.poolId + ' · ' + item.catalogId));
|
|
78
|
+
const remove = el('button', '解除绑定', 'quiet'); remove.type = 'button'; remove.addEventListener('click', () => {
|
|
79
|
+
const confirm = el('button', '确认解除', 'secondary danger'); confirm.type = 'button'; confirm.addEventListener('click', () => save('bindings/remove', { catalogId: item.catalogId, providerId: item.providerId, confirm: true }));
|
|
80
|
+
const cancel = el('button', '取消', 'quiet'); cancel.type = 'button'; cancel.addEventListener('click', render); row.replaceChildren(el('span', '解除 ' + item.providerId + '?恢复继承环境,历史保留。'), confirm, cancel);
|
|
81
|
+
}); row.append(remove); return row;
|
|
82
|
+
}));
|
|
83
|
+
const selected = $('network-probe-provider').value;
|
|
84
|
+
options($('network-probe-provider'), data.providers.map(provider => [provider.id, provider.name]), selected || data.providers[0]?.id);
|
|
85
|
+
controls();
|
|
86
|
+
}
|
|
87
|
+
function fill(prefix, values) { for (const [name, value] of Object.entries(values)) { const field = $('network-' + prefix + '-' + name); if (field.type === 'checkbox') field.checked = value; else field.value = value ?? ''; } }
|
|
88
|
+
function edit(kind, record) {
|
|
89
|
+
if (busy) return;
|
|
90
|
+
draft = { kind, id: record?.id || null, record, revision: data.network.revision, providerRevision: data.catalog.revision }; dirty = false; unknown = false; feedback('');
|
|
91
|
+
$('network-editor').hidden = false; $('network-delete-confirm').hidden = true;
|
|
92
|
+
for (const type of ['node', 'pool', 'binding']) $('network-' + type + '-fields').hidden = type !== kind;
|
|
93
|
+
// Disabled sections must not participate in native form validation.
|
|
94
|
+
for (const type of ['node', 'pool']) for (const field of $('network-' + type + '-fields').querySelectorAll('input,select')) field.disabled = type !== kind;
|
|
95
|
+
$('network-remove').hidden = !record || kind === 'binding';
|
|
96
|
+
$('network-editor-title').textContent = kind === 'binding' ? '设置供应商出口' : (record ? '编辑' : '添加') + (kind === 'node' ? '节点' : '出口池');
|
|
97
|
+
if (kind === 'node') {
|
|
98
|
+
fill('node', { id: record?.id || '', label: record?.label || '', protocol: record?.protocol || 'http', host: record?.host || '', port: record?.port || '', enabled: record?.enabled ?? true,
|
|
99
|
+
auth: Boolean(record?.auth), username: record?.auth?.usernameEnv || '', password: record?.auth?.passwordEnv || '' });
|
|
100
|
+
$('network-node-id').readOnly = Boolean(record); $('network-auth-fields').hidden = !$('network-node-auth').checked;
|
|
101
|
+
} else if (kind === 'pool') {
|
|
102
|
+
fill('pool', { id: record?.id || '', label: record?.label || '', mode: record?.mode || 'ordered', enabled: record?.enabled ?? true });
|
|
103
|
+
$('network-pool-id').readOnly = Boolean(record); candidates = structuredClone(record?.candidates || []); renderCandidates();
|
|
104
|
+
} else {
|
|
105
|
+
$('network-binding-provider').textContent = record.name; const binding = data.network.bindings.find(item => item.catalogId === data.catalog.catalogId && item.providerId === record.id);
|
|
106
|
+
options($('network-binding-pool'), [['', '继承环境 · 解除显式绑定'], ...data.network.pools.map(pool => [pool.id, pool.label + ' · ' + statusLabel(pool.status)])], binding?.poolId || ''); bindingNote();
|
|
107
|
+
}
|
|
108
|
+
render(); $('network-editor-title').scrollIntoView({ block: 'nearest' });
|
|
109
|
+
}
|
|
110
|
+
function bindingNote() {
|
|
111
|
+
const pool = data.network.pools.find(item => item.id === $('network-binding-pool').value);
|
|
112
|
+
$('network-binding-note').textContent = !pool ? '保存后明确解除此连接的出口绑定,恢复继承当前环境;不改变已有任务身份。'
|
|
113
|
+
: pool.status !== 'configured' ? '此池为空、停用或没有可用候选。保存绑定会阻止新请求,不会偷偷直连。' : '保存后使用此池允许的候选;只在连接预检失败时按顺序选择备用,不透明重放生成。';
|
|
114
|
+
}
|
|
115
|
+
function renderCandidates() {
|
|
116
|
+
$('network-candidates').replaceChildren(...candidates.map((candidate, index) => {
|
|
117
|
+
const row = el('div', '', 'network-candidate'); row.append(el('span', (index + 1) + '. ' + nodeLabel(candidate)));
|
|
118
|
+
for (const [name, step] of [['上移', -1], ['下移', 1], ['移除', 0]]) {
|
|
119
|
+
const button = el('button', name, 'quiet'); button.type = 'button'; button.disabled = step !== 0 && (index + step < 0 || index + step >= candidates.length);
|
|
120
|
+
button.addEventListener('click', () => { if (!step) candidates.splice(index, 1); else [candidates[index], candidates[index + step]] = [candidates[index + step], candidates[index]]; changed(); renderCandidates(); }); row.append(button);
|
|
121
|
+
} return row;
|
|
122
|
+
}));
|
|
123
|
+
options($('network-candidate-choice'), [...data.network.nodes.filter(node => !candidates.some(candidate => candidate.nodeId === node.id)).map(node => [node.id, node.label]),
|
|
124
|
+
...(!candidates.some(candidate => candidate.kind === 'direct') ? [['@direct', '明确允许直连']] : [])]);
|
|
125
|
+
$('network-candidate-add').disabled = candidates.length >= 16 || !$('network-candidate-choice').options.length; controls();
|
|
126
|
+
}
|
|
127
|
+
async function refresh() {
|
|
128
|
+
if (!client || busy) return;
|
|
129
|
+
const owner = client, ticket = generation; readController?.abort(); const controller = readController = new AbortController();
|
|
130
|
+
feedback('正在读取出口配置…');
|
|
131
|
+
try {
|
|
132
|
+
const value = await owner.get('/api/v0/network-management/settings', { signal: controller.signal });
|
|
133
|
+
if (!active(ticket, owner) || controller.signal.aborted) return;
|
|
134
|
+
if (draft) return; // A background read cannot rebase an open editor's CAS.
|
|
135
|
+
if (value.contractVersion !== 0) throw new TypeError('不支持的网络设置版本');
|
|
136
|
+
if (data?.network?.revision !== value.network?.revision) $('network-probe-result').hidden = true;
|
|
137
|
+
data = value; onChanged(data); render(); feedback('');
|
|
138
|
+
if (section === 'doctor' && visible) await readDoctor();
|
|
139
|
+
} catch (error) { if (active(ticket, owner) && !controller.signal.aborted) { if (!draft) { data = null; onChanged(null); render(); } feedback(describe(error), true); } }
|
|
140
|
+
}
|
|
141
|
+
async function save(action, values = {}) {
|
|
142
|
+
if (!client || busy || unknown) return;
|
|
143
|
+
const owner = client, ticket = generation; readController?.abort(); busy = true; const controller = writeController = new AbortController(); controls(); feedback('正在保存…');
|
|
144
|
+
try {
|
|
145
|
+
const result = await owner.postNetwork(action, action === 'init' ? {} : { expectedRevision: draft?.revision || data.network.revision, ...values }, { signal: controller.signal });
|
|
146
|
+
if (!active(ticket, owner)) return;
|
|
147
|
+
data = result.settings; onChanged(data); $('network-probe-result').hidden = true; closeEditor(); feedback(result.changed ? '已保存。配置变更已生效,作品与任务历史保留。' : '设置未变化。');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!active(ticket, owner)) return;
|
|
150
|
+
unknown = error instanceof TypeError || ['AbortError', 'TimeoutError'].includes(error.name);
|
|
151
|
+
feedback(unknown ? '保存结果尚未确认,请重新读取并核对。不会自动重发。' : describe(error), true);
|
|
152
|
+
} finally { if (active(ticket, owner)) { busy = false; writeController = null; render(); } }
|
|
153
|
+
}
|
|
154
|
+
async function readDoctor() {
|
|
155
|
+
const owner = client, ticket = generation, controller = readController;
|
|
156
|
+
if (!owner || busy) return;
|
|
157
|
+
try {
|
|
158
|
+
const result = await owner.get('/api/v0/network-management/doctor', { signal: controller?.signal });
|
|
159
|
+
if (!active(ticket, owner) || !visible || section !== 'doctor') return;
|
|
160
|
+
const fields = [['检查位置', '当前 Core · ' + result.process.platform + ' / ' + result.process.arch], ['Node', result.process.node],
|
|
161
|
+
['代理变量', Object.entries(result.environment.variables).filter(([, exists]) => exists).map(([name]) => name).join('、') || '未发现'],
|
|
162
|
+
['Node 环境代理开关', result.environment.nodeFlagRequested ? '已请求 · 实际路径仍需检测' : '未发现启用请求'], ['宿主策略 / 系统 TUN', '未知 · 不根据配置猜测'], ['读取时间', new Date(result.observedAt).toLocaleTimeString('zh-CN', { hour12: false })]];
|
|
163
|
+
if (result.configuration.state === 'invalid') fields.push(['配置需核对', result.configuration.message + ' ' + result.configuration.issue]);
|
|
164
|
+
$('network-environment').replaceChildren(...fields.flatMap(([key, value]) => [el('dt', key), el('dd', value)]));
|
|
165
|
+
} catch (error) { if (active(ticket, owner) && !controller?.signal.aborted) feedback(describe(error), true); }
|
|
166
|
+
}
|
|
167
|
+
function enter(next) { section = next; closeEditor(); if (next === 'doctor') void readDoctor(); }
|
|
168
|
+
for (const button of document.querySelectorAll('[data-network-section]')) button.addEventListener('click', () => leave(() => enter(button.dataset.networkSection)));
|
|
169
|
+
$('network-new').addEventListener('click', () => edit(section === 'nodes' ? 'node' : 'pool'));
|
|
170
|
+
$('network-refresh').addEventListener('click', () => leave(() => { closeEditor(); void refresh(); }));
|
|
171
|
+
for (const id of ['network-back', 'network-cancel']) $(id).addEventListener('click', () => leave(closeEditor));
|
|
172
|
+
$('network-leave-no').addEventListener('click', () => { pendingLeave = null; $('network-leave-confirm').hidden = true; });
|
|
173
|
+
$('network-leave-yes').addEventListener('click', () => { const action = pendingLeave; pendingLeave = null; dirty = unknown = false; $('network-leave-confirm').hidden = true; action?.(); });
|
|
174
|
+
$('network-form').addEventListener('input', changed); $('network-form').addEventListener('change', changed);
|
|
175
|
+
$('network-node-auth').addEventListener('change', () => { $('network-auth-fields').hidden = !$('network-node-auth').checked; });
|
|
176
|
+
$('network-pool-mode').addEventListener('change', controls);
|
|
177
|
+
$('network-binding-pool').addEventListener('change', bindingNote);
|
|
178
|
+
$('network-candidate-add').addEventListener('click', () => { const id = $('network-candidate-choice').value; if (!id) return; candidates.push(id === '@direct' ? { kind: 'direct' } : { kind: 'node', nodeId: id }); changed(); renderCandidates(); });
|
|
179
|
+
$('network-init').addEventListener('click', () => save('init'));
|
|
180
|
+
$('network-form').addEventListener('submit', event => {
|
|
181
|
+
event.preventDefault(); if (!draft) return;
|
|
182
|
+
if (draft.kind === 'binding') {
|
|
183
|
+
const poolId = $('network-binding-pool').value;
|
|
184
|
+
if (!poolId && !data.network.bindings.some(item => item.catalogId === data.catalog.catalogId && item.providerId === draft.id)) { closeEditor(); feedback('设置未变化。'); return; }
|
|
185
|
+
return save(poolId ? 'bindings/set' : 'bindings/remove', poolId ? { expectedProviderRevision: draft.providerRevision, providerId: draft.id, poolId }
|
|
186
|
+
: { catalogId: data.catalog.catalogId, providerId: draft.id, confirm: true });
|
|
187
|
+
}
|
|
188
|
+
const kind = draft.kind, record = { id: $('network-' + kind + '-id').value, label: $('network-' + kind + '-label').value, enabled: $('network-' + kind + '-enabled').checked };
|
|
189
|
+
if (kind === 'node') Object.assign(record, { protocol: $('network-node-protocol').value, host: $('network-node-host').value, port: Number($('network-node-port').value),
|
|
190
|
+
auth: $('network-node-auth').checked ? { usernameEnv: $('network-node-username').value, passwordEnv: $('network-node-password').value } : null });
|
|
191
|
+
else Object.assign(record, { mode: $('network-pool-mode').value, candidates });
|
|
192
|
+
void save(kind + 's/' + (draft.id ? 'set' : 'add'), { [kind]: record });
|
|
193
|
+
});
|
|
194
|
+
$('network-remove').addEventListener('click', () => { $('network-delete-confirm').hidden = false; });
|
|
195
|
+
$('network-delete-no').addEventListener('click', () => { $('network-delete-confirm').hidden = true; });
|
|
196
|
+
$('network-delete-yes').addEventListener('click', () => { if (draft) void save(draft.kind + 's/remove', { id: draft.id, confirm: true }); });
|
|
197
|
+
$('network-probe-target').addEventListener('change', () => { const mode = $('network-probe-target').value; $('network-probe-url-field').hidden = mode === 'provider';
|
|
198
|
+
$('network-probe-url').required = mode !== 'provider'; if (mode === 'ip' && !$('network-probe-url').value) $('network-probe-url').value = 'https://api.ipify.org?format=json'; });
|
|
199
|
+
$('network-probe-form').addEventListener('input', () => { $('network-probe-result').hidden = true; });
|
|
200
|
+
$('network-probe-form').addEventListener('change', () => { $('network-probe-result').hidden = true; });
|
|
201
|
+
$('network-probe-form').addEventListener('submit', async event => {
|
|
202
|
+
event.preventDefault(); if (!client || busy) return;
|
|
203
|
+
const owner = client, ticket = generation, controller = probeController = new AbortController(); busy = true; controls(); feedback('正在检测这条连接…');
|
|
204
|
+
try {
|
|
205
|
+
const target = $('network-probe-target').value;
|
|
206
|
+
const result = await owner.postNetwork('probe', { expectedRevision: data.network.revision, expectedProviderRevision: data.catalog.revision,
|
|
207
|
+
providerId: $('network-probe-provider').value, target, ...(target === 'provider' ? {} : { url: $('network-probe-url').value }) }, { signal: controller.signal });
|
|
208
|
+
if (!active(ticket, owner) || controller.signal.aborted || !visible) return;
|
|
209
|
+
const node = $('network-probe-result'); node.replaceChildren(el('strong', result.ok ? '本次检测通过' : result.reachable ? '已连接 · 结果需要核对' : '本次未通过'));
|
|
210
|
+
node.append(el('p', (data.providers.find(provider => provider.id === result.providerId)?.name || result.providerId) + ' · ' + result.target.origin, 'field-note'));
|
|
211
|
+
node.append(el('p', [result.route ? result.route.route === 'proxy' ? '出口池 ' + result.route.poolId + ' / 节点 ' + result.route.nodeId : result.route.route === 'direct' ? '明确直连' : '继承环境' : '尚未派发',
|
|
212
|
+
result.httpStatus ? 'HTTP ' + result.httpStatus : null, result.elapsedMs + ' ms', result.observedIp ? '观察到 IP:' + result.observedIp : null].filter(Boolean).join(' · ')));
|
|
213
|
+
if (result.issue) node.append(el('p', ({ IRIS_NETWORK_POOL_EXHAUSTED: '候选均未通过连接预检,请检查节点和代理认证。', IRIS_NETWORK_POOL_UNAVAILABLE: '出口池为空或停用,未隐式直连。',
|
|
214
|
+
IRIS_NETWORK_PROBE_TIMEOUT: '检测超过8秒,已停止等待。', IRIS_NETWORK_PROBE_TOO_LARGE: '响应超过16 KiB,已停止读取。', IRIS_NETWORK_TARGET_HTTP_ERROR: '目标已返回响应,请核对地址及服务状态。',
|
|
215
|
+
IRIS_NETWORK_IP_NOT_OBSERVED: '已连接,但JSON中没有有效的ip字段;不推断出口IP。', IRIS_NETWORK_TRANSPORT: '选定路线的请求失败,未自动换出口重发。' })[result.issue] || '未能完成本次GET,请核对目标、证书与网络。', 'field-note'));
|
|
216
|
+
node.append(el('p', new Date(result.observedAt).toLocaleTimeString('zh-CN', { hour12: false }) + ' · 仅此目标、此时刻;不代表全部任务出口。', 'field-note'));
|
|
217
|
+
node.hidden = false; feedback('');
|
|
218
|
+
} catch (error) { if (active(ticket, owner)) feedback(controller.signal.aborted ? '检测已取消。' : describe(error), !controller.signal.aborted); }
|
|
219
|
+
finally { if (active(ticket, owner)) { busy = false; probeController = null; controls(); } }
|
|
220
|
+
});
|
|
221
|
+
$('network-probe-cancel').addEventListener('click', () => probeController?.abort());
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
connect(value) { client = value; generation++; data = null; draft = null; section = 'bindings'; dirty = unknown = busy = false; return refresh(); },
|
|
224
|
+
disconnect() { generation++; readController?.abort(); probeController?.abort(); writeController?.abort(); client = data = draft = null; busy = dirty = unknown = visible = false; pendingLeave = null;
|
|
225
|
+
$('network-list').replaceChildren(); $('network-stats').replaceChildren(); $('network-environment').replaceChildren(); $('network-other-list').replaceChildren(); $('network-editor').hidden = true;
|
|
226
|
+
$('network-probe-result').replaceChildren(); $('network-probe-result').hidden = true; $('network-form').reset(); $('network-probe-form').reset(); $('network-leave-confirm').hidden = true;
|
|
227
|
+
for (const id of ['network-candidates', 'network-probe-provider', 'network-binding-pool', 'network-binding-provider']) $(id).replaceChildren(); onChanged(null); feedback(''); },
|
|
228
|
+
show(value) { const leaving = visible && !value; visible = value; if (leaving) { readController?.abort(); probeController?.abort(); } else if (value && client && !draft && !busy) void refresh(); },
|
|
229
|
+
refresh() { if (!dirty && !draft && !busy) return refresh(); },
|
|
230
|
+
focusProvider(id) { if (!data) return; leave(() => { const provider = data.providers.find(item => item.id === id); if (provider && data.catalog.state === 'ready') edit('binding', provider); else enter('bindings'); }); }
|
|
231
|
+
});
|
|
232
|
+
}
|
package/lib/web/plugin-panel.js
CHANGED
|
@@ -3,7 +3,8 @@ const node = (tag, text, className) => { const item = document.createElement(tag
|
|
|
3
3
|
const freeze = value => { if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); } return value; };
|
|
4
4
|
const ARTIFACT = /^artifact_[a-f0-9]{24}$/, RUN = /^run_[a-f0-9]{24}$/;
|
|
5
5
|
const MEDIA = { 'image/png': 'content', 'image/jpeg': 'content', 'image/webp': 'content',
|
|
6
|
-
'audio/wav': 'audio', 'audio/mpeg': 'audio', 'audio/ogg': 'audio', 'audio/flac': 'audio', 'text/plain': 'text'
|
|
6
|
+
'audio/wav': 'audio', 'audio/mpeg': 'audio', 'audio/ogg': 'audio', 'audio/flac': 'audio', 'text/plain': 'text',
|
|
7
|
+
'video/mp4': 'video', 'video/webm': 'video' };
|
|
7
8
|
|
|
8
9
|
/** Trusted same-origin modules. Shadow DOM isolates styles, not permissions or JavaScript. */
|
|
9
10
|
export function createPluginPanel({ navigate, describe, onProvidersSaved }) {
|
|
@@ -56,6 +57,7 @@ export function createPluginPanel({ navigate, describe, onProvidersSaved }) {
|
|
|
56
57
|
run: (id, value) => { if (typeof id !== 'string' || !RUN.test(id)) throw new TypeError('流程 ID 无效'); return read('/api/v0/runs/' + id, value); },
|
|
57
58
|
models: value => read('/api/v0/models', value),
|
|
58
59
|
audioModels: value => read('/api/v0/audio-models', value),
|
|
60
|
+
videoModels: value => read('/api/v0/video-models', value),
|
|
59
61
|
artifact: (artifact, value) => {
|
|
60
62
|
if (typeof artifact?.id !== 'string' || !ARTIFACT.test(artifact.id) || !Object.hasOwn(MEDIA, artifact?.mediaType)) throw new TypeError('作品 ID 或媒体类型无效');
|
|
61
63
|
return read('/api/v0/artifacts/' + artifact.id + '/' + MEDIA[artifact.mediaType], { ...value, binary: true });
|
|
@@ -171,7 +173,7 @@ export function createPluginPanel({ navigate, describe, onProvidersSaved }) {
|
|
|
171
173
|
connect(http, facts) {
|
|
172
174
|
client = http;
|
|
173
175
|
session = freeze({ contractVersion: facts.contractVersion, instanceId: facts.instanceId, rootId: facts.rootId,
|
|
174
|
-
operations: [...facts.operations], readOnly: facts.readOnly, audio: facts.audio === true, providerManagement: facts.providerManagement === true,
|
|
176
|
+
operations: [...facts.operations], readOnly: facts.readOnly, audio: facts.audio === true, video: facts.video === true, providerManagement: facts.providerManagement === true,
|
|
175
177
|
pluginExecution: facts.pluginExecution === true, pluginOperations: structuredClone(facts.pluginOperations || []) });
|
|
176
178
|
manifest = freeze(structuredClone(facts.panelContributions || { contractVersion: 0, pages: [], settings: [], renderers: [] }));
|
|
177
179
|
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'data-density', 'data-accent'] });
|