@webskill/sdk 0.4.0 → 0.5.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/browser.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as parseSkillMarkdown, B as unzipWithLimits, H as verifyManifest, L as resolveInsideRoot, M as readResponseWithLimit, O as messageOf, T as isValidSkillName, 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 normalizeToolContent, O as bridgeError, R as mergeCatalogEntries, U as normalizeToolError, W as parseBridgeRequest, _ as OpenAiCompatibleClient, d as FsRunSnapshotStore, i as AgentLoop, j as createWebSkillApi, l as FsArtifactStore, o as CapabilityApproval, u as FsMemoryStore, v as ProgressiveRouter, z as networkPolicyLibSource } from "./dist-C-Sh0MDU.js";
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 { J as normalizeToolError, P as createWebSkillApi, U as mergeCatalogEntries, W as networkPolicyLibSource, Y as parseBridgeRequest, a as AnthropicClient, d as FsMemoryStore, f as FsRunSnapshotStore, g as GoogleGenAiClient, i as AgentLoop, k as bridgeError, o as CapabilityApproval, q as normalizeToolContent, u as FsArtifactStore, v as OpenAiCompatibleClient, y as ProgressiveRouter } from "./dist-6C03DShK.js";
3
3
  import { n as MockLlmClient } from "./testing-DDCJWvgA.js";
4
4
 
5
5
  //#region ../browser/dist/index.js
@@ -371,17 +371,26 @@ self.onmessage = async function (event) {
371
371
  }
372
372
  };
373
373
  `;
374
- let esbuildPromise;
374
+ /** 按资源地址缓存,而不是全局单例:不同地址必须各自加载,否则先到的实例会顶替后来的配置 */
375
+ const esbuildCache = /* @__PURE__ */ new Map();
375
376
  async function loadEsbuild(options) {
376
- esbuildPromise ??= (async () => {
377
- const mod = await import(
378
- /* @vite-ignore */
379
- options.esbuildUrl
377
+ const key = `${options.esbuildUrl}\n${options.wasmUrl ?? ""}`;
378
+ let pending = esbuildCache.get(key);
379
+ if (!pending) {
380
+ pending = (async () => {
381
+ const mod = await import(
382
+ /* @vite-ignore */
383
+ options.esbuildUrl
380
384
  );
381
- await mod.initialize(options.wasmUrl ? { wasmURL: options.wasmUrl } : {});
382
- return mod;
383
- })();
384
- return esbuildPromise;
385
+ await mod.initialize(options.wasmUrl ? { wasmURL: options.wasmUrl } : {});
386
+ return mod;
387
+ })().catch((e) => {
388
+ esbuildCache.delete(key);
389
+ throw e;
390
+ });
391
+ esbuildCache.set(key, pending);
392
+ }
393
+ return pending;
385
394
  }
386
395
  async function sha256Hex(data) {
387
396
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data));
@@ -400,19 +409,25 @@ var TsTranspiler = class {
400
409
  this.#options = deps.options;
401
410
  this.#cacheDir = deps.cacheDir ?? ".webskill/ts-cache";
402
411
  }
403
- /** 返回 JS 源码;转译失败TOOL_EXECUTION_FAILED(带 esbuild 诊断) */
412
+ /** 返回 JS 源码;转译器不可用TS_TRANSPILER_UNAVAILABLE,语法错 TS_TRANSPILE_FAILED */
404
413
  async transpile(scriptPath, source) {
405
414
  const cacheKey = `${this.#cacheDir}/${await sha256Hex(`${scriptPath}\n${source}`)}.js`;
406
415
  if (await this.#fs.exists(cacheKey)) return this.#fs.readText(cacheKey);
416
+ let esbuild;
417
+ try {
418
+ esbuild = await loadEsbuild(this.#options);
419
+ } catch (e) {
420
+ throw new WebSkillError("TS_TRANSPILER_UNAVAILABLE", `Failed to load the TypeScript transpiler from "${this.#options.esbuildUrl}": ${messageOf(e)}`, e);
421
+ }
407
422
  let code;
408
423
  try {
409
- code = (await (await loadEsbuild(this.#options)).transform(source, {
424
+ code = (await esbuild.transform(source, {
410
425
  loader: "ts",
411
426
  target: "es2022",
412
427
  format: "esm"
413
428
  })).code;
414
429
  } catch (e) {
415
- throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to transpile ${scriptPath}: ${e instanceof Error ? e.message : String(e)}`, e);
430
+ throw new WebSkillError("TS_TRANSPILE_FAILED", `Failed to transpile ${scriptPath}: ${messageOf(e)}`, e);
416
431
  }
417
432
  await this.#fs.writeText(cacheKey, code);
418
433
  return code;
@@ -461,8 +476,26 @@ async function extractZipWeb(fs, data, destRoot, limits) {
461
476
  else await fs.writeBinary(target, content);
462
477
  }
463
478
  }
464
- const asInstallFailed = (e) => e instanceof WebSkillError && (e.code === "INSTALL_FAILED" || e.code === "TOOL_UNSUPPORTED") ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
479
+ /**
480
+ * 验签失败不能被折叠成 INSTALL_FAILED:四种失败的宿主处置完全不同
481
+ * (引导选策略 / 提示重下 / 引导添加密钥 / 安全事件),与 Node 侧同口径。
482
+ */
483
+ const PRESERVED_INSTALL_CODES = /* @__PURE__ */ new Set([
484
+ "SIGNATURE_MISSING",
485
+ "SIGNATURE_MALFORMED",
486
+ "SIGNATURE_UNTRUSTED_KEY",
487
+ "SIGNATURE_MISMATCH",
488
+ "SIGNATURE_UNSUPPORTED"
489
+ ]);
490
+ 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);
465
491
  const lockfilePath = (root) => `${root}/${SKILLS_LOCKFILE}`;
492
+ /** 未注入信任库时的空实现:策略仍然生效,只是没有任何受信公钥 */
493
+ const EMPTY_TRUSTED_KEYS = {
494
+ list: () => Promise.resolve([]),
495
+ get: () => Promise.resolve(void 0),
496
+ add: () => Promise.reject(new WebSkillError("TOOL_UNSUPPORTED", "No trusted key store is configured")),
497
+ remove: () => Promise.reject(new WebSkillError("TOOL_UNSUPPORTED", "No trusted key store is configured"))
498
+ };
466
499
  /**
467
500
  * D5:浏览器技能安装(http(s) zip / ArrayBuffer;tar/git/npm → TOOL_UNSUPPORTED)。
468
501
  * 流程与 Node 对齐:staging → 解析 name → validateSkills(失败零残留)→ managed root →
@@ -475,12 +508,34 @@ var BrowserSkillManager = class {
475
508
  #fetchImpl;
476
509
  #archiveLimits;
477
510
  #onChanged;
511
+ #trustedKeys;
512
+ #unsignedPolicy;
513
+ #onWarning;
478
514
  constructor(deps) {
479
515
  this.#fs = deps.fs;
480
516
  this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
481
517
  this.#fetchImpl = deps.fetchImpl;
482
518
  this.#archiveLimits = deps.archiveLimits;
483
519
  this.#onChanged = deps.onChanged;
520
+ this.#trustedKeys = deps.signature?.trustedKeys ?? EMPTY_TRUSTED_KEYS;
521
+ this.#unsignedPolicy = deps.signature?.unsigned ?? "warn";
522
+ this.#onWarning = deps.onWarning;
523
+ }
524
+ /**
525
+ * 安装期验签:manifest 生成之后、提交 swap 之前。失败直接抛出,
526
+ * 暂存目录由 install 的 finally 清理,不落盘。
527
+ */
528
+ async #verifySignature(skillDir, manifest) {
529
+ const verdict = await verifySkillSignature({
530
+ manifest,
531
+ signature: await readSkillSignature(this.#fs, skillDir),
532
+ trustedKeys: this.#trustedKeys,
533
+ policy: { unsigned: this.#unsignedPolicy }
534
+ });
535
+ if (verdict.warning !== void 0) this.#onWarning?.({
536
+ skill: manifest.name,
537
+ message: verdict.warning
538
+ });
484
539
  }
485
540
  async install(source, options) {
486
541
  const fs = this.#fs;
@@ -546,6 +601,7 @@ var BrowserSkillManager = class {
546
601
  ...version ? { version } : {},
547
602
  source
548
603
  });
604
+ await this.#verifySignature(finalDir, manifest);
549
605
  targetDir = `${this.#managedRoot}/${name}`;
550
606
  const backupDir = `${stagingRoot}/backup`;
551
607
  const hadPrevious = await fs.exists(targetDir);
@@ -614,6 +670,7 @@ var BrowserSkillManager = class {
614
670
  source
615
671
  });
616
672
  if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
673
+ await this.#verifySignature(`${finalRoot}/${entry.name}`, manifest);
617
674
  manifests.push(manifest);
618
675
  }
619
676
  const swapped = [];
@@ -906,7 +963,7 @@ var BrowserWorkerScriptExecutor = class {
906
963
  const [hasJs, hasTs] = await Promise.all([this.#fs.exists(jsPath), this.#fs.exists(tsPath)]);
907
964
  if (hasJs && hasTs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
908
965
  if (hasTs) {
909
- if (!this.#transpiler) throw new WebSkillError("TOOL_UNSUPPORTED", `Browser sandbox executes .js only unless TypeScript support is configured ("${scriptName}.ts")`);
966
+ if (!this.#transpiler) throw new WebSkillError("TOOL_UNSUPPORTED", `Browser sandbox executes .js only unless TypeScript support is configured ("${scriptName}.ts"). Enable it under runtime configuration sandbox.typescript and provide an esbuild-wasm URL.`);
910
967
  return {
911
968
  path: tsPath,
912
969
  source: await this.#transpiler.transpile(tsPath, await this.#fs.readText(tsPath))
@@ -1123,6 +1180,389 @@ function installWebSkillNavigator(config) {
1123
1180
  nav["webskill"] = api;
1124
1181
  return api;
1125
1182
  }
1183
+ function languageModel() {
1184
+ const candidate = globalThis.LanguageModel;
1185
+ return typeof candidate?.availability === "function" && typeof candidate.create === "function" ? candidate : void 0;
1186
+ }
1187
+ /** 缺 API 时区分「这个浏览器根本不支持」与「Chromium 但版本/开关没到位」,两者的用户动作不同 */
1188
+ function missingReason() {
1189
+ const nav = globalThis.navigator;
1190
+ return nav?.userAgentData?.brands?.some((entry) => /Chromium|Google Chrome/i.test(entry.brand)) === true || /Chrome\//.test(nav?.userAgent ?? "") ? "api-missing" : "unsupported-browser";
1191
+ }
1192
+ /**
1193
+ * 进入模型配置界面时调用(FR-4.7):不可用时给出具体原因,禁止「选了之后才失败」。
1194
+ * 模型还在下载也算不可用——这一轮对话等不到它。
1195
+ */
1196
+ async function probeChromeBuiltinAvailability() {
1197
+ const api = languageModel();
1198
+ if (!api) return {
1199
+ available: false,
1200
+ reason: missingReason()
1201
+ };
1202
+ try {
1203
+ return await api.availability() === "available" ? { available: true } : {
1204
+ available: false,
1205
+ reason: "model-not-ready"
1206
+ };
1207
+ } catch {
1208
+ return {
1209
+ available: false,
1210
+ reason: "model-not-ready"
1211
+ };
1212
+ }
1213
+ }
1214
+ const textOf = (message) => message.content.map((part) => part.type === "text" ? part.text : `[${part.type}]`).join("").trim();
1215
+ /** tool 角色 Prompt API 不认识,降级成 user 文本——内置模型本来就不做工具调用 */
1216
+ function toInitialPrompt(message) {
1217
+ const text = textOf(message);
1218
+ if (message.role === "assistant") return {
1219
+ role: "assistant",
1220
+ content: text
1221
+ };
1222
+ return {
1223
+ role: "user",
1224
+ content: message.role === "tool" ? `Tool result: ${text}` : text
1225
+ };
1226
+ }
1227
+ function splitInput(input) {
1228
+ const system = input.messages.filter((m) => m.role === "system").map(textOf).filter((text) => text !== "");
1229
+ const rest = input.messages.filter((m) => m.role !== "system");
1230
+ const last = rest.at(-1);
1231
+ return {
1232
+ initialPrompts: [...system.length > 0 ? [{
1233
+ role: "system",
1234
+ content: system.join("\n\n")
1235
+ }] : [], ...rest.slice(0, -1).map(toInitialPrompt)],
1236
+ prompt: last ? toInitialPrompt(last).content : ""
1237
+ };
1238
+ }
1239
+ /**
1240
+ * 浏览器内置模型客户端(Prompt API)。**不支持工具调用**——`input.tools` 被忽略,
1241
+ * 模型只会产出文本,因此技能里的脚本步骤在这一档跑不起来(FR-4.8 要求 UI 标注该限制)。
1242
+ * @experimental
1243
+ */
1244
+ var ChromeBuiltinLlmClient = class {
1245
+ #temperature;
1246
+ constructor(options = {}) {
1247
+ if (options.temperature !== void 0) this.#temperature = options.temperature;
1248
+ }
1249
+ async #session(input) {
1250
+ const api = languageModel();
1251
+ if (!api) throw new WebSkillError("LLM_UNAVAILABLE", "The Chrome built-in model is not available in this browser (Prompt API missing)");
1252
+ const { initialPrompts, prompt } = splitInput(input);
1253
+ const temperature = input.temperature ?? this.#temperature;
1254
+ return {
1255
+ session: await api.create({
1256
+ ...initialPrompts.length > 0 ? { initialPrompts } : {},
1257
+ ...temperature !== void 0 ? { temperature } : {},
1258
+ ...input.signal ? { signal: input.signal } : {}
1259
+ }),
1260
+ prompt
1261
+ };
1262
+ }
1263
+ async complete(input) {
1264
+ const { session, prompt } = await this.#session(input);
1265
+ try {
1266
+ return { content: [{
1267
+ type: "text",
1268
+ text: await session.prompt(prompt, ...input.signal ? [{ signal: input.signal }] : [])
1269
+ }] };
1270
+ } finally {
1271
+ session.destroy?.();
1272
+ }
1273
+ }
1274
+ async *stream(input) {
1275
+ const { session, prompt } = await this.#session(input);
1276
+ if (!session.promptStreaming) {
1277
+ try {
1278
+ const text = await session.prompt(prompt, ...input.signal ? [{ signal: input.signal }] : []);
1279
+ yield {
1280
+ type: "text-delta",
1281
+ delta: text
1282
+ };
1283
+ yield {
1284
+ type: "done",
1285
+ content: text
1286
+ };
1287
+ } finally {
1288
+ session.destroy?.();
1289
+ }
1290
+ return;
1291
+ }
1292
+ let content = "";
1293
+ try {
1294
+ const reader = session.promptStreaming(prompt, ...input.signal ? [{ signal: input.signal }] : []).getReader();
1295
+ for (;;) {
1296
+ const { done, value } = await reader.read();
1297
+ if (done) break;
1298
+ if (value !== void 0 && value !== "") {
1299
+ content += value;
1300
+ yield {
1301
+ type: "text-delta",
1302
+ delta: value
1303
+ };
1304
+ }
1305
+ }
1306
+ yield {
1307
+ type: "done",
1308
+ content
1309
+ };
1310
+ } finally {
1311
+ session.destroy?.();
1312
+ }
1313
+ }
1314
+ async checkAvailability() {
1315
+ return (await probeChromeBuiltinAvailability()).available;
1316
+ }
1317
+ };
1318
+ /**
1319
+ * 配置 → `LlmClient`。收在 SDK 里是为了让下游只跟 `RuntimeConfigStore` 打交道,
1320
+ * 不必自己维护「provider → client 类」的映射(设计 04 §5)。
1321
+ */
1322
+ function createLlmClient(config) {
1323
+ if (config.provider === "chrome-builtin") return new ChromeBuiltinLlmClient();
1324
+ const common = {
1325
+ ...config.baseUrl?.trim() ? { baseUrl: config.baseUrl.trim() } : {},
1326
+ ...config.requestTimeoutMs !== void 0 ? { requestTimeoutMs: config.requestTimeoutMs } : {}
1327
+ };
1328
+ const apiKey = config.apiKey?.trim() ?? "";
1329
+ if (config.provider === "anthropic") return new AnthropicClient({
1330
+ ...common,
1331
+ apiKey,
1332
+ model: config.model
1333
+ });
1334
+ if (config.provider === "google") return new GoogleGenAiClient({
1335
+ ...common,
1336
+ apiKey,
1337
+ model: config.model
1338
+ });
1339
+ return new OpenAiCompatibleClient({
1340
+ ...common,
1341
+ ...apiKey !== "" ? { apiKey } : {},
1342
+ model: config.model
1343
+ });
1344
+ }
1345
+ function recognitionCtor() {
1346
+ const scope = globalThis;
1347
+ return scope.SpeechRecognition ?? scope.webkitSpeechRecognition;
1348
+ }
1349
+ /**
1350
+ * 渲染期调用(FR-7.2):不可用时按钮置灰并说明原因,禁止呈现为可用后才失败。
1351
+ *
1352
+ * 顺序是刻意的——构造器都不存在时谈安全上下文没有意义;
1353
+ * 构造器在但站点是 http,识别一定会被浏览器拒绝,这条原因用户能自己解决。
1354
+ * @experimental
1355
+ */
1356
+ function checkDictationAvailability() {
1357
+ if (recognitionCtor() === void 0) return {
1358
+ available: false,
1359
+ reason: "api-missing"
1360
+ };
1361
+ if (globalThis.isSecureContext === false) return {
1362
+ available: false,
1363
+ reason: "insecure-context"
1364
+ };
1365
+ return { available: true };
1366
+ }
1367
+ /** 浏览器的 error 码 → 稳定错误码;权限被拒要单独区分,UI 的降级提示不一样(FR-7.6) */
1368
+ function toError(code, message) {
1369
+ 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 });
1370
+ return new WebSkillError("DICTATION_FAILED", message ?? `Speech recognition failed: ${code}`, { reason: code });
1371
+ }
1372
+ /**
1373
+ * 开始一次语音识别(FR-7.1)。
1374
+ *
1375
+ * **只能由用户手势调用**(FR-7.3)。设计上的保证是它不进工具列表:
1376
+ * agent 拿不到「开始录音」这个动作,因此不存在自主开麦的路径。
1377
+ * @experimental
1378
+ */
1379
+ function startDictation(options) {
1380
+ const Ctor = recognitionCtor();
1381
+ if (Ctor === void 0) throw new WebSkillError("DICTATION_UNAVAILABLE", "Speech recognition is not available in this browser.");
1382
+ const recognition = new Ctor();
1383
+ recognition.lang = options.lang;
1384
+ recognition.continuous = true;
1385
+ recognition.interimResults = true;
1386
+ /** stop() 之后浏览器仍可能派发一轮回调;这里做一次闸门,保证「停了就不再收结果」 */
1387
+ let stopped = false;
1388
+ recognition.onresult = (event) => {
1389
+ if (stopped) return;
1390
+ let interim = "";
1391
+ for (let index = event.resultIndex; index < event.results.length; index += 1) {
1392
+ const result = event.results[index];
1393
+ if (result === void 0) continue;
1394
+ const text = result[0]?.transcript ?? "";
1395
+ if (text === "") continue;
1396
+ if (result.isFinal) options.onFinal(text);
1397
+ else interim += text;
1398
+ }
1399
+ if (interim !== "") options.onInterim(interim);
1400
+ };
1401
+ recognition.onerror = (event) => {
1402
+ if (stopped) return;
1403
+ if (event.error === "no-speech" || event.error === "aborted") return;
1404
+ options.onError(toError(event.error, event.message));
1405
+ };
1406
+ try {
1407
+ recognition.start();
1408
+ } catch (e) {
1409
+ throw new WebSkillError("DICTATION_UNAVAILABLE", `Speech recognition could not start: ${String(e)}`);
1410
+ }
1411
+ return { stop() {
1412
+ if (stopped) return;
1413
+ stopped = true;
1414
+ recognition.onresult = null;
1415
+ recognition.onerror = null;
1416
+ recognition.onend = null;
1417
+ recognition.stop();
1418
+ } };
1419
+ }
1420
+ /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值 */
1421
+ const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
1422
+ /** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
1423
+ const ROLE_BY_TAG = {
1424
+ A: "link",
1425
+ BUTTON: "button",
1426
+ H1: "heading",
1427
+ H2: "heading",
1428
+ H3: "heading",
1429
+ H4: "heading",
1430
+ H5: "heading",
1431
+ H6: "heading",
1432
+ IMG: "img",
1433
+ LI: "listitem",
1434
+ NAV: "navigation",
1435
+ OL: "list",
1436
+ P: "paragraph",
1437
+ SELECT: "combobox",
1438
+ TABLE: "table",
1439
+ TD: "cell",
1440
+ TEXTAREA: "textbox",
1441
+ TH: "columnheader",
1442
+ TR: "row",
1443
+ UL: "list"
1444
+ };
1445
+ const INPUT_TYPE_ROLE = {
1446
+ button: "button",
1447
+ checkbox: "checkbox",
1448
+ radio: "radio",
1449
+ submit: "button"
1450
+ };
1451
+ /** 名称与值都截断:一个超长文本节点不该把上下文吃光 */
1452
+ const MAX_TEXT = 200;
1453
+ const clip = (text) => {
1454
+ const normalized = text.replace(/\s+/g, " ").trim();
1455
+ return normalized.length > MAX_TEXT ? `${normalized.slice(0, MAX_TEXT)}…` : normalized;
1456
+ };
1457
+ function roleOf(element) {
1458
+ const explicit = element.getAttribute("role");
1459
+ if (explicit !== null && explicit.trim() !== "") return explicit.trim();
1460
+ if (element.tagName === "INPUT") {
1461
+ const type = (element.getAttribute("type") ?? "text").toLowerCase();
1462
+ return INPUT_TYPE_ROLE[type] ?? "textbox";
1463
+ }
1464
+ return ROLE_BY_TAG[element.tagName] ?? "generic";
1465
+ }
1466
+ /** 自身直系文本(不含后代元素的文本):后代会各自成为节点,重复没有意义 */
1467
+ function ownText(element) {
1468
+ let text = "";
1469
+ for (const node of element.childNodes) if (node.nodeType === 3) text += node.nodeValue ?? "";
1470
+ return clip(text);
1471
+ }
1472
+ function nameOf(element, doc) {
1473
+ const label = element.getAttribute("aria-label");
1474
+ if (label !== null && label.trim() !== "") return clip(label);
1475
+ const labelledBy = element.getAttribute("aria-labelledby");
1476
+ if (labelledBy !== null) {
1477
+ const parts = labelledBy.split(/\s+/).map((id) => doc.getElementById(id)?.textContent ?? "").filter((part) => part.trim() !== "");
1478
+ if (parts.length > 0) return clip(parts.join(" "));
1479
+ }
1480
+ if (element.tagName === "IMG") {
1481
+ const alt = element.getAttribute("alt");
1482
+ if (alt !== null && alt.trim() !== "") return clip(alt);
1483
+ }
1484
+ const own = ownText(element);
1485
+ return own === "" ? void 0 : own;
1486
+ }
1487
+ function valueOf(element) {
1488
+ if (element.tagName === "INPUT") {
1489
+ const type = (element.getAttribute("type") ?? "text").toLowerCase();
1490
+ if (SECRET_INPUT_TYPES.has(type)) return void 0;
1491
+ const value = element.value;
1492
+ return value === "" ? void 0 : clip(value);
1493
+ }
1494
+ if (element.tagName === "TEXTAREA" || element.tagName === "SELECT") {
1495
+ const value = element.value;
1496
+ return value === "" ? void 0 : clip(value);
1497
+ }
1498
+ }
1499
+ /**
1500
+ * 隐藏节点不进结果:屏幕上看不见的东西,用户没有机会发现它被读走了。
1501
+ * jsdom 里 `getComputedStyle` 可用但布局是假的,因此只看显式声明,不看尺寸。
1502
+ */
1503
+ function hidden(element, view) {
1504
+ if (element.hasAttribute("hidden")) return true;
1505
+ if (element.getAttribute("aria-hidden") === "true") return true;
1506
+ const inlineStyle = element.getAttribute("style") ?? "";
1507
+ if (/display\s*:\s*none|visibility\s*:\s*hidden/i.test(inlineStyle)) return true;
1508
+ if (view !== null) {
1509
+ const computed = view.getComputedStyle(element);
1510
+ if (computed.display === "none" || computed.visibility === "hidden") return true;
1511
+ }
1512
+ return false;
1513
+ }
1514
+ /** script / style 的正文是代码,不是页面内容 */
1515
+ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
1516
+ "SCRIPT",
1517
+ "STYLE",
1518
+ "NOSCRIPT",
1519
+ "TEMPLATE"
1520
+ ]);
1521
+ function describe(element, excluded, doc, view) {
1522
+ if (excluded.has(element)) return void 0;
1523
+ if (SKIPPED_TAGS.has(element.tagName)) return void 0;
1524
+ if (hidden(element, view)) return void 0;
1525
+ const children = [];
1526
+ for (const child of element.children) {
1527
+ const node = describe(child, excluded, doc, view);
1528
+ if (node !== void 0) children.push(node);
1529
+ }
1530
+ const name = nameOf(element, doc);
1531
+ const value = valueOf(element);
1532
+ const role = roleOf(element);
1533
+ if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
1534
+ return {
1535
+ role,
1536
+ ...name !== void 0 ? { name } : {},
1537
+ ...value !== void 0 ? { value } : {},
1538
+ ...children.length > 0 ? { children } : {}
1539
+ };
1540
+ }
1541
+ /**
1542
+ * DOM 遍历实现(设计 09 §2 的「实现层」)。产出只有角色 / 名称 / 值,
1543
+ * **不含任何 HTML 标记、class、data 属性或注释**(FR-10.4)。
1544
+ *
1545
+ * 判定顺序是先 `include` 取子树再 `exclude` 剪枝:`exclude` 命中的元素连同
1546
+ * 其整棵后代一起消失,而不是只去掉那一个节点。
1547
+ * @experimental
1548
+ */
1549
+ function createDomPerceptionReader(options = {}) {
1550
+ return { read(scope) {
1551
+ const doc = options.document ?? globalThis.document;
1552
+ if (doc === void 0 || scope.include.length === 0) return [];
1553
+ const view = doc.defaultView;
1554
+ const excluded = /* @__PURE__ */ new Set();
1555
+ for (const selector of scope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
1556
+ const roots = [];
1557
+ for (const selector of scope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
1558
+ const nodes = [];
1559
+ for (const root of roots) {
1560
+ const node = describe(root, excluded, doc, view);
1561
+ if (node !== void 0) nodes.push(node);
1562
+ }
1563
+ return nodes;
1564
+ } };
1565
+ }
1126
1566
  const isRecord = (v) => typeof v === "object" && v !== null;
1127
1567
  const isNonEmptyString = (v) => typeof v === "string" && v !== "";
1128
1568
  /** Worker 侧入站校验:非法消息返回 undefined(忽略) */
@@ -1638,4 +2078,4 @@ var WorkerRuntimeClient = class {
1638
2078
  };
1639
2079
 
1640
2080
  //#endregion
1641
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, createIframeWorker, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
2081
+ export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, IframeWorkerLike, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, checkDictationAvailability, createDomPerceptionReader, createIframeWorker, createLlmClient, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startWorkerRuntimeHost };