@webskill/sdk 0.0.5 → 0.0.6

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.
@@ -1,4 +1,5 @@
1
1
  import { parse } from "yaml";
2
+ import { zipSync } from "fflate";
2
3
 
3
4
  //#region ../core/dist/index.js
4
5
  /** 所有公开 API 抛出的结构化错误,code 供上层可编程处理 */
@@ -268,8 +269,128 @@ function checkSkillRules(input) {
268
269
  message: `allowed-tools entry ${JSON.stringify(entry)} of directory ${dirName} does not match any script in scripts/`
269
270
  });
270
271
  }
272
+ if (metadata && input.knownSkillNames) {
273
+ const dependencies = metadata["dependencies"];
274
+ if (Array.isArray(dependencies)) {
275
+ for (const dep of dependencies) if (typeof dep === "string" && !input.knownSkillNames.has(dep)) issues.push({
276
+ code: "SKILL_UNKNOWN_DEPENDENCY",
277
+ severity: "error",
278
+ message: `Skill ${JSON.stringify(metadata.name)} depends on unknown skill ${JSON.stringify(dep)}`
279
+ });
280
+ }
281
+ }
271
282
  return issues;
272
283
  }
284
+ /**
285
+ * 循环依赖检测(全量邻接表,discovery 两趟扫描第二趟调用)。
286
+ * 每个环只报一次(error 级,阻断进 Catalog);自引用视为长度 1 的环。
287
+ */
288
+ function checkDependencyCycles(adjacency) {
289
+ const issues = [];
290
+ const involved = /* @__PURE__ */ new Set();
291
+ const reported = /* @__PURE__ */ new Set();
292
+ const state = /* @__PURE__ */ new Map();
293
+ const stack = [];
294
+ const visit = (node) => {
295
+ if (state.get(node) === "done") return;
296
+ if (state.get(node) === "visiting") {
297
+ const cycle = [...stack.slice(stack.indexOf(node)), node];
298
+ const key = cycle.slice(0, -1).sort().join("");
299
+ if (!reported.has(key)) {
300
+ reported.add(key);
301
+ for (const name of cycle.slice(0, -1)) involved.add(name);
302
+ issues.push({
303
+ code: "SKILL_CIRCULAR_DEPENDENCY",
304
+ severity: "error",
305
+ message: `Circular skill dependency detected: ${cycle.join(" -> ")}`
306
+ });
307
+ }
308
+ return;
309
+ }
310
+ state.set(node, "visiting");
311
+ stack.push(node);
312
+ for (const dep of adjacency.get(node) ?? []) visit(dep);
313
+ stack.pop();
314
+ state.set(node, "done");
315
+ };
316
+ for (const node of adjacency.keys()) visit(node);
317
+ return {
318
+ issues,
319
+ involved
320
+ };
321
+ }
322
+ /**
323
+ * 技能包集(skill pack)格式与打包逻辑(node/browser 共用单一来源):
324
+ * webskill.skill-pack.json { schemaVersion: 1, skills: [{ name, digest }] }
325
+ * <skill-a>/SKILL.md … webskill.skill-manifest.json
326
+ * <skill-b>/SKILL.md …
327
+ * fflate 环境无关;文件读取经 FileSystemProvider 抽象。
328
+ */
329
+ /** 包级清单文件名(位于 zip 根,不参与各技能 digest) */
330
+ const SKILL_PACK_FILE = "webskill.skill-pack.json";
331
+ const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
332
+ const baseName$1 = (p) => p.split("/").pop() ?? p;
333
+ async function listFilesRecursive(fs, root, prefix = "") {
334
+ const out = [];
335
+ for (const entry of await fs.list(root)) {
336
+ const rel = prefix === "" ? baseName$1(entry.path) : `${prefix}/${baseName$1(entry.path)}`;
337
+ if (entry.type === "directory") out.push(...await listFilesRecursive(fs, entry.path, rel));
338
+ else out.push(rel);
339
+ }
340
+ return out;
341
+ }
342
+ /**
343
+ * 多技能打包为单一 zip(包级清单 + 各技能目录含自身 manifest)。
344
+ * manifestBuilder 负责确保技能目录内 manifest 文件存在并返回之(digest 入包级清单)。
345
+ */
346
+ async function exportSkills(fs, input) {
347
+ if (input.roots.length === 0) throw new WebSkillError("EXPORT_FAILED", "exportSkills requires at least one skill root");
348
+ const files = {};
349
+ const skills = [];
350
+ try {
351
+ for (const root of input.roots) {
352
+ const base = root.replace(/\/+$/, "");
353
+ const manifest = await input.manifestBuilder(base);
354
+ skills.push({
355
+ name: manifest.name,
356
+ digest: manifest.integrity.digest
357
+ });
358
+ for (const rel of await listFilesRecursive(fs, base)) files[`${manifest.name}/${rel}`] = await fs.readBinary(`${base}/${rel}`);
359
+ }
360
+ } catch (e) {
361
+ if (e instanceof WebSkillError) throw e;
362
+ throw new WebSkillError("EXPORT_FAILED", `Failed to export skill pack: ${messageOf$2(e)}`, e);
363
+ }
364
+ const pack = {
365
+ schemaVersion: 1,
366
+ skills
367
+ };
368
+ files[SKILL_PACK_FILE] = new TextEncoder().encode(JSON.stringify(pack, null, 2));
369
+ return zipSync(files, { level: 6 });
370
+ }
371
+ /** 包级清单解析 + 形状校验;损坏 → INSTALL_FAILED(安装侧保证零残留) */
372
+ function parseSkillPackManifest(text) {
373
+ let raw;
374
+ try {
375
+ raw = JSON.parse(text);
376
+ } catch (e) {
377
+ throw new WebSkillError("INSTALL_FAILED", `Skill pack manifest is not valid JSON: ${messageOf$2(e)}`, e);
378
+ }
379
+ const invalid = (detail) => {
380
+ throw new WebSkillError("INSTALL_FAILED", `Skill pack manifest is corrupted: ${detail}`);
381
+ };
382
+ if (typeof raw !== "object" || raw === null) invalid("not an object");
383
+ const record = raw;
384
+ if (record["schemaVersion"] !== 1) invalid("schemaVersion must be 1");
385
+ if (!Array.isArray(record["skills"]) || record["skills"].length === 0) invalid("\"skills\" must be a non-empty array");
386
+ for (const entry of record["skills"]) {
387
+ if (typeof entry !== "object" || entry === null) invalid("skill entry is not an object");
388
+ const { name, digest } = entry;
389
+ if (typeof name !== "string" || name === "") invalid("skill entry missing a valid \"name\"");
390
+ if (typeof digest !== "string" || digest === "") invalid(`skill "${name}" missing a valid "digest"`);
391
+ }
392
+ return raw;
393
+ }
273
394
  /** 由条目列表构建 Catalog,保证按名称排序 */
274
395
  function buildCatalog(entries) {
275
396
  return { skills: [...entries].sort((a, b) => a.name.localeCompare(b.name)) };
@@ -297,7 +418,7 @@ var SkillReader = class {
297
418
  return this.#fs.readBinary(resolveInsideRoot(this.#rootFor(skillName), relativePath));
298
419
  }
299
420
  };
