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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/README.en.md +109 -311
  3. package/README.md +106 -309
  4. package/bin/dsh-iris.js +452 -92
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +2 -0
  7. package/docs/ARCHITECTURE.md +13 -3
  8. package/docs/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +87 -0
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +62 -0
  12. package/docs/CORE_CONNECTIONS.md +101 -0
  13. package/docs/CORE_QUICKSTART.md +110 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  15. package/docs/CORE_SERVICE.md +101 -0
  16. package/docs/CORE_SKILLS.md +35 -3
  17. package/docs/DSH_COMPATIBILITY.md +21 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +6 -2
  21. package/docs/HEADLESS_CLI.md +30 -5
  22. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  23. package/docs/IMAGE_FLOW.md +4 -0
  24. package/docs/IMAGE_GENERATION.md +127 -0
  25. package/docs/LOCAL_API.md +4 -2
  26. package/docs/LOCAL_API_CONTRACT.md +42 -15
  27. package/docs/LOCAL_CREATION.md +7 -1
  28. package/docs/LOCAL_OPERATIONS.md +46 -0
  29. package/docs/LOCAL_SELECTION.md +57 -0
  30. package/docs/LOCAL_WEB.md +207 -0
  31. package/docs/MCP.md +212 -0
  32. package/docs/MEDIA_SCHEDULING.md +72 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/NETWORK_RESOURCES.md +106 -0
  35. package/docs/NETWORK_ROUTING.md +103 -0
  36. package/docs/NETWORK_WORKBENCH.md +65 -0
  37. package/docs/PANEL_EXTENSIONS.md +64 -0
  38. package/docs/PLUGINS.md +193 -0
  39. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  40. package/docs/PLUGIN_STARTUP.md +113 -0
  41. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  42. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  43. package/docs/PROVIDER_EXTENSIONS.md +82 -0
  44. package/docs/PROVIDER_HEALTH.md +5 -1
  45. package/docs/PROVIDER_NETWORK.md +45 -0
  46. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  47. package/docs/PROVIDER_SETTINGS.md +91 -0
  48. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
  49. package/docs/ROADMAP.md +18 -8
  50. package/docs/SECURITY.md +1 -1
  51. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  52. package/docs/VIDEO_FLOWS.md +89 -0
  53. package/docs/VISION_CLI.md +1 -1
  54. package/docs/VISION_MODEL.md +2 -0
  55. package/docs/WEB_PROTOTYPE.md +21 -6
  56. package/docs/WEB_RECOVERY.md +46 -0
  57. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  58. package/docs/WORKBENCH_LOGIN.md +63 -0
  59. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  60. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  61. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  62. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  63. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  64. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  65. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  66. package/docs/releases/0.3.0-alpha.1.md +51 -0
  67. package/docs/releases/0.3.0-beta.1.md +34 -0
  68. package/docs/screenshots.md +41 -1
  69. package/lib/actions.js +56 -4
  70. package/lib/adapters.js +75 -49
  71. package/lib/api.js +23 -13
  72. package/lib/application-runtime.js +73 -17
  73. package/lib/application-writer.js +193 -0
  74. package/lib/audio-flow-input.js +37 -0
  75. package/lib/client.d.ts +3 -3
  76. package/lib/client.js +402 -72
  77. package/lib/comfyui-adapter.js +95 -0
  78. package/lib/comfyui-client.js +60 -0
  79. package/lib/comfyui-inspection.js +73 -0
  80. package/lib/comfyui-workflow.js +94 -0
  81. package/lib/command-service.js +18 -5
  82. package/lib/config.js +49 -10
  83. package/lib/configured-image-runtime.js +69 -0
  84. package/lib/connection-skill.js +98 -0
  85. package/lib/connection-template.js +23 -0
  86. package/lib/core-artifact-store.js +84 -28
  87. package/lib/core-bridge-selection.js +31 -0
  88. package/lib/core-bridge-tools.js +85 -0
  89. package/lib/core-capability-catalog.js +192 -0
  90. package/lib/core-connection-diagnostics.js +49 -0
  91. package/lib/core-connection-store.js +277 -0
  92. package/lib/core-connections.js +225 -0
  93. package/lib/core-delivery-cache.js +8 -3
  94. package/lib/core-maintenance.js +9 -3
  95. package/lib/core-runtime.js +2 -1
  96. package/lib/core-service-client.js +174 -0
  97. package/lib/core-tasks.js +113 -21
  98. package/lib/core-writer-lease.js +16 -0
  99. package/lib/creation-request-store.js +41 -64
  100. package/lib/doctor.js +2 -2
  101. package/lib/dsh-core-adapter.js +61 -9
  102. package/lib/dsh-host-adapter.js +4 -1
  103. package/lib/dsh-provider-config.js +1 -1
  104. package/lib/dsh-service-bridge.js +67 -0
  105. package/lib/execution-ownership.js +43 -0
  106. package/lib/flow-store.js +156 -14
  107. package/lib/generation-input.js +10 -1
  108. package/lib/headless-vision.js +7 -7
  109. package/lib/image-edit-input.js +1 -1
  110. package/lib/image-flow.js +97 -31
  111. package/lib/index.js +47 -22
  112. package/lib/local-api-projection.js +15 -6
  113. package/lib/local-api-token-directory.js +31 -0
  114. package/lib/local-api.js +861 -71
  115. package/lib/local-creation.js +96 -24
  116. package/lib/local-operations.js +36 -0
  117. package/lib/local-request-root.js +86 -0
  118. package/lib/local-web-assets.js +39 -0
  119. package/lib/mcp-bridge.js +58 -0
  120. package/lib/mcp-check.js +26 -0
  121. package/lib/mcp-config.js +42 -0
  122. package/lib/mcp-delivery.js +46 -0
  123. package/lib/mcp-http.js +174 -0
  124. package/lib/mcp-server.js +69 -0
  125. package/lib/media-scheduler.js +194 -0
  126. package/lib/model-directory-service.js +37 -10
  127. package/lib/model-directory.js +3 -3
  128. package/lib/models.js +2 -2
  129. package/lib/music-transport.js +6 -3
  130. package/lib/network-config-service.js +123 -0
  131. package/lib/network-config.js +127 -0
  132. package/lib/network-doctor.js +94 -0
  133. package/lib/network-management.js +51 -0
  134. package/lib/network-policy.js +19 -0
  135. package/lib/network-route-evidence.js +38 -0
  136. package/lib/network-routing.js +283 -0
  137. package/lib/operation-runner.js +281 -0
  138. package/lib/operation-store.js +216 -0
  139. package/lib/panel-extension.js +36 -0
  140. package/lib/plugin-contract.js +194 -0
  141. package/lib/plugin-execution.js +51 -0
  142. package/lib/plugin-launch.js +86 -0
  143. package/lib/plugin-operation-store.js +101 -0
  144. package/lib/plugin-operations.js +28 -0
  145. package/lib/plugin-registry.js +109 -0
  146. package/lib/plugin-selection.js +15 -0
  147. package/lib/plugin-source-image.js +5 -0
  148. package/lib/plugin-web-assets.js +54 -0
  149. package/lib/plugins.d.ts +307 -0
  150. package/lib/plugins.js +59 -0
  151. package/lib/provider-adapter.js +7 -3
  152. package/lib/provider-adapters.js +92 -22
  153. package/lib/provider-catalog.js +64 -36
  154. package/lib/provider-config-format.js +51 -0
  155. package/lib/provider-config-service.js +143 -31
  156. package/lib/provider-context.d.ts +90 -0
  157. package/lib/provider-context.js +42 -0
  158. package/lib/provider-credentials.js +97 -0
  159. package/lib/provider-diagnostics.js +56 -0
  160. package/lib/provider-extensions.js +29 -0
  161. package/lib/provider-health.js +18 -6
  162. package/lib/provider-monitor.js +81 -0
  163. package/lib/provider-network.js +52 -0
  164. package/lib/provider-protocol.js +73 -22
  165. package/lib/provider-registry.js +128 -0
  166. package/lib/provider-response.js +38 -0
  167. package/lib/provider-settings.js +136 -0
  168. package/lib/provider-task-runner.js +132 -43
  169. package/lib/provider-transport.js +16 -0
  170. package/lib/render.js +11 -1
  171. package/lib/scheduling-policy.js +66 -0
  172. package/lib/skill-installation.js +23 -11
  173. package/lib/tasks.js +3 -2
  174. package/lib/video-flow-input.js +16 -0
  175. package/lib/video-input.js +12 -5
  176. package/lib/vision-model-routing.js +5 -0
  177. package/lib/web/app.js +703 -0
  178. package/lib/web/appearance.js +111 -0
  179. package/lib/web/comfyui.js +146 -0
  180. package/lib/web/connections.js +516 -0
  181. package/lib/web/creation.js +359 -0
  182. package/lib/web/flow-view.js +34 -0
  183. package/lib/web/gallery.js +133 -0
  184. package/lib/web/index.html +344 -0
  185. package/lib/web/media-client.js +218 -0
  186. package/lib/web/network.js +232 -0
  187. package/lib/web/plugin-panel.js +223 -0
  188. package/lib/web/plugin-status.js +140 -0
  189. package/lib/web/plugin-surface.css +17 -0
  190. package/lib/web/providers.js +513 -0
  191. package/lib/web/recovery.js +105 -0
  192. package/lib/web/startup.js +18 -0
  193. package/lib/web/styles.css +571 -0
  194. package/lib/web/transport.js +101 -0
  195. package/lib/web/workbench-login.js +165 -0
  196. package/lib/workbench-artifacts.js +3 -2
  197. package/lib/workbench-auth-cli.js +47 -0
  198. package/lib/workbench-auth.js +200 -0
  199. package/package.json +24 -6
  200. package/plugins/contact-sheet/index.d.mts +4 -0
  201. package/plugins/contact-sheet/index.mjs +33 -0
  202. package/plugins/contact-sheet/web/board.css +14 -0
  203. package/plugins/contact-sheet/web/board.mjs +93 -0
  204. package/skills/iris-core/SKILL.md +8 -4
  205. package/skills/iris-core/references/cli.md +2 -2
  206. package/skills/iris-core/references/comfyui.md +69 -0
  207. package/skills/iris-core/references/tasks.md +2 -0
  208. package/templates/plugin-starter/LICENSE +21 -0
  209. package/templates/plugin-starter/README.md +74 -0
  210. package/templates/plugin-starter/assets/peach.png +0 -0
  211. package/templates/plugin-starter/assets/violet.png +0 -0
  212. package/templates/plugin-starter/index.mjs +27 -0
  213. package/templates/plugin-starter/package.json +18 -0
  214. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  215. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  216. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  217. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  218. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  219. package/templates/plugin-starter/web/board.css +27 -0
  220. package/templates/plugin-starter/web/board.mjs +160 -0
  221. package/templates/plugin-starter/web/png.mjs +23 -0
  222. package/templates/provider-starter/LICENSE +21 -0
  223. package/templates/provider-starter/README.md +50 -0
  224. package/templates/provider-starter/index.d.mts +5 -0
  225. package/templates/provider-starter/index.mjs +165 -0
  226. package/templates/provider-starter/package.json +13 -0
  227. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  228. package/templates/provider-starter/scripts/environment.mjs +122 -0
  229. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  230. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  231. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  232. package/templates/provider-starter/scripts/setup.mjs +44 -0
  233. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  234. package/user_guide.md +5 -1
