@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.
Files changed (174) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.en.md +20 -13
  3. package/README.md +20 -13
  4. package/bin/dsh-iris.js +143 -51
  5. package/docs/AGENT_CORE_QUICKSTART.md +2 -2
  6. package/docs/AGENT_QUICKSTART.md +1 -1
  7. package/docs/ARCHITECTURE.md +5 -5
  8. package/docs/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +20 -4
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +1 -1
  12. package/docs/CORE_CONNECTIONS.md +3 -1
  13. package/docs/CORE_QUICKSTART.md +7 -5
  14. package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
  15. package/docs/CORE_SERVICE.md +9 -5
  16. package/docs/CORE_SKILLS.md +2 -2
  17. package/docs/DSH_COMPATIBILITY.md +11 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RUNTIME.md +2 -2
  20. package/docs/HEADLESS_CLI.md +29 -4
  21. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  22. package/docs/IMAGE_FLOW.md +2 -2
  23. package/docs/IMAGE_GENERATION.md +1 -1
  24. package/docs/LOCAL_API.md +2 -2
  25. package/docs/LOCAL_API_CONTRACT.md +17 -15
  26. package/docs/LOCAL_CREATION.md +2 -2
  27. package/docs/LOCAL_OPERATIONS.md +2 -2
  28. package/docs/LOCAL_SELECTION.md +1 -1
  29. package/docs/LOCAL_WEB.md +11 -5
  30. package/docs/MCP.md +30 -9
  31. package/docs/MEDIA_SCHEDULING.md +72 -0
  32. package/docs/NETWORK_RESOURCES.md +106 -0
  33. package/docs/NETWORK_ROUTING.md +103 -0
  34. package/docs/NETWORK_WORKBENCH.md +65 -0
  35. package/docs/PANEL_EXTENSIONS.md +1 -1
  36. package/docs/PLUGINS.md +9 -5
  37. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  38. package/docs/PLUGIN_STARTUP.md +15 -2
  39. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  40. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  41. package/docs/PROVIDER_EXTENSIONS.md +8 -4
  42. package/docs/PROVIDER_HEALTH.md +4 -2
  43. package/docs/PROVIDER_NETWORK.md +45 -0
  44. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  45. package/docs/PROVIDER_SETTINGS.md +13 -7
  46. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
  47. package/docs/ROADMAP.md +6 -6
  48. package/docs/SECURITY.md +1 -1
  49. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  50. package/docs/VIDEO_FLOWS.md +89 -0
  51. package/docs/VISION_MODEL.md +2 -0
  52. package/docs/WEB_PROTOTYPE.md +3 -3
  53. package/docs/WEB_RECOVERY.md +2 -2
  54. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  55. package/docs/WORKBENCH_LOGIN.md +2 -2
  56. package/docs/releases/0.3.0-beta.1.md +34 -0
  57. package/docs/screenshots.md +2 -2
  58. package/lib/actions.js +5 -4
  59. package/lib/adapters.js +58 -41
  60. package/lib/api.js +23 -13
  61. package/lib/application-runtime.js +44 -15
  62. package/lib/application-writer.js +193 -0
  63. package/lib/client.d.ts +3 -3
  64. package/lib/client.js +62 -25
  65. package/lib/comfyui-adapter.js +3 -2
  66. package/lib/comfyui-client.js +10 -1
  67. package/lib/command-service.js +18 -5
  68. package/lib/config.js +13 -9
  69. package/lib/configured-image-runtime.js +28 -9
  70. package/lib/connection-skill.js +9 -0
  71. package/lib/core-artifact-store.js +68 -26
  72. package/lib/core-bridge-tools.js +5 -3
  73. package/lib/core-capability-catalog.js +21 -11
  74. package/lib/core-delivery-cache.js +8 -3
  75. package/lib/core-service-client.js +6 -2
  76. package/lib/core-tasks.js +86 -11
  77. package/lib/creation-request-store.js +8 -5
  78. package/lib/doctor.js +2 -2
  79. package/lib/dsh-core-adapter.js +61 -9
  80. package/lib/dsh-provider-config.js +1 -1
  81. package/lib/dsh-service-bridge.js +1 -0
  82. package/lib/flow-store.js +64 -17
  83. package/lib/image-flow.js +39 -17
  84. package/lib/index.js +10 -5
  85. package/lib/local-api-projection.js +6 -2
  86. package/lib/local-api.js +137 -49
  87. package/lib/local-creation.js +12 -5
  88. package/lib/local-operations.js +2 -2
  89. package/lib/local-request-root.js +2 -2
  90. package/lib/local-web-assets.js +2 -0
  91. package/lib/mcp-http.js +82 -9
  92. package/lib/mcp-server.js +23 -1
  93. package/lib/media-scheduler.js +194 -0
  94. package/lib/model-directory-service.js +25 -8
  95. package/lib/music-transport.js +4 -2
  96. package/lib/network-config-service.js +123 -0
  97. package/lib/network-config.js +127 -0
  98. package/lib/network-doctor.js +94 -0
  99. package/lib/network-management.js +51 -0
  100. package/lib/network-policy.js +19 -0
  101. package/lib/network-route-evidence.js +38 -0
  102. package/lib/network-routing.js +283 -0
  103. package/lib/operation-runner.js +83 -38
  104. package/lib/operation-store.js +6 -6
  105. package/lib/plugins.d.ts +25 -6
  106. package/lib/plugins.js +2 -1
  107. package/lib/provider-adapter.js +2 -2
  108. package/lib/provider-adapters.js +37 -5
  109. package/lib/provider-catalog.js +33 -16
  110. package/lib/provider-config-format.js +51 -0
  111. package/lib/provider-config-service.js +52 -22
  112. package/lib/provider-context.d.ts +90 -0
  113. package/lib/provider-context.js +42 -0
  114. package/lib/provider-credentials.js +97 -0
  115. package/lib/provider-diagnostics.js +5 -5
  116. package/lib/provider-health.js +10 -5
  117. package/lib/provider-monitor.js +5 -3
  118. package/lib/provider-network.js +52 -0
  119. package/lib/provider-protocol.js +3 -2
  120. package/lib/provider-registry.js +15 -5
  121. package/lib/provider-response.js +10 -0
  122. package/lib/provider-settings.js +14 -12
  123. package/lib/provider-task-runner.js +113 -37
  124. package/lib/provider-transport.js +16 -0
  125. package/lib/render.js +11 -1
  126. package/lib/scheduling-policy.js +66 -0
  127. package/lib/tasks.js +3 -2
  128. package/lib/video-flow-input.js +16 -0
  129. package/lib/video-input.js +12 -5
  130. package/lib/vision-model-routing.js +2 -0
  131. package/lib/web/app.js +107 -46
  132. package/lib/web/connections.js +151 -12
  133. package/lib/web/creation.js +29 -25
  134. package/lib/web/flow-view.js +4 -3
  135. package/lib/web/gallery.js +17 -16
  136. package/lib/web/index.html +58 -5
  137. package/lib/web/media-client.js +18 -4
  138. package/lib/web/network.js +232 -0
  139. package/lib/web/plugin-panel.js +4 -2
  140. package/lib/web/plugin-status.js +140 -0
  141. package/lib/web/providers.js +34 -13
  142. package/lib/web/recovery.js +2 -2
  143. package/lib/web/styles.css +54 -1
  144. package/lib/web/transport.js +6 -1
  145. package/lib/workbench-artifacts.js +3 -2
  146. package/package.json +5 -2
  147. package/skills/iris-core/references/comfyui.md +1 -1
  148. package/templates/plugin-starter/LICENSE +21 -0
  149. package/templates/plugin-starter/README.md +74 -0
  150. package/templates/plugin-starter/assets/peach.png +0 -0
  151. package/templates/plugin-starter/assets/violet.png +0 -0
  152. package/templates/plugin-starter/index.mjs +27 -0
  153. package/templates/plugin-starter/package.json +18 -0
  154. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  155. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  156. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  157. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  158. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  159. package/templates/plugin-starter/web/board.css +27 -0
  160. package/templates/plugin-starter/web/board.mjs +160 -0
  161. package/templates/plugin-starter/web/png.mjs +23 -0
  162. package/templates/provider-starter/LICENSE +21 -0
  163. package/templates/provider-starter/README.md +50 -0
  164. package/templates/provider-starter/index.d.mts +5 -0
  165. package/templates/provider-starter/index.mjs +165 -0
  166. package/templates/provider-starter/package.json +13 -0
  167. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  168. package/templates/provider-starter/scripts/environment.mjs +122 -0
  169. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  170. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  171. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  172. package/templates/provider-starter/scripts/setup.mjs +44 -0
  173. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  174. package/user_guide.md +3 -3
