@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
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
import { normalizeCoreOptions } from './core-contract.js';
|
|
4
|
-
import { createCoreRuntime } from './core-runtime.js';
|
|
4
|
+
import { createCoreRuntime, inspectCoreWriterLease } from './core-runtime.js';
|
|
5
5
|
import { createCommandService } from './command-service.js';
|
|
6
6
|
import { normalizeGenerationInput } from './generation-input.js';
|
|
7
7
|
import { createProviderTaskRunner } from './provider-task-runner.js';
|
|
8
8
|
import { createImageFlowService } from './image-flow.js';
|
|
9
9
|
import { createLocalCreationService } from './local-creation.js';
|
|
10
|
+
import { createLocalOperationService } from './local-operations.js';
|
|
11
|
+
import { reservePluginOperation } from './plugin-operations.js';
|
|
12
|
+
import { invokeReservedPlugin } from './plugin-execution.js';
|
|
13
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
14
|
+
import { createApplicationWriter } from './application-writer.js';
|
|
15
|
+
import { createMediaScheduler } from './media-scheduler.js';
|
|
10
16
|
|
|
11
17
|
// Internal first-slice execution surface, not a public API or an HTTP allowlist.
|
|
12
18
|
const COMMAND_MODES = Object.freeze({
|
|
@@ -15,7 +21,11 @@ const COMMAND_MODES = Object.freeze({
|
|
|
15
21
|
'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
|
|
16
22
|
'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
|
|
17
23
|
'flow.inspect': 'reader', 'flow.list': 'reader',
|
|
18
|
-
'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer'
|
|
24
|
+
'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer', 'creation.generate': 'writer',
|
|
25
|
+
'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer', 'creation.video': 'writer',
|
|
26
|
+
'creation.root': 'reader', 'creation.lookup': 'reader',
|
|
27
|
+
'execution.init': 'writer', 'operation.accept': 'writer', 'operation.transition': 'writer',
|
|
28
|
+
'operation.inspect': 'reader', 'operation.lookup': 'reader', 'operation.list': 'reader'
|
|
19
29
|
});
|
|
20
30
|
|
|
21
31
|
export class ApplicationRuntimeError extends Error {
|
|
@@ -31,7 +41,8 @@ function closed() {
|
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
/** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
|
|
34
|
-
export function createApplicationRuntime({ dataRoot, resolveImageCandidates,
|
|
44
|
+
export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter,
|
|
45
|
+
scheduling, scheduler: suppliedScheduler } = {}) {
|
|
35
46
|
const root = normalizeCoreOptions({ dataRoot }).dataRoot;
|
|
36
47
|
if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
|
|
37
48
|
throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
|
|
@@ -39,22 +50,30 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
39
50
|
if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
|
|
40
51
|
throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
|
|
41
52
|
}
|
|
53
|
+
if (resolveAudioCandidates !== undefined && typeof resolveAudioCandidates !== 'function') throw new TypeError('音频候选解析须为函数');
|
|
54
|
+
if (resolveVideoCandidates !== undefined && typeof resolveVideoCandidates !== 'function') throw new TypeError('视频候选解析须为函数');
|
|
42
55
|
let state = 'open';
|
|
43
56
|
const queue = [];
|
|
44
57
|
const active = new Set();
|
|
45
58
|
let writer = null;
|
|
46
59
|
let disposePromise;
|
|
47
60
|
let cleanupFailure;
|
|
61
|
+
const scheduler = suppliedScheduler || (scheduling ? createMediaScheduler(scheduling) : undefined);
|
|
62
|
+
const managedJobs = new Set();
|
|
63
|
+
const cooperative = scheduler ? createApplicationWriter({ dataRoot: root, policy: scheduler.policy, scheduler,
|
|
64
|
+
onFailure(error) { cleanupFailure ||= error; state = 'closing'; cooperative.abort(); stopAll(); } }) : undefined;
|
|
48
65
|
|
|
49
66
|
function snapshot() {
|
|
50
67
|
return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
|
|
51
|
-
activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure)
|
|
68
|
+
activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure),
|
|
69
|
+
...(cooperative ? { ...cooperative.snapshot(), schedulingEnabled: true } : {}) });
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
|
|
55
73
|
|
|
56
74
|
function rejectQueued(job, error) {
|
|
57
75
|
detach(job);
|
|
76
|
+
job.resource.emitDestroy();
|
|
58
77
|
job.reject(error);
|
|
59
78
|
}
|
|
60
79
|
|
|
@@ -80,7 +99,16 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
80
99
|
try {
|
|
81
100
|
runtime.start();
|
|
82
101
|
result = await job.action(runtime);
|
|
83
|
-
} catch (error) {
|
|
102
|
+
} catch (error) {
|
|
103
|
+
failed = true; operationError = error;
|
|
104
|
+
if (job.mode === 'reader' && error?.code === 'IRIS_ARTIFACT_INDEX_INVALID' && error.missingFile === true) {
|
|
105
|
+
const lease = inspectCoreWriterLease(root);
|
|
106
|
+
if (lease.status === 'present' && lease.ownerStatus === 'alive') {
|
|
107
|
+
operationError = new ApplicationRuntimeError('IRIS_CORE_DATA_ROOT_BUSY', '作品读取遇到活动写入,请在写入结束后显式重试');
|
|
108
|
+
Object.defineProperty(operationError, 'cause', { value: error });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
84
112
|
try { await (job.disposal || runtime.dispose()); }
|
|
85
113
|
catch (error) {
|
|
86
114
|
cleanupFailure ||= error;
|
|
@@ -97,7 +125,7 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
97
125
|
|
|
98
126
|
function start(job) {
|
|
99
127
|
active.add(job);
|
|
100
|
-
job.done = Promise.resolve().then(async () => {
|
|
128
|
+
job.done = job.resource.runInAsyncScope(() => Promise.resolve().then(async () => {
|
|
101
129
|
let result, error, failed = false;
|
|
102
130
|
try { result = await perform(job); }
|
|
103
131
|
catch (caught) { failed = true; error = caught; }
|
|
@@ -105,8 +133,9 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
105
133
|
active.delete(job);
|
|
106
134
|
if (writer === job) writer = null;
|
|
107
135
|
if (failed) job.reject(error); else job.resolve(result);
|
|
136
|
+
job.resource.emitDestroy();
|
|
108
137
|
dispatch();
|
|
109
|
-
});
|
|
138
|
+
}));
|
|
110
139
|
}
|
|
111
140
|
|
|
112
141
|
function dispatch() {
|
|
@@ -115,12 +144,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
115
144
|
start(writer);
|
|
116
145
|
}
|
|
117
146
|
|
|
118
|
-
function schedule(mode, action, { signal } = {}) {
|
|
147
|
+
function schedule(mode, action, { signal, managed = false, transaction = false, finalize = false, key } = {}) {
|
|
119
148
|
if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
|
|
120
149
|
if (state !== 'open') return Promise.reject(closed());
|
|
121
150
|
if (signal?.aborted) return Promise.reject(aborted());
|
|
151
|
+
if (mode === 'writer' && cooperative) {
|
|
152
|
+
const promise = cooperative.execute(action, { signal, managed: managed || transaction, transaction, finalize, key });
|
|
153
|
+
managedJobs.add(promise);
|
|
154
|
+
void promise.then(() => managedJobs.delete(promise), () => managedJobs.delete(promise));
|
|
155
|
+
return promise;
|
|
156
|
+
}
|
|
122
157
|
let job;
|
|
123
|
-
const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false
|
|
158
|
+
const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false,
|
|
159
|
+
resource: new AsyncResource('IrisApplicationJob') }; });
|
|
124
160
|
job.onAbort = () => {
|
|
125
161
|
const index = queue.indexOf(job);
|
|
126
162
|
if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
|
|
@@ -146,15 +182,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
146
182
|
const ownedInput = structuredClone(input);
|
|
147
183
|
return schedule(COMMAND_MODES[name], runtime => {
|
|
148
184
|
const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
|
|
149
|
-
const
|
|
185
|
+
const resolve = () => resolveTaskAdapter(task, { signal: runtime.signal });
|
|
186
|
+
const adapter = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
150
187
|
if (runtime.signal.aborted) throw aborted();
|
|
151
188
|
return adapter;
|
|
152
189
|
} } : {};
|
|
153
|
-
if (name.startsWith('
|
|
190
|
+
if (name === 'execution.init' || name.startsWith('operation.')) return createLocalOperationService(runtime).execute(name, ownedInput);
|
|
191
|
+
if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates,
|
|
154
192
|
onPrepared: options?.onPrepared }).execute(name, ownedInput);
|
|
155
|
-
if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates }).execute(name, ownedInput);
|
|
193
|
+
if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates }).execute(name, ownedInput);
|
|
156
194
|
return createCommandService(runtime, ports).execute(name, ownedInput);
|
|
157
|
-
}, options
|
|
195
|
+
}, { ...options, managed: true,
|
|
196
|
+
key: ownedInput?.run_id ? `run:${ownedInput.run_id}` : ownedInput?.task_id ? `task:${ownedInput.task_id}`
|
|
197
|
+
: name.startsWith('creation.') && ownedInput?.key ? `creation:${name}:${ownedInput.key}` : undefined });
|
|
158
198
|
}
|
|
159
199
|
|
|
160
200
|
async function generateImage(input, options) {
|
|
@@ -163,18 +203,31 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
163
203
|
throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
|
|
164
204
|
}
|
|
165
205
|
return schedule('writer', async runtime => {
|
|
166
|
-
const
|
|
206
|
+
const resolve = () => resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
|
|
207
|
+
const candidates = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
167
208
|
if (runtime.signal.aborted) throw aborted();
|
|
168
209
|
return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
|
|
169
210
|
providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
|
|
170
|
-
}, options);
|
|
211
|
+
}, { ...options, managed: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Explicit trusted assembly only. No HTTP/MCP route or persistent Operation action is added. */
|
|
215
|
+
async function executePluginOperation(registry, id, input, { allowedOperations = [], dependency, signal } = {}) {
|
|
216
|
+
const reserved = reservePluginOperation(registry, id, input, allowedOperations, dependency);
|
|
217
|
+
try {
|
|
218
|
+
return await schedule(reserved.mode, runtime => runtime.run(reserved.mode === 'reader' ? 'inspect' : 'execute',
|
|
219
|
+
() => invokeReservedPlugin(runtime, reserved)), { signal });
|
|
220
|
+
} finally { reserved.release(); }
|
|
171
221
|
}
|
|
172
222
|
|
|
173
223
|
function dispose() {
|
|
174
224
|
if (disposePromise) return disposePromise;
|
|
175
225
|
state = 'closing';
|
|
176
226
|
stopAll();
|
|
177
|
-
|
|
227
|
+
cooperative?.abort();
|
|
228
|
+
disposePromise = Promise.allSettled([...active].map(job => job.done).concat([...managedJobs])).then(async () => {
|
|
229
|
+
await cooperative?.dispose();
|
|
230
|
+
if (!suppliedScheduler) scheduler?.dispose();
|
|
178
231
|
if (cleanupFailure) throw cleanupFailure;
|
|
179
232
|
state = 'closed';
|
|
180
233
|
return snapshot();
|
|
@@ -182,5 +235,8 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
182
235
|
return disposePromise;
|
|
183
236
|
}
|
|
184
237
|
|
|
185
|
-
return Object.freeze({ execute, generateImage, run, dispose, snapshot
|
|
238
|
+
return Object.freeze({ execute, generateImage, executePluginOperation, run, dispose, snapshot, scheduler,
|
|
239
|
+
schedulingPhase(key) { return cooperative?.phase(key) || null; },
|
|
240
|
+
runMedia(action, options) { return schedule('writer', action, { ...options, managed: true }); },
|
|
241
|
+
transaction(action, { finalize = false } = {}) { return schedule('writer', action, { transaction: true, finalize }); } });
|
|
186
242
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
2
|
+
import { createCoreRuntime } from './core-runtime.js';
|
|
3
|
+
import { schedulingError } from './scheduling-policy.js';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
|
|
6
|
+
const aborted = () => Object.assign(schedulingError('IRIS_APP_ABORTED', 'Iris 操作已取消'), { name: 'AbortError' });
|
|
7
|
+
const closed = () => schedulingError('IRIS_APP_CLOSED', 'Iris 应用运行时正在关闭或已关闭');
|
|
8
|
+
|
|
9
|
+
/** Internal cooperative writer. Only known media code may loan its write turn. */
|
|
10
|
+
export function createApplicationWriter({ dataRoot, policy, scheduler, onFailure }) {
|
|
11
|
+
const pending = [], active = new Set(), taskGate = {};
|
|
12
|
+
let rootRuntime, owner, closing = false, failure, retiring, disposal;
|
|
13
|
+
const snapshot = () => ({ queuedWrites: pending.filter(entry => entry.kind === 'start').length,
|
|
14
|
+
activeWrites: Number(Boolean(rootRuntime)), writeTurn: Number(Boolean(owner)),
|
|
15
|
+
managedWrites: active.size, waitingWrites: [...active].filter(job => job.parked).length, cleanupFailed: Boolean(failure) });
|
|
16
|
+
|
|
17
|
+
function remove(entry) {
|
|
18
|
+
const index = pending.indexOf(entry);
|
|
19
|
+
if (index >= 0) pending.splice(index, 1);
|
|
20
|
+
clearTimeout(entry.timer); entry.signal?.removeEventListener('abort', entry.abort);
|
|
21
|
+
}
|
|
22
|
+
function enqueue(entry) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
entry.resolve = resolve; entry.reject = reject;
|
|
25
|
+
pending.push(entry); pump();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function retire() {
|
|
29
|
+
const runtime = rootRuntime;
|
|
30
|
+
try { await runtime.dispose(); }
|
|
31
|
+
catch (error) {
|
|
32
|
+
failure = error; closing = true; onFailure?.(error);
|
|
33
|
+
for (const entry of [...pending]) { remove(entry); entry.reject(error); }
|
|
34
|
+
throw error;
|
|
35
|
+
} finally { rootRuntime = null; retiring = null; pump(); }
|
|
36
|
+
}
|
|
37
|
+
function pump() {
|
|
38
|
+
if (owner || retiring) return;
|
|
39
|
+
// A waiting exclusive callback cannot block the resumes that let earlier media jobs finish.
|
|
40
|
+
let index = pending.findIndex(entry => entry.kind !== 'start' || entry.transaction);
|
|
41
|
+
if (index < 0) index = 0;
|
|
42
|
+
let entry = pending[index];
|
|
43
|
+
if (!entry) {
|
|
44
|
+
if (rootRuntime && !active.size) {
|
|
45
|
+
retiring = Promise.resolve().then(retire); void retiring.catch(() => {});
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (entry.kind === 'start' && performance.now() >= entry.deadline) {
|
|
50
|
+
remove(entry); entry.reject(schedulingError('IRIS_SCHEDULER_QUEUE_TIMEOUT', '本地写入等候超时;未开始本次操作')); pump(); return;
|
|
51
|
+
}
|
|
52
|
+
if (entry.kind === 'start' && (!entry.managed && active.size || [...active].some(job => !job.managed))) return;
|
|
53
|
+
remove(entry);
|
|
54
|
+
if (entry.kind === 'start') {
|
|
55
|
+
try {
|
|
56
|
+
if (!rootRuntime) { rootRuntime = createCoreRuntime({ dataRoot, mode: 'writer' }); rootRuntime.start(); }
|
|
57
|
+
} catch (error) {
|
|
58
|
+
entry.reject(error);
|
|
59
|
+
// start may have retained a failed lease; dispose must report that failure.
|
|
60
|
+
retiring = Promise.resolve().then(retire); void retiring.catch(() => {}); return;
|
|
61
|
+
}
|
|
62
|
+
const job = entry.job;
|
|
63
|
+
active.add(job); owner = job;
|
|
64
|
+
entry.resolve();
|
|
65
|
+
} else { owner = entry.job; entry.resolve(); }
|
|
66
|
+
}
|
|
67
|
+
function release(job) {
|
|
68
|
+
if (owner !== job) throw new Error('Iris writer turn ownership mismatch');
|
|
69
|
+
owner = null;
|
|
70
|
+
let error;
|
|
71
|
+
try { scheduler?.changed(dataRoot); } catch (caught) { error = caught; }
|
|
72
|
+
pump(); return error;
|
|
73
|
+
}
|
|
74
|
+
async function networkWait(job, action, reason = 'network') {
|
|
75
|
+
if (!job.managed || job.parked) throw new Error('Iris write turn can only be loaned once by managed media');
|
|
76
|
+
job.parked = reason; const releaseError = release(job);
|
|
77
|
+
let result, error;
|
|
78
|
+
try { if (releaseError) throw releaseError; result = await action(); } catch (caught) { error = caught; }
|
|
79
|
+
// Even an aborted operation reacquires its turn before recording acceptance/terminal facts.
|
|
80
|
+
await enqueue({ kind: 'resume', job }); job.parked = null; job.phase = null;
|
|
81
|
+
if (error) throw error;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
async function checkpoint(job, action) {
|
|
85
|
+
if (job.finished) throw closed();
|
|
86
|
+
const parked = job.parked;
|
|
87
|
+
if (parked) await enqueue({ kind: 'checkpoint', job });
|
|
88
|
+
if (owner !== job) throw new Error('Core checkpoint requires the current write turn');
|
|
89
|
+
try {
|
|
90
|
+
const value = action();
|
|
91
|
+
if (value && typeof value.then === 'function') throw new TypeError('Core checkpoint 必须同步完成');
|
|
92
|
+
return value;
|
|
93
|
+
} finally { if (parked) { const error = release(job); if (error) throw error; } }
|
|
94
|
+
}
|
|
95
|
+
async function execute(action, { signal, managed = false, transaction = false, finalize = false, key } = {}) {
|
|
96
|
+
if (closing) throw closed();
|
|
97
|
+
signal?.throwIfAborted();
|
|
98
|
+
if (!finalize && active.size + pending.filter(entry => entry.kind === 'start').length >= policy.queueLimit) {
|
|
99
|
+
throw schedulingError('IRIS_SCHEDULER_QUEUE_FULL', '本地写入队列已满;请查询原操作后再明确重试');
|
|
100
|
+
}
|
|
101
|
+
const resource = new AsyncResource('IrisApplicationWriter'), controller = new AbortController();
|
|
102
|
+
const job = { managed, parked: null, key, controller, cleanup: [], inFlight: new Set(), finished: false };
|
|
103
|
+
// Only the runner uses finalize for short terminal writes of already accepted Operations.
|
|
104
|
+
// They must drain under the writer even when new work has reached its admission budget.
|
|
105
|
+
const entry = { kind: 'start', job, signal, managed, transaction, deadline: finalize ? Infinity : performance.now() + policy.queueTimeoutMs };
|
|
106
|
+
// Same durable Run/creation key must not overlap even if its candidate resolver waits.
|
|
107
|
+
if (key && ([...active].some(other => other.key === key) || pending.some(other => other.job.key === key))) {
|
|
108
|
+
resource.emitDestroy(); throw schedulingError('IRIS_APP_OPERATION_BUSY', '原 Run 或创建 Key 正在执行;请查询原操作');
|
|
109
|
+
}
|
|
110
|
+
entry.abort = () => {
|
|
111
|
+
controller.abort();
|
|
112
|
+
if (!active.has(job)) { remove(entry); entry.reject(aborted()); pump(); }
|
|
113
|
+
};
|
|
114
|
+
signal?.addEventListener('abort', entry.abort, { once: true });
|
|
115
|
+
const start = enqueue(entry);
|
|
116
|
+
// Queued starts are bounded; resumes/terminal persistence never expire behind this timer.
|
|
117
|
+
if (pending.includes(entry) && !finalize) entry.timer = setTimeout(() => {
|
|
118
|
+
remove(entry); entry.reject(schedulingError('IRIS_SCHEDULER_QUEUE_TIMEOUT', '本地写入等候超时;未开始本次操作')); pump();
|
|
119
|
+
}, Math.max(1, entry.deadline - performance.now()));
|
|
120
|
+
let result, operationError;
|
|
121
|
+
try {
|
|
122
|
+
await start;
|
|
123
|
+
// The queue entry's abort listener was removed at admission; active jobs keep their own signal.
|
|
124
|
+
signal?.addEventListener('abort', entry.abort, { once: true });
|
|
125
|
+
if (signal?.aborted) controller.abort();
|
|
126
|
+
controller.signal.throwIfAborted();
|
|
127
|
+
const runtime = Object.freeze({
|
|
128
|
+
get signal() { return controller.signal; }, taskGate,
|
|
129
|
+
run(operation, callback) {
|
|
130
|
+
if (job.finished) throw closed();
|
|
131
|
+
if (owner !== job) throw new Error('Core operation requires the current write turn');
|
|
132
|
+
const promise = rootRuntime.run(operation, context => callback(Object.freeze({ ...context, signal: controller.signal })));
|
|
133
|
+
job.inFlight.add(promise);
|
|
134
|
+
void promise.then(() => job.inFlight.delete(promise), () => job.inFlight.delete(promise));
|
|
135
|
+
return promise;
|
|
136
|
+
},
|
|
137
|
+
snapshot: () => rootRuntime.snapshot(),
|
|
138
|
+
onDispose(callback) {
|
|
139
|
+
if (job.finished) throw closed();
|
|
140
|
+
if (typeof callback !== 'function') throw new TypeError('Core cleanup 必须是函数');
|
|
141
|
+
job.cleanup.push(callback); return () => { const i = job.cleanup.indexOf(callback); if (i < 0) return false; job.cleanup.splice(i, 1); return true; };
|
|
142
|
+
},
|
|
143
|
+
networkWait: (callback, reason) => networkWait(job, callback, reason),
|
|
144
|
+
checkpoint: callback => checkpoint(job, callback),
|
|
145
|
+
reportScheduling(phase) { if (job.parked) job.phase = phase; },
|
|
146
|
+
scheduler,
|
|
147
|
+
schedulingState: () => job.parked || 'writing'
|
|
148
|
+
});
|
|
149
|
+
result = await resource.runInAsyncScope(action, undefined, runtime);
|
|
150
|
+
} catch (error) { operationError = error; }
|
|
151
|
+
finally {
|
|
152
|
+
signal?.removeEventListener('abort', entry.abort);
|
|
153
|
+
if (active.has(job)) {
|
|
154
|
+
while (job.inFlight.size) await Promise.allSettled([...job.inFlight]);
|
|
155
|
+
job.finished = true; controller.abort();
|
|
156
|
+
try { for (const callback of job.cleanup.reverse()) await callback(); }
|
|
157
|
+
catch (error) {
|
|
158
|
+
failure = error; closing = true;
|
|
159
|
+
// A failed per-job cleanup keeps the same retained-lease semantics as CoreRuntime.
|
|
160
|
+
rootRuntime.onDispose(() => { throw error; });
|
|
161
|
+
onFailure?.(error); operationError ||= error;
|
|
162
|
+
}
|
|
163
|
+
active.delete(job);
|
|
164
|
+
if (owner === job) { const error = release(job); operationError ||= error; } else pump();
|
|
165
|
+
if (retiring) {
|
|
166
|
+
try { await retiring; }
|
|
167
|
+
catch (error) { operationError = operationError ? Object.assign(new AggregateError([operationError, error], '操作和写者释放均失败'), { code: error.code }) : error; }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
resource.emitDestroy();
|
|
171
|
+
}
|
|
172
|
+
if (operationError) throw operationError;
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
function dispose() {
|
|
176
|
+
if (disposal) return disposal;
|
|
177
|
+
closing = true;
|
|
178
|
+
for (const entry of [...pending]) if (entry.kind === 'start') { remove(entry); entry.reject(closed()); }
|
|
179
|
+
for (const job of active) job.controller.abort();
|
|
180
|
+
disposal = (async () => {
|
|
181
|
+
// The application awaits admitted jobs before invoking this final drain.
|
|
182
|
+
if (active.size) throw new Error('Iris writer disposed before jobs drained');
|
|
183
|
+
pump(); if (retiring) await retiring;
|
|
184
|
+
if (failure) throw failure;
|
|
185
|
+
})();
|
|
186
|
+
return disposal;
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze({ execute, snapshot, dispose, phase(key) { const job = [...active].find(job => job.key === key); return job?.phase || job?.parked || null; }, abort() {
|
|
189
|
+
closing = true;
|
|
190
|
+
for (const entry of [...pending]) if (entry.kind === 'start') { remove(entry); entry.reject(closed()); }
|
|
191
|
+
for (const job of active) job.controller.abort();
|
|
192
|
+
} });
|
|
193
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { normalizeGenerationInput } from './generation-input.js';
|
|
2
|
+
import { parseModelRef } from './models.js';
|
|
3
|
+
|
|
4
|
+
export const AUDIO_RECIPES = Object.freeze({
|
|
5
|
+
'speech/v0': Object.freeze({ capability: 'tts', step: 'speak' }),
|
|
6
|
+
'transcribe/v0': Object.freeze({ capability: 'transcribe', step: 'transcribe' })
|
|
7
|
+
});
|
|
8
|
+
export const MAX_AUDIO_INPUT_BYTES = 20 * 1024 * 1024;
|
|
9
|
+
export const AUDIO_TYPES = Object.freeze({ 'audio/wav': 'wav', 'audio/mpeg': 'mp3', 'audio/ogg': 'ogg', 'audio/flac': 'flac' });
|
|
10
|
+
export const isAudioRecipe = recipe => Object.hasOwn(AUDIO_RECIPES, recipe);
|
|
11
|
+
const invalid = message => { throw Object.assign(new Error(message), { code: 'IRIS_AUDIO_INPUT_INVALID' }); };
|
|
12
|
+
|
|
13
|
+
export function normalizeAudioFlowInput(recipe, input) {
|
|
14
|
+
if (!isAudioRecipe(recipe) || !input || typeof input !== 'object' || Array.isArray(input)) invalid('不支持的音频流程输入');
|
|
15
|
+
const speech = recipe === 'speech/v0', required = speech ? ['model_ref', 'text'] : ['model_ref', 'source_artifact_id'];
|
|
16
|
+
if (Object.keys(input).some(key => ![...required, ...(speech ? ['voice'] : [])].includes(key))
|
|
17
|
+
|| required.some(key => typeof input[key] !== 'string') || !parseModelRef(input.model_ref)
|
|
18
|
+
|| input.voice !== undefined && typeof input.voice !== 'string') invalid('请提供明确模型和文字或当前工作区的音频作品');
|
|
19
|
+
if (speech) {
|
|
20
|
+
const normalized = normalizeGenerationInput('tts', input);
|
|
21
|
+
return { model_ref: normalized.modelRef, ...normalized.providerInput };
|
|
22
|
+
}
|
|
23
|
+
if (!/^artifact_[a-f0-9]{24}$/.test(input.source_artifact_id)) invalid('转写来源须为当前工作区的音频作品');
|
|
24
|
+
return { model_ref: input.model_ref, source_artifact_id: input.source_artifact_id };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Bounded container sniffing, not a decoder or a claim of provider compatibility. */
|
|
28
|
+
export function readImportAudio(bytes, mediaType) {
|
|
29
|
+
if (!(bytes instanceof Uint8Array) || !bytes.length || bytes.length > MAX_AUDIO_INPUT_BYTES) invalid('音频须为最多20 MiB的WAV、MP3、OGG或FLAC');
|
|
30
|
+
const value = Buffer.from(bytes);
|
|
31
|
+
const actual = value.length >= 12 && value.toString('ascii', 0, 4) === 'RIFF' && value.toString('ascii', 8, 12) === 'WAVE' ? 'audio/wav'
|
|
32
|
+
: value.length >= 10 && (value.toString('ascii', 0, 3) === 'ID3' || value[0] === 0xff && (value[1] & 0xe0) === 0xe0) ? 'audio/mpeg'
|
|
33
|
+
: value.length >= 27 && value.toString('ascii', 0, 4) === 'OggS' ? 'audio/ogg'
|
|
34
|
+
: value.length >= 8 && value.toString('ascii', 0, 4) === 'fLaC' ? 'audio/flac' : null;
|
|
35
|
+
if (!actual || actual !== mediaType) invalid('音频容器与Content-Type不一致,或不是支持的格式');
|
|
36
|
+
return { bytes: value, mediaType: actual, metadata: {} };
|
|
37
|
+
}
|
package/lib/client.d.ts
CHANGED