@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.
Files changed (177) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.en.md +104 -304
  3. package/README.md +101 -302
  4. package/bin/dsh-iris.js +446 -64
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +7 -5
  7. package/docs/ARCHITECTURE.md +20 -4
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CLI_MANAGEMENT.md +1 -1
  10. package/docs/CONTACT_SHEET.md +62 -0
  11. package/docs/CORE_CONNECTIONS.md +99 -0
  12. package/docs/CORE_QUICKSTART.md +108 -0
  13. package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
  14. package/docs/CORE_SERVICE.md +97 -0
  15. package/docs/CORE_SKILLS.md +94 -0
  16. package/docs/DSH_COMPATIBILITY.md +14 -4
  17. package/docs/DSH_CORE_MIGRATION.md +4 -2
  18. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +40 -0
  21. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  22. package/docs/HEADLESS_CLI.md +12 -4
  23. package/docs/HOST_DOCTOR.md +13 -1
  24. package/docs/IMAGE_FLOW.md +75 -0
  25. package/docs/IMAGE_GENERATION.md +127 -0
  26. package/docs/LOCAL_API.md +62 -0
  27. package/docs/LOCAL_API_CONTRACT.md +140 -0
  28. package/docs/LOCAL_CREATION.md +56 -0
  29. package/docs/LOCAL_OPERATIONS.md +46 -0
  30. package/docs/LOCAL_SELECTION.md +57 -0
  31. package/docs/LOCAL_WEB.md +201 -0
  32. package/docs/MCP.md +191 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/PANEL_EXTENSIONS.md +64 -0
  35. package/docs/PLUGINS.md +189 -0
  36. package/docs/PLUGIN_STARTUP.md +100 -0
  37. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  38. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  39. package/docs/PROVIDER_HEALTH.md +4 -2
  40. package/docs/PROVIDER_SETTINGS.md +85 -0
  41. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  42. package/docs/ROADMAP.md +28 -6
  43. package/docs/SECURITY.md +9 -1
  44. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  45. package/docs/VISION_CLI.md +2 -2
  46. package/docs/WEB_PROTOTYPE.md +62 -0
  47. package/docs/WEB_RECOVERY.md +46 -0
  48. package/docs/WORKBENCH_LOGIN.md +63 -0
  49. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  50. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  51. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  52. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  53. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  54. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  55. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  56. package/docs/releases/0.2.7.md +62 -0
  57. package/docs/releases/0.3.0-alpha.1.md +51 -0
  58. package/docs/screenshots.md +41 -1
  59. package/lib/actions.js +132 -70
  60. package/lib/adapters.js +20 -11
  61. package/lib/api.js +38 -38
  62. package/lib/application-runtime.js +213 -0
  63. package/lib/audio-flow-input.js +37 -0
  64. package/lib/client.js +434 -55
  65. package/lib/comfyui-adapter.js +94 -0
  66. package/lib/comfyui-client.js +51 -0
  67. package/lib/comfyui-inspection.js +73 -0
  68. package/lib/comfyui-workflow.js +94 -0
  69. package/lib/config.js +36 -1
  70. package/lib/configured-image-runtime.js +50 -0
  71. package/lib/connection-skill.js +89 -0
  72. package/lib/connection-template.js +23 -0
  73. package/lib/core-artifact-store.js +21 -2
  74. package/lib/core-bridge-selection.js +31 -0
  75. package/lib/core-bridge-tools.js +83 -0
  76. package/lib/core-capability-catalog.js +182 -0
  77. package/lib/core-connection-diagnostics.js +49 -0
  78. package/lib/core-connection-store.js +277 -0
  79. package/lib/core-connections.js +225 -0
  80. package/lib/core-maintenance.js +31 -2
  81. package/lib/core-runtime.js +20 -0
  82. package/lib/core-service-client.js +170 -0
  83. package/lib/core-tasks.js +84 -13
  84. package/lib/core-writer-lease.js +16 -0
  85. package/lib/creation-request-store.js +133 -0
  86. package/lib/doctor.js +12 -1
  87. package/lib/dsh-core-adapter.js +666 -572
  88. package/lib/dsh-host-adapter.js +8 -2
  89. package/lib/dsh-service-bridge.js +66 -0
  90. package/lib/execution-ownership.js +43 -0
  91. package/lib/flow-store.js +290 -0
  92. package/lib/generation-input.js +10 -1
  93. package/lib/headless-vision.js +7 -7
  94. package/lib/image-edit-input.js +1 -1
  95. package/lib/image-flow.js +232 -0
  96. package/lib/index.js +94 -68
  97. package/lib/local-api-projection.js +32 -0
  98. package/lib/local-api-token-directory.js +31 -0
  99. package/lib/local-api.js +1083 -0
  100. package/lib/local-creation.js +150 -0
  101. package/lib/local-operations.js +36 -0
  102. package/lib/local-request-root.js +86 -0
  103. package/lib/local-web-assets.js +37 -0
  104. package/lib/mcp-bridge.js +58 -0
  105. package/lib/mcp-check.js +26 -0
  106. package/lib/mcp-config.js +42 -0
  107. package/lib/mcp-delivery.js +46 -0
  108. package/lib/mcp-http.js +101 -0
  109. package/lib/mcp-server.js +47 -0
  110. package/lib/model-directory-service.js +16 -6
  111. package/lib/model-directory.js +3 -3
  112. package/lib/models.js +2 -2
  113. package/lib/music-transport.js +2 -1
  114. package/lib/operation-runner.js +236 -0
  115. package/lib/operation-store.js +216 -0
  116. package/lib/panel-extension.js +36 -0
  117. package/lib/plugin-contract.js +194 -0
  118. package/lib/plugin-execution.js +51 -0
  119. package/lib/plugin-launch.js +86 -0
  120. package/lib/plugin-operation-store.js +101 -0
  121. package/lib/plugin-operations.js +28 -0
  122. package/lib/plugin-registry.js +109 -0
  123. package/lib/plugin-selection.js +15 -0
  124. package/lib/plugin-source-image.js +5 -0
  125. package/lib/plugin-web-assets.js +54 -0
  126. package/lib/plugins.d.ts +288 -0
  127. package/lib/plugins.js +58 -0
  128. package/lib/private-storage.js +12 -0
  129. package/lib/provider-adapter.js +5 -1
  130. package/lib/provider-adapters.js +56 -18
  131. package/lib/provider-catalog.js +40 -29
  132. package/lib/provider-config-service.js +105 -23
  133. package/lib/provider-diagnostics.js +56 -0
  134. package/lib/provider-extensions.js +29 -0
  135. package/lib/provider-health.js +8 -1
  136. package/lib/provider-monitor.js +79 -0
  137. package/lib/provider-protocol.js +70 -20
  138. package/lib/provider-registry.js +118 -0
  139. package/lib/provider-response.js +28 -0
  140. package/lib/provider-settings.js +134 -0
  141. package/lib/provider-task-runner.js +40 -7
  142. package/lib/render-address.js +35 -0
  143. package/lib/render.js +52 -13
  144. package/lib/skill-installation.js +211 -0
  145. package/lib/vision-model-routing.js +3 -0
  146. package/lib/web/app.js +642 -0
  147. package/lib/web/appearance.js +111 -0
  148. package/lib/web/comfyui.js +146 -0
  149. package/lib/web/connections.js +377 -0
  150. package/lib/web/creation.js +355 -0
  151. package/lib/web/flow-view.js +33 -0
  152. package/lib/web/gallery.js +132 -0
  153. package/lib/web/index.html +291 -0
  154. package/lib/web/media-client.js +204 -0
  155. package/lib/web/plugin-panel.js +221 -0
  156. package/lib/web/plugin-surface.css +17 -0
  157. package/lib/web/providers.js +492 -0
  158. package/lib/web/recovery.js +105 -0
  159. package/lib/web/startup.js +18 -0
  160. package/lib/web/styles.css +518 -0
  161. package/lib/web/transport.js +96 -0
  162. package/lib/web/workbench-login.js +165 -0
  163. package/lib/workbench-artifacts.js +1 -2
  164. package/lib/workbench-auth-cli.js +47 -0
  165. package/lib/workbench-auth.js +200 -0
  166. package/lib/workbench-management.js +8 -9
  167. package/package.json +22 -6
  168. package/plugins/contact-sheet/index.d.mts +4 -0
  169. package/plugins/contact-sheet/index.mjs +33 -0
  170. package/plugins/contact-sheet/web/board.css +14 -0
  171. package/plugins/contact-sheet/web/board.mjs +93 -0
  172. package/skills/iris-core/SKILL.md +44 -0
  173. package/skills/iris-core/agents/openai.yaml +4 -0
  174. package/skills/iris-core/references/cli.md +95 -0
  175. package/skills/iris-core/references/comfyui.md +69 -0
  176. package/skills/iris-core/references/tasks.md +60 -0
  177. 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 { configureProviderHostForDsh } from './dsh-core-adapter.js';