@@ -0,0 +1,140 @@
1
+ // Read-only presentation of Core declarations and exact saved grants. Never grants or calls a business.
2
+ const $ = selector => document.querySelector(selector);
3
+ const kinds = { providers: '模型协议', operations: '业务操作', pages: '页面', settings: '设置表单', renderers: '作品预览' };
4
+ const lifecycle = { active: '已加载', loading: '加载中', closing: '正在关闭', closed: '已关闭' };
5
+ const sameDependency = (a, b) => ['contractVersion', 'pluginId', 'pluginVersion', 'kind', 'contributionId', 'contributionVersion'].every(key => a[key] === b[key]);
6
+ const node = (tag, text = '', className = '') => {
7
+ const value = document.createElement(tag); value.textContent = text; value.className = className; return value;
8
+ };
9
+ const reason = (code, label, hint) => ({ code, label, hint });
10
+
11
+ function authorization(record, pin, entry, state) {
12
+ if (record.revokedAt || record.state === 'revoked') return reason('revoked', '已撤销', '此连接不能发起新调用。');
13
+ if (!entry) return reason('missing', '业务未加载', '在启动选择中恢复原插件和准确业务版本;历史记录仍保留。');
14
+ if (!sameDependency(pin, entry.dependency)) return reason('version', '版本不一致', '当前加载版本与原授权不同,请核对准确依赖;不会自动替换授权。');
15
+ if (state !== 'active') return reason('closed', '插件不可用', '插件注册实例正在关闭或已关闭。');
16
+ if (entry.access.mcp !== 'requires-selection') return reason('disabled', '服务未启用', '在启动选择文件的 operations 中明确选择此业务,再正常重启服务。');
17
+ if (!record.scopes.includes('execute')) return reason('scope', '缺少执行权限', '在客户端连接设置中明确允许执行。');
18
+ if (!record.tools.includes('iris_core_plugin_call')) return reason('tool', '未选择调用工具', '在客户端连接设置中选择 plugin_call 工具。');
19
+ return reason('permitted', '授权允许执行', '配置满足调用条件;业务未实测。客户端还需连接并协商所需权限。');
20
+ }
21
+
22
+ /** Diagnostic projection only. Core remains the authority for every real invocation. */
23
+ export function projectPluginStatus(catalog, records) {
24
+ if (catalog.contractVersion !== 0 || !catalog.contributions || !Array.isArray(catalog.plugins)) throw new TypeError('不支持的插件目录版本');
25
+ const entries = Object.entries(catalog.contributions).flatMap(([kind, items]) => items.map(item => ({ ...item, kind })));
26
+ const authorizations = records === null ? null : records.flatMap(record => (record.pluginOperations || []).map(pin => {
27
+ const entry = entries.find(item => item.kind === 'operations' && item.id === pin.contributionId);
28
+ return { connectionId: record.connectionId, connectionLabel: record.label, pin, ...authorization(record, pin, entry, catalog.state),
29
+ instances: (record.instances || []).map(instance => ({ runtimeInstanceId: instance.runtimeInstanceId,
30
+ ...(instance.state !== 'connected' ? reason('disconnected', '握手已断开', '不是当前可用实例。')
31
+ : !(instance.pluginOperations || []).some(value => sameDependency(value, pin)) ? reason('unselected', '握手未选择此业务', '实例范围小于保存的授权。')
32
+ : authorization(instance, pin, entry, catalog.state)) })) };
33
+ }));
34
+ return { state: catalog.state, coreVersion: catalog.coreVersion, authorizations,
35
+ enabled: entries.filter(item => item.kind === 'operations' && item.access.web === 'via-operation').length,
36
+ plugins: catalog.plugins.map(plugin => ({ ...plugin, state: catalog.state,
37
+ contributions: entries.filter(item => item.dependency.pluginId === plugin.id) })),
38
+ missing: (authorizations || []).filter(item => !entries.some(entry => entry.kind === 'operations' && entry.id === item.pin.contributionId)) };
39
+ }
40
+
41
+ function contributionStatus(entry) {
42
+ if (entry.access.local === 'unavailable') return '当前不可用';
43
+ if (entry.kind === 'providers') return '协议已加载 · 模型服务另行配置与实测';
44
+ if (entry.kind === 'operations') return entry.access.mcp === 'requires-selection' ? '服务已启用 · 客户端需单独授权' : '服务未启用';
45
+ return entry.access.web === 'available-in-panel' ? '已选入工作台' : '未选入工作台';
46
+ }
47
+
48
+ export function createPluginStatusPanel({ describe, navigate }) {
49
+ let client, session, visible = false, ticket = 0, controller;
50
+ function cancel() { ticket++; controller?.abort(); controller = null; $('#plugins-refresh').disabled = false; $('#plugins-list').setAttribute('aria-busy', 'false'); }
51
+ function clear() {
52
+ for (const id of ['plugins-list', 'plugins-missing', 'plugins-stats']) $('#' + id).replaceChildren();
53
+ for (const id of ['plugins-feedback', 'plugins-authorizations-note', 'plugins-updated']) { $('#' + id).textContent = ''; $('#' + id).hidden = true; }
54
+ $('#plugins-empty').hidden = true; $('#plugins-missing-section').hidden = true;
55
+ }
56
+ function feedback(text, error = false) { const target = $('#plugins-feedback'); target.textContent = text; target.hidden = !text; target.classList.toggle('error', error); }
57
+ function renderGrant(value) {
58
+ const item = node('li', '', 'plugin-grant'); item.dataset.status = value.code;
59
+ const heading = node('div', '', 'plugin-grant-heading'); heading.append(node('strong', value.connectionLabel));
60
+ item.append(heading, node('span', value.code === 'permitted' ? '授权允许执行 · 未实测' : value.label, 'plugin-state'));
61
+ item.append(node('p', value.hint, 'field-note'));
62
+ const pin = value.pin;
63
+ item.append(node('p', pin.pluginId + '@' + pin.pluginVersion + ' · ' + pin.contributionId + '@' + pin.contributionVersion, 'plugin-identity'));
64
+ if (value.instances.length) {
65
+ const facts = node('ul', '', 'plugin-instances');
66
+ for (const instance of value.instances) facts.append(node('li', instance.label + (instance.code === 'permitted' ? ' · 未实测' : '')));
67
+ item.append(facts, node('p', value.instances.length + ' 个握手实例记录,不代表客户端持续在线。', 'field-note'));
68
+ } else item.append(node('p', '当前无握手实例记录,不表示客户端安装失败。', 'field-note'));
69
+ return item;
70
+ }
71
+ const grantRows = values => values.map(renderGrant);
72
+ function render(value) {
73
+ const grants = value.authorizations;
74
+ const open = new Set([...document.querySelectorAll('.plugin-status-card[open]')].map(card => card.dataset.pluginId));
75
+ const stats = [['已加载插件', value.plugins.filter(plugin => plugin.state === 'active').length], ['已启用业务', value.enabled], ['业务授权', grants === null ? '未知' : grants.filter(item => item.code !== 'revoked').length]];
76
+ $('#plugins-stats').replaceChildren(...stats.map(([label, count]) => { const row = node('div'); row.append(node('dt', label), node('dd', String(count))); return row; }));
77
+ $('#plugins-empty').hidden = value.plugins.length !== 0;
78
+ $('#plugins-list').replaceChildren(...value.plugins.map(plugin => {
79
+ const card = node('details', '', 'card plugin-status-card'); card.dataset.pluginId = plugin.id;
80
+ card.open = open.has(plugin.id);
81
+ const summary = node('summary'), mark = node('span', '◇', 'plugin-status-mark'); mark.setAttribute('aria-hidden', 'true');
82
+ const title = node('span', '', 'plugin-status-title'); title.append(node('strong', plugin.id), node('small', 'v' + plugin.version + ' · ' + plugin.contributions.length + ' 项贡献'));
83
+ const disclosure = node('span', card.open ? '收起' : '展开', 'disclosure');
84
+ summary.append(mark, title, node('span', lifecycle[plugin.state], 'plugin-state'), disclosure); card.append(summary);
85
+ card.addEventListener('toggle', () => { disclosure.textContent = card.open ? '收起' : '展开'; });
86
+ const body = node('div', '', 'plugin-status-body'), compatibility = plugin.compatibility;
87
+ body.append(node('p', 'Core 兼容范围:' + compatibility.min + ' ≤ 版本 < ' + compatibility.maxExclusive, 'plugin-identity'),
88
+ node('p', (value.coreVersion ? '装配时 Core ' + value.coreVersion + ',' : '') + '版本已通过加载校验。' + (plugin.state === 'active' ? '加载不代表媒体实测。' : '当前注册实例' + lifecycle[plugin.state] + '。'), 'field-note'));
89
+ const contributions = node('ul', '', 'plugin-contribution-status');
90
+ for (const entry of plugin.contributions) {
91
+ const row = node('li'); row.dataset.contributionId = entry.id;
92
+ row.append(node('small', kinds[entry.kind], 'plugin-kind'), node('strong', entry.title), node('p', contributionStatus(entry), 'field-note'), node('p', entry.id + '@' + entry.version, 'plugin-identity'));
93
+ contributions.append(row);
94
+ }
95
+ body.append(contributions);
96
+ const own = grants?.filter(item => item.pin.pluginId === plugin.id);
97
+ body.append(node('h3', '客户端授权'));
98
+ if (own?.length) { const list = node('ul', '', 'plugin-grants'); list.append(...grantRows(own)); body.append(list); }
99
+ else body.append(node('p', grants ? '尚无客户端选择此插件业务。' : '授权信息尚未读取,不能判断客户端选择。', 'field-note'));
100
+ card.append(body); return card;
101
+ }));
102
+ const missing = grants?.filter(item => value.missing.some(value => value.connectionId === item.connectionId && sameDependency(value.pin, item.pin))) || [];
103
+ $('#plugins-missing-section').hidden = !missing.length;
104
+ $('#plugins-missing').replaceChildren(...grantRows(missing));
105
+ }
106
+ async function refresh() {
107
+ if (!client || !visible) return;
108
+ cancel(); const at = ticket, connection = client, activeSession = session;
109
+ controller = new AbortController(); const { signal } = controller;
110
+ clear(); $('#plugins-refresh').disabled = true; $('#plugins-list').setAttribute('aria-busy', 'true'); feedback('正在读取插件状态…');
111
+ const valid = () => at === ticket && client === connection && visible && !signal.aborted;
112
+ try {
113
+ const catalog = await connection.get('/api/v0/capabilities', { signal });
114
+ if (!valid()) return;
115
+ render(projectPluginStatus(catalog, null)); feedback('');
116
+ let records;
117
+ try {
118
+ const result = await connection.get(activeSession.connectionManagement ? '/api/v0/connection-management/connections' : '/api/v0/connections', { signal });
119
+ if (result.contractVersion !== 0 || !Array.isArray(result.connections)) throw new TypeError('不支持的连接列表版本');
120
+ records = result.connections;
121
+ } catch (error) {
122
+ if (!valid()) return;
123
+ // A service without either static grants or managed connections has no /connections route.
124
+ if (!activeSession.connectionManagement && error.status === 404) records = [];
125
+ else { const note = $('#plugins-authorizations-note'); note.hidden = false; note.textContent = '授权信息未能读取:' + describe(error) + ';插件目录已读取。'; }
126
+ }
127
+ if (!valid()) return;
128
+ if (records) render(projectPluginStatus(catalog, records));
129
+ const updated = $('#plugins-updated'); updated.hidden = false; updated.textContent = '读取于 ' + new Date().toLocaleTimeString('zh-CN') + ' · 不调用业务或测试模型';
130
+ } catch (error) { if (valid()) { clear(); feedback('插件目录未能读取:' + describe(error), true); } }
131
+ finally { if (valid()) { controller = null; $('#plugins-refresh').disabled = false; $('#plugins-list').setAttribute('aria-busy', 'false'); } }
132
+ }
133
+ $('#plugins-refresh').addEventListener('click', refresh);
134
+ $('#plugins-connections').addEventListener('click', () => navigate('connections'));
135
+ return {
136
+ connect(connection, facts) { cancel(); clear(); client = connection; session = facts; $('#plugins-connections').hidden = !facts.connectionManagement; },
137
+ show(value) { visible = value; if (visible) void refresh(); else cancel(); },
138
+ disconnect() { visible = false; cancel(); clear(); client = session = null; $('#plugins-panel').hidden = true; }
139
+ };
140
+ }
@@ -1,15 +1,18 @@
1
1
  import { readWorkflowImport, workflowSlots, buildWorkflow, renderWorkflowParameters, inspectionMessage, parameterLabel } from './comfyui.js';
