@webskill/sdk 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/agent.d.ts +3 -2
  2. package/dist/agent.js +3 -1102
  3. package/dist/browser.d.ts +258 -11
  4. package/dist/browser.js +758 -46
  5. package/dist/{catalogComponents-DTcYfpLQ-CcoOaz-Z.js → catalogComponents-BFoqpT1v-CjUBZ3bc.js} +253 -14
  6. package/dist/{dist-BViUeszk.js → dist-B-cOu08W.js} +526 -76
  7. package/dist/{dist-1OFC-zax.js → dist-DTHZS2k1.js} +520 -28
  8. package/dist/dist-qnlI2Iup.js +1280 -0
  9. package/dist/{eventTypes-s2uwAcLG-Go3l_dUe.js → eventTypes-FllCrX-Z-DNDeHWoG.js} +6 -2
  10. package/dist/governance.d.ts +7 -3
  11. package/dist/governance.js +1 -1
  12. package/dist/{index-DtFdMKBX.d.ts → index-D3mONFHD.d.ts} +220 -7
  13. package/dist/{index-B0QPLWPZ.d.ts → index-DACk2_XZ.d.ts} +110 -7
  14. package/dist/{index-BF4E1a9j.d.ts → index-DWbs58LF.d.ts} +227 -14
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +3 -3
  17. package/dist/mcp.d.ts +35 -7
  18. package/dist/mcp.js +88 -21
  19. package/dist/node.d.ts +3 -3
  20. package/dist/node.js +52 -2
  21. package/dist/{openUiLibrary-CIrV--Ad-B8zG_91e.js → openUiLibrary-D5u8oIvx-BLOAQCho.js} +3 -3
  22. package/dist/processSandboxEntry.js +6 -0
  23. package/dist/sandboxWorkerEntry.js +6 -0
  24. package/dist/{skillVersionStore-D-qHk9ZE-DBsYYCWn.d.ts → skillVersionStore-D-qHk9ZE-BcmFLykd.d.ts} +1 -1
  25. package/dist/testing.d.ts +1 -1
  26. package/dist/{types-CrRcT-LM-DZAp8sWv.d.ts → types-C26b05fW-CdrRCRDb.d.ts} +20 -4
  27. package/dist/ui-react.d.ts +2 -2
  28. package/dist/ui-react.js +28 -10
  29. package/dist/ui-vue.d.ts +1 -1
  30. package/dist/ui-vue.js +2 -2
  31. package/dist/ui.d.ts +4 -4
  32. package/dist/ui.js +3 -3
  33. package/dist/{webskillLitCatalog-BJrphK0y-SGmRXaaO.js → webskillLitCatalog-DME6PBkV-CmYNLlIT.js} +135 -16
  34. package/package.json +1 -1
package/dist/browser.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { D as exportSkills, F as parseSkillPackManifest, G as unzipWithLimits, I as readResponseWithLimit, J as verifySkillSignature, K as validateSkills, L as readSkillSignature, M as messageOf, O as isAtomicTempPath, P as parseSkillMarkdown, T as detectSkillArchiveShapeFromFs, V as resolveInsideRoot, b as buildManifest, c as SKILL_MANIFEST_FILE, d as SKILL_PACK_FILE, g as assertRemoteUrlAllowed, h as WebSkillError, i as MANIFEST_EXCLUDED_FILES, k as isValidSkillName, p as SkillDiscovery, q as verifyManifest, s as SKILLS_LOCKFILE, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
2
- import { B as bridgeError, C as ProgressiveRouter, S as OpenAiCompatibleClient, U as createWebSkillApi, a as AgentLoop, b as GoogleGenAiClient, c as CapabilityApproval, dt as parseBridgeRequest, g as FsRunSnapshotStore, h as FsMemoryStore, it as mergeCatalogEntries, lt as normalizeToolContent, m as FsArtifactStore, o as AnthropicClient, ot as networkPolicyLibSource, ut as normalizeToolError } from "./dist-BViUeszk.js";
2
+ import { $ as createWebSkillApi, D as OpenAiCompatibleClient, O as ProgressiveRouter, T as GoogleGenAiClient, X as bridgeError, b as FsMemoryStore, bt as normalizeToolError, c as AnthropicClient, gt as networkPolicyLibSource, mt as mergeCatalogEntries, s as AgentLoop, u as CapabilityApproval, x as FsRunSnapshotStore, xt as parseBridgeRequest, y as FsArtifactStore, yt as normalizeToolContent } from "./dist-B-cOu08W.js";
3
+ import { T as toFrameScopes, w as toActionFrameScopes } from "./dist-qnlI2Iup.js";
3
4
  import { n as MockLlmClient } from "./testing-WPTyXQYt.js";
4
5
 
5
6
  //#region ../browser/dist/index.js
@@ -304,6 +305,18 @@ function makeContext(msg) {
304
305
  readReference: function (path) {
305
306
  return callCapability('readReference', { path: path });
306
307
  },
308
+ readAsset: function (path) {
309
+ return callCapability('readAsset', { path: path });
310
+ },
311
+ fetchData: function (sourceId, params) {
312
+ return callCapability('fetchData', { sourceId: sourceId, params: params });
313
+ },
314
+ readAssetBinary: function (path) {
315
+ // 线上是 number[](与 writeArtifact 同口径),还给脚本的必须是 Uint8Array
316
+ return callCapability('readAssetBinary', { path: path }).then(function (bytes) {
317
+ return new Uint8Array(bytes);
318
+ });
319
+ },
307
320
  writeArtifact: function (path, content, options) {
308
321
  return callCapability('writeArtifact', {
309
322
  path: path,
@@ -1018,8 +1031,10 @@ var BrowserWorkerScriptExecutor = class {
1018
1031
  }) : void 0;
1019
1032
  this.#capabilities = {
1020
1033
  readReference: deps.capabilities?.readReference ?? true,
1034
+ readAsset: deps.capabilities?.readAsset ?? true,
1021
1035
  writeArtifact: deps.capabilities?.writeArtifact ?? true,
1022
- confirm: deps.capabilities?.confirm ?? true
1036
+ confirm: deps.capabilities?.confirm ?? true,
1037
+ fetchData: deps.capabilities?.fetchData ?? false
1023
1038
  };
1024
1039
  this.#networkPolicy = deps.networkPolicy ?? "deny-all";
1025
1040
  this.#approval = new CapabilityApproval({
@@ -1192,6 +1207,29 @@ var BrowserWorkerScriptExecutor = class {
1192
1207
  value: await context.readReference(request.path)
1193
1208
  };
1194
1209
  }
1210
+ case "readAsset":
1211
+ case "readAssetBinary": {
1212
+ const read = request.kind === "readAsset" ? context.readAsset : context.readAssetBinary;
1213
+ if (!read) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"readAsset\" is disabled");
1214
+ const denied = await gate("readAsset", `Script "${context.skillName}" wants to read asset "${request.path}"`, { path: request.path });
1215
+ if (denied) return denied;
1216
+ const value = await read(request.path);
1217
+ return {
1218
+ id: request.id,
1219
+ ok: true,
1220
+ value: typeof value === "string" ? value : Array.from(value)
1221
+ };
1222
+ }
1223
+ case "fetchData": {
1224
+ if (!context.fetchData) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"fetchData\" is disabled");
1225
+ const denied = await gate("fetchData", `Script "${context.skillName}" wants to fetch data source "${request.sourceId}"`, { sourceId: request.sourceId });
1226
+ if (denied) return denied;
1227
+ return {
1228
+ id: request.id,
1229
+ ok: true,
1230
+ value: await context.fetchData(request.sourceId, request.params)
1231
+ };
1232
+ }
1195
1233
  case "writeArtifact": {
1196
1234
  const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
1197
1235
  path: request.path,
@@ -1282,10 +1320,10 @@ async function probeChromeBuiltinAvailability() {
1282
1320
  };
1283
1321
  }
