@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,101 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
|
|
5
|
+
import { requestDirectory } from './local-request-root.js';
|
|
6
|
+
import { checkedPluginDependency, pluginFields, pluginJson, pluginFail, pluginObject } from './plugin-contract.js';
|
|
7
|
+
import { inspectCoreArtifactMetadata, findCoreArtifactMetadata } from './core-artifacts.js';
|
|
8
|
+
|
|
9
|
+
const ID = /^op_[a-f0-9]{64}$/, ARTIFACT = /^artifact_[a-f0-9]{24}$/, UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
10
|
+
const STATES = ['queued', 'running', 'succeeded', 'failed', 'canceled', 'interrupted'];
|
|
11
|
+
const CODES = ['dependency_unavailable', 'permission_denied', 'execution_failed'];
|
|
12
|
+
const invalid = () => pluginFail('IRIS_OPERATION_STORE_INVALID', '持久插件 Operation 记录损坏或绑定不一致');
|
|
13
|
+
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
14
|
+
const canonical = value => Array.isArray(value) ? value.map(canonical) : value !== null && typeof value === 'object'
|
|
15
|
+
? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value;
|
|
16
|
+
export const pluginInputDigest = (id, input, dependency, sourceArtifactIds = []) => 'sha256:' + hash(JSON.stringify(canonical(['plugin.execute/v1', id, input, checkedPluginDependency(dependency), sourceArtifactIds])));
|
|
17
|
+
const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
18
|
+
const ids = value => Array.isArray(value) && value.length <= 16 && new Set(value).size === value.length && value.every(id => typeof id === 'string' && ARTIFACT.test(id));
|
|
19
|
+
const fileFor = (root, marker, id) => { if (!ID.test(id)) invalid(); return path.join(requestDirectory(root, marker, 'operations'), id + '.json'); };
|
|
20
|
+
const save = (root, marker, record, create = false) => {
|
|
21
|
+
validatePluginOperation(record, record.id, marker);
|
|
22
|
+
(create ? atomicCreatePrivate : atomicWritePrivate)(fileFor(root, marker, record.id), JSON.stringify(record) + '\n');
|
|
23
|
+
return structuredClone(record);
|
|
24
|
+
};
|
|
25
|
+
const now = record => new Date(Math.max(Date.now(), Date.parse(record.updatedAt))).toISOString();
|
|
26
|
+
|
|
27
|
+
export function validatePluginOperation(record, id, marker) {
|
|
28
|
+
if (!pluginFields(record, ['schemaVersion', 'id', 'rootId', 'revision', 'action', 'operationId', 'dependency', 'input', 'inputDigest',
|
|
29
|
+
'sourceArtifactIds', 'reservedArtifactIds', 'ownerInstanceId', 'createdAt', 'updatedAt', 'state', 'cancelRequested', 'startedAt', 'finishedAt', 'result', 'failureCode'],
|
|
30
|
+
['schemaVersion', 'id', 'rootId', 'revision', 'action', 'operationId', 'dependency', 'input', 'inputDigest', 'sourceArtifactIds', 'reservedArtifactIds',
|
|
31
|
+
'ownerInstanceId', 'createdAt', 'updatedAt', 'state', 'cancelRequested']) || record.schemaVersion !== 1 || record.action !== 'plugin.execute'
|
|
32
|
+
|| !ID.test(id) || record.id !== id || record.rootId !== marker.rootId || !Number.isSafeInteger(record.revision) || record.revision < 1
|
|
33
|
+
|| !UUID.test(record.ownerInstanceId) || !STATES.includes(record.state) || !iso(record.createdAt) || !iso(record.updatedAt) || record.updatedAt < record.createdAt
|
|
34
|
+
|| record.cancelRequested !== (record.state === 'canceled') || !ids(record.sourceArtifactIds) || !ids(record.reservedArtifactIds)) invalid();
|
|
35
|
+
const dependency = checkedPluginDependency(record.dependency);
|
|
36
|
+
if (dependency.kind !== 'operations' || dependency.contributionId !== record.operationId
|
|
37
|
+
|| !pluginObject(record.input) || record.inputDigest !== pluginInputDigest(record.operationId, record.input, dependency, record.sourceArtifactIds)
|
|
38
|
+
|| Buffer.byteLength(JSON.stringify(record.input)) > 65536) invalid();
|
|
39
|
+
if (record.reservedArtifactIds.some((value, i) => value !== 'artifact_' + hash(JSON.stringify([record.id, i])).slice(0, 24))) invalid();
|
|
40
|
+
const active = ['queued', 'running'].includes(record.state), started = Object.hasOwn(record, 'startedAt');
|
|
41
|
+
if (record.state === 'queued' && (record.revision !== 1 || started || record.reservedArtifactIds.length || record.updatedAt !== record.createdAt)
|
|
42
|
+
|| record.state === 'running' && !started || started && (!iso(record.startedAt) || record.startedAt < record.createdAt || record.startedAt > record.updatedAt)
|
|
43
|
+
|| record.state === 'failed' && !CODES.includes(record.failureCode) || record.state !== 'failed' && Object.hasOwn(record, 'failureCode')) invalid();
|
|
44
|
+
if (active) { if (Object.hasOwn(record, 'finishedAt') || Object.hasOwn(record, 'result')) invalid(); }
|
|
45
|
+
else if (!iso(record.finishedAt) || record.finishedAt !== record.updatedAt || !pluginFields(record.result, ['value', 'artifactIds'])
|
|
46
|
+
|| !ids(record.result.artifactIds) || record.result.artifactIds.some(value => !record.reservedArtifactIds.includes(value))
|
|
47
|
+
|| record.state === 'succeeded' && (!started || record.result.artifactIds.length !== record.reservedArtifactIds.length)
|
|
48
|
+
|| record.state !== 'succeeded' && record.result.value !== null
|
|
49
|
+
|| Buffer.byteLength(JSON.stringify(record.result.value)) > 128 * 1024) invalid();
|
|
50
|
+
return record;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validatePluginBinding(root, record) {
|
|
54
|
+
for (const id of record.sourceArtifactIds) inspectCoreArtifactMetadata(root, id);
|
|
55
|
+
for (const id of record.result?.artifactIds || []) {
|
|
56
|
+
const artifact = inspectCoreArtifactMetadata(root, id);
|
|
57
|
+
if (artifact.kind !== 'plugin-image' || artifact.metadata.pluginOperationId !== record.id
|
|
58
|
+
|| JSON.stringify(artifact.relations) !== JSON.stringify(record.sourceArtifactIds.map(artifactId => ({ type: 'derived-from', artifactId })))) invalid();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function read(root, marker, id) {
|
|
62
|
+
const file = fileFor(root, marker, id), stat = fs.lstatSync(file);
|
|
63
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024) invalid();
|
|
64
|
+
let record; try { record = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { invalid(); }
|
|
65
|
+
return validatePluginOperation(record, id, marker);
|
|
66
|
+
}
|
|
67
|
+
export function pluginSourceArtifacts(input, fields) {
|
|
68
|
+
const result = [...new Set(fields.flatMap(key => input[key] === undefined ? [] : Array.isArray(input[key]) ? input[key] : [input[key]]))];
|
|
69
|
+
if (!ids(result)) pluginFail('IRIS_PLUGIN_INPUT_INVALID', '业务来源须为最多 16 个有效 Artifact ID');
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
export function acceptPluginOperation(root, marker, id, { operationId, input, dependency, ownerInstanceId, sourceArtifactIds }) {
|
|
73
|
+
for (const artifactId of sourceArtifactIds) inspectCoreArtifactMetadata(root, artifactId);
|
|
74
|
+
const time = new Date().toISOString();
|
|
75
|
+
return save(root, marker, { schemaVersion: 1, id, rootId: marker.rootId, revision: 1, action: 'plugin.execute', operationId,
|
|
76
|
+
dependency: checkedPluginDependency(dependency), input: pluginJson(input), inputDigest: pluginInputDigest(operationId, input, dependency, sourceArtifactIds),
|
|
77
|
+
sourceArtifactIds, reservedArtifactIds: [], ownerInstanceId, createdAt: time, updatedAt: time, state: 'queued', cancelRequested: false }, true);
|
|
78
|
+
}
|
|
79
|
+
export function reservePluginArtifact(root, marker, id) {
|
|
80
|
+
const record = read(root, marker, id);
|
|
81
|
+
if (record.state !== 'running' || record.reservedArtifactIds.length >= 16) pluginFail('IRIS_PLUGIN_OPERATION_DENIED', '当前业务不可导入更多作品');
|
|
82
|
+
const artifactId = 'artifact_' + hash(JSON.stringify([record.id, record.reservedArtifactIds.length])).slice(0, 24);
|
|
83
|
+
save(root, marker, { ...record, revision: record.revision + 1, updatedAt: now(record), reservedArtifactIds: [...record.reservedArtifactIds, artifactId] });
|
|
84
|
+
return artifactId;
|
|
85
|
+
}
|
|
86
|
+
export function transitionPluginOperation(root, marker, value) {
|
|
87
|
+
const record = read(root, marker, value.id);
|
|
88
|
+
if (record.revision !== value.expectedRevision) pluginFail('IRIS_OPERATION_REVISION_CONFLICT', 'Operation 已改变');
|
|
89
|
+
const allowed = record.state === 'queued' ? ['running', 'failed', 'canceled', 'interrupted'] : record.state === 'running' ? STATES.filter(state => !['queued', 'running'].includes(state)) : [];
|
|
90
|
+
if (!allowed.includes(value.state)) pluginFail('IRIS_OPERATION_TRANSITION_INVALID', '结束业务不重新排队或重放');
|
|
91
|
+
if (value.state === 'failed' ? !CODES.includes(value.failureCode) : value.failureCode !== undefined) pluginFail('IRIS_OPERATION_INPUT_INVALID', '业务失败类别无效');
|
|
92
|
+
const time = now(record), terminal = value.state !== 'running';
|
|
93
|
+
const artifacts = terminal ? record.reservedArtifactIds.filter(id => findCoreArtifactMetadata(root, id)) : [];
|
|
94
|
+
return save(root, marker, { ...record, revision: record.revision + 1, state: value.state, updatedAt: time, cancelRequested: value.state === 'canceled',
|
|
95
|
+
...(terminal ? { finishedAt: time, result: { value: value.state === 'succeeded' ? pluginJson(value.value) : null, artifactIds: artifacts } } : { startedAt: time }),
|
|
96
|
+
...(value.failureCode ? { failureCode: value.failureCode } : {}) });
|
|
97
|
+
}
|
|
98
|
+
export function projectPluginOperation(record) {
|
|
99
|
+
const { schemaVersion: _version, ownerInstanceId: _owner, reservedArtifactIds: _planned, ...value } = record;
|
|
100
|
+
return { contractVersion: 0, ...structuredClone(value) };
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Internal Core invocation boundary. No Provider implementations, Zod, module loading or filesystem.
|
|
2
|
+
import { freezePluginValue, pluginJson, pluginFail } from './plugin-contract.js';
|
|
3
|
+
|
|
4
|
+
const registries = new WeakMap();
|
|
5
|
+
export function registerPluginOperations(registry, state) { registries.set(registry, state); }
|
|
6
|
+
|
|
7
|
+
/** Reserve across the Core queue; the public registry never exposes an executable handler. */
|
|
8
|
+
export function reservePluginOperation(registry, id, input, allowedOperations, expectedDependency) {
|
|
9
|
+
const owned = registries.get(registry);
|
|
10
|
+
if (!owned) pluginFail('IRIS_PLUGIN_INVALID', '需要当前插件注册实例');
|
|
11
|
+
owned.requireActive();
|
|
12
|
+
const operation = owned.operations.get(id);
|
|
13
|
+
if (!operation) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', '业务贡献未加载');
|
|
14
|
+
if (!Array.isArray(allowedOperations) || !allowedOperations.includes(id)) pluginFail('IRIS_PLUGIN_OPERATION_DENIED', '业务贡献未获本次 Core 装配授权');
|
|
15
|
+
const dependency = registry.dependency('operations', id);
|
|
16
|
+
if (expectedDependency !== undefined) {
|
|
17
|
+
registry.assertDependency(expectedDependency);
|
|
18
|
+
if (expectedDependency.kind !== 'operations' || expectedDependency.contributionId !== id) pluginFail('IRIS_PLUGIN_DEPENDENCY_MISMATCH', '不能用其他贡献的持久依赖执行当前业务');
|
|
19
|
+
}
|
|
20
|
+
const checked = operation.schema.safeParse(pluginJson(input));
|
|
21
|
+
if (!checked.success) pluginFail('IRIS_PLUGIN_INPUT_INVALID', '输入不符合 ' + id + ' 的 Schema');
|
|
22
|
+
let release;
|
|
23
|
+
const done = new Promise(resolve => { release = () => { owned.active.delete(done); resolve(); }; });
|
|
24
|
+
owned.active.add(done);
|
|
25
|
+
return { mode: operation.descriptor.mode, commands: operation.descriptor.commands, artifactInputs: operation.descriptor.artifactInputs || [],
|
|
26
|
+
input: freezePluginValue(checked.data), dependency, release,
|
|
27
|
+
async invoke(context) { return { dependency, value: pluginJson(await operation.handler(freezePluginValue(checked.data), context)) }; } };
|
|
28
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createProviderRegistry } from './provider-extensions.js';
|
|
3
|
+
import { definePlugin, assertPluginCompatibility, checkedPluginDependency, pluginDependency, qualifiedPluginId,
|
|
4
|
+
pluginFields, pluginJson, pluginFail, comparePluginVersions, PLUGIN_KINDS } from './plugin-contract.js';
|
|
5
|
+
import { registerPluginOperations } from './plugin-operations.js';
|
|
6
|
+
|
|
7
|
+
const unavailable = () => pluginFail('IRIS_PLUGIN_CLOSED', '插件注册实例正在关闭或已关闭');
|
|
8
|
+
|
|
9
|
+
/** Manifest validation precedes every activation. Core owns call reservations and cleanup. */
|
|
10
|
+
export async function createPluginRegistry({ plugins, coreVersion }) {
|
|
11
|
+
if (!Array.isArray(plugins)) pluginFail('IRIS_PLUGIN_INVALID', 'plugins 须为明确的插件数组');
|
|
12
|
+
comparePluginVersions(coreVersion, coreVersion);
|
|
13
|
+
const definitions = plugins.map(definePlugin), entries = new Map(), contributionIds = new Set(), operations = new Map(), cleanups = [], active = new Set();
|
|
14
|
+
for (const definition of definitions) {
|
|
15
|
+
const { manifest } = definition; assertPluginCompatibility(manifest, coreVersion);
|
|
16
|
+
if (entries.has(manifest.id)) pluginFail('IRIS_PLUGIN_INVALID', '插件 ID 重复:' + manifest.id);
|
|
17
|
+
entries.set(manifest.id, definition);
|
|
18
|
+
for (const kind of PLUGIN_KINDS) for (const contribution of manifest.contributions[kind]) {
|
|
19
|
+
const key = kind + ':' + qualifiedPluginId(manifest.id, contribution.id);
|
|
20
|
+
if (contributionIds.has(key)) pluginFail('IRIS_PLUGIN_INVALID', '贡献完整 ID 重复:' + key);
|
|
21
|
+
contributionIds.add(key);
|
|
22
|
+
}
|
|
23
|
+
for (const operation of manifest.contributions.operations) {
|
|
24
|
+
let schema;
|
|
25
|
+
try { schema = z.fromJSONSchema(operation.inputSchema); }
|
|
26
|
+
catch (cause) { throw Object.assign(new Error('插件输入 Schema 无法编译:' + manifest.id + '.' + operation.id, { cause }), { code: 'IRIS_PLUGIN_INVALID' }); }
|
|
27
|
+
operations.set(qualifiedPluginId(manifest.id, operation.id), { manifest, descriptor: operation, schema });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let state = 'loading', providers, disposal, cleanupFailed = false;
|
|
31
|
+
|
|
32
|
+
function dispose() {
|
|
33
|
+
if (!disposal) {
|
|
34
|
+
state = 'closing';
|
|
35
|
+
const providerDisposal = providers?.dispose();
|
|
36
|
+
void providerDisposal?.catch(() => {}); // Awaited below after operation reservations drain.
|
|
37
|
+
disposal = (async () => {
|
|
38
|
+
await Promise.allSettled([...active]);
|
|
39
|
+
const errors = [];
|
|
40
|
+
try { await providerDisposal; } catch (error) { errors.push(error); }
|
|
41
|
+
for (const cleanup of [...cleanups].reverse()) {
|
|
42
|
+
try { await cleanup(); } catch (error) { errors.push(error); }
|
|
43
|
+
}
|
|
44
|
+
operations.clear(); state = 'closed'; cleanupFailed = errors.length > 0;
|
|
45
|
+
if (errors.length) throw new AggregateError(errors, '插件释放失败');
|
|
46
|
+
})();
|
|
47
|
+
}
|
|
48
|
+
return disposal;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const factories = [];
|
|
53
|
+
for (const { manifest, activate } of definitions) {
|
|
54
|
+
let activating = true;
|
|
55
|
+
const context = Object.freeze({ coreVersion, plugin: Object.freeze({ id: manifest.id, version: manifest.version }),
|
|
56
|
+
onDispose(cleanup) {
|
|
57
|
+
if (!activating || typeof cleanup !== 'function') pluginFail('IRIS_PLUGIN_LIFECYCLE', '资源释放须在 activate 完成前注册');
|
|
58
|
+
cleanups.push(cleanup);
|
|
59
|
+
} });
|
|
60
|
+
let implementation;
|
|
61
|
+
try { implementation = await activate(context); } finally { activating = false; }
|
|
62
|
+
if (!pluginFields(implementation, ['providers', 'operations'], [])) pluginFail('IRIS_PLUGIN_INVALID', 'activate 须返回 Provider/业务实现映射;资源通过 onDispose 释放');
|
|
63
|
+
for (const kind of ['providers', 'operations']) {
|
|
64
|
+
const handlers = implementation[kind] === undefined ? {} : implementation[kind];
|
|
65
|
+
if (!pluginFields(handlers, manifest.contributions[kind].map(item => item.id))) pluginFail('IRIS_PLUGIN_INVALID', '实现必须与声明的 ' + kind + ' 一一对应');
|
|
66
|
+
if (kind === 'operations') for (const descriptor of manifest.contributions.operations) {
|
|
67
|
+
if (typeof handlers[descriptor.id] !== 'function') pluginFail('IRIS_PLUGIN_INVALID', '业务操作必须为函数');
|
|
68
|
+
operations.get(qualifiedPluginId(manifest.id, descriptor.id)).handler = handlers[descriptor.id];
|
|
69
|
+
}
|
|
70
|
+
else if (manifest.contributions.providers.length) factories.push(() => ({ contractVersion: 0, id: manifest.id, version: manifest.version,
|
|
71
|
+
protocols: manifest.contributions.providers.map(descriptor => {
|
|
72
|
+
const handler = handlers[descriptor.id];
|
|
73
|
+
if (!pluginFields(handler, ['normalizeEndpoint', 'createAdapter', 'supportsModel', 'supportsImageEdit'], ['normalizeEndpoint', 'createAdapter'])) {
|
|
74
|
+
pluginFail('IRIS_PLUGIN_INVALID', 'Provider 实现字段无效');
|
|
75
|
+
}
|
|
76
|
+
return { ...handler, id: qualifiedPluginId(manifest.id, descriptor.id), label: descriptor.title,
|
|
77
|
+
capabilities: descriptor.capabilities, configuration: descriptor.configuration,
|
|
78
|
+
dependency: pluginDependency(manifest, 'providers', descriptor.id) };
|
|
79
|
+
}) }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
providers = await createProviderRegistry({ extensions: factories });
|
|
83
|
+
state = 'active';
|
|
84
|
+
} catch (error) {
|
|
85
|
+
try { await dispose(); } catch (cleanup) { throw new AggregateError([error, cleanup], '插件激活与释放均失败'); }
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function dependency(kind, id) {
|
|
90
|
+
const manifest = definitions.map(item => item.manifest).find(item => item.contributions[kind]?.some(value => qualifiedPluginId(item.id, value.id) === id));
|
|
91
|
+
if (!manifest) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', '所需插件贡献未加载');
|
|
92
|
+
return pluginDependency(manifest, kind, manifest.contributions[kind].find(item => qualifiedPluginId(manifest.id, item.id) === id).id);
|
|
93
|
+
}
|
|
94
|
+
function assertDependency(value) {
|
|
95
|
+
const expected = checkedPluginDependency(value), current = dependency(expected.kind, expected.contributionId);
|
|
96
|
+
if (Object.keys(expected).some(key => expected[key] !== current[key])) pluginFail('IRIS_PLUGIN_DEPENDENCY_MISMATCH', '原任务依赖的插件或贡献版本不一致;请恢复准确版本,不会重新提交');
|
|
97
|
+
return current;
|
|
98
|
+
}
|
|
99
|
+
const registry = Object.freeze({
|
|
100
|
+
providers,
|
|
101
|
+
snapshot: () => ({ contractVersion: 0, coreVersion, state, activeCalls: active.size, cleanupFailed,
|
|
102
|
+
plugins: definitions.map(({ manifest }) => pluginJson(manifest)),
|
|
103
|
+
contributions: Object.fromEntries(PLUGIN_KINDS.map(kind => [kind, definitions.flatMap(({ manifest }) => manifest.contributions[kind]
|
|
104
|
+
.map(entry => ({ ...pluginJson(entry), id: qualifiedPluginId(manifest.id, entry.id), dependency: pluginJson(pluginDependency(manifest, kind, entry.id)) })))])) }),
|
|
105
|
+
dependency, assertDependency, dispose
|
|
106
|
+
});
|
|
107
|
+
registerPluginOperations(registry, { operations, active, requireActive: () => { if (state !== 'active') unavailable(); } });
|
|
108
|
+
return registry;
|
|
109
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { checkedPluginDependency, pluginFail } from './plugin-contract.js';
|
|
2
|
+
|
|
3
|
+
/** Saved authority is an exact dependency list, never a live registry expansion. */
|
|
4
|
+
export function checkedPluginSelection(value = []) {
|
|
5
|
+
if (!Array.isArray(value) || value.length > 16) pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务须为最多 16 项明确依赖');
|
|
6
|
+
let result;
|
|
7
|
+
try { result = value.map(checkedPluginDependency); }
|
|
8
|
+
catch (error) { if (error.code !== 'IRIS_PLUGIN_INVALID') throw error; pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务依赖格式无效'); }
|
|
9
|
+
if (result.some(item => item.kind !== 'operations') || new Set(result.map(item => item.contributionId)).size !== result.length) pluginFail('IRIS_CONNECTION_INPUT_INVALID', '插件业务依赖重复或类型无效');
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
export function hasPluginSelection(values, dependency) {
|
|
13
|
+
const pin = checkedPluginDependency(dependency);
|
|
14
|
+
return values.some(value => JSON.stringify(checkedPluginDependency(value)) === JSON.stringify(pin));
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Shared source-image limits for the catalog and the plugin command boundary.
|
|
2
|
+
const MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
|
3
|
+
export function isPluginSourceImage(artifact) {
|
|
4
|
+
return MEDIA_TYPES.has(artifact.mediaType) && artifact.size <= 8 * 1024 * 1024;
|
|
5
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { freezePluginValue, pluginFail, pluginFields } from './plugin-contract.js';
|
|
4
|
+
|
|
5
|
+
const WEB_KINDS = ['pages', 'settings', 'renderers'];
|
|
6
|
+
const resources = new WeakMap();
|
|
7
|
+
const invalid = message => pluginFail('IRIS_PLUGIN_WEB_INVALID', message);
|
|
8
|
+
|
|
9
|
+
/** Only explicitly declared entry/styles. Never serve a plugin directory or evaluate browser JS. */
|
|
10
|
+
export function pinPluginWebAssets(definition, file) {
|
|
11
|
+
const root = path.dirname(file), entries = new Map();
|
|
12
|
+
let total = 0;
|
|
13
|
+
for (const kind of WEB_KINDS) for (const item of definition.manifest.contributions[kind]) {
|
|
14
|
+
const assets = new Map();
|
|
15
|
+
for (const relative of [item.entry, ...(item.styles || [])]) {
|
|
16
|
+
const target = fs.realpathSync(path.join(root, relative)), rel = path.relative(root, target), stat = fs.statSync(target);
|
|
17
|
+
if (!rel || rel.startsWith('..' + path.sep) || path.isAbsolute(rel) || !stat.isFile()) {
|
|
18
|
+
pluginFail('IRIS_PLUGIN_INVALID', 'Web 入口必须位于插件目录内');
|
|
19
|
+
}
|
|
20
|
+
if (stat.size > 8 * 1024 * 1024 || (total += stat.size) > 16 * 1024 * 1024) invalid('插件 Web 资源超过 8 MiB 单文件 / 16 MiB 总量限制');
|
|
21
|
+
assets.set(relative, { bytes: fs.readFileSync(target), mediaType: relative.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8' });
|
|
22
|
+
}
|
|
23
|
+
entries.set(kind + ':' + definition.manifest.id + '.' + item.id, assets);
|
|
24
|
+
}
|
|
25
|
+
return entries;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerPluginWebAssets(registry, entries) { resources.set(registry, entries); }
|
|
29
|
+
|
|
30
|
+
/** A Core service opts in to exact contribution IDs. Installing a plugin never opts in. */
|
|
31
|
+
export function createPluginPanelAssembly(registry, selection = {}) {
|
|
32
|
+
if (!pluginFields(selection, WEB_KINDS, [])) invalid('Panel 组合选择须为 pages/settings/renderers 的明确 ID 列表');
|
|
33
|
+
const source = registry?.snapshot(), pinned = resources.get(registry), routes = new Map(), manifest = { contractVersion: 0 }, selected = {};
|
|
34
|
+
if (source && source.state !== 'active') pluginFail('IRIS_PLUGIN_CLOSED', '插件注册实例正在关闭或已关闭');
|
|
35
|
+
for (const kind of WEB_KINDS) {
|
|
36
|
+
const ids = selection[kind] ?? [];
|
|
37
|
+
if (!Array.isArray(ids) || ids.some(id => typeof id !== 'string') || new Set(ids).size !== ids.length) invalid('Panel 选择须为不重复的完整贡献 ID');
|
|
38
|
+
selected[kind] = [...ids];
|
|
39
|
+
manifest[kind] = ids.map(id => {
|
|
40
|
+
const item = source?.contributions[kind].find(value => value.id === id);
|
|
41
|
+
if (!item) pluginFail('IRIS_PLUGIN_CONTRIBUTION_MISSING', 'Panel 贡献未加载:' + kind + ':' + id);
|
|
42
|
+
const assets = pinned?.get(kind + ':' + id);
|
|
43
|
+
if (!assets) pluginFail('IRIS_PLUGIN_WEB_ASSET_MISSING', 'Panel 贡献须经显式文件入口加载并固定资源:' + id);
|
|
44
|
+
const base = '/plugin-assets/' + item.dependency.pluginId + '/' + item.dependency.pluginVersion + '/' + kind + '/' + id + '/';
|
|
45
|
+
for (const [name, asset] of assets) routes.set(base + name, asset);
|
|
46
|
+
return { id, title: item.title, dependency: item.dependency, entry: base + item.entry,
|
|
47
|
+
styles: (item.styles || []).map(name => base + name), ...(kind === 'renderers' ? { mediaTypes: item.mediaTypes } : {}) };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const active = () => !registry || registry.snapshot().state === 'active';
|
|
51
|
+
freezePluginValue(manifest); freezePluginValue(selected);
|
|
52
|
+
return Object.freeze({ selection: selected, manifest,
|
|
53
|
+
has: route => active() && routes.has(route), read: async route => active() ? routes.get(route) || null : null });
|
|
54
|
+
}
|
package/lib/plugins.d.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/** Experimental v0. Trusted local code; no sandbox, discovery, installation or implicit grant. */
|
|
2
|
+
import type { ProviderCapability, ProviderMethod, ProviderOperations, ProviderConnection, ProviderFactoryContext, ProviderErrorRecord } from './provider-context.js';
|
|
3
|
+
export { PROVIDER_FACTORY_CONTEXT_VERSION } from './provider-context.js';
|
|
4
|
+
export type { ProviderCapability, ProviderMethod, ProviderOperations, ProviderConnection, ProviderFactoryContext, ProviderNetwork,
|
|
5
|
+
ProviderErrorRecord, ProviderStage, ProviderAcceptance, ProviderErrorCategory, ProviderOperationContext, ProviderSubmitRequest,
|
|
6
|
+
ProviderSubmissionResult, ProviderRemoteRequest, ProviderPollResult, ProviderCancelResult, ProviderDownloadRequest,
|
|
7
|
+
ProviderDiscoveryRequest, ProviderDiscoveryResult, ProviderArtifact, ProviderImageInput } from './provider-context.js';
|
|
8
|
+
export class ProviderContractError extends Error {
|
|
9
|
+
constructor(message: string, details?: Partial<ProviderErrorRecord>);
|
|
10
|
+
readonly stage: ProviderErrorRecord['stage'];
|
|
11
|
+
readonly category: ProviderErrorRecord['category'];
|
|
12
|
+
readonly acceptance: ProviderErrorRecord['acceptance'];
|
|
13
|
+
readonly retryable: boolean;
|
|
14
|
+
readonly safeMessage: string;
|
|
15
|
+
readonly httpStatus?: number;
|
|
16
|
+
readonly providerCode?: string;
|
|
17
|
+
/** Optional transient transport hint, read by Core health before error persistence. */
|
|
18
|
+
retryAfterMs?: number;
|
|
19
|
+
toJSON(): ProviderErrorRecord;
|
|
20
|
+
}
|
|
21
|
+
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };
|
|
22
|
+
export type PluginKind = 'providers' | 'operations' | 'pages' | 'renderers' | 'settings';
|
|
23
|
+
export const PLUGIN_CONTRACT_VERSION: 0;
|
|
24
|
+
export const PLUGIN_KINDS: readonly PluginKind[];
|
|
25
|
+
export type PluginCoreCommand = 'artifact.inspect' | 'artifact.read' | 'artifact.list' | 'task.inspect' | 'task.list' | 'artifact.import';
|
|
26
|
+
export const PLUGIN_CORE_COMMANDS: Readonly<Record<PluginCoreCommand, 'reader' | 'writer'>>;
|
|
27
|
+
export class PluginContractError extends Error { constructor(code: string, message: string); readonly code: string; }
|
|
28
|
+
export interface PluginDependency {
|
|
29
|
+
readonly contractVersion: 0;
|
|
30
|
+
readonly pluginId: string;
|
|
31
|
+
readonly pluginVersion: string;
|
|
32
|
+
readonly kind: PluginKind;
|
|
33
|
+
/** Fully qualified pluginId.localId. */
|
|
34
|
+
readonly contributionId: string;
|
|
35
|
+
readonly contributionVersion: string;
|
|
36
|
+
}
|
|
37
|
+
export interface Contribution { readonly id: string; readonly version: string; readonly title: string; }
|
|
38
|
+
export interface ProviderContribution extends Contribution {
|
|
39
|
+
readonly capabilities: readonly ('image-gen' | 'video-gen' | 'tts' | 'music-gen' | 'transcribe')[];
|
|
40
|
+
readonly configuration?: readonly { key: 'baseUrl' | 'mediaBaseUrl' | 'apiKey'; label: string; type: 'url' | 'secret'; required?: boolean; advanced?: boolean }[];
|
|
41
|
+
}
|
|
42
|
+
export interface OperationContribution extends Contribution {
|
|
43
|
+
readonly mode: 'reader' | 'writer';
|
|
44
|
+
readonly commands: readonly PluginCoreCommand[];
|
|
45
|
+
/** Top-level string or string-array input fields containing source Artifact IDs. Bound at acceptance. */
|
|
46
|
+
readonly artifactInputs?: readonly string[];
|
|
47
|
+
/** Closed root object; supported JSON Schema subset documented in PLUGINS.md. */
|
|
48
|
+
readonly inputSchema: { readonly type: 'object'; readonly additionalProperties: false; readonly [key: string]: JsonValue };
|
|
49
|
+
}
|
|
50
|
+
export interface PageContribution extends Contribution {
|
|
51
|
+
/** Plugin-relative bundled JS/MJS module. Explicit entry loader pins it before Panel selection. */
|
|
52
|
+
readonly entry: string;
|
|
53
|
+
readonly styles?: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
export interface RendererContribution extends PageContribution { readonly mediaTypes: readonly string[]; }
|
|
56
|
+
export interface SettingsContribution extends PageContribution {}
|
|
57
|
+
export interface PluginPanelSelection {
|
|
58
|
+
readonly pages?: readonly string[];
|
|
59
|
+
readonly settings?: readonly string[];
|
|
60
|
+
readonly renderers?: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
export interface PluginManifest {
|
|
63
|
+
readonly contractVersion: 0;
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly version: string;
|
|
66
|
+
/** SemVer precedence interval, min inclusive/maxExclusive exclusive. Not npm range syntax. */
|
|
67
|
+
readonly compatibility: { readonly min: string; readonly maxExclusive: string };
|
|
68
|
+
readonly contributions: {
|
|
69
|
+
readonly providers?: readonly ProviderContribution[];
|
|
70
|
+
readonly operations?: readonly OperationContribution[];
|
|
71
|
+
readonly pages?: readonly PageContribution[];
|
|
72
|
+
readonly renderers?: readonly RendererContribution[];
|
|
73
|
+
readonly settings?: readonly SettingsContribution[];
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export interface PluginOperationContext {
|
|
77
|
+
readonly signal: AbortSignal;
|
|
78
|
+
readonly dependency: PluginDependency;
|
|
79
|
+
readonly core: { execute(command: PluginCoreCommand, input: Record<string, unknown>): Promise<unknown> };
|
|
80
|
+
}
|
|
81
|
+
export type PluginOperation = (input: Readonly<Record<string, JsonValue>>, context: PluginOperationContext) => JsonValue | Promise<JsonValue>;
|
|
82
|
+
export interface ProviderAdapterInput {
|
|
83
|
+
id: string; protocol: string; capabilities: ProviderCapability[]; contractVersion?: 0;
|
|
84
|
+
operations: ProviderOperations;
|
|
85
|
+
unsupported?: Partial<Record<ProviderMethod, string>>;
|
|
86
|
+
prepareInput?: (...args: any[]) => unknown;
|
|
87
|
+
unsupportedPreparation?: string;
|
|
88
|
+
pluginDependency?: PluginDependency;
|
|
89
|
+
}
|
|
90
|
+
export type ProviderAdapter = Readonly<Omit<ProviderAdapterInput, 'contractVersion' | 'capabilities' | 'operations' | 'unsupported'> & {
|
|
91
|
+
contractVersion: 0;
|
|
92
|
+
capabilities: readonly ProviderCapability[];
|
|
93
|
+
operations: Readonly<ProviderOperations>;
|
|
94
|
+
unsupported: Readonly<Partial<Record<ProviderMethod, { readonly reason: string }>>>;
|
|
95
|
+
}>;
|
|
96
|
+
export function defineProviderAdapter(input: ProviderAdapterInput): ProviderAdapter;
|
|
97
|
+
export function normalizeMediaEndpoint(value: string): string;
|
|
98
|
+
export interface ProviderImplementation {
|
|
99
|
+
normalizeEndpoint(value: string): string;
|
|
100
|
+
createAdapter(provider: Readonly<ProviderConnection>, context: Readonly<ProviderFactoryContext>): ProviderAdapter;
|
|
101
|
+
supportsModel?(input: { provider: Record<string, unknown>; modelId: string; capability: string; checkEndpoint?: boolean }): boolean;
|
|
102
|
+
supportsImageEdit?(modelId: string): boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface PluginActivationContext {
|
|
105
|
+
readonly coreVersion: string;
|
|
106
|
+
readonly plugin: { readonly id: string; readonly version: string };
|
|
107
|
+
/** Register during activate, including before awaited initialization; reverse order cleanup. */
|
|
108
|
+
onDispose(cleanup: () => void | Promise<void>): void;
|
|
109
|
+
}
|
|
110
|
+
export interface PluginImplementation {
|
|
111
|
+
readonly providers?: Readonly<Record<string, ProviderImplementation>>;
|
|
112
|
+
readonly operations?: Readonly<Record<string, PluginOperation>>;
|
|
113
|
+
}
|
|
114
|
+
export interface CorePlugin {
|
|
115
|
+
readonly manifest: PluginManifest;
|
|
116
|
+
activate(context: PluginActivationContext): PluginImplementation | Promise<PluginImplementation>;
|
|
117
|
+
}
|
|
118
|
+
export function definePlugin(definition: CorePlugin): CorePlugin;
|
|
119
|
+
export interface ProviderRegistry {
|
|
120
|
+
descriptor(id: string): Readonly<Record<string, any>> | undefined;
|
|
121
|
+
snapshot(): { state: string; protocols: Record<string, any>[]; extensions: { id: string; version: string }[]; activeCalls: number; cleanupFailed: boolean; contractVersion: 0 };
|
|
122
|
+
createAdapter(id: string, provider: Record<string, any>, options?: Record<string, any>): ProviderAdapter;
|
|
123
|
+
dispose(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
export interface CorePluginRegistry {
|
|
126
|
+
readonly providers: ProviderRegistry;
|
|
127
|
+
snapshot(): {
|
|
128
|
+
contractVersion: 0; coreVersion: string; state: string; activeCalls: number; cleanupFailed: boolean;
|
|
129
|
+
plugins: PluginManifest[];
|
|
130
|
+
contributions: {
|
|
131
|
+
providers: (ProviderContribution & { dependency: PluginDependency })[];
|
|
132
|
+
operations: (OperationContribution & { dependency: PluginDependency })[];
|
|
133
|
+
pages: (PageContribution & { dependency: PluginDependency })[];
|
|
134
|
+
renderers: (RendererContribution & { dependency: PluginDependency })[];
|
|
135
|
+
settings: (SettingsContribution & { dependency: PluginDependency })[];
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
dependency(kind: PluginKind, qualifiedId: string): PluginDependency;
|
|
139
|
+
assertDependency(dependency: PluginDependency): PluginDependency;
|
|
140
|
+
dispose(): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
export function createCorePluginRegistry(options?: { plugins?: CorePlugin[]; coreVersion?: string }): Promise<CorePluginRegistry>;
|
|
143
|
+
export function loadCorePluginRegistry(options?: { entries?: string[]; coreVersion?: string }): Promise<CorePluginRegistry>;
|
|
144
|
+
export type CoreCapabilityScope = 'read' | 'create' | 'execute' | 'stop';
|
|
145
|
+
export type CoreBridgeCapability = 'image-edit' | 'image-generate' | 'tts' | 'transcribe';
|
|
146
|
+
export interface ContributionAccess {
|
|
147
|
+
readonly local: 'requires-configuration' | 'requires-grant' | 'descriptor-only' | 'unavailable';
|
|
148
|
+
readonly web: 'via-media-flow' | 'via-operation' | 'not-exposed' | 'durable-operation-required' | 'not-mounted' | 'available-in-panel' | 'unavailable';
|
|
149
|
+
readonly mcp: 'via-media-flow' | 'requires-selection' | 'not-exposed' | 'durable-operation-required' | 'not-applicable' | 'unavailable';
|
|
150
|
+
}
|
|
151
|
+
export interface CatalogContribution extends Contribution {
|
|
152
|
+
readonly dependency: PluginDependency;
|
|
153
|
+
/** Declaration/assembly facts only; never the caller's authorization or service health. */
|
|
154
|
+
readonly access: ContributionAccess;
|
|
155
|
+
}
|
|
156
|
+
export interface CoreCapabilitySnapshot {
|
|
157
|
+
readonly contractVersion: 0;
|
|
158
|
+
readonly state: string;
|
|
159
|
+
readonly coreVersion?: string;
|
|
160
|
+
readonly scopeOptions: readonly { readonly id: CoreCapabilityScope; readonly title: string }[];
|
|
161
|
+
readonly controlTools: readonly string[];
|
|
162
|
+
readonly groups: readonly {
|
|
163
|
+
readonly id: CoreBridgeCapability; readonly recipe: string; readonly title: string; readonly label: string; readonly shortTitle: string;
|
|
164
|
+
readonly media: string; readonly requiresAudio?: boolean; readonly tools: readonly string[];
|
|
165
|
+
}[];
|
|
166
|
+
readonly tools: readonly { readonly name: string; readonly scope: CoreCapabilityScope; readonly description: string }[];
|
|
167
|
+
readonly pluginTools: CoreCapabilitySnapshot['tools'];
|
|
168
|
+
readonly pluginControlTools: readonly string[];
|
|
169
|
+
readonly media: readonly { readonly id: string; readonly title: string; readonly note?: string; readonly capabilities: readonly CoreBridgeCapability[] }[];
|
|
170
|
+
readonly plugins: readonly { readonly id: string; readonly version: string; readonly compatibility: PluginManifest['compatibility'] }[];
|
|
171
|
+
readonly contributions: {
|
|
172
|
+
readonly providers: readonly (ProviderContribution & CatalogContribution)[];
|
|
173
|
+
readonly operations: readonly (OperationContribution & CatalogContribution)[];
|
|
174
|
+
/** No browser module path is exposed or mounted by discovery. */
|
|
175
|
+
readonly pages: readonly CatalogContribution[];
|
|
176
|
+
readonly renderers: readonly (CatalogContribution & { readonly mediaTypes: readonly string[] })[];
|
|
177
|
+
readonly settings: readonly CatalogContribution[];
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export interface CoreCapabilityCatalog {
|
|
181
|
+
snapshot(options?: { audioEnabled?: boolean; videoEnabled?: boolean; panelContributions?: PluginPanelSelection; allowedOperations?: readonly string[] }): CoreCapabilitySnapshot;
|
|
182
|
+
/** Validate an explicit local selection. Does not invoke, persist a grant or enable MCP/Web. */
|
|
183
|
+
selectOperations(ids?: readonly string[]): readonly (OperationContribution & CatalogContribution)[];
|
|
184
|
+
}
|
|
185
|
+
export function createCoreCapabilityCatalog(options?: { registry?: CorePluginRegistry }): CoreCapabilityCatalog;
|
|
186
|
+
export interface CorePluginHost {
|
|
187
|
+
call(id: string, input: Record<string, JsonValue>, options?: { dependency?: PluginDependency; signal?: AbortSignal }): Promise<{ dependency: PluginDependency; value: JsonValue }>;
|
|
188
|
+
snapshot(): { state: string; queuedWrites: number; activeWrites: number; activeReaders: number; cleanupFailed: boolean };
|
|
189
|
+
/** Caller disposes the shared registry after every host has closed. */
|
|
190
|
+
dispose(): Promise<ReturnType<CorePluginHost['snapshot']>>;
|
|
191
|
+
}
|
|
192
|
+
export function createCorePluginHost(options: { dataRoot: string; registry: CorePluginRegistry; allowedOperations?: string[] }): CorePluginHost;
|
|
193
|
+
export interface CorePluginWorkbenchSnapshot {
|
|
194
|
+
readonly contractVersion: 0;
|
|
195
|
+
readonly state: string;
|
|
196
|
+
readonly origin: string | null;
|
|
197
|
+
/** Media writes remain disabled; optional Provider editing has a separate owner gate. */
|
|
198
|
+
readonly readOnly: true;
|
|
199
|
+
readonly activeReads: number;
|
|
200
|
+
readonly cleanupFailed?: boolean;
|
|
201
|
+
}
|
|
202
|
+
export interface CorePluginWorkbench {
|
|
203
|
+
start(options?: { port?: number }): Promise<{ readonly origin: string; readonly accessToken: string; readonly loginUrl: string; readonly loginExpiresAt: string }>;
|
|
204
|
+
snapshot(): CorePluginWorkbenchSnapshot;
|
|
205
|
+
readonly closed: Promise<CorePluginWorkbenchSnapshot>;
|
|
206
|
+
dispose(): Promise<CorePluginWorkbenchSnapshot>;
|
|
207
|
+
}
|
|
208
|
+
export interface CorePasswordPluginWorkbench extends Omit<CorePluginWorkbench, 'start'> {
|
|
209
|
+
start(options?: { port?: number }): Promise<{ readonly origin: string; readonly accessToken: string; readonly loginUrl: string;
|
|
210
|
+
readonly authentication: { readonly mode: 'password'; readonly configured: boolean; readonly authId: string;
|
|
211
|
+
readonly rememberDays: 30; readonly minPasswordLength: 8; readonly maxPasswordLength: 128 } }>;
|
|
212
|
+
}
|
|
213
|
+
export interface CorePluginWorkbenchOptions {
|
|
214
|
+
dataRoot: string; registry?: CorePluginRegistry; panelContributions?: PluginPanelSelection;
|
|
215
|
+
panelManifestPath?: string; providerConfigPath?: string; allowProviderManagement?: boolean;
|
|
216
|
+
/** Omitted: no business runner. []: reconcile/read history only. IDs: explicitly enabled durable businesses. */
|
|
217
|
+
allowedOperations?: string[];
|
|
218
|
+
/** Explicit private directory outside dataRoot. Enables owner connection management. */
|
|
219
|
+
connectionStore?: string;
|
|
220
|
+
/** Enable the standard HTTP /mcp edge. Does not start a second Core. */
|
|
221
|
+
allowMcp?: boolean;
|
|
222
|
+
/** Optional persistent browser password/session store, in a private directory outside dataRoot. */
|
|
223
|
+
workbenchAuth?: { directory: string };
|
|
224
|
+
}
|
|
225
|
+
export function createCorePluginWorkbench(options: CorePluginWorkbenchOptions & { workbenchAuth: { directory: string } }): CorePasswordPluginWorkbench;
|
|
226
|
+
export function createCorePluginWorkbench(options: Omit<CorePluginWorkbenchOptions, 'workbenchAuth'> & { workbenchAuth?: undefined }): CorePluginWorkbench;
|
|
227
|
+
export function createCorePluginWorkbench(options: CorePluginWorkbenchOptions): CorePluginWorkbench | CorePasswordPluginWorkbench;
|
|
228
|
+
export function checkedPluginDependency(value: unknown): PluginDependency;
|
|
229
|
+
export function comparePluginVersions(a: string, b: string): -1 | 0 | 1;
|
|
230
|
+
|
|
231
|
+
export interface PluginTheme { readonly mode: 'light' | 'dark'; readonly density: string; readonly accent: 'iris'; }
|
|
232
|
+
export interface PluginWebSession {
|
|
233
|
+
readonly contractVersion: 0;
|
|
234
|
+
readonly instanceId: string;
|
|
235
|
+
readonly rootId?: string | null;
|
|
236
|
+
readonly operations: readonly string[];
|
|
237
|
+
readonly readOnly: boolean;
|
|
238
|
+
readonly audio: boolean;
|
|
239
|
+
readonly video: boolean;
|
|
240
|
+
readonly providerManagement: boolean;
|
|
241
|
+
readonly pluginExecution: boolean;
|
|
242
|
+
readonly pluginOperations: readonly PluginDependency[];
|
|
243
|
+
}
|
|
244
|
+
export interface PluginArtifact { readonly id: string; readonly mediaType: string; readonly label?: string; }
|
|
245
|
+
export interface PluginWebRequestOptions { readonly signal?: AbortSignal; }
|
|
246
|
+
export interface PluginBusinessRequest {
|
|
247
|
+
readonly key: string;
|
|
248
|
+
readonly operationId: string;
|
|
249
|
+
readonly dependency: PluginDependency;
|
|
250
|
+
readonly input: Readonly<Record<string, JsonValue>>;
|
|
251
|
+
}
|
|
252
|
+
export interface PluginDurableOperation {
|
|
253
|
+
readonly contractVersion: 0;
|
|
254
|
+
readonly id: string;
|
|
255
|
+
readonly rootId: string;
|
|
256
|
+
readonly action: 'plugin.execute';
|
|
257
|
+
readonly operationId: string;
|
|
258
|
+
readonly dependency: PluginDependency;
|
|
259
|
+
readonly input: Readonly<Record<string, JsonValue>>;
|
|
260
|
+
readonly inputDigest: string;
|
|
261
|
+
readonly sourceArtifactIds: readonly string[];
|
|
262
|
+
readonly revision: number;
|
|
263
|
+
readonly state: 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled' | 'interrupted';
|
|
264
|
+
readonly cancelRequested: boolean;
|
|
265
|
+
readonly createdAt: string;
|
|
266
|
+
readonly updatedAt: string;
|
|
267
|
+
readonly startedAt?: string;
|
|
268
|
+
readonly finishedAt?: string;
|
|
269
|
+
readonly failureCode?: 'dependency_unavailable' | 'permission_denied' | 'execution_failed';
|
|
270
|
+
readonly result?: { readonly value: JsonValue; readonly artifactIds: readonly string[] };
|
|
271
|
+
}
|
|
272
|
+
export interface PluginWebApi {
|
|
273
|
+
runs(query?: Readonly<Record<string, string>>, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
274
|
+
run(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
275
|
+
models(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
276
|
+
audioModels(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
277
|
+
videoModels(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
278
|
+
artifact(artifact: PluginArtifact, options?: PluginWebRequestOptions): Promise<Blob>;
|
|
279
|
+
/** Only settings mounts in an explicitly enabled owner Provider-management session. */
|
|
280
|
+
readonly providers?: {
|
|
281
|
+
read(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
282
|
+
save(input: Record<string, JsonValue>, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
283
|
+
};
|
|
284
|
+
/** Present only when the service explicitly owns a durable plugin runner. Core checks each request. */
|
|
285
|
+
readonly pluginOperations?: {
|
|
286
|
+
catalog(query?: Readonly<Record<string, string>>, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
287
|
+
history(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
288
|
+
execute(input: PluginBusinessRequest, options?: PluginWebRequestOptions): Promise<{ contractVersion: 0; operation: PluginDurableOperation; idempotentReplay: boolean }>;
|
|
289
|
+
inspect(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
290
|
+
lookup(key: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
291
|
+
stop(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** Same-origin trusted modules; Shadow DOM is a style boundary, not a security sandbox. */
|
|
295
|
+
export interface PluginWebContext<Container = unknown> {
|
|
296
|
+
readonly container: Container;
|
|
297
|
+
readonly signal: AbortSignal;
|
|
298
|
+
readonly dependency: PluginDependency;
|
|
299
|
+
readonly session: PluginWebSession;
|
|
300
|
+
readonly api: PluginWebApi;
|
|
301
|
+
readonly theme: { current(): PluginTheme; subscribe(listener: (theme: PluginTheme) => void): () => void };
|
|
302
|
+
}
|
|
303
|
+
export interface PluginPageModule<Container = unknown> { mount(context: PluginWebContext<Container>): void | (() => void) | Promise<void | (() => void)>; }
|
|
304
|
+
export interface PluginSettingsModule<Container = unknown> extends PluginPageModule<Container> {}
|
|
305
|
+
export interface PluginRendererModule<Container = unknown> {
|
|
306
|
+
mount(context: PluginWebContext<Container> & { readonly artifact: PluginArtifact }): void | (() => void) | Promise<void | (() => void)>;
|
|
307
|
+
}
|