300
- const baseName$1 = (path) => path.split("/").pop() ?? path;
421
+ const baseName$2 = (path) => path.split("/").pop() ?? path;
301
422
  var SkillDiscovery = class {
302
423
  #fs;
303
424
  #roots;
@@ -313,7 +434,9 @@ var SkillDiscovery = class {
313
434
  this.#index.clear();
314
435
  const entries = [];
315
436
  const issues = [];
316
- const claimedNames = /* @__PURE__ */ new Set();
437
+ const candidates = [];
438
+ const knownSkillNames = /* @__PURE__ */ new Set();
439
+ const adjacency = /* @__PURE__ */ new Map();
317
440
  for (const root of this.#roots) {
318
441
  if (!await this.#fs.exists(root)) {
319
442
  issues.push({
@@ -326,7 +449,7 @@ var SkillDiscovery = class {
326
449
  }
327
450
  for (const child of await this.#fs.list(root)) {
328
451
  if (child.type !== "directory") continue;
329
- const dirName = baseName$1(child.path);
452
+ const dirName = baseName$2(child.path);
330
453
  const skillRoot = `${root}/${dirName}`;
331
454
  const skillMdPath = `${skillRoot}/SKILL.md`;
332
455
  const hasSkillMd = await this.#fs.exists(skillMdPath);
@@ -337,33 +460,53 @@ var SkillDiscovery = class {
337
460
  } catch (e) {
338
461
  parseError = e instanceof WebSkillError ? e : new WebSkillError("SKILL_INVALID_METADATA", String(e));
339
462
  }
340
- const scriptFileNames = await this.#listScriptFileNames(skillRoot);
341
- const candidateIssues = checkSkillRules({
463
+ candidates.push({
342
464
  dirName,
465
+ skillRoot,
343
466
  hasSkillMd,
344
467
  metadata,
345
468
  parseError,
346
- scriptFileNames,
347
- existingNames: claimedNames
348
- });
349
- for (const issue of candidateIssues) issue.path ??= skillRoot;
350
- issues.push(...candidateIssues);
351
- if (metadata) claimedNames.add(metadata.name);
352
- if (candidateIssues.some((i) => i.severity === "error") || !metadata) continue;
353
- this.#index.set(metadata.name, skillRoot);
354
- entries.push({
355
- name: metadata.name,
356
- description: metadata.description,
357
- version: typeof metadata["version"] === "string" ? metadata["version"] : void 0,
358
- license: typeof metadata["license"] === "string" ? metadata["license"] : void 0,
359
- root: skillRoot,
360
- source: "local",
361
- hasScripts: scriptFileNames.length > 0,
362
- hasReferences: await this.#fs.exists(`${skillRoot}/references`),
363
- hasAssets: await this.#fs.exists(`${skillRoot}/assets`)
469
+ scriptFileNames: await this.#listScriptFileNames(skillRoot)
364
470
  });
471
+ if (metadata) {
472
+ knownSkillNames.add(metadata.name);
473
+ const dependencies = metadata["dependencies"];
474
+ adjacency.set(metadata.name, Array.isArray(dependencies) ? dependencies.filter((d) => typeof d === "string") : []);
475
+ }
365
476
  }
366
477
  }
478
+ const cycles = checkDependencyCycles(adjacency);
479
+ const claimedNames = /* @__PURE__ */ new Set();
480
+ for (const candidate of candidates) {
481
+ const { metadata } = candidate;
482
+ const candidateIssues = checkSkillRules({
483
+ dirName: candidate.dirName,
484
+ hasSkillMd: candidate.hasSkillMd,
485
+ metadata,
486
+ parseError: candidate.parseError,
487
+ scriptFileNames: candidate.scriptFileNames,
488
+ existingNames: claimedNames,
489
+ knownSkillNames
490
+ });
491
+ for (const issue of candidateIssues) issue.path ??= candidate.skillRoot;
492
+ issues.push(...candidateIssues);
493
+ if (metadata) claimedNames.add(metadata.name);
494
+ const excludedByCycle = metadata !== void 0 && cycles.involved.has(metadata.name);
495
+ if (candidateIssues.some((i) => i.severity === "error") || !metadata || excludedByCycle) continue;
496
+ this.#index.set(metadata.name, candidate.skillRoot);
497
+ entries.push({
498
+ name: metadata.name,
499
+ description: metadata.description,
500
+ version: typeof metadata["version"] === "string" ? metadata["version"] : void 0,
501
+ license: typeof metadata["license"] === "string" ? metadata["license"] : void 0,
502
+ root: candidate.skillRoot,
503
+ source: "local",
504
+ hasScripts: candidate.scriptFileNames.length > 0,
505
+ hasReferences: await this.#fs.exists(`${candidate.skillRoot}/references`),
506
+ hasAssets: await this.#fs.exists(`${candidate.skillRoot}/assets`)
507
+ });
508
+ }
509
+ issues.push(...cycles.issues);
367
510
  entries.sort((a, b) => a.name.localeCompare(b.name));
368
511
  return {
369
512
  entries,
@@ -395,7 +538,7 @@ var SkillDiscovery = class {
395
538
  async #listScriptFileNames(skillRoot) {
396
539
  const scriptsDir = `${skillRoot}/scripts`;
397
540
  if (!await this.#fs.exists(scriptsDir)) return [];
398
- return (await this.#fs.list(scriptsDir)).filter((s) => s.type === "file").map((s) => baseName$1(s.path));
541
+ return (await this.#fs.list(scriptsDir)).filter((s) => s.type === "file").map((s) => baseName$2(s.path));
399
542
  }
400
543
  };
401
544
  /**
@@ -941,12 +1084,42 @@ function createScriptContext(deps) {
941
1084
  ...onWarning ? { onWarning } : {}
942
1085
  };
943
1086
  }
1087
+ const isRecord$1 = (v) => typeof v === "object" && v !== null;
944
1088
  /**
945
- * 默认的结果渲染构造:LLM 最终输出markdown block,
946
- * run.artifacts file blocks;summary terminationReason。
1089
+ * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 ChartSpec;
1090
+ * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
947
1091
  */
948
- function buildRenderResult(run, output) {
949
- const blocks = [];
1092
+ function extractChartSpec(data) {
1093
+ if (!isRecord$1(data)) return void 0;
1094
+ const raw = data["$chart"];
1095
+ if (!isRecord$1(raw)) return void 0;
1096
+ const { kind, labels, series } = raw;
1097
+ if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
1098
+ if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
1099
+ if (!Array.isArray(series)) return void 0;
1100
+ const validSeries = [];
1101
+ for (const item of series) {
1102
+ if (!isRecord$1(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
1103
+ const name = item["name"];
1104
+ validSeries.push({
1105
+ ...typeof name === "string" ? { name } : {},
1106
+ data: item["data"]
1107
+ });
1108
+ }
1109
+ const title = raw["title"];
1110
+ return {
1111
+ kind,
1112
+ labels,
1113
+ series: validSeries,
1114
+ ...typeof title === "string" ? { title } : {}
1115
+ };
1116
+ }
1117
+ /**
1118
+ * 默认的结果渲染构造:run 内收集的 renderBlocks(chart 等)在前,
1119
+ * LLM 最终输出 → markdown block,run.artifacts → file blocks 在后;summary 取 terminationReason。
1120
+ */
1121
+ function buildRenderResult(run, output, renderBlocks = []) {
1122
+ const blocks = [...renderBlocks];
950
1123
  if (output.trim() !== "") blocks.push({
951
1124
  type: "markdown",
952
1125
  text: output
@@ -1053,131 +1226,6 @@ var EventBus = class {
1053
1226
  for (const listener of this.#listeners.get("*") ?? []) listener(frozen);
1054
1227
  }
1055
1228
  };
1056
- const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
1057
- /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
1058
- var HookRunner = class {
1059
- #hooks = /* @__PURE__ */ new Map();
1060
- timeoutMs;
1061
- failOnHookError;
1062
- onWarning;
1063
- constructor(options = {}) {
1064
- this.timeoutMs = options.timeoutMs ?? 5e3;
1065
- this.failOnHookError = options.failOnHookError ?? false;
1066
- this.onWarning = options.onWarning;
1067
- }
1068
- register(phase, hook) {
1069
- const key = phase;
1070
- const list = this.#hooks.get(key) ?? [];
1071
- list.push(hook);
1072
- this.#hooks.set(key, list);
1073
- return this;
1074
- }
1075
- async run(phase, ctx) {
1076
- const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
1077
- for (const hook of hooks) try {
1078
- await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
1079
- } catch (e) {
1080
- if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf$1(e), e);
1081
- this.onWarning?.(messageOf$1(e));
1082
- }
1083
- }
1084
- #withTimeout(promise, phase) {
1085
- return new Promise((resolve, reject) => {
1086
- const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
1087
- promise.then((v) => {
1088
- clearTimeout(timer);
1089
- resolve(v);
1090
- }, (e) => {
1091
- clearTimeout(timer);
1092
- reject(e instanceof Error ? e : new Error(String(e)));
1093
- });
1094
- });
1095
- }
1096
- };
1097
- /** 内存 MemoryStore:测试与浏览器降级用 */
1098
- var InMemoryStore = class {
1099
- #data = /* @__PURE__ */ new Map();
1100
- async get(scope, key) {
1101
- return this.#data.get(scope)?.get(key);
1102
- }
1103
- async set(scope, key, value) {
1104
- let bucket = this.#data.get(scope);
1105
- if (!bucket) {
1106
- bucket = /* @__PURE__ */ new Map();
1107
- this.#data.set(scope, bucket);
1108
- }
1109
- bucket.set(key, value);
1110
- }
1111
- async delete(scope, key) {
1112
- this.#data.get(scope)?.delete(key);
1113
- }
1114
- async list(scope) {
1115
- return [...(this.#data.get(scope) ?? /* @__PURE__ */ new Map()).entries()].map(([key, value]) => ({
1116
- key,
1117
- value
1118
- }));
1119
- }
1120
- async clear(scope) {
1121
- if (scope === void 0) this.#data.clear();
1122
- else this.#data.delete(scope);
1123
- }
1124
- };
1125
- const encode = (s) => encodeURIComponent(s);
1126
- /**
1127
- * 基于 FileSystemProvider 的 MemoryStore:
1128
- * <root>/<encodeURIComponent(scope)>/<encodeURIComponent(key)>.json
1129
- * scope/key 全部编码防路径穿越;环境无关,构造必须显式传 fs。
1130
- */
1131
- var FsMemoryStore = class {
1132
- #root;
1133
- #fs;
1134
- constructor(deps) {
1135
- this.#root = deps.root.replace(/\/+$/, "");
1136
- this.#fs = deps.fs;
1137
- }
1138
- #scopeDir(scope) {
1139
- return `${this.#root}/${encode(scope)}`;
1140
- }
1141
- #keyPath(scope, key) {
1142
- return `${this.#scopeDir(scope)}/${encode(key)}.json`;
1143
- }
1144
- async get(scope, key) {
1145
- const path = this.#keyPath(scope, key);
1146
- if (!await this.#fs.exists(path)) return void 0;
1147
- return JSON.parse(await this.#fs.readText(path));
1148
- }
1149
- async set(scope, key, value) {
1150
- await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
1151
- }
1152
- async delete(scope, key) {
1153
- const path = this.#keyPath(scope, key);
1154
- if (await this.#fs.exists(path)) await this.#fs.remove(path);
1155
- }
1156
- async list(scope) {
1157
- const dir = this.#scopeDir(scope);
1158
- if (!await this.#fs.exists(dir)) return [];
1159
- const out = [];
1160
- for (const entry of await this.#fs.list(dir)) {
1161
- if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
1162
- const fileName = entry.path.split("/").pop() ?? "";
1163
- const key = decodeURIComponent(fileName.replace(/\.json$/, ""));
1164
- out.push({
1165
- key,
1166
- value: JSON.parse(await this.#fs.readText(entry.path))
1167
- });
1168
- }
1169
- return out.sort((a, b) => a.key.localeCompare(b.key));
1170
- }
1171
- async clear(scope) {
1172
- if (scope !== void 0) {
1173
- const dir = this.#scopeDir(scope);
1174
- if (await this.#fs.exists(dir)) await this.#fs.remove(dir, { recursive: true });
1175
- return;
1176
- }
1177
- if (!await this.#fs.exists(this.#root)) return;
1178
- for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
1179
- }
1180
- };
1181
1229
  /** 内存 ArtifactStore:测试与浏览器降级用;索引随进程生命周期存在 */
1182
1230
  var MemoryArtifactStore = class {
1183
1231
  #byRun = /* @__PURE__ */ new Map();
@@ -1222,227 +1270,44 @@ var MemoryArtifactStore = class {
1222
1270
  return artifact;
1223
1271
  }
1224
1272
  };
1225
- const INDEX_FILE = "index.json";
1226
- /**
1227
- * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
1228
- * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
1229
- * 环境无关(OPFS/Node 均可),构造必须显式传 fs。
1230
- */
1231
- var FsArtifactStore = class {
1232
- #root;
1233
- #fs;
1234
- constructor(deps) {
1235
- this.#root = deps.root.replace(/\/+$/, "");
1236
- this.#fs = deps.fs;
1237
- }
1238
- async createTextArtifact(input) {
1239
- const size = new TextEncoder().encode(input.content).length;
1240
- await this.#fs.writeText(this.#artifactPath(input.runId, input.path), input.content);
1241
- return this.#persist({
1242
- ...input,
1243
- type: "text",
1244
- size
1245
- });
1246
- }
1247
- async createBinaryArtifact(input) {
1248
- await this.#fs.writeBinary(this.#artifactPath(input.runId, input.path), input.content);
1249
- return this.#persist({
1250
- ...input,
1251
- type: "binary",
1252
- size: input.content.length
1253
- });
1273
+ /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1274
+ var TraceRecorder = class {
1275
+ #runId;
1276
+ #clock;
1277
+ #events = [];
1278
+ #seq = 0;
1279
+ constructor(runId, clock = {}) {
1280
+ this.#runId = runId;
1281
+ this.#clock = clock;
1254
1282
  }
1255
- async listArtifacts(runId) {
1256
- const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
1257
- if (!await this.#fs.exists(indexPath)) return [];
1258
- const raw = await this.#fs.readText(indexPath);
1259
- return JSON.parse(raw).artifacts ?? [];
1283
+ record(type, opts) {
1284
+ const event = {
1285
+ id: this.#clock.createId?.() ?? `evt-${++this.#seq}`,
1286
+ runId: this.#runId,
1287
+ ts: this.#clock.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
1288
+ type,
1289
+ ...opts?.message !== void 0 ? { message: opts.message } : {},
1290
+ ...opts?.data !== void 0 ? { data: opts.data } : {}
1291
+ };
1292
+ this.#events.push(event);
1293
+ return event;
1260
1294
  }
1261
- #artifactPath(runId, artifactPath) {
1262
- return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
1295
+ list() {
1296
+ return [...this.#events];
1263
1297
  }
1264
- async #persist(input) {
1265
- const artifact = {
1266
- id: `${input.runId}/${input.path}`,
1267
- runId: input.runId,
1268
- path: input.path,
1269
- type: input.type,
1270
- mimeType: input.mimeType,
1271
- size: input.size,
1272
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1273
- metadata: input.metadata
1274
- };
1275
- const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
1276
- await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
1277
- return artifact;
1278
- }
1279
- };
1280
- const isRecord = (v) => typeof v === "object" && v !== null;
1281
- const isNonEmptyString = (v) => typeof v === "string" && v !== "";
1282
- /** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
1283
- function parseBridgeRequest(data) {
1284
- if (!isRecord(data)) return void 0;
1285
- const { kind, id } = data;
1286
- if (!isNonEmptyString(id)) return void 0;
1287
- switch (kind) {
1288
- case "readReference": return isNonEmptyString(data["path"]) ? {
1289
- kind,
1290
- id,
1291
- path: data["path"]
1292
- } : void 0;
1293
- case "writeArtifact": {
1294
- const { path, content, mimeType } = data;
1295
- const validContent = typeof content === "string" || Array.isArray(content) && content.every((n) => typeof n === "number");
1296
- if (!isNonEmptyString(path) || !validContent) return void 0;
1297
- return {
1298
- kind,
1299
- id,
1300
- path,
1301
- content,
1302
- ...isNonEmptyString(mimeType) ? { mimeType } : {}
1303
- };
1304
- }
1305
- case "confirm": return isNonEmptyString(data["message"]) ? {
1306
- kind,
1307
- id,
1308
- message: data["message"]
1309
- } : void 0;
1310
- default: return;
1311
- }
1312
- }
1313
- function bridgeError(id, code, message) {
1314
- return {
1315
- id,
1316
- ok: false,
1317
- error: {
1318
- code,
1319
- message
1320
- }
1321
- };
1322
- }
1323
- /**
1324
- * 判定 URL 是否被策略放行:
1325
- * - 'deny-all'(默认)全拒;'allow-all' 全放
1326
- * - { allow } 条目支持三种写法:
1327
- * - 精确域名 'api.example.com'(忽略端口与路径)
1328
- * - 通配 '*.example.com'(含 apex 与任意深度子域)
1329
- * - 源 'http://localhost:3000'(协议 + host + port 全等)
1330
- * - URL 解析失败一律拒绝
1331
- */
1332
- function isNetworkAllowed(policy, url) {
1333
- if (policy === "allow-all") return true;
1334
- if (policy === "deny-all" || !policy || typeof policy !== "object") return false;
1335
- var allow = policy.allow;
1336
- if (!Array.isArray(allow)) return false;
1337
- var parsed;
1338
- try {
1339
- parsed = new URL(url);
1340
- } catch {
1341
- return false;
1342
- }
1343
- var host = parsed.hostname.toLowerCase();
1344
- for (var i = 0; i < allow.length; i++) {
1345
- var entry = allow[i];
1346
- if (typeof entry !== "string" || entry === "") continue;
1347
- if (entry.indexOf("://") !== -1) {
1348
- try {
1349
- if (new URL(entry).origin === parsed.origin) return true;
1350
- } catch {}
1351
- continue;
1352
- }
1353
- var rule = entry.toLowerCase();
1354
- if (rule.indexOf("*.") === 0) {
1355
- var suffix = rule.slice(2);
1356
- if (host === suffix || host.endsWith("." + suffix)) return true;
1357
- } else if (host === rule) return true;
1358
- }
1359
- return false;
1360
- }
1361
- /** 阻断 trace 用的脱敏 host(解析失败返回占位,不记录完整 URL) */
1362
- function networkUrlHost(url) {
1363
- try {
1364
- return new URL(url).hostname;
1365
- } catch {
1366
- return "(unparseable-url)";
1367
- }
1368
- }
1369
- /**
1370
- * Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
1371
- * 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
1372
- * once-per-run(默认)在 run 内记忆已批准能力,every-call 每次调用都问。
1373
- */
1374
- var CapabilityApproval = class {
1375
- #uiBridge;
1376
- #scope;
1377
- /** runId → 已批准能力集合(once-per-run 记忆) */
1378
- #approved = /* @__PURE__ */ new Map();
1379
- #seq = 0;
1380
- constructor(deps = {}) {
1381
- this.#uiBridge = deps.uiBridge;
1382
- this.#scope = deps.scope ?? "once-per-run";
1383
- }
1384
- async authorize(input) {
1385
- const { runId, capability, mode } = input;
1386
- if (mode === false) return "disabled";
1387
- if (mode !== "require-approval") return "allowed";
1388
- if (this.#scope === "once-per-run" && this.#approved.get(runId)?.has(capability)) return "allowed";
1389
- if (!this.#uiBridge) return "denied";
1390
- if ((await this.#uiBridge.request({
1391
- type: "authorize",
1392
- id: `authorize-${runId}-${capability}-${++this.#seq}`,
1393
- capability,
1394
- message: input.message,
1395
- ...input.details !== void 0 ? { details: input.details } : {}
1396
- })).value !== true) return "denied";
1397
- if (this.#scope === "once-per-run") {
1398
- let set = this.#approved.get(runId);
1399
- if (!set) {
1400
- set = /* @__PURE__ */ new Set();
1401
- this.#approved.set(runId, set);
1402
- }
1403
- set.add(capability);
1404
- }
1405
- return "allowed";
1406
- }
1407
- };
1408
- /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
1409
- var TraceRecorder = class {
1410
- #runId;
1411
- #clock;
1412
- #events = [];
1413
- #seq = 0;
1414
- constructor(runId, clock = {}) {
1415
- this.#runId = runId;
1416
- this.#clock = clock;
1417
- }
1418
- record(type, opts) {
1419
- const event = {
1420
- id: this.#clock.createId?.() ?? `evt-${++this.#seq}`,
1421
- runId: this.#runId,
1422
- ts: this.#clock.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
1423
- type,
1424
- ...opts?.message !== void 0 ? { message: opts.message } : {},
1425
- ...opts?.data !== void 0 ? { data: opts.data } : {}
1426
- };
1427
- this.#events.push(event);
1428
- return event;
1429
- }
1430
- list() {
1431
- return [...this.#events];
1432
- }
1433
- };
1434
- /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1435
- var RunTerminated = class extends Error {
1436
- outcome;
1437
- constructor(outcome) {
1438
- super(outcome.message);
1439
- this.outcome = outcome;
1298
+ };
1299
+ /** 交互终态(取消/超时):从工具执行深处直接终止 run */
1300
+ var RunTerminated = class extends Error {
1301
+ outcome;
1302
+ constructor(outcome) {
1303
+ super(outcome.message);
1304
+ this.outcome = outcome;
1440
1305
  }
1441
1306
  };
1442
1307
  /** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
1443
1308
  var BridgeRequestError = class extends Error {};
1444
1309
  const baseName = (p) => p.split("/").pop() ?? p;
1445
- const messageOf = (e) => e instanceof Error ? e.message : String(e);
1310
+ const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
1446
1311
  const toolError = (code, message) => ({
1447
1312
  ok: false,
1448
1313
  content: [],
@@ -1507,7 +1372,8 @@ var AgentLoop = class {
1507
1372
  now,
1508
1373
  interactionSeq: 0,
1509
1374
  messages: [],
1510
- turn: 0
1375
+ turn: 0,
1376
+ renderBlocks: []
1511
1377
  };
1512
1378
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1513
1379
  const route = this.#deps.catalogFilter ? {
@@ -1523,7 +1389,7 @@ var AgentLoop = class {
1523
1389
  try {
1524
1390
  return await source.listToolSpecs();
1525
1391
  } catch (e) {
1526
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
1392
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf$1(e)}` });
1527
1393
  return [];
1528
1394
  }
1529
1395
  }))).flat();
@@ -1537,7 +1403,7 @@ var AgentLoop = class {
1537
1403
  try {
1538
1404
  return await this.#turnLoop(state, startMs, 1, externalSpecs);
1539
1405
  } catch (e) {
1540
- if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
1406
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1541
1407
  throw e;
1542
1408
  }
1543
1409
  }
@@ -1576,7 +1442,7 @@ var AgentLoop = class {
1576
1442
  });
1577
1443
  } catch (e) {
1578
1444
  const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
1579
- return finish("failed", "llm-error", messageOf(e), code);
1445
+ return finish("failed", "llm-error", messageOf$1(e), code);
1580
1446
  }
1581
1447
  trace.record("llm.response", { data: {
1582
1448
  turn,
@@ -1628,21 +1494,21 @@ var AgentLoop = class {
1628
1494
  });
1629
1495
  const bridge = this.#deps.uiBridge;
1630
1496
  if (status === "completed" && bridge?.renderResult && this.#config.renderResult !== "off") try {
1631
- const request = buildRenderResult(run, output);
1497
+ const request = buildRenderResult(run, output, state.renderBlocks);
1632
1498
  await bridge.renderResult(request);
1633
1499
  trace.record("ui.rendered", { data: { blockCount: request.blocks.length } });
1634
1500
  } catch (e) {
1635
- trace.record("run.warning", { message: `renderResult failed: ${messageOf(e)}` });
1501
+ trace.record("run.warning", { message: `renderResult failed: ${messageOf$1(e)}` });
1636
1502
  }
1637
1503
  if (this.#deps.snapshotStore) try {
1638
1504
  await this.#deps.snapshotStore.delete(state.runId);
1639
1505
  } catch (e) {
1640
- trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
1506
+ trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf$1(e)}` });
1641
1507
  }
1642
1508
  try {
1643
1509
  await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1644
1510
  } catch (e) {
1645
- trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
1511
+ trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf$1(e)}` });
1646
1512
  }
1647
1513
  run.trace = trace.list();
1648
1514
  return {
@@ -1677,7 +1543,7 @@ var AgentLoop = class {
1677
1543
  try {
1678
1544
  await store.save(snapshot);
1679
1545
  } catch (e) {
1680
- state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf(e)}` });
1546
+ state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf$1(e)}` });
1681
1547
  }
1682
1548
  }
1683
1549
  /**
@@ -1710,7 +1576,8 @@ var AgentLoop = class {
1710
1576
  now,
1711
1577
  interactionSeq: 0,
1712
1578
  messages: snapshot.messages.map((m) => ({ ...m })),
1713
- turn: snapshot.turn
1579
+ turn: snapshot.turn,
1580
+ renderBlocks: []
1714
1581
  };
1715
1582
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1716
1583
  trace.record("run.resumed", { data: {
@@ -1776,14 +1643,14 @@ var AgentLoop = class {
1776
1643
  try {
1777
1644
  return await source.listToolSpecs();
1778
1645
  } catch (e) {
1779
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
1646
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf$1(e)}` });
1780
1647
  return [];
1781
1648
  }
1782
1649
  }))).flat();
1783
1650
  try {
1784
1651
  return await this.#turnLoop(state, startMs, snapshot.turn + 1, externalSpecs);
1785
1652
  } catch (e) {
1786
- if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
1653
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1787
1654
  throw e;
1788
1655
  }
1789
1656
  }
@@ -1875,8 +1742,8 @@ var AgentLoop = class {
1875
1742
  message: e.message,
1876
1743
  code: "RUN_INTERACTION_TIMEOUT"
1877
1744
  });
1878
- state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
1879
- throw new BridgeRequestError(messageOf(e));
1745
+ state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf$1(e)}` });
1746
+ throw new BridgeRequestError(messageOf$1(e));
1880
1747
  }
1881
1748
  if (response.cancelled) throw new RunTerminated({
1882
1749
  status: "cancelled",
@@ -1922,11 +1789,20 @@ var AgentLoop = class {
1922
1789
  result = source ? await source.call(call.name, call.arguments) : toolError(resolution.code, resolution.message);
1923
1790
  }
1924
1791
  }
1925
- if (result.ok) state.trace.record("tool.completed", { data: {
1926
- name: call.name,
1927
- callId: call.id
1928
- } });
1929
- else state.trace.record("tool.failed", {
1792
+ if (result.ok) {
1793
+ state.trace.record("tool.completed", { data: {
1794
+ name: call.name,
1795
+ callId: call.id
1796
+ } });
1797
+ for (const item of result.content) {
1798
+ if (item.type !== "json") continue;
1799
+ const chart = extractChartSpec(item.data);
1800
+ if (chart) state.renderBlocks.push({
1801
+ type: "chart",
1802
+ chart
1803
+ });
1804
+ }
1805
+ } else state.trace.record("tool.failed", {
1930
1806
  message: result.error?.message,
1931
1807
  data: {
1932
1808
  name: call.name,
@@ -1980,7 +1856,7 @@ var AgentLoop = class {
1980
1856
  };
1981
1857
  } catch (e) {
1982
1858
  if (e instanceof WebSkillError && e.code === "SKILL_NOT_FOUND" && (this.#deps.skillProviders?.length ?? 0) > 0) return this.#handleReadExternalSkill(skillName, pathArg, state);
1983
- return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `read_skill_file failed: ${messageOf(e)}`);
1859
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `read_skill_file failed: ${messageOf$1(e)}`);
1984
1860
  }
1985
1861
  }
1986
1862
  /** 经外部技能提供者读取技能文档/资源;skillKey 为技能名或 mcp:// URI */
@@ -2023,42 +1899,58 @@ var AgentLoop = class {
2023
1899
  }
2024
1900
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}`);
2025
1901
  }
2026
- /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用 */
2027
- async #activateSkill(skillName, state) {
1902
+ /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
1903
+ async #activateSkill(skillName, state, via) {
2028
1904
  state.activated.add(skillName);
2029
- state.trace.record("skill.activated", { data: { skillName } });
1905
+ state.trace.record("skill.activated", { data: {
1906
+ skillName,
1907
+ ...via ? { via } : {}
1908
+ } });
2030
1909
  await this.#lifecycle("activate", state, { skillName });
2031
1910
  await this.#writeActivationMemory(skillName, state);
2032
1911
  const root = this.#deps.skillIndex.get(skillName);
2033
1912
  if (!root) return "";
2034
- const executor = this.#deps.executor;
2035
- if (!executor) return "";
2036
1913
  let allowedTools;
1914
+ let dependencies = [];
2037
1915
  try {
2038
1916
  const { metadata } = parseSkillMarkdown(await this.#deps.fs.readText(`${root}/SKILL.md`));
1917
+ const rawDeps = metadata["dependencies"];
1918
+ if (Array.isArray(rawDeps)) dependencies = rawDeps.filter((d) => typeof d === "string");
2039
1919
  const raw = metadata["allowed-tools"];
2040
1920
  if (raw !== void 0) if (Array.isArray(raw)) allowedTools = raw.filter((e) => typeof e === "string");
2041
1921
  else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
2042
1922
  } catch {}
1923
+ const executor = this.#deps.executor;
2043
1924
  const loaded = [];
2044
- let scriptFiles = [];
2045
- try {
2046
- scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
2047
- } catch {
2048
- return "";
2049
- }
2050
- for (const file of scriptFiles) {
2051
- const match = /^(.*)\.(ts|js)$/.exec(file);
2052
- if (!match?.[1]) continue;
2053
- if (allowedTools && !allowedTools.includes(match[1])) continue;
1925
+ if (executor) {
1926
+ let scriptFiles = [];
2054
1927
  try {
2055
- const def = await executor.loadDefinition(root, match[1]);
2056
- await this.#enrichDefinition(root, match[1], def, state);
2057
- state.activatedTools.set(def.name, def);
2058
- loaded.push(def.name);
2059
- } catch {}
1928
+ scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
1929
+ } catch {
1930
+ scriptFiles = [];
1931
+ }
1932
+ for (const file of scriptFiles) {
1933
+ const match = /^(.*)\.(ts|js)$/.exec(file);
1934
+ if (!match?.[1]) continue;
1935
+ if (allowedTools && !allowedTools.includes(match[1])) continue;
1936
+ try {
1937
+ const def = await executor.loadDefinition(root, match[1]);
1938
+ await this.#enrichDefinition(root, match[1], def, state);
1939
+ state.activatedTools.set(def.name, def);
1940
+ loaded.push(def.name);
1941
+ } catch {}
1942
+ }
1943
+ }
1944
+ let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
1945
+ for (const dep of dependencies) {
1946
+ if (state.activated.has(dep)) continue;
1947
+ if (!this.#deps.skillIndex.has(dep)) {
1948
+ state.trace.record("run.warning", { message: `Skill "${skillName}" depends on unknown skill "${dep}"; skipped` });
1949
+ continue;
1950
+ }
1951
+ note += await this.#activateSkill(dep, state, skillName);
2060
1952
  }
2061
- return loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
1953
+ return note;
2062
1954
  }
2063
1955
  /**
2064
1956
  * D2 Schema 兜底链(同一 run 内激活时只算一次):
@@ -2071,7 +1963,7 @@ var AgentLoop = class {
2071
1963
  def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
2072
1964
  return;
2073
1965
  } catch (e) {
2074
- state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf(e)}` });
1966
+ state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf$1(e)}` });
2075
1967
  }
2076
1968
  if (!this.#deps.schemaInferer) return;
2077
1969
  for (const ext of ["ts", "js"]) {
@@ -2133,7 +2025,7 @@ var AgentLoop = class {
2133
2025
  } catch (e) {
2134
2026
  if (e instanceof RunTerminated) throw e;
2135
2027
  await this.#bumpSkillStat(skillName, "failures", state);
2136
- return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf(e)}`);
2028
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf$1(e)}`);
2137
2029
  }
2138
2030
  }
2139
2031
  /** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
@@ -2172,7 +2064,7 @@ var AgentLoop = class {
2172
2064
  try {
2173
2065
  await this.#deps.memory?.set(scope, key, value);
2174
2066
  } catch (e) {
2175
- state.trace.record("run.warning", { message: `Memory write failed: ${messageOf(e)}` });
2067
+ state.trace.record("run.warning", { message: `Memory write failed: ${messageOf$1(e)}` });
2176
2068
  }
2177
2069
  }
2178
2070
  async #writeActivationMemory(skillName, state) {
@@ -2428,6 +2320,393 @@ var WebSkillRuntime = class {
2428
2320
  }, this.#deps.config).resume(snapshot);
2429
2321
  }
2430
2322
  };
2323
+ /**
2324
+ * navigator.webskill 门面装配层:零新业务逻辑,全部直转
2325
+ * SkillDiscovery / validateSkills / WebSkillRuntime / SkillManager。
2326
+ */
2327
+ function createWebSkillApi(deps) {
2328
+ const { fs, roots } = deps;
2329
+ let runtime;
2330
+ const lazyRuntime = () => {
2331
+ runtime ??= new WebSkillRuntime({
2332
+ fs,
2333
+ roots,
2334
+ llm: deps.llm,
2335
+ ...deps.executor ? { executor: deps.executor } : {},
2336
+ ...deps.artifactStore ? { artifactStore: deps.artifactStore } : {},
2337
+ ...deps.loopConfig ? { config: deps.loopConfig } : {}
2338
+ });
2339
+ return runtime;
2340
+ };
2341
+ const requireManager = () => {
2342
+ if (!deps.skillManager) throw new WebSkillError("TOOL_UNSUPPORTED", "No skill manager is configured; install/uninstall is unavailable");
2343
+ return deps.skillManager;
2344
+ };
2345
+ return {
2346
+ async discover(path) {
2347
+ const { entries } = await new SkillDiscovery(fs, [path]).discover();
2348
+ return buildCatalog(entries);
2349
+ },
2350
+ async read(name) {
2351
+ const { entries } = await new SkillDiscovery(fs, roots).discover();
2352
+ const entry = entries.find((e) => e.name === name);
2353
+ if (!entry) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${name}`);
2354
+ const skillMdPath = `${entry.root}/SKILL.md`;
2355
+ const { metadata, body } = parseSkillMarkdown(await fs.readText(skillMdPath));
2356
+ return {
2357
+ metadata,
2358
+ body,
2359
+ location: {
2360
+ skillRoot: entry.root,
2361
+ skillMdPath,
2362
+ source: entry.source
2363
+ }
2364
+ };
2365
+ },
2366
+ validate(path) {
2367
+ return validateSkills(fs, [path]);
2368
+ },
2369
+ async run(prompt) {
2370
+ return (await lazyRuntime().run(prompt)).run;
2371
+ },
2372
+ async install(url) {
2373
+ const manifest = await requireManager().install(sourceFromUrl(url));
2374
+ return {
2375
+ name: manifest.name,
2376
+ ...manifest.version !== void 0 ? { version: manifest.version } : {},
2377
+ installedAt: manifest.installedAt,
2378
+ integrity: manifest.integrity
2379
+ };
2380
+ },
2381
+ async uninstall(name) {
2382
+ await requireManager().uninstall(name);
2383
+ }
2384
+ };
2385
+ }
2386
+ /** install URL 分派:http(s)…​.zip → http;…​.git → git(无 git 能力的 manager 自行 TOOL_UNSUPPORTED);其余 → local */
2387
+ function sourceFromUrl(url) {
2388
+ const bare = url.split("?")[0] ?? url;
2389
+ if (/^https?:\/\//.test(url) && bare.endsWith(".zip")) return {
2390
+ type: "http",
2391
+ url
2392
+ };
2393
+ if (bare.endsWith(".git")) return {
2394
+ type: "git",
2395
+ url
2396
+ };
2397
+ return {
2398
+ type: "local",
2399
+ path: url
2400
+ };
2401
+ }
2402
+ const messageOf = (e) => e instanceof Error ? e.message : String(e);
2403
+ /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
2404
+ var HookRunner = class {
2405
+ #hooks = /* @__PURE__ */ new Map();
2406
+ timeoutMs;
2407
+ failOnHookError;
2408
+ onWarning;
2409
+ constructor(options = {}) {
2410
+ this.timeoutMs = options.timeoutMs ?? 5e3;
2411
+ this.failOnHookError = options.failOnHookError ?? false;
2412
+ this.onWarning = options.onWarning;
2413
+ }
2414
+ register(phase, hook) {
2415
+ const key = phase;
2416
+ const list = this.#hooks.get(key) ?? [];
2417
+ list.push(hook);
2418
+ this.#hooks.set(key, list);
2419
+ return this;
2420
+ }
2421
+ async run(phase, ctx) {
2422
+ const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
2423
+ for (const hook of hooks) try {
2424
+ await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
2425
+ } catch (e) {
2426
+ if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf(e), e);
2427
+ this.onWarning?.(messageOf(e));
2428
+ }
2429
+ }
2430
+ #withTimeout(promise, phase) {
2431
+ return new Promise((resolve, reject) => {
2432
+ const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
2433
+ promise.then((v) => {
2434
+ clearTimeout(timer);
2435
+ resolve(v);
2436
+ }, (e) => {
2437
+ clearTimeout(timer);
2438
+ reject(e instanceof Error ? e : new Error(String(e)));
2439
+ });
2440
+ });
2441
+ }
2442
+ };
2443
+ /** 内存 MemoryStore:测试与浏览器降级用 */
2444
+ var InMemoryStore = class {
2445
+ #data = /* @__PURE__ */ new Map();
2446
+ async get(scope, key) {
2447
+ return this.#data.get(scope)?.get(key);
2448
+ }
2449
+ async set(scope, key, value) {
2450
+ let bucket = this.#data.get(scope);
2451
+ if (!bucket) {
2452
+ bucket = /* @__PURE__ */ new Map();
2453
+ this.#data.set(scope, bucket);
2454
+ }
2455
+ bucket.set(key, value);
2456
+ }
2457
+ async delete(scope, key) {
2458
+ this.#data.get(scope)?.delete(key);
2459
+ }
2460
+ async list(scope) {
2461
+ return [...(this.#data.get(scope) ?? /* @__PURE__ */ new Map()).entries()].map(([key, value]) => ({
2462
+ key,
2463
+ value
2464
+ }));
2465
+ }
2466
+ async clear(scope) {
2467
+ if (scope === void 0) this.#data.clear();
2468
+ else this.#data.delete(scope);
2469
+ }
2470
+ };
2471
+ const encode = (s) => encodeURIComponent(s);
2472
+ /**
2473
+ * 基于 FileSystemProvider 的 MemoryStore:
2474
+ * <root>/<encodeURIComponent(scope)>/<encodeURIComponent(key)>.json
2475
+ * scope/key 全部编码防路径穿越;环境无关,构造必须显式传 fs。
2476
+ */
2477
+ var FsMemoryStore = class {
2478
+ #root;
2479
+ #fs;
2480
+ constructor(deps) {
2481
+ this.#root = deps.root.replace(/\/+$/, "");
2482
+ this.#fs = deps.fs;
2483
+ }
2484
+ #scopeDir(scope) {
2485
+ return `${this.#root}/${encode(scope)}`;
2486
+ }
2487
+ #keyPath(scope, key) {
2488
+ return `${this.#scopeDir(scope)}/${encode(key)}.json`;
2489
+ }
2490
+ async get(scope, key) {
2491
+ const path = this.#keyPath(scope, key);
2492
+ if (!await this.#fs.exists(path)) return void 0;
2493
+ return JSON.parse(await this.#fs.readText(path));
2494
+ }
2495
+ async set(scope, key, value) {
2496
+ await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
2497
+ }
2498
+ async delete(scope, key) {
2499
+ const path = this.#keyPath(scope, key);
2500
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
2501
+ }
2502
+ async list(scope) {
2503
+ const dir = this.#scopeDir(scope);
2504
+ if (!await this.#fs.exists(dir)) return [];
2505
+ const out = [];
2506
+ for (const entry of await this.#fs.list(dir)) {
2507
+ if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
2508
+ const fileName = entry.path.split("/").pop() ?? "";
2509
+ const key = decodeURIComponent(fileName.replace(/\.json$/, ""));
2510
+ out.push({
2511
+ key,
2512
+ value: JSON.parse(await this.#fs.readText(entry.path))
2513
+ });
2514
+ }
2515
+ return out.sort((a, b) => a.key.localeCompare(b.key));
2516
+ }
2517
+ async clear(scope) {
2518
+ if (scope !== void 0) {
2519
+ const dir = this.#scopeDir(scope);
2520
+ if (await this.#fs.exists(dir)) await this.#fs.remove(dir, { recursive: true });
2521
+ return;
2522
+ }
2523
+ if (!await this.#fs.exists(this.#root)) return;
2524
+ for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
2525
+ }
2526
+ };
2527
+ const INDEX_FILE = "index.json";
2528
+ /**
2529
+ * 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
2530
+ * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
2531
+ * 环境无关(OPFS/Node 均可),构造必须显式传 fs。
2532
+ */
2533
+ var FsArtifactStore = class {
2534
+ #root;
2535
+ #fs;
2536
+ constructor(deps) {
2537
+ this.#root = deps.root.replace(/\/+$/, "");
2538
+ this.#fs = deps.fs;
2539
+ }
2540
+ async createTextArtifact(input) {
2541
+ const size = new TextEncoder().encode(input.content).length;
2542
+ await this.#fs.writeText(this.#artifactPath(input.runId, input.path), input.content);
2543
+ return this.#persist({
2544
+ ...input,
2545
+ type: "text",
2546
+ size
2547
+ });
2548
+ }
2549
+ async createBinaryArtifact(input) {
2550
+ await this.#fs.writeBinary(this.#artifactPath(input.runId, input.path), input.content);
2551
+ return this.#persist({
2552
+ ...input,
2553
+ type: "binary",
2554
+ size: input.content.length
2555
+ });
2556
+ }
2557
+ async listArtifacts(runId) {
2558
+ const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
2559
+ if (!await this.#fs.exists(indexPath)) return [];
2560
+ const raw = await this.#fs.readText(indexPath);
2561
+ return JSON.parse(raw).artifacts ?? [];
2562
+ }
2563
+ #artifactPath(runId, artifactPath) {
2564
+ return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
2565
+ }
2566
+ async #persist(input) {
2567
+ const artifact = {
2568
+ id: `${input.runId}/${input.path}`,
2569
+ runId: input.runId,
2570
+ path: input.path,
2571
+ type: input.type,
2572
+ mimeType: input.mimeType,
2573
+ size: input.size,
2574
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2575
+ metadata: input.metadata
2576
+ };
2577
+ const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
2578
+ await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
2579
+ return artifact;
2580
+ }
2581
+ };
2582
+ const isRecord = (v) => typeof v === "object" && v !== null;
2583
+ const isNonEmptyString = (v) => typeof v === "string" && v !== "";
2584
+ /** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
2585
+ function parseBridgeRequest(data) {
2586
+ if (!isRecord(data)) return void 0;
2587
+ const { kind, id } = data;
2588
+ if (!isNonEmptyString(id)) return void 0;
2589
+ switch (kind) {
2590
+ case "readReference": return isNonEmptyString(data["path"]) ? {
2591
+ kind,
2592
+ id,
2593
+ path: data["path"]
2594
+ } : void 0;
2595
+ case "writeArtifact": {
2596
+ const { path, content, mimeType } = data;
2597
+ const validContent = typeof content === "string" || Array.isArray(content) && content.every((n) => typeof n === "number");
2598
+ if (!isNonEmptyString(path) || !validContent) return void 0;
2599
+ return {
2600
+ kind,
2601
+ id,
2602
+ path,
2603
+ content,
2604
+ ...isNonEmptyString(mimeType) ? { mimeType } : {}
2605
+ };
2606
+ }
2607
+ case "confirm": return isNonEmptyString(data["message"]) ? {
2608
+ kind,
2609
+ id,
2610
+ message: data["message"]
2611
+ } : void 0;
2612
+ default: return;
2613
+ }
2614
+ }
2615
+ function bridgeError(id, code, message) {
2616
+ return {
2617
+ id,
2618
+ ok: false,
2619
+ error: {
2620
+ code,
2621
+ message
2622
+ }
2623
+ };
2624
+ }
2625
+ /**
2626
+ * 判定 URL 是否被策略放行:
2627
+ * - 'deny-all'(默认)全拒;'allow-all' 全放
2628
+ * - { allow } 条目支持三种写法:
2629
+ * - 精确域名 'api.example.com'(忽略端口与路径)
2630
+ * - 通配 '*.example.com'(含 apex 与任意深度子域)
2631
+ * - 源 'http://localhost:3000'(协议 + host + port 全等)
2632
+ * - URL 解析失败一律拒绝
2633
+ */
2634
+ function isNetworkAllowed(policy, url) {
2635
+ if (policy === "allow-all") return true;
2636
+ if (policy === "deny-all" || !policy || typeof policy !== "object") return false;
2637
+ var allow = policy.allow;
2638
+ if (!Array.isArray(allow)) return false;
2639
+ var parsed;
2640
+ try {
2641
+ parsed = new URL(url);
2642
+ } catch {
2643
+ return false;
2644
+ }
2645
+ var host = parsed.hostname.toLowerCase();
2646
+ for (var i = 0; i < allow.length; i++) {
2647
+ var entry = allow[i];
2648
+ if (typeof entry !== "string" || entry === "") continue;
2649
+ if (entry.indexOf("://") !== -1) {
2650
+ try {
2651
+ if (new URL(entry).origin === parsed.origin) return true;
2652
+ } catch {}
2653
+ continue;
2654
+ }
2655
+ var rule = entry.toLowerCase();
2656
+ if (rule.indexOf("*.") === 0) {
2657
+ var suffix = rule.slice(2);
2658
+ if (host === suffix || host.endsWith("." + suffix)) return true;
2659
+ } else if (host === rule) return true;
2660
+ }
2661
+ return false;
2662
+ }
2663
+ /** 阻断 trace 用的脱敏 host(解析失败返回占位,不记录完整 URL) */
2664
+ function networkUrlHost(url) {
2665
+ try {
2666
+ return new URL(url).hostname;
2667
+ } catch {
2668
+ return "(unparseable-url)";
2669
+ }
2670
+ }
2671
+ /**
2672
+ * Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
2673
+ * 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
2674
+ * once-per-run(默认)在 run 内记忆已批准能力,every-call 每次调用都问。
2675
+ */
2676
+ var CapabilityApproval = class {
2677
+ #uiBridge;
2678
+ #scope;
2679
+ /** runId → 已批准能力集合(once-per-run 记忆) */
2680
+ #approved = /* @__PURE__ */ new Map();
2681
+ #seq = 0;
2682
+ constructor(deps = {}) {
2683
+ this.#uiBridge = deps.uiBridge;
2684
+ this.#scope = deps.scope ?? "once-per-run";
2685
+ }
2686
+ async authorize(input) {
2687
+ const { runId, capability, mode } = input;
2688
+ if (mode === false) return "disabled";
2689
+ if (mode !== "require-approval") return "allowed";
2690
+ if (this.#scope === "once-per-run" && this.#approved.get(runId)?.has(capability)) return "allowed";
2691
+ if (!this.#uiBridge) return "denied";
2692
+ if ((await this.#uiBridge.request({
2693
+ type: "authorize",
2694
+ id: `authorize-${runId}-${capability}-${++this.#seq}`,
2695
+ capability,
2696
+ message: input.message,
2697
+ ...input.details !== void 0 ? { details: input.details } : {}
2698
+ })).value !== true) return "denied";
2699
+ if (this.#scope === "once-per-run") {
2700
+ let set = this.#approved.get(runId);
2701
+ if (!set) {
2702
+ set = /* @__PURE__ */ new Set();
2703
+ this.#approved.set(runId, set);
2704
+ }
2705
+ set.add(capability);
2706
+ }
2707
+ return "allowed";
2708
+ }
2709
+ };
2431
2710
 
2432
2711
  //#endregion
2433
- export { normalizePath as $, mergeCatalogEntries as A, SKILL_MANIFEST_FILE as B, WebSkillRuntime as C, fromVercelResult as D, createScriptContext as E, schemaToForm as F, WebSkillError as G, SKILL_NAME_PATTERN as H, toLlmToolSpec as I, checkSkillRules as J, buildCatalog as K, toVercelToolSpecs as L, normalizeToolContent as M, parseBridgeRequest as N, fromVercelStreamPart as O, resolveToolName as P, jsonRenderer as Q, MemoryFS as R, TraceRecorder as S, buildRenderResult as T, SkillDiscovery as U, SKILL_NAME_MAX_LENGTH as V, SkillReader as W, escapeXml as X, computeDigest as Y, isValidSkillName as Z, ProgressiveRouter as _, CapabilityApproval as a, verifyManifest as at, READ_SKILL_FILE_TOOL_NAME as b, FsMemoryStore as c, HookRunner as d, parseSkillMarkdown as et, InMemoryStore as f, OpenAiCompatibleClient as g, MockUiBridge as h, AgentLoop as i, validateSkills as it, networkUrlHost as j, isNetworkAllowed as k, FsRunSnapshotStore as l, MockLlmClient as m, ASK_USER_TOOL as n, renderCatalogJson as nt, EventBus as o, xmlRenderer as ot, MemoryArtifactStore as p, buildManifest as q, ASK_USER_TOOL_NAME as r, resolveInsideRoot as rt, FsArtifactStore as s, ASK_USER_INPUT_SCHEMA as t, renderAvailableSkillsXml as tt, FullDisclosureRouter as u, READ_SKILL_FILE_INPUT_SCHEMA as v, bridgeError as w, RUN_SNAPSHOT_SCHEMA_VERSION as x, READ_SKILL_FILE_TOOL as y, SKILLS_LOCKFILE as z };
2712
+ export { computeDigest as $, fromVercelStreamPart as A, MemoryFS as B, WebSkillRuntime as C, createWebSkillApi as D, createScriptContext as E, parseBridgeRequest as F, SKILL_PACK_FILE as G, SKILL_MANIFEST_FILE as H, resolveToolName as I, WebSkillError as J, SkillDiscovery as K, schemaToForm as L, mergeCatalogEntries as M, networkUrlHost as N, extractChartSpec as O, normalizeToolContent as P, checkSkillRules as Q, toLlmToolSpec as R, TraceRecorder as S, buildRenderResult as T, SKILL_NAME_MAX_LENGTH as U, SKILLS_LOCKFILE as V, SKILL_NAME_PATTERN as W, buildManifest as X, buildCatalog as Y, checkDependencyCycles as Z, ProgressiveRouter as _, CapabilityApproval as a, parseSkillMarkdown as at, READ_SKILL_FILE_TOOL_NAME as b, FsMemoryStore as c, renderCatalogJson as ct, HookRunner as d, verifyManifest as dt, escapeXml as et, InMemoryStore as f, xmlRenderer as ft, OpenAiCompatibleClient as g, MockUiBridge as h, AgentLoop as i, normalizePath as it, isNetworkAllowed as j, fromVercelResult as k, FsRunSnapshotStore as l, resolveInsideRoot as lt, MockLlmClient as m, ASK_USER_TOOL as n, isValidSkillName as nt, EventBus as o, parseSkillPackManifest as ot, MemoryArtifactStore as p, SkillReader as q, ASK_USER_TOOL_NAME as r, jsonRenderer as rt, FsArtifactStore as s, renderAvailableSkillsXml as st, ASK_USER_INPUT_SCHEMA as t, exportSkills as tt, FullDisclosureRouter as u, validateSkills as ut, READ_SKILL_FILE_INPUT_SCHEMA as v, bridgeError as w, RUN_SNAPSHOT_SCHEMA_VERSION as x, READ_SKILL_FILE_TOOL as y, toVercelToolSpecs as z };