@webskill/sdk 0.15.0 → 0.17.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 +466 -66
- package/dist/{approval-BnLLCOG0.js → approval-CBPnGPIm.js} +57 -4
- package/dist/browser.d.ts +214 -7
- package/dist/browser.js +487 -47
- package/dist/{eventTypes-Y07N8IAC.js → eventTypes-CwciaqCU.js} +23 -0
- package/dist/governance.d.ts +26 -3
- package/dist/governance.js +1 -1
- package/dist/{index-CQYo4tcT.d.ts → index-1sQZ4VBN.d.ts} +324 -16
- package/dist/{index-Oc3H89TJ.d.ts → index-BDyXe-a5.d.ts} +1 -1
- package/dist/{index-CU2md8R1.d.ts → index-CaLsVI-m.d.ts} +7 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/{miniChart-owdCdaw-.js → miniChart-lauNtZu8.js} +2 -7
- package/dist/node.d.ts +3 -3
- package/dist/node.js +14 -8
- package/dist/{openUiLibrary-CY_ADoe0.js → openUiLibrary-BJtCG5B1.js} +1 -1
- package/dist/{openUiSpecLang-DuOCdw9G.js → openUiSpecLang-B4QQ9Tfo.js} +4 -3
- package/dist/{skillVersionStore-B24Jjjry-C4zomhMZ.d.ts → skillVersionStore-B24Jjjry-v2CCU0Xo.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-C3V6lAeO-BIf7UB2e.d.ts → types-Btpdd1y--BcxQ10Fa.d.ts} +20 -3
- package/dist/{types-TUydnxaj.js → types-BuacBov_.js} +14 -3
- package/dist/ui-react.d.ts +9 -3
- package/dist/ui-react.js +15 -4
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +2 -2
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +4 -4
- package/dist/{webSkillApi-DOOyz2G5.js → webSkillApi-Ce2OO5S5.js} +1 -1
- package/dist/{webskillLitCatalog-DSqVFQjK.js → webskillLitCatalog-BBkQjKuN.js} +2 -2
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -2,11 +2,11 @@ import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
|
|
|
2
2
|
import { A as SKILL_MANIFEST_FILE, D as verifySkillSignature, M as buildManifest, O as MANIFEST_EXCLUDED_FILES, P as verifyManifest, b as parseSkillMarkdown, c as unzipWithLimits, f as SKILL_PACK_FILE, i as buildCatalog, k as SKILLS_LOCKFILE, m as parseSkillPackManifest, n as SkillDiscovery, o as readResponseWithLimit, p as exportSkills, t as validateSkills, u as detectSkillArchiveShapeFromFs, w as readSkillSignature, y as isValidSkillName } from "./skill-CAJMsLod.js";
|
|
3
3
|
import { a as atomicWriteText, o as isAtomicTempPath, r as resolveInsideRoot } from "./pathSecurity-B1owvJAF.js";
|
|
4
4
|
import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
|
|
5
|
-
import { a as networkPolicyLibSource, c as bridgeError, d as FsMemoryStore, g as FsRunSnapshotStore, l as parseBridgeRequest, nt as ProgressiveRouter, p as AgentLoop, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-
|
|
5
|
+
import { a as networkPolicyLibSource, c as bridgeError, d as FsMemoryStore, g as FsRunSnapshotStore, l as parseBridgeRequest, nt as ProgressiveRouter, p as AgentLoop, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-CBPnGPIm.js";
|
|
6
6
|
import { a as GoogleGenAiClient, o as AnthropicClient, s as OpenAiCompatibleClient, t as MockLlmClient } from "./llm-B7lLH0ZI.js";
|
|
7
7
|
import { n as normalizeToolContent, t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
|
|
8
|
-
import { t as createWebSkillApi } from "./webSkillApi-
|
|
9
|
-
import { a as
|
|
8
|
+
import { t as createWebSkillApi } from "./webSkillApi-Ce2OO5S5.js";
|
|
9
|
+
import { a as frameLabel, i as toFrameScopes, o as frameSteps, r as toActionFrameScopes } from "./types-BuacBov_.js";
|
|
10
10
|
|
|
11
11
|
//#region ../browser/src/fs/featureDetection.ts
|
|
12
12
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
@@ -1355,13 +1355,16 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
1355
1355
|
}
|
|
1356
1356
|
/** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
|
|
1357
1357
|
async #handleBridge(request, context) {
|
|
1358
|
-
const gate = async (capability, message, details) => {
|
|
1358
|
+
const gate = async (capability, message, details = {}) => {
|
|
1359
1359
|
const decision = await this.#approval.authorize({
|
|
1360
1360
|
runId: context.runId,
|
|
1361
1361
|
capability,
|
|
1362
1362
|
mode: this.#capabilities[capability],
|
|
1363
1363
|
message,
|
|
1364
|
-
|
|
1364
|
+
details: {
|
|
1365
|
+
skill: context.skillName,
|
|
1366
|
+
...details
|
|
1367
|
+
}
|
|
1365
1368
|
});
|
|
1366
1369
|
if (decision === "allowed") return void 0;
|
|
1367
1370
|
return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled by the sandbox capability settings` : `Capability "${capability}" was denied by the user`);
|
|
@@ -1419,7 +1422,7 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
1419
1422
|
}
|
|
1420
1423
|
case "confirm": {
|
|
1421
1424
|
if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
|
|
1422
|
-
const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}
|
|
1425
|
+
const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`, { question: request.message });
|
|
1423
1426
|
if (denied) return denied;
|
|
1424
1427
|
return {
|
|
1425
1428
|
id: request.id,
|
|
@@ -1677,7 +1680,7 @@ function checkDictationAvailability() {
|
|
|
1677
1680
|
return { available: true };
|
|
1678
1681
|
}
|
|
1679
1682
|
/** 浏览器的 error 码 → 稳定错误码;权限被拒要单独区分,UI 的降级提示不一样(FR-7.6) */
|
|
1680
|
-
function toError(code, message) {
|
|
1683
|
+
function toError$1(code, message) {
|
|
1681
1684
|
if (code === "not-allowed" || code === "service-not-allowed") return new WebSkillError("DICTATION_PERMISSION_DENIED", "Microphone access was denied. Voice input is unavailable until you allow it in the browser.", { reason: code });
|
|
1682
1685
|
return new WebSkillError("DICTATION_FAILED", message ?? `Speech recognition failed: ${code}`, { reason: code });
|
|
1683
1686
|
}
|
|
@@ -1713,7 +1716,7 @@ function startDictation(options) {
|
|
|
1713
1716
|
recognition.onerror = (event) => {
|
|
1714
1717
|
if (stopped) return;
|
|
1715
1718
|
if (event.error === "no-speech" || event.error === "aborted") return;
|
|
1716
|
-
options.onError(toError(event.error, event.message));
|
|
1719
|
+
options.onError(toError$1(event.error, event.message));
|
|
1717
1720
|
};
|
|
1718
1721
|
try {
|
|
1719
1722
|
recognition.start();
|
|
@@ -1730,6 +1733,107 @@ function startDictation(options) {
|
|
|
1730
1733
|
} };
|
|
1731
1734
|
}
|
|
1732
1735
|
|
|
1736
|
+
//#endregion
|
|
1737
|
+
//#region ../browser/src/media/camera.ts
|
|
1738
|
+
/**
|
|
1739
|
+
* 默认最长边:宿主要「最大分辨率」,给一个远超当前消费级摄像头的 ideal 让浏览器自己贴顶
|
|
1740
|
+
* @experimental
|
|
1741
|
+
*/
|
|
1742
|
+
const DEFAULT_CAMERA_MAX_DIMENSION = 5e3;
|
|
1743
|
+
function mediaDevices() {
|
|
1744
|
+
return globalThis.navigator?.mediaDevices;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* 渲染期调用(FR-14.2):不可用时按钮置灰并说明原因。
|
|
1748
|
+
*
|
|
1749
|
+
* 顺序与语音一致——`getUserMedia` 压根不存在时谈安全上下文没有意义;
|
|
1750
|
+
* API 在但站点是 http,取流一定会被浏览器拒绝,这条原因用户能自己解决。
|
|
1751
|
+
* @experimental
|
|
1752
|
+
*/
|
|
1753
|
+
function checkCameraAvailability() {
|
|
1754
|
+
if (typeof mediaDevices()?.getUserMedia !== "function") return {
|
|
1755
|
+
available: false,
|
|
1756
|
+
reason: "api-missing"
|
|
1757
|
+
};
|
|
1758
|
+
if (globalThis.isSecureContext === false) return {
|
|
1759
|
+
available: false,
|
|
1760
|
+
reason: "insecure-context"
|
|
1761
|
+
};
|
|
1762
|
+
return { available: true };
|
|
1763
|
+
}
|
|
1764
|
+
/** DOMException 的 name → 稳定错误码;权限被拒要单独区分,UI 的降级提示不一样(FR-14.6) */
|
|
1765
|
+
function toError(e) {
|
|
1766
|
+
const raw = e?.name;
|
|
1767
|
+
const name = typeof raw === "string" ? raw : "";
|
|
1768
|
+
if (name === "NotAllowedError" || name === "SecurityError") return new WebSkillError("CAMERA_PERMISSION_DENIED", "Camera access was denied. Photo capture is unavailable until you allow it in the browser.", { reason: name });
|
|
1769
|
+
const message = e?.message;
|
|
1770
|
+
return new WebSkillError("CAMERA_UNAVAILABLE", `The camera could not be opened: ${typeof message === "string" && message !== "" ? message : String(e)}`, { ...name !== "" ? { reason: name } : {} });
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* 打开摄像头取景(FR-14.1)。
|
|
1774
|
+
*
|
|
1775
|
+
* **只能由用户手势调用**(FR-14.4)。设计上的保证是它不进工具列表:
|
|
1776
|
+
* agent 拿不到「打开摄像头」这个动作,因此不存在自主开摄像头的路径。
|
|
1777
|
+
* @experimental
|
|
1778
|
+
*/
|
|
1779
|
+
async function openCamera(options = {}) {
|
|
1780
|
+
const devices = mediaDevices();
|
|
1781
|
+
if (typeof devices?.getUserMedia !== "function") throw new WebSkillError("CAMERA_UNAVAILABLE", "Camera capture is not available in this browser.");
|
|
1782
|
+
const max = options.maxDimension ?? 5e3;
|
|
1783
|
+
let stream;
|
|
1784
|
+
try {
|
|
1785
|
+
stream = await devices.getUserMedia({
|
|
1786
|
+
video: {
|
|
1787
|
+
width: { ideal: max },
|
|
1788
|
+
height: { ideal: max },
|
|
1789
|
+
...options.facingMode !== void 0 ? { facingMode: options.facingMode } : {}
|
|
1790
|
+
},
|
|
1791
|
+
audio: false
|
|
1792
|
+
});
|
|
1793
|
+
} catch (e) {
|
|
1794
|
+
throw toError(e);
|
|
1795
|
+
}
|
|
1796
|
+
return {
|
|
1797
|
+
stream,
|
|
1798
|
+
resolution() {
|
|
1799
|
+
const settings = stream.getVideoTracks()[0]?.getSettings();
|
|
1800
|
+
return {
|
|
1801
|
+
width: settings?.width ?? 0,
|
|
1802
|
+
height: settings?.height ?? 0
|
|
1803
|
+
};
|
|
1804
|
+
},
|
|
1805
|
+
close() {
|
|
1806
|
+
for (const track of stream.getTracks()) track.stop();
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* 把取景元素的**当前一帧**编码成文件(FR-14.5)。
|
|
1812
|
+
*
|
|
1813
|
+
* 尺寸取 `videoWidth`/`videoHeight`(轨道原生分辨率),不取 CSS 尺寸——
|
|
1814
|
+
* 预览区被布局缩到 320px 宽,照着画出来的就是 320px 的照片。
|
|
1815
|
+
* 编码交给平台的 canvas,不手写编码器(AGENTS.md §6)。
|
|
1816
|
+
* @experimental
|
|
1817
|
+
*/
|
|
1818
|
+
async function capturePhoto(video, options = {}) {
|
|
1819
|
+
const width = video.videoWidth;
|
|
1820
|
+
const height = video.videoHeight;
|
|
1821
|
+
if (width === 0 || height === 0) throw new WebSkillError("CAMERA_CAPTURE_FAILED", "The camera has not produced a frame yet.");
|
|
1822
|
+
const type = options.type ?? "image/jpeg";
|
|
1823
|
+
const canvas = document.createElement("canvas");
|
|
1824
|
+
canvas.width = width;
|
|
1825
|
+
canvas.height = height;
|
|
1826
|
+
const context = canvas.getContext("2d");
|
|
1827
|
+
if (context === null) throw new WebSkillError("CAMERA_CAPTURE_FAILED", "This browser did not provide a 2D canvas context.");
|
|
1828
|
+
context.drawImage(video, 0, 0, width, height);
|
|
1829
|
+
const blob = await new Promise((resolve) => {
|
|
1830
|
+
canvas.toBlob(resolve, type, options.quality ?? .92);
|
|
1831
|
+
});
|
|
1832
|
+
if (blob === null) throw new WebSkillError("CAMERA_CAPTURE_FAILED", `The captured frame could not be encoded as ${type}.`);
|
|
1833
|
+
const name = options.fileName ?? `photo-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jpg`;
|
|
1834
|
+
return new File([blob], name, { type: blob.type === "" ? type : blob.type });
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1733
1837
|
//#endregion
|
|
1734
1838
|
//#region ../browser/src/media/compressImage.ts
|
|
1735
1839
|
/** 动图无法在不丢帧的前提下走单帧编码管线,超限直接拒绝(裁决 D-2) */
|
|
@@ -2013,6 +2117,17 @@ function isAnimatedWebp(bytes) {
|
|
|
2013
2117
|
].every((byte, index) => bytes[12 + index] === byte) && ((bytes[20] ?? 0) & 2) !== 0;
|
|
2014
2118
|
}
|
|
2015
2119
|
/**
|
|
2120
|
+
* APNG:`acTL` 块。规范要求它排在第一个 `IDAT` 之前,所以嗅探头部就够。
|
|
2121
|
+
* 魔数与静态 PNG 完全相同,只看魔数会让加载动画这类素材当作静态图原样发出去。
|
|
2122
|
+
*/
|
|
2123
|
+
function isAnimatedPng(bytes) {
|
|
2124
|
+
for (let i = 8; i + 4 <= bytes.length; i += 1) {
|
|
2125
|
+
if (bytes[i] === 73 && bytes[i + 1] === 68 && bytes[i + 2] === 65 && bytes[i + 3] === 84) return false;
|
|
2126
|
+
if (bytes[i] === 97 && bytes[i + 1] === 99 && bytes[i + 2] === 84 && bytes[i + 3] === 76) return true;
|
|
2127
|
+
}
|
|
2128
|
+
return false;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2016
2131
|
* 按魔数认图片类型(服务端声明只作兜底)。
|
|
2017
2132
|
*
|
|
2018
2133
|
* 图片接口常年是「无扩展名 + `application/octet-stream`」这种组合(查询串带 file id 的尤其多),
|
|
@@ -2065,7 +2180,7 @@ async function rasterizeToPng(blob) {
|
|
|
2065
2180
|
* 照抄声明会让端点解码失败并连累整次请求(一张图 400,37 张全废)。
|
|
2066
2181
|
*
|
|
2067
2182
|
* 魔数对得上也不等于发得出去,还有两类会被端点整次拒收,必须重编码成单帧位图:
|
|
2068
|
-
* - **动图**(GIF 天然多帧,WebP 看 `VP8X`
|
|
2183
|
+
* - **动图**(GIF 天然多帧,WebP 看 `VP8X` 的动画位,PNG 看 `acTL` 块):图片通道普遍只收静态图;
|
|
2069
2184
|
* - **超长边**:横幅类素材常见 7680×120 这种尺寸,字节数却很小,
|
|
2070
2185
|
* 纯按字节压缩的预算根本不会触发,原样发出去就撞上端点的边长上限。
|
|
2071
2186
|
*
|
|
@@ -2077,7 +2192,7 @@ async function toDeliverableBlob(blob, oversized = false) {
|
|
|
2077
2192
|
const sniffed = sniffImageMime(head);
|
|
2078
2193
|
if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) {
|
|
2079
2194
|
const typed = sniffed === declared ? blob : new Blob([blob], { type: sniffed });
|
|
2080
|
-
const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head);
|
|
2195
|
+
const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head) || sniffed === "image/png" && isAnimatedPng(head);
|
|
2081
2196
|
return oversized || animated ? await rasterizeToPng(typed) : typed;
|
|
2082
2197
|
}
|
|
2083
2198
|
const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
|
|
@@ -2158,11 +2273,20 @@ const ROUTE_ATTRIBUTES = [
|
|
|
2158
2273
|
"data-href",
|
|
2159
2274
|
"data-url"
|
|
2160
2275
|
];
|
|
2161
|
-
/**
|
|
2276
|
+
/**
|
|
2277
|
+
* 可被启发式信号提升的角色:其余角色要么本就能发句柄,要么带着不该被覆盖的语义(D-15-3)。
|
|
2278
|
+
*
|
|
2279
|
+
* `listitem` 在列表里本该只是结构,但 `<ul><li>` 是自绘下拉 / 菜单的主流写法,
|
|
2280
|
+
* 选项往往就是不带 role 的 `<li>`(0.17.0 分册 11)。它与另外三个一样受
|
|
2281
|
+
* 「有可操作后代就不提升」约束,所以真正的语义列表不会被误伤。
|
|
2282
|
+
*
|
|
2283
|
+
* 这张白名单**只约束启发式信号**。宿主探针点名的元素走另一条路(分册 12)。
|
|
2284
|
+
*/
|
|
2162
2285
|
const PROMOTABLE_ROLES = /* @__PURE__ */ new Set([
|
|
2163
2286
|
"generic",
|
|
2164
2287
|
"img",
|
|
2165
|
-
"cell"
|
|
2288
|
+
"cell",
|
|
2289
|
+
"listitem"
|
|
2166
2290
|
]);
|
|
2167
2291
|
/** 合成名的前缀,必须让模型看得出「这不是页面上的字,别拿它去搜」(FR-15.4 第 3 级) */
|
|
2168
2292
|
const SYNTHETIC_NAME_PREFIX = "unlabeled control";
|
|
@@ -2264,10 +2388,11 @@ function synthesizeName(element, nameOf) {
|
|
|
2264
2388
|
}
|
|
2265
2389
|
/** 提升后的角色与导航目标;未提升时返回 undefined */
|
|
2266
2390
|
function promoteRole(input) {
|
|
2267
|
-
const { element, role, doc, hasActionableDescendant, context } = input;
|
|
2391
|
+
const { element, role, doc, hasActionableDescendant, selfActionable, context } = input;
|
|
2268
2392
|
const hinted = hintedRoleOf(element, context.roleHints);
|
|
2269
2393
|
if (hinted !== void 0) return { role: hinted };
|
|
2270
|
-
if (
|
|
2394
|
+
if (selfActionable) return void 0;
|
|
2395
|
+
if (!(context.interactiveHint?.(element) === true) && !PROMOTABLE_ROLES.has(role)) return void 0;
|
|
2271
2396
|
if (hasActionableDescendant && role !== "img") return void 0;
|
|
2272
2397
|
if (!looksInteractive(element, context)) return void 0;
|
|
2273
2398
|
const href = navigationTargetOf(element, doc);
|
|
@@ -2281,7 +2406,7 @@ function promoteRole(input) {
|
|
|
2281
2406
|
//#region ../browser/src/dom/perception.ts
|
|
2282
2407
|
/** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
|
|
2283
2408
|
const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
|
|
2284
|
-
/**
|
|
2409
|
+
/** 可被操作的角色:只有这些节点的句柄能拿去写页面 */
|
|
2285
2410
|
const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
|
|
2286
2411
|
"button",
|
|
2287
2412
|
"checkbox",
|
|
@@ -2293,9 +2418,34 @@ const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
|
|
|
2293
2418
|
"switch",
|
|
2294
2419
|
"textbox"
|
|
2295
2420
|
]);
|
|
2421
|
+
/**
|
|
2422
|
+
* 只能做子树锚点的容器角色(分册 14 §6.1)。
|
|
2423
|
+
*
|
|
2424
|
+
* 这些节点拿到的是“读”句柄:`perceive_page(ref)` 能以它为根续读,
|
|
2425
|
+
* `act_on_page` 拿到它则一律被拒。不取 generic:那等于给每个 div 发句柄。
|
|
2426
|
+
*/
|
|
2427
|
+
const ANCHOR_ROLES = /* @__PURE__ */ new Set([
|
|
2428
|
+
"article",
|
|
2429
|
+
"dialog",
|
|
2430
|
+
"feed",
|
|
2431
|
+
"grid",
|
|
2432
|
+
"group",
|
|
2433
|
+
"list",
|
|
2434
|
+
"listitem",
|
|
2435
|
+
"main",
|
|
2436
|
+
"navigation",
|
|
2437
|
+
"region",
|
|
2438
|
+
"row",
|
|
2439
|
+
"rowgroup",
|
|
2440
|
+
"table",
|
|
2441
|
+
"tabpanel",
|
|
2442
|
+
"tree",
|
|
2443
|
+
"treeitem"
|
|
2444
|
+
]);
|
|
2296
2445
|
/** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
|
|
2297
2446
|
const ROLE_BY_TAG = {
|
|
2298
2447
|
A: "link",
|
|
2448
|
+
ARTICLE: "article",
|
|
2299
2449
|
BUTTON: "button",
|
|
2300
2450
|
CANVAS: "img",
|
|
2301
2451
|
H1: "heading",
|
|
@@ -2469,7 +2619,8 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
|
|
|
2469
2619
|
element,
|
|
2470
2620
|
role,
|
|
2471
2621
|
doc,
|
|
2472
|
-
hasActionableDescendant: children.some((child) => child.ref !== void 0),
|
|
2622
|
+
hasActionableDescendant: children.some((child) => child.ref !== void 0 && handles?.isAction(child.ref) === true),
|
|
2623
|
+
selfActionable: ACTIONABLE_ROLES.has(role),
|
|
2473
2624
|
context: promotion
|
|
2474
2625
|
});
|
|
2475
2626
|
if (promoted !== void 0) {
|
|
@@ -2602,11 +2753,13 @@ var HandleIssuer = class {
|
|
|
2602
2753
|
}
|
|
2603
2754
|
issue(element, role, frame) {
|
|
2604
2755
|
if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
|
|
2605
|
-
|
|
2756
|
+
const kind = ACTIONABLE_ROLES.has(role) ? "action" : ANCHOR_ROLES.has(role) ? "anchor" : void 0;
|
|
2757
|
+
if (kind === void 0) return void 0;
|
|
2606
2758
|
const known = this.#refByElement.get(element);
|
|
2607
2759
|
if (known !== void 0 && this.#table.has(known)) {
|
|
2608
2760
|
this.#table.set(known, {
|
|
2609
2761
|
element,
|
|
2762
|
+
kind,
|
|
2610
2763
|
...frame !== void 0 ? { frame } : {}
|
|
2611
2764
|
});
|
|
2612
2765
|
return known;
|
|
@@ -2616,11 +2769,16 @@ var HandleIssuer = class {
|
|
|
2616
2769
|
const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2617
2770
|
this.#table.set(ref, {
|
|
2618
2771
|
element,
|
|
2772
|
+
kind,
|
|
2619
2773
|
...frame !== void 0 ? { frame } : {}
|
|
2620
2774
|
});
|
|
2621
2775
|
this.#refByElement.set(element, ref);
|
|
2622
2776
|
return ref;
|
|
2623
2777
|
}
|
|
2778
|
+
/** 角色提升要的是「有可操作后代」,容器锚点不算;混进来会把可点容器判成已有出口 */
|
|
2779
|
+
isAction(ref) {
|
|
2780
|
+
return this.#table.get(ref)?.kind === "action";
|
|
2781
|
+
}
|
|
2624
2782
|
get table() {
|
|
2625
2783
|
return this.#table;
|
|
2626
2784
|
}
|
|
@@ -2721,12 +2879,20 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2721
2879
|
};
|
|
2722
2880
|
/** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」。按展示串匹配(FR-18.2) */
|
|
2723
2881
|
const actionScopeFor = (label) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => frameLabel(entry.frame) === label);
|
|
2724
|
-
|
|
2882
|
+
/**
|
|
2883
|
+
* @param subtree 不为 `undefined` 时只读这一棵子树(分册 14)。
|
|
2884
|
+
* 它**不是一个新的范围来源**:子树必须先落在某条帧授权的 include 根下、
|
|
2885
|
+
* 且未被 exclude 命中,否则遍历**根本不开始**(`anchored: false`)。
|
|
2886
|
+
* 命中时也只是把那一帧的 `roots` 换成这一个元素,遍历器与 exclude 判定一字未改——
|
|
2887
|
+
* 「带 `ref` 的结果是不带它时的子集」因此是个结构事实(FR-14.2)。
|
|
2888
|
+
*/
|
|
2889
|
+
function collect(scope, capture, subtree) {
|
|
2725
2890
|
const root = rootDocument();
|
|
2726
2891
|
if (root === void 0) return {
|
|
2727
2892
|
nodes: [],
|
|
2728
2893
|
frameNotes: [],
|
|
2729
|
-
nestedFrames: []
|
|
2894
|
+
nestedFrames: [],
|
|
2895
|
+
anchored: false
|
|
2730
2896
|
};
|
|
2731
2897
|
handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
2732
2898
|
handles.prune();
|
|
@@ -2818,6 +2984,7 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2818
2984
|
issuer.useSets(actionable, actionExcluded);
|
|
2819
2985
|
return node !== void 0;
|
|
2820
2986
|
}
|
|
2987
|
+
let anchored = subtree === void 0;
|
|
2821
2988
|
for (const { scope: frameScope, label, doc } of resolvedScopes) {
|
|
2822
2989
|
const view = doc.defaultView;
|
|
2823
2990
|
const tag = label === "self" ? void 0 : label;
|
|
@@ -2825,14 +2992,27 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2825
2992
|
issuer.useSets(sets.actionable, sets.excluded);
|
|
2826
2993
|
const excluded = /* @__PURE__ */ new Set();
|
|
2827
2994
|
for (const selector of frameScope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
|
|
2828
|
-
|
|
2995
|
+
let roots = [];
|
|
2829
2996
|
for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
|
|
2997
|
+
if (subtree !== void 0) {
|
|
2998
|
+
const inScope = roots.some((element) => element === subtree || element.contains(subtree));
|
|
2999
|
+
const cut = [...excluded].some((element) => element === subtree || element.contains(subtree));
|
|
3000
|
+
if (!inScope || cut) continue;
|
|
3001
|
+
roots = [subtree];
|
|
3002
|
+
anchored = true;
|
|
3003
|
+
}
|
|
2830
3004
|
for (const element of roots) {
|
|
2831
3005
|
const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
|
|
2832
3006
|
if (node !== void 0) nodes.push(node);
|
|
2833
3007
|
}
|
|
2834
3008
|
descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
|
|
2835
3009
|
}
|
|
3010
|
+
if (subtree !== void 0) return {
|
|
3011
|
+
nodes,
|
|
3012
|
+
frameNotes,
|
|
3013
|
+
nestedFrames,
|
|
3014
|
+
anchored
|
|
3015
|
+
};
|
|
2836
3016
|
pruneElevated();
|
|
2837
3017
|
const view = root.defaultView;
|
|
2838
3018
|
const sets = actionSets(root, actionScopeFor("self"));
|
|
@@ -2852,15 +3032,16 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2852
3032
|
return {
|
|
2853
3033
|
nodes,
|
|
2854
3034
|
frameNotes,
|
|
2855
|
-
nestedFrames
|
|
3035
|
+
nestedFrames,
|
|
3036
|
+
anchored
|
|
2856
3037
|
};
|
|
2857
3038
|
}
|
|
2858
|
-
async function readWithImages(scope, capture) {
|
|
3039
|
+
async function readWithImages(scope, capture, subtree) {
|
|
2859
3040
|
const targets = [];
|
|
2860
|
-
const { nodes, frameNotes, nestedFrames } = collect(scope, {
|
|
3041
|
+
const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, {
|
|
2861
3042
|
targets,
|
|
2862
3043
|
minImageArea: capture.minImageArea ?? 0
|
|
2863
|
-
});
|
|
3044
|
+
}, subtree);
|
|
2864
3045
|
const selected = targets.slice(0, capture.maxImages);
|
|
2865
3046
|
const images = [];
|
|
2866
3047
|
let imageFailures = 0;
|
|
@@ -2888,6 +3069,7 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2888
3069
|
images,
|
|
2889
3070
|
imagesOmitted: targets.length - selected.length,
|
|
2890
3071
|
imageFailures,
|
|
3072
|
+
anchored,
|
|
2891
3073
|
...frameNotes.length > 0 ? { frameNotes } : {},
|
|
2892
3074
|
...nestedFrames.length > 0 ? { nestedFrames } : {}
|
|
2893
3075
|
};
|
|
@@ -2904,6 +3086,28 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2904
3086
|
}
|
|
2905
3087
|
return readWithImages(scope, capture);
|
|
2906
3088
|
}
|
|
3089
|
+
/**
|
|
3090
|
+
* 子树续读(分册 14 FR-14.1)。三条判定全在遍历开始**之前**:
|
|
3091
|
+
* 句柄不认识 → `unknown-ref`;解析得到的元素已离开文档、
|
|
3092
|
+
* 或不在任何一条帧授权的 include 根下、或被 exclude 连根剪掉 → `out-of-scope`。
|
|
3093
|
+
*
|
|
3094
|
+
* 拒绝走返回值而不是抛错:跨上下文的 reject 到本地就是一个没有错误码的 `Error`。
|
|
3095
|
+
*/
|
|
3096
|
+
async function readSubtree(scope, ref, capture) {
|
|
3097
|
+
const element = handles?.table.get(ref)?.element;
|
|
3098
|
+
if (element === void 0 || !element.isConnected) return { rejected: "unknown-ref" };
|
|
3099
|
+
if (capture?.images === true && capture.maxImages > 0) {
|
|
3100
|
+
const { anchored, ...result } = await readWithImages(scope, capture, element);
|
|
3101
|
+
return anchored ? result : { rejected: "out-of-scope" };
|
|
3102
|
+
}
|
|
3103
|
+
const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, void 0, element);
|
|
3104
|
+
if (!anchored) return { rejected: "out-of-scope" };
|
|
3105
|
+
return {
|
|
3106
|
+
nodes,
|
|
3107
|
+
...frameNotes.length > 0 ? { frameNotes } : {},
|
|
3108
|
+
...nestedFrames.length > 0 ? { nestedFrames } : {}
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
2907
3111
|
/** 当前活动模态;跨帧不适用(模态提升只在主文档语义下成立) */
|
|
2908
3112
|
function modalSnapshot() {
|
|
2909
3113
|
const root = rootDocument();
|
|
@@ -2916,7 +3120,9 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2916
3120
|
}
|
|
2917
3121
|
return {
|
|
2918
3122
|
read,
|
|
3123
|
+
readSubtree,
|
|
2919
3124
|
resolve: (ref) => handles?.table.get(ref)?.element,
|
|
3125
|
+
handleKindOf: (ref) => handles?.table.get(ref)?.kind,
|
|
2920
3126
|
frameOf: (ref) => handles?.table.get(ref)?.frame,
|
|
2921
3127
|
modalSnapshot,
|
|
2922
3128
|
elevateNewModals: (before) => {
|
|
@@ -2960,6 +3166,35 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2960
3166
|
}
|
|
2961
3167
|
}
|
|
2962
3168
|
|
|
3169
|
+
//#endregion
|
|
3170
|
+
//#region ../browser/src/remote/documentIdentity.ts
|
|
3171
|
+
/**
|
|
3172
|
+
* 文档标识(0.16.0 分册 11 · FR-11.1)。
|
|
3173
|
+
*
|
|
3174
|
+
* 它有且只有两个消费者,且**必须是同一个函数**:
|
|
3175
|
+
* ① 感知回复里的 `document.key`,决定这批句柄进哪个分片;
|
|
3176
|
+
* ② `PageActionOutcome.navigated`,执行前后各取一次。
|
|
3177
|
+
* 两处各写一套判定,就会出现「操作说导航了、分片说没换」的不一致(AC-11.4 / AC-13.4 验的就是这个)。
|
|
3178
|
+
*/
|
|
3179
|
+
/** 每个文档实例一次。页面重载即换,因此真导航天然换 key */
|
|
3180
|
+
const instance = (() => {
|
|
3181
|
+
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
3182
|
+
crypto.getRandomValues(bytes);
|
|
3183
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3184
|
+
})();
|
|
3185
|
+
/**
|
|
3186
|
+
* `instance` + 当前地址。真导航换 `instance`,帧内路由(SPA)换 `href`,
|
|
3187
|
+
* 两类都被这一个串覆盖——靠 origin 比对识别不出后者。
|
|
3188
|
+
*
|
|
3189
|
+
* `href` 含 hash 是有意的:纯 hash 跳转在很多站点上会换掉整块内容,
|
|
3190
|
+
* 判成换文档是安全侧的选择(代价是多感知一次;漏判没有便宜的补救)。
|
|
3191
|
+
* @experimental
|
|
3192
|
+
*/
|
|
3193
|
+
function documentKey(view) {
|
|
3194
|
+
const href = view?.location.href;
|
|
3195
|
+
return href === void 0 ? void 0 : `${instance}\u0000${href}`;
|
|
3196
|
+
}
|
|
3197
|
+
|
|
2963
3198
|
//#endregion
|
|
2964
3199
|
//#region ../browser/src/dom/pageAction.ts
|
|
2965
3200
|
const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
|
|
@@ -3044,6 +3279,67 @@ function fillValue(element, value) {
|
|
|
3044
3279
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3045
3280
|
}
|
|
3046
3281
|
const norm = (text) => (text ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
3282
|
+
/**
|
|
3283
|
+
* 激活一个元素。
|
|
3284
|
+
*
|
|
3285
|
+
* `click()` 只定义在 `HTMLElement` 上:内联 `<svg>`(以及其它外来命名空间的元素)
|
|
3286
|
+
* 拿到的是 `undefined`,直接调会抛 `TypeError`,整次操作倒在最后一步。
|
|
3287
|
+
* 图标把监听器挂在 `<svg>` 本体上是常见写法,所以这条路径必须通。
|
|
3288
|
+
* 退路是派发一个真正的 `click` 事件:它照样冒泡,祖先 `<a>` 的激活行为也照样跑。
|
|
3289
|
+
*/
|
|
3290
|
+
function activate(element, init) {
|
|
3291
|
+
const native = element.click;
|
|
3292
|
+
if (typeof native === "function") native.call(element);
|
|
3293
|
+
else element.dispatchEvent(new MouseEvent("click", init));
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* 一次「像人一样」的点击。
|
|
3297
|
+
*
|
|
3298
|
+
* `HTMLElement.click()` **只**派发 `click`,不派发指针/鼠标序列,也不移动焦点。
|
|
3299
|
+
* 大量自研下拉、弹层、菜单是在 `pointerdown` / `mousedown` 上开的(这样拖拽和
|
|
3300
|
+
* 「点外面关闭」才好写),于是模型「点了但什么都没发生」,人手点却正常——
|
|
3301
|
+
* 差别不在权限,在事件序列。真实用户点击本来就会先后产生 down 与 click 两拨事件,
|
|
3302
|
+
* 因此补齐序列不会造成「点两下」。
|
|
3303
|
+
*
|
|
3304
|
+
* 激活行为仍交给 `.click()`:链接跳转、复选框翻转这些默认动作由它保证。
|
|
3305
|
+
*/
|
|
3306
|
+
function press(element) {
|
|
3307
|
+
const view = element.ownerDocument.defaultView;
|
|
3308
|
+
const rect = element.getBoundingClientRect();
|
|
3309
|
+
const base = {
|
|
3310
|
+
bubbles: true,
|
|
3311
|
+
cancelable: true,
|
|
3312
|
+
composed: true,
|
|
3313
|
+
detail: 1,
|
|
3314
|
+
button: 0,
|
|
3315
|
+
clientX: rect.left + rect.width / 2,
|
|
3316
|
+
clientY: rect.top + rect.height / 2
|
|
3317
|
+
};
|
|
3318
|
+
const pointer = {
|
|
3319
|
+
...base,
|
|
3320
|
+
pointerId: 1,
|
|
3321
|
+
pointerType: "mouse",
|
|
3322
|
+
isPrimary: true
|
|
3323
|
+
};
|
|
3324
|
+
const Pointer = view?.PointerEvent;
|
|
3325
|
+
if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerdown", {
|
|
3326
|
+
...pointer,
|
|
3327
|
+
buttons: 1
|
|
3328
|
+
}));
|
|
3329
|
+
if (element.dispatchEvent(new MouseEvent("mousedown", {
|
|
3330
|
+
...base,
|
|
3331
|
+
buttons: 1
|
|
3332
|
+
}))) element.focus?.();
|
|
3333
|
+
if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerup", {
|
|
3334
|
+
...pointer,
|
|
3335
|
+
buttons: 0
|
|
3336
|
+
}));
|
|
3337
|
+
element.dispatchEvent(new MouseEvent("mouseup", {
|
|
3338
|
+
...base,
|
|
3339
|
+
buttons: 0
|
|
3340
|
+
}));
|
|
3341
|
+
activate(element, base);
|
|
3342
|
+
}
|
|
3047
3343
|
/** 轮询等待,上限固定:等不到就失败,不能无限挂着一次工具调用 */
|
|
3048
3344
|
async function waitFor(probe, timeoutMs = 2e3) {
|
|
3049
3345
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -3082,12 +3378,18 @@ async function selectOption(element, value) {
|
|
|
3082
3378
|
} : fail(`The control did not accept "${value}".`);
|
|
3083
3379
|
}
|
|
3084
3380
|
const doc = element.ownerDocument;
|
|
3085
|
-
element
|
|
3381
|
+
press(element);
|
|
3086
3382
|
const listbox = await waitFor(() => doc.querySelector("[role=listbox], [role=grid], [role=menu]") ?? void 0);
|
|
3087
3383
|
if (listbox === null || listbox === void 0) return fail("The options panel did not open.");
|
|
3088
3384
|
const option = [...listbox.querySelectorAll("[role=option], [role=gridcell], [role=menuitem], option")].find((candidate) => norm(accessibleName(candidate)) === norm(value));
|
|
3089
3385
|
if (option === void 0) return fail(`No option named "${value}" is available.`);
|
|
3090
|
-
option
|
|
3386
|
+
activate(option, {
|
|
3387
|
+
bubbles: true,
|
|
3388
|
+
cancelable: true,
|
|
3389
|
+
composed: true,
|
|
3390
|
+
detail: 1,
|
|
3391
|
+
button: 0
|
|
3392
|
+
});
|
|
3091
3393
|
return await waitFor(() => {
|
|
3092
3394
|
const shown = `${accessibleName(element) ?? ""} ${element.value ?? ""}`;
|
|
3093
3395
|
return norm(shown).includes(norm(value)) ? true : void 0;
|
|
@@ -3120,7 +3422,7 @@ function setToggle(element, value, target) {
|
|
|
3120
3422
|
target,
|
|
3121
3423
|
noop: true
|
|
3122
3424
|
};
|
|
3123
|
-
element
|
|
3425
|
+
press(element);
|
|
3124
3426
|
return {
|
|
3125
3427
|
ok: true,
|
|
3126
3428
|
target
|
|
@@ -3163,7 +3465,44 @@ function createDomPageActionExecutor(options) {
|
|
|
3163
3465
|
elevated: true
|
|
3164
3466
|
} : described;
|
|
3165
3467
|
};
|
|
3468
|
+
/**
|
|
3469
|
+
* 后退(0.16.0 分册 13 / FR-13.6)。
|
|
3470
|
+
*
|
|
3471
|
+
* 两种「没退成」都返回 `ok: true, noop: true` 而不是失败:
|
|
3472
|
+
* 返回失败模型会重试,而重试同样退不动——它需要的是「这条路走不通」的事实,
|
|
3473
|
+
* 不是一个可以再试一次的错误(与 `set` 的 noop 同惯例)。
|
|
3474
|
+
*/
|
|
3475
|
+
const goBack = async () => {
|
|
3476
|
+
const view = (options.document ?? globalThis.document)?.defaultView;
|
|
3477
|
+
const target = { role: "document" };
|
|
3478
|
+
if (view === void 0 || view === null) return {
|
|
3479
|
+
ok: false,
|
|
3480
|
+
target,
|
|
3481
|
+
reason: "No browsing context is available."
|
|
3482
|
+
};
|
|
3483
|
+
if (view.history.length <= 1) return {
|
|
3484
|
+
ok: true,
|
|
3485
|
+
target,
|
|
3486
|
+
noop: true
|
|
3487
|
+
};
|
|
3488
|
+
const keyBefore = documentKey(view);
|
|
3489
|
+
view.history.back();
|
|
3490
|
+
return await waitFor(() => {
|
|
3491
|
+
const key = documentKey(view);
|
|
3492
|
+
return key !== void 0 && key !== keyBefore ? key : void 0;
|
|
3493
|
+
}, 500) === void 0 ? {
|
|
3494
|
+
ok: true,
|
|
3495
|
+
target,
|
|
3496
|
+
noop: true
|
|
3497
|
+
} : {
|
|
3498
|
+
ok: true,
|
|
3499
|
+
target,
|
|
3500
|
+
navigated: true,
|
|
3501
|
+
documentUrl: view.location.href
|
|
3502
|
+
};
|
|
3503
|
+
};
|
|
3166
3504
|
const execute = async (request) => {
|
|
3505
|
+
if (request.action === "back") return await goBack();
|
|
3167
3506
|
const element = reader.resolve(request.ref);
|
|
3168
3507
|
if (element === void 0) return {
|
|
3169
3508
|
ok: false,
|
|
@@ -3176,36 +3515,37 @@ function createDomPageActionExecutor(options) {
|
|
|
3176
3515
|
target,
|
|
3177
3516
|
reason
|
|
3178
3517
|
});
|
|
3518
|
+
if (reader.handleKindOf(request.ref) === "anchor") return fail("That reference points at a container, which cannot be acted on. Use it with perceive_page to read inside it, then act on an element that carries its own reference. If nothing inside it has one, this part of the page offers no action and retrying here will not help.");
|
|
3179
3519
|
if (reader.modalStateOf(element) === "unelevated") return fail("That dialog is not in the authorized scope because it was opened manually. Ask me to open it, or add it to the host allowlist.");
|
|
3180
3520
|
if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
|
|
3181
3521
|
if (hidden(element)) return fail("The element is not visible.");
|
|
3182
3522
|
if (element.hasAttribute("disabled")) return fail("The element is disabled.");
|
|
3183
3523
|
const modalsBefore = reader.modalSnapshot();
|
|
3184
3524
|
const frameView = element.ownerDocument.defaultView;
|
|
3185
|
-
const
|
|
3525
|
+
const keyBefore = documentKey(frameView);
|
|
3186
3526
|
const done = async (outcome) => {
|
|
3187
3527
|
if (!outcome.ok) return outcome;
|
|
3188
3528
|
const settled = await waitFor(() => {
|
|
3189
3529
|
const modal = reader.elevateNewModals(modalsBefore);
|
|
3190
3530
|
if (modal !== void 0) return { modal };
|
|
3191
|
-
const
|
|
3192
|
-
return
|
|
3531
|
+
const key = documentKey(frameView);
|
|
3532
|
+
return key !== void 0 && key !== keyBefore ? { key } : void 0;
|
|
3193
3533
|
}, 500);
|
|
3194
3534
|
const elevated = settled !== void 0 && "modal" in settled ? settled.modal : reader.elevateNewModals(modalsBefore);
|
|
3195
3535
|
const name = elevated === void 0 ? void 0 : accessibleName(elevated) ?? "dialog";
|
|
3196
|
-
const
|
|
3197
|
-
const navigated =
|
|
3536
|
+
const keyAfter = documentKey(frameView);
|
|
3537
|
+
const navigated = keyAfter !== void 0 && keyBefore !== void 0 && keyAfter !== keyBefore;
|
|
3198
3538
|
return {
|
|
3199
3539
|
...outcome,
|
|
3200
3540
|
...name !== void 0 ? { elevatedModal: name } : {},
|
|
3201
3541
|
...navigated ? {
|
|
3202
3542
|
navigated: true,
|
|
3203
|
-
documentUrl:
|
|
3543
|
+
documentUrl: frameView?.location.href
|
|
3204
3544
|
} : {}
|
|
3205
3545
|
};
|
|
3206
3546
|
};
|
|
3207
3547
|
if (request.action === "click") {
|
|
3208
|
-
element
|
|
3548
|
+
press(element);
|
|
3209
3549
|
return await done({
|
|
3210
3550
|
ok: true,
|
|
3211
3551
|
target
|
|
@@ -3262,35 +3602,114 @@ function createDomPageActionExecutor(options) {
|
|
|
3262
3602
|
|
|
3263
3603
|
//#endregion
|
|
3264
3604
|
//#region ../browser/src/remote/registry.ts
|
|
3605
|
+
/**
|
|
3606
|
+
* 分片数的防泄漏软上限(分册 11 / DV-2)。
|
|
3607
|
+
*
|
|
3608
|
+
* 它**不是** FR-11.5 那个容量闸门:那一条管的是「同时有几个**标签页**可操作」,
|
|
3609
|
+
* 触顶硬拒绝,落在 `TabWorksetPolicy`。这里管的是同一个标签页里连续跳页攒下的历史分片,
|
|
3610
|
+
* 那些本来就该失效,按最久未使用逐出即可,逐出不构成对模型的拒绝。
|
|
3611
|
+
* 软上限必须宽于 `TAB_WORKSET_MAX_MAX`(分册 18),否则工作集里**还活着**的页
|
|
3612
|
+
* 会因为别的页新发分片而被逐出;64 = 天花板 32 × 每页两代历史。
|
|
3613
|
+
*/
|
|
3614
|
+
const SHARD_SOFT_LIMIT = 64;
|
|
3615
|
+
/** 不给 `shard` 时用的分片键;单分片宿主全程待在这一个分片里 */
|
|
3616
|
+
const DEFAULT_SHARD = "";
|
|
3617
|
+
/**
|
|
3618
|
+
* 把解析结果翻译成给模型看的补充说明(分册 11 / FR-11.6);
|
|
3619
|
+
* 接到 `PageActionPolicyOptions.diagnose` 上即可:
|
|
3620
|
+
*
|
|
3621
|
+
* ```ts
|
|
3622
|
+
* diagnose: (ref) => explainResolution(targets.resolve(ref))
|
|
3623
|
+
* ```
|
|
3624
|
+
*
|
|
3625
|
+
* `unknown` 返回 `undefined` 而不是自己拼一句:让 policy 落回它的缺省文案,
|
|
3626
|
+
* 于是 AC-11.3 要求的「与 0.15.0 逐字相同」是**构造性**成立的,
|
|
3627
|
+
* 而不是靠两个文件里的字符串抄得一样。
|
|
3628
|
+
* @experimental
|
|
3629
|
+
*/
|
|
3630
|
+
function explainResolution(resolution) {
|
|
3631
|
+
if (resolution.status === "unfocused") return "This reference belongs to a different page. Switch to that page first, then retry.";
|
|
3632
|
+
if (resolution.status === "expired") return "The page this reference belongs to has changed or was closed. Perceive the page again and retry.";
|
|
3633
|
+
}
|
|
3265
3634
|
/** @experimental */
|
|
3266
|
-
function createRemoteTargetRegistry() {
|
|
3267
|
-
|
|
3635
|
+
function createRemoteTargetRegistry(options = {}) {
|
|
3636
|
+
const maxShards = Math.max(1, Math.trunc(options.maxShards ?? 64));
|
|
3637
|
+
let shards = /* @__PURE__ */ new Map();
|
|
3638
|
+
let refIndex = /* @__PURE__ */ new Map();
|
|
3639
|
+
let focused = DEFAULT_SHARD;
|
|
3640
|
+
const touch = (key, shard) => {
|
|
3641
|
+
shards.delete(key);
|
|
3642
|
+
shards.set(key, shard);
|
|
3643
|
+
while (shards.size > maxShards) {
|
|
3644
|
+
const oldest = shards.keys().next();
|
|
3645
|
+
if (oldest.done === true) break;
|
|
3646
|
+
shards.delete(oldest.value);
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3268
3649
|
return {
|
|
3269
3650
|
/**
|
|
3270
3651
|
* 整表替换而非合并(FR-13.2):合并会让上一次感知的 ref 在本次感知后仍然可执行——
|
|
3271
3652
|
* 那正是 `PAGE_ACTION_STALE_REF` 要拦的东西。本地形态里旧 ref 天然失效
|
|
3272
3653
|
* (DOM 重新遍历、句柄表 prune 掉离开文档的条目),远端必须手动复现这个失效语义,
|
|
3273
3654
|
* 否则远端比本地更松。
|
|
3655
|
+
*
|
|
3656
|
+
* 0.16.0 起替换的是**该分片**的表,别的分片不受影响——这是相对 0.15.0 的唯一语义变化。
|
|
3274
3657
|
*/
|
|
3275
|
-
replace(entries) {
|
|
3276
|
-
const
|
|
3658
|
+
replace(entries, document) {
|
|
3659
|
+
const shard = document?.key ?? DEFAULT_SHARD;
|
|
3660
|
+
const table = /* @__PURE__ */ new Map();
|
|
3277
3661
|
for (const entry of entries) {
|
|
3278
3662
|
const { ref, role, name, secret, frame, elevated } = entry;
|
|
3279
|
-
|
|
3663
|
+
table.set(ref, {
|
|
3280
3664
|
role,
|
|
3281
3665
|
...name !== void 0 ? { name } : {},
|
|
3282
3666
|
...secret === true ? { secret: true } : {},
|
|
3283
3667
|
...frame !== void 0 ? { frame } : {},
|
|
3284
3668
|
...elevated === true ? { elevated: true } : {}
|
|
3285
3669
|
});
|
|
3670
|
+
refIndex.set(ref, shard);
|
|
3286
3671
|
}
|
|
3287
|
-
|
|
3672
|
+
touch(shard, {
|
|
3673
|
+
table,
|
|
3674
|
+
...document !== void 0 ? { document } : {}
|
|
3675
|
+
});
|
|
3676
|
+
focused = shard;
|
|
3288
3677
|
},
|
|
3289
3678
|
get(ref) {
|
|
3290
|
-
return table.get(ref);
|
|
3679
|
+
return shards.get(focused)?.table.get(ref);
|
|
3680
|
+
},
|
|
3681
|
+
resolve(ref) {
|
|
3682
|
+
const target = shards.get(focused)?.table.get(ref);
|
|
3683
|
+
if (target !== void 0) return {
|
|
3684
|
+
status: "ok",
|
|
3685
|
+
target
|
|
3686
|
+
};
|
|
3687
|
+
const owner = refIndex.get(ref);
|
|
3688
|
+
if (owner === void 0) return { status: "unknown" };
|
|
3689
|
+
return shards.get(owner)?.table.has(ref) === true ? { status: "unfocused" } : { status: "expired" };
|
|
3690
|
+
},
|
|
3691
|
+
focus(shard) {
|
|
3692
|
+
focused = shard;
|
|
3693
|
+
},
|
|
3694
|
+
get focused() {
|
|
3695
|
+
return focused === DEFAULT_SHARD ? void 0 : focused;
|
|
3696
|
+
},
|
|
3697
|
+
describePage() {
|
|
3698
|
+
return shards.get(focused)?.document;
|
|
3699
|
+
},
|
|
3700
|
+
drop(shard) {
|
|
3701
|
+
shards.delete(shard);
|
|
3702
|
+
},
|
|
3703
|
+
clear() {
|
|
3704
|
+
shards = /* @__PURE__ */ new Map();
|
|
3705
|
+
refIndex = /* @__PURE__ */ new Map();
|
|
3706
|
+
focused = DEFAULT_SHARD;
|
|
3291
3707
|
},
|
|
3292
3708
|
get size() {
|
|
3293
|
-
return table.size;
|
|
3709
|
+
return shards.get(focused)?.table.size ?? 0;
|
|
3710
|
+
},
|
|
3711
|
+
get shardCount() {
|
|
3712
|
+
return shards.size;
|
|
3294
3713
|
}
|
|
3295
3714
|
};
|
|
3296
3715
|
}
|
|
@@ -3338,14 +3757,15 @@ async function roundTrip(transport, request, expect, code, timeoutMs) {
|
|
|
3338
3757
|
* @experimental
|
|
3339
3758
|
*/
|
|
3340
3759
|
function createRemotePerceptionReader(options) {
|
|
3341
|
-
const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
|
|
3760
|
+
const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS, onDocument } = options;
|
|
3342
3761
|
return { async read(scope, capture) {
|
|
3343
3762
|
const reply = await roundTrip(transport, {
|
|
3344
3763
|
type: "perceive",
|
|
3345
3764
|
scope,
|
|
3346
3765
|
...capture !== void 0 ? { capture } : {}
|
|
3347
3766
|
}, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
|
|
3348
|
-
targets.replace(reply.targets);
|
|
3767
|
+
targets.replace(reply.targets, reply.document);
|
|
3768
|
+
if (reply.document !== void 0) onDocument?.(reply.document);
|
|
3349
3769
|
return reply.result;
|
|
3350
3770
|
} };
|
|
3351
3771
|
}
|
|
@@ -3416,8 +3836,25 @@ function createPageAgentHandler(options = {}) {
|
|
|
3416
3836
|
});
|
|
3417
3837
|
const executor = createDomPageActionExecutor({
|
|
3418
3838
|
reader,
|
|
3839
|
+
...options.document !== void 0 ? { document: options.document } : {},
|
|
3419
3840
|
...options.pickFiles !== void 0 ? { pickFiles: options.pickFiles } : {}
|
|
3420
3841
|
});
|
|
3842
|
+
/**
|
|
3843
|
+
* 这批句柄属于哪个文档(FR-11.1)。`key` 与 `PageActionOutcome.navigated`
|
|
3844
|
+
* 出自同一个 `documentKey()` —— 这是「操作说导航了、分片说没换」不可能发生的构造性保证。
|
|
3845
|
+
*/
|
|
3846
|
+
const describeDocument = () => {
|
|
3847
|
+
const doc = options.document ?? globalThis.document;
|
|
3848
|
+
const key = documentKey(doc?.defaultView);
|
|
3849
|
+
if (key === void 0) return void 0;
|
|
3850
|
+
const url = doc.defaultView?.location.href;
|
|
3851
|
+
const title = doc.title;
|
|
3852
|
+
return {
|
|
3853
|
+
key,
|
|
3854
|
+
...url !== void 0 ? { url } : {},
|
|
3855
|
+
...title !== void 0 && title !== "" ? { title } : {}
|
|
3856
|
+
};
|
|
3857
|
+
};
|
|
3421
3858
|
return { async handle(request) {
|
|
3422
3859
|
try {
|
|
3423
3860
|
if (request.type === "perceive") {
|
|
@@ -3431,10 +3868,12 @@ function createPageAgentHandler(options = {}) {
|
|
|
3431
3868
|
...target
|
|
3432
3869
|
});
|
|
3433
3870
|
}
|
|
3871
|
+
const identity = describeDocument();
|
|
3434
3872
|
return {
|
|
3435
3873
|
type: "perceive-result",
|
|
3436
3874
|
result,
|
|
3437
|
-
targets
|
|
3875
|
+
targets,
|
|
3876
|
+
...identity !== void 0 ? { document: identity } : {}
|
|
3438
3877
|
};
|
|
3439
3878
|
}
|
|
3440
3879
|
return {
|
|
@@ -3634,7 +4073,8 @@ function createFrameRouter(options) {
|
|
|
3634
4073
|
...imageFailures > 0 ? { imageFailures } : {},
|
|
3635
4074
|
...frameNotes.length > 0 ? { frameNotes } : {}
|
|
3636
4075
|
},
|
|
3637
|
-
targets
|
|
4076
|
+
targets,
|
|
4077
|
+
...main.reply.document !== void 0 ? { document: main.reply.document } : {}
|
|
3638
4078
|
};
|
|
3639
4079
|
}
|
|
3640
4080
|
async function execute(request) {
|
|
@@ -5151,4 +5591,4 @@ function originOf(blockedURI) {
|
|
|
5151
5591
|
}
|
|
5152
5592
|
|
|
5153
5593
|
//#endregion
|
|
5154
|
-
export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_FRAME_BUDGET, 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, createDocumentSurfaceHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createFrameRouter, 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 };
|
|
5594
|
+
export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_CAMERA_MAX_DIMENSION, DEFAULT_FRAME_BUDGET, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, SANDBOX_PAGE_SCRIPT_SOURCE, SHARD_SOFT_LIMIT, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, capturePhoto, checkCameraAvailability, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDocumentSurfaceHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createFrameRouter, createIframeWorker, createLlmClient, createPageAgentHandler, createRemotePageActionExecutor, createRemotePerceptionReader, createRemoteTargetRegistry, deleteMemoryEncryptionKey, documentKey, explainResolution, extractDocxText, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openCamera, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };
|