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