@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/agent.d.ts +2 -0
- package/dist/agent.js +867 -0
- package/dist/browser.d.ts +137 -4
- package/dist/browser.js +457 -17
- package/dist/{catalogComponents-KsujmL4b-Clx1kCnU.js → catalogComponents-DV7cPpUm-C77AEEx9.js} +256 -92
- package/dist/{dist-C-Sh0MDU.js → dist-6C03DShK.js} +159 -19
- package/dist/{dist-D9Lcn5Pp.js → dist-bewtXYlO.js} +592 -28
- package/dist/governance.d.ts +46 -4
- package/dist/governance.js +44 -1
- package/dist/{index-CHXxDccV.d.ts → index-Bsqg4ftU.d.ts} +97 -7
- package/dist/index-D_7ZZjkl.d.ts +411 -0
- package/dist/{index-DLfR2Y6I.d.ts → index-vBz_FC9w.d.ts} +77 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/node.d.ts +3 -3
- package/dist/node.js +1 -1
- package/dist/{openUiLibrary-YLS-cxyT-C96jWDQq.js → openUiLibrary-W3Ce896k-ClFTRZFs.js} +3 -3
- package/dist/{skillVersionStore-uyefLPR1-DXOzbksv.d.ts → skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts} +4 -3
- package/dist/testing.d.ts +1 -1
- package/dist/{types-7Wcg--Vh-1YlQ4jF9.d.ts → types-D_hoCri8-BnNPiZCi.d.ts} +29 -4
- package/dist/ui-react.d.ts +26 -5
- package/dist/ui-react.js +96 -21
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +25 -6
- package/dist/ui.d.ts +4 -4
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-CSTbhBe_-CYIs5BX8.js → webskillLitCatalog-_mugzRHx-DiuJpCuf.js} +88 -2
- package/package.json +6 -1
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 {
|
|
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
|
-
|
|
374
|
+
/** 按资源地址缓存,而不是全局单例:不同地址必须各自加载,否则先到的实例会顶替后来的配置 */
|
|
375
|
+
const esbuildCache = /* @__PURE__ */ new Map();
|
|
375
376
|
async function loadEsbuild(options) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
|
|
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 };
|