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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.en.md +103 -312
  3. package/README.md +100 -310
  4. package/bin/dsh-iris.js +333 -65
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +2 -0
  7. package/docs/ARCHITECTURE.md +13 -3
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CONTACT_SHEET.md +62 -0
  10. package/docs/CORE_CONNECTIONS.md +99 -0
  11. package/docs/CORE_QUICKSTART.md +108 -0
  12. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  13. package/docs/CORE_SERVICE.md +97 -0
  14. package/docs/CORE_SKILLS.md +35 -3
  15. package/docs/DSH_COMPATIBILITY.md +10 -0
  16. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  17. package/docs/DSH_RUNTIME.md +6 -2
  18. package/docs/HEADLESS_CLI.md +2 -2
  19. package/docs/IMAGE_FLOW.md +4 -0
  20. package/docs/IMAGE_GENERATION.md +127 -0
  21. package/docs/LOCAL_API.md +4 -2
  22. package/docs/LOCAL_API_CONTRACT.md +39 -14
  23. package/docs/LOCAL_CREATION.md +7 -1
  24. package/docs/LOCAL_OPERATIONS.md +46 -0
  25. package/docs/LOCAL_SELECTION.md +57 -0
  26. package/docs/LOCAL_WEB.md +201 -0
  27. package/docs/MCP.md +191 -0
  28. package/docs/MUSIC.md +2 -0
  29. package/docs/PANEL_EXTENSIONS.md +64 -0
  30. package/docs/PLUGINS.md +189 -0
  31. package/docs/PLUGIN_STARTUP.md +100 -0
  32. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  33. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  34. package/docs/PROVIDER_HEALTH.md +3 -1
  35. package/docs/PROVIDER_SETTINGS.md +85 -0
  36. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  37. package/docs/ROADMAP.md +17 -7
  38. package/docs/VISION_CLI.md +1 -1
  39. package/docs/WEB_PROTOTYPE.md +21 -6
  40. package/docs/WEB_RECOVERY.md +46 -0
  41. package/docs/WORKBENCH_LOGIN.md +63 -0
  42. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  43. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  44. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  45. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  46. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  47. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  48. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  49. package/docs/releases/0.3.0-alpha.1.md +51 -0
  50. package/docs/screenshots.md +41 -1
  51. package/lib/actions.js +53 -2
  52. package/lib/adapters.js +20 -11
  53. package/lib/application-runtime.js +34 -7
  54. package/lib/audio-flow-input.js +37 -0
  55. package/lib/client.js +346 -53
  56. package/lib/comfyui-adapter.js +94 -0
  57. package/lib/comfyui-client.js +51 -0
  58. package/lib/comfyui-inspection.js +73 -0
  59. package/lib/comfyui-workflow.js +94 -0
  60. package/lib/config.js +36 -1
  61. package/lib/configured-image-runtime.js +50 -0
  62. package/lib/connection-skill.js +89 -0
  63. package/lib/connection-template.js +23 -0
  64. package/lib/core-artifact-store.js +16 -2
  65. package/lib/core-bridge-selection.js +31 -0
  66. package/lib/core-bridge-tools.js +83 -0
  67. package/lib/core-capability-catalog.js +182 -0
  68. package/lib/core-connection-diagnostics.js +49 -0
  69. package/lib/core-connection-store.js +277 -0
  70. package/lib/core-connections.js +225 -0
  71. package/lib/core-maintenance.js +9 -3
  72. package/lib/core-runtime.js +2 -1
  73. package/lib/core-service-client.js +170 -0
  74. package/lib/core-tasks.js +29 -12
  75. package/lib/core-writer-lease.js +16 -0
  76. package/lib/creation-request-store.js +38 -64
  77. package/lib/dsh-host-adapter.js +4 -1
  78. package/lib/dsh-service-bridge.js +66 -0
  79. package/lib/execution-ownership.js +43 -0
  80. package/lib/flow-store.js +105 -10
  81. package/lib/generation-input.js +10 -1
  82. package/lib/headless-vision.js +7 -7
  83. package/lib/image-edit-input.js +1 -1
  84. package/lib/image-flow.js +73 -29
  85. package/lib/index.js +37 -17
  86. package/lib/local-api-projection.js +10 -5
  87. package/lib/local-api-token-directory.js +31 -0
  88. package/lib/local-api.js +769 -67
  89. package/lib/local-creation.js +89 -24
  90. package/lib/local-operations.js +36 -0
  91. package/lib/local-request-root.js +86 -0
  92. package/lib/local-web-assets.js +37 -0
  93. package/lib/mcp-bridge.js +58 -0
  94. package/lib/mcp-check.js +26 -0
  95. package/lib/mcp-config.js +42 -0
  96. package/lib/mcp-delivery.js +46 -0
  97. package/lib/mcp-http.js +101 -0
  98. package/lib/mcp-server.js +47 -0
  99. package/lib/model-directory-service.js +16 -6
  100. package/lib/model-directory.js +3 -3
  101. package/lib/models.js +2 -2
  102. package/lib/music-transport.js +2 -1
  103. package/lib/operation-runner.js +236 -0
  104. package/lib/operation-store.js +216 -0
  105. package/lib/panel-extension.js +36 -0
  106. package/lib/plugin-contract.js +194 -0
  107. package/lib/plugin-execution.js +51 -0
  108. package/lib/plugin-launch.js +86 -0
  109. package/lib/plugin-operation-store.js +101 -0
  110. package/lib/plugin-operations.js +28 -0
  111. package/lib/plugin-registry.js +109 -0
  112. package/lib/plugin-selection.js +15 -0
  113. package/lib/plugin-source-image.js +5 -0
  114. package/lib/plugin-web-assets.js +54 -0
  115. package/lib/plugins.d.ts +288 -0
  116. package/lib/plugins.js +58 -0
  117. package/lib/provider-adapter.js +5 -1
  118. package/lib/provider-adapters.js +56 -18
  119. package/lib/provider-catalog.js +40 -29
  120. package/lib/provider-config-service.js +105 -23
  121. package/lib/provider-diagnostics.js +56 -0
  122. package/lib/provider-extensions.js +29 -0
  123. package/lib/provider-health.js +8 -1
  124. package/lib/provider-monitor.js +79 -0
  125. package/lib/provider-protocol.js +70 -20
  126. package/lib/provider-registry.js +118 -0
  127. package/lib/provider-response.js +28 -0
  128. package/lib/provider-settings.js +134 -0
  129. package/lib/provider-task-runner.js +22 -9
  130. package/lib/skill-installation.js +23 -11
  131. package/lib/vision-model-routing.js +3 -0
  132. package/lib/web/app.js +642 -0
  133. package/lib/web/appearance.js +111 -0
  134. package/lib/web/comfyui.js +146 -0
  135. package/lib/web/connections.js +377 -0
  136. package/lib/web/creation.js +355 -0
  137. package/lib/web/flow-view.js +33 -0
  138. package/lib/web/gallery.js +132 -0
  139. package/lib/web/index.html +291 -0
  140. package/lib/web/media-client.js +204 -0
  141. package/lib/web/plugin-panel.js +221 -0
  142. package/lib/web/plugin-surface.css +17 -0
  143. package/lib/web/providers.js +492 -0
  144. package/lib/web/recovery.js +105 -0
  145. package/lib/web/startup.js +18 -0
  146. package/lib/web/styles.css +518 -0
  147. package/lib/web/transport.js +96 -0
  148. package/lib/web/workbench-login.js +165 -0
  149. package/lib/workbench-auth-cli.js +47 -0
  150. package/lib/workbench-auth.js +200 -0
  151. package/package.json +20 -5
  152. package/plugins/contact-sheet/index.d.mts +4 -0
  153. package/plugins/contact-sheet/index.mjs +33 -0
  154. package/plugins/contact-sheet/web/board.css +14 -0
  155. package/plugins/contact-sheet/web/board.mjs +93 -0
  156. package/skills/iris-core/SKILL.md +8 -4
  157. package/skills/iris-core/references/cli.md +2 -2
  158. package/skills/iris-core/references/comfyui.md +69 -0
  159. package/skills/iris-core/references/tasks.md +2 -0
  160. package/user_guide.md +5 -1
@@ -0,0 +1,377 @@
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
+ export function createConnectionPanel({ describe }) {
10
+ let client, epoch = 0, catalog, records = [], selected = null, key, busy = false, visible = false, viewTicket = 0;
11
+ let scopes = {}, control = [];
12
+ let capabilities = [], tools = [], pluginOperations = [], allowedScopes = ['read'], templates = [], skillBundle = null;
13
+ let view = 'settings', exportDelivery = null;
14
+ let formDirty = false;
15
+ const closeButton = element('button', '返回列表', 'quiet'); closeButton.type = 'button'; closeButton.id = 'connection-cancel';
16
+ const closeArea = element('div', '', 'provider-footer'); closeArea.append(closeButton); $('#connection-editor').append(closeArea);
17
+ const urls = new Set();
18
+ const current = () => records.find(record => record.connectionId === selected);
19
+ const valid = (at, connection) => at === epoch && client === connection;
20
+ function editor(open) { $('#connection-editor').hidden = !open; $('#connections-overview').hidden = open; }
21
+ function setView(next) {
22
+ if (next !== view) clearExport(); view = next;
23
+ for (const button of document.querySelectorAll('[data-connection-view]')) {
24
+ const active = button.dataset.connectionView === view; button.setAttribute('aria-pressed', String(active));
25
+ $('#connection-view-' + button.dataset.connectionView).hidden = !active;
26
+ }
27
+ }
28
+ function feedback(text = '', error = false) {
29
+ const node = $('#connections-feedback'); node.hidden = !text; node.textContent = text; node.classList.toggle('error', error);
30
+ }
31
+ function clearExport() {
32
+ viewTicket++;
33
+ exportDelivery = null;
34
+ $('#connection-configuration').value = ''; $('#connection-export-result').hidden = true;
35
+ $('#connection-export-note').textContent = '';
36
+ skillBundle = null; $('#connection-skill-preview').value = ''; $('#connection-skill-result').hidden = true;
37
+ for (const url of urls) URL.revokeObjectURL(url); urls.clear();
38
+ }
39
+ function controls() {
40
+ const record = current(), editable = !record || record.managed && !record.revokedAt;
41
+ $('#connection-fields').disabled = busy || !catalog || !editable;
42
+ 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',
43
+ 'connection-template', 'connection-template-import', 'connection-template-example', 'connection-template-file', 'connection-skill-generate']) $('#' + id).disabled = busy || !catalog;
44
+ for (const id of ['connection-transport', 'connection-format']) $('#' + id).disabled = busy || !catalog || Boolean($('#connection-template').value);
45
+ $('#connections-refresh').disabled = busy;
46
+ $('#connection-back').disabled = busy;
47
+ closeButton.disabled = busy;
48
+ for (const node of document.querySelectorAll('#connections-list .connection-item')) node.disabled = busy;
49
+ for (const button of document.querySelectorAll('[data-connection-view]')) button.disabled = busy || button.dataset.connectionView === 'delivery' && (!record?.managed || Boolean(record.revokedAt)) || button.dataset.connectionView === 'details' && !record;
50
+ }
51
+ function renderList() {
52
+ $('#connections-count').textContent = String(records.filter(record => record.state !== 'revoked').length);
53
+ $('#connections-empty').hidden = records.length !== 0;
54
+ $('#connections-list').replaceChildren(...records.map(record => {
55
+ const button = element('button', '', 'connection-item service-card'); button.type = 'button'; button.dataset.connectionId = record.connectionId;
56
+ button.setAttribute('aria-current', String(record.connectionId === selected));
57
+ const heading = element('span', '', 'service-card-heading'); heading.append(element('span', 'MCP', 'service-mark'), element('small', states[record.state] || record.state, 'service-status'));
58
+ const media = record.capabilities.map(id => { const group = catalog.groups.find(group => group.id === id); return group?.shortTitle || group?.title || id; });
59
+ for (const pin of record.pluginOperations || []) media.push(catalog.contributions?.operations?.find(item => item.id === pin.contributionId)?.title || pin.contributionId);
60
+ button.append(heading, element('strong', record.label), element('span', media.join(' · ') || '仅控制工具', 'service-endpoint'),
61
+ element('span', record.lastContact ? '最近使用 · ' + date(record.lastContact.at) : '等待客户端首次使用', 'service-meta'), element('span', record.revokedAt ? '查看与管理 →' : '接入与管理 →', 'service-action'));
62
+ button.addEventListener('click', () => { if (!busy) { select(record.connectionId); editor(true); setView(record.managed && !record.revokedAt ? 'delivery' : 'details'); } }); return button;
63
+ }));
64
+ controls();
65
+ }
66
+ function option(value, title, selectedValues, change, { disabled = false, note = '' } = {}) {
67
+ const label = element('label', '', 'check-option'), input = document.createElement('input');
68
+ input.type = 'checkbox'; input.value = value; input.checked = selectedValues.includes(value); input.disabled = disabled;
69
+ input.addEventListener('change', change); const text = element('span', title);
70
+ if (note) text.append(element('small', note)); label.append(input, text); return label;
71
+ }
72
+ function enabledTools() {
73
+ return new Set([...control, ...catalog.groups.filter(group => capabilities.includes(group.id)).flatMap(group => group.tools),
74
+ ...(pluginOperations.length ? catalog.pluginControlTools || [] : [])].filter(name => !catalog.availableTools || catalog.availableTools.includes(name)));
75
+ }
76
+ function renderTools() {
77
+ const enabled = enabledTools();
78
+ $('#connection-tools').replaceChildren(...catalog.tools.filter(tool => enabled.has(tool.name)).map(tool =>
79
+ option(tool.name, tool.name.replace('iris_core_', ''), tools, () => {
80
+ tools = checked('#connection-tools input'); countTools(); clearExport();
81
+ }, { disabled: !allowedScopes.includes(tool.scope) || !catalog.availableScopes.includes(tool.scope), note: scopes[tool.scope] })));
82
+ countTools();
83
+ }
84
+ function countTools() {
85
+ const count = catalog.tools.filter(tool => tools.includes(tool.name) && allowedScopes.includes(tool.scope) && catalog.availableScopes.includes(tool.scope)
86
+ && enabledTools().has(tool.name)).length;
87
+ $('#connection-tool-count').textContent = count + ' 个可用';
88
+ }
89
+ function renderOptions() {
90
+ const mediaOptions = catalog.media.map(media => option(media.id, media.title,
91
+ media.capabilities.some(id => capabilities.includes(id)) ? [media.id] : [], () => {
92
+ const enabled = checked('#connection-media input');
93
+ capabilities = catalog.media.filter(item => enabled.includes(item.id)).flatMap(item => {
94
+ const selected = item.capabilities.filter(id => capabilities.includes(id));
95
+ return selected.length ? selected : item.capabilities.slice(0, 1);
96
+ });
97
+ tools = [...enabledTools()];
98
+ renderOptions(); clearExport();
99
+ }, { disabled: !media.capabilities.length, note: !media.capabilities.length ? '未接入' : media.note || '' }));
100
+ $('#connection-media').replaceChildren(...mediaOptions.filter((_, index) => catalog.media[index].capabilities.length));
101
+ $('#connection-media-unavailable').replaceChildren(...mediaOptions.filter((_, index) => !catalog.media[index].capabilities.length));
102
+ const image = catalog.media.find(item => item.id === 'image');
103
+ $('#connection-image-options').replaceChildren(...(image?.capabilities || []).map(id => option(id,
104
+ catalog.groups.find(group => group.id === id).label || catalog.groups.find(group => group.id === id).title, capabilities, () => {
105
+ capabilities = [...capabilities.filter(id => !image.capabilities.includes(id)), ...checked('#connection-image-options input')];
106
+ tools = [...enabledTools()];
107
+ renderOptions(); clearExport();
108
+ })));
109
+ const entries = (catalog.contributions?.operations || []).filter(item => item.access.mcp === 'requires-selection');
110
+ const choices = entries.map(item => ({ title: item.title, dependency: item.dependency, available: true, imageInput: !!item.artifactInputs?.length }));
111
+ 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 });
112
+ $('#connection-plugin-section').hidden = !choices.length;
113
+ $('#connection-plugin-options').replaceChildren(...choices.map(item => option(JSON.stringify(item.dependency), item.title,
114
+ pluginOperations.map(pin => JSON.stringify(pin)), () => {
115
+ pluginOperations = checked('#connection-plugin-options input').map(value => JSON.parse(value));
116
+ const enabled = enabledTools();
117
+ tools = [...new Set([...tools, ...(pluginOperations.length ? catalog.pluginControlTools || [] : [])])].filter(name => enabled.has(name));
118
+ renderOptions(); clearExport();
119
+ }, { note: item.dependency.pluginVersion + ' · ' + (item.available ? '需允许执行;只开放此准确业务版本' : '当前不可用;保留选择可读取原记录')
120
+ + (item.imageInput ? ' · 图片输入共享本空间内 ≤8 MiB PNG/JPEG/WebP 素材' : '') })));
121
+ $('#connection-scopes').replaceChildren(...Object.entries(scopes).map(([id, title]) => option(id, title, allowedScopes, () => {
122
+ allowedScopes = checked('#connection-scopes input'); renderOptions(); clearExport();
123
+ }, { disabled: id === 'read' || !catalog.availableScopes.includes(id) && !allowedScopes.includes(id), note: catalog.availableScopes.includes(id) ? '' : '服务未开启' })));
124
+ renderTools();
125
+ }
126
+ function fact(label, value) { $('#connection-facts').append(element('dt', label), element('dd', value)); }
127
+ function select(id = null) {
128
+ selected = id; key = crypto.randomUUID(); clearExport(); feedback();
129
+ const record = current();
130
+ capabilities = [...(record?.capabilities || catalog.defaults?.capabilities || catalog.groups.slice(0, 1).map(group => group.id))];
131
+ pluginOperations = structuredClone(record?.pluginOperations || []);
132
+ const enabled = enabledTools();
133
+ tools = [...(record?.tools || catalog.tools.filter(tool => enabled.has(tool.name)).map(tool => tool.name))];
134
+ allowedScopes = [...(record?.scopes || ['read'])];
135
+ $('#connection-name').value = record?.label || '';
136
+ $('#connection-editor-title').textContent = record ? '连接设置' : '新建连接';
137
+ $('#connection-state').textContent = record ? states[record.state] : '尚未保存';
138
+ $('#connection-save').textContent = record ? '保存更改' : '创建连接';
139
+ $('#connection-save-note').textContent = record?.revokedAt ? '连接已撤销。可删除记录,需要使用时请新建连接。'
140
+ : record && !record.managed ? '静态授权由启动文件管理。' : '仅更新授权配置,不调用模型。';
141
+ $('#connection-facts').replaceChildren(); $('#connection-facts').hidden = !record;
142
+ if (record) {
143
+ const instances = [...record.instances].sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
144
+ const contact = record.lastContact || instances.find(instance => instance.declaredClient)?.declaredClient;
145
+ fact('连接身份', record.connectionId);
146
+ fact('客户端声明', contact ? contact.clientInfo.name + ' · ' + contact.clientInfo.version : '尚未收到');
147
+ if (contact) fact('最近声明', date(contact.at) + ' · ' + contact.transport);
148
+ fact('桥接', record.adapterId || 'iris.mcp');
149
+ if (instances.length) fact('桥版本', instances[0].harnessVersion);
150
+ fact('实例记录', record.instances.length ? record.instances.length + ' 个握手实例(不表示持续在线)' : '当前无握手实例');
151
+ }
152
+ $('#connection-export').hidden = !record?.managed || Boolean(record.revokedAt);
153
+ $('#connection-revoke-section').hidden = !record?.managed;
154
+ $('#connection-revoke-confirm').hidden = true; $('#connection-revoke').hidden = Boolean(record?.revokedAt);
155
+ $('#connection-delete-confirm').hidden = true; $('#connection-delete').hidden = false;
156
+ $('#connection-tool-details').open = false;
157
+ $('#connection-skill-details').open = false;
158
+ $('#connection-transport option[value="http"]').disabled = !catalog.httpEnabled;
159
+ if (!record) setView('settings'); else if (view === 'delivery' && (!record.managed || record.revokedAt)) setView('details');
160
+ transportNote();
161
+ renderOptions(); renderList();
162
+ formDirty = false; $('#connection-leave-confirm').hidden = true; closeButton.textContent = '返回列表';
163
+ }
164
+ async function reload(connection) {
165
+ const result = await connection.get(BASE + '/connections');
166
+ if (result.contractVersion !== 0) throw new TypeError('不支持的连接列表版本');
167
+ return result.connections;
168
+ }
169
+ function renderContributions() {
170
+ const section = $('#connection-contributions'), list = $('#connection-contribution-list');
171
+ list.replaceChildren(); section.hidden = !catalog?.plugins?.length;
172
+ if (section.hidden) return;
173
+ const labels = { providers: '模型协议', operations: '业务操作', pages: '页面', renderers: '作品渲染器', settings: '设置表单' };
174
+ const status = { 'requires-grant': '需显式本地授权', 'requires-configuration': '需配置模型服务',
175
+ 'descriptor-only': '仅声明', unavailable: '已不可用', 'not-exposed': 'MCP 尚未接入',
176
+ 'durable-operation-required': 'MCP 待接入持久执行', 'not-mounted': '页面尚未挂载', 'available-in-panel': '已选入工作台' };
177
+ Object.assign(status, { 'via-operation': 'Core 持久执行', 'requires-selection': '用户选择后可经 MCP 调用' });
178
+ for (const [kind, entries] of Object.entries(catalog.contributions)) for (const entry of entries) {
179
+ const item = element('li'), remote = kind === 'operations' ? entry.access.mcp : ['pages', 'renderers', 'settings'].includes(kind) ? entry.access.web : null;
180
+ item.append(element('strong', entry.title), document.createTextNode(' · ' + labels[kind] + ' · ' + (status[entry.access.local] || entry.access.local)
181
+ + (remote ? ' · ' + (status[remote] || remote) : '')),
182
+ element('p', entry.dependency.pluginId + '@' + entry.dependency.pluginVersion + ' · ' + entry.id + '@' + entry.version, 'field-note'));
183
+ list.append(item);
184
+ }
185
+ }
186
+ function setCatalog(value) {
187
+ catalog = { ...value, tools: [...value.tools, ...(value.pluginTools || [])] };
188
+ // A running preview may still serve the earlier v0 DTO. Derive safe choices from its declarations.
189
+ scopes = Object.fromEntries((catalog.scopeOptions || catalog.availableScopes.map(id => ({ id, title: id }))).map(item => [item.id, item.title]));
190
+ control = catalog.controlTools || catalog.tools.filter(tool => !catalog.groups.some(group => group.tools.includes(tool.name))).map(tool => tool.name);
191
+ renderContributions();
192
+ }
193
+ function transportNote() {
194
+ $('#connection-transport-note').textContent = $('#connection-transport').value === 'http'
195
+ ? 'HTTP:客户端连接此 Iris 服务的 /mcp 地址,使用导出配置中的专属密钥。Iris 服务需保持运行。'
196
+ : 'stdio:客户端按 command / args 启动本机 MCP 桥接进程。保留本机路径,Iris 服务需保持运行。';
197
+ }
198
+ function templateChoice() {
199
+ const template = templates.find(item => item.choiceId === $('#connection-template').value), note = $('#connection-template-note');
200
+ note.replaceChildren(); note.hidden = !template;
201
+ if (template) {
202
+ $('#connection-transport').value = template.transport; $('#connection-format').value = template.format;
203
+ note.append(document.createTextNode((template.hint || '模板只应用传输和格式。') + ' 不增加授权。'));
204
+ if (template.documentationUrl) {
205
+ const link = element('a', '查看客户端文档'); link.href = template.documentationUrl; link.target = '_blank'; link.rel = 'noopener noreferrer'; note.append(link);
206
+ }
207
+ }
208
+ clearExport(); transportNote(); controls();
209
+ }
210
+ function renderTemplates(selectedId = $('#connection-template').value) {
211
+ const manual = element('option', '手动选择'); manual.value = '';
212
+ $('#connection-template').replaceChildren(manual, ...templates.map(template => {
213
+ const node = element('option', template.title); node.value = template.choiceId; node.disabled = template.transport === 'http' && !catalog.httpEnabled; return node;
214
+ }));
215
+ $('#connection-template').value = templates.some(template => template.choiceId === selectedId && (template.transport !== 'http' || catalog.httpEnabled)) ? selectedId : '';
216
+ templateChoice();
217
+ }
218
+ function setTemplates(value) {
219
+ templates = [...(value.presets || []).map(item => ({ ...item, choiceId: 'preset:' + item.presetId })), ...value.templates.map(item => ({ ...item, choiceId: item.templateId }))];
220
+ const formats = value.formats || ['mcpServers', 'entry'];
221
+ for (const option of $('#connection-format').options) option.disabled = !formats.includes(option.value);
222
+ }
223
+ function download(value, filename, mediaType = 'application/json') {
224
+ const url = URL.createObjectURL(new Blob([value], { type: mediaType })); urls.add(url);
225
+ const link = document.createElement('a'); link.href = url; link.download = filename; link.click();
226
+ setTimeout(() => { URL.revokeObjectURL(url); urls.delete(url); }, 1000);
227
+ }
228
+ async function perform(action) {
229
+ if (busy || !client) return;
230
+ const at = epoch, connection = client; busy = true; controls(); feedback('正在处理…');
231
+ try { await action({ at, connection, valid: () => valid(at, connection) }); }
232
+ catch (error) {
233
+ if (valid(at, connection)) feedback((!error.status || error.status >= 500 ? '尚未确认操作结果,请刷新连接记录核对。' : '')
234
+ + (error.name === 'TimeoutError' ? '请求等待超时。' : describe(error)), true);
235
+ }
236
+ finally { if (valid(at, connection)) { busy = false; controls(); } }
237
+ }
238
+ $('#connection-form').addEventListener('submit', event => {
239
+ event.preventDefault(); const record = current();
240
+ if (record && (!record.managed || record.revokedAt)) return;
241
+ perform(async ctx => {
242
+ const value = { label: $('#connection-name').value, scopes: allowedScopes, capabilities, tools,
243
+ ...(pluginOperations.length || record?.pluginOperations?.length ? { pluginOperations } : {}) };
244
+ const result = await ctx.connection.postManagement(BASE + '/connections' + (record ? '/' + selected + '/update' : ''),
245
+ { ...value, ...(record ? { expectedRevision: record.revision } : { key }) });
246
+ const next = await reload(ctx.connection); if (!ctx.valid()) return;
247
+ records = next; select(result.connectionId); if (!record) setView('delivery'); feedback(record ? '更改已保存,后续调用按新授权检查。' : '连接已创建。生成配置并交给客户端后,即可接入。');
248
+ });
249
+ });
250
+ $('#connection-export-button').addEventListener('click', () => {
251
+ clearExport();
252
+ const id = selected, ticket = viewTicket;
253
+ perform(async ctx => {
254
+ const choice = templates.find(item => item.choiceId === $('#connection-template').value);
255
+ const result = await ctx.connection.postManagement(BASE + '/connections/' + id + '/export', choice
256
+ ? choice.presetId ? { presetId: choice.presetId } : { templateId: choice.templateId }
257
+ : { transport: $('#connection-transport').value, format: $('#connection-format').value });
258
+ if (!ctx.valid()) return;
259
+ if (selected !== id || !visible || viewTicket !== ticket) { feedback('配置已准备,但页面已切换。需要时请重新生成配置。'); return; }
260
+ $('#connection-configuration').value = result.content ?? JSON.stringify(result.configuration, null, 2) + '\n';
261
+ exportDelivery = { filename: result.filename || 'iris-mcp.json', mediaType: result.mediaType || 'application/json' };
262
+ $('#connection-download').textContent = exportDelivery.filename.endsWith('.toml') ? '下载 TOML' : '下载 JSON';
263
+ $('#connection-export-note').textContent = result.containsCredentials ? '此 HTTP 配置含连接密钥,请仅交给获授权的本机客户端。' : 'stdio 配置引用本机私有文件,需要使用本机路径。';
264
+ $('#connection-export-result').hidden = false; feedback('配置已生成。客户端是否安装或加载,需要在客户端确认。');
265
+ });
266
+ });
267
+ $('#connection-copy').addEventListener('click', async () => {
268
+ const at = epoch;
269
+ try { await navigator.clipboard.writeText($('#connection-configuration').value); if (at === epoch) feedback('配置已复制。'); }
270
+ catch (error) { if (at === epoch) feedback('未能复制,请手动选择配置文本。' + error.message, true); }
271
+ });
272
+ $('#connection-download').addEventListener('click', () => {
273
+ const value = $('#connection-configuration').value; if (!value) return;
274
+ download(value, exportDelivery.filename, exportDelivery.mediaType);
275
+ clearExport();
276
+ $('#connection-export-note').textContent = ''; feedback('配置下载已交给浏览器。');
277
+ });
278
+ $('#connection-template').addEventListener('change', templateChoice);
279
+ $('#connection-template-example').addEventListener('click', () => { if (catalog) download(JSON.stringify(catalog.templateExample, null, 2) + '\n', 'iris-template.json'); });
280
+ $('#connection-template-import').addEventListener('click', () => $('#connection-template-file').click());
281
+ $('#connection-template-file').addEventListener('change', () => {
282
+ const file = $('#connection-template-file').files[0]; $('#connection-template-file').value = ''; if (!file) return;
283
+ perform(async ctx => {
284
+ if (file.size > 8192) throw Object.assign(new Error('模板最多 8 KiB。'), { status: 400 });
285
+ let value;
286
+ try { value = JSON.parse(await file.text()); }
287
+ catch (_) { throw Object.assign(new Error('请选择有效的 JSON 模板。'), { status: 400 }); }
288
+ if (!ctx.valid()) return;
289
+ const imported = await ctx.connection.postManagement(BASE + '/templates', value);
290
+ const result = await ctx.connection.get(BASE + '/templates'); if (!ctx.valid()) return;
291
+ setTemplates(result); renderTemplates(imported.templateId); feedback('模板已保存,仅应用传输与格式;连接授权没有改变。');
292
+ });
293
+ });
294
+ $('#connection-skill-generate').addEventListener('click', () => {
295
+ clearExport(); const id = selected, ticket = viewTicket;
296
+ perform(async ctx => {
297
+ const result = await ctx.connection.postManagement(BASE + '/connections/' + id + '/skill', {});
298
+ if (!ctx.valid()) return;
299
+ if (selected !== id || !visible || viewTicket !== ticket) { feedback('说明已准备,但页面已切换。需要时请重新生成说明。'); return; }
300
+ skillBundle = result.bundle; $('#connection-skill-preview').value = result.preview; $('#connection-skill-result').hidden = false;
301
+ feedback('说明已生成。下载并安装后,还需在客户端确认已发现此 Skill。');
302
+ });
303
+ });
304
+ $('#connection-skill-download').addEventListener('click', () => {
305
+ if (!skillBundle) return;
306
+ download(JSON.stringify(skillBundle, null, 2) + '\n', 'iris-skill.json'); clearExport(); feedback('Skill 文件下载已交给浏览器,尚未安装。');
307
+ });
308
+ $('#connection-skill-clear').addEventListener('click', clearExport);
309
+ $('#connection-revoke').addEventListener('click', () => { $('#connection-revoke-confirm').hidden = false; $('#connection-revoke').hidden = true; $('#connection-delete').hidden = true; clearExport(); });
310
+ $('#connection-revoke-no').addEventListener('click', () => { $('#connection-revoke-confirm').hidden = true; $('#connection-revoke').hidden = false; $('#connection-delete').hidden = false; });
311
+ $('#connection-revoke-yes').addEventListener('click', () => perform(async ctx => {
312
+ const record = current();
313
+ await ctx.connection.postManagement(BASE + '/connections/' + selected + '/revoke', { expectedRevision: record.revision });
314
+ const id = selected, next = await reload(ctx.connection); if (!ctx.valid()) return;
315
+ records = next; select(id); feedback('连接已撤销,旧配置不能再调用 Core。');
316
+ }));
317
+ $('#connection-delete').addEventListener('click', () => { $('#connection-delete-confirm').hidden = false; $('#connection-delete').hidden = true; $('#connection-revoke').hidden = true; clearExport(); });
318
+ $('#connection-delete-no').addEventListener('click', () => { $('#connection-delete-confirm').hidden = true; $('#connection-delete').hidden = false; $('#connection-revoke').hidden = Boolean(current()?.revokedAt); });
319
+ $('#connection-delete-yes').addEventListener('click', () => perform(async ctx => {
320
+ const record = current();
321
+ await ctx.connection.postManagement(BASE + '/connections/' + selected + '/delete', { expectedRevision: record.revision });
322
+ const next = await reload(ctx.connection); if (!ctx.valid()) return;
323
+ records = next; select(records[0]?.connectionId || null); editor(false); feedback('连接已删除,旧配置已失效;作品和任务保留。');
324
+ }));
325
+ $('#connection-new').addEventListener('click', () => {
326
+ if (busy || !catalog) return;
327
+ select(); editor(true); setView('settings'); feedback('已准备新连接。填写名称、选择功能和权限后,点击“创建连接”。');
328
+ const name = $('#connection-name'); name.focus({ preventScroll: true }); name.scrollIntoView({ block: 'center', behavior: 'instant' });
329
+ });
330
+ function closeEditor(discard = false) {
331
+ if (busy) return;
332
+ if (formDirty && !discard) { $('#connection-leave-confirm').hidden = false; $('#connection-leave-confirm').scrollIntoView({ block: 'center' }); $('#connection-leave-keep').focus(); return; }
333
+ clearExport(); formDirty = false; $('#connection-leave-confirm').hidden = true; editor(false); feedback(); $('#connection-new').focus();
334
+ }
335
+ $('#connection-form').addEventListener('input', () => { formDirty = true; closeButton.textContent = '取消编辑'; });
336
+ $('#connection-back').addEventListener('click', () => closeEditor());
337
+ closeButton.addEventListener('click', () => closeEditor());
338
+ $('#connection-leave-keep').addEventListener('click', () => { $('#connection-leave-confirm').hidden = true; });
339
+ $('#connection-leave-discard').addEventListener('click', () => closeEditor(true));
340
+ for (const button of document.querySelectorAll('[data-connection-view]')) button.addEventListener('click', () => setView(button.dataset.connectionView));
341
+ $('#connections-refresh').addEventListener('click', () => perform(async ctx => {
342
+ if (!catalog) { await connect(ctx.connection); return; }
343
+ const [next, saved, available] = await Promise.all([reload(ctx.connection), ctx.connection.get(BASE + '/templates'), ctx.connection.get(BASE + '/catalog')]); if (!ctx.valid()) return;
344
+ records = next; setTemplates(saved); setCatalog(available); renderTemplates(); select(selected); feedback('连接记录已刷新。');
345
+ }));
346
+ for (const id of ['connection-transport', 'connection-format', 'connection-name']) $('#' + id).addEventListener('input', clearExport);
347
+ $('#connection-transport').addEventListener('change', transportNote);
348
+ $('#connection-clear-export').addEventListener('click', clearExport);
349
+ async function connect(connection) {
350
+ const at = ++epoch; client = connection; busy = true; records = []; selected = null; editor(false); clearExport(); feedback('正在读取连接…'); controls();
351
+ try {
352
+ const [available, next, saved] = await Promise.all([connection.get(BASE + '/catalog'), connection.get(BASE + '/connections'), connection.get(BASE + '/templates')]);
353
+ if (!valid(at, connection)) return;
354
+ if (available.contractVersion !== 0 || next.contractVersion !== 0 || saved.contractVersion !== 0) throw new TypeError('不支持的连接管理版本');
355
+ setCatalog(available); records = next.connections; setTemplates(saved);
356
+ $('#connection-service-note').textContent = catalog.httpEnabled ? 'HTTP MCP 已开启 · ' + location.origin + '/mcp。创建连接会生成专属授权,不会自动配置或启动客户端。'
357
+ : '可用 stdio 接入 · 此服务未开启 HTTP MCP。创建连接不会自动配置或启动客户端。';
358
+ renderTemplates(); select(records[0]?.connectionId || null); feedback();
359
+ } catch (error) { if (valid(at, connection)) feedback(describe(error), true); }
360
+ finally { if (valid(at, connection)) { busy = false; controls(); } }
361
+ }
362
+ return Object.freeze({
363
+ connect,
364
+ show(value) { if (visible !== value) viewTicket++; visible = value; if (!value) clearExport(); },
365
+ disconnect() {
366
+ epoch++; client = null; busy = false; visible = false; catalog = null; records = []; templates = []; selected = null; editor(false); clearExport(); feedback();
367
+ formDirty = false; $('#connection-leave-confirm').hidden = true; closeButton.textContent = '返回列表';
368
+ $('#connection-template').replaceChildren(); $('#connection-template-note').replaceChildren(); $('#connection-template-note').hidden = true; $('#connection-template-file').value = '';
369
+ $('#connections-list').replaceChildren(); $('#connection-facts').replaceChildren(); $('#connection-name').value = '';
370
+ $('#connection-contribution-list').replaceChildren(); $('#connection-contributions').hidden = true;
371
+ $('#connection-service-note').textContent = '';
372
+ for (const id of ['connection-media', 'connection-scopes', 'connection-tools']) $('#' + id).replaceChildren();
373
+ $('#connection-export').hidden = true; $('#connection-revoke-section').hidden = true; $('#connection-fields').disabled = true;
374
+ $('#connections-count').textContent = '0';
375
+ }
376
+ });
377
+ }