2
+ import { networkRouteLabel } from './network.js';
2
3
 
3
4
  const LABELS = { 'image-gen': '图片', 'video-gen': '视频', tts: '语音', 'music-gen': '音乐', transcribe: '转写', vision: '视觉' };
4
5
  const $ = id => document.getElementById(id);
5
6
  const element = (tag, text, className) => { const node = document.createElement(tag); if (text) node.textContent = text; if (className) node.className = className; return node; };
6
7
  const ref = (providerId, modelId) => encodeURIComponent(providerId) + '::' + encodeURIComponent(modelId);
7
8
 
8
- export function createProviderPanel({ describe, onSaved, onSettings, onProbe = () => {} }) {
9
+ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = () => {}, onNetworkOpen = () => {} }) {
9
10
  let client, data, draft, busy = false, dirty = false, unknown = false, generation = 0, originalIds = new Set();
10
11
  let policies = {}, touched = new Set(), directory = [], comfyTemplate = null, slots = [], importRead = 0, importAdded = false;
11
12
  let checkController, apiDraft = null, editing = false, view = 'connection';
12
13
  let section = 'suppliers', diagnostics = null, healthCapability = 'image-gen', pendingLeave;
14
+ let networkFacts = null;
15
+ const routeFor = id => networkFacts?.providers.find(provider => provider.id === id)?.route;
13
16
  function editor(open) {
14
17
  editing = open; $('provider-editor').hidden = !open; $('providers-overview').hidden = open || section !== 'suppliers';
15
18
  $('provider-sections').hidden = open;
@@ -22,10 +25,10 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
22
25
  $('provider-view-' + button.dataset.providerView).hidden = !active;
23
26
  }
24
27
  }
