@webskill/sdk 0.0.5 → 0.1.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.
@@ -1,502 +1,6 @@
1
- import { parse } from "yaml";
1
+ import { C as renderAvailableSkillsXml, E as validateSkills, T as resolveInsideRoot, c as SkillDiscovery, d as buildCatalog, l as SkillReader, t as MemoryArtifactStore, u as WebSkillError, x as parseSkillMarkdown } from "./memoryArtifactStore-C9lFVqPF-U8nXvqL9.js";
2
2
 
3
- //#region ../core/dist/index.js
4
- /** 所有公开 API 抛出的结构化错误,code 供上层可编程处理 */
5
- var WebSkillError = class extends Error {
6
- code;
7
- details;
8
- constructor(code, message, details) {
9
- super(message);
10
- this.name = "WebSkillError";
11
- this.code = code;
12
- this.details = details;
13
- }
14
- };
15
- /** 技能目录内的安装清单文件名(不参与 files 列表与 digest) */
16
- const SKILL_MANIFEST_FILE = "webskill.skill-manifest.json";
17
- /** managed root 下的锁定文件名(不参与 files 列表与 digest) */
18
- const SKILLS_LOCKFILE = "skills.lock.json";
19
- /** 整体 digest:逐文件 "<path>:<sha256>\n" 拼接串(文件按 path 排序)的 sha256(hash 由调用方提供,可为 WebCrypto 异步实现) */
20
- async function computeDigest(files, sha256) {
21
- return sha256([...files].sort((a, b) => a.path.localeCompare(b.path)).map((f) => `${f.path}:${f.sha256}\n`).join(""));
22
- }
23
- /** 由逐文件清单构建 manifest(digest 由 computeDigest 计算) */
24
- async function buildManifest(input) {
25
- return {
26
- schemaVersion: 1,
27
- name: input.name,
28
- ...input.version !== void 0 ? { version: input.version } : {},
29
- source: input.source,
30
- installedAt: input.installedAt,
31
- integrity: {
32
- algorithm: "sha256",
33
- digest: await computeDigest(input.files, input.sha256)
34
- },
35
- files: input.files
36
- };
37
- }
38
- /** 按 manifest.files 比对实际 hash(调用方负责计算 actualHashes) */
39
- function verifyManifest(manifest, actualHashes) {
40
- const mismatches = [];
41
- for (const file of manifest.files) {
42
- const actual = actualHashes.get(file.path);
43
- if (actual === void 0 || actual !== file.sha256) mismatches.push(file.path);
44
- }
45
- return {
46
- ok: mismatches.length === 0,
47
- mismatches
48
- };
49
- }
50
- const ROOT = "/";
51
- /** 内存文件系统:路径一律规范化为 `/` 分隔,写入自动创建父目录 */
52
- var MemoryFS = class {
53
- kind = "memory";
54
- #entries = /* @__PURE__ */ new Map();
55
- constructor() {
56
- this.#entries.set(ROOT, {
57
- type: "directory",
58
- content: /* @__PURE__ */ new Uint8Array(0),
59
- mtimeMs: Date.now()
60
- });
61
- }
62
- #normalize(path) {
63
- const out = [];
64
- for (const seg of path.replace(/\\/g, "/").split("/")) {
65
- if (seg === "" || seg === ".") continue;
66
- if (seg === "..") out.pop();
67
- else out.push(seg);
68
- }
69
- return ROOT + out.join("/");
70
- }
71
- #parentOf(key) {
72
- const i = key.lastIndexOf("/");
73
- return i <= 0 ? ROOT : key.slice(0, i);
74
- }
75
- #ensureParents(key) {
76
- const chain = [];
77
- let dir = this.#parentOf(key);
78
- while (!this.#entries.has(dir)) {
79
- chain.push(dir);
80
- dir = this.#parentOf(dir);
81
- }
82
- for (const d of chain.reverse()) this.#entries.set(d, {
83
- type: "directory",
84
- content: /* @__PURE__ */ new Uint8Array(0),
85
- mtimeMs: Date.now()
86
- });
87
- }
88
- #getFile(key) {
89
- const entry = this.#entries.get(key);
90
- if (!entry || entry.type !== "file") throw new WebSkillError("FS_NOT_FOUND", `File not found: ${key}`);
91
- return entry;
92
- }
93
- #getDirectory(key) {
94
- const entry = this.#entries.get(key);
95
- if (!entry || entry.type !== "directory") throw new WebSkillError("FS_NOT_FOUND", `Directory not found: ${key}`);
96
- return entry;
97
- }
98
- async readText(path) {
99
- return new TextDecoder().decode(this.#getFile(this.#normalize(path)).content);
100
- }
101
- async writeText(path, content) {
102
- await this.writeBinary(path, new TextEncoder().encode(content));
103
- }
104
- async readBinary(path) {
105
- return this.#getFile(this.#normalize(path)).content;
106
- }
107
- async writeBinary(path, content) {
108
- const key = this.#normalize(path);
109
- this.#ensureParents(key);
110
- this.#entries.set(key, {
111
- type: "file",
112
- content,
113
- mtimeMs: Date.now()
114
- });
115
- }
116
- async exists(path) {
117
- return this.#entries.has(this.#normalize(path));
118
- }
119
- async stat(path) {
120
- const key = this.#normalize(path);
121
- const entry = this.#entries.get(key);
122
- if (!entry) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
123
- return {
124
- path: key,
125
- type: entry.type,
126
- size: entry.type === "file" ? entry.content.length : void 0,
127
- mtimeMs: entry.mtimeMs
128
- };
129
- }
130
- async list(path) {
131
- const key = this.#normalize(path);
132
- this.#getDirectory(key);
133
- const prefix = key === ROOT ? ROOT : key + "/";
134
- const out = [];
135
- for (const [k, entry] of this.#entries) {
136
- if (k === key || !k.startsWith(prefix)) continue;
137
- if (k.slice(prefix.length).includes("/")) continue;
138
- out.push({
139
- path: k,
140
- type: entry.type,
141
- size: entry.type === "file" ? entry.content.length : void 0,
142
- mtimeMs: entry.mtimeMs
143
- });
144
- }
145
- return out;
146
- }
147
- async mkdir(path) {
148
- const key = this.#normalize(path);
149
- if (this.#entries.has(key)) return;
150
- this.#ensureParents(key);
151
- this.#entries.set(key, {
152
- type: "directory",
153
- content: /* @__PURE__ */ new Uint8Array(0),
154
- mtimeMs: Date.now()
155
- });
156
- }
157
- async remove(path, options) {
158
- const key = this.#normalize(path);
159
- if (!this.#entries.get(key)) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
160
- if (key === ROOT) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", "Cannot delete root directory");
161
- const prefix = key + "/";
162
- const descendants = [...this.#entries.keys()].filter((k) => k.startsWith(prefix));
163
- if (descendants.length > 0 && !options?.recursive) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Directory not empty, use recursive: ${key}`);
164
- for (const k of descendants) this.#entries.delete(k);
165
- this.#entries.delete(key);
166
- }
167
- };
168
- /** 统一分隔符为 `/`,去除 `.` 段与重复分隔符(不解析 `..`) */
169
- function normalizePath(path) {
170
- return path.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
171
- }
172
- /**
173
- * 将相对路径安全地解析到 root 之内。
174
- * 拒绝空路径、绝对路径(`/`、`\\`、`C:\` 形式)与任何 `..` 段。
175
- */
176
- function resolveInsideRoot(root, relativePath) {
177
- const fail = (reason) => {
178
- throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path outside root: ${reason} (path: ${JSON.stringify(relativePath)})`);
179
- };
180
- if (relativePath.trim() === "") fail("empty path");
181
- if (relativePath.startsWith("/") || relativePath.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(relativePath)) fail("absolute paths not allowed");
182
- const segments = relativePath.replace(/\\/g, "/").split("/");
183
- if (segments.some((s) => s === "..")) fail("`..` segments not allowed");
184
- const clean = segments.filter((s) => s !== "" && s !== ".");
185
- if (clean.length === 0) fail("path resolves to empty");
186
- const normalizedRoot = normalizePath(root);
187
- const joined = normalizedRoot === "" ? clean.join("/") : `${normalizedRoot}/${clean.join("/")}`;
188
- return root.startsWith("/") || root.startsWith("\\") ? `/${joined}` : joined;
189
- }
190
- const FRONTMATTER_RE = /^---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---[^\S\r\n]*(?:\r?\n|$)([\s\S]*)$/;
191
- const invalid = (message, details) => {
192
- throw new WebSkillError("SKILL_INVALID_METADATA", message, details);
193
- };
194
- function parseSkillMarkdown(raw) {
195
- const match = FRONTMATTER_RE.exec(raw);
196
- if (!match) invalid("SKILL.md missing YAML frontmatter (must start with a `---` line)");
197
- const [, yamlText, bodyRaw] = match;
198
- let data;
199
- try {
200
- data = parse(yamlText);
201
- } catch (cause) {
202
- invalid("frontmatter YAML parse failed", cause);
203
- }
204
- if (typeof data !== "object" || data === null || Array.isArray(data)) invalid("frontmatter must be a YAML object");
205
- const metadata = data;
206
- if (typeof metadata["name"] !== "string" || metadata["name"].trim() === "") invalid("frontmatter missing required `name` field");
207
- if (typeof metadata["description"] !== "string" || metadata["description"].trim() === "") invalid("frontmatter missing required `description` field");
208
- return {
209
- metadata,
210
- body: bodyRaw.trim()
211
- };
212
- }
213
- const SKILL_NAME_MAX_LENGTH = 64;
214
- /** Agent Skills 官方命名规范:小写字母/数字/连字符,不允许首尾或连续连字符 */
215
- const SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
216
- function isValidSkillName(name) {
217
- return name.length <= 64 && SKILL_NAME_PATTERN.test(name);
218
- }
219
- const SCRIPT_EXTENSION_RE = /\.(ts|js)$/;
220
- /** D2 schema sidecar(scripts/<name>.schema.json)属于协议内文件,不算非法脚本 */
221
- const SCHEMA_SIDECAR_RE = /\.schema\.json$/;
222
- /**
223
- * 技能合规规则的唯一实现。SkillDiscovery 与 validateSkills 都必须调用本函数,
224
- * 禁止各自重复实现规则(避免两套规则漂移)。
225
- */
226
- function checkSkillRules(input) {
227
- const { dirName } = input;
228
- if (!input.hasSkillMd) return [{
229
- code: "SKILL_NOT_FOUND",
230
- severity: "warning",
231
- message: `Directory ${dirName} missing SKILL.md, not treated as a skill, skipped`
232
- }];
233
- if (input.parseError) return [{
234
- code: input.parseError.code,
235
- severity: "error",
236
- message: `SKILL.md parse failed for directory ${dirName}: ${input.parseError.message}`
237
- }];
238
- const issues = [];
239
- const metadata = input.metadata;
240
- if (metadata) {
241
- if (!isValidSkillName(metadata.name)) issues.push({
242
- code: "SKILL_INVALID_NAME",
243
- severity: "error",
244
- message: `Skill name ${JSON.stringify(metadata.name)} does not conform to naming convention (lowercase letters/digits/hyphens, ≤64 chars)`
245
- });
246
- if (metadata.name !== dirName) issues.push({
247
- code: "SKILL_INVALID_NAME",
248
- severity: "error",
249
- message: `Skill name ${JSON.stringify(metadata.name)} does not match directory name ${JSON.stringify(dirName)}`
250
- });
251
- if (input.existingNames?.has(metadata.name)) issues.push({
252
- code: "SKILL_DUPLICATE_NAME",
253
- severity: "error",
254
- message: `Duplicate skill name ${JSON.stringify(metadata.name)}`
255
- });
256
- }
257
- for (const fileName of input.scriptFileNames ?? []) if (!SCRIPT_EXTENSION_RE.test(fileName) && !SCHEMA_SIDECAR_RE.test(fileName)) issues.push({
258
- code: "SKILL_UNSUPPORTED_SCRIPT",
259
- severity: "error",
260
- message: `Unsupported script file in scripts/ of directory ${dirName}: ${fileName} (only .ts/.js and .schema.json sidecars allowed)`
261
- });
262
- const allowedTools = metadata?.["allowed-tools"];
263
- if (Array.isArray(allowedTools)) {
264
- const scriptNames = new Set((input.scriptFileNames ?? []).filter((f) => SCRIPT_EXTENSION_RE.test(f)).map((f) => f.replace(SCRIPT_EXTENSION_RE, "")));
265
- for (const entry of allowedTools) if (typeof entry === "string" && !scriptNames.has(entry)) issues.push({
266
- code: "SKILL_UNKNOWN_ALLOWED_TOOL",
267
- severity: "warning",
268
- message: `allowed-tools entry ${JSON.stringify(entry)} of directory ${dirName} does not match any script in scripts/`
269
- });
270
- }
271
- return issues;
272
- }
273
- /** 由条目列表构建 Catalog,保证按名称排序 */
274
- function buildCatalog(entries) {
275
- return { skills: [...entries].sort((a, b) => a.name.localeCompare(b.name)) };
276
- }
277
- /** 按技能名读取技能根目录内的文件,全部读取强制经过路径安全检查 */
278
- var SkillReader = class {
279
- #fs;
280
- #index;
281
- constructor(fs, index) {
282
- this.#fs = fs;
283
- this.#index = index;
284
- }
285
- #rootFor(skillName) {
286
- const root = this.#index.get(skillName);
287
- if (!root) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${skillName}`);
288
- return root;
289
- }
290
- async readSkillMarkdown(skillName) {
291
- return this.readSkillFile(skillName, "SKILL.md");
292
- }
293
- async readSkillFile(skillName, relativePath) {
294
- return this.#fs.readText(resolveInsideRoot(this.#rootFor(skillName), relativePath));
295
- }
296
- async readSkillBinary(skillName, relativePath) {
297
- return this.#fs.readBinary(resolveInsideRoot(this.#rootFor(skillName), relativePath));
298
- }
299
- };
300
- const baseName$1 = (path) => path.split("/").pop() ?? path;
301
- var SkillDiscovery = class {
302
- #fs;
303
- #roots;
304
- /** name → skillRoot,与 SkillReader 共享同一 Map 引用 */
305
- #index = /* @__PURE__ */ new Map();
306
- #reader;
307
- constructor(fs, roots) {
308
- this.#fs = fs;
309
- this.#roots = roots;
310
- this.#reader = new SkillReader(fs, this.#index);
311
- }
312
- async discover() {
313
- this.#index.clear();
314
- const entries = [];
315
- const issues = [];
316
- const claimedNames = /* @__PURE__ */ new Set();
317
- for (const root of this.#roots) {
318
- if (!await this.#fs.exists(root)) {
319
- issues.push({
320
- code: "FS_NOT_FOUND",
321
- severity: "warning",
322
- message: `Skill root directory not found, skipped: ${root}`,
323
- path: root
324
- });
325
- continue;
326
- }
327
- for (const child of await this.#fs.list(root)) {
328
- if (child.type !== "directory") continue;
329
- const dirName = baseName$1(child.path);
330
- const skillRoot = `${root}/${dirName}`;
331
- const skillMdPath = `${skillRoot}/SKILL.md`;
332
- const hasSkillMd = await this.#fs.exists(skillMdPath);
333
- let metadata;
334
- let parseError;
335
- if (hasSkillMd) try {
336
- metadata = parseSkillMarkdown(await this.#fs.readText(skillMdPath)).metadata;
337
- } catch (e) {
338
- parseError = e instanceof WebSkillError ? e : new WebSkillError("SKILL_INVALID_METADATA", String(e));
339
- }
340
- const scriptFileNames = await this.#listScriptFileNames(skillRoot);
341
- const candidateIssues = checkSkillRules({
342
- dirName,
343
- hasSkillMd,
344
- metadata,
345
- 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`)
364
- });
365
- }
366
- }
367
- entries.sort((a, b) => a.name.localeCompare(b.name));
368
- return {
369
- entries,
370
- issues
371
- };
372
- }
373
- async catalog() {
374
- const { entries, issues } = await this.discover();
375
- return {
376
- catalog: buildCatalog(entries),
377
- issues
378
- };
379
- }
380
- /** 按需加载技能完整文档;技能不存在时抛 SKILL_NOT_FOUND */
381
- async loadDocument(skillName) {
382
- if (this.#index.size === 0) await this.discover();
383
- const { metadata, body } = parseSkillMarkdown(await this.#reader.readSkillMarkdown(skillName));
384
- const skillRoot = this.#index.get(skillName);
385
- return {
386
- metadata,
387
- body,
388
- location: {
389
- skillRoot,
390
- skillMdPath: `${skillRoot}/SKILL.md`,
391
- source: "local"
392
- }
393
- };
394
- }
395
- async #listScriptFileNames(skillRoot) {
396
- const scriptsDir = `${skillRoot}/scripts`;
397
- 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));
399
- }
400
- };
401
- /**
402
- * 合规校验。内部直接复用 SkillDiscovery 的扫描逻辑,
403
- * 规则判定同样落在 checkSkillRules 单一来源上。
404
- */
405
- async function validateSkills(fs, roots) {
406
- const { issues } = await new SkillDiscovery(fs, roots).discover();
407
- return {
408
- ok: !issues.some((i) => i.severity === "error"),
409
- issues
410
- };
411
- }
412
- function renderCatalogJson(catalog) {
413
- return JSON.stringify(catalog, null, 2);
414
- }
415
- const jsonRenderer = {
416
- format: "json",
417
- render: renderCatalogJson
418
- };
419
- /** 转义 XML 五个特殊字符:& < > " ' */
420
- function escapeXml(text) {
421
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
422
- }
423
- function renderAvailableSkillsXml(catalog) {
424
- const lines = ["<available_skills>"];
425
- for (const skill of catalog.skills) {
426
- lines.push(" <skill>");
427
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
428
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
429
- if (skill.version !== void 0) lines.push(` <version>${escapeXml(skill.version)}</version>`);
430
- if (skill.license !== void 0) lines.push(` <license>${escapeXml(skill.license)}</license>`);
431
- lines.push(` <source>${escapeXml(skill.source)}</source>`);
432
- lines.push(" </skill>");
433
- }
434
- lines.push("</available_skills>");
435
- return lines.join("\n");
436
- }
437
- const xmlRenderer = {
438
- format: "xml",
439
- render: renderAvailableSkillsXml
440
- };
441
-
442
- //#endregion
443
3
  //#region ../runtime/dist/index.js
