@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,516 @@
1
+ // Explicit owner management only. No discovery, credential storage or automatic media call.
2
+ const BASE = '/api/v0/connection-management';
3
+ const $ = selector => document.querySelector(selector);
4
+ const states = { pending: '尚未使用', used: '使用过', revoked: '已撤销', configured: '静态配置' };
5
+ const element = (tag, text, className) => { const node = document.createElement(tag); node.textContent = text; if (className) node.className = className; return node; };
6
+ const checked = selector => [...document.querySelectorAll(selector + ':checked')].map(input => input.value);
7
+ const date = value => new Date(value).toLocaleString('zh-CN');
8
+
9
+ function selectedToolNames(record, catalog) {
10
+ const controls = catalog.controlTools || catalog.tools.filter(tool => !catalog.groups.some(group => group.tools.includes(tool.name))).map(tool => tool.name);
11
+ return new Set([...controls, ...catalog.groups.filter(group => record.capabilities.includes(group.id)).flatMap(group => group.tools),
12
+ ...(record.pluginOperations?.length ? catalog.pluginControlTools || [] : [])]);
13
+ }
14
+ function effectiveTools(record, catalog) {
15
+ const selected = selectedToolNames(record, catalog);
16
+ const definitions = new Map([...catalog.tools, ...(catalog.pluginTools || [])].map(tool => [tool.name, tool]));
17
+ return record.tools.filter(name => selected.has(name) && catalog.availableTools.includes(name)
18
+ && definitions.has(name) && record.scopes.includes(definitions.get(name).scope) && catalog.availableScopes.includes(definitions.get(name).scope));
19
+ }
20
+ const samePin = (a, b) => ['contractVersion', 'pluginId', 'pluginVersion', 'kind', 'contributionId', 'contributionVersion'].every(key => a[key] === b[key]);
21
+
22
+ /** Owner GET facts only. This is a configuration check, never a transport probe or authority. */
23
+ export function inspectMcpSetup(record, catalog, { transport = 'stdio', location = 'same' } = {}) {
24
+ if (catalog.contractVersion !== 0) throw new TypeError('不支持的连接管理版本');
25
+ const issues = [], issue = (code, message) => issues.push({ code, message });
26
+ const revoked = Boolean(record.revokedAt || record.state === 'revoked');
27
+ if (location !== 'same') issue('location', '当前导出使用 Core 主机的回环地址与本机路径,不能直接交给另一台机器上的客户端。');
28
+ if (transport === 'http' && !catalog.httpEnabled) issue('transport', '此服务没有开启 HTTP MCP;可选本机 stdio,或在下次明确启动服务时开启 HTTP。');
29
+ if (revoked) issue('revoked', '此连接已撤销,旧配置不能继续使用。需要接入时请新建连接。');
30
+ if (record.scopes.some(scope => !catalog.availableScopes.includes(scope))) issue('scope', '保存的操作权限超出此服务当前开放的范围,请核对服务开关与连接设置。');
31
+ if (record.capabilities.some(id => !catalog.groups.some(group => group.id === id))) issue('capability', '此工作区没有开放全部已选媒体分组,原授权未被自动替换。');
32
+ for (const pin of record.pluginOperations || []) {
33
+ const entry = catalog.contributions.operations.find(item => item.id === pin.contributionId);
34
+ if (!entry) issue('plugin', pin.contributionId + ':业务未加载,历史仍保留,请核对准确插件版本。');
35
+ else if (!samePin(entry.dependency, pin)) issue('plugin', pin.contributionId + ':当前业务与保存的准确依赖版本不一致。');
36
+ else if (entry.access.mcp !== 'requires-selection') issue('plugin', pin.contributionId + ':此服务尚未启用该业务;历史读取可能仍可用。');
37
+ }
38
+ const tools = revoked ? [] : effectiveTools(record, catalog);
39
+ if (!revoked && !tools.length) issue('tools', '当前选择没有可用工具,请明确选择功能、工具与对应权限;不会自动补齐。');
40
+ return { ok: issues.length === 0, issues, tools, mediaTested: false, transportTested: false };
41
+ }
42
+
43
+ /** A raw bridge/self-check handshake is not an MCP client's declaration. No online inference. */
44
+ export function mcpClientEvidence(record, catalog) {
45
+ if (record.revokedAt || record.state === 'revoked') return { state: 'revoked', label: '连接已撤销', instances: [] };
46
+ const instances = (record.instances || []).filter(instance => instance.declaredClient?.source === 'client-declared').map(instance => ({
47
+ state: instance.state, at: instance.declaredClient.at, clientInfo: instance.declaredClient.clientInfo,
48
+ transport: instance.declaredClient.transport, tools: effectiveTools(instance, catalog) }));
49
+ if (instances.some(instance => instance.state === 'connected')) return { state: 'observed', label: '收到客户端握手与声明', instances };
50
+ if (instances.length) return { state: 'disconnected', label: '客户端握手已断开', instances };
51
+ if (record.lastContact) return { state: 'historical', label: '有历史客户端声明', instances, contact: record.lastContact };
52
+ if (record.instances?.some(instance => instance.state === 'connected')) return { state: 'bridge', label: '仅收到桥接握手', instances };
53
+ return { state: 'waiting', label: '等待客户端握手与声明', instances };
54
+ }
55
+
56
+ export function createConnectionPanel({ describe }) {
57
+ let client, epoch = 0, catalog, records = [], selected = null, key, busy = false, visible = false, viewTicket = 0;
58
+ let scopes = {};
59
+ let capabilities = [], tools = [], pluginOperations = [], allowedScopes = ['read'], templates = [], skillBundle = null;
60
+ let view = 'settings', exportDelivery = null;
61
+ let formDirty = false;
62
+ const exports = new Map(), inspections = new Map();
63
+ let checkController = null;
64
+ const closeButton = element('button', '返回列表', 'quiet'); closeButton.type = 'button'; closeButton.id = 'connection-cancel';
65
+ const closeArea = element('div', '', 'provider-footer'); closeArea.append(closeButton); $('#connection-editor').append(closeArea);
66
+ const urls = new Set();
67
+ const current = () => records.find(record => record.connectionId === selected);
68
+ const valid = (at, connection) => at === epoch && client === connection;
69
+ function editor(open) { $('#connection-editor').hidden = !open; $('#connections-overview').hidden = open; }
70
+ function setView(next) {
71
+ if (next !== view) cancelCheck();
72
+ if (next !== view) clearExport(); view = next;
73
+ for (const button of document.querySelectorAll('[data-connection-view]')) {
74
+ const active = button.dataset.connectionView === view; button.setAttribute('aria-pressed', String(active));
75
+ $('#connection-view-' + button.dataset.connectionView).hidden = !active;
76
+ }
77
+ renderOnboarding(); controls();
78
+ }
79
+ function feedback(text = '', error = false) {
80
+ const node = $('#connections-feedback'); node.hidden = !text; node.textContent = text; node.classList.toggle('error', error);
81
+ }
82
+ function clearExport() {
83
+ viewTicket++;
84
+ exportDelivery = null;
85
+ $('#connection-configuration').value = ''; $('#connection-export-result').hidden = true;
86
+ $('#connection-export-note').textContent = '';
87
+ skillBundle = null; $('#connection-skill-preview').value = ''; $('#connection-skill-result').hidden = true;
88
+ for (const url of urls) URL.revokeObjectURL(url); urls.clear();
89
+ renderOnboarding();
90
+ }
91
+ function controls() {
92
+ const record = current(), editable = !record || record.managed && !record.revokedAt;
93
+ $('#connection-fields').disabled = busy || !catalog || !editable;
94
+ for (const id of ['connection-new', 'connection-export-button', 'connection-revoke', 'connection-revoke-yes', 'connection-revoke-no', 'connection-delete', 'connection-delete-yes', 'connection-delete-no',
95
+ 'connection-template', 'connection-template-import', 'connection-template-example', 'connection-template-file', 'connection-skill-generate']) $('#' + id).disabled = busy || !catalog;
96
+ for (const id of ['connection-transport', 'connection-format']) $('#' + id).disabled = busy || !catalog || Boolean($('#connection-template').value);
97
+ $('#connections-refresh').disabled = busy;
98
+ $('#connection-back').disabled = busy;
99
+ closeButton.disabled = busy;
100
+ for (const node of document.querySelectorAll('#connections-list .connection-item')) node.disabled = busy;
101
+ for (const button of document.querySelectorAll('[data-connection-view]')) button.disabled = busy || button.dataset.connectionView === 'delivery' && (!record?.managed || Boolean(record.revokedAt)) || ['details', 'verification'].includes(button.dataset.connectionView) && !record;
102
+ $('#connection-export-button').disabled ||= formDirty || $('#connection-location').value !== 'same';
103
+ $('#connection-check').disabled = busy || !catalog || !record || formDirty;
104
+ $('#connection-location').disabled = busy;
105
+ $('#connection-verify-next').disabled = busy || !record;
106
+ }
107
+ function renderList() {
108
+ $('#connections-count').textContent = String(records.filter(record => record.state !== 'revoked').length);
109
+ $('#connections-empty').hidden = records.length !== 0;
110
+ $('#connections-list').replaceChildren(...records.map(record => {
111
+ const button = element('button', '', 'connection-item service-card'); button.type = 'button'; button.dataset.connectionId = record.connectionId;
112
+ button.setAttribute('aria-current', String(record.connectionId === selected));
113
+ const heading = element('span', '', 'service-card-heading'); heading.append(element('span', 'MCP', 'service-mark'), element('small', states[record.state] || record.state, 'service-status'));
114
+ const media = record.capabilities.map(id => { const group = catalog.groups.find(group => group.id === id); return group?.shortTitle || group?.title || id; });
115
+ for (const pin of record.pluginOperations || []) media.push(catalog.contributions?.operations?.find(item => item.id === pin.contributionId)?.title || pin.contributionId);
116
+ button.append(heading, element('strong', record.label), element('span', media.join(' · ') || '仅控制工具', 'service-endpoint'),
117
+ element('span', record.lastContact ? '最近使用 · ' + date(record.lastContact.at) : '等待客户端首次使用', 'service-meta'), element('span', record.revokedAt ? '查看与管理 →' : '接入与管理 →', 'service-action'));
118
+ button.addEventListener('click', () => { if (!busy) { select(record.connectionId); editor(true); setView(record.managed && !record.revokedAt ? 'delivery' : 'details'); } }); return button;
119
+ }));
120
+ controls();
121
+ }
122
+ function option(value, title, selectedValues, change, { disabled = false, note = '' } = {}) {
123
+ const label = element('label', '', 'check-option'), input = document.createElement('input');
124
+ input.type = 'checkbox'; input.value = value; input.checked = selectedValues.includes(value); input.disabled = disabled;
125
+ input.addEventListener('change', change); const text = element('span', title);
126
+ if (note) text.append(element('small', note)); label.append(input, text); return label;
127
+ }
128
+ function enabledTools() {
129
+ return new Set([...selectedToolNames({ capabilities, pluginOperations }, catalog)].filter(name => !catalog.availableTools || catalog.availableTools.includes(name)));
130
+ }
131
+ function renderTools() {
132
+ const enabled = enabledTools();
133
+ $('#connection-tools').replaceChildren(...catalog.tools.filter(tool => enabled.has(tool.name)).map(tool =>
134
+ option(tool.name, tool.name.replace('iris_core_', ''), tools, () => {
135
+ tools = checked('#connection-tools input'); countTools(); clearExport();
136
+ }, { disabled: !allowedScopes.includes(tool.scope) || !catalog.availableScopes.includes(tool.scope), note: scopes[tool.scope] })));
137
+ countTools();
138
+ }
139
+ function countTools() {
140
+ const count = catalog.tools.filter(tool => tools.includes(tool.name) && allowedScopes.includes(tool.scope) && catalog.availableScopes.includes(tool.scope)
141
+ && enabledTools().has(tool.name)).length;
142
+ $('#connection-tool-count').textContent = count + ' 个可用';
143
+ }
144
+ function renderOptions() {
145
+ const mediaOptions = catalog.media.map(media => option(media.id, media.title,
146
+ media.capabilities.some(id => capabilities.includes(id)) ? [media.id] : [], () => {
147
+ const enabled = checked('#connection-media input');
148
+ capabilities = catalog.media.filter(item => enabled.includes(item.id)).flatMap(item => {
149
+ const selected = item.capabilities.filter(id => capabilities.includes(id));
150
+ return selected.length ? selected : item.capabilities.slice(0, 1);
151
+ });
152
+ tools = [...enabledTools()];
153
+ renderOptions(); clearExport();
154
+ }, { disabled: !media.capabilities.length, note: !media.capabilities.length ? '未接入' : media.note || '' }));
155
+ $('#connection-media').replaceChildren(...mediaOptions.filter((_, index) => catalog.media[index].capabilities.length));
156
+ $('#connection-media-unavailable').replaceChildren(...mediaOptions.filter((_, index) => !catalog.media[index].capabilities.length));
157
+ const image = catalog.media.find(item => item.id === 'image');
158
+ $('#connection-image-options').replaceChildren(...(image?.capabilities || []).map(id => option(id,
159
+ catalog.groups.find(group => group.id === id).label || catalog.groups.find(group => group.id === id).title, capabilities, () => {
160
+ capabilities = [...capabilities.filter(id => !image.capabilities.includes(id)), ...checked('#connection-image-options input')];
161
+ tools = [...enabledTools()];
162
+ renderOptions(); clearExport();
163
+ })));
164
+ const entries = (catalog.contributions?.operations || []).filter(item => item.access.mcp === 'requires-selection');
165
+ const choices = entries.map(item => ({ title: item.title, dependency: item.dependency, available: true, imageInput: !!item.artifactInputs?.length }));
166
+ for (const pin of pluginOperations) if (!choices.some(item => JSON.stringify(item.dependency) === JSON.stringify(pin))) choices.push({ title: pin.contributionId, dependency: pin, available: false });
167
+ $('#connection-plugin-section').hidden = !choices.length;
168
+ $('#connection-plugin-options').replaceChildren(...choices.map(item => option(JSON.stringify(item.dependency), item.title,
169
+ pluginOperations.map(pin => JSON.stringify(pin)), () => {
170
+ pluginOperations = checked('#connection-plugin-options input').map(value => JSON.parse(value));
171
+ const enabled = enabledTools();
172
+ tools = [...new Set([...tools, ...(pluginOperations.length ? catalog.pluginControlTools || [] : [])])].filter(name => enabled.has(name));
173
+ renderOptions(); clearExport();
174
+ }, { note: item.dependency.pluginVersion + ' · ' + (item.available ? '需允许执行;只开放此准确业务版本' : '当前不可用;保留选择可读取原记录')
175
+ + (item.imageInput ? ' · 图片输入共享本空间内 ≤8 MiB PNG/JPEG/WebP 素材' : '') })));
176
+ $('#connection-scopes').replaceChildren(...Object.entries(scopes).map(([id, title]) => option(id, title, allowedScopes, () => {
177
+ allowedScopes = checked('#connection-scopes input'); renderOptions(); clearExport();
178
+ }, { disabled: id === 'read' || !catalog.availableScopes.includes(id) && !allowedScopes.includes(id), note: catalog.availableScopes.includes(id) ? '' : '服务未开启' })));
179
+ renderTools();
180
+ }
181
+ function fact(label, value) { $('#connection-facts').append(element('dt', label), element('dd', value)); }
182
+ function select(id = null) {
183
+ selected = id; key = crypto.randomUUID(); clearExport(); feedback();
184
+ const record = current();
185
+ capabilities = [...(record?.capabilities || catalog.defaults?.capabilities || catalog.groups.slice(0, 1).map(group => group.id))];
186
+ pluginOperations = structuredClone(record?.pluginOperations || []);
187
+ const enabled = enabledTools();
188
+ tools = [...(record?.tools || catalog.tools.filter(tool => enabled.has(tool.name)).map(tool => tool.name))];
189
+ allowedScopes = [...(record?.scopes || ['read'])];
190
+ $('#connection-name').value = record?.label || '';
191
+ $('#connection-editor-title').textContent = record ? '连接设置' : '新建连接';
192
+ $('#connection-state').textContent = record ? states[record.state] : '尚未保存';
193
+ $('#connection-save').textContent = record ? '保存更改' : '创建连接';
194
+ $('#connection-save-note').textContent = record?.revokedAt ? '连接已撤销。可删除记录,需要使用时请新建连接。'
195
+ : record && !record.managed ? '静态授权由启动文件管理。' : '仅更新授权配置,不调用模型。';
196
+ $('#connection-facts').replaceChildren(); $('#connection-facts').hidden = !record;
197
+ if (record) {
198
+ const instances = [...record.instances].sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
199
+ const contact = record.lastContact || instances.find(instance => instance.declaredClient)?.declaredClient;
200
+ fact('连接身份', record.connectionId);
201
+ fact('客户端声明', contact ? contact.clientInfo.name + ' · ' + contact.clientInfo.version : '尚未收到');
202
+ if (contact) fact('最近声明', date(contact.at) + ' · ' + contact.transport);
203
+ fact('桥接', record.adapterId || 'iris.mcp');
204
+ if (instances.length) fact('桥版本', instances[0].harnessVersion);
205
+ fact('实例记录', record.instances.length ? record.instances.length + ' 个握手实例(不表示持续在线)' : '当前无握手实例');
206
+ }
207
+ $('#connection-export').hidden = !record?.managed || Boolean(record.revokedAt);
208
+ $('#connection-revoke-section').hidden = !record?.managed;
209
+ $('#connection-revoke-confirm').hidden = true; $('#connection-revoke').hidden = Boolean(record?.revokedAt);
210
+ $('#connection-delete-confirm').hidden = true; $('#connection-delete').hidden = false;
211
+ $('#connection-tool-details').open = false;
212
+ $('#connection-skill-details').open = false;
213
+ $('#connection-transport option[value="http"]').disabled = !catalog.httpEnabled;
214
+ if (!record) setView('settings'); else if (view === 'delivery' && (!record.managed || record.revokedAt)) setView('details');
215
+ transportNote();
216
+ renderOptions(); renderList();
217
+ formDirty = false; $('#connection-leave-confirm').hidden = true; closeButton.textContent = '返回列表';
218
+ renderOnboarding(); controls();
219
+ }
220
+ async function reload(connection) {
221
+ const result = await connection.get(BASE + '/connections');
222
+ if (result.contractVersion !== 0) throw new TypeError('不支持的连接列表版本');
223
+ return result.connections;
224
+ }
225
+ function renderContributions() {
226
+ const section = $('#connection-contributions'), list = $('#connection-contribution-list');
227
+ list.replaceChildren(); section.hidden = !catalog?.plugins?.length;
228
+ if (section.hidden) return;
229
+ const labels = { providers: '模型协议', operations: '业务操作', pages: '页面', renderers: '作品渲染器', settings: '设置表单' };
230
+ const status = { 'requires-grant': '需显式本地授权', 'requires-configuration': '需配置模型服务',
231
+ 'descriptor-only': '仅声明', unavailable: '已不可用', 'not-exposed': 'MCP 尚未接入',
232
+ 'durable-operation-required': 'MCP 待接入持久执行', 'not-mounted': '页面尚未挂载', 'available-in-panel': '已选入工作台' };
233
+ Object.assign(status, { 'via-operation': 'Core 持久执行', 'requires-selection': '用户选择后可经 MCP 调用' });
234
+ for (const [kind, entries] of Object.entries(catalog.contributions)) for (const entry of entries) {
235
+ const item = element('li'), remote = kind === 'operations' ? entry.access.mcp : ['pages', 'renderers', 'settings'].includes(kind) ? entry.access.web : null;
236
+ item.append(element('strong', entry.title), document.createTextNode(' · ' + labels[kind] + ' · ' + (status[entry.access.local] || entry.access.local)
237
+ + (remote ? ' · ' + (status[remote] || remote) : '')),
238
+ element('p', entry.dependency.pluginId + '@' + entry.dependency.pluginVersion + ' · ' + entry.id + '@' + entry.version, 'field-note'));
239
+ list.append(item);
240
+ }
241
+ }
242
+ function setCatalog(value) {
243
+ catalog = { ...value, tools: [...value.tools, ...(value.pluginTools || [])] };
244
+ // A running preview may still serve the earlier v0 DTO. Derive safe choices from its declarations.
245
+ scopes = Object.fromEntries((catalog.scopeOptions || catalog.availableScopes.map(id => ({ id, title: id }))).map(item => [item.id, item.title]));
246
+ renderContributions();
247
+ }
248
+ function transportNote() {
249
+ $('#connection-transport-note').textContent = $('#connection-transport').value === 'http'
250
+ ? 'HTTP:客户端连接此 Iris 服务的 /mcp 地址,使用导出配置中的专属密钥。Iris 服务需保持运行。'
251
+ : 'stdio:客户端按 command / args 启动本机 MCP 桥接进程。保留本机路径,Iris 服务需保持运行。';
252
+ }
253
+ function templateChoice() {
254
+ const template = templates.find(item => item.choiceId === $('#connection-template').value), note = $('#connection-template-note');
255
+ note.replaceChildren(); note.hidden = !template;
256
+ if (template) {
257
+ $('#connection-transport').value = template.transport; $('#connection-format').value = template.format;
258
+ note.append(document.createTextNode((template.hint || '模板只应用传输和格式。') + ' 不增加授权。'));
259
+ if (template.documentationUrl) {
260
+ const link = element('a', '查看客户端文档'); link.href = template.documentationUrl; link.target = '_blank'; link.rel = 'noopener noreferrer'; note.append(link);
261
+ }
262
+ }
263
+ clearExport(); transportNote(); controls();
264
+ }
265
+ function renderTemplates(selectedId = $('#connection-template').value) {
266
+ const manual = element('option', '手动选择'); manual.value = '';
267
+ $('#connection-template').replaceChildren(manual, ...templates.map(template => {
268
+ const node = element('option', template.title); node.value = template.choiceId; node.disabled = template.transport === 'http' && !catalog.httpEnabled; return node;
269
+ }));
270
+ $('#connection-template').value = templates.some(template => template.choiceId === selectedId && (template.transport !== 'http' || catalog.httpEnabled)) ? selectedId : '';
271
+ templateChoice();
272
+ }
273
+ function setTemplates(value) {
274
+ templates = [...(value.presets || []).map(item => ({ ...item, choiceId: 'preset:' + item.presetId })), ...value.templates.map(item => ({ ...item, choiceId: item.templateId }))];
275
+ const formats = value.formats || ['mcpServers', 'entry'];
276
+ for (const option of $('#connection-format').options) option.disabled = !formats.includes(option.value);
277
+ }
278
+ const choiceKey = () => [$('#connection-location').value, $('#connection-transport').value, $('#connection-format').value].join(':');
279
+ function renderOnboarding() {
280
+ const record = current();
281
+ const scopeTitles = allowedScopes.map(id => scopes[id] || id);
282
+ const names = (catalog?.groups || []).filter(group => capabilities.includes(group.id)).map(group => group.label || group.title);
283
+ for (const pin of pluginOperations) names.push(catalog?.contributions?.operations?.find(item => samePin(item.dependency, pin))?.title || pin.contributionId);
284
+ $('#connection-grant-summary').textContent = (names.join('、') || '仅控制工具') + ' · ' + scopeTitles.join('、');
285
+ $('#connection-unsaved-note').hidden = !formDirty;
286
+ $('#connection-location-note').textContent = $('#connection-location').value === 'same'
287
+ ? '这里指运行 Agent 的机器,不是打开此网页的手机或电脑。两种传输都需要 Core 保持运行。'
288
+ : '当前配置含 Core 主机的回环地址或本机路径,不能直接交给另一台机器上的客户端。请先确认部署方案;本页不会开启公网访问。';
289
+ const exported = record && exports.get(record.connectionId), inspected = record && inspections.get(record.connectionId);
290
+ const exportCurrent = exported && exported.revision === record.revision && exported.choice === choiceKey() && !formDirty;
291
+ const inspectionCurrent = inspected && inspected.revision === record.revision && inspected.choice === choiceKey() && !formDirty;
292
+ $('#connection-export-state').textContent = exportCurrent ? '当前配置已生成' : '本页未记录当前配置导出';
293
+ $('#connection-export-evidence').textContent = exportCurrent ? date(exported.at) + ' · ' + exported.transport + ' · ' + exported.format + ';仍需导入客户端。'
294
+ : '只记录本次登录中生成的配置。若你已在客户端配置过,可直接核对接入。';
295
+ $('#connection-check-state').textContent = inspectionCurrent ? inspected.ok ? 'Iris 端配置检查通过' : '有待核对项' : '尚未检查当前配置';
296
+ $('#connection-check-state').dataset.state = inspectionCurrent ? inspected.ok ? 'passed' : 'limited' : 'pending';
297
+ $('#connection-check-evidence').textContent = inspectionCurrent ? date(inspected.at) + ' · 只读取此服务与保存授权,未检查客户端传输或媒体。'
298
+ : '检查不会建立 MCP 连接,不会代替客户端握手或调用模型。';
299
+ $('#connection-check-issues').replaceChildren(...(inspectionCurrent ? inspected.issues : []).map(item => { const row = element('li', item.message); row.dataset.check = item.code; return row; }));
300
+ $('#connection-check-tools').replaceChildren(...(inspectionCurrent ? inspected.tools : []).map(name => element('li', name)));
301
+ $('#connection-check-tools-note').textContent = inspectionCurrent ? inspected.tools.length + ' 个工具按当前授权允许使用;需在客户端确认是否已发现。' : '完成只读检查后显示 Core 当前允许的工具。';
302
+ const evidence = record && catalog ? mcpClientEvidence(record, catalog) : null;
303
+ $('#connection-client-state').textContent = evidence?.label || '等待创建连接';
304
+ $('#connection-client-state').dataset.state = evidence?.state || 'waiting';
305
+ $('#connection-client-evidence').replaceChildren();
306
+ for (const instance of evidence?.instances || []) {
307
+ const item = element('li', instance.clientInfo.name + ' · ' + instance.clientInfo.version + ' · ' + instance.transport + ' · '
308
+ + (instance.state === 'connected' ? '握手记录' : '已断开') + ' · ' + date(instance.at));
309
+ const details = element('details', '', 'tool-details'), summary = element('summary', ''), disclosure = element('span', '展开', 'disclosure');
310
+ summary.append(element('span', '本次协商允许 ' + instance.tools.length + ' 个工具'), disclosure);
311
+ const names = element('ul', '', 'connection-check-tools'); names.append(...instance.tools.map(name => element('li', name)));
312
+ details.append(summary, names); details.addEventListener('toggle', () => { disclosure.textContent = details.open ? '收起' : '展开'; }); item.append(details);
313
+ $('#connection-client-evidence').append(item);
314
+ }
315
+ if (evidence?.contact) $('#connection-client-evidence').append(element('li', evidence.contact.clientInfo.name + ' · ' + date(evidence.contact.at) + ' · 仅为历史声明'));
316
+ }
317
+ function cancelCheck() {
318
+ if (!checkController) return;
319
+ checkController.abort(); checkController = null; busy = false;
320
+ feedback('只读检查已取消,未建立客户端连接。');
321
+ }
322
+ async function checkSetup() {
323
+ const original = current();
324
+ if (busy || !client || !original || formDirty || !visible) return;
325
+ const at = epoch, connection = client, id = selected, ticket = viewTicket, choice = choiceKey();
326
+ const controller = new AbortController(); checkController = controller; inspections.delete(id); renderOnboarding(); busy = true; controls(); feedback('正在只读核对 Iris 配置和客户端记录…');
327
+ const currentRequest = () => valid(at, connection) && checkController === controller && visible && selected === id && viewTicket === ticket;
328
+ try {
329
+ const options = { signal: controller.signal };
330
+ const [available, saved, session] = await Promise.all([connection.get(BASE + '/catalog', options), connection.get(BASE + '/connections', options), connection.get('/api/v0/session', options)]);
331
+ if (!currentRequest()) return;
332
+ if ([available, saved, session].some(value => value.contractVersion !== 0)) throw new TypeError('不支持的连接检查版本');
333
+ records = saved.connections; setCatalog(available);
334
+ const record = current();
335
+ if (!record || record.revision !== original.revision) {
336
+ inspections.delete(id); exports.delete(id); select(record?.connectionId || null);
337
+ feedback('连接授权已变化,请核对当前设置后重新检查。', true); return;
338
+ }
339
+ const result = inspectMcpSetup(record, catalog, { transport: $('#connection-transport').value, location: $('#connection-location').value });
340
+ inspections.set(id, { ...result, at: new Date().toISOString(), revision: record.revision, choice });
341
+ // No draft can be active here. Refresh the badge/facts from the same saved record as the evidence.
342
+ select(id);
343
+ feedback(result.ok ? mcpClientEvidence(record, catalog).state === 'observed'
344
+ ? '已读取当前配置与客户端声明;握手不代表持续在线或媒体已实测。'
345
+ : 'Iris 端配置检查通过。请在客户端导入配置,再核对实际握手;媒体尚未实测。'
346
+ : '发现需核对的配置项,读取或历史功能可能仍可用。', !result.ok);
347
+ } catch (error) {
348
+ if (currentRequest()) { inspections.delete(id); renderOnboarding(); feedback('只读检查未完成:' + describe(error), true); }
349
+ } finally { if (checkController === controller) { checkController = null; busy = false; controls(); } }
350
+ }
351
+ function download(value, filename, mediaType = 'application/json') {
352
+ const url = URL.createObjectURL(new Blob([value], { type: mediaType })); urls.add(url);
353
+ const link = document.createElement('a'); link.href = url; link.download = filename; link.click();
354
+ setTimeout(() => { URL.revokeObjectURL(url); urls.delete(url); }, 1000);
355
+ }
356
+ async function perform(action) {
357
+ if (busy || !client) return;
358
+ const at = epoch, connection = client; busy = true; controls(); feedback('正在处理…');
359
+ try { await action({ at, connection, valid: () => valid(at, connection) }); }
360
+ catch (error) {
361
+ if (valid(at, connection)) feedback((!error.status || error.status >= 500 ? '尚未确认操作结果,请刷新连接记录核对。' : '')
362
+ + (error.name === 'TimeoutError' ? '请求等待超时。' : describe(error)), true);
363
+ }
364
+ finally { if (valid(at, connection)) { busy = false; controls(); } }
365
+ }
366
+ $('#connection-form').addEventListener('submit', event => {
367
+ event.preventDefault(); const record = current();
368
+ if (record && (!record.managed || record.revokedAt)) return;
369
+ perform(async ctx => {
370
+ const value = { label: $('#connection-name').value, scopes: allowedScopes, capabilities, tools,
371
+ ...(pluginOperations.length || record?.pluginOperations?.length ? { pluginOperations } : {}) };
372
+ const result = await ctx.connection.postManagement(BASE + '/connections' + (record ? '/' + selected + '/update' : ''),
373
+ { ...value, ...(record ? { expectedRevision: record.revision } : { key }) });
374
+ const next = await reload(ctx.connection); if (!ctx.valid()) return;
375
+ records = next; select(result.connectionId); if (!record) setView('delivery'); feedback(record ? '更改已保存,后续调用按新授权检查。' : '连接已创建。生成配置并交给客户端后,即可接入。');
376
+ });
377
+ });
378
+ $('#connection-export-button').addEventListener('click', () => {
379
+ if (formDirty || $('#connection-location').value !== 'same') return;
380
+ clearExport();
381
+ const id = selected, ticket = viewTicket;
382
+ perform(async ctx => {
383
+ const choice = templates.find(item => item.choiceId === $('#connection-template').value);
384
+ const result = await ctx.connection.postManagement(BASE + '/connections/' + id + '/export', choice
385
+ ? choice.presetId ? { presetId: choice.presetId } : { templateId: choice.templateId }
386
+ : { transport: $('#connection-transport').value, format: $('#connection-format').value });
387
+ if (!ctx.valid()) return;
388
+ if (selected !== id || !visible || viewTicket !== ticket) return;
389
+ $('#connection-configuration').value = result.content ?? JSON.stringify(result.configuration, null, 2) + '\n';
390
+ exportDelivery = { filename: result.filename || 'iris-mcp.json', mediaType: result.mediaType || 'application/json' };
391
+ $('#connection-download').textContent = exportDelivery.filename.endsWith('.toml') ? '下载 TOML' : '下载 JSON';
392
+ $('#connection-export-note').textContent = result.containsCredentials ? '此 HTTP 配置含连接密钥,请仅交给获授权的本机客户端。' : 'stdio 配置引用本机私有文件,需要使用本机路径。';
393
+ $('#connection-export-result').hidden = false; feedback('配置已生成。客户端是否安装或加载,需要在客户端确认。');
394
+ const record = current();
395
+ exports.set(id, { at: new Date().toISOString(), revision: record.revision, choice: choiceKey(), transport: $('#connection-transport').value, format: $('#connection-format').value });
396
+ renderOnboarding();
397
+ });
398
+ });
399
+ $('#connection-copy').addEventListener('click', async () => {
400
+ const at = epoch, id = selected, ticket = viewTicket;
401
+ const currentCopy = () => at === epoch && id === selected && ticket === viewTicket && visible;
402
+ try { await navigator.clipboard.writeText($('#connection-configuration').value); if (currentCopy()) feedback('配置已复制。'); }
403
+ catch (error) { if (currentCopy()) feedback('未能复制,请手动选择配置文本。' + error.message, true); }
404
+ });
405
+ $('#connection-download').addEventListener('click', () => {
406
+ const value = $('#connection-configuration').value; if (!value) return;
407
+ download(value, exportDelivery.filename, exportDelivery.mediaType);
408
+ clearExport();
409
+ $('#connection-export-note').textContent = ''; feedback('配置下载已交给浏览器。');
410
+ });
411
+ $('#connection-template').addEventListener('change', templateChoice);
412
+ $('#connection-template-example').addEventListener('click', () => { if (catalog) download(JSON.stringify(catalog.templateExample, null, 2) + '\n', 'iris-template.json'); });
413
+ $('#connection-template-import').addEventListener('click', () => $('#connection-template-file').click());
414
+ $('#connection-template-file').addEventListener('change', () => {
415
+ const file = $('#connection-template-file').files[0]; $('#connection-template-file').value = ''; if (!file) return;
416
+ perform(async ctx => {
417
+ if (file.size > 8192) throw Object.assign(new Error('模板最多 8 KiB。'), { status: 400 });
418
+ let value;
419
+ try { value = JSON.parse(await file.text()); }
420
+ catch (_) { throw Object.assign(new Error('请选择有效的 JSON 模板。'), { status: 400 }); }
421
+ if (!ctx.valid()) return;
422
+ const imported = await ctx.connection.postManagement(BASE + '/templates', value);
423
+ const result = await ctx.connection.get(BASE + '/templates'); if (!ctx.valid()) return;
424
+ setTemplates(result); renderTemplates(imported.templateId); feedback('模板已保存,仅应用传输与格式;连接授权没有改变。');
425
+ });
426
+ });
427
+ $('#connection-skill-generate').addEventListener('click', () => {
428
+ clearExport(); const id = selected, ticket = viewTicket;
429
+ perform(async ctx => {
430
+ const result = await ctx.connection.postManagement(BASE + '/connections/' + id + '/skill', {});
431
+ if (!ctx.valid()) return;
432
+ if (selected !== id || !visible || viewTicket !== ticket) return;
433
+ skillBundle = result.bundle; $('#connection-skill-preview').value = result.preview; $('#connection-skill-result').hidden = false;
434
+ feedback('说明已生成。下载并安装后,还需在客户端确认已发现此 Skill。');
435
+ });
436
+ });
437
+ $('#connection-skill-download').addEventListener('click', () => {
438
+ if (!skillBundle) return;
439
+ download(JSON.stringify(skillBundle, null, 2) + '\n', 'iris-skill.json'); clearExport(); feedback('Skill 文件下载已交给浏览器,尚未安装。');
440
+ });
441
+ $('#connection-skill-clear').addEventListener('click', clearExport);
442
+ $('#connection-revoke').addEventListener('click', () => { $('#connection-revoke-confirm').hidden = false; $('#connection-revoke').hidden = true; $('#connection-delete').hidden = true; clearExport(); });
443
+ $('#connection-revoke-no').addEventListener('click', () => { $('#connection-revoke-confirm').hidden = true; $('#connection-revoke').hidden = false; $('#connection-delete').hidden = false; });
444
+ $('#connection-revoke-yes').addEventListener('click', () => perform(async ctx => {
445
+ const record = current();
446
+ await ctx.connection.postManagement(BASE + '/connections/' + selected + '/revoke', { expectedRevision: record.revision });
447
+ const id = selected, next = await reload(ctx.connection); if (!ctx.valid()) return;
448
+ records = next; select(id); feedback('连接已撤销,旧配置不能再调用 Core。');
449
+ }));
450
+ $('#connection-delete').addEventListener('click', () => { $('#connection-delete-confirm').hidden = false; $('#connection-delete').hidden = true; $('#connection-revoke').hidden = true; clearExport(); });
451
+ $('#connection-delete-no').addEventListener('click', () => { $('#connection-delete-confirm').hidden = true; $('#connection-delete').hidden = false; $('#connection-revoke').hidden = Boolean(current()?.revokedAt); });
452
+ $('#connection-delete-yes').addEventListener('click', () => perform(async ctx => {
453
+ const record = current();
454
+ await ctx.connection.postManagement(BASE + '/connections/' + selected + '/delete', { expectedRevision: record.revision });
455
+ const next = await reload(ctx.connection); if (!ctx.valid()) return;
456
+ records = next; select(records[0]?.connectionId || null); editor(false); feedback('连接已删除,旧配置已失效;作品和任务保留。');
457
+ }));
458
+ $('#connection-new').addEventListener('click', () => {
459
+ if (busy || !catalog) return;
460
+ select(); editor(true); setView('settings'); feedback('已准备新连接。填写名称、选择功能和权限后,点击“创建连接”。');
461
+ const name = $('#connection-name'); name.focus({ preventScroll: true }); name.scrollIntoView({ block: 'center', behavior: 'instant' });
462
+ });
463
+ function closeEditor(discard = false) {
464
+ if (busy) return;
465
+ if (formDirty && !discard) { $('#connection-leave-confirm').hidden = false; $('#connection-leave-confirm').scrollIntoView({ block: 'center' }); $('#connection-leave-keep').focus(); return; }
466
+ clearExport(); formDirty = false; $('#connection-leave-confirm').hidden = true; editor(false); feedback(); $('#connection-new').focus();
467
+ }
468
+ $('#connection-form').addEventListener('input', () => { formDirty = true; closeButton.textContent = '取消编辑'; renderOnboarding(); controls(); });
469
+ $('#connection-back').addEventListener('click', () => closeEditor());
470
+ closeButton.addEventListener('click', () => closeEditor());
471
+ $('#connection-leave-keep').addEventListener('click', () => { $('#connection-leave-confirm').hidden = true; });
472
+ $('#connection-leave-discard').addEventListener('click', () => closeEditor(true));
473
+ for (const button of document.querySelectorAll('[data-connection-view]')) button.addEventListener('click', () => setView(button.dataset.connectionView));
474
+ $('#connections-refresh').addEventListener('click', () => perform(async ctx => {
475
+ if (!catalog) { await connect(ctx.connection); return; }
476
+ const [next, saved, available] = await Promise.all([reload(ctx.connection), ctx.connection.get(BASE + '/templates'), ctx.connection.get(BASE + '/catalog')]); if (!ctx.valid()) return;
477
+ records = next; setTemplates(saved); setCatalog(available); renderTemplates(); select(selected); feedback('连接记录已刷新。');
478
+ }));
479
+ for (const id of ['connection-transport', 'connection-format', 'connection-name']) $('#' + id).addEventListener('input', clearExport);
480
+ $('#connection-transport').addEventListener('change', transportNote);
481
+ $('#connection-clear-export').addEventListener('click', clearExport);
482
+ $('#connection-location').addEventListener('change', () => { clearExport(); controls(); });
483
+ $('#connection-check').addEventListener('click', checkSetup);
484
+ $('#connection-verify-next').addEventListener('click', () => setView('verification'));
485
+ $('#connection-return-settings').addEventListener('click', () => setView('settings'));
486
+ async function connect(connection) {
487
+ const at = ++epoch; client = connection; busy = true; records = []; selected = null; editor(false); clearExport(); feedback('正在读取连接…'); controls();
488
+ try {
489
+ const [available, next, saved] = await Promise.all([connection.get(BASE + '/catalog'), connection.get(BASE + '/connections'), connection.get(BASE + '/templates')]);
490
+ if (!valid(at, connection)) return;
491
+ if (available.contractVersion !== 0 || next.contractVersion !== 0 || saved.contractVersion !== 0) throw new TypeError('不支持的连接管理版本');
492
+ setCatalog(available); records = next.connections; setTemplates(saved);
493
+ $('#connection-service-note').textContent = catalog.httpEnabled ? 'HTTP MCP 已开启 · ' + location.origin + '/mcp。创建连接会生成专属授权,不会自动配置或启动客户端。'
494
+ : '可用 stdio 接入 · 此服务未开启 HTTP MCP。创建连接不会自动配置或启动客户端。';
495
+ renderTemplates(); select(records[0]?.connectionId || null); feedback();
496
+ } catch (error) { if (valid(at, connection)) feedback(describe(error), true); }
497
+ finally { if (valid(at, connection)) { busy = false; controls(); } }
498
+ }
499
+ return Object.freeze({
500
+ connect,
501
+ show(value) { if (visible !== value) viewTicket++; visible = value; if (!value) { cancelCheck(); clearExport(); feedback(); controls(); } },
502
+ disconnect() {
503
+ cancelCheck(); exports.clear(); inspections.clear();
504
+ capabilities = []; tools = []; pluginOperations = []; allowedScopes = ['read']; scopes = {}; $('#connection-location').value = 'same';
505
+ epoch++; client = null; busy = false; visible = false; catalog = null; records = []; templates = []; selected = null; editor(false); clearExport(); feedback();
506
+ formDirty = false; $('#connection-leave-confirm').hidden = true; closeButton.textContent = '返回列表';
507
+ $('#connection-template').replaceChildren(); $('#connection-template-note').replaceChildren(); $('#connection-template-note').hidden = true; $('#connection-template-file').value = '';
508
+ $('#connections-list').replaceChildren(); $('#connection-facts').replaceChildren(); $('#connection-name').value = '';
509
+ $('#connection-contribution-list').replaceChildren(); $('#connection-contributions').hidden = true;
510
+ $('#connection-service-note').textContent = '';
511
+ for (const id of ['connection-media', 'connection-scopes', 'connection-tools']) $('#' + id).replaceChildren();
512
+ $('#connection-export').hidden = true; $('#connection-revoke-section').hidden = true; $('#connection-fields').disabled = true;
513
+ $('#connections-count').textContent = '0';
514
+ }
515
+ });
516
+ }