@webskill/sdk 0.5.0 → 0.7.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.
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +274 -42
- package/dist/browser.d.ts +127 -5
- package/dist/browser.js +611 -17
- package/dist/{catalogComponents-DV7cPpUm-C77AEEx9.js → catalogComponents-Dr5dFMAb-DKH_7VPI.js} +848 -974
- package/dist/{dist-6C03DShK.js → dist-D0qW6e40.js} +1213 -210
- package/dist/{dist-bewtXYlO.js → dist-DnYG2-eY.js} +53 -14
- package/dist/{env--jJB-TSX-04klhTYi.js → env-8cY40DXB-CGnEVZby.js} +7 -6
- package/dist/{env-BPUBZCwJ-4jat_SVG.d.ts → env-AK3cSMEA-Dli6QU5E.d.ts} +4 -3
- package/dist/eventTypes-DjIQpt8Y-Bj3vghj4.js +32 -0
- package/dist/governance.d.ts +78 -11
- package/dist/governance.js +173 -50
- package/dist/{index-D_7ZZjkl.d.ts → index-Ba3xFtfz.d.ts} +222 -8
- package/dist/{index-Bsqg4ftU.d.ts → index-BwsK9lGk.d.ts} +13 -7
- package/dist/{index-vBz_FC9w.d.ts → index-DkbABR43.d.ts} +336 -52
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +159 -11
- package/dist/mcp.js +276 -45
- package/dist/{memoryArtifactStore-BtOeB_hm-tj3fC5ip.js → memoryArtifactStore-52Zn9npI-BMPYwvoy.js} +10 -2
- package/dist/node.d.ts +4 -4
- package/dist/node.js +11 -3
- package/dist/{openUiLibrary-W3Ce896k-ClFTRZFs.js → openUiLibrary-Bdrji9qK-D2LxmM-a.js} +3 -3
- package/dist/{skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts → skillVersionStore-Bl-ElD45-CWPvGvoq.d.ts} +8 -2
- package/dist/{testing-DDCJWvgA.js → testing-CYTFqkDm.js} +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/dist/{types-D_hoCri8-BnNPiZCi.d.ts → types-CcxRLdJG-DCXyw1US.d.ts} +62 -10
- package/dist/ui-react.d.ts +5 -3
- package/dist/ui-react.js +72 -47
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +13 -8
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/dist/{webskillLitCatalog-_mugzRHx-DiuJpCuf.js → webskillLitCatalog-_mugzRHx-B_54vxum.js} +1 -1
- 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 {
|
|
3
|
-
import { n as MockLlmClient } from "./testing-
|
|
2
|
+
import { B as bridgeError, C as ProgressiveRouter, S as OpenAiCompatibleClient, U as createWebSkillApi, a as AgentLoop, b as GoogleGenAiClient, c as CapabilityApproval, ct as normalizeToolError, g as FsRunSnapshotStore, h as FsMemoryStore, it as networkPolicyLibSource, lt as parseBridgeRequest, m as FsArtifactStore, nt as mergeCatalogEntries, o as AnthropicClient, st as normalizeToolContent } from "./dist-D0qW6e40.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
|
-
|
|
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,218 @@ function startDictation(options) {
|
|
|
1417
1422
|
recognition.stop();
|
|
1418
1423
|
} };
|
|
1419
1424
|
}
|
|
1420
|
-
/**
|
|
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
|
+
}
|
|
1618
|
+
/** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
|
|
1421
1619
|
const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
|
|
1620
|
+
/** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
|
|
1621
|
+
const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
|
|
1622
|
+
"button",
|
|
1623
|
+
"checkbox",
|
|
1624
|
+
"combobox",
|
|
1625
|
+
"form",
|
|
1626
|
+
"link",
|
|
1627
|
+
"radio",
|
|
1628
|
+
"searchbox",
|
|
1629
|
+
"switch",
|
|
1630
|
+
"textbox"
|
|
1631
|
+
]);
|
|
1422
1632
|
/** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
|
|
1423
1633
|
const ROLE_BY_TAG = {
|
|
1424
1634
|
A: "link",
|
|
1425
1635
|
BUTTON: "button",
|
|
1636
|
+
CANVAS: "img",
|
|
1426
1637
|
H1: "heading",
|
|
1427
1638
|
H2: "heading",
|
|
1428
1639
|
H3: "heading",
|
|
@@ -1435,6 +1646,7 @@ const ROLE_BY_TAG = {
|
|
|
1435
1646
|
OL: "list",
|
|
1436
1647
|
P: "paragraph",
|
|
1437
1648
|
SELECT: "combobox",
|
|
1649
|
+
SVG: "img",
|
|
1438
1650
|
TABLE: "table",
|
|
1439
1651
|
TD: "cell",
|
|
1440
1652
|
TEXTAREA: "textbox",
|
|
@@ -1454,14 +1666,14 @@ const clip = (text) => {
|
|
|
1454
1666
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
1455
1667
|
return normalized.length > MAX_TEXT ? `${normalized.slice(0, MAX_TEXT)}…` : normalized;
|
|
1456
1668
|
};
|
|
1457
|
-
function roleOf(element) {
|
|
1669
|
+
function roleOf$1(element) {
|
|
1458
1670
|
const explicit = element.getAttribute("role");
|
|
1459
1671
|
if (explicit !== null && explicit.trim() !== "") return explicit.trim();
|
|
1460
1672
|
if (element.tagName === "INPUT") {
|
|
1461
1673
|
const type = (element.getAttribute("type") ?? "text").toLowerCase();
|
|
1462
1674
|
return INPUT_TYPE_ROLE[type] ?? "textbox";
|
|
1463
1675
|
}
|
|
1464
|
-
return ROLE_BY_TAG[element.tagName] ?? "generic";
|
|
1676
|
+
return ROLE_BY_TAG[element.tagName.toUpperCase()] ?? "generic";
|
|
1465
1677
|
}
|
|
1466
1678
|
/** 自身直系文本(不含后代元素的文本):后代会各自成为节点,重复没有意义 */
|
|
1467
1679
|
function ownText(element) {
|
|
@@ -1500,7 +1712,7 @@ function valueOf(element) {
|
|
|
1500
1712
|
* 隐藏节点不进结果:屏幕上看不见的东西,用户没有机会发现它被读走了。
|
|
1501
1713
|
* jsdom 里 `getComputedStyle` 可用但布局是假的,因此只看显式声明,不看尺寸。
|
|
1502
1714
|
*/
|
|
1503
|
-
function hidden(element, view) {
|
|
1715
|
+
function hidden$1(element, view) {
|
|
1504
1716
|
if (element.hasAttribute("hidden")) return true;
|
|
1505
1717
|
if (element.getAttribute("aria-hidden") === "true") return true;
|
|
1506
1718
|
const inlineStyle = element.getAttribute("style") ?? "";
|
|
@@ -1518,27 +1730,59 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
|
|
|
1518
1730
|
"NOSCRIPT",
|
|
1519
1731
|
"TEMPLATE"
|
|
1520
1732
|
]);
|
|
1521
|
-
function describe(element, excluded, doc, view) {
|
|
1733
|
+
function describe(element, excluded, doc, view, targets, handles) {
|
|
1522
1734
|
if (excluded.has(element)) return void 0;
|
|
1523
1735
|
if (SKIPPED_TAGS.has(element.tagName)) return void 0;
|
|
1524
|
-
if (hidden(element, view)) return void 0;
|
|
1736
|
+
if (hidden$1(element, view)) return void 0;
|
|
1525
1737
|
const children = [];
|
|
1526
1738
|
for (const child of element.children) {
|
|
1527
|
-
const node = describe(child, excluded, doc, view);
|
|
1739
|
+
const node = describe(child, excluded, doc, view, targets, handles);
|
|
1528
1740
|
if (node !== void 0) children.push(node);
|
|
1529
1741
|
}
|
|
1530
1742
|
const name = nameOf(element, doc);
|
|
1531
1743
|
const value = valueOf(element);
|
|
1532
|
-
const role = roleOf(element);
|
|
1744
|
+
const role = roleOf$1(element);
|
|
1533
1745
|
if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
|
|
1534
|
-
|
|
1746
|
+
const node = {
|
|
1535
1747
|
role,
|
|
1536
1748
|
...name !== void 0 ? { name } : {},
|
|
1537
1749
|
...value !== void 0 ? { value } : {},
|
|
1538
1750
|
...children.length > 0 ? { children } : {}
|
|
1539
1751
|
};
|
|
1752
|
+
const ref = handles?.issue(element, role);
|
|
1753
|
+
if (ref !== void 0) node.ref = ref;
|
|
1754
|
+
if (targets !== void 0 && isCapturableElement(element)) targets.push({
|
|
1755
|
+
element,
|
|
1756
|
+
node
|
|
1757
|
+
});
|
|
1758
|
+
return node;
|
|
1540
1759
|
}
|
|
1541
1760
|
/**
|
|
1761
|
+
* 句柄发放与解析。表在 reader 实例内、每次 `read()` 整体替换:
|
|
1762
|
+
* 全局表是第二套状态且会跨会话泄漏;不替换则页面变了还能用旧句柄点到别的东西。
|
|
1763
|
+
*/
|
|
1764
|
+
var HandleIssuer = class {
|
|
1765
|
+
#table = /* @__PURE__ */ new Map();
|
|
1766
|
+
#actionable;
|
|
1767
|
+
#excluded;
|
|
1768
|
+
constructor(actionable, excluded) {
|
|
1769
|
+
this.#actionable = actionable;
|
|
1770
|
+
this.#excluded = excluded;
|
|
1771
|
+
}
|
|
1772
|
+
issue(element, role) {
|
|
1773
|
+
if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
|
|
1774
|
+
if (!ACTIONABLE_ROLES.has(role)) return void 0;
|
|
1775
|
+
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
1776
|
+
crypto.getRandomValues(bytes);
|
|
1777
|
+
const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1778
|
+
this.#table.set(ref, element);
|
|
1779
|
+
return ref;
|
|
1780
|
+
}
|
|
1781
|
+
get table() {
|
|
1782
|
+
return this.#table;
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
/**
|
|
1542
1786
|
* DOM 遍历实现(设计 09 §2 的「实现层」)。产出只有角色 / 名称 / 值,
|
|
1543
1787
|
* **不含任何 HTML 标记、class、data 属性或注释**(FR-10.4)。
|
|
1544
1788
|
*
|
|
@@ -1547,21 +1791,204 @@ function describe(element, excluded, doc, view) {
|
|
|
1547
1791
|
* @experimental
|
|
1548
1792
|
*/
|
|
1549
1793
|
function createDomPerceptionReader(options = {}) {
|
|
1550
|
-
|
|
1551
|
-
|
|
1794
|
+
let handles;
|
|
1795
|
+
const resolveDocument = () => options.document ?? globalThis.document;
|
|
1796
|
+
/** 按当前 DOM 重算一次可操作集合;发句柄与执行前各算一次 */
|
|
1797
|
+
const actionSets = (doc) => {
|
|
1798
|
+
const actionable = /* @__PURE__ */ new Set();
|
|
1799
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
1800
|
+
const scope = options.actionScope;
|
|
1801
|
+
if (scope === void 0 || scope.include.length === 0) return {
|
|
1802
|
+
actionable,
|
|
1803
|
+
excluded
|
|
1804
|
+
};
|
|
1805
|
+
for (const selector of scope.include) for (const root of doc.querySelectorAll(selector)) {
|
|
1806
|
+
actionable.add(root);
|
|
1807
|
+
for (const descendant of root.querySelectorAll("*")) actionable.add(descendant);
|
|
1808
|
+
}
|
|
1809
|
+
for (const selector of scope.exclude ?? []) for (const root of doc.querySelectorAll(selector)) {
|
|
1810
|
+
excluded.add(root);
|
|
1811
|
+
for (const descendant of root.querySelectorAll("*")) excluded.add(descendant);
|
|
1812
|
+
}
|
|
1813
|
+
return {
|
|
1814
|
+
actionable,
|
|
1815
|
+
excluded
|
|
1816
|
+
};
|
|
1817
|
+
};
|
|
1818
|
+
function collect(scope, targets) {
|
|
1819
|
+
const doc = resolveDocument();
|
|
1552
1820
|
if (doc === void 0 || scope.include.length === 0) return [];
|
|
1553
1821
|
const view = doc.defaultView;
|
|
1822
|
+
const sets = actionSets(doc);
|
|
1823
|
+
handles = new HandleIssuer(sets.actionable, sets.excluded);
|
|
1554
1824
|
const excluded = /* @__PURE__ */ new Set();
|
|
1555
1825
|
for (const selector of scope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
|
|
1556
1826
|
const roots = [];
|
|
1557
1827
|
for (const selector of scope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
|
|
1558
1828
|
const nodes = [];
|
|
1559
1829
|
for (const root of roots) {
|
|
1560
|
-
const node = describe(root, excluded, doc, view);
|
|
1830
|
+
const node = describe(root, excluded, doc, view, targets, handles);
|
|
1561
1831
|
if (node !== void 0) nodes.push(node);
|
|
1562
1832
|
}
|
|
1563
1833
|
return nodes;
|
|
1564
|
-
}
|
|
1834
|
+
}
|
|
1835
|
+
async function readWithImages(scope, capture) {
|
|
1836
|
+
const targets = [];
|
|
1837
|
+
const nodes = collect(scope, targets);
|
|
1838
|
+
const selected = targets.slice(0, capture.maxImages);
|
|
1839
|
+
const images = [];
|
|
1840
|
+
let imageFailures = 0;
|
|
1841
|
+
for (const [index, target] of selected.entries()) {
|
|
1842
|
+
const result = await captureElementImage(target.element, {
|
|
1843
|
+
id: `img-${index + 1}`,
|
|
1844
|
+
maxBytes: capture.maxImageBytes
|
|
1845
|
+
});
|
|
1846
|
+
if (isCaptureFailure(result)) {
|
|
1847
|
+
target.node.imageNote = result.reason;
|
|
1848
|
+
imageFailures += 1;
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
target.node.imageId = result.id;
|
|
1852
|
+
images.push({
|
|
1853
|
+
id: result.id,
|
|
1854
|
+
mimeType: result.mimeType,
|
|
1855
|
+
data: result.data,
|
|
1856
|
+
level: result.level
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
for (const target of targets.slice(capture.maxImages)) target.node.imageNote = "Not captured: the per-message image limit was reached";
|
|
1860
|
+
return {
|
|
1861
|
+
nodes,
|
|
1862
|
+
images,
|
|
1863
|
+
imagesOmitted: targets.length - selected.length,
|
|
1864
|
+
imageFailures
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function read(scope, capture) {
|
|
1868
|
+
if (capture === void 0) return collect(scope, void 0);
|
|
1869
|
+
if (!capture.images || capture.maxImages <= 0) return Promise.resolve({ nodes: collect(scope, void 0) });
|
|
1870
|
+
return readWithImages(scope, capture);
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
read,
|
|
1874
|
+
resolve: (ref) => handles?.table.get(ref),
|
|
1875
|
+
inActionScope: (element) => {
|
|
1876
|
+
const doc = resolveDocument();
|
|
1877
|
+
if (doc === void 0) return false;
|
|
1878
|
+
const sets = actionSets(doc);
|
|
1879
|
+
return sets.actionable.has(element) && !sets.excluded.has(element);
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
|
|
1884
|
+
/** 只看显式声明,不看尺寸:jsdom 有 getComputedStyle 但没有布局 */
|
|
1885
|
+
function hidden(element) {
|
|
1886
|
+
if (element.hasAttribute("hidden")) return true;
|
|
1887
|
+
if (element.getAttribute("aria-hidden") === "true") return true;
|
|
1888
|
+
const inlineStyle = element.getAttribute("style") ?? "";
|
|
1889
|
+
if (/display\s*:\s*none|visibility\s*:\s*hidden/i.test(inlineStyle)) return true;
|
|
1890
|
+
const view = element.ownerDocument.defaultView;
|
|
1891
|
+
if (view !== null) {
|
|
1892
|
+
const computed = view.getComputedStyle(element);
|
|
1893
|
+
if (computed.display === "none" || computed.visibility === "hidden") return true;
|
|
1894
|
+
}
|
|
1895
|
+
return false;
|
|
1896
|
+
}
|
|
1897
|
+
const inputType = (element) => (element.getAttribute("type") ?? "text").toLowerCase();
|
|
1898
|
+
const isSecret = (element) => element.tagName === "INPUT" && SECRET_INPUT_TYPES.has(inputType(element));
|
|
1899
|
+
function accessibleName(element) {
|
|
1900
|
+
const label = element.getAttribute("aria-label");
|
|
1901
|
+
if (label !== null && label.trim() !== "") return label.trim();
|
|
1902
|
+
const text = element.textContent?.replace(/\s+/g, " ").trim() ?? "";
|
|
1903
|
+
if (text !== "") return text;
|
|
1904
|
+
const placeholder = element.getAttribute("placeholder");
|
|
1905
|
+
return placeholder !== null && placeholder.trim() !== "" ? placeholder.trim() : void 0;
|
|
1906
|
+
}
|
|
1907
|
+
function roleOf(element) {
|
|
1908
|
+
const explicit = element.getAttribute("role");
|
|
1909
|
+
if (explicit !== null && explicit.trim() !== "") return explicit.trim();
|
|
1910
|
+
if (element.tagName === "INPUT") {
|
|
1911
|
+
const type = inputType(element);
|
|
1912
|
+
return type === "checkbox" || type === "radio" ? type : type === "button" || type === "submit" ? "button" : "textbox";
|
|
1913
|
+
}
|
|
1914
|
+
if (element.tagName === "BUTTON") return "button";
|
|
1915
|
+
if (element.tagName === "A") return "link";
|
|
1916
|
+
if (element.tagName === "TEXTAREA") return "textbox";
|
|
1917
|
+
if (element.tagName === "SELECT") return "combobox";
|
|
1918
|
+
if (element.tagName === "FORM") return "form";
|
|
1919
|
+
return "generic";
|
|
1920
|
+
}
|
|
1921
|
+
const describeElement = (element) => {
|
|
1922
|
+
const name = accessibleName(element);
|
|
1923
|
+
return {
|
|
1924
|
+
role: roleOf(element),
|
|
1925
|
+
...name !== void 0 ? { name } : {},
|
|
1926
|
+
...isSecret(element) ? { secret: true } : {}
|
|
1927
|
+
};
|
|
1928
|
+
};
|
|
1929
|
+
/**
|
|
1930
|
+
* 受控组件必须收到 `input` / `change`:直接写 `.value` 在 React 上会被下一次渲染覆盖,
|
|
1931
|
+
* 表现为「填了又没了」。原生 setter 是绕过 React value 劫持的唯一办法。
|
|
1932
|
+
*/
|
|
1933
|
+
function fillValue(element, value) {
|
|
1934
|
+
const prototype = element.tagName === "INPUT" ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
1935
|
+
const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
1936
|
+
if (setter) setter.call(element, value);
|
|
1937
|
+
else element.value = value;
|
|
1938
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1939
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1940
|
+
}
|
|
1941
|
+
/** @experimental */
|
|
1942
|
+
function createDomPageActionExecutor(options) {
|
|
1943
|
+
const { reader } = options;
|
|
1944
|
+
const describe = (ref) => {
|
|
1945
|
+
const element = reader.resolve(ref);
|
|
1946
|
+
return element === void 0 ? void 0 : describeElement(element);
|
|
1947
|
+
};
|
|
1948
|
+
const execute = (request) => {
|
|
1949
|
+
const element = reader.resolve(request.ref);
|
|
1950
|
+
if (element === void 0) return {
|
|
1951
|
+
ok: false,
|
|
1952
|
+
target: { role: "generic" },
|
|
1953
|
+
reason: "The element reference is unknown or expired."
|
|
1954
|
+
};
|
|
1955
|
+
const target = describeElement(element);
|
|
1956
|
+
const fail = (reason) => ({
|
|
1957
|
+
ok: false,
|
|
1958
|
+
target,
|
|
1959
|
+
reason
|
|
1960
|
+
});
|
|
1961
|
+
if (!reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
|
|
1962
|
+
if (hidden(element)) return fail("The element is not visible.");
|
|
1963
|
+
if (element.hasAttribute("disabled")) return fail("The element is disabled.");
|
|
1964
|
+
if (request.action === "click") {
|
|
1965
|
+
element.click();
|
|
1966
|
+
return {
|
|
1967
|
+
ok: true,
|
|
1968
|
+
target
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
if (request.action === "fill") {
|
|
1972
|
+
if (!FILLABLE_TAGS.has(element.tagName)) return fail("The element is not a text control.");
|
|
1973
|
+
if (element.hasAttribute("readonly")) return fail("The element is read-only.");
|
|
1974
|
+
fillValue(element, request.value ?? "");
|
|
1975
|
+
return {
|
|
1976
|
+
ok: true,
|
|
1977
|
+
target
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
const form = element.tagName === "FORM" ? element : element.form;
|
|
1981
|
+
if (!form) return fail("The element does not belong to a form.");
|
|
1982
|
+
form.requestSubmit();
|
|
1983
|
+
return {
|
|
1984
|
+
ok: true,
|
|
1985
|
+
target
|
|
1986
|
+
};
|
|
1987
|
+
};
|
|
1988
|
+
return {
|
|
1989
|
+
execute,
|
|
1990
|
+
describe
|
|
1991
|
+
};
|
|
1565
1992
|
}
|
|
1566
1993
|
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
1567
1994
|
const isNonEmptyString = (v) => typeof v === "string" && v !== "";
|
|
@@ -2076,6 +2503,173 @@ var WorkerRuntimeClient = class {
|
|
|
2076
2503
|
}
|
|
2077
2504
|
}
|
|
2078
2505
|
};
|
|
2506
|
+
/**
|
|
2507
|
+
* 静态加密的 MemoryStore 包装(FR-19.5)。落盘形态:
|
|
2508
|
+
* `{ v: 1, iv: base64, ct: base64 }`;每次写入生成新的 12 字节随机 IV
|
|
2509
|
+
* ——AES-GCM 下 IV 重用会同时毁掉机密性与完整性,不能复用。
|
|
2510
|
+
*
|
|
2511
|
+
* 算法与密钥都交给 Web Crypto,本文件不实现任何密码学原语。
|
|
2512
|
+
* @experimental
|
|
2513
|
+
*/
|
|
2514
|
+
const ENVELOPE_VERSION = 1;
|
|
2515
|
+
const toBase64 = (bytes) => {
|
|
2516
|
+
let binary = "";
|
|
2517
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2518
|
+
return btoa(binary);
|
|
2519
|
+
};
|
|
2520
|
+
const allocate = (length) => new Uint8Array(new ArrayBuffer(length));
|
|
2521
|
+
const fromBase64 = (text) => {
|
|
2522
|
+
const binary = atob(text);
|
|
2523
|
+
const bytes = allocate(binary.length);
|
|
2524
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
2525
|
+
return bytes;
|
|
2526
|
+
};
|
|
2527
|
+
const encodeUtf8 = (text) => {
|
|
2528
|
+
const bytes = allocate(text.length * 3);
|
|
2529
|
+
const { written } = new TextEncoder().encodeInto(text, bytes);
|
|
2530
|
+
return bytes.subarray(0, written);
|
|
2531
|
+
};
|
|
2532
|
+
const isEnvelope = (value) => {
|
|
2533
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2534
|
+
const record = value;
|
|
2535
|
+
return record["v"] === ENVELOPE_VERSION && typeof record["iv"] === "string" && typeof record["ct"] === "string";
|
|
2536
|
+
};
|
|
2537
|
+
/**
|
|
2538
|
+
* 某条已读出的记忆值是否是密文信封。观测方(如 console)据此决定要不要去取密钥:
|
|
2539
|
+
* 加密关着的时候不该为了读一条明文而凭空建出一把密钥。
|
|
2540
|
+
* @experimental
|
|
2541
|
+
*/
|
|
2542
|
+
function isEncryptedMemoryValue(value) {
|
|
2543
|
+
return isEnvelope(value);
|
|
2544
|
+
}
|
|
2545
|
+
function requireSubtle() {
|
|
2546
|
+
const subtle = globalThis.crypto?.subtle;
|
|
2547
|
+
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.");
|
|
2548
|
+
return subtle;
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* 用 AES-GCM 包装一个 MemoryStore:值加密后再交给 `inner`,读取时解密。
|
|
2552
|
+
* 密钥不可用(未解锁 / 无 Web Crypto)时**拒绝读写并抛结构化错误**,
|
|
2553
|
+
* 不静默降级成明文——那会让「已加密」的承诺失效而用户无从察觉。
|
|
2554
|
+
*
|
|
2555
|
+
* `scopes` 限定加密范围。默认全加密;宿主通常只加密存放个人数据的
|
|
2556
|
+
* `user:` 作用域,把运行观测(会话参数历史、技能统计)留作明文供 console 读取。
|
|
2557
|
+
* @experimental
|
|
2558
|
+
*/
|
|
2559
|
+
function createEncryptedMemoryStore(inner, key, options = {}) {
|
|
2560
|
+
const covers = options.scopes ?? (() => true);
|
|
2561
|
+
const requireKey = () => {
|
|
2562
|
+
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.");
|
|
2563
|
+
return key;
|
|
2564
|
+
};
|
|
2565
|
+
const decrypt = async (raw) => {
|
|
2566
|
+
if (raw === void 0 || raw === null) return raw;
|
|
2567
|
+
if (!isEnvelope(raw)) return raw;
|
|
2568
|
+
const plain = await requireSubtle().decrypt({
|
|
2569
|
+
name: "AES-GCM",
|
|
2570
|
+
iv: fromBase64(raw.iv)
|
|
2571
|
+
}, requireKey(), fromBase64(raw.ct));
|
|
2572
|
+
return JSON.parse(new TextDecoder().decode(plain));
|
|
2573
|
+
};
|
|
2574
|
+
const store = {
|
|
2575
|
+
async get(scope, storeKey) {
|
|
2576
|
+
const raw = await inner.get(scope, storeKey);
|
|
2577
|
+
return covers(scope) ? decrypt(raw) : raw;
|
|
2578
|
+
},
|
|
2579
|
+
async set(scope, storeKey, value) {
|
|
2580
|
+
if (!covers(scope)) {
|
|
2581
|
+
await inner.set(scope, storeKey, value);
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
const subtle = requireSubtle();
|
|
2585
|
+
const iv = crypto.getRandomValues(allocate(12));
|
|
2586
|
+
const ct = await subtle.encrypt({
|
|
2587
|
+
name: "AES-GCM",
|
|
2588
|
+
iv
|
|
2589
|
+
}, requireKey(), encodeUtf8(JSON.stringify(value ?? null)));
|
|
2590
|
+
const envelope = {
|
|
2591
|
+
v: ENVELOPE_VERSION,
|
|
2592
|
+
iv: toBase64(iv),
|
|
2593
|
+
ct: toBase64(new Uint8Array(ct))
|
|
2594
|
+
};
|
|
2595
|
+
await inner.set(scope, storeKey, envelope);
|
|
2596
|
+
},
|
|
2597
|
+
async delete(scope, storeKey) {
|
|
2598
|
+
await inner.delete(scope, storeKey);
|
|
2599
|
+
},
|
|
2600
|
+
async list(scope) {
|
|
2601
|
+
const entries = await inner.list(scope);
|
|
2602
|
+
if (!covers(scope)) return entries;
|
|
2603
|
+
return Promise.all(entries.map(async (entry) => ({
|
|
2604
|
+
key: entry.key,
|
|
2605
|
+
value: await decrypt(entry.value)
|
|
2606
|
+
})));
|
|
2607
|
+
},
|
|
2608
|
+
async clear(scope) {
|
|
2609
|
+
await inner.clear(scope);
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
if (inner.transaction) store.transaction = async (scope, fn) => inner.transaction(scope, async (raw) => fn(createEncryptedMemoryStore(raw, key, options)));
|
|
2613
|
+
return store;
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* 生成一把不可导出的 AES-GCM 256 密钥(FR-19.5)。不可导出意味着页面脚本
|
|
2617
|
+
* 拿不到原始字节,只能通过 Web Crypto 使用它;宿主负责持久化这个句柄(如 IndexedDB)。
|
|
2618
|
+
* @experimental
|
|
2619
|
+
*/
|
|
2620
|
+
async function generateMemoryEncryptionKey() {
|
|
2621
|
+
return requireSubtle().generateKey({
|
|
2622
|
+
name: "AES-GCM",
|
|
2623
|
+
length: 256
|
|
2624
|
+
}, false, ["encrypt", "decrypt"]);
|
|
2625
|
+
}
|
|
2626
|
+
const KEY_DB_NAME = "webskill-keys";
|
|
2627
|
+
const KEY_STORE_NAME = "keys";
|
|
2628
|
+
function request(req) {
|
|
2629
|
+
return new Promise((resolve, reject) => {
|
|
2630
|
+
req.onsuccess = () => resolve(req.result);
|
|
2631
|
+
req.onerror = () => reject(req.error ?? /* @__PURE__ */ new Error("IndexedDB request failed"));
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
function openDb() {
|
|
2635
|
+
const indexedDb = globalThis.indexedDB;
|
|
2636
|
+
if (indexedDb === void 0) throw new WebSkillError("PROFILE_KEY_UNAVAILABLE", "IndexedDB is unavailable, so the encryption key for stored user data cannot be persisted.");
|
|
2637
|
+
return new Promise((resolve, reject) => {
|
|
2638
|
+
const open = indexedDb.open(KEY_DB_NAME, 1);
|
|
2639
|
+
open.onupgradeneeded = () => {
|
|
2640
|
+
if (!open.result.objectStoreNames.contains(KEY_STORE_NAME)) open.result.createObjectStore(KEY_STORE_NAME);
|
|
2641
|
+
};
|
|
2642
|
+
open.onsuccess = () => resolve(open.result);
|
|
2643
|
+
open.onerror = () => reject(open.error ?? /* @__PURE__ */ new Error("IndexedDB open failed"));
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* 取(必要时生成)本地数据加密密钥并持久化在 IndexedDB(FR-19.5)。
|
|
2648
|
+
* 存的是 `CryptoKey` 句柄本身而不是密钥字节:结构化克隆能存它,
|
|
2649
|
+
* 而它是 non-extractable 的,页面脚本与扩展都读不出原始字节。
|
|
2650
|
+
* @experimental
|
|
2651
|
+
*/
|
|
2652
|
+
async function openMemoryEncryptionKey(name = "user-profile") {
|
|
2653
|
+
const db = await openDb();
|
|
2654
|
+
try {
|
|
2655
|
+
const existing = await request(db.transaction(KEY_STORE_NAME, "readonly").objectStore(KEY_STORE_NAME).get(name));
|
|
2656
|
+
if (existing instanceof CryptoKey) return existing;
|
|
2657
|
+
const created = await generateMemoryEncryptionKey();
|
|
2658
|
+
await request(db.transaction(KEY_STORE_NAME, "readwrite").objectStore(KEY_STORE_NAME).put(created, name));
|
|
2659
|
+
return created;
|
|
2660
|
+
} finally {
|
|
2661
|
+
db.close();
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
/** 删除本地数据加密密钥;清除画像与记录时一并调用,避免留下解不开的旧密文 @experimental */
|
|
2665
|
+
async function deleteMemoryEncryptionKey(name = "user-profile") {
|
|
2666
|
+
const db = await openDb();
|
|
2667
|
+
try {
|
|
2668
|
+
await request(db.transaction(KEY_STORE_NAME, "readwrite").objectStore(KEY_STORE_NAME).delete(name));
|
|
2669
|
+
} finally {
|
|
2670
|
+
db.close();
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2079
2673
|
|
|
2080
2674
|
//#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 };
|
|
2675
|
+
export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractZipWeb, generateMemoryEncryptionKey, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startWorkerRuntimeHost };
|