25
- const connectionFields = ['url', 'media-url', 'protocol', 'auth', 'key-action', 'key'];
28
+ const connectionFields = ['url', 'media-url', 'protocol', 'auth', 'key-action', 'key', 'key-env'];
26
29
  const apiFields = () => Object.fromEntries(connectionFields.map(field => [field, $('provider-' + field).value]));
27
30
  function restoreApiDraft() {
28
- const fields = apiDraft || { url: '', 'media-url': '', protocol: 'openai-images', auth: 'bearer', 'key-action': draft.hasCredential ? 'keep' : 'replace', key: '' };
31
+ const fields = apiDraft || { url: '', 'media-url': '', protocol: 'openai-images', auth: 'bearer', 'key-action': draft.hasCredential || draft.apiKeyEnv ? 'keep' : 'replace', key: '', 'key-env': draft.apiKeyEnv || '' };
29
32
  for (const field of connectionFields) $('provider-' + field).value = fields[field];
30
33
  keyAction();
31
34
  }
@@ -49,6 +52,7 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
49
52
  $('provider-pool-save').disabled ||= !touched.size;
50
53
  for (const button of document.querySelectorAll('[data-provider-section]')) button.disabled = busy || !data;
51
54
  const saved = data?.providers.find(provider => provider.id === draft?.id);
55
+ $('provider-network-link').hidden = !saved || !networkFacts;
52
56
  $('provider-management-tools').hidden = !saved;
53
57
  for (const id of ['provider-toggle', 'provider-delete', 'provider-delete-no']) $(id).disabled = busy || dirty || unknown;
54
58
  $('provider-toggle').textContent = saved?.enabled ? '停用供应商' : '恢复供应商';
@@ -81,9 +85,10 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
81
85
  button.dataset.providerTone = String(index % 6);
82
86
  button.setAttribute('aria-current', String(provider.id === draft?.id));
83
87
  const heading = element('span', '', 'service-card-heading');
84
- heading.append(element('span', provider.mediaProtocol === 'comfyui' ? 'CF' : 'API', 'service-mark'), element('small', !provider.enabled ? '已停用' : provider.configured ? '已配置' : '待配置', 'service-status'));
88
+ heading.append(element('span', provider.mediaProtocol === 'comfyui' ? 'CF' : 'API', 'service-mark'), element('small', !provider.enabled ? '已停用' : provider.configured ? '已配置' : provider.apiKeyEnv ? '环境凭据未就绪' : '待配置', 'service-status'));
85
89
  button.append(heading, element('strong', provider.name), element('span', provider.baseUrl || '尚未填写地址', 'service-endpoint'),
86
90
  element('span', provider.models.length + ' 个模型 · ' + (provider.mediaProtocol === 'comfyui' ? 'ComfyUI 工作流' : 'API 服务'), 'service-meta'), element('span', '配置服务 →', 'service-action'));
91
+ if (networkFacts) button.insertBefore(element('span', '配置出口 · ' + networkRouteLabel(routeFor(provider.id)), 'network-route-note'), button.lastChild);
87
92
  button.addEventListener('click', () => { if (busy) return; if (dirty || comfyTemplate) { editor(true); feedback('当前有未保存编辑,请先保存,或重新读取后切换。', true); return; } edit(provider); editor(true); setView('connection'); feedback(''); });
88
93
  return button;
89
94
  }));
@@ -140,6 +145,11 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
140
145
  options(select, [['disabled', '停用'], ['auto', '自动选择'], ['manual', '手动排序']], policies[cap].mode);
141
146
  select.addEventListener('change', () => { policies[cap].mode = select.value; touched.add(cap); mark(); renderPolicies(); });
142
147
  heading.append(select); row.append(heading);
148
+ if (networkFacts) {
149
+ const ids = all.filter(provider => provider.models.some(model => model.capabilities.includes(cap))).map(provider => provider.id);
150
+ const routes = [...new Set(ids.map(id => networkRouteLabel(routeFor(id))))];
151
+ row.append(element('p', '候选配置出口 · ' + (routes.join(' / ') || '暂无候选') + ';实际路线以任务记录为准。', 'field-note network-route-note'));
152
+ }
143
153
  if (policies[cap].mode === 'manual') {
144
154
  const candidates = all.flatMap(provider => provider.models.filter(model => model.capabilities.includes(cap)).map(model => ({ name: provider.name + ' / ' + model.id, ref: ref(provider.id, model.id) })));
145
155
  for (const saved of policies[cap].models) if (!candidates.some(item => item.ref === saved)) candidates.push({ name: saved + ' · 当前不可用', ref: saved });
@@ -175,15 +185,21 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
175
185
  $('provider-editor-title').textContent = provider ? '编辑供应商' : '添加供应商';
176
186
  for (const [key, field] of [['name', 'name'], ['url', 'baseUrl'], ['media-url', 'mediaBaseUrl'], ['auth', 'auth']]) $('provider-' + key).value = draft[field];
177
187
  options($('provider-protocol'), data.protocols.map(item => [item.id, item.label]), draft.mediaProtocol);
178
- $('provider-enabled').checked = draft.enabled; $('provider-key-action').value = provider ? 'keep' : 'replace'; $('provider-key').value = ''; keyAction();
179
- $('provider-key-state').textContent = draft.hasCredential ? '已保存 · 不回显' : '尚未保存';
188
+ $('provider-enabled').checked = draft.enabled; $('provider-key-action').value = provider ? 'keep' : 'replace'; $('provider-key').value = ''; $('provider-key-env').value = draft.apiKeyEnv || ''; keyAction();
189
+ $('provider-key-state').textContent = draft.apiKeyEnv ? '环境变量 · ' + ({ ready: '已就绪', missing: '未就绪', invalid: '值无效' }[draft.credentialStatus] || '未就绪') : draft.hasCredential ? '已保存 · 不回显' : '尚未保存';
180
190
  $('provider-id-note').textContent = '稳定 ID:' + draft.id; $('provider-preset').value = draft.mediaProtocol === 'comfyui' ? 'comfyui' : 'custom'; $('provider-model-id').value = '';
181
191
  if (draft.mediaProtocol !== 'comfyui') apiDraft = apiFields();
182
192
  $('provider-advanced').open = false; $('provider-management-tools').open = false; $('provider-delete-confirm').hidden = true; $('provider-directory').hidden = true;
183
193
  $('provider-directory-note').textContent = '目录只使用已保存连接;成功不代表媒体实测通过。';
184
194
  list(); renderModels(); renderPolicies(); controls();
185
195
  }
186
- function keyAction() { const replacing = $('provider-key-action').value === 'replace'; $('provider-key-field').hidden = !replacing; $('provider-key').required = replacing; if (!replacing) $('provider-key').value = ''; }
196
+ function keyAction() {
197
+ const action = $('provider-key-action').value;
198
+ $('provider-key-field').hidden = action !== 'replace'; $('provider-key').required = action === 'replace';
199
+ $('provider-key-env-field').hidden = action !== 'env' && !(action === 'keep' && draft?.apiKeyEnv);
200
+ $('provider-key-env').required = action === 'env'; $('provider-key-env').readOnly = action !== 'env';
201
+ if (action !== 'replace') $('provider-key').value = '';
202
+ }
187
203
  function readForm() {
188
204
  draft.name = $('provider-name').value; draft.baseUrl = $('provider-url').value; draft.mediaBaseUrl = $('provider-media-url').value;
189
205
  draft.mediaProtocol = $('provider-protocol').value; draft.auth = $('provider-auth').value; draft.enabled = $('provider-enabled').checked;
@@ -206,7 +222,7 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
206
222
  } catch (error) { if (ticket === generation) { feedback(describe(error), true); onSettings(null, describe(error)); } }