23
- import { cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, projectCoreTaskForDsh, readCoreArtifactMediaForDsh, resumeProviderTaskWatchesForDsh, stopProviderTaskWatchesForDsh } from './dsh-core-adapter.js';
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 mediaRouteMounted = false;
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' || mediaRouteMounted) return;
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
- mediaRouteMounted = true;
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 releaseProviderHost = configureProviderHostForDsh(() => dshHost(ctx));
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
- try {
249
- const resumed = await resumeProviderTaskWatchesForDsh();
250
- if (resumed.length) console.log(`[iris] 已恢复接管 ${resumed.length} 个 Core 图片任务`);
251
- } catch (error) {
252
- console.error('[iris] 恢复 Core 图片后台任务失败(已隔离,不影响宿主):', error?.message || error);
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 execute = async () => {
288
- if (visionTools.has(definition.name) && !args?.model) await store.refreshAutomaticDirectories({ capability: 'vision', signal: exec.signal });
289
- return definition.execute(args, exec);
290
- };
291
- return providerTools.has(definition.name)
292
- ? withDshProviderConfig(dshHost(ctx), execute, { signal: exec.signal }) : execute();
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(host).saveImage({ data: new Uint8Array(fs.readFileSync(f)), mediaType, name: path.basename(f) });
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(host).saveImage({
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(host).saveImage({
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(host).saveImage({ data: image.bytes, mediaType: mt, name: path.basename(p) }), signal);
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(host, exec, id), signal)) || findOwnAttachment(id);
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(host).readImage(hit.ref, signal), signal);
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(host).saveImage({
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(host).saveImage({
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(host).saveImage({
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
+ }