@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,76 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createEnvironment, businessId } from './environment.mjs';
|
|
5
|
+
import { setup } from './setup.mjs';
|
|
6
|
+
import { mcpProbe } from './mcp-probe.mjs';
|
|
7
|
+
|
|
8
|
+
const env = await createEnvironment(), checks = [];
|
|
9
|
+
const parsed = result => { assert(!result.isError, JSON.stringify(result)); return result.structuredContent ?? JSON.parse(result.content.find(item => item.type === 'text').text); };
|
|
10
|
+
try {
|
|
11
|
+
const configBefore = await readFile(env.prepared.configPath);
|
|
12
|
+
await assert.rejects(setup(env.prepared.directory), { code: 'EEXIST' });
|
|
13
|
+
await assert.rejects(setup(env.templateRoot + '/private-demo'), /outside the plugin project/);
|
|
14
|
+
assert.deepEqual(await readFile(env.prepared.configPath), configBefore);
|
|
15
|
+
checks.push('setup rejects an existing directory without replacing its configuration');
|
|
16
|
+
await env.start({ enabled: false });
|
|
17
|
+
assert.equal((await env.http('/api/v0/connection-management/connections')).connections.length, 0);
|
|
18
|
+
const call = env.request(); await env.submit(call, 403);
|
|
19
|
+
checks.push('loading the package grants no connection access; a disabled business cannot execute');
|
|
20
|
+
await env.close(); await env.start();
|
|
21
|
+
const session = await env.http('/api/v0/session');
|
|
22
|
+
assert.equal(session.workbenchMediaReadOnly, true);
|
|
23
|
+
assert.equal(session.panelContributions.pages[0].dependency.pluginId, 'example.starter-board');
|
|
24
|
+
assert.equal(session.panelContributions.renderers[0].id, 'example.starter-board.png');
|
|
25
|
+
for (const item of [...session.panelContributions.pages, ...session.panelContributions.renderers]) {
|
|
26
|
+
for (const route of [item.entry, ...item.styles]) assert.equal((await fetch(env.started.origin + route)).status, 200);
|
|
27
|
+
}
|
|
28
|
+
checks.push('the independent page and PNG renderer are actually served from selected, pinned package assets');
|
|
29
|
+
const unselected = await env.connection([]);
|
|
30
|
+
const without = await mcpProbe((await env.exported(unselected)).configuration);
|
|
31
|
+
assert(!(await without.request('tools/list', {})).tools.some(item => item.name === 'iris_core_plugin_call'));
|
|
32
|
+
const record = await env.connection(), config = (await env.exported(record)).configuration;
|
|
33
|
+
let client = await mcpProbe(config);
|
|
34
|
+
const names = (await client.request('tools/list', {})).tools.map(item => item.name);
|
|
35
|
+
assert(names.includes('iris_core_plugin_call')); assert(!names.includes('iris_core_continue'));
|
|
36
|
+
const catalog = parsed(await client.call('iris_core_plugin_catalog', {}));
|
|
37
|
+
assert.equal(catalog.operations[0].id, businessId);
|
|
38
|
+
const accepted = parsed(await client.call('iris_core_plugin_call', call));
|
|
39
|
+
const operation = await env.terminal(accepted.operation.id); assert.equal(operation.state, 'succeeded', JSON.stringify(operation));
|
|
40
|
+
assert.deepEqual(operation.sourceArtifactIds, env.prepared.artifactIds);
|
|
41
|
+
const outputId = operation.result.artifactIds[0];
|
|
42
|
+
const bytes = Buffer.from((await client.call('iris_core_image', { artifactId: outputId })).content.find(item => item.type === 'image').data, 'base64');
|
|
43
|
+
assert.deepEqual([...bytes.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]);
|
|
44
|
+
assert.equal(bytes.readUInt32BE(16), 292); assert.equal(bytes.readUInt32BE(20), 152);
|
|
45
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
46
|
+
checks.push('a user-selected, business-only connection executes the real PNG operation through formal HTTP MCP');
|
|
47
|
+
assert.equal(parsed(await client.call('iris_core_plugin_call', call)).operation.id, operation.id);
|
|
48
|
+
await env.submit({ ...call, input: { ...call.input, columns: 1 } }, 409);
|
|
49
|
+
checks.push('same UUID returns the saved Operation; changed input under that UUID is rejected');
|
|
50
|
+
await env.close(); await env.start();
|
|
51
|
+
assert.deepEqual((await env.exported(record)).configuration, config);
|
|
52
|
+
client = await mcpProbe(config);
|
|
53
|
+
assert.deepEqual(parsed(await client.call('iris_core_operation', { operationId: operation.id })).operation, operation);
|
|
54
|
+
assert.equal(parsed(await client.call('iris_core_lookup', { key: call.key })).operation.id, operation.id);
|
|
55
|
+
assert.equal((await env.http('/api/v0/plugin-operations/catalog')).artifacts.length, 3);
|
|
56
|
+
const restored = Buffer.from((await client.call('iris_core_image', { artifactId: outputId })).content.find(item => item.type === 'image').data, 'base64');
|
|
57
|
+
assert.deepEqual(restored, bytes);
|
|
58
|
+
checks.push('restart preserves the original connection, UUID, exact Operation and PNG bytes without another business execution');
|
|
59
|
+
await env.update(record, ['read']);
|
|
60
|
+
client = await mcpProbe(config);
|
|
61
|
+
assert(!(await client.request('tools/list', {})).tools.some(item => item.name === 'iris_core_plugin_call'));
|
|
62
|
+
assert.equal(parsed(await client.call('iris_core_operation', { operationId: operation.id })).operation.state, 'succeeded');
|
|
63
|
+
await assert.rejects(client.call('iris_core_plugin_call', { ...call, key: crypto.randomUUID() }), /Tool iris_core_plugin_call not found/);
|
|
64
|
+
checks.push('reduced authority removes execution tools and rejects explicit calls while retaining granted history reads');
|
|
65
|
+
await env.close(); await env.start({ version: '0.1.1' });
|
|
66
|
+
await env.submit({ ...call, key: crypto.randomUUID() }, 409);
|
|
67
|
+
assert.deepEqual((await env.http('/api/v0/operations/' + operation.id)).operation, operation);
|
|
68
|
+
checks.push('a different installed version cannot replace a saved exact plugin dependency');
|
|
69
|
+
await env.close(); await env.start({ present: false });
|
|
70
|
+
assert.deepEqual((await env.http('/api/v0/operations/' + operation.id)).operation, operation);
|
|
71
|
+
assert.deepEqual((await env.http('/api/v0/plugin-operations/catalog')).operations, []);
|
|
72
|
+
assert.deepEqual(Buffer.from(await (await env.raw('/api/v0/artifacts/' + outputId + '/content')).arrayBuffer()), bytes);
|
|
73
|
+
await env.submit({ ...call, key: crypto.randomUUID() }, 403);
|
|
74
|
+
checks.push('missing implementation blocks new execution but keeps Core history and original artifact readable');
|
|
75
|
+
console.log(JSON.stringify({ ok: true, checks, work: env.work, operationId: operation.id, artifactId: outputId, sha256: digest }, null, 2));
|
|
76
|
+
} finally { await env.close(); }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.starter-intro { margin-bottom:24px; }
|
|
2
|
+
.starter-eyebrow { color:var(--accent); font-size:11px; letter-spacing:1.5px; }
|
|
3
|
+
.starter-intro h2 { font-size:clamp(21px,3vw,28px); letter-spacing:-.6px; margin:8px 0; }
|
|
4
|
+
.starter-intro::after { content:""; display:block; height:3px; width:80px; border-radius:9px; margin-top:20px; background:linear-gradient(105deg,#45317D,#6F69B8,#BB78AD,#E59B76,#ED9C91,#F0D09C); }
|
|
5
|
+
.muted { color:var(--muted); font-size:13px; line-height:1.6; }
|
|
6
|
+
.starter-layout { display:grid; grid-template-columns:minmax(0,1fr) 220px; gap:20px; align-items:start; }
|
|
7
|
+
.starter-choices { display:grid; grid-template-columns:repeat(auto-fill,minmax(110px,1fr)); gap:12px; min-width:0; }
|
|
8
|
+
.starter-choice { position:relative; padding:10px; border:1px solid var(--line); border-radius:12px; background:var(--soft); cursor:pointer; }
|
|
9
|
+
.starter-choice:has(input:checked) { border-color:var(--accent); background:var(--tint); }
|
|
10
|
+
.starter-choice input { position:absolute; top:10px; left:10px; }
|
|
11
|
+
.starter-choice img { display:block; width:100%; height:120px; object-fit:contain; border-radius:6px; }
|
|
12
|
+
.starter-choice span { display:block; text-align:center; margin-top:8px; overflow-wrap:anywhere; }
|
|
13
|
+
.starter-editor { padding:18px; border:1px solid var(--line); border-radius:14px; background:var(--soft); }
|
|
14
|
+
.starter-editor label { display:block; font-size:13px; margin-bottom:14px; }
|
|
15
|
+
.starter-editor select { display:block; width:100%; margin-top:6px; min-height:40px; padding:6px; border:1px solid var(--line); border-radius:8px; color:var(--ink); background:var(--panel); }
|
|
16
|
+
.starter-editor button { display:block; width:100%; min-height:42px; margin:10px 0; border:0; border-radius:8px; padding:9px; background:var(--accent); color:var(--on-accent); cursor:pointer; }
|
|
17
|
+
.starter-editor button:disabled { opacity:.5; cursor:default; }
|
|
18
|
+
.starter-editor .secondary { border:1px solid var(--line); background:transparent; color:var(--ink); }
|
|
19
|
+
.starter-editor details { margin-top:18px; font-size:13px; }
|
|
20
|
+
.starter-editor summary { cursor:pointer; }
|
|
21
|
+
.starter-status { font-size:13px; line-height:1.8; overflow-wrap:anywhere; margin:20px 0; }
|
|
22
|
+
.starter-result { border-top:1px solid var(--line); padding-top:20px; }
|
|
23
|
+
.starter-result img, .starter-preview img { display:block; max-width:100%; max-height:600px; object-fit:contain; margin:0 auto 16px; }
|
|
24
|
+
.starter-result a { display:inline-block; padding:10px 14px; border:1px solid var(--line); border-radius:8px; color:var(--accent); }
|
|
25
|
+
:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
|
|
26
|
+
[hidden] { display:none !important; }
|
|
27
|
+
@media(max-width:680px) { .starter-layout { grid-template-columns:1fr; } .starter-editor { order:-1; } }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const element = (tag, text, className) => {
|
|
2
|
+
const node = document.createElement(tag);
|
|
3
|
+
if (text) node.textContent = text;
|
|
4
|
+
if (className) node.className = className;
|
|
5
|
+
return node;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function mount({ container, api, session, dependency, signal: parentSignal, theme }) {
|
|
9
|
+
const lifetime = new AbortController(), signal = AbortSignal.any([parentSignal, lifetime.signal]);
|
|
10
|
+
const service = api.pluginOperations, urls = new Set();
|
|
11
|
+
const operationId = dependency.pluginId + '.create';
|
|
12
|
+
const storageKey = operationId + ':' + session.rootId;
|
|
13
|
+
let receipt, selected = [], catalog, busy = false, timer, renderedId, unsubscribe, closed = false;
|
|
14
|
+
function cleanup() {
|
|
15
|
+
if (closed) return;
|
|
16
|
+
closed = true; lifetime.abort(); clearTimeout(timer); unsubscribe?.();
|
|
17
|
+
for (const url of urls) URL.revokeObjectURL(url);
|
|
18
|
+
urls.clear(); container.replaceChildren();
|
|
19
|
+
}
|
|
20
|
+
signal.addEventListener('abort', cleanup, { once: true });
|
|
21
|
+
const intro = element('header', '', 'starter-intro');
|
|
22
|
+
intro.append(element('span', 'LOCAL · NO MODEL CALLS', 'starter-eyebrow'), element('h2', '让灵感,并排相遇。'),
|
|
23
|
+
element('p', '选择图片,制作一张属于你的灵感对照板。', 'muted'));
|
|
24
|
+
const layout = element('div', '', 'starter-layout'), choices = element('div', '', 'starter-choices');
|
|
25
|
+
choices.setAttribute('aria-label', '选择来源图片');
|
|
26
|
+
const editor = element('form', '', 'starter-editor');
|
|
27
|
+
const columnsLabel = element('label', '每行图片数'), columns = document.createElement('select'); columns.name = 'columns';
|
|
28
|
+
for (let i = 1; i <= 4; i++) columns.add(new Option(String(i), String(i)));
|
|
29
|
+
columns.value = '2'; columnsLabel.append(columns);
|
|
30
|
+
const sizeLabel = element('label', '单张边长'), size = document.createElement('select'); size.name = 'size';
|
|
31
|
+
for (const n of [128, 256, 512]) size.add(new Option(n + ' px', String(n)));
|
|
32
|
+
size.value = '256'; sizeLabel.append(size);
|
|
33
|
+
const count = element('p', '尚未选择图片', 'muted'), submit = element('button', '制作对照板');
|
|
34
|
+
submit.type = 'submit'; submit.disabled = true;
|
|
35
|
+
const recover = element('button', '查询原操作', 'secondary'); recover.type = 'button'; recover.hidden = true;
|
|
36
|
+
const fresh = element('button', '新建一张', 'secondary'); fresh.type = 'button'; fresh.hidden = true;
|
|
37
|
+
const more = element('button', '加载更多图片', 'secondary'); more.type = 'button'; more.hidden = true;
|
|
38
|
+
const details = element('details'), summary = element('summary', '使用说明');
|
|
39
|
+
details.append(summary, element('p', '按选择顺序排版,最多 16 张。作品保存在 Core;刷新只查询原操作,不会自动重新制作。', 'muted'));
|
|
40
|
+
editor.append(columnsLabel, sizeLabel, count, submit, recover, fresh, more, details);
|
|
41
|
+
const status = element('p', '正在读取现有图片…', 'starter-status');
|
|
42
|
+
status.setAttribute('role', 'status');
|
|
43
|
+
const output = element('section', '', 'starter-result'); output.hidden = true;
|
|
44
|
+
layout.append(choices, editor); container.append(intro, layout, status, output);
|
|
45
|
+
function remember(value) {
|
|
46
|
+
value ? sessionStorage.setItem(storageKey, JSON.stringify(value)) : sessionStorage.removeItem(storageKey);
|
|
47
|
+
receipt = value;
|
|
48
|
+
}
|
|
49
|
+
function controls() {
|
|
50
|
+
const locked = busy || Boolean(receipt);
|
|
51
|
+
submit.disabled = locked || !selected.length || !catalog?.operations.some(item => item.id === operationId && item.available);
|
|
52
|
+
columns.disabled = size.disabled = locked; more.disabled = busy;
|
|
53
|
+
for (const checkbox of choices.querySelectorAll('input')) checkbox.disabled = locked || !checkbox.checked && selected.length >= 16;
|
|
54
|
+
}
|
|
55
|
+
function note(error) { status.textContent = [error.message, error.code, error.requestId].filter(Boolean).join(' · '); }
|
|
56
|
+
async function present(operation) {
|
|
57
|
+
signal.throwIfAborted();
|
|
58
|
+
if (operation.action !== 'plugin.execute' || operation.operationId !== operationId) throw new Error('此恢复标识属于另一项业务,请到业务记录核对。');
|
|
59
|
+
status.textContent = ({ queued: '已接收,等待处理', running: '正在排版', succeeded: '对照板已保存',
|
|
60
|
+
interrupted: '操作已中断,不会自动重做', canceled: '已停止,记录保留', failed: '操作未完成,请查看业务记录' })[operation.state] + ' · ' + operation.id;
|
|
61
|
+
fresh.hidden = ['queued', 'running'].includes(operation.state); recover.hidden = true;
|
|
62
|
+
if (operation.state === 'succeeded' && renderedId !== operation.id) {
|
|
63
|
+
const id = operation.result.artifactIds[0];
|
|
64
|
+
const blob = await api.artifact({ id, mediaType: 'image/png' }, { signal }); signal.throwIfAborted();
|
|
65
|
+
const url = URL.createObjectURL(blob); urls.add(url);
|
|
66
|
+
const image = element('img'); image.src = url; image.alt = '已保存的灵感对照板';
|
|
67
|
+
const link = element('a', '下载原始 PNG'); link.href = url; link.download = 'iris-board-' + id + '.png';
|
|
68
|
+
output.replaceChildren(image, link); output.hidden = false; renderedId = operation.id;
|
|
69
|
+
} else if (['queued', 'running'].includes(operation.state)) {
|
|
70
|
+
clearTimeout(timer); timer = setTimeout(() => { void poll(operation.id); }, 450);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function poll(id) {
|
|
74
|
+
try { await present((await service.inspect(id, { signal })).operation); }
|
|
75
|
+
catch (error) { if (!signal.aborted) { note(error); recover.hidden = false; } }
|
|
76
|
+
}
|
|
77
|
+
async function lookup() {
|
|
78
|
+
if (busy || !receipt) return;
|
|
79
|
+
busy = true; controls();
|
|
80
|
+
try { await present((await service.lookup(receipt.key, { signal })).operation); }
|
|
81
|
+
catch (error) { if (!signal.aborted) { note(error); recover.hidden = false; if (error.status === 404) fresh.hidden = false; } }
|
|
82
|
+
finally { busy = false; if (!signal.aborted) controls(); }
|
|
83
|
+
}
|
|
84
|
+
editor.addEventListener('submit', async event => {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
if (busy || receipt || !selected.length) return;
|
|
87
|
+
const contribution = catalog.operations.find(item => item.id === operationId && item.available);
|
|
88
|
+
if (!contribution) return;
|
|
89
|
+
busy = true;
|
|
90
|
+
// Save the UUID before submitting. Recovery only looks up this UUID.
|
|
91
|
+
try { remember({ key: crypto.randomUUID() }); }
|
|
92
|
+
catch { busy = false; status.textContent = '浏览器无法保存恢复标识,本次未提交。请允许会话存储后再试。'; controls(); return; }
|
|
93
|
+
controls();
|
|
94
|
+
status.textContent = '正在保存本次操作 · ' + receipt.key;
|
|
95
|
+
try {
|
|
96
|
+
const accepted = await service.execute({ key: receipt.key, operationId, dependency: contribution.dependency,
|
|
97
|
+
input: { artifactIds: [...selected], columns: Number(columns.value), cellSize: Number(size.value) } }, { signal });
|
|
98
|
+
await present(accepted.operation);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!signal.aborted) { note(error); status.textContent += '。请查询原操作;未自动重做。'; recover.hidden = false; }
|
|
101
|
+
} finally { busy = false; if (!signal.aborted) controls(); }
|
|
102
|
+
}, { signal });
|
|
103
|
+
recover.addEventListener('click', () => { void lookup(); }, { signal });
|
|
104
|
+
fresh.addEventListener('click', () => {
|
|
105
|
+
if (busy) return;
|
|
106
|
+
try { remember(null); }
|
|
107
|
+
catch { status.textContent = '无法清除恢复标识,原操作保留;请先检查浏览器存储。'; return; }
|
|
108
|
+
renderedId = undefined; output.hidden = true; output.replaceChildren();
|
|
109
|
+
// Only keep URLs that still back source thumbnails.
|
|
110
|
+
const thumbnails = new Set([...choices.querySelectorAll('img')].map(image => image.src));
|
|
111
|
+
for (const url of urls) if (!thumbnails.has(url)) { URL.revokeObjectURL(url); urls.delete(url); }
|
|
112
|
+
fresh.hidden = recover.hidden = true; status.textContent = '选择图片后,可以新建一次操作。'; controls();
|
|
113
|
+
}, { signal });
|
|
114
|
+
async function appendImages(page) {
|
|
115
|
+
for (const artifact of page.artifacts) {
|
|
116
|
+
const label = element('label', '', 'starter-choice'), checkbox = document.createElement('input');
|
|
117
|
+
checkbox.type = 'checkbox'; checkbox.value = artifact.id;
|
|
118
|
+
const image = element('img'); image.alt = '来源图片 ' + artifact.id;
|
|
119
|
+
label.append(checkbox, image, element('span', artifact.id.slice(-8), 'muted')); choices.append(label);
|
|
120
|
+
const blob = await api.artifact({ id: artifact.id, mediaType: artifact.mediaType }, { signal }); signal.throwIfAborted();
|
|
121
|
+
const url = URL.createObjectURL(blob); urls.add(url); image.src = url;
|
|
122
|
+
checkbox.addEventListener('change', () => {
|
|
123
|
+
selected = checkbox.checked ? [...selected, artifact.id] : selected.filter(id => id !== artifact.id);
|
|
124
|
+
count.textContent = '已选择 ' + selected.length + ' 张 · 按选择顺序排版'; controls();
|
|
125
|
+
}, { signal });
|
|
126
|
+
}
|
|
127
|
+
more.hidden = page.nextOffset === null || page.nextOffset === undefined; controls();
|
|
128
|
+
}
|
|
129
|
+
more.addEventListener('click', async () => {
|
|
130
|
+
if (busy || catalog.nextOffset === null) return;
|
|
131
|
+
busy = true; controls();
|
|
132
|
+
try {
|
|
133
|
+
catalog = await service.catalog({ limit: '30', offset: String(catalog.nextOffset) }, { signal });
|
|
134
|
+
signal.throwIfAborted(); await appendImages(catalog);
|
|
135
|
+
} catch (error) { if (!signal.aborted) note(error); }
|
|
136
|
+
finally { busy = false; if (!signal.aborted) controls(); }
|
|
137
|
+
}, { signal });
|
|
138
|
+
try {
|
|
139
|
+
signal.throwIfAborted();
|
|
140
|
+
const updateTheme = value => { container.dataset.theme = value.mode; };
|
|
141
|
+
updateTheme(theme.current()); unsubscribe = theme.subscribe(updateTheme);
|
|
142
|
+
if (!service) { status.textContent = '此服务尚未启用插件业务执行,请核对启动配置。'; return cleanup; }
|
|
143
|
+
catalog = await service.catalog({ limit: '30' }, { signal }); signal.throwIfAborted();
|
|
144
|
+
await appendImages(catalog);
|
|
145
|
+
status.textContent = catalog.artifacts.length ? '选择图片即可开始,不调用模型。' : '还没有图片。请先通过创作页或 CLI 导入。';
|
|
146
|
+
let saved;
|
|
147
|
+
try { saved = sessionStorage.getItem(storageKey); }
|
|
148
|
+
catch { status.textContent = '无法读取浏览器恢复标识,请到业务记录查看已有操作。'; }
|
|
149
|
+
if (saved) {
|
|
150
|
+
try {
|
|
151
|
+
receipt = JSON.parse(saved);
|
|
152
|
+
if (!receipt || !/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(receipt.key)) throw new Error('invalid receipt');
|
|
153
|
+
} catch {
|
|
154
|
+
receipt = { invalid: true }; status.textContent = '浏览器恢复标识损坏,请核对业务记录后点击新建。'; fresh.hidden = false;
|
|
155
|
+
}
|
|
156
|
+
if (!receipt.invalid) await lookup();
|
|
157
|
+
}
|
|
158
|
+
controls(); return cleanup;
|
|
159
|
+
} catch (error) { cleanup(); throw error; }
|
|
160
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function mount({ container, artifact, api, signal: parentSignal, theme }) {
|
|
2
|
+
const lifetime = new AbortController(), signal = AbortSignal.any([parentSignal, lifetime.signal]);
|
|
3
|
+
let url, unsubscribe, closed = false;
|
|
4
|
+
const image = new Image(); image.alt = 'Core 中保存的原始 PNG';
|
|
5
|
+
function cleanup() {
|
|
6
|
+
if (closed) return;
|
|
7
|
+
closed = true; lifetime.abort(); unsubscribe?.();
|
|
8
|
+
if (url) URL.revokeObjectURL(url);
|
|
9
|
+
image.removeAttribute('src'); container.replaceChildren();
|
|
10
|
+
}
|
|
11
|
+
signal.addEventListener('abort', cleanup, { once: true });
|
|
12
|
+
try {
|
|
13
|
+
const bytes = await api.artifact(artifact, { signal }); signal.throwIfAborted();
|
|
14
|
+
url = URL.createObjectURL(bytes); image.src = url;
|
|
15
|
+
await image.decode(); signal.throwIfAborted();
|
|
16
|
+
const caption = document.createElement('p'); caption.className = 'muted';
|
|
17
|
+
caption.textContent = image.naturalWidth + ' × ' + image.naturalHeight + ' · ' + bytes.size.toLocaleString() + ' bytes';
|
|
18
|
+
const update = value => { container.dataset.theme = value.mode; };
|
|
19
|
+
update(theme.current()); unsubscribe = theme.subscribe(update);
|
|
20
|
+
container.classList.add('starter-preview'); container.append(image, caption);
|
|
21
|
+
return cleanup;
|
|
22
|
+
} catch (error) { cleanup(); throw error; }
|
|
23
|
+
}
|
|
@@ -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,50 @@
|
|
|
1
|
+
# Wan Provider starter / 万相供应商模板
|
|
2
|
+
|
|
3
|
+
A copyable, independent Provider plugin using only `@mokuyoaxis/dsh-iris/plugins` and the public `package.json` export to locate the CLI. It implements the Wan 2.6 asynchronous image protocol itself. It does not call Iris's private transports or duplicate its Task store, queue, authentication or MCP server.
|
|
4
|
+
|
|
5
|
+
This template requires **Core 0.3.0-beta.1 and Provider factory context v1**. It checks `PROVIDER_FACTORY_CONTEXT_VERSION === 1` before setup. Install the exact beta package or a verified compatible Core archive. The older alpha.1 package fails with an explicit feature error and creates no data.
|
|
6
|
+
|
|
7
|
+
## Copy and check
|
|
8
|
+
|
|
9
|
+
Copy this entire directory outside the Iris repository into a new project, then install the explicitly chosen Core archive in that project:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --save-exact @mokuyoaxis/dsh-iris@0.3.0-beta.1
|
|
13
|
+
node scripts/protocol-smoke.mjs
|
|
14
|
+
npm test
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
These tests use synthetic credentials and loopback HTTP services only. They initialize new private temporary roots, run the actual installed Core CLI and formal HTTP MCP, and preserve their evidence directories. No provider account, paid call or harness configuration is involved. Archive acceptance separately checks the same template as its own npm tarball. The template has no runtime dependency beyond its Core peer and Node 22.1.0+.
|
|
18
|
+
|
|
19
|
+
`private: true` prevents accidentally publishing this example. Before publishing your own plugin, choose your package/plugin/contribution IDs, review its implementation and compatibility, update its independent versions, then deliberately remove that flag. The local `npm pack --ignore-scripts` acceptance does not publish anything.
|
|
20
|
+
|
|
21
|
+
## Configure a new workspace
|
|
22
|
+
|
|
23
|
+
Provide the correct workspace/region API **root** from your own account. Do not append `/api/v1` or `/compatible-mode/v1`. Export your credential through your normal secure environment mechanism before starting Core; do not paste the key into a command argument or this project. Setup receives only the environment variable's **name**:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run setup -- \
|
|
27
|
+
--directory /absolute/path/to/new-private-wan-workspace \
|
|
28
|
+
--endpoint https://YOUR-WORKSPACE-API-ROOT \
|
|
29
|
+
--credential-env YOUR_WAN_KEY_VARIABLE
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The parent directory must already exist. Setup refuses any existing target, even an empty directory, and refuses data inside the plugin project. It writes private Provider/startup files, initializes a new image-generation execution root through the public CLI, and prints exact `startArgv`. It never reads the key, starts a server, imports another harness's settings or submits a generation. Failed setup leaves its partial directory for inspection.
|
|
33
|
+
|
|
34
|
+
Run the printed argv to open Core Web. The default Web login uses Core's normal first-use access passphrase flow. The contributed protocol appears in Provider settings. Read its actual model directory, review the result, explicitly save the supported model and select its image-generation policy. Setup leaves the model list empty and image generation disabled until that selection. Discovering a directory grants no execution authority.
|
|
35
|
+
|
|
36
|
+
In Connections, select **image generation** and the required read/create/execute/stop permissions, then export the ordinary MCP configuration for your client. This Provider reuses the Core image tools; installing it does not add an independent MCP tool or grant a connection new rights. Core owns execution and artifacts. Keep the plugin installed at the configured path and its version unchanged while recovering its original tasks.
|
|
37
|
+
|
|
38
|
+
## Supported protocol and limits
|
|
39
|
+
|
|
40
|
+
- Identity: `example.wan-image` 0.1.0, contribution `example.wan-image.images` 0.1.0; Plugin/Adapter contract v0 plus factory context v1. Rename these together for your own implementation.
|
|
41
|
+
- `GET /api/v1/models?capabilities=IG&page_no=…&page_size=100`: validate a complete, stable directory of at most 1,000 rows, then expose only actually returned `wan2.6-t2i`. A recognized ID is an implementation boundary, not a fabricated model pool or proof of account availability.
|
|
42
|
+
- `POST /api/v1/services/aigc/image-generation/generation`: `X-DashScope-Async: enable`, `messages` text input, explicit `n` defaulting to 1, optional `width*height`. This is the Wan 2.6 format, not the older Wan 2.5 endpoint. Core's image Run currently requests one image; the adapter accepts 1–4 for other public consumers.
|
|
43
|
+
- Poll only the original `/api/v1/tasks/{task_id}`. Missing submission identity or malformed/oversized submission responses mean **unknown acceptance**. Never retry a POST or switch a model/exit automatically after an ambiguous submission.
|
|
44
|
+
- Download PNG results, at most 8 MiB each, into the exact new staging path supplied by Core. API requests refuse redirects; signed CDN downloads follow redirects through the same borrowed Core network port and receive no API Authorization header. JSON responses are limited to 1 MiB. Upstream details are not reflected into safe errors.
|
|
45
|
+
- No source upload, image edit, remote cancellation or unsupported model fallback. Stopping local waiting does not prove remote cancellation or stopped billing. Remote task IDs/result URLs have the vendor's limited lifetime; persistence cannot recover a vendor-expired result.
|
|
46
|
+
- The adapter owns no agents, background timers or queues. Core resolves the current credential for each factory, owns network routing/scheduling, and drains each full awaited method, including body reads and child requests. Await every request and body before returning; do not keep the borrowed port for later background work or dispose it.
|
|
47
|
+
|
|
48
|
+
The plugin is trusted Node code, not a network/filesystem sandbox. This reference intentionally uses only the borrowed `context.network.fetch` for its HTTP requests and writes only the supplied delivery file.
|
|
49
|
+
|
|
50
|
+
The protocol was checked against the official [Wan image API](https://help.aliyun.com/zh/model-studio/text-to-image-v2-api-reference) and [model directory API](https://help.aliyun.com/zh/model-studio/list-models) on 2026-11-03. Acceptance uses controlled local responses; it does not certify a live account, region, billing behavior or every Wan model.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as iris from '@mokuyoaxis/dsh-iris/plugins';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
if (iris.PROVIDER_FACTORY_CONTEXT_VERSION !== 1 || typeof iris.ProviderContractError !== 'function') {
|
|
5
|
+
throw new Error('Wan Provider starter requires Iris Provider factory context v1. Published 0.3.0-alpha.1 does not include it; use the verified newer Core candidate described in README.');
|
|
6
|
+
}
|
|
7
|
+
const { definePlugin, defineProviderAdapter, ProviderContractError, normalizeMediaEndpoint } = iris;
|
|
8
|
+
export const PROVIDER_PROTOCOL = 'example.wan-image.images';
|
|
9
|
+
export const SUPPORTED_MODEL = 'wan2.6-t2i';
|
|
10
|
+
const JSON_LIMIT = 1024 * 1024, IMAGE_LIMIT = 8 * 1024 * 1024, DIRECTORY_LIMIT = 1000;
|
|
11
|
+
|
|
12
|
+
function failure(message, stage, acceptance, category = 'protocol', details = {}) {
|
|
13
|
+
return new ProviderContractError(message, { stage, acceptance, category, ...details });
|
|
14
|
+
}
|
|
15
|
+
function endpoint(value) {
|
|
16
|
+
const base = normalizeMediaEndpoint(value);
|
|
17
|
+
if (/\/(?:api\/v1|compatible-mode\/v1)$/.test(new URL(base).pathname)) {
|
|
18
|
+
throw failure('Use the workspace API root, without /api/v1 or /compatible-mode/v1.', 'validate', 'not_accepted', 'invalid_request');
|
|
19
|
+
}
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
function boundedSignal(request) {
|
|
23
|
+
const deadline = AbortSignal.timeout(request.timeoutMs ?? 30000);
|
|
24
|
+
return request.signal ? AbortSignal.any([request.signal, deadline]) : deadline;
|
|
25
|
+
}
|
|
26
|
+
async function readBody(response, limit, stage, acceptance) {
|
|
27
|
+
if (!response.body) throw failure('Response has no body.', stage, acceptance);
|
|
28
|
+
const reader = response.body.getReader(), chunks = []; let size = 0;
|
|
29
|
+
try {
|
|
30
|
+
for (;;) {
|
|
31
|
+
const item = await reader.read(); if (item.done) break;
|
|
32
|
+
size += item.value.byteLength;
|
|
33
|
+
if (size > limit) {
|
|
34
|
+
await reader.cancel(); throw failure('Response exceeds the documented starter size limit.', stage, acceptance);
|
|
35
|
+
}
|
|
36
|
+
chunks.push(Buffer.from(item.value));
|
|
37
|
+
}
|
|
38
|
+
return Buffer.concat(chunks, size);
|
|
39
|
+
} finally { reader.releaseLock(); }
|
|
40
|
+
}
|
|
41
|
+
function json(bytes, stage, acceptance) {
|
|
42
|
+
try { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
|
|
43
|
+
catch { throw failure('Provider response is not valid UTF-8 JSON.', stage, acceptance); }
|
|
44
|
+
}
|
|
45
|
+
function httpFailure(response, operation) {
|
|
46
|
+
const ambiguous = [408, 409, 425, 499].includes(response.status);
|
|
47
|
+
const known = operation === 'submit' && response.status >= 400 && response.status < 500 && !ambiguous;
|
|
48
|
+
const acceptance = operation === 'submit' ? known ? 'not_accepted' : 'unknown' : operation === 'discover' ? 'not_accepted' : 'accepted';
|
|
49
|
+
const category = [401, 403].includes(response.status) ? 'authentication' : response.status === 429 ? 'rate_limit' : response.status >= 500 ? 'provider' : 'invalid_request';
|
|
50
|
+
const error = failure(`Wan HTTP request failed (${response.status}).`, operation === 'discover' ? 'validate' : operation, acceptance, category,
|
|
51
|
+
{ httpStatus: response.status, retryable: response.status === 429 || response.status >= 500 });
|
|
52
|
+
if (response.status === 429) {
|
|
53
|
+
const value = response.headers.get('retry-after'), seconds = value && Number(value), date = value && Date.parse(value);
|
|
54
|
+
if (Number.isFinite(seconds) && seconds >= 0) error.retryAfterMs = Math.max(1000, seconds * 1000);
|
|
55
|
+
else if (Number.isFinite(date) && date > Date.now()) error.retryAfterMs = date - Date.now();
|
|
56
|
+
}
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default definePlugin({
|
|
61
|
+
manifest: { contractVersion: 0, id: 'example.wan-image', version: '0.1.0',
|
|
62
|
+
compatibility: { min: '0.3.0-alpha.1', maxExclusive: '0.4.0' },
|
|
63
|
+
contributions: { providers: [{ id: 'images', version: '0.1.0', title: 'Wan 2.6 async image reference', capabilities: ['image-gen'],
|
|
64
|
+
configuration: [{ key: 'baseUrl', type: 'url', label: 'Workspace API root', required: true },
|
|
65
|
+
{ key: 'mediaBaseUrl', type: 'url', label: 'Separate media API root', advanced: true },
|
|
66
|
+
{ key: 'apiKey', type: 'secret', label: 'API key', required: true }] }] } },
|
|
67
|
+
activate() {
|
|
68
|
+
// No timers, agents or global state to close. Core owns the borrowed network port.
|
|
69
|
+
return { providers: { images: { normalizeEndpoint: endpoint,
|
|
70
|
+
supportsModel: ({ modelId, capability }) => modelId === SUPPORTED_MODEL && capability === 'image-gen',
|
|
71
|
+
supportsImageEdit: () => false,
|
|
72
|
+
createAdapter(provider, context) {
|
|
73
|
+
if (context?.contractVersion !== 1 || typeof context.network?.fetch !== 'function') {
|
|
74
|
+
throw failure('Core must supply Provider factory context v1.', 'validate', 'not_accepted', 'invalid_request');
|
|
75
|
+
}
|
|
76
|
+
const base = endpoint(provider.mediaBaseUrl || provider.baseUrl), key = provider.apiKey || '';
|
|
77
|
+
if (provider.auth !== 'none' && !key) throw failure('Configure an API credential in Core before calling this Provider.', 'validate', 'not_accepted', 'authentication');
|
|
78
|
+
const headers = { 'Content-Type': 'application/json', ...(key ? { Authorization: 'Bearer ' + key } : {}) };
|
|
79
|
+
async function api(route, request, operation, payload) {
|
|
80
|
+
const response = await context.network.fetch(base + route, { method: payload === undefined ? 'GET' : 'POST',
|
|
81
|
+
redirect: 'error', signal: boundedSignal(request), headers: { ...headers, ...(operation === 'submit' ? { 'X-DashScope-Async': 'enable' } : {}) },
|
|
82
|
+
...(payload === undefined ? {} : { body: JSON.stringify(payload) }) });
|
|
83
|
+
const acceptance = operation === 'submit' ? 'unknown' : operation === 'discover' ? 'not_accepted' : 'accepted';
|
|
84
|
+
const stage = operation === 'discover' ? 'validate' : operation === 'submit' ? 'response' : operation;
|
|
85
|
+
const bytes = await readBody(response, JSON_LIMIT, stage, acceptance);
|
|
86
|
+
if (!response.ok) throw httpFailure(response, operation);
|
|
87
|
+
return json(bytes, stage, acceptance);
|
|
88
|
+
}
|
|
89
|
+
return defineProviderAdapter({ id: provider.id, protocol: PROVIDER_PROTOCOL, capabilities: ['image'], operations: {
|
|
90
|
+
async discover(request = {}) {
|
|
91
|
+
const models = [], seen = new Set(); let total;
|
|
92
|
+
for (let page = 1; page <= 10; page++) {
|
|
93
|
+
const value = await api(`/api/v1/models?capabilities=IG&page_no=${page}&page_size=100`, request, 'discover');
|
|
94
|
+
const output = value?.output;
|
|
95
|
+
if (!output || !Array.isArray(output.models) || output.models.length > 100 || !Number.isSafeInteger(output.total)
|
|
96
|
+
|| output.total < 0 || output.total > DIRECTORY_LIMIT || output.page_no !== page || output.page_size !== 100
|
|
97
|
+
|| total !== undefined && total !== output.total) throw failure('Model directory pagination is invalid or changed; keep the previous complete snapshot.', 'validate', 'not_accepted');
|
|
98
|
+
total = output.total;
|
|
99
|
+
for (const model of output.models) {
|
|
100
|
+
if (!model || typeof model.model !== 'string' || !model.model || seen.has(model.model)) throw failure('Model directory contains invalid or duplicate IDs.', 'validate', 'not_accepted');
|
|
101
|
+
seen.add(model.model);
|
|
102
|
+
if (model.model === SUPPORTED_MODEL) models.push({ id: model.model, capabilities: ['image-gen'] });
|
|
103
|
+
}
|
|
104
|
+
if (seen.size === total) return { models };
|
|
105
|
+
if (!output.models.length || seen.size > total) throw failure('Model directory is incomplete.', 'validate', 'not_accepted');
|
|
106
|
+
}
|
|
107
|
+
throw failure('Model directory exceeded the bounded page count.', 'validate', 'not_accepted');
|
|
108
|
+
},
|
|
109
|
+
async submit(request) {
|
|
110
|
+
const { input } = request;
|
|
111
|
+
if (request.capability !== 'image' || request.model !== SUPPORTED_MODEL || input.image !== undefined) {
|
|
112
|
+
throw failure('This reference supports only Wan 2.6 text-to-image; image editing and other models are unsupported.', 'validate', 'not_accepted', 'invalid_request');
|
|
113
|
+
}
|
|
114
|
+
if (typeof input.prompt !== 'string' || !input.prompt.trim() || Array.from(input.prompt).length > 2100
|
|
115
|
+
|| input.n !== undefined && (!Number.isSafeInteger(input.n) || input.n < 1 || input.n > 4)
|
|
116
|
+
|| input.size !== undefined && (typeof input.size !== 'string' || !/^\d+[x*]\d+$/i.test(input.size))) {
|
|
117
|
+
throw failure('Use a nonempty prompt of at most 2100 characters, n=1–4 and an optional width*height size.', 'validate', 'not_accepted', 'invalid_request');
|
|
118
|
+
}
|
|
119
|
+
const value = await api('/api/v1/services/aigc/image-generation/generation', request, 'submit', { model: request.model,
|
|
120
|
+
input: { messages: [{ role: 'user', content: [{ text: input.prompt }] }] },
|
|
121
|
+
parameters: { n: input.n ?? 1, ...(input.size ? { size: input.size.replace(/x/i, '*') } : {}) } });
|
|
122
|
+
if (typeof value?.output?.task_id !== 'string' || !value.output.task_id.trim()) throw failure('Submission response has no remote task ID; acceptance remains unknown.', 'response', 'unknown');
|
|
123
|
+
return { kind: 'accepted', remoteTaskId: value.output.task_id };
|
|
124
|
+
},
|
|
125
|
+
async poll(request) {
|
|
126
|
+
const value = await api('/api/v1/tasks/' + encodeURIComponent(request.remoteTaskId), request, 'poll'), output = value?.output;
|
|
127
|
+
if (['PENDING', 'RUNNING'].includes(output?.task_status)) return { kind: 'pending', progress: output.task_status };
|
|
128
|
+
if (output?.task_status === 'SUCCEEDED') {
|
|
129
|
+
if (!Array.isArray(output.choices) || output.choices.some(choice => !Array.isArray(choice?.message?.content)
|
|
130
|
+
|| choice.message.content.some(item => !item || typeof item !== 'object'))) {
|
|
131
|
+
throw failure('Successful task response contains malformed image choices.', 'poll', 'accepted');
|
|
132
|
+
}
|
|
133
|
+
const images = output.choices.flatMap(choice => choice.message.content).flatMap(item => typeof item.image === 'string' && item.image ? [item.image] : []);
|
|
134
|
+
if (!images?.length || images.length > 4) throw failure('Successful task response has no usable image results.', 'poll', 'accepted');
|
|
135
|
+
return { kind: 'succeeded', artifacts: images.map(url => ({ kind: 'remote-url', url, mediaType: 'image/png' })) };
|
|
136
|
+
}
|
|
137
|
+
const kind = { FAILED: 'failed', CANCELED: 'canceled', UNKNOWN: 'unknown' }[output?.task_status];
|
|
138
|
+
if (kind) return { kind, error: failure('Remote task is ' + kind + '.', 'poll', 'accepted', kind === 'canceled' ? 'aborted' : kind === 'unknown' ? 'unknown' : 'provider') };
|
|
139
|
+
throw failure('Remote task response has an unrecognized status.', 'poll', 'accepted');
|
|
140
|
+
},
|
|
141
|
+
async download(request) {
|
|
142
|
+
if (request.artifact.kind !== 'remote-url') throw failure('Wan result must be a remote image URL.', 'download', 'accepted');
|
|
143
|
+
const url = new URL(request.artifact.url);
|
|
144
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw failure('Result URL must use HTTP(S) without embedded credentials.', 'download', 'accepted');
|
|
145
|
+
// Presigned CDN URLs never receive the API Authorization header. Redirects use the same Core port.
|
|
146
|
+
const response = await context.network.fetch(url, { signal: boundedSignal(request) });
|
|
147
|
+
const bytes = await readBody(response, IMAGE_LIMIT, 'download', 'accepted');
|
|
148
|
+
if (!response.ok) throw httpFailure(response, 'download');
|
|
149
|
+
writeFileSync(request.targetPath, bytes, { mode: 0o600, flag: 'wx' });
|
|
150
|
+
return { bytes: bytes.length };
|
|
151
|
+
},
|
|
152
|
+
mapError(error, defaults = {}) {
|
|
153
|
+
if (error instanceof ProviderContractError) return error;
|
|
154
|
+
const aborted = error?.name === 'AbortError', timeout = error?.name === 'TimeoutError';
|
|
155
|
+
return failure('Wan transport or local delivery failed; inspect the original Core Task before taking another action.',
|
|
156
|
+
error?.stage || defaults.stage || 'submit', error?.acceptance || defaults.acceptance || 'unknown',
|
|
157
|
+
error?.category || (aborted ? 'aborted' : timeout ? 'timeout' : ['ENOSPC', 'EDQUOT', 'EACCES', 'EPERM', 'EROFS', 'EIO', 'EEXIST'].includes(error?.code) ? 'local_io' : 'network'),
|
|
158
|
+
{ ...(typeof error?.code === 'string' && error.code.startsWith('IRIS_') ? { providerCode: error.code } : {}), retryable: aborted ? false : true });
|
|
159
|
+
}
|
|
160
|
+
}, unsupported: { cancel: 'Remote cancellation is outside this reference; stopping Core waiting does not cancel billing.' },
|
|
161
|
+
unsupportedPreparation: 'This text-to-image protocol does not upload source images.' });
|
|
162
|
+
}
|
|
163
|
+
} } };
|
|
164
|
+
}
|
|
165
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iris-provider-starter-wan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "An independent Wan 2.6 image Provider reference using the Iris public plugin context.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": { "types": "./index.d.mts", "default": "./index.mjs" },
|
|
8
|
+
"files": ["index.mjs", "index.d.mts", "scripts", "README.md", "LICENSE"],
|
|
9
|
+
"scripts": { "setup": "node scripts/setup.mjs", "test": "node scripts/smoke.mjs" },
|
|
10
|
+
"peerDependencies": { "@mokuyoaxis/dsh-iris": ">=0.3.0-alpha.1 <0.4.0" },
|
|
11
|
+
"engines": { "node": ">=22.1.0" },
|
|
12
|
+
"license": "MIT"
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
export const cleanEnvironment = () => ({ ...process.env, IRIS_IMPORT_WORKBENCH_CONFIG: '', IRIS_CORE_CONNECTION_FILE: '', IRIS_CORE_SCOPES: '', NODE_OPTIONS: '' });
|
|
11
|
+
const execute = promisify(execFile);
|
|
12
|
+
export async function runCore(dataRoot, ...args) {
|
|
13
|
+
return runCli([...args, '--data-root', dataRoot]);
|
|
14
|
+
}
|
|
15
|
+
export async function runCli(args) {
|
|
16
|
+
const { stdout } = await execute(process.execPath, [coreCli, ...args], {
|
|
17
|
+
timeout: 30_000, maxBuffer: 2 * 1024 * 1024, env: cleanEnvironment()
|
|
18
|
+
});
|
|
19
|
+
const value = JSON.parse(stdout);
|
|
20
|
+
if (value.ok === false) throw new Error('Core command failed: ' + JSON.stringify(value));
|
|
21
|
+
return value.result ?? value;
|
|
22
|
+
}
|