@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -1
- package/README.en.md +109 -311
- package/README.md +106 -309
- package/bin/dsh-iris.js +452 -92
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +87 -0
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +101 -0
- package/docs/CORE_QUICKSTART.md +110 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +101 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +21 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +30 -5
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +42 -15
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +207 -0
- package/docs/MCP.md +212 -0
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/MUSIC.md +2 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +193 -0
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +113 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +82 -0
- package/docs/PROVIDER_HEALTH.md +5 -1
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +91 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +56 -4
- package/lib/adapters.js +75 -49
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +73 -17
- package/lib/application-writer.js +193 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +402 -72
- package/lib/comfyui-adapter.js +95 -0
- package/lib/comfyui-client.js +60 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/command-service.js +18 -5
- package/lib/config.js +49 -10
- package/lib/configured-image-runtime.js +69 -0
- package/lib/connection-skill.js +98 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +84 -28
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +85 -0
- package/lib/core-capability-catalog.js +192 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +174 -0
- package/lib/core-tasks.js +113 -21
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +41 -64
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +67 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +156 -14
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +97 -31
- package/lib/index.js +47 -22
- package/lib/local-api-projection.js +15 -6
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +861 -71
- package/lib/local-creation.js +96 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +39 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +174 -0
- package/lib/mcp-server.js +69 -0
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +37 -10
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +6 -3
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +281 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +307 -0
- package/lib/plugins.js +59 -0
- package/lib/provider-adapter.js +7 -3
- package/lib/provider-adapters.js +92 -22
- package/lib/provider-catalog.js +64 -36
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +143 -31
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +18 -6
- package/lib/provider-monitor.js +81 -0
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +73 -22
- package/lib/provider-registry.js +128 -0
- package/lib/provider-response.js +38 -0
- package/lib/provider-settings.js +136 -0
- package/lib/provider-task-runner.js +132 -43
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/skill-installation.js +23 -11
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +5 -0
- package/lib/web/app.js +703 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +516 -0
- package/lib/web/creation.js +359 -0
- package/lib/web/flow-view.js +34 -0
- package/lib/web/gallery.js +133 -0
- package/lib/web/index.html +344 -0
- package/lib/web/media-client.js +218 -0
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +223 -0
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +513 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +571 -0
- package/lib/web/transport.js +101 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +3 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +24 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +5 -1
package/lib/core-tasks.js
CHANGED
|
@@ -8,7 +8,10 @@ import { parseModelRef } from './models.js';
|
|
|
8
8
|
import { providerErrorRecord } from './provider-contract.js';
|
|
9
9
|
import { deriveLegacyStatus, semanticViolations } from './task-semantics.js';
|
|
10
10
|
import { readDeliveryCache } from './core-delivery-cache.js';
|
|
11
|
-
import { flowTaskBinding, readFlowRun, validFlowBinding } from './flow-store.js';
|
|
11
|
+
import { flowStep, flowStepId, flowTaskBinding, flowCapability, readFlowRun, validFlowBinding } from './flow-store.js';
|
|
12
|
+
import { checkedPluginDependency, assertPluginDependencyMatches, PluginContractError } from './plugin-contract.js';
|
|
13
|
+
import { checkedNetworkRequirement, sameNetworkRequirement } from './network-policy.js';
|
|
14
|
+
import { checkedNetworkRouteFact, checkedNetworkRouteRecord, networkRouteFactKey, MAX_NETWORK_ROUTE_FACTS } from './network-route-evidence.js';
|
|
12
15
|
|
|
13
16
|
const STORE_DIR = 'task-store';
|
|
14
17
|
const STORE_VERSION = 'v0';
|
|
@@ -103,7 +106,7 @@ function appendAttemptStage(attempt, key, reference) {
|
|
|
103
106
|
|
|
104
107
|
function validateRecord(record, expectedId) {
|
|
105
108
|
if (!record || typeof record !== 'object' || Array.isArray(record)
|
|
106
|
-
|| record.schemaVersion !== 0 || record.semanticsVersion
|
|
109
|
+
|| record.schemaVersion !== 0 || ![2, 3].includes(record.semanticsVersion)
|
|
107
110
|
|| record.id !== expectedId || !CAPABILITIES.has(record.capability)
|
|
108
111
|
|| !Array.isArray(record.attempts) || !Array.isArray(record.artifactIds)
|
|
109
112
|
|| !Number.isSafeInteger(record.revision) || record.revision < 1
|
|
@@ -111,8 +114,9 @@ function validateRecord(record, expectedId) {
|
|
|
111
114
|
|| (record.deliveryCache !== undefined && record.deliveryCache !== true)
|
|
112
115
|
|| (record.providerBinding !== undefined && !PROVIDER_BINDING.test(record.providerBinding))
|
|
113
116
|
|| (record.retriedFrom !== undefined && !TASK_ID.test(record.retriedFrom))
|
|
114
|
-
|| (record.flowBinding !== undefined && (
|
|
115
|
-
|
|
117
|
+
|| (record.flowBinding !== undefined && (!validFlowBinding(record.flowBinding)
|
|
118
|
+
|| ({ edit: 'image', generate: 'image', speak: 'tts', transcribe: 'transcribe', video: 'video' })[record.flowBinding.stepId] !== record.capability))
|
|
119
|
+
|| (record.sourceArtifactId !== undefined && (!['image', 'transcribe'].includes(record.capability) || !ARTIFACT_ID.test(record.sourceArtifactId)))) {
|
|
116
120
|
fail('IRIS_TASK_INVALID', 'Task 记录结构无效');
|
|
117
121
|
}
|
|
118
122
|
if (record.attempts.some((attempt, index) => !attempt || !ATTEMPT_ID.test(attempt.id)
|
|
@@ -123,6 +127,36 @@ function validateRecord(record, expectedId) {
|
|
|
123
127
|
|| !validAttemptStageTimestamps(attempt.stageTimestamps))) {
|
|
124
128
|
fail('IRIS_TASK_INVALID', 'Task Attempt 记录结构无效');
|
|
125
129
|
}
|
|
130
|
+
try {
|
|
131
|
+
if (record.semanticsVersion === 3) {
|
|
132
|
+
checkedNetworkRequirement(record.networkRequirement);
|
|
133
|
+
if (!Array.isArray(record.networkRoutes) || record.networkRoutes.length > MAX_NETWORK_ROUTE_FACTS) throw new Error('routes');
|
|
134
|
+
for (const attempt of record.attempts) {
|
|
135
|
+
checkedNetworkRequirement(attempt.networkRequirement);
|
|
136
|
+
if (!sameNetworkRequirement(record.networkRequirement, attempt.networkRequirement)) throw new Error('attempt requirement');
|
|
137
|
+
}
|
|
138
|
+
const keys = new Set();
|
|
139
|
+
for (const route of record.networkRoutes) {
|
|
140
|
+
const { phase, firstAt, lastAt, requestCount, ...fact } = checkedNetworkRouteRecord(route);
|
|
141
|
+
if (!sameNetworkRequirement(record.networkRequirement, { schemaVersion: 1, configId: fact.configId, catalogId: fact.catalogId })) throw new Error('route requirement');
|
|
142
|
+
const attempt = fact.attemptId === undefined ? undefined : record.attempts.find(value => value.id === fact.attemptId);
|
|
143
|
+
if (fact.attemptId !== undefined && (!attempt || attempt.providerId !== fact.providerId)) throw new Error('route attempt');
|
|
144
|
+
const key = networkRouteFactKey(fact);
|
|
145
|
+
if (keys.has(key)) throw new Error('duplicate route');
|
|
146
|
+
keys.add(key);
|
|
147
|
+
}
|
|
148
|
+
} else if (record.networkRequirement !== undefined || record.networkRoutes !== undefined
|
|
149
|
+
|| record.attempts.some(attempt => attempt.networkRequirement !== undefined)) throw new Error('legacy requirement');
|
|
150
|
+
} catch (_) { fail('IRIS_TASK_INVALID', 'Task 或 Attempt 的网络要求、版本或路线事实无效'); }
|
|
151
|
+
try {
|
|
152
|
+
for (const item of [record, ...record.attempts]) if (item.pluginDependency !== undefined) {
|
|
153
|
+
if (checkedPluginDependency(item.pluginDependency).kind !== 'providers' || !item.providerBinding) fail('IRIS_TASK_INVALID', 'Task 插件依赖需要 Provider 与端点绑定');
|
|
154
|
+
}
|
|
155
|
+
assertPluginDependencyMatches(record.pluginDependency, record.attempts.at(-1)?.pluginDependency);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (!(error instanceof PluginContractError)) throw error;
|
|
158
|
+
fail('IRIS_TASK_INVALID', 'Task 或 Attempt 的插件依赖无效或不一致');
|
|
159
|
+
}
|
|
126
160
|
const violations = semanticViolations(record);
|
|
127
161
|
if (violations.length) fail('IRIS_TASK_INVALID', 'Task 事实语义冲突:' + violations.join(', '));
|
|
128
162
|
return record;
|
|
@@ -178,11 +212,13 @@ function newTaskRecord(input, id) {
|
|
|
178
212
|
const retriedFrom = input.retriedFrom === undefined ? undefined : String(input.retriedFrom);
|
|
179
213
|
if (retriedFrom !== undefined && !TASK_ID.test(retriedFrom)) fail('IRIS_TASK_INPUT_INVALID', 'retriedFrom 必须是合法 Task ID');
|
|
180
214
|
const sourceArtifactId = input.sourceArtifactId;
|
|
181
|
-
if (sourceArtifactId !== undefined && (
|
|
215
|
+
if (sourceArtifactId !== undefined && (!['image', 'transcribe'].includes(capability) || !ARTIFACT_ID.test(sourceArtifactId))) fail('IRIS_TASK_INPUT_INVALID', '来源须为改图或转写的合法 Artifact ID');
|
|
182
216
|
const now = new Date().toISOString();
|
|
217
|
+
const networkRequirement = input.networkRequirement === undefined ? undefined : checkedNetworkRequirement(input.networkRequirement);
|
|
183
218
|
return {
|
|
184
219
|
schemaVersion: 0,
|
|
185
|
-
semanticsVersion: 2,
|
|
220
|
+
semanticsVersion: networkRequirement ? 3 : 2,
|
|
221
|
+
...(networkRequirement ? { networkRequirement, networkRoutes: [] } : {}),
|
|
186
222
|
id,
|
|
187
223
|
capability,
|
|
188
224
|
status: 'running',
|
|
@@ -210,9 +246,11 @@ export function createCoreTask(dataRoot, input = {}) {
|
|
|
210
246
|
/** Flow 只能接回自己持久预留的 Task;原有 Task 没有该字段,读取语义不变。 */
|
|
211
247
|
export function assertFlowTaskMatches(task, run) {
|
|
212
248
|
const providerId = parseModelRef(run.input.model_ref).providerId;
|
|
213
|
-
if (task.id !== run.
|
|
249
|
+
if (task.id !== flowStep(run).taskId || task.capability !== flowCapability(run) || task.sourceArtifactId !== run.source?.artifactId
|
|
214
250
|
|| !validFlowBinding(task.flowBinding) || task.flowBinding.runId !== run.id
|
|
251
|
+
|| task.flowBinding.stepId !== flowStepId(run)
|
|
215
252
|
|| task.flowBinding.inputDigest !== run.inputDigest
|
|
253
|
+
|| !sameNetworkRequirement(task.networkRequirement, run.networkRequirement)
|
|
216
254
|
|| task.attempts.some(attempt => attempt.providerId !== providerId
|
|
217
255
|
|| attempt.model !== run.input.model_ref || attempt.providerBinding !== run.providerBinding)
|
|
218
256
|
|| (task.attempts.length && (task.providerId !== providerId || task.modelRef !== run.input.model_ref || task.providerBinding !== run.providerBinding))) {
|
|
@@ -228,13 +266,13 @@ export function findFlowTask(dataRoot, run) {
|
|
|
228
266
|
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('IRIS_FLOW_TASK_CONFLICT', '流程 Task 目录不安全');
|
|
229
267
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
230
268
|
}
|
|
231
|
-
try { fs.lstatSync(taskFile(dataRoot, run.
|
|
269
|
+
try { fs.lstatSync(taskFile(dataRoot, flowStep(run).taskId)); }
|
|
232
270
|
catch (error) {
|
|
233
271
|
if (error.code !== 'ENOENT') throw error;
|
|
234
|
-
if (run.
|
|
272
|
+
if (flowStep(run).claimed) fail('IRIS_FLOW_TASK_CONFLICT', '流程已建立的 Task 丢失,禁止重建或重新生成');
|
|
235
273
|
return null;
|
|
236
274
|
}
|
|
237
|
-
return assertFlowTaskMatches(readRecord(dataRoot, run.
|
|
275
|
+
return assertFlowTaskMatches(readRecord(dataRoot, flowStep(run).taskId), run);
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
/** 仅从已提交的 Run 读取 ID;排他发布,新旧身份均不能覆盖。 */
|
|
@@ -242,7 +280,8 @@ export function claimFlowTask(dataRoot, runId) {
|
|
|
242
280
|
const run = readFlowRun(dataRoot, runId);
|
|
243
281
|
const previous = findFlowTask(dataRoot, run);
|
|
244
282
|
if (previous) return previous;
|
|
245
|
-
return writeRecord(dataRoot, { ...newTaskRecord({ capability:
|
|
283
|
+
return writeRecord(dataRoot, { ...newTaskRecord({ capability: flowCapability(run), sourceArtifactId: run.source?.artifactId,
|
|
284
|
+
...(run.networkRequirement ? { networkRequirement: run.networkRequirement } : {}) }, flowStep(run).taskId),
|
|
246
285
|
flowBinding: flowTaskBinding(run) }, true);
|
|
247
286
|
}
|
|
248
287
|
|
|
@@ -255,9 +294,7 @@ export function inspectCoreTask(dataRoot, id) {
|
|
|
255
294
|
* skipInvalid 只服务用户侧只读投影:单条损坏记录跳过并计入 dropped,
|
|
256
295
|
* 其余记录照常返回;默认严格模式不变,损坏记录仍使整体读取失败。
|
|
257
296
|
*/
|
|
258
|
-
|
|
259
|
-
const start = Math.max(0, Math.trunc(Number(offset) || 0));
|
|
260
|
-
const size = Math.max(1, Math.min(200, Math.trunc(Number(limit) || 50)));
|
|
297
|
+
function taskRecords(dataRoot, skipInvalid = false) {
|
|
261
298
|
const directory = taskDirectory(dataRoot);
|
|
262
299
|
let names;
|
|
263
300
|
for (const managed of [
|
|
@@ -267,10 +304,7 @@ export function listCoreTasks(dataRoot, { offset = 0, limit = 50, skipInvalid =
|
|
|
267
304
|
try { stat = fs.lstatSync(managed); }
|
|
268
305
|
catch (error) {
|
|
269
306
|
if (error?.code === 'ENOENT') {
|
|
270
|
-
return
|
|
271
|
-
schemaVersion: 0, total: 0, offset: start, limit: size, dropped: 0,
|
|
272
|
-
tasks: Object.freeze([])
|
|
273
|
-
});
|
|
307
|
+
return { records: [], dropped: 0 };
|
|
274
308
|
}
|
|
275
309
|
fail('IRIS_TASK_STORE_INVALID', 'Task Store 无法读取');
|
|
276
310
|
}
|
|
@@ -288,6 +322,20 @@ export function listCoreTasks(dataRoot, { offset = 0, limit = 50, skipInvalid =
|
|
|
288
322
|
dropped += 1;
|
|
289
323
|
}
|
|
290
324
|
}
|
|
325
|
+
return { records, dropped };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Strict single-pass admission view, including pending intents after a crash. No mutation or pagination rescan. */
|
|
329
|
+
export function coreRemoteTaskIdentities(dataRoot) {
|
|
330
|
+
return taskRecords(dataRoot).records.filter(task => !['succeeded', 'failed', 'canceled'].includes(task.outcome)
|
|
331
|
+
&& (['accepted', 'unknown'].includes(task.acceptance) || task.phase === 'submitting' && task.attempts.length))
|
|
332
|
+
.map(task => ({ providerId: task.providerId, modelRef: task.modelRef }));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function listCoreTasks(dataRoot, { offset = 0, limit = 50, skipInvalid = false, status, capability, provider_id, model_ref, outcome, delivery_state } = {}) {
|
|
336
|
+
const start = Math.max(0, Math.trunc(Number(offset) || 0));
|
|
337
|
+
const size = Math.max(1, Math.min(200, Math.trunc(Number(limit) || 50)));
|
|
338
|
+
const { records, dropped } = taskRecords(dataRoot, skipInvalid);
|
|
291
339
|
records.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
292
340
|
const matches = records.filter(record => Object.entries({ status, capability, providerId: provider_id, modelRef: model_ref, outcome, deliveryState: delivery_state })
|
|
293
341
|
.every(([field, value]) => value === undefined || record[field] === value));
|
|
@@ -302,13 +350,17 @@ export function beginCoreAttempt(dataRoot, taskId, fields = {}) {
|
|
|
302
350
|
const providerId = String(fields.providerId || '').trim();
|
|
303
351
|
const model = String(fields.model || '').trim();
|
|
304
352
|
const parsed = parseModelRef(model);
|
|
353
|
+
const pluginDependency = fields.pluginDependency === undefined ? undefined : checkedPluginDependency(fields.pluginDependency);
|
|
354
|
+
const networkRequirement = fields.networkRequirement === undefined ? undefined : checkedNetworkRequirement(fields.networkRequirement);
|
|
305
355
|
if (!providerId || !parsed || parsed.providerId !== providerId
|
|
306
356
|
|| (fields.providerBinding !== undefined && !PROVIDER_BINDING.test(fields.providerBinding))
|
|
307
|
-
|| (fields.selectionReason !== undefined && !ATTEMPT_SELECTION_REASONS.has(fields.selectionReason))
|
|
357
|
+
|| (fields.selectionReason !== undefined && !ATTEMPT_SELECTION_REASONS.has(fields.selectionReason))
|
|
358
|
+
|| (pluginDependency && (pluginDependency.kind !== 'providers' || !fields.providerBinding))) {
|
|
308
359
|
fail('IRIS_ATTEMPT_INPUT_INVALID', 'Attempt 的 providerId 与复合 model 必须一致');
|
|
309
360
|
}
|
|
310
361
|
let created;
|
|
311
362
|
mutate(dataRoot, taskId, (task) => {
|
|
363
|
+
if (!sameNetworkRequirement(task.networkRequirement, networkRequirement)) fail('IRIS_NETWORK_REQUIREMENT_MISMATCH', '新 Attempt 必须保持原 Task 的网络要求,不会隐式升级或解除');
|
|
312
364
|
if (task.acceptance === 'accepted' || task.acceptance === 'unknown') {
|
|
313
365
|
fail('IRIS_TASK_RESUBMIT_FORBIDDEN', 'Task 已受理或受理未知,禁止创建新 Attempt');
|
|
314
366
|
}
|
|
@@ -325,7 +377,9 @@ export function beginCoreAttempt(dataRoot, taskId, fields = {}) {
|
|
|
325
377
|
startedAt: submitStartedAt,
|
|
326
378
|
stageTimestamps: { queuedAt, submitStartedAt },
|
|
327
379
|
...(fields.selectionReason !== undefined ? { selectionReason: fields.selectionReason } : {}),
|
|
328
|
-
...(fields.providerBinding !== undefined ? { providerBinding: fields.providerBinding } : {})
|
|
380
|
+
...(fields.providerBinding !== undefined ? { providerBinding: fields.providerBinding } : {}),
|
|
381
|
+
...(networkRequirement ? { networkRequirement } : {}),
|
|
382
|
+
...(pluginDependency ? { pluginDependency } : {})
|
|
329
383
|
};
|
|
330
384
|
task.attempts.push(created);
|
|
331
385
|
task.activeAttemptId = created.id;
|
|
@@ -333,6 +387,8 @@ export function beginCoreAttempt(dataRoot, taskId, fields = {}) {
|
|
|
333
387
|
task.modelRef = model;
|
|
334
388
|
if (created.providerBinding) task.providerBinding = created.providerBinding;
|
|
335
389
|
else delete task.providerBinding;
|
|
390
|
+
if (created.pluginDependency) task.pluginDependency = created.pluginDependency;
|
|
391
|
+
else delete task.pluginDependency;
|
|
336
392
|
task.phase = 'submitting';
|
|
337
393
|
task.acceptance = 'none';
|
|
338
394
|
task.watchState = 'idle';
|
|
@@ -346,6 +402,37 @@ export function beginCoreAttempt(dataRoot, taskId, fields = {}) {
|
|
|
346
402
|
return Object.freeze(clone(created));
|
|
347
403
|
}
|
|
348
404
|
|
|
405
|
+
/** Persist before dispatch. A record proves an attempted route, never acceptance or public IP. */
|
|
406
|
+
export function recordCoreNetworkRoute(dataRoot, taskId, input) {
|
|
407
|
+
const supplied = checkedNetworkRouteFact(input);
|
|
408
|
+
return mutate(dataRoot, taskId, task => {
|
|
409
|
+
const requirement = { schemaVersion: 1, configId: supplied.configId, catalogId: supplied.catalogId };
|
|
410
|
+
if (task.semanticsVersion !== 3 || !sameNetworkRequirement(task.networkRequirement, requirement)) {
|
|
411
|
+
fail('IRIS_NETWORK_REQUIREMENT_MISMATCH', '路线事实必须属于原 Task 的已保存网络要求;不会隐式转换旧任务');
|
|
412
|
+
}
|
|
413
|
+
const attemptId = supplied.attemptId ?? task.activeAttemptId;
|
|
414
|
+
const fact = checkedNetworkRouteFact({ ...supplied, ...(attemptId ? { attemptId } : {}) });
|
|
415
|
+
const attempt = attemptId ? task.attempts.find(value => value.id === attemptId) : undefined;
|
|
416
|
+
if (attemptId && (!attempt || attempt.providerId !== fact.providerId)
|
|
417
|
+
|| !attemptId && task.providerId !== undefined && task.providerId !== fact.providerId) {
|
|
418
|
+
fail('IRIS_NETWORK_ROUTE_INVALID', '路线事实与 Task 原 Provider 或 Attempt 不一致');
|
|
419
|
+
}
|
|
420
|
+
const key = networkRouteFactKey(fact);
|
|
421
|
+
const previous = task.networkRoutes.find(value => {
|
|
422
|
+
const { phase, firstAt, lastAt, requestCount, ...identity } = value;
|
|
423
|
+
return networkRouteFactKey(identity) === key;
|
|
424
|
+
});
|
|
425
|
+
const now = timestampAfter(task.updatedAt, previous?.lastAt);
|
|
426
|
+
if (previous) {
|
|
427
|
+
if (previous.requestCount === Number.MAX_SAFE_INTEGER) fail('IRIS_NETWORK_ROUTE_LIMIT', '路线事实计数已超出支持范围,停止派发');
|
|
428
|
+
previous.requestCount++; previous.lastAt = now;
|
|
429
|
+
} else {
|
|
430
|
+
if (task.networkRoutes.length === MAX_NETWORK_ROUTE_FACTS) fail('IRIS_NETWORK_ROUTE_LIMIT', 'Task 不同路线事实已达128项,停止派发并保留历史');
|
|
431
|
+
task.networkRoutes.push({ ...fact, phase: 'dispatch_started', firstAt: now, lastAt: now, requestCount: 1 });
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
349
436
|
export function recordCoreAttemptResult(dataRoot, taskId, snapshot) {
|
|
350
437
|
return mutate(dataRoot, taskId, (task) => {
|
|
351
438
|
const attempt = task.attempts.find((item) => item.id === snapshot?.id);
|
|
@@ -519,7 +606,7 @@ export function failCoreDelivery(dataRoot, taskId, error) {
|
|
|
519
606
|
task.phase = 'terminal';
|
|
520
607
|
task.watchState = 'idle';
|
|
521
608
|
task.deliveryState = 'failed';
|
|
522
|
-
task.lastError = safeError(error, { stage: 'download', category: 'local_io', acceptance: 'accepted' });
|
|
609
|
+
task.lastError = safeError(error, { stage: 'download', category: error?.category || 'local_io', acceptance: 'accepted' });
|
|
523
610
|
task.finishedAt = new Date().toISOString();
|
|
524
611
|
});
|
|
525
612
|
}
|
|
@@ -599,6 +686,11 @@ export function recoverCoreTask(dataRoot, taskId) {
|
|
|
599
686
|
});
|
|
600
687
|
});
|
|
601
688
|
}
|
|
689
|
+
// 拒绝受理已落盘、最终失败尚未落盘时,只收口原 Attempt;恢复绝不接着试下一候选。
|
|
690
|
+
if (current.phase === 'terminal' && current.acceptance === 'not_accepted' && current.outcome === 'none') {
|
|
691
|
+
return finalizeCoreNoAcceptance(dataRoot, taskId,
|
|
692
|
+
current.lastError || '供应商明确未受理;提交在最终状态落盘前中断');
|
|
693
|
+
}
|
|
602
694
|
if (current.outcome === 'succeeded' && ['pending', 'downloading'].includes(current.deliveryState)) {
|
|
603
695
|
return mutate(dataRoot, taskId, (task) => {
|
|
604
696
|
task.phase = 'terminal';
|
package/lib/core-writer-lease.js
CHANGED
|
@@ -44,6 +44,22 @@ function optionalRecord(file) {
|
|
|
44
44
|
catch (error) { if (error?.code === 'ENOENT') return null; throw error; }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Recognize only an unpublished preparation from a proven-dead owner. Keep it
|
|
48
|
+
* as evidence: initialization may ignore this metadata, but never removes it. */
|
|
49
|
+
export function isAbandonedWriterPreparation(root, name) {
|
|
50
|
+
const match = /^\.iris-runtime-writer-v0\.owner\.json\.prepare-([1-9][0-9]*)-[a-f0-9]{12}$/.exec(name);
|
|
51
|
+
if (!match) return false;
|
|
52
|
+
try {
|
|
53
|
+
const file = path.join(root, name), stat = fs.lstatSync(file);
|
|
54
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) return false;
|
|
55
|
+
const owner = checkedOwner(readRecord(file).value);
|
|
56
|
+
return String(owner.pid) === match[1] && writerOwnerStatus(owner.pid) === 'missing';
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.code && !['ENOENT', 'EACCES', 'EPERM'].includes(error.code)) throw error;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
/** 完整私有记录先在根中准备,再以 hard link 原子发布;已有目标绝不覆盖。 */
|
|
48
64
|
export function publishLeaseRecord(root, target, value) {
|
|
49
65
|
const prepared = privateSibling(path.join(root, WRITER_INTENT_FILE), 'prepare');
|
|
@@ -3,8 +3,11 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { validateFlowCreation } from './flow-store.js';
|
|
7
|
+
import { inspectLocalRequestRoot, initializeLocalRequestRoot, requestDirectory } from './local-request-root.js';
|
|
8
|
+
import { normalizeComfyParameters } from './comfyui-workflow.js';
|
|
9
|
+
import { AUDIO_TYPES, MAX_AUDIO_INPUT_BYTES, normalizeAudioFlowInput } from './audio-flow-input.js';
|
|
10
|
+
import { VIDEO_RECIPE, normalizeVideoFlowInput } from './video-flow-input.js';
|
|
8
11
|
|
|
9
12
|
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
10
13
|
const ID = /^req_[a-f0-9]{64}$/;
|
|
@@ -13,6 +16,8 @@ const SHA = /^[a-f0-9]{64}$/;
|
|
|
13
16
|
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
14
17
|
const TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
|
15
18
|
const MAX_IMAGE = 20 * 1024 * 1024;
|
|
19
|
+
export const CREATION_KINDS = Object.freeze(['image-import', 'flow-create', 'generation-create', 'audio-import', 'speech-create', 'transcription-create', 'video-create']);
|
|
20
|
+
export const isImportCreation = kind => ['image-import', 'audio-import'].includes(kind);
|
|
16
21
|
const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
17
22
|
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
18
23
|
const copy = value => structuredClone(value);
|
|
@@ -22,16 +27,6 @@ function keys(value, allowed, required = allowed) {
|
|
|
22
27
|
}
|
|
23
28
|
export function creationFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
24
29
|
const invalid = () => creationFail('IRIS_CREATION_STORE_INVALID', '创建请求区未知、损坏或尚未完整初始化');
|
|
25
|
-
function directory(root, segments, create = false) {
|
|
26
|
-
let current = root;
|
|
27
|
-
for (const segment of segments) {
|
|
28
|
-
current = path.join(current, segment);
|
|
29
|
-
if (create && !fs.existsSync(current)) fs.mkdirSync(current, { mode: 0o700 });
|
|
30
|
-
const stat = fs.lstatSync(current);
|
|
31
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
|
|
32
|
-
}
|
|
33
|
-
return current;
|
|
34
|
-
}
|
|
35
30
|
function jsonFile(file) {
|
|
36
31
|
const stat = fs.lstatSync(file);
|
|
37
32
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) invalid();
|
|
@@ -39,54 +34,29 @@ function jsonFile(file) {
|
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
export function inspectCreationRoot(root, { allowAbsent = false } = {}) {
|
|
42
|
-
|
|
43
|
-
try { base = directory(root, ['creation-requests']); }
|
|
44
|
-
catch (error) {
|
|
45
|
-
if (error.code === 'ENOENT' && !fs.existsSync(path.join(root, 'creation-requests'))) {
|
|
46
|
-
if (allowAbsent) return null;
|
|
47
|
-
creationFail('IRIS_CREATION_ROOT_REQUIRED', '请先在空的专用 Flow 根显式执行 api init');
|
|
48
|
-
}
|
|
49
|
-
if (error.code === 'ENOENT') invalid();
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
if (fs.readdirSync(base).some(name => name !== 'v0')) invalid();
|
|
54
|
-
const version = directory(root, ['creation-requests', 'v0']);
|
|
55
|
-
if (fs.readdirSync(version).some(name => !['root.json', 'requests'].includes(name)
|
|
56
|
-
&& !/^root\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name))) invalid();
|
|
57
|
-
const marker = jsonFile(path.join(version, 'root.json'));
|
|
58
|
-
if (!keys(marker, ['schemaVersion', 'kind', 'rootId', 'createdAt']) || marker.schemaVersion !== 0
|
|
59
|
-
|| marker.kind !== 'iris-creation-requests' || !UUID.test(marker.rootId) || !iso(marker.createdAt)) invalid();
|
|
60
|
-
directory(root, ['creation-requests', 'v0', 'requests']);
|
|
61
|
-
return copy(marker);
|
|
62
|
-
} catch (error) { if (error.code === 'ENOENT') invalid(); throw error; }
|
|
37
|
+
return inspectLocalRequestRoot(root, { allowAbsent });
|
|
63
38
|
}
|
|
64
39
|
|
|
65
40
|
export function initializeCreationRoot(root) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (fs.readdirSync(root).some(name => !['flow-store', WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name))
|
|
70
|
-
|| fs.readdirSync(path.join(root, 'flow-store')).some(name => name !== 'v0')
|
|
71
|
-
|| fs.readdirSync(path.join(root, 'flow-store/v0')).some(name => !['root.json', 'runs'].includes(name))
|
|
72
|
-
|| fs.readdirSync(path.join(root, 'flow-store/v0/runs')).length) {
|
|
73
|
-
creationFail('IRIS_CREATION_ROOT_NOT_EMPTY', '请求区仅在新的空 Flow 根初始化;不转换已有创作数据');
|
|
74
|
-
}
|
|
75
|
-
const marker = { schemaVersion: 0, kind: 'iris-creation-requests', rootId: crypto.randomUUID(), createdAt: new Date().toISOString() };
|
|
76
|
-
directory(root, ['creation-requests', 'v0', 'requests'], true);
|
|
77
|
-
atomicCreatePrivate(path.join(root, 'creation-requests/v0/root.json'), JSON.stringify(marker, null, 2) + '\n');
|
|
78
|
-
return copy(marker);
|
|
41
|
+
const marker = initializeLocalRequestRoot(root, 0);
|
|
42
|
+
listCreationRequests(root);
|
|
43
|
+
return marker;
|
|
79
44
|
}
|
|
80
45
|
|
|
81
46
|
export function creationRequestId(marker, kind, key) {
|
|
82
47
|
if (!UUID.test(key)) creationFail('IRIS_CREATION_KEY_INVALID', 'Idempotency-Key 须为单个小写 UUIDv4');
|
|
83
|
-
if (!
|
|
48
|
+
if (!CREATION_KINDS.includes(kind)) throw new TypeError('未知本地创建类型');
|
|
84
49
|
return 'req_' + hash(JSON.stringify(['creation-requests/v0', marker.rootId, kind, key]));
|
|
85
50
|
}
|
|
86
51
|
export function creationInputDigest(kind, input) {
|
|
87
|
-
return 'sha256:' + hash(JSON.stringify(
|
|
88
|
-
|
|
89
|
-
|
|
52
|
+
if (kind === 'video-create') return 'sha256:' + hash(JSON.stringify(['video-create/v0', normalizeVideoFlowInput(input)]));
|
|
53
|
+
if (['speech-create', 'transcription-create'].includes(kind)) return 'sha256:' + hash(JSON.stringify([kind + '/v0',
|
|
54
|
+
normalizeAudioFlowInput(kind === 'speech-create' ? 'speech/v0' : 'transcribe/v0', input)]));
|
|
55
|
+
return 'sha256:' + hash(JSON.stringify(isImportCreation(kind)
|
|
56
|
+
? [kind + '/v0', input.mediaType, input.size, input.sha256]
|
|
57
|
+
: kind === 'generation-create' ? ['generation-create/v0', 'image-generate/v0', input.model_ref, input.prompt, input.size ?? null, 1,
|
|
58
|
+
...(input.workflow_parameters !== undefined ? [['comfyui-parameters/v0', normalizeComfyParameters(input.workflow_parameters)]] : [])]
|
|
59
|
+
: ['flow-create/v0', 'image-edit/v0', input.source_artifact_id, input.model_ref, input.prompt, input.size ?? null, 1]));
|
|
90
60
|
}
|
|
91
61
|
function validate(record, id, marker) {
|
|
92
62
|
if (!keys(record, ['schemaVersion', 'id', 'rootId', 'kind', 'inputDigest', 'createdAt', 'intent', 'state', 'completedAt', 'result'],
|
|
@@ -96,14 +66,20 @@ function validate(record, id, marker) {
|
|
|
96
66
|
|| !['prepared', 'succeeded'].includes(record.state)) invalid();
|
|
97
67
|
const intent = record.intent;
|
|
98
68
|
let expectedResult, expectedDigest;
|
|
99
|
-
if (record.kind
|
|
69
|
+
if (isImportCreation(record.kind)) {
|
|
70
|
+
const audio = record.kind === 'audio-import';
|
|
100
71
|
if (!keys(intent, ['artifactId', 'mediaType', 'size', 'sha256', 'metadata', 'createdAt'])
|
|
101
|
-
||
|
|
102
|
-
|| !
|
|
103
|
-
|| !
|
|
72
|
+
|| audio && ![3, 4].includes(marker.schemaVersion)
|
|
73
|
+
|| !ARTIFACT.test(intent.artifactId) || !(audio ? Object.hasOwn(AUDIO_TYPES, intent.mediaType) : TYPES.has(intent.mediaType)) || !SHA.test(intent.sha256)
|
|
74
|
+
|| !Number.isSafeInteger(intent.size) || intent.size < 1 || intent.size > (audio ? MAX_AUDIO_INPUT_BYTES : MAX_IMAGE)
|
|
75
|
+
|| (audio ? !keys(intent.metadata, []) : !keys(intent.metadata, ['width', 'height']) || !['width', 'height'].every(key => Number.isSafeInteger(intent.metadata[key]) && intent.metadata[key] > 0))) invalid();
|
|
104
76
|
expectedDigest = creationInputDigest(record.kind, intent);
|
|
105
77
|
expectedResult = { artifactId: intent.artifactId };
|
|
106
|
-
} else if (
|
|
78
|
+
} else if (['flow-create', 'generation-create', 'speech-create', 'transcription-create', 'video-create'].includes(record.kind)) {
|
|
79
|
+
if (record.kind === 'video-create' ? marker.schemaVersion !== 4 || intent.recipe !== VIDEO_RECIPE
|
|
80
|
+
: record.kind === 'generation-create' ? ![2, 3, 4].includes(marker.schemaVersion) || intent.recipe !== 'image-generate/v0'
|
|
81
|
+
: record.kind === 'flow-create' ? intent.recipe !== undefined
|
|
82
|
+
: ![3, 4].includes(marker.schemaVersion) || intent.recipe !== (record.kind === 'speech-create' ? 'speech/v0' : 'transcribe/v0')) invalid();
|
|
107
83
|
try { validateFlowCreation(intent); } catch (_) { invalid(); }
|
|
108
84
|
expectedDigest = creationInputDigest(record.kind, intent.input);
|
|
109
85
|
expectedResult = { runId: intent.runId };
|
|
@@ -118,7 +94,7 @@ function validate(record, id, marker) {
|
|
|
118
94
|
export function readCreationRequest(root, id, marker = inspectCreationRoot(root)) {
|
|
119
95
|
if (!ID.test(id)) invalid();
|
|
120
96
|
let record;
|
|
121
|
-
try { record = jsonFile(path.join(root, '
|
|
97
|
+
try { record = jsonFile(path.join(requestDirectory(root, marker, 'requests'), id + '.json')); }
|
|
122
98
|
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
123
99
|
return copy(validate(record, id, marker));
|
|
124
100
|
}
|
|
@@ -126,7 +102,7 @@ export function listCreationRequests(root) {
|
|
|
126
102
|
const marker = inspectCreationRoot(root, { allowAbsent: true });
|
|
127
103
|
if (!marker) return [];
|
|
128
104
|
const records = [];
|
|
129
|
-
for (const name of fs.readdirSync(
|
|
105
|
+
for (const name of fs.readdirSync(requestDirectory(root, marker, 'requests'))) {
|
|
130
106
|
if (/^req_[a-f0-9]{64}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name)) continue;
|
|
131
107
|
if (!/^req_[a-f0-9]{64}\.json$/.test(name)) invalid();
|
|
132
108
|
const record = readCreationRequest(root, name.slice(0, -5), marker);
|
|
@@ -138,22 +114,23 @@ export function listCreationRequests(root) {
|
|
|
138
114
|
export function publishCreationRequest(root, record) {
|
|
139
115
|
const marker = inspectCreationRoot(root);
|
|
140
116
|
validate(record, record.id, marker);
|
|
141
|
-
atomicCreatePrivate(path.join(root, '
|
|
117
|
+
atomicCreatePrivate(path.join(requestDirectory(root, marker, 'requests'), record.id + '.json'), JSON.stringify(record, null, 2) + '\n');
|
|
142
118
|
return copy(record);
|
|
143
119
|
}
|
|
144
120
|
export function completeCreationRequest(root, record) {
|
|
145
121
|
const current = readCreationRequest(root, record.id);
|
|
146
122
|
if (!current || JSON.stringify(current) !== JSON.stringify(record) || current.state !== 'prepared') invalid();
|
|
147
123
|
const finished = { ...record, state: 'succeeded', completedAt: new Date().toISOString(),
|
|
148
|
-
result: record.kind
|
|
149
|
-
|
|
150
|
-
|
|
124
|
+
result: isImportCreation(record.kind) ? { artifactId: record.intent.artifactId } : { runId: record.intent.runId } };
|
|
125
|
+
const marker = inspectCreationRoot(root);
|
|
126
|
+
validate(finished, record.id, marker);
|
|
127
|
+
atomicWritePrivate(path.join(requestDirectory(root, marker, 'requests'), record.id + '.json'), JSON.stringify(finished, null, 2) + '\n');
|
|
151
128
|
return copy(finished);
|
|
152
129
|
}
|
|
153
130
|
export function creationReferences(root) {
|
|
154
131
|
return listCreationRequests(root).filter(record => record.state === 'prepared').map(record => ({
|
|
155
|
-
requestId: record.id, taskIds: record.kind
|
|
156
|
-
artifactIds: record.kind
|
|
157
|
-
: [record.intent.source
|
|
132
|
+
requestId: record.id, taskIds: !isImportCreation(record.kind) ? [record.intent.taskId] : [],
|
|
133
|
+
artifactIds: isImportCreation(record.kind) ? [record.intent.artifactId]
|
|
134
|
+
: [record.intent.source?.artifactId, record.intent.comparisonArtifactId].filter(Boolean)
|
|
158
135
|
}));
|
|
159
136
|
}
|
package/lib/doctor.js
CHANGED
|
@@ -224,9 +224,9 @@ export async function doctor(options = {}) {
|
|
|
224
224
|
const nodeVersion = options.nodeVersion || process.versions.node;
|
|
225
225
|
const checks = [];
|
|
226
226
|
|
|
227
|
-
checks.push(atLeast(parseVersion(nodeVersion), [22,
|
|
227
|
+
checks.push(atLeast(parseVersion(nodeVersion), [22, 1, 0])
|
|
228
228
|
? result('node', 'ok', `Node.js ${nodeVersion}`)
|
|
229
|
-
: result('node', 'error', `Node.js ${nodeVersion} 低于最低要求 22.
|
|
229
|
+
: result('node', 'error', `Node.js ${nodeVersion} 低于最低要求 22.1.0`, '升级 Node.js 后再运行 Iris。'));
|
|
230
230
|
try { await sharpLoader(); checks.push(result('sharp', 'ok', 'sharp 可加载')); }
|
|
231
231
|
catch (_) { checks.push(result('sharp', 'error', 'sharp 无法加载', '在当前平台重新安装 @mokuyoaxis/dsh-iris,确认 sharp 有可用的 ARM64/系统构建。')); }
|
|
232
232
|
for (const command of ['ffmpeg', 'ffprobe']) {
|
package/lib/dsh-core-adapter.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { finished } from 'node:stream/promises';
|
|
5
6
|
import { runtimeProviders, assignments, irisHome, recordRateLimit, recordAvailability, refreshAutomaticDirectories } from './config.js';
|
|
6
7
|
import { withDshProviderConfig } from './dsh-provider-config.js';
|
|
7
8
|
import { createCommandService } from './command-service.js';
|
|
8
|
-
import { createCoreArtifact, inspectCoreArtifact,
|
|
9
|
+
import { createCoreArtifact, inspectCoreArtifact, listCoreArtifactMetadata, readCoreArtifactBytes, readCoreArtifactStream } from './core-artifacts.js';
|
|
9
10
|
import { createApplicationRuntime } from './application-runtime.js';
|
|
10
11
|
import { normalizeCoreOptions } from './core-contract.js';
|
|
11
12
|
import { readSummaryFrames } from './summary-input.js';
|
|
@@ -375,7 +376,9 @@ export function createDshRuntime({ dataRoot: requestedRoot, getHost = () => unde
|
|
|
375
376
|
? { text: input.text, ...(input.voice !== undefined ? { voice: input.voice } : {}) }
|
|
376
377
|
: input.audio_url !== undefined
|
|
377
378
|
? { audio_url: input.audio_url }
|
|
378
|
-
: { prompt: input.prompt,
|
|
379
|
+
: { prompt: input.prompt,
|
|
380
|
+
...(input.videoMode !== undefined ? { video_mode: input.videoMode } : {}),
|
|
381
|
+
...(input.sourceArtifactId !== undefined ? { source_artifact_id: input.sourceArtifactId } : {}) },
|
|
379
382
|
model_ref: input.modelRef,
|
|
380
383
|
confirm_billing: input.confirmBilling
|
|
381
384
|
});
|
|
@@ -503,7 +506,7 @@ export function createDshRuntime({ dataRoot: requestedRoot, getHost = () => unde
|
|
|
503
506
|
return true;
|
|
504
507
|
}
|
|
505
508
|
|
|
506
|
-
/** DSH
|
|
509
|
+
/** DSH 启动时恢复本地拒绝终态;仅为可查询且有远端受理证据的 Task 继续盯守。 */
|
|
507
510
|
async function resumeProviderTaskWatchesForDsh(options = {}) {
|
|
508
511
|
options.signal.throwIfAborted();
|
|
509
512
|
if (!fs.existsSync(dataRoot)) return Object.freeze([]);
|
|
@@ -518,11 +521,14 @@ export function createDshRuntime({ dataRoot: requestedRoot, getHost = () => unde
|
|
|
518
521
|
}));
|
|
519
522
|
const resumed = [];
|
|
520
523
|
for (const original of records) {
|
|
521
|
-
|
|
524
|
+
const incompleteRejection = original.phase === 'terminal'
|
|
525
|
+
&& original.acceptance === 'not_accepted' && original.outcome === 'none';
|
|
526
|
+
if (!WATCHABLE_CAPABILITIES.has(original.capability) && !incompleteRejection) continue;
|
|
522
527
|
try {
|
|
523
528
|
options.signal.throwIfAborted();
|
|
524
529
|
if (observableProviderTask(original)) await configuredAdapterForTask(original, options.signal);
|
|
525
530
|
const needsRecovery = (original.phase === 'submitting' && original.acceptance === 'none')
|
|
531
|
+
|| incompleteRejection
|
|
526
532
|
|| original.cancelState === 'requested'
|
|
527
533
|
|| (original.outcome === 'succeeded' && ['pending', 'downloading'].includes(original.deliveryState))
|
|
528
534
|
|| (original.acceptance === 'accepted' && original.watchState === 'active');
|
|
@@ -597,6 +603,7 @@ export function createDshRuntime({ dataRoot: requestedRoot, getHost = () => unde
|
|
|
597
603
|
schemaVersion: 0, available: false, readOnly: true, degraded: false, droppedTasks: 0,
|
|
598
604
|
tasks: Object.freeze({ total: 0, recent: Object.freeze([]) }),
|
|
599
605
|
artifacts: Object.freeze({ total: 0, recent: Object.freeze([]) }),
|
|
606
|
+
activity: Object.freeze({ running: 0, paused: 0, attention: 0, truncated: false, recent: Object.freeze([]) }),
|
|
600
607
|
userTasks: Object.freeze([])
|
|
601
608
|
}));
|
|
602
609
|
}
|
|
@@ -604,25 +611,39 @@ export function createDshRuntime({ dataRoot: requestedRoot, getHost = () => unde
|
|
|
604
611
|
const tasks = listCoreTasks(dataRoot, { limit: options.limit || 12, skipInvalid: true });
|
|
605
612
|
let artifacts = { total: 0, artifacts: Object.freeze([]) };
|
|
606
613
|
let artifactDegraded = false;
|
|
607
|
-
try {
|
|
614
|
+
try {
|
|
615
|
+
const metadata = listCoreArtifactMetadata(dataRoot), limit = Math.max(1, Math.min(200, Number(options.limit) || 12));
|
|
616
|
+
artifacts = { total: metadata.artifacts.length, artifacts: metadata.artifacts.slice(0, limit),
|
|
617
|
+
dropped: metadata.dropped, legacySkipped: metadata.legacySkipped };
|
|
618
|
+
artifactDegraded = metadata.dropped > 0;
|
|
619
|
+
}
|
|
608
620
|
catch (_) { artifactDegraded = true; }
|
|
621
|
+
const records = listAllCoreTaskRecords(dataRoot), prefs = readCoreAttentionPrefs();
|
|
609
622
|
const userTasks = decorateCoreUserRows(
|
|
610
623
|
tasks.tasks.map((task) => projectCoreTaskUserRow(task, artifacts.artifacts)),
|
|
611
|
-
|
|
612
|
-
readCoreAttentionPrefs()
|
|
624
|
+
records, prefs
|
|
613
625
|
);
|
|
626
|
+
const visible = decorateCoreUserRows(records.map(task => projectCoreTaskUserRow(task, artifacts.artifacts)), records, prefs)
|
|
627
|
+
.filter(row => !row.suppressed && row.disposition !== 'hidden' && !row.historical);
|
|
628
|
+
const running = visible.filter(row => row.userState === 'running');
|
|
629
|
+
const paused = visible.filter(row => row.userState === 'observation_paused');
|
|
630
|
+
const attention = visible.filter(row => ['attention', 'delivery_failed'].includes(row.userState) && row.disposition !== 'acknowledged');
|
|
614
631
|
return Object.freeze({
|
|
615
632
|
schemaVersion: 0, available: true, readOnly: true,
|
|
616
633
|
degraded: artifactDegraded || tasks.dropped > 0,
|
|
617
634
|
droppedTasks: tasks.dropped,
|
|
618
635
|
tasks: Object.freeze({ total: tasks.total, recent: tasks.tasks }),
|
|
619
|
-
artifacts: Object.freeze({ total: artifacts.total, recent: artifacts.artifacts
|
|
636
|
+
artifacts: Object.freeze({ total: artifacts.total, recent: artifacts.artifacts,
|
|
637
|
+
...(artifacts.dropped ? { dropped: artifacts.dropped } : {}),
|
|
638
|
+
...(artifacts.legacySkipped ? { legacySkipped: artifacts.legacySkipped } : {}) }),
|
|
639
|
+
activity: Object.freeze({ running: running.length, paused: paused.length, attention: attention.length,
|
|
640
|
+
truncated: records.length < tasks.total, recent: Object.freeze([...running, ...paused].slice(0, 10)) }),
|
|
620
641
|
userTasks: Object.freeze(userTasks)
|
|
621
642
|
});
|
|
622
643
|
}), options.signal);
|
|
623
644
|
}
|
|
624
645
|
|
|
625
|
-
/** bounded
|
|
646
|
+
/** bounded:自动静默与活动摘要复用同一扫描;超界明确标注统计不完整。 */
|
|
626
647
|
const SUCCESSOR_SCAN_LIMIT = 2000;
|
|
627
648
|
function listAllCoreTaskRecords(dataRoot) {
|
|
628
649
|
const all = [];
|
|
@@ -687,6 +708,36 @@ export function createDshRuntime({ dataRoot: requestedRoot, getHost = () => unde
|
|
|
687
708
|
}
|
|
688
709
|
}
|
|
689
710
|
|
|
711
|
+
/** Hold the reader and DSH lifetime until the media stream has closed; HEAD only hashes. */
|
|
712
|
+
async function streamCoreArtifactMediaForDsh(artifactId, consume, options = {}) {
|
|
713
|
+
const id = String(artifactId || '').trim();
|
|
714
|
+
if (!/^artifact_[a-f0-9]{24}$/.test(id) || !fs.existsSync(dataRoot)) {
|
|
715
|
+
throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_NOT_FOUND', 'Core Artifact 不存在');
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
return await withReader(runtime => runtime.run('inspect', async ({ dataRoot, signal }) => {
|
|
719
|
+
const result = options.headOnly ? { artifact: inspectCoreArtifact(dataRoot, id) }
|
|
720
|
+
: readCoreArtifactStream(dataRoot, id, { signal });
|
|
721
|
+
const closed = result.stream && finished(result.stream, { cleanup: true });
|
|
722
|
+
void closed?.catch(() => {}); // The consumer reports stream errors; observe them during setup too.
|
|
723
|
+
try {
|
|
724
|
+
const type = result.artifact.mediaType;
|
|
725
|
+
if (!type.startsWith('image/') && !type.startsWith('audio/') && !['video/mp4', 'video/webm', 'text/plain', 'application/json'].includes(type)) {
|
|
726
|
+
throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_UNSUPPORTED', '当前 DSH 媒体预览不支持此 Artifact 类型');
|
|
727
|
+
}
|
|
728
|
+
return await consume(result);
|
|
729
|
+
} finally {
|
|
730
|
+
result.stream?.destroy();
|
|
731
|
+
await closed?.catch(() => {}); // Drain the handle even after an early response or cancellation.
|
|
732
|
+
}
|
|
733
|
+
}), options.signal);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (options.signal?.aborted || error?.code === 'IRIS_APP_CLOSED' || error?.code === 'IRIS_CORE_LEASE_INVALID') throw error;
|
|
736
|
+
if (error instanceof DshCoreAdapterError) throw error;
|
|
737
|
+
throw new DshCoreAdapterError('IRIS_DSH_ARTIFACT_NOT_FOUND', 'Core Artifact 不存在');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
690
741
|
/**
|
|
691
742
|
* 将已经 ready 的 Core 图片投影为 DSH attachment blocks。重复调用只重复宿主展示,
|
|
692
743
|
* 不修改 Task/Artifact,也不触发 submit、poll 或 download。
|
|
@@ -769,6 +820,7 @@ export function createDshRuntime({ dataRoot: requestedRoot, getHost = () => unde
|
|
|
769
820
|
maintainWorkbenchForDsh: (...args) => invoke(maintainWorkbenchForDsh, 2, args),
|
|
770
821
|
coreSnapshotForDsh: (...args) => invoke(coreSnapshotForDsh, 0, args),
|
|
771
822
|
readCoreArtifactMediaForDsh: (...args) => invoke(readCoreArtifactMediaForDsh, 1, args),
|
|
823
|
+
streamCoreArtifactMediaForDsh: (...args) => invoke(streamCoreArtifactMediaForDsh, 2, args),
|
|
772
824
|
projectCoreTaskForDsh: (...args) => invoke(projectCoreTaskForDsh, 2, args),
|
|
773
825
|
});
|
|
774
826
|
}
|