@webskill/sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent.d.ts +2 -2
  2. package/dist/agent.js +57 -15
  3. package/dist/browser.d.ts +101 -5
  4. package/dist/browser.js +421 -11
  5. package/dist/{catalogComponents-DV7cPpUm-C77AEEx9.js → catalogComponents-Dr5dFMAb-Dacibl1e.js} +126 -44
  6. package/dist/{dist-bewtXYlO.js → dist-DnYG2-eY.js} +53 -14
  7. package/dist/{dist-6C03DShK.js → dist-DusANsrn.js} +981 -185
  8. package/dist/{env--jJB-TSX-04klhTYi.js → env-8cY40DXB-CGnEVZby.js} +7 -6
  9. package/dist/{env-BPUBZCwJ-4jat_SVG.d.ts → env-AK3cSMEA-Dli6QU5E.d.ts} +4 -3
  10. package/dist/governance.d.ts +3 -3
  11. package/dist/governance.js +1 -1
  12. package/dist/{index-Bsqg4ftU.d.ts → index-BMocOEi0.d.ts} +13 -7
  13. package/dist/{index-D_7ZZjkl.d.ts → index-BuTpBMzr.d.ts} +69 -6
  14. package/dist/{index-vBz_FC9w.d.ts → index-C-KFAZoF.d.ts} +298 -52
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +3 -3
  17. package/dist/mcp.d.ts +39 -8
  18. package/dist/mcp.js +53 -19
  19. package/dist/{memoryArtifactStore-BtOeB_hm-tj3fC5ip.js → memoryArtifactStore-52Zn9npI-BMPYwvoy.js} +10 -2
  20. package/dist/node.d.ts +4 -4
  21. package/dist/node.js +9 -2
  22. package/dist/{openUiLibrary-W3Ce896k-ClFTRZFs.js → openUiLibrary-Bdrji9qK-DzAxRlTY.js} +3 -3
  23. package/dist/{skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts → skillVersionStore-BzLbzFOL-CxdAFWO2.d.ts} +1 -1
  24. package/dist/{testing-DDCJWvgA.js → testing-CYTFqkDm.js} +1 -1
  25. package/dist/testing.d.ts +2 -2
  26. package/dist/testing.js +3 -3
  27. package/dist/{types-D_hoCri8-BnNPiZCi.d.ts → types-4pg-qp_I-Gq63X8Oa.d.ts} +33 -8
  28. package/dist/ui-react.d.ts +2 -2
  29. package/dist/ui-react.js +66 -23
  30. package/dist/ui-vue.d.ts +1 -1
  31. package/dist/ui-vue.js +13 -8
  32. package/dist/ui.d.ts +3 -3
  33. package/dist/ui.js +2 -2
  34. package/dist/{webskillLitCatalog-_mugzRHx-DiuJpCuf.js → webskillLitCatalog-_mugzRHx-B_54vxum.js} +1 -1
  35. package/package.json +1 -1
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { A as parseSkillMarkdown, B as unzipWithLimits, H as verifyManifest, L as resolveInsideRoot, M as readResponseWithLimit, N as readSkillSignature, O as messageOf, T as isValidSkillName, U as verifySkillSignature, V as validateSkills, _ as atomicWriteText, f as SkillDiscovery, h as assertRemoteUrlAllowed, j as parseSkillPackManifest, m as WebSkillError, o as SKILLS_LOCKFILE, r as MANIFEST_EXCLUDED_FILES, s as SKILL_MANIFEST_FILE, u as SKILL_PACK_FILE, v as buildCatalog, w as exportSkills, y as buildManifest } from "./dist-8oQRa8Xz.js";
2
- import { J as normalizeToolError, P as createWebSkillApi, U as mergeCatalogEntries, W as networkPolicyLibSource, Y as parseBridgeRequest, a as AnthropicClient, d as FsMemoryStore, f as FsRunSnapshotStore, g as GoogleGenAiClient, i as AgentLoop, k as bridgeError, o as CapabilityApproval, q as normalizeToolContent, u as FsArtifactStore, v as OpenAiCompatibleClient, y as ProgressiveRouter } from "./dist-6C03DShK.js";
3
- import { n as MockLlmClient } from "./testing-DDCJWvgA.js";
2
+ import { H as createWebSkillApi, S as ProgressiveRouter, a as AgentLoop, at as normalizeToolContent, c as CapabilityApproval, et as mergeCatalogEntries, h as FsRunSnapshotStore, m as FsMemoryStore, nt as networkPolicyLibSource, o as AnthropicClient, ot as normalizeToolError, p as FsArtifactStore, st as parseBridgeRequest, x as OpenAiCompatibleClient, y as GoogleGenAiClient, z as bridgeError } from "./dist-DusANsrn.js";
3
+ import { n as MockLlmClient } from "./testing-CYTFqkDm.js";
4
4
 
