@webskill/sdk 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent.d.ts +2 -2
  2. package/dist/agent.js +218 -28
  3. package/dist/browser.d.ts +177 -4
  4. package/dist/browser.js +444 -35
  5. package/dist/{catalogComponents-Dr5dFMAb-Dacibl1e.js → catalogComponents-DfxxfUvn-D55Gbb2l.js} +4016 -1226
  6. package/dist/{dist-8oQRa8Xz.js → dist-59XlqDuv.js} +93 -6
  7. package/dist/{dist-DnYG2-eY.js → dist-CJqQsIm9.js} +498 -118
  8. package/dist/{dist-DusANsrn.js → dist-DmI5SBBF.js} +437 -55
  9. package/dist/eventTypes-g1BXL6x5-CibcOftR.js +37 -0
  10. package/dist/governance.d.ts +91 -11
  11. package/dist/governance.js +194 -52
  12. package/dist/{index-C-KFAZoF.d.ts → index-K-eewlGL.d.ts} +176 -75
  13. package/dist/{index-BMocOEi0.d.ts → index-P9J2LTfU.d.ts} +163 -6
  14. package/dist/{index-BuTpBMzr.d.ts → index-fLskQfAS.d.ts} +156 -5
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +4 -4
  17. package/dist/mcp.d.ts +129 -6
  18. package/dist/mcp.js +235 -28
  19. package/dist/{memoryArtifactStore-52Zn9npI-BMPYwvoy.js → memoryArtifactStore-52Zn9npI-upv5OWYf.js} +1 -1
  20. package/dist/node.d.ts +3 -3
  21. package/dist/node.js +4 -3
  22. package/dist/{openUiLibrary-Bdrji9qK-DzAxRlTY.js → openUiLibrary-DURlAxjk-CU6AzfSW.js} +3 -3
  23. package/dist/{skillVersionStore-BzLbzFOL-CxdAFWO2.d.ts → skillVersionStore-Bl-ElD45-gRfSaAby.d.ts} +8 -2
  24. package/dist/{testing-CYTFqkDm.js → testing-BCUO5gZR.js} +2 -2
  25. package/dist/testing.d.ts +1 -1
  26. package/dist/testing.js +2 -2
  27. package/dist/{types-4pg-qp_I-Gq63X8Oa.d.ts → types-B3n0cMZu-BdcqQ35O.d.ts} +74 -7
  28. package/dist/ui-react.d.ts +16 -7
  29. package/dist/ui-react.js +159 -108
  30. package/dist/ui-vue.d.ts +1 -1
  31. package/dist/ui-vue.js +2 -2
  32. package/dist/ui.d.ts +4 -4
  33. package/dist/ui.js +3 -3
  34. package/dist/{webskillLitCatalog-_mugzRHx-B_54vxum.js → webskillLitCatalog-DwTwSBFt-DiXXpNZA.js} +22 -3
  35. package/package.json +2 -2
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
- import { A as parseSkillMarkdown, B as unzipWithLimits, H as verifyManifest, L as resolveInsideRoot, M as readResponseWithLimit, N as readSkillSignature, O as messageOf, T as isValidSkillName, U as verifySkillSignature, V as validateSkills, _ as atomicWriteText, f as SkillDiscovery, h as assertRemoteUrlAllowed, j as parseSkillPackManifest, m as WebSkillError, o as SKILLS_LOCKFILE, r as MANIFEST_EXCLUDED_FILES, s as SKILL_MANIFEST_FILE, u as SKILL_PACK_FILE, v as buildCatalog, w as exportSkills, y as buildManifest } from "./dist-8oQRa8Xz.js";
2
- import { H as createWebSkillApi, S as ProgressiveRouter, a as AgentLoop, at as normalizeToolContent, c as CapabilityApproval, et as mergeCatalogEntries, h as FsRunSnapshotStore, m as FsMemoryStore, nt as networkPolicyLibSource, o as AnthropicClient, ot as normalizeToolError, p as FsArtifactStore, st as parseBridgeRequest, x as OpenAiCompatibleClient, y as GoogleGenAiClient, z as bridgeError } from "./dist-DusANsrn.js";
3
- import { n as MockLlmClient } from "./testing-CYTFqkDm.js";
1
+ import { A as messageOf, D as isValidSkillName, E as exportSkills, F as readSkillSignature, G as verifyManifest, K as verifySkillSignature, M as parseSkillMarkdown, N as parseSkillPackManifest, P as readResponseWithLimit, U as unzipWithLimits, W as validateSkills, _ as atomicWriteText, f as SkillDiscovery, h as assertRemoteUrlAllowed, m as WebSkillError, o as SKILLS_LOCKFILE, r as MANIFEST_EXCLUDED_FILES, s as SKILL_MANIFEST_FILE, u as SKILL_PACK_FILE, v as buildCatalog, w as detectSkillArchiveShapeFromFs, y as buildManifest, z as resolveInsideRoot } from "./dist-59XlqDuv.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-DmI5SBBF.js";
3
+ import { n as MockLlmClient } from "./testing-BCUO5gZR.js";
4
4
 
5
5
  //#region ../browser/dist/index.js
6
6
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -490,7 +490,8 @@ const PRESERVED_INSTALL_CODES = /* @__PURE__ */ new Set([
490
490
  "SIGNATURE_MALFORMED",
491
491
  "SIGNATURE_UNTRUSTED_KEY",
492
492
  "SIGNATURE_MISMATCH",
493
- "SIGNATURE_UNSUPPORTED"
493
+ "SIGNATURE_UNSUPPORTED",
494
+ "SKILL_DUPLICATE_NAME"
494
495
  ]);
495
496
  const asInstallFailed = (e) => e instanceof WebSkillError && (e.code === "INSTALL_FAILED" || e.code === "TOOL_UNSUPPORTED" || PRESERVED_INSTALL_CODES.has(e.code)) ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
496
497
  const lockfilePath = (root) => `${root}/${SKILLS_LOCKFILE}`;
@@ -510,6 +511,8 @@ const EMPTY_TRUSTED_KEYS = {
510
511
  var BrowserSkillManager = class {
511
512
  #fs;
512
513
  #managedRoot;
514
+ /** 只读技能根(宿主自带的本地技能):参与导出与依赖存在性判定,但不可卸载 */
515
+ #skillRoots;
513
516
  #fetchImpl;
514
517
  #archiveLimits;
515
518
  #onChanged;
@@ -519,6 +522,7 @@ var BrowserSkillManager = class {
519
522
  constructor(deps) {
520
523
  this.#fs = deps.fs;
521
524
  this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
525
+ this.#skillRoots = (deps.skillRoots ?? []).map((root) => root.replace(/\/+$/, ""));
522
526
  this.#fetchImpl = deps.fetchImpl;
523
527
  this.#archiveLimits = deps.archiveLimits;
524
528
  this.#onChanged = deps.onChanged;
@@ -572,17 +576,16 @@ var BrowserSkillManager = class {
572
576
  const contentDir = `${stagingRoot}/content`;
573
577
  await fs.mkdir(contentDir);
574
578
  await extractZipWeb(fs, data, contentDir, this.#archiveLimits);
575
- const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
576
- if (await fs.exists(packFilePath)) return (await this.#installPack({
577
- contentDir,
578
- manifest: parseSkillPackManifest(await fs.readText(packFilePath))
579
- }, source, stagingRoot))[0];
580
- let skillDir = contentDir;
581
- if (!await fs.exists(`${contentDir}/SKILL.md`)) {
582
- const dirs = (await fs.list(contentDir)).filter((e) => e.type === "directory");
583
- if (dirs.length === 1 && await fs.exists(`${dirs[0]?.path}/SKILL.md`)) skillDir = dirs[0].path;
584
- else throw new WebSkillError("INSTALL_FAILED", "Archive does not contain a SKILL.md at its root or in a single top-level directory");
579
+ const detection = await detectSkillArchiveShapeFromFs(fs, contentDir);
580
+ if (detection.shape === "invalid") throw new WebSkillError("INSTALL_FAILED", detection.reason ?? "Archive layout is not recognised");
581
+ if (detection.shape === "pack-set") {
582
+ const packRoot = detection.rootDir === void 0 ? contentDir : `${contentDir}/${detection.rootDir}`;
583
+ return (await this.#installPack({
584
+ contentDir: packRoot,
585
+ manifest: parseSkillPackManifest(await fs.readText(`${packRoot}/${SKILL_PACK_FILE}`))
586
+ }, source, stagingRoot))[0];
585
587
  }
588
+ const skillDir = detection.rootDir === void 0 ? contentDir : `${contentDir}/${detection.rootDir}`;
586
589
  let name;
587
590
  let version;
588
591
  try {
@@ -593,13 +596,18 @@ var BrowserSkillManager = class {
593
596
  throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md: ${messageOf(e)}`, e);
594
597
  }
595
598
  if (!isValidSkillName(name)) throw new WebSkillError("INSTALL_FAILED", `Invalid skill name in SKILL.md (path traversal rejected): ${JSON.stringify(name)}`);
599
+ if (options?.replace === "reject" && await fs.exists(`${this.#managedRoot}/${name}`)) throw new WebSkillError("SKILL_DUPLICATE_NAME", `A skill named "${name}" is already installed. Installing it again replaces the existing files; use the packages flow if that is intended.`);
596
600
  const finalRoot = `${stagingRoot}/final`;
597
601
  const finalDir = `${finalRoot}/${name}`;
598
602
  await copyDir(fs, skillDir, finalDir);
599
- const report = await validateSkills(fs, [finalRoot]);
603
+ const report = await validateSkills(fs, [finalRoot], { extraKnownSkillNames: await this.listSkillNames() });
600
604
  if (!report.ok) {
601
- const errors = report.issues.filter((i) => i.severity === "error");
602
- throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
605
+ const errors = report.issues.filter((i) => i.severity === "error" && i.code !== "SKILL_UNKNOWN_DEPENDENCY");
606
+ for (const issue of report.issues) if (issue.code === "SKILL_UNKNOWN_DEPENDENCY") this.#onWarning?.({
607
+ skill: name,
608
+ message: issue.message
609
+ });
610
+ if (errors.length > 0) throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
603
611
  }
604
612
  const manifest = await this.#createManifest(finalDir, {
605
613
  name,
@@ -714,6 +722,21 @@ var BrowserSkillManager = class {
714
722
  throw asInstallFailed(e);
715
723
  }
716
724
  }
725
+ /** 技能的真实根:先 managed,再只读根。都没有则 undefined */
726
+ async #resolveSkillRoot(name) {
727
+ const managed = `${this.#managedRoot}/${name}`;
728
+ if (await this.#fs.exists(managed)) return {
729
+ root: managed,
730
+ managed: true
731
+ };
732
+ for (const base of this.#skillRoots) {
733
+ const candidate = `${base}/${name}`;
734
+ if (await this.#fs.exists(candidate)) return {
735
+ root: candidate,
736
+ managed: false
737
+ };
738
+ }
739
+ }
717
740
  /**
718
741
  * 浏览器导出(单技能与包集同构:始终产出含 webskill.skill-pack.json 的 zip 字节,
719
742
  * 统一安装路径;保存/下载由应用层处理)。
@@ -721,11 +744,13 @@ var BrowserSkillManager = class {
721
744
  async exportArchive(names) {
722
745
  const list = Array.isArray(names) ? names : [names];
723
746
  if (list.length === 0) throw new WebSkillError("EXPORT_FAILED", "exportArchive requires at least one skill name");
724
- const roots = list.map((name) => {
747
+ const roots = [];
748
+ for (const name of list) {
725
749
  if (!isValidSkillName(name)) throw new WebSkillError("EXPORT_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
726
- return `${this.#managedRoot}/${name}`;
727
- });
728
- for (const [index, root] of roots.entries()) if (!await this.#fs.exists(root)) throw new WebSkillError("EXPORT_FAILED", `Skill "${list[index]}" is not installed`);
750
+ const resolved = await this.#resolveSkillRoot(name);
751
+ if (resolved === void 0) throw new WebSkillError("EXPORT_FAILED", `Skill "${name}" is not installed`);
752
+ roots.push(resolved.root);
753
+ }
729
754
  return exportSkills(this.#fs, {
730
755
  roots,
731
756
  manifestBuilder: (root) => this.#readManifest(root)
@@ -733,8 +758,10 @@ var BrowserSkillManager = class {
733
758
  }
734
759
  async uninstall(name) {
735
760
  if (!isValidSkillName(name)) throw new WebSkillError("UNINSTALL_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
736
- const targetDir = `${this.#managedRoot}/${name}`;
737
- if (!await this.#fs.exists(targetDir)) throw new WebSkillError("UNINSTALL_FAILED", `Skill "${name}" is not installed`);
761
+ const resolved = await this.#resolveSkillRoot(name);
762
+ if (resolved === void 0) throw new WebSkillError("UNINSTALL_FAILED", `Skill "${name}" is not installed`);
763
+ if (!resolved.managed) throw new WebSkillError("UNINSTALL_FAILED", `Skill "${name}" ships with the host application and is not managed by the skill manager; remove it from the host's skill root instead.`);
764
+ const targetDir = resolved.root;
738
765
  try {
739
766
  await this.#fs.remove(targetDir, { recursive: true });
740
767
  await this.#removeLockEntry(name);
@@ -763,6 +790,22 @@ var BrowserSkillManager = class {
763
790
  throw new WebSkillError("INTEGRITY_FAILED", `Skills lockfile at ${path} is corrupted: ${messageOf(e)}`, e);
764
791
  }
765
792
  }
793
+ /**
794
+ * 库里已有的技能名:managed 目录 + 宿主声明的只读根。读失败按「库为空」处理,不阻断安装。
795
+ * 与 `listInstalled()` 的区别是**含本地技能**(后者只看安装账本)。
796
+ * @experimental
797
+ */
798
+ async listSkillNames() {
799
+ const names = [];
800
+ for (const root of [this.#managedRoot, ...this.#skillRoots]) {
801
+ if (!await this.#fs.exists(root).catch(() => false)) continue;
802
+ for (const stat of await this.#fs.list(root).catch(() => [])) {
803
+ const name = stat.path.split("/").pop() ?? "";
804
+ if (stat.type === "directory" && isValidSkillName(name)) names.push(name);
805
+ }
806
+ }
807
+ return names;
808
+ }
766
809
  async #createManifest(skillRoot, input) {
767
810
  const files = [];
768
811
  for (const rel of (await listFiles(this.#fs, skillRoot)).sort()) {
@@ -787,7 +830,16 @@ var BrowserSkillManager = class {
787
830
  }
788
831
  async #readManifest(skillRoot) {
789
832
  const path = `${skillRoot}/${SKILL_MANIFEST_FILE}`;
790
- if (!await this.#fs.exists(path)) throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest not found at ${path} (was the skill installed via BrowserSkillManager?)`);
833
+ if (!await this.#fs.exists(path)) {
834
+ const name = skillRoot.split("/").pop() ?? skillRoot;
835
+ return this.#createManifest(skillRoot, {
836
+ name,
837
+ source: {
838
+ type: "local",
839
+ path: skillRoot
840
+ }
841
+ });
842
+ }
791
843
  try {
792
844
  return JSON.parse(await this.#fs.readText(path));
793
845
  } catch (e) {
@@ -1615,8 +1667,20 @@ function describeError(e) {
1615
1667
  if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
1616
1668
  return e instanceof Error ? e.message : String(e);
1617
1669
  }
1618
- /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值 */
1670
+ /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
1619
1671
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
1672
+ /** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
1673
+ const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
1674
+ "button",
1675
+ "checkbox",
1676
+ "combobox",
1677
+ "form",
1678
+ "link",
1679
+ "radio",
1680
+ "searchbox",
1681
+ "switch",
1682
+ "textbox"
1683
+ ]);
1620
1684
  /** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
1621
1685
  const ROLE_BY_TAG = {
1622
1686
  A: "link",
@@ -1654,7 +1718,7 @@ const clip = (text) => {
1654
1718
  const normalized = text.replace(/\s+/g, " ").trim();
1655
1719
  return normalized.length > MAX_TEXT ? `${normalized.slice(0, MAX_TEXT)}…` : normalized;
1656
1720
  };
1657
- function roleOf(element) {
1721
+ function roleOf$1(element) {
1658
1722
  const explicit = element.getAttribute("role");
1659
1723
  if (explicit !== null && explicit.trim() !== "") return explicit.trim();
1660
1724
  if (element.tagName === "INPUT") {
@@ -1700,7 +1764,7 @@ function valueOf(element) {
1700
1764
  * 隐藏节点不进结果:屏幕上看不见的东西,用户没有机会发现它被读走了。
1701
1765
  * jsdom 里 `getComputedStyle` 可用但布局是假的,因此只看显式声明,不看尺寸。
1702
1766
  */
1703
- function hidden(element, view) {
1767
+ function hidden$1(element, view) {
1704
1768
  if (element.hasAttribute("hidden")) return true;
1705
1769
  if (element.getAttribute("aria-hidden") === "true") return true;
1706
1770
  const inlineStyle = element.getAttribute("style") ?? "";
@@ -1718,18 +1782,18 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
1718
1782
  "NOSCRIPT",
1719
1783
  "TEMPLATE"
1720
1784
  ]);
1721
- function describe(element, excluded, doc, view, targets) {
1785
+ function describe(element, excluded, doc, view, targets, handles) {
1722
1786
  if (excluded.has(element)) return void 0;
1723
1787
  if (SKIPPED_TAGS.has(element.tagName)) return void 0;
1724
- if (hidden(element, view)) return void 0;
1788
+ if (hidden$1(element, view)) return void 0;
1725
1789
  const children = [];
1726
1790
  for (const child of element.children) {
1727
- const node = describe(child, excluded, doc, view, targets);
1791
+ const node = describe(child, excluded, doc, view, targets, handles);
1728
1792
  if (node !== void 0) children.push(node);
1729
1793
  }
1730
1794
  const name = nameOf(element, doc);
1731
1795
  const value = valueOf(element);
1732
- const role = roleOf(element);
1796
+ const role = roleOf$1(element);
1733
1797
  if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
1734
1798
  const node = {
1735
1799
  role,
@@ -1737,6 +1801,8 @@ function describe(element, excluded, doc, view, targets) {
1737
1801
  ...value !== void 0 ? { value } : {},
1738
1802
  ...children.length > 0 ? { children } : {}
1739
1803
  };
1804
+ const ref = handles?.issue(element, role);
1805
+ if (ref !== void 0) node.ref = ref;
1740
1806
  if (targets !== void 0 && isCapturableElement(element)) targets.push({
1741
1807
  element,
1742
1808
  node
@@ -1744,6 +1810,31 @@ function describe(element, excluded, doc, view, targets) {
1744
1810
  return node;
1745
1811
  }
1746
1812
  /**
1813
+ * 句柄发放与解析。表在 reader 实例内、每次 `read()` 整体替换:
1814
+ * 全局表是第二套状态且会跨会话泄漏;不替换则页面变了还能用旧句柄点到别的东西。
1815
+ */
1816
+ var HandleIssuer = class {
1817
+ #table = /* @__PURE__ */ new Map();
1818
+ #actionable;
1819
+ #excluded;
1820
+ constructor(actionable, excluded) {
1821
+ this.#actionable = actionable;
1822
+ this.#excluded = excluded;
1823
+ }
1824
+ issue(element, role) {
1825
+ if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
1826
+ if (!ACTIONABLE_ROLES.has(role)) return void 0;
1827
+ const bytes = /* @__PURE__ */ new Uint8Array(8);
1828
+ crypto.getRandomValues(bytes);
1829
+ const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
1830
+ this.#table.set(ref, element);
1831
+ return ref;
1832
+ }
1833
+ get table() {
1834
+ return this.#table;
1835
+ }
1836
+ };
1837
+ /**
1747
1838
  * DOM 遍历实现(设计 09 §2 的「实现层」)。产出只有角色 / 名称 / 值,
1748
1839
  * **不含任何 HTML 标记、class、data 属性或注释**(FR-10.4)。
1749
1840
  *
@@ -1752,17 +1843,43 @@ function describe(element, excluded, doc, view, targets) {
1752
1843
  * @experimental
1753
1844
  */
1754
1845
  function createDomPerceptionReader(options = {}) {
1846
+ let handles;
1847
+ const resolveDocument = () => options.document ?? globalThis.document;
1848
+ /** 按当前 DOM 重算一次可操作集合;发句柄与执行前各算一次 */
1849
+ const actionSets = (doc) => {
1850
+ const actionable = /* @__PURE__ */ new Set();
1851
+ const excluded = /* @__PURE__ */ new Set();
1852
+ const scope = options.actionScope;
1853
+ if (scope === void 0 || scope.include.length === 0) return {
1854
+ actionable,
1855
+ excluded
1856
+ };
1857
+ for (const selector of scope.include) for (const root of doc.querySelectorAll(selector)) {
1858
+ actionable.add(root);
1859
+ for (const descendant of root.querySelectorAll("*")) actionable.add(descendant);
1860
+ }
1861
+ for (const selector of scope.exclude ?? []) for (const root of doc.querySelectorAll(selector)) {
1862
+ excluded.add(root);
1863
+ for (const descendant of root.querySelectorAll("*")) excluded.add(descendant);
1864
+ }
1865
+ return {
1866
+ actionable,
1867
+ excluded
1868
+ };
1869
+ };
1755
1870
  function collect(scope, targets) {
1756
- const doc = options.document ?? globalThis.document;
1871
+ const doc = resolveDocument();
1757
1872
  if (doc === void 0 || scope.include.length === 0) return [];
1758
1873
  const view = doc.defaultView;
1874
+ const sets = actionSets(doc);
1875
+ handles = new HandleIssuer(sets.actionable, sets.excluded);
1759
1876
  const excluded = /* @__PURE__ */ new Set();
1760
1877
  for (const selector of scope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
1761
1878
  const roots = [];
1762
1879
  for (const selector of scope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
1763
1880
  const nodes = [];
1764
1881
  for (const root of roots) {
1765
- const node = describe(root, excluded, doc, view, targets);
1882
+ const node = describe(root, excluded, doc, view, targets, handles);
1766
1883
  if (node !== void 0) nodes.push(node);
1767
1884
  }
1768
1885
  return nodes;
@@ -1804,7 +1921,126 @@ function createDomPerceptionReader(options = {}) {
1804
1921
  if (!capture.images || capture.maxImages <= 0) return Promise.resolve({ nodes: collect(scope, void 0) });
1805
1922
  return readWithImages(scope, capture);
1806
1923
  }
1807
- return { read };
1924
+ return {
1925
+ read,
1926
+ resolve: (ref) => handles?.table.get(ref),
1927
+ inActionScope: (element) => {
1928
+ const doc = resolveDocument();
1929
+ if (doc === void 0) return false;
1930
+ const sets = actionSets(doc);
1931
+ return sets.actionable.has(element) && !sets.excluded.has(element);
1932
+ }
1933
+ };
1934
+ }
1935
+ const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
1936
+ /** 只看显式声明,不看尺寸:jsdom 有 getComputedStyle 但没有布局 */
1937
+ function hidden(element) {
1938
+ if (element.hasAttribute("hidden")) return true;
1939
+ if (element.getAttribute("aria-hidden") === "true") return true;
1940
+ const inlineStyle = element.getAttribute("style") ?? "";
1941
+ if (/display\s*:\s*none|visibility\s*:\s*hidden/i.test(inlineStyle)) return true;
1942
+ const view = element.ownerDocument.defaultView;
1943
+ if (view !== null) {
1944
+ const computed = view.getComputedStyle(element);
1945
+ if (computed.display === "none" || computed.visibility === "hidden") return true;
1946
+ }
1947
+ return false;
1948
+ }
1949
+ const inputType = (element) => (element.getAttribute("type") ?? "text").toLowerCase();
1950
+ const isSecret = (element) => element.tagName === "INPUT" && SECRET_INPUT_TYPES.has(inputType(element));
1951
+ function accessibleName(element) {
1952
+ const label = element.getAttribute("aria-label");
1953
+ if (label !== null && label.trim() !== "") return label.trim();
1954
+ const text = element.textContent?.replace(/\s+/g, " ").trim() ?? "";
1955
+ if (text !== "") return text;
1956
+ const placeholder = element.getAttribute("placeholder");
1957
+ return placeholder !== null && placeholder.trim() !== "" ? placeholder.trim() : void 0;
1958
+ }
1959
+ function roleOf(element) {
1960
+ const explicit = element.getAttribute("role");
1961
+ if (explicit !== null && explicit.trim() !== "") return explicit.trim();
1962
+ if (element.tagName === "INPUT") {
1963
+ const type = inputType(element);
1964
+ return type === "checkbox" || type === "radio" ? type : type === "button" || type === "submit" ? "button" : "textbox";
1965
+ }
1966
+ if (element.tagName === "BUTTON") return "button";
1967
+ if (element.tagName === "A") return "link";
1968
+ if (element.tagName === "TEXTAREA") return "textbox";
1969
+ if (element.tagName === "SELECT") return "combobox";
1970
+ if (element.tagName === "FORM") return "form";
1971
+ return "generic";
1972
+ }
1973
+ const describeElement = (element) => {
1974
+ const name = accessibleName(element);
1975
+ return {
1976
+ role: roleOf(element),
1977
+ ...name !== void 0 ? { name } : {},
1978
+ ...isSecret(element) ? { secret: true } : {}
1979
+ };
1980
+ };
1981
+ /**
1982
+ * 受控组件必须收到 `input` / `change`:直接写 `.value` 在 React 上会被下一次渲染覆盖,
1983
+ * 表现为「填了又没了」。原生 setter 是绕过 React value 劫持的唯一办法。
1984
+ */
1985
+ function fillValue(element, value) {
1986
+ const prototype = element.tagName === "INPUT" ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
1987
+ const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
1988
+ if (setter) setter.call(element, value);
1989
+ else element.value = value;
1990
+ element.dispatchEvent(new Event("input", { bubbles: true }));
1991
+ element.dispatchEvent(new Event("change", { bubbles: true }));
1992
+ }
1993
+ /** @experimental */
1994
+ function createDomPageActionExecutor(options) {
1995
+ const { reader } = options;
1996
+ const describe = (ref) => {
1997
+ const element = reader.resolve(ref);
1998
+ return element === void 0 ? void 0 : describeElement(element);
1999
+ };
2000
+ const execute = (request) => {
2001
+ const element = reader.resolve(request.ref);
2002
+ if (element === void 0) return {
2003
+ ok: false,
2004
+ target: { role: "generic" },
2005
+ reason: "The element reference is unknown or expired."
2006
+ };
2007
+ const target = describeElement(element);
2008
+ const fail = (reason) => ({
2009
+ ok: false,
2010
+ target,
2011
+ reason
2012
+ });
2013
+ if (!reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
2014
+ if (hidden(element)) return fail("The element is not visible.");
2015
+ if (element.hasAttribute("disabled")) return fail("The element is disabled.");
2016
+ if (request.action === "click") {
2017
+ element.click();
2018
+ return {
2019
+ ok: true,
2020
+ target
2021
+ };
2022
+ }
2023
+ if (request.action === "fill") {
2024
+ if (!FILLABLE_TAGS.has(element.tagName)) return fail("The element is not a text control.");
2025
+ if (element.hasAttribute("readonly")) return fail("The element is read-only.");
2026
+ fillValue(element, request.value ?? "");
2027
+ return {
2028
+ ok: true,
2029
+ target
2030
+ };
2031
+ }
2032
+ const form = element.tagName === "FORM" ? element : element.form;
2033
+ if (!form) return fail("The element does not belong to a form.");
2034
+ form.requestSubmit();
2035
+ return {
2036
+ ok: true,
2037
+ target
2038
+ };
2039
+ };
2040
+ return {
2041
+ execute,
2042
+ describe
2043
+ };
1808
2044
  }
1809
2045
  const isRecord = (v) => typeof v === "object" && v !== null;
1810
2046
  const isNonEmptyString = (v) => typeof v === "string" && v !== "";
@@ -2486,6 +2722,179 @@ async function deleteMemoryEncryptionKey(name = "user-profile") {
2486
2722
  db.close();
2487
2723
  }
2488
2724
  }
2725
+ /**
2726
+ * 端口 × 宿主类型的**唯一定义**(AC-17.4)。
2727
+ * About 页与装配工厂都读这一份;护栏 `port-matrix-single-source` 盯着不许再抄。
2728
+ */
2729
+ const HOST_PORT_MATRIX = [
2730
+ {
2731
+ id: "install / uninstall",
2732
+ surface: "console-backend",
2733
+ key: "install",
2734
+ applicability: {
2735
+ browser: "expected",
2736
+ node: "expected"
2737
+ },
2738
+ note: "Wire ConsoleBackend.install to let the console install and remove skills."
2739
+ },
2740
+ {
2741
+ id: "export (zip)",
2742
+ surface: "console-backend",
2743
+ key: "exportSkill",
2744
+ applicability: {
2745
+ browser: "expected",
2746
+ node: "expected"
2747
+ },
2748
+ note: "Wire ConsoleBackend.exportSkill to enable zip export."
2749
+ },
2750
+ {
2751
+ id: "export (tar)",
2752
+ surface: "console-backend",
2753
+ key: "exportSkillTar",
2754
+ applicability: {
2755
+ browser: "not-applicable",
2756
+ node: "expected"
2757
+ },
2758
+ note: "Browsers have no tar toolchain in the platform; zip export covers the same need."
2759
+ },
2760
+ {
2761
+ id: "integrity",
2762
+ surface: "console-backend",
2763
+ key: "verifyIntegrity",
2764
+ applicability: {
2765
+ browser: "expected",
2766
+ node: "expected"
2767
+ },
2768
+ note: "Wire ConsoleBackend.verifyIntegrity to enable the integrity report."
2769
+ },
2770
+ {
2771
+ id: "editing",
2772
+ surface: "console-backend",
2773
+ key: "writeSkillFile",
2774
+ applicability: {
2775
+ browser: "expected",
2776
+ node: "expected"
2777
+ },
2778
+ note: "Wire ConsoleBackend.writeSkillFile to enable in-place skill editing."
2779
+ },
2780
+ {
2781
+ id: "versions",
2782
+ surface: "console-backend",
2783
+ key: "listVersions",
2784
+ applicability: {
2785
+ browser: "expected",
2786
+ node: "expected"
2787
+ },
2788
+ note: "Wire ConsoleBackend.listVersions to show version history and rollback."
2789
+ },
2790
+ {
2791
+ id: "runs",
2792
+ surface: "console-backend",
2793
+ key: "runs",
2794
+ applicability: {
2795
+ browser: "expected",
2796
+ node: "expected"
2797
+ },
2798
+ note: "Wire ConsoleBackend.runs to read run history; without it the runs pages have no source."
2799
+ },
2800
+ {
2801
+ id: "sessions",
2802
+ surface: "console-backend",
2803
+ key: "sessions",
2804
+ applicability: {
2805
+ browser: "expected",
2806
+ node: "expected"
2807
+ },
2808
+ note: "Wire ConsoleBackend.sessions to list chat sessions."
2809
+ },
2810
+ {
2811
+ id: "page perception",
2812
+ surface: "chatbot-adapter",
2813
+ key: "pagePerception",
2814
+ applicability: {
2815
+ browser: "expected",
2816
+ node: "not-applicable"
2817
+ },
2818
+ note: "Page perception reads the live DOM; there is no page outside a browser."
2819
+ },
2820
+ {
2821
+ id: "skill candidates",
2822
+ surface: "chatbot-config",
2823
+ key: "skillCandidates",
2824
+ applicability: {
2825
+ browser: "expected",
2826
+ node: "expected"
2827
+ },
2828
+ note: "Wire skillCandidates so skill-miss suggestions can be reviewed in the console."
2829
+ },
2830
+ {
2831
+ id: "remote endpoints",
2832
+ surface: "connect-facade",
2833
+ key: "listEndpoints",
2834
+ applicability: {
2835
+ browser: "expected",
2836
+ node: "expected"
2837
+ },
2838
+ note: "Wire ConnectFacade.listEndpoints to manage remote MCP endpoints."
2839
+ },
2840
+ {
2841
+ id: "page-declared skills",
2842
+ surface: "connect-facade",
2843
+ key: "temporarySkills",
2844
+ applicability: {
2845
+ browser: "expected",
2846
+ node: "not-applicable"
2847
+ },
2848
+ note: "Page-declared temporary skills come from the document; Node hosts have none."
2849
+ }
2850
+ ];
2851
+ function inspectHostWiring(input) {
2852
+ const kind = input.kind ?? "browser";
2853
+ return HOST_PORT_MATRIX.map((spec) => {
2854
+ if (!(spec.surface in input.surfaces)) return {
2855
+ ...spec,
2856
+ status: "unknown"
2857
+ };
2858
+ if (spec.applicability[kind] === "not-applicable") return {
2859
+ ...spec,
2860
+ status: "not-applicable"
2861
+ };
2862
+ const surface = input.surfaces[spec.surface];
2863
+ const wired = surface !== void 0 && surface[spec.key] !== void 0;
2864
+ return {
2865
+ ...spec,
2866
+ status: wired ? "wired" : "missing"
2867
+ };
2868
+ });
2869
+ }
2870
+ /** 缺口 = 该接却没接的项;`unknown` 不算缺口,没看到不等于没接 */
2871
+ function missingPorts(reports) {
2872
+ return reports.filter((report) => report.status === "missing");
2873
+ }
2874
+ function createBrowserChatbotHost(options) {
2875
+ const { fs, managedRoot, skillRoots, navigation, overrides } = options;
2876
+ const chatbotAdapter = {
2877
+ storage: fs,
2878
+ skillRoots,
2879
+ navigation,
2880
+ skillManager: new BrowserSkillManager({
2881
+ fs,
2882
+ managedRoot,
2883
+ skillRoots
2884
+ }),
2885
+ ...options.pagePerception !== void 0 ? { pagePerception: options.pagePerception } : {}
2886
+ };
2887
+ const consoleBackend = options.consoleBackend ?? { catalog: async () => ({ entries: [] }) };
2888
+ const connectFacade = options.connectFacade ?? {
2889
+ listEndpoints: async () => [],
2890
+ listTools: async () => []
2891
+ };
2892
+ return {
2893
+ chatbotAdapter: overrides?.chatbotAdapter ?? chatbotAdapter,
2894
+ consoleBackend: overrides?.consoleBackend ?? consoleBackend,
2895
+ connectFacade: overrides?.connectFacade ?? connectFacade
2896
+ };
2897
+ }
2489
2898
 
2490
2899
  //#endregion
2491
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createDomPerceptionReader, createEncryptedMemoryStore, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractZipWeb, generateMemoryEncryptionKey, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startWorkerRuntimeHost };
2900
+ 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 };