@webskill/sdk 0.0.6 → 0.1.1
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/README.md +180 -46
- package/dist/browser.d.ts +5 -1
- package/dist/browser.js +9 -2
- package/dist/{dist-33_zuOCm.js → dist-Cc3Mqi3_.js} +5 -1
- package/dist/{dist-nXiR40hi.js → dist-D5fsiETF.js} +36 -752
- package/dist/{dist-BQruQ9Hv.js → dist-Dv4a1Jkm.js} +15 -3
- package/dist/governance.d.ts +3 -2
- package/dist/governance.js +2 -2
- package/dist/{index-vi7GPemR.d.ts → index-Dc4X2N54.d.ts} +3 -1
- package/dist/{index-DCifjtJx.d.ts → index-oG5Nzgb9.d.ts} +19 -595
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mcp.d.ts +3 -1
- package/dist/mcp.js +3 -1
- package/dist/memoryArtifactStore-C9lFVqPF-U8nXvqL9.js +637 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +3 -2
- package/dist/testing-pn3NhXSV.js +117 -0
- package/dist/testing.d.ts +73 -0
- package/dist/testing.js +4 -0
- package/dist/types-CgNC-oQu-Dq_A1yA-.d.ts +539 -0
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-react.js +1 -1
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +17 -4
- package/dist/ui.js +2 -2
- package/package.json +5 -1
|
@@ -1,645 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { zipSync } from "fflate";
|
|
1
|
+
import { C as renderAvailableSkillsXml, E as validateSkills, T as resolveInsideRoot, c as SkillDiscovery, d as buildCatalog, l as SkillReader, t as MemoryArtifactStore, u as WebSkillError, x as parseSkillMarkdown } from "./memoryArtifactStore-C9lFVqPF-U8nXvqL9.js";
|
|
3
2
|
|
|
4
|
-
//#region ../core/dist/index.js
|
|
5
|
-
/** 所有公开 API 抛出的结构化错误,code 供上层可编程处理 */
|
|
6
|
-
var WebSkillError = class extends Error {
|
|
7
|
-
code;
|
|
8
|
-
details;
|
|
9
|
-
constructor(code, message, details) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "WebSkillError";
|
|
12
|
-
this.code = code;
|
|
13
|
-
this.details = details;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
/** 技能目录内的安装清单文件名(不参与 files 列表与 digest) */
|
|
17
|
-
const SKILL_MANIFEST_FILE = "webskill.skill-manifest.json";
|
|
18
|
-
/** managed root 下的锁定文件名(不参与 files 列表与 digest) */
|
|
19
|
-
const SKILLS_LOCKFILE = "skills.lock.json";
|
|
20
|
-
/** 整体 digest:逐文件 "<path>:<sha256>\n" 拼接串(文件按 path 排序)的 sha256(hash 由调用方提供,可为 WebCrypto 异步实现) */
|
|
21
|
-
async function computeDigest(files, sha256) {
|
|
22
|
-
return sha256([...files].sort((a, b) => a.path.localeCompare(b.path)).map((f) => `${f.path}:${f.sha256}\n`).join(""));
|
|
23
|
-
}
|
|
24
|
-
/** 由逐文件清单构建 manifest(digest 由 computeDigest 计算) */
|
|
25
|
-
async function buildManifest(input) {
|
|
26
|
-
return {
|
|
27
|
-
schemaVersion: 1,
|
|
28
|
-
name: input.name,
|
|
29
|
-
...input.version !== void 0 ? { version: input.version } : {},
|
|
30
|
-
source: input.source,
|
|
31
|
-
installedAt: input.installedAt,
|
|
32
|
-
integrity: {
|
|
33
|
-
algorithm: "sha256",
|
|
34
|
-
digest: await computeDigest(input.files, input.sha256)
|
|
35
|
-
},
|
|
36
|
-
files: input.files
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/** 按 manifest.files 比对实际 hash(调用方负责计算 actualHashes) */
|
|
40
|
-
function verifyManifest(manifest, actualHashes) {
|
|
41
|
-
const mismatches = [];
|
|
42
|
-
for (const file of manifest.files) {
|
|
43
|
-
const actual = actualHashes.get(file.path);
|
|
44
|
-
if (actual === void 0 || actual !== file.sha256) mismatches.push(file.path);
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
ok: mismatches.length === 0,
|
|
48
|
-
mismatches
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
const ROOT = "/";
|
|
52
|
-
/** 内存文件系统:路径一律规范化为 `/` 分隔,写入自动创建父目录 */
|
|
53
|
-
var MemoryFS = class {
|
|
54
|
-
kind = "memory";
|
|
55
|
-
#entries = /* @__PURE__ */ new Map();
|
|
56
|
-
constructor() {
|
|
57
|
-
this.#entries.set(ROOT, {
|
|
58
|
-
type: "directory",
|
|
59
|
-
content: /* @__PURE__ */ new Uint8Array(0),
|
|
60
|
-
mtimeMs: Date.now()
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
#normalize(path) {
|
|
64
|
-
const out = [];
|
|
65
|
-
for (const seg of path.replace(/\\/g, "/").split("/")) {
|
|
66
|
-
if (seg === "" || seg === ".") continue;
|
|
67
|
-
if (seg === "..") out.pop();
|
|
68
|
-
else out.push(seg);
|
|
69
|
-
}
|
|
70
|
-
return ROOT + out.join("/");
|
|
71
|
-
}
|
|
72
|
-
#parentOf(key) {
|
|
73
|
-
const i = key.lastIndexOf("/");
|
|
74
|
-
return i <= 0 ? ROOT : key.slice(0, i);
|
|
75
|
-
}
|
|
76
|
-
#ensureParents(key) {
|
|
77
|
-
const chain = [];
|
|
78
|
-
let dir = this.#parentOf(key);
|
|
79
|
-
while (!this.#entries.has(dir)) {
|
|
80
|
-
chain.push(dir);
|
|
81
|
-
dir = this.#parentOf(dir);
|
|
82
|
-
}
|
|
83
|
-
for (const d of chain.reverse()) this.#entries.set(d, {
|
|
84
|
-
type: "directory",
|
|
85
|
-
content: /* @__PURE__ */ new Uint8Array(0),
|
|
86
|
-
mtimeMs: Date.now()
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
#getFile(key) {
|
|
90
|
-
const entry = this.#entries.get(key);
|
|
91
|
-
if (!entry || entry.type !== "file") throw new WebSkillError("FS_NOT_FOUND", `File not found: ${key}`);
|
|
92
|
-
return entry;
|
|
93
|
-
}
|
|
94
|
-
#getDirectory(key) {
|
|
95
|
-
const entry = this.#entries.get(key);
|
|
96
|
-
if (!entry || entry.type !== "directory") throw new WebSkillError("FS_NOT_FOUND", `Directory not found: ${key}`);
|
|
97
|
-
return entry;
|
|
98
|
-
}
|
|
99
|
-
async readText(path) {
|
|
100
|
-
return new TextDecoder().decode(this.#getFile(this.#normalize(path)).content);
|
|
101
|
-
}
|
|
102
|
-
async writeText(path, content) {
|
|
103
|
-
await this.writeBinary(path, new TextEncoder().encode(content));
|
|
104
|
-
}
|
|
105
|
-
async readBinary(path) {
|
|
106
|
-
return this.#getFile(this.#normalize(path)).content;
|
|
107
|
-
}
|
|
108
|
-
async writeBinary(path, content) {
|
|
109
|
-
const key = this.#normalize(path);
|
|
110
|
-
this.#ensureParents(key);
|
|
111
|
-
this.#entries.set(key, {
|
|
112
|
-
type: "file",
|
|
113
|
-
content,
|
|
114
|
-
mtimeMs: Date.now()
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
async exists(path) {
|
|
118
|
-
return this.#entries.has(this.#normalize(path));
|
|
119
|
-
}
|
|
120
|
-
async stat(path) {
|
|
121
|
-
const key = this.#normalize(path);
|
|
122
|
-
const entry = this.#entries.get(key);
|
|
123
|
-
if (!entry) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
|
|
124
|
-
return {
|
|
125
|
-
path: key,
|
|
126
|
-
type: entry.type,
|
|
127
|
-
size: entry.type === "file" ? entry.content.length : void 0,
|
|
128
|
-
mtimeMs: entry.mtimeMs
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
async list(path) {
|
|
132
|
-
const key = this.#normalize(path);
|
|
133
|
-
this.#getDirectory(key);
|
|
134
|
-
const prefix = key === ROOT ? ROOT : key + "/";
|
|
135
|
-
const out = [];
|
|
136
|
-
for (const [k, entry] of this.#entries) {
|
|
137
|
-
if (k === key || !k.startsWith(prefix)) continue;
|
|
138
|
-
if (k.slice(prefix.length).includes("/")) continue;
|
|
139
|
-
out.push({
|
|
140
|
-
path: k,
|
|
141
|
-
type: entry.type,
|
|
142
|
-
size: entry.type === "file" ? entry.content.length : void 0,
|
|
143
|
-
mtimeMs: entry.mtimeMs
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
return out;
|
|
147
|
-
}
|
|
148
|
-
async mkdir(path) {
|
|
149
|
-
const key = this.#normalize(path);
|
|
150
|
-
if (this.#entries.has(key)) return;
|
|
151
|
-
this.#ensureParents(key);
|
|
152
|
-
this.#entries.set(key, {
|
|
153
|
-
type: "directory",
|
|
154
|
-
content: /* @__PURE__ */ new Uint8Array(0),
|
|
155
|
-
mtimeMs: Date.now()
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
async remove(path, options) {
|
|
159
|
-
const key = this.#normalize(path);
|
|
160
|
-
if (!this.#entries.get(key)) throw new WebSkillError("FS_NOT_FOUND", `Path not found: ${key}`);
|
|
161
|
-
if (key === ROOT) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", "Cannot delete root directory");
|
|
162
|
-
const prefix = key + "/";
|
|
163
|
-
const descendants = [...this.#entries.keys()].filter((k) => k.startsWith(prefix));
|
|
164
|
-
if (descendants.length > 0 && !options?.recursive) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Directory not empty, use recursive: ${key}`);
|
|
165
|
-
for (const k of descendants) this.#entries.delete(k);
|
|
166
|
-
this.#entries.delete(key);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
/** 统一分隔符为 `/`,去除 `.` 段与重复分隔符(不解析 `..`) */
|
|
170
|
-
function normalizePath(path) {
|
|
171
|
-
return path.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* 将相对路径安全地解析到 root 之内。
|
|
175
|
-
* 拒绝空路径、绝对路径(`/`、`\\`、`C:\` 形式)与任何 `..` 段。
|
|
176
|
-
*/
|
|
177
|
-
function resolveInsideRoot(root, relativePath) {
|
|
178
|
-
const fail = (reason) => {
|
|
179
|
-
throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path outside root: ${reason} (path: ${JSON.stringify(relativePath)})`);
|
|
180
|
-
};
|
|
181
|
-
if (relativePath.trim() === "") fail("empty path");
|
|
182
|
-
if (relativePath.startsWith("/") || relativePath.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(relativePath)) fail("absolute paths not allowed");
|
|
183
|
-
const segments = relativePath.replace(/\\/g, "/").split("/");
|
|
184
|
-
if (segments.some((s) => s === "..")) fail("`..` segments not allowed");
|
|
185
|
-
const clean = segments.filter((s) => s !== "" && s !== ".");
|
|
186
|
-
if (clean.length === 0) fail("path resolves to empty");
|
|
187
|
-
const normalizedRoot = normalizePath(root);
|
|
188
|
-
const joined = normalizedRoot === "" ? clean.join("/") : `${normalizedRoot}/${clean.join("/")}`;
|
|
189
|
-
return root.startsWith("/") || root.startsWith("\\") ? `/${joined}` : joined;
|
|
190
|
-
}
|
|
191
|
-
const FRONTMATTER_RE = /^---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---[^\S\r\n]*(?:\r?\n|$)([\s\S]*)$/;
|
|
192
|
-
const invalid = (message, details) => {
|
|
193
|
-
throw new WebSkillError("SKILL_INVALID_METADATA", message, details);
|
|
194
|
-
};
|
|
195
|
-
function parseSkillMarkdown(raw) {
|
|
196
|
-
const match = FRONTMATTER_RE.exec(raw);
|
|
197
|
-
if (!match) invalid("SKILL.md missing YAML frontmatter (must start with a `---` line)");
|
|
198
|
-
const [, yamlText, bodyRaw] = match;
|
|
199
|
-
let data;
|
|
200
|
-
try {
|
|
201
|
-
data = parse(yamlText);
|
|
202
|
-
} catch (cause) {
|
|
203
|
-
invalid("frontmatter YAML parse failed", cause);
|
|
204
|
-
}
|
|
205
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) invalid("frontmatter must be a YAML object");
|
|
206
|
-
const metadata = data;
|
|
207
|
-
if (typeof metadata["name"] !== "string" || metadata["name"].trim() === "") invalid("frontmatter missing required `name` field");
|
|
208
|
-
if (typeof metadata["description"] !== "string" || metadata["description"].trim() === "") invalid("frontmatter missing required `description` field");
|
|
209
|
-
return {
|
|
210
|
-
metadata,
|
|
211
|
-
body: bodyRaw.trim()
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
const SKILL_NAME_MAX_LENGTH = 64;
|
|
215
|
-
/** Agent Skills 官方命名规范:小写字母/数字/连字符,不允许首尾或连续连字符 */
|
|
216
|
-
const SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
217
|
-
function isValidSkillName(name) {
|
|
218
|
-
return name.length <= 64 && SKILL_NAME_PATTERN.test(name);
|
|
219
|
-
}
|
|
220
|
-
const SCRIPT_EXTENSION_RE = /\.(ts|js)$/;
|
|
221
|
-
/** D2 schema sidecar(scripts/<name>.schema.json)属于协议内文件,不算非法脚本 */
|
|
222
|
-
const SCHEMA_SIDECAR_RE = /\.schema\.json$/;
|
|
223
|
-
/**
|
|
224
|
-
* 技能合规规则的唯一实现。SkillDiscovery 与 validateSkills 都必须调用本函数,
|
|
225
|
-
* 禁止各自重复实现规则(避免两套规则漂移)。
|
|
226
|
-
*/
|
|
227
|
-
function checkSkillRules(input) {
|
|
228
|
-
const { dirName } = input;
|
|
229
|
-
if (!input.hasSkillMd) return [{
|
|
230
|
-
code: "SKILL_NOT_FOUND",
|
|
231
|
-
severity: "warning",
|
|
232
|
-
message: `Directory ${dirName} missing SKILL.md, not treated as a skill, skipped`
|
|
233
|
-
}];
|
|
234
|
-
if (input.parseError) return [{
|
|
235
|
-
code: input.parseError.code,
|
|
236
|
-
severity: "error",
|
|
237
|
-
message: `SKILL.md parse failed for directory ${dirName}: ${input.parseError.message}`
|
|
238
|
-
}];
|
|
239
|
-
const issues = [];
|
|
240
|
-
const metadata = input.metadata;
|
|
241
|
-
if (metadata) {
|
|
242
|
-
if (!isValidSkillName(metadata.name)) issues.push({
|
|
243
|
-
code: "SKILL_INVALID_NAME",
|
|
244
|
-
severity: "error",
|
|
245
|
-
message: `Skill name ${JSON.stringify(metadata.name)} does not conform to naming convention (lowercase letters/digits/hyphens, ≤64 chars)`
|
|
246
|
-
});
|
|
247
|
-
if (metadata.name !== dirName) issues.push({
|
|
248
|
-
code: "SKILL_INVALID_NAME",
|
|
249
|
-
severity: "error",
|
|
250
|
-
message: `Skill name ${JSON.stringify(metadata.name)} does not match directory name ${JSON.stringify(dirName)}`
|
|
251
|
-
});
|
|
252
|
-
if (input.existingNames?.has(metadata.name)) issues.push({
|
|
253
|
-
code: "SKILL_DUPLICATE_NAME",
|
|
254
|
-
severity: "error",
|
|
255
|
-
message: `Duplicate skill name ${JSON.stringify(metadata.name)}`
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
for (const fileName of input.scriptFileNames ?? []) if (!SCRIPT_EXTENSION_RE.test(fileName) && !SCHEMA_SIDECAR_RE.test(fileName)) issues.push({
|
|
259
|
-
code: "SKILL_UNSUPPORTED_SCRIPT",
|
|
260
|
-
severity: "error",
|
|
261
|
-
message: `Unsupported script file in scripts/ of directory ${dirName}: ${fileName} (only .ts/.js and .schema.json sidecars allowed)`
|
|
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
|
-
}
|
|
282
|
-
return issues;
|
|
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
|
-
}
|
|
394
|
-
/** 由条目列表构建 Catalog,保证按名称排序 */
|
|
395
|
-
function buildCatalog(entries) {
|
|
396
|
-
return { skills: [...entries].sort((a, b) => a.name.localeCompare(b.name)) };
|
|
397
|
-
}
|
|
398
|
-
/** 按技能名读取技能根目录内的文件,全部读取强制经过路径安全检查 */
|
|
399
|
-
var SkillReader = class {
|
|
400
|
-
#fs;
|
|
401
|
-
#index;
|
|
402
|
-
constructor(fs, index) {
|
|
403
|
-
this.#fs = fs;
|
|
404
|
-
this.#index = index;
|
|
405
|
-
}
|
|
406
|
-
#rootFor(skillName) {
|
|
407
|
-
const root = this.#index.get(skillName);
|
|
408
|
-
if (!root) throw new WebSkillError("SKILL_NOT_FOUND", `Unknown skill: ${skillName}`);
|
|
409
|
-
return root;
|
|
410
|
-
}
|
|
411
|
-
async readSkillMarkdown(skillName) {
|
|
412
|
-
return this.readSkillFile(skillName, "SKILL.md");
|
|
413
|
-
}
|
|
414
|
-
async readSkillFile(skillName, relativePath) {
|
|
415
|
-
return this.#fs.readText(resolveInsideRoot(this.#rootFor(skillName), relativePath));
|
|
416
|
-
}
|
|
417
|
-
async readSkillBinary(skillName, relativePath) {
|
|
418
|
-
return this.#fs.readBinary(resolveInsideRoot(this.#rootFor(skillName), relativePath));
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
const baseName$2 = (path) => path.split("/").pop() ?? path;
|
|
422
|
-
var SkillDiscovery = class {
|
|
423
|
-
#fs;
|
|
424
|
-
#roots;
|
|
425
|
-
/** name → skillRoot,与 SkillReader 共享同一 Map 引用 */
|
|
426
|
-
#index = /* @__PURE__ */ new Map();
|
|
427
|
-
#reader;
|
|
428
|
-
constructor(fs, roots) {
|
|
429
|
-
this.#fs = fs;
|
|
430
|
-
this.#roots = roots;
|
|
431
|
-
this.#reader = new SkillReader(fs, this.#index);
|
|
432
|
-
}
|
|
433
|
-
async discover() {
|
|
434
|
-
this.#index.clear();
|
|
435
|
-
const entries = [];
|
|
436
|
-
const issues = [];
|
|
437
|
-
const candidates = [];
|
|
438
|
-
const knownSkillNames = /* @__PURE__ */ new Set();
|
|
439
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
440
|
-
for (const root of this.#roots) {
|
|
441
|
-
if (!await this.#fs.exists(root)) {
|
|
442
|
-
issues.push({
|
|
443
|
-
code: "FS_NOT_FOUND",
|
|
444
|
-
severity: "warning",
|
|
445
|
-
message: `Skill root directory not found, skipped: ${root}`,
|
|
446
|
-
path: root
|
|
447
|
-
});
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
for (const child of await this.#fs.list(root)) {
|
|
451
|
-
if (child.type !== "directory") continue;
|
|
452
|
-
const dirName = baseName$2(child.path);
|
|
453
|
-
const skillRoot = `${root}/${dirName}`;
|
|
454
|
-
const skillMdPath = `${skillRoot}/SKILL.md`;
|
|
455
|
-
const hasSkillMd = await this.#fs.exists(skillMdPath);
|
|
456
|
-
let metadata;
|
|
457
|
-
let parseError;
|
|
458
|
-
if (hasSkillMd) try {
|
|
459
|
-
metadata = parseSkillMarkdown(await this.#fs.readText(skillMdPath)).metadata;
|
|
460
|
-
} catch (e) {
|
|
461
|
-
parseError = e instanceof WebSkillError ? e : new WebSkillError("SKILL_INVALID_METADATA", String(e));
|
|
462
|
-
}
|
|
463
|
-
candidates.push({
|
|
464
|
-
dirName,
|
|
465
|
-
skillRoot,
|
|
466
|
-
hasSkillMd,
|
|
467
|
-
metadata,
|
|
468
|
-
parseError,
|
|
469
|
-
scriptFileNames: await this.#listScriptFileNames(skillRoot)
|
|
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
|
-
}
|
|
476
|
-
}
|
|
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);
|
|
510
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
511
|
-
return {
|
|
512
|
-
entries,
|
|
513
|
-
issues
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
async catalog() {
|
|
517
|
-
const { entries, issues } = await this.discover();
|
|
518
|
-
return {
|
|
519
|
-
catalog: buildCatalog(entries),
|
|
520
|
-
issues
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
/** 按需加载技能完整文档;技能不存在时抛 SKILL_NOT_FOUND */
|
|
524
|
-
async loadDocument(skillName) {
|
|
525
|
-
if (this.#index.size === 0) await this.discover();
|
|
526
|
-
const { metadata, body } = parseSkillMarkdown(await this.#reader.readSkillMarkdown(skillName));
|
|
527
|
-
const skillRoot = this.#index.get(skillName);
|
|
528
|
-
return {
|
|
529
|
-
metadata,
|
|
530
|
-
body,
|
|
531
|
-
location: {
|
|
532
|
-
skillRoot,
|
|
533
|
-
skillMdPath: `${skillRoot}/SKILL.md`,
|
|
534
|
-
source: "local"
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
async #listScriptFileNames(skillRoot) {
|
|
539
|
-
const scriptsDir = `${skillRoot}/scripts`;
|
|
540
|
-
if (!await this.#fs.exists(scriptsDir)) return [];
|
|
541
|
-
return (await this.#fs.list(scriptsDir)).filter((s) => s.type === "file").map((s) => baseName$2(s.path));
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
/**
|
|
545
|
-
* 合规校验。内部直接复用 SkillDiscovery 的扫描逻辑,
|
|
546
|
-
* 规则判定同样落在 checkSkillRules 单一来源上。
|
|
547
|
-
*/
|
|
548
|
-
async function validateSkills(fs, roots) {
|
|
549
|
-
const { issues } = await new SkillDiscovery(fs, roots).discover();
|
|
550
|
-
return {
|
|
551
|
-
ok: !issues.some((i) => i.severity === "error"),
|
|
552
|
-
issues
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
function renderCatalogJson(catalog) {
|
|
556
|
-
return JSON.stringify(catalog, null, 2);
|
|
557
|
-
}
|
|
558
|
-
const jsonRenderer = {
|
|
559
|
-
format: "json",
|
|
560
|
-
render: renderCatalogJson
|
|
561
|
-
};
|
|
562
|
-
/** 转义 XML 五个特殊字符:& < > " ' */
|
|
563
|
-
function escapeXml(text) {
|
|
564
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
565
|
-
}
|
|
566
|
-
function renderAvailableSkillsXml(catalog) {
|
|
567
|
-
const lines = ["<available_skills>"];
|
|
568
|
-
for (const skill of catalog.skills) {
|
|
569
|
-
lines.push(" <skill>");
|
|
570
|
-
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
571
|
-
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
572
|
-
if (skill.version !== void 0) lines.push(` <version>${escapeXml(skill.version)}</version>`);
|
|
573
|
-
if (skill.license !== void 0) lines.push(` <license>${escapeXml(skill.license)}</license>`);
|
|
574
|
-
lines.push(` <source>${escapeXml(skill.source)}</source>`);
|
|
575
|
-
lines.push(" </skill>");
|
|
576
|
-
}
|
|
577
|
-
lines.push("</available_skills>");
|
|
578
|
-
return lines.join("\n");
|
|
579
|
-
}
|
|
580
|
-
const xmlRenderer = {
|
|
581
|
-
format: "xml",
|
|
582
|
-
render: renderAvailableSkillsXml
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
//#endregion
|
|
586
3
|
//#region ../runtime/dist/index.js
|
|
587
|
-
/**
|
|
588
|
-
* 预设响应序列的确定性 LlmClient;handler 形式可按会话状态动态应答。
|
|
589
|
-
* 默认不含 stream(存量行为);构造传 { streaming: true } 启用流式:
|
|
590
|
-
* 队列项 { stream: [...] } 按预设 delta 序列产出,LlmResponse 项自动合成增量。
|
|
591
|
-
*/
|
|
592
|
-
var MockLlmClient = class {
|
|
593
|
-
calls = [];
|
|
594
|
-
stream;
|
|
595
|
-
#queue;
|
|
596
|
-
constructor(responses, options) {
|
|
597
|
-
this.#queue = [...responses];
|
|
598
|
-
if (options?.streaming) this.stream = (input) => this.#streamImpl(input);
|
|
599
|
-
}
|
|
600
|
-
async complete(input) {
|
|
601
|
-
this.calls.push(input);
|
|
602
|
-
const next = this.#queue.shift();
|
|
603
|
-
if (!next) throw new WebSkillError("LLM_REQUEST_FAILED", "MockLlmClient: response queue exhausted");
|
|
604
|
-
if (typeof next === "function") return next(input);
|
|
605
|
-
if ("stream" in next) {
|
|
606
|
-
const events = typeof next.stream === "function" ? next.stream(input) : next.stream;
|
|
607
|
-
let content = "";
|
|
608
|
-
let toolCalls;
|
|
609
|
-
for (const event of events) if (event.type === "text-delta") content += event.delta;
|
|
610
|
-
else if (event.type === "tool-calls") toolCalls = event.toolCalls;
|
|
611
|
-
else if (event.type === "done" && event.content !== void 0) content = event.content;
|
|
612
|
-
return {
|
|
613
|
-
content,
|
|
614
|
-
toolCalls
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
return next;
|
|
618
|
-
}
|
|
619
|
-
async *#streamImpl(input) {
|
|
620
|
-
this.calls.push(input);
|
|
621
|
-
const next = this.#queue.shift();
|
|
622
|
-
if (!next) throw new WebSkillError("LLM_REQUEST_FAILED", "MockLlmClient: response queue exhausted");
|
|
623
|
-
if (typeof next !== "function" && "stream" in next) {
|
|
624
|
-
const events = typeof next.stream === "function" ? next.stream(input) : next.stream;
|
|
625
|
-
for (const event of events) yield event;
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
const response = typeof next === "function" ? await next(input) : next;
|
|
629
|
-
if (response.content) {
|
|
630
|
-
const chunkSize = Math.max(1, Math.ceil(response.content.length / 3));
|
|
631
|
-
for (let i = 0; i < response.content.length; i += chunkSize) yield {
|
|
632
|
-
type: "text-delta",
|
|
633
|
-
delta: response.content.slice(i, i + chunkSize)
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
if (response.toolCalls?.length) yield {
|
|
637
|
-
type: "tool-calls",
|
|
638
|
-
toolCalls: response.toolCalls
|
|
639
|
-
};
|
|
640
|
-
yield { type: "done" };
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
4
|
const toOpenAiMessage = (msg) => {
|
|
644
5
|
if (msg.role === "tool") return {
|
|
645
6
|
role: "tool",
|
|
@@ -919,7 +280,7 @@ var FullDisclosureRouter = class {
|
|
|
919
280
|
}
|
|
920
281
|
async route(catalog) {
|
|
921
282
|
const sections = [];
|
|
922
|
-
for (const entry of catalog.
|
|
283
|
+
for (const entry of catalog.entries) {
|
|
923
284
|
const doc = await this.#discovery.loadDocument(entry.name);
|
|
924
285
|
sections.push(`<skill name="${entry.name}">\n${doc.body}\n</skill>`);
|
|
925
286
|
}
|
|
@@ -1173,33 +534,6 @@ function mapFieldType(prop) {
|
|
|
1173
534
|
default: return "textarea";
|
|
1174
535
|
}
|
|
1175
536
|
}
|
|
1176
|
-
/**
|
|
1177
|
-
* 测试用 UiBridge:按请求 id 或类型预设应答(永不 resolve 的 handler 可模拟超时);
|
|
1178
|
-
* 记录全部请求供断言。
|
|
1179
|
-
*/
|
|
1180
|
-
var MockUiBridge = class {
|
|
1181
|
-
requests = [];
|
|
1182
|
-
#answers = /* @__PURE__ */ new Map();
|
|
1183
|
-
#fallback;
|
|
1184
|
-
/** key 为请求 id 或请求类型('ask' | 'confirm' | 'form' | 'select') */
|
|
1185
|
-
respondTo(key, answer) {
|
|
1186
|
-
this.#answers.set(key, answer);
|
|
1187
|
-
return this;
|
|
1188
|
-
}
|
|
1189
|
-
setFallback(answer) {
|
|
1190
|
-
this.#fallback = answer;
|
|
1191
|
-
return this;
|
|
1192
|
-
}
|
|
1193
|
-
async request(input) {
|
|
1194
|
-
this.requests.push(input);
|
|
1195
|
-
const answer = this.#answers.get(input.id) ?? this.#answers.get(input.type) ?? this.#fallback;
|
|
1196
|
-
if (!answer) return {
|
|
1197
|
-
id: input.id,
|
|
1198
|
-
cancelled: true
|
|
1199
|
-
};
|
|
1200
|
-
return typeof answer === "function" ? answer(input) : answer;
|
|
1201
|
-
}
|
|
1202
|
-
};
|
|
1203
537
|
/** 生命周期事件总线:只读观测,支持按阶段或通配订阅 */
|
|
1204
538
|
var EventBus = class {
|
|
1205
539
|
#listeners = /* @__PURE__ */ new Map();
|
|
@@ -1226,50 +560,6 @@ var EventBus = class {
|
|
|
1226
560
|
for (const listener of this.#listeners.get("*") ?? []) listener(frozen);
|
|
1227
561
|
}
|
|
1228
562
|
};
|
|
1229
|
-
/** 内存 ArtifactStore:测试与浏览器降级用;索引随进程生命周期存在 */
|
|
1230
|
-
var MemoryArtifactStore = class {
|
|
1231
|
-
#byRun = /* @__PURE__ */ new Map();
|
|
1232
|
-
#seq = 0;
|
|
1233
|
-
async createTextArtifact(input) {
|
|
1234
|
-
return this.#record({
|
|
1235
|
-
runId: input.runId,
|
|
1236
|
-
path: input.path,
|
|
1237
|
-
type: "text",
|
|
1238
|
-
size: new TextEncoder().encode(input.content).length,
|
|
1239
|
-
mimeType: input.mimeType,
|
|
1240
|
-
metadata: input.metadata
|
|
1241
|
-
});
|
|
1242
|
-
}
|
|
1243
|
-
async createBinaryArtifact(input) {
|
|
1244
|
-
return this.#record({
|
|
1245
|
-
runId: input.runId,
|
|
1246
|
-
path: input.path,
|
|
1247
|
-
type: "binary",
|
|
1248
|
-
size: input.content.length,
|
|
1249
|
-
mimeType: input.mimeType,
|
|
1250
|
-
metadata: input.metadata
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
async listArtifacts(runId) {
|
|
1254
|
-
return [...this.#byRun.get(runId) ?? []];
|
|
1255
|
-
}
|
|
1256
|
-
#record(input) {
|
|
1257
|
-
const artifact = {
|
|
1258
|
-
id: `art-${++this.#seq}`,
|
|
1259
|
-
runId: input.runId,
|
|
1260
|
-
path: input.path,
|
|
1261
|
-
type: input.type,
|
|
1262
|
-
mimeType: input.mimeType,
|
|
1263
|
-
size: input.size,
|
|
1264
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1265
|
-
metadata: input.metadata
|
|
1266
|
-
};
|
|
1267
|
-
const list = this.#byRun.get(input.runId) ?? [];
|
|
1268
|
-
list.push(artifact);
|
|
1269
|
-
this.#byRun.set(input.runId, list);
|
|
1270
|
-
return artifact;
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
563
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
1274
564
|
var TraceRecorder = class {
|
|
1275
565
|
#runId;
|
|
@@ -1378,11 +668,11 @@ var AgentLoop = class {
|
|
|
1378
668
|
if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
|
|
1379
669
|
const route = this.#deps.catalogFilter ? {
|
|
1380
670
|
...input.route,
|
|
1381
|
-
catalog: {
|
|
671
|
+
catalog: { entries: await this.#deps.catalogFilter(input.route.catalog.entries) }
|
|
1382
672
|
} : input.route;
|
|
1383
673
|
trace.record("skill.routed", { data: {
|
|
1384
674
|
strategy: route.strategy,
|
|
1385
|
-
skillCount: route.catalog.
|
|
675
|
+
skillCount: route.catalog.entries.length
|
|
1386
676
|
} });
|
|
1387
677
|
await this.#lifecycle("route", state, { strategy: route.strategy });
|
|
1388
678
|
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
@@ -2117,6 +1407,7 @@ const SNAPSHOT_SUFFIX = ".snapshot.json";
|
|
|
2117
1407
|
/**
|
|
2118
1408
|
* FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
|
|
2119
1409
|
* 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
|
|
1410
|
+
* @experimental
|
|
2120
1411
|
*/
|
|
2121
1412
|
var FsRunSnapshotStore = class {
|
|
2122
1413
|
#root;
|
|
@@ -2164,7 +1455,10 @@ var FsRunSnapshotStore = class {
|
|
|
2164
1455
|
return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
|
|
2165
1456
|
}
|
|
2166
1457
|
};
|
|
2167
|
-
/**
|
|
1458
|
+
/**
|
|
1459
|
+
* runtime 门面:组合 discovery / router / agent loop / lifecycle
|
|
1460
|
+
* @stable
|
|
1461
|
+
*/
|
|
2168
1462
|
var WebSkillRuntime = class {
|
|
2169
1463
|
#deps;
|
|
2170
1464
|
#discovery;
|
|
@@ -2208,8 +1502,8 @@ var WebSkillRuntime = class {
|
|
|
2208
1502
|
return [];
|
|
2209
1503
|
}
|
|
2210
1504
|
}))).flat();
|
|
2211
|
-
const catalog = providerEntries.length > 0 ? {
|
|
2212
|
-
const filteredCatalog = this.#deps.catalogFilter ? {
|
|
1505
|
+
const catalog = providerEntries.length > 0 ? { entries: mergeCatalogEntries(cache.catalog.entries, providerEntries) } : cache.catalog;
|
|
1506
|
+
const filteredCatalog = this.#deps.catalogFilter ? { entries: await this.#deps.catalogFilter(catalog.entries) } : catalog;
|
|
2213
1507
|
const route = await this.#router.route(filteredCatalog);
|
|
2214
1508
|
const result = await new AgentLoop({
|
|
2215
1509
|
llm: this.#deps.llm,
|
|
@@ -2257,6 +1551,7 @@ var WebSkillRuntime = class {
|
|
|
2257
1551
|
* D3 恢复 interrupted run:
|
|
2258
1552
|
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;schemaVersion 不匹配 → 删除 + RUN_SNAPSHOT_INCOMPATIBLE;
|
|
2259
1553
|
* 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
|
|
1554
|
+
* @experimental
|
|
2260
1555
|
*/
|
|
2261
1556
|
async resumeRun(runId) {
|
|
2262
1557
|
const store = this.#deps.snapshotStore;
|
|
@@ -2323,6 +1618,7 @@ var WebSkillRuntime = class {
|
|
|
2323
1618
|
/**
|
|
2324
1619
|
* navigator.webskill 门面装配层:零新业务逻辑,全部直转
|
|
2325
1620
|
* SkillDiscovery / validateSkills / WebSkillRuntime / SkillManager。
|
|
1621
|
+
* @stable
|
|
2326
1622
|
*/
|
|
2327
1623
|
function createWebSkillApi(deps) {
|
|
2328
1624
|
const { fs, roots } = deps;
|
|
@@ -2440,34 +1736,6 @@ var HookRunner = class {
|
|
|
2440
1736
|
});
|
|
2441
1737
|
}
|
|
2442
1738
|
};
|
|
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
1739
|
const encode = (s) => encodeURIComponent(s);
|
|
2472
1740
|
/**
|
|
2473
1741
|
* 基于 FileSystemProvider 的 MemoryStore:
|
|
@@ -2490,7 +1758,12 @@ var FsMemoryStore = class {
|
|
|
2490
1758
|
async get(scope, key) {
|
|
2491
1759
|
const path = this.#keyPath(scope, key);
|
|
2492
1760
|
if (!await this.#fs.exists(path)) return void 0;
|
|
2493
|
-
|
|
1761
|
+
try {
|
|
1762
|
+
return JSON.parse(await this.#fs.readText(path));
|
|
1763
|
+
} catch {
|
|
1764
|
+
await this.#fs.remove(path);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
2494
1767
|
}
|
|
2495
1768
|
async set(scope, key, value) {
|
|
2496
1769
|
await this.#fs.writeText(this.#keyPath(scope, key), JSON.stringify(value, null, 2));
|
|
@@ -2507,10 +1780,14 @@ var FsMemoryStore = class {
|
|
|
2507
1780
|
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
2508
1781
|
const fileName = entry.path.split("/").pop() ?? "";
|
|
2509
1782
|
const key = decodeURIComponent(fileName.replace(/\.json$/, ""));
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
1783
|
+
try {
|
|
1784
|
+
out.push({
|
|
1785
|
+
key,
|
|
1786
|
+
value: JSON.parse(await this.#fs.readText(entry.path))
|
|
1787
|
+
});
|
|
1788
|
+
} catch {
|
|
1789
|
+
await this.#fs.remove(entry.path);
|
|
1790
|
+
}
|
|
2514
1791
|
}
|
|
2515
1792
|
return out.sort((a, b) => a.key.localeCompare(b.key));
|
|
2516
1793
|
}
|
|
@@ -2558,7 +1835,12 @@ var FsArtifactStore = class {
|
|
|
2558
1835
|
const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
|
|
2559
1836
|
if (!await this.#fs.exists(indexPath)) return [];
|
|
2560
1837
|
const raw = await this.#fs.readText(indexPath);
|
|
2561
|
-
|
|
1838
|
+
try {
|
|
1839
|
+
return JSON.parse(raw).artifacts ?? [];
|
|
1840
|
+
} catch {
|
|
1841
|
+
await this.#fs.remove(indexPath);
|
|
1842
|
+
return [];
|
|
1843
|
+
}
|
|
2562
1844
|
}
|
|
2563
1845
|
#artifactPath(runId, artifactPath) {
|
|
2564
1846
|
return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
|
|
@@ -2630,6 +1912,7 @@ function bridgeError(id, code, message) {
|
|
|
2630
1912
|
* - 通配 '*.example.com'(含 apex 与任意深度子域)
|
|
2631
1913
|
* - 源 'http://localhost:3000'(协议 + host + port 全等)
|
|
2632
1914
|
* - URL 解析失败一律拒绝
|
|
1915
|
+
* @stable
|
|
2633
1916
|
*/
|
|
2634
1917
|
function isNetworkAllowed(policy, url) {
|
|
2635
1918
|
if (policy === "allow-all") return true;
|
|
@@ -2672,6 +1955,7 @@ function networkUrlHost(url) {
|
|
|
2672
1955
|
* Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
|
|
2673
1956
|
* 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
|
|
2674
1957
|
* once-per-run(默认)在 run 内记忆已批准能力,every-call 每次调用都问。
|
|
1958
|
+
* @stable
|
|
2675
1959
|
*/
|
|
2676
1960
|
var CapabilityApproval = class {
|
|
2677
1961
|
#uiBridge;
|
|
@@ -2709,4 +1993,4 @@ var CapabilityApproval = class {
|
|
|
2709
1993
|
};
|
|
2710
1994
|
|
|
2711
1995
|
//#endregion
|
|
2712
|
-
export {
|
|
1996
|
+
export { normalizeToolContent as A, createWebSkillApi as C, isNetworkAllowed as D, fromVercelStreamPart as E, toVercelToolSpecs as F, resolveToolName as M, schemaToForm as N, mergeCatalogEntries as O, toLlmToolSpec as P, createScriptContext as S, fromVercelResult as T, RUN_SNAPSHOT_SCHEMA_VERSION as _, CapabilityApproval as a, bridgeError as b, FsMemoryStore as c, HookRunner as d, OpenAiCompatibleClient as f, READ_SKILL_FILE_TOOL_NAME as g, READ_SKILL_FILE_TOOL as h, AgentLoop as i, parseBridgeRequest as j, networkUrlHost as k, FsRunSnapshotStore as l, READ_SKILL_FILE_INPUT_SCHEMA as m, ASK_USER_TOOL as n, EventBus as o, ProgressiveRouter as p, ASK_USER_TOOL_NAME as r, FsArtifactStore as s, ASK_USER_INPUT_SCHEMA as t, FullDisclosureRouter as u, TraceRecorder as v, extractChartSpec as w, buildRenderResult as x, WebSkillRuntime as y };
|