444
- /**
445
- * 预设响应序列的确定性 LlmClient;handler 形式可按会话状态动态应答。
446
- * 默认不含 stream(存量行为);构造传 { streaming: true } 启用流式:
447
- * 队列项 { stream: [...] } 按预设 delta 序列产出,LlmResponse 项自动合成增量。
448
- */
449
- var MockLlmClient = class {
450
- calls = [];
451
- stream;
452
- #queue;
453
- constructor(responses, options) {
454
- this.#queue = [...responses];
455
- if (options?.streaming) this.stream = (input) => this.#streamImpl(input);
456
- }
457
- async complete(input) {
458
- this.calls.push(input);
459
- const next = this.#queue.shift();
460
- if (!next) throw new WebSkillError("LLM_REQUEST_FAILED", "MockLlmClient: response queue exhausted");
461
- if (typeof next === "function") return next(input);
462
- if ("stream" in next) {
463
- const events = typeof next.stream === "function" ? next.stream(input) : next.stream;
464
- let content = "";
465
- let toolCalls;
466
- for (const event of events) if (event.type === "text-delta") content += event.delta;
467
- else if (event.type === "tool-calls") toolCalls = event.toolCalls;
468
- else if (event.type === "done" && event.content !== void 0) content = event.content;
469
- return {
470
- content,
471
- toolCalls
472
- };
473
- }
474
- return next;
475
- }
476
- async *#streamImpl(input) {
477
- this.calls.push(input);
478
- const next = this.#queue.shift();
479
- if (!next) throw new WebSkillError("LLM_REQUEST_FAILED", "MockLlmClient: response queue exhausted");
480
- if (typeof next !== "function" && "stream" in next) {
481
- const events = typeof next.stream === "function" ? next.stream(input) : next.stream;
482
- for (const event of events) yield event;
483
- return;
484
- }
485
- const response = typeof next === "function" ? await next(input) : next;
486
- if (response.content) {
487
- const chunkSize = Math.max(1, Math.ceil(response.content.length / 3));
488
- for (let i = 0; i < response.content.length; i += chunkSize) yield {
489
- type: "text-delta",
490
- delta: response.content.slice(i, i + chunkSize)
491
- };
492
- }
493
- if (response.toolCalls?.length) yield {
494
- type: "tool-calls",
495
- toolCalls: response.toolCalls
496
- };
497
- yield { type: "done" };
498
- }
499
- };
500
4
  const toOpenAiMessage = (msg) => {
501
5
  if (msg.role === "tool") return {
502
6
  role: "tool",
@@ -776,7 +280,7 @@ var FullDisclosureRouter = class {
776
280
  }
777
281
  async route(catalog) {
778
282
  const sections = [];
779
- for (const entry of catalog.skills) {
283
+ for (const entry of catalog.entries) {
780
284
  const doc = await this.#discovery.loadDocument(entry.name);
781
285
  sections.push(`<skill name="${entry.name}">\n${doc.body}\n</skill>`);
782
286
  }
@@ -941,12 +445,42 @@ function createScriptContext(deps) {
941
445
  ...onWarning ? { onWarning } : {}
942
446
  };
943
447
  }
448
+ const isRecord$1 = (v) => typeof v === "object" && v !== null;
449
+ /**
450
+ * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
451
+ * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
452
+ */
453
+ function extractChartSpec(data) {
454
+ if (!isRecord$1(data)) return void 0;
455
+ const raw = data["$chart"];
456
+ if (!isRecord$1(raw)) return void 0;
457
+ const { kind, labels, series } = raw;
458
+ if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
459
+ if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
460
+ if (!Array.isArray(series)) return void 0;
461
+ const validSeries = [];
462
+ for (const item of series) {
463
+ if (!isRecord$1(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
464
+ const name = item["name"];
465
+ validSeries.push({
466
+ ...typeof name === "string" ? { name } : {},
467
+ data: item["data"]
468
+ });
469
+ }
470
+ const title = raw["title"];
471
+ return {
472
+ kind,
473
+ labels,
474
+ series: validSeries,
475
+ ...typeof title === "string" ? { title } : {}
476
+ };
477
+ }
944
478
  /**
945
- * 默认的结果渲染构造:LLM 最终输出 markdown block,
946
- * run.artifacts → file blockssummary 取 terminationReason。
479
+ * 默认的结果渲染构造:run 内收集的 renderBlocks(chart 等)在前,
480
+ * LLM 最终输出 → markdown block,run.artifacts → file blocks 在后;summary 取 terminationReason。
947
481
  */
948
- function buildRenderResult(run, output) {
949
- const blocks = [];
482
+ function buildRenderResult(run, output, renderBlocks = []) {
483
+ const blocks = [...renderBlocks];
950
484
  if (output.trim() !== "") blocks.push({
951
485
  type: "markdown",
952
486
  text: output
@@ -1000,33 +534,6 @@ function mapFieldType(prop) {
1000
534
  default: return "textarea";
1001
535
  }
1002
536
  }
1003
- /**
1004
- * 测试用 UiBridge:按请求 id 或类型预设应答(永不 resolve 的 handler 可模拟超时);
1005
- * 记录全部请求供断言。
1006
- */
1007
- var MockUiBridge = class {
1008
- requests = [];
1009
- #answers = /* @__PURE__ */ new Map();
1010
- #fallback;
1011
- /** key 为请求 id 或请求类型('ask' | 'confirm' | 'form' | 'select') */
1012
- respondTo(key, answer) {
1013
- this.#answers.set(key, answer);
1014
- return this;
1015
- }
1016
- setFallback(answer) {
1017
- this.#fallback = answer;
1018
- return this;
1019
- }
1020
- async request(input) {
1021
- this.requests.push(input);
1022
- const answer = this.#answers.get(input.id) ?? this.#answers.get(input.type) ?? this.#fallback;
1023
- if (!answer) return {
1024
- id: input.id,
1025
- cancelled: true
1026
- };
1027
- return typeof answer === "function" ? answer(input) : answer;
1028
- }
1029
- };
1030
537
  /** 生命周期事件总线:只读观测,支持按阶段或通配订阅 */
1031
538
  var EventBus = class {
1032
539
  #listeners = /* @__PURE__ */ new Map();
@@ -1053,408 +560,56 @@ var EventBus = class {
1053
560
  for (const listener of this.#listeners.get("*") ?? []) listener(frozen);
1054
561
  }
1055
562
  };
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;
563
+ /** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
564
+ var TraceRecorder = class {
565
+ #runId;
566
+ #clock;
567
+ #events = [];
568
+ #seq = 0;
569
+ constructor(runId, clock = {}) {
570
+ this.#runId = runId;
571
+ this.#clock = clock;
1074
572
  }
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
- }
573
+ record(type, opts) {
574
+ const event = {
575
+ id: this.#clock.createId?.() ?? `evt-${++this.#seq}`,
576
+ runId: this.#runId,
577
+ ts: this.#clock.now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
578
+ type,
579
+ ...opts?.message !== void 0 ? { message: opts.message } : {},
580
+ ...opts?.data !== void 0 ? { data: opts.data } : {}
581
+ };
582
+ this.#events.push(event);
583
+ return event;
1083
584
  }
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
- });
585
+ list() {
586
+ return [...this.#events];
1095
587
  }
1096
588
  };
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);
589
+ /** 交互终态(取消/超时):从工具执行深处直接终止 run */
590
+ var RunTerminated = class extends Error {
591
+ outcome;
592
+ constructor(outcome) {
593
+ super(outcome.message);
594
+ this.outcome = outcome;
1123
595
  }
1124
596
  };
1125
- const encode = (s) => encodeURIComponent(s);
597
+ /** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
598
+ var BridgeRequestError = class extends Error {};
599
+ const baseName = (p) => p.split("/").pop() ?? p;
600
+ const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
601
+ const toolError = (code, message) => ({
602
+ ok: false,
603
+ content: [],
604
+ error: {
605
+ code,
606
+ message
607
+ }
608
+ });
1126
609
  /**
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
- /** 内存 ArtifactStore:测试与浏览器降级用;索引随进程生命周期存在 */
1182
- var MemoryArtifactStore = class {
1183
- #byRun = /* @__PURE__ */ new Map();
1184
- #seq = 0;
1185
- async createTextArtifact(input) {
1186
- return this.#record({
1187
- runId: input.runId,
1188
- path: input.path,
1189
- type: "text",
1190
- size: new TextEncoder().encode(input.content).length,
1191
- mimeType: input.mimeType,
1192
- metadata: input.metadata
1193
- });
1194
- }
1195
- async createBinaryArtifact(input) {
1196
- return this.#record({
1197
- runId: input.runId,
1198
- path: input.path,
1199
- type: "binary",
1200
- size: input.content.length,
1201
- mimeType: input.mimeType,
1202
- metadata: input.metadata
1203
- });
1204
- }
1205
- async listArtifacts(runId) {
1206
- return [...this.#byRun.get(runId) ?? []];
1207
- }
1208
- #record(input) {
1209
- const artifact = {
1210
- id: `art-${++this.#seq}`,
1211
- runId: input.runId,
1212
- path: input.path,
1213
- type: input.type,
1214
- mimeType: input.mimeType,
1215
- size: input.size,
1216
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1217
- metadata: input.metadata
1218
- };
1219
- const list = this.#byRun.get(input.runId) ?? [];
1220
- list.push(artifact);
1221
- this.#byRun.set(input.runId, list);
1222
- return artifact;
1223
- }
1224
- };
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
- });
1254
- }
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 ?? [];
1260
- }
1261
- #artifactPath(runId, artifactPath) {
1262
- return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
1263
- }
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;
1440
- }
1441
- };
1442
- /** bridge.request 自身异常(非超时):恢复 running 后转为工具错误回喂 */
1443
- var BridgeRequestError = class extends Error {};
1444
- const baseName = (p) => p.split("/").pop() ?? p;
1445
- const messageOf = (e) => e instanceof Error ? e.message : String(e);
1446
- const toolError = (code, message) => ({
1447
- ok: false,
1448
- content: [],
1449
- error: {
1450
- code,
1451
- message
1452
- }
1453
- });
1454
- /**
1455
- * 多轮 Agent 循环。
1456
- * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
1457
- * 缺参/确认/提问经 UiBridge 行内 await 暂停恢复;取消/超时/LLM 异常/护栏超限才终止 run。
610
+ * 多轮 Agent 循环。
611
+ * 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
612
+ * 缺参/确认/提问经 UiBridge 行内 await 暂停恢复;取消/超时/LLM 异常/护栏超限才终止 run
1458
613
  */