207
223
  finally { if (ticket === generation) { busy = false; controls(); } }
208
224
  }
209
- const reasonLabels = { provider_disabled: '供应商已停用', connection_unconfigured: '连接未就绪', function_disabled: '功能已停用', unconfirmed: '用途待确认', protocol_unsupported: '当前协议不支持', model_missing: '模型不存在,已隔离', cooling: '限流冷却中', quota: '额度或预算耗尽', outside_pool: '未加入当前模型池' };
225
+ const reasonLabels = { provider_disabled: '供应商已停用', credential_unavailable: '环境凭据未就绪', connection_unconfigured: '连接未就绪', function_disabled: '功能已停用', unconfirmed: '用途待确认', protocol_unsupported: '当前协议不支持', model_missing: '模型不存在,已隔离', cooling: '限流冷却中', quota: '额度或预算耗尽', outside_pool: '未加入当前模型池' };
210
226
  const time = value => value ? new Date(value).toLocaleString('zh-CN') : '尚无记录';
211
227
  async function readDiagnostics() {
212
228
  if (!client || !data) return;
@@ -233,12 +249,15 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
233
249
  card.append(element('p', (model.reason ? reasonLabels[model.reason] : '候选 #' + model.rank) + ' · '
234
250
  + ({ verified: '近期成功', failed: '认证或权限受阻', configured: '尚无近期成功证据' }[model.state] || model.state), 'field-note'));
235
251
  card.append(element('p', '最近证据 · ' + time(model.evidence?.at) + (model.retryAt ? ' · 冷却至 ' + time(model.retryAt) : ''), 'field-note'));
236
- const supported = diagnostics.execution && model.eligible && (healthCapability === 'image-gen' || healthCapability === 'tts' && diagnostics.audio || healthCapability === 'transcribe' && diagnostics.audio);
252
+ const videoReady = diagnostics.video && !/s2v/i.test(model.modelId);
253
+ const supported = diagnostics.execution && model.eligible && (healthCapability === 'image-gen' || healthCapability === 'video-gen' && videoReady
254
+ || healthCapability === 'tts' && diagnostics.audio || healthCapability === 'transcribe' && diagnostics.audio);
237
255
  if (supported) {
238
256
  const test = element('button', '准备功能验证', 'secondary'); test.type = 'button'; test.dataset.probeModel = model.ref;
239
257
  test.disabled = busy; test.addEventListener('click', () => onProbe({ capability: healthCapability, modelRef: model.ref, comfy: model.comfy })); card.append(test);
240
258
  card.append(element('p', model.comfy ? '在创作页核对工作流和参数后提交,可能产生费用。' : '打开指定模型的验证草稿;点击创作页的生成按钮后才调用,可能产生费用。', 'field-note'));
241
- } else card.append(element('p', ['video-gen', 'music-gen', 'vision'].includes(healthCapability) ? '此功能的 Web 执行尚未接入,请使用现有 CLI / DSH 明确验证。' : '请先开放执行、配置功能池并处理受阻原因,再准备验证。', 'field-note'));
259
+ } else card.append(element('p', healthCapability === 'video-gen' ? '文生视频验证需要新 v4 工作区、执行权限与可用候选;S2V 使用现有 CLI / DSH。'
260
+ : ['music-gen', 'vision'].includes(healthCapability) ? '此功能的 Web 执行尚未接入,请使用现有 CLI / DSH 明确验证。' : '请先开放执行、配置功能池并处理受阻原因,再准备验证。', 'field-note'));
242
261
  return card;
243
262
  }) : [element('p', diagnostics ? '当前没有已标注此用途的模型。' : '检测状态尚未读取;不会当作通过。', 'field-note')]));
244
263
  $('provider-monitors').replaceChildren(...(data?.providers || []).map(provider => {
@@ -259,7 +278,7 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
259
278
  }));
260
279
  }
261
280
  function enterSection(next) {
262
- section = next; draft = apiDraft = null; dirty = unknown = false; touched.clear(); resetComfyImport(); $('provider-key').value = '';
281
+ section = next; draft = apiDraft = null; dirty = unknown = false; touched.clear(); resetComfyImport(); $('provider-key').value = ''; $('provider-key-env').value = '';
263
282
  policies = structuredClone(data?.assignments || {}); editor(false);
264
283
  if (data) list();
265
284
  $('provider-pool-panel').hidden = next !== 'pools'; $('provider-health-panel').hidden = next !== 'health';
@@ -318,7 +337,7 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
318
337
  if ($('provider-protocol').value !== 'comfyui') resetComfyImport(); controls();
319
338
  });
320
339
  $('provider-auth').addEventListener('change', () => {
321
- if ($('provider-auth').value === 'none' && !draft.hasCredential && !$('provider-key').value) { $('provider-key-action').value = 'keep'; keyAction(); }
340
+ if ($('provider-auth').value === 'none' && !draft.hasCredential && !$('provider-key').value && $('provider-key-action').value !== 'env') { $('provider-key-action').value = 'keep'; keyAction(); }
322
341
  });
323
342
  $('provider-new').addEventListener('click', () => { if (!data || busy) return; if (dirty || comfyTemplate) { editor(true); feedback('请先保存当前草稿,或重新读取后添加。', true); return; } edit(); editor(true); setView('connection'); feedback(''); $('provider-name').focus(); });
324
343
  for (const button of document.querySelectorAll('[data-provider-view]')) button.addEventListener('click', () => setView(button.dataset.providerView));
@@ -336,6 +355,7 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
336
355
  $('provider-pool-form').addEventListener('submit', event => { event.preventDefault(); if (touched.size) void manageProvider('policies', { assignments: Object.fromEntries([...touched].map(cap => [cap, policies[cap]])) }); });
337
356
  $('provider-pool-cancel').addEventListener('click', () => leave(() => enterSection('pools')));
338
357
  $('provider-health-refresh').addEventListener('click', () => readDiagnostics());
358
+ $('provider-network-link').addEventListener('click', () => leave(() => { const id = draft?.id; enterSection('suppliers'); onNetworkOpen(id); }));
339
359
  $('provider-toggle').addEventListener('click', () => { const saved = data.providers.find(provider => provider.id === draft.id); void manageProvider('enabled', { providerId: saved.id, enabled: !saved.enabled }); });
340
360
  $('provider-delete').addEventListener('click', () => manageProvider('impact', { providerId: draft.id }));
341
361
  $('provider-delete-yes').addEventListener('click', () => manageProvider('remove', { providerId: draft.id, confirm: true }));
@@ -377,7 +397,7 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
377
397
  const provider = Object.fromEntries(['id', 'name', 'baseUrl', 'mediaBaseUrl', 'mediaProtocol', 'enabled', 'auth'].map(key => [key, draft[key]]));
378
398
  provider.models = draft.models.map(model => ({ id: model.id, capabilities: model.capabilities, imageProtocol: model.imageProtocol || 'auto', musicProtocol: model.musicProtocol || '',
379
399
  ...(model.comfyWorkflow?.graph ? { comfyWorkflow: model.comfyWorkflow } : {}) }));
380
- const action = $('provider-key-action').value, credential = { action, ...(action === 'replace' ? { value: $('provider-key').value } : {}) };
400
+ const action = $('provider-key-action').value, credential = { action, ...(action === 'replace' ? { value: $('provider-key').value } : action === 'env' ? { name: $('provider-key-env').value } : {}) };
381
401
  const input = { expectedRevision: data.revision, provider, credential, assignments: Object.fromEntries([...touched].map(cap => [cap, policies[cap]])) };
