@lokvis/runtime 0.2.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +87 -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 +106 -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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test fakes(W2.1 从 opfs-asset-store.test.ts /
|
|
3
|
+
* opfs-metadata-persistence.test.ts 去重提取)
|
|
4
|
+
*
|
|
5
|
+
* 提供内存模拟的 OPFS FileSystemFileHandle / FileSystemDirectoryHandle,
|
|
6
|
+
* 供 OpfsAssetStore 单测与 OPFS 元数据持久化测试共用。
|
|
7
|
+
*
|
|
8
|
+
* 行为契约:
|
|
9
|
+
* - FakeFileHandle.createWritable() 返回 write/close,write 把 blob 存入实例
|
|
10
|
+
* - FakeFileHandle.getFile() 返回最近写入的 blob;未写入时抛 "File not found"
|
|
11
|
+
* - FakeDirHandle 支持 getDirectoryHandle / getFileHandle / removeEntry,
|
|
12
|
+
* create:true 时自动创建,否则不存在抛错
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 内存模拟的 OPFS 文件句柄(FileSystemFileHandle 子集) */
|
|
16
|
+
export class FakeFileHandle {
|
|
17
|
+
private blob: Blob | null = null;
|
|
18
|
+
|
|
19
|
+
async createWritable(): Promise<{
|
|
20
|
+
write: (data: Blob) => Promise<void>;
|
|
21
|
+
close: () => Promise<void>;
|
|
22
|
+
}> {
|
|
23
|
+
return {
|
|
24
|
+
write: async (data) => {
|
|
25
|
+
this.blob = data;
|
|
26
|
+
},
|
|
27
|
+
close: async () => {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getFile(): Promise<Blob> {
|
|
32
|
+
if (!this.blob) throw new Error('File not found');
|
|
33
|
+
return this.blob;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 内存模拟的 OPFS 目录句柄(FileSystemDirectoryHandle 子集) */
|
|
38
|
+
export class FakeDirHandle {
|
|
39
|
+
private files = new Map<string, FakeFileHandle>();
|
|
40
|
+
private dirs = new Map<string, FakeDirHandle>();
|
|
41
|
+
|
|
42
|
+
async getDirectoryHandle(
|
|
43
|
+
name: string,
|
|
44
|
+
opts?: { create?: boolean }
|
|
45
|
+
): Promise<FakeDirHandle> {
|
|
46
|
+
let d = this.dirs.get(name);
|
|
47
|
+
if (!d) {
|
|
48
|
+
if (!opts?.create) throw new Error(`Directory not found: ${name}`);
|
|
49
|
+
d = new FakeDirHandle();
|
|
50
|
+
this.dirs.set(name, d);
|
|
51
|
+
}
|
|
52
|
+
return d;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getFileHandle(
|
|
56
|
+
name: string,
|
|
57
|
+
opts?: { create?: boolean }
|
|
58
|
+
): Promise<FakeFileHandle> {
|
|
59
|
+
let f = this.files.get(name);
|
|
60
|
+
if (!f) {
|
|
61
|
+
if (!opts?.create) throw new Error(`File not found: ${name}`);
|
|
62
|
+
f = new FakeFileHandle();
|
|
63
|
+
this.files.set(name, f);
|
|
64
|
+
}
|
|
65
|
+
return f;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async removeEntry(name: string): Promise<void> {
|
|
69
|
+
this.files.delete(name);
|
|
70
|
+
this.dirs.delete(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -13,7 +13,9 @@ import type {
|
|
|
13
13
|
EngineSelectionStrategy,
|
|
14
14
|
ExifData,
|
|
15
15
|
HistoryEntry,
|
|
16
|
+
ImageMetadata,
|
|
16
17
|
McpManifest,
|
|
18
|
+
PdfInfo,
|
|
17
19
|
PluginConfig,
|
|
18
20
|
PluginInstaller,
|
|
19
21
|
} from '@lokvis/schema';
|
|
@@ -77,8 +79,23 @@ export interface RuntimeConfig {
|
|
|
77
79
|
* - false(默认):批量上限 10 文件、并发 4、workflow 槽位 5
|
|
78
80
|
* - true:批量无上限、并发 16、workflow 槽位无限
|
|
79
81
|
* 由 cloud 侧 createLokvis({ auth }) 注入 session 后置为 true。
|
|
82
|
+
*
|
|
83
|
+
* 注:`isPro` 仅控制本地批量/槽位门控(四环)。AI 调用配额由
|
|
84
|
+
* `@lokvis/cloud-bridge` 的 `CloudBilling` 据 `plan` 精确判定
|
|
85
|
+
* ('pro' 解锁四环但 AI 配额为 0,需 'cloud_pro' 才有 AI 配额)。
|
|
80
86
|
*/
|
|
81
87
|
isPro?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* 用户订阅计划(G1)。比 `isPro` 更细粒度,用于 AI 调用计费判定。
|
|
90
|
+
* - 'free'(默认):免费用户,isPro=false
|
|
91
|
+
* - 'pro':Pro 订阅($9/月),isPro=true(解锁四环),AI 配额=0
|
|
92
|
+
* - 'cloud_pro':Cloud Pro 订阅,isPro=true,AI 配额=10/天
|
|
93
|
+
* - 'enterprise':企业版,isPro=true,AI 配额=∞
|
|
94
|
+
*
|
|
95
|
+
* 由 SDK 据 `auth.plan` 注入;未传时默认 'free'。
|
|
96
|
+
* `isPro` 派生自 `plan !== 'free'`(在 SDK resolvePlan 中计算)。
|
|
97
|
+
*/
|
|
98
|
+
plan?: Plan;
|
|
82
99
|
/**
|
|
83
100
|
* 内存预算(字节,W3.3 MemoryGuard)。
|
|
84
101
|
* 默认 512MB。BatchProcessor 据此在内存压力高时收缩并发槽位。
|
|
@@ -86,9 +103,41 @@ export interface RuntimeConfig {
|
|
|
86
103
|
memoryBudget?: number;
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Runtime 内部构造参数(W21.6)。
|
|
108
|
+
*
|
|
109
|
+
* `ownsAssetStore` 不出现在公共 `RuntimeConfig` 上,仅供 `createRuntime` 工厂
|
|
110
|
+
* 内部向 `LokvisRuntimeImpl` 传递"assetStore 是否由工厂创建"的标记 —— 工厂
|
|
111
|
+
* 创建的 store 由 Runtime 拥有,dispose() 时负责调用 assetStore.dispose?.();
|
|
112
|
+
* 注入路径由消费方自行管理生命周期。
|
|
113
|
+
*
|
|
114
|
+
* 类型层面 SDK 用户传不进此字段;`LokvisRuntimeImpl` 构造函数另有运行时
|
|
115
|
+
* 守卫,即使 JS 用户绕过类型系统传 `assetStore + ownsAssetStore:true`,
|
|
116
|
+
* 仍会被强制为 false(防止越权清理注入的 store)。
|
|
117
|
+
*/
|
|
118
|
+
export interface InternalRuntimeInit {
|
|
119
|
+
/** assetStore 是否由 Runtime 拥有(工厂创建路径)。注入路径强制为 false。 */
|
|
120
|
+
ownsAssetStore?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
/** Runtime 状态 */
|
|
90
124
|
export type RuntimeStatus = 'idle' | 'running' | 'paused' | 'error';
|
|
91
125
|
|
|
126
|
+
/**
|
|
127
|
+
* 用户订阅计划(G1)。
|
|
128
|
+
*
|
|
129
|
+
* 与 `@lokvis/cloud-bridge` 的 `AuthenticatedUser.plan` 对齐,但收窄为字面量联合
|
|
130
|
+
* (cloud-bridge 保留 `string` 以兼容未来新增 plan 而无需发版)。
|
|
131
|
+
*
|
|
132
|
+
* - `free`:免费用户,本地工具无限制,cloud AI 不可用
|
|
133
|
+
* - `pro`:Pro 订阅($9/月),解锁本地四环门控,cloud AI 仍不可用(配额=0)
|
|
134
|
+
* - `cloud_pro`:Cloud Pro 订阅,解锁四环 + cloud AI 配额(10/天)
|
|
135
|
+
* - `enterprise`:企业版,无任何限制
|
|
136
|
+
*
|
|
137
|
+
* `isPro` 派生自 `plan !== 'free'`。AI 调用配额由 `CloudBilling.planQuotas` 判定。
|
|
138
|
+
*/
|
|
139
|
+
export type Plan = 'free' | 'pro' | 'cloud_pro' | 'enterprise';
|
|
140
|
+
|
|
92
141
|
/**
|
|
93
142
|
* `toMcpManifest()` 选项。
|
|
94
143
|
*
|
|
@@ -120,6 +169,11 @@ export interface LokvisRuntime {
|
|
|
120
169
|
readonly eventBus: EventBus;
|
|
121
170
|
/** 是否为 Pro 模式(影响批量上限/并发槽位/workflow 数,W6.2) */
|
|
122
171
|
readonly isPro: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* 用户订阅计划(G1)。比 `isPro` 更细粒度,用于 AI 调用计费判定。
|
|
174
|
+
* `isPro === (plan !== 'free')`,二者保持一致。
|
|
175
|
+
*/
|
|
176
|
+
readonly plan: Plan;
|
|
123
177
|
/** 批量处理器(W6.1:并发控制 + 进度 + 失败重试) */
|
|
124
178
|
readonly batch: BatchProcessor;
|
|
125
179
|
|
|
@@ -148,6 +202,25 @@ export interface LokvisRuntime {
|
|
|
148
202
|
* 与 AssetStore 中孤儿资产泄漏。ui-react 应在 Workspace 卸载时调用。
|
|
149
203
|
*/
|
|
150
204
|
disposeWorkflow(workflowId: string): Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* 销毁整个 Runtime:取消所有运行中 workflow + 批处理任务,
|
|
207
|
+
* 清空所有历史栈(触发 outputs 资产回收),清理 eventBus 订阅。
|
|
208
|
+
*
|
|
209
|
+
* W21.6: 修复长会话 / SPA 卸载场景的资源泄漏。在以下时机调用:
|
|
210
|
+
* - SPA 整体卸载(window beforeunload 或 React root unmount)
|
|
211
|
+
* - 测试 afterEach 清理
|
|
212
|
+
* - 消费方明确知道不再使用此 runtime 实例时
|
|
213
|
+
*
|
|
214
|
+
* AssetStore 的清理策略:
|
|
215
|
+
* - 若 Runtime 通过 createRuntime 工厂创建 store(默认路径):dispose()
|
|
216
|
+
* 会调用 assetStore.dispose?.() 关闭 Dexie 连接 / 清空内存 Map
|
|
217
|
+
* - 若消费方注入 store(config.assetStore):Runtime 不清理,由消费方
|
|
218
|
+
* 在合适的时机调用 store.dispose?.()
|
|
219
|
+
*
|
|
220
|
+
* 调用 dispose() 后再调 run()/cancel() 等方法会抛 'Runtime is disposed'。
|
|
221
|
+
* 幂等:重复调用为 no-op。
|
|
222
|
+
*/
|
|
223
|
+
dispose(): Promise<void>;
|
|
151
224
|
|
|
152
225
|
// ─── 历史与撤销 ──────────────────────────────────────
|
|
153
226
|
/** 获取工作流的执行历史 */
|
|
@@ -192,6 +265,41 @@ export interface LokvisRuntime {
|
|
|
192
265
|
* @returns ExifData;非 image / 无 EXIF / 解析失败 / reader 未注册返回 null
|
|
193
266
|
*/
|
|
194
267
|
readAssetExif(id: AssetId): Promise<ExifData | null>;
|
|
268
|
+
/**
|
|
269
|
+
* 读取 image 资产的 dimensions/format 元数据。
|
|
270
|
+
*
|
|
271
|
+
* 走 MetadataReader 机制(与 readAssetExif 同一设计):Runtime 持有
|
|
272
|
+
* plugin-image 通过 `ctx.registerMetadataReader('image.read-metadata', fn)`
|
|
273
|
+
* 注册的 reader 引用,按名调用。reader 内部调 engine-image/node 的
|
|
274
|
+
* getMetadata(sharp .metadata())。
|
|
275
|
+
*
|
|
276
|
+
* 用途:mcp-server 在 image tool 处理完成后,读取输出 Blob 的精确尺寸
|
|
277
|
+
* 用于结果文本报告。Plugin 未安装时优雅降级返回 null。
|
|
278
|
+
*
|
|
279
|
+
* 架构意义:使 mcp-server 不再直接 import @lokvis/engine-image(违反
|
|
280
|
+
* 五层架构单向依赖),改为通过 Runtime 间接调用(见 A1 修复)。
|
|
281
|
+
*
|
|
282
|
+
* @param id 资产 ID(须为 image 类型)
|
|
283
|
+
* @returns ImageMetadata;非 image / reader 未注册 / 解析失败返回 null
|
|
284
|
+
*/
|
|
285
|
+
readAssetImageMetadata(id: AssetId): Promise<ImageMetadata | null>;
|
|
286
|
+
/**
|
|
287
|
+
* 读取 pdf 资产的页数。
|
|
288
|
+
*
|
|
289
|
+
* 走 MetadataReader 机制:Runtime 持有 plugin-pdf 通过
|
|
290
|
+
* `ctx.registerMetadataReader('pdf.read-info', fn)` 注册的 reader 引用,
|
|
291
|
+
* 按名调用。reader 内部调 engine-pdf 的 getPdfInfo(pdf-lib getPageCount)。
|
|
292
|
+
*
|
|
293
|
+
* 用途:mcp-server 在 pdf tool 处理完成后,读取输出 Blob 的页数用于结果
|
|
294
|
+
* 文本报告。Plugin 未安装时优雅降级返回 null。
|
|
295
|
+
*
|
|
296
|
+
* 架构意义:使 mcp-server 不再直接 import @lokvis/engine-pdf(违反
|
|
297
|
+
* 五层架构单向依赖),改为通过 Runtime 间接调用(见 A1 修复)。
|
|
298
|
+
*
|
|
299
|
+
* @param id 资产 ID(须为 pdf 类型)
|
|
300
|
+
* @returns PdfInfo;非 pdf / reader 未注册 / 解析失败返回 null
|
|
301
|
+
*/
|
|
302
|
+
readAssetPdfInfo(id: AssetId): Promise<PdfInfo | null>;
|
|
195
303
|
/** 删除资产 */
|
|
196
304
|
removeAsset(id: AssetId): Promise<void>;
|
|
197
305
|
/** 列出所有资产 */
|
|
@@ -214,6 +322,14 @@ export interface LokvisRuntime {
|
|
|
214
322
|
capabilities(): Promise<Capability[]>;
|
|
215
323
|
/** 检查能力是否可用 */
|
|
216
324
|
hasCapability(name: string): Promise<boolean>;
|
|
325
|
+
/**
|
|
326
|
+
* 检查能力是否仅有 stub 实现(无可用引擎)。
|
|
327
|
+
*
|
|
328
|
+
* UI 层据此为 stub-only 能力显示 "Coming Soon" 标记(A7),
|
|
329
|
+
* 避免用户选择后在工作流执行阶段才收到 stub error。
|
|
330
|
+
* 返回 true 表示该能力已声明但仅有占位实现,resolve() 会跳过。
|
|
331
|
+
*/
|
|
332
|
+
isStubOnly(name: string): Promise<boolean>;
|
|
217
333
|
|
|
218
334
|
// ─── MCP 暴露(见 docs/AI生态冲击调整方案.md §6) ─────
|
|
219
335
|
/**
|
package/src/worker-host.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isWorkerReady,
|
|
27
27
|
isWorkerEvent,
|
|
28
28
|
isWorkerFatalError,
|
|
29
|
+
unwrapBlobRef,
|
|
29
30
|
type WorkerRequest,
|
|
30
31
|
type WorkerPing,
|
|
31
32
|
type WorkerCancel,
|
|
@@ -106,6 +107,18 @@ export class WorkerHandshakeError extends Error {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 安全调用 Node.js 的 Timer.unref()。
|
|
112
|
+
*
|
|
113
|
+
* `unref` 是 Node.js 对 `setTimeout`/`setInterval` 返回值的扩展
|
|
114
|
+
* (返回 `NodeJS.Timeout`),浏览器中两者返回 `number`(无 `unref`)。
|
|
115
|
+
* 用类型断言 + 可选链兼容两种环境,避免跨包 typecheck 时因
|
|
116
|
+
* `@types/node` 未加载而报错。
|
|
117
|
+
*/
|
|
118
|
+
function maybeUnref(handle: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>): void {
|
|
119
|
+
(handle as { unref?: () => void }).unref?.();
|
|
120
|
+
}
|
|
121
|
+
|
|
109
122
|
/**
|
|
110
123
|
* 请求被 AbortSignal 取消(W3.5 cancel 贯穿)。
|
|
111
124
|
*
|
|
@@ -227,7 +240,19 @@ export class WorkerHost {
|
|
|
227
240
|
): void {
|
|
228
241
|
const set = this.listeners.get(type);
|
|
229
242
|
if (!set) return;
|
|
230
|
-
|
|
243
|
+
// 对齐 AGENTS.md EventBus 安全规范:遍历副本 + try/catch
|
|
244
|
+
// 防止 handler 回调中 unsubscribe 触发 set.delete mutate 正在迭代的 Set,
|
|
245
|
+
// 以及单个 handler 抛错中断后续分发
|
|
246
|
+
for (const h of [...set]) {
|
|
247
|
+
try {
|
|
248
|
+
(h as Listener<WorkerHostEventMap[K]>)(payload);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(
|
|
251
|
+
`[WorkerHost] listener for "${type}" threw:`,
|
|
252
|
+
err
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
231
256
|
}
|
|
232
257
|
|
|
233
258
|
private log(message: string): void {
|
|
@@ -334,6 +359,7 @@ export class WorkerHost {
|
|
|
334
359
|
|
|
335
360
|
// 等待 ready 握手(超时或协议不匹配则拒绝,由 catch 清理 transport)
|
|
336
361
|
let readyTimer: ReturnType<typeof setTimeout>;
|
|
362
|
+
let readyOff: (() => void) | undefined;
|
|
337
363
|
try {
|
|
338
364
|
await new Promise<void>((resolve, reject) => {
|
|
339
365
|
let done = false;
|
|
@@ -346,17 +372,19 @@ export class WorkerHost {
|
|
|
346
372
|
)
|
|
347
373
|
);
|
|
348
374
|
}, this.opts.readyTimeoutMs);
|
|
349
|
-
readyTimer
|
|
375
|
+
maybeUnref(readyTimer);
|
|
350
376
|
|
|
351
|
-
|
|
377
|
+
readyOff = this.on('ready', () => {
|
|
352
378
|
if (done) return;
|
|
353
379
|
done = true;
|
|
354
380
|
clearTimeout(readyTimer);
|
|
355
|
-
|
|
381
|
+
readyOff?.();
|
|
356
382
|
resolve();
|
|
357
383
|
});
|
|
358
384
|
});
|
|
359
385
|
} catch (err) {
|
|
386
|
+
// W21.6: 失败时也移除 ready 监听器,避免 stale 闭包累积
|
|
387
|
+
readyOff?.();
|
|
360
388
|
clearTimeout(readyTimer!);
|
|
361
389
|
// this.transport 在 spawn 顶部已赋值,teardownTransport 可安全清理
|
|
362
390
|
// (clearHeartbeat 对未启动的心跳是无害 no-op)
|
|
@@ -396,7 +424,7 @@ export class WorkerHost {
|
|
|
396
424
|
if (!pending) return; // 已超时或已取消
|
|
397
425
|
this.pending.delete(data.id);
|
|
398
426
|
clearTimeout(pending.timer);
|
|
399
|
-
if (data.ok) pending.resolve(data.result);
|
|
427
|
+
if (data.ok) pending.resolve(unwrapBlobRef(data.result));
|
|
400
428
|
else {
|
|
401
429
|
const e = data.error;
|
|
402
430
|
const err = new Error(e.message);
|
|
@@ -481,7 +509,7 @@ export class WorkerHost {
|
|
|
481
509
|
this.heartbeatTimer = setInterval(() => {
|
|
482
510
|
this.sendPing();
|
|
483
511
|
}, this.opts.heartbeatIntervalMs);
|
|
484
|
-
this.heartbeatTimer
|
|
512
|
+
maybeUnref(this.heartbeatTimer);
|
|
485
513
|
}
|
|
486
514
|
|
|
487
515
|
private sendPing(): void {
|
|
@@ -496,7 +524,7 @@ export class WorkerHost {
|
|
|
496
524
|
this.pongTimer = setTimeout(() => {
|
|
497
525
|
this.handleCrash(`heartbeat timeout (no pong in ${this.opts.heartbeatTimeoutMs}ms)`);
|
|
498
526
|
}, this.opts.heartbeatTimeoutMs);
|
|
499
|
-
this.pongTimer
|
|
527
|
+
maybeUnref(this.pongTimer);
|
|
500
528
|
}
|
|
501
529
|
|
|
502
530
|
private clearHeartbeat(): void {
|
package/src/worker-protocol.ts
CHANGED
|
@@ -84,6 +84,53 @@ export interface WorkerResponseErr {
|
|
|
84
84
|
|
|
85
85
|
export type WorkerResponse = WorkerResponseOk | WorkerResponseErr;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* BlobRef:Blob 的可转移信封(W21.4 Transferable 优化)。
|
|
89
|
+
*
|
|
90
|
+
* Blob 本身不是 Transferable,只有 ArrayBuffer 是。Worker 把处理后的
|
|
91
|
+
* Blob 拆成 { meta, buffer },通过 postMessage 的 transfer list 零拷贝
|
|
92
|
+
* 移交 ArrayBuffer 给主线程;主线程收到后用
|
|
93
|
+
* `new Blob([buffer], { type: meta.type })` 重组。
|
|
94
|
+
*
|
|
95
|
+
* 协议约定(与 packages/engine-image/src/worker-adapter.ts 的 BlobRef 同步声明,
|
|
96
|
+
* 遵循 AGENTS.md 中 WorkerCancel 的先例——两处各自声明,注释标注同步):
|
|
97
|
+
* - response.result.kind === 'blob' → 解包 BlobRef,重组 Blob
|
|
98
|
+
* - response.result 无 kind 字段 → 非 Blob 结果(如元数据),直接使用
|
|
99
|
+
*/
|
|
100
|
+
export interface BlobRef {
|
|
101
|
+
kind: 'blob';
|
|
102
|
+
meta: { size: number; type: string };
|
|
103
|
+
buffer: ArrayBuffer;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 类型守卫:是否为 BlobRef 信封(W21.4) */
|
|
107
|
+
export function isBlobRef(data: unknown): data is BlobRef {
|
|
108
|
+
return (
|
|
109
|
+
typeof data === 'object' &&
|
|
110
|
+
data !== null &&
|
|
111
|
+
(data as { kind?: unknown }).kind === 'blob' &&
|
|
112
|
+
// 注意:typeof null === 'object'(JS 历史遗留),必须显式排除 null,
|
|
113
|
+
// 否则 { kind:'blob', meta:null, buffer:ArrayBuffer } 会被误判为 BlobRef,
|
|
114
|
+
// 导致 unwrapBlobRef 访问 meta.type 时抛 TypeError。
|
|
115
|
+
(data as { meta?: unknown }).meta !== null &&
|
|
116
|
+
typeof (data as { meta?: unknown }).meta === 'object' &&
|
|
117
|
+
(data as { buffer?: unknown }).buffer instanceof ArrayBuffer
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 解包 BlobRef:若 result 为 BlobRef 信封,重组为 Blob;
|
|
123
|
+
* 否则原样返回(W21.4)。
|
|
124
|
+
*
|
|
125
|
+
* 供 WorkerHost.handleMessage 在 resolve pending 前调用。
|
|
126
|
+
*/
|
|
127
|
+
export function unwrapBlobRef(result: unknown): unknown {
|
|
128
|
+
if (isBlobRef(result)) {
|
|
129
|
+
return new Blob([result.buffer], { type: result.meta.type });
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
87
134
|
/** 心跳回应 */
|
|
88
135
|
export interface WorkerPong {
|
|
89
136
|
id: string;
|
|
@@ -1,124 +0,0 @@
|
|
|
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
|
-
import type { AssetType, Workflow, WorkflowAuthor, WorkflowCategory, WorkflowInput, WorkflowOutput } from '@lokvis/schema';
|
|
29
|
-
/** 工作流最大节点数(M1 MVP 约束) */
|
|
30
|
-
export declare const MAX_WORKFLOW_STEPS = 5;
|
|
31
|
-
/** Builder 内部节点结构(构造中,与 WorkflowNode 略有差异:label 可独立设置) */
|
|
32
|
-
interface BuilderNode {
|
|
33
|
-
/** 内部生成的节点 id(builder 内部唯一) */
|
|
34
|
-
id: string;
|
|
35
|
-
capability: string;
|
|
36
|
-
params: Record<string, unknown>;
|
|
37
|
-
label?: string;
|
|
38
|
-
}
|
|
39
|
-
/** WorkflowBuilder 构造选项 */
|
|
40
|
-
export interface WorkflowBuilderOptions {
|
|
41
|
-
/** 工作流 ID(必填,需全局唯一) */
|
|
42
|
-
id: string;
|
|
43
|
-
/** 工作流名称 */
|
|
44
|
-
name: string;
|
|
45
|
-
/** 工作流描述(可选,默认为空字符串) */
|
|
46
|
-
description?: string;
|
|
47
|
-
/** 作者信息(可选,默认本地用户) */
|
|
48
|
-
author?: WorkflowAuthor;
|
|
49
|
-
/** 分类(可选,默认 'image') */
|
|
50
|
-
category?: WorkflowCategory;
|
|
51
|
-
/** 标签(可选) */
|
|
52
|
-
tags?: string[];
|
|
53
|
-
/** 是否为官方工作流(可选,默认 false) */
|
|
54
|
-
official?: boolean;
|
|
55
|
-
/** 最大节点数(可选,默认 MAX_WORKFLOW_STEPS;测试或特殊场景可放宽) */
|
|
56
|
-
maxSteps?: number;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* WorkflowBuilder - 链式构造线性 Workflow。
|
|
60
|
-
*
|
|
61
|
-
* 抛错时机:
|
|
62
|
-
* - add() 超过 maxSteps 时立即抛错(避免构造完才发现步数超限)
|
|
63
|
-
* - build() 在节点为空 / 输入输出未设置时抛错
|
|
64
|
-
* - remove()/move() 在节点 id 不存在时静默(链式 API 容错)
|
|
65
|
-
*/
|
|
66
|
-
export declare class WorkflowBuilder {
|
|
67
|
-
private readonly options;
|
|
68
|
-
private readonly maxSteps;
|
|
69
|
-
private nodes;
|
|
70
|
-
private input;
|
|
71
|
-
private output;
|
|
72
|
-
private nodeCounter;
|
|
73
|
-
constructor(options: WorkflowBuilderOptions);
|
|
74
|
-
/** 设置工作流输入定义 */
|
|
75
|
-
setInput(input: WorkflowInput): this;
|
|
76
|
-
/** 设置工作流输出定义 */
|
|
77
|
-
setOutput(output: WorkflowOutput): this;
|
|
78
|
-
/**
|
|
79
|
-
* 添加一个节点到链尾。
|
|
80
|
-
*
|
|
81
|
-
* @param capability 能力名,如 `image.resize`
|
|
82
|
-
* @param params 能力参数(可选,默认空对象)
|
|
83
|
-
* @param label 节点标签(可选,UI 显示用)
|
|
84
|
-
* @throws Error 当节点数达到 maxSteps 时
|
|
85
|
-
*/
|
|
86
|
-
add(capability: string, params?: Record<string, unknown>, label?: string): this;
|
|
87
|
-
/**
|
|
88
|
-
* 按 capability 或节点 id 移除节点。
|
|
89
|
-
* 若有多个同 capability 节点,只移除第一个。
|
|
90
|
-
* 若不存在则静默(链式 API 容错)。
|
|
91
|
-
*
|
|
92
|
-
* @param capabilityOrId 能力名或节点 id
|
|
93
|
-
*/
|
|
94
|
-
remove(capabilityOrId: string): this;
|
|
95
|
-
/**
|
|
96
|
-
* 移动节点到新位置(线性链中重排)。
|
|
97
|
-
*
|
|
98
|
-
* @param from 源位置(0-based 索引)
|
|
99
|
-
* @param to 目标位置(0-based 索引,移动后该节点的新位置)
|
|
100
|
-
* @throws Error 当索引越界
|
|
101
|
-
*/
|
|
102
|
-
move(from: number, to: number): this;
|
|
103
|
-
/** 交换两个节点位置 */
|
|
104
|
-
swap(i: number, j: number): this;
|
|
105
|
-
/** 更新指定节点的参数 */
|
|
106
|
-
updateParams(nodeId: string, params: Record<string, unknown>): this;
|
|
107
|
-
/** 当前节点数 */
|
|
108
|
-
get size(): number;
|
|
109
|
-
/** 是否已达最大节点数 */
|
|
110
|
-
get isFull(): boolean;
|
|
111
|
-
/** 获取节点的只读副本(用于 UI 预览) */
|
|
112
|
-
getNodes(): ReadonlyArray<Readonly<BuilderNode>>;
|
|
113
|
-
/**
|
|
114
|
-
* 构造 Workflow 实例。
|
|
115
|
-
*
|
|
116
|
-
* @throws Error 当节点为空 / 输入未设置 / 输出未设置
|
|
117
|
-
*/
|
|
118
|
-
build(): Workflow;
|
|
119
|
-
}
|
|
120
|
-
/** 从 Workflow 反向构造 Builder(用于编辑已有工作流) */
|
|
121
|
-
export declare function workflowToBuilder(workflow: Workflow, maxSteps?: number): WorkflowBuilder;
|
|
122
|
-
/** AssetType 用于类型导出(便于消费方) */
|
|
123
|
-
export type { AssetType };
|
|
124
|
-
//# sourceMappingURL=workflow-builder.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"workflow-builder.d.ts","sourceRoot":"","sources":["../src/workflow-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,QAAQ,EACR,cAAc,EACd,gBAAgB,EAEhB,aAAa,EAEb,cAAc,EACf,MAAM,gBAAgB,CAAC;AAExB,0BAA0B;AAC1B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,0DAA0D;AAC1D,UAAU,WAAW;IACnB,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,2BAA2B;AAC3B,MAAM,WAAW,sBAAsB;IACrC,uBAAuB;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,aAAa;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,WAAW,CAAK;gBAEZ,OAAO,EAAE,sBAAsB;IAO3C,gBAAgB;IAChB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAKpC,gBAAgB;IAChB,SAAS,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKvC;;;;;;;OAOG;IACH,GAAG,CACD,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,KAAK,CAAC,EAAE,MAAM,GACb,IAAI;IAkBP;;;;;;OAMG;IACH,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAUpC;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAkBpC,eAAe;IACf,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAgBhC,gBAAgB;IAChB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAQnE,YAAY;IACZ,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,gBAAgB;IAChB,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,0BAA0B;IAC1B,QAAQ,IAAI,aAAa,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAIhD;;;;OAIG;IACH,KAAK,IAAI,QAAQ;CAoDlB;AAED,yCAAyC;AACzC,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,eAAe,CAoBxF;AAED,8BAA8B;AAC9B,YAAY,EAAE,SAAS,EAAE,CAAC"}
|