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