@lokvis/runtime 0.2.2 → 0.4.2
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/dist/asset-store.d.ts +13 -0
- package/dist/asset-store.d.ts.map +1 -1
- package/dist/asset-store.js +15 -2
- package/dist/asset-store.js.map +1 -1
- package/dist/batch-processor.d.ts +32 -161
- package/dist/batch-processor.d.ts.map +1 -1
- package/dist/batch-processor.js +107 -333
- package/dist/batch-processor.js.map +1 -1
- package/dist/batch-progress.d.ts +94 -0
- package/dist/batch-progress.d.ts.map +1 -0
- package/dist/batch-progress.js +128 -0
- package/dist/batch-progress.js.map +1 -0
- package/dist/batch-scheduler.d.ts +70 -0
- package/dist/batch-scheduler.d.ts.map +1 -0
- package/dist/batch-scheduler.js +147 -0
- package/dist/batch-scheduler.js.map +1 -0
- package/dist/browser-detect.d.ts +115 -0
- package/dist/browser-detect.d.ts.map +1 -0
- package/dist/browser-detect.js +160 -0
- package/dist/browser-detect.js.map +1 -0
- package/dist/concurrency-controller.d.ts +31 -0
- package/dist/concurrency-controller.d.ts.map +1 -0
- package/dist/concurrency-controller.js +33 -0
- package/dist/concurrency-controller.js.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +74 -0
- package/dist/errors.js.map +1 -0
- package/dist/executor.d.ts +9 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +102 -48
- package/dist/executor.js.map +1 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +3 -2
- package/dist/history.js.map +1 -1
- package/dist/idb-asset-store.d.ts.map +1 -1
- package/dist/idb-asset-store.js +6 -1
- package/dist/idb-asset-store.js.map +1 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -15
- package/dist/index.js.map +1 -1
- package/dist/managers/asset-manager.d.ts +114 -0
- package/dist/managers/asset-manager.d.ts.map +1 -0
- package/dist/managers/asset-manager.js +238 -0
- package/dist/managers/asset-manager.js.map +1 -0
- package/dist/managers/history-manager.d.ts +100 -0
- package/dist/managers/history-manager.d.ts.map +1 -0
- package/dist/managers/history-manager.js +296 -0
- package/dist/managers/history-manager.js.map +1 -0
- package/dist/managers/mcp-manifest-builder.d.ts +18 -0
- package/dist/managers/mcp-manifest-builder.d.ts.map +1 -0
- package/dist/managers/mcp-manifest-builder.js +99 -0
- package/dist/managers/mcp-manifest-builder.js.map +1 -0
- package/dist/managers/quota-manager.d.ts +42 -0
- package/dist/managers/quota-manager.d.ts.map +1 -0
- package/dist/managers/quota-manager.js +126 -0
- package/dist/managers/quota-manager.js.map +1 -0
- package/dist/managers/workflow-coordinator.d.ts +62 -0
- package/dist/managers/workflow-coordinator.d.ts.map +1 -0
- package/dist/managers/workflow-coordinator.js +125 -0
- package/dist/managers/workflow-coordinator.js.map +1 -0
- package/dist/opfs-asset-store.d.ts.map +1 -1
- package/dist/opfs-asset-store.js +12 -2
- package/dist/opfs-asset-store.js.map +1 -1
- package/dist/plugin-context.d.ts +35 -0
- package/dist/plugin-context.d.ts.map +1 -0
- package/dist/plugin-context.js +62 -0
- package/dist/plugin-context.js.map +1 -0
- package/dist/plugin-permissions.d.ts +99 -0
- package/dist/plugin-permissions.d.ts.map +1 -0
- package/dist/plugin-permissions.js +205 -0
- package/dist/plugin-permissions.js.map +1 -0
- package/dist/runtime-impl.d.ts +116 -0
- package/dist/runtime-impl.d.ts.map +1 -0
- package/dist/runtime-impl.js +270 -0
- package/dist/runtime-impl.js.map +1 -0
- package/dist/runtime.d.ts +10 -233
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +12 -978
- package/dist/runtime.js.map +1 -1
- package/dist/test-utils/fakes.d.ts +35 -0
- package/dist/test-utils/fakes.d.ts.map +1 -0
- package/dist/test-utils/fakes.js +60 -0
- package/dist/test-utils/fakes.js.map +1 -0
- package/dist/types.d.ts +113 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-host.d.ts.map +1 -1
- package/dist/worker-host.js +32 -9
- package/dist/worker-host.js.map +1 -1
- package/dist/worker-protocol.d.ts +30 -0
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-protocol.js +24 -0
- package/dist/worker-protocol.js.map +1 -1
- package/package.json +2 -2
- package/src/asset-store.ts +32 -2
- package/src/batch-processor.ts +133 -510
- package/src/batch-progress.ts +228 -0
- package/src/batch-scheduler.ts +209 -0
- package/src/browser-detect.ts +221 -0
- package/src/concurrency-controller.ts +52 -0
- package/src/errors.ts +82 -0
- package/src/executor.ts +122 -55
- package/src/history.ts +3 -2
- package/src/idb-asset-store.ts +7 -1
- package/src/index.ts +50 -15
- package/src/managers/asset-manager.ts +259 -0
- package/src/managers/history-manager.ts +327 -0
- package/src/managers/mcp-manifest-builder.ts +108 -0
- package/src/managers/quota-manager.ts +149 -0
- package/src/managers/workflow-coordinator.ts +158 -0
- package/src/opfs-asset-store.ts +16 -2
- package/src/plugin-context.ts +87 -0
- package/src/plugin-permissions.ts +228 -0
- package/src/runtime-impl.ts +298 -0
- package/src/runtime.ts +16 -1116
- package/src/test-utils/fakes.ts +72 -0
- package/src/types.ts +116 -0
- package/src/worker-host.ts +35 -7
- package/src/worker-protocol.ts +47 -0
- package/dist/workflow-builder.d.ts +0 -124
- package/dist/workflow-builder.d.ts.map +0 -1
- package/dist/workflow-builder.js +0 -240
- package/dist/workflow-builder.js.map +0 -1
- package/src/workflow-builder.ts +0 -303
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConcurrencyController - 批量作业并发槽位控制(W4.4 拆分)
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 提供免费/Pro 默认并发上限常量
|
|
6
|
+
* - 按 MemoryGuard 压力动态收缩并发槽位
|
|
7
|
+
*
|
|
8
|
+
* 设计原则:
|
|
9
|
+
* - 无状态:仅依赖注入的 memoryGuard
|
|
10
|
+
* - 纯函数:current() 基于 baseConcurrency + 当前 pressure 计算
|
|
11
|
+
*/
|
|
12
|
+
import type { MemoryGuard, MemoryPressure } from './memory-guard.js';
|
|
13
|
+
|
|
14
|
+
/** 默认并发槽位 */
|
|
15
|
+
export const FREE_CONCURRENCY = 4;
|
|
16
|
+
export const PRO_CONCURRENCY = 16;
|
|
17
|
+
|
|
18
|
+
/** 按 MemoryPressure 收缩并发槽位 */
|
|
19
|
+
export function shrinkConcurrencyByPressure(
|
|
20
|
+
base: number,
|
|
21
|
+
pressure: MemoryPressure
|
|
22
|
+
): number {
|
|
23
|
+
switch (pressure) {
|
|
24
|
+
case 'critical':
|
|
25
|
+
return 1;
|
|
26
|
+
case 'high':
|
|
27
|
+
return Math.max(1, Math.floor(base / 2));
|
|
28
|
+
case 'elevated':
|
|
29
|
+
return Math.max(2, Math.floor((base * 3) / 4));
|
|
30
|
+
default:
|
|
31
|
+
return base;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 并发控制器:基于 MemoryGuard 压力动态收缩槽位。
|
|
37
|
+
*
|
|
38
|
+
* 不持有 baseConcurrency(每 job 独立),current() 接收 base 并返回当前实际可用槽位。
|
|
39
|
+
*/
|
|
40
|
+
export class ConcurrencyController {
|
|
41
|
+
private readonly memoryGuard?: MemoryGuard;
|
|
42
|
+
|
|
43
|
+
constructor(opts: { memoryGuard?: MemoryGuard }) {
|
|
44
|
+
this.memoryGuard = opts.memoryGuard;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 当前并发槽位(按内存压力动态收缩) */
|
|
48
|
+
current(baseConcurrency: number): number {
|
|
49
|
+
const pressure = this.memoryGuard?.getPressure() ?? 'low';
|
|
50
|
+
return shrinkConcurrencyByPressure(baseConcurrency, pressure);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime 类型化错误类(T10:消除 SDK message 模式匹配)
|
|
3
|
+
*
|
|
4
|
+
* 这些错误类供 runtime 各模块在对应场景抛出,替代原先的 `throw new Error(...)`。
|
|
5
|
+
* SDK 的 `fromLokvisError()` 通过 `instanceof` 检测这些类,包装为对应的 SDK
|
|
6
|
+
* 错误类(LokvisError 子类),无需再依赖 message 字符串模式匹配。
|
|
7
|
+
*
|
|
8
|
+
* 命名与 SDK 错误类对应(如 runtime `AssetNotFoundError` → SDK `AssetNotFoundError`),
|
|
9
|
+
* 但 runtime 侧继承 `Error`(不依赖 SDK 的 `LokvisError`),符合五层架构
|
|
10
|
+
* (Runtime 不依赖 SDK)。SDK import 时加 `Runtime` 前缀别名避免命名冲突
|
|
11
|
+
* (与 QuotaExceededError / Worker*Error 等现有模式一致)。
|
|
12
|
+
*
|
|
13
|
+
* super message 与原 `throw new Error(...)` 的 message 完全一致,保证:
|
|
14
|
+
* - 日志/堆栈输出不变
|
|
15
|
+
* - 依赖 message 文案的测试不破
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 资产未找到(assetStore.get 返回空) */
|
|
19
|
+
export class AssetNotFoundError extends Error {
|
|
20
|
+
readonly assetId: string;
|
|
21
|
+
constructor(assetId: string) {
|
|
22
|
+
super(`Asset not found: ${assetId}`);
|
|
23
|
+
this.name = 'AssetNotFoundError';
|
|
24
|
+
this.assetId = assetId;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 资产 Blob 未找到(getBlob 在 store 中找不到对应 blob) */
|
|
29
|
+
export class AssetBlobNotFoundError extends Error {
|
|
30
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.name = 'AssetBlobNotFoundError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 工作流结构校验失败(节点 ID 重复 / edge 引用未知节点 / 自环等) */
|
|
37
|
+
export class WorkflowInvalidError extends Error {
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'WorkflowInvalidError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 工作流包含环(拓扑排序后 sorted.length !== nodes.length) */
|
|
45
|
+
export class WorkflowCycleError extends Error {
|
|
46
|
+
constructor(message: string) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'WorkflowCycleError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 工作流节点执行失败(transform 节点缺 capability 字段) */
|
|
53
|
+
export class WorkflowNodeError extends Error {
|
|
54
|
+
readonly nodeId: string;
|
|
55
|
+
constructor(nodeId: string, message: string) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'WorkflowNodeError';
|
|
58
|
+
this.nodeId = nodeId;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 能力未注册(capabilityRegistry.resolve 返回空且非 stub-only) */
|
|
63
|
+
export class CapabilityNotRegisteredError extends Error {
|
|
64
|
+
readonly capability: string;
|
|
65
|
+
constructor(capability: string) {
|
|
66
|
+
super(`No implementation registered for capability "${capability}"`);
|
|
67
|
+
this.name = 'CapabilityNotRegisteredError';
|
|
68
|
+
this.capability = capability;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 能力仅有 stub 实现(需安装真实 engine 插件) */
|
|
73
|
+
export class CapabilityStubOnlyError extends Error {
|
|
74
|
+
readonly capability: string;
|
|
75
|
+
constructor(capability: string) {
|
|
76
|
+
super(
|
|
77
|
+
`Capability "${capability}" is not yet available (only stub engine registered). Install a real engine plugin to use this capability.`
|
|
78
|
+
);
|
|
79
|
+
this.name = 'CapabilityStubOnlyError';
|
|
80
|
+
this.capability = capability;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/executor.ts
CHANGED
|
@@ -17,6 +17,14 @@ import type {
|
|
|
17
17
|
import type { EventBus } from '@lokvis/schema';
|
|
18
18
|
import type { AssetStore } from './asset-store.js';
|
|
19
19
|
import type { CapabilityRegistry } from './capability-registry.js';
|
|
20
|
+
import {
|
|
21
|
+
AssetNotFoundError,
|
|
22
|
+
WorkflowInvalidError,
|
|
23
|
+
WorkflowCycleError,
|
|
24
|
+
WorkflowNodeError,
|
|
25
|
+
CapabilityNotRegisteredError,
|
|
26
|
+
CapabilityStubOnlyError,
|
|
27
|
+
} from './errors.js';
|
|
20
28
|
|
|
21
29
|
/** 执行中的工作流状态 */
|
|
22
30
|
interface RunningWorkflow {
|
|
@@ -75,7 +83,7 @@ function topologicalSort(nodes: WorkflowNode[], edges: WorkflowEdge[]): Workflow
|
|
|
75
83
|
const seen = new Set<string>();
|
|
76
84
|
for (const n of nodes) {
|
|
77
85
|
if (seen.has(n.id)) {
|
|
78
|
-
throw new
|
|
86
|
+
throw new WorkflowInvalidError(`Workflow contains duplicate node id: ${n.id}`);
|
|
79
87
|
}
|
|
80
88
|
seen.add(n.id);
|
|
81
89
|
}
|
|
@@ -90,7 +98,7 @@ function topologicalSort(nodes: WorkflowNode[], edges: WorkflowEdge[]): Workflow
|
|
|
90
98
|
// 被静默吞掉,进而导致下游节点入度无法归零 → 误判为环)
|
|
91
99
|
for (const edge of edges) {
|
|
92
100
|
if (!nodeMap.has(edge.from)) {
|
|
93
|
-
throw new
|
|
101
|
+
throw new WorkflowInvalidError(
|
|
94
102
|
`Workflow edge references unknown source node: "${edge.from}". ` +
|
|
95
103
|
`Edge endpoints must reference existing nodes; ` +
|
|
96
104
|
`reserved sentinel nodes (e.g. "__input__") are not supported ` +
|
|
@@ -99,13 +107,13 @@ function topologicalSort(nodes: WorkflowNode[], edges: WorkflowEdge[]): Workflow
|
|
|
99
107
|
);
|
|
100
108
|
}
|
|
101
109
|
if (!nodeMap.has(edge.to)) {
|
|
102
|
-
throw new
|
|
110
|
+
throw new WorkflowInvalidError(
|
|
103
111
|
`Workflow edge references unknown target node: "${edge.to}". ` +
|
|
104
112
|
`Edge endpoints must reference existing nodes.`
|
|
105
113
|
);
|
|
106
114
|
}
|
|
107
115
|
if (edge.from === edge.to) {
|
|
108
|
-
throw new
|
|
116
|
+
throw new WorkflowInvalidError(
|
|
109
117
|
`Workflow contains self-loop on node: "${edge.from}". ` +
|
|
110
118
|
`Self-loops create cycles and are not allowed.`
|
|
111
119
|
);
|
|
@@ -133,7 +141,7 @@ function topologicalSort(nodes: WorkflowNode[], edges: WorkflowEdge[]): Workflow
|
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
if (sorted.length !== nodes.length) {
|
|
136
|
-
throw new
|
|
144
|
+
throw new WorkflowCycleError('Workflow contains a cycle, cannot execute');
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
return sorted;
|
|
@@ -167,7 +175,7 @@ export class WorkflowExecutor {
|
|
|
167
175
|
currentAssets = await Promise.all(
|
|
168
176
|
ids.map(async (id) => {
|
|
169
177
|
const asset = await this.config.assetStore.get(id);
|
|
170
|
-
if (!asset) throw new
|
|
178
|
+
if (!asset) throw new AssetNotFoundError(id);
|
|
171
179
|
return asset;
|
|
172
180
|
})
|
|
173
181
|
);
|
|
@@ -198,65 +206,109 @@ export class WorkflowExecutor {
|
|
|
198
206
|
// 过滤掉 load 和 export 节点(load 由输入处理,export 由结果处理)
|
|
199
207
|
const transformNodes = sortedNodes.filter((n) => n.type === 'transform');
|
|
200
208
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
// E1: 多 target 机制
|
|
210
|
+
// targets 存在且非空时,为每个 target 独立执行一次 transform 链,
|
|
211
|
+
// target.params 浅合并到每个节点的 params(target 优先)。
|
|
212
|
+
// 无 targets 时等价于单次执行(targetParams = undefined)。
|
|
213
|
+
const targets = workflow.outputs.targets;
|
|
214
|
+
const targetParamSets: (Record<string, unknown> | undefined)[] =
|
|
215
|
+
targets && targets.length > 0
|
|
216
|
+
? targets.map((t) => t.params)
|
|
217
|
+
: [undefined];
|
|
218
|
+
|
|
219
|
+
// 保存原始输入资产,每个 target 执行前重置
|
|
220
|
+
const inputAssets = [...currentAssets];
|
|
221
|
+
const allOutputs: Asset[] = [];
|
|
222
|
+
/**
|
|
223
|
+
* 各 transform 节点的输出 AssetId(按 node.id 索引)。
|
|
224
|
+
* 仅单 target 场景填充(多 target 同一节点会有多份输出,
|
|
225
|
+
* 索引歧义,留空让调用方走事件总线监听 node:finished)。
|
|
226
|
+
*/
|
|
227
|
+
const stepOutputs: Record<string, AssetId[]> = {};
|
|
228
|
+
const isSingleTarget = targetParamSets.length === 1;
|
|
229
|
+
|
|
230
|
+
for (const targetParams of targetParamSets) {
|
|
231
|
+
// 每个 target 开始前重置为原始输入
|
|
232
|
+
currentAssets = [...inputAssets];
|
|
233
|
+
|
|
234
|
+
// 依次执行每个 transform 节点
|
|
235
|
+
for (const node of transformNodes) {
|
|
236
|
+
// 暂停则挂起,等待 resume/cancel 唤醒(Promise resolver,无轮询)
|
|
237
|
+
if (state.status === 'paused') {
|
|
238
|
+
await this.waitForResume(workflowId);
|
|
239
|
+
}
|
|
240
|
+
// 唤醒后或每轮起始,若已被取消立即跳出(避免执行多余节点)
|
|
241
|
+
if (state.status === 'cancelled') {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
211
244
|
|
|
212
|
-
|
|
213
|
-
|
|
245
|
+
state.currentNodeId = node.id;
|
|
246
|
+
const nodeStart = Date.now();
|
|
214
247
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
248
|
+
this.config.eventBus.emit({
|
|
249
|
+
type: 'node:started',
|
|
250
|
+
workflowId,
|
|
251
|
+
nodeId: node.id,
|
|
252
|
+
inputs: currentAssets,
|
|
253
|
+
});
|
|
221
254
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
255
|
+
// 解析能力实现(transform 节点必须有 capability)
|
|
256
|
+
const capability = node.capability;
|
|
257
|
+
if (!capability) {
|
|
258
|
+
throw new WorkflowNodeError(node.id, `Transform node "${node.id}" has no capability`);
|
|
259
|
+
}
|
|
260
|
+
const impl = this.config.capabilityRegistry.resolve(capability);
|
|
261
|
+
if (!impl) {
|
|
262
|
+
if (this.config.capabilityRegistry.isStubOnly(capability)) {
|
|
263
|
+
throw new CapabilityStubOnlyError(capability);
|
|
264
|
+
}
|
|
265
|
+
throw new CapabilityNotRegisteredError(capability);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// E1: target params 覆盖 node params(浅合并,target 优先)
|
|
269
|
+
// 注:展开 undefined 是 no-op,无需 ?? {} fallback(oxlint no-empty-fallback-in-spread)
|
|
270
|
+
const mergedParams = targetParams
|
|
271
|
+
? { ...node.params, ...targetParams }
|
|
272
|
+
: (node.params ?? {});
|
|
273
|
+
|
|
274
|
+
// 执行能力
|
|
275
|
+
const ctx = createExecutionContext(
|
|
276
|
+
workflowId,
|
|
277
|
+
node.id,
|
|
278
|
+
abortController.signal
|
|
279
|
+
);
|
|
280
|
+
const outputs = await impl.execute(currentAssets, mergedParams, ctx);
|
|
281
|
+
currentAssets = outputs;
|
|
282
|
+
|
|
283
|
+
// 单 target 时记录该节点输出,供调用方读取中间结果
|
|
284
|
+
// (多 target 场景跳过,避免 last-write-wins 语义歧义)
|
|
285
|
+
if (isSingleTarget) {
|
|
286
|
+
stepOutputs[node.id] = currentAssets.map((a) => a.id);
|
|
233
287
|
}
|
|
234
|
-
|
|
288
|
+
|
|
289
|
+
this.config.eventBus.emit({
|
|
290
|
+
type: 'node:finished',
|
|
291
|
+
workflowId,
|
|
292
|
+
nodeId: node.id,
|
|
293
|
+
capability,
|
|
294
|
+
params: mergedParams,
|
|
295
|
+
outputs: currentAssets,
|
|
296
|
+
duration: Date.now() - nodeStart,
|
|
297
|
+
});
|
|
235
298
|
}
|
|
236
299
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
abortController.signal
|
|
242
|
-
);
|
|
243
|
-
const outputs = await impl.execute(currentAssets, node.params ?? {}, ctx);
|
|
244
|
-
currentAssets = outputs;
|
|
245
|
-
|
|
246
|
-
this.config.eventBus.emit({
|
|
247
|
-
type: 'node:finished',
|
|
248
|
-
workflowId,
|
|
249
|
-
nodeId: node.id,
|
|
250
|
-
capability,
|
|
251
|
-
params: node.params ?? {},
|
|
252
|
-
outputs: currentAssets,
|
|
253
|
-
duration: Date.now() - nodeStart,
|
|
254
|
-
});
|
|
300
|
+
// 收集当前 target 的输出(cancelled 时也保留已完成的节点输出)
|
|
301
|
+
allOutputs.push(...currentAssets);
|
|
302
|
+
|
|
303
|
+
if (state.status === 'cancelled') break;
|
|
255
304
|
}
|
|
256
305
|
|
|
257
306
|
const result: WorkflowResult = {
|
|
258
307
|
workflowId,
|
|
259
|
-
outputs:
|
|
308
|
+
outputs: allOutputs.map((a) => a.id),
|
|
309
|
+
// 单 target 且非取消时暴露中间步骤输出;其他场景保持 undefined,
|
|
310
|
+
// 调用方需要中间结果时应改用 eventBus 监听 node:finished 事件
|
|
311
|
+
stepOutputs: isSingleTarget && state.status !== 'cancelled' ? stepOutputs : undefined,
|
|
260
312
|
duration: Date.now() - startTime,
|
|
261
313
|
status: state.status === 'cancelled' ? 'cancelled' : 'completed',
|
|
262
314
|
};
|
|
@@ -308,6 +360,21 @@ export class WorkflowExecutor {
|
|
|
308
360
|
this.config.eventBus.emit({ type: 'workflow:cancelled', workflowId });
|
|
309
361
|
}
|
|
310
362
|
|
|
363
|
+
/**
|
|
364
|
+
* 取消所有运行中的工作流(W21.6 runtime.dispose 用)。
|
|
365
|
+
*
|
|
366
|
+
* 遍历 running Map,对每个 state 执行 cancel 的同步部分
|
|
367
|
+
* (status = cancelled + abort + 唤醒 resume resolver + emit event)。
|
|
368
|
+
* 不等待 executor.execute 内部循环跳出 —— abort 后循环在下个 await
|
|
369
|
+
* 点自然抛 AbortError,资源随 Promise reject 释放。
|
|
370
|
+
*/
|
|
371
|
+
cancelAll(): void {
|
|
372
|
+
// 复制一份避免 cancel 内 emit 触发的 listener 回调 mutate 原集合
|
|
373
|
+
for (const workflowId of [...this.running.keys()]) {
|
|
374
|
+
void this.cancel(workflowId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
311
378
|
/** 暂停执行 */
|
|
312
379
|
async pause(workflowId: string): Promise<void> {
|
|
313
380
|
const state = this.running.get(workflowId);
|
package/src/history.ts
CHANGED
|
@@ -189,8 +189,9 @@ export class HistoryStack {
|
|
|
189
189
|
for (const entry of entries) {
|
|
190
190
|
try {
|
|
191
191
|
onEvict(entry);
|
|
192
|
-
} catch {
|
|
193
|
-
// onEvict
|
|
192
|
+
} catch (err) {
|
|
193
|
+
// onEvict 失败不应阻断历史操作,但需记录便于调试
|
|
194
|
+
console.warn(`[lokvis] history onEvict threw for entry ${entry.id}:`, err);
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
}
|
package/src/idb-asset-store.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
parseBlobPath,
|
|
17
17
|
prepareImport,
|
|
18
18
|
} from './asset-store.js';
|
|
19
|
+
import { AssetBlobNotFoundError } from './errors.js';
|
|
19
20
|
|
|
20
21
|
/** IndexedDB 不可用时抛出 */
|
|
21
22
|
export class IdbUnavailableError extends Error {
|
|
@@ -97,7 +98,7 @@ export async function createIdbAssetStore(
|
|
|
97
98
|
const id = parseBlobPath(handle.path, IDB_PATH_PREFIX);
|
|
98
99
|
const record = await db.assets.get(id);
|
|
99
100
|
if (!record) {
|
|
100
|
-
throw new
|
|
101
|
+
throw new AssetBlobNotFoundError(`Blob not found in IndexedDB for path: ${handle.path}`);
|
|
101
102
|
}
|
|
102
103
|
return record.blob;
|
|
103
104
|
},
|
|
@@ -117,5 +118,10 @@ export async function createIdbAssetStore(
|
|
|
117
118
|
await db.assets.put({ id, asset, blob });
|
|
118
119
|
return asset;
|
|
119
120
|
},
|
|
121
|
+
|
|
122
|
+
// W21.6: 关闭 Dexie 连接(IDB 数据不删除,下次创建 store 时可恢复)。
|
|
123
|
+
async dispose() {
|
|
124
|
+
db.close();
|
|
125
|
+
},
|
|
120
126
|
};
|
|
121
127
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,23 +3,58 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lokvis Runtime - Browser-local workflow execution engine.
|
|
5
5
|
* 所有核心工作流在浏览器执行,Cloudflare 只承担边缘服务。
|
|
6
|
+
*
|
|
7
|
+
* 公共 API 收敛策略(O-3):
|
|
8
|
+
* - 4 个模块全量透传:types / errors / browser-detect / runtime(仅 4 个符号)
|
|
9
|
+
* - 7 个模块仅命名导出被外部消费的符号(AssetStore / 错误类 / BatchProcessor 等)
|
|
10
|
+
* - 8 个纯内部模块(event-bus / capability-registry / executor / worker-protocol /
|
|
11
|
+
* history / memory-guard / plugin-permissions / plugin-context)不从公共入口透传,
|
|
12
|
+
* runtime 内部代码与测试通过相对路径 import。
|
|
6
13
|
*/
|
|
7
14
|
|
|
15
|
+
// ─── 全量透传(公共 API) ──────────────────────────────────────────
|
|
8
16
|
export * from './types.js';
|
|
9
|
-
export * from './
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './opfs-asset-store.js';
|
|
12
|
-
export * from './idb-asset-store.js';
|
|
13
|
-
export * from './capability-registry.js';
|
|
14
|
-
export * from './executor.js';
|
|
15
|
-
export * from './worker-protocol.js';
|
|
16
|
-
export * from './worker-host.js';
|
|
17
|
-
export * from './history.js';
|
|
18
|
-
export * from './history-store.js';
|
|
19
|
-
export * from './memory-guard.js';
|
|
20
|
-
export * from './degradation.js';
|
|
21
|
-
export * from './batch-processor.js';
|
|
22
|
-
export * from './workflow-builder.js';
|
|
17
|
+
export * from './errors.js';
|
|
18
|
+
export * from './browser-detect.js';
|
|
23
19
|
export * from './runtime.js';
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
// ─── 命名导出(仅被外部消费的符号) ────────────────────────────────
|
|
22
|
+
|
|
23
|
+
// asset-store:mcp-server 的 NodeAssetStore 实现此接口
|
|
24
|
+
export type { AssetStore } from './asset-store.js';
|
|
25
|
+
|
|
26
|
+
// opfs-asset-store:仅错误类被 SDK errors 引用
|
|
27
|
+
export { OpfsUnavailableError } from './opfs-asset-store.js';
|
|
28
|
+
|
|
29
|
+
// idb-asset-store:仅错误类被 SDK errors 引用
|
|
30
|
+
export { IdbUnavailableError } from './idb-asset-store.js';
|
|
31
|
+
|
|
32
|
+
// degradation:仅错误类被 SDK errors 引用
|
|
33
|
+
export { DegradationRejectedError } from './degradation.js';
|
|
34
|
+
|
|
35
|
+
// worker-host:6 个错误类被 SDK errors 引用
|
|
36
|
+
export {
|
|
37
|
+
WorkerCrashedError,
|
|
38
|
+
WorkerRestartingError,
|
|
39
|
+
WorkerDeadError,
|
|
40
|
+
WorkerRequestTimeoutError,
|
|
41
|
+
WorkerHandshakeError,
|
|
42
|
+
WorkerRequestAbortedError,
|
|
43
|
+
} from './worker-host.js';
|
|
44
|
+
|
|
45
|
+
// batch-processor:BatchProcessor 类型 + 错误类 + 常量被 SDK 引用
|
|
46
|
+
export {
|
|
47
|
+
BatchProcessor,
|
|
48
|
+
BatchLimitExceededError,
|
|
49
|
+
FREE_BATCH_LIMIT,
|
|
50
|
+
} from './batch-processor.js';
|
|
51
|
+
export type {
|
|
52
|
+
BatchJob,
|
|
53
|
+
BatchItem,
|
|
54
|
+
BatchItemInput,
|
|
55
|
+
EnqueueOptions,
|
|
56
|
+
BatchProgress,
|
|
57
|
+
} from './batch-processor.js';
|
|
58
|
+
|
|
59
|
+
// history-store:HistoryStore / HistoryStoreOptions 是 RuntimeConfig 字段类型契约
|
|
60
|
+
export type { HistoryStore, HistoryStoreOptions } from './history-store.js';
|