@webskill/sdk 0.0.2 → 0.0.4
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/browser.d.ts +78 -2
- package/dist/browser.js +388 -7
- package/dist/{dist-BMioPPri.js → dist-Bvhbxrrt.js} +13 -31
- package/dist/{dist-BbyIk4vG.js → dist-COsE72Ct.js} +426 -106
- package/dist/governance.d.ts +2 -2
- package/dist/governance.js +2 -2
- package/dist/{index-DF5ea732.d.ts → index-BsbfEiaC.d.ts} +3 -53
- package/dist/{index-j3Z3hbDk.d.ts → index-CqDIreSE.d.ts} +141 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/node.d.ts +3 -3
- package/dist/node.js +2 -2
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as LlmClient, C as InteractionPolicy, It as FileStat, Jt as SkillInstallSource, Lt as FileSystemProvider, N as LlmResponse, P as LlmStreamEvent, Q as RenderResultRequest, St as bridgeError, T as InteractionResponse, Zt as SkillManifest, ct as ScriptExecutionContext, d as BridgeResponse, dt as ToolDefinition, kt as parseBridgeRequest, l as BridgeCapabilities, lt as ScriptExecutor, m as ExternalToolSource, p as ExternalSkillProvider, pt as ToolResult, rn as VerifyResult, tn as SkillsLockfile, u as BridgeRequest, vt as UiBridge, w as InteractionRequest } from "./index-CqDIreSE.js";
|
|
2
2
|
//#region ../browser/dist/index.d.ts
|
|
3
3
|
//#region src/fs/featureDetection.d.ts
|
|
4
4
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
@@ -42,6 +42,60 @@ declare class OpfsProvider implements FileSystemProvider {
|
|
|
42
42
|
*/
|
|
43
43
|
declare const WORKER_BOOTSTRAP_SOURCE: string;
|
|
44
44
|
//#endregion
|
|
45
|
+
//#region src/executor/tsTranspiler.d.ts
|
|
46
|
+
interface TypeScriptSupportOptions {
|
|
47
|
+
/** 应用托管的 esbuild-wasm JS(browser build)URL;Node/vitest 可传 'esbuild-wasm' specifier */
|
|
48
|
+
esbuildUrl: string;
|
|
49
|
+
/** 应用托管的 esbuild.wasm URL;Node 下可省略(自动加载) */
|
|
50
|
+
wasmUrl?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* D4:宿主侧 .ts → JS 纯类型擦除转译(静态无执行,Worker 永远只见 JS)。
|
|
54
|
+
* OPFS 缓存:键 = 源码 sha256,跨执行/跨 Worker 只转一次。
|
|
55
|
+
*/
|
|
56
|
+
declare class TsTranspiler {
|
|
57
|
+
#private;
|
|
58
|
+
constructor(deps: {
|
|
59
|
+
fs: FileSystemProvider;
|
|
60
|
+
options: TypeScriptSupportOptions;
|
|
61
|
+
cacheDir?: string;
|
|
62
|
+
});
|
|
63
|
+
/** 返回 JS 源码;转译失败 → TOOL_EXECUTION_FAILED(带 esbuild 诊断) */
|
|
64
|
+
transpile(scriptPath: string, source: string): Promise<string>;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/skillManagement/browserSkillManager.d.ts
|
|
68
|
+
/**
|
|
69
|
+
* D5:浏览器技能安装(http(s) zip / ArrayBuffer;tar/git/npm → TOOL_UNSUPPORTED)。
|
|
70
|
+
* 流程与 Node 对齐:staging → 解析 name → validateSkills(失败零残留)→ managed root →
|
|
71
|
+
* manifest(WebCrypto sha256)→ skills.lock.json;zip-slip 防护;manifest/lockfile 与 Node 同格式。
|
|
72
|
+
*/
|
|
73
|
+
declare class BrowserSkillManager {
|
|
74
|
+
#private;
|
|
75
|
+
constructor(deps: {
|
|
76
|
+
fs: FileSystemProvider;
|
|
77
|
+
managedRoot: string;
|
|
78
|
+
fetchImpl?: typeof fetch;
|
|
79
|
+
});
|
|
80
|
+
install(source: SkillInstallSource, options?: {
|
|
81
|
+
expectedSha256?: string;
|
|
82
|
+
}): Promise<SkillManifest>;
|
|
83
|
+
uninstall(name: string): Promise<void>;
|
|
84
|
+
verifyIntegrity(name: string): Promise<VerifyResult>;
|
|
85
|
+
listInstalled(): Promise<SkillsLockfile>;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/skillManagement/zipExtract.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* zip 解包(fflate,浏览器无依赖)。
|
|
91
|
+
* 每个条目路径过 resolveInsideRoot:拒绝 `..` 段、绝对路径、反斜杠穿越(与 node 规则一致)。
|
|
92
|
+
*/
|
|
93
|
+
declare function extractZipWeb(fs: FileSystemProvider, data: Uint8Array, destRoot: string): Promise<void>;
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/skillManagement/checksumWeb.d.ts
|
|
96
|
+
/** WebCrypto sha256(浏览器/Worker 通用;Node 20+ 全局 crypto 同样可用) */
|
|
97
|
+
declare function sha256HexWeb(data: Uint8Array | string): Promise<string>;
|
|
98
|
+
//#endregion
|
|
45
99
|
//#region src/executor/workerScriptExecutor.d.ts
|
|
46
100
|
/** 主线程持有的 Worker 最小接口(vitest 可用 loopback 替身) */
|
|
47
101
|
interface WorkerLike {
|
|
@@ -62,6 +116,8 @@ declare class BrowserWorkerScriptExecutor implements ScriptExecutor {
|
|
|
62
116
|
fs: FileSystemProvider;
|
|
63
117
|
workerFactory?: WorkerFactory;
|
|
64
118
|
capabilities?: BridgeCapabilities;
|
|
119
|
+
/** D4 opt-in:配置 esbuild-wasm 资产 URL 后 .ts 经宿主侧转译执行(未配置 .ts → TOOL_UNSUPPORTED) */
|
|
120
|
+
typescript?: TypeScriptSupportOptions;
|
|
65
121
|
});
|
|
66
122
|
loadDefinition(skillRoot: string, scriptName: string): Promise<ToolDefinition>;
|
|
67
123
|
execute(input: {
|
|
@@ -94,6 +150,11 @@ type WorkerRequest = {
|
|
|
94
150
|
roots?: string[];
|
|
95
151
|
/** OPFS 根目录名(默认 'webskill');测试隔离用 */
|
|
96
152
|
opfsRootName?: string;
|
|
153
|
+
/** D4 opt-in:esbuild-wasm 资产 URL(启用 .ts 脚本宿主侧转译) */
|
|
154
|
+
typescript?: {
|
|
155
|
+
esbuildUrl: string;
|
|
156
|
+
wasmUrl?: string;
|
|
157
|
+
};
|
|
97
158
|
};
|
|
98
159
|
} | {
|
|
99
160
|
type: 'run';
|
|
@@ -104,6 +165,13 @@ type WorkerRequest = {
|
|
|
104
165
|
type: 'cancel';
|
|
105
166
|
id: string;
|
|
106
167
|
runId: string;
|
|
168
|
+
} | {
|
|
169
|
+
type: 'list-interrupted';
|
|
170
|
+
id: string;
|
|
171
|
+
} | {
|
|
172
|
+
type: 'resume';
|
|
173
|
+
id: string;
|
|
174
|
+
runId: string;
|
|
107
175
|
} | {
|
|
108
176
|
type: 'status';
|
|
109
177
|
id: string;
|
|
@@ -191,10 +259,18 @@ declare class WorkerRuntimeClient {
|
|
|
191
259
|
interaction?: InteractionPolicy;
|
|
192
260
|
roots?: string[];
|
|
193
261
|
opfsRootName?: string;
|
|
262
|
+
typescript?: {
|
|
263
|
+
esbuildUrl: string;
|
|
264
|
+
wasmUrl?: string;
|
|
265
|
+
};
|
|
194
266
|
}): Promise<WorkerEvent>;
|
|
195
267
|
run(prompt: string, runId?: string): Promise<WorkerEvent>;
|
|
196
268
|
status(runId: string): Promise<WorkerEvent>;
|
|
197
269
|
cancel(runId: string): Promise<WorkerEvent>;
|
|
270
|
+
/** D3:列出可恢复的 interrupted run */
|
|
271
|
+
listInterrupted(): Promise<WorkerEvent>;
|
|
272
|
+
/** D3:恢复 interrupted run(新 Worker 重建后重新发起等待中的交互) */
|
|
273
|
+
resume(runId: string): Promise<WorkerEvent>;
|
|
198
274
|
dispose(): void;
|
|
199
275
|
}
|
|
200
276
|
//#endregion
|
|
@@ -216,4 +292,4 @@ declare class WorkerUiBridge implements UiBridge {
|
|
|
216
292
|
onTextDelta(runId: string, delta: string): Promise<void>;
|
|
217
293
|
}
|
|
218
294
|
//#endregion
|
|
219
|
-
export { type BridgeCapabilities, type BridgeRequest, type BridgeResponse, BrowserWorkerScriptExecutor, type LlmClientConfig, type MainToWorkerMessage, OpfsProvider, type SerializedError, WORKER_BOOTSTRAP_SOURCE, type WorkerEvent, type WorkerFactory, type WorkerLike, type WorkerRequest, WorkerRuntimeClient, type WorkerRuntimeClientDeps, type WorkerRuntimeHostOverrides, type WorkerScopeLike, WorkerUiBridge, bridgeError, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, startWorkerRuntimeHost };
|
|
295
|
+
export { type BridgeCapabilities, type BridgeRequest, type BridgeResponse, BrowserSkillManager, BrowserWorkerScriptExecutor, type LlmClientConfig, type MainToWorkerMessage, OpfsProvider, type SerializedError, TsTranspiler, type TypeScriptSupportOptions, WORKER_BOOTSTRAP_SOURCE, type WorkerEvent, type WorkerFactory, type WorkerLike, type WorkerRequest, WorkerRuntimeClient, type WorkerRuntimeClientDeps, type WorkerRuntimeHostOverrides, type WorkerScopeLike, WorkerUiBridge, bridgeError, extractZipWeb, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
|
package/dist/browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as parseBridgeRequest, B as SkillDiscovery, C as bridgeError, H as WebSkillError, I as SKILLS_LOCKFILE, J as isValidSkillName, L as SKILL_MANIFEST_FILE, O as mergeCatalogEntries, U as buildCatalog, W as buildManifest, Z as parseSkillMarkdown, b as RUN_SNAPSHOT_SCHEMA_VERSION, c as FsRunSnapshotStore, et as resolveInsideRoot, g as ProgressiveRouter, h as OpenAiCompatibleClient, i as AgentLoop, k as normalizeToolContent, nt as verifyManifest, o as FsArtifactStore, p as MockLlmClient, s as FsMemoryStore, tt as validateSkills } from "./dist-COsE72Ct.js";
|
|
2
|
+
import { unzipSync } from "fflate";
|
|
2
3
|
|
|
3
4
|
//#region ../browser/dist/index.js
|
|
4
5
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
@@ -267,6 +268,264 @@ self.onmessage = async function (event) {
|
|
|
267
268
|
}
|
|
268
269
|
};
|
|
269
270
|
`;
|
|
271
|
+
let esbuildPromise;
|
|
272
|
+
async function loadEsbuild(options) {
|
|
273
|
+
esbuildPromise ??= (async () => {
|
|
274
|
+
const mod = await import(
|
|
275
|
+
/* @vite-ignore */
|
|
276
|
+
options.esbuildUrl
|
|
277
|
+
);
|
|
278
|
+
await mod.initialize(options.wasmUrl ? { wasmURL: options.wasmUrl } : {});
|
|
279
|
+
return mod;
|
|
280
|
+
})();
|
|
281
|
+
return esbuildPromise;
|
|
282
|
+
}
|
|
283
|
+
async function sha256Hex(data) {
|
|
284
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data));
|
|
285
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* D4:宿主侧 .ts → JS 纯类型擦除转译(静态无执行,Worker 永远只见 JS)。
|
|
289
|
+
* OPFS 缓存:键 = 源码 sha256,跨执行/跨 Worker 只转一次。
|
|
290
|
+
*/
|
|
291
|
+
var TsTranspiler = class {
|
|
292
|
+
#fs;
|
|
293
|
+
#options;
|
|
294
|
+
#cacheDir;
|
|
295
|
+
constructor(deps) {
|
|
296
|
+
this.#fs = deps.fs;
|
|
297
|
+
this.#options = deps.options;
|
|
298
|
+
this.#cacheDir = deps.cacheDir ?? ".webskill/ts-cache";
|
|
299
|
+
}
|
|
300
|
+
/** 返回 JS 源码;转译失败 → TOOL_EXECUTION_FAILED(带 esbuild 诊断) */
|
|
301
|
+
async transpile(scriptPath, source) {
|
|
302
|
+
const cacheKey = `${this.#cacheDir}/${await sha256Hex(`${scriptPath}\n${source}`)}.js`;
|
|
303
|
+
if (await this.#fs.exists(cacheKey)) return this.#fs.readText(cacheKey);
|
|
304
|
+
let code;
|
|
305
|
+
try {
|
|
306
|
+
code = (await (await loadEsbuild(this.#options)).transform(source, {
|
|
307
|
+
loader: "ts",
|
|
308
|
+
target: "es2022",
|
|
309
|
+
format: "esm"
|
|
310
|
+
})).code;
|
|
311
|
+
} catch (e) {
|
|
312
|
+
throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to transpile ${scriptPath}: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
313
|
+
}
|
|
314
|
+
await this.#fs.writeText(cacheKey, code);
|
|
315
|
+
return code;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
/** WebCrypto sha256(浏览器/Worker 通用;Node 20+ 全局 crypto 同样可用) */
|
|
319
|
+
async function sha256HexWeb(data) {
|
|
320
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
321
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
322
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
323
|
+
}
|
|
324
|
+
/** 递归拷贝目录(含子目录与二进制文件) */
|
|
325
|
+
async function copyDir(fs, from, to) {
|
|
326
|
+
await fs.mkdir(to);
|
|
327
|
+
for (const entry of await fs.list(from)) {
|
|
328
|
+
const name = entry.path.split("/").pop() ?? "";
|
|
329
|
+
if (entry.type === "directory") await copyDir(fs, entry.path, `${to}/${name}`);
|
|
330
|
+
else await fs.writeBinary(`${to}/${name}`, await fs.readBinary(entry.path));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** 递归列出目录内全部文件(相对路径,posix 分隔) */
|
|
334
|
+
async function listFiles(fs, root, prefix = "") {
|
|
335
|
+
const out = [];
|
|
336
|
+
for (const entry of await fs.list(root)) {
|
|
337
|
+
const name = entry.path.split("/").pop() ?? "";
|
|
338
|
+
const rel = prefix === "" ? name : `${prefix}/${name}`;
|
|
339
|
+
if (entry.type === "directory") out.push(...await listFiles(fs, entry.path, rel));
|
|
340
|
+
else out.push(rel);
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
/** 删除目录(不存在时静默) */
|
|
345
|
+
async function removeDirQuiet(fs, dir) {
|
|
346
|
+
try {
|
|
347
|
+
if (await fs.exists(dir)) await fs.remove(dir, { recursive: true });
|
|
348
|
+
} catch {}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* zip 解包(fflate,浏览器无依赖)。
|
|
352
|
+
* 每个条目路径过 resolveInsideRoot:拒绝 `..` 段、绝对路径、反斜杠穿越(与 node 规则一致)。
|
|
353
|
+
*/
|
|
354
|
+
async function extractZipWeb(fs, data, destRoot) {
|
|
355
|
+
let entries;
|
|
356
|
+
try {
|
|
357
|
+
entries = unzipSync(data);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
throw new WebSkillError("INSTALL_FAILED", `Failed to read zip archive: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
360
|
+
}
|
|
361
|
+
for (const [entryPath, content] of Object.entries(entries)) {
|
|
362
|
+
const target = resolveInsideRoot(destRoot, entryPath);
|
|
363
|
+
if (entryPath.endsWith("/")) await fs.mkdir(target);
|
|
364
|
+
else await fs.writeBinary(target, content);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
368
|
+
const asInstallFailed = (e) => e instanceof WebSkillError && (e.code === "INSTALL_FAILED" || e.code === "TOOL_UNSUPPORTED") ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf$1(e)}`, e);
|
|
369
|
+
const lockfilePath = (root) => `${root}/${SKILLS_LOCKFILE}`;
|
|
370
|
+
/**
|
|
371
|
+
* D5:浏览器技能安装(http(s) zip / ArrayBuffer;tar/git/npm → TOOL_UNSUPPORTED)。
|
|
372
|
+
* 流程与 Node 对齐:staging → 解析 name → validateSkills(失败零残留)→ managed root →
|
|
373
|
+
* manifest(WebCrypto sha256)→ skills.lock.json;zip-slip 防护;manifest/lockfile 与 Node 同格式。
|
|
374
|
+
*/
|
|
375
|
+
var BrowserSkillManager = class {
|
|
376
|
+
#fs;
|
|
377
|
+
#managedRoot;
|
|
378
|
+
#fetchImpl;
|
|
379
|
+
constructor(deps) {
|
|
380
|
+
this.#fs = deps.fs;
|
|
381
|
+
this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
|
|
382
|
+
this.#fetchImpl = deps.fetchImpl;
|
|
383
|
+
}
|
|
384
|
+
async install(source, options) {
|
|
385
|
+
const fs = this.#fs;
|
|
386
|
+
const stagingRoot = `${this.#managedRoot}/.staging-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
387
|
+
let targetDir;
|
|
388
|
+
try {
|
|
389
|
+
if (source.type !== "http" && source.type !== "archive") throw new WebSkillError("TOOL_UNSUPPORTED", `Install source "${source.type}" is not supported in the browser (http zip / archive only)`);
|
|
390
|
+
let data;
|
|
391
|
+
if (source.type === "http") {
|
|
392
|
+
let res;
|
|
393
|
+
try {
|
|
394
|
+
res = await (this.#fetchImpl ?? fetch)(source.url);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
|
|
397
|
+
}
|
|
398
|
+
if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
|
|
399
|
+
data = new Uint8Array(await res.arrayBuffer());
|
|
400
|
+
} else data = new Uint8Array(source.data);
|
|
401
|
+
if (options?.expectedSha256 !== void 0) {
|
|
402
|
+
const actual = await sha256HexWeb(data);
|
|
403
|
+
if (actual !== options.expectedSha256) throw new WebSkillError("INSTALL_FAILED", `Archive checksum mismatch: expected ${options.expectedSha256}, got ${actual}`);
|
|
404
|
+
}
|
|
405
|
+
const contentDir = `${stagingRoot}/content`;
|
|
406
|
+
await fs.mkdir(contentDir);
|
|
407
|
+
await extractZipWeb(fs, data, contentDir);
|
|
408
|
+
let skillDir = contentDir;
|
|
409
|
+
if (!await fs.exists(`${contentDir}/SKILL.md`)) {
|
|
410
|
+
const dirs = (await fs.list(contentDir)).filter((e) => e.type === "directory");
|
|
411
|
+
if (dirs.length === 1 && await fs.exists(`${dirs[0]?.path}/SKILL.md`)) skillDir = dirs[0].path;
|
|
412
|
+
else throw new WebSkillError("INSTALL_FAILED", "Archive does not contain a SKILL.md at its root or in a single top-level directory");
|
|
413
|
+
}
|
|
414
|
+
let name;
|
|
415
|
+
let version;
|
|
416
|
+
try {
|
|
417
|
+
const { metadata } = parseSkillMarkdown(await fs.readText(`${skillDir}/SKILL.md`));
|
|
418
|
+
name = metadata.name;
|
|
419
|
+
version = typeof metadata["version"] === "string" ? metadata["version"] : void 0;
|
|
420
|
+
} catch (e) {
|
|
421
|
+
throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md: ${messageOf$1(e)}`, e);
|
|
422
|
+
}
|
|
423
|
+
const finalRoot = `${stagingRoot}/final`;
|
|
424
|
+
const finalDir = `${finalRoot}/${name}`;
|
|
425
|
+
await copyDir(fs, skillDir, finalDir);
|
|
426
|
+
const report = await validateSkills(fs, [finalRoot]);
|
|
427
|
+
if (!report.ok) {
|
|
428
|
+
const errors = report.issues.filter((i) => i.severity === "error");
|
|
429
|
+
throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
430
|
+
}
|
|
431
|
+
targetDir = `${this.#managedRoot}/${name}`;
|
|
432
|
+
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
433
|
+
await copyDir(fs, finalDir, targetDir);
|
|
434
|
+
const manifest = await this.#createManifest(targetDir, {
|
|
435
|
+
name,
|
|
436
|
+
...version ? { version } : {},
|
|
437
|
+
source
|
|
438
|
+
});
|
|
439
|
+
await this.#upsertLockEntry(name, {
|
|
440
|
+
digest: manifest.integrity.digest,
|
|
441
|
+
installedAt: manifest.installedAt,
|
|
442
|
+
source
|
|
443
|
+
});
|
|
444
|
+
targetDir = void 0;
|
|
445
|
+
return manifest;
|
|
446
|
+
} catch (e) {
|
|
447
|
+
if (targetDir) await removeDirQuiet(fs, targetDir);
|
|
448
|
+
throw asInstallFailed(e);
|
|
449
|
+
} finally {
|
|
450
|
+
await removeDirQuiet(fs, stagingRoot);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async uninstall(name) {
|
|
454
|
+
if (!isValidSkillName(name)) throw new WebSkillError("UNINSTALL_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
|
|
455
|
+
const targetDir = `${this.#managedRoot}/${name}`;
|
|
456
|
+
if (!await this.#fs.exists(targetDir)) throw new WebSkillError("UNINSTALL_FAILED", `Skill "${name}" is not installed`);
|
|
457
|
+
try {
|
|
458
|
+
await this.#fs.remove(targetDir, { recursive: true });
|
|
459
|
+
await this.#removeLockEntry(name);
|
|
460
|
+
} catch (e) {
|
|
461
|
+
throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf$1(e)}`, e);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async verifyIntegrity(name) {
|
|
465
|
+
if (!isValidSkillName(name)) throw new WebSkillError("INTEGRITY_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
|
|
466
|
+
const manifest = await this.#readManifest(`${this.#managedRoot}/${name}`);
|
|
467
|
+
const actualHashes = /* @__PURE__ */ new Map();
|
|
468
|
+
for (const file of manifest.files) {
|
|
469
|
+
const path = `${this.#managedRoot}/${name}/${file.path}`;
|
|
470
|
+
if (await this.#fs.exists(path)) actualHashes.set(file.path, await sha256HexWeb(await this.#fs.readBinary(path)));
|
|
471
|
+
}
|
|
472
|
+
return verifyManifest(manifest, actualHashes);
|
|
473
|
+
}
|
|
474
|
+
async listInstalled() {
|
|
475
|
+
const path = lockfilePath(this.#managedRoot);
|
|
476
|
+
if (!await this.#fs.exists(path)) return {
|
|
477
|
+
schemaVersion: 1,
|
|
478
|
+
skills: {}
|
|
479
|
+
};
|
|
480
|
+
try {
|
|
481
|
+
return JSON.parse(await this.#fs.readText(path));
|
|
482
|
+
} catch (e) {
|
|
483
|
+
throw new WebSkillError("INTEGRITY_FAILED", `Skills lockfile at ${path} is corrupted: ${messageOf$1(e)}`, e);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async #createManifest(skillRoot, input) {
|
|
487
|
+
const excluded = /* @__PURE__ */ new Set([SKILL_MANIFEST_FILE, SKILLS_LOCKFILE]);
|
|
488
|
+
const files = [];
|
|
489
|
+
for (const rel of (await listFiles(this.#fs, skillRoot)).sort()) {
|
|
490
|
+
if (excluded.has(rel.split("/").pop() ?? "")) continue;
|
|
491
|
+
const content = await this.#fs.readBinary(`${skillRoot}/${rel}`);
|
|
492
|
+
files.push({
|
|
493
|
+
path: rel,
|
|
494
|
+
size: content.length,
|
|
495
|
+
sha256: await sha256HexWeb(content)
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const manifest = await buildManifest({
|
|
499
|
+
name: input.name,
|
|
500
|
+
...input.version !== void 0 ? { version: input.version } : {},
|
|
501
|
+
source: input.source,
|
|
502
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
503
|
+
files,
|
|
504
|
+
sha256: sha256HexWeb
|
|
505
|
+
});
|
|
506
|
+
await this.#fs.writeText(`${skillRoot}/${SKILL_MANIFEST_FILE}`, JSON.stringify(manifest, null, 2));
|
|
507
|
+
return manifest;
|
|
508
|
+
}
|
|
509
|
+
async #readManifest(skillRoot) {
|
|
510
|
+
const path = `${skillRoot}/${SKILL_MANIFEST_FILE}`;
|
|
511
|
+
if (!await this.#fs.exists(path)) throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest not found at ${path} (was the skill installed via BrowserSkillManager?)`);
|
|
512
|
+
try {
|
|
513
|
+
return JSON.parse(await this.#fs.readText(path));
|
|
514
|
+
} catch (e) {
|
|
515
|
+
throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${messageOf$1(e)}`, e);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async #upsertLockEntry(name, entry) {
|
|
519
|
+
const lockfile = await this.listInstalled();
|
|
520
|
+
lockfile.skills[name] = entry;
|
|
521
|
+
await this.#fs.writeText(lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
|
|
522
|
+
}
|
|
523
|
+
async #removeLockEntry(name) {
|
|
524
|
+
const lockfile = await this.listInstalled();
|
|
525
|
+
delete lockfile.skills[name];
|
|
526
|
+
await this.#fs.writeText(lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
|
|
527
|
+
}
|
|
528
|
+
};
|
|
270
529
|
const defaultWorkerFactory = () => {
|
|
271
530
|
const blob = new Blob([WORKER_BOOTSTRAP_SOURCE], { type: "text/javascript" });
|
|
272
531
|
const url = URL.createObjectURL(blob);
|
|
@@ -284,9 +543,14 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
284
543
|
#fs;
|
|
285
544
|
#workerFactory;
|
|
286
545
|
#capabilities;
|
|
546
|
+
#transpiler;
|
|
287
547
|
constructor(deps) {
|
|
288
548
|
this.#fs = deps.fs;
|
|
289
549
|
this.#workerFactory = deps.workerFactory ?? defaultWorkerFactory;
|
|
550
|
+
this.#transpiler = deps.typescript ? new TsTranspiler({
|
|
551
|
+
fs: deps.fs,
|
|
552
|
+
options: deps.typescript
|
|
553
|
+
}) : void 0;
|
|
290
554
|
this.#capabilities = {
|
|
291
555
|
readReference: deps.capabilities?.readReference ?? true,
|
|
292
556
|
writeArtifact: deps.capabilities?.writeArtifact ?? true,
|
|
@@ -299,7 +563,13 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
299
563
|
const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
|
|
300
564
|
const [hasJs, hasTs] = await Promise.all([this.#fs.exists(jsPath), this.#fs.exists(tsPath)]);
|
|
301
565
|
if (hasJs && hasTs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
|
|
302
|
-
if (hasTs)
|
|
566
|
+
if (hasTs) {
|
|
567
|
+
if (!this.#transpiler) throw new WebSkillError("TOOL_UNSUPPORTED", `Browser sandbox executes .js only unless TypeScript support is configured ("${scriptName}.ts")`);
|
|
568
|
+
return {
|
|
569
|
+
path: tsPath,
|
|
570
|
+
source: await this.#transpiler.transpile(tsPath, await this.#fs.readText(tsPath))
|
|
571
|
+
};
|
|
572
|
+
}
|
|
303
573
|
if (!hasJs) throw new WebSkillError("TOOL_NOT_FOUND", `Script "${scriptName}" not found under ${skillRoot}/scripts`);
|
|
304
574
|
return {
|
|
305
575
|
path: jsPath,
|
|
@@ -474,11 +744,16 @@ function parseMainMessage(data) {
|
|
|
474
744
|
...isNonEmptyString(data["runId"]) ? { runId: data["runId"] } : {}
|
|
475
745
|
} : void 0;
|
|
476
746
|
case "cancel":
|
|
477
|
-
case "status":
|
|
747
|
+
case "status":
|
|
748
|
+
case "resume": return isNonEmptyString(data["id"]) && isNonEmptyString(data["runId"]) ? {
|
|
478
749
|
type: data["type"],
|
|
479
750
|
id: data["id"],
|
|
480
751
|
runId: data["runId"]
|
|
481
752
|
} : void 0;
|
|
753
|
+
case "list-interrupted": return isNonEmptyString(data["id"]) ? {
|
|
754
|
+
type: "list-interrupted",
|
|
755
|
+
id: data["id"]
|
|
756
|
+
} : void 0;
|
|
482
757
|
case "interaction-response": {
|
|
483
758
|
const response = data["response"];
|
|
484
759
|
return isNonEmptyString(data["workerMessageId"]) && isRecord(response) && isNonEmptyString(response["id"]) ? {
|
|
@@ -616,10 +891,15 @@ function startWorkerRuntimeHost(scope, overrides = {}) {
|
|
|
616
891
|
llm: (overrides.createLlm ?? defaultCreateLlm)(config.llm),
|
|
617
892
|
executor: new BrowserWorkerScriptExecutor({
|
|
618
893
|
fs,
|
|
619
|
-
...overrides.workerFactory ? { workerFactory: overrides.workerFactory } : {}
|
|
894
|
+
...overrides.workerFactory ? { workerFactory: overrides.workerFactory } : {},
|
|
895
|
+
...config.typescript ? { typescript: config.typescript } : {}
|
|
620
896
|
}),
|
|
621
897
|
...config.interaction ? { interaction: config.interaction } : {},
|
|
622
|
-
roots: config.roots ?? ["/skills"]
|
|
898
|
+
roots: config.roots ?? ["/skills"],
|
|
899
|
+
snapshotStore: new FsRunSnapshotStore({
|
|
900
|
+
root: "snapshots",
|
|
901
|
+
fs
|
|
902
|
+
})
|
|
623
903
|
};
|
|
624
904
|
post({
|
|
625
905
|
type: "ready",
|
|
@@ -665,7 +945,8 @@ function startWorkerRuntimeHost(scope, overrides = {}) {
|
|
|
665
945
|
uiBridge,
|
|
666
946
|
...state.interaction ? { interaction: state.interaction } : {},
|
|
667
947
|
...overrides.externalTools ? { externalTools: overrides.externalTools } : {},
|
|
668
|
-
...overrides.skillProviders ? { skillProviders: overrides.skillProviders } : {}
|
|
948
|
+
...overrides.skillProviders ? { skillProviders: overrides.skillProviders } : {},
|
|
949
|
+
snapshotStore: state.snapshotStore
|
|
669
950
|
}).run({
|
|
670
951
|
sessionId: `session-${runId}`,
|
|
671
952
|
userPrompt: prompt,
|
|
@@ -706,6 +987,91 @@ function startWorkerRuntimeHost(scope, overrides = {}) {
|
|
|
706
987
|
} } });
|
|
707
988
|
return;
|
|
708
989
|
}
|
|
990
|
+
case "list-interrupted":
|
|
991
|
+
if (!state) {
|
|
992
|
+
result(message.id, { error: {
|
|
993
|
+
code: "RUN_FAILED",
|
|
994
|
+
message: "Host is not initialized"
|
|
995
|
+
} });
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
state.snapshotStore.list().then((snapshots) => result(message.id, { result: { snapshots } }), (e) => result(message.id, { error: serializeError(e) }));
|
|
999
|
+
return;
|
|
1000
|
+
case "resume":
|
|
1001
|
+
if (!state) {
|
|
1002
|
+
result(message.id, { error: {
|
|
1003
|
+
code: "RUN_FAILED",
|
|
1004
|
+
message: "Host is not initialized"
|
|
1005
|
+
} });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
(async () => {
|
|
1009
|
+
const snapshot = await state.snapshotStore.load(message.runId);
|
|
1010
|
+
if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${message.runId}"`);
|
|
1011
|
+
if (snapshot.schemaVersion !== 1) {
|
|
1012
|
+
await state.snapshotStore.delete(message.runId);
|
|
1013
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${message.runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
|
|
1016
|
+
await state.snapshotStore.delete(message.runId);
|
|
1017
|
+
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1018
|
+
return {
|
|
1019
|
+
output: "Interaction timed out before the run was resumed",
|
|
1020
|
+
run: {
|
|
1021
|
+
id: message.runId,
|
|
1022
|
+
sessionId: snapshot.sessionId,
|
|
1023
|
+
status: "failed",
|
|
1024
|
+
phase: "fail",
|
|
1025
|
+
userPrompt: snapshot.userPrompt,
|
|
1026
|
+
startedAt: snapshot.startedAt,
|
|
1027
|
+
endedAt,
|
|
1028
|
+
terminationReason: "interaction-timeout",
|
|
1029
|
+
activeSkillNames: snapshot.activeSkillNames,
|
|
1030
|
+
artifacts: [],
|
|
1031
|
+
trace: [{
|
|
1032
|
+
id: `evt-expired-${message.runId}`,
|
|
1033
|
+
runId: message.runId,
|
|
1034
|
+
ts: endedAt,
|
|
1035
|
+
type: "run.failed",
|
|
1036
|
+
message: `Interaction timed out before the run was resumed (expired at ${snapshot.interactionExpiresAt})`,
|
|
1037
|
+
data: {
|
|
1038
|
+
reason: "interaction-timeout",
|
|
1039
|
+
code: "RUN_INTERACTION_TIMEOUT"
|
|
1040
|
+
}
|
|
1041
|
+
}]
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const resumeUiBridge = new WorkerUiBridge(scope);
|
|
1046
|
+
activeUiBridge = resumeUiBridge;
|
|
1047
|
+
try {
|
|
1048
|
+
return await new AgentLoop({
|
|
1049
|
+
llm: state.llm,
|
|
1050
|
+
executor: state.executor,
|
|
1051
|
+
artifactStore: new FsArtifactStore({
|
|
1052
|
+
root: "artifacts",
|
|
1053
|
+
fs: state.fs
|
|
1054
|
+
}),
|
|
1055
|
+
memory: new FsMemoryStore({
|
|
1056
|
+
root: "memory",
|
|
1057
|
+
fs: state.fs
|
|
1058
|
+
}),
|
|
1059
|
+
fs: state.fs,
|
|
1060
|
+
skillIndex: /* @__PURE__ */ new Map(),
|
|
1061
|
+
uiBridge: resumeUiBridge,
|
|
1062
|
+
...state.interaction ? { interaction: state.interaction } : {},
|
|
1063
|
+
...overrides.externalTools ? { externalTools: overrides.externalTools } : {},
|
|
1064
|
+
...overrides.skillProviders ? { skillProviders: overrides.skillProviders } : {},
|
|
1065
|
+
snapshotStore: state.snapshotStore
|
|
1066
|
+
}).resume(snapshot);
|
|
1067
|
+
} finally {
|
|
1068
|
+
activeUiBridge = void 0;
|
|
1069
|
+
}
|
|
1070
|
+
})().then((runResult) => {
|
|
1071
|
+
runs.set(message.runId, { status: runResult.run.status });
|
|
1072
|
+
result(message.id, { result: runResult });
|
|
1073
|
+
}, (e) => result(message.id, { error: serializeError(e) }));
|
|
1074
|
+
return;
|
|
709
1075
|
case "cancel":
|
|
710
1076
|
if (!runs.get(message.runId)) {
|
|
711
1077
|
result(message.id, { error: {
|
|
@@ -771,6 +1137,21 @@ var WorkerRuntimeClient = class {
|
|
|
771
1137
|
runId
|
|
772
1138
|
});
|
|
773
1139
|
}
|
|
1140
|
+
/** D3:列出可恢复的 interrupted run */
|
|
1141
|
+
listInterrupted() {
|
|
1142
|
+
return this.#call({
|
|
1143
|
+
type: "list-interrupted",
|
|
1144
|
+
id: this.#nextId()
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
/** D3:恢复 interrupted run(新 Worker 重建后重新发起等待中的交互) */
|
|
1148
|
+
resume(runId) {
|
|
1149
|
+
return this.#call({
|
|
1150
|
+
type: "resume",
|
|
1151
|
+
id: this.#nextId(),
|
|
1152
|
+
runId
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
774
1155
|
dispose() {
|
|
775
1156
|
for (const [id, pending] of this.#pending) {
|
|
776
1157
|
clearTimeout(pending.timer);
|
|
@@ -853,4 +1234,4 @@ var WorkerRuntimeClient = class {
|
|
|
853
1234
|
};
|
|
854
1235
|
|
|
855
1236
|
//#endregion
|
|
856
|
-
export { BrowserWorkerScriptExecutor, OpfsProvider, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, startWorkerRuntimeHost };
|
|
1237
|
+
export { BrowserSkillManager, BrowserWorkerScriptExecutor, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, extractZipWeb, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
|