1459
614
  var AgentLoop = class {
1460
615
  #deps;
@@ -1507,23 +662,24 @@ var AgentLoop = class {
1507
662
  now,
1508
663
  interactionSeq: 0,
1509
664
  messages: [],
1510
- turn: 0
665
+ turn: 0,
666
+ renderBlocks: []
1511
667
  };
1512
668
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1513
669
  const route = this.#deps.catalogFilter ? {
1514
670
  ...input.route,
1515
- catalog: { skills: await this.#deps.catalogFilter(input.route.catalog.skills) }
671
+ catalog: { entries: await this.#deps.catalogFilter(input.route.catalog.entries) }
1516
672
  } : input.route;
1517
673
  trace.record("skill.routed", { data: {
1518
674
  strategy: route.strategy,
1519
- skillCount: route.catalog.skills.length
675
+ skillCount: route.catalog.entries.length
1520
676
  } });
1521
677
  await this.#lifecycle("route", state, { strategy: route.strategy });
1522
678
  const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
1523
679
  try {
1524
680
  return await source.listToolSpecs();
1525
681
  } catch (e) {
1526
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
682
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf$1(e)}` });
1527
683
  return [];
1528
684
  }
1529
685
  }))).flat();
@@ -1537,7 +693,7 @@ var AgentLoop = class {
1537
693
  try {
1538
694
  return await this.#turnLoop(state, startMs, 1, externalSpecs);
1539
695
  } catch (e) {
1540
- if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
696
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1541
697
  throw e;
1542
698
  }
1543
699
  }
@@ -1576,7 +732,7 @@ var AgentLoop = class {
1576
732
  });
1577
733
  } catch (e) {
1578
734
  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);
735
+ return finish("failed", "llm-error", messageOf$1(e), code);
1580
736
  }
1581
737
  trace.record("llm.response", { data: {
1582
738
  turn,
@@ -1628,21 +784,21 @@ var AgentLoop = class {
1628
784
  });
1629
785
  const bridge = this.#deps.uiBridge;
1630
786
  if (status === "completed" && bridge?.renderResult && this.#config.renderResult !== "off") try {
1631
- const request = buildRenderResult(run, output);
787
+ const request = buildRenderResult(run, output, state.renderBlocks);
1632
788
  await bridge.renderResult(request);
1633
789
  trace.record("ui.rendered", { data: { blockCount: request.blocks.length } });
1634
790
  } catch (e) {
1635
- trace.record("run.warning", { message: `renderResult failed: ${messageOf(e)}` });
791
+ trace.record("run.warning", { message: `renderResult failed: ${messageOf$1(e)}` });
1636
792
  }
1637
793
  if (this.#deps.snapshotStore) try {
1638
794
  await this.#deps.snapshotStore.delete(state.runId);
1639
795
  } catch (e) {
1640
- trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
796
+ trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf$1(e)}` });
1641
797
  }
