@mokuyoaxis/dsh-iris 0.2.6 → 0.2.7
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 +23 -1
- package/README.en.md +28 -19
- package/README.md +27 -18
- package/bin/dsh-iris.js +140 -26
- package/docs/AGENT_QUICKSTART.md +5 -5
- package/docs/ARCHITECTURE.md +9 -3
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
- package/docs/CORE_SKILLS.md +62 -0
- package/docs/DSH_COMPATIBILITY.md +4 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RUNTIME.md +36 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +11 -3
- package/docs/HOST_DOCTOR.md +13 -1
- package/docs/IMAGE_FLOW.md +71 -0
- package/docs/LOCAL_API.md +60 -0
- package/docs/LOCAL_API_CONTRACT.md +115 -0
- package/docs/LOCAL_CREATION.md +50 -0
- package/docs/PROVIDER_HEALTH.md +1 -1
- package/docs/ROADMAP.md +14 -2
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/WEB_PROTOTYPE.md +47 -0
- package/docs/releases/0.2.7.md +62 -0
- package/lib/actions.js +79 -68
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +186 -0
- package/lib/client.js +89 -3
- package/lib/core-artifact-store.js +5 -0
- package/lib/core-maintenance.js +25 -2
- package/lib/core-runtime.js +19 -0
- package/lib/core-tasks.js +62 -8
- package/lib/creation-request-store.js +159 -0
- package/lib/doctor.js +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/flow-store.js +195 -0
- package/lib/image-flow.js +188 -0
- package/lib/index.js +59 -53
- package/lib/local-api-projection.js +27 -0
- package/lib/local-api.js +381 -0
- package/lib/local-creation.js +85 -0
- package/lib/private-storage.js +12 -0
- package/lib/provider-task-runner.js +22 -2
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +199 -0
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-management.js +8 -9
- package/package.json +3 -2
- package/skills/iris-core/SKILL.md +40 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/tasks.md +58 -0
- package/user_guide.md +14 -8
package/lib/dsh-host-adapter.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { defineHostAdapter } from './host-contract.js';
|
|
7
7
|
import { BrowserHtmlRenderer } from './render.js';
|
|
8
|
+
import { resolveRenderAddress } from './render-address.js';
|
|
8
9
|
import { createDshProviderSource } from './dsh-provider-source.js';
|
|
9
10
|
|
|
10
11
|
export function detectDshVersion({ entry = process.argv[1] } = {}) {
|
|
@@ -160,8 +161,10 @@ export function createDshHostAdapter(ctx, { version = detectDshVersion() } = {})
|
|
|
160
161
|
|
|
161
162
|
const browser = service(ctx, 'browser');
|
|
162
163
|
if (browser && ['open', 'openUrl', 'screenshot', 'close'].every((name) => typeof browser[name] === 'function')) {
|
|
163
|
-
const
|
|
164
|
+
const getAddress = () => resolveRenderAddress(service(ctx, 'webServer') || service(ctx, 'httpServer'));
|
|
165
|
+
const renderer = new BrowserHtmlRenderer({ browser, getAddress });
|
|
164
166
|
ports.browser = Object.freeze({
|
|
167
|
+
get address() { return getAddress(); },
|
|
165
168
|
async renderHtml(input) {
|
|
166
169
|
const { png } = await renderer.render(input);
|
|
167
170
|
return { bytes: new Uint8Array(png), mediaType: 'image/png' };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
|
|
7
|
+
import { WRITER_LEASE_DIR, WRITER_INTENT_FILE } from './core-writer-lease.js';
|
|
8
|
+
import { normalizeGenerationInput } from './generation-input.js';
|
|
9
|
+
import { parseModelRef } from './models.js';
|
|
10
|
+
|
|
11
|
+
const RUN = /^run_[a-f0-9]{24}$/;
|
|
12
|
+
const TASK = /^task_[a-f0-9]{24}$/;
|
|
13
|
+
const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
14
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
15
|
+
const BINDING = /^sha256:[a-f0-9]{64}$/;
|
|
16
|
+
const RECIPE = 'image-edit/v0';
|
|
17
|
+
|
|
18
|
+
export class FlowError extends Error {
|
|
19
|
+
constructor(code, message) { super(message); this.name = 'FlowError'; this.code = code; }
|
|
20
|
+
}
|
|
21
|
+
export function flowFail(code, message) { throw new FlowError(code, message); }
|
|
22
|
+
|
|
23
|
+
function keys(value, allowed, required = allowed) {
|
|
24
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value)
|
|
25
|
+
&& Object.keys(value).every(key => allowed.includes(key)) && required.every(key => Object.hasOwn(value, key)));
|
|
26
|
+
}
|
|
27
|
+
function iso(value) { return typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value; }
|
|
28
|
+
const copy = value => Object.freeze(structuredClone(value));
|
|
29
|
+
export const flowId = prefix => prefix + '_' + crypto.randomBytes(12).toString('hex');
|
|
30
|
+
export const flowSha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
31
|
+
|
|
32
|
+
export function validFlowBinding(value) {
|
|
33
|
+
return keys(value, ['runId', 'stepId', 'inputDigest']) && RUN.test(value.runId)
|
|
34
|
+
&& value.stepId === 'edit' && BINDING.test(value.inputDigest);
|
|
35
|
+
}
|
|
36
|
+
export function flowTaskBinding(run) { return { runId: run.id, stepId: 'edit', inputDigest: run.inputDigest }; }
|
|
37
|
+
|
|
38
|
+
function inputDigest(run) {
|
|
39
|
+
return 'sha256:' + flowSha256(JSON.stringify([RECIPE, run.source.artifactId, run.source.sha256,
|
|
40
|
+
run.input.model_ref, run.providerBinding, run.input.prompt, run.input.size ?? null, 1]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function reference(value) { return keys(value, ['artifactId', 'sha256']) && ARTIFACT.test(value.artifactId) && SHA.test(value.sha256); }
|
|
44
|
+
function validate(run, expectedId) {
|
|
45
|
+
let normalized;
|
|
46
|
+
try { normalized = normalizeGenerationInput('image', run?.input); }
|
|
47
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程创作输入无效'); }
|
|
48
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding',
|
|
49
|
+
'inputDigest', 'edit', 'compare', 'output', 'comparison', 'selection'],
|
|
50
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding', 'inputDigest', 'edit', 'compare'])
|
|
51
|
+
|| run.schemaVersion !== 0 || run.recipe !== RECIPE || !RUN.test(run.id) || run.id !== expectedId
|
|
52
|
+
|| !Number.isSafeInteger(run.revision) || run.revision < 1 || !iso(run.createdAt) || !iso(run.updatedAt)
|
|
53
|
+
|| !reference(run.source) || !BINDING.test(run.providerBinding)
|
|
54
|
+
|| !parseModelRef(run.input.model_ref) || normalized.modelRef !== run.input.model_ref
|
|
55
|
+
|| run.input.prompt !== normalized.providerInput.prompt || run.input.n !== 1
|
|
56
|
+
|| (run.input.size !== undefined && run.input.size !== normalized.providerInput.size)
|
|
57
|
+
|| normalized.sourceArtifactId !== run.source.artifactId
|
|
58
|
+
|| !keys(run.edit, ['taskId', 'claimed']) || !TASK.test(run.edit.taskId) || typeof run.edit.claimed !== 'boolean'
|
|
59
|
+
|| !keys(run.compare, ['artifactId']) || !ARTIFACT.test(run.compare.artifactId)
|
|
60
|
+
|| run.compare.artifactId === run.source.artifactId || run.inputDigest !== inputDigest(run)
|
|
61
|
+
|| (run.output !== undefined && (!reference(run.output) || !run.edit.claimed
|
|
62
|
+
|| [run.source.artifactId, run.compare.artifactId].includes(run.output.artifactId)))
|
|
63
|
+
|| (run.comparison !== undefined && (!reference(run.comparison) || !run.output || run.comparison.artifactId !== run.compare.artifactId))
|
|
64
|
+
|| (run.selection !== undefined && (!reference(run.selection) || !run.comparison
|
|
65
|
+
|| ![run.source, run.output].some(item => item.artifactId === run.selection.artifactId && item.sha256 === run.selection.sha256)))) {
|
|
66
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '流程记录结构或绑定摘要冲突');
|
|
67
|
+
}
|
|
68
|
+
return run;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function directory(root, leaf, create = false) {
|
|
72
|
+
let current = root;
|
|
73
|
+
for (const segment of ['flow-store', 'v0', ...(leaf ? [leaf] : [])]) {
|
|
74
|
+
current = path.join(current, segment);
|
|
75
|
+
if (create && !fs.existsSync(current)) fs.mkdirSync(current, { mode: 0o700 });
|
|
76
|
+
const stat = fs.lstatSync(current);
|
|
77
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) flowFail('IRIS_FLOW_STORE_UNSAFE', '流程存储目录不安全');
|
|
78
|
+
}
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
function jsonFile(file) {
|
|
82
|
+
const stat = fs.lstatSync(file);
|
|
83
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) flowFail('IRIS_FLOW_RECORD_INVALID', '流程记录不是受支持的普通文件');
|
|
84
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
85
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程记录不是有效 JSON'); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function inspectFlowRoot(root) {
|
|
89
|
+
let marker;
|
|
90
|
+
try { marker = jsonFile(path.join(directory(root), 'root.json')); }
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error.code === 'ENOENT') flowFail('IRIS_FLOW_ROOT_REQUIRED', '请先在新的专用数据根执行 flow init,再导入图片');
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
if (!keys(marker, ['schemaVersion', 'kind', 'createdAt']) || marker.schemaVersion !== 0
|
|
96
|
+
|| marker.kind !== 'iris-image-flow-root' || !iso(marker.createdAt)) flowFail('IRIS_FLOW_RECORD_INVALID', '流程根声明无效或版本不受支持');
|
|
97
|
+
directory(root, 'runs');
|
|
98
|
+
return copy(marker);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function initializeFlowRoot(root) {
|
|
102
|
+
try { fs.lstatSync(path.join(root, 'flow-store')); return inspectFlowRoot(root); }
|
|
103
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
104
|
+
if (fs.readdirSync(root).some(name => ![WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name))) {
|
|
105
|
+
flowFail('IRIS_FLOW_ROOT_NOT_EMPTY', '流程只在新的专用数据根启用;不迁移已有 Task/Artifact 数据根');
|
|
106
|
+
}
|
|
107
|
+
directory(root, 'runs', true);
|
|
108
|
+
const marker = { schemaVersion: 0, kind: 'iris-image-flow-root', createdAt: new Date().toISOString() };
|
|
109
|
+
atomicCreatePrivate(path.join(directory(root), 'root.json'), JSON.stringify(marker, null, 2) + '\n');
|
|
110
|
+
return copy(marker);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function recordFile(root, id) {
|
|
114
|
+
if (typeof id !== 'string' || !RUN.test(id)) flowFail('IRIS_FLOW_ID_INVALID', 'Run ID 格式无效');
|
|
115
|
+
return path.join(directory(root, 'runs'), id + '.json');
|
|
116
|
+
}
|
|
117
|
+
export function readFlowRun(root, id) {
|
|
118
|
+
inspectFlowRoot(root);
|
|
119
|
+
let record;
|
|
120
|
+
try { record = jsonFile(recordFile(root, id)); }
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code === 'ENOENT') flowFail('IRIS_FLOW_NOT_FOUND', '流程记录不存在');
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return copy(validate(record, id));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Internal immutable creation intent; HTTP callers cannot choose these identities. */
|
|
129
|
+
export function prepareFlowCreation({ source, input, providerBinding }) {
|
|
130
|
+
return { runId: flowId('run'), taskId: flowId('task'), comparisonArtifactId: flowId('artifact'),
|
|
131
|
+
createdAt: new Date().toISOString(), source, input, providerBinding };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function validateFlowCreation(intent) {
|
|
135
|
+
if (!keys(intent, ['runId', 'taskId', 'comparisonArtifactId', 'createdAt', 'source', 'input', 'providerBinding'])) {
|
|
136
|
+
flowFail('IRIS_FLOW_RECORD_INVALID', '流程创建意图无效');
|
|
137
|
+
}
|
|
138
|
+
const record = { schemaVersion: 0, recipe: RECIPE, id: intent.runId, revision: 1,
|
|
139
|
+
createdAt: intent.createdAt, updatedAt: intent.createdAt, source: intent.source, input: intent.input,
|
|
140
|
+
providerBinding: intent.providerBinding, edit: { taskId: intent.taskId, claimed: false },
|
|
141
|
+
compare: { artifactId: intent.comparisonArtifactId } };
|
|
142
|
+
record.inputDigest = inputDigest(record);
|
|
143
|
+
return validate(record, record.id);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function commitFlowCreation(root, intent, { allowCreate = true } = {}) {
|
|
147
|
+
inspectFlowRoot(root);
|
|
148
|
+
const record = validateFlowCreation(intent);
|
|
149
|
+
let current;
|
|
150
|
+
try { current = readFlowRun(root, record.id); }
|
|
151
|
+
catch (error) { if (error.code !== 'IRIS_FLOW_NOT_FOUND') throw error; }
|
|
152
|
+
if (current) {
|
|
153
|
+
if (current.createdAt !== record.createdAt || current.inputDigest !== record.inputDigest
|
|
154
|
+
|| current.edit.taskId !== record.edit.taskId || current.compare.artifactId !== record.compare.artifactId) {
|
|
155
|
+
flowFail('IRIS_FLOW_INPUT_CONFLICT', '原流程与预留创建身份不一致');
|
|
156
|
+
}
|
|
157
|
+
return current;
|
|
158
|
+
}
|
|
159
|
+
if (!allowCreate) flowFail('IRIS_FLOW_NOT_FOUND', '原流程记录已不可用');
|
|
160
|
+
atomicCreatePrivate(recordFile(root, record.id), JSON.stringify(record, null, 2) + '\n');
|
|
161
|
+
return copy(record);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createFlowRun(root, input) {
|
|
165
|
+
return commitFlowCreation(root, prepareFlowCreation(input));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function updateFlowRun(root, previous, patch) {
|
|
169
|
+
const current = readFlowRun(root, previous.id);
|
|
170
|
+
if (current.revision !== previous.revision) flowFail('IRIS_FLOW_REVISION_CONFLICT', '流程在本次操作期间已改变');
|
|
171
|
+
if (!keys(patch, ['edit', 'output', 'comparison', 'selection'], [])) throw new TypeError('未知流程更新字段');
|
|
172
|
+
const record = { ...current, ...patch, revision: current.revision + 1, updatedAt: new Date().toISOString() };
|
|
173
|
+
validate(record, current.id);
|
|
174
|
+
atomicWritePrivate(recordFile(root, record.id), JSON.stringify(record, null, 2) + '\n');
|
|
175
|
+
return copy(record);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function listFlowRuns(root) {
|
|
179
|
+
inspectFlowRoot(root);
|
|
180
|
+
const records = [];
|
|
181
|
+
for (const name of fs.readdirSync(directory(root, 'runs'))) {
|
|
182
|
+
if (/^run_[a-f0-9]{24}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name)) continue; // 保留未发布的原子写现场。
|
|
183
|
+
if (!/^run_[a-f0-9]{24}\.json$/.test(name)) flowFail('IRIS_FLOW_RECORD_INVALID', '流程目录包含未知记录,停止盘点');
|
|
184
|
+
records.push(readFlowRun(root, name.slice(0, -5)));
|
|
185
|
+
}
|
|
186
|
+
return records.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 管理入口不能忽略损坏/未知的流程根,从而丢掉引用。普通 Core 根保持零写。 */
|
|
190
|
+
export function flowReferences(root) {
|
|
191
|
+
try { fs.lstatSync(path.join(root, 'flow-store')); }
|
|
192
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
193
|
+
return listFlowRuns(root).map(run => ({ run, taskIds: [run.edit.taskId],
|
|
194
|
+
artifactIds: [...new Set([run.source.artifactId, run.compare.artifactId, run.output?.artifactId, run.selection?.artifactId].filter(Boolean))] }));
|
|
195
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
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 { commitFlowCreation, flowFail, flowSha256, initializeFlowRoot, inspectFlowRoot, listFlowRuns, prepareFlowCreation, readFlowRun, updateFlowRun } from './flow-store.js';
|
|
12
|
+
|
|
13
|
+
function exact(input, allowed) {
|
|
14
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !allowed.includes(key))) {
|
|
15
|
+
flowFail('IRIS_FLOW_INPUT_INVALID', '流程输入包含不支持的字段');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const reference = artifact => ({ artifactId: artifact.id, sha256: artifact.digest.value });
|
|
19
|
+
|
|
20
|
+
function readReference(root, expected) {
|
|
21
|
+
const value = readCoreArtifactBytes(root, expected.artifactId);
|
|
22
|
+
if (value.artifact.digest.value !== expected.sha256) flowFail('IRIS_FLOW_ARTIFACT_CONFLICT', '流程引用的作品摘要已改变');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function facts(root, run) {
|
|
27
|
+
const source = readReference(root, run.source);
|
|
28
|
+
const task = findFlowTask(root, run);
|
|
29
|
+
if (run.output) {
|
|
30
|
+
if (task.deliveryState !== 'ready' || task.artifactIds.length !== 1 || task.artifactIds[0] !== run.output.artifactId) {
|
|
31
|
+
flowFail('IRIS_FLOW_TASK_CONFLICT', '流程输出与原 Task 的交付事实不一致');
|
|
32
|
+
}
|
|
33
|
+
const { artifact } = readReference(root, run.output);
|
|
34
|
+
if (artifact.metadata.taskId !== task.id || !artifact.relations.some(edge => edge.type === 'derived-from' && edge.artifactId === source.artifact.id)) {
|
|
35
|
+
flowFail('IRIS_FLOW_ARTIFACT_CONFLICT', '流程输出缺少原 Task 或来源关系');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let comparison;
|
|
39
|
+
if (run.comparison) {
|
|
40
|
+
comparison = readReference(root, run.comparison).artifact;
|
|
41
|
+
if (comparison.kind !== 'pixel-diff' || comparison.relations.length !== 2
|
|
42
|
+
|| ![run.source.artifactId, run.output.artifactId].every(id => comparison.relations.some(edge => edge.type === 'derived-from' && edge.artifactId === id))) {
|
|
43
|
+
flowFail('IRIS_FLOW_ARTIFACT_CONFLICT', '比较作品与流程来源不一致');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let status = 'not_started';
|
|
47
|
+
if (task?.acceptance === 'unknown' || ['unknown', 'failed', 'canceled'].includes(task?.outcome)) status = 'needs_attention';
|
|
48
|
+
else if (task?.deliveryState === 'ready') status = run.comparison ? run.selection ? 'selected' : 'awaiting_selection' : 'needs_comparison';
|
|
49
|
+
else if (task?.deliveryState === 'failed') status = 'delivery_failed';
|
|
50
|
+
else if (task?.attempts.length) status = task.acceptance === 'accepted' ? 'running' : 'needs_attention';
|
|
51
|
+
return { contractVersion: 0, run, task, status, ...(comparison ? { metrics: comparison.metadata } : {}) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Internal fixed recipe. All state/leases belong to the supplied operation-scoped runtime. */
|
|
55
|
+
export function createImageFlowService(runtime, { resolveImageCandidates, resolveTaskAdapter } = {}) {
|
|
56
|
+
function creationInput(input) {
|
|
57
|
+
const normalized = normalizeGenerationInput('image', input);
|
|
58
|
+
if (!normalized.sourceArtifactId || !parseModelRef(normalized.modelRef) || normalized.providerInput.n !== 1) {
|
|
59
|
+
flowFail('IRIS_FLOW_INPUT_INVALID', '首版流程需要来源 Artifact、明确 model_ref 和 n=1');
|
|
60
|
+
}
|
|
61
|
+
return { prompt: normalized.providerInput.prompt, model_ref: normalized.modelRef,
|
|
62
|
+
source_artifact_id: normalized.sourceArtifactId, n: 1,
|
|
63
|
+
...(normalized.providerInput.size !== undefined ? { size: normalized.providerInput.size } : {}) };
|
|
64
|
+
}
|
|
65
|
+
async function prepareCreation(root, signal, input) {
|
|
66
|
+
const owned = creationInput(input);
|
|
67
|
+
inspectFlowRoot(root);
|
|
68
|
+
const source = await readImageEditArtifact(root, owned.source_artifact_id, signal);
|
|
69
|
+
const candidates = await candidatesFor(owned, signal);
|
|
70
|
+
return prepareFlowCreation({ source: { artifactId: owned.source_artifact_id, sha256: flowSha256(source.bytes) },
|
|
71
|
+
input: owned, providerBinding: candidates[0].providerBinding });
|
|
72
|
+
}
|
|
73
|
+
async function candidatesFor(runInput, signal, binding) {
|
|
74
|
+
if (!resolveImageCandidates) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '创建或首次提交流程需要显式 Provider 配置');
|
|
75
|
+
signal.throwIfAborted();
|
|
76
|
+
const candidates = await resolveImageCandidates({ modelRef: runInput.model_ref, signal });
|
|
77
|
+
signal.throwIfAborted();
|
|
78
|
+
const parsed = parseModelRef(runInput.model_ref);
|
|
79
|
+
if (!Array.isArray(candidates) || candidates.length !== 1 || candidates[0].model !== runInput.model_ref
|
|
80
|
+
|| candidates[0].adapter?.id !== parsed.providerId || !/^sha256:[a-f0-9]{64}$/.test(candidates[0].providerBinding)) {
|
|
81
|
+
flowFail('IRIS_FLOW_INPUT_CONFLICT', '流程必须绑定单一明确模型和有效的连接摘要');
|
|
82
|
+
}
|
|
83
|
+
if (binding !== undefined && candidates[0].providerBinding !== binding) flowFail('IRIS_FLOW_PROVIDER_CHANGED', '连接或协议已改变;原流程不重新绑定或生成');
|
|
84
|
+
return imageEditCandidates([{ ...candidates[0], providerModel: parsed.modelId, selectionReason: 'explicit' }]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function adapterFor(task, signal) {
|
|
88
|
+
if (!resolveTaskAdapter) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '观察或远端补交付需要原 Task 的 Provider 配置');
|
|
89
|
+
const adapter = await resolveTaskAdapter(task, { signal });
|
|
90
|
+
signal.throwIfAborted();
|
|
91
|
+
return adapter;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function continueRun(root, signal, id) {
|
|
95
|
+
let run = readFlowRun(root, id);
|
|
96
|
+
let task = facts(root, run).task;
|
|
97
|
+
if (task?.attempts.length && !run.edit.claimed) flowFail('IRIS_FLOW_TASK_CONFLICT', '步骤检查点与已开始的 Task 冲突');
|
|
98
|
+
const runner = createProviderTaskRunner(runtime);
|
|
99
|
+
if (!task || !task.attempts.length) {
|
|
100
|
+
const candidates = await candidatesFor(run.input, signal, run.providerBinding);
|
|
101
|
+
task = claimFlowTask(root, run.id);
|
|
102
|
+
if (!run.edit.claimed) run = updateFlowRun(root, run, { edit: { ...run.edit, claimed: true } });
|
|
103
|
+
signal.throwIfAborted();
|
|
104
|
+
const normalized = normalizeGenerationInput('image', run.input);
|
|
105
|
+
task = (await runner.submit({ capability: 'image', candidates, providerInput: normalized.providerInput,
|
|
106
|
+
sourceArtifactId: run.source.artifactId, flowRunId: run.id })).task;
|
|
107
|
+
} else {
|
|
108
|
+
task = recoverCoreTask(root, task.id);
|
|
109
|
+
assertFlowTaskMatches(task, run);
|
|
110
|
+
if (task.outcome === 'succeeded' && task.deliveryState === 'failed') {
|
|
111
|
+
const adapter = task.deliveryCache && !deliveryCacheNeedsProvider(root, task) ? undefined : await adapterFor(task, signal);
|
|
112
|
+
task = await runner.redeliver(task.id, adapter);
|
|
113
|
+
} else if (task.acceptance === 'accepted' && task.outcome === 'none' && task.remoteTaskId) {
|
|
114
|
+
task = await runner.observe(task.id, await adapterFor(task, signal), { resumeActive: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
signal.throwIfAborted();
|
|
118
|
+
if (task.deliveryState !== 'ready') return facts(root, run);
|
|
119
|
+
if (task.artifactIds.length !== 1) flowFail('IRIS_FLOW_RESULT_UNSUPPORTED', '首版流程只支持一个改图结果,原 Task 与作品已保留');
|
|
120
|
+
const output = readCoreArtifactBytes(root, task.artifactIds[0]);
|
|
121
|
+
if (!run.output) run = updateFlowRun(root, run, { output: reference(output.artifact) });
|
|
122
|
+
facts(root, run);
|
|
123
|
+
if (!run.comparison) {
|
|
124
|
+
const source = readReference(root, run.source);
|
|
125
|
+
const diff = await pixelDiff({ inputA: source.bytes, inputB: output.bytes, grid: 8, topRegions: 3 });
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
const artifact = createCoreArtifact(root, { artifactId: run.compare.artifactId, createdAt: run.createdAt,
|
|
128
|
+
bytes: diff.heatmap, expectedDigest: flowSha256(diff.heatmap), expectedSize: diff.heatmap.length,
|
|
129
|
+
kind: 'pixel-diff', mediaType: diff.mime,
|
|
130
|
+
metadata: { ratio: diff.ratio, diffPixels: diff.diffPixels, totalPixels: diff.totalPixels,
|
|
131
|
+
width: diff.width, height: diff.height, worstRegions: diff.worstRegions },
|
|
132
|
+
relations: [run.source.artifactId, run.output.artifactId].map(artifactId => ({ type: 'derived-from', artifactId })) });
|
|
133
|
+
run = updateFlowRun(root, run, { comparison: reference(artifact) });
|
|
134
|
+
}
|
|
135
|
+
return facts(root, run);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function execute(name, input = {}) {
|
|
139
|
+
if (name === 'flow.init') {
|
|
140
|
+
exact(input, []);
|
|
141
|
+
return runtime.run('execute', ({ dataRoot }) => ({ contractVersion: 0, root: initializeFlowRoot(dataRoot) }));
|
|
142
|
+
}
|
|
143
|
+
if (name === 'flow.create') {
|
|
144
|
+
const owned = creationInput(input);
|
|
145
|
+
return runtime.run('execute', async ({ dataRoot, signal }) => {
|
|
146
|
+
const run = commitFlowCreation(dataRoot, await prepareCreation(dataRoot, signal, owned));
|
|
147
|
+
return facts(dataRoot, run);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (name === 'flow.list') {
|
|
151
|
+
exact(input, []);
|
|
152
|
+
return runtime.run('inspect', ({ dataRoot }) => {
|
|
153
|
+
const runs = listFlowRuns(dataRoot);
|
|
154
|
+
return { contractVersion: 0, total: runs.length, runs: runs.map(run => ({ id: run.id, recipe: run.recipe,
|
|
155
|
+
createdAt: run.createdAt, source: run.source, modelRef: run.input.model_ref, taskId: run.edit.taskId })) };
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (name === 'flow.inspect') {
|
|
159
|
+
exact(input, ['run_id']);
|
|
160
|
+
return runtime.run('inspect', ({ dataRoot }) => facts(dataRoot, readFlowRun(dataRoot, input.run_id)));
|
|
161
|
+
}
|
|
162
|
+
if (name === 'flow.continue') {
|
|
163
|
+
exact(input, ['run_id']);
|
|
164
|
+
return runtime.run('execute', ({ dataRoot, signal }) => continueRun(dataRoot, signal, input.run_id));
|
|
165
|
+
}
|
|
166
|
+
if (name === 'flow.select' || name === 'flow.export') {
|
|
167
|
+
exact(input, name === 'flow.select' ? ['run_id', 'artifact_id'] : ['run_id', 'output_path']);
|
|
168
|
+
return runtime.run('execute', ({ dataRoot }) => {
|
|
169
|
+
let run = readFlowRun(dataRoot, input.run_id);
|
|
170
|
+
facts(dataRoot, run);
|
|
171
|
+
if (!run.comparison) flowFail('IRIS_FLOW_NOT_READY', '请先完成改图与比较,再选择结果');
|
|
172
|
+
if (name === 'flow.select') {
|
|
173
|
+
const selected = [run.source, run.output].find(item => item.artifactId === input.artifact_id);
|
|
174
|
+
if (!selected) flowFail('IRIS_FLOW_SELECTION_INVALID', '只能选择本流程的原图或新版');
|
|
175
|
+
if (run.selection?.artifactId !== selected.artifactId) run = updateFlowRun(dataRoot, run, { selection: selected });
|
|
176
|
+
return facts(dataRoot, run);
|
|
177
|
+
}
|
|
178
|
+
if (!run.selection) flowFail('IRIS_FLOW_SELECTION_REQUIRED', '请先明确选择要导出的原图或新版');
|
|
179
|
+
return { ...facts(dataRoot, run), export: exportCoreArtifact(dataRoot, run.selection.artifactId, input.output_path) };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
throw new TypeError('未知的内部 Flow 命令');
|
|
183
|
+
}
|
|
184
|
+
return Object.freeze({ execute, prepareCreation, inspectCreation: (root, intent, options) => {
|
|
185
|
+
readReference(root, intent.source);
|
|
186
|
+
return facts(root, commitFlowCreation(root, intent, options));
|
|
187
|
+
} });
|
|
188
|
+
}
|