@mokuyoaxis/dsh-iris 0.2.6 → 0.3.0-alpha.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 +103 -1
- package/README.en.md +104 -304
- package/README.md +101 -302
- package/bin/dsh-iris.js +446 -64
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +7 -5
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/AUDIO_FLOWS.md +71 -0
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +99 -0
- package/docs/CORE_QUICKSTART.md +108 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +94 -0
- package/docs/DSH_COMPATIBILITY.md +14 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +40 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +12 -4
- package/docs/HOST_DOCTOR.md +13 -1
- package/docs/IMAGE_FLOW.md +75 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +62 -0
- package/docs/LOCAL_API_CONTRACT.md +140 -0
- package/docs/LOCAL_CREATION.md +56 -0
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +201 -0
- package/docs/MCP.md +191 -0
- package/docs/MUSIC.md +2 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +189 -0
- package/docs/PLUGIN_STARTUP.md +100 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
- package/docs/PROVIDER_EXTENSIONS.md +78 -0
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +28 -6
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +62 -0
- package/docs/WEB_RECOVERY.md +46 -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.2.7.md +62 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +132 -70
- package/lib/adapters.js +20 -11
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +213 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +434 -55
- package/lib/comfyui-adapter.js +94 -0
- package/lib/comfyui-client.js +51 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/config.js +36 -1
- package/lib/configured-image-runtime.js +50 -0
- package/lib/connection-skill.js +89 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +21 -2
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +83 -0
- package/lib/core-capability-catalog.js +182 -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-maintenance.js +31 -2
- package/lib/core-runtime.js +20 -0
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +84 -13
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +133 -0
- package/lib/doctor.js +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +8 -2
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +290 -0
- 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 +232 -0
- package/lib/index.js +94 -68
- package/lib/local-api-projection.js +32 -0
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +1083 -0
- package/lib/local-creation.js +150 -0
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +37 -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 +101 -0
- package/lib/mcp-server.js +47 -0
- package/lib/model-directory-service.js +16 -6
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +2 -1
- package/lib/operation-runner.js +236 -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 +288 -0
- package/lib/plugins.js +58 -0
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapter.js +5 -1
- package/lib/provider-adapters.js +56 -18
- package/lib/provider-catalog.js +40 -29
- package/lib/provider-config-service.js +105 -23
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +8 -1
- package/lib/provider-monitor.js +79 -0
- package/lib/provider-protocol.js +70 -20
- package/lib/provider-registry.js +118 -0
- package/lib/provider-response.js +28 -0
- package/lib/provider-settings.js +134 -0
- package/lib/provider-task-runner.js +40 -7
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +211 -0
- package/lib/vision-model-routing.js +3 -0
- package/lib/web/app.js +642 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +377 -0
- package/lib/web/creation.js +355 -0
- package/lib/web/flow-view.js +33 -0
- package/lib/web/gallery.js +132 -0
- package/lib/web/index.html +291 -0
- package/lib/web/media-client.js +204 -0
- package/lib/web/plugin-panel.js +221 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +492 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +518 -0
- package/lib/web/transport.js +96 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/lib/workbench-management.js +8 -9
- package/package.json +22 -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 +44 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +60 -0
- package/user_guide.md +19 -9
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { createCoreArtifact, exportCoreArtifact, readCoreArtifactBytes } from './core-artifacts.js';
|
|
4
|
+
import { assertFlowTaskMatches, claimFlowTask, findFlowTask, recoverCoreTask } from './core-tasks.js';
|
|
5
|
+
import { deliveryCacheNeedsProvider } from './core-delivery-cache.js';
|
|
6
|
+
import { normalizeGenerationInput } from './generation-input.js';
|
|
7
|
+
import { imageEditCandidates, readImageEditArtifact } from './image-edit-input.js';
|
|
8
|
+
import { parseModelRef } from './models.js';
|
|
9
|
+
import { pixelDiff } from './pixels.js';
|
|
10
|
+
import { createProviderTaskRunner } from './provider-task-runner.js';
|
|
11
|
+
import { prepareProviderInput } from './provider-adapter.js';
|
|
12
|
+
import { AUDIO_RECIPES, AUDIO_TYPES, isAudioRecipe, normalizeAudioFlowInput, readImportAudio } from './audio-flow-input.js';
|
|
13
|
+
import { commitFlowCreation, flowFail, flowSha256, flowStep, flowStepId, initializeFlowRoot, inspectFlowRoot,
|
|
14
|
+
IMAGE_GENERATE_RECIPE, requireImageGenerationRoot, requireAudioRoot, listFlowRuns, prepareFlowCreation, readFlowRun, updateFlowRun } from './flow-store.js';
|
|
15
|
+
|
|
16
|
+
function exact(input, allowed) {
|
|
17
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !allowed.includes(key))) {
|
|
18
|
+
flowFail('IRIS_FLOW_INPUT_INVALID', '流程输入包含不支持的字段');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const reference = artifact => ({ artifactId: artifact.id, sha256: artifact.digest.value });
|
|
22
|
+
|
|
23
|
+
function readReference(root, expected) {
|
|
24
|
+
const value = readCoreArtifactBytes(root, expected.artifactId);
|
|
25
|
+
if (value.artifact.digest.value !== expected.sha256) flowFail('IRIS_FLOW_ARTIFACT_CONFLICT', '流程引用的作品摘要已改变');
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function facts(root, run) {
|
|
30
|
+
const source = run.source ? readReference(root, run.source) : null;
|
|
31
|
+
const task = findFlowTask(root, run);
|
|
32
|
+
if (run.output) {
|
|
33
|
+
if (task.deliveryState !== 'ready' || task.artifactIds.length !== 1 || task.artifactIds[0] !== run.output.artifactId) {
|
|
34
|
+
flowFail('IRIS_FLOW_TASK_CONFLICT', '流程输出与原 Task 的交付事实不一致');
|
|
35
|
+
}
|
|
36
|
+
const { artifact } = readReference(root, run.output);
|
|
37
|
+
if (artifact.metadata.taskId !== task.id || source && !artifact.relations.some(edge => edge.type === 'derived-from' && edge.artifactId === source.artifact.id)
|
|
38
|
+
|| !source && artifact.relations.length) {
|
|
39
|
+
flowFail('IRIS_FLOW_ARTIFACT_CONFLICT', '流程输出缺少原 Task 或来源关系');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let comparison;
|
|
43
|
+
if (run.comparison) {
|
|
44
|
+
comparison = readReference(root, run.comparison).artifact;
|
|
45
|
+
if (comparison.kind !== 'pixel-diff' || comparison.relations.length !== 2
|
|
46
|
+
|| ![run.source.artifactId, run.output.artifactId].every(id => comparison.relations.some(edge => edge.type === 'derived-from' && edge.artifactId === id))) {
|
|
47
|
+
flowFail('IRIS_FLOW_ARTIFACT_CONFLICT', '比较作品与流程来源不一致');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let status = 'not_started';
|
|
51
|
+
if (task?.acceptance === 'unknown' || ['unknown', 'failed', 'canceled'].includes(task?.outcome)) status = 'needs_attention';
|
|
52
|
+
else if (task?.deliveryState === 'ready') status = isAudioRecipe(run.recipe) ? run.output ? 'ready' : 'needs_result'
|
|
53
|
+
: run.output && (run.recipe === IMAGE_GENERATE_RECIPE || run.comparison)
|
|
54
|
+
? run.selection ? 'selected' : 'awaiting_selection' : run.recipe === IMAGE_GENERATE_RECIPE ? 'needs_result' : 'needs_comparison';
|
|
55
|
+
else if (task?.deliveryState === 'failed') status = 'delivery_failed';
|
|
56
|
+
else if (task?.attempts.length) status = task.acceptance === 'accepted' ? 'running' : 'needs_attention';
|
|
57
|
+
return { contractVersion: 0, run, task, status, ...(comparison ? { metrics: comparison.metadata } : {}) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Fixed image/audio recipes, sharing operation-scoped leases and the original Task. */
|
|
61
|
+
export function createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter } = {}) {
|
|
62
|
+
function creationInput(input) {
|
|
63
|
+
if (isAudioRecipe(input?.recipe)) {
|
|
64
|
+
const { recipe, ...value } = input;
|
|
65
|
+
return { recipe, ...normalizeAudioFlowInput(recipe, value) };
|
|
66
|
+
}
|
|
67
|
+
const normalized = normalizeGenerationInput('image', input);
|
|
68
|
+
if (!parseModelRef(normalized.modelRef) || normalized.providerInput.n !== 1) {
|
|
69
|
+
flowFail('IRIS_FLOW_INPUT_INVALID', '图片流程需要明确 model_ref 和 n=1;改图还需来源 Artifact');
|
|
70
|
+
}
|
|
71
|
+
return { prompt: normalized.providerInput.prompt, model_ref: normalized.modelRef,
|
|
72
|
+
...(normalized.sourceArtifactId ? { source_artifact_id: normalized.sourceArtifactId } : {}), n: 1,
|
|
73
|
+
...(normalized.providerInput.size !== undefined ? { size: normalized.providerInput.size } : {}),
|
|
74
|
+
...(normalized.providerInput.workflow_parameters !== undefined ? { workflow_parameters: normalized.providerInput.workflow_parameters } : {}) };
|
|
75
|
+
}
|
|
76
|
+
async function prepareCreation(root, signal, input) {
|
|
77
|
+
const owned = creationInput(input);
|
|
78
|
+
inspectFlowRoot(root);
|
|
79
|
+
const audio = isAudioRecipe(owned.recipe);
|
|
80
|
+
if (audio) requireAudioRoot(root);
|
|
81
|
+
else if (!owned.source_artifact_id) requireImageGenerationRoot(root);
|
|
82
|
+
const source = owned.source_artifact_id ? audio ? readAudioSource(root, owned.source_artifact_id) : await readImageEditArtifact(root, owned.source_artifact_id, signal) : null;
|
|
83
|
+
const candidates = await candidatesFor(owned, signal);
|
|
84
|
+
const { recipe, ...audioInput } = owned;
|
|
85
|
+
return prepareFlowCreation({ ...(source ? { source: { artifactId: owned.source_artifact_id, sha256: flowSha256(source.bytes) } } : {}),
|
|
86
|
+
input: audio ? audioInput : owned, providerBinding: candidates[0].providerBinding, ...(audio ? { recipe } : {}) });
|
|
87
|
+
}
|
|
88
|
+
function readAudioSource(root, id) {
|
|
89
|
+
const source = readCoreArtifactBytes(root, id);
|
|
90
|
+
readImportAudio(source.bytes, source.artifact.mediaType);
|
|
91
|
+
return source;
|
|
92
|
+
}
|
|
93
|
+
async function candidatesFor(runInput, signal, binding) {
|
|
94
|
+
const audio = isAudioRecipe(runInput.recipe), resolver = audio ? resolveAudioCandidates : resolveImageCandidates;
|
|
95
|
+
if (!resolver) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '创建或首次提交流程需要显式 Provider 配置');
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
const candidates = await resolver({ modelRef: runInput.model_ref, signal, input: runInput,
|
|
98
|
+
...(audio ? { capability: AUDIO_RECIPES[runInput.recipe].capability } : {}) });
|
|
99
|
+
signal.throwIfAborted();
|
|
100
|
+
const parsed = parseModelRef(runInput.model_ref);
|
|
101
|
+
if (!Array.isArray(candidates) || candidates.length !== 1 || candidates[0].model !== runInput.model_ref
|
|
102
|
+
|| candidates[0].adapter?.id !== parsed.providerId || !/^sha256:[a-f0-9]{64}$/.test(candidates[0].providerBinding)) {
|
|
103
|
+
flowFail('IRIS_FLOW_INPUT_CONFLICT', '流程必须绑定单一明确模型和有效的连接摘要');
|
|
104
|
+
}
|
|
105
|
+
if (binding !== undefined && candidates[0].providerBinding !== binding) flowFail('IRIS_FLOW_PROVIDER_CHANGED', '连接或协议已改变;原流程不重新绑定或生成');
|
|
106
|
+
const resolved = [{ ...candidates[0], providerModel: parsed.modelId, selectionReason: 'explicit' }];
|
|
107
|
+
return runInput.source_artifact_id && !audio ? imageEditCandidates(resolved) : resolved;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function adapterFor(task, signal) {
|
|
111
|
+
if (!resolveTaskAdapter) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '观察或远端补交付需要原 Task 的 Provider 配置');
|
|
112
|
+
const adapter = await resolveTaskAdapter(task, { signal });
|
|
113
|
+
signal.throwIfAborted();
|
|
114
|
+
return adapter;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function continueRun(root, signal, id) {
|
|
118
|
+
let run = readFlowRun(root, id);
|
|
119
|
+
let task = facts(root, run).task;
|
|
120
|
+
if (task?.attempts.length && !flowStep(run).claimed) flowFail('IRIS_FLOW_TASK_CONFLICT', '步骤检查点与已开始的 Task 冲突');
|
|
121
|
+
const runner = createProviderTaskRunner(runtime);
|
|
122
|
+
if (!task || !task.attempts.length) {
|
|
123
|
+
const audio = isAudioRecipe(run.recipe);
|
|
124
|
+
let candidates = await candidatesFor({ ...run.input, ...(audio ? { recipe: run.recipe } : {}) }, signal, run.providerBinding);
|
|
125
|
+
if (run.recipe === 'transcribe/v0') {
|
|
126
|
+
const source = readAudioSource(root, run.source.artifactId);
|
|
127
|
+
candidates = candidates.map(candidate => ({ ...candidate, prepareInput: async ({ signal }) => {
|
|
128
|
+
const prepared = await prepareProviderInput(candidate.adapter, { model: candidate.providerModel, bytes: source.bytes,
|
|
129
|
+
filename: source.artifact.id + '.' + AUDIO_TYPES[source.artifact.mediaType], signal });
|
|
130
|
+
return { audioUrl: prepared.url };
|
|
131
|
+
} }));
|
|
132
|
+
}
|
|
133
|
+
task = claimFlowTask(root, run.id);
|
|
134
|
+
if (!flowStep(run).claimed) run = updateFlowRun(root, run, { [flowStepId(run)]: { ...flowStep(run), claimed: true } });
|
|
135
|
+
signal.throwIfAborted();
|
|
136
|
+
const normalized = audio ? Object.fromEntries(Object.entries(run.input).filter(([key]) => key !== 'model_ref'))
|
|
137
|
+
: normalizeGenerationInput('image', run.input).providerInput;
|
|
138
|
+
task = (await runner.submit({ capability: audio ? AUDIO_RECIPES[run.recipe].capability : 'image', candidates, providerInput: normalized,
|
|
139
|
+
sourceArtifactId: run.source?.artifactId, flowRunId: run.id })).task;
|
|
140
|
+
} else {
|
|
141
|
+
task = recoverCoreTask(root, task.id);
|
|
142
|
+
assertFlowTaskMatches(task, run);
|
|
143
|
+
if (task.outcome === 'succeeded' && task.deliveryState === 'failed') {
|
|
144
|
+
const adapter = task.deliveryCache && !deliveryCacheNeedsProvider(root, task) ? undefined : await adapterFor(task, signal);
|
|
145
|
+
task = await runner.redeliver(task.id, adapter);
|
|
146
|
+
} else if (task.acceptance === 'accepted' && task.outcome === 'none' && task.remoteTaskId) {
|
|
147
|
+
task = await runner.observe(task.id, await adapterFor(task, signal), { resumeActive: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
signal.throwIfAborted();
|
|
151
|
+
if (task.deliveryState !== 'ready') return facts(root, run);
|
|
152
|
+
if (task.artifactIds.length !== 1) flowFail('IRIS_FLOW_RESULT_UNSUPPORTED', '当前流程只支持一个结果,原 Task 与作品已保留');
|
|
153
|
+
const output = readCoreArtifactBytes(root, task.artifactIds[0]);
|
|
154
|
+
if (!run.output) run = updateFlowRun(root, run, { output: reference(output.artifact) });
|
|
155
|
+
facts(root, run);
|
|
156
|
+
if (run.source && !run.comparison && !isAudioRecipe(run.recipe)) {
|
|
157
|
+
const source = readReference(root, run.source);
|
|
158
|
+
const diff = await pixelDiff({ inputA: source.bytes, inputB: output.bytes, grid: 8, topRegions: 3 });
|
|
159
|
+
signal.throwIfAborted();
|
|
160
|
+
const artifact = createCoreArtifact(root, { artifactId: run.compare.artifactId, createdAt: run.createdAt,
|
|
161
|
+
bytes: diff.heatmap, expectedDigest: flowSha256(diff.heatmap), expectedSize: diff.heatmap.length,
|
|
162
|
+
kind: 'pixel-diff', mediaType: diff.mime,
|
|
163
|
+
metadata: { ratio: diff.ratio, diffPixels: diff.diffPixels, totalPixels: diff.totalPixels,
|
|
164
|
+
width: diff.width, height: diff.height, worstRegions: diff.worstRegions },
|
|
165
|
+
relations: [run.source.artifactId, run.output.artifactId].map(artifactId => ({ type: 'derived-from', artifactId })) });
|
|
166
|
+
run = updateFlowRun(root, run, { comparison: reference(artifact) });
|
|
167
|
+
}
|
|
168
|
+
return facts(root, run);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function execute(name, input = {}) {
|
|
172
|
+
if (name === 'flow.init') {
|
|
173
|
+
exact(input, []);
|
|
174
|
+
return runtime.run('execute', ({ dataRoot }) => ({ contractVersion: 0, root: initializeFlowRoot(dataRoot) }));
|
|
175
|
+
}
|
|
176
|
+
if (name === 'flow.create') {
|
|
177
|
+
const owned = creationInput(input);
|
|
178
|
+
return runtime.run('execute', async ({ dataRoot, signal }) => {
|
|
179
|
+
const run = commitFlowCreation(dataRoot, await prepareCreation(dataRoot, signal, owned));
|
|
180
|
+
return facts(dataRoot, run);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (name === 'flow.list') {
|
|
184
|
+
exact(input, []);
|
|
185
|
+
return runtime.run('inspect', ({ dataRoot }) => {
|
|
186
|
+
const runs = listFlowRuns(dataRoot);
|
|
187
|
+
return { contractVersion: 0, total: runs.length, runs: runs.map(run => ({ id: run.id, recipe: run.recipe,
|
|
188
|
+
createdAt: run.createdAt, ...(run.source ? { source: run.source } : {}), modelRef: run.input.model_ref, taskId: flowStep(run).taskId })) };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (name === 'flow.inspect') {
|
|
192
|
+
exact(input, ['run_id']);
|
|
193
|
+
return runtime.run('inspect', ({ dataRoot }) => facts(dataRoot, readFlowRun(dataRoot, input.run_id)));
|
|
194
|
+
}
|
|
195
|
+
if (name === 'flow.continue') {
|
|
196
|
+
exact(input, ['run_id']);
|
|
197
|
+
return runtime.run('execute', ({ dataRoot, signal }) => continueRun(dataRoot, signal, input.run_id));
|
|
198
|
+
}
|
|
199
|
+
if (name === 'flow.select' || name === 'flow.export') {
|
|
200
|
+
exact(input, name === 'flow.select' ? ['run_id', 'artifact_id', 'expected_revision'] : ['run_id', 'output_path']);
|
|
201
|
+
if (input.expected_revision !== undefined && (!Number.isSafeInteger(input.expected_revision) || input.expected_revision < 1)) {
|
|
202
|
+
flowFail('IRIS_FLOW_INPUT_INVALID', '预期流程版本必须是正整数');
|
|
203
|
+
}
|
|
204
|
+
return runtime.run('execute', ({ dataRoot }) => {
|
|
205
|
+
let run = readFlowRun(dataRoot, input.run_id);
|
|
206
|
+
if (input.expected_revision !== undefined && run.revision !== input.expected_revision) {
|
|
207
|
+
flowFail('IRIS_FLOW_REVISION_CONFLICT', '流程已改变,请读取当前版本后明确选择');
|
|
208
|
+
}
|
|
209
|
+
facts(dataRoot, run);
|
|
210
|
+
if (isAudioRecipe(run.recipe)) {
|
|
211
|
+
if (name === 'flow.select') flowFail('IRIS_FLOW_SELECTION_INVALID', '音频和转写为单一结果,无需选择图片版本');
|
|
212
|
+
if (!run.output) flowFail('IRIS_FLOW_NOT_READY', '请先接回原任务的结果');
|
|
213
|
+
return { ...facts(dataRoot, run), export: exportCoreArtifact(dataRoot, run.output.artifactId, input.output_path) };
|
|
214
|
+
}
|
|
215
|
+
if (!run.output || run.source && !run.comparison) flowFail('IRIS_FLOW_NOT_READY', '请先保存生成结果;改图还需完成比较');
|
|
216
|
+
if (name === 'flow.select') {
|
|
217
|
+
const selected = [run.source, run.output].filter(Boolean).find(item => item.artifactId === input.artifact_id);
|
|
218
|
+
if (!selected) flowFail('IRIS_FLOW_SELECTION_INVALID', '只能选择本流程的原图或新版');
|
|
219
|
+
if (run.selection?.artifactId !== selected.artifactId) run = updateFlowRun(dataRoot, run, { selection: selected });
|
|
220
|
+
return facts(dataRoot, run);
|
|
221
|
+
}
|
|
222
|
+
if (!run.selection) flowFail('IRIS_FLOW_SELECTION_REQUIRED', '请先明确选择要导出的原图或新版');
|
|
223
|
+
return { ...facts(dataRoot, run), export: exportCoreArtifact(dataRoot, run.selection.artifactId, input.output_path) };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
throw new TypeError('未知的内部 Flow 命令');
|
|
227
|
+
}
|
|
228
|
+
return Object.freeze({ execute, prepareCreation, inspectCreation: (root, intent, options) => {
|
|
229
|
+
if (intent.source) readReference(root, intent.source);
|
|
230
|
+
return facts(root, commitFlowCreation(root, intent, options));
|
|
231
|
+
} });
|
|
232
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -19,8 +19,8 @@ import { waitForModelWork } from './model-call-runtime.js';
|
|
|
19
19
|
import { hasHostPort, requireHostPort } from './host-contract.js';
|
|
20
20
|
import { createDshHostAdapter } from './dsh-host-adapter.js';
|
|
21
21
|
import { withDshProviderConfig } from './dsh-provider-config.js';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { createDshRuntime, dshCoreDataRoot } from './dsh-core-adapter.js';
|
|
23
|
+
import { ModelPortError } from './model-port-contract.js';
|
|
24
24
|
import { imageDimensions } from './pixels.js';
|
|
25
25
|
import { runLocateRequest, runSummaryRequest, describeGeneratedImage } from './composite-vision-routing.js';
|
|
26
26
|
import { normalizeSummarySource, summaryFrameRecords, summaryTimelineLabel } from './summary-input.js';
|
|
@@ -66,39 +66,47 @@ function outputDir() {
|
|
|
66
66
|
|
|
67
67
|
/* ---------------- 媒体路由:/iris/media/:taskId/:token/:name ---------------- */
|
|
68
68
|
|
|
69
|
-
let
|
|
70
|
-
function mountIrisRoutes(routeCtx) {
|
|
69
|
+
let latestNativeRuntime;
|
|
70
|
+
function mountIrisRoutes(routeCtx, core, routes) {
|
|
71
71
|
// Cordis 服务只在 DSH 宿主边界探测;Command 与通用工具辅助函数不接收原始 ctx
|
|
72
72
|
const host = dshHost(routeCtx);
|
|
73
73
|
const reg = hasHostPort(host, 'routes') ? host.ports.routes : undefined;
|
|
74
|
-
if (!reg || typeof reg.register !== 'function' ||
|
|
74
|
+
if (!reg || typeof reg.register !== 'function' || routes.mounted) return;
|
|
75
|
+
const disposers = []; let released = false;
|
|
76
|
+
const release = () => {
|
|
77
|
+
if (released) return;
|
|
78
|
+
released = true;
|
|
79
|
+
const errors = [];
|
|
80
|
+
for (const dispose of disposers.reverse()) { try { if (typeof dispose === 'function') dispose(); } catch (error) { errors.push(error); } }
|
|
81
|
+
if (routes.mounted && latestNativeRuntime === core) closeAllSse();
|
|
82
|
+
routes.mounted = false;
|
|
83
|
+
if (errors.length) throw new AggregateError(errors, 'Iris 路由释放失败');
|
|
84
|
+
};
|
|
75
85
|
try {
|
|
76
86
|
const disposeMedia = reg.register({
|
|
77
87
|
kind: 'prefix',
|
|
78
88
|
path: '/iris/media',
|
|
79
89
|
handler: guarded(serveMedia)
|
|
80
90
|
});
|
|
91
|
+
disposers.push(disposeMedia);
|
|
81
92
|
const disposeApi = reg.register({
|
|
82
93
|
kind: 'prefix',
|
|
83
94
|
path: '/iris/api',
|
|
84
|
-
handler: guarded((req, res) => serveApi(req, res, dshHost(routeCtx)))
|
|
95
|
+
handler: guarded((req, res) => serveApi(req, res, dshHost(routeCtx), core))
|
|
85
96
|
});
|
|
97
|
+
disposers.push(disposeApi);
|
|
86
98
|
const disposeRender = reg.register({
|
|
87
99
|
kind: 'prefix',
|
|
88
100
|
path: '/iris/render',
|
|
89
101
|
handler: guarded(serveRender)
|
|
90
102
|
});
|
|
91
|
-
|
|
103
|
+
disposers.push(disposeRender);
|
|
104
|
+
routeCtx.effect(() => release, 'iris: media+api+actions+render routes');
|
|
105
|
+
routes.mounted = true;
|
|
92
106
|
recordHostRoutes(EXPECTED_IRIS_ROUTES);
|
|
93
|
-
routeCtx.effect(() => () => {
|
|
94
|
-
if (typeof disposeMedia === 'function') disposeMedia();
|
|
95
|
-
if (typeof disposeApi === 'function') disposeApi();
|
|
96
|
-
if (typeof disposeRender === 'function') disposeRender();
|
|
97
|
-
closeAllSse(); // SSE 长连接随插件停用一起关闭,不留悬挂连接
|
|
98
|
-
mediaRouteMounted = false;
|
|
99
|
-
}, 'iris: media+api+actions+render routes');
|
|
100
107
|
console.log('[iris] 媒体+工作台+操作路由已挂载:/iris/media · /iris/api · /iris/api/actions · /iris/render');
|
|
101
108
|
} catch (err) {
|
|
109
|
+
try { release(); } catch (cleanup) { console.error('[iris] 路由回滚失败:', cleanup.message); }
|
|
102
110
|
console.error('[iris] 路由挂载失败:', err && err.message);
|
|
103
111
|
}
|
|
104
112
|
}
|
|
@@ -181,11 +189,11 @@ function coreTaskTerminalProblem(task, label = '任务') {
|
|
|
181
189
|
return label + '需要人工处理';
|
|
182
190
|
}
|
|
183
191
|
|
|
184
|
-
async function awaitCoreTerminal(taskId, { timeoutMs, signal }) {
|
|
192
|
+
async function awaitCoreTerminal(core, taskId, { timeoutMs, signal }) {
|
|
185
193
|
const deadline = Date.now() + timeoutMs;
|
|
186
194
|
for (;;) {
|
|
187
195
|
if (signal?.aborted) throw new Error('已取消;Core 任务仍保留远端受理事实,可稍后查询');
|
|
188
|
-
const task = await inspectProviderTaskForDsh(taskId, { signal });
|
|
196
|
+
const task = await core.inspectProviderTaskForDsh(taskId, { signal });
|
|
189
197
|
if (coreTaskWaitFinished(task)) return task;
|
|
190
198
|
if (Date.now() > deadline) return null;
|
|
191
199
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -221,7 +229,25 @@ function safeRegister(host, definition) {
|
|
|
221
229
|
|
|
222
230
|
export async function apply(ctx) {
|
|
223
231
|
beginHostRuntime({ pluginId: IRIS_PLUGIN_ID, version: PACKAGE_VERSION });
|
|
224
|
-
const
|
|
232
|
+
const core = createDshRuntime({ dataRoot: dshCoreDataRoot(), getHost: () => dshHost(ctx) });
|
|
233
|
+
latestNativeRuntime = core;
|
|
234
|
+
ctx.provide('irisRuntime', core);
|
|
235
|
+
const routes = { mounted: false };
|
|
236
|
+
const { cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, projectCoreTaskForDsh, readCoreArtifactMediaForDsh, resumeProviderTaskWatchesForDsh } = core;
|
|
237
|
+
ctx.effect(() => () => {
|
|
238
|
+
if (latestNativeRuntime === core) tasks.stopWatchAll();
|
|
239
|
+
return core.dispose();
|
|
240
|
+
}, 'iris: DSH runtime');
|
|
241
|
+
// Explicit additional connection for shared Flow tools. Legacy native media/skills/UI retain their own roots.
|
|
242
|
+
if (process.env.IRIS_CORE_CONNECTION_FILE) {
|
|
243
|
+
const { createDshServiceBridge } = await import('./dsh-service-bridge.js');
|
|
244
|
+
const bridge = createDshServiceBridge({ connectionFile: process.env.IRIS_CORE_CONNECTION_FILE,
|
|
245
|
+
requestedScopes: (process.env.IRIS_CORE_SCOPES || 'read').split(','), getHost: () => dshHost(ctx), harnessVersion: dshHost(ctx).version });
|
|
246
|
+
ctx.provide('irisCoreService', bridge);
|
|
247
|
+
ctx.effect(() => () => bridge.dispose(), 'iris: Core service connection');
|
|
248
|
+
try { await bridge.start(); }
|
|
249
|
+
catch (error) { console.error('[iris] Core 服务连接失败:', error?.code || 'IRIS_CORE_SERVICE_UNAVAILABLE'); }
|
|
250
|
+
}
|
|
225
251
|
guard('Iris 私有存储权限收紧', () => hardenPrivateTree(store.irisHome()));
|
|
226
252
|
|
|
227
253
|
guard('上传临时文件清理', () => purgeStaleUploads());
|
|
@@ -245,27 +271,18 @@ export async function apply(ctx) {
|
|
|
245
271
|
if (resumed.length) console.log(`[iris] 已恢复接管 ${resumed.length} 个后台任务`);
|
|
246
272
|
});
|
|
247
273
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (resumed.length) console.log(
|
|
251
|
-
}
|
|
252
|
-
console.error('[iris] 恢复 Core
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Fiber 清理:插件停用/更新时停掉 legacy 与 Core 的全部 Host 盯守句柄
|
|
256
|
-
guard('盯守句柄注册', () => {
|
|
257
|
-
ctx.effect(() => () => {
|
|
258
|
-
tasks.stopWatchAll();
|
|
259
|
-
stopProviderTaskWatchesForDsh();
|
|
260
|
-
releaseProviderHost();
|
|
261
|
-
});
|
|
274
|
+
// Owned background setup lets unload cancel a slow connection resolver during takeover.
|
|
275
|
+
void resumeProviderTaskWatchesForDsh().then(resumed => {
|
|
276
|
+
if (resumed.length) console.log('[iris] 已恢复接管 ' + resumed.length + ' 个 Core 媒体任务');
|
|
277
|
+
}, error => {
|
|
278
|
+
if (core.snapshot().state === 'open') console.error('[iris] 恢复 Core 后台任务失败:', error?.code || 'unknown');
|
|
262
279
|
});
|
|
263
280
|
|
|
264
281
|
// 媒体路由(视频/音频在对话流里的可播通路;图片仍走原生附件)
|
|
265
|
-
ctx.inject(['webServer'], mountIrisRoutes);
|
|
266
|
-
ctx.inject(['httpServer'], mountIrisRoutes);
|
|
282
|
+
ctx.inject(['webServer'], routeCtx => mountIrisRoutes(routeCtx, core, routes));
|
|
283
|
+
ctx.inject(['httpServer'], routeCtx => mountIrisRoutes(routeCtx, core, routes));
|
|
267
284
|
// 若 webServer 已在运行,inject 回调也会触发;这里兜底立即挂载一次
|
|
268
|
-
guard('媒体路由兜底挂载', () => mountIrisRoutes(ctx));
|
|
285
|
+
guard('媒体路由兜底挂载', () => mountIrisRoutes(ctx, core, routes));
|
|
269
286
|
|
|
270
287
|
// DSH 的 Skill registry 是可选宿主能力。存在时把 npm 包内三项 Skill 注册到
|
|
271
288
|
// 全局层;项目目录中的同名 Skill 仍由 DSH 原生优先级覆盖。
|
|
@@ -283,13 +300,22 @@ export async function apply(ctx) {
|
|
|
283
300
|
'iris_media_summarize', 'iris_locate', 'iris_long_ocr']);
|
|
284
301
|
const visionTools = new Set(['iris_look_at_image', 'iris_relook_attachment', 'iris_media_summarize', 'iris_locate', 'iris_long_ocr']);
|
|
285
302
|
function registerIrisTool(definition) {
|
|
286
|
-
safeRegister(host, { ...definition, execute(args, exec) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
303
|
+
safeRegister(host, { ...definition, execute(args, exec = {}) {
|
|
304
|
+
const owned = structuredClone(args || {});
|
|
305
|
+
return core.runHostOperation(signal => {
|
|
306
|
+
const execute = async () => {
|
|
307
|
+
if (visionTools.has(definition.name) && !owned.model) await store.refreshAutomaticDirectories({ capability: 'vision', signal });
|
|
308
|
+
signal.throwIfAborted();
|
|
309
|
+
return definition.execute(owned, { ...exec, signal });
|
|
310
|
+
};
|
|
311
|
+
return providerTools.has(definition.name)
|
|
312
|
+
? withDshProviderConfig(dshHost(ctx), execute, { signal }) : execute();
|
|
313
|
+
}, { signal: exec.signal }).catch(error => {
|
|
314
|
+
if (visionTools.has(definition.name) && exec.signal?.aborted && error === exec.signal.reason) {
|
|
315
|
+
throw new ModelPortError('IRIS_MODEL_ABORTED');
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
});
|
|
293
319
|
} });
|
|
294
320
|
}
|
|
295
321
|
if (!hasHostPort(host, 'tools')) {
|
|
@@ -299,7 +325,7 @@ export async function apply(ctx) {
|
|
|
299
325
|
|
|
300
326
|
async function coreImageToolResult(submitted, prompt, signal) {
|
|
301
327
|
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal });
|
|
302
|
-
if (!coreTaskWaitFinished(final)) final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.image, signal });
|
|
328
|
+
if (!coreTaskWaitFinished(final)) final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: AWAIT_MS.image, signal });
|
|
303
329
|
if (!final) return { blocks: [{ type: 'text', text: `[iris] Core 图片任务仍在后台观察(task: ${submitted.taskId})。稍后可用 iris_task_status 查询进度与 Artifact。` }] };
|
|
304
330
|
const terminalProblem = coreTaskTerminalProblem(final, '图像生成');
|
|
305
331
|
if (terminalProblem) throw new Error(terminalProblem);
|
|
@@ -345,9 +371,9 @@ export async function apply(ctx) {
|
|
|
345
371
|
const attachmentId = String(args.attachment_id || '').trim();
|
|
346
372
|
if (Boolean(imagePath) === Boolean(attachmentId)) throw new Error('图片导入须选择image_path或attachment_id之一');
|
|
347
373
|
const input = imagePath ? { image_path: imagePath } : {
|
|
348
|
-
image_bytes: (await resolveVisionImageInput(dshHost(ctx), exec, { attachment_id: attachmentId }, exec.signal)).bytes
|
|
374
|
+
image_bytes: (await resolveVisionImageInput(core, dshHost(ctx), exec, { attachment_id: attachmentId }, exec.signal)).bytes
|
|
349
375
|
};
|
|
350
|
-
return runAction(dshHost(ctx), 'image_import', input, { signal: exec.signal });
|
|
376
|
+
return runAction(dshHost(ctx), 'image_import', input, { signal: exec.signal, core });
|
|
351
377
|
}
|
|
352
378
|
});
|
|
353
379
|
|
|
@@ -374,7 +400,7 @@ export async function apply(ctx) {
|
|
|
374
400
|
isConcurrencySafe: () => false,
|
|
375
401
|
async execute(args, exec) {
|
|
376
402
|
const prompt = String(args.prompt || '').trim();
|
|
377
|
-
const submitted = await runAction(dshHost(ctx), 'image', args, { signal: exec.signal });
|
|
403
|
+
const submitted = await runAction(dshHost(ctx), 'image', args, { signal: exec.signal, core });
|
|
378
404
|
if (submitted.storage === 'core') {
|
|
379
405
|
return coreImageToolResult(submitted, prompt, exec.signal);
|
|
380
406
|
}
|
|
@@ -393,7 +419,7 @@ export async function apply(ctx) {
|
|
|
393
419
|
const refs = [];
|
|
394
420
|
for (const f of files) {
|
|
395
421
|
const mediaType = MEDIA_TYPES[path.extname(f).toLowerCase()] || 'image/png';
|
|
396
|
-
const ref = await attachmentService(
|
|
422
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({ data: new Uint8Array(fs.readFileSync(f)), mediaType, name: path.basename(f) });
|
|
397
423
|
refs.push(ref);
|
|
398
424
|
blocks.push({ type: 'image', attachment: ref });
|
|
399
425
|
}
|
|
@@ -437,7 +463,7 @@ export async function apply(ctx) {
|
|
|
437
463
|
output: { schema: { type: 'object' }, render: (_args, value) => value.blocks },
|
|
438
464
|
isConcurrencySafe: () => false,
|
|
439
465
|
async execute(args, exec) {
|
|
440
|
-
const submitted = await runAction(dshHost(ctx), 'image_edit', args, { signal: exec.signal });
|
|
466
|
+
const submitted = await runAction(dshHost(ctx), 'image_edit', args, { signal: exec.signal, core });
|
|
441
467
|
return coreImageToolResult(submitted, String(args.prompt || '').trim(), exec.signal);
|
|
442
468
|
}
|
|
443
469
|
});
|
|
@@ -468,11 +494,11 @@ export async function apply(ctx) {
|
|
|
468
494
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
469
495
|
isConcurrencySafe: () => false,
|
|
470
496
|
async execute(args, exec) {
|
|
471
|
-
const submitted = await runAction(dshHost(ctx), 'video', { ...args, session_id: exec.agent?.session?.id }, { signal: exec.signal });
|
|
497
|
+
const submitted = await runAction(dshHost(ctx), 'video', { ...args, session_id: exec.agent?.session?.id }, { signal: exec.signal, core });
|
|
472
498
|
if (submitted.storage === 'core') {
|
|
473
499
|
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal: exec.signal });
|
|
474
500
|
if (!coreTaskWaitFinished(final)) {
|
|
475
|
-
final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.video, signal: exec.signal });
|
|
501
|
+
final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: AWAIT_MS.video, signal: exec.signal });
|
|
476
502
|
}
|
|
477
503
|
if (!final) {
|
|
478
504
|
return `[iris] Core 视频任务已提交并转入后台观察(task: ${submitted.taskId},remoteTask: ${submitted.remoteTaskId})。\n` +
|
|
@@ -518,7 +544,7 @@ export async function apply(ctx) {
|
|
|
518
544
|
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
|
|
519
545
|
isConcurrencySafe: () => true,
|
|
520
546
|
async execute(args, exec) {
|
|
521
|
-
const submitted = await runAction(dshHost(ctx), 'music', args, { signal: exec.signal });
|
|
547
|
+
const submitted = await runAction(dshHost(ctx), 'music', args, { signal: exec.signal, core });
|
|
522
548
|
return submitted.text + '\nartifact: ' + submitted.artifactIds.join(', ') + '\n' +
|
|
523
549
|
submitted.artifactIds.map(id => '[♪ 音乐播放](/iris/api/core/artifact/' + encodeURIComponent(id) + '/media)').join('\n');
|
|
524
550
|
}
|
|
@@ -540,7 +566,7 @@ export async function apply(ctx) {
|
|
|
540
566
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
541
567
|
isConcurrencySafe: () => true,
|
|
542
568
|
async execute(args, exec) {
|
|
543
|
-
const submitted = await runAction(dshHost(ctx), 'tts', args, { signal: exec.signal });
|
|
569
|
+
const submitted = await runAction(dshHost(ctx), 'tts', args, { signal: exec.signal, core });
|
|
544
570
|
if (submitted.storage === 'core') {
|
|
545
571
|
const final = await inspectProviderTaskForDsh(submitted.taskId, { signal: exec.signal });
|
|
546
572
|
const terminalProblem = coreTaskTerminalProblem(final, '语音合成');
|
|
@@ -577,11 +603,11 @@ export async function apply(ctx) {
|
|
|
577
603
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
578
604
|
isConcurrencySafe: () => false,
|
|
579
605
|
async execute(args, exec) {
|
|
580
|
-
const submitted = await runAction(dshHost(ctx), 'transcribe', args, { signal: exec.signal });
|
|
606
|
+
const submitted = await runAction(dshHost(ctx), 'transcribe', args, { signal: exec.signal, core });
|
|
581
607
|
if (submitted.storage === 'core') {
|
|
582
608
|
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal: exec.signal });
|
|
583
609
|
if (!coreTaskWaitFinished(final)) {
|
|
584
|
-
final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.transcribe, signal: exec.signal });
|
|
610
|
+
final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: AWAIT_MS.transcribe, signal: exec.signal });
|
|
585
611
|
}
|
|
586
612
|
if (!final) {
|
|
587
613
|
return `[iris] Core 转写任务已提交并转入后台观察(task: ${submitted.taskId})。完成后用 iris_task_status 查询进度与 Artifact。`;
|
|
@@ -641,7 +667,7 @@ export async function apply(ctx) {
|
|
|
641
667
|
const refs = [];
|
|
642
668
|
for (let i = 0; i < frames.length; i++) {
|
|
643
669
|
const f = frames[i];
|
|
644
|
-
const ref = await attachmentService(
|
|
670
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
645
671
|
data: new Uint8Array(f.bytes),
|
|
646
672
|
mediaType,
|
|
647
673
|
name: `iris-frame-${Date.now()}-${i + 1}.${ext}`
|
|
@@ -714,8 +740,8 @@ export async function apply(ctx) {
|
|
|
714
740
|
try {
|
|
715
741
|
const audio = await extractAudioTrack({ inputPath: p, signal });
|
|
716
742
|
try {
|
|
717
|
-
const submitted = await runAction(dshHost(ctx), 'transcribe', { audio_path: audio.filePath }, { signal });
|
|
718
|
-
const final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: 120000, signal });
|
|
743
|
+
const submitted = await runAction(dshHost(ctx), 'transcribe', { audio_path: audio.filePath }, { signal, core });
|
|
744
|
+
const final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: 120000, signal });
|
|
719
745
|
if (final?.outcome === 'succeeded' && final.deliveryState === 'ready') {
|
|
720
746
|
const texts = [];
|
|
721
747
|
for (const artifactId of final.artifactIds) {
|
|
@@ -738,7 +764,7 @@ export async function apply(ctx) {
|
|
|
738
764
|
});
|
|
739
765
|
// 成功摘要的同一张拼图作为显式工具产物发布。
|
|
740
766
|
if (exec.signal?.aborted) throw exec.signal.reason;
|
|
741
|
-
const ref = await attachmentService(
|
|
767
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
742
768
|
data: new Uint8Array(sheet.buffer),
|
|
743
769
|
mediaType: 'image/png',
|
|
744
770
|
name: `iris-sheet-${Date.now()}.png`
|
|
@@ -795,7 +821,7 @@ export async function apply(ctx) {
|
|
|
795
821
|
async prepareImage(signal) {
|
|
796
822
|
if (source === 'artifact_id') return visionImageForDsh({ artifactId: args.artifact_id, signal });
|
|
797
823
|
const image = await readVisionImageFile(p, mt, signal);
|
|
798
|
-
await waitForModelWork(() => attachmentService(
|
|
824
|
+
await waitForModelWork(() => attachmentService(dshHost(ctx)).saveImage({ data: image.bytes, mediaType: mt, name: path.basename(p) }), signal);
|
|
799
825
|
return image;
|
|
800
826
|
}
|
|
801
827
|
});
|
|
@@ -827,10 +853,10 @@ export async function apply(ctx) {
|
|
|
827
853
|
return runVisionTool(dshHost(ctx), exec, { origin: 'relook', model: args.model, question,
|
|
828
854
|
async prepareImage(signal) {
|
|
829
855
|
// 会话扫描、解引用与读图都属于这次操作的总预算。
|
|
830
|
-
const hit = (await waitForModelWork(() => sessionAttachmentRef(
|
|
856
|
+
const hit = (await waitForModelWork(() => sessionAttachmentRef(dshHost(ctx), exec, id), signal)) || findOwnAttachment(id);
|
|
831
857
|
if (!hit) throw new Error('iris: 图片引用不可用');
|
|
832
858
|
if (hit.absPath) return readVisionImageFile(hit.absPath, hit.ref.mediaType || MEDIA_TYPES[path.extname(hit.absPath).toLowerCase()] || 'image/png', signal);
|
|
833
|
-
const stored = await waitForModelWork(() => attachmentService(
|
|
859
|
+
const stored = await waitForModelWork(() => attachmentService(dshHost(ctx)).readImage(hit.ref, signal), signal);
|
|
834
860
|
return { bytes: new Uint8Array(stored.data), mediaType: stored.mediaType || hit.ref.mediaType };
|
|
835
861
|
}
|
|
836
862
|
});
|
|
@@ -852,7 +878,7 @@ export async function apply(ctx) {
|
|
|
852
878
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
853
879
|
isConcurrencySafe: () => true,
|
|
854
880
|
async execute(args) {
|
|
855
|
-
const result = await runAction(dshHost(ctx), 'status', args || {});
|
|
881
|
+
const result = await runAction(dshHost(ctx), 'status', args || {}, { core });
|
|
856
882
|
return result.text;
|
|
857
883
|
}
|
|
858
884
|
});
|
|
@@ -894,7 +920,7 @@ export async function apply(ctx) {
|
|
|
894
920
|
signal: exec.signal
|
|
895
921
|
});
|
|
896
922
|
if (exec.signal && exec.signal.aborted) throw new Error('已取消');
|
|
897
|
-
const ref = await attachmentService(
|
|
923
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
898
924
|
data: new Uint8Array(result.bytes),
|
|
899
925
|
mediaType: result.mediaType,
|
|
900
926
|
name: `iris-crop-${Date.now()}.png`
|
|
@@ -940,7 +966,7 @@ export async function apply(ctx) {
|
|
|
940
966
|
grid: args.grid, topRegions: args.top_regions, signal: exec.signal
|
|
941
967
|
});
|
|
942
968
|
const { ratio, diffPixels, totalPixels, width, height, worstRegions } = result.metrics;
|
|
943
|
-
const ref = await attachmentService(
|
|
969
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
944
970
|
data: new Uint8Array(result.bytes),
|
|
945
971
|
mediaType: result.mediaType,
|
|
946
972
|
name: `iris-diff-${Date.now()}.png`
|
|
@@ -985,7 +1011,7 @@ export async function apply(ctx) {
|
|
|
985
1011
|
async execute(args, exec) {
|
|
986
1012
|
const r = await runLocateRequest(dshHost(ctx), {
|
|
987
1013
|
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability, model: args.model, target: args.target,
|
|
988
|
-
prepareImage: signal => resolveVisionImageInput(dshHost(ctx), exec, args, signal), signal: exec.signal
|
|
1014
|
+
prepareImage: signal => resolveVisionImageInput(core, dshHost(ctx), exec, args, signal), signal: exec.signal
|
|
989
1015
|
});
|
|
990
1016
|
const { width, height } = r;
|
|
991
1017
|
const sourceNote = args.artifact_id ? `\nCore Artifact: ${args.artifact_id}` : '';
|
|
@@ -1031,7 +1057,7 @@ export async function apply(ctx) {
|
|
|
1031
1057
|
});
|
|
1032
1058
|
const png = Buffer.from(rendered.bytes);
|
|
1033
1059
|
const { width: outW, height: outH } = await imageDimensions(png);
|
|
1034
|
-
const ref = await attachmentService(
|
|
1060
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
1035
1061
|
data: new Uint8Array(png),
|
|
1036
1062
|
mediaType: 'image/png',
|
|
1037
1063
|
name: `iris-html-${Date.now()}.png`
|
|
@@ -1074,7 +1100,7 @@ export async function apply(ctx) {
|
|
|
1074
1100
|
const host = dshHost(ctx);
|
|
1075
1101
|
const result = await runOcrRequest(host, {
|
|
1076
1102
|
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
|
|
1077
|
-
prepareImage: operationSignal => resolveVisionImageInput(host, exec, args, operationSignal),
|
|
1103
|
+
prepareImage: operationSignal => resolveVisionImageInput(core, host, exec, args, operationSignal),
|
|
1078
1104
|
chunkHeight: args.chunk_height,
|
|
1079
1105
|
overlap: args.overlap,
|
|
1080
1106
|
signal: exec.signal
|
|
@@ -1133,10 +1159,10 @@ async function resolveImageInput(host, exec, { image_path, attachment_id }) {
|
|
|
1133
1159
|
}
|
|
1134
1160
|
|
|
1135
1161
|
/** 视觉解引用在整体预算内;路径限额读,Host 会话/附件等待可取消。 */
|
|
1136
|
-
async function resolveVisionImageInput(host, exec, input, signal) {
|
|
1162
|
+
async function resolveVisionImageInput(core, host, exec, input, signal) {
|
|
1137
1163
|
const source = normalizeVisionImageSource(input, { allowAttachment: true });
|
|
1138
1164
|
const { image_path, artifact_id, attachment_id } = input;
|
|
1139
|
-
if (source === 'artifact_id') return visionImageForDsh({ artifactId: artifact_id, signal });
|
|
1165
|
+
if (source === 'artifact_id') return core.visionImageForDsh({ artifactId: artifact_id, signal });
|
|
1140
1166
|
if (image_path) {
|
|
1141
1167
|
const file = String(image_path).trim();
|
|
1142
1168
|
const mediaType = MEDIA_TYPES[path.extname(file).toLowerCase()];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// HTTP DTOs whitelist existing Core facts; never expose records or configuration wholesale.
|
|
2
|
+
import { isAudioRecipe } from './audio-flow-input.js';
|
|
3
|
+
const STATUSES = new Set(['not_started', 'running', 'needs_attention', 'delivery_failed',
|
|
4
|
+
'needs_comparison', 'needs_result', 'awaiting_selection', 'selected', 'ready']);
|
|
5
|
+
|
|
6
|
+
export function projectLocalApiFlow(facts) {
|
|
7
|
+
if (facts.contractVersion !== 0 || !['image-edit/v0', 'image-generate/v0'].includes(facts.run.recipe) && !isAudioRecipe(facts.run.recipe) || !STATUSES.has(facts.status)) {
|
|
8
|
+
throw new TypeError('不支持的流程契约或状态');
|
|
9
|
+
}
|
|
10
|
+
const { run, task } = facts;
|
|
11
|
+
const ref = value => value ? { artifactId: value.artifactId } : null;
|
|
12
|
+
return {
|
|
13
|
+
contractVersion: 0, id: run.id, recipe: run.recipe, revision: run.revision,
|
|
14
|
+
createdAt: run.createdAt, updatedAt: run.updatedAt, status: facts.status,
|
|
15
|
+
input: { ...(isAudioRecipe(run.recipe) ? run.recipe === 'speech/v0' ? { text: run.input.text,
|
|
16
|
+
...(run.input.voice !== undefined ? { voice: run.input.voice } : {}) } : {} : { prompt: run.input.prompt }), modelRef: run.input.model_ref,
|
|
17
|
+
...(run.input.size !== undefined ? { size: run.input.size } : {}),
|
|
18
|
+
...(run.input.workflow_parameters !== undefined ? { workflowParameters: run.input.workflow_parameters } : {}) },
|
|
19
|
+
source: ref(run.source), output: ref(run.output), comparison: ref(run.comparison), selection: ref(run.selection),
|
|
20
|
+
task: task ? { id: task.id, acceptance: task.acceptance, outcome: task.outcome,
|
|
21
|
+
deliveryState: task.deliveryState, attemptCount: task.attempts.length } : null,
|
|
22
|
+
metrics: facts.metrics ? Object.fromEntries(['ratio', 'diffPixels', 'totalPixels', 'width', 'height']
|
|
23
|
+
.map(key => [key, facts.metrics[key]])) : null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function projectLocalApiRunSummary(run) {
|
|
28
|
+
return { id: run.id, recipe: run.recipe, createdAt: run.createdAt,
|
|
29
|
+
updatedAt: run.updatedAt, revision: run.revision, modelRef: run.input.model_ref,
|
|
30
|
+
source: run.source ? { artifactId: run.source.artifactId } : null, output: run.output ? { artifactId: run.output.artifactId } : null,
|
|
31
|
+
selection: run.selection ? { artifactId: run.selection.artifactId } : null };
|
|
32
|
+
}
|