1284
1322
  }
1285
- const textOf = (message) => message.content.map((part) => part.type === "text" ? part.text : `[${part.type}]`).join("").trim();
1323
+ const textOf$1 = (message) => message.content.map((part) => part.type === "text" ? part.text : `[${part.type}]`).join("").trim();
1286
1324
  /** tool 角色 Prompt API 不认识,降级成 user 文本——内置模型本来就不做工具调用 */
1287
1325
  function toInitialPrompt(message) {
1288
- const text = textOf(message);
1326
+ const text = textOf$1(message);
1289
1327
  if (message.role === "assistant") return {
1290
1328
  role: "assistant",
1291
1329
  content: text
@@ -1296,7 +1334,7 @@ function toInitialPrompt(message) {
1296
1334
  };
1297
1335
  }
1298
1336
  function splitInput(input) {
1299
- const system = input.messages.filter((m) => m.role === "system").map(textOf).filter((text) => text !== "");
1337
+ const system = input.messages.filter((m) => m.role === "system").map(textOf$1).filter((text) => text !== "");
1300
1338
  const rest = input.messages.filter((m) => m.role !== "system");
1301
1339
  const last = rest.at(-1);
1302
1340
  return {
@@ -1736,6 +1774,17 @@ const clip = (text) => {
1736
1774
  const normalized = text.replace(/\s+/g, " ").trim();
1737
1775
  return normalized.length > MAX_TEXT ? `${normalized.slice(0, MAX_TEXT)}…` : normalized;
1738
1776
  };
1777
+ /**
1778
+ * 链接目标,已按文档基准解析成绝对 URL。
1779
+ *
1780
+ * `<a>` / `<area>` 的 `.href` 属性天然是解析过的;`role="link"` 的其它元素没有这个属性,
1781
+ * 只能退回原始属性值——那种情况下 URL 是否可用由取数侧的准入判定说了算。
1782
+ */
1783
+ function hrefOf(element) {
1784
+ const anchor = element;
1785
+ if (typeof anchor.href === "string" && anchor.href !== "") return anchor.href;
1786
+ return element.getAttribute("href") ?? void 0;
1787
+ }
1739
1788
  function roleOf$1(element) {
1740
1789
  const explicit = element.getAttribute("role");
1741
1790
  if (explicit !== null && explicit.trim() !== "") return explicit.trim();
@@ -1804,26 +1853,32 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
1804
1853
  "NOSCRIPT",
1805
1854
  "TEMPLATE"
1806
1855
  ]);
1807
- function describe(element, excluded, doc, view, targets, handles) {
1856
+ /** 活动模态的三种形态;Radix 之流用 aria-modal,原生用 <dialog open> */
1857
+ const MODAL_SELECTOR = "[role=dialog][open], dialog[open], [aria-modal=true]";
1858
+ function describe(element, excluded, doc, view, targets, handles, frame, provenance) {
1808
1859
  if (excluded.has(element)) return void 0;
1809
1860
  if (SKIPPED_TAGS.has(element.tagName)) return void 0;
1810
1861
  if (hidden$1(element, view)) return void 0;
1811
1862
  const children = [];
1812
1863
  for (const child of element.children) {
1813
- const node = describe(child, excluded, doc, view, targets, handles);
1864
+ const node = describe(child, excluded, doc, view, targets, handles, frame, provenance);
1814
1865
  if (node !== void 0) children.push(node);
1815
1866
  }
1816
1867
  const name = nameOf(element, doc);
1817
1868
  const value = valueOf(element);
1818
1869
  const role = roleOf$1(element);
1870
+ const href = role === "link" ? hrefOf(element) : void 0;
1819
1871
  if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
1820
1872
  const node = {
1821
1873
  role,
1822
1874
  ...name !== void 0 ? { name } : {},
1823
1875
  ...value !== void 0 ? { value } : {},
1876
+ ...frame !== void 0 ? { frame } : {},
1877
+ ...href !== void 0 ? { href } : {},
1878
+ ...provenance !== void 0 ? { provenance } : {},
1824
1879
  ...children.length > 0 ? { children } : {}
1825
1880
  };
1826
- const ref = handles?.issue(element, role);
1881
+ const ref = handles?.issue(element, role, frame);
1827
1882
  if (ref !== void 0) node.ref = ref;
1828
1883
  if (targets !== void 0 && isCapturableElement(element)) targets.push({
1829
1884
  element,
@@ -1843,13 +1898,21 @@ var HandleIssuer = class {
1843
1898
  this.#actionable = actionable;
1844
1899
  this.#excluded = excluded;
1845
1900
  }
1846
- issue(element, role) {
1901
+ /** 每帧一份可操作集合;发句柄时按帧替换,避免 A 帧的集合拿去判 B 帧的元素 */
1902
+ useSets(actionable, excluded) {
1903
+ this.#actionable = actionable;
1904
+ this.#excluded = excluded;
1905
+ }
1906
+ issue(element, role, frame) {
1847
1907
  if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
1848
1908
  if (!ACTIONABLE_ROLES.has(role)) return void 0;
1849
1909
  const bytes = /* @__PURE__ */ new Uint8Array(8);
1850
1910
  crypto.getRandomValues(bytes);
1851
1911
  const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
1852
- this.#table.set(ref, element);
1912
+ this.#table.set(ref, {
1913
+ element,
1914
+ ...frame !== void 0 ? { frame } : {}
1915
+ });
1853
1916
  return ref;
1854
1917
  }
1855
1918
  get table() {
@@ -1866,12 +1929,54 @@ var HandleIssuer = class {
1866
1929
  */
1867
1930
  function createDomPerceptionReader(options = {}) {
1868
1931
  let handles;
1869
- const resolveDocument = () => options.document ?? globalThis.document;
1932
+ /** 授权生效时记下各帧的 origin,之后每次感知比对(FR-24.3) */
1933
+ const frameOrigins = /* @__PURE__ */ new Map();
1934
+ /**
1935
+ * 被提升的模态(FR-25.6):**只装由已授权操作打开的那些**。
1936
+ * 用户自己点开的对话框不在里面——它在操作前的快照里,差集会把它排除。
1937
+ */
1938
+ const elevatedModals = /* @__PURE__ */ new Set();
1939
+ const rootDocument = () => options.document ?? globalThis.document;
1940
+ /**
1941
+ * 把一条帧授权解析成 Document。
1942
+ *
1943
+ * 跨源时 `contentDocument` 在多数浏览器返回 `null` 而**不抛异常**,
1944
+ * 只有部分属性访问才抛 —— 所以两种形态都要处理,只写 try/catch 会漏。
1945
+ */
1946
+ function resolveFrame(frame, root) {
1947
+ if (frame === "self") return { doc: root };
1948
+ const element = root.querySelector(frame);
1949
+ if (element === null || element.tagName !== "IFRAME") return { note: {
1950
+ frame,
1951
+ reason: "not-found",
1952
+ message: `Frame "${frame}" was not found in the page.`
1953
+ } };
1954
+ let doc;
1955
+ let origin;
1956
+ try {
1957
+ doc = element.contentDocument;
1958
+ origin = element.contentWindow?.location.origin;
1959
+ } catch {
1960
+ doc = null;
1961
+ }
1962
+ if (doc === null || origin === void 0) return { note: {
1963
+ frame,
1964
+ reason: "cross-origin",
1965
+ message: `Frame "${frame}" is cross-origin and was not read.`
1966
+ } };
1967
+ const known = frameOrigins.get(frame);
1968
+ if (known === void 0) frameOrigins.set(frame, origin);
1969
+ else if (known !== origin) return { note: {
1970
+ frame,
1971
+ reason: "origin-changed",
1972
+ message: `Frame "${frame}" now points at ${origin} instead of the authorized ${known}; the grant was revoked.`
1973
+ } };
1974
+ return { doc };
1975
+ }
1870
1976
  /** 按当前 DOM 重算一次可操作集合;发句柄与执行前各算一次 */
1871
- const actionSets = (doc) => {
1977
+ const actionSets = (doc, scope) => {
1872
1978
  const actionable = /* @__PURE__ */ new Set();
1873
1979
  const excluded = /* @__PURE__ */ new Set();
1874
- const scope = options.actionScope;
1875
1980
  if (scope === void 0 || scope.include.length === 0) return {
1876
1981
  actionable,
1877
1982
  excluded
@@ -1889,26 +1994,67 @@ function createDomPerceptionReader(options = {}) {
1889
1994
  excluded
1890
1995
  };
1891
1996
  };
1997
+ /** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」 */
1998
+ const actionScopeFor = (frame) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => entry.frame === frame);
1892
1999
  function collect(scope, targets) {
1893
- const doc = resolveDocument();
1894
- if (doc === void 0 || scope.include.length === 0) return [];
1895
- const view = doc.defaultView;
1896
- const sets = actionSets(doc);
1897
- handles = new HandleIssuer(sets.actionable, sets.excluded);
1898
- const excluded = /* @__PURE__ */ new Set();
1899
- for (const selector of scope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
1900
- const roots = [];
1901
- for (const selector of scope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
2000
+ const root = rootDocument();
2001
+ if (root === void 0) return {
2002
+ nodes: [],
2003
+ frameNotes: []
2004
+ };
2005
+ handles = new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
1902
2006
  const nodes = [];
1903
- for (const root of roots) {
1904
- const node = describe(root, excluded, doc, view, targets, handles);
2007
+ const frameNotes = [];
2008
+ for (const frameScope of toFrameScopes(scope)) {
2009
+ if (frameScope.include.length === 0) continue;
2010
+ const resolved = resolveFrame(frameScope.frame, root);
2011
+ if ("note" in resolved) {
2012
+ frameNotes.push(resolved.note);
2013
+ nodes.push({
2014
+ role: "note",
2015
+ name: resolved.note.message,
2016
+ frame: frameScope.frame
2017
+ });
2018
+ continue;
2019
+ }
2020
+ const doc = resolved.doc;
2021
+ const view = doc.defaultView;
2022
+ const tag = frameScope.frame === "self" ? void 0 : frameScope.frame;
2023
+ const sets = actionSets(doc, actionScopeFor(frameScope.frame));
2024
+ handles.useSets(sets.actionable, sets.excluded);
2025
+ const excluded = /* @__PURE__ */ new Set();
2026
+ for (const selector of frameScope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
2027
+ const roots = [];
2028
+ for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
2029
+ for (const element of roots) {
2030
+ const node = describe(element, excluded, doc, view, targets, handles, tag, void 0);
2031
+ if (node !== void 0) nodes.push(node);
2032
+ }
2033
+ }
2034
+ pruneElevated();
2035
+ const view = root.defaultView;
2036
+ const sets = actionSets(root, actionScopeFor("self"));
2037
+ for (const modal of elevatedModals) {
2038
+ handles.useSets(/* @__PURE__ */ new Set([modal, ...modal.querySelectorAll("*")]), sets.excluded);
2039
+ const node = describe(modal, /* @__PURE__ */ new Set(), root, view, targets, handles, void 0, "modal-elevated");
1905
2040
  if (node !== void 0) nodes.push(node);
1906
2041
  }
1907
- return nodes;
2042
+ for (const modal of root.querySelectorAll(MODAL_SELECTOR)) {
2043
+ if (elevatedModals.has(modal)) continue;
2044
+ const name = nameOf(modal, root) ?? "dialog";
2045
+ nodes.push({
2046
+ role: "note",
2047
+ name: `The dialog "${name}" is open but outside the authorized scope because it was opened manually. Ask the user to let you open it instead.`
2048
+ });
2049
+ }
2050
+ return {
2051
+ nodes,
2052
+ frameNotes
2053
+ };
1908
2054
  }
1909
2055
  async function readWithImages(scope, capture) {
1910
2056
  const targets = [];
1911
- const nodes = collect(scope, targets);
2057
+ const { nodes, frameNotes } = collect(scope, targets);
1912
2058
  const selected = targets.slice(0, capture.maxImages);
1913
2059
  const images = [];
1914
2060
  let imageFailures = 0;
@@ -1935,24 +2081,68 @@ function createDomPerceptionReader(options = {}) {
1935
2081
  nodes,
1936
2082
  images,
1937
2083
  imagesOmitted: targets.length - selected.length,
1938
- imageFailures
2084
+ imageFailures,
2085
+ ...frameNotes.length > 0 ? { frameNotes } : {}
1939
2086
  };
1940
2087
  }
1941
2088
  function read(scope, capture) {
1942
- if (capture === void 0) return collect(scope, void 0);
1943
- if (!capture.images || capture.maxImages <= 0) return Promise.resolve({ nodes: collect(scope, void 0) });
2089
+ if (capture === void 0) return collect(scope, void 0).nodes;
2090
+ if (!capture.images || capture.maxImages <= 0) {
2091
+ const { nodes, frameNotes } = collect(scope, void 0);
2092
+ return Promise.resolve({
2093
+ nodes,
2094
+ ...frameNotes.length > 0 ? { frameNotes } : {}
2095
+ });
2096
+ }
1944
2097
  return readWithImages(scope, capture);
1945
2098
  }
2099
+ /** 当前活动模态;跨帧不适用(模态提升只在主文档语义下成立) */
2100
+ function modalSnapshot() {
2101
+ const root = rootDocument();
2102
+ if (root === void 0) return /* @__PURE__ */ new Set();
2103
+ return new Set(root.querySelectorAll(MODAL_SELECTOR));
2104
+ }
2105
+ /** 关掉或已从文档移除的模态立刻失去提升:授权不得跨对话框生命周期存活 */
2106
+ function pruneElevated() {
2107
+ for (const modal of elevatedModals) if (!(modal.isConnected && modal.matches(MODAL_SELECTOR))) elevatedModals.delete(modal);
2108
+ }
1946
2109
  return {
1947
2110
  read,
1948
- resolve: (ref) => handles?.table.get(ref),
2111
+ resolve: (ref) => handles?.table.get(ref)?.element,
2112
+ frameOf: (ref) => handles?.table.get(ref)?.frame,
2113
+ modalSnapshot,
2114
+ elevateNewModals: (before) => {
2115
+ pruneElevated();
2116
+ for (const modal of modalSnapshot()) {
2117
+ if (before.has(modal)) continue;
2118
+ elevatedModals.add(modal);
2119
+ return modal;
2120
+ }
2121
+ },
2122
+ modalStateOf: (element) => {
2123
+ pruneElevated();
2124
+ const modal = element.closest(MODAL_SELECTOR);
2125
+ if (modal === null) return "none";
2126
+ return elevatedModals.has(modal) ? "elevated" : "unelevated";
2127
+ },
1949
2128
  inActionScope: (element) => {
1950
- const doc = resolveDocument();
1951
- if (doc === void 0) return false;
1952
- const sets = actionSets(doc);
2129
+ const doc = element.ownerDocument;
2130
+ const frame = frameOfDocument(doc);
2131
+ if (frame === void 0) return false;
2132
+ const sets = actionSets(doc, actionScopeFor(frame));
1953
2133
  return sets.actionable.has(element) && !sets.excluded.has(element);
1954
2134
  }
1955
2135
  };
2136
+ /** 反查一个 Document 属于哪条帧授权;不在授权列表里的帧一律判不可操作 */
2137
+ function frameOfDocument(doc) {
2138
+ const root = rootDocument();
2139
+ if (root === void 0) return void 0;
2140
+ if (doc === root) return "self";
2141
+ for (const frame of frameOrigins.keys()) {
2142
+ const element = root.querySelector(frame);
2143
+ if (element?.tagName === "IFRAME" && element.contentDocument === doc) return frame;
2144
+ }
2145
+ }
1956
2146
  }
1957
2147
  const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
1958
2148
  /** 只看显式声明,不看尺寸:jsdom 有 getComputedStyle 但没有布局 */
@@ -1970,13 +2160,36 @@ function hidden(element) {
1970
2160
  }
1971
2161
  const inputType = (element) => (element.getAttribute("type") ?? "text").toLowerCase();
1972
2162
  const isSecret = (element) => element.tagName === "INPUT" && SECRET_INPUT_TYPES.has(inputType(element));
2163
+ const textOf = (element) => element?.textContent?.replace(/\s+/g, " ").trim() ?? "";
2164
+ /**
2165
+ * 可访问名称,按 accname 的优先级取。
2166
+ *
2167
+ * `aria-labelledby` 与关联 `<label>` 这两级不能省:`<input type="radio">`
2168
+ * 自己既没有 textContent 也没有 placeholder,少了它们审计里只剩 role + frame,
2169
+ * 事后看不出用户批准的到底是哪个单选组(UI-UX8 UX-08)。
2170
+ */
1973
2171
  function accessibleName(element) {
2172
+ const labelledBy = element.getAttribute("aria-labelledby");
2173
+ if (labelledBy !== null && labelledBy.trim() !== "") {
2174
+ const doc = element.ownerDocument;
2175
+ const joined = labelledBy.split(/\s+/).map((id) => textOf(doc.getElementById(id))).filter((part) => part !== "").join(" ");
2176
+ if (joined !== "") return joined;
2177
+ }
1974
2178
  const label = element.getAttribute("aria-label");
1975
2179
  if (label !== null && label.trim() !== "") return label.trim();
1976
- const text = element.textContent?.replace(/\s+/g, " ").trim() ?? "";
1977
- if (text !== "") return text;
2180
+ const labels = element.labels;
2181
+ if (labels && labels.length > 0) {
2182
+ const joined = [...labels].map((node) => textOf(node)).filter((part) => part !== "").join(" ");
2183
+ if (joined !== "") return joined;
2184
+ }
2185
+ const own = textOf(element);
2186
+ if (own !== "") return own;
2187
+ const wrapping = textOf(element.closest("label"));
2188
+ if (wrapping !== "") return wrapping;
1978
2189
  const placeholder = element.getAttribute("placeholder");
1979
- return placeholder !== null && placeholder.trim() !== "" ? placeholder.trim() : void 0;
2190
+ if (placeholder !== null && placeholder.trim() !== "") return placeholder.trim();
2191
+ const title = element.getAttribute("title");
2192
+ return title !== null && title.trim() !== "" ? title.trim() : void 0;
1980
2193
  }
1981
2194
  function roleOf(element) {
1982
2195
  const explicit = element.getAttribute("role");
@@ -2012,52 +2225,202 @@ function fillValue(element, value) {
2012
2225
  element.dispatchEvent(new Event("input", { bubbles: true }));
2013
2226
  element.dispatchEvent(new Event("change", { bubbles: true }));
2014
2227
  }
2228
+ const norm = (text) => (text ?? "").replace(/\s+/g, " ").trim().toLowerCase();
2229
+ /** 轮询等待,上限固定:等不到就失败,不能无限挂着一次工具调用 */
2230
+ async function waitFor(probe, timeoutMs = 2e3) {
2231
+ const deadline = Date.now() + timeoutMs;
2232
+ for (;;) {
2233
+ const hit = probe();
2234
+ if (hit !== void 0) return hit;
2235
+ if (Date.now() >= deadline) return void 0;
2236
+ await new Promise((resolve) => setTimeout(resolve, 25));
2237
+ }
2238
+ }
2239
+ /**
2240
+ * 一个动作覆盖三种选择控件(FR-25.2):原生 select 直接设值,
2241
+ * ARIA 组合与日期格点走「点开 → 等浮层 → 按可访问名匹配 → 点中」。
2242
+ *
2243
+ * **收尾一定要回读确认**:浮层里点了不等于选上了(受控组件可能拒绝、
2244
+ * 也可能匹配到了另一个同名项)。少了这一步就会向模型报假成功。
2245
+ */
2246
+ async function selectOption(element, value) {
2247
+ const target = describeElement(element);
2248
+ const fail = (reason) => ({
2249
+ ok: false,
2250
+ target,
2251
+ reason
2252
+ });
2253
+ if (value === "") return fail("The select action needs a value.");
2254
+ if (element.tagName === "SELECT") {
2255
+ const select = element;
2256
+ const option = [...select.options].find((candidate) => norm(candidate.label) === norm(value) || norm(candidate.value) === norm(value));
2257
+ if (option === void 0) return fail(`No option named "${value}" is available.`);
2258
+ select.value = option.value;
2259
+ select.dispatchEvent(new Event("input", { bubbles: true }));
2260
+ select.dispatchEvent(new Event("change", { bubbles: true }));
2261
+ return norm(select.selectedOptions[0]?.label) === norm(option.label) ? {
2262
+ ok: true,
2263
+ target
2264
+ } : fail(`The control did not accept "${value}".`);
2265
+ }
2266
+ const doc = element.ownerDocument;
2267
+ element.click();
2268
+ const listbox = await waitFor(() => doc.querySelector("[role=listbox], [role=grid], [role=menu]") ?? void 0);
2269
+ if (listbox === null || listbox === void 0) return fail("The options panel did not open.");
2270
+ const option = [...listbox.querySelectorAll("[role=option], [role=gridcell], [role=menuitem], option")].find((candidate) => norm(accessibleName(candidate)) === norm(value));
2271
+ if (option === void 0) return fail(`No option named "${value}" is available.`);
2272
+ option.click();
2273
+ return await waitFor(() => {
2274
+ const shown = `${accessibleName(element) ?? ""} ${element.value ?? ""}`;
2275
+ return norm(shown).includes(norm(value)) ? true : void 0;
2276
+ }) === true ? {
2277
+ ok: true,
2278
+ target
2279
+ } : fail(`The control did not settle on "${value}".`);
2280
+ }
2281
+ /**
2282
+ * 幂等置值(FR-25.4):当前态已是目标态就**不点**,避免「点一下反而关掉」。
2283
+ * 未操作时仍返回 ok —— 返回失败模型会重试,假装点了审计会失真。
2284
+ */
2285
+ function setToggle(element, value, target) {
2286
+ const wanted = norm(value);
2287
+ if (wanted !== "true" && wanted !== "false") return {
2288
+ ok: false,
2289
+ target,
2290
+ reason: "Use \"true\" or \"false\"."
2291
+ };
2292
+ const desired = wanted === "true";
2293
+ const aria = element.getAttribute("aria-checked") ?? element.getAttribute("aria-pressed");
2294
+ const current = aria !== null ? aria === "true" : element.tagName === "INPUT" ? element.checked : void 0;
2295
+ if (current === void 0) return {
2296
+ ok: false,
2297
+ target,
2298
+ reason: "The element is not a toggle."
2299
+ };
2300
+ if (current === desired) return {
2301
+ ok: true,
2302
+ target,
2303
+ noop: true
2304
+ };
2305
+ element.click();
2306
+ return {
2307
+ ok: true,
2308
+ target
2309
+ };
2310
+ }
2311
+ /**
2312
+ * 用户选中的文件写进目标 input;`DataTransfer` 是唯一能构造 `FileList` 的途径。
2313
+ *
2314
+ * 失败要**返回**而不是抛:执行器的契约是「一律给出结构化结果」,
2315
+ * 抛出去会连带跳过策略层的审计,那比操作失败本身更糟。
2316
+ */
2317
+ function attachFiles(input, files) {
2318
+ try {
2319
+ const transfer = new DataTransfer();
2320
+ for (const file of files) transfer.items.add(file);
2321
+ input.files = transfer.files;
2322
+ } catch (e) {
2323
+ return `This browser cannot attach files programmatically: ${e instanceof Error ? e.message : String(e)}`;
2324
+ }
2325
+ input.dispatchEvent(new Event("input", { bubbles: true }));
2326
+ input.dispatchEvent(new Event("change", { bubbles: true }));
2327
+ }
2015
2328
  /** @experimental */
2016
2329
  function createDomPageActionExecutor(options) {
2017
2330
  const { reader } = options;
2331
+ /** 帧标识从 reader 的句柄表取,不另存一份:两张表会有两套过期时机 */
2332
+ const withFrame = (ref, described) => {
2333
+ const frame = reader.frameOf(ref);
2334
+ return {
2335
+ ...described,
2336
+ ...frame !== void 0 ? { frame } : {}
2337
+ };
2338
+ };
2018
2339
  const describe = (ref) => {
2019
2340
  const element = reader.resolve(ref);
2020
- return element === void 0 ? void 0 : describeElement(element);
2341
+ if (element === void 0) return void 0;
2342
+ const described = withFrame(ref, describeElement(element));
2343
+ return reader.modalStateOf(element) === "elevated" ? {
2344
+ ...described,
2345
+ elevated: true
2346
+ } : described;
2021
2347
  };
2022
- const execute = (request) => {
2348
+ const execute = async (request) => {
2023
2349
  const element = reader.resolve(request.ref);
2024
2350
  if (element === void 0) return {
2025
2351
  ok: false,
2026
2352
  target: { role: "generic" },
2027
2353
  reason: "The element reference is unknown or expired."
2028
2354
  };
2029
- const target = describeElement(element);
2355
+ const target = withFrame(request.ref, describeElement(element));
2030
2356
  const fail = (reason) => ({
2031
2357
  ok: false,
2032
2358
  target,
2033
2359
  reason
2034
2360
  });
2035
- if (!reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
2361
+ 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.");
2362
+ if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
2036
2363
  if (hidden(element)) return fail("The element is not visible.");
2037
2364
  if (element.hasAttribute("disabled")) return fail("The element is disabled.");
2365
+ const modalsBefore = reader.modalSnapshot();
2366
+ const done = async (outcome) => {
2367
+ if (!outcome.ok) return outcome;
2368
+ const elevated = await waitFor(() => reader.elevateNewModals(modalsBefore), 500) ?? reader.elevateNewModals(modalsBefore);
2369
+ const name = elevated === void 0 ? void 0 : accessibleName(elevated) ?? "dialog";
2370
+ return name === void 0 ? outcome : {
2371
+ ...outcome,
2372
+ elevatedModal: name
2373
+ };
2374
+ };
2038
2375
  if (request.action === "click") {
2039
2376
  element.click();
2040
- return {
2377
+ return await done({
2041
2378
  ok: true,
2042
2379
  target
2043
- };
2380
+ });
2044
2381
  }
2045
2382
  if (request.action === "fill") {
2046
2383
  if (!FILLABLE_TAGS.has(element.tagName)) return fail("The element is not a text control.");
2047
2384
  if (element.hasAttribute("readonly")) return fail("The element is read-only.");
2048
2385
  fillValue(element, request.value ?? "");
2049
- return {
2386
+ return await done({
2050
2387
  ok: true,
2051
2388
  target
2389
+ });
2390
+ }
2391
+ if (request.action === "select") {
2392
+ const outcome = await selectOption(element, request.value ?? "");
2393
+ return outcome.ok ? await done({
2394
+ ...outcome,
2395
+ target
2396
+ }) : {
2397
+ ...outcome,
2398
+ target
2052
2399
  };
2053
2400
  }
2401
+ if (request.action === "set") return await done(setToggle(element, request.value ?? "", target));
2402
+ if (request.action === "attach") {
2403
+ const files = await options.pickFiles?.();
2404
+ if (files === void 0 || files.length === 0) return {
2405
+ ok: false,
2406
+ target,
2407
+ reason: "The user cancelled the file selection."
2408
+ };
2409
+ if (element.tagName !== "INPUT" || element.type !== "file") return fail("The element is not a file input.");
2410
+ const failure = attachFiles(element, files);
2411
+ if (failure !== void 0) return fail(failure);
2412
+ return await done({
2413
+ ok: true,
2414
+ target
2415
+ });
2416
+ }
2054
2417
  const form = element.tagName === "FORM" ? element : element.form;
2055
2418
  if (!form) return fail("The element does not belong to a form.");
2056
2419
  form.requestSubmit();
2057
- return {
2420
+ return await done({
2058
2421
  ok: true,
2059
2422
  target
2060
- };
2423
+ });
2061
2424
  };
2062
2425
  return {
2063
2426
  execute,
@@ -2917,6 +3280,355 @@ function createBrowserChatbotHost(options) {
2917
3280
  connectFacade: overrides?.connectFacade ?? connectFacade
2918
3281
  };
2919
3282
  }
3283
+ const DEFAULT_TIMEOUT_MS = 3e4;
3284
+ /**
3285
+ * 默认的链接文档读取实现(分册 22 §2.2)。
3286
+ *
3287
+ * 出站准入(scheme / 私有地址)与同源判定都在**引擎侧**完成,这里只负责取字节:
3288
+ * 把判定散到实现里,宿主换一个 reader 就能绕过整套闸门。
3289
+ * @experimental
3290
+ */
3291
+ function createFetchLinkedDocumentReader(options = {}) {
3292
+ const origin = "origin" in options ? options.origin : globalThis.location?.origin;
3293
+ const doFetch = options.fetch ?? globalThis.fetch;
3294
+ return {
3295
+ ...origin !== void 0 ? { origin } : {},
3296
+ async read(url) {
3297
+ const controller = new AbortController();
3298
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
3299
+ try {
3300
+ const response = await doFetch(url, { signal: controller.signal });
3301
+ if (!response.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `The document responded with HTTP ${response.status}.`);
3302
+ const buffer = await response.arrayBuffer();
3303
+ return {
3304
+ mimeType: response.headers.get("content-type") ?? "application/octet-stream",
3305
+ bytes: new Uint8Array(buffer)
3306
+ };
3307
+ } finally {
3308
+ clearTimeout(timer);
3309
+ }
3310
+ }
3311
+ };
3312
+ }
3313
+ /**
3314
+ * docx 正文抽取(分册 22 §2.4 / FR-23.7)。
3315
+ *
3316
+ * **零新增依赖**:zip 用仓内既有的 `unzipWithLimits`(自带解压体积上限,
3317
+ * 顺带挡住 zip 炸弹),XML 用浏览器原生 `DOMParser`。
3318
+ *
3319
+ * ⚠️ `DOMParser` 是浏览器 API,Node 没有 —— 所以这个模块只在 `@webskill/browser`。
3320
+ * Node 宿主没有 docx 能力,这是**显式声明**的限制(AC-G15),不是静默降级;
3321
+ * 收紧到 Node 侧的备案是 D55。
3322
+ */
3323
+ /**
3324
+ * **不抽取**的内容,逐条写明而不是笼统说「尽力而为」:
3325
+ * 样式、标题层级、图片、批注、修订、页眉页脚、脚注。
3326
+ *
3327
+ * 模型拿到的是纯正文文本流。把「这是 H1」这类信息也抽出来需要解析
3328
+ * `styles.xml` 的样式继承链,那是另一个量级的工作,本版不做。
3329
+ */
3330
+ const DOCX_UNEXTRACTED = [
3331
+ "styles",
3332
+ "heading levels",
3333
+ "images",
3334
+ "comments",
3335
+ "tracked changes",
3336
+ "headers and footers",
3337
+ "footnotes"
3338
+ ];
3339
+ const DOCX_ENTRY = "word/document.xml";
3340
+ /** 段落内的行内元素:文本、软换行、制表符 */
3341
+ function inlineText(node) {
3342
+ let out = "";
3343
+ for (const child of node.children) {
3344
+ const tag = child.tagName.replace(/^w:/, "");
3345
+ if (tag === "t") out += child.textContent ?? "";
3346
+ else if (tag === "br") out += "\n";
3347
+ else if (tag === "tab") out += " ";
3348
+ else out += inlineText(child);
3349
+ }
3350
+ return out;
3351
+ }
3352
+ /**
3353
+ * 表格按「行一行、单元格用制表符分隔」展开。
3354
+ * 不还原合并单元格——还原它需要读 gridSpan/vMerge,且模型多半用不上。
3355
+ */
3356
+ function tableText(table, paragraphOf) {
3357
+ const rows = [];
3358
+ for (const row of table.getElementsByTagName("w:tr")) {
3359
+ const cells = [];
3360
+ for (const cell of row.getElementsByTagName("w:tc")) {
3361
+ const paragraphs = [...cell.getElementsByTagName("w:p")].map(paragraphOf);
3362
+ cells.push(paragraphs.join(" ").trim());
3363
+ }
3364
+ rows.push(cells.join(" "));
3365
+ }
3366
+ return rows.join("\n");
3367
+ }
3368
+ /**
3369
+ * 从 docx 字节里抽出正文文本。
3370
+ *
3371
+ * @throws WebSkillError 归档损坏、缺 `word/document.xml`、XML 解析失败
3372
+ */
3373
+ async function extractDocxText(bytes) {
3374
+ const entry = (await unzipWithLimits(bytes)).find(([path]) => path === DOCX_ENTRY);
3375
+ if (entry === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `This file is not a Word document: it has no ${DOCX_ENTRY} entry.`);
3376
+ const xml = new TextDecoder().decode(entry[1]);
3377
+ const doc = new DOMParser().parseFromString(xml, "application/xml");
3378
+ if (doc.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "The Word document body could not be parsed as XML.");
3379
+ const body = doc.getElementsByTagName("w:body")[0];
3380
+ if (body === void 0) return "";
3381
+ const paragraphOf = (paragraph) => inlineText(paragraph);
3382
+ const blocks = [];
3383
+ for (const child of body.children) {
3384
+ const tag = child.tagName.replace(/^w:/, "");
3385
+ if (tag === "p") blocks.push(paragraphOf(child));
3386
+ else if (tag === "tbl") blocks.push(tableText(child, paragraphOf));
3387
+ }
3388
+ return blocks.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3389
+ }
3390
+ /**
3391
+ * 文档投放面的 CSP 单一来源(0.11.0 分册 18 FR-18.3 / AC-G21)。
3392
+ *
3393
+ * ⚠️ 策略**只能**由 viewer 路由的响应头下发。
3394
+ * 实测:`<meta http-equiv="Content-Security-Policy" content="sandbox …">` 里的
3395
+ * `sandbox` 指令**完全无效**——同一份文档,`<meta>` 下 `localStorage` 写入成功,
3396
+ * 换成响应头才抛 `SecurityError`。规范规定 `sandbox` / `frame-ancestors` /
3397
+ * `report-uri` 在 `<meta>` 里被忽略。
3398
+ * 不要试图用 `<meta>` 省掉那条 HTTP 路由。
3399
+ */
3400
+ /**
3401
+ * 固定的 sandbox token,**不接受宿主配置**。
3402
+ *
3403
+ * `allow-same-origin` 给了就等于隔离归零(文档能读主应用存储、经 opener 反控),
3404
+ * 所以它连「可配置」都不该是。`allow-downloads` 本版不给:没有消费场景。
3405
+ */
3406
+ const SANDBOX_TOKENS = ["allow-scripts", "allow-modals"];
3407
+ /**
3408
+ * 宿主配置值不得夹带 CSP 分隔符 —— 否则一个来源字符串就能追加任意指令,
3409
+ * 白名单形同虚设。这是配置注入,不是理论风险。
3410
+ */
3411
+ function assertSource(value, directive) {
3412
+ if (value.trim() === "" || /[;,'"\s]/.test(value)) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${directive} source ${JSON.stringify(value)}: sources must be a single token without whitespace, quotes, ";" or ","`);
3413
+ }
3414
+ function directive(name, defaults, extra) {
3415
+ for (const value of extra ?? []) assertSource(value, name);
3416
+ const sources = [...defaults, ...extra ?? []];
3417
+ return `${name} ${sources.length > 0 ? sources.join(" ") : "'none'"}`;
3418
+ }
3419
+ /**
3420
+ * 生成 viewer 路由的 `Content-Security-Policy` 响应头值。
3421
+ *
3422
+ * 宿主只能**加白名单来源**,加不了 sandbox token —— 隔离边界的策略不由被隔离方
3423
+ * 也不由宿主的白名单配置决定。
3424
+ */
3425
+ function viewerCspHeader(options) {
3426
+ const host = options.hostOrigin;
3427
+ assertSource(host, "host-origin");
3428
+ return [
3429
+ `sandbox ${SANDBOX_TOKENS.join(" ")}`,
3430
+ "default-src 'none'",
3431
+ `script-src ${host}`,
3432
+ `style-src ${host} 'unsafe-inline'`,
3433
+ directive("img-src", [host, "data:"], options.imgSrc),
3434
+ directive("font-src", [host], options.fontSrc),
3435
+ directive("connect-src", [], options.connectSrc)
3436
+ ].join("; ");
3437
+ }
3438
+ /** 本版固定的 sandbox token(供判据与装配文档引用,避免第二份清单) */
3439
+ const VIEWER_SANDBOX_TOKENS = SANDBOX_TOKENS;
3440
+ const DEFAULT_MAX_VIEWER_PAYLOAD_BYTES = 2e6;
3441
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
3442
+ /** hello 的重发间隔:viewer 脚本加载完之前投出去的消息全部丢失 */
3443
+ const HELLO_RETRY_MS = 100;
3444
+ /** 审计事件类型(与既有事件同一条链) */
3445
+ const DOCUMENT_SURFACE_AUDIT_EVENT = "document.surface.open";
3446
+ const payloadBytes = (value) => new TextEncoder().encode(JSON.stringify(value) ?? "").length;
3447
+ /**
3448
+ * 打开一个文档投放面。
3449
+ *
3450
+ * `window.open` **必须在用户手势里调**,否则被浏览器拦截;不能在 run 收尾时自动开。
3451
+ * 也**不加 `noopener`** —— 需要句柄才能把内容 post 进去。反控面不靠 `noopener` 封,
3452
+ * 靠沙箱本身:无 `allow-same-origin` 读不到 opener 的任何东西,
3453
+ * 无 `allow-top-navigation` 改不了它的地址。
3454
+ */
3455
+ async function openDocumentSurface(input, options) {
3456
+ const limit = options.maxPayloadBytes ?? 2e6;
3457
+ const size = payloadBytes(input.document);
3458
+ if (size > limit) {
3459
+ await audit(options, input, {
3460
+ outcome: "rejected",
3461
+ reason: "payload-too-large",
3462
+ bytes: size
3463
+ });
3464
+ throw new WebSkillError("VALIDATION_FAILED", `Document payload is ${size} bytes, over the ${limit} byte budget; reduce the data before opening the viewer`);
3465
+ }
3466
+ if (!await options.confirm({
3467
+ skillName: input.skillName,
3468
+ dataSource: input.dataSource
3469
+ })) {
3470
+ await audit(options, input, {
3471
+ outcome: "cancelled",
3472
+ bytes: size
3473
+ });
3474
+ throw new WebSkillError("DOCUMENT_SURFACE_DECLINED", "The user declined to open the document surface");
3475
+ }
3476
+ const target = (options.open ?? defaultOpen)(options.viewerUrl, "_blank");
3477
+ if (!target) {
3478
+ await audit(options, input, {
3479
+ outcome: "blocked",
3480
+ reason: "popup-blocked",
3481
+ bytes: size
3482
+ });
3483
+ throw new WebSkillError("DOCUMENT_SURFACE_UNAVAILABLE", "The browser blocked the viewer window; open it from a direct user gesture");
3484
+ }
3485
+ const channelId = newChannelId();
3486
+ try {
3487
+ await handshake(target, channelId, options);
3488
+ } catch (e) {
3489
+ target.close();
3490
+ await audit(options, input, {
3491
+ outcome: "failed",
3492
+ reason: messageOf(e),
3493
+ bytes: size
3494
+ });
3495
+ throw e;
3496
+ }
3497
+ target.postMessage({
3498
+ channelId,
3499
+ type: "webskill:viewer-document",
3500
+ document: input.document
3501
+ }, viewerOrigin(options));
3502
+ await audit(options, input, {
3503
+ outcome: "opened",
3504
+ bytes: size
3505
+ });
3506
+ return target;
3507
+ }
3508
+ function defaultOpen(url, target) {
3509
+ return globalThis.open?.(url, target) ?? null;
3510
+ }
3511
+ /** viewer 是 opaque origin,`postMessage` 的 targetOrigin 只能是 `*` —— 用 channelId 做配对校验 */
3512
+ function viewerOrigin(_options) {
3513
+ return "*";
3514
+ }
3515
+ function newChannelId() {
3516
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
3517
+ globalThis.crypto?.getRandomValues(bytes);
3518
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
3519
+ }
3520
+ /**
3521
+ * 等 viewer 外壳回报就绪。
3522
+ *
3523
+ * hello 必须**重发**:`window.open` 一返回就 post 的那一条,viewer 的脚本还没加载,
3524
+ * 消息直接丢掉(`postMessage` 不排队等监听器)。只发一次的写法在本机偶尔能过——
3525
+ * 那是缓存命中让文档加载快过第一条消息,换台机器就必然超时。
3526
+ *
3527
+ * 必须同时校验 `event.source === target` 与 `channelId`:
3528
+ * 只看 origin 不行(opaque origin 的 `event.origin` 是 `"null"`,任何沙箱页面都一样)。
3529
+ */
3530
+ function handshake(target, channelId, options) {
3531
+ const timeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
3532
+ const scope = globalThis;
3533
+ return new Promise((resolve, reject) => {
3534
+ const done = (fn) => {
3535
+ clearTimeout(timer);
3536
+ clearInterval(retry);
3537
+ scope.removeEventListener("message", onMessage);
3538
+ fn();
3539
+ };
3540
+ const timer = setTimeout(() => {
3541
+ done(() => reject(new WebSkillError("DOCUMENT_SURFACE_UNAVAILABLE", `The viewer did not report ready within ${timeoutMs}ms`)));
3542
+ }, timeoutMs);
3543
+ const onMessage = (event) => {
3544
+ if (event.source !== target) return;
3545
+ const data = event.data;
3546
+ if (data?.type !== "webskill:viewer-ready" || data.channelId !== channelId) return;
3547
+ done(resolve);
3548
+ };
3549
+ scope.addEventListener("message", onMessage);
3550
+ const hello = () => target.postMessage({
3551
+ channelId,
3552
+ type: "webskill:viewer-hello"
3553
+ }, "*");
3554
+ const retry = setInterval(hello, HELLO_RETRY_MS);
3555
+ hello();
3556
+ });
3557
+ }
3558
+ async function audit(options, input, data) {
3559
+ await options.audit?.append({
3560
+ type: DOCUMENT_SURFACE_AUDIT_EVENT,
3561
+ target: input.skillName,
3562
+ data: {
3563
+ skillName: input.skillName,
3564
+ dataSource: input.dataSource,
3565
+ ...data
3566
+ }
3567
+ }).catch(() => void 0);
3568
+ }
3569
+ /**
3570
+ * 启动外壳:等主应用的 hello → 回报 ready → 收文档 → 写进 DOM。
3571
+ *
3572
+ * `channelId` 必须由**主应用**在 hello 里给定并全程比对。opaque origin 下
3573
+ * `event.origin` 恒为 `"null"`,任何沙箱页面都一样,靠 origin 分辨不了来源;
3574
+ * 而 `event.source` 在这一侧只能确认「来自 opener」,确认不了是哪一次投放。
3575
+ */
3576
+ function startViewerShell(host, mount, onData) {
3577
+ let channelId;
3578
+ const onMessage = (event) => {
3579
+ const data = event.data;
3580
+ if (typeof data?.channelId !== "string" || data.channelId === "") return;
3581
+ if (data.type === "webskill:viewer-hello") {
3582
+ channelId = data.channelId;
3583
+ host.opener?.postMessage({
3584
+ type: "webskill:viewer-ready",
3585
+ channelId
3586
+ }, "*");
3587
+ return;
3588
+ }
3589
+ if (data.type !== "webskill:viewer-document" || channelId === void 0 || data.channelId !== channelId) return;
3590
+ const doc = data.document;
3591
+ if (!doc) return;
3592
+ mount.style.textContent = doc.css ?? "";
3593
+ mount.content.innerHTML = doc.html;
3594
+ onData?.(doc.data);
3595
+ };
3596
+ host.addEventListener("message", onMessage);
3597
+ return { dispose: () => {
3598
+ host.removeEventListener("message", onMessage);
3599
+ } };
3600
+ }
3601
+ /**
3602
+ * 监听 CSP 违规并把被拦资源汇总成一句话。
3603
+ *
3604
+ * 必须在**文档写入之前**挂上:违规事件不补发,晚一步就永远收不到。
3605
+ * 同一个来源只报一次——一份文档引十个图标会刷出十条同样的提示。
3606
+ */
3607
+ function watchBlockedResources(host, notice) {
3608
+ const origins = /* @__PURE__ */ new Set();
3609
+ const onViolation = (event) => {
3610
+ const origin = originOf(event.blockedURI);
3611
+ if (origin === "" || origins.has(origin)) return;
3612
+ origins.add(origin);
3613
+ notice.textContent = blockedMessage([...origins]);
3614
+ };
3615
+ host.addEventListener("securitypolicyviolation", onViolation);
3616
+ return { dispose: () => {
3617
+ host.removeEventListener("securitypolicyviolation", onViolation);
3618
+ } };
3619
+ }
3620
+ /** 提示文案的单一来源;判据直接引用,不各写一份字面量 */
3621
+ function blockedMessage(origins) {
3622
+ return `Blocked by this page's content policy: ${origins.join(", ")}. The document fell back to locally available resources, so printed output may differ from the author's intent.`;
3623
+ }
3624
+ /** 只留 origin:完整 URL 里的路径对用户没有信息量,还会把提示撑得很长 */
3625
+ function originOf(blockedURI) {
3626
+ try {
3627
+ return new URL(blockedURI).origin;
3628
+ } catch {
3629
+ return blockedURI;
3630
+ }
3631
+ }
2920
3632
 
2921
3633
  //#endregion
2922
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startWorkerRuntimeHost };
3634
+ 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, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractDocxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };