@mokuyoaxis/dsh-iris 0.2.6 → 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.
- package/CHANGELOG.md +103 -1
- package/README.en.md +104 -304
- package/README.md +101 -302
- package/bin/dsh-iris.js +446 -64
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +7 -5
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/AUDIO_FLOWS.md +71 -0
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +99 -0
- package/docs/CORE_QUICKSTART.md +108 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +94 -0
- package/docs/DSH_COMPATIBILITY.md +14 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +40 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +12 -4
- package/docs/HOST_DOCTOR.md +13 -1
- package/docs/IMAGE_FLOW.md +75 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +62 -0
- package/docs/LOCAL_API_CONTRACT.md +140 -0
- package/docs/LOCAL_CREATION.md +56 -0
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +201 -0
- package/docs/MCP.md +191 -0
- package/docs/MUSIC.md +2 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +189 -0
- package/docs/PLUGIN_STARTUP.md +100 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
- package/docs/PROVIDER_EXTENSIONS.md +78 -0
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +28 -6
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +62 -0
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.2.7.md +62 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +132 -70
- package/lib/adapters.js +20 -11
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +213 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +434 -55
- package/lib/comfyui-adapter.js +94 -0
- package/lib/comfyui-client.js +51 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/config.js +36 -1
- package/lib/configured-image-runtime.js +50 -0
- package/lib/connection-skill.js +89 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +21 -2
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +83 -0
- package/lib/core-capability-catalog.js +182 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-maintenance.js +31 -2
- package/lib/core-runtime.js +20 -0
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +84 -13
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +133 -0
- package/lib/doctor.js +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +8 -2
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +290 -0
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +232 -0
- package/lib/index.js +94 -68
- package/lib/local-api-projection.js +32 -0
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +1083 -0
- package/lib/local-creation.js +150 -0
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +37 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +101 -0
- package/lib/mcp-server.js +47 -0
- package/lib/model-directory-service.js +16 -6
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +2 -1
- package/lib/operation-runner.js +236 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +288 -0
- package/lib/plugins.js +58 -0
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapter.js +5 -1
- package/lib/provider-adapters.js +56 -18
- package/lib/provider-catalog.js +40 -29
- package/lib/provider-config-service.js +105 -23
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +8 -1
- package/lib/provider-monitor.js +79 -0
- package/lib/provider-protocol.js +70 -20
- package/lib/provider-registry.js +118 -0
- package/lib/provider-response.js +28 -0
- package/lib/provider-settings.js +134 -0
- package/lib/provider-task-runner.js +40 -7
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +211 -0
- package/lib/vision-model-routing.js +3 -0
- package/lib/web/app.js +642 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +377 -0
- package/lib/web/creation.js +355 -0
- package/lib/web/flow-view.js +33 -0
- package/lib/web/gallery.js +132 -0
- package/lib/web/index.html +291 -0
- package/lib/web/media-client.js +204 -0
- package/lib/web/plugin-panel.js +221 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +492 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +518 -0
- package/lib/web/transport.js +96 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/lib/workbench-management.js +8 -9
- package/package.json +22 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +44 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +60 -0
- package/user_guide.md +19 -9
|
@@ -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
|
+
}
|