382
402
  $('provider-close-confirm').hidden = true; busy = true; controls(); feedback('正在保存连接、模型与用途…');
383
403
  try {
@@ -485,6 +505,7 @@ export function createProviderPanel({ describe, onSaved, onSettings, onProbe = (
485
505
  $('comfyui-replace-yes').addEventListener('click', () => addWorkflow(true));
486
506
  $('comfyui-replace-no').addEventListener('click', () => { $('comfyui-replace').hidden = true; });
487
507
  return Object.freeze({
508
+ setNetworkFacts(value) { networkFacts = value; if (data) { list(); if (section === 'pools') renderPolicies(); controls(); } },
488
509
  async connect(value) { client = value; generation++; dirty = false; data = null; draft = null; section = 'suppliers'; diagnostics = null; editor(false); $('provider-pool-panel').hidden = $('provider-health-panel').hidden = true; await refresh(); },
489
510
  disconnect() { generation++; resetComfyImport(); client = null; data = draft = apiDraft = diagnostics = null; pendingLeave = null; directory = []; policies = {}; touched.clear(); dirty = busy = unknown = false; section = 'suppliers'; editor(false); $('provider-pool-panel').hidden = $('provider-health-panel').hidden = true; for (const id of ['provider-spectrum','provider-health-models','provider-monitors']) $(id).replaceChildren(); $('provider-key').value = ''; $('provider-form').reset(); $('provider-models').replaceChildren(); $('providers-list').replaceChildren(); $('provider-directory-list').replaceChildren(); $('provider-assignments').replaceChildren(); $('providers-reload-confirm').hidden = true; feedback(''); },
490
511
  show(visible) { if (visible && client && data && !busy && !dirty && !comfyTemplate) void refresh(); }
@@ -4,13 +4,13 @@ export const MAX_RECOVERY_ACTIONS = 8;
4
4
  const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
5
5
  const IDS = { artifactId: /^artifact_[a-f0-9]{24}$/, runId: /^run_[a-f0-9]{24}$/, operationId: /^op_[a-f0-9]{64}$/ };
6
6
  const FIELDS = {
7
- 'image-import': ['key', 'artifactId'], 'flow-create': ['key', 'artifactId', 'runId'], 'generation-create': ['key', 'runId'],
7
+ 'image-import': ['key', 'artifactId'], 'flow-create': ['key', 'artifactId', 'runId'], 'generation-create': ['key', 'runId'], 'video-create': ['key', 'runId'],
8
8
  'audio-import': ['key', 'artifactId'], 'speech-create': ['key', 'runId'], 'transcription-create': ['key', 'artifactId', 'runId'],
9
9
  continue: ['key', 'runId', 'expectedRevision', 'operationId'],
10
10
  selection: ['runId', 'artifactId', 'expectedRevision'], stop: ['operationId']
11
11
  };
12
12
  const REQUIRED = {
13
- 'image-import': ['key'], 'flow-create': ['key', 'artifactId'], 'generation-create': ['key'], continue: ['key', 'runId', 'expectedRevision'],
13
+ 'image-import': ['key'], 'flow-create': ['key', 'artifactId'], 'generation-create': ['key'], 'video-create': ['key'], continue: ['key', 'runId', 'expectedRevision'],
14
14
  'audio-import': ['key'], 'speech-create': ['key'], 'transcription-create': ['key', 'artifactId'],
15
15
  selection: ['runId', 'artifactId', 'expectedRevision'], stop: ['operationId']
16
16
  };
@@ -13,6 +13,30 @@
13
13
  --purple:#b8b2ef; --warning:#edc981; --error:#ffb4ab;
14
14
  }
15
15
  * { box-sizing:border-box; }
16
+ .network-grid { align-items:stretch; }
17
+ #network-stats { grid-template-columns:repeat(4,minmax(0,1fr)); }
18
+ #network-panel .badge { flex-shrink:0; white-space:nowrap; }
19
+ .network-grid .network-card { --network-color:#45317D; min-width:0; position:relative; padding:20px; background:linear-gradient(135deg,color-mix(in srgb,var(--network-color) 7%,var(--panel)),var(--panel) 80%); }
20
+ .network-card::before { content:""; display:block; width:34px; height:3px; border-radius:3px; background:var(--network-color); margin-bottom:18px; }
21
+ .network-grid [data-network-tone="1"] { --network-color:#6F69B8; }.network-grid [data-network-tone="2"] { --network-color:#BB78AD; }.network-grid [data-network-tone="3"] { --network-color:#E59B76; }.network-grid [data-network-tone="4"] { --network-color:#ED9C91; }.network-grid [data-network-tone="5"] { --network-color:#F0D09C; }
22
+ .network-kicker { color:var(--muted); font-size:12px; letter-spacing:.04em; }
23
+ .network-card h3 { margin:14px 0 6px; font-size:19px; overflow-wrap:anywhere; }
24
+ .network-route { font-weight:550; overflow-wrap:anywhere; margin-bottom:8px; }
25
+ .network-card-action { padding-left:0; margin-top:12px; }
26
+ .network-route-note { display:block; font-size:12px; color:var(--muted); overflow-wrap:anywhere; }
27
+ .network-link { margin-bottom:12px; }
28
+ .network-setup { padding:22px; margin-bottom:20px; }.network-setup button { margin:14px 0; }.network-setup pre { white-space:pre-wrap; overflow-wrap:anywhere; font-size:12px; }
29
+ .network-editor { max-width:850px; }.network-editor .field-label { display:flex; flex-direction:column; gap:5px; margin-bottom:12px; }.network-editor h3 { margin:18px 0 10px; }
30
+ .network-node-address { display:grid; grid-template-columns:100px minmax(0,1fr) 110px; gap:12px; }
31
+ .network-editor .mini-check { margin:10px 0; }.network-binding-name { font-size:18px; font-weight:600; margin-bottom:16px; overflow-wrap:anywhere; }
32
+ .network-candidates { display:grid; gap:8px; }.network-candidate { display:flex; align-items:center; flex-wrap:wrap; gap:5px; padding:8px 10px; border:1px solid var(--line); border-radius:9px; }.network-candidate>span { flex:1; min-width:120px; overflow-wrap:anywhere; }.network-candidate button { min-height:36px; padding:5px 8px; font-size:12px; }
33
+ .network-candidate-add { display:flex; gap:8px; margin:12px 0; }.network-candidate-add select { flex:1; }
34
+ #network-doctor { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); align-items:start; gap:20px; }
35
+ .network-doctor-card,.network-probe-card { padding:22px; min-width:0; }.network-facts { display:grid; grid-template-columns:minmax(90px,.65fr) minmax(0,1.4fr); gap:10px 16px; margin:20px 0; }.network-facts dt { color:var(--muted); }.network-facts dd { margin:0; overflow-wrap:anywhere; }
36
+ .network-probe-card .field-label { display:flex; flex-direction:column; gap:5px; margin:16px 0; }.network-probe-result { border-top:1px solid var(--line); padding-top:16px; margin-top:16px; overflow-wrap:anywhere; }.network-probe-result strong { display:block; margin-bottom:8px; }.network-other-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:12px 0; }.network-other-row span { flex:1; overflow-wrap:anywhere; }
37
+ #network-other-bindings { margin-top:20px; padding:18px; }
38
+ @media(max-width:950px) { #network-doctor { grid-template-columns:1fr; } }
39
+ @media(max-width:600px) { #network-stats { grid-template-columns:repeat(2,minmax(0,1fr)); }.network-node-address { grid-template-columns:minmax(0,1fr) minmax(0,1fr); }.network-node-address>label:nth-child(2) { grid-column:1/-1; grid-row:2; }.network-card,.network-doctor-card,.network-probe-card { padding:17px; }.network-facts { grid-template-columns:1fr; gap:3px; }.network-facts dd { margin-bottom:12px; } }
16
40
  [hidden] { display:none!important; }
17
41
  body,h1,h2,h3,p,figure { margin:0; }
18
42
  button,input,select,textarea { font:inherit; min-width:0; }
@@ -71,6 +95,23 @@ body.connected .site-header { position:sticky; top:0; z-index:8; }
71
95
  .workspace-nav { display:grid; gap:5px; }
72
96
  .plugin-navigation { display:grid; gap:5px; border-top:1px solid var(--line); padding-top:8px; margin-top:5px; }
73
97
  .plugin-slot { min-width:0; }
98
+ .plugins-panel { max-width:1040px; margin:0 auto 32px; }
99
+ .plugin-status-list { display:grid; gap:14px; margin:18px 0; }
100
+ .plugin-status-card { overflow:hidden; border-top:2px solid color-mix(in srgb,var(--purple) 35%,var(--line)); }
101
+ .plugin-status-card>summary { display:flex; align-items:center; gap:14px; padding:18px 20px; cursor:pointer; list-style:none; }
102
+ .plugin-status-card>summary::-webkit-details-marker { display:none; }
103
+ .plugin-status-mark { display:grid; place-items:center; width:40px; height:40px; border-radius:12px; flex-shrink:0; color:var(--accent); background:var(--tint); font-size:24px; }
104
+ .plugin-status-title { flex:1; min-width:0; }.plugin-status-title strong,.plugin-status-title small { display:block; overflow-wrap:anywhere; }.plugin-status-title small { color:var(--muted); font-size:12px; margin-top:4px; }
105
+ .plugin-state { display:inline-block; color:var(--accent); background:var(--tint); padding:3px 9px; border-radius:20px; font-size:11px; flex-shrink:0; }
106
+ .plugin-status-body { padding:0 20px 20px; }.plugin-identity { font-size:11px; color:var(--muted); overflow-wrap:anywhere; }
107
+ .plugin-contribution-status,.plugin-grants,.plugin-instances { list-style:none; padding:0; margin:12px 0; }
108
+ .plugin-contribution-status { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
109
+ .plugin-contribution-status>li { min-width:0; padding:14px; border:1px solid var(--line); border-radius:10px; }.plugin-contribution-status strong { display:block; overflow-wrap:anywhere; }
110
+ .plugin-kind { display:block; color:var(--muted); font-size:11px; margin-bottom:4px; }.plugin-contribution-status .plugin-identity { margin-top:6px; }
111
+ .plugin-grant { padding:14px 0; border-bottom:1px solid var(--line); }.plugin-grant:last-child { border-bottom:0; }.plugin-grant-heading { margin-bottom:6px; overflow-wrap:anywhere; }.plugin-grant[data-status="version"]>.plugin-state,.plugin-grant[data-status="missing"]>.plugin-state { color:var(--warning); background:var(--soft); }
112
+ .plugin-instances { color:var(--muted); font-size:12px; margin:8px 0 0; }
113
+ .plugin-missing-section { padding:20px; margin:18px 0; }.plugin-status-footer { display:flex; gap:16px; align-items:center; justify-content:space-between; flex-wrap:wrap; margin:20px 0; }.plugin-status-footer p { max-width:650px; margin:0; }.plugin-status-help code { display:block; font-size:11px; overflow-wrap:anywhere; padding:8px 0; }
114
+ @media(max-width:640px) { .plugin-status-card>summary { flex-wrap:wrap; gap:10px; padding:15px; }.plugin-status-title { flex-basis:calc(100% - 60px); }.plugin-status-card>summary>.plugin-state { margin-left:50px; }.plugin-status-card>summary>.disclosure { margin-left:auto; }.plugin-status-body { padding:0 15px 15px; }.plugin-contribution-status { grid-template-columns:minmax(0,1fr); }.plugin-missing-section { padding:15px; } }
74
115
  .plugin-slot.card { padding:20px; }
75
116
  .plugin-settings-card { margin-bottom:12px; padding:16px; }
76
117
  .plugin-history-item { padding:20px; margin:14px 0; }
@@ -313,6 +354,12 @@ dt { color:var(--muted); } dd { margin:0; overflow-wrap:anywhere; } .run-details
313
354
  .check-option small { font-size:12px; display:block; color:var(--muted); font-weight:400; }
314
355
  .scope-options,.connection-form-actions,.export-options,.export-actions { display:flex; flex-wrap:wrap; gap:8px; }
315
356
  .connection-form-actions { align-items:center; margin-top:18px; } .connection-form-actions span { font-size:12px; }
357
+ .connection-steps { flex-wrap:wrap; column-gap:16px; row-gap:2px; }.connection-steps button:last-child { margin-left:auto; }
358
+ .connection-draft-note { margin:-8px 0 18px; padding:10px 12px; background:var(--soft); border-radius:8px; color:var(--warning); }
359
+ .connection-grant-review { margin-top:20px; padding:12px 14px; border-radius:10px; background:var(--soft); }.connection-grant-review p { font-size:12px; overflow-wrap:anywhere; margin-top:4px; }
360
+ .connection-next { margin:20px 0 10px; padding-top:16px; border-top:1px solid var(--line); }
361
+ .connection-evidence { padding:0; margin:20px 0; list-style:none; display:grid; gap:12px; }.connection-evidence>li { display:grid; grid-template-columns:30px minmax(0,1fr); gap:12px; padding:16px; border:1px solid var(--line); border-radius:11px; }.connection-evidence-number { display:grid; place-items:center; align-self:start; width:28px; height:28px; border-radius:9px; color:var(--accent); background:var(--tint); font-size:12px; }.connection-evidence h3 { font-size:13px; margin:3px 0; }.connection-check-issues,.connection-client-evidence,.connection-check-tools { padding-left:16px; font-size:12px; overflow-wrap:anywhere; margin:8px 0; }.connection-check-issues { color:var(--warning); }.connection-check-tools { max-height:200px; overflow:auto; font-family:monospace; }.connection-evidence [data-state="passed"] { color:var(--accent); }.connection-evidence [data-state="limited"] { color:var(--warning); }
362
+ @media(max-width:640px) { .connection-steps { gap:2px 13px; }.connection-steps button:last-child { margin-left:0; }.connection-evidence>li { padding:12px; gap:9px; } }
316
363
  #connection-tools { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:6px; padding:6px 0 12px; }
317
364
  .connection-section { border-top:1px solid var(--line); margin-top:22px; padding-top:3px; }
318
365
  .export-options { align-items:end; margin:13px 0; } .export-options label { font-size:12px; color:var(--muted); }
@@ -447,7 +494,7 @@ footer { display:flex; justify-content:space-between; gap:12px; padding:24px 0;
447
494
  .nav-item>span:first-child { display:none; }
448
495
  .workspace-nav { flex-wrap:wrap; }
449
496
  .recent-panel { width:100%; margin:0; } .recent-panel:not([open]) { max-height:44px; } #history-list { max-height:190px; overflow:auto; }
450
- body[data-page="overview"] .recent-panel,body[data-page="connections"] .recent-panel,body[data-page="providers"] .recent-panel { display:none; }
497
+ body[data-page="overview"] .recent-panel,body[data-page="connections"] .recent-panel,body[data-page="providers"] .recent-panel,body[data-page="plugins"] .recent-panel { display:none; }
451
498
  .site-header { min-height:54px; } .breadcrumb { font-size:12px; }
452
499
  }
453
500
  @media (max-width:640px) {
@@ -488,6 +535,9 @@ footer { display:flex; justify-content:space-between; gap:12px; padding:24px 0;
488
535
  .appearance-dialog { padding:20px; }
489
536
  .site-header { flex-wrap:wrap; }
490
537
  }
538
+ @media (max-width:520px) {
539
+ .provider-credentials-fields { grid-template-columns:1fr; }
540
+ }
491
541
  @media (max-width:360px) {
492
542
  .app-sidebar { gap:10px; padding-inline:14px; } .nav-item { padding-inline:9px; }
493
543
  .brand { font-size:23px; } .site-header .breadcrumb { display:none; } .header-actions { width:100%; justify-content:end; }
@@ -516,3 +566,6 @@ audio { display:block; width:100%; max-width:100%; min-width:0; margin-block:10p
516
566
  .gallery-audio .export-actions button { padding-inline:9px; font-size:12px; }
517
567
  .gallery-audio .field-note { white-space:normal; }
518
568
  @media(max-width:480px) { .audio-result-header { gap:12px; } .audio-result-header h3 { font-size:17px; } .audio-result-icon { width:48px; height:48px; } .transcript-result { padding:12px; } }
569
+ .video-result { padding: clamp(16px, 3vw, 28px); }
570
+ .video-result video, .gallery-audio video { display: block; width: 100%; max-height: 65vh; margin-block: 16px; border-radius: 16px; background: #16141c; object-fit: contain; }
571
+ .video-result video[hidden], .gallery-audio video[hidden] { display: none; }
@@ -56,7 +56,7 @@ export function createReadClient(token, { onUnauthorized = () => {} } = {}) {
56
56
  get,
57
57
  callPlugin({ key, ...value }, { signal } = {}) { return post('/api/v0/plugin-operations', value, { key: id(key, KEY), signal }); },
58
58
  lookupCreation(kind, key, options) {
59
- if (!['image-import', 'flow-create', 'generation-create', 'audio-import', 'speech-create', 'transcription-create'].includes(kind)) throw new TypeError('创建类型无效');
59
+ if (!['image-import', 'flow-create', 'generation-create', 'video-create', 'audio-import', 'speech-create', 'transcription-create'].includes(kind)) throw new TypeError('创建类型无效');
60
60
  return get('/api/v0/creation-requests?' + new URLSearchParams({ kind, key: id(key, KEY) }), options);
61
61
  },
62
62
  lookupOperation(key, options) { return get('/api/v0/operations?' + new URLSearchParams({ key: id(key, KEY) }), options); },
@@ -67,6 +67,7 @@ export function createReadClient(token, { onUnauthorized = () => {} } = {}) {
67
67
  return post('/api/v0/images', undefined, { body: image, mediaType: image.type, key: id(key, KEY), signal });
68
68
  },
69
69
  createRun(value, { key, signal } = {}) { return post('/api/v0/runs', value, { key: id(key, KEY), signal }); },
70
+ createVideo(value, { key, signal } = {}) { return post('/api/v0/videos', value, { key: id(key, KEY), signal }); },
70
71
  createGeneration(value, { key, signal } = {}) { return post('/api/v0/generations', value, { key: id(key, KEY), signal }); },
71
72
  uploadAudio(audio, { key, signal } = {}) {
72
73
  if (!(audio instanceof Blob) || !['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac'].includes(audio.type)) throw new TypeError('需要WAV/MP3/OGG/FLAC音频');
@@ -91,6 +92,10 @@ export function createReadClient(token, { onUnauthorized = () => {} } = {}) {
91
92
  if (!['save', 'discover', 'workflow-check', 'policies', 'enabled', 'remove', 'impact', 'check', 'monitor'].includes(action)) throw new TypeError('仅允许明确的供应商管理操作');
92
93
  return post('/api/v0/provider-management/' + action, value, { signal });
93
94
  },
95
+ postNetwork(action, value, { signal } = {}) {
96
+ if (!['init', 'nodes/add', 'nodes/set', 'nodes/remove', 'pools/add', 'pools/set', 'pools/remove', 'bindings/set', 'bindings/remove', 'probe'].includes(action)) throw new TypeError('仅允许明确的网络管理操作');
97
+ return post('/api/v0/network-management/' + action, value, { signal });
98
+ },
94
99
  close() { lifetime.abort(); for (const job of [...reads.splice(0), ...media.splice(0)]) job.reject(lifetime.signal.reason); }
95
100
  });
96
101
  }
@@ -18,11 +18,12 @@ export function normalizeGalleryQuery(input = {}) {
18
18
  export async function workbenchArtifacts(input, options = {}) {
19
19
  const query = normalizeGalleryQuery(input);
20
20
  const items = [];
21
- let droppedCore = 0, coreError = '';
21
+ let droppedCore = 0, legacyCoreSkipped = 0, coreError = '';
22
22
  if (query.source !== 'legacy') {
23
23
  try {
24
24
  const core = await options.core.galleryArtifactsForDsh(options);
25
25
  droppedCore = core.dropped;
26
+ legacyCoreSkipped = core.legacySkipped || 0;
26
27
  for (const artifact of core.artifacts) {
27
28
  if (artifact.kind === 'host-input') continue;
28
29
  items.push({ id: artifact.id, file: artifact.id, mime: artifact.mediaType,
@@ -47,5 +48,5 @@ export async function workbenchArtifacts(input, options = {}) {
47
48
  const offset = matches.length ? Math.min(query.offset, Math.floor((matches.length - 1) / query.limit) * query.limit) : 0;
48
49
  return { total: matches.length, offset, limit: query.limit, kinds,
49
50
  counts: { core: matches.filter(item => item.source === 'core').length, legacy: matches.filter(item => item.source === 'legacy').length },
50
- droppedCore, ...(coreError ? { coreError } : {}), items: matches.slice(offset, offset + query.limit) };
51
+ droppedCore, legacyCoreSkipped, ...(coreError ? { coreError } : {}), items: matches.slice(offset, offset + query.limit) };
51
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mokuyoaxis/dsh-iris",
3
- "version": "0.3.0-alpha.1",
3
+ "version": "0.3.0-beta.1",
4
4
  "description": "A multimodal workspace for Agents: Core Web, MCP, CLI and native DSH tools with model pools, durable media tasks, ComfyUI and explicit plugins.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -25,6 +25,8 @@
25
25
  },
26
26
  "files": [
27
27
  "plugins/contact-sheet",
28
+ "templates/plugin-starter",
29
+ "templates/provider-starter",
28
30
  "lib",
29
31
  "bin",
30
32
  "cordis.patch.yml",
@@ -79,7 +81,7 @@
79
81
  ],
80
82
  "license": "MIT",
81
83
  "engines": {
82
- "node": ">=22.0.0",
84
+ "node": ">=22.1.0",
83
85
  "dsh": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
84
86
  },
85
87
  "scripts": {
@@ -114,6 +116,7 @@
114
116
  "dependencies": {
115
117
  "@modelcontextprotocol/server": "2.3.1",
116
118
  "sharp": "^0.35.5",
119
+ "undici": "7.30.1",
117
120
  "zod": "4.6.7"
118
121
  },
119
122
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  # ComfyUI workflows · 工作流、参数与 API 搭配
2
2
 
3
- This reference describes the **unreleased 0.3-target implementation**, not npm 0.2.7. Confirm the installed command and returned catalog before using it; missing support does not authorize installing or upgrading software. Keep the explicit roots and private provider file from the [CLI reference](cli.md).
3
+ This reference describes the **0.3.0-beta.1 implementation**, not stable npm 0.2.7. Confirm the installed command and returned catalog before using it; missing support does not authorize installing or upgrading software. Keep the explicit roots and private provider file from the [CLI reference](cli.md).
4
4
 
5
5
  ## Ask before creating · 提问确认需求
6
6
 
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Messia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.