1642
798
  try {
1643
799
  await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
1644
800
  } catch (e) {
1645
- trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
801
+ trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf$1(e)}` });
1646
802
  }
1647
803
  run.trace = trace.list();
1648
804
  return {
@@ -1677,7 +833,7 @@ var AgentLoop = class {
1677
833
  try {
1678
834
  await store.save(snapshot);
1679
835
  } catch (e) {
1680
- state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf(e)}` });
836
+ state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf$1(e)}` });
1681
837
  }
1682
838
  }
1683
839
  /**
@@ -1710,7 +866,8 @@ var AgentLoop = class {
1710
866
  now,
1711
867
  interactionSeq: 0,
1712
868
  messages: snapshot.messages.map((m) => ({ ...m })),
1713
- turn: snapshot.turn
869
+ turn: snapshot.turn,
870
+ renderBlocks: []
1714
871
  };
1715
872
  if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
1716
873
  trace.record("run.resumed", { data: {
@@ -1776,14 +933,14 @@ var AgentLoop = class {
1776
933
  try {
1777
934
  return await source.listToolSpecs();
1778
935
  } catch (e) {
1779
- trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
936
+ trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf$1(e)}` });
1780
937
  return [];
1781
938
  }
1782
939
  }))).flat();
1783
940
  try {
1784
941
  return await this.#turnLoop(state, startMs, snapshot.turn + 1, externalSpecs);
1785
942
  } catch (e) {
1786
- if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
943
+ if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
1787
944
  throw e;
1788
945
  }
1789
946
  }
@@ -1875,8 +1032,8 @@ var AgentLoop = class {
1875
1032
  message: e.message,
1876
1033
  code: "RUN_INTERACTION_TIMEOUT"
1877
1034
  });
1878
- state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf(e)}` });
1879
- throw new BridgeRequestError(messageOf(e));
1035
+ state.trace.record("run.warning", { message: `UiBridge request failed: ${messageOf$1(e)}` });
1036
+ throw new BridgeRequestError(messageOf$1(e));
1880
1037
  }
1881
1038
  if (response.cancelled) throw new RunTerminated({
1882
1039
  status: "cancelled",
@@ -1922,11 +1079,20 @@ var AgentLoop = class {
1922
1079
  result = source ? await source.call(call.name, call.arguments) : toolError(resolution.code, resolution.message);
1923
1080
  }
1924
1081
  }
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", {
1082
+ if (result.ok) {
1083
+ state.trace.record("tool.completed", { data: {
1084
+ name: call.name,
1085
+ callId: call.id
1086
+ } });
1087
+ for (const item of result.content) {
1088
+ if (item.type !== "json") continue;
1089
+ const chart = extractChartSpec(item.data);
1090
+ if (chart) state.renderBlocks.push({
1091
+ type: "chart",
1092
+ chart
1093
+ });
1094
+ }
1095
+ } else state.trace.record("tool.failed", {
1930
1096
  message: result.error?.message,
1931
1097
  data: {
1932
1098
  name: call.name,
@@ -1980,7 +1146,7 @@ var AgentLoop = class {
1980
1146
  };
1981
1147
  } catch (e) {
1982
1148
  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)}`);
1149
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `read_skill_file failed: ${messageOf$1(e)}`);
1984
1150
  }
1985
1151
  }
1986
1152
  /** 经外部技能提供者读取技能文档/资源;skillKey 为技能名或 mcp:// URI */
@@ -2023,411 +1189,794 @@ var AgentLoop = class {
2023
1189
  }
