@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.
- package/CHANGELOG.md +143 -1
- package/README.en.md +109 -311
- package/README.md +106 -309
- package/bin/dsh-iris.js +452 -92
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +87 -0
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +101 -0
- package/docs/CORE_QUICKSTART.md +110 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +101 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +21 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +30 -5
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +42 -15
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +207 -0
- package/docs/MCP.md +212 -0
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/MUSIC.md +2 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +193 -0
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +113 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +82 -0
- package/docs/PROVIDER_HEALTH.md +5 -1
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +91 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_ARTIFACTS.md +6 -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.3.0-alpha.1.md +51 -0
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +56 -4
- package/lib/adapters.js +75 -49
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +73 -17
- package/lib/application-writer.js +193 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +402 -72
- package/lib/comfyui-adapter.js +95 -0
- package/lib/comfyui-client.js +60 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/command-service.js +18 -5
- package/lib/config.js +49 -10
- package/lib/configured-image-runtime.js +69 -0
- package/lib/connection-skill.js +98 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +84 -28
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +85 -0
- package/lib/core-capability-catalog.js +192 -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-delivery-cache.js +8 -3
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +174 -0
- package/lib/core-tasks.js +113 -21
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +41 -64
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +67 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +156 -14
- 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 +97 -31
- package/lib/index.js +47 -22
- package/lib/local-api-projection.js +15 -6
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +861 -71
- package/lib/local-creation.js +96 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +39 -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 +174 -0
- package/lib/mcp-server.js +69 -0
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +37 -10
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +6 -3
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +281 -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 +307 -0
- package/lib/plugins.js +59 -0
- package/lib/provider-adapter.js +7 -3
- package/lib/provider-adapters.js +92 -22
- package/lib/provider-catalog.js +64 -36
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +143 -31
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +18 -6
- package/lib/provider-monitor.js +81 -0
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +73 -22
- package/lib/provider-registry.js +128 -0
- package/lib/provider-response.js +38 -0
- package/lib/provider-settings.js +136 -0
- package/lib/provider-task-runner.js +132 -43
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/skill-installation.js +23 -11
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +5 -0
- package/lib/web/app.js +703 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +516 -0
- package/lib/web/creation.js +359 -0
- package/lib/web/flow-view.js +34 -0
- package/lib/web/gallery.js +133 -0
- package/lib/web/index.html +344 -0
- package/lib/web/media-client.js +218 -0
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +223 -0
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +513 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +571 -0
- package/lib/web/transport.js +101 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +3 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +24 -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 +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +5 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.intro { margin-bottom:24px; }
|
|
2
|
+
.intro h2 { font-size:24px; letter-spacing:-.6px; }
|
|
3
|
+
.board-layout { display:grid; grid-template-columns:minmax(0,1fr) 240px; gap:20px; align-items:start; }
|
|
4
|
+
.image-choices { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:12px; }
|
|
5
|
+
.image-choice { position:relative; margin:0; padding:8px; background:var(--soft); border:1px solid var(--line); border-radius:10px; cursor:pointer; }
|
|
6
|
+
.image-choice:has(input:checked) { border-color:var(--accent); background:var(--tint); }
|
|
7
|
+
.image-choice input { position:absolute; top:10px; left:10px; min-height:0; }
|
|
8
|
+
.image-choice img { display:block; width:100%; height:112px; object-fit:contain; }
|
|
9
|
+
.image-choice span { display:block; text-align:center; margin-top:6px; }
|
|
10
|
+
form button { display:block; width:100%; margin-top:12px; }
|
|
11
|
+
.board-result { margin-top:24px; border-top:1px solid var(--line); padding-top:20px; }
|
|
12
|
+
.board-result img { display:block; max-height:600px; margin:0 auto 16px; object-fit:contain; }
|
|
13
|
+
.board-result a { display:inline-block; padding:9px 14px; border:1px solid var(--line); border-radius:8px; text-decoration:none; }
|
|
14
|
+
@media(max-width:680px) { .board-layout { grid-template-columns:1fr; } form { order:-1; } }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const element = (tag, text, className) => { const node = document.createElement(tag); if (text) node.textContent = text; if (className) node.className = className; return node; };
|
|
2
|
+
|
|
3
|
+
export async function mount({ container, api, session, signal }) {
|
|
4
|
+
const service = api.pluginOperations, urls = new Set(), timers = new Set();
|
|
5
|
+
const keyName = 'iris.contact-sheet.' + session.rootId, operationId = 'community.contact-sheet.create';
|
|
6
|
+
let request, selected = [], catalog, busy = false;
|
|
7
|
+
const intro = element('div', '', 'intro'); intro.append(element('h2', '让灵感,并排相遇。'), element('p', '选择现有图片,离线排成一张对照板。原图保持不变。', 'muted'));
|
|
8
|
+
const layout = element('div', '', 'board-layout'), choices = element('div', '', 'image-choices'), editor = element('form', '', 'card');
|
|
9
|
+
choices.setAttribute('aria-label', '选择来源图片');
|
|
10
|
+
const columnsLabel = element('label', '每行图片数'), columns = document.createElement('select'); columns.name = 'columns';
|
|
11
|
+
for (let i = 1; i <= 4; i++) columns.add(new Option(String(i), String(i))); columns.value = '2'; columnsLabel.append(columns);
|
|
12
|
+
const sizeLabel = element('label', '单张边长'), size = document.createElement('select'); size.name = 'size';
|
|
13
|
+
for (const i of [128, 256, 512]) size.add(new Option(i + ' px', String(i))); size.value = '256'; sizeLabel.append(size);
|
|
14
|
+
const count = element('p', '尚未选择图片', 'field-note'), submit = element('button', '制作对照板'); submit.type = 'submit'; submit.disabled = true;
|
|
15
|
+
const fresh = element('button', '新建一张', 'secondary'); fresh.type = 'button'; fresh.hidden = true;
|
|
16
|
+
const recover = element('button', '查询原操作', 'secondary'); recover.type = 'button'; recover.hidden = true;
|
|
17
|
+
const more = element('button', '加载更多图片', 'secondary'); more.type = 'button'; more.hidden = true;
|
|
18
|
+
editor.append(columnsLabel, sizeLabel, count, submit, recover, fresh, more);
|
|
19
|
+
const status = element('p', '正在读取现有作品…', 'field-note'); status.setAttribute('role', 'status'); status.dataset.boardStatus = 'true';
|
|
20
|
+
const output = element('section', '', 'board-result'); output.hidden = true;
|
|
21
|
+
layout.append(choices, editor); container.append(intro, layout, status, output);
|
|
22
|
+
function remember(value) {
|
|
23
|
+
request = value;
|
|
24
|
+
try { value ? sessionStorage.setItem(keyName, JSON.stringify(value)) : sessionStorage.removeItem(keyName); }
|
|
25
|
+
catch (_) { status.textContent = '浏览器无法保存恢复标识;请保留本次 Operation ID。'; }
|
|
26
|
+
}
|
|
27
|
+
function controls() {
|
|
28
|
+
const locked = busy || Boolean(request);
|
|
29
|
+
submit.disabled = locked || !selected.length || !catalog?.operations.some(item => item.id === operationId && item.available);
|
|
30
|
+
columns.disabled = size.disabled = locked; more.disabled = busy;
|
|
31
|
+
for (const checkbox of choices.querySelectorAll('input')) checkbox.disabled = locked || !checkbox.checked && selected.length >= 16;
|
|
32
|
+
}
|
|
33
|
+
function note(error) { status.textContent = [error.message, error.code, error.requestId].filter(Boolean).join(' · '); }
|
|
34
|
+
async function present(operation) {
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
if (operation.action !== 'plugin.execute' || operation.operationId !== operationId) throw new Error('恢复标识不属于此对照板业务,请核对业务记录');
|
|
37
|
+
status.textContent = ({ queued: '已接收,正在等待处理', running: '正在排版', succeeded: '对照板已保存', interrupted: '处理已中断,保留原记录;不会自动重做', canceled: '已停止,原记录保留', failed: '处理未完成,请核对插件与授权' })[operation.state]
|
|
38
|
+
+ ' · ' + operation.id;
|
|
39
|
+
fresh.hidden = ['queued', 'running'].includes(operation.state);
|
|
40
|
+
recover.hidden = true;
|
|
41
|
+
if (operation.state === 'succeeded' && operation.result.artifactIds.length) {
|
|
42
|
+
const id = operation.result.artifactIds[0], blob = await api.artifact({ id, mediaType: 'image/png' }); signal.throwIfAborted();
|
|
43
|
+
const url = URL.createObjectURL(blob); urls.add(url); const image = element('img'); image.src = url; image.alt = '已保存的多图对照板';
|
|
44
|
+
const link = element('a', '下载原始 PNG'); link.href = url; link.download = 'iris-contact-sheet-' + id + '.png';
|
|
45
|
+
output.replaceChildren(image, link); output.hidden = false;
|
|
46
|
+
} else if (['queued', 'running'].includes(operation.state)) {
|
|
47
|
+
const timer = setTimeout(() => { timers.delete(timer); void poll(operation.id); }, 450); timers.add(timer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function poll(id) { try { await present((await service.inspect(id)).operation); } catch (error) { if (!signal.aborted) { note(error); recover.hidden = false; } } }
|
|
51
|
+
editor.addEventListener('submit', async event => {
|
|
52
|
+
event.preventDefault(); if (busy || request || !selected.length) return;
|
|
53
|
+
const contribution = catalog.operations.find(item => item.id === operationId && item.available); if (!contribution) return;
|
|
54
|
+
busy = true; remember({ key: crypto.randomUUID(), operationId, dependency: contribution.dependency,
|
|
55
|
+
input: { artifactIds: selected, columns: Number(columns.value), cellSize: Number(size.value) } }); controls(); status.textContent = '正在保存本次操作…';
|
|
56
|
+
try { const result = await service.execute(request); await present(result.operation); }
|
|
57
|
+
catch (error) { if (!signal.aborted) { note(error); status.textContent += '。请查询原操作,未自动重做。'; recover.hidden = false; } }
|
|
58
|
+
finally { busy = false; if (!signal.aborted) controls(); }
|
|
59
|
+
});
|
|
60
|
+
fresh.addEventListener('click', () => { remember(null); output.hidden = true; output.replaceChildren(); fresh.hidden = recover.hidden = true; status.textContent = '选择图片后可以新建一次操作。'; controls(); });
|
|
61
|
+
async function lookup() {
|
|
62
|
+
try { await present((await service.lookup(request.key)).operation); }
|
|
63
|
+
catch (error) { if (!signal.aborted) { note(error); recover.hidden = false; if (error.status === 404) fresh.hidden = false; } }
|
|
64
|
+
}
|
|
65
|
+
recover.addEventListener('click', () => { if (!busy && request) void lookup(); });
|
|
66
|
+
signal.addEventListener('abort', () => { for (const timer of timers) clearTimeout(timer); for (const url of urls) URL.revokeObjectURL(url); }, { once: true });
|
|
67
|
+
if (!service) { status.textContent = '此服务未开启插件业务执行。'; return; }
|
|
68
|
+
catalog = await service.catalog({ limit: '30' }); signal.throwIfAborted();
|
|
69
|
+
async function appendImages(page) { for (const artifact of page.artifacts) {
|
|
70
|
+
const label = element('label', '', 'image-choice'), checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = artifact.id;
|
|
71
|
+
const image = element('img'); image.alt = '来源图片 ' + artifact.id;
|
|
72
|
+
label.append(checkbox, image, element('span', artifact.id.slice(-8), 'muted')); choices.append(label);
|
|
73
|
+
const blob = await api.artifact({ id: artifact.id, mediaType: artifact.mediaType }); signal.throwIfAborted();
|
|
74
|
+
const url = URL.createObjectURL(blob); urls.add(url); image.src = url;
|
|
75
|
+
checkbox.addEventListener('change', () => { selected = checkbox.checked ? [...selected, artifact.id] : selected.filter(id => id !== artifact.id);
|
|
76
|
+
count.textContent = '已选择 ' + selected.length + ' 张 · 按选择顺序排版'; controls(); });
|
|
77
|
+
} more.hidden = page.nextOffset === null || page.nextOffset === undefined; controls(); }
|
|
78
|
+
more.addEventListener('click', async () => {
|
|
79
|
+
if (busy || catalog.nextOffset === null) return;
|
|
80
|
+
busy = true; controls();
|
|
81
|
+
try { catalog = await service.catalog({ limit: '30', offset: String(catalog.nextOffset) }); signal.throwIfAborted(); await appendImages(catalog); }
|
|
82
|
+
catch (error) { if (!signal.aborted) note(error); }
|
|
83
|
+
finally { busy = false; if (!signal.aborted) controls(); }
|
|
84
|
+
});
|
|
85
|
+
await appendImages(catalog);
|
|
86
|
+
if (!catalog.artifacts.length) choices.append(element('p', '当前没有可用图片,请先通过现有创作入口或 CLI 导入。', 'muted'));
|
|
87
|
+
status.textContent = '最多选择 16 张图片,不调用模型。';
|
|
88
|
+
try { const saved = sessionStorage.getItem(keyName); if (saved) request = JSON.parse(saved); }
|
|
89
|
+
catch (_) { status.textContent = '原恢复标识无法读取,请先核对业务记录。'; }
|
|
90
|
+
if (request) await lookup();
|
|
91
|
+
controls();
|
|
92
|
+
return () => { for (const timer of timers) clearTimeout(timer); for (const url of urls) URL.revokeObjectURL(url); container.replaceChildren(); };
|
|
93
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: iris-core
|
|
3
|
-
description: "Create, edit, and retrieve persistent Iris images through the standalone CLI without a running DSH host. Use for local
|
|
3
|
+
description: "Create, edit, and retrieve persistent Iris images through the standalone CLI without a running DSH host. Use for API images, configured ComfyUI workflows and LoRA parameters, local imports, revisions, task recovery, and artifact export."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Iris Core · 核心图片工作流
|
|
@@ -11,13 +11,17 @@ Organize the work around the image the user wants to receive. Reuse existing ass
|
|
|
11
11
|
|
|
12
12
|
Use the [standalone CLI reference](references/cli.md) to confirm the installed command, explicit data root, and private provider configuration needed for generation. Reuse information already established in the conversation. Do not scan another application's configuration or assume a personal directory, provider, or model.
|
|
13
13
|
|
|
14
|
-
A visible skill does not prove that its execution tools are available. Report a missing command, connection, or capability precisely. This
|
|
14
|
+
A visible skill does not prove that its execution tools are available. Report a missing command, connection, or capability precisely. This skill binds to the CLI. For an explicitly selected MCP connection, use its connection-specific skill and actual authorized tools; do not bypass a missing MCP capability through the CLI.
|
|
15
|
+
|
|
16
|
+
## Clarify a ComfyUI request · 先确认工作流与要求
|
|
17
|
+
|
|
18
|
+
For ComfyUI, read the [workflow, parameters, LoRA and recovery reference](references/comfyui.md). Ask a concise, bundled question for missing image requirements, workflow choice and parameter preferences before creating or executing work. Read-only catalog inspection may help the user choose. Reuse answers and authorization already given; a complete request needs no repeated questionnaire. API-only requests follow their established configuration and do not require ComfyUI setup questions.
|
|
15
19
|
|
|
16
20
|
## Plan the image work · 按作品组织步骤
|
|
17
21
|
|
|
18
22
|
- Import a local image when it needs to become a persistent source. Reuse an existing Artifact in the current root without importing it again.
|
|
19
23
|
- Generate for a new image request. For a revision, pass the source Artifact and requested changes to create a related version while retaining the original. Retrieve existing results instead of regenerating them.
|
|
20
|
-
-
|
|
24
|
+
- For ordinary API work, use the configured capability policy when it matches the user's choice. If the pool mixes API and ComfyUI, choose an actual allowed API reference explicitly to keep an API-only request on that path. ComfyUI always uses a specific workflow confirmed from the current catalog; never guess from a model family name. Names, catalog entries, and past success do not establish present availability.
|
|
21
25
|
- Add only steps needed for the requested result. An explicit edit can proceed directly; a generation request does not require extra paid vision checks or another generation.
|
|
22
26
|
- Wait for actual delivery before starting a dependent operation. When content verification is required, use an image-reading capability available in the current host. Metadata and file existence alone do not establish visual correctness.
|
|
23
27
|
|
|
@@ -37,4 +41,4 @@ Preserve the original Task after interruption or unknown acceptance; do not prob
|
|
|
37
41
|
|
|
38
42
|
Return the actual image file or a host-accessible asset, with the Task/Artifact IDs needed to continue. Distinguish availability, export, display in the conversation, and visual verification. Report partial results and unmet conditions accurately.
|
|
39
43
|
|
|
40
|
-
This
|
|
44
|
+
This skill covers images and their tasks. The ComfyUI path requires 0.3.0-alpha.1 or compatible source; stable npm 0.2.7 does not include it. Check the actual installed version. Existing DSH skills continue to guide DSH-native tools and workbench workflows.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The current executable binding is `dsh-iris`, which uses shared Application/Core operations without a running DSH host. Check the installed command with `dsh-iris --help`. In a confirmed source checkout, `node /absolute/path/to/iris/bin/dsh-iris.js` is also available. A missing command does not authorize installing software or changing package versions.
|
|
4
4
|
|
|
5
|
-
These image, artifact, and task commands exist in 0.2.6
|
|
5
|
+
These image, artifact, and task commands exist in 0.2.6; the Core skill is bundled from 0.2.7. The development [ComfyUI reference](comfyui.md) has its own version and parameter requirements. Flow commands and a workbench are not prerequisites for the standalone API image path below.
|
|
6
6
|
|
|
7
7
|
## Establish the context · 明确本次上下文
|
|
8
8
|
|
|
@@ -50,7 +50,7 @@ Write the user's original or authorized revised request into the input file, for
|
|
|
50
50
|
dsh-iris run image --data-root "$IRIS_DATA_ROOT" --provider-config "$IRIS_PROVIDER_FILE" --input-file "$IRIS_INPUT_FILE"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Set `n` to the requested count (1–4); do not increase it for testing. Supply `size` only when requested and supported.
|
|
53
|
+
Set `n` to the requested count (1–4); do not increase it for testing. Supply `size` only when requested and supported. Use the configured API policy when it matches the request; explicitly select an actual allowed API model when a mixed pool could choose ComfyUI. ComfyUI always requires the confirmed workflow and `n:1`; follow its separate reference. An explicit choice has no candidate fallback. Default candidate switching requires evidence of non-acceptance.
|
|
54
54
|
|
|
55
55
|
## Edit an existing image · 编辑已有图片
|
|
56
56
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# ComfyUI workflows · 工作流、参数与 API 搭配
|
|
2
|
+
|
|
3
|
+
This reference describes the **0.3.0-beta.1 implementation**, not stable npm 0.2.7. Confirm the installed command and returned catalog before using it; missing support does not authorize installing or upgrading software. Keep the explicit roots and private provider file from the [CLI reference](cli.md).
|
|
4
|
+
|
|
5
|
+
## Ask before creating · 提问确认需求
|
|
6
|
+
|
|
7
|
+
ComfyUI selects an executable workflow with its own exposed controls. Before creating or executing a request, establish:
|
|
8
|
+
|
|
9
|
+
- The desired subject, style, changes and output. Ask whether a source image must be preserved when the request suggests editing.
|
|
10
|
+
- The configured workflow to use, or whether the user prefers API generation or wants a recommendation from the available workflows.
|
|
11
|
+
- Any required LoRA and separate model/CLIP weights; size, seed or reproducibility requirements. Offer to keep the workflow defaults for unspecified settings; do not require the user to understand every sampler control.
|
|
12
|
+
|
|
13
|
+
Ask only for missing information, preferably in one compact question. For example: “What should the image contain and look like? Which of these configured workflows should I use? Do you need a particular LoRA, size or seed, or should I keep that workflow's defaults?” Use the user's language and real catalog choices. If the user delegates selection, choose a suitable actual workflow and state the choice and assumptions. Reuse established answers and authorization; do not ask again before each already authorized stage.
|
|
14
|
+
|
|
15
|
+
Read-only directory inspection may precede the question. While essential choices remain unanswered, do not generate a sample, create a trial Run/Task, change a provider, or install a workflow/model. A missing workflow is a configuration gap to explain, not permission to fabricate a template or silently substitute an API model.
|
|
16
|
+
|
|
17
|
+
## Read the actual directory · 使用真实目录
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dsh-iris models list --provider-config "$IRIS_PROVIDER_FILE"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Choose the returned model `ref`. A `comfyWorkflow` summary identifies the configured template; read its `parameters` for actual IDs, labels, types, defaults, ranges and declared choices. Use `imageRouting` and edit support to distinguish API models from workflows, rather than guessing from names such as SDXL or Flux. A configured entry is not proof that the service, nodes or GPU are usable.
|
|
24
|
+
|
|
25
|
+
LoRA names belong to **existing workflow slots**. Read current available choices through an explicit read-only workflow check when needed and authorized. The check queries node definitions without generating or changing configuration; it cannot establish real inference success. If options are truncated, do not claim an unseen name was verified. Ask for the intended installed name or retain the confirmed default.
|
|
26
|
+
|
|
27
|
+
Only override declared parameter IDs and supported scalar values. Keep model and CLIP weights separate, including zero/negative values when allowed; do not set them equal without the user's intent. Multiple LoRAs require existing slots in the workflow. A fixed v0 template exposes no runtime parameters. Do not invent a node, a LoRA filename, an arbitrary graph patch or an undeclared negative-prompt field. Integer seeds must fit the JavaScript safe range; keep sampler defaults unless a change is requested or delegated.
|
|
28
|
+
|
|
29
|
+
## Execute the selected path · 执行明确入口
|
|
30
|
+
|
|
31
|
+
For CLI image generation, write the approved input to a new private JSON file: actual `model_ref`, `prompt`, `n:1`, optional supported `size`, and optional `workflow_parameters` containing only requested overrides. Omit an empty parameter map. Omit size when the workflow has no size binding. Then use the existing image entry:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
dsh-iris run image --data-root "$IRIS_DATA_ROOT" --provider-config "$IRIS_PROVIDER_FILE" --input-file "$IRIS_INPUT_FILE"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
An optional static check uses a **separate** input file containing just `workflow_parameters` with the same approved overrides; omit `--input` if there are none:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
dsh-iris models workflow-check "$IRIS_MODEL_REF" --provider-config "$IRIS_PROVIDER_FILE" --input "$(cat "$IRIS_CHECK_FILE")"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The file contains data, not executable shell text. Never substitute prompt text directly into shell code. Read the check's `ok`, issues and limits; exit code 0 alone is not a passing inspection. Checking and generating are separate actions.
|
|
44
|
+
|
|
45
|
+
For persistent Flow creation, the input also uses `model_ref` and `workflow_parameters`. Text-to-image requires an explicitly initialized new v2 execution root; do not migrate an existing root. Create once, keep the returned Run, and explicitly continue it. A Flow retains private request input, while a standalone Task does not retain the prompt. Follow the invocation supported by the installed version rather than switching formats mid-task.
|
|
46
|
+
|
|
47
|
+
For an authorized MCP connection, follow its generated skill and actual tool schemas: the directory returns `modelRef`, creation uses `workflowParameters`, and creation saves a Run without generating. A missing directory or execution tool is a capability gap; do not call an unavailable tool or switch to shell commands to bypass it. Owner workflow checks are not automatically available through media MCP tools.
|
|
48
|
+
|
|
49
|
+
## Combine API and ComfyUI · 选择与串联
|
|
50
|
+
|
|
51
|
+
| User goal | Supported approach |
|
|
52
|
+
|---|---|
|
|
53
|
+
| A quick API image | Use the configured API policy or an explicit allowed API model; skip the ComfyUI questionnaire unless the request needs it |
|
|
54
|
+
| An existing workflow, LoRA or controlled seed | Confirm the actual ComfyUI workflow and exposed parameters before generation |
|
|
55
|
+
| ComfyUI base image, followed by API revision | Generate once, wait for the original Artifact, then pass that same-root Artifact to an explicitly chosen API model that supports editing |
|
|
56
|
+
| API image refined by ComfyUI | Explain that the current Iris ComfyUI adapter has no source-image upload/binding; do not claim a reverse image-edit chain works |
|
|
57
|
+
| Compare API and ComfyUI | Only when requested, plan two independent generations and their additional work/cost; keep distinct task identities, then show actual results |
|
|
58
|
+
|
|
59
|
+
For the supported ComfyUI → API chain, use the existing image-edit input: `source_artifact_id` for CLI or `sourceArtifactId` for MCP. Do not carry `workflow_parameters`/`workflowParameters` into the API edit. Both stages need their actual permissions and user intent; authorization for the whole stated chain need not be requested again per step. Keep the original image and the second stage's lineage. If stage two fails, recover stage two instead of regenerating stage one.
|
|
60
|
+
|
|
61
|
+
Do not silently switch backends as recovery. An API substitute may lose LoRA, workflow or seed semantics. If a substitute is already authorized, establish the original request's acceptance and what tradeoffs that authorization covers; unknown acceptance still requires preserving the original task and any possible duplicate work/billing. Never rebind an old Task/Run to a different model or new template.
|
|
62
|
+
|
|
63
|
+
## Recover original facts · 恢复原任务
|
|
64
|
+
|
|
65
|
+
- Standalone CLI: inspect the returned Task in its original root, then use [task recovery](tasks.md) to observe, wait or redeliver that same Task. Preserve it when acceptance is unknown; do not test by submitting again.
|
|
66
|
+
- Persistent Flow/MCP: retain Run/Operation/Task/Artifact IDs, current Run revision and original request UUIDs. Read the original records after interruption. An Operation completing does not prove the remote image completed; explicitly continue the original Run only when authorized and supported by its state. Do not apply standalone Task retry to work around a Flow binding.
|
|
67
|
+
- A lost creation reply does not justify a new key. Recover the original creation identity when the selected entry supports it; otherwise read the original records and report any remaining uncertainty before further work.
|
|
68
|
+
- Query the original remote job rather than rechecking node catalogs as a substitute for task observation. A changed template can block old-task rebinding; preserve the old record and explain the mismatch.
|
|
69
|
+
- Stopping local waiting is not confirmed remote cancellation. The current ComfyUI adapter does not send `/interrupt`. Retrieve and present the actual delivered image; visual quality requires viewing it, not just reading metadata.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Inspect the Task in its original data root first. Tasks do not persist prompts; do not reconstruct the user's input from a task summary. Inspection has no provider side effects. Observe or recover only as needed for the current request.
|
|
4
4
|
|
|
5
|
+
For ComfyUI and mixed API work, see [workflow and recovery details](comfyui.md). Keep each stage's original Task. A persistent Flow uses its original Run/Operation and binding; do not use a standalone retry to change that Flow's model or template.
|
|
6
|
+
|
|
5
7
|
| Recorded fact | Action |
|
|
6
8
|
|---|---|
|
|
7
9
|
| Accepted and still queued/running | Observe or wait for the original Task; do not create another generation |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Messia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Iris Plugin Starter · 独立插件模板
|
|
2
|
+
|
|
3
|
+
A real, offline image-board plugin: choose existing Core images, make a PNG contact sheet, recover its original Operation after restart, and optionally expose the business to an explicitly authorized MCP connection. No provider account, GPU, or extra runtime dependency is required.
|
|
4
|
+
|
|
5
|
+
This project has its own package name, plugin ID, plugin version, page and PNG renderer. Its image business delegates to Iris's **public** `@mokuyoaxis/dsh-iris/contact-sheet` export, version `0.1.0`. It is a starting point for independent plugins, not a second copy of Core or a new image-generation protocol.
|
|
6
|
+
|
|
7
|
+
## Start · 开始使用
|
|
8
|
+
|
|
9
|
+
Requires Node.js 22+ and Core `0.3.0-alpha.1` or a later compatible 0.3 build. Core beta.1 requires Node.js **22.1.0+**. Stable Iris `0.2.7` has no plugin SDK. The template is distributed with Core beta.1; the older alpha.1 package does **not** contain this directory.
|
|
10
|
+
|
|
11
|
+
Copy `templates/plugin-starter` from a current Iris checkout into a **new** directory outside that checkout. From the installed beta.1 package, copy `node_modules/@mokuyoaxis/dsh-iris/templates/plugin-starter` instead. The copied project can use the public SDK already present in alpha.1:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd /absolute/my-iris-plugin
|
|
15
|
+
npm install --save-dev --save-exact @mokuyoaxis/dsh-iris@0.3.0-beta.1
|
|
16
|
+
npm test
|
|
17
|
+
npm run setup -- --directory /absolute/new-private-demo
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`setup` requires an existing parent and a destination that does not exist, outside the plugin project. It creates a private demo directory, initializes a fresh execution root, imports two plain-colour sample PNGs, and writes absolute plugin-entry paths to `plugins.json`. Existing directories are rejected; an interrupted setup is retained for inspection, not silently retried. It prints paths and the exact startup argv, without starting a server or installing anything.
|
|
21
|
+
|
|
22
|
+
Start the workbench in the foreground from this installed project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm exec -- dsh-iris plugins inspect --plugin-config /absolute/new-private-demo/plugins.json
|
|
26
|
+
npm exec -- dsh-iris web serve --data-root /absolute/new-private-demo/core \
|
|
27
|
+
--plugin-config /absolute/new-private-demo/plugins.json --port 3182 \
|
|
28
|
+
--connection-store /absolute/new-private-demo/connections \
|
|
29
|
+
--allow-connection-management --allow-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Use the printed login URL to set your workbench password. Open **灵感对照板**, select pictures, then click **制作对照板**. This business does not need `--allow-execution`; media creation and plugin operations have separate gates. Change the port explicitly if occupied. Keep the service running while a harness uses MCP.
|
|
33
|
+
|
|
34
|
+
## MCP · 用户明确选择
|
|
35
|
+
|
|
36
|
+
Loading a package, enabling a business, and authorizing a connection are separate steps. In **连接**, create a connection, deselect unused media groups, choose `example.starter-board.create`, and grant `read` + `execute` (`stop` is optional). Export the transport and configuration format accepted by your harness, then import that configuration there. Iris does not install or scan harnesses.
|
|
37
|
+
|
|
38
|
+
After connecting, inspect `iris_core_plugin_catalog` and use its actual `dependency` pin. For this template the business input is:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"artifactIds": ["artifact_<24 lowercase hex characters>"],
|
|
43
|
+
"columns": 2,
|
|
44
|
+
"cellSize": 256
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use real image IDs from the catalog, 1–16 images in desired order, 1–4 columns and a cell size of 128–512. Call `iris_core_plugin_call` with `{ key, operationId, dependency, input }`; generate a UUID once before submitting. Reuse that UUID only with identical input. After an uncertain response, use `iris_core_lookup` first. `iris_core_operation` reads the saved state; `iris_core_image` delivers the saved PNG. The test protocol driver under `scripts/` is only an acceptance probe, not an MCP client implementation for production harnesses.
|
|
49
|
+
|
|
50
|
+
## Develop · 修改与生命周期
|
|
51
|
+
|
|
52
|
+
| File | Responsibility |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `index.mjs` | Independent identity, compatibility, declared Core commands, operation/page/renderer versions and activation |
|
|
55
|
+
| `web/board.mjs` | Shared session/API/theme, explicit submit, saved UUID lookup, listeners/timers/blob URL cleanup |
|
|
56
|
+
| `web/png.mjs` | Reads original artifact bytes through the provided API and releases its image URL on unmount |
|
|
57
|
+
| `scripts/setup.mjs` | New-directory-only initialization through the public CLI |
|
|
58
|
+
| `scripts/smoke.mjs` | Real Core/MCP acceptance using fresh, private temporary data |
|
|
59
|
+
|
|
60
|
+
Rename the package and plugin ID before sharing your own plugin. Update the IDs in `scripts/setup.mjs` and the acceptance expectations too. The page derives its operation ID from its own plugin identity; it does not hardcode a Core installation path or request an owner token. Declare every browser entry and CSS resource in the manifest. Files outside those declarations are not automatically served; keep browser modules self-contained unless a future public asset contract says otherwise.
|
|
61
|
+
|
|
62
|
+
The delegated operation declares only `artifact.read` and `artifact.import`, with `artifactIds` declared as source inputs. Core validates closed JSON input, source access, current authorization, durable execution and output registration. A direct `createCorePluginHost().call()` is not a durable Operation; use the workbench/API/MCP business route when persistence is required. The template owns no backend resources. If you add sockets, watchers or similar resources, register cleanup using `context.onDispose()` during `activate`, and make work observe its AbortSignal. Close workbenches/hosts before disposing their shared registry.
|
|
63
|
+
|
|
64
|
+
Core saves the exact plugin and contribution versions with each Operation. Treat changed business semantics as a plugin/operation version change. Upgrading the underlying contact-sheet implementation requires review; this template deliberately refuses a version other than `0.1.0`. An incompatible/missing plugin blocks new execution and cannot substitute for a saved version. History and committed image bytes remain Core-owned. Interrupted work is retained as interrupted; it is not automatically submitted again. Editing the plugin requires a normal service stop and restart; there is no hot reload.
|
|
65
|
+
|
|
66
|
+
These are trusted Node and same-origin browser modules. Shadow DOM isolates styles, not code. Load code you trust. The small Core command interface constrains business calls but does not sandbox arbitrary Node imports. No arbitrary plugin settings storage or credential-reference API is invented here.
|
|
67
|
+
|
|
68
|
+
## Acceptance · 自检范围
|
|
69
|
+
|
|
70
|
+
`npm test` creates a separate private temporary root and closes its own services. It verifies real PNG execution, explicit selection/authorization, duplicate UUID conflict, same-port restart, original bytes, dependency version change, plugin absence and reduced permissions. It prints a safe evidence path and PNG hash; temporary evidence is retained. It does not touch daily DSH/harness configuration or call a paid model. SDK-created test sessions use temporary owner tokens; ordinary CLI workbenches use password login.
|
|
71
|
+
|
|
72
|
+
The Iris repository additionally verifies packed Core and plugin archives outside the checkout and tests this page/renderer in a real browser. This does not establish compatibility with every harness, operating system or future plugin. See the public [plugin SDK](https://github.com/mokuyoaxis/dsh-iris/blob/main/docs/PLUGINS.md) and [startup guide](https://github.com/mokuyoaxis/dsh-iris/blob/main/docs/PLUGIN_STARTUP.md) for the full contracts.
|
|
73
|
+
|
|
74
|
+
To distribute your own package, review its identity/version/compatibility, replace this README, set your own repository metadata, and remove `private: true` deliberately when ready. Run `npm pack --ignore-scripts --dry-run --json`, inspect the allowlisted files, then test the real archive from another project before publishing. Keep the MIT attribution when reusing these files. Never include private demo roots or connection credentials in your package.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { definePlugin } from '@mokuyoaxis/dsh-iris/plugins';
|
|
2
|
+
import contactSheet from '@mokuyoaxis/dsh-iris/contact-sheet';
|
|
3
|
+
|
|
4
|
+
// Review the delegated business before changing persistent operation semantics.
|
|
5
|
+
// This version is independent of both the npm package and Core's version.
|
|
6
|
+
if (contactSheet.manifest.version !== '0.1.0') {
|
|
7
|
+
throw new Error('Starter expects contact-sheet 0.1.0; review its changes and this plugin version before upgrading.');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default definePlugin({
|
|
11
|
+
manifest: {
|
|
12
|
+
contractVersion: 0,
|
|
13
|
+
id: 'example.starter-board',
|
|
14
|
+
version: '0.1.0',
|
|
15
|
+
compatibility: { min: '0.3.0-alpha.1', maxExclusive: '0.4.0' },
|
|
16
|
+
contributions: {
|
|
17
|
+
operations: contactSheet.manifest.contributions.operations,
|
|
18
|
+
pages: [{ id: 'board', version: '0.1.0', title: '灵感对照板', entry: 'web/board.mjs', styles: ['web/board.css'] }],
|
|
19
|
+
renderers: [{ id: 'png', version: '0.1.0', title: '对照板预览', entry: 'web/png.mjs', styles: ['web/board.css'], mediaTypes: ['image/png'] }]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
activate(context) {
|
|
23
|
+
// Core owns execution, permissions and artifact storage. No new queue/root.
|
|
24
|
+
// If you acquire resources here, register real cleanup with context.onDispose.
|
|
25
|
+
return contactSheet.activate(context);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iris-plugin-starter-board",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "An independent Iris contact-sheet plugin using only the public SDK.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": "./index.mjs",
|
|
8
|
+
"files": ["index.mjs", "web", "assets", "scripts", "README.md", "LICENSE"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"setup": "node scripts/setup.mjs",
|
|
11
|
+
"test": "node scripts/smoke.mjs"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@mokuyoaxis/dsh-iris": ">=0.3.0-alpha.1 <0.4.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": { "node": ">=22.0.0" },
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const packageFile = fileURLToPath(import.meta.resolve('@mokuyoaxis/dsh-iris/package.json'));
|
|
8
|
+
const metadata = JSON.parse(await readFile(packageFile, 'utf8'));
|
|
9
|
+
export const coreCli = path.resolve(path.dirname(packageFile), metadata.bin['dsh-iris']);
|
|
10
|
+
const execute = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
export async function runCore(dataRoot, ...args) {
|
|
13
|
+
const { stdout } = await execute(process.execPath, [coreCli, ...args, '--data-root', dataRoot], {
|
|
14
|
+
timeout: 30_000, maxBuffer: 2 * 1024 * 1024,
|
|
15
|
+
env: { ...process.env, IRIS_IMPORT_WORKBENCH_CONFIG: '', IRIS_CORE_CONNECTION_FILE: '', IRIS_CORE_SCOPES: '' }
|
|
16
|
+
});
|
|
17
|
+
const result = JSON.parse(stdout);
|
|
18
|
+
if (result.ok === false) throw new Error('Core command failed: ' + JSON.stringify(result));
|
|
19
|
+
return result.result ?? result;
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Acceptance helper: installed public SDK + CLI + HTTP. Not plugin runtime code.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, chmod, readFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { loadCorePluginRegistry, createCorePluginRegistry, createCorePluginWorkbench, definePlugin } from '@mokuyoaxis/dsh-iris/plugins';
|
|
8
|
+
import plugin from '../index.mjs';
|
|
9
|
+
import { setup } from './setup.mjs';
|
|
10
|
+
|
|
11
|
+
export const businessId = plugin.manifest.id + '.create';
|
|
12
|
+
export async function createEnvironment() {
|
|
13
|
+
const work = await mkdtemp(path.join(os.tmpdir(), 'iris-starter-')); await chmod(work, 0o700);
|
|
14
|
+
const prepared = await setup(path.join(work, 'demo'));
|
|
15
|
+
const config = JSON.parse(await readFile(prepared.configPath, 'utf8'));
|
|
16
|
+
let api, registry, started, port;
|
|
17
|
+
async function start({ present = true, enabled = true, version } = {}) {
|
|
18
|
+
if (version) registry = await createCorePluginRegistry({ plugins: [definePlugin({
|
|
19
|
+
manifest: { ...plugin.manifest, version }, activate: context => plugin.activate(context)
|
|
20
|
+
})] });
|
|
21
|
+
else registry = await loadCorePluginRegistry({ entries: present ? [prepared.entry] : [] });
|
|
22
|
+
api = createCorePluginWorkbench({ dataRoot: prepared.dataRoot, registry,
|
|
23
|
+
allowedOperations: present && enabled ? [businessId] : [], connectionStore: prepared.connectionStore, allowMcp: true,
|
|
24
|
+
...(present && !version ? { panelContributions: config.panel } : {}) });
|
|
25
|
+
started = await api.start(port ? { port } : {}); port ??= Number(new URL(started.origin).port);
|
|
26
|
+
return started;
|
|
27
|
+
}
|
|
28
|
+
async function close() { await api?.dispose(); api = undefined; await registry?.dispose(); registry = undefined; }
|
|
29
|
+
async function raw(route, { value, key, headers, method = value === undefined ? 'GET' : 'POST' } = {}) {
|
|
30
|
+
return fetch(started.origin + route, { method, headers: {
|
|
31
|
+
Authorization: 'Bearer ' + started.accessToken, ...(value === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
32
|
+
...(key ? { 'Idempotency-Key': key } : {}), ...headers
|
|
33
|
+
}, ...(value === undefined ? {} : { body: JSON.stringify(value) }), signal: AbortSignal.timeout(20_000) });
|
|
34
|
+
}
|
|
35
|
+
async function http(route, options, expected = 200) {
|
|
36
|
+
const response = await raw(route, options), value = await response.json();
|
|
37
|
+
assert.equal(response.status, expected, JSON.stringify(value)); return value;
|
|
38
|
+
}
|
|
39
|
+
async function terminal(id) {
|
|
40
|
+
for (let i = 0; i < 300; i++) {
|
|
41
|
+
const operation = (await http('/api/v0/operations/' + id)).operation;
|
|
42
|
+
if (!['queued', 'running'].includes(operation.state)) return operation;
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
44
|
+
}
|
|
45
|
+
assert.fail('Operation did not finish');
|
|
46
|
+
}
|
|
47
|
+
const pin = () => registry.dependency('operations', businessId);
|
|
48
|
+
const request = () => ({ key: crypto.randomUUID(), operationId: businessId, dependency: pin(),
|
|
49
|
+
input: { artifactIds: prepared.artifactIds, columns: 2, cellSize: 128 } });
|
|
50
|
+
const submit = ({ key, ...value }, expected = 202) => http('/api/v0/plugin-operations', { key, value }, expected);
|
|
51
|
+
const base = '/api/v0/connection-management/connections';
|
|
52
|
+
async function connection(pins = [pin()]) {
|
|
53
|
+
const result = await http(base, { value: { key: crypto.randomUUID(), label: 'Independent starter test',
|
|
54
|
+
scopes: ['read', 'execute', 'stop'], capabilities: [], pluginOperations: pins } }, 201);
|
|
55
|
+
return (await http(base)).connections.find(item => item.connectionId === result.connectionId);
|
|
56
|
+
}
|
|
57
|
+
const exported = record => http(base + '/' + record.connectionId + '/export', { value: { transport: 'http', format: 'entry' } });
|
|
58
|
+
const update = (record, scopes) => http(base + '/' + record.connectionId + '/update', { value: {
|
|
59
|
+
expectedRevision: record.revision, label: record.label, scopes, capabilities: record.capabilities,
|
|
60
|
+
tools: record.tools, pluginOperations: record.pluginOperations
|
|
61
|
+
} });
|
|
62
|
+
return { work, prepared, config, start, close, raw, http, terminal, pin, request, submit, connection, exported, update,
|
|
63
|
+
get registry() { return registry; }, get started() { return started; },
|
|
64
|
+
templateRoot: fileURLToPath(new URL('..', import.meta.url)) };
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Test-only protocol driver. Iris itself uses the official MCP server SDK.
|
|
2
|
+
// Harnesses should import exported MCP configuration, not copy this driver.
|
|
3
|
+
export async function mcpProbe(configuration) {
|
|
4
|
+
let sequence = 0;
|
|
5
|
+
async function request(method, params, notification = false) {
|
|
6
|
+
const id = notification ? undefined : ++sequence;
|
|
7
|
+
const response = await fetch(configuration.url, { method: 'POST', headers: {
|
|
8
|
+
...configuration.headers, 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream',
|
|
9
|
+
'MCP-Protocol-Version': '2025-11-25'
|
|
10
|
+
}, body: JSON.stringify({ jsonrpc: '2.0', ...(notification ? {} : { id }), method, params }), signal: AbortSignal.timeout(20_000) });
|
|
11
|
+
const text = await response.text();
|
|
12
|
+
if (notification && response.status === 202) return;
|
|
13
|
+
const messages = response.headers.get('content-type')?.startsWith('text/event-stream')
|
|
14
|
+
? text.split('\n').filter(line => line.startsWith('data:')).map(line => JSON.parse(line.slice(5))) : [JSON.parse(text)];
|
|
15
|
+
const message = messages.find(item => item.id === id) || messages.at(-1);
|
|
16
|
+
if (!response.ok || message.error) throw new Error(message.error?.message || 'MCP HTTP ' + response.status);
|
|
17
|
+
return message.result;
|
|
18
|
+
}
|
|
19
|
+
await request('initialize', { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'iris-plugin-starter-test', version: '0.1.0' } });
|
|
20
|
+
await request('notifications/initialized', {}, true);
|
|
21
|
+
return { request, call: (name, args) => request('tools/call', { name, arguments: args }) };
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { coreCli, runCore } from './core-cli.mjs';
|
|
5
|
+
|
|
6
|
+
export async function setup(directory) {
|
|
7
|
+
if (!path.isAbsolute(directory)) throw new Error('--directory must be an absolute path to a new directory.');
|
|
8
|
+
const projectRoot = fileURLToPath(new URL('..', import.meta.url)), relative = path.relative(projectRoot, directory);
|
|
9
|
+
if (relative === '' || !relative.startsWith('..' + path.sep) && relative !== '..' && !path.isAbsolute(relative)) {
|
|
10
|
+
throw new Error('Demo data must be outside the plugin project so it cannot enter its npm package.');
|
|
11
|
+
}
|
|
12
|
+
// EEXIST deliberately rejects existing user data, including empty directories.
|
|
13
|
+
await mkdir(directory, { mode: 0o700 });
|
|
14
|
+
const dataRoot = path.join(directory, 'core'), connectionStore = path.join(directory, 'connections');
|
|
15
|
+
const entry = fileURLToPath(new URL('../index.mjs', import.meta.url));
|
|
16
|
+
const config = {
|
|
17
|
+
schemaVersion: 0, entries: [entry], operations: ['example.starter-board.create'],
|
|
18
|
+
panel: { pages: ['example.starter-board.board'], renderers: ['example.starter-board.png'] }
|
|
19
|
+
};
|
|
20
|
+
const configPath = path.join(directory, 'plugins.json');
|
|
21
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
22
|
+
await runCore(dataRoot, 'flow', 'init');
|
|
23
|
+
await runCore(dataRoot, 'execution', 'init');
|
|
24
|
+
const artifactIds = [];
|
|
25
|
+
for (const name of ['violet.png', 'peach.png']) {
|
|
26
|
+
const imagePath = fileURLToPath(new URL('../assets/' + name, import.meta.url));
|
|
27
|
+
const result = await runCore(dataRoot, 'artifact', 'import', '--input', JSON.stringify({ image_path: imagePath }));
|
|
28
|
+
artifactIds.push(result.artifact.id);
|
|
29
|
+
}
|
|
30
|
+
return { directory, dataRoot, connectionStore, configPath, entry, artifactIds };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
34
|
+
if (process.argv.length !== 4 || process.argv[2] !== '--directory') {
|
|
35
|
+
throw new Error('Usage: npm run setup -- --directory /absolute/new-private-directory');
|
|
36
|
+
}
|
|
37
|
+
const result = await setup(process.argv[3]);
|
|
38
|
+
// Exact argv avoids shell quoting assumptions; no server or browser is started.
|
|
39
|
+
console.log(JSON.stringify({ ...result, startArgv: [process.execPath, coreCli, 'web', 'serve',
|
|
40
|
+
'--data-root', result.dataRoot, '--plugin-config', result.configPath,
|
|
41
|
+
'--connection-store', result.connectionStore, '--allow-connection-management', '--allow-mcp', '--port', '3182'] }, null, 2));
|
|
42
|
+
}
|