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