@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 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 +63 -1
- package/README.en.md +20 -13
- package/README.md +20 -13
- package/bin/dsh-iris.js +143 -51
- package/docs/AGENT_CORE_QUICKSTART.md +2 -2
- package/docs/AGENT_QUICKSTART.md +1 -1
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +20 -4
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +1 -1
- package/docs/CORE_CONNECTIONS.md +3 -1
- package/docs/CORE_QUICKSTART.md +7 -5
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
- package/docs/CORE_SERVICE.md +9 -5
- package/docs/CORE_SKILLS.md +2 -2
- package/docs/DSH_COMPATIBILITY.md +11 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RUNTIME.md +2 -2
- package/docs/HEADLESS_CLI.md +29 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +2 -2
- package/docs/IMAGE_GENERATION.md +1 -1
- package/docs/LOCAL_API.md +2 -2
- package/docs/LOCAL_API_CONTRACT.md +17 -15
- package/docs/LOCAL_CREATION.md +2 -2
- package/docs/LOCAL_OPERATIONS.md +2 -2
- package/docs/LOCAL_SELECTION.md +1 -1
- package/docs/LOCAL_WEB.md +11 -5
- package/docs/MCP.md +30 -9
- package/docs/MEDIA_SCHEDULING.md +72 -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 +1 -1
- package/docs/PLUGINS.md +9 -5
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +15 -2
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +8 -4
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +13 -7
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +3 -3
- package/docs/WEB_RECOVERY.md +2 -2
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +2 -2
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +2 -2
- package/lib/actions.js +5 -4
- package/lib/adapters.js +58 -41
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +44 -15
- package/lib/application-writer.js +193 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +62 -25
- package/lib/comfyui-adapter.js +3 -2
- package/lib/comfyui-client.js +10 -1
- package/lib/command-service.js +18 -5
- package/lib/config.js +13 -9
- package/lib/configured-image-runtime.js +28 -9
- package/lib/connection-skill.js +9 -0
- package/lib/core-artifact-store.js +68 -26
- package/lib/core-bridge-tools.js +5 -3
- package/lib/core-capability-catalog.js +21 -11
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-service-client.js +6 -2
- package/lib/core-tasks.js +86 -11
- package/lib/creation-request-store.js +8 -5
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +1 -0
- package/lib/flow-store.js +64 -17
- package/lib/image-flow.js +39 -17
- package/lib/index.js +10 -5
- package/lib/local-api-projection.js +6 -2
- package/lib/local-api.js +137 -49
- package/lib/local-creation.js +12 -5
- package/lib/local-operations.js +2 -2
- package/lib/local-request-root.js +2 -2
- package/lib/local-web-assets.js +2 -0
- package/lib/mcp-http.js +82 -9
- package/lib/mcp-server.js +23 -1
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +25 -8
- package/lib/music-transport.js +4 -2
- 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 +83 -38
- package/lib/operation-store.js +6 -6
- package/lib/plugins.d.ts +25 -6
- package/lib/plugins.js +2 -1
- package/lib/provider-adapter.js +2 -2
- package/lib/provider-adapters.js +37 -5
- package/lib/provider-catalog.js +33 -16
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +52 -22
- 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 +5 -5
- package/lib/provider-health.js +10 -5
- package/lib/provider-monitor.js +5 -3
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +3 -2
- package/lib/provider-registry.js +15 -5
- package/lib/provider-response.js +10 -0
- package/lib/provider-settings.js +14 -12
- package/lib/provider-task-runner.js +113 -37
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- 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 +2 -0
- package/lib/web/app.js +107 -46
- package/lib/web/connections.js +151 -12
- package/lib/web/creation.js +29 -25
- package/lib/web/flow-view.js +4 -3
- package/lib/web/gallery.js +17 -16
- package/lib/web/index.html +58 -5
- package/lib/web/media-client.js +18 -4
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +4 -2
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/providers.js +34 -13
- package/lib/web/recovery.js +2 -2
- package/lib/web/styles.css +54 -1
- package/lib/web/transport.js +6 -1
- package/lib/workbench-artifacts.js +3 -2
- package/package.json +5 -2
- package/skills/iris-core/references/comfyui.md +1 -1
- 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 +3 -3
package/lib/api.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import crypto from 'node:crypto';
|
|
16
|
+
import { pipeline } from 'node:stream/promises';
|
|
16
17
|
import { irisHome, capabilitiesOf, onChange as onConfigChange, providerHealthSnapshot } from './config.js';
|
|
17
18
|
import { mimeOf, mediaUrl } from './media.js';
|
|
18
19
|
import * as tasks from './tasks.js';
|
|
@@ -65,7 +66,7 @@ function providerPublic(p) {
|
|
|
65
66
|
mediaBaseUrl: p.mediaBaseUrl || '',
|
|
66
67
|
enabled: p.enabled !== false,
|
|
67
68
|
...selectMediaProtocol(p),
|
|
68
|
-
apiKeyHint: p.dshProvider ? '由 DSH 管理' : keyHint(p.apiKey),
|
|
69
|
+
apiKeyHint: p.dshProvider ? '由 DSH 管理' : p.apiKeyEnv ? '由环境变量提供' : keyHint(p.apiKey),
|
|
69
70
|
...(p.dshProvider ? { dshProvider: p.dshProvider } : {}),
|
|
70
71
|
capabilities: capabilitiesOf(p),
|
|
71
72
|
// 迁移/推断结果是否与原始声明不同(UI 提示旧配置已推断)
|
|
@@ -445,19 +446,22 @@ function handleWorkbenchPost(req, res, execute, download = false, core) {
|
|
|
445
446
|
|
|
446
447
|
async function serveCoreArtifactMedia(artifactId, method, res, core) {
|
|
447
448
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
449
|
+
await core.streamCoreArtifactMediaForDsh(artifactId, async result => {
|
|
450
|
+
if (res.writableEnded || res.destroyed) return;
|
|
451
|
+
res.writeHead(200, {
|
|
452
|
+
'Content-Type': result.artifact.mediaType,
|
|
453
|
+
'Content-Length': result.artifact.size,
|
|
454
|
+
'Content-Disposition': 'inline',
|
|
455
|
+
'Cache-Control': 'private, max-age=3600',
|
|
456
|
+
'X-Content-Type-Options': 'nosniff',
|
|
457
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
458
|
+
'Referrer-Policy': 'no-referrer'
|
|
459
|
+
});
|
|
460
|
+
if (method === 'HEAD') res.end();
|
|
461
|
+
else await pipeline(result.stream, res);
|
|
462
|
+
}, { headOnly: method === 'HEAD' });
|
|
460
463
|
} catch (_) {
|
|
464
|
+
if (res.headersSent || res.destroyed) { res.destroy(); return; }
|
|
461
465
|
return sendJson(res, 404, { error: 'not found' });
|
|
462
466
|
}
|
|
463
467
|
}
|
|
@@ -567,10 +571,16 @@ function handleCoreTaskRetry(taskId, req, res, core) {
|
|
|
567
571
|
error.statusCode = 400;
|
|
568
572
|
throw error;
|
|
569
573
|
}
|
|
574
|
+
if (body.video_mode !== undefined && ['img_data_url', 'first_frame_path', 'audio_path'].some(field => Object.hasOwn(body, field))) {
|
|
575
|
+
throw Object.assign(new Error('DSH 视频重试仅提供明确的文生入口;图生/数字人视频请通过视频工具或 CLI 重新提供首帧和音频'), {
|
|
576
|
+
code: 'IRIS_COMMAND_INPUT_INVALID'
|
|
577
|
+
});
|
|
578
|
+
}
|
|
570
579
|
const prompt = String(body.prompt || '');
|
|
571
580
|
const result = await core.retryProviderTaskForDsh({
|
|
572
581
|
taskId,
|
|
573
582
|
prompt,
|
|
583
|
+
...(body.video_mode !== undefined ? { videoMode: body.video_mode } : {}),
|
|
574
584
|
...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
|
|
575
585
|
...(body.text !== undefined ? { text: String(body.text) } : {}),
|
|
576
586
|
...(body.voice !== undefined ? { voice: String(body.voice) } : {}),
|
|
@@ -10,6 +10,9 @@ import { createLocalCreationService } from './local-creation.js';
|
|
|
10
10
|
import { createLocalOperationService } from './local-operations.js';
|
|
11
11
|
import { reservePluginOperation } from './plugin-operations.js';
|
|
12
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';
|
|
13
16
|
|
|
14
17
|
// Internal first-slice execution surface, not a public API or an HTTP allowlist.
|
|
15
18
|
const COMMAND_MODES = Object.freeze({
|
|
@@ -19,7 +22,7 @@ const COMMAND_MODES = Object.freeze({
|
|
|
19
22
|
'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
|
|
20
23
|
'flow.inspect': 'reader', 'flow.list': 'reader',
|
|
21
24
|
'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer', 'creation.generate': 'writer',
|
|
22
|
-
'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer',
|
|
25
|
+
'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer', 'creation.video': 'writer',
|
|
23
26
|
'creation.root': 'reader', 'creation.lookup': 'reader',
|
|
24
27
|
'execution.init': 'writer', 'operation.accept': 'writer', 'operation.transition': 'writer',
|
|
25
28
|
'operation.inspect': 'reader', 'operation.lookup': 'reader', 'operation.list': 'reader'
|
|
@@ -38,7 +41,8 @@ function closed() {
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
|
|
41
|
-
export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter
|
|
44
|
+
export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter,
|
|
45
|
+
scheduling, scheduler: suppliedScheduler } = {}) {
|
|
42
46
|
const root = normalizeCoreOptions({ dataRoot }).dataRoot;
|
|
43
47
|
if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
|
|
44
48
|
throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
|
|
@@ -47,22 +51,29 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
47
51
|
throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
|
|
48
52
|
}
|
|
49
53
|
if (resolveAudioCandidates !== undefined && typeof resolveAudioCandidates !== 'function') throw new TypeError('音频候选解析须为函数');
|
|
54
|
+
if (resolveVideoCandidates !== undefined && typeof resolveVideoCandidates !== 'function') throw new TypeError('视频候选解析须为函数');
|
|
50
55
|
let state = 'open';
|
|
51
56
|
const queue = [];
|
|
52
57
|
const active = new Set();
|
|
53
58
|
let writer = null;
|
|
54
59
|
let disposePromise;
|
|
55
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;
|
|
56
65
|
|
|
57
66
|
function snapshot() {
|
|
58
67
|
return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
|
|
59
|
-
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 } : {}) });
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
|
|
63
73
|
|
|
64
74
|
function rejectQueued(job, error) {
|
|
65
75
|
detach(job);
|
|
76
|
+
job.resource.emitDestroy();
|
|
66
77
|
job.reject(error);
|
|
67
78
|
}
|
|
68
79
|
|
|
@@ -114,7 +125,7 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
114
125
|
|
|
115
126
|
function start(job) {
|
|
116
127
|
active.add(job);
|
|
117
|
-
job.done = Promise.resolve().then(async () => {
|
|
128
|
+
job.done = job.resource.runInAsyncScope(() => Promise.resolve().then(async () => {
|
|
118
129
|
let result, error, failed = false;
|
|
119
130
|
try { result = await perform(job); }
|
|
120
131
|
catch (caught) { failed = true; error = caught; }
|
|
@@ -122,8 +133,9 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
122
133
|
active.delete(job);
|
|
123
134
|
if (writer === job) writer = null;
|
|
124
135
|
if (failed) job.reject(error); else job.resolve(result);
|
|
136
|
+
job.resource.emitDestroy();
|
|
125
137
|
dispatch();
|
|
126
|
-
});
|
|
138
|
+
}));
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
function dispatch() {
|
|
@@ -132,12 +144,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
132
144
|
start(writer);
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
function schedule(mode, action, { signal } = {}) {
|
|
147
|
+
function schedule(mode, action, { signal, managed = false, transaction = false, finalize = false, key } = {}) {
|
|
136
148
|
if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
|
|
137
149
|
if (state !== 'open') return Promise.reject(closed());
|
|
138
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
|
+
}
|
|
139
157
|
let job;
|
|
140
|
-
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') }; });
|
|
141
160
|
job.onAbort = () => {
|
|
142
161
|
const index = queue.indexOf(job);
|
|
143
162
|
if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
|
|
@@ -163,16 +182,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
163
182
|
const ownedInput = structuredClone(input);
|
|
164
183
|
return schedule(COMMAND_MODES[name], runtime => {
|
|
165
184
|
const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
|
|
166
|
-
const
|
|
185
|
+
const resolve = () => resolveTaskAdapter(task, { signal: runtime.signal });
|
|
186
|
+
const adapter = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
167
187
|
if (runtime.signal.aborted) throw aborted();
|
|
168
188
|
return adapter;
|
|
169
189
|
} } : {};
|
|
170
190
|
if (name === 'execution.init' || name.startsWith('operation.')) return createLocalOperationService(runtime).execute(name, ownedInput);
|
|
171
|
-
if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates,
|
|
191
|
+
if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates,
|
|
172
192
|
onPrepared: options?.onPrepared }).execute(name, ownedInput);
|
|
173
|
-
if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates }).execute(name, ownedInput);
|
|
193
|
+
if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates }).execute(name, ownedInput);
|
|
174
194
|
return createCommandService(runtime, ports).execute(name, ownedInput);
|
|
175
|
-
}, 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 });
|
|
176
198
|
}
|
|
177
199
|
|
|
178
200
|
async function generateImage(input, options) {
|
|
@@ -181,11 +203,12 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
181
203
|
throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
|
|
182
204
|
}
|
|
183
205
|
return schedule('writer', async runtime => {
|
|
184
|
-
const
|
|
206
|
+
const resolve = () => resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
|
|
207
|
+
const candidates = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
185
208
|
if (runtime.signal.aborted) throw aborted();
|
|
186
209
|
return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
|
|
187
210
|
providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
|
|
188
|
-
}, options);
|
|
211
|
+
}, { ...options, managed: true });
|
|
189
212
|
}
|
|
190
213
|
|
|
191
214
|
/** Explicit trusted assembly only. No HTTP/MCP route or persistent Operation action is added. */
|
|
@@ -201,7 +224,10 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
201
224
|
if (disposePromise) return disposePromise;
|
|
202
225
|
state = 'closing';
|
|
203
226
|
stopAll();
|
|
204
|
-
|
|
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();
|
|
205
231
|
if (cleanupFailure) throw cleanupFailure;
|
|
206
232
|
state = 'closed';
|
|
207
233
|
return snapshot();
|
|
@@ -209,5 +235,8 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
|
|
|
209
235
|
return disposePromise;
|
|
210
236
|
}
|
|
211
237
|
|
|
212
|
-
return Object.freeze({ execute, generateImage, executePluginOperation, 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 }); } });
|
|
213
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
|
+
}
|
package/lib/client.d.ts
CHANGED
package/lib/client.js
CHANGED
|
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
21
21
|
var exports = module.exports;
|
|
22
22
|
|
|
23
23
|
var React = require('react');
|
|
24
|
-
var IRIS_CLIENT_VERSION = '0.3.0-
|
|
24
|
+
var IRIS_CLIENT_VERSION = '0.3.0-beta.1';
|
|
25
25
|
var HOST_CLIENT_PROTOCOL_VERSION = 0;
|
|
26
26
|
var reportedSeats = [];
|
|
27
27
|
|
|
@@ -567,6 +567,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
567
567
|
for (var i = 0; i < sharedListeners.length; i++) {
|
|
568
568
|
sharedListeners[i](data);
|
|
569
569
|
}
|
|
570
|
+
window.dispatchEvent(new CustomEvent('iris-core-refresh-tick'));
|
|
570
571
|
}
|
|
571
572
|
function load() {
|
|
572
573
|
fetch('/iris/api/state')
|
|
@@ -574,7 +575,6 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
574
575
|
.then(function (data) {
|
|
575
576
|
if (data) {
|
|
576
577
|
apply(data);
|
|
577
|
-
window.dispatchEvent(new CustomEvent('iris-core-refresh-tick'));
|
|
578
578
|
}
|
|
579
579
|
})
|
|
580
580
|
.catch(function () { /* 网络抖动静默 */ });
|
|
@@ -1623,9 +1623,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1623
1623
|
p.protocolInferred ? React.createElement('div', { className: 'iris-pm-note' }, '协议为推断值,请确认') : null,
|
|
1624
1624
|
p.mediaProtocol === 'comfyui' ? React.createElement('details', {}, React.createElement('summary', {}, '连接设置'),
|
|
1625
1625
|
React.createElement('div', { className: 'iris-pm-note' }, '服务地址:' + (p.mediaBaseUrl || p.baseUrl)),
|
|
1626
|
-
React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
|
|
1626
|
+
!p.dshProvider ? React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }) : null,
|
|
1627
1627
|
!p.dshProvider ? React.createElement(ProviderAuthenticationEditor, { provider: p, onDone: refresh }) : null)
|
|
1628
|
-
: React.createElement(React.Fragment, {}, React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
|
|
1628
|
+
: React.createElement(React.Fragment, {}, !p.dshProvider ? React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }) : null,
|
|
1629
1629
|
!p.dshProvider ? React.createElement(ProviderAuthenticationEditor, { provider: p, onDone: refresh }) : null,
|
|
1630
1630
|
React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh })),
|
|
1631
1631
|
React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体协议'), React.createElement('select', { value: p.protocolInferred ? 'auto' : (p.mediaProtocol || 'auto'), onChange: function (e) { setProtocol(p, e.target.value); } },
|
|
@@ -2676,8 +2676,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
2676
2676
|
.then(function (value) {
|
|
2677
2677
|
if (request.current !== controller || controller.signal.aborted) return;
|
|
2678
2678
|
setData(value);
|
|
2679
|
-
|
|
2680
|
-
|
|
2679
|
+
var notes = [];
|
|
2680
|
+
if (value.coreError) notes.push('Core 作品暂时不可用,当前计数仅含可读取作品。');
|
|
2681
|
+
if (value.droppedCore) notes.push('已跳过 ' + value.droppedCore + ' 条损坏、缺失或不支持格式的 Core 作品。');
|
|
2682
|
+
if (value.legacyCoreSkipped) notes.push(value.legacyCoreSkipped + ' 条早期 Core 作品尚未编入当前目录;可按 ID 读取,或显式重建索引后显示。');
|
|
2683
|
+
setNote(notes.join(' '));
|
|
2681
2684
|
})
|
|
2682
2685
|
.catch(function (error) { if (request.current === controller && !controller.signal.aborted) setNote(error.message); })
|
|
2683
2686
|
.finally(function () { if (request.current === controller) { request.current = null; setBusy(false); } });
|
|
@@ -2868,33 +2871,46 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
2868
2871
|
var EMPTY_CORE_USER = Object.freeze({ rows: [], available: false, degraded: false, droppedTasks: 0 });
|
|
2869
2872
|
|
|
2870
2873
|
function useCoreUserTasks() {
|
|
2871
|
-
var
|
|
2874
|
+
var store = useCoreUserTasks.shared;
|
|
2875
|
+
if (!store) store = useCoreUserTasks.shared = { data: EMPTY_CORE_USER, listeners: [], sequence: 0, refresh: null };
|
|
2876
|
+
var pair = React.useState(store.data);
|
|
2872
2877
|
var setData = pair[1];
|
|
2873
2878
|
React.useEffect(function () {
|
|
2874
|
-
|
|
2879
|
+
function listener(data) { setData(data); }
|
|
2880
|
+
store.listeners.push(listener);
|
|
2881
|
+
setData(store.data);
|
|
2875
2882
|
function load() {
|
|
2876
|
-
var current = ++sequence;
|
|
2883
|
+
var current = ++store.sequence;
|
|
2877
2884
|
fetch('/iris/api/core/snapshot?limit=200')
|
|
2878
2885
|
.then(function (res) { return res.ok ? res.json() : null; })
|
|
2879
2886
|
.then(function (data) {
|
|
2880
|
-
if (!
|
|
2881
|
-
|
|
2887
|
+
if (!store.listeners.length || current !== store.sequence || !data || !Array.isArray(data.userTasks)) return;
|
|
2888
|
+
store.data = {
|
|
2882
2889
|
rows: data.userTasks,
|
|
2890
|
+
activity: data.activity,
|
|
2883
2891
|
available: !!data.available,
|
|
2884
2892
|
degraded: !!data.degraded,
|
|
2885
2893
|
droppedTasks: Number(data.droppedTasks || 0)
|
|
2886
|
-
}
|
|
2894
|
+
};
|
|
2895
|
+
store.listeners.forEach(function (notify) { notify(store.data); });
|
|
2887
2896
|
})
|
|
2888
2897
|
.catch(function () { /* Core 不可用不阻塞 legacy 任务区 */ });
|
|
2889
2898
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2899
|
+
if (store.listeners.length === 1) {
|
|
2900
|
+
store.refresh = load;
|
|
2901
|
+
load();
|
|
2902
|
+
window.addEventListener('iris-core-artifacts-changed', load);
|
|
2903
|
+
window.addEventListener('iris-core-refresh-tick', load);
|
|
2904
|
+
}
|
|
2894
2905
|
return function () {
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2906
|
+
var index = store.listeners.indexOf(listener);
|
|
2907
|
+
if (index >= 0) store.listeners.splice(index, 1);
|
|
2908
|
+
if (!store.listeners.length) {
|
|
2909
|
+
store.sequence++;
|
|
2910
|
+
window.removeEventListener('iris-core-artifacts-changed', store.refresh);
|
|
2911
|
+
window.removeEventListener('iris-core-refresh-tick', store.refresh);
|
|
2912
|
+
store.refresh = null; store.data = EMPTY_CORE_USER;
|
|
2913
|
+
}
|
|
2898
2914
|
};
|
|
2899
2915
|
}, []);
|
|
2900
2916
|
return pair[0];
|
|
@@ -3018,11 +3034,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
3018
3034
|
if (!props.row || props.row.retryable !== true) return null;
|
|
3019
3035
|
return React.createElement(CoreTaskManualButton, {
|
|
3020
3036
|
id: props.row.id, action: 'retry',
|
|
3021
|
-
idleLabel: '重试为新任务', busyLabel: '创建中…', failPrefix: '重试',
|
|
3037
|
+
idleLabel: props.row.capability === 'video' ? '重新创建文生视频' : '重试为新任务', busyLabel: '创建中…', failPrefix: '重试',
|
|
3022
3038
|
title: '创建一个新任务重新生成,可能产生重复费用;生成指令需要重新输入',
|
|
3023
3039
|
confirm: '将创建一个新任务并可能产生重复生成费用,确定继续?',
|
|
3024
3040
|
createdPrefix: '已创建新任务:',
|
|
3025
3041
|
buildBody: function () {
|
|
3042
|
+
var videoMode;
|
|
3043
|
+
if (props.row.capability === 'video') {
|
|
3044
|
+
videoMode = window.prompt('旧视频 Task 不保存模式、首帧和音频。本按钮仅创建新的文生视频;图生视频或数字人视频请取消,并通过视频工具或 CLI 重新提供首帧/音频。\n如明确选择文生视频,请输入 t2v:');
|
|
3045
|
+
if (videoMode === null || String(videoMode).trim() !== 't2v') return null;
|
|
3046
|
+
}
|
|
3026
3047
|
var isTts = props.row.capability === 'tts';
|
|
3027
3048
|
var isTranscribe = props.row.capability === 'transcribe';
|
|
3028
3049
|
var draft = window.prompt(isTts
|
|
@@ -3040,7 +3061,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
3040
3061
|
}
|
|
3041
3062
|
return isTts ? { text: draft, confirmBilling: true }
|
|
3042
3063
|
: isTranscribe ? { audio_url: draft, confirmBilling: true }
|
|
3043
|
-
: { prompt: draft, confirmBilling: true, ...(
|
|
3064
|
+
: { prompt: draft, confirmBilling: true, ...(videoMode ? { video_mode: 't2v' } : {}),
|
|
3065
|
+
...(sourceArtifactId ? { source_artifact_id: String(sourceArtifactId).trim() } : {}) };
|
|
3044
3066
|
}
|
|
3045
3067
|
});
|
|
3046
3068
|
}
|
|
@@ -4245,13 +4267,21 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
4245
4267
|
function ProgressDock() {
|
|
4246
4268
|
var state = useIrisState(5000);
|
|
4247
4269
|
var running = (state && state.tasks && state.tasks.running) || [];
|
|
4248
|
-
|
|
4270
|
+
var coreUser = useCoreUserTasks();
|
|
4271
|
+
var coreRows = coreUser.activity && coreUser.activity.recent || [];
|
|
4272
|
+
if (!running.length && !coreRows.length) return null;
|
|
4249
4273
|
return React.createElement('div', { className: 'iris-progress-dock' },
|
|
4250
4274
|
running.map(function (t) {
|
|
4251
4275
|
return React.createElement('div', { key: t.id, className: 'iris-progress-row' },
|
|
4252
4276
|
React.createElement('span', {}, '🫧 ' + t.cap),
|
|
4253
4277
|
React.createElement('span', {}, t.model || ''),
|
|
4254
4278
|
React.createElement('span', {}, String(t.progress || '')));
|
|
4279
|
+
}),
|
|
4280
|
+
coreRows.map(function (row) {
|
|
4281
|
+
return React.createElement('div', { key: 'core:' + row.id, className: 'iris-progress-row' },
|
|
4282
|
+
React.createElement('span', {}, 'Core · ' + row.capability),
|
|
4283
|
+
React.createElement('span', {}, row.model),
|
|
4284
|
+
React.createElement('span', {}, row.label));
|
|
4255
4285
|
}));
|
|
4256
4286
|
}
|
|
4257
4287
|
|
|
@@ -4280,6 +4310,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
4280
4310
|
|
|
4281
4311
|
function FloatingBubble() {
|
|
4282
4312
|
var state = useIrisState(5000);
|
|
4313
|
+
var coreUser = useCoreUserTasks();
|
|
4314
|
+
var activity = coreUser.activity;
|
|
4283
4315
|
var providers = (state && state.providers) || [];
|
|
4284
4316
|
var fallbackConfigured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
|
|
4285
4317
|
var overallState = state && state.health && HEALTH_META[state.health.overall]
|
|
@@ -4386,9 +4418,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
4386
4418
|
style = { right: DEFAULT_X + 'px', bottom: DEFAULT_Y + 'px' };
|
|
4387
4419
|
}
|
|
4388
4420
|
|
|
4389
|
-
var
|
|
4390
|
-
var
|
|
4391
|
-
|
|
4421
|
+
var coreCount = activity ? activity.running + activity.paused + activity.attention : 0;
|
|
4422
|
+
var badgeCount = running.length + attention.length + coreCount;
|
|
4423
|
+
var badgeTitle = '旧任务:' + running.length + ' 运行中,' + attention.length + ' 待处理';
|
|
4424
|
+
if (activity) badgeTitle += ';Core:' + activity.running + ' 运行中,' + activity.paused + ' 观察暂停,' + activity.attention + ' 待处理'
|
|
4425
|
+
+ (activity.truncated ? '(部分较早的 Core 记录尚未统计)' : '');
|
|
4426
|
+
var badge = badgeCount || activity && activity.truncated
|
|
4427
|
+
? React.createElement('span', { className: 'iris-bubble-badge', title: badgeTitle, 'aria-label': badgeTitle },
|
|
4428
|
+
badgeCount ? String(badgeCount) + (activity && activity.truncated ? '+' : '') : '…')
|
|
4392
4429
|
: null;
|
|
4393
4430
|
|
|
4394
4431
|
var panel = null;
|
package/lib/comfyui-adapter.js
CHANGED
|
@@ -6,6 +6,7 @@ import { parseImageSize } from './provider-protocol.js';
|
|
|
6
6
|
import { createComfyUiClient, comfyFail as fail } from './comfyui-client.js';
|
|
7
7
|
import { inspectComfyWorkflow } from './comfyui-inspection.js';
|
|
8
8
|
import { writePrivateFile } from './private-storage.js';
|
|
9
|
+
import { inheritedProviderFetch } from './provider-network.js';
|
|
9
10
|
|
|
10
11
|
const TASK = /^[A-Za-z0-9_-]{1,200}$/;
|
|
11
12
|
function imageLocation(value) {
|
|
@@ -16,8 +17,8 @@ function imageLocation(value) {
|
|
|
16
17
|
}
|
|
17
18
|
return { filename: value.filename, subfolder: value.subfolder, type: value.type };
|
|
18
19
|
}
|
|
19
|
-
export function createComfyUiProviderAdapter(provider, { fetchPort =
|
|
20
|
-
const client = createComfyUiClient(provider, { fetchPort }), { route, request } = client;
|
|
20
|
+
export function createComfyUiProviderAdapter(provider, { fetchPort = inheritedProviderFetch, network } = {}) {
|
|
21
|
+
const client = createComfyUiClient(provider, { fetchPort, network }), { route, request } = client;
|
|
21
22
|
function workflow(model) {
|
|
22
23
|
const value = model ? provider.models?.find(entry => entry.id === model)?.comfyWorkflow : provider.comfyWorkflow;
|
|
23
24
|
try { return checkedComfyWorkflow(value); }
|