@@ -0,0 +1,223 @@
1
+ const $ = selector => document.querySelector(selector);
2
+ const node = (tag, text, className) => { const item = document.createElement(tag); if (text) item.textContent = text; if (className) item.className = className; return item; };
3
+ const freeze = value => { if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); } return value; };
4
+ const ARTIFACT = /^artifact_[a-f0-9]{24}$/, RUN = /^run_[a-f0-9]{24}$/;
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',
7
+ 'video/mp4': 'video', 'video/webm': 'video' };
8
+
9
+ /** Trusted same-origin modules. Shadow DOM isolates styles, not permissions or JavaScript. */
10
+ export function createPluginPanel({ navigate, describe, onProvidersSaved }) {
11
+ let client, session, manifest, page = '', artifacts = [], currentArtifact, active = new Map();
12
+ const themeListeners = new Set();
13
+ const themeValue = () => Object.freeze({ mode: document.documentElement.dataset.theme || 'light',
14
+ density: document.documentElement.dataset.density || 'comfortable', accent: 'iris' });
15
+ const themeObserver = new MutationObserver(() => {
16
+ for (const listener of themeListeners) listener(themeValue());
17
+ });
18
+ function report(item, stage, error, target = $('#plugin-feedback')) {
19
+ const message = item.id + ' · ' + stage + ' · ' + describe(error);
20
+ target.textContent = message; target.hidden = false; target.classList.add('error');
21
+ target.dataset.code = 'IRIS_PLUGIN_WEB_' + stage.toUpperCase();
22
+ }
23
+ function stop(slot) { const previous = active.get(slot); active.delete(slot); previous?.(); }
24
+ function stopAll() { for (const slot of [...active.keys()]) stop(slot); }
25
+ function mount(slot, item, extra = {}) {
26
+ stop(slot); slot.replaceChildren();
27
+ const lifetime = new AbortController(), signal = lifetime.signal, http = client;
28
+ const host = node('div', '', 'plugin-surface'); host.dataset.pluginContribution = item.id;
29
+ const status = node('p', '正在加载…', 'field-note'); status.setAttribute('role', 'status');
30
+ slot.append(host, status);
31
+ const shadow = host.attachShadow({ mode: 'open' }), container = node('div', '', 'plugin-content');
32
+ const unsubscriptions = new Set();
33
+ let cleanup, settled = false, released = false;
34
+ async function release() {
35
+ if (released || !settled) return; released = true;
36
+ try { await cleanup?.(); } catch (error) { report(item, 'cleanup', error); }
37
+ }
38
+ function close() {
39
+ signal.aborted || lifetime.abort();
40
+ for (const unsubscribe of unsubscriptions) unsubscribe();
41
+ host.remove(); status.remove(); void release();
42
+ }
43
+ active.set(slot, close);
44
+ function live() { signal.throwIfAborted(); if (http !== client) throw new DOMException('登录上下文已关闭', 'AbortError'); }
45
+ function options(value = {}) { live(); return { ...value, signal: AbortSignal.any([signal, ...(value.signal ? [value.signal] : [])]) }; }
46
+ const read = (path, value) => http.get(path, options(value));
47
+ const api = Object.freeze({
48
+ ...(session.pluginExecution ? { pluginOperations: Object.freeze({
49
+ catalog: (query = {}, value) => read('/api/v0/plugin-operations/catalog?' + new URLSearchParams(query), value),
50
+ history: value => read('/api/v0/plugin-operations', value),
51
+ execute: (request, value) => http.callPlugin(request, options(value)),
52
+ inspect: (id, value) => http.getOperation(id, options(value)),
53
+ lookup: (key, value) => http.lookupOperation(key, options(value)),
54
+ stop: (id, value) => http.stopOperation(id, options(value))
55
+ }) } : {}),
56
+ runs: (query = {}, value) => read('/api/v0/runs?' + new URLSearchParams(query), value),
57
+ run: (id, value) => { if (typeof id !== 'string' || !RUN.test(id)) throw new TypeError('流程 ID 无效'); return read('/api/v0/runs/' + id, value); },
58
+ models: value => read('/api/v0/models', value),
59
+ audioModels: value => read('/api/v0/audio-models', value),
60
+ videoModels: value => read('/api/v0/video-models', value),
61
+ artifact: (artifact, value) => {
62
+ if (typeof artifact?.id !== 'string' || !ARTIFACT.test(artifact.id) || !Object.hasOwn(MEDIA, artifact?.mediaType)) throw new TypeError('作品 ID 或媒体类型无效');
63
+ return read('/api/v0/artifacts/' + artifact.id + '/' + MEDIA[artifact.mediaType], { ...value, binary: true });
64
+ },
65
+ ...(session.providerManagement && item.dependency.kind === 'settings' ? { providers: Object.freeze({
66
+ read: value => read('/api/v0/provider-management/settings', value),
67
+ async save(input, value) {
68
+ const result = await http.postProvider('save', input, options(value));
69
+ live(); onProvidersSaved?.(result); return result;
70
+ }
71
+ }) } : {})
72
+ });
73
+ const theme = Object.freeze({ current: themeValue,
74
+ subscribe(listener) {
75
+ live();
76
+ if (typeof listener !== 'function') throw new TypeError('主题监听须为函数');
77
+ const subscription = value => { try { listener(value); } catch (error) { report(item, 'theme', error); } };
78
+ const off = () => { themeListeners.delete(subscription); unsubscriptions.delete(off); };
79
+ themeListeners.add(subscription); unsubscriptions.add(off); return off;
80
+ }
81
+ });
82
+ const context = Object.freeze({ container, signal, dependency: item.dependency, api, session, theme, ...extra });
83
+ let stage = 'styles';
84
+ void (async () => {
85
+ try {
86
+ await Promise.all(['/plugin-surface.css', ...item.styles].map(href => new Promise((resolve, reject) => {
87
+ const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href;
88
+ const abort = () => reject(signal.reason);
89
+ link.addEventListener('load', () => { signal.removeEventListener('abort', abort); resolve(); }, { once: true });
90
+ link.addEventListener('error', () => { signal.removeEventListener('abort', abort); reject(new Error('样式资源加载失败')); }, { once: true });
91
+ signal.addEventListener('abort', abort, { once: true }); shadow.append(link);
92
+ })));
93
+ live(); stage = 'module';
94
+ const module = await import(item.entry);
95
+ live(); if (typeof module.mount !== 'function') throw new Error('入口须导出 mount(context)');
96
+ shadow.append(container); stage = 'mount';
97
+ cleanup = await module.mount(context);
98
+ if (cleanup !== undefined && typeof cleanup !== 'function') { cleanup = undefined; throw new Error('mount 只能返回释放函数或 undefined'); }
99
+ if (!signal.aborted) status.hidden = true;
100
+ } catch (error) {
101
+ if (!signal.aborted) {
102
+ // A failed mount has no live API/listeners; keep its diagnostic outside its removed host.
103
+ lifetime.abort(); for (const unsubscribe of unsubscriptions) unsubscribe(); host.remove();
104
+ report(item, stage, error, status);
105
+ }
106
+ } finally { settled = true; if (signal.aborted) await release(); }
107
+ })();
108
+ }
109
+ function renderPage() {
110
+ const item = manifest?.pages.find(value => 'plugin:' + value.id === page);
111
+ $('#plugin-page-panel').hidden = !item;
112
+ $('#plugin-settings-panel').hidden = page !== 'plugin-settings';
113
+ $('#plugin-history-panel').hidden = page !== 'plugin-history';
114
+ for (const button of $('#plugin-navigation').querySelectorAll('button')) button.setAttribute('aria-current', button.dataset.page === page ? 'page' : 'false');
115
+ if (item) {
116
+ $('#plugin-page-title').textContent = item.title;
117
+ $('#plugin-page-identity').textContent = item.dependency.pluginId + ' · v' + item.dependency.pluginVersion;
118
+ mount($('#plugin-page-slot'), item);
119
+ }
120
+ if (page === 'plugin-settings') {
121
+ for (const details of $('#plugin-settings-list').children) if (details.open) mount(details.querySelector('.plugin-slot'), manifest.settings.find(value => value.id === details.dataset.contribution));
122
+ }
123
+ if (page === 'plugin-history') showHistory();
124
+ }
125
+ function showHistory() {
126
+ const slot = $('#plugin-history-list'); stop(slot); slot.replaceChildren();
127
+ const controller = new AbortController(), signal = controller.signal, http = client, urls = new Set();
128
+ active.set(slot, () => { controller.abort(); for (const url of urls) URL.revokeObjectURL(url); });
129
+ $('#plugin-history-feedback').textContent = '正在读取保存的业务记录…';
130
+ void http.get('/api/v0/plugin-operations', { signal }).then(value => {
131
+ if (signal.aborted || http !== client) return;
132
+ $('#plugin-history-feedback').textContent = value.operations.length ? '显示最近业务;缺少插件仍可读取原记录。' : '尚无保存的业务记录。';
133
+ for (const operation of value.operations) {
134
+ const article = node('article', '', 'card plugin-history-item'), title = node('h2', operation.operationId);
135
+ const identity = node('p', operation.dependency.pluginId + ' · v' + operation.dependency.pluginVersion, 'field-note');
136
+ const status = node('p', operation.state + ' · ' + operation.id, 'field-note'); article.dataset.operationId = operation.id;
137
+ article.append(title, identity, status);
138
+ for (const id of operation.result?.artifactIds || []) {
139
+ const preview = node('button', '查看原作品', 'secondary'); preview.type = 'button';
140
+ preview.addEventListener('click', async () => {
141
+ preview.disabled = true;
142
+ try {
143
+ const blob = await http.get('/api/v0/artifacts/' + id + '/content', { signal, binary: true });
144
+ if (signal.aborted || http !== client) return;
145
+ const extension = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' }[blob.type];
146
+ if (!extension) throw new Error('此业务作品不是支持的图片类型');
147
+ const url = URL.createObjectURL(blob); urls.add(url); const image = node('img'); image.src = url; image.alt = '此业务保存的原始作品';
148
+ const link = node('a', '下载原图'); link.href = url; link.download = 'iris-' + id + '.' + extension; article.append(image, link); preview.hidden = true;
149
+ } catch (error) { if (!signal.aborted) { status.textContent = describe(error); preview.disabled = false; } }
150
+ }); article.append(preview);
151
+ }
152
+ slot.append(article);
153
+ }
154
+ }).catch(error => { if (!signal.aborted) $('#plugin-history-feedback').textContent = describe(error); });
155
+ }
156
+ $('#plugin-history-refresh').addEventListener('click', () => { if (client && page === 'plugin-history') showHistory(); });
157
+ function renderPreview() {
158
+ stop($('#plugin-preview-slot')); $('#plugin-preview-slot').replaceChildren();
159
+ const id = $('#plugin-renderer').value, item = manifest?.renderers.find(value => value.id === id && value.mediaTypes.includes(currentArtifact?.mediaType));
160
+ if (page === 'studio' && $('#plugin-preview').open && item) mount($('#plugin-preview-slot'), item, { artifact: currentArtifact });
161
+ }
162
+ function selectArtifact() {
163
+ stop($('#plugin-preview-slot'));
164
+ currentArtifact = artifacts.find(value => value.id === $('#plugin-artifact').value);
165
+ $('#plugin-renderer').replaceChildren(new Option('默认预览', ''), ...(manifest?.renderers || [])
166
+ .filter(item => item.mediaTypes.includes(currentArtifact?.mediaType)).map(item => new Option(item.title, item.id)));
167
+ renderPreview();
168
+ }
169
+ $('#plugin-renderer').addEventListener('change', renderPreview);
170
+ $('#plugin-artifact').addEventListener('change', selectArtifact);
171
+ $('#plugin-preview').addEventListener('toggle', renderPreview);
172
+ return Object.freeze({
173
+ connect(http, facts) {
174
+ client = http;
175
+ session = freeze({ contractVersion: facts.contractVersion, instanceId: facts.instanceId, rootId: facts.rootId,
176
+ operations: [...facts.operations], readOnly: facts.readOnly, audio: facts.audio === true, video: facts.video === true, providerManagement: facts.providerManagement === true,
177
+ pluginExecution: facts.pluginExecution === true, pluginOperations: structuredClone(facts.pluginOperations || []) });
178
+ manifest = freeze(structuredClone(facts.panelContributions || { contractVersion: 0, pages: [], settings: [], renderers: [] }));
179
+ themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'data-density', 'data-accent'] });
180
+ const nav = $('#plugin-navigation'); nav.replaceChildren(); nav.hidden = !manifest.pages.length && !manifest.settings.length && !session.pluginExecution;
181
+ for (const item of [...manifest.pages, ...(manifest.settings.length ? [{ id: '', title: '扩展设置' }] : []), ...(session.pluginExecution ? [{ id: 'history', title: '业务记录' }] : [])]) {
182
+ const button = node('button', item.title, 'nav-item'); button.type = 'button';
183
+ button.dataset.page = item.id === 'history' ? 'plugin-history' : item.id ? 'plugin:' + item.id : 'plugin-settings';
184
+ button.addEventListener('click', () => navigate(button.dataset.page)); nav.append(button);
185
+ }
186
+ $('#plugin-settings-list').replaceChildren(...manifest.settings.map(item => {
187
+ const details = node('details', '', 'card plugin-settings-card'); details.dataset.contribution = item.id;
188
+ const summary = node('summary'), label = node('span', item.title), disclosure = node('span', '展开', 'disclosure');
189
+ summary.append(label, disclosure);
190
+ const identity = node('p', item.dependency.pluginId + ' · v' + item.dependency.pluginVersion, 'field-note');
191
+ const slot = node('div', '', 'plugin-slot'); details.append(summary, identity, slot);
192
+ details.addEventListener('toggle', () => {
193
+ disclosure.textContent = details.open ? '收起' : '展开';
194
+ if (details.isConnected && details.open && page === 'plugin-settings' && client && manifest.settings.includes(item)) mount(slot, item); else stop(slot);
195
+ }); return details;
196
+ }));
197
+ },
198
+ show(next) {
199
+ if (page === next) return;
200
+ $('#plugin-feedback').hidden = true; stopAll(); page = next;
201
+ if (client) { renderPage(); renderPreview(); }
202
+ else { $('#plugin-page-panel').hidden = true; $('#plugin-settings-panel').hidden = true; $('#plugin-history-panel').hidden = true; }
203
+ },
204
+ previewArtifacts(values, preferredId) {
205
+ artifacts = values.filter(value => value && manifest?.renderers.some(item => item.mediaTypes.includes(value.mediaType))).map(value => freeze({ ...value }));
206
+ const selected = artifacts.find(value => value.id === preferredId) || artifacts[0];
207
+ $('#plugin-preview').hidden = !artifacts.length; $('#plugin-preview').open = false;
208
+ $('#plugin-artifact').replaceChildren(...artifacts.map(value => new Option(value.label, value.id)));
209
+ if (selected) $('#plugin-artifact').value = selected.id;
210
+ selectArtifact();
211
+ },
212
+ clearPreview() { stop($('#plugin-preview-slot')); artifacts = []; currentArtifact = null; $('#plugin-preview').hidden = true; $('#plugin-preview').open = false; $('#plugin-preview-slot').replaceChildren(); },
213
+ disconnect() {
214
+ $('#plugin-feedback').hidden = true;
215
+ stopAll(); themeObserver.disconnect(); themeListeners.clear(); client = session = manifest = null; page = '';
216
+ $('#plugin-navigation').replaceChildren(); $('#plugin-navigation').hidden = true;
217
+ $('#plugin-page-panel').hidden = $('#plugin-settings-panel').hidden = $('#plugin-history-panel').hidden = true;
218
+ $('#plugin-history-list').replaceChildren();
219
+ $('#plugin-page-slot').replaceChildren(); $('#plugin-settings-list').replaceChildren();
220
+ artifacts = []; currentArtifact = null; $('#plugin-preview').hidden = true; $('#plugin-preview').open = false;
221
+ }
222
+ });
223
+ }
@@ -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
+ }
@@ -0,0 +1,17 @@
1
+ :host { display:block; color:var(--ink); font:inherit; color-scheme:inherit; }
2
+ * { box-sizing:border-box; }
3
+ [hidden] { display:none!important; }
4
+ .plugin-content { min-width:0; overflow-wrap:anywhere; }
5
+ h1,h2,h3,p,figure { margin:0 0 12px; }
6
+ button,input,select,textarea { font:inherit; color:inherit; max-width:100%; }
7
+ button,input,select,textarea { border:1px solid var(--line); border-radius:8px; padding:8px 12px; background:var(--panel); min-height:40px; }
8
+ button { cursor:pointer; background:var(--accent); color:var(--on-accent); }
9
+ button:disabled { opacity:.5; cursor:default; }
10
+ button.secondary { background:var(--panel); color:var(--ink); }
11
+ label { display:grid; gap:6px; margin-bottom:12px; }
12
+ a { color:var(--accent); }
13
+ :focus-visible { outline:2px solid var(--purple); outline-offset:3px; }
14
+ .muted,.field-note { color:var(--muted); font-size:12px; }
15
+ .card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:16px; }
16
+ .error { color:var(--error); }
17
+ img,video,audio { max-width:100%; }