5
5
  //#region ../browser/dist/index.js
6
6
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -63,7 +63,12 @@ var OpfsProvider = class {
63
63
  const name = segments.pop();
64
64
  if (!name) throw new WebSkillError("FS_NOT_FOUND", `Invalid file path: ${p}`);
65
65
  const handle = await (await this.#walkDir(`/${segments.join("/")}`, true)).getFileHandle(name, { create: true });
66
- const size = (await handle.getFile()).size;
66
+ let size = 0;
67
+ try {
68
+ size = (await handle.getFile()).size;
69
+ } catch (e) {
70
+ if (!isDomException(e, "NotFoundError")) throw e;
71
+ }
67
72
  const writable = await handle.createWritable({ keepExistingData: true });
68
73
  await writable.write({
69
74
  type: "write",
@@ -1417,12 +1422,206 @@ function startDictation(options) {
1417
1422
  recognition.stop();
1418
1423
  } };
1419
1424
  }
1425
+ /** 动图无法在不丢帧的前提下走单帧编码管线,超限直接拒绝(裁决 D-2) */
1426
+ const UNCOMPRESSABLE_MIME_TYPES = /* @__PURE__ */ new Set(["image/gif"]);
1427
+ const QUALITY_STEPS = [
1428
+ .9,
1429
+ .7,
1430
+ .5
1431
+ ];
1432
+ const SCALE_STEP = .75;
1433
+ const MAX_ROUNDS = 5;
1434
+ /**
1435
+ * 把图片压到字节预算内(0.6.0 FR-11.3)。
1436
+ * 先逐级降质,仍超限则按长边逐步缩放,最多 5 轮;仍超限抛 `ATTACHMENT_TOO_LARGE`。
1437
+ *
1438
+ * 编码一律交给平台(`OffscreenCanvas.convertToBlob`),不手写编码器(AGENTS.md §6)。
1439
+ * 因此本函数只能在浏览器里用,落在 `@webskill/browser` 而不是 core/runtime(AC-G6)。
1440
+ */
1441
+ async function compressImageToBudget(file, maxBytes) {
1442
+ const originalBytes = file.size;
1443
+ if (originalBytes <= maxBytes) return {
1444
+ blob: file,
1445
+ originalBytes,
1446
+ compressedBytes: originalBytes,
1447
+ scaled: false
1448
+ };
1449
+ if (UNCOMPRESSABLE_MIME_TYPES.has(file.type)) throw new WebSkillError("ATTACHMENT_TYPE_REJECTED", `GIF cannot be compressed without dropping animation; the file is ${formatBytes(originalBytes)} which exceeds the ${formatBytes(maxBytes)} limit`);
1450
+ if (typeof createImageBitmap !== "function" || typeof OffscreenCanvas !== "function") throw new WebSkillError("ATTACHMENT_TOO_LARGE", `Image is ${formatBytes(originalBytes)} which exceeds the ${formatBytes(maxBytes)} limit, and this browser cannot re-encode images`);
1451
+ const bitmap = await createImageBitmap(file);
1452
+ try {
1453
+ let width = bitmap.width;
1454
+ let height = bitmap.height;
1455
+ let scaled = false;
1456
+ let best;
1457
+ for (let round = 0; round < MAX_ROUNDS; round += 1) {
1458
+ for (const quality of QUALITY_STEPS) {
1459
+ const blob = await encode(bitmap, width, height, quality);
1460
+ const candidate = {
1461
+ blob,
1462
+ originalBytes,
1463
+ compressedBytes: blob.size,
1464
+ scaled,
1465
+ quality
1466
+ };
1467
+ if (blob.size <= maxBytes) return candidate;
1468
+ if (!best || blob.size < best.compressedBytes) best = candidate;
1469
+ }
1470
+ width = Math.max(1, Math.round(width * SCALE_STEP));
1471
+ height = Math.max(1, Math.round(height * SCALE_STEP));
1472
+ scaled = true;
1473
+ }
1474
+ throw new WebSkillError("ATTACHMENT_TOO_LARGE", `Image is still ${formatBytes(best?.compressedBytes ?? originalBytes)} after compression, which exceeds the ${formatBytes(maxBytes)} limit`);
1475
+ } finally {
1476
+ bitmap.close();
1477
+ }
1478
+ }
1479
+ async function encode(bitmap, width, height, quality) {
1480
+ const canvas = new OffscreenCanvas(width, height);
1481
+ const ctx = canvas.getContext("2d");
1482
+ if (!ctx) throw new WebSkillError("ATTACHMENT_TOO_LARGE", "Failed to acquire a 2D canvas context for image compression");
1483
+ ctx.drawImage(bitmap, 0, 0, width, height);
1484
+ return canvas.convertToBlob({
1485
+ type: "image/jpeg",
1486
+ quality
1487
+ });
1488
+ }
1489
+ function formatBytes(bytes) {
1490
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
1491
+ }
1492
+ /** @experimental */
1493
+ function isCaptureFailure(result) {
1494
+ return "reason" in result;
1495
+ }
1496
+ const CAPTURABLE_TAGS = /* @__PURE__ */ new Set([
1497
+ "IMG",
1498
+ "CANVAS",
1499
+ "SVG"
1500
+ ]);
1501
+ /** 只有这三种标签能取像;其余元素连取像入口都不进(FR-12.1,由 T-02-15 守门) @experimental */
1502
+ function isCapturableElement(element) {
1503
+ return CAPTURABLE_TAGS.has(element.tagName.toUpperCase());
1504
+ }
1505
+ function base64Of(bytes) {
1506
+ let binary = "";
1507
+ const chunk = 32768;
1508
+ for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
1509
+ return btoa(binary);
1510
+ }
1511
+ function splitDataUrl(dataUrl) {
1512
+ const comma = dataUrl.indexOf(",");
1513
+ if (!dataUrl.startsWith("data:") || comma === -1) return void 0;
1514
+ const meta = dataUrl.slice(5, comma);
1515
+ if (!meta.endsWith(";base64")) return void 0;
1516
+ return {
1517
+ mimeType: meta.slice(0, -7),
1518
+ data: dataUrl.slice(comma + 1)
1519
+ };
1520
+ }
1521
+ /** 压到预算内;压不下去时把失败原因原样上抛给调用方转成 `CaptureFailure` */
1522
+ async function toBudget(id, level, blob, maxBytes) {
1523
+ const compressed = await compressImageToBudget(blob, maxBytes);
1524
+ const buffer = await compressed.blob.arrayBuffer();
1525
+ return {
1526
+ id,
1527
+ mimeType: compressed.blob.type === "" ? "application/octet-stream" : compressed.blob.type,
1528
+ data: base64Of(new Uint8Array(buffer)),
1529
+ level,
1530
+ originalBytes: compressed.originalBytes,
1531
+ bytes: compressed.compressedBytes
1532
+ };
1533
+ }
1534
+ /** L1:`<img>` 取原图字节。跨域且无 CORS 头时 fetch 失败,不改用 canvas 重绘(同样会被污染) */
1535
+ async function captureImg(element, id, maxBytes) {
1536
+ const src = element.currentSrc !== "" ? element.currentSrc : element.src;
1537
+ if (src === "") throw new Error("the <img> element has no resolved source");
1538
+ const inline = splitDataUrl(src);
1539
+ if (inline) {
1540
+ const bytes = Uint8Array.from(atob(inline.data), (char) => char.charCodeAt(0));
1541
+ return toBudget(id, "src", new Blob([bytes], { type: inline.mimeType }), maxBytes);
1542
+ }
1543
+ const response = await fetch(src, { mode: "cors" });
1544
+ if (response.type === "opaque") throw new Error("the response is opaque, so its bytes cannot be read");
1545
+ if (!response.ok) throw new Error(`fetching the source returned HTTP ${response.status}`);
1546
+ return toBudget(id, "src", await response.blob(), maxBytes);
1547
+ }
1548
+ /** L2:`<canvas>` 读回。画布被跨域内容污染时 `toDataURL` 抛 SecurityError,不再兜底(裁决 D-3) */
1549
+ async function captureCanvas(element, id, maxBytes) {
1550
+ const parsed = splitDataUrl(element.toDataURL("image/png"));
1551
+ if (parsed === void 0) throw new Error("the canvas produced an unreadable data URL");
1552
+ const bytes = Uint8Array.from(atob(parsed.data), (char) => char.charCodeAt(0));
1553
+ return toBudget(id, "canvas", new Blob([bytes], { type: parsed.mimeType }), maxBytes);
1554
+ }
1555
+ /** L2:内联 `<svg>` 序列化。SVG 是文本,不进压缩管线——重编码成位图反而丢信息 */
1556
+ function captureSvg(element, id, maxBytes) {
1557
+ const markup = new XMLSerializer().serializeToString(element);
1558
+ const data = base64Of(new TextEncoder().encode(markup));
1559
+ const bytes = data.length;
1560
+ if (bytes > maxBytes) throw new Error(`the serialized SVG is ${bytes} bytes, which exceeds the budget`);
1561
+ return {
1562
+ id,
1563
+ mimeType: "image/svg+xml",
1564
+ data,
1565
+ level: "canvas",
1566
+ originalBytes: bytes,
1567
+ bytes
1568
+ };
1569
+ }
1570
+ /**
1571
+ * 取一个元素的图像(0.6.0 FR-12.1)。
1572
+ *
1573
+ * 按元素类型分派,不做无意义的尝试:`<img>` 只走 L1,`<canvas>` / `<svg>` 只走 L2,
1574
+ * 两者互不兜底。失败返回 `CaptureFailure` 而不是抛错——一张图抓不到不该让整次感知失败。
1575
+ * @experimental
1576
+ */
1577
+ async function captureElementImage(element, options) {
1578
+ const { id, maxBytes } = options;
1579
+ const tag = element.tagName.toUpperCase();
1580
+ if (tag === "IMG") try {
1581
+ return await captureImg(element, id, maxBytes);
1582
+ } catch (e) {
1583
+ return {
1584
+ id,
1585
+ reason: `L1 could not read the image source: ${describeError(e)}`,
1586
+ triedLevels: ["src"]
1587
+ };
1588
+ }
1589
+ if (tag === "CANVAS") try {
1590
+ return await captureCanvas(element, id, maxBytes);
1591
+ } catch (e) {
1592
+ return {
1593
+ id,
1594
+ reason: `L2 could not read the canvas: ${describeError(e)}`,
1595
+ triedLevels: ["canvas"]
1596
+ };
1597
+ }
1598
+ if (tag === "SVG") try {
1599
+ return captureSvg(element, id, maxBytes);
1600
+ } catch (e) {
1601
+ return {
1602
+ id,
1603
+ reason: `L2 could not serialize the SVG: ${describeError(e)}`,
1604
+ triedLevels: ["canvas"]
1605
+ };
1606
+ }
1607
+ return {
1608
+ id,
1609
+ reason: `<${element.tagName.toLowerCase()}> is not a capturable element`,
1610
+ triedLevels: []
1611
+ };
1612
+ }
1613
+ function describeError(e) {
1614
+ if (e instanceof DOMException && e.name === "SecurityError") return "the canvas is tainted by cross-origin data";
1615
+ if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
1616
+ return e instanceof Error ? e.message : String(e);
1617
+ }
1420
1618
  /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值 */
1421
1619
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
1422
1620
  /** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
1423
1621
  const ROLE_BY_TAG = {
1424
1622
  A: "link",
1425
1623
  BUTTON: "button",
1624
+ CANVAS: "img",
1426
1625
  H1: "heading",
1427
1626
  H2: "heading",
1428
1627
  H3: "heading",
@@ -1435,6 +1634,7 @@ const ROLE_BY_TAG = {
1435
1634
  OL: "list",
1436
1635
  P: "paragraph",
1437
1636
  SELECT: "combobox",
1637
+ SVG: "img",
1438
1638
  TABLE: "table",
1439
1639
  TD: "cell",
1440
1640
  TEXTAREA: "textbox",
@@ -1461,7 +1661,7 @@ function roleOf(element) {
1461
1661
  const type = (element.getAttribute("type") ?? "text").toLowerCase();
1462
1662
  return INPUT_TYPE_ROLE[type] ?? "textbox";
1463
1663
  }
1464
- return ROLE_BY_TAG[element.tagName] ?? "generic";
1664
+ return ROLE_BY_TAG[element.tagName.toUpperCase()] ?? "generic";
1465
1665
  }
1466
1666
  /** 自身直系文本(不含后代元素的文本):后代会各自成为节点,重复没有意义 */
1467
1667
  function ownText(element) {
@@ -1518,25 +1718,30 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
1518
1718
  "NOSCRIPT",
1519
1719
  "TEMPLATE"
1520
1720
  ]);
1521
- function describe(element, excluded, doc, view) {
1721
+ function describe(element, excluded, doc, view, targets) {
1522
1722
  if (excluded.has(element)) return void 0;
1523
1723
  if (SKIPPED_TAGS.has(element.tagName)) return void 0;
1524
1724
  if (hidden(element, view)) return void 0;
1525
1725
  const children = [];
1526
1726
  for (const child of element.children) {
1527
- const node = describe(child, excluded, doc, view);
1727
+ const node = describe(child, excluded, doc, view, targets);
1528
1728
  if (node !== void 0) children.push(node);
1529
1729
  }
1530
1730
  const name = nameOf(element, doc);
1531
1731
  const value = valueOf(element);
1532
1732
  const role = roleOf(element);
1533
1733
  if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
1534
- return {
1734
+ const node = {
1535
1735
  role,
1536
1736
  ...name !== void 0 ? { name } : {},
1537
1737
  ...value !== void 0 ? { value } : {},
1538
1738
  ...children.length > 0 ? { children } : {}
1539
1739
  };
1740
+ if (targets !== void 0 && isCapturableElement(element)) targets.push({
1741
+ element,
1742
+ node
1743
+ });
1744
+ return node;
1540
1745
  }
1541
1746
  /**
1542
1747
  * DOM 遍历实现(设计 09 §2 的「实现层」)。产出只有角色 / 名称 / 值,
@@ -1547,7 +1752,7 @@ function describe(element, excluded, doc, view) {
1547
1752
  * @experimental
1548
1753
  */
1549
1754
  function createDomPerceptionReader(options = {}) {
1550
- return { read(scope) {
1755
+ function collect(scope, targets) {
1551
1756
  const doc = options.document ?? globalThis.document;
1552
1757
  if (doc === void 0 || scope.include.length === 0) return [];
1553
1758
  const view = doc.defaultView;
@@ -1557,11 +1762,49 @@ function createDomPerceptionReader(options = {}) {
1557
1762
  for (const selector of scope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
1558
1763
  const nodes = [];
1559
1764
  for (const root of roots) {
1560
- const node = describe(root, excluded, doc, view);
1765
+ const node = describe(root, excluded, doc, view, targets);
1561
1766
  if (node !== void 0) nodes.push(node);
1562
1767
  }
1563
1768
  return nodes;
1564
- } };
1769
+ }
1770
+ async function readWithImages(scope, capture) {
1771
+ const targets = [];
1772
+ const nodes = collect(scope, targets);
1773
+ const selected = targets.slice(0, capture.maxImages);
1774
+ const images = [];
1775
+ let imageFailures = 0;
1776
+ for (const [index, target] of selected.entries()) {
1777
+ const result = await captureElementImage(target.element, {
1778
+ id: `img-${index + 1}`,
1779
+ maxBytes: capture.maxImageBytes
1780
+ });
1781
+ if (isCaptureFailure(result)) {
1782
+ target.node.imageNote = result.reason;
1783
+ imageFailures += 1;
1784
+ continue;
1785
+ }
1786
+ target.node.imageId = result.id;
1787
+ images.push({
1788
+ id: result.id,
1789
+ mimeType: result.mimeType,
1790
+ data: result.data,
1791
+ level: result.level
1792
+ });
1793
+ }
1794
+ for (const target of targets.slice(capture.maxImages)) target.node.imageNote = "Not captured: the per-message image limit was reached";
1795
+ return {
1796
+ nodes,
1797
+ images,
1798
+ imagesOmitted: targets.length - selected.length,
1799
+ imageFailures
1800
+ };
1801
+ }
1802
+ function read(scope, capture) {
1803
+ if (capture === void 0) return collect(scope, void 0);
1804
+ if (!capture.images || capture.maxImages <= 0) return Promise.resolve({ nodes: collect(scope, void 0) });
1805
+ return readWithImages(scope, capture);
1806
+ }
1807
+ return { read };
1565
1808
  }
1566
1809
  const isRecord = (v) => typeof v === "object" && v !== null;
1567
1810
  const isNonEmptyString = (v) => typeof v === "string" && v !== "";
@@ -2076,6 +2319,173 @@ var WorkerRuntimeClient = class {
2076
2319
  }
2077
2320
  }
2078
2321
  };
2322
+ /**
2323
+ * 静态加密的 MemoryStore 包装(FR-19.5)。落盘形态:
2324
+ * `{ v: 1, iv: base64, ct: base64 }`;每次写入生成新的 12 字节随机 IV
2325
+ * ——AES-GCM 下 IV 重用会同时毁掉机密性与完整性,不能复用。
2326
+ *
2327
+ * 算法与密钥都交给 Web Crypto,本文件不实现任何密码学原语。
2328
+ * @experimental
2329
+ */
2330
+ const ENVELOPE_VERSION = 1;
2331
+ const toBase64 = (bytes) => {
2332
+ let binary = "";
2333
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2334
+ return btoa(binary);
2335
+ };
2336
+ const allocate = (length) => new Uint8Array(new ArrayBuffer(length));
2337
+ const fromBase64 = (text) => {
2338
+ const binary = atob(text);
2339
+ const bytes = allocate(binary.length);
2340
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
2341
+ return bytes;
2342
+ };
2343
+ const encodeUtf8 = (text) => {
2344
+ const bytes = allocate(text.length * 3);
2345
+ const { written } = new TextEncoder().encodeInto(text, bytes);
2346
+ return bytes.subarray(0, written);
2347
+ };
2348
+ const isEnvelope = (value) => {
2349
+ if (typeof value !== "object" || value === null) return false;
2350
+ const record = value;
2351
+ return record["v"] === ENVELOPE_VERSION && typeof record["iv"] === "string" && typeof record["ct"] === "string";
2352
+ };
2353
+ /**
2354
+ * 某条已读出的记忆值是否是密文信封。观测方(如 console)据此决定要不要去取密钥:
2355
+ * 加密关着的时候不该为了读一条明文而凭空建出一把密钥。
2356
+ * @experimental
2357
+ */
2358
+ function isEncryptedMemoryValue(value) {
2359
+ return isEnvelope(value);
2360
+ }
2361
+ function requireSubtle() {
2362
+ const subtle = globalThis.crypto?.subtle;
2363
+ if (subtle === void 0) throw new WebSkillError("PROFILE_KEY_UNAVAILABLE", "Web Crypto is unavailable, so encrypted memory cannot be read or written. Serve the page over HTTPS or disable encrypted storage.");
2364
+ return subtle;
2365
+ }
2366
+ /**
2367
+ * 用 AES-GCM 包装一个 MemoryStore:值加密后再交给 `inner`,读取时解密。
2368
+ * 密钥不可用(未解锁 / 无 Web Crypto)时**拒绝读写并抛结构化错误**,
2369
+ * 不静默降级成明文——那会让「已加密」的承诺失效而用户无从察觉。
2370
+ *
2371
+ * `scopes` 限定加密范围。默认全加密;宿主通常只加密存放个人数据的
2372
+ * `user:` 作用域,把运行观测(会话参数历史、技能统计)留作明文供 console 读取。
2373
+ * @experimental
2374
+ */
2375
+ function createEncryptedMemoryStore(inner, key, options = {}) {
2376
+ const covers = options.scopes ?? (() => true);
2377
+ const requireKey = () => {
2378
+ if (key === void 0) throw new WebSkillError("PROFILE_KEY_UNAVAILABLE", "The encryption key for stored user data is unavailable, so the store refused to read or write.");
2379
+ return key;
2380
+ };
2381
+ const decrypt = async (raw) => {
2382
+ if (raw === void 0 || raw === null) return raw;
2383
+ if (!isEnvelope(raw)) return raw;
2384
+ const plain = await requireSubtle().decrypt({
2385
+ name: "AES-GCM",
2386
+ iv: fromBase64(raw.iv)
2387
+ }, requireKey(), fromBase64(raw.ct));
2388
+ return JSON.parse(new TextDecoder().decode(plain));
2389
+ };
2390
+ const store = {
2391
+ async get(scope, storeKey) {
2392
+ const raw = await inner.get(scope, storeKey);
2393
+ return covers(scope) ? decrypt(raw) : raw;
2394
+ },
2395
+ async set(scope, storeKey, value) {
2396
+ if (!covers(scope)) {
2397
+ await inner.set(scope, storeKey, value);
2398
+ return;
2399
+ }
2400
+ const subtle = requireSubtle();
2401
+ const iv = crypto.getRandomValues(allocate(12));
2402
+ const ct = await subtle.encrypt({
2403
+ name: "AES-GCM",
2404
+ iv
2405
+ }, requireKey(), encodeUtf8(JSON.stringify(value ?? null)));
2406
+ const envelope = {
2407
+ v: ENVELOPE_VERSION,
2408
+ iv: toBase64(iv),
2409
+ ct: toBase64(new Uint8Array(ct))
2410
+ };
2411
+ await inner.set(scope, storeKey, envelope);
2412
+ },
2413
+ async delete(scope, storeKey) {
2414
+ await inner.delete(scope, storeKey);
2415
+ },
2416
+ async list(scope) {
2417
+ const entries = await inner.list(scope);
2418
+ if (!covers(scope)) return entries;
2419
+ return Promise.all(entries.map(async (entry) => ({
2420
+ key: entry.key,
2421
+ value: await decrypt(entry.value)
2422
+ })));
2423
+ },
2424
+ async clear(scope) {
2425
+ await inner.clear(scope);
2426
+ }
2427
+ };
2428
+ if (inner.transaction) store.transaction = async (scope, fn) => inner.transaction(scope, async (raw) => fn(createEncryptedMemoryStore(raw, key, options)));
2429
+ return store;
2430
+ }
2431
+ /**
2432
+ * 生成一把不可导出的 AES-GCM 256 密钥(FR-19.5)。不可导出意味着页面脚本
2433
+ * 拿不到原始字节,只能通过 Web Crypto 使用它;宿主负责持久化这个句柄(如 IndexedDB)。
2434
+ * @experimental
2435
+ */
2436
+ async function generateMemoryEncryptionKey() {
2437
+ return requireSubtle().generateKey({
2438
+ name: "AES-GCM",
2439
+ length: 256
2440
+ }, false, ["encrypt", "decrypt"]);
2441
+ }
2442
+ const KEY_DB_NAME = "webskill-keys";
2443
+ const KEY_STORE_NAME = "keys";
2444
+ function request(req) {
2445
+ return new Promise((resolve, reject) => {
2446
+ req.onsuccess = () => resolve(req.result);
2447
+ req.onerror = () => reject(req.error ?? /* @__PURE__ */ new Error("IndexedDB request failed"));
2448
+ });
2449
+ }
2450
+ function openDb() {
2451
+ const indexedDb = globalThis.indexedDB;
2452
+ if (indexedDb === void 0) throw new WebSkillError("PROFILE_KEY_UNAVAILABLE", "IndexedDB is unavailable, so the encryption key for stored user data cannot be persisted.");
2453
+ return new Promise((resolve, reject) => {
2454
+ const open = indexedDb.open(KEY_DB_NAME, 1);
2455
+ open.onupgradeneeded = () => {
2456
+ if (!open.result.objectStoreNames.contains(KEY_STORE_NAME)) open.result.createObjectStore(KEY_STORE_NAME);
2457
+ };
2458
+ open.onsuccess = () => resolve(open.result);
2459
+ open.onerror = () => reject(open.error ?? /* @__PURE__ */ new Error("IndexedDB open failed"));
2460
+ });
2461
+ }
2462
+ /**
2463
+ * 取(必要时生成)本地数据加密密钥并持久化在 IndexedDB(FR-19.5)。
2464
+ * 存的是 `CryptoKey` 句柄本身而不是密钥字节:结构化克隆能存它,
2465
+ * 而它是 non-extractable 的,页面脚本与扩展都读不出原始字节。
2466
+ * @experimental
2467
+ */
2468
+ async function openMemoryEncryptionKey(name = "user-profile") {
2469
+ const db = await openDb();
2470
+ try {
2471
+ const existing = await request(db.transaction(KEY_STORE_NAME, "readonly").objectStore(KEY_STORE_NAME).get(name));
2472
+ if (existing instanceof CryptoKey) return existing;
2473
+ const created = await generateMemoryEncryptionKey();
2474
+ await request(db.transaction(KEY_STORE_NAME, "readwrite").objectStore(KEY_STORE_NAME).put(created, name));
2475
+ return created;
2476
+ } finally {
2477
+ db.close();
2478
+ }
2479
+ }
2480
+ /** 删除本地数据加密密钥;清除画像与记录时一并调用,避免留下解不开的旧密文 @experimental */
2481
+ async function deleteMemoryEncryptionKey(name = "user-profile") {
2482
+ const db = await openDb();
2483
+ try {
2484
+ await request(db.transaction(KEY_STORE_NAME, "readwrite").objectStore(KEY_STORE_NAME).delete(name));
2485
+ } finally {
2486
+ db.close();
2487
+ }
2488
+ }
2079
2489
 
2080
2490
  //#endregion
2081
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, checkDictationAvailability, createDomPerceptionReader, createIframeWorker, createLlmClient, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startWorkerRuntimeHost };
2491
+ export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createDomPerceptionReader, createEncryptedMemoryStore, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractZipWeb, generateMemoryEncryptionKey, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startWorkerRuntimeHost };