@webskill/sdk 0.0.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/browser.d.ts +219 -0
- package/dist/browser.js +856 -0
- package/dist/dist-9fczg9Pa.js +1122 -0
- package/dist/dist-B_ldNWwT.js +1975 -0
- package/dist/dist-RcqvzAGF.js +15382 -0
- package/dist/governance.d.ts +440 -0
- package/dist/governance.js +844 -0
- package/dist/index-88KNOnbr.d.ts +955 -0
- package/dist/index-Bq-bTWh3.d.ts +219 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/mcp.d.ts +237 -0
- package/dist/mcp.js +513 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.js +4 -0
- package/dist/ui-react.d.ts +40 -0
- package/dist/ui-react.js +239 -0
- package/dist/ui-vue.d.ts +72 -0
- package/dist/ui-vue.js +196 -0
- package/dist/ui.d.ts +171 -0
- package/dist/ui.js +4 -0
- package/package.json +91 -0
package/dist/browser.js
ADDED
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { D as normalizeToolContent, E as mergeCatalogEntries, I as SkillDiscovery, O as parseBridgeRequest, R as WebSkillError, f as MockLlmClient, h as ProgressiveRouter, i as AgentLoop, m as OpenAiCompatibleClient, o as FsArtifactStore, s as FsMemoryStore, x as bridgeError, z as buildCatalog } from "./dist-B_ldNWwT.js";
|
|
2
|
+
|
|
3
|
+
//#region ../browser/dist/index.js
|
|
4
|
+
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
5
|
+
function isOpfsAvailable(globalScope) {
|
|
6
|
+
return typeof (globalScope ?? globalThis).navigator?.storage?.getDirectory === "function";
|
|
7
|
+
}
|
|
8
|
+
const isDomException = (e, name) => e instanceof DOMException && e.name === name;
|
|
9
|
+
const mapError = (e, path) => {
|
|
10
|
+
if (isDomException(e, "NotFoundError")) return new WebSkillError("FS_NOT_FOUND", `Path not found: ${path}`);
|
|
11
|
+
if (isDomException(e, "NotAllowedError")) return new WebSkillError("FS_PERMISSION_DENIED", `Permission denied: ${path}`, e);
|
|
12
|
+
if (e instanceof WebSkillError) return e;
|
|
13
|
+
return new WebSkillError("FS_NOT_FOUND", `OPFS operation failed at ${path}: ${String(e)}`, e);
|
|
14
|
+
};
|
|
15
|
+
/** 基于 navigator.storage.getDirectory() 的 FileSystemProvider(路径一律 POSIX 风格) */
|
|
16
|
+
var OpfsProvider = class {
|
|
17
|
+
kind = "opfs";
|
|
18
|
+
#rootName;
|
|
19
|
+
#rootPromise;
|
|
20
|
+
constructor(deps = {}) {
|
|
21
|
+
this.#rootName = deps.rootName ?? "webskill";
|
|
22
|
+
}
|
|
23
|
+
async #root() {
|
|
24
|
+
this.#rootPromise ??= (async () => {
|
|
25
|
+
return (await navigator.storage.getDirectory()).getDirectoryHandle(this.#rootName, { create: true });
|
|
26
|
+
})();
|
|
27
|
+
return this.#rootPromise;
|
|
28
|
+
}
|
|
29
|
+
#segments(p) {
|
|
30
|
+
return p.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".");
|
|
31
|
+
}
|
|
32
|
+
async #walkDir(p, create) {
|
|
33
|
+
let dir = await this.#root();
|
|
34
|
+
for (const segment of this.#segments(p)) dir = await dir.getDirectoryHandle(segment, { create });
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
async #walkFile(p) {
|
|
38
|
+
const segments = this.#segments(p);
|
|
39
|
+
const name = segments.pop();
|
|
40
|
+
if (!name) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${p}`);
|
|
41
|
+
return (await this.#walkDir(`/${segments.join("/")}`, false)).getFileHandle(name);
|
|
42
|
+
}
|
|
43
|
+
async #wrap(p, op) {
|
|
44
|
+
try {
|
|
45
|
+
return await op();
|
|
46
|
+
} catch (e) {
|
|
47
|
+
throw mapError(e, p);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async readText(p) {
|
|
51
|
+
return this.#wrap(p, async () => {
|
|
52
|
+
return (await (await this.#walkFile(p)).getFile()).text();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async writeText(p, content) {
|
|
56
|
+
await this.writeBinary(p, new TextEncoder().encode(content));
|
|
57
|
+
}
|
|
58
|
+
async readBinary(p) {
|
|
59
|
+
return this.#wrap(p, async () => {
|
|
60
|
+
const handle = await this.#walkFile(p);
|
|
61
|
+
return new Uint8Array(await (await handle.getFile()).arrayBuffer());
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async writeBinary(p, content) {
|
|
65
|
+
await this.#wrap(p, async () => {
|
|
66
|
+
const segments = this.#segments(p);
|
|
67
|
+
const name = segments.pop();
|
|
68
|
+
if (!name) throw new WebSkillError("FS_NOT_FOUND", `Invalid file path: ${p}`);
|
|
69
|
+
const writable = await (await (await this.#walkDir(`/${segments.join("/")}`, true)).getFileHandle(name, { create: true })).createWritable();
|
|
70
|
+
await writable.write(content);
|
|
71
|
+
await writable.close();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async exists(p) {
|
|
75
|
+
try {
|
|
76
|
+
await this.stat(p);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async stat(p) {
|
|
83
|
+
return this.#wrap(p, async () => {
|
|
84
|
+
if (this.#segments(p).length === 0) return {
|
|
85
|
+
path: "/",
|
|
86
|
+
type: "directory"
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
const file = await (await this.#walkFile(p)).getFile();
|
|
90
|
+
return {
|
|
91
|
+
path: p,
|
|
92
|
+
type: "file",
|
|
93
|
+
size: file.size,
|
|
94
|
+
mtimeMs: file.lastModified
|
|
95
|
+
};
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (isDomException(e, "NotFoundError") || isDomException(e, "TypeMismatchError")) return {
|
|
98
|
+
path: p,
|
|
99
|
+
type: "directory",
|
|
100
|
+
name: (await this.#walkDir(p, false)).name
|
|
101
|
+
};
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async list(p) {
|
|
107
|
+
return this.#wrap(p, async () => {
|
|
108
|
+
const dir = await this.#walkDir(p, false);
|
|
109
|
+
const out = [];
|
|
110
|
+
for await (const [name, handle] of dir.entries()) {
|
|
111
|
+
const base = p.replace(/\/+$/, "");
|
|
112
|
+
out.push({
|
|
113
|
+
path: `${base}/${name}`,
|
|
114
|
+
type: handle.kind === "directory" ? "directory" : "file"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async mkdir(p) {
|
|
121
|
+
await this.#wrap(p, async () => {
|
|
122
|
+
await this.#walkDir(p, true);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async remove(p, options) {
|
|
126
|
+
await this.#wrap(p, async () => {
|
|
127
|
+
const segments = this.#segments(p);
|
|
128
|
+
const name = segments.pop();
|
|
129
|
+
if (!name) throw new WebSkillError("FS_NOT_FOUND", `Invalid path: ${p}`);
|
|
130
|
+
await (await this.#walkDir(`/${segments.join("/")}`, false)).removeEntry(name, { recursive: options?.recursive ?? false });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Worker 引导源码(字符串 → Blob URL → new Worker(url, {type:'module'}))。
|
|
136
|
+
* 注意:内容为纯 JS,不能含反引号与 ${ 占位(外层用模板字符串内嵌)。
|
|
137
|
+
*
|
|
138
|
+
* 协议(宿主 ↔ Worker):
|
|
139
|
+
* - 宿主 → Worker:{type:'load', id, source} 请求加载脚本定义
|
|
140
|
+
* - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args} 请求执行
|
|
141
|
+
* - Worker → 宿主:{type:'load-result', id, ok, definition?, error?}
|
|
142
|
+
* - Worker → 宿主:{type:'execute-result', id, ok, value?, stdout, stderr, error?}
|
|
143
|
+
* - Worker → 宿主:{type:'bridge', request} 能力桥 RPC(readReference/writeArtifact/confirm)
|
|
144
|
+
* - 宿主 → Worker:{type:'bridge-response', response}
|
|
145
|
+
*
|
|
146
|
+
* .js 源码经 data: URL 动态 import;主线程从不 import 技能脚本。
|
|
147
|
+
*/
|
|
148
|
+
const WORKER_BOOTSTRAP_SOURCE = String.raw`
|
|
149
|
+
'use strict';
|
|
150
|
+
|
|
151
|
+
var pending = new Map();
|
|
152
|
+
var bridgeSeq = 0;
|
|
153
|
+
|
|
154
|
+
function loadModule(source) {
|
|
155
|
+
var url = 'data:text/javascript;charset=utf-8,' + encodeURIComponent(source);
|
|
156
|
+
return import(url);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function postError(type, id, code, message, extra) {
|
|
160
|
+
var msg = { type: type, id: id, ok: false, error: { code: code, message: message } };
|
|
161
|
+
if (extra) Object.assign(msg, extra);
|
|
162
|
+
self.postMessage(msg);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function callCapability(kind, payload) {
|
|
166
|
+
var request = Object.assign({ kind: kind, id: 'br-' + (++bridgeSeq) }, payload);
|
|
167
|
+
return new Promise(function (resolve, reject) {
|
|
168
|
+
pending.set(request.id, { resolve: resolve, reject: reject });
|
|
169
|
+
self.postMessage({ type: 'bridge', request: request });
|
|
170
|
+
}).then(function (response) {
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
var err = new Error((response.error && response.error.message) || 'capability call failed');
|
|
173
|
+
err.code = (response.error && response.error.code) || 'TOOL_EXECUTION_FAILED';
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
return response.value;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function makeContext(msg) {
|
|
181
|
+
return {
|
|
182
|
+
skillName: msg.skillName,
|
|
183
|
+
runId: msg.runId,
|
|
184
|
+
readReference: function (path) {
|
|
185
|
+
return callCapability('readReference', { path: path });
|
|
186
|
+
},
|
|
187
|
+
writeArtifact: function (path, content, options) {
|
|
188
|
+
return callCapability('writeArtifact', {
|
|
189
|
+
path: path,
|
|
190
|
+
content: typeof content === 'string' ? content : Array.from(content || []),
|
|
191
|
+
mimeType: options && options.mimeType,
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
confirm: function (message) {
|
|
195
|
+
return callCapability('confirm', { message: message });
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
self.onmessage = async function (event) {
|
|
201
|
+
var msg = event.data;
|
|
202
|
+
if (!msg || typeof msg !== 'object') return;
|
|
203
|
+
|
|
204
|
+
if (msg.type === 'bridge-response') {
|
|
205
|
+
var entry = pending.get(msg.response && msg.response.id);
|
|
206
|
+
if (entry) {
|
|
207
|
+
pending.delete(msg.response.id);
|
|
208
|
+
entry.resolve(msg.response);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (msg.type === 'load') {
|
|
214
|
+
try {
|
|
215
|
+
var mod = await loadModule(msg.source);
|
|
216
|
+
self.postMessage({
|
|
217
|
+
type: 'load-result',
|
|
218
|
+
id: msg.id,
|
|
219
|
+
ok: true,
|
|
220
|
+
definition: {
|
|
221
|
+
description: typeof mod.description === 'string' ? mod.description : undefined,
|
|
222
|
+
inputSchema: mod.inputSchema !== undefined ? mod.inputSchema : undefined,
|
|
223
|
+
hasRun: typeof mod.run === 'function',
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
} catch (e) {
|
|
227
|
+
postError('load-result', msg.id, 'TOOL_EXECUTION_FAILED', String((e && e.message) || e));
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (msg.type === 'execute') {
|
|
233
|
+
var stdout = [];
|
|
234
|
+
var stderr = [];
|
|
235
|
+
var originals = {
|
|
236
|
+
log: console.log, info: console.info, debug: console.debug,
|
|
237
|
+
warn: console.warn, error: console.error,
|
|
238
|
+
};
|
|
239
|
+
console.log = console.info = console.debug = function () {
|
|
240
|
+
stdout.push(Array.from(arguments).map(String).join(' '));
|
|
241
|
+
};
|
|
242
|
+
console.warn = console.error = function () {
|
|
243
|
+
stderr.push(Array.from(arguments).map(String).join(' '));
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
var mod2 = await loadModule(msg.source);
|
|
247
|
+
if (typeof mod2.run !== 'function') {
|
|
248
|
+
throw new Error('Script does not export a run function');
|
|
249
|
+
}
|
|
250
|
+
var value = await mod2.run(msg.args, makeContext(msg));
|
|
251
|
+
self.postMessage({
|
|
252
|
+
type: 'execute-result', id: msg.id, ok: true,
|
|
253
|
+
value: value === undefined ? null : value,
|
|
254
|
+
stdout: stdout, stderr: stderr,
|
|
255
|
+
});
|
|
256
|
+
} catch (e2) {
|
|
257
|
+
postError(
|
|
258
|
+
'execute-result', msg.id,
|
|
259
|
+
(e2 && e2.code) || 'TOOL_EXECUTION_FAILED',
|
|
260
|
+
String((e2 && e2.message) || e2),
|
|
261
|
+
{ stdout: stdout, stderr: stderr },
|
|
262
|
+
);
|
|
263
|
+
} finally {
|
|
264
|
+
console.log = originals.log; console.info = originals.info; console.debug = originals.debug;
|
|
265
|
+
console.warn = originals.warn; console.error = originals.error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
`;
|
|
270
|
+
const defaultWorkerFactory = () => {
|
|
271
|
+
const blob = new Blob([WORKER_BOOTSTRAP_SOURCE], { type: "text/javascript" });
|
|
272
|
+
const url = URL.createObjectURL(blob);
|
|
273
|
+
const worker = new Worker(url, { type: "module" });
|
|
274
|
+
URL.revokeObjectURL(url);
|
|
275
|
+
return worker;
|
|
276
|
+
};
|
|
277
|
+
const baseName = (p) => p.split("/").pop() ?? p;
|
|
278
|
+
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
279
|
+
/**
|
|
280
|
+
* Web Worker 脚本沙箱执行器:loadDefinition 与 execute 都在 Worker 内完成,
|
|
281
|
+
* 主线程从不 import 技能脚本;每次执行独立 Worker;超时 terminate 强杀。
|
|
282
|
+
*/
|
|
283
|
+
var BrowserWorkerScriptExecutor = class {
|
|
284
|
+
#fs;
|
|
285
|
+
#workerFactory;
|
|
286
|
+
#capabilities;
|
|
287
|
+
constructor(deps) {
|
|
288
|
+
this.#fs = deps.fs;
|
|
289
|
+
this.#workerFactory = deps.workerFactory ?? defaultWorkerFactory;
|
|
290
|
+
this.#capabilities = {
|
|
291
|
+
readReference: deps.capabilities?.readReference ?? true,
|
|
292
|
+
writeArtifact: deps.capabilities?.writeArtifact ?? true,
|
|
293
|
+
confirm: deps.capabilities?.confirm ?? true
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** 定位 scripts/<name>.js;.ts → TOOL_UNSUPPORTED(D4);冲突/缺失结构化报错 */
|
|
297
|
+
async #locateScript(skillRoot, scriptName) {
|
|
298
|
+
const jsPath = `${skillRoot}/scripts/${scriptName}.js`;
|
|
299
|
+
const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
|
|
300
|
+
const [hasJs, hasTs] = await Promise.all([this.#fs.exists(jsPath), this.#fs.exists(tsPath)]);
|
|
301
|
+
if (hasJs && hasTs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
|
|
302
|
+
if (hasTs) throw new WebSkillError("TOOL_UNSUPPORTED", `Browser sandbox executes .js only; "${scriptName}.ts" needs transpilation (deferred: D4)`);
|
|
303
|
+
if (!hasJs) throw new WebSkillError("TOOL_NOT_FOUND", `Script "${scriptName}" not found under ${skillRoot}/scripts`);
|
|
304
|
+
return {
|
|
305
|
+
path: jsPath,
|
|
306
|
+
source: await this.#fs.readText(jsPath)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async loadDefinition(skillRoot, scriptName) {
|
|
310
|
+
const script = await this.#locateScript(skillRoot, scriptName);
|
|
311
|
+
const skillName = baseName(skillRoot);
|
|
312
|
+
const worker = this.#workerFactory();
|
|
313
|
+
try {
|
|
314
|
+
const data = await this.#request(worker, {
|
|
315
|
+
type: "load",
|
|
316
|
+
id: "load-1",
|
|
317
|
+
source: script.source
|
|
318
|
+
});
|
|
319
|
+
if (!data.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${data.error?.message ?? "unknown error"}`);
|
|
320
|
+
return {
|
|
321
|
+
name: `${skillName}__${scriptName}`,
|
|
322
|
+
description: data.definition?.description,
|
|
323
|
+
inputSchema: data.definition?.inputSchema,
|
|
324
|
+
source: "script",
|
|
325
|
+
skillName
|
|
326
|
+
};
|
|
327
|
+
} finally {
|
|
328
|
+
worker.terminate();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async execute(input) {
|
|
332
|
+
const { skillRoot, scriptName, args, context, timeoutMs } = input;
|
|
333
|
+
let script;
|
|
334
|
+
try {
|
|
335
|
+
script = await this.#locateScript(skillRoot, scriptName);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
content: [],
|
|
340
|
+
error: {
|
|
341
|
+
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
342
|
+
message: messageOf(e)
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const worker = this.#workerFactory();
|
|
347
|
+
try {
|
|
348
|
+
const response = await this.#request(worker, {
|
|
349
|
+
type: "execute",
|
|
350
|
+
id: "exec-1",
|
|
351
|
+
skillName: context.skillName,
|
|
352
|
+
runId: context.runId,
|
|
353
|
+
source: script.source,
|
|
354
|
+
args
|
|
355
|
+
}, timeoutMs, (bridgeRequest) => this.#handleBridge(bridgeRequest, context));
|
|
356
|
+
if (!response.ok) {
|
|
357
|
+
const stderrSummary = response.stderr?.length ? ` | stderr: ${response.stderr.join(" | ").slice(0, 500)}` : "";
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
content: [],
|
|
361
|
+
error: {
|
|
362
|
+
code: response.error?.code ?? "TOOL_EXECUTION_FAILED",
|
|
363
|
+
message: `${response.error?.message ?? "script execution failed"}${stderrSummary}`
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const content = normalizeToolContent(response.value);
|
|
368
|
+
if (response.stdout?.length) content.push({
|
|
369
|
+
type: "text",
|
|
370
|
+
text: response.stdout.join("\n")
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
content
|
|
375
|
+
};
|
|
376
|
+
} catch (e) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
content: [],
|
|
380
|
+
error: {
|
|
381
|
+
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
382
|
+
message: messageOf(e)
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
} finally {
|
|
386
|
+
worker.terminate();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** 发消息并按 id 等回包;可选超时;onBridge 处理执行期间的能力桥请求 */
|
|
390
|
+
#request(worker, message, timeoutMs, onBridge) {
|
|
391
|
+
const id = String(message["id"]);
|
|
392
|
+
return new Promise((resolve, reject) => {
|
|
393
|
+
let settled = false;
|
|
394
|
+
const timer = timeoutMs !== void 0 ? setTimeout(() => {
|
|
395
|
+
if (settled) return;
|
|
396
|
+
settled = true;
|
|
397
|
+
worker.terminate();
|
|
398
|
+
reject(new WebSkillError("RUN_TIMEOUT", `Script timed out after ${timeoutMs}ms (worker terminated)`));
|
|
399
|
+
}, timeoutMs) : void 0;
|
|
400
|
+
worker.addEventListener("message", (event) => {
|
|
401
|
+
const data = event.data;
|
|
402
|
+
if (!data || typeof data !== "object") return;
|
|
403
|
+
if (data.type === "bridge" && onBridge) {
|
|
404
|
+
const request = parseBridgeRequest(data.request);
|
|
405
|
+
const respond = (response) => worker.postMessage({
|
|
406
|
+
type: "bridge-response",
|
|
407
|
+
response
|
|
408
|
+
});
|
|
409
|
+
if (!request) {
|
|
410
|
+
respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (data.id !== id || settled) return;
|
|
417
|
+
settled = true;
|
|
418
|
+
if (timer) clearTimeout(timer);
|
|
419
|
+
resolve(data);
|
|
420
|
+
});
|
|
421
|
+
worker.postMessage(message);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/** 能力桥宿主侧处理:能力关闭即 TOOL_UNSUPPORTED;readReference/writeArtifact/confirm 委托 context */
|
|
425
|
+
async #handleBridge(request, context) {
|
|
426
|
+
const unsupported = (capability) => bridgeError(request.id, "TOOL_UNSUPPORTED", `Capability "${capability}" is disabled`);
|
|
427
|
+
try {
|
|
428
|
+
switch (request.kind) {
|
|
429
|
+
case "readReference":
|
|
430
|
+
if (!this.#capabilities.readReference) return unsupported("readReference");
|
|
431
|
+
return {
|
|
432
|
+
id: request.id,
|
|
433
|
+
ok: true,
|
|
434
|
+
value: await context.readReference(request.path)
|
|
435
|
+
};
|
|
436
|
+
case "writeArtifact": {
|
|
437
|
+
if (!this.#capabilities.writeArtifact) return unsupported("writeArtifact");
|
|
438
|
+
const content = typeof request.content === "string" ? request.content : new Uint8Array(request.content);
|
|
439
|
+
const artifact = await context.writeArtifact(request.path, content, { ...request.mimeType ? { mimeType: request.mimeType } : {} });
|
|
440
|
+
return {
|
|
441
|
+
id: request.id,
|
|
442
|
+
ok: true,
|
|
443
|
+
value: artifact
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
case "confirm":
|
|
447
|
+
if (!this.#capabilities.confirm || !context.confirm) return unsupported("confirm");
|
|
448
|
+
return {
|
|
449
|
+
id: request.id,
|
|
450
|
+
ok: true,
|
|
451
|
+
value: await context.confirm(request.message)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
} catch (e) {
|
|
455
|
+
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
460
|
+
const isNonEmptyString = (v) => typeof v === "string" && v !== "";
|
|
461
|
+
/** Worker 侧入站校验:非法消息返回 undefined(忽略) */
|
|
462
|
+
function parseMainMessage(data) {
|
|
463
|
+
if (!isRecord(data) || !isNonEmptyString(data["type"])) return void 0;
|
|
464
|
+
switch (data["type"]) {
|
|
465
|
+
case "init": return isNonEmptyString(data["id"]) && isRecord(data["config"]) ? {
|
|
466
|
+
type: "init",
|
|
467
|
+
id: data["id"],
|
|
468
|
+
config: data["config"]
|
|
469
|
+
} : void 0;
|
|
470
|
+
case "run": return isNonEmptyString(data["id"]) && typeof data["prompt"] === "string" ? {
|
|
471
|
+
type: "run",
|
|
472
|
+
id: data["id"],
|
|
473
|
+
prompt: data["prompt"],
|
|
474
|
+
...isNonEmptyString(data["runId"]) ? { runId: data["runId"] } : {}
|
|
475
|
+
} : void 0;
|
|
476
|
+
case "cancel":
|
|
477
|
+
case "status": return isNonEmptyString(data["id"]) && isNonEmptyString(data["runId"]) ? {
|
|
478
|
+
type: data["type"],
|
|
479
|
+
id: data["id"],
|
|
480
|
+
runId: data["runId"]
|
|
481
|
+
} : void 0;
|
|
482
|
+
case "interaction-response": {
|
|
483
|
+
const response = data["response"];
|
|
484
|
+
return isNonEmptyString(data["workerMessageId"]) && isRecord(response) && isNonEmptyString(response["id"]) ? {
|
|
485
|
+
type: "interaction-response",
|
|
486
|
+
workerMessageId: data["workerMessageId"],
|
|
487
|
+
response: {
|
|
488
|
+
id: response["id"],
|
|
489
|
+
...response["value"] !== void 0 ? { value: response["value"] } : {},
|
|
490
|
+
...response["cancelled"] === true ? { cancelled: true } : {}
|
|
491
|
+
}
|
|
492
|
+
} : void 0;
|
|
493
|
+
}
|
|
494
|
+
default: return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** 主线程侧入站校验 */
|
|
498
|
+
function parseWorkerEvent(data) {
|
|
499
|
+
if (!isRecord(data) || !isNonEmptyString(data["type"])) return;
|
|
500
|
+
if (data["type"] === "render-result") return isRecord(data["request"]) && Array.isArray(data["request"]["blocks"]) ? {
|
|
501
|
+
type: "render-result",
|
|
502
|
+
request: data["request"]
|
|
503
|
+
} : void 0;
|
|
504
|
+
if (data["type"] === "text-delta") return isNonEmptyString(data["runId"]) && typeof data["delta"] === "string" ? {
|
|
505
|
+
type: "text-delta",
|
|
506
|
+
runId: data["runId"],
|
|
507
|
+
delta: data["delta"]
|
|
508
|
+
} : void 0;
|
|
509
|
+
if (!isNonEmptyString(data["id"])) return;
|
|
510
|
+
switch (data["type"]) {
|
|
511
|
+
case "ready": return {
|
|
512
|
+
type: "ready",
|
|
513
|
+
id: data["id"]
|
|
514
|
+
};
|
|
515
|
+
case "result": {
|
|
516
|
+
const error = data["error"];
|
|
517
|
+
return {
|
|
518
|
+
type: "result",
|
|
519
|
+
id: data["id"],
|
|
520
|
+
...data["result"] !== void 0 ? { result: data["result"] } : {},
|
|
521
|
+
...isRecord(error) && typeof error["code"] === "string" && typeof error["message"] === "string" ? { error: {
|
|
522
|
+
code: error["code"],
|
|
523
|
+
message: error["message"]
|
|
524
|
+
} } : {}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
case "interaction": return isNonEmptyString(data["workerMessageId"]) && isRecord(data["request"]) ? {
|
|
528
|
+
type: "interaction",
|
|
529
|
+
id: data["id"],
|
|
530
|
+
workerMessageId: data["workerMessageId"],
|
|
531
|
+
request: data["request"]
|
|
532
|
+
} : void 0;
|
|
533
|
+
default: return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Worker 内 UiBridge:request → 'interaction' 消息到主线程,
|
|
538
|
+
* 挂起等待 'interaction-response' → resolve(行内 await 跨线程成立)。
|
|
539
|
+
* renderResult 为单向通知('render-result' 消息,主线程页面桥消费)。
|
|
540
|
+
*/
|
|
541
|
+
var WorkerUiBridge = class {
|
|
542
|
+
#scope;
|
|
543
|
+
#pending = /* @__PURE__ */ new Map();
|
|
544
|
+
#seq = 0;
|
|
545
|
+
constructor(scope) {
|
|
546
|
+
this.#scope = scope;
|
|
547
|
+
}
|
|
548
|
+
/** 宿主转发主线程的 interaction-response */
|
|
549
|
+
handleInteractionResponse(workerMessageId, response) {
|
|
550
|
+
const resolve = this.#pending.get(workerMessageId);
|
|
551
|
+
if (resolve) {
|
|
552
|
+
this.#pending.delete(workerMessageId);
|
|
553
|
+
resolve(response);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/** 取消全部挂起请求(cancel 语义:视为用户取消) */
|
|
557
|
+
cancelAll() {
|
|
558
|
+
for (const [id, resolve] of this.#pending) resolve({
|
|
559
|
+
id,
|
|
560
|
+
cancelled: true
|
|
561
|
+
});
|
|
562
|
+
this.#pending.clear();
|
|
563
|
+
}
|
|
564
|
+
request(input) {
|
|
565
|
+
const workerMessageId = `ui-${++this.#seq}`;
|
|
566
|
+
return new Promise((resolve) => {
|
|
567
|
+
this.#pending.set(workerMessageId, resolve);
|
|
568
|
+
this.#scope.postMessage({
|
|
569
|
+
type: "interaction",
|
|
570
|
+
id: workerMessageId,
|
|
571
|
+
workerMessageId,
|
|
572
|
+
request: input
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async renderResult(input) {
|
|
577
|
+
this.#scope.postMessage({
|
|
578
|
+
type: "render-result",
|
|
579
|
+
request: input
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async onTextDelta(runId, delta) {
|
|
583
|
+
this.#scope.postMessage({
|
|
584
|
+
type: "text-delta",
|
|
585
|
+
runId,
|
|
586
|
+
delta
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
const serializeError = (e) => ({
|
|
591
|
+
code: e instanceof WebSkillError ? e.code : "RUN_FAILED",
|
|
592
|
+
message: e instanceof Error ? e.message : String(e)
|
|
593
|
+
});
|
|
594
|
+
const defaultCreateLlm = (config) => config.mockResponses ? new MockLlmClient(config.mockResponses, { streaming: config.mockStreaming ?? false }) : new OpenAiCompatibleClient(config);
|
|
595
|
+
/**
|
|
596
|
+
* Worker 内宿主(engine-in-worker):
|
|
597
|
+
* 组合 fs + FsArtifactStore/FsMemoryStore + LlmClient + AgentLoop + 沙箱执行器;
|
|
598
|
+
* UiBridge 经 interaction/interaction-response 消息桥接主线程。
|
|
599
|
+
*/
|
|
600
|
+
function startWorkerRuntimeHost(scope, overrides = {}) {
|
|
601
|
+
let state;
|
|
602
|
+
/** runId → 运行状态(status/cancel 用) */
|
|
603
|
+
const runs = /* @__PURE__ */ new Map();
|
|
604
|
+
/** 当前活跃交互桥(cancel 时取消挂起交互) */
|
|
605
|
+
let activeUiBridge;
|
|
606
|
+
const post = (data) => scope.postMessage(data);
|
|
607
|
+
const result = (id, payload) => post({
|
|
608
|
+
type: "result",
|
|
609
|
+
id,
|
|
610
|
+
...payload
|
|
611
|
+
});
|
|
612
|
+
async function handleInit(id, config) {
|
|
613
|
+
const fs = overrides.fs ?? new OpfsProvider(config.opfsRootName ? { rootName: config.opfsRootName } : {});
|
|
614
|
+
state = {
|
|
615
|
+
fs,
|
|
616
|
+
llm: (overrides.createLlm ?? defaultCreateLlm)(config.llm),
|
|
617
|
+
executor: new BrowserWorkerScriptExecutor({
|
|
618
|
+
fs,
|
|
619
|
+
...overrides.workerFactory ? { workerFactory: overrides.workerFactory } : {}
|
|
620
|
+
}),
|
|
621
|
+
...config.interaction ? { interaction: config.interaction } : {},
|
|
622
|
+
roots: config.roots ?? ["/skills"]
|
|
623
|
+
};
|
|
624
|
+
post({
|
|
625
|
+
type: "ready",
|
|
626
|
+
id
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async function handleRun(id, prompt, requestedRunId) {
|
|
630
|
+
if (!state) {
|
|
631
|
+
result(id, { error: {
|
|
632
|
+
code: "RUN_FAILED",
|
|
633
|
+
message: "Host is not initialized"
|
|
634
|
+
} });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const runId = requestedRunId ?? `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
638
|
+
runs.set(runId, { status: "running" });
|
|
639
|
+
try {
|
|
640
|
+
const { entries } = await new SkillDiscovery(state.fs, state.roots).discover();
|
|
641
|
+
const providerEntries = (await Promise.all((overrides.skillProviders ?? []).map(async (p) => {
|
|
642
|
+
try {
|
|
643
|
+
return await p.listSkills();
|
|
644
|
+
} catch {
|
|
645
|
+
return [];
|
|
646
|
+
}
|
|
647
|
+
}))).flat();
|
|
648
|
+
const catalog = providerEntries.length > 0 ? { skills: mergeCatalogEntries(buildCatalog(entries).skills, providerEntries) } : buildCatalog(entries);
|
|
649
|
+
const route = await new ProgressiveRouter().route(catalog);
|
|
650
|
+
const uiBridge = new WorkerUiBridge(scope);
|
|
651
|
+
activeUiBridge = uiBridge;
|
|
652
|
+
const runResult = await new AgentLoop({
|
|
653
|
+
llm: state.llm,
|
|
654
|
+
executor: state.executor,
|
|
655
|
+
artifactStore: new FsArtifactStore({
|
|
656
|
+
root: "artifacts",
|
|
657
|
+
fs: state.fs
|
|
658
|
+
}),
|
|
659
|
+
memory: new FsMemoryStore({
|
|
660
|
+
root: "memory",
|
|
661
|
+
fs: state.fs
|
|
662
|
+
}),
|
|
663
|
+
fs: state.fs,
|
|
664
|
+
skillIndex: new Map(entries.map((e) => [e.name, e.root])),
|
|
665
|
+
uiBridge,
|
|
666
|
+
...state.interaction ? { interaction: state.interaction } : {},
|
|
667
|
+
...overrides.externalTools ? { externalTools: overrides.externalTools } : {},
|
|
668
|
+
...overrides.skillProviders ? { skillProviders: overrides.skillProviders } : {}
|
|
669
|
+
}).run({
|
|
670
|
+
sessionId: `session-${runId}`,
|
|
671
|
+
userPrompt: prompt,
|
|
672
|
+
route,
|
|
673
|
+
runId
|
|
674
|
+
});
|
|
675
|
+
runs.set(runId, { status: runResult.run.status });
|
|
676
|
+
result(id, { result: runResult });
|
|
677
|
+
} catch (e) {
|
|
678
|
+
runs.set(runId, { status: "failed" });
|
|
679
|
+
result(id, { error: serializeError(e) });
|
|
680
|
+
} finally {
|
|
681
|
+
activeUiBridge = void 0;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
scope.addEventListener("message", (event) => {
|
|
685
|
+
const message = parseMainMessage(event.data);
|
|
686
|
+
if (!message) return;
|
|
687
|
+
if (message.type === "interaction-response") {
|
|
688
|
+
activeUiBridge?.handleInteractionResponse(message.workerMessageId, message.response);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
switch (message.type) {
|
|
692
|
+
case "init":
|
|
693
|
+
handleInit(message.id, message.config).catch((e) => result(message.id, { error: serializeError(e) }));
|
|
694
|
+
return;
|
|
695
|
+
case "run":
|
|
696
|
+
handleRun(message.id, message.prompt, message.runId);
|
|
697
|
+
return;
|
|
698
|
+
case "status": {
|
|
699
|
+
const info = runs.get(message.runId);
|
|
700
|
+
result(message.id, { ...info ? { result: {
|
|
701
|
+
runId: message.runId,
|
|
702
|
+
status: info.status
|
|
703
|
+
} } : { error: {
|
|
704
|
+
code: "RUN_FAILED",
|
|
705
|
+
message: `Unknown run: ${message.runId}`
|
|
706
|
+
} } });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
case "cancel":
|
|
710
|
+
if (!runs.get(message.runId)) {
|
|
711
|
+
result(message.id, { error: {
|
|
712
|
+
code: "RUN_FAILED",
|
|
713
|
+
message: `Unknown run: ${message.runId}`
|
|
714
|
+
} });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (activeUiBridge) {
|
|
718
|
+
activeUiBridge.cancelAll();
|
|
719
|
+
result(message.id, { result: {
|
|
720
|
+
runId: message.runId,
|
|
721
|
+
cancelling: true
|
|
722
|
+
} });
|
|
723
|
+
} else result(message.id, { error: {
|
|
724
|
+
code: "RUN_FAILED",
|
|
725
|
+
message: "Run cannot be cancelled in its current state"
|
|
726
|
+
} });
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/** 主线程客户端:init/run/cancel/status/dispose 的 Promise API + 交互转发 */
|
|
731
|
+
var WorkerRuntimeClient = class {
|
|
732
|
+
#worker;
|
|
733
|
+
#uiBridge;
|
|
734
|
+
#timeoutMs;
|
|
735
|
+
#pending = /* @__PURE__ */ new Map();
|
|
736
|
+
#seq = 0;
|
|
737
|
+
constructor(deps) {
|
|
738
|
+
this.#worker = deps.worker;
|
|
739
|
+
this.#uiBridge = deps.uiBridge;
|
|
740
|
+
this.#timeoutMs = deps.timeoutMs ?? 12e4;
|
|
741
|
+
this.#worker.addEventListener("message", (event) => {
|
|
742
|
+
this.#onMessage(event.data);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
init(config) {
|
|
746
|
+
return this.#call({
|
|
747
|
+
type: "init",
|
|
748
|
+
id: this.#nextId(),
|
|
749
|
+
config
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
run(prompt, runId) {
|
|
753
|
+
return this.#call({
|
|
754
|
+
type: "run",
|
|
755
|
+
id: this.#nextId(),
|
|
756
|
+
prompt,
|
|
757
|
+
...runId ? { runId } : {}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
status(runId) {
|
|
761
|
+
return this.#call({
|
|
762
|
+
type: "status",
|
|
763
|
+
id: this.#nextId(),
|
|
764
|
+
runId
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
cancel(runId) {
|
|
768
|
+
return this.#call({
|
|
769
|
+
type: "cancel",
|
|
770
|
+
id: this.#nextId(),
|
|
771
|
+
runId
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
dispose() {
|
|
775
|
+
for (const [id, pending] of this.#pending) {
|
|
776
|
+
clearTimeout(pending.timer);
|
|
777
|
+
pending.resolve({
|
|
778
|
+
type: "result",
|
|
779
|
+
id,
|
|
780
|
+
error: {
|
|
781
|
+
code: "RUN_FAILED",
|
|
782
|
+
message: "Client disposed"
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
this.#pending.clear();
|
|
787
|
+
this.#worker.terminate();
|
|
788
|
+
}
|
|
789
|
+
#nextId() {
|
|
790
|
+
return `req-${++this.#seq}`;
|
|
791
|
+
}
|
|
792
|
+
#call(message) {
|
|
793
|
+
const timeoutMs = this.#timeoutMs;
|
|
794
|
+
return new Promise((resolve) => {
|
|
795
|
+
const timer = setTimeout(() => {
|
|
796
|
+
this.#pending.delete(message.id);
|
|
797
|
+
resolve({
|
|
798
|
+
type: "result",
|
|
799
|
+
id: message.id,
|
|
800
|
+
error: {
|
|
801
|
+
code: "RUN_TIMEOUT",
|
|
802
|
+
message: `Worker request "${message.type}" timed out after ${timeoutMs}ms`
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}, timeoutMs);
|
|
806
|
+
this.#pending.set(message.id, {
|
|
807
|
+
resolve,
|
|
808
|
+
timer
|
|
809
|
+
});
|
|
810
|
+
this.#worker.postMessage(message);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
async #onMessage(data) {
|
|
814
|
+
const event = parseWorkerEvent(data);
|
|
815
|
+
if (!event) return;
|
|
816
|
+
if (event.type === "interaction") {
|
|
817
|
+
let response;
|
|
818
|
+
if (!this.#uiBridge) response = {
|
|
819
|
+
id: event.request.id,
|
|
820
|
+
cancelled: true
|
|
821
|
+
};
|
|
822
|
+
else try {
|
|
823
|
+
response = await this.#uiBridge.request(event.request);
|
|
824
|
+
} catch (e) {
|
|
825
|
+
response = {
|
|
826
|
+
id: event.request.id,
|
|
827
|
+
cancelled: true
|
|
828
|
+
};
|
|
829
|
+
console.warn(`UiBridge request failed, responding as cancelled: ${e instanceof Error ? e.message : String(e)}`);
|
|
830
|
+
}
|
|
831
|
+
this.#worker.postMessage({
|
|
832
|
+
type: "interaction-response",
|
|
833
|
+
workerMessageId: event.workerMessageId,
|
|
834
|
+
response
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (event.type === "render-result") {
|
|
839
|
+
await this.#uiBridge?.renderResult?.(event.request);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (event.type === "text-delta") {
|
|
843
|
+
await this.#uiBridge?.onTextDelta?.(event.runId, event.delta);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const pending = this.#pending.get(event.id);
|
|
847
|
+
if (pending) {
|
|
848
|
+
this.#pending.delete(event.id);
|
|
849
|
+
clearTimeout(pending.timer);
|
|
850
|
+
pending.resolve(event);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
//#endregion
|
|
856
|
+
export { BrowserWorkerScriptExecutor, OpfsProvider, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, startWorkerRuntimeHost };
|