@lokvis/runtime 0.2.0-beta.0 → 0.2.0
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/LICENSE +21 -0
- package/dist/asset-store.d.ts +5 -2
- package/dist/asset-store.d.ts.map +1 -1
- package/dist/asset-store.js +93 -3
- package/dist/asset-store.js.map +1 -1
- package/dist/batch-processor.d.ts +200 -0
- package/dist/batch-processor.d.ts.map +1 -0
- package/dist/batch-processor.js +505 -0
- package/dist/batch-processor.js.map +1 -0
- package/dist/degradation.d.ts +109 -0
- package/dist/degradation.d.ts.map +1 -0
- package/dist/degradation.js +184 -0
- package/dist/degradation.js.map +1 -0
- package/dist/history-store.d.ts +76 -0
- package/dist/history-store.d.ts.map +1 -0
- package/dist/history-store.js +109 -0
- package/dist/history-store.js.map +1 -0
- package/dist/idb-asset-store.js +1 -1
- package/dist/idb-asset-store.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-guard.d.ts +109 -0
- package/dist/memory-guard.d.ts.map +1 -0
- package/dist/memory-guard.js +161 -0
- package/dist/memory-guard.js.map +1 -0
- package/dist/opfs-asset-store.d.ts +35 -4
- package/dist/opfs-asset-store.d.ts.map +1 -1
- package/dist/opfs-asset-store.js +94 -10
- package/dist/opfs-asset-store.js.map +1 -1
- package/dist/runtime.d.ts +111 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +311 -13
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +74 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-host.d.ts +13 -0
- package/dist/worker-host.d.ts.map +1 -1
- package/dist/worker-host.js +47 -15
- package/dist/worker-host.js.map +1 -1
- package/dist/worker-protocol.d.ts +10 -1
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-protocol.js.map +1 -1
- package/dist/workflow-builder.d.ts +124 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +240 -0
- package/dist/workflow-builder.js.map +1 -0
- package/package.json +3 -2
- package/src/asset-store.ts +103 -3
- package/src/batch-processor.ts +699 -0
- package/src/degradation.ts +241 -0
- package/src/history-store.ts +159 -0
- package/src/idb-asset-store.ts +1 -1
- package/src/index.ts +5 -0
- package/src/memory-guard.ts +202 -0
- package/src/opfs-asset-store.ts +118 -10
- package/src/runtime.ts +345 -9
- package/src/types.ts +70 -0
- package/src/worker-host.ts +56 -16
- package/src/worker-protocol.ts +11 -1
- package/src/workflow-builder.ts +303 -0
package/src/worker-host.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
isWorkerFatalError,
|
|
29
29
|
type WorkerRequest,
|
|
30
30
|
type WorkerPing,
|
|
31
|
+
type WorkerCancel,
|
|
31
32
|
} from './worker-protocol.js';
|
|
32
33
|
|
|
33
34
|
// ─── 传输层抽象 ─────────────────────────────────────────────────
|
|
@@ -105,6 +106,22 @@ export class WorkerHandshakeError extends Error {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* 请求被 AbortSignal 取消(W3.5 cancel 贯穿)。
|
|
111
|
+
*
|
|
112
|
+
* 与超时/崩溃不同:cancel 是调用方主动发起的,Host 会向 Worker
|
|
113
|
+
* 发送 WorkerCancel 消息让在途操作尽快中止,同时本地立即 reject,
|
|
114
|
+
* 不等 Worker 回响应。
|
|
115
|
+
*/
|
|
116
|
+
export class WorkerRequestAbortedError extends Error {
|
|
117
|
+
readonly method: string;
|
|
118
|
+
constructor(method: string) {
|
|
119
|
+
super(`Request "${method}" was aborted via AbortSignal`);
|
|
120
|
+
this.name = 'WorkerRequestAbortedError';
|
|
121
|
+
this.method = method;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
// ─── Host 选项与事件 ────────────────────────────────────────────
|
|
109
126
|
|
|
110
127
|
export interface WorkerHostOptions {
|
|
@@ -230,14 +247,8 @@ export class WorkerHost {
|
|
|
230
247
|
async dispose(): Promise<void> {
|
|
231
248
|
if (this.status === 'disposed') return;
|
|
232
249
|
this.status = 'disposed';
|
|
233
|
-
this.clearHeartbeat();
|
|
234
250
|
this.rejectAllPending(new Error('WorkerHost disposed'));
|
|
235
|
-
this.
|
|
236
|
-
this.offError?.();
|
|
237
|
-
this.offMessage = null;
|
|
238
|
-
this.offError = null;
|
|
239
|
-
this.transport?.terminate();
|
|
240
|
-
this.transport = null;
|
|
251
|
+
this.teardownTransport();
|
|
241
252
|
this.listeners.clear();
|
|
242
253
|
}
|
|
243
254
|
|
|
@@ -247,7 +258,12 @@ export class WorkerHost {
|
|
|
247
258
|
async request<T = unknown>(
|
|
248
259
|
method: string,
|
|
249
260
|
params?: unknown,
|
|
250
|
-
options?: {
|
|
261
|
+
options?: {
|
|
262
|
+
transfer?: Transferable[];
|
|
263
|
+
timeoutMs?: number;
|
|
264
|
+
/** 取消信号:触发时向 Worker 发送 WorkerCancel 并立即 reject(W3.5) */
|
|
265
|
+
signal?: AbortSignal;
|
|
266
|
+
}
|
|
251
267
|
): Promise<T> {
|
|
252
268
|
if (this.status === 'disposed') throw new Error('WorkerHost is disposed');
|
|
253
269
|
if (this.status === 'dead') throw new WorkerDeadError(this.restartCount);
|
|
@@ -255,6 +271,12 @@ export class WorkerHost {
|
|
|
255
271
|
throw new WorkerRestartingError();
|
|
256
272
|
}
|
|
257
273
|
|
|
274
|
+
const signal = options?.signal;
|
|
275
|
+
// 已取消:不发送,直接 reject
|
|
276
|
+
if (signal?.aborted) {
|
|
277
|
+
throw new WorkerRequestAbortedError(method);
|
|
278
|
+
}
|
|
279
|
+
|
|
258
280
|
const id = createRequestId();
|
|
259
281
|
const timeoutMs = options?.timeoutMs ?? this.opts.requestTimeoutMs;
|
|
260
282
|
const msg: WorkerRequest = { id, type: 'request', method, params };
|
|
@@ -262,13 +284,34 @@ export class WorkerHost {
|
|
|
262
284
|
return new Promise<T>((resolve, reject) => {
|
|
263
285
|
const timer = setTimeout(() => {
|
|
264
286
|
if (this.pending.delete(id)) {
|
|
287
|
+
offAbort();
|
|
265
288
|
reject(new WorkerRequestTimeoutError(method, timeoutMs));
|
|
266
289
|
}
|
|
267
290
|
}, timeoutMs);
|
|
268
291
|
|
|
292
|
+
// W3.5:signal 触发 → 发 WorkerCancel + 立即 reject(不等 Worker 回响应)
|
|
293
|
+
const onAbort = () => {
|
|
294
|
+
if (this.pending.delete(id)) {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
const cancelMsg: WorkerCancel = { type: 'cancel', id };
|
|
297
|
+
this.send(cancelMsg);
|
|
298
|
+
reject(new WorkerRequestAbortedError(method));
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const offAbort = signal
|
|
302
|
+
? () => signal.removeEventListener('abort', onAbort)
|
|
303
|
+
: () => {};
|
|
304
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
305
|
+
|
|
269
306
|
this.pending.set(id, {
|
|
270
|
-
resolve: (v) =>
|
|
271
|
-
|
|
307
|
+
resolve: (v) => {
|
|
308
|
+
offAbort();
|
|
309
|
+
resolve(v as T);
|
|
310
|
+
},
|
|
311
|
+
reject: (e) => {
|
|
312
|
+
offAbort();
|
|
313
|
+
reject(e);
|
|
314
|
+
},
|
|
272
315
|
timer,
|
|
273
316
|
method,
|
|
274
317
|
});
|
|
@@ -315,12 +358,9 @@ export class WorkerHost {
|
|
|
315
358
|
});
|
|
316
359
|
} catch (err) {
|
|
317
360
|
clearTimeout(readyTimer!);
|
|
318
|
-
this.
|
|
319
|
-
|
|
320
|
-
this.
|
|
321
|
-
this.offError = null;
|
|
322
|
-
transport.terminate();
|
|
323
|
-
this.transport = null;
|
|
361
|
+
// this.transport 在 spawn 顶部已赋值,teardownTransport 可安全清理
|
|
362
|
+
// (clearHeartbeat 对未启动的心跳是无害 no-op)
|
|
363
|
+
this.teardownTransport();
|
|
324
364
|
throw err as Error;
|
|
325
365
|
}
|
|
326
366
|
|
package/src/worker-protocol.ts
CHANGED
|
@@ -48,7 +48,17 @@ export interface WorkerPing {
|
|
|
48
48
|
ts: number;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* 取消一个正在执行的请求(W3.5 cancel 贯穿)。
|
|
53
|
+
* Host 在 AbortSignal 触发时发送,Worker 据此中止当前计算。
|
|
54
|
+
*/
|
|
55
|
+
export interface WorkerCancel {
|
|
56
|
+
type: 'cancel';
|
|
57
|
+
/** 要取消的请求 id */
|
|
58
|
+
id: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type WorkerMessageToWorker = WorkerRequest | WorkerPing | WorkerCancel;
|
|
52
62
|
|
|
53
63
|
// ─── Worker → Host 消息 ─────────────────────────────────────────
|
|
54
64
|
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowBuilder - 线性工作流链式构造器(W10.1)
|
|
3
|
+
*
|
|
4
|
+
* 提供链式 API 构造线性 Workflow(第一年不支持 Branch/Loop/Condition/Parallel)。
|
|
5
|
+
*
|
|
6
|
+
* 限制:
|
|
7
|
+
* - 最多 5 步(M1 MVP 约束,免费用户定位,避免复杂度过高)
|
|
8
|
+
* - 节点必须为 transform 类型(load/export 由执行器隐式处理)
|
|
9
|
+
* - 自动生成线性 edges:node[i] → node[i+1]
|
|
10
|
+
*
|
|
11
|
+
* 使用示例:
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const workflow = new WorkflowBuilder('my-workflow', 'Web 优化')
|
|
14
|
+
* .setInput({ type: 'image', multiple: true })
|
|
15
|
+
* .setOutput({ type: 'image', format: 'webp' })
|
|
16
|
+
* .add('image.resize', { width: 1920, fit: 'inside' })
|
|
17
|
+
* .add('image.compress', { format: 'webp', quality: 80 })
|
|
18
|
+
* .add('image.watermark', { text: '@lokvis' })
|
|
19
|
+
* .build();
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* 与 workflow-slice 的 buildLinearWorkflow 区别:
|
|
23
|
+
* - buildLinearWorkflow 是内部工具函数,从 WorkspaceNode[] 构造无校验
|
|
24
|
+
* - WorkflowBuilder 是公开 API,链式调用 + 5 步上限校验 + 元数据完善
|
|
25
|
+
*
|
|
26
|
+
* @module workflow-builder
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
AssetType,
|
|
31
|
+
Workflow,
|
|
32
|
+
WorkflowAuthor,
|
|
33
|
+
WorkflowCategory,
|
|
34
|
+
WorkflowEdge,
|
|
35
|
+
WorkflowInput,
|
|
36
|
+
WorkflowNode,
|
|
37
|
+
WorkflowOutput,
|
|
38
|
+
} from '@lokvis/schema';
|
|
39
|
+
|
|
40
|
+
/** 工作流最大节点数(M1 MVP 约束) */
|
|
41
|
+
export const MAX_WORKFLOW_STEPS = 5;
|
|
42
|
+
|
|
43
|
+
/** Builder 内部节点结构(构造中,与 WorkflowNode 略有差异:label 可独立设置) */
|
|
44
|
+
interface BuilderNode {
|
|
45
|
+
/** 内部生成的节点 id(builder 内部唯一) */
|
|
46
|
+
id: string;
|
|
47
|
+
capability: string;
|
|
48
|
+
params: Record<string, unknown>;
|
|
49
|
+
label?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** WorkflowBuilder 构造选项 */
|
|
53
|
+
export interface WorkflowBuilderOptions {
|
|
54
|
+
/** 工作流 ID(必填,需全局唯一) */
|
|
55
|
+
id: string;
|
|
56
|
+
/** 工作流名称 */
|
|
57
|
+
name: string;
|
|
58
|
+
/** 工作流描述(可选,默认为空字符串) */
|
|
59
|
+
description?: string;
|
|
60
|
+
/** 作者信息(可选,默认本地用户) */
|
|
61
|
+
author?: WorkflowAuthor;
|
|
62
|
+
/** 分类(可选,默认 'image') */
|
|
63
|
+
category?: WorkflowCategory;
|
|
64
|
+
/** 标签(可选) */
|
|
65
|
+
tags?: string[];
|
|
66
|
+
/** 是否为官方工作流(可选,默认 false) */
|
|
67
|
+
official?: boolean;
|
|
68
|
+
/** 最大节点数(可选,默认 MAX_WORKFLOW_STEPS;测试或特殊场景可放宽) */
|
|
69
|
+
maxSteps?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* WorkflowBuilder - 链式构造线性 Workflow。
|
|
74
|
+
*
|
|
75
|
+
* 抛错时机:
|
|
76
|
+
* - add() 超过 maxSteps 时立即抛错(避免构造完才发现步数超限)
|
|
77
|
+
* - build() 在节点为空 / 输入输出未设置时抛错
|
|
78
|
+
* - remove()/move() 在节点 id 不存在时静默(链式 API 容错)
|
|
79
|
+
*/
|
|
80
|
+
export class WorkflowBuilder {
|
|
81
|
+
private readonly options: WorkflowBuilderOptions;
|
|
82
|
+
private readonly maxSteps: number;
|
|
83
|
+
private nodes: BuilderNode[] = [];
|
|
84
|
+
private input: WorkflowInput | null = null;
|
|
85
|
+
private output: WorkflowOutput | null = null;
|
|
86
|
+
private nodeCounter = 0;
|
|
87
|
+
|
|
88
|
+
constructor(options: WorkflowBuilderOptions) {
|
|
89
|
+
if (!options.id) throw new Error('WorkflowBuilder: id is required');
|
|
90
|
+
if (!options.name) throw new Error('WorkflowBuilder: name is required');
|
|
91
|
+
this.options = options;
|
|
92
|
+
this.maxSteps = options.maxSteps ?? MAX_WORKFLOW_STEPS;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 设置工作流输入定义 */
|
|
96
|
+
setInput(input: WorkflowInput): this {
|
|
97
|
+
this.input = input;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 设置工作流输出定义 */
|
|
102
|
+
setOutput(output: WorkflowOutput): this {
|
|
103
|
+
this.output = output;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 添加一个节点到链尾。
|
|
109
|
+
*
|
|
110
|
+
* @param capability 能力名,如 `image.resize`
|
|
111
|
+
* @param params 能力参数(可选,默认空对象)
|
|
112
|
+
* @param label 节点标签(可选,UI 显示用)
|
|
113
|
+
* @throws Error 当节点数达到 maxSteps 时
|
|
114
|
+
*/
|
|
115
|
+
add(
|
|
116
|
+
capability: string,
|
|
117
|
+
params: Record<string, unknown> = {},
|
|
118
|
+
label?: string
|
|
119
|
+
): this {
|
|
120
|
+
if (!capability) throw new Error('WorkflowBuilder.add: capability is required');
|
|
121
|
+
if (this.nodes.length >= this.maxSteps) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`WorkflowBuilder.add: max steps (${this.maxSteps}) reached; ` +
|
|
124
|
+
`cannot add more nodes. Adjust maxSteps option if needed.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
this.nodeCounter += 1;
|
|
128
|
+
this.nodes.push({
|
|
129
|
+
id: `${this.options.id}-node-${this.nodeCounter}`,
|
|
130
|
+
capability,
|
|
131
|
+
params,
|
|
132
|
+
label,
|
|
133
|
+
});
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 按 capability 或节点 id 移除节点。
|
|
139
|
+
* 若有多个同 capability 节点,只移除第一个。
|
|
140
|
+
* 若不存在则静默(链式 API 容错)。
|
|
141
|
+
*
|
|
142
|
+
* @param capabilityOrId 能力名或节点 id
|
|
143
|
+
*/
|
|
144
|
+
remove(capabilityOrId: string): this {
|
|
145
|
+
const idx = this.nodes.findIndex(
|
|
146
|
+
(n) => n.id === capabilityOrId || n.capability === capabilityOrId
|
|
147
|
+
);
|
|
148
|
+
if (idx >= 0) {
|
|
149
|
+
this.nodes.splice(idx, 1);
|
|
150
|
+
}
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 移动节点到新位置(线性链中重排)。
|
|
156
|
+
*
|
|
157
|
+
* @param from 源位置(0-based 索引)
|
|
158
|
+
* @param to 目标位置(0-based 索引,移动后该节点的新位置)
|
|
159
|
+
* @throws Error 当索引越界
|
|
160
|
+
*/
|
|
161
|
+
move(from: number, to: number): this {
|
|
162
|
+
if (from < 0 || from >= this.nodes.length) {
|
|
163
|
+
throw new Error(`WorkflowBuilder.move: from index ${from} out of range`);
|
|
164
|
+
}
|
|
165
|
+
if (to < 0 || to >= this.nodes.length) {
|
|
166
|
+
throw new Error(`WorkflowBuilder.move: to index ${to} out of range`);
|
|
167
|
+
}
|
|
168
|
+
if (from === to) return this;
|
|
169
|
+
const [node] = this.nodes.splice(from, 1);
|
|
170
|
+
// splice(from, 1) 在已校验的 in-range 索引上必返回 1 元素;
|
|
171
|
+
// 此处显式检查以满足 noUncheckedIndexedAccess,并在不变量被打破时报错
|
|
172
|
+
if (!node) {
|
|
173
|
+
throw new Error(`WorkflowBuilder.move: source node at index ${from} missing`);
|
|
174
|
+
}
|
|
175
|
+
this.nodes.splice(to, 0, node);
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 交换两个节点位置 */
|
|
180
|
+
swap(i: number, j: number): this {
|
|
181
|
+
if (i < 0 || i >= this.nodes.length || j < 0 || j >= this.nodes.length) {
|
|
182
|
+
throw new Error('WorkflowBuilder.swap: index out of range');
|
|
183
|
+
}
|
|
184
|
+
if (i === j) return this;
|
|
185
|
+
const a = this.nodes[i];
|
|
186
|
+
const b = this.nodes[j];
|
|
187
|
+
// 上方范围校验保证 i / j 在界内;显式检查以满足 noUncheckedIndexedAccess
|
|
188
|
+
if (!a || !b) {
|
|
189
|
+
throw new Error('WorkflowBuilder.swap: node missing (invariant violated)');
|
|
190
|
+
}
|
|
191
|
+
this.nodes[i] = b;
|
|
192
|
+
this.nodes[j] = a;
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 更新指定节点的参数 */
|
|
197
|
+
updateParams(nodeId: string, params: Record<string, unknown>): this {
|
|
198
|
+
const node = this.nodes.find((n) => n.id === nodeId);
|
|
199
|
+
if (node) {
|
|
200
|
+
node.params = { ...node.params, ...params };
|
|
201
|
+
}
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 当前节点数 */
|
|
206
|
+
get size(): number {
|
|
207
|
+
return this.nodes.length;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 是否已达最大节点数 */
|
|
211
|
+
get isFull(): boolean {
|
|
212
|
+
return this.nodes.length >= this.maxSteps;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 获取节点的只读副本(用于 UI 预览) */
|
|
216
|
+
getNodes(): ReadonlyArray<Readonly<BuilderNode>> {
|
|
217
|
+
return this.nodes.map((n) => ({ ...n }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 构造 Workflow 实例。
|
|
222
|
+
*
|
|
223
|
+
* @throws Error 当节点为空 / 输入未设置 / 输出未设置
|
|
224
|
+
*/
|
|
225
|
+
build(): Workflow {
|
|
226
|
+
if (this.nodes.length === 0) {
|
|
227
|
+
throw new Error('WorkflowBuilder.build: no nodes added');
|
|
228
|
+
}
|
|
229
|
+
if (!this.input) {
|
|
230
|
+
throw new Error('WorkflowBuilder.build: input not set (call setInput first)');
|
|
231
|
+
}
|
|
232
|
+
if (!this.output) {
|
|
233
|
+
throw new Error('WorkflowBuilder.build: output not set (call setOutput first)');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const workflowNodes: WorkflowNode[] = this.nodes.map((n) => ({
|
|
237
|
+
id: n.id,
|
|
238
|
+
type: 'transform' as const,
|
|
239
|
+
capability: n.capability,
|
|
240
|
+
params: n.params,
|
|
241
|
+
label: n.label,
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const edges: WorkflowEdge[] = [];
|
|
245
|
+
for (let i = 0; i < workflowNodes.length - 1; i++) {
|
|
246
|
+
const fromNode = workflowNodes[i];
|
|
247
|
+
const toNode = workflowNodes[i + 1];
|
|
248
|
+
// 循环边界 i < length - 1 保证 i 与 i+1 均在界内;显式检查以满足 noUncheckedIndexedAccess
|
|
249
|
+
if (!fromNode || !toNode) {
|
|
250
|
+
throw new Error('WorkflowBuilder.build: edge node missing (invariant violated)');
|
|
251
|
+
}
|
|
252
|
+
edges.push({
|
|
253
|
+
from: fromNode.id,
|
|
254
|
+
to: toNode.id,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
return {
|
|
260
|
+
$schema: `https://lokvis.dev/schemas/workflow.json`,
|
|
261
|
+
id: this.options.id,
|
|
262
|
+
version: '1.0.0',
|
|
263
|
+
name: this.options.name,
|
|
264
|
+
description: this.options.description ?? '',
|
|
265
|
+
author: this.options.author ?? { id: 'local', name: 'Local User' },
|
|
266
|
+
category: this.options.category ?? 'image',
|
|
267
|
+
tags: this.options.tags ?? [],
|
|
268
|
+
nodes: workflowNodes,
|
|
269
|
+
edges,
|
|
270
|
+
inputs: this.input,
|
|
271
|
+
outputs: this.output,
|
|
272
|
+
official: this.options.official ?? false,
|
|
273
|
+
createdAt: now,
|
|
274
|
+
updatedAt: now,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 从 Workflow 反向构造 Builder(用于编辑已有工作流) */
|
|
280
|
+
export function workflowToBuilder(workflow: Workflow, maxSteps?: number): WorkflowBuilder {
|
|
281
|
+
const builder = new WorkflowBuilder({
|
|
282
|
+
id: workflow.id,
|
|
283
|
+
name: workflow.name,
|
|
284
|
+
description: workflow.description,
|
|
285
|
+
author: workflow.author,
|
|
286
|
+
category: workflow.category,
|
|
287
|
+
tags: workflow.tags,
|
|
288
|
+
official: workflow.official,
|
|
289
|
+
maxSteps,
|
|
290
|
+
});
|
|
291
|
+
builder.setInput(workflow.inputs);
|
|
292
|
+
builder.setOutput(workflow.outputs);
|
|
293
|
+
// 按拓扑顺序添加节点(线性链 nodes 数组即顺序)
|
|
294
|
+
for (const node of workflow.nodes) {
|
|
295
|
+
if (node.type === 'transform' && node.capability) {
|
|
296
|
+
builder.add(node.capability, node.params ?? {}, node.label);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return builder;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** AssetType 用于类型导出(便于消费方) */
|
|
303
|
+
export type { AssetType };
|