@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/operation-runner.js
CHANGED
|
@@ -13,6 +13,8 @@ import { acceptOperation, inspectExecutionRoot, operationId, readOperation, read
|
|
|
13
13
|
import { reservePluginOperation } from './plugin-operations.js';
|
|
14
14
|
import { invokeReservedPlugin } from './plugin-execution.js';
|
|
15
15
|
import { acceptPluginOperation, pluginInputDigest, pluginSourceArtifacts, reservePluginArtifact } from './plugin-operation-store.js';
|
|
16
|
+
import { readFlowRun } from './flow-store.js';
|
|
17
|
+
import { findFlowTask } from './core-tasks.js';
|
|
16
18
|
|
|
17
19
|
const activeState = record => ['queued', 'running'].includes(record.state);
|
|
18
20
|
const busy = () => operationFail('IRIS_CORE_DATA_ROOT_BUSY', '当前写者正在执行,请用原 Key 稍后明确重试');
|
|
@@ -28,17 +30,20 @@ export function recoverExecutionLease(root, options) {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** One execution owner, one finite continue per accepted Operation. Construction performs no I/O. */
|
|
31
|
-
export function createOperationRunner({ dataRoot, application: suppliedApplication, resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter, onExecutionError,
|
|
33
|
+
export function createOperationRunner({ dataRoot, application: suppliedApplication, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter, onExecutionError,
|
|
32
34
|
pluginRegistry, allowedOperations = [] } = {}) {
|
|
33
35
|
const root = normalizeCoreOptions({ dataRoot }).dataRoot;
|
|
34
|
-
const application = suppliedApplication || createApplicationRuntime({ dataRoot: root, resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter });
|
|
35
|
-
const ports = { resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter };
|
|
36
|
+
const application = suppliedApplication || createApplicationRuntime({ dataRoot: root, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter });
|
|
37
|
+
const ports = { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter };
|
|
36
38
|
if (onExecutionError !== undefined && typeof onExecutionError !== 'function') throw new TypeError('执行错误报告器须为函数');
|
|
37
|
-
const queue = [];
|
|
38
|
-
|
|
39
|
+
const queue = [], active = new Map(), finalizing = new Map();
|
|
40
|
+
const scheduled = Boolean(application.scheduler);
|
|
41
|
+
const transaction = (action, options) => application.transaction ? application.transaction(action, options) : application.run('writer', action);
|
|
42
|
+
let state = 'new', physicalRoot, directory, ownership, lease, ownerInstanceId, draining, disposal, failure, wake;
|
|
39
43
|
|
|
40
44
|
function snapshot() {
|
|
41
|
-
return Object.freeze({ state, queued: queue.length, active: active
|
|
45
|
+
return Object.freeze({ state, queued: queue.length, active: active.keys().next().value || null,
|
|
46
|
+
...(scheduled ? { activeOperations: [...active.keys()] } : {}),
|
|
42
47
|
executionEnabled: state === 'open', failed: Boolean(failure) });
|
|
43
48
|
}
|
|
44
49
|
function requireOwner() {
|
|
@@ -53,20 +58,28 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
53
58
|
}
|
|
54
59
|
function failRunner(error) {
|
|
55
60
|
failure ||= error; state = 'failed';
|
|
56
|
-
active
|
|
61
|
+
for (const job of active.values()) job.controller.abort();
|
|
57
62
|
}
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
function finishIfActive(id, terminal, failureCode) {
|
|
64
|
+
if (finalizing.has(id)) return finalizing.get(id);
|
|
65
|
+
const completion = transaction(runtime => runtime.run('execute', ({ dataRoot }) => {
|
|
60
66
|
requireOwner();
|
|
61
67
|
const record = readOperation(dataRoot, id);
|
|
62
68
|
if (!record || !activeState(record)) return record;
|
|
63
69
|
if (record.ownerInstanceId !== ownerInstanceId) operationFail('IRIS_EXECUTION_OWNERSHIP_LOST', 'Operation 不属于本次执行实例');
|
|
64
|
-
return transitionOperation(dataRoot, { id, expectedRevision: record.revision, state: terminal
|
|
65
|
-
|
|
70
|
+
return transitionOperation(dataRoot, { id, expectedRevision: record.revision, state: terminal,
|
|
71
|
+
...(failureCode ? { failureCode } : {}) });
|
|
72
|
+
}), { finalize: true }).finally(() => { finalizing.delete(id); });
|
|
73
|
+
finalizing.set(id, completion);
|
|
74
|
+
return completion;
|
|
66
75
|
}
|
|
67
76
|
async function perform(job) {
|
|
77
|
+
let admitted = false;
|
|
68
78
|
try {
|
|
69
|
-
|
|
79
|
+
const saved = readOperation(physicalRoot, job.id);
|
|
80
|
+
job.runId = saved?.runId;
|
|
81
|
+
const execute = saved?.action === 'flow.continue' && application.runMedia ? application.runMedia : (action, options) => application.run('writer', action, options);
|
|
82
|
+
await execute(runtime => { admitted = true; return runtime.run('execute', async ({ dataRoot }) => {
|
|
70
83
|
requireOwner();
|
|
71
84
|
let record = readOperation(dataRoot, job.id);
|
|
72
85
|
if (!record || !activeState(record)) return;
|
|
@@ -107,28 +120,39 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
107
120
|
state: job.stopReason || (executionError ? 'failed' : 'succeeded'),
|
|
108
121
|
...(executionError && !job.stopReason ? { failureCode: 'execution_failed' } : {}) });
|
|
109
122
|
if (executionError && !job.stopReason) onExecutionError?.(executionError, job.id);
|
|
110
|
-
}), { signal: job.controller.signal });
|
|
123
|
+
}); }, { signal: job.controller.signal, ...(saved?.runId ? { key: `run:${saved.runId}` } : {}) });
|
|
111
124
|
} catch (error) {
|
|
112
125
|
if (job.stopReason && error.name === 'AbortError' && !application.snapshot().cleanupFailed) {
|
|
113
126
|
await finishIfActive(job.id, job.stopReason);
|
|
127
|
+
} else if (scheduled && !admitted && !application.snapshot().cleanupFailed
|
|
128
|
+
&& ['IRIS_SCHEDULER_QUEUE_TIMEOUT', 'IRIS_SCHEDULER_QUEUE_FULL'].includes(error.code)) {
|
|
129
|
+
await finishIfActive(job.id, job.stopReason || 'failed', job.stopReason ? undefined : 'execution_failed');
|
|
130
|
+
if (!job.stopReason) onExecutionError?.(error, job.id);
|
|
114
131
|
} else throw error;
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
134
|
function pump() {
|
|
118
|
-
if (draining
|
|
135
|
+
if (draining) { wake?.(); return; }
|
|
136
|
+
if (state !== 'open') return;
|
|
137
|
+
let blocked = false;
|
|
119
138
|
draining = (async () => {
|
|
120
|
-
while (state === 'open' && queue.length) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
139
|
+
while ((state === 'open' && queue.length && !blocked) || active.size) {
|
|
140
|
+
while (state === 'open' && queue.length && !blocked && active.size < (scheduled ? MAX_ACTIVE_OPERATIONS : 1)) {
|
|
141
|
+
const job = queue.shift(); active.set(job.id, job);
|
|
142
|
+
job.done = perform(job).catch(error => {
|
|
143
|
+
if (error.code !== 'IRIS_CORE_DATA_ROOT_BUSY') throw error;
|
|
144
|
+
queue.unshift(job); blocked = true; // Explicit retry only; no lease stealing or polling timer.
|
|
145
|
+
}).finally(() => { active.delete(job.id); });
|
|
146
|
+
}
|
|
147
|
+
if (active.size) {
|
|
148
|
+
const added = new Promise(resolve => { wake = resolve; });
|
|
149
|
+
await Promise.race([...active.values()].map(job => job.done).concat(added));
|
|
150
|
+
wake = null;
|
|
127
151
|
}
|
|
128
|
-
active = null;
|
|
129
152
|
}
|
|
130
153
|
})();
|
|
131
|
-
void draining.then(() => { draining = null;
|
|
154
|
+
void draining.then(() => { draining = null; if (state === 'open' && queue.length && !blocked) queueMicrotask(pump); },
|
|
155
|
+
error => { draining = null; failRunner(error); });
|
|
132
156
|
}
|
|
133
157
|
async function start() {
|
|
134
158
|
if (state !== 'new') operationFail('IRIS_EXECUTION_LIFECYCLE', '执行服务只能显式启动一次');
|
|
@@ -138,7 +162,7 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
138
162
|
lease = createCoreRuntime({ dataRoot: directory, mode: 'writer' }); lease.start();
|
|
139
163
|
ownership = inspectRunnerArea(directory); ownerInstanceId = ownership.owner.instanceId;
|
|
140
164
|
state = 'open';
|
|
141
|
-
await
|
|
165
|
+
await transaction(runtime => runtime.run('execute', ({ dataRoot }) => {
|
|
142
166
|
requireOwner();
|
|
143
167
|
for (const record of readAllOperations(dataRoot).filter(activeState)) {
|
|
144
168
|
transitionOperation(dataRoot, { id: record.id, expectedRevision: record.revision, state: 'interrupted' });
|
|
@@ -163,19 +187,27 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
163
187
|
if (previous.state === 'queued' && previous.ownerInstanceId === ownerInstanceId) queueMicrotask(pump);
|
|
164
188
|
return { contractVersion: 0, operation: projectOperation(previous), idempotentReplay: true };
|
|
165
189
|
}
|
|
166
|
-
if (application.snapshot().activeWrites || application.snapshot().queuedWrites) busy();
|
|
190
|
+
if (!scheduled && (application.snapshot().activeWrites || application.snapshot().queuedWrites)) busy();
|
|
167
191
|
const reservation = reservePluginOperation(pluginRegistry, input.operationId, input.input, allowedOperations, input.dependency);
|
|
168
|
-
let record;
|
|
192
|
+
let record, replay = false;
|
|
169
193
|
try {
|
|
170
|
-
record = await
|
|
194
|
+
record = await transaction(runtime => runtime.run('execute', ({ dataRoot }) => {
|
|
171
195
|
writable(); authorize?.();
|
|
172
|
-
|
|
196
|
+
const current = readOperation(dataRoot, id, marker);
|
|
197
|
+
if (current) {
|
|
198
|
+
if (current.action !== 'plugin.execute' || current.inputDigest !== pluginInputDigest(input.operationId, input.input, input.dependency, current.sourceArtifactIds)) {
|
|
199
|
+
operationFail('IRIS_OPERATION_KEY_CONFLICT', '该 Key 已绑定另一业务或输入');
|
|
200
|
+
}
|
|
201
|
+
replay = true; return current;
|
|
202
|
+
}
|
|
203
|
+
if (readAllOperations(dataRoot).filter(activeState).length >= MAX_ACTIVE_OPERATIONS - Number(scheduled)) operationFail('IRIS_OPERATION_QUEUE_FULL', '未结束的 Operation 已达上限;调度模式保留一个位置供原任务恢复');
|
|
173
204
|
return acceptPluginOperation(dataRoot, marker, id, { ...input, input: reservation.input,
|
|
174
205
|
sourceArtifactIds: pluginSourceArtifacts(reservation.input, reservation.artifactInputs), ownerInstanceId });
|
|
175
206
|
}));
|
|
176
207
|
} finally { reservation.release(); }
|
|
177
|
-
queue.push({ id, controller: new AbortController(), stopReason: null, authorize });
|
|
178
|
-
|
|
208
|
+
if (!replay) queue.push({ id, controller: new AbortController(), stopReason: null, authorize });
|
|
209
|
+
if (!replay || record.state === 'queued' && record.ownerInstanceId === ownerInstanceId) queueMicrotask(pump);
|
|
210
|
+
return { contractVersion: 0, operation: projectOperation(record), idempotentReplay: replay };
|
|
179
211
|
}
|
|
180
212
|
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).length !== 3
|
|
181
213
|
|| !['key', 'runId', 'expectedRunRevision'].every(key => Object.hasOwn(input, key))) {
|
|
@@ -190,9 +222,14 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
190
222
|
if (previous.state === 'queued' && previous.ownerInstanceId === ownerInstanceId) queueMicrotask(pump);
|
|
191
223
|
return { contractVersion: 0, operation: projectOperation(previous), idempotentReplay: true };
|
|
192
224
|
}
|
|
193
|
-
if (application.snapshot().activeWrites || application.snapshot().queuedWrites) busy();
|
|
194
|
-
const accepted = await
|
|
195
|
-
writable();
|
|
225
|
+
if (!scheduled && (application.snapshot().activeWrites || application.snapshot().queuedWrites)) busy();
|
|
226
|
+
const accepted = await transaction(runtime => runtime.run('execute', ({ dataRoot }) => {
|
|
227
|
+
writable();
|
|
228
|
+
if (scheduled && !readOperation(dataRoot, id) && readAllOperations(dataRoot).filter(activeState).length >= MAX_ACTIVE_OPERATIONS - 1
|
|
229
|
+
&& !findFlowTask(dataRoot, readFlowRun(dataRoot, input.runId))?.attempts.length) {
|
|
230
|
+
operationFail('IRIS_OPERATION_QUEUE_FULL', '新提交队列已满;保留一个位置用于原任务查询和交付');
|
|
231
|
+
}
|
|
232
|
+
return acceptOperation(dataRoot, { ...input, ownerInstanceId });
|
|
196
233
|
}));
|
|
197
234
|
if (!accepted.idempotentReplay) {
|
|
198
235
|
queue.push({ id: accepted.operation.id, controller: new AbortController(), stopReason: null });
|
|
@@ -206,10 +243,14 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
206
243
|
if (!record) operationFail('IRIS_OPERATION_NOT_FOUND', 'Operation 不存在');
|
|
207
244
|
if (activeState(record)) {
|
|
208
245
|
if (record.ownerInstanceId !== ownerInstanceId) operationFail('IRIS_EXECUTION_OWNERSHIP_LOST', '不能停止另一执行实例的操作');
|
|
209
|
-
const job = active
|
|
246
|
+
const job = active.get(id) || queue.find(job => job.id === id);
|
|
210
247
|
if (job) { job.stopReason = 'canceled'; job.controller.abort(); }
|
|
211
|
-
if (active
|
|
212
|
-
else
|
|
248
|
+
if (active.has(id)) await job.done;
|
|
249
|
+
else {
|
|
250
|
+
await finishIfActive(id, 'canceled');
|
|
251
|
+
const index = queue.indexOf(job);
|
|
252
|
+
if (index >= 0) queue.splice(index, 1);
|
|
253
|
+
}
|
|
213
254
|
}
|
|
214
255
|
const saved = readOperation(physicalRoot, id);
|
|
215
256
|
if (activeState(saved)) operationFail('IRIS_EXECUTION_STOP_UNSAVED', '停止尚未持久保存,请查询原 Operation');
|
|
@@ -222,7 +263,7 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
222
263
|
if (state === 'new') { state = 'closed'; return disposal = suppliedApplication ? Promise.resolve(snapshot()) : application.dispose().then(snapshot); }
|
|
223
264
|
if (state === 'closed') return Promise.resolve(snapshot());
|
|
224
265
|
if (state !== 'failed') state = 'closing';
|
|
225
|
-
|
|
266
|
+
for (const job of active.values()) { job.stopReason ||= 'interrupted'; job.controller.abort(); }
|
|
226
267
|
disposal = (async () => {
|
|
227
268
|
if (draining) await draining;
|
|
228
269
|
if (failure) throw failure;
|
|
@@ -232,5 +273,9 @@ export function createOperationRunner({ dataRoot, application: suppliedApplicati
|
|
|
232
273
|
})();
|
|
233
274
|
return disposal;
|
|
234
275
|
}
|
|
235
|
-
return Object.freeze({ start, submit, stop, idle, dispose, snapshot
|
|
276
|
+
return Object.freeze({ start, submit, stop, idle, dispose, snapshot,
|
|
277
|
+
scheduling(id) {
|
|
278
|
+
const job = active.get(id), phase = job?.runId && application.schedulingPhase(`run:${job.runId}`);
|
|
279
|
+
return scheduled && phase ? { phase } : undefined;
|
|
280
|
+
} });
|
|
236
281
|
}
|
package/lib/operation-store.js
CHANGED
|
@@ -34,11 +34,11 @@ function input(value, allowed, required = allowed) {
|
|
|
34
34
|
}
|
|
35
35
|
export function inspectExecutionRoot(root) {
|
|
36
36
|
const marker = inspectLocalRequestRoot(root, { allowAbsent: true });
|
|
37
|
-
if (![1, 2, 3].includes(marker?.schemaVersion)) operationFail('IRIS_OPERATION_ROOT_REQUIRED', 'Operation 需要新的专用执行根,请显式执行 execution init;不转换原根');
|
|
37
|
+
if (![1, 2, 3, 4].includes(marker?.schemaVersion)) operationFail('IRIS_OPERATION_ROOT_REQUIRED', 'Operation 需要新的专用执行根,请显式执行 execution init;不转换原根');
|
|
38
38
|
return marker;
|
|
39
39
|
}
|
|
40
|
-
export function initializeExecutionRoot(root, { imageGeneration = false, audio = false } = {}) {
|
|
41
|
-
const marker = initializeLocalRequestRoot(root, audio ? 3 : imageGeneration ? 2 : 1);
|
|
40
|
+
export function initializeExecutionRoot(root, { imageGeneration = false, audio = false, video = false } = {}) {
|
|
41
|
+
const marker = initializeLocalRequestRoot(root, video ? 4 : audio ? 3 : imageGeneration ? 2 : 1);
|
|
42
42
|
listCreationRequests(root); readAllOperations(root);
|
|
43
43
|
return marker;
|
|
44
44
|
}
|
|
@@ -64,7 +64,7 @@ function validate(record, id, marker) {
|
|
|
64
64
|
|| !UUID.test(record.ownerInstanceId) || !STATES.has(record.state) || typeof record.cancelRequested !== 'boolean'
|
|
65
65
|
|| !iso(record.createdAt) || !iso(record.updatedAt) || record.updatedAt < record.createdAt
|
|
66
66
|
|| !keys(record.binding, ['taskId', 'sourceArtifactId', 'comparisonArtifactId', 'inputDigest'])
|
|
67
|
-
|| !TASK.test(record.binding.taskId) || !(marker.schemaVersion
|
|
67
|
+
|| !TASK.test(record.binding.taskId) || !([3, 4].includes(marker.schemaVersion) && (record.binding.sourceArtifactId === null || ARTIFACT.test(record.binding.sourceArtifactId))
|
|
68
68
|
&& (record.binding.comparisonArtifactId === null || ARTIFACT.test(record.binding.comparisonArtifactId))
|
|
69
69
|
|| record.binding.sourceArtifactId === null && record.binding.comparisonArtifactId === null && marker.schemaVersion === 2
|
|
70
70
|
|| ARTIFACT.test(record.binding.sourceArtifactId) && ARTIFACT.test(record.binding.comparisonArtifactId))
|
|
@@ -76,7 +76,7 @@ function validate(record, id, marker) {
|
|
|
76
76
|
|| record.state === 'running' && (!hasStarted || record.cancelRequested)
|
|
77
77
|
|| record.state === 'succeeded' && (!hasStarted || record.cancelRequested)
|
|
78
78
|
|| record.cancelRequested !== (record.state === 'canceled')
|
|
79
|
-
|| record.state === 'failed' &&
|
|
79
|
+
|| record.state === 'failed' && !FAILURE_CODES.has(record.failureCode)
|
|
80
80
|
|| record.state !== 'failed' && Object.hasOwn(record, 'failureCode')) invalid();
|
|
81
81
|
if (ACTIVE.has(record.state)) {
|
|
82
82
|
if (Object.hasOwn(record, 'finishedAt') || Object.hasOwn(record, 'result')) invalid();
|
|
@@ -172,7 +172,7 @@ export function transitionOperation(root, value) {
|
|
|
172
172
|
const allowed = current.state === 'queued' ? ['running', 'canceled', 'interrupted', 'failed']
|
|
173
173
|
: current.state === 'running' ? ['succeeded', 'failed', 'canceled', 'interrupted'] : [];
|
|
174
174
|
if (!allowed.includes(value.state)) operationFail('IRIS_OPERATION_TRANSITION_INVALID', '不允许此 Operation 状态转换;结束记录不重新排队');
|
|
175
|
-
if (value.state === 'failed' ? !FAILURE_CODES.has(value.failureCode)
|
|
175
|
+
if (value.state === 'failed' ? !FAILURE_CODES.has(value.failureCode)
|
|
176
176
|
: Object.hasOwn(value, 'failureCode')) operationFail('IRIS_OPERATION_INPUT_INVALID', '失败记录只接受有限错误类别,不保存异常原文');
|
|
177
177
|
const { run, task } = validateBinding(root, current);
|
|
178
178
|
if (value.state === 'running' && run.revision !== current.expectedRunRevision) operationFail('IRIS_OPERATION_RUN_CHANGED', 'Run revision 已改变,未开始本次调用');
|
package/lib/plugins.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
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
|
+
}
|
|
2
21
|
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };
|
|
3
22
|
export type PluginKind = 'providers' | 'operations' | 'pages' | 'renderers' | 'settings';
|
|
4
23
|
export const PLUGIN_CONTRACT_VERSION: 0;
|
|
@@ -60,11 +79,9 @@ export interface PluginOperationContext {
|
|
|
60
79
|
readonly core: { execute(command: PluginCoreCommand, input: Record<string, unknown>): Promise<unknown> };
|
|
61
80
|
}
|
|
62
81
|
export type PluginOperation = (input: Readonly<Record<string, JsonValue>>, context: PluginOperationContext) => JsonValue | Promise<JsonValue>;
|
|
63
|
-
export type ProviderCapability = 'image' | 'video' | 'tts' | 'music' | 'transcribe';
|
|
64
|
-
export type ProviderMethod = 'discover' | 'submit' | 'poll' | 'cancel' | 'download' | 'mapError';
|
|
65
82
|
export interface ProviderAdapterInput {
|
|
66
83
|
id: string; protocol: string; capabilities: ProviderCapability[]; contractVersion?: 0;
|
|
67
|
-
operations:
|
|
84
|
+
operations: ProviderOperations;
|
|
68
85
|
unsupported?: Partial<Record<ProviderMethod, string>>;
|
|
69
86
|
prepareInput?: (...args: any[]) => unknown;
|
|
70
87
|
unsupportedPreparation?: string;
|
|
@@ -73,14 +90,14 @@ export interface ProviderAdapterInput {
|
|
|
73
90
|
export type ProviderAdapter = Readonly<Omit<ProviderAdapterInput, 'contractVersion' | 'capabilities' | 'operations' | 'unsupported'> & {
|
|
74
91
|
contractVersion: 0;
|
|
75
92
|
capabilities: readonly ProviderCapability[];
|
|
76
|
-
operations: Readonly<
|
|
93
|
+
operations: Readonly<ProviderOperations>;
|
|
77
94
|
unsupported: Readonly<Partial<Record<ProviderMethod, { readonly reason: string }>>>;
|
|
78
95
|
}>;
|
|
79
96
|
export function defineProviderAdapter(input: ProviderAdapterInput): ProviderAdapter;
|
|
80
97
|
export function normalizeMediaEndpoint(value: string): string;
|
|
81
98
|
export interface ProviderImplementation {
|
|
82
99
|
normalizeEndpoint(value: string): string;
|
|
83
|
-
createAdapter(provider:
|
|
100
|
+
createAdapter(provider: Readonly<ProviderConnection>, context: Readonly<ProviderFactoryContext>): ProviderAdapter;
|
|
84
101
|
supportsModel?(input: { provider: Record<string, unknown>; modelId: string; capability: string; checkEndpoint?: boolean }): boolean;
|
|
85
102
|
supportsImageEdit?(modelId: string): boolean;
|
|
86
103
|
}
|
|
@@ -161,7 +178,7 @@ export interface CoreCapabilitySnapshot {
|
|
|
161
178
|
};
|
|
162
179
|
}
|
|
163
180
|
export interface CoreCapabilityCatalog {
|
|
164
|
-
snapshot(options?: { audioEnabled?: boolean; panelContributions?: PluginPanelSelection; allowedOperations?: readonly string[] }): CoreCapabilitySnapshot;
|
|
181
|
+
snapshot(options?: { audioEnabled?: boolean; videoEnabled?: boolean; panelContributions?: PluginPanelSelection; allowedOperations?: readonly string[] }): CoreCapabilitySnapshot;
|
|
165
182
|
/** Validate an explicit local selection. Does not invoke, persist a grant or enable MCP/Web. */
|
|
166
183
|
selectOperations(ids?: readonly string[]): readonly (OperationContribution & CatalogContribution)[];
|
|
167
184
|
}
|
|
@@ -219,6 +236,7 @@ export interface PluginWebSession {
|
|
|
219
236
|
readonly operations: readonly string[];
|
|
220
237
|
readonly readOnly: boolean;
|
|
221
238
|
readonly audio: boolean;
|
|
239
|
+
readonly video: boolean;
|
|
222
240
|
readonly providerManagement: boolean;
|
|
223
241
|
readonly pluginExecution: boolean;
|
|
224
242
|
readonly pluginOperations: readonly PluginDependency[];
|
|
@@ -256,6 +274,7 @@ export interface PluginWebApi {
|
|
|
256
274
|
run(id: string, options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
257
275
|
models(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
258
276
|
audioModels(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
277
|
+
videoModels(options?: PluginWebRequestOptions): Promise<JsonValue>;
|
|
259
278
|
artifact(artifact: PluginArtifact, options?: PluginWebRequestOptions): Promise<Blob>;
|
|
260
279
|
/** Only settings mounts in an explicitly enabled owner Provider-management session. */
|
|
261
280
|
readonly providers?: {
|
package/lib/plugins.js
CHANGED
|
@@ -13,7 +13,8 @@ export { createCoreCapabilityCatalog } from './core-capability-catalog.js';
|
|
|
13
13
|
|
|
14
14
|
export { definePlugin, PluginContractError, PLUGIN_CONTRACT_VERSION, PLUGIN_KINDS, PLUGIN_CORE_COMMANDS,
|
|
15
15
|
checkedPluginDependency, comparePluginVersions } from './plugin-contract.js';
|
|
16
|
-
export { defineProviderAdapter, normalizeMediaEndpoint } from './provider-extensions.js';
|
|
16
|
+
export { defineProviderAdapter, ProviderContractError, normalizeMediaEndpoint } from './provider-extensions.js';
|
|
17
|
+
export { PROVIDER_FACTORY_CONTEXT_VERSION } from './provider-context.js';
|
|
17
18
|
|
|
18
19
|
export function createCorePluginRegistry({ plugins = [], coreVersion } = {}) {
|
|
19
20
|
coreVersion ??= JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
package/lib/provider-adapter.js
CHANGED
|
@@ -196,7 +196,7 @@ export function providerAdapterSnapshot(adapter) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/** 提交前的可选上传能力;不创建 Task,也不属于六个生命周期操作。 */
|
|
199
|
-
export async function prepareProviderInput(adapter, input = {}) {
|
|
199
|
+
export async function prepareProviderInput(adapter, input = {}, operationContext = {}) {
|
|
200
200
|
if (typeof adapter?.prepareInput !== 'function') {
|
|
201
201
|
const error = new ProviderContractError(adapter?.unsupportedPreparation || '协议未实现临时输入上传', {
|
|
202
202
|
stage: 'upload', category: 'invalid_request', acceptance: 'not_accepted',
|
|
@@ -206,7 +206,7 @@ export async function prepareProviderInput(adapter, input = {}) {
|
|
|
206
206
|
throw error;
|
|
207
207
|
}
|
|
208
208
|
try {
|
|
209
|
-
const value = await adapter.prepareInput(input);
|
|
209
|
+
const value = await adapter.prepareInput(input, operationContext);
|
|
210
210
|
if (typeof value?.url !== 'string' || !value.url.trim()) {
|
|
211
211
|
throw new ProviderContractError('Provider 输入准备必须返回非空 url', {
|
|
212
212
|
stage: 'upload', category: 'protocol', acceptance: 'not_accepted'
|
package/lib/provider-adapters.js
CHANGED
|
@@ -16,6 +16,11 @@ import { modelUnavailable, modelUnavailableReason, modelAvailabilityBinding } fr
|
|
|
16
16
|
import { prepareVisionImage, visionInputLimits } from './vision-image-input.js';
|
|
17
17
|
import { createComfyUiProviderAdapter } from './comfyui-adapter.js';
|
|
18
18
|
import { PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
|
|
19
|
+
import { resolveProviderCredential, assertDiscoveryCredentials } from './provider-credentials.js';
|
|
20
|
+
import { assertProviderNetwork } from './provider-network.js';
|
|
21
|
+
import { providerNetworkRequirement, sameNetworkRequirement, networkPolicyError } from './network-policy.js';
|
|
22
|
+
import { createProviderTransport } from './provider-transport.js';
|
|
23
|
+
import { hasProviderContext } from './provider-context.js';
|
|
19
24
|
|
|
20
25
|
const AMBIGUOUS_4XX = new Set([408, 409, 425, 499]);
|
|
21
26
|
|
|
@@ -449,15 +454,33 @@ export function builtinProviderExtension() {
|
|
|
449
454
|
}
|
|
450
455
|
|
|
451
456
|
export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
457
|
+
const network = options.network === undefined ? undefined : assertProviderNetwork(options.network);
|
|
458
|
+
const requirement = providerNetworkRequirement(provider);
|
|
459
|
+
if (requirement && (!network || !sameNetworkRequirement(requirement, network.networkRequirement) || network.providerId !== provider.id)) {
|
|
460
|
+
throw networkPolicyError('IRIS_NETWORK_CONTEXT_REQUIRED', '此供应商需要匹配的显式网络上下文,不会静默使用默认出口');
|
|
461
|
+
}
|
|
462
|
+
if (network && (options.transport !== undefined || options.fetchPort !== undefined)) {
|
|
463
|
+
throw new TypeError('network 不能与显式 transport 或 fetchPort 同时配置');
|
|
464
|
+
}
|
|
465
|
+
const factoryOptions = network ? { ...options, transport: createProviderTransport(network.fetch), fetchPort: network.fetch } : options;
|
|
466
|
+
provider = resolveProviderCredential(provider);
|
|
452
467
|
const selection = options.protocol
|
|
453
468
|
? selectMediaProtocol({ ...provider, mediaProtocol: options.protocol, protocolInferred: false })
|
|
454
469
|
: selectMediaProtocol(provider);
|
|
455
470
|
const factory = Object.hasOwn(PROTOCOL_FACTORIES, selection.mediaProtocol) ? PROTOCOL_FACTORIES[selection.mediaProtocol] : null;
|
|
456
471
|
if (!options.registry && !factory) throw unsupportedProtocolError(selection.mediaProtocol);
|
|
457
|
-
const adapter = options.registry ? options.registry.createAdapter(selection.mediaProtocol, provider,
|
|
472
|
+
const adapter = options.registry ? options.registry.createAdapter(selection.mediaProtocol, provider, factoryOptions) : factory(provider, factoryOptions);
|
|
458
473
|
const operations = { ...adapter.operations };
|
|
459
474
|
const key = providerApiKey(provider);
|
|
460
475
|
const scrub = value => typeof value === 'string' && key ? value.split(key).join('[REDACTED]') : value;
|
|
476
|
+
if (operations.discover) {
|
|
477
|
+
const discover = operations.discover;
|
|
478
|
+
operations.discover = async (...args) => {
|
|
479
|
+
const result = await discover(...args);
|
|
480
|
+
assertDiscoveryCredentials(result, key);
|
|
481
|
+
return result;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
461
484
|
operations.mapError = (error, context) => {
|
|
462
485
|
let mapped;
|
|
463
486
|
try { mapped = adapter.operations.mapError(error, context); } catch (failure) { mapped = failure; }
|
|
@@ -475,7 +498,7 @@ export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
|
475
498
|
for (const name of ['submit', 'poll']) {
|
|
476
499
|
if (!operations[name]) continue;
|
|
477
500
|
const run = operations[name];
|
|
478
|
-
operations[name] = async request => {
|
|
501
|
+
operations[name] = async (request, context) => {
|
|
479
502
|
const modelId = request.model || options.modelId;
|
|
480
503
|
const report = error => reportModelRateLimit(provider, modelId, {
|
|
481
504
|
httpStatus: error.httpStatus ?? error.status, category: error.category,
|
|
@@ -502,12 +525,12 @@ export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
|
502
525
|
}
|
|
503
526
|
if (name === 'submit' && !options.allowRateLimited && modelRateLimit(provider, modelId)) {
|
|
504
527
|
const blocked = modelRateLimit(provider, modelId);
|
|
505
|
-
throw new ProviderContractError(blocked.until ? '该模型正在限流冷却,请稍后使用' : '该模型额度或预算耗尽,请手动实测成功后再使用', {
|
|
528
|
+
if (!(options.deferCooling && blocked.until)) throw new ProviderContractError(blocked.until ? '该模型正在限流冷却,请稍后使用' : '该模型额度或预算耗尽,请手动实测成功后再使用', {
|
|
506
529
|
stage: 'validate', category: 'rate_limit', acceptance: 'not_accepted'
|
|
507
530
|
});
|
|
508
531
|
}
|
|
509
532
|
try {
|
|
510
|
-
const result = await run(request);
|
|
533
|
+
const result = await run(request, context);
|
|
511
534
|
if (result.error) report(result.error);
|
|
512
535
|
availability(result.error || { ok: ['completed', 'accepted'].includes(result.kind) });
|
|
513
536
|
return result;
|
|
@@ -518,5 +541,14 @@ export function createConfiguredProviderAdapter(provider, options = {}) {
|
|
|
518
541
|
}
|
|
519
542
|
};
|
|
520
543
|
}
|
|
521
|
-
|
|
544
|
+
if (network && !hasProviderContext(adapter)) {
|
|
545
|
+
for (const name of Object.keys(operations)) {
|
|
546
|
+
if (name === 'mapError') continue;
|
|
547
|
+
const operation = operations[name];
|
|
548
|
+
operations[name] = (...args) => network.run(() => operation(...args), { ...args[1], operation: name });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return Object.freeze({ ...adapter, ...(requirement ? { networkRequirement: requirement } : {}), operations: Object.freeze(operations),
|
|
552
|
+
...(options.deferCooling ? { schedulingReadyAt: modelId => Date.parse(modelRateLimit(provider, modelId)?.until || '') || 0 } : {}),
|
|
553
|
+
...(network && !hasProviderContext(adapter) && adapter.prepareInput ? { prepareInput: (...args) => network.run(() => adapter.prepareInput(...args), { ...args[1], operation: 'prepareInput' }) } : {}) });
|
|
522
554
|
}
|
package/lib/provider-catalog.js
CHANGED
|
@@ -12,6 +12,8 @@ import { normalizeVisionInputLimits } from './vision-image-input.js';
|
|
|
12
12
|
import { modelDirectorySummary, modelUnavailableReason, validateModelDirectory } from './model-directory.js';
|
|
13
13
|
import { checkedComfyWorkflow, comfyWorkflowDigest } from './comfyui-workflow.js';
|
|
14
14
|
import { assertPluginDependencyMatches } from './plugin-contract.js';
|
|
15
|
+
import { validateProviderCredential, assertProviderCredential, providerCredentialStatus, createProviderSecretScrubber } from './provider-credentials.js';
|
|
16
|
+
import { decodeProviderConfig } from './provider-config-format.js';
|
|
15
17
|
|
|
16
18
|
export class ProviderCatalogError extends Error {
|
|
17
19
|
constructor(code, message) {
|
|
@@ -46,6 +48,7 @@ export function loadProviderCatalog(configPath) {
|
|
|
46
48
|
let parsed;
|
|
47
49
|
try { parsed = JSON.parse(source); }
|
|
48
50
|
catch (_) { fail('IRIS_PROVIDER_CONFIG_INVALID', 'provider-config 不是有效 JSON'); }
|
|
51
|
+
parsed = decodeProviderConfig(parsed);
|
|
49
52
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
|
|
50
53
|
|| !Array.isArray(parsed.providers)
|
|
51
54
|
|| (parsed.assignments !== undefined && (!parsed.assignments || typeof parsed.assignments !== 'object'
|
|
@@ -54,6 +57,7 @@ export function loadProviderCatalog(configPath) {
|
|
|
54
57
|
}
|
|
55
58
|
for (const [capability, assignment] of Object.entries(parsed.assignments || {})) functionPolicy(assignment, capability);
|
|
56
59
|
for (const provider of parsed.providers) {
|
|
60
|
+
validateProviderCredential(provider);
|
|
57
61
|
validateModelDirectory(provider.modelDirectory);
|
|
58
62
|
for (const model of provider.models || []) if (model?.comfyWorkflow !== undefined) checkedComfyWorkflow(model.comfyWorkflow);
|
|
59
63
|
}
|
|
@@ -67,9 +71,14 @@ const CANDIDATE_CAP_LABEL = Object.freeze({
|
|
|
67
71
|
});
|
|
68
72
|
|
|
69
73
|
/** 与 DSH 共用功能策略;CLI 不附加宿主资源。 */
|
|
70
|
-
function candidatesFromCatalog(catalog, requestedModelRef, capability, registry) {
|
|
74
|
+
function candidatesFromCatalog(catalog, requestedModelRef, capability, registry, { deferCooling = false } = {}) {
|
|
71
75
|
const label = CANDIDATE_CAP_LABEL[capability];
|
|
72
76
|
if (!label) fail('IRIS_PROVIDER_TASK_OBSERVE_UNSUPPORTED', '当前 Headless 候选链只开放图片、视频、语音与转写能力');
|
|
77
|
+
const requestedProvider = parseModelRef(String(requestedModelRef || '').trim())?.providerId;
|
|
78
|
+
if (requestedProvider) {
|
|
79
|
+
const provider = catalog.providers.find(item => item.id === requestedProvider && item.enabled !== false);
|
|
80
|
+
if (provider) assertProviderCredential(provider);
|
|
81
|
+
}
|
|
73
82
|
const providers = catalog.providers.filter(isConfiguredProvider);
|
|
74
83
|
const configuredPool = functionModelPool(providers, capability, catalog.assignments?.[capability], { registry });
|
|
75
84
|
const pool = orderedModels(configuredPool, capability, catalog.assignments?.[capability], { includeRateLimited: true, includeUnavailable: true });
|
|
@@ -81,7 +90,7 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability, registry)
|
|
|
81
90
|
if (!exact) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `指定模型未分配给${label}功能、未启用或没有所需能力`);
|
|
82
91
|
const provider = providers.find((item) => item.id === exact.providerId);
|
|
83
92
|
if (exact.unavailableCapabilities?.includes(capability)) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', modelUnavailableReason());
|
|
84
|
-
if (exact.rateLimited) fail('IRIS_PROVIDER_MODEL_RATE_LIMITED', modelRateLimit(provider, exact.id)?.until
|
|
93
|
+
if (exact.rateLimited && !(deferCooling && modelRateLimit(provider, exact.id)?.until)) fail('IRIS_PROVIDER_MODEL_RATE_LIMITED', modelRateLimit(provider, exact.id)?.until
|
|
85
94
|
? '指定模型正在限流冷却,请稍后使用或手动实测'
|
|
86
95
|
: '指定模型额度或预算耗尽,请手动实测成功后再使用');
|
|
87
96
|
return Object.freeze([Object.freeze({
|
|
@@ -90,8 +99,14 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability, registry)
|
|
|
90
99
|
})]);
|
|
91
100
|
}
|
|
92
101
|
const assignment = catalog.assignments?.[capability];
|
|
93
|
-
const ordered = pool.filter(model => !model.rateLimited &&
|
|
94
|
-
|
|
102
|
+
const ordered = pool.filter(model => (!model.rateLimited || deferCooling && modelRateLimit(providers.find(provider => provider.id === model.providerId), model.id)?.until)
|
|
103
|
+
&& !model.unavailableCapabilities?.includes(capability));
|
|
104
|
+
if (!ordered.length) {
|
|
105
|
+
const missing = catalog.providers.find(provider => provider.enabled !== false && provider.auth !== 'none' && provider.apiKeyEnv
|
|
106
|
+
&& providerCredentialStatus(provider) !== 'ready' && providerModels(provider).some(model => model.capabilities.includes(capability)));
|
|
107
|
+
if (missing) assertProviderCredential(missing);
|
|
108
|
+
fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `请检查${label}功能的启用方式、已确认模型、连接和协议;当前没有可用候选`);
|
|
109
|
+
}
|
|
95
110
|
return Object.freeze(ordered.map((item) => Object.freeze({
|
|
96
111
|
provider: providers.find((provider) => provider.id === item.providerId),
|
|
97
112
|
model: item.id,
|
|
@@ -100,17 +115,17 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability, registry)
|
|
|
100
115
|
})));
|
|
101
116
|
}
|
|
102
117
|
|
|
103
|
-
export function imageCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
104
|
-
return Object.freeze(candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.IMAGE, registry).map(route =>
|
|
118
|
+
export function imageCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
119
|
+
return Object.freeze(candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.IMAGE, registry, options).map(route =>
|
|
105
120
|
Object.freeze({ ...route, provider: providerForImageModel(route.provider, route.model, registry) })));
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
export function videoCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
109
|
-
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.VIDEO, registry);
|
|
123
|
+
export function videoCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
124
|
+
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.VIDEO, registry, options);
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
export function ttsCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
113
|
-
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TTS, registry);
|
|
127
|
+
export function ttsCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
128
|
+
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TTS, registry, options);
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
export function musicCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
@@ -118,8 +133,8 @@ export function musicCandidatesFromCatalog(catalog, requestedModelRef = '', regi
|
|
|
118
133
|
Object.freeze({ ...route, provider: providerForMusicModel(route.provider, route.model, registry) })));
|
|
119
134
|
}
|
|
120
135
|
|
|
121
|
-
export function transcribeCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
122
|
-
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TRANSCRIBE, registry);
|
|
136
|
+
export function transcribeCandidatesFromCatalog(catalog, requestedModelRef = '', registry, options) {
|
|
137
|
+
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TRANSCRIBE, registry, options);
|
|
123
138
|
}
|
|
124
139
|
|
|
125
140
|
export function visionCandidatesFromCatalog(catalog, requestedModelRef = '', registry) {
|
|
@@ -128,9 +143,10 @@ export function visionCandidatesFromCatalog(catalog, requestedModelRef = '', reg
|
|
|
128
143
|
|
|
129
144
|
/** 只读配置投影;不构造 Adapter、不发现模型、不暴露端点或凭据。 */
|
|
130
145
|
export function providerCatalogSnapshot(catalog) {
|
|
146
|
+
const scrub = createProviderSecretScrubber(catalog.providers);
|
|
131
147
|
const safeLabel = (value) => value
|
|
132
|
-
? redactProviderMessage(String(value).replace(/https?:\/\/[^\s<>"']+/gi, '[URL]')) : '';
|
|
133
|
-
return {
|
|
148
|
+
? redactProviderMessage(String(scrub(value)).replace(/https?:\/\/[^\s<>"']+/gi, '[URL]')) : '';
|
|
149
|
+
return scrub({
|
|
134
150
|
schemaVersion: 1,
|
|
135
151
|
providers: catalog.providers.filter((provider) => provider && typeof provider === 'object').map((provider) => {
|
|
136
152
|
const models = providerModels(provider);
|
|
@@ -145,7 +161,7 @@ export function providerCatalogSnapshot(catalog) {
|
|
|
145
161
|
modelCount: models.length, directory: modelDirectorySummary(provider)
|
|
146
162
|
};
|
|
147
163
|
})
|
|
148
|
-
};
|
|
164
|
+
});
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
export function catalogCapabilitySnapshot(catalog, registry) {
|
|
@@ -196,7 +212,8 @@ export function providerForTaskFromCatalog(catalog, task, registry) {
|
|
|
196
212
|
const matches = catalog.providers.filter((item) => item && item.id === task.providerId);
|
|
197
213
|
if (matches.length > 1) fail('IRIS_PROVIDER_CONFIG_AMBIGUOUS', 'provider-config 存在重复 Provider ID');
|
|
198
214
|
const configured = matches[0];
|
|
199
|
-
if (
|
|
215
|
+
if (configured && configured.enabled !== false) assertProviderCredential(configured);
|
|
216
|
+
if (!configured || configured.enabled === false || (configured.auth !== 'none' && providerCredentialStatus(configured) !== 'ready')) fail('IRIS_PROVIDER_TASK_PROVIDER_UNAVAILABLE', 'Task 原 Provider 已移除、停用或缺少凭据');
|
|
200
217
|
const provider = task.capability === 'image' ? providerForImageModel(configured, parsed.modelId, registry)
|
|
201
218
|
: task.capability === 'music' ? providerForMusicModel(configured, parsed.modelId, registry) : configured;
|
|
202
219
|
assertPluginDependencyMatches(task.pluginDependency, providerProtocolDescriptor(selectMediaProtocol(provider).mediaProtocol, registry)?.dependency);
|