@webskill/sdk 0.13.0 → 0.14.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 +3 -3
- package/dist/agent.js +3 -3
- package/dist/browser.d.ts +225 -7
- package/dist/browser.js +659 -47
- package/dist/{catalogComponents-BgAJN0p8-CYEXSk45.js → catalogComponents-BgAJN0p8-Cr55ouOg.js} +2 -2
- package/dist/{dist-ExSQky4C.js → dist-Bfga1TmS.js} +45 -5
- package/dist/{dist-GK6dtjRv.js → dist-BnqAkSS6.js} +940 -49
- package/dist/{dist-DqcL6jKO.js → dist-CiaDIqkV.js} +2 -2
- package/dist/{dist-Bev6i6Ip.js → dist-D5YhlCdY.js} +59 -1
- package/dist/governance.d.ts +3 -3
- package/dist/governance.js +2 -2
- package/dist/{index-DkvBhJQy.d.ts → index-CWoKOFBP.d.ts} +47 -3
- package/dist/{index-iBm9tJL_.d.ts → index-DQwGvHAI.d.ts} +389 -9
- package/dist/{index-C3XdItd_.d.ts → index-sd-gVKey.d.ts} +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/mcp.d.ts +56 -3
- package/dist/mcp.js +36 -3
- package/dist/{memoryArtifactStore-52Zn9npI-LbCQaqyx.js → memoryArtifactStore-52Zn9npI-BxtUkrgV.js} +1 -1
- package/dist/node.d.ts +3 -3
- package/dist/node.js +6 -6
- package/dist/{openUiLibrary-BKXW7Iwx-CWWBVOkE.js → openUiLibrary-BKXW7Iwx-DjvEnMlJ.js} +2 -2
- package/dist/{skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts → skillVersionStore-D-qHk9ZE-C0OFDTQY.d.ts} +1 -1
- package/dist/{testing-WPTyXQYt.js → testing-Cm8MseLF.js} +2 -2
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/{types-DLctJep_-B5G4uk2u.d.ts → types-C3V6lAeO-BgeOvc1Y.d.ts} +63 -4
- package/dist/ui-react.d.ts +2 -2
- package/dist/ui-react.js +5 -5
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/dist/{webskillLitCatalog-D_zCqeQF-C9lrvvMr.js → webskillLitCatalog-D_zCqeQF-swXsWoAe.js} +1 -1
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { $ as bridgeError, A as ProgressiveRouter, Ct as normalizeToolError, E as GoogleGenAiClient, St as normalizeToolContent, _t as mergeCatalogEntries, b as FsMemoryStore, c as AnthropicClient, k as OpenAiCompatibleClient, nt as createWebSkillApi, s as AgentLoop, u as CapabilityApproval, wt as parseBridgeRequest, x as FsRunSnapshotStore, y as FsArtifactStore, yt as networkPolicyLibSource } from "./dist-
|
|
3
|
-
import {
|
|
4
|
-
import { n as MockLlmClient } from "./testing-
|
|
1
|
+
import { $ as unzipWithLimits, B as messageOf, E as buildManifest, G as readSkillSignature, H as parseSkillMarkdown, I as isAtomicTempPath, L as isValidSkillName, M as detectSkillArchiveShapeFromFs, P as exportSkills, S as assertRemoteUrlAllowed, T as buildCatalog, U as parseSkillPackManifest, W as readResponseWithLimit, Y as resolveInsideRoot, _ as SkillDiscovery, b as WebSkillError, c as MANIFEST_EXCLUDED_FILES, d as SKILLS_LOCKFILE, et as validateSkills, f as SKILL_MANIFEST_FILE, h as SKILL_PACK_FILE, nt as verifySkillSignature, tt as verifyManifest, w as atomicWriteText } from "./dist-D5YhlCdY.js";
|
|
2
|
+
import { $ as bridgeError, A as ProgressiveRouter, Ct as normalizeToolError, E as GoogleGenAiClient, St as normalizeToolContent, _t as mergeCatalogEntries, b as FsMemoryStore, c as AnthropicClient, k as OpenAiCompatibleClient, nt as createWebSkillApi, s as AgentLoop, u as CapabilityApproval, wt as parseBridgeRequest, x as FsRunSnapshotStore, y as FsArtifactStore, yt as networkPolicyLibSource } from "./dist-Bfga1TmS.js";
|
|
3
|
+
import { A as frameLabel, M as toActionFrameScopes, N as toFrameScopes, j as frameSteps } from "./dist-BnqAkSS6.js";
|
|
4
|
+
import { n as MockLlmClient } from "./testing-Cm8MseLF.js";
|
|
5
5
|
|
|
6
6
|
//#region ../browser/dist/index.js
|
|
7
7
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
@@ -905,12 +905,16 @@ var IframeWorkerLike = class {
|
|
|
905
905
|
#channelId;
|
|
906
906
|
#hostWindow;
|
|
907
907
|
#hostListener;
|
|
908
|
-
|
|
908
|
+
#readyTimer;
|
|
909
|
+
constructor(iframe, hostWindow, channelId, readyTimeout) {
|
|
909
910
|
this.#iframe = iframe;
|
|
910
911
|
this.#hostWindow = hostWindow;
|
|
911
912
|
this.#channelId = channelId;
|
|
912
913
|
this.#hostListener = (event) => this.#onMessage(event);
|
|
913
914
|
this.#hostWindow.addEventListener("message", this.#hostListener);
|
|
915
|
+
if (readyTimeout) this.#readyTimer = setTimeout(() => {
|
|
916
|
+
this.#failQueue(`Sandbox document did not become ready within ${readyTimeout.ms}ms: ${readyTimeout.documentUrl}. Ensure it is listed in both manifest sandbox.pages and web_accessible_resources.`);
|
|
917
|
+
}, readyTimeout.ms);
|
|
914
918
|
}
|
|
915
919
|
get ready() {
|
|
916
920
|
return this.#ready;
|
|
@@ -919,10 +923,38 @@ var IframeWorkerLike = class {
|
|
|
919
923
|
return this.#channelId;
|
|
920
924
|
}
|
|
921
925
|
markReady() {
|
|
926
|
+
this.#clearReadyTimer();
|
|
922
927
|
this.#ready = true;
|
|
923
928
|
for (const payload of this.#queue) this.#post(payload);
|
|
924
929
|
this.#queue = [];
|
|
925
930
|
}
|
|
931
|
+
#clearReadyTimer() {
|
|
932
|
+
if (this.#readyTimer !== void 0) {
|
|
933
|
+
clearTimeout(this.#readyTimer);
|
|
934
|
+
this.#readyTimer = void 0;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* 就绪超时(FR-12.4):`createIframeWorker` 同步返回,超时无处可抛,只能走回包通道。
|
|
939
|
+
* 而 `#request` 按 `data.id` 匹配回包,id 对不上的消息被静默丢弃——
|
|
940
|
+
* 所以必须**逐条**回复队列里每一条消息的 id,否则调用方一直挂着。
|
|
941
|
+
*/
|
|
942
|
+
#failQueue(message) {
|
|
943
|
+
const queued = this.#queue;
|
|
944
|
+
this.#queue = [];
|
|
945
|
+
for (const payload of queued) {
|
|
946
|
+
const id = payload.id;
|
|
947
|
+
if (typeof id === "string") this.emit({
|
|
948
|
+
id,
|
|
949
|
+
ok: false,
|
|
950
|
+
error: {
|
|
951
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
952
|
+
message
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
this.terminate();
|
|
957
|
+
}
|
|
926
958
|
#post(payload) {
|
|
927
959
|
this.#iframe.contentWindow?.postMessage({
|
|
928
960
|
[ENVELOPE]: true,
|
|
@@ -953,11 +985,18 @@ var IframeWorkerLike = class {
|
|
|
953
985
|
for (const listener of this.#listeners) listener({ data });
|
|
954
986
|
}
|
|
955
987
|
terminate() {
|
|
988
|
+
this.#clearReadyTimer();
|
|
956
989
|
this.#hostWindow.removeEventListener("message", this.#hostListener);
|
|
957
990
|
this.#iframe.remove();
|
|
958
991
|
}
|
|
959
992
|
};
|
|
960
|
-
/**
|
|
993
|
+
/**
|
|
994
|
+
* srcdoc 文档:内嵌 Worker 引导源码 + 双向中继(BOOTSTRAP 经 JSON 字符串注入,防 <\/script> 截断)。
|
|
995
|
+
*
|
|
996
|
+
* **这段文本被 FR-12.5 / AC-12.1 冻结在 0.13.0 基线上**:既有网页宿主不给 `documentUrl`,
|
|
997
|
+
* 必须逐字节拿到与升级前相同的沙箱文档。因此它不与 `SANDBOX_PAGE_SCRIPT_SOURCE` 共用模板——
|
|
998
|
+
* 抽模板必然改动生成文本。两份中继的一致性改由行为对拍守卫(见 `sandboxRelayParity` 用例)。
|
|
999
|
+
*/
|
|
961
1000
|
function sandboxDocument(bootstrapSource, channelId) {
|
|
962
1001
|
const bootstrapJson = JSON.stringify(bootstrapSource).replace(/<\//g, "<\\/");
|
|
963
1002
|
return `<!doctype html>
|
|
@@ -984,16 +1023,96 @@ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload: { type: '
|
|
|
984
1023
|
<\/script></body></html>`;
|
|
985
1024
|
}
|
|
986
1025
|
/**
|
|
987
|
-
*
|
|
1026
|
+
* 静态沙箱页的中继脚本源码(分册 12 / FR-12.3)。
|
|
1027
|
+
*
|
|
1028
|
+
* 宿主把它落成一个独立文件并用 `<script src>` 引入——**不得内联**:
|
|
1029
|
+
* sandbox 页虽允许 `unsafe-inline`,但外链能让「沙箱页脚本 = SDK 原样产物」在审计时可核对。
|
|
1030
|
+
*
|
|
1031
|
+
* 与 srcdoc 形态的两点区别,都源于「静态页在加载完时还不知道要跑什么」:
|
|
1032
|
+
* - **引导源码**随宿主的 `sandbox-init` 消息到达,而不是内嵌在文档里;
|
|
1033
|
+
* - **channelId** 由第一条来自 `parent` 的消息 latch。SDK 导出的是一份与宿主无关的源码,
|
|
1034
|
+
* 不可能预知宿主这次生成的 id。latch 前先认 `event.source === parent`:少了这句,
|
|
1035
|
+
* 任何把本页嵌进去的第三方 frame 都能抢先 latch 一个自己的 id,
|
|
1036
|
+
* 让真正的宿主消息全部被丢弃——一个纯粹的拒绝服务面。
|
|
1037
|
+
*
|
|
1038
|
+
* 本源码不含任何扩展 API:sandbox 页拿不到它们(实证)。
|
|
1039
|
+
* `ENVELOPE` 与宿主侧同源(同一个模块常量),两边各写一份字面量会在改名时静默失配。
|
|
1040
|
+
* @experimental
|
|
1041
|
+
*/
|
|
1042
|
+
const SANDBOX_PAGE_SCRIPT_SOURCE = `const ENVELOPE = ${JSON.stringify(ENVELOPE)};
|
|
1043
|
+
let CHANNEL_ID = null;
|
|
1044
|
+
let worker = null;
|
|
1045
|
+
function post(payload) {
|
|
1046
|
+
parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload }, '*');
|
|
1047
|
+
}
|
|
1048
|
+
function startWorker(bootstrapSource) {
|
|
1049
|
+
if (worker || typeof bootstrapSource !== 'string') return;
|
|
1050
|
+
const blob = new Blob([bootstrapSource], { type: 'text/javascript' });
|
|
1051
|
+
const url = URL.createObjectURL(blob);
|
|
1052
|
+
// 注意:opaque origin 下 module Worker 无法加载(实证),classic Worker + data: URL 动态导入可用
|
|
1053
|
+
worker = new Worker(url);
|
|
1054
|
+
URL.revokeObjectURL(url);
|
|
1055
|
+
worker.addEventListener('message', (event) => post(event.data));
|
|
1056
|
+
worker.addEventListener('error', (event) => post({ type: 'sandbox-worker-error', message: event.message }));
|
|
1057
|
+
post({ type: 'sandbox-ready' });
|
|
1058
|
+
}
|
|
1059
|
+
window.addEventListener('message', (event) => {
|
|
1060
|
+
const data = event.data;
|
|
1061
|
+
if (!data || data[ENVELOPE] !== true) return;
|
|
1062
|
+
if (event.source !== parent) return;
|
|
1063
|
+
if (CHANNEL_ID === null) {
|
|
1064
|
+
if (typeof data.channelId !== 'string') return;
|
|
1065
|
+
CHANNEL_ID = data.channelId;
|
|
1066
|
+
} else if (data.channelId !== CHANNEL_ID) return;
|
|
1067
|
+
const payload = data.payload;
|
|
1068
|
+
if (payload && payload.type === 'sandbox-init') {
|
|
1069
|
+
startWorker(payload.bootstrapSource);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (worker) worker.postMessage(payload);
|
|
1073
|
+
});
|
|
1074
|
+
`;
|
|
1075
|
+
/**
|
|
1076
|
+
* `Document` 判别式。不用 `'documentUrl' in v`:`Document` 上没有该属性,
|
|
1077
|
+
* 但一个只给了 `doc` 的选项对象上也没有,两者会被判到同一侧。
|
|
1078
|
+
*/
|
|
1079
|
+
function isDocument(value) {
|
|
1080
|
+
return value.nodeType === 9;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* 创建 opaque iframe 沙箱 Worker:srcdoc(默认)或宿主指定的静态沙箱页承载 classic Worker。
|
|
988
1084
|
* 返回 IframeWorkerLike(协议与 blob Worker 完全一致:load/execute/bridge/network-blocked)。
|
|
989
1085
|
*/
|
|
990
|
-
function createIframeWorker(bootstrapSource,
|
|
1086
|
+
function createIframeWorker(bootstrapSource, options = {}) {
|
|
1087
|
+
const resolved = isDocument(options) ? { doc: options } : options;
|
|
1088
|
+
const doc = resolved.doc ?? document;
|
|
991
1089
|
const channelId = nextChannelId();
|
|
992
1090
|
const iframe = doc.createElement("iframe");
|
|
993
|
-
iframe.setAttribute("sandbox", "allow-scripts");
|
|
994
1091
|
iframe.style.display = "none";
|
|
995
|
-
|
|
996
|
-
const
|
|
1092
|
+
const hostWindow = doc.defaultView ?? window;
|
|
1093
|
+
const { documentUrl } = resolved;
|
|
1094
|
+
if (documentUrl === void 0) {
|
|
1095
|
+
iframe.setAttribute("sandbox", "allow-scripts");
|
|
1096
|
+
iframe.srcdoc = sandboxDocument(bootstrapSource, channelId);
|
|
1097
|
+
const worker = new IframeWorkerLike(iframe, hostWindow, channelId);
|
|
1098
|
+
doc.body.appendChild(iframe);
|
|
1099
|
+
return worker;
|
|
1100
|
+
}
|
|
1101
|
+
const worker = new IframeWorkerLike(iframe, hostWindow, channelId, {
|
|
1102
|
+
ms: resolved.readyTimeoutMs ?? 1e4,
|
|
1103
|
+
documentUrl
|
|
1104
|
+
});
|
|
1105
|
+
iframe.addEventListener("load", () => {
|
|
1106
|
+
iframe.contentWindow?.postMessage({
|
|
1107
|
+
[ENVELOPE]: true,
|
|
1108
|
+
channelId,
|
|
1109
|
+
payload: {
|
|
1110
|
+
type: "sandbox-init",
|
|
1111
|
+
bootstrapSource
|
|
1112
|
+
}
|
|
1113
|
+
}, "*");
|
|
1114
|
+
});
|
|
1115
|
+
iframe.src = documentUrl;
|
|
997
1116
|
doc.body.appendChild(iframe);
|
|
998
1117
|
return worker;
|
|
999
1118
|
}
|
|
@@ -1195,7 +1314,7 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
1195
1314
|
...details !== void 0 ? { details } : {}
|
|
1196
1315
|
});
|
|
1197
1316
|
if (decision === "allowed") return void 0;
|
|
1198
|
-
return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
|
|
1317
|
+
return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled by the sandbox capability settings` : `Capability "${capability}" was denied by the user`);
|
|
1199
1318
|
};
|
|
1200
1319
|
try {
|
|
1201
1320
|
switch (request.kind) {
|
|
@@ -1222,7 +1341,7 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
1222
1341
|
};
|
|
1223
1342
|
}
|
|
1224
1343
|
case "fetchData": {
|
|
1225
|
-
if (!context.fetchData) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"fetchData\" is
|
|
1344
|
+
if (!context.fetchData) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"fetchData\" is unavailable: this host wired no data source access at all. This is not the capability switch being off — a data source has to be declared in the host settings first.");
|
|
1226
1345
|
const denied = await gate("fetchData", `Script "${context.skillName}" wants to fetch data source "${request.sourceId}"`, { sourceId: request.sourceId });
|
|
1227
1346
|
if (denied) return denied;
|
|
1228
1347
|
return {
|
|
@@ -1633,10 +1752,12 @@ function splitDataUrl(dataUrl) {
|
|
|
1633
1752
|
/** 压到预算内;压不下去时把失败原因原样上抛给调用方转成 `CaptureFailure` */
|
|
1634
1753
|
async function toBudget(id, level, blob, maxBytes) {
|
|
1635
1754
|
const compressed = await compressImageToBudget(blob, maxBytes);
|
|
1755
|
+
const mimeType = compressed.blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
1756
|
+
if (!DELIVERABLE_IMAGE_MIME_TYPES.has(mimeType)) throw new Error(`the bytes normalized to "${mimeType === "" ? "no content type" : mimeType}", which is not one of ${[...DELIVERABLE_IMAGE_MIME_TYPES].join(", ")}`);
|
|
1636
1757
|
const buffer = await compressed.blob.arrayBuffer();
|
|
1637
1758
|
return {
|
|
1638
1759
|
id,
|
|
1639
|
-
mimeType
|
|
1760
|
+
mimeType,
|
|
1640
1761
|
data: base64Of(new Uint8Array(buffer)),
|
|
1641
1762
|
level,
|
|
1642
1763
|
originalBytes: compressed.originalBytes,
|
|
@@ -1647,15 +1768,16 @@ async function toBudget(id, level, blob, maxBytes) {
|
|
|
1647
1768
|
async function captureImg(element, id, maxBytes) {
|
|
1648
1769
|
const src = element.currentSrc !== "" ? element.currentSrc : element.src;
|
|
1649
1770
|
if (src === "") throw new Error("the <img> element has no resolved source");
|
|
1771
|
+
const oversized = Math.max(element.naturalWidth, element.naturalHeight) > MAX_RASTER_EDGE;
|
|
1650
1772
|
const inline = splitDataUrl(src);
|
|
1651
1773
|
if (inline) {
|
|
1652
1774
|
const bytes = Uint8Array.from(atob(inline.data), (char) => char.charCodeAt(0));
|
|
1653
|
-
return toBudget(id, "src", await toDeliverableBlob(new Blob([bytes], { type: inline.mimeType })), maxBytes);
|
|
1775
|
+
return toBudget(id, "src", await toDeliverableBlob(new Blob([bytes], { type: inline.mimeType }), oversized), maxBytes);
|
|
1654
1776
|
}
|
|
1655
1777
|
const response = await fetch(src, { mode: "cors" });
|
|
1656
1778
|
if (response.type === "opaque") throw new Error("the response is opaque, so its bytes cannot be read");
|
|
1657
1779
|
if (!response.ok) throw new Error(`fetching the source returned HTTP ${response.status}`);
|
|
1658
|
-
return toBudget(id, "src", await toDeliverableBlob(await response.blob()), maxBytes);
|
|
1780
|
+
return toBudget(id, "src", await toDeliverableBlob(await response.blob(), oversized), maxBytes);
|
|
1659
1781
|
}
|
|
1660
1782
|
/** L2:`<canvas>` 读回。画布被跨域内容污染时 `toDataURL` 抛 SecurityError,不再兜底(裁决 D-3) */
|
|
1661
1783
|
async function captureCanvas(element, id, maxBytes) {
|
|
@@ -1797,6 +1919,18 @@ const DELIVERABLE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
|
1797
1919
|
/** 用于认出 SVG 正文的前缀嗅探长度:`<?xml`、注释、DOCTYPE 都可能排在 `<svg` 之前 */
|
|
1798
1920
|
const SVG_SNIFF_BYTES = 1024;
|
|
1799
1921
|
/**
|
|
1922
|
+
* WebP 动图:扩展头 `VP8X` 的 flags 字节第 1 位。位置固定在文件前 24 字节内,
|
|
1923
|
+
* 不必解码整图就能判出来。
|
|
1924
|
+
*/
|
|
1925
|
+
function isAnimatedWebp(bytes) {
|
|
1926
|
+
return [
|
|
1927
|
+
86,
|
|
1928
|
+
80,
|
|
1929
|
+
56,
|
|
1930
|
+
88
|
|
1931
|
+
].every((byte, index) => bytes[12 + index] === byte) && ((bytes[20] ?? 0) & 2) !== 0;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1800
1934
|
* 按魔数认图片类型(服务端声明只作兜底)。
|
|
1801
1935
|
*
|
|
1802
1936
|
* 图片接口常年是「无扩展名 + `application/octet-stream`」这种组合(查询串带 file id 的尤其多),
|
|
@@ -1844,16 +1978,29 @@ async function rasterizeToPng(blob) {
|
|
|
1844
1978
|
* 两类真实站点上很常见的图会在最后一步被模型侧丢掉:服务端把图报成 `application/octet-stream`,
|
|
1845
1979
|
* 以及矢量图(三家提供方的图片通道都不收 `image/svg+xml`,分册 20 §0.1)。
|
|
1846
1980
|
* 前者只是声明错了,按魔数改回来即可;后者必须就地栅格化,否则等于没取到图。
|
|
1981
|
+
*
|
|
1982
|
+
* **声明一律不采信**:大站 CDN 会按 `Accept` 头改发 AVIF 却仍旧报 `image/webp`,
|
|
1983
|
+
* 照抄声明会让端点解码失败并连累整次请求(一张图 400,37 张全废)。
|
|
1984
|
+
*
|
|
1985
|
+
* 魔数对得上也不等于发得出去,还有两类会被端点整次拒收,必须重编码成单帧位图:
|
|
1986
|
+
* - **动图**(GIF 天然多帧,WebP 看 `VP8X` 的动画位):图片通道普遍只收静态图;
|
|
1987
|
+
* - **超长边**:横幅类素材常见 7680×120 这种尺寸,字节数却很小,
|
|
1988
|
+
* 纯按字节压缩的预算根本不会触发,原样发出去就撞上端点的边长上限。
|
|
1989
|
+
*
|
|
1990
|
+
* @param oversized 调用方已知原图长边超限(`<img>` 能直接读到 `naturalWidth`,不必为此解码)
|
|
1847
1991
|
*/
|
|
1848
|
-
async function toDeliverableBlob(blob) {
|
|
1992
|
+
async function toDeliverableBlob(blob, oversized = false) {
|
|
1849
1993
|
const declared = blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
1850
|
-
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
1994
|
+
const head = new Uint8Array(await blob.slice(0, SVG_SNIFF_BYTES).arrayBuffer());
|
|
1995
|
+
const sniffed = sniffImageMime(head);
|
|
1996
|
+
if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) {
|
|
1997
|
+
const typed = sniffed === declared ? blob : new Blob([blob], { type: sniffed });
|
|
1998
|
+
const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head);
|
|
1999
|
+
return oversized || animated ? await rasterizeToPng(typed) : typed;
|
|
2000
|
+
}
|
|
1854
2001
|
const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
|
|
1855
2002
|
if (mimeType === void 0) throw new Error(`the response is not a recognizable image (the server described it as "${blob.type === "" ? "no content type" : blob.type}")`);
|
|
1856
|
-
return await rasterizeToPng(new Blob([
|
|
2003
|
+
return await rasterizeToPng(new Blob([blob], { type: mimeType }));
|
|
1857
2004
|
}
|
|
1858
2005
|
/**
|
|
1859
2006
|
* 取一个元素的图像(0.6.0 FR-12.1)。
|
|
@@ -1902,6 +2049,148 @@ function describeError(e) {
|
|
|
1902
2049
|
if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
|
|
1903
2050
|
return e instanceof Error ? e.message : String(e);
|
|
1904
2051
|
}
|
|
2052
|
+
/**
|
|
2053
|
+
* 非标可交互元素的识别与角色提升(0.14.0 分册 15)。
|
|
2054
|
+
*
|
|
2055
|
+
* 现代前端用 `div` / `span` 做按钮是常态,它们在无障碍树里全落到 `generic`,
|
|
2056
|
+
* 于是拿不到句柄——模型读得到那几个字,却点不动。本模块补的就是这一段判定。
|
|
2057
|
+
*
|
|
2058
|
+
* 整套逻辑默认关闭,由 `createDomPerceptionReader` 的 `promoteRoles` 开启(D-15-1)。
|
|
2059
|
+
*/
|
|
2060
|
+
/**
|
|
2061
|
+
* class / id 里的交互语义词根(FR-15.2)。
|
|
2062
|
+
*
|
|
2063
|
+
* 带词边界匹配:`table` 含 `tab`、`container` 含 `nav`,子串匹配会把它们全误伤(D-15-8)。
|
|
2064
|
+
* 图标类单列一支:`icon-view` / `el-icon-download` / `anticon-*` 这类命名带着具体动作语义。
|
|
2065
|
+
*/
|
|
2066
|
+
const INTERACTIVE_TOKEN = /(?:^|[^a-z])(btn|button|actions?|click(?:able)?|link|menu|nav|tabs?|trigger|operate|operations?|handle|entry|toolbar|[a-z]*icons?(?:[-_][a-z0-9]+)?)(?:[^a-z]|$)/i;
|
|
2067
|
+
/** 从图标类名反推语义:`el-icon-view` → `view`、`icon-download` → `download`(FR-15.4 第 2 级) */
|
|
2068
|
+
const ICON_CLASS = /[a-z]*icons?[-_]([a-z0-9][a-z0-9-_]*)$/i;
|
|
2069
|
+
/** 框架路由属性:值非空即视为确凿的导航线索(FR-15.3 第 2 条) */
|
|
2070
|
+
const ROUTE_ATTRIBUTES = [
|
|
2071
|
+
"to",
|
|
2072
|
+
"routerlink",
|
|
2073
|
+
"data-href",
|
|
2074
|
+
"data-url"
|
|
2075
|
+
];
|
|
2076
|
+
/** 只有这三种角色会被提升:其余角色要么本就能发句柄,要么带着不该被覆盖的语义(D-15-3) */
|
|
2077
|
+
const PROMOTABLE_ROLES = /* @__PURE__ */ new Set([
|
|
2078
|
+
"generic",
|
|
2079
|
+
"img",
|
|
2080
|
+
"cell"
|
|
2081
|
+
]);
|
|
2082
|
+
/** 合成名的前缀,必须让模型看得出「这不是页面上的字,别拿它去搜」(FR-15.4 第 3 级) */
|
|
2083
|
+
const SYNTHETIC_NAME_PREFIX = "unlabeled control";
|
|
2084
|
+
/** 往上找具名祖先的层数上限:再往上就是整页容器,拿它当限定语反而更含糊 */
|
|
2085
|
+
const SYNTHETIC_NAME_LOOKUP_DEPTH = 5;
|
|
2086
|
+
const classAndId = (element) => `${element.getAttribute("class") ?? ""} ${element.getAttribute("id") ?? ""}`;
|
|
2087
|
+
/** `cursor: pointer` —— 页面自己声明了可点。`getComputedStyle` 最贵,殿后 */
|
|
2088
|
+
function looksClickable(element, view) {
|
|
2089
|
+
if (view === null) return false;
|
|
2090
|
+
try {
|
|
2091
|
+
return view.getComputedStyle(element).cursor === "pointer";
|
|
2092
|
+
} catch {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* 五级信号,命中任一即为真(FR-15.1)。
|
|
2098
|
+
*
|
|
2099
|
+
* 求值顺序按**成本**排而不是按强度排:前面的廉价信号命中就短路,
|
|
2100
|
+
* 把 `getComputedStyle` 这条唯一会触发样式重算的路径留到最后。
|
|
2101
|
+
*/
|
|
2102
|
+
function looksInteractive(element, context) {
|
|
2103
|
+
if (context.interactiveHint?.(element) === true) return true;
|
|
2104
|
+
const tabindex = element.getAttribute("tabindex");
|
|
2105
|
+
if (tabindex !== null && tabindex.trim() !== "" && tabindex.trim() !== "-1") return true;
|
|
2106
|
+
const title = element.getAttribute("title");
|
|
2107
|
+
if (title !== null && title.trim() !== "") return true;
|
|
2108
|
+
if (INTERACTIVE_TOKEN.test(classAndId(element))) return true;
|
|
2109
|
+
return looksClickable(element, context.view);
|
|
2110
|
+
}
|
|
2111
|
+
/** 图标类名反推出的语义词;没有图标类则 undefined */
|
|
2112
|
+
function iconHintOf(element) {
|
|
2113
|
+
for (const cls of (element.getAttribute("class") ?? "").split(/\s+/)) {
|
|
2114
|
+
const match = ICON_CLASS.exec(cls);
|
|
2115
|
+
if (match === null) continue;
|
|
2116
|
+
const semantic = match[1]?.split("__")[0];
|
|
2117
|
+
if (semantic !== void 0 && semantic !== "") return semantic;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
/** 宿主逃生舱的角色;命中即无条件提升成声明的角色(FR-15.6) */
|
|
2121
|
+
function hintedRoleOf(element, hints) {
|
|
2122
|
+
if (hints === void 0) return void 0;
|
|
2123
|
+
for (const hint of hints) for (const selector of hint.selectors) try {
|
|
2124
|
+
if (element.matches(selector)) return hint.role;
|
|
2125
|
+
} catch {}
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* 确凿的导航线索(FR-15.3):拿得到就判 `link`,拿不到一律 `button`。
|
|
2129
|
+
*
|
|
2130
|
+
* **不看** `cursor` / class 词根 / `onclick` 这类间接迹象——猜错会让模型以为「点了会跳走」
|
|
2131
|
+
* 而放弃先把页内信息读完,代价比多标一个 `button` 大。
|
|
2132
|
+
*/
|
|
2133
|
+
function navigationTargetOf(element, doc) {
|
|
2134
|
+
const own = resolvedHref(element, doc);
|
|
2135
|
+
if (own !== void 0) return own;
|
|
2136
|
+
for (const attribute of ROUTE_ATTRIBUTES) {
|
|
2137
|
+
const value = element.getAttribute(attribute);
|
|
2138
|
+
if (value !== null && value.trim() !== "") return value.trim();
|
|
2139
|
+
}
|
|
2140
|
+
const anchor = element.closest("a[href]");
|
|
2141
|
+
if (anchor !== null && anchor.getAttribute("role") === null) return resolvedHref(anchor, doc);
|
|
2142
|
+
}
|
|
2143
|
+
/** `href` 解析成绝对地址;不导航的形态(`#`、`javascript:`、空值)一律不算线索 */
|
|
2144
|
+
function resolvedHref(element, doc) {
|
|
2145
|
+
const raw = element.getAttribute("href") ?? element.getAttribute("xlink:href");
|
|
2146
|
+
if (raw === null) return void 0;
|
|
2147
|
+
const trimmed = raw.trim();
|
|
2148
|
+
if (trimmed === "" || trimmed === "#" || /^javascript:/i.test(trimmed)) return void 0;
|
|
2149
|
+
try {
|
|
2150
|
+
const url = new URL(trimmed, doc.baseURI);
|
|
2151
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
|
|
2152
|
+
} catch {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* 算不出名字时合成一个可指称的称呼(FR-15.4 第 3 级)。
|
|
2158
|
+
*
|
|
2159
|
+
* 用「最近具名祖先 + 同级序位」而不是坐标:坐标随滚动和窗口尺寸变,
|
|
2160
|
+
* 用户在屏幕上对不上号,模型跨两次感知也认不出是同一个东西。
|
|
2161
|
+
*/
|
|
2162
|
+
function synthesizeName(element, nameOf) {
|
|
2163
|
+
const siblings = element.parentElement?.children;
|
|
2164
|
+
let position = 0;
|
|
2165
|
+
if (siblings !== void 0) {
|
|
2166
|
+
for (const [index, sibling] of [...siblings].entries()) if (sibling === element) {
|
|
2167
|
+
position = index + 1;
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const ordinal = position > 0 ? `${SYNTHETIC_NAME_PREFIX} #${position}` : SYNTHETIC_NAME_PREFIX;
|
|
2172
|
+
let ancestor = element.parentElement;
|
|
2173
|
+
for (let depth = 0; depth < SYNTHETIC_NAME_LOOKUP_DEPTH && ancestor !== null; depth += 1) {
|
|
2174
|
+
const label = nameOf(ancestor);
|
|
2175
|
+
if (label !== void 0 && label !== "") return `${ordinal} in "${label}"`;
|
|
2176
|
+
ancestor = ancestor.parentElement;
|
|
2177
|
+
}
|
|
2178
|
+
return ordinal;
|
|
2179
|
+
}
|
|
2180
|
+
/** 提升后的角色与导航目标;未提升时返回 undefined */
|
|
2181
|
+
function promoteRole(input) {
|
|
2182
|
+
const { element, role, doc, hasActionableDescendant, context } = input;
|
|
2183
|
+
const hinted = hintedRoleOf(element, context.roleHints);
|
|
2184
|
+
if (hinted !== void 0) return { role: hinted };
|
|
2185
|
+
if (!PROMOTABLE_ROLES.has(role)) return void 0;
|
|
2186
|
+
if (hasActionableDescendant && role !== "img") return void 0;
|
|
2187
|
+
if (!looksInteractive(element, context)) return void 0;
|
|
2188
|
+
const href = navigationTargetOf(element, doc);
|
|
2189
|
+
return href !== void 0 ? {
|
|
2190
|
+
role: "link",
|
|
2191
|
+
href
|
|
2192
|
+
} : { role: "button" };
|
|
2193
|
+
}
|
|
1905
2194
|
/** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
|
|
1906
2195
|
const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
|
|
1907
2196
|
/** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
|
|
@@ -1979,6 +2268,33 @@ function ownText(element) {
|
|
|
1979
2268
|
for (const node of element.childNodes) if (node.nodeType === 3) text += node.nodeValue ?? "";
|
|
1980
2269
|
return clip(text);
|
|
1981
2270
|
}
|
|
2271
|
+
/** 名字全在元素外面的那几种控件 */
|
|
2272
|
+
const FORM_CONTROL_TAGS = /* @__PURE__ */ new Set([
|
|
2273
|
+
"INPUT",
|
|
2274
|
+
"TEXTAREA",
|
|
2275
|
+
"SELECT"
|
|
2276
|
+
]);
|
|
2277
|
+
/**
|
|
2278
|
+
* 表单控件的名字来源:`<label>`(关联式与包裹式)、`placeholder`、`title`。
|
|
2279
|
+
*
|
|
2280
|
+
* 缺了这几级,一个填着「基小建」的输入框在模型眼里就是无名的 textbox——
|
|
2281
|
+
* 旁边的 `<label>` 文字它读得到,却接不到这个框上,于是「值明明在结果里,
|
|
2282
|
+
* 模型却说不出这是谁」。操作侧的 `accessibleName` 一直取这几级,两边不一致
|
|
2283
|
+
* 还会让「感知时叫不出名字、点下去审计里却有名字」。
|
|
2284
|
+
*/
|
|
2285
|
+
function externalNameOf(element) {
|
|
2286
|
+
const labels = element.labels;
|
|
2287
|
+
if (labels && labels.length > 0) {
|
|
2288
|
+
const joined = [...labels].map((node) => node.textContent ?? "").filter((part) => part.trim() !== "").join(" ");
|
|
2289
|
+
if (joined.trim() !== "") return clip(joined);
|
|
2290
|
+
}
|
|
2291
|
+
const wrapping = element.closest("label")?.textContent ?? "";
|
|
2292
|
+
if (wrapping.trim() !== "") return clip(wrapping);
|
|
2293
|
+
for (const attribute of ["placeholder", "title"]) {
|
|
2294
|
+
const value = element.getAttribute(attribute);
|
|
2295
|
+
if (value !== null && value.trim() !== "") return clip(value);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
1982
2298
|
function nameOf(element, doc) {
|
|
1983
2299
|
const label = element.getAttribute("aria-label");
|
|
1984
2300
|
if (label !== null && label.trim() !== "") return clip(label);
|
|
@@ -1991,6 +2307,7 @@ function nameOf(element, doc) {
|
|
|
1991
2307
|
const alt = element.getAttribute("alt");
|
|
1992
2308
|
if (alt !== null && alt.trim() !== "") return clip(alt);
|
|
1993
2309
|
}
|
|
2310
|
+
if (FORM_CONTROL_TAGS.has(element.tagName)) return externalNameOf(element);
|
|
1994
2311
|
const own = ownText(element);
|
|
1995
2312
|
return own === "" ? void 0 : own;
|
|
1996
2313
|
}
|
|
@@ -2046,20 +2363,38 @@ function isBelowMinArea(element, minImageArea) {
|
|
|
2046
2363
|
if (size === void 0) return false;
|
|
2047
2364
|
return size.width * size.height < minImageArea;
|
|
2048
2365
|
}
|
|
2049
|
-
function describe(element, excluded, doc, view, capture, handles, frame, provenance) {
|
|
2366
|
+
function describe(element, excluded, doc, view, capture, handles, frame, provenance, promotion) {
|
|
2050
2367
|
if (excluded.has(element)) return void 0;
|
|
2051
2368
|
if (SKIPPED_TAGS.has(element.tagName)) return void 0;
|
|
2052
2369
|
if (hidden$1(element, view)) return void 0;
|
|
2053
2370
|
const children = [];
|
|
2054
2371
|
for (const child of element.children) {
|
|
2055
|
-
const node = describe(child, excluded, doc, view, capture, handles, frame, provenance);
|
|
2372
|
+
const node = describe(child, excluded, doc, view, capture, handles, frame, provenance, promotion);
|
|
2056
2373
|
if (node !== void 0) children.push(node);
|
|
2057
2374
|
}
|
|
2058
|
-
|
|
2375
|
+
let name = nameOf(element, doc);
|
|
2059
2376
|
const value = valueOf(element);
|
|
2060
|
-
|
|
2061
|
-
|
|
2377
|
+
let role = roleOf$1(element);
|
|
2378
|
+
let promotedHref;
|
|
2379
|
+
if (promotion !== void 0) {
|
|
2380
|
+
const promoted = promoteRole({
|
|
2381
|
+
element,
|
|
2382
|
+
role,
|
|
2383
|
+
doc,
|
|
2384
|
+
hasActionableDescendant: children.some((child) => child.ref !== void 0),
|
|
2385
|
+
context: promotion
|
|
2386
|
+
});
|
|
2387
|
+
if (promoted !== void 0) {
|
|
2388
|
+
role = promoted.role;
|
|
2389
|
+
promotedHref = promoted.href;
|
|
2390
|
+
if (name === void 0) name = clip(iconHintOf(element) ?? synthesizeName(element, (a) => nameOf(a, doc)));
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
const href = promotedHref ?? (role === "link" ? hrefOf(element) : void 0);
|
|
2062
2394
|
if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
|
|
2395
|
+
if (promotion !== void 0 && role === "generic" && name === void 0 && value === void 0) {
|
|
2396
|
+
if (children.length === 1) return children[0];
|
|
2397
|
+
}
|
|
2063
2398
|
const node = {
|
|
2064
2399
|
role,
|
|
2065
2400
|
...name !== void 0 ? { name } : {},
|
|
@@ -2110,6 +2445,46 @@ function frameHint(iframe) {
|
|
|
2110
2445
|
return "unnamed";
|
|
2111
2446
|
}
|
|
2112
2447
|
/**
|
|
2448
|
+
* 可路由的绝对地址(分册 16 FR-16.1)。
|
|
2449
|
+
*
|
|
2450
|
+
* 只认 http(s):`about:blank` / `srcdoc` / `data:` 这些外壳寻不到址,
|
|
2451
|
+
* 交出去只会让它拿一个匹配不上任何 frameId 的字符串去猜。
|
|
2452
|
+
*/
|
|
2453
|
+
function httpUrlOf(value, base) {
|
|
2454
|
+
try {
|
|
2455
|
+
const url = new URL(value, base);
|
|
2456
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
|
|
2457
|
+
} catch {
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
function routableUrlOf(iframe, doc) {
|
|
2462
|
+
try {
|
|
2463
|
+
const live = iframe.contentWindow?.location?.href;
|
|
2464
|
+
if (live !== void 0 && live !== "about:blank") {
|
|
2465
|
+
const routable = httpUrlOf(live, doc.baseURI);
|
|
2466
|
+
if (routable !== void 0) return routable;
|
|
2467
|
+
}
|
|
2468
|
+
} catch {}
|
|
2469
|
+
const src = iframe.getAttribute("src");
|
|
2470
|
+
if (src === null || src.trim() === "") return void 0;
|
|
2471
|
+
return httpUrlOf(src, doc.baseURI);
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* 同源嵌套帧的文档;跨源与尚未就绪时返回 `undefined`。
|
|
2475
|
+
*
|
|
2476
|
+
* 跨源时 `contentDocument` 返回 `null` 而**不抛异常**,某些引擎在别的属性上才抛——
|
|
2477
|
+
* 两种形态都要接住,只写 try/catch 会漏掉前一种。
|
|
2478
|
+
*/
|
|
2479
|
+
function sameOriginDocumentOf(iframe) {
|
|
2480
|
+
try {
|
|
2481
|
+
const inner = iframe.contentDocument;
|
|
2482
|
+
return inner === null || inner.body === null ? void 0 : inner;
|
|
2483
|
+
} catch {
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2113
2488
|
* 句柄发放与解析。表在 reader 实例内、**跨感知保留**(分册 18 FR-18.5):
|
|
2114
2489
|
* 每次 read() 作废全部旧句柄会让多步任务陷入「读—点—失效—再读」的空转,
|
|
2115
2490
|
* 而安全性本就不由句柄寿命承担——执行前的 `modalStateOf` / `inActionScope` /
|
|
@@ -2180,6 +2555,12 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2180
2555
|
*/
|
|
2181
2556
|
const elevatedModals = /* @__PURE__ */ new Set();
|
|
2182
2557
|
const rootDocument = () => options.document ?? globalThis.document;
|
|
2558
|
+
/** 提升未开启时返回 undefined,遍历侧据此整段跳过(AC-G10) */
|
|
2559
|
+
const promotionOf = (view) => options.promoteRoles === true ? {
|
|
2560
|
+
view,
|
|
2561
|
+
...options.interactiveHint !== void 0 ? { interactiveHint: options.interactiveHint } : {},
|
|
2562
|
+
...options.roleHints !== void 0 ? { roleHints: options.roleHints } : {}
|
|
2563
|
+
} : void 0;
|
|
2183
2564
|
/**
|
|
2184
2565
|
* 把一条帧授权解析成 Document,逐层下钻(分册 18 FR-18.3)。
|
|
2185
2566
|
*
|
|
@@ -2256,13 +2637,15 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2256
2637
|
const root = rootDocument();
|
|
2257
2638
|
if (root === void 0) return {
|
|
2258
2639
|
nodes: [],
|
|
2259
|
-
frameNotes: []
|
|
2640
|
+
frameNotes: [],
|
|
2641
|
+
nestedFrames: []
|
|
2260
2642
|
};
|
|
2261
2643
|
handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
2262
2644
|
handles.prune();
|
|
2263
2645
|
const issuer = handles;
|
|
2264
2646
|
const nodes = [];
|
|
2265
2647
|
const frameNotes = [];
|
|
2648
|
+
const nestedFrames = [];
|
|
2266
2649
|
/** 同一个未授权 iframe 至多报一次(它可能同时落在多个 include 根的子树里) */
|
|
2267
2650
|
const reportedUnauthorized = /* @__PURE__ */ new Set();
|
|
2268
2651
|
const resolvedScopes = [];
|
|
@@ -2287,6 +2670,66 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2287
2670
|
doc: resolved.doc
|
|
2288
2671
|
});
|
|
2289
2672
|
}
|
|
2673
|
+
/**
|
|
2674
|
+
* 范围内未授权嵌套帧的处置(分册 16 FR-16.1)。
|
|
2675
|
+
*
|
|
2676
|
+
* 递归而不是循环固定层数:0.13.0 D-18-8 用「路径是宿主写死的数组」论证深度上限,
|
|
2677
|
+
* 自动发现一进来这个前提就没了。兜底改由**帧数与节点总预算**承担,
|
|
2678
|
+
* 层数是错的度量(见分册 16 §0.2)。
|
|
2679
|
+
*/
|
|
2680
|
+
function descendFrames(roots, doc, excluded, frameScope, label, tag, actionable, actionExcluded) {
|
|
2681
|
+
for (const element of roots) for (const iframe of nestedFramesIn(element, excluded)) {
|
|
2682
|
+
if (authorizedFrames.has(iframe)) continue;
|
|
2683
|
+
if (reportedUnauthorized.has(iframe)) continue;
|
|
2684
|
+
reportedUnauthorized.add(iframe);
|
|
2685
|
+
const hint = frameHint(iframe);
|
|
2686
|
+
if (options.discoverNestedFrames === true) {
|
|
2687
|
+
const url = routableUrlOf(iframe, doc);
|
|
2688
|
+
if (url !== void 0) {
|
|
2689
|
+
nestedFrames.push({
|
|
2690
|
+
url,
|
|
2691
|
+
hint,
|
|
2692
|
+
...tag !== void 0 ? { frame: tag } : {}
|
|
2693
|
+
});
|
|
2694
|
+
continue;
|
|
2695
|
+
}
|
|
2696
|
+
const inner = sameOriginDocumentOf(iframe);
|
|
2697
|
+
if (inner !== void 0 && inlineFrame(inner, iframe, frameScope, label, tag, actionable, actionExcluded)) continue;
|
|
2698
|
+
}
|
|
2699
|
+
nodes.push({
|
|
2700
|
+
role: "note",
|
|
2701
|
+
name: `A nested frame "${hint}" inside "${label}" is not part of the authorized scope and was not read. Ask the user to authorize it if its content is needed.`,
|
|
2702
|
+
...tag !== void 0 ? { frame: tag } : {}
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* 无地址同源帧的就地内联;成功返回 `true`。
|
|
2708
|
+
*
|
|
2709
|
+
* `srcdoc` / `about:blank` 没有可路由地址,外壳寻不到址,只能由父帧代读。
|
|
2710
|
+
*
|
|
2711
|
+
* 可操作性**继承自那个 `<iframe>` 元素本身**:它由父页面写入内容,视觉上就是父页面的一部分,
|
|
2712
|
+
* 父帧的 action scope 把它圈进来了就等于圈进了它的内容;没圈进来则帧内一个句柄都不发。
|
|
2713
|
+
* exclude 在子文档里重新求值——敏感控件在每一层都要剪掉,不因为它在帧里就放宽(§0.1 边界 3)。
|
|
2714
|
+
*/
|
|
2715
|
+
function inlineFrame(inner, iframe, frameScope, label, tag, actionable, actionExcluded) {
|
|
2716
|
+
const innerExcluded = /* @__PURE__ */ new Set();
|
|
2717
|
+
for (const selector of frameScope.exclude ?? []) for (const target of inner.querySelectorAll(selector)) innerExcluded.add(target);
|
|
2718
|
+
const innerActionable = /* @__PURE__ */ new Set();
|
|
2719
|
+
const innerActionExcluded = /* @__PURE__ */ new Set();
|
|
2720
|
+
if (actionable.has(iframe)) {
|
|
2721
|
+
innerActionable.add(inner.body);
|
|
2722
|
+
for (const descendant of inner.body.querySelectorAll("*")) innerActionable.add(descendant);
|
|
2723
|
+
for (const selector of actionScopeFor(label)?.exclude ?? []) for (const target of inner.querySelectorAll(selector)) innerActionExcluded.add(target);
|
|
2724
|
+
}
|
|
2725
|
+
issuer.useSets(innerActionable, innerActionExcluded);
|
|
2726
|
+
const innerView = inner.defaultView;
|
|
2727
|
+
const node = describe(inner.body, innerExcluded, inner, innerView, capture, issuer, tag, "inline-frame", promotionOf(innerView));
|
|
2728
|
+
if (node !== void 0) nodes.push(node);
|
|
2729
|
+
descendFrames([inner.body], inner, innerExcluded, frameScope, label, tag, innerActionable, innerActionExcluded);
|
|
2730
|
+
issuer.useSets(actionable, actionExcluded);
|
|
2731
|
+
return node !== void 0;
|
|
2732
|
+
}
|
|
2290
2733
|
for (const { scope: frameScope, label, doc } of resolvedScopes) {
|
|
2291
2734
|
const view = doc.defaultView;
|
|
2292
2735
|
const tag = label === "self" ? void 0 : label;
|
|
@@ -2297,27 +2740,17 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2297
2740
|
const roots = [];
|
|
2298
2741
|
for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
|
|
2299
2742
|
for (const element of roots) {
|
|
2300
|
-
const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0);
|
|
2743
|
+
const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
|
|
2301
2744
|
if (node !== void 0) nodes.push(node);
|
|
2302
2745
|
}
|
|
2303
|
-
|
|
2304
|
-
if (authorizedFrames.has(iframe)) continue;
|
|
2305
|
-
if (reportedUnauthorized.has(iframe)) continue;
|
|
2306
|
-
reportedUnauthorized.add(iframe);
|
|
2307
|
-
const hint = frameHint(iframe);
|
|
2308
|
-
nodes.push({
|
|
2309
|
-
role: "note",
|
|
2310
|
-
name: `A nested frame "${hint}" inside "${label}" is not part of the authorized scope and was not read. Ask the user to authorize it if its content is needed.`,
|
|
2311
|
-
...tag !== void 0 ? { frame: tag } : {}
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2746
|
+
descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
|
|
2314
2747
|
}
|
|
2315
2748
|
pruneElevated();
|
|
2316
2749
|
const view = root.defaultView;
|
|
2317
2750
|
const sets = actionSets(root, actionScopeFor("self"));
|
|
2318
2751
|
for (const modal of elevatedModals) {
|
|
2319
2752
|
issuer.useSets(/* @__PURE__ */ new Set([modal, ...modal.querySelectorAll("*")]), sets.excluded);
|
|
2320
|
-
const node = describe(modal, /* @__PURE__ */ new Set(), root, view, capture, issuer, void 0, "modal-elevated");
|
|
2753
|
+
const node = describe(modal, /* @__PURE__ */ new Set(), root, view, capture, issuer, void 0, "modal-elevated", promotionOf(view));
|
|
2321
2754
|
if (node !== void 0) nodes.push(node);
|
|
2322
2755
|
}
|
|
2323
2756
|
for (const modal of root.querySelectorAll(MODAL_SELECTOR)) {
|
|
@@ -2330,12 +2763,13 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2330
2763
|
}
|
|
2331
2764
|
return {
|
|
2332
2765
|
nodes,
|
|
2333
|
-
frameNotes
|
|
2766
|
+
frameNotes,
|
|
2767
|
+
nestedFrames
|
|
2334
2768
|
};
|
|
2335
2769
|
}
|
|
2336
2770
|
async function readWithImages(scope, capture) {
|
|
2337
2771
|
const targets = [];
|
|
2338
|
-
const { nodes, frameNotes } = collect(scope, {
|
|
2772
|
+
const { nodes, frameNotes, nestedFrames } = collect(scope, {
|
|
2339
2773
|
targets,
|
|
2340
2774
|
minImageArea: capture.minImageArea ?? 0
|
|
2341
2775
|
});
|
|
@@ -2366,16 +2800,18 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2366
2800
|
images,
|
|
2367
2801
|
imagesOmitted: targets.length - selected.length,
|
|
2368
2802
|
imageFailures,
|
|
2369
|
-
...frameNotes.length > 0 ? { frameNotes } : {}
|
|
2803
|
+
...frameNotes.length > 0 ? { frameNotes } : {},
|
|
2804
|
+
...nestedFrames.length > 0 ? { nestedFrames } : {}
|
|
2370
2805
|
};
|
|
2371
2806
|
}
|
|
2372
2807
|
function read(scope, capture) {
|
|
2373
2808
|
if (capture === void 0) return collect(scope, void 0).nodes;
|
|
2374
2809
|
if (!capture.images || capture.maxImages <= 0) {
|
|
2375
|
-
const { nodes, frameNotes } = collect(scope, void 0);
|
|
2810
|
+
const { nodes, frameNotes, nestedFrames } = collect(scope, void 0);
|
|
2376
2811
|
return Promise.resolve({
|
|
2377
2812
|
nodes,
|
|
2378
|
-
...frameNotes.length > 0 ? { frameNotes } : {}
|
|
2813
|
+
...frameNotes.length > 0 ? { frameNotes } : {},
|
|
2814
|
+
...nestedFrames.length > 0 ? { nestedFrames } : {}
|
|
2379
2815
|
});
|
|
2380
2816
|
}
|
|
2381
2817
|
return readWithImages(scope, capture);
|
|
@@ -2732,6 +3168,182 @@ function createDomPageActionExecutor(options) {
|
|
|
2732
3168
|
describe
|
|
2733
3169
|
};
|
|
2734
3170
|
}
|
|
3171
|
+
/** @experimental */
|
|
3172
|
+
function createRemoteTargetRegistry() {
|
|
3173
|
+
let table = /* @__PURE__ */ new Map();
|
|
3174
|
+
return {
|
|
3175
|
+
/**
|
|
3176
|
+
* 整表替换而非合并(FR-13.2):合并会让上一次感知的 ref 在本次感知后仍然可执行——
|
|
3177
|
+
* 那正是 `PAGE_ACTION_STALE_REF` 要拦的东西。本地形态里旧 ref 天然失效
|
|
3178
|
+
* (DOM 重新遍历、句柄表 prune 掉离开文档的条目),远端必须手动复现这个失效语义,
|
|
3179
|
+
* 否则远端比本地更松。
|
|
3180
|
+
*/
|
|
3181
|
+
replace(entries) {
|
|
3182
|
+
const next = /* @__PURE__ */ new Map();
|
|
3183
|
+
for (const entry of entries) {
|
|
3184
|
+
const { ref, role, name, secret, frame, elevated } = entry;
|
|
3185
|
+
next.set(ref, {
|
|
3186
|
+
role,
|
|
3187
|
+
...name !== void 0 ? { name } : {},
|
|
3188
|
+
...secret === true ? { secret: true } : {},
|
|
3189
|
+
...frame !== void 0 ? { frame } : {},
|
|
3190
|
+
...elevated === true ? { elevated: true } : {}
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
table = next;
|
|
3194
|
+
},
|
|
3195
|
+
get(ref) {
|
|
3196
|
+
return table.get(ref);
|
|
3197
|
+
},
|
|
3198
|
+
get size() {
|
|
3199
|
+
return table.size;
|
|
3200
|
+
}
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
/** 缺省单次往返上限(FR-13.2 / FR-13.3) */
|
|
3204
|
+
const DEFAULT_PAGE_AGENT_TIMEOUT_MS = 1e4;
|
|
3205
|
+
/**
|
|
3206
|
+
* 一次带超时的往返,并把三类失败统一成 `WebSkillError`(FR-13.6)。
|
|
3207
|
+
*
|
|
3208
|
+
* 三类都必须显式失败,**绝不降级成空结果**:
|
|
3209
|
+
* 空节点数组的含义是「页面上什么都没有」,模型收到它会据此作答,
|
|
3210
|
+
* 比报错危险得多——报错至少不会产出一个自信的错误回答。
|
|
3211
|
+
*
|
|
3212
|
+
* @param expect 期望的应答类型;对不上通常意味着装错了 agent 版本,
|
|
3213
|
+
* 静默容忍会把问题推到很远的地方才暴露。
|
|
3214
|
+
*/
|
|
3215
|
+
async function roundTrip(transport, request, expect, code, timeoutMs) {
|
|
3216
|
+
let timer;
|
|
3217
|
+
let reply;
|
|
3218
|
+
try {
|
|
3219
|
+
reply = await Promise.race([transport.send(request), new Promise((_resolve, reject) => {
|
|
3220
|
+
timer = setTimeout(() => reject(new WebSkillError(code, `Page agent request "${request.type}" timed out after ${timeoutMs}ms with no reply from the page.`)), timeoutMs);
|
|
3221
|
+
})]);
|
|
3222
|
+
} catch (e) {
|
|
3223
|
+
if (e instanceof WebSkillError) throw e;
|
|
3224
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3225
|
+
throw new WebSkillError(code, `Page agent request "${request.type}" failed: ${message}`);
|
|
3226
|
+
} finally {
|
|
3227
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3228
|
+
}
|
|
3229
|
+
if (reply.type === "error") throw new WebSkillError(reply.code, reply.message);
|
|
3230
|
+
if (reply.type !== expect) throw new WebSkillError(code, `Page agent replied with "${reply.type}" to a "${request.type}" request; expected "${expect}".`);
|
|
3231
|
+
return reply;
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* 跨上下文的页面感知(分册 13 / FR-13.2)。
|
|
3235
|
+
*
|
|
3236
|
+
* 把句柄描述表写进 registry,**只把 `result` 返回给调用方**——
|
|
3237
|
+
* 返回值就是标准 `PerceptionResult`,不夹带任何新字段(AC-13.2)。
|
|
3238
|
+
* @experimental
|
|
3239
|
+
*/
|
|
3240
|
+
function createRemotePerceptionReader(options) {
|
|
3241
|
+
const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
|
|
3242
|
+
return { async read(scope, capture) {
|
|
3243
|
+
const reply = await roundTrip(transport, {
|
|
3244
|
+
type: "perceive",
|
|
3245
|
+
scope,
|
|
3246
|
+
...capture !== void 0 ? { capture } : {}
|
|
3247
|
+
}, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
|
|
3248
|
+
targets.replace(reply.targets);
|
|
3249
|
+
return reply.result;
|
|
3250
|
+
} };
|
|
3251
|
+
}
|
|
3252
|
+
/**
|
|
3253
|
+
* 跨上下文的页面操作执行器(分册 13 / FR-13.3)。
|
|
3254
|
+
* @experimental
|
|
3255
|
+
*/
|
|
3256
|
+
function createRemotePageActionExecutor(options) {
|
|
3257
|
+
const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
|
|
3258
|
+
return {
|
|
3259
|
+
/**
|
|
3260
|
+
* **同步**查表。`PageActionPolicy.act()` 在弹确认卡之前调它,
|
|
3261
|
+
* 拿到的 target 决定卡上写什么、是否 `secret`、是否 `elevated`——
|
|
3262
|
+
* 改成异步就是改 `@webskill/agent` 的公开契约。
|
|
3263
|
+
*
|
|
3264
|
+
* 表里没有即 `undefined`,于是 `act()` 抛 `PAGE_ACTION_STALE_REF`:
|
|
3265
|
+
* 没感知过就执行 = 拒绝,宁可让模型多感知一次(fail-closed)。
|
|
3266
|
+
*/
|
|
3267
|
+
describe(ref) {
|
|
3268
|
+
return targets.get(ref);
|
|
3269
|
+
},
|
|
3270
|
+
/**
|
|
3271
|
+
* 原样返回远端**执行那一刻**回填的 outcome(FR-13.5):
|
|
3272
|
+
* 不用 registry 里的快照拼装,也不补默认值——
|
|
3273
|
+
* 补出来的 `false` 与「远端没报」在审计上不是一回事。
|
|
3274
|
+
*/
|
|
3275
|
+
async execute(request) {
|
|
3276
|
+
return (await roundTrip(transport, {
|
|
3277
|
+
type: "execute",
|
|
3278
|
+
request
|
|
3279
|
+
}, "execute-result", "TOOL_EXECUTION_FAILED", timeoutMs)).outcome;
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
/** 深度收集句柄;顺序即遍历序,便于与本地形态逐条比对 */
|
|
3284
|
+
function collectRefs(nodes, out = []) {
|
|
3285
|
+
for (const node of nodes) {
|
|
3286
|
+
if (node.ref !== void 0) out.push(node.ref);
|
|
3287
|
+
if (node.children !== void 0) collectRefs(node.children, out);
|
|
3288
|
+
}
|
|
3289
|
+
return out;
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* 页面侧处理器(分册 13 / FR-13.7)。
|
|
3293
|
+
*
|
|
3294
|
+
* 必须是工厂而不是裸函数:`DomPerceptionReader` **是有状态的**——
|
|
3295
|
+
* `resolve(ref)` 依赖上一次感知发出的句柄表,模态提升状态也跨调用累积。
|
|
3296
|
+
* 每次请求新建 reader,`execute` 就永远 resolve 不到 `perceive` 发出的 ref。
|
|
3297
|
+
*
|
|
3298
|
+
* 它**不自己收发消息**:只做「请求进、应答出」。监听消息通道、判断消息是不是给自己的、
|
|
3299
|
+
* 回不回 `true`——那些全是宿主的事,本包不得出现任何扩展 API。
|
|
3300
|
+
* @experimental
|
|
3301
|
+
*/
|
|
3302
|
+
function createPageAgentHandler(options = {}) {
|
|
3303
|
+
const reader = createDomPerceptionReader({
|
|
3304
|
+
...options.document !== void 0 ? { document: options.document } : {},
|
|
3305
|
+
...options.actionScope !== void 0 ? { actionScope: options.actionScope } : {},
|
|
3306
|
+
...options.promoteRoles !== void 0 ? { promoteRoles: options.promoteRoles } : {},
|
|
3307
|
+
...options.interactiveHint !== void 0 ? { interactiveHint: options.interactiveHint } : {},
|
|
3308
|
+
...options.roleHints !== void 0 ? { roleHints: options.roleHints } : {},
|
|
3309
|
+
...options.discoverNestedFrames !== void 0 ? { discoverNestedFrames: options.discoverNestedFrames } : {}
|
|
3310
|
+
});
|
|
3311
|
+
const executor = createDomPageActionExecutor({
|
|
3312
|
+
reader,
|
|
3313
|
+
...options.pickFiles !== void 0 ? { pickFiles: options.pickFiles } : {}
|
|
3314
|
+
});
|
|
3315
|
+
return { async handle(request) {
|
|
3316
|
+
try {
|
|
3317
|
+
if (request.type === "perceive") {
|
|
3318
|
+
const raw = request.capture === void 0 ? reader.read(request.scope) : await reader.read(request.scope, request.capture);
|
|
3319
|
+
const result = Array.isArray(raw) ? { nodes: raw } : raw;
|
|
3320
|
+
const targets = [];
|
|
3321
|
+
for (const ref of collectRefs(result.nodes)) {
|
|
3322
|
+
const target = executor.describe(ref);
|
|
3323
|
+
if (target !== void 0) targets.push({
|
|
3324
|
+
ref,
|
|
3325
|
+
...target
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
return {
|
|
3329
|
+
type: "perceive-result",
|
|
3330
|
+
result,
|
|
3331
|
+
targets
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
return {
|
|
3335
|
+
type: "execute-result",
|
|
3336
|
+
outcome: await executor.execute(request.request)
|
|
3337
|
+
};
|
|
3338
|
+
} catch (e) {
|
|
3339
|
+
return {
|
|
3340
|
+
type: "error",
|
|
3341
|
+
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
3342
|
+
message: e instanceof Error ? e.message : String(e)
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
} };
|
|
3346
|
+
}
|
|
2735
3347
|
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
2736
3348
|
const isNonEmptyString = (v) => typeof v === "string" && v !== "";
|
|
2737
3349
|
/** Worker 侧入站校验:非法消息返回 undefined(忽略) */
|
|
@@ -4185,4 +4797,4 @@ function originOf(blockedURI) {
|
|
|
4185
4797
|
}
|
|
4186
4798
|
|
|
4187
4799
|
//#endregion
|
|
4188
|
-
export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractDocxText, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };
|
|
4800
|
+
export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, SANDBOX_PAGE_SCRIPT_SOURCE, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, createPageAgentHandler, createRemotePageActionExecutor, createRemotePerceptionReader, createRemoteTargetRegistry, deleteMemoryEncryptionKey, extractDocxText, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };
|