2024
1190
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}`);
2025
1191
  }
2026
- /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用 */
2027
- async #activateSkill(skillName, state) {
1192
+ /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
1193
+ async #activateSkill(skillName, state, via) {
2028
1194
  state.activated.add(skillName);
2029
- state.trace.record("skill.activated", { data: { skillName } });
1195
+ state.trace.record("skill.activated", { data: {
1196
+ skillName,
1197
+ ...via ? { via } : {}
1198
+ } });
2030
1199
  await this.#lifecycle("activate", state, { skillName });
2031
1200
  await this.#writeActivationMemory(skillName, state);
2032
1201
  const root = this.#deps.skillIndex.get(skillName);
2033
1202
  if (!root) return "";
2034
- const executor = this.#deps.executor;
2035
- if (!executor) return "";
2036
1203
  let allowedTools;
1204
+ let dependencies = [];
2037
1205
  try {
2038
1206
  const { metadata } = parseSkillMarkdown(await this.#deps.fs.readText(`${root}/SKILL.md`));
1207
+ const rawDeps = metadata["dependencies"];
1208
+ if (Array.isArray(rawDeps)) dependencies = rawDeps.filter((d) => typeof d === "string");
2039
1209
  const raw = metadata["allowed-tools"];
2040
1210
  if (raw !== void 0) if (Array.isArray(raw)) allowedTools = raw.filter((e) => typeof e === "string");
2041
1211
  else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
2042
1212
  } catch {}
1213
+ const executor = this.#deps.executor;
2043
1214
  const loaded = [];
2044
- let scriptFiles = [];
1215
+ if (executor) {
1216
+ let scriptFiles = [];
1217
+ try {
1218
+ scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
1219
+ } catch {
1220
+ scriptFiles = [];
1221
+ }
1222
+ for (const file of scriptFiles) {
1223
+ const match = /^(.*)\.(ts|js)$/.exec(file);
1224
+ if (!match?.[1]) continue;
1225
+ if (allowedTools && !allowedTools.includes(match[1])) continue;
1226
+ try {
1227
+ const def = await executor.loadDefinition(root, match[1]);
1228
+ await this.#enrichDefinition(root, match[1], def, state);
1229
+ state.activatedTools.set(def.name, def);
1230
+ loaded.push(def.name);
1231
+ } catch {}
1232
+ }
1233
+ }
1234
+ let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
1235
+ for (const dep of dependencies) {
1236
+ if (state.activated.has(dep)) continue;
1237
+ if (!this.#deps.skillIndex.has(dep)) {
1238
+ state.trace.record("run.warning", { message: `Skill "${skillName}" depends on unknown skill "${dep}"; skipped` });
1239
+ continue;
1240
+ }
1241
+ note += await this.#activateSkill(dep, state, skillName);
1242
+ }
1243
+ return note;
1244
+ }
1245
+ /**
1246
+ * D2 Schema 兜底链(同一 run 内激活时只算一次):
1247
+ * 显式 inputSchema > sidecar scripts/<name>.schema.json > schemaInferer 推导 > schemaUnavailable
1248
+ */
1249
+ async #enrichDefinition(skillRoot, scriptName, def, state) {
1250
+ if (def.inputSchema) return;
1251
+ const sidecar = `${skillRoot}/scripts/${scriptName}.schema.json`;
1252
+ if (await this.#deps.fs.exists(sidecar)) try {
1253
+ def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
1254
+ return;
1255
+ } catch (e) {
1256
+ state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf$1(e)}` });
1257
+ }
1258
+ if (!this.#deps.schemaInferer) return;
1259
+ for (const ext of ["ts", "js"]) {
1260
+ const scriptPath = `${skillRoot}/scripts/${scriptName}.${ext}`;
1261
+ if (!await this.#deps.fs.exists(scriptPath)) continue;
1262
+ const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
1263
+ if (inferred) def.inputSchema = inferred;
1264
+ return;
1265
+ }
1266
+ }
1267
+ async #handleScriptTool(call, skillName, scriptName, state) {
1268
+ const root = this.#deps.skillIndex.get(skillName);
1269
+ if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
1270
+ if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
1271
+ const def = state.activatedTools.get(call.name);
1272
+ if (!def) return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available for skill "${skillName}"`);
1273
+ let args = call.arguments;
1274
+ const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
1275
+ if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
1276
+ const value = await this.#interact(state, {
1277
+ type: "form",
1278
+ id: this.#nextInteractionId(state),
1279
+ title: `Missing parameters for ${call.name}`,
1280
+ fields: schemaToForm(def.inputSchema, args)
1281
+ }, {
1282
+ tool: call.name,
1283
+ missing
1284
+ });
1285
+ if (typeof value === "object" && value !== null) args = {
1286
+ ...args,
1287
+ ...value
1288
+ };
1289
+ } catch (e) {
1290
+ if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
1291
+ throw e;
1292
+ }
1293
+ const context = createScriptContext({
1294
+ fs: this.#deps.fs,
1295
+ artifactStore: this.#deps.artifactStore,
1296
+ skillName,
1297
+ skillRoot: root,
1298
+ runId: state.runId,
1299
+ confirm: (message) => this.#confirm(message, state),
1300
+ onWarning: (message) => state.trace.record("run.warning", { message })
1301
+ });
1302
+ try {
1303
+ const before = new Set((await this.#deps.artifactStore.listArtifacts(state.runId)).map((a) => a.id));
1304
+ const result = await this.#deps.executor.execute({
1305
+ skillRoot: root,
1306
+ scriptName,
1307
+ args,
1308
+ context,
1309
+ timeoutMs: state.toolTimeoutMs
1310
+ });
1311
+ const fresh = (await this.#deps.artifactStore.listArtifacts(state.runId)).filter((a) => !before.has(a.id));
1312
+ if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
1313
+ await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
1314
+ return result;
1315
+ } catch (e) {
1316
+ if (e instanceof RunTerminated) throw e;
1317
+ await this.#bumpSkillStat(skillName, "failures", state);
1318
+ return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf$1(e)}`);
1319
+ }
1320
+ }
1321
+ /** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
1322
+ async #confirm(message, state) {
1323
+ if (this.#policy.confirmations === "auto-approve") return true;
1324
+ if (!this.#deps.uiBridge) {
1325
+ state.trace.record("run.warning", { message: "No UiBridge configured; confirm() auto-approved" });
1326
+ return true;
1327
+ }
1328
+ try {
1329
+ return await this.#interact(state, {
1330
+ type: "confirm",
1331
+ id: this.#nextInteractionId(state),
1332
+ message,
1333
+ defaultValue: true
1334
+ }, { kind: "confirm" }) !== false;
1335
+ } catch (e) {
1336
+ if (e instanceof BridgeRequestError) {
1337
+ state.trace.record("run.warning", { message: `confirm() bridge failed, auto-approved: ${e.message}` });
1338
+ return true;
1339
+ }
1340
+ throw e;
1341
+ }
1342
+ }
1343
+ #nextInteractionId(state) {
1344
+ return `int-${++state.interactionSeq}`;
1345
+ }
1346
+ async #memoryGet(scope, key) {
1347
+ try {
1348
+ return await this.#deps.memory?.get(scope, key);
1349
+ } catch {
1350
+ return;
1351
+ }
1352
+ }
1353
+ async #memorySet(scope, key, value, state) {
1354
+ try {
1355
+ await this.#deps.memory?.set(scope, key, value);
1356
+ } catch (e) {
1357
+ state.trace.record("run.warning", { message: `Memory write failed: ${messageOf$1(e)}` });
1358
+ }
1359
+ }
1360
+ async #writeActivationMemory(skillName, state) {
1361
+ if (!this.#deps.memory) return;
1362
+ const sessionScope = `session:${state.run.sessionId}`;
1363
+ await this.#memorySet(sessionScope, "activeSkillNames", [...state.activated].sort(), state);
1364
+ await this.#bumpSkillStat(skillName, "activations", state);
1365
+ if (this.#deps.longTerm?.enabled) {
1366
+ const userScope = `user:${this.#deps.longTerm.userId}`;
1367
+ const usage = await this.#memoryGet(userScope, "skillUsage") ?? {};
1368
+ usage[skillName] = (usage[skillName] ?? 0) + 1;
1369
+ await this.#memorySet(userScope, "skillUsage", usage, state);
1370
+ }
1371
+ }
1372
+ async #bumpSkillStat(skillName, field, state) {
1373
+ if (!this.#deps.memory) return;
1374
+ const scope = `skill:${skillName}`;
1375
+ const stats = await this.#memoryGet(scope, "stats") ?? {
1376
+ activations: 0,
1377
+ successes: 0,
1378
+ failures: 0
1379
+ };
1380
+ stats[field] = (stats[field] ?? 0) + 1;
1381
+ await this.#memorySet(scope, "stats", stats, state);
1382
+ }
1383
+ async #appendParamHistory(state, request, value) {
1384
+ if (!this.#deps.memory) return;
1385
+ const scope = `session:${state.run.sessionId}`;
1386
+ const history = await this.#memoryGet(scope, "paramHistory") ?? [];
1387
+ history.push({
1388
+ ts: state.now(),
1389
+ interactionId: request.id,
1390
+ type: request.type,
1391
+ value
1392
+ });
1393
+ await this.#memorySet(scope, "paramHistory", history, state);
1394
+ }
1395
+ };
1396
+ /**
1397
+ * Catalog 合并:同名本地优先;外部条目的 mcp:// 根 URI 保留,
1398
+ * 调用方可凭 URI 强制选择外部技能(绕过本地优先)。
1399
+ */
1400
+ function mergeCatalogEntries(localEntries, providerEntries) {
1401
+ const byName = new Map(localEntries.map((e) => [e.name, e]));
1402
+ for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
1403
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
1404
+ }
1405
+ const RUN_SNAPSHOT_SCHEMA_VERSION = 1;
1406
+ const SNAPSHOT_SUFFIX = ".snapshot.json";
1407
+ /**
1408
+ * FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
1409
+ * 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
1410
+ * @experimental
1411
+ */
1412
+ var FsRunSnapshotStore = class {
1413
+ #root;
1414
+ #fs;
1415
+ constructor(deps) {
1416
+ this.#root = deps.root.replace(/\/+$/, "");
1417
+ this.#fs = deps.fs;
1418
+ }
1419
+ #path(runId) {
1420
+ return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
1421
+ }
1422
+ async save(snapshot) {
1423
+ await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
1424
+ }
1425
+ async load(runId) {
1426
+ const path = this.#path(runId);
1427
+ if (!await this.#fs.exists(path)) return void 0;
1428
+ let parsed;
2045
1429
  try {
2046
- scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
2047
- } catch {
2048
- return "";
1430
+ parsed = JSON.parse(await this.#fs.readText(path));
1431
+ } catch (e) {
1432
+ await this.#fs.remove(path).catch(() => void 0);
1433
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
1434
+ }
1435
+ const snapshot = parsed;
1436
+ if (typeof snapshot !== "object" || snapshot === null || snapshot.runId !== runId) {
1437
+ await this.#fs.remove(path).catch(() => void 0);
1438
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
2049
1439
  }
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;
1440
+ return snapshot;
1441
+ }
1442
+ async delete(runId) {
1443
+ const path = this.#path(runId);
1444
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
1445
+ }
1446
+ async list() {
1447
+ if (!await this.#fs.exists(this.#root)) return [];
1448
+ const out = [];
1449
+ for (const entry of await this.#fs.list(this.#root)) {
1450
+ if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
2054
1451
  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);
1452
+ out.push(JSON.parse(await this.#fs.readText(entry.path)));
2059
1453
  } catch {}
2060
1454
  }
2061
- return loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
1455
+ return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
1456
+ }
1457
+ };
1458
+ /**
1459
+ * runtime 门面:组合 discovery / router / agent loop / lifecycle
1460
+ * @stable
1461
+ */
1462
+ var WebSkillRuntime = class {
1463
+ #deps;
1464
+ #discovery;
1465
+ #router;
1466
+ #session;
1467
+ #events;
1468
+ #catalogCache;
1469
+ constructor(deps) {
1470
+ this.#deps = deps;
1471
+ this.#discovery = new SkillDiscovery(deps.fs, deps.roots);
1472
+ this.#router = deps.router ?? new ProgressiveRouter();
1473
+ this.#events = deps.eventBus ?? new EventBus();
1474
+ this.#session = {
1475
+ id: `session-${Math.random().toString(36).slice(2, 10)}`,
1476
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1477
+ };
1478
+ }
1479
+ get session() {
1480
+ return this.#session;
1481
+ }
1482
+ /** 生命周期事件总线(只读观测) */
1483
+ get events() {
1484
+ return this.#events;
1485
+ }
1486
+ async discover() {
1487
+ const result = await this.#discovery.discover();
1488
+ this.#catalogCache = {
1489
+ catalog: buildCatalog(result.entries),
1490
+ index: new Map(result.entries.map((e) => [e.name, e.root]))
1491
+ };
1492
+ return result;
1493
+ }
1494
+ async run(userPrompt) {
1495
+ if (!this.#catalogCache) await this.discover();
1496
+ const cache = this.#catalogCache;
1497
+ if (!cache) throw new Error("discover() did not populate the catalog cache");
1498
+ const providerEntries = (await Promise.all((this.#deps.skillProviders ?? []).map(async (p) => {
1499
+ try {
1500
+ return await p.listSkills();
1501
+ } catch {
1502
+ return [];
1503
+ }
1504
+ }))).flat();
1505
+ const catalog = providerEntries.length > 0 ? { entries: mergeCatalogEntries(cache.catalog.entries, providerEntries) } : cache.catalog;
1506
+ const filteredCatalog = this.#deps.catalogFilter ? { entries: await this.#deps.catalogFilter(catalog.entries) } : catalog;
1507
+ const route = await this.#router.route(filteredCatalog);
1508
+ const result = await new AgentLoop({
1509
+ llm: this.#deps.llm,
1510
+ executor: this.#deps.executor,
1511
+ schemaInferer: this.#deps.schemaInferer,
1512
+ artifactStore: this.#deps.artifactStore,
1513
+ fs: this.#deps.fs,
1514
+ skillIndex: cache.index,
1515
+ model: this.#deps.model,
1516
+ uiBridge: this.#deps.uiBridge,
1517
+ interaction: this.#deps.interaction,
1518
+ memory: this.#deps.memory,
1519
+ hooks: this.#deps.hooks,
1520
+ eventBus: this.#events,
1521
+ longTerm: this.#deps.longTerm,
1522
+ externalTools: this.#deps.externalTools,
1523
+ skillProviders: this.#deps.skillProviders,
1524
+ catalogFilter: this.#deps.catalogFilter,
1525
+ snapshotStore: this.#deps.snapshotStore
1526
+ }, this.#deps.config).run({
1527
+ sessionId: this.#session.id,
1528
+ userPrompt,
1529
+ route
1530
+ });
1531
+ if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
1532
+ prompt: userPrompt,
1533
+ run: result.run
1534
+ }).catch((e) => {
1535
+ result.run.trace.push({
1536
+ id: `evt-miss-${Date.now()}`,
1537
+ runId: result.run.id,
1538
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1539
+ type: "run.warning",
1540
+ message: `onSkillMiss hook failed: ${e instanceof Error ? e.message : String(e)}`
1541
+ });
1542
+ });
1543
+ return result;
1544
+ }
1545
+ /** D3:列出可恢复的 interrupted run(供 UI 展示"未完成任务") */
1546
+ async listInterruptedRuns() {
1547
+ if (!this.#deps.snapshotStore) return [];
1548
+ return this.#deps.snapshotStore.list();
2062
1549
  }
2063
1550
  /**
2064
- * D2 Schema 兜底链(同一 run 内激活时只算一次):
2065
- * 显式 inputSchema > sidecar scripts/<name>.schema.json > schemaInferer 推导 > schemaUnavailable
1551
+ * D3 恢复 interrupted run
1552
+ * 不存在 RUN_SNAPSHOT_NOT_FOUND;schemaVersion 不匹配 删除 + RUN_SNAPSHOT_INCOMPATIBLE;
1553
+ * 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
1554
+ * @experimental
2066
1555
  */
2067
- async #enrichDefinition(skillRoot, scriptName, def, state) {
2068
- if (def.inputSchema) return;
2069
- const sidecar = `${skillRoot}/scripts/${scriptName}.schema.json`;
2070
- if (await this.#deps.fs.exists(sidecar)) try {
2071
- def.inputSchema = JSON.parse(await this.#deps.fs.readText(sidecar));
2072
- return;
2073
- } catch (e) {
2074
- state.trace.record("run.warning", { message: `Failed to parse schema sidecar ${sidecar}: ${messageOf(e)}` });
2075
- }
2076
- if (!this.#deps.schemaInferer) return;
2077
- for (const ext of ["ts", "js"]) {
2078
- const scriptPath = `${skillRoot}/scripts/${scriptName}.${ext}`;
2079
- if (!await this.#deps.fs.exists(scriptPath)) continue;
2080
- const inferred = this.#deps.schemaInferer.inferSchemaFromSource(await this.#deps.fs.readText(scriptPath), { fileName: `${scriptName}.${ext}` });
2081
- if (inferred) def.inputSchema = inferred;
2082
- return;
1556
+ async resumeRun(runId) {
1557
+ const store = this.#deps.snapshotStore;
1558
+ const snapshot = store ? await store.load(runId) : void 0;
1559
+ if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${runId}"`);
1560
+ if (snapshot.schemaVersion !== 1) {
1561
+ await store.delete(runId);
1562
+ throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
2083
1563
  }
2084
- }
2085
- async #handleScriptTool(call, skillName, scriptName, state) {
2086
- const root = this.#deps.skillIndex.get(skillName);
2087
- if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
2088
- if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
2089
- const def = state.activatedTools.get(call.name);
2090
- if (!def) return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available for skill "${skillName}"`);
2091
- let args = call.arguments;
2092
- const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
2093
- if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
2094
- const value = await this.#interact(state, {
2095
- type: "form",
2096
- id: this.#nextInteractionId(state),
2097
- title: `Missing parameters for ${call.name}`,
2098
- fields: schemaToForm(def.inputSchema, args)
2099
- }, {
2100
- tool: call.name,
2101
- missing
2102
- });
2103
- if (typeof value === "object" && value !== null) args = {
2104
- ...args,
2105
- ...value
1564
+ if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
1565
+ await store.delete(runId);
1566
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
1567
+ return {
1568
+ output: "Interaction timed out before the run was resumed",
1569
+ run: {
1570
+ id: runId,
1571
+ sessionId: snapshot.sessionId,
1572
+ status: "failed",
1573
+ phase: "fail",
1574
+ userPrompt: snapshot.userPrompt,
1575
+ startedAt: snapshot.startedAt,
1576
+ endedAt,
1577
+ terminationReason: "interaction-timeout",
1578
+ activeSkillNames: snapshot.activeSkillNames,
1579
+ artifacts: [],
1580
+ trace: [{
1581
+ id: `evt-expired-${runId}`,
1582
+ runId,
1583
+ ts: endedAt,
1584
+ type: "run.failed",
1585
+ message: `Interaction timed out before the run was resumed (expired at ${snapshot.interactionExpiresAt})`,
1586
+ data: {
1587
+ reason: "interaction-timeout",
1588
+ code: "RUN_INTERACTION_TIMEOUT"
1589
+ }
1590
+ }]
1591
+ }
2106
1592
  };
2107
- } catch (e) {
2108
- if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
2109
- throw e;
2110
1593
  }
2111
- const context = createScriptContext({
2112
- fs: this.#deps.fs,
1594
+ if (!this.#catalogCache) await this.discover();
1595
+ const cache = this.#catalogCache;
1596
+ if (!cache) throw new WebSkillError("RUN_FAILED", "discover() did not populate the catalog cache");
1597
+ return new AgentLoop({
1598
+ llm: this.#deps.llm,
1599
+ executor: this.#deps.executor,
1600
+ schemaInferer: this.#deps.schemaInferer,
2113
1601
  artifactStore: this.#deps.artifactStore,
2114
- skillName,
2115
- skillRoot: root,
2116
- runId: state.runId,
2117
- confirm: (message) => this.#confirm(message, state),
2118
- onWarning: (message) => state.trace.record("run.warning", { message })
1602
+ fs: this.#deps.fs,
1603
+ skillIndex: cache.index,
1604
+ model: this.#deps.model,
1605
+ uiBridge: this.#deps.uiBridge,
1606
+ interaction: this.#deps.interaction,
1607
+ memory: this.#deps.memory,
1608
+ hooks: this.#deps.hooks,
1609
+ eventBus: this.#events,
1610
+ longTerm: this.#deps.longTerm,
1611
+ externalTools: this.#deps.externalTools,
1612
+ skillProviders: this.#deps.skillProviders,
1613
+ catalogFilter: this.#deps.catalogFilter,
1614
+ snapshotStore: store
1615
+ }, this.#deps.config).resume(snapshot);
1616
+ }
1617
+ };
1618
+ /**
1619
+ * navigator.webskill 门面装配层:零新业务逻辑,全部直转
1620
+ * SkillDiscovery / validateSkills / WebSkillRuntime / SkillManager。
1621
+ * @stable
1622
+ */
1623
+ function createWebSkillApi(deps) {
1624
+ const { fs, roots } = deps;
1625
+ let runtime;
1626
+ const lazyRuntime = () => {
1627
+ runtime ??= new WebSkillRuntime({
1628
+ fs,
1629
+ roots,
1630
+ llm: deps.llm,
1631
+ ...deps.executor ? { executor: deps.executor } : {},
1632
+ ...deps.artifactStore ? { artifactStore: deps.artifactStore } : {},
1633
+ ...deps.loopConfig ? { config: deps.loopConfig } : {}
2119
1634
  });
2120
- try {
2121
- const before = new Set((await this.#deps.artifactStore.listArtifacts(state.runId)).map((a) => a.id));
2122
- const result = await this.#deps.executor.execute({
2123
- skillRoot: root,
2124
- scriptName,
2125
- args,
2126
- context,
2127
- timeoutMs: state.toolTimeoutMs
2128
- });
2129
- const fresh = (await this.#deps.artifactStore.listArtifacts(state.runId)).filter((a) => !before.has(a.id));
2130
- if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
2131
- await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
2132
- return result;
2133
- } catch (e) {
2134
- if (e instanceof RunTerminated) throw e;
2135
- await this.#bumpSkillStat(skillName, "failures", state);
2136
- return toolError(e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", `Tool "${call.name}" failed: ${messageOf(e)}`);
1635
+ return runtime;
1636
+ };
1637
+ const requireManager = () => {
1638
+ if (!deps.skillManager) throw new WebSkillError("TOOL_UNSUPPORTED", "No skill manager is configured; install/uninstall is unavailable");
1639
+ return deps.skillManager;
1640
+ };
1641
+ return {
1642
+ async discover(path) {
1643
+ const { entries } = await new SkillDiscovery(fs, [path]).discover();
1644
+ return buildCatalog(entries);
1645
+ },
1646
+ async read(name) {
1647
+ const { entries } = await new SkillDiscovery(fs, roots).discover();
1648
+ const entry = entries.find((e) => e.name === name);
1649
+ if (!entry) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${name}`);
1650
+ const skillMdPath = `${entry.root}/SKILL.md`;
1651
+ const { metadata, body } = parseSkillMarkdown(await fs.readText(skillMdPath));
1652
+ return {
1653
+ metadata,
1654
+ body,
1655
+ location: {
1656
+ skillRoot: entry.root,
1657
+ skillMdPath,
1658
+ source: entry.source
1659
+ }
1660
+ };
1661
+ },
1662
+ validate(path) {
1663
+ return validateSkills(fs, [path]);
1664
+ },
1665
+ async run(prompt) {
1666
+ return (await lazyRuntime().run(prompt)).run;
1667
+ },
1668
+ async install(url) {
1669
+ const manifest = await requireManager().install(sourceFromUrl(url));
1670
+ return {
1671
+ name: manifest.name,
1672
+ ...manifest.version !== void 0 ? { version: manifest.version } : {},
1673
+ installedAt: manifest.installedAt,
1674
+ integrity: manifest.integrity
1675
+ };
1676
+ },
1677
+ async uninstall(name) {
1678
+ await requireManager().uninstall(name);
2137
1679
  }
1680
+ };
1681
+ }
1682
+ /** install URL 分派:http(s)…​.zip → http;…​.git → git(无 git 能力的 manager 自行 TOOL_UNSUPPORTED);其余 → local */
1683
+ function sourceFromUrl(url) {
1684
+ const bare = url.split("?")[0] ?? url;
1685
+ if (/^https?:\/\//.test(url) && bare.endsWith(".zip")) return {
1686
+ type: "http",
1687
+ url
1688
+ };
1689
+ if (bare.endsWith(".git")) return {
1690
+ type: "git",
1691
+ url
1692
+ };
1693
+ return {
1694
+ type: "local",
1695
+ path: url
1696
+ };
1697
+ }
1698
+ const messageOf = (e) => e instanceof Error ? e.message : String(e);
1699
+ /** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
1700
+ var HookRunner = class {
1701
+ #hooks = /* @__PURE__ */ new Map();
1702
+ timeoutMs;
1703
+ failOnHookError;
1704
+ onWarning;
1705
+ constructor(options = {}) {
1706
+ this.timeoutMs = options.timeoutMs ?? 5e3;
1707
+ this.failOnHookError = options.failOnHookError ?? false;
1708
+ this.onWarning = options.onWarning;
2138
1709
  }
2139
- /** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
2140
- async #confirm(message, state) {
2141
- if (this.#policy.confirmations === "auto-approve") return true;
2142
- if (!this.#deps.uiBridge) {
2143
- state.trace.record("run.warning", { message: "No UiBridge configured; confirm() auto-approved" });
2144
- return true;
2145
- }
2146
- try {
2147
- return await this.#interact(state, {
2148
- type: "confirm",
2149
- id: this.#nextInteractionId(state),
2150
- message,
2151
- defaultValue: true
2152
- }, { kind: "confirm" }) !== false;
1710
+ register(phase, hook) {
1711
+ const key = phase;
1712
+ const list = this.#hooks.get(key) ?? [];
1713
+ list.push(hook);
1714
+ this.#hooks.set(key, list);
1715
+ return this;
1716
+ }
1717
+ async run(phase, ctx) {
1718
+ const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
1719
+ for (const hook of hooks) try {
1720
+ await this.#withTimeout(Promise.resolve().then(() => hook(ctx)), phase);
2153
1721
  } catch (e) {
2154
- if (e instanceof BridgeRequestError) {
2155
- state.trace.record("run.warning", { message: `confirm() bridge failed, auto-approved: ${e.message}` });
2156
- return true;
2157
- }
2158
- throw e;
1722
+ if (this.failOnHookError) throw e instanceof WebSkillError ? e : new WebSkillError("RUN_FAILED", messageOf(e), e);
1723
+ this.onWarning?.(messageOf(e));
2159
1724
  }
2160
1725
  }
2161
- #nextInteractionId(state) {
2162
- return `int-${++state.interactionSeq}`;
1726
+ #withTimeout(promise, phase) {
1727
+ return new Promise((resolve, reject) => {
1728
+ const timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Lifecycle hook for phase "${phase}" timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
1729
+ promise.then((v) => {
1730
+ clearTimeout(timer);
1731
+ resolve(v);
1732
+ }, (e) => {
1733
+ clearTimeout(timer);
1734
+ reject(e instanceof Error ? e : new Error(String(e)));
1735
+ });
1736
+ });
2163
1737
  }
2164
- async #memoryGet(scope, key) {
2165
- try {
2166
- return await this.#deps.memory?.get(scope, key);
2167
- } catch {
2168
- return;
2169
- }
1738
+ };
1739
+ const encode = (s) => encodeURIComponent(s);
1740
+ /**
1741
+ * 基于 FileSystemProvider 的 MemoryStore:
1742
+ * <root>/<encodeURIComponent(scope)>/<encodeURIComponent(key)>.json
1743
+ * scope/key 全部编码防路径穿越;环境无关,构造必须显式传 fs。
1744
+ */
1745
+ var FsMemoryStore = class {
1746
+ #root;
1747
+ #fs;
1748
+ constructor(deps) {
1749
+ this.#root = deps.root.replace(/\/+$/, "");
1750
+ this.#fs = deps.fs;
2170
1751
  }
2171
- async #memorySet(scope, key, value, state) {
2172
- try {
2173
- await this.#deps.memory?.set(scope, key, value);
2174
- } catch (e) {
2175
- state.trace.record("run.warning", { message: `Memory write failed: ${messageOf(e)}` });
2176
- }
1752
+ #scopeDir(scope) {
1753
+ return `${this.#root}/${encode(scope)}`;
2177
1754
  }
2178
- async #writeActivationMemory(skillName, state) {
2179
- if (!this.#deps.memory) return;
2180
- const sessionScope = `session:${state.run.sessionId}`;
2181
- await this.#memorySet(sessionScope, "activeSkillNames", [...state.activated].sort(), state);
2182
- await this.#bumpSkillStat(skillName, "activations", state);
2183
- if (this.#deps.longTerm?.enabled) {
2184
- const userScope = `user:${this.#deps.longTerm.userId}`;
2185
- const usage = await this.#memoryGet(userScope, "skillUsage") ?? {};
2186
- usage[skillName] = (usage[skillName] ?? 0) + 1;
2187
- await this.#memorySet(userScope, "skillUsage", usage, state);
2188
- }
1755
+ #keyPath(scope, key) {
1756
+ return `${this.#scopeDir(scope)}/${encode(key)}.json`;
2189
1757
  }
2190
- async #bumpSkillStat(skillName, field, state) {
2191
- if (!this.#deps.memory) return;
2192
- const scope = `skill:${skillName}`;
2193
- const stats = await this.#memoryGet(scope, "stats") ?? {
2194
- activations: 0,
2195
- successes: 0,
2196
- failures: 0
2197
- };
2198
- stats[field] = (stats[field] ?? 0) + 1;
2199
- await this.#memorySet(scope, "stats", stats, state);
1758
+ async get(scope, key) {
1759
+ const path = this.#keyPath(scope, key);
1760
+ if (!await this.#fs.exists(path)) return void 0;
1761
+ return JSON.parse(await this.#fs.readText(path));
2200
1762
  }
2201
- async #appendParamHistory(state, request, value) {
2202
- if (!this.#deps.memory) return;
2203
- const scope = `session:${state.run.sessionId}`;
2204
- const history = await this.#memoryGet(scope, "paramHistory") ?? [];
2205
- history.push({
2206
- ts: state.now(),
2207
- interactionId: request.id,
2208
- type: request.type,
2209
- value
2210
- });
2211
- await this.#memorySet(scope, "paramHistory", history, state);
1763
+ async set(scope, key, value) {
1764
+ await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
1765
+ }
1766
+ async delete(scope, key) {
1767
+ const path = this.#keyPath(scope, key);
1768
+ if (await this.#fs.exists(path)) await this.#fs.remove(path);
1769
+ }
1770
+ async list(scope) {
1771
+ const dir = this.#scopeDir(scope);
1772
+ if (!await this.#fs.exists(dir)) return [];
1773
+ const out = [];
1774
+ for (const entry of await this.#fs.list(dir)) {
1775
+ if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
1776
+ const fileName = entry.path.split("/").pop() ?? "";
1777
+ const key = decodeURIComponent(fileName.replace(/\.json$/, ""));
1778
+ out.push({
1779
+ key,
1780
+ value: JSON.parse(await this.#fs.readText(entry.path))
1781
+ });
1782
+ }
1783
+ return out.sort((a, b) => a.key.localeCompare(b.key));
1784
+ }
1785
+ async clear(scope) {
1786
+ if (scope !== void 0) {
1787
+ const dir = this.#scopeDir(scope);
1788
+ if (await this.#fs.exists(dir)) await this.#fs.remove(dir, { recursive: true });
1789
+ return;
1790
+ }
1791
+ if (!await this.#fs.exists(this.#root)) return;
1792
+ for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
2212
1793
  }
2213
1794
  };
1795
+ const INDEX_FILE = "index.json";
2214
1796
  /**
2215
- * Catalog 合并:同名本地优先;外部条目的 mcp:// URI 保留,
2216
- * 调用方可凭 URI 强制选择外部技能(绕过本地优先)。
2217
- */
2218
- function mergeCatalogEntries(localEntries, providerEntries) {
2219
- const byName = new Map(localEntries.map((e) => [e.name, e]));
2220
- for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
2221
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2222
- }
2223
- const RUN_SNAPSHOT_SCHEMA_VERSION = 1;
2224
- const SNAPSHOT_SUFFIX = ".snapshot.json";
2225
- /**
2226
- * FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
2227
- * 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
1797
+ * 基于 FileSystemProvider ArtifactStore:产物落盘 <root>/<runId>/<path>,
1798
+ * 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
1799
+ * 环境无关(OPFS/Node 均可),构造必须显式传 fs。
2228
1800
  */
2229
- var FsRunSnapshotStore = class {
1801
+ var FsArtifactStore = class {
2230
1802
  #root;
2231
1803
  #fs;
2232
1804
  constructor(deps) {
2233
1805
  this.#root = deps.root.replace(/\/+$/, "");
2234
1806
  this.#fs = deps.fs;
2235
1807
  }
2236
- #path(runId) {
2237
- return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
1808
+ async createTextArtifact(input) {
1809
+ const size = new TextEncoder().encode(input.content).length;
1810
+ await this.#fs.writeText(this.#artifactPath(input.runId, input.path), input.content);
1811
+ return this.#persist({
1812
+ ...input,
1813
+ type: "text",
1814
+ size
1815
+ });
2238
1816
  }
2239
- async save(snapshot) {
2240
- await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
1817
+ async createBinaryArtifact(input) {
1818
+ await this.#fs.writeBinary(this.#artifactPath(input.runId, input.path), input.content);
1819
+ return this.#persist({
1820
+ ...input,
1821
+ type: "binary",
1822
+ size: input.content.length
1823
+ });
2241
1824
  }
2242
- async load(runId) {
2243
- const path = this.#path(runId);
2244
- if (!await this.#fs.exists(path)) return void 0;
2245
- let parsed;
2246
- try {
2247
- parsed = JSON.parse(await this.#fs.readText(path));
2248
- } catch (e) {
2249
- await this.#fs.remove(path).catch(() => void 0);
2250
- throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
2251
- }
2252
- const snapshot = parsed;
2253
- if (typeof snapshot !== "object" || snapshot === null || snapshot.runId !== runId) {
2254
- await this.#fs.remove(path).catch(() => void 0);
2255
- throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
1825
+ async listArtifacts(runId) {
1826
+ const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
1827
+ if (!await this.#fs.exists(indexPath)) return [];
1828
+ const raw = await this.#fs.readText(indexPath);
1829
+ return JSON.parse(raw).artifacts ?? [];
1830
+ }
1831
+ #artifactPath(runId, artifactPath) {
1832
+ return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
1833
+ }
1834
+ async #persist(input) {
1835
+ const artifact = {
1836
+ id: `${input.runId}/${input.path}`,
1837
+ runId: input.runId,
1838
+ path: input.path,
1839
+ type: input.type,
1840
+ mimeType: input.mimeType,
1841
+ size: input.size,
1842
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1843
+ metadata: input.metadata
1844
+ };
1845
+ const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
1846
+ await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
1847
+ return artifact;
1848
+ }
1849
+ };
1850
+ const isRecord = (v) => typeof v === "object" && v !== null;
1851
+ const isNonEmptyString = (v) => typeof v === "string" && v !== "";
1852
+ /** 入站校验:非法 kind / 缺 id / 缺字段 → undefined(调用方忽略并记 warning) */
1853
+ function parseBridgeRequest(data) {
1854
+ if (!isRecord(data)) return void 0;
1855
+ const { kind, id } = data;
1856
+ if (!isNonEmptyString(id)) return void 0;
1857
+ switch (kind) {
1858
+ case "readReference": return isNonEmptyString(data["path"]) ? {
1859
+ kind,
1860
+ id,
1861
+ path: data["path"]
1862
+ } : void 0;
1863
+ case "writeArtifact": {
1864
+ const { path, content, mimeType } = data;
1865
+ const validContent = typeof content === "string" || Array.isArray(content) && content.every((n) => typeof n === "number");
1866
+ if (!isNonEmptyString(path) || !validContent) return void 0;
1867
+ return {
1868
+ kind,
1869
+ id,
1870
+ path,
1871
+ content,
1872
+ ...isNonEmptyString(mimeType) ? { mimeType } : {}
1873
+ };
2256
1874
  }
2257
- return snapshot;
1875
+ case "confirm": return isNonEmptyString(data["message"]) ? {
1876
+ kind,
1877
+ id,
1878
+ message: data["message"]
1879
+ } : void 0;
1880
+ default: return;
2258
1881
  }
2259
- async delete(runId) {
2260
- const path = this.#path(runId);
2261
- if (await this.#fs.exists(path)) await this.#fs.remove(path);
1882
+ }
1883
+ function bridgeError(id, code, message) {
1884
+ return {
1885
+ id,
1886
+ ok: false,
1887
+ error: {
1888
+ code,
1889
+ message
1890
+ }
1891
+ };
1892
+ }
1893
+ /**
1894
+ * 判定 URL 是否被策略放行:
1895
+ * - 'deny-all'(默认)全拒;'allow-all' 全放
1896
+ * - { allow } 条目支持三种写法:
1897
+ * - 精确域名 'api.example.com'(忽略端口与路径)
1898
+ * - 通配 '*.example.com'(含 apex 与任意深度子域)
1899
+ * - 源 'http://localhost:3000'(协议 + host + port 全等)
1900
+ * - URL 解析失败一律拒绝
1901
+ * @stable
1902
+ */
1903
+ function isNetworkAllowed(policy, url) {
1904
+ if (policy === "allow-all") return true;
1905
+ if (policy === "deny-all" || !policy || typeof policy !== "object") return false;
1906
+ var allow = policy.allow;
1907
+ if (!Array.isArray(allow)) return false;
1908
+ var parsed;
1909
+ try {
1910
+ parsed = new URL(url);
1911
+ } catch {
1912
+ return false;
2262
1913
  }
2263
- async list() {
2264
- if (!await this.#fs.exists(this.#root)) return [];
2265
- const out = [];
2266
- for (const entry of await this.#fs.list(this.#root)) {
2267
- if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
1914
+ var host = parsed.hostname.toLowerCase();
1915
+ for (var i = 0; i < allow.length; i++) {
1916
+ var entry = allow[i];
1917
+ if (typeof entry !== "string" || entry === "") continue;
1918
+ if (entry.indexOf("://") !== -1) {
2268
1919
  try {
2269
- out.push(JSON.parse(await this.#fs.readText(entry.path)));
1920
+ if (new URL(entry).origin === parsed.origin) return true;
2270
1921
  } catch {}
1922
+ continue;
2271
1923
  }
2272
- return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
2273
- }
2274
- };
2275
- /** runtime 门面:组合 discovery / router / agent loop / lifecycle */
2276
- var WebSkillRuntime = class {
2277
- #deps;
2278
- #discovery;
2279
- #router;
2280
- #session;
2281
- #events;
2282
- #catalogCache;
2283
- constructor(deps) {
2284
- this.#deps = deps;
2285
- this.#discovery = new SkillDiscovery(deps.fs, deps.roots);
2286
- this.#router = deps.router ?? new ProgressiveRouter();
2287
- this.#events = deps.eventBus ?? new EventBus();
2288
- this.#session = {
2289
- id: `session-${Math.random().toString(36).slice(2, 10)}`,
2290
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2291
- };
2292
- }
2293
- get session() {
2294
- return this.#session;
1924
+ var rule = entry.toLowerCase();
1925
+ if (rule.indexOf("*.") === 0) {
1926
+ var suffix = rule.slice(2);
1927
+ if (host === suffix || host.endsWith("." + suffix)) return true;
1928
+ } else if (host === rule) return true;
2295
1929
  }
2296
- /** 生命周期事件总线(只读观测) */
2297
- get events() {
2298
- return this.#events;
1930
+ return false;
1931
+ }
1932
+ /** 阻断 trace 用的脱敏 host(解析失败返回占位,不记录完整 URL) */
1933
+ function networkUrlHost(url) {
1934
+ try {
1935
+ return new URL(url).hostname;
1936
+ } catch {
1937
+ return "(unparseable-url)";
2299
1938
  }
2300
- async discover() {
2301
- const result = await this.#discovery.discover();
2302
- this.#catalogCache = {
2303
- catalog: buildCatalog(result.entries),
2304
- index: new Map(result.entries.map((e) => [e.name, e.root]))
2305
- };
2306
- return result;
1939
+ }
1940
+ /**
1941
+ * Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
1942
+ * 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
1943
+ * once-per-run(默认)在 run 内记忆已批准能力,every-call 每次调用都问。
1944
+ * @stable
1945
+ */
1946
+ var CapabilityApproval = class {
1947
+ #uiBridge;
1948
+ #scope;
1949
+ /** runId → 已批准能力集合(once-per-run 记忆) */
1950
+ #approved = /* @__PURE__ */ new Map();
1951
+ #seq = 0;
1952
+ constructor(deps = {}) {
1953
+ this.#uiBridge = deps.uiBridge;
1954
+ this.#scope = deps.scope ?? "once-per-run";
2307
1955
  }
2308
- async run(userPrompt) {
2309
- if (!this.#catalogCache) await this.discover();
2310
- const cache = this.#catalogCache;
2311
- if (!cache) throw new Error("discover() did not populate the catalog cache");
2312
- const providerEntries = (await Promise.all((this.#deps.skillProviders ?? []).map(async (p) => {
2313
- try {
2314
- return await p.listSkills();
2315
- } catch {
2316
- return [];
1956
+ async authorize(input) {
1957
+ const { runId, capability, mode } = input;
1958
+ if (mode === false) return "disabled";
1959
+ if (mode !== "require-approval") return "allowed";
1960
+ if (this.#scope === "once-per-run" && this.#approved.get(runId)?.has(capability)) return "allowed";
1961
+ if (!this.#uiBridge) return "denied";
1962
+ if ((await this.#uiBridge.request({
1963
+ type: "authorize",
1964
+ id: `authorize-${runId}-${capability}-${++this.#seq}`,
1965
+ capability,
1966
+ message: input.message,
1967
+ ...input.details !== void 0 ? { details: input.details } : {}
1968
+ })).value !== true) return "denied";
1969
+ if (this.#scope === "once-per-run") {
1970
+ let set = this.#approved.get(runId);
1971
+ if (!set) {
1972
+ set = /* @__PURE__ */ new Set();
1973
+ this.#approved.set(runId, set);
2317
1974
  }
2318
- }))).flat();
2319
- const catalog = providerEntries.length > 0 ? { skills: mergeCatalogEntries(cache.catalog.skills, providerEntries) } : cache.catalog;
2320
- const filteredCatalog = this.#deps.catalogFilter ? { skills: await this.#deps.catalogFilter(catalog.skills) } : catalog;
2321
- const route = await this.#router.route(filteredCatalog);
2322
- const result = await new AgentLoop({
2323
- llm: this.#deps.llm,
2324
- executor: this.#deps.executor,
2325
- schemaInferer: this.#deps.schemaInferer,
2326
- artifactStore: this.#deps.artifactStore,
2327
- fs: this.#deps.fs,
2328
- skillIndex: cache.index,
2329
- model: this.#deps.model,
2330
- uiBridge: this.#deps.uiBridge,
2331
- interaction: this.#deps.interaction,
2332
- memory: this.#deps.memory,
2333
- hooks: this.#deps.hooks,
2334
- eventBus: this.#events,
2335
- longTerm: this.#deps.longTerm,
2336
- externalTools: this.#deps.externalTools,
2337
- skillProviders: this.#deps.skillProviders,
2338
- catalogFilter: this.#deps.catalogFilter,
2339
- snapshotStore: this.#deps.snapshotStore
2340
- }, this.#deps.config).run({
2341
- sessionId: this.#session.id,
2342
- userPrompt,
2343
- route
2344
- });
2345
- if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
2346
- prompt: userPrompt,
2347
- run: result.run
2348
- }).catch((e) => {
2349
- result.run.trace.push({
2350
- id: `evt-miss-${Date.now()}`,
2351
- runId: result.run.id,
2352
- ts: (/* @__PURE__ */ new Date()).toISOString(),
2353
- type: "run.warning",
2354
- message: `onSkillMiss hook failed: ${e instanceof Error ? e.message : String(e)}`
2355
- });
2356
- });
2357
- return result;
2358
- }
2359
- /** D3:列出可恢复的 interrupted run(供 UI 展示"未完成任务") */
2360
- async listInterruptedRuns() {
2361
- if (!this.#deps.snapshotStore) return [];
2362
- return this.#deps.snapshotStore.list();
2363
- }
2364
- /**
2365
- * D3 恢复 interrupted run:
2366
- * 不存在 → RUN_SNAPSHOT_NOT_FOUND;schemaVersion 不匹配 → 删除 + RUN_SNAPSHOT_INCOMPATIBLE;
2367
- * 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
2368
- */
2369
- async resumeRun(runId) {
2370
- const store = this.#deps.snapshotStore;
2371
- const snapshot = store ? await store.load(runId) : void 0;
2372
- if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${runId}"`);
2373
- if (snapshot.schemaVersion !== 1) {
2374
- await store.delete(runId);
2375
- throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
2376
- }
2377
- if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
2378
- await store.delete(runId);
2379
- const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2380
- return {
2381
- output: "Interaction timed out before the run was resumed",
2382
- run: {
2383
- id: runId,
2384
- sessionId: snapshot.sessionId,
2385
- status: "failed",
2386
- phase: "fail",
2387
- userPrompt: snapshot.userPrompt,
2388
- startedAt: snapshot.startedAt,
2389
- endedAt,
2390
- terminationReason: "interaction-timeout",
2391
- activeSkillNames: snapshot.activeSkillNames,
2392
- artifacts: [],
2393
- trace: [{
2394
- id: `evt-expired-${runId}`,
2395
- runId,
2396
- ts: endedAt,
2397
- type: "run.failed",
2398
- message: `Interaction timed out before the run was resumed (expired at ${snapshot.interactionExpiresAt})`,
2399
- data: {
2400
- reason: "interaction-timeout",
2401
- code: "RUN_INTERACTION_TIMEOUT"
2402
- }
2403
- }]
2404
- }
2405
- };
1975
+ set.add(capability);
2406
1976
  }
2407
- if (!this.#catalogCache) await this.discover();
2408
- const cache = this.#catalogCache;
2409
- if (!cache) throw new WebSkillError("RUN_FAILED", "discover() did not populate the catalog cache");
2410
- return new AgentLoop({
2411
- llm: this.#deps.llm,
2412
- executor: this.#deps.executor,
2413
- schemaInferer: this.#deps.schemaInferer,
2414
- artifactStore: this.#deps.artifactStore,
2415
- fs: this.#deps.fs,
2416
- skillIndex: cache.index,
2417
- model: this.#deps.model,
2418
- uiBridge: this.#deps.uiBridge,
2419
- interaction: this.#deps.interaction,
2420
- memory: this.#deps.memory,
2421
- hooks: this.#deps.hooks,
2422
- eventBus: this.#events,
2423
- longTerm: this.#deps.longTerm,
2424
- externalTools: this.#deps.externalTools,
2425
- skillProviders: this.#deps.skillProviders,
2426
- catalogFilter: this.#deps.catalogFilter,
2427
- snapshotStore: store
2428
- }, this.#deps.config).resume(snapshot);
1977
+ return "allowed";
2429
1978
  }
2430
1979
  };
2431
1980
 
2432
1981
  //#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 };
1982
+ export { normalizeToolContent as A, createWebSkillApi as C, isNetworkAllowed as D, fromVercelStreamPart as E, toVercelToolSpecs as F, resolveToolName as M, schemaToForm as N, mergeCatalogEntries as O, toLlmToolSpec as P, createScriptContext as S, fromVercelResult as T, RUN_SNAPSHOT_SCHEMA_VERSION as _, CapabilityApproval as a, bridgeError as b, FsMemoryStore as c, HookRunner as d, OpenAiCompatibleClient as f, READ_SKILL_FILE_TOOL_NAME as g, READ_SKILL_FILE_TOOL as h, AgentLoop as i, parseBridgeRequest as j, networkUrlHost as k, FsRunSnapshotStore as l, READ_SKILL_FILE_INPUT_SCHEMA as m, ASK_USER_TOOL as n, EventBus as o, ProgressiveRouter as p, ASK_USER_TOOL_NAME as r, FsArtifactStore as s, ASK_USER_INPUT_SCHEMA as t, FullDisclosureRouter as u, TraceRecorder as v, extractChartSpec as w, buildRenderResult as x, WebSkillRuntime as y };