@webskill/sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/browser.d.ts +219 -0
- package/dist/browser.js +856 -0
- package/dist/dist-9fczg9Pa.js +1122 -0
- package/dist/dist-B_ldNWwT.js +1975 -0
- package/dist/dist-RcqvzAGF.js +15382 -0
- package/dist/governance.d.ts +440 -0
- package/dist/governance.js +844 -0
- package/dist/index-88KNOnbr.d.ts +955 -0
- package/dist/index-Bq-bTWh3.d.ts +219 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/mcp.d.ts +237 -0
- package/dist/mcp.js +513 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.js +4 -0
- package/dist/ui-react.d.ts +40 -0
- package/dist/ui-react.js +239 -0
- package/dist/ui-vue.d.ts +72 -0
- package/dist/ui-vue.js +196 -0
- package/dist/ui.d.ts +171 -0
- package/dist/ui.js +4 -0
- package/package.json +91 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import { H as isValidSkillName, J as resolveInsideRoot, R as WebSkillError, Y as validateSkills } from "./dist-B_ldNWwT.js";
|
|
2
|
+
import { i as NodeFS } from "./dist-9fczg9Pa.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { mkdtemp } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
//#region ../governance/dist/index.js
|
|
9
|
+
const invalid = (message, details) => {
|
|
10
|
+
throw new WebSkillError("CANDIDATE_INVALID", message, details);
|
|
11
|
+
};
|
|
12
|
+
/** 剥 markdown fence 与 <think> 块、截取首尾 {};非对象 → CANDIDATE_INVALID */
|
|
13
|
+
function parseJsonObject(raw) {
|
|
14
|
+
const fenced = raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```(?:json)?/gi, "");
|
|
15
|
+
const start = fenced.indexOf("{");
|
|
16
|
+
const end = fenced.lastIndexOf("}");
|
|
17
|
+
if (start < 0 || end <= start) invalid("LLM output does not contain a JSON object");
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(fenced.slice(start, end + 1));
|
|
21
|
+
} catch (e) {
|
|
22
|
+
invalid("LLM output is not valid JSON", e);
|
|
23
|
+
}
|
|
24
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) invalid("LLM output JSON is not an object");
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
/** 小写连字符化、≤64、过 isValidSkillName */
|
|
28
|
+
function sanitizeCandidateName(raw) {
|
|
29
|
+
const name = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 64);
|
|
30
|
+
if (!isValidSkillName(name)) invalid(`Candidate name ${JSON.stringify(raw)} cannot be normalized to a valid skill name`);
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
const kindOf = (path) => {
|
|
34
|
+
if (path === "SKILL.md") return "skill-md";
|
|
35
|
+
if (path.startsWith("scripts/")) return "script";
|
|
36
|
+
if (path.startsWith("references/")) return "reference";
|
|
37
|
+
return "asset";
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 文件归一:补 SKILL.md;路径含 `..` / 以 `/` 开头 / 含 `\` → CANDIDATE_INVALID;
|
|
41
|
+
* scripts/ 下必须是 .ts/.js。
|
|
42
|
+
*/
|
|
43
|
+
function normalizeCandidateFiles(files, name, description) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const p = file.path;
|
|
47
|
+
if (typeof p !== "string" || p === "" || p.includes("..") || p.startsWith("/") || p.includes("\\")) invalid(`Candidate file path is not allowed: ${JSON.stringify(p)}`);
|
|
48
|
+
const kind = kindOf(p);
|
|
49
|
+
if (kind === "script" && !/\.(ts|js)$/.test(p)) invalid(`Candidate script must be .ts or .js: ${p}`);
|
|
50
|
+
out.push({
|
|
51
|
+
path: p,
|
|
52
|
+
kind,
|
|
53
|
+
content: String(file.content ?? "")
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!out.some((f) => f.kind === "skill-md")) out.unshift({
|
|
57
|
+
path: "SKILL.md",
|
|
58
|
+
kind: "skill-md",
|
|
59
|
+
content: `---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`
|
|
60
|
+
});
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/** 含脚本且 LLM 自报 low → 强制 medium 并追加"必须人工审批"原因 */
|
|
64
|
+
function normalizeRisk(declared, files, reasons) {
|
|
65
|
+
const hasScript = files.some((f) => f.kind === "script");
|
|
66
|
+
const risk = declared === "low" || declared === "medium" || declared === "high" ? hasScript && declared === "low" ? "medium" : declared : hasScript ? "medium" : "low";
|
|
67
|
+
const out = [...reasons];
|
|
68
|
+
if (hasScript && declared === "low") out.push("Candidate contains scripts: risk forced to medium, human approval is required");
|
|
69
|
+
else if (hasScript && declared !== "low") out.push("Candidate contains executable scripts");
|
|
70
|
+
return {
|
|
71
|
+
risk,
|
|
72
|
+
reasons: out
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** 不信任归一化管线全链路;状态恒为 draft */
|
|
76
|
+
function normalizeCandidate(input) {
|
|
77
|
+
const { raw } = input;
|
|
78
|
+
const description = typeof raw["description"] === "string" ? raw["description"] : "";
|
|
79
|
+
if (description.trim() === "") invalid("Candidate is missing a description");
|
|
80
|
+
const name = sanitizeCandidateName(typeof raw["name"] === "string" ? raw["name"] : invalid("Candidate is missing a name"));
|
|
81
|
+
const files = normalizeCandidateFiles((Array.isArray(raw["files"]) ? raw["files"] : []).map((f) => ({
|
|
82
|
+
path: String(f?.["path"] ?? ""),
|
|
83
|
+
content: String(f?.["content"] ?? "")
|
|
84
|
+
})), name, description);
|
|
85
|
+
const declaredReasons = Array.isArray(raw["riskReasons"]) ? raw["riskReasons"].map(String) : [];
|
|
86
|
+
const { risk, reasons } = normalizeRisk(raw["risk"], files, declaredReasons);
|
|
87
|
+
const now = input.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
88
|
+
return {
|
|
89
|
+
id: input.createId?.() ?? `cand-${Math.random().toString(36).slice(2, 10)}`,
|
|
90
|
+
name,
|
|
91
|
+
description,
|
|
92
|
+
status: "draft",
|
|
93
|
+
source: input.source,
|
|
94
|
+
risk,
|
|
95
|
+
riskReasons: reasons,
|
|
96
|
+
files,
|
|
97
|
+
...Array.isArray(raw["suggestedTests"]) ? { suggestedTests: raw["suggestedTests"].map(String) } : {},
|
|
98
|
+
createdAt: now,
|
|
99
|
+
updatedAt: now
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** 候选校验:缺 SKILL.md / 非法扩展名 → CANDIDATE_INVALID */
|
|
103
|
+
function validateCandidate(candidate) {
|
|
104
|
+
if (!candidate.files.some((f) => f.kind === "skill-md")) throw new WebSkillError("CANDIDATE_INVALID", `Candidate "${candidate.name}" is missing SKILL.md`);
|
|
105
|
+
for (const file of candidate.files) if (file.kind === "script" && !/\.(ts|js)$/.test(file.path)) throw new WebSkillError("CANDIDATE_INVALID", `Candidate "${candidate.name}" has an unsupported script file: ${file.path}`);
|
|
106
|
+
}
|
|
107
|
+
const dirOf$1 = (root) => `${root}/.webskill/candidates`;
|
|
108
|
+
/** 逐文件持久化的候选存储:<managedRoot>/.webskill/candidates/<id>.json */
|
|
109
|
+
var CandidateStore = class {
|
|
110
|
+
#root;
|
|
111
|
+
#fs;
|
|
112
|
+
constructor(deps) {
|
|
113
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
114
|
+
this.#fs = deps.fs;
|
|
115
|
+
}
|
|
116
|
+
async save(candidate) {
|
|
117
|
+
validateCandidate(candidate);
|
|
118
|
+
await this.#fs.writeText(`${dirOf$1(this.#root)}/${candidate.id}.json`, JSON.stringify(candidate, null, 2));
|
|
119
|
+
}
|
|
120
|
+
async get(id) {
|
|
121
|
+
const path = `${dirOf$1(this.#root)}/${id}.json`;
|
|
122
|
+
if (!await this.#fs.exists(path)) throw new WebSkillError("GOVERNANCE_FAILED", `Candidate not found: ${id}`);
|
|
123
|
+
return JSON.parse(await this.#fs.readText(path));
|
|
124
|
+
}
|
|
125
|
+
async list() {
|
|
126
|
+
const dir = dirOf$1(this.#root);
|
|
127
|
+
if (!await this.#fs.exists(dir)) return [];
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const entry of await this.#fs.list(dir)) {
|
|
130
|
+
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
131
|
+
out.push(JSON.parse(await this.#fs.readText(entry.path)));
|
|
132
|
+
}
|
|
133
|
+
return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
134
|
+
}
|
|
135
|
+
async updateStatus(id, status, now) {
|
|
136
|
+
const candidate = await this.get(id);
|
|
137
|
+
candidate.status = status;
|
|
138
|
+
candidate.updatedAt = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
139
|
+
await this.save(candidate);
|
|
140
|
+
return candidate;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
/** 硬门禁:仅 published 可转换为 Catalog 条目,否则 APPROVAL_REQUIRED */
|
|
144
|
+
function candidateToCatalogEntry(candidate) {
|
|
145
|
+
if (candidate.status !== "published") throw new WebSkillError("APPROVAL_REQUIRED", `Candidate "${candidate.name}" is not published (status: ${candidate.status})`);
|
|
146
|
+
return {
|
|
147
|
+
name: candidate.name,
|
|
148
|
+
description: candidate.description,
|
|
149
|
+
root: `candidate://${candidate.id}`,
|
|
150
|
+
source: "generated",
|
|
151
|
+
hasScripts: candidate.files.some((f) => f.kind === "script"),
|
|
152
|
+
hasReferences: candidate.files.some((f) => f.kind === "reference"),
|
|
153
|
+
hasAssets: candidate.files.some((f) => f.kind === "asset")
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 文本补全:优先走流式(慢速本地模型下非流式整响应缓冲会触发 fetch 头超时,
|
|
158
|
+
* SSE 首包秒到不受此限);无 stream 的客户端回退 complete。
|
|
159
|
+
*/
|
|
160
|
+
async function completeText(llm, messages) {
|
|
161
|
+
if (llm.stream) {
|
|
162
|
+
let content = "";
|
|
163
|
+
let doneContent;
|
|
164
|
+
for await (const event of llm.stream({ messages })) if (event.type === "text-delta") content += event.delta;
|
|
165
|
+
else if (event.type === "done") doneContent = event.content;
|
|
166
|
+
return doneContent ?? content;
|
|
167
|
+
}
|
|
168
|
+
return (await llm.complete({ messages })).content ?? "";
|
|
169
|
+
}
|
|
170
|
+
const GENERATION_PROMPT = (task, context) => [
|
|
171
|
+
"You generate a skill candidate as STRICT JSON only (no markdown, no explanation).",
|
|
172
|
+
"Schema: {\"name\": string, \"description\": string, \"risk\": \"low\"|\"medium\"|\"high\",",
|
|
173
|
+
" \"riskReasons\": string[], \"files\": [{\"path\": string, \"content\": string}], \"suggestedTests\": string[]}",
|
|
174
|
+
"Rules:",
|
|
175
|
+
"- The candidate is ALWAYS a draft. Do NOT claim approved or published status.",
|
|
176
|
+
"- \"name\" must be lowercase letters/digits/hyphens.",
|
|
177
|
+
"- Include a SKILL.md file with YAML frontmatter (name, description) and a Markdown body.",
|
|
178
|
+
"- Script files must live under scripts/ and end with .ts or .js.",
|
|
179
|
+
"- Keep the candidate MINIMAL: at most 2 files, each under 60 lines. Short contents only.",
|
|
180
|
+
context ? `- Context: ${context}` : "",
|
|
181
|
+
`Task: ${task}`
|
|
182
|
+
].filter(Boolean).join("\n");
|
|
183
|
+
/** LLM 候选生成器:prompt 强制约束 + 不信任归一化管线 + candidate.created 审计 */
|
|
184
|
+
var LlmCandidateGenerator = class {
|
|
185
|
+
#llm;
|
|
186
|
+
#audit;
|
|
187
|
+
#now;
|
|
188
|
+
#createId;
|
|
189
|
+
constructor(deps) {
|
|
190
|
+
this.#llm = deps.llm;
|
|
191
|
+
this.#audit = deps.audit;
|
|
192
|
+
this.#now = deps.now;
|
|
193
|
+
this.#createId = deps.createId;
|
|
194
|
+
}
|
|
195
|
+
async generate(input) {
|
|
196
|
+
const candidate = normalizeCandidate({
|
|
197
|
+
raw: parseJsonObject(await completeText(this.#llm, [{
|
|
198
|
+
role: "user",
|
|
199
|
+
content: GENERATION_PROMPT(input.prompt, input.context)
|
|
200
|
+
}])),
|
|
201
|
+
source: input.source,
|
|
202
|
+
...this.#now ? { now: this.#now } : {},
|
|
203
|
+
...this.#createId ? { createId: this.#createId } : {}
|
|
204
|
+
});
|
|
205
|
+
await this.#audit?.append({
|
|
206
|
+
type: "candidate.created",
|
|
207
|
+
target: candidate.id,
|
|
208
|
+
data: {
|
|
209
|
+
name: candidate.name,
|
|
210
|
+
source: candidate.source,
|
|
211
|
+
risk: candidate.risk
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return candidate;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
/** 默认策略:任何候选都必须人工审批 */
|
|
218
|
+
var AlwaysHumanApprovalPolicy = class {
|
|
219
|
+
evaluate(candidate) {
|
|
220
|
+
return {
|
|
221
|
+
needsHuman: true,
|
|
222
|
+
reason: `Candidate "${candidate.name}" requires human approval (risk: ${candidate.risk})`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
/** 规则组合策略:首个命中的规则胜出,全部未命中走 fallback(默认 AlwaysHuman) */
|
|
227
|
+
var CompositeApprovalPolicy = class {
|
|
228
|
+
#rules;
|
|
229
|
+
#fallback;
|
|
230
|
+
constructor(rules, fallback) {
|
|
231
|
+
this.#rules = rules;
|
|
232
|
+
this.#fallback = fallback ?? new AlwaysHumanApprovalPolicy();
|
|
233
|
+
}
|
|
234
|
+
evaluate(candidate) {
|
|
235
|
+
for (const rule of this.#rules) {
|
|
236
|
+
const decision = rule(candidate);
|
|
237
|
+
if (decision) return decision;
|
|
238
|
+
}
|
|
239
|
+
return this.#fallback.evaluate(candidate);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
243
|
+
/** 审批工作流:review(UiBridge confirm 真实接线)/ publish(校验→安装→版本→审计) */
|
|
244
|
+
var ApprovalWorkflow = class {
|
|
245
|
+
#policy;
|
|
246
|
+
#audit;
|
|
247
|
+
#store;
|
|
248
|
+
#skillManager;
|
|
249
|
+
#versions;
|
|
250
|
+
#fs;
|
|
251
|
+
constructor(deps) {
|
|
252
|
+
this.#policy = deps.policy;
|
|
253
|
+
this.#audit = deps.audit;
|
|
254
|
+
this.#store = deps.store;
|
|
255
|
+
this.#skillManager = deps.skillManager;
|
|
256
|
+
this.#versions = deps.versions;
|
|
257
|
+
this.#fs = deps.fs ?? new NodeFS();
|
|
258
|
+
}
|
|
259
|
+
/** 策略评估;needs-human 时经 UiBridge confirm 真实询问,按应答迁移状态 */
|
|
260
|
+
async review(candidateId, input) {
|
|
261
|
+
const candidate = await this.#store.get(candidateId);
|
|
262
|
+
if (candidate.status !== "draft" && candidate.status !== "pending-review") throw new WebSkillError("GOVERNANCE_FAILED", `Candidate "${candidateId}" cannot be reviewed from status "${candidate.status}"`);
|
|
263
|
+
const decision = this.#policy.evaluate(candidate);
|
|
264
|
+
let approved;
|
|
265
|
+
if (decision.needsHuman) {
|
|
266
|
+
if (!input.uiBridge) {
|
|
267
|
+
await this.#store.updateStatus(candidateId, "pending-review");
|
|
268
|
+
throw new WebSkillError("APPROVAL_REQUIRED", `Candidate "${candidate.name}" requires human approval: ${decision.reason}`);
|
|
269
|
+
}
|
|
270
|
+
await this.#store.updateStatus(candidateId, "pending-review");
|
|
271
|
+
const response = await input.uiBridge.request({
|
|
272
|
+
type: "confirm",
|
|
273
|
+
id: `approval-${candidateId}`,
|
|
274
|
+
message: `Approve candidate "${candidate.name}" (risk: ${candidate.risk})? ${decision.reason}`,
|
|
275
|
+
defaultValue: false
|
|
276
|
+
});
|
|
277
|
+
approved = response.cancelled !== true && response.value === true;
|
|
278
|
+
} else approved = true;
|
|
279
|
+
const updated = await this.#store.updateStatus(candidateId, approved ? "approved" : "rejected");
|
|
280
|
+
await this.#audit.append({
|
|
281
|
+
type: "candidate.reviewed",
|
|
282
|
+
target: candidateId,
|
|
283
|
+
actor: input.actor,
|
|
284
|
+
data: {
|
|
285
|
+
approved,
|
|
286
|
+
reason: decision.reason
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return updated;
|
|
290
|
+
}
|
|
291
|
+
/** publish 全链路:approved 前置 → 写出 staging → validateSkills → install → 版本 → 审计 */
|
|
292
|
+
async publish(candidateId, input) {
|
|
293
|
+
const candidate = await this.#store.get(candidateId);
|
|
294
|
+
if (candidate.status !== "approved") throw new WebSkillError("APPROVAL_REQUIRED", `Candidate "${candidate.name}" must be approved before publishing (status: ${candidate.status})`);
|
|
295
|
+
const stagingRoot = (await mkdtemp(path.join(tmpdir(), "webskill-candidate-"))).split(path.sep).join("/");
|
|
296
|
+
try {
|
|
297
|
+
const skillDir = `${stagingRoot}/${candidate.name}`;
|
|
298
|
+
for (const file of candidate.files) await this.#fs.writeText(resolveInsideRoot(skillDir, file.path), file.content);
|
|
299
|
+
const report = await validateSkills(this.#fs, [stagingRoot]);
|
|
300
|
+
if (!report.ok) {
|
|
301
|
+
const errors = report.issues.filter((i) => i.severity === "error");
|
|
302
|
+
throw new WebSkillError("GOVERNANCE_FAILED", `Candidate "${candidate.name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
303
|
+
}
|
|
304
|
+
const manifest = await this.#skillManager.install({
|
|
305
|
+
type: "local",
|
|
306
|
+
path: skillDir
|
|
307
|
+
});
|
|
308
|
+
await this.#versions.add(candidate.name, {
|
|
309
|
+
reason: `Publish candidate ${candidateId}`,
|
|
310
|
+
manifest
|
|
311
|
+
});
|
|
312
|
+
await this.#store.updateStatus(candidateId, "published");
|
|
313
|
+
await this.#audit.append({
|
|
314
|
+
type: "skill.published",
|
|
315
|
+
target: candidate.name,
|
|
316
|
+
actor: input.actor,
|
|
317
|
+
data: {
|
|
318
|
+
candidateId,
|
|
319
|
+
digest: manifest.integrity.digest
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
return manifest;
|
|
323
|
+
} catch (e) {
|
|
324
|
+
if (e instanceof WebSkillError) throw e;
|
|
325
|
+
throw new WebSkillError("GOVERNANCE_FAILED", `Failed to publish candidate "${candidateId}": ${messageOf$1(e)}`, e);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const fileOf$1 = (root) => `${root}/.webskill/audit.jsonl`;
|
|
330
|
+
/** JSONL 追加的审计日志:<managedRoot>/.webskill/audit.jsonl;跨实例可恢复查询 */
|
|
331
|
+
var FsAuditLog = class {
|
|
332
|
+
#root;
|
|
333
|
+
#fs;
|
|
334
|
+
#now;
|
|
335
|
+
#createId;
|
|
336
|
+
constructor(deps) {
|
|
337
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
338
|
+
this.#fs = deps.fs;
|
|
339
|
+
this.#now = deps.now;
|
|
340
|
+
this.#createId = deps.createId;
|
|
341
|
+
}
|
|
342
|
+
async append(event) {
|
|
343
|
+
const full = {
|
|
344
|
+
id: event.id ?? this.#createId?.() ?? `audit-${Math.random().toString(36).slice(2, 10)}`,
|
|
345
|
+
ts: event.ts ?? this.#now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
346
|
+
type: event.type,
|
|
347
|
+
target: event.target,
|
|
348
|
+
...event.actor !== void 0 ? { actor: event.actor } : {},
|
|
349
|
+
...event.data !== void 0 ? { data: event.data } : {}
|
|
350
|
+
};
|
|
351
|
+
const path = fileOf$1(this.#root);
|
|
352
|
+
const existing = await this.#fs.exists(path) ? await this.#fs.readText(path) : "";
|
|
353
|
+
const prefix = existing === "" || existing.endsWith("\n") ? existing : `${existing}\n`;
|
|
354
|
+
await this.#fs.writeText(path, `${prefix}${JSON.stringify(full)}\n`);
|
|
355
|
+
return full;
|
|
356
|
+
}
|
|
357
|
+
async query(filter) {
|
|
358
|
+
const path = fileOf$1(this.#root);
|
|
359
|
+
if (!await this.#fs.exists(path)) return [];
|
|
360
|
+
const raw = await this.#fs.readText(path);
|
|
361
|
+
const events = [];
|
|
362
|
+
for (const line of raw.split("\n")) {
|
|
363
|
+
if (line.trim() === "") continue;
|
|
364
|
+
const event = JSON.parse(line);
|
|
365
|
+
if (filter.target !== void 0 && event.target !== filter.target) continue;
|
|
366
|
+
if (filter.type !== void 0 && event.type !== filter.type) continue;
|
|
367
|
+
if (filter.since !== void 0 && event.ts < filter.since) continue;
|
|
368
|
+
events.push(event);
|
|
369
|
+
}
|
|
370
|
+
return events;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const dirOf = (root, skillName) => `${root}/.webskill/versions/${skillName}`;
|
|
374
|
+
/** 版本存储:manifest 快照 + parentVersionId 链;回滚 = 追加新版本(谱系不断) */
|
|
375
|
+
var SkillVersionStore = class {
|
|
376
|
+
#root;
|
|
377
|
+
#fs;
|
|
378
|
+
#now;
|
|
379
|
+
#createId;
|
|
380
|
+
#audit;
|
|
381
|
+
constructor(deps) {
|
|
382
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
383
|
+
this.#fs = deps.fs;
|
|
384
|
+
this.#now = deps.now;
|
|
385
|
+
this.#createId = deps.createId;
|
|
386
|
+
this.#audit = deps.audit;
|
|
387
|
+
}
|
|
388
|
+
async add(skillName, input) {
|
|
389
|
+
const existing = await this.list(skillName);
|
|
390
|
+
const version = {
|
|
391
|
+
versionId: this.#createId?.() ?? `v-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
392
|
+
parentVersionId: input.parentVersionId ?? existing.at(-1)?.versionId,
|
|
393
|
+
createdAt: this.#now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
394
|
+
reason: input.reason,
|
|
395
|
+
manifest: input.manifest
|
|
396
|
+
};
|
|
397
|
+
await this.#fs.writeText(`${dirOf(this.#root, skillName)}/${version.versionId}.json`, JSON.stringify(version, null, 2));
|
|
398
|
+
return version;
|
|
399
|
+
}
|
|
400
|
+
async list(skillName) {
|
|
401
|
+
const dir = dirOf(this.#root, skillName);
|
|
402
|
+
if (!await this.#fs.exists(dir)) return [];
|
|
403
|
+
const out = [];
|
|
404
|
+
for (const entry of await this.#fs.list(dir)) {
|
|
405
|
+
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
406
|
+
out.push(JSON.parse(await this.#fs.readText(entry.path)));
|
|
407
|
+
}
|
|
408
|
+
return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
409
|
+
}
|
|
410
|
+
async get(skillName, versionId) {
|
|
411
|
+
const path = `${dirOf(this.#root, skillName)}/${versionId}.json`;
|
|
412
|
+
if (!await this.#fs.exists(path)) throw new WebSkillError("GOVERNANCE_FAILED", `Version "${versionId}" of skill "${skillName}" not found`);
|
|
413
|
+
return JSON.parse(await this.#fs.readText(path));
|
|
414
|
+
}
|
|
415
|
+
/** 回滚:基于旧 manifest 追加新版本 + skill.rolled_back 审计 */
|
|
416
|
+
async rollback(skillName, targetVersionId, input) {
|
|
417
|
+
const target = await this.get(skillName, targetVersionId);
|
|
418
|
+
const auditEvent = await this.#audit?.append({
|
|
419
|
+
type: "skill.rolled_back",
|
|
420
|
+
target: skillName,
|
|
421
|
+
...input.actor !== void 0 ? { actor: input.actor } : {},
|
|
422
|
+
data: { targetVersionId }
|
|
423
|
+
});
|
|
424
|
+
return this.add(skillName, {
|
|
425
|
+
reason: input.reason ?? `Rollback to ${targetVersionId}`,
|
|
426
|
+
manifest: target.manifest,
|
|
427
|
+
...auditEvent ? { auditEventId: auditEvent.id } : {}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const ERROR_CAUSES = {
|
|
432
|
+
TOOL_NOT_FOUND: "The skill script or tool is missing",
|
|
433
|
+
TOOL_EXECUTION_FAILED: "The script failed at runtime",
|
|
434
|
+
TOOL_UNSUPPORTED: "A required capability or format is unsupported",
|
|
435
|
+
RUN_TIMEOUT: "The script or loop timed out",
|
|
436
|
+
FS_NOT_FOUND: "A required file is missing",
|
|
437
|
+
FS_PATH_OUTSIDE_ROOT: "The script attempted an out-of-root path access",
|
|
438
|
+
LLM_REQUEST_FAILED: "The LLM endpoint failed",
|
|
439
|
+
SKILL_INVALID_METADATA: "The skill metadata is invalid"
|
|
440
|
+
};
|
|
441
|
+
/** 失败诊断:trace 摘要 → LLM 诊断(JSON);无 LLM 时规则版(错误码归类) */
|
|
442
|
+
var FailureAnalyzer = class {
|
|
443
|
+
#llm;
|
|
444
|
+
constructor(deps) {
|
|
445
|
+
this.#llm = deps?.llm;
|
|
446
|
+
}
|
|
447
|
+
async analyze(input) {
|
|
448
|
+
const { run } = input;
|
|
449
|
+
const failed = run.trace.filter((e) => e.type === "tool.failed" || e.type === "run.failed");
|
|
450
|
+
const firstCode = failed.map((e) => e.data?.["code"]).find((c) => typeof c === "string");
|
|
451
|
+
if (this.#llm) {
|
|
452
|
+
const summary = failed.slice(0, 10).map((e) => `${e.type}: ${e.message ?? ""} ${JSON.stringify(e.data ?? {})}`).join("\n");
|
|
453
|
+
const response = await this.#llm.complete({ messages: [{
|
|
454
|
+
role: "user",
|
|
455
|
+
content: [
|
|
456
|
+
"Diagnose this failed agent run as STRICT JSON only:",
|
|
457
|
+
"{\"cause\": string, \"suggestedFix\": string, \"errorCode\"?: string}",
|
|
458
|
+
`Run termination: ${run.terminationReason ?? "unknown"}`,
|
|
459
|
+
`Failures:\n${summary || "(none recorded)"}`
|
|
460
|
+
].join("\n")
|
|
461
|
+
}] });
|
|
462
|
+
try {
|
|
463
|
+
const parsed = parseJsonObject(response.content ?? "");
|
|
464
|
+
return {
|
|
465
|
+
cause: String(parsed["cause"] ?? "Unknown cause"),
|
|
466
|
+
suggestedFix: String(parsed["suggestedFix"] ?? "Manual inspection required"),
|
|
467
|
+
...typeof parsed["errorCode"] === "string" ? { errorCode: parsed["errorCode"] } : firstCode ? { errorCode: firstCode } : {}
|
|
468
|
+
};
|
|
469
|
+
} catch {}
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
cause: firstCode ? ERROR_CAUSES[firstCode] ?? `Failure with code ${firstCode}` : "Unknown failure (no error code in trace)",
|
|
473
|
+
suggestedFix: firstCode ? `Inspect the component responsible for ${firstCode}` : "Inspect the run trace manually",
|
|
474
|
+
...firstCode ? { errorCode: firstCode } : {}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* 修订选项规划:patch(按诊断给具体文件变更)/ rollback(有旧版本时)/ quarantine。
|
|
480
|
+
* 注意:任何 option 的 apply 都必须经 ApprovalWorkflow 审批后执行。
|
|
481
|
+
*/
|
|
482
|
+
var RepairPlanner = class {
|
|
483
|
+
plan(input) {
|
|
484
|
+
const { diagnosis, skillName, versions } = input;
|
|
485
|
+
const options = [{
|
|
486
|
+
kind: "patch",
|
|
487
|
+
description: `Patch "${skillName}": ${diagnosis.suggestedFix}`,
|
|
488
|
+
patch: [{
|
|
489
|
+
path: "SKILL.md",
|
|
490
|
+
content: `<!-- repair patch for: ${diagnosis.cause} -->\n<!-- ${diagnosis.suggestedFix} -->\n`
|
|
491
|
+
}]
|
|
492
|
+
}];
|
|
493
|
+
const previous = versions.at(-2) ?? versions.at(-1);
|
|
494
|
+
if (previous) options.push({
|
|
495
|
+
kind: "rollback",
|
|
496
|
+
description: `Rollback "${skillName}" to version ${previous.versionId}`,
|
|
497
|
+
targetVersionId: previous.versionId
|
|
498
|
+
});
|
|
499
|
+
options.push({
|
|
500
|
+
kind: "quarantine",
|
|
501
|
+
description: `Quarantine "${skillName}" until it is repaired (cause: ${diagnosis.cause})`
|
|
502
|
+
});
|
|
503
|
+
return options;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const fileOf = (root) => `${root}/.webskill/states.json`;
|
|
507
|
+
/**
|
|
508
|
+
* 降级治理:states.json 持久化;失败达阈值 → quarantined;
|
|
509
|
+
* canRoute 仅 active;canExecute active|deprecated;disabled 执行抛 SKILL_DISABLED;
|
|
510
|
+
* 卸载/状态变更经 UiBridge confirm 审批。
|
|
511
|
+
*/
|
|
512
|
+
var SkillStatePolicy = class {
|
|
513
|
+
#root;
|
|
514
|
+
#fs;
|
|
515
|
+
#audit;
|
|
516
|
+
#failureThreshold;
|
|
517
|
+
#loaded;
|
|
518
|
+
constructor(deps) {
|
|
519
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
520
|
+
this.#fs = deps.fs;
|
|
521
|
+
this.#audit = deps.audit;
|
|
522
|
+
this.#failureThreshold = deps.failureThreshold ?? 3;
|
|
523
|
+
}
|
|
524
|
+
async #load() {
|
|
525
|
+
if (this.#loaded) return this.#loaded;
|
|
526
|
+
const path = fileOf(this.#root);
|
|
527
|
+
this.#loaded = await this.#fs.exists(path) ? JSON.parse(await this.#fs.readText(path)) : {
|
|
528
|
+
states: {},
|
|
529
|
+
failures: {}
|
|
530
|
+
};
|
|
531
|
+
return this.#loaded;
|
|
532
|
+
}
|
|
533
|
+
async #persist() {
|
|
534
|
+
const data = await this.#load();
|
|
535
|
+
await this.#fs.writeText(fileOf(this.#root), JSON.stringify(data, null, 2));
|
|
536
|
+
}
|
|
537
|
+
async getState(skillName) {
|
|
538
|
+
return (await this.#load()).states[skillName] ?? "active";
|
|
539
|
+
}
|
|
540
|
+
async listStates() {
|
|
541
|
+
return { ...(await this.#load()).states };
|
|
542
|
+
}
|
|
543
|
+
async setState(skillName, state, input) {
|
|
544
|
+
const data = await this.#load();
|
|
545
|
+
data.states[skillName] = state;
|
|
546
|
+
if (state === "active") delete data.failures[skillName];
|
|
547
|
+
await this.#persist();
|
|
548
|
+
await this.#audit?.append({
|
|
549
|
+
type: `skill.${state === "active" ? "reactivated" : state === "quarantined" ? "quarantined" : state === "deprecated" ? "deprecated" : "disabled"}`,
|
|
550
|
+
target: skillName,
|
|
551
|
+
...input.actor !== void 0 ? { actor: input.actor } : {},
|
|
552
|
+
...input.reason !== void 0 ? { data: { reason: input.reason } } : {}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/** 失败计数;达阈值(默认 3)→ quarantined + 审计 */
|
|
556
|
+
async recordFailure(skillName, input = {}) {
|
|
557
|
+
const data = await this.#load();
|
|
558
|
+
const count = (data.failures[skillName] ?? 0) + 1;
|
|
559
|
+
data.failures[skillName] = count;
|
|
560
|
+
let state = data.states[skillName] ?? "active";
|
|
561
|
+
if (count >= this.#failureThreshold && state === "active") {
|
|
562
|
+
state = "quarantined";
|
|
563
|
+
data.states[skillName] = "quarantined";
|
|
564
|
+
await this.#audit?.append({
|
|
565
|
+
type: "skill.quarantined",
|
|
566
|
+
target: skillName,
|
|
567
|
+
...input.actor !== void 0 ? { actor: input.actor } : {},
|
|
568
|
+
data: {
|
|
569
|
+
failures: count,
|
|
570
|
+
threshold: this.#failureThreshold
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
await this.#persist();
|
|
575
|
+
return state;
|
|
576
|
+
}
|
|
577
|
+
canRoute(state) {
|
|
578
|
+
return state === "active";
|
|
579
|
+
}
|
|
580
|
+
canExecute(state) {
|
|
581
|
+
return state === "active" || state === "deprecated";
|
|
582
|
+
}
|
|
583
|
+
/** disabled → SKILL_DISABLED;quarantined → SKILL_QUARANTINED */
|
|
584
|
+
async assertExecutable(skillName) {
|
|
585
|
+
const state = await this.getState(skillName);
|
|
586
|
+
if (state === "disabled") throw new WebSkillError("SKILL_DISABLED", `Skill "${skillName}" is disabled`);
|
|
587
|
+
if (state === "quarantined") throw new WebSkillError("SKILL_QUARANTINED", `Skill "${skillName}" is quarantined`);
|
|
588
|
+
}
|
|
589
|
+
/** runtime catalogFilter 实现:quarantined/deprecated/disabled 技能被路由过滤 */
|
|
590
|
+
catalogFilter() {
|
|
591
|
+
return async (entries) => {
|
|
592
|
+
const out = [];
|
|
593
|
+
for (const entry of entries) if (this.canRoute(await this.getState(entry.name))) out.push(entry);
|
|
594
|
+
return out;
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/** 卸载需显式审批(UiBridge confirm 真实接线);拒绝 → APPROVAL_REQUIRED */
|
|
598
|
+
async uninstall(skillName, input) {
|
|
599
|
+
const response = await input.uiBridge.request({
|
|
600
|
+
type: "confirm",
|
|
601
|
+
id: `uninstall-${skillName}`,
|
|
602
|
+
message: `Uninstall skill "${skillName}"? This removes it from the managed root.`,
|
|
603
|
+
defaultValue: false
|
|
604
|
+
});
|
|
605
|
+
if (response.cancelled === true || response.value !== true) throw new WebSkillError("APPROVAL_REQUIRED", `Uninstall of skill "${skillName}" was rejected by ${input.actor}`);
|
|
606
|
+
await input.skillManager.uninstall(skillName);
|
|
607
|
+
const data = await this.#load();
|
|
608
|
+
delete data.states[skillName];
|
|
609
|
+
delete data.failures[skillName];
|
|
610
|
+
await this.#persist();
|
|
611
|
+
await this.#audit?.append({
|
|
612
|
+
type: "skill.uninstalled",
|
|
613
|
+
target: skillName,
|
|
614
|
+
actor: input.actor
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
619
|
+
function matchExpected(expected, output, run) {
|
|
620
|
+
if (expected === void 0) return run.status === "completed";
|
|
621
|
+
if (typeof expected === "string") return output.includes(expected);
|
|
622
|
+
return expected(output, run);
|
|
623
|
+
}
|
|
624
|
+
/** 批量评估:顺序执行(单任务异常不中断批);expected 参与判定;结构化报告 */
|
|
625
|
+
var EvaluationRunner = class {
|
|
626
|
+
#runtime;
|
|
627
|
+
#now;
|
|
628
|
+
#createId;
|
|
629
|
+
constructor(deps) {
|
|
630
|
+
this.#runtime = deps.runtime;
|
|
631
|
+
this.#now = deps.now;
|
|
632
|
+
this.#createId = deps.createId;
|
|
633
|
+
}
|
|
634
|
+
async run(tasks) {
|
|
635
|
+
const results = [];
|
|
636
|
+
for (const task of tasks) try {
|
|
637
|
+
const { output, run } = await this.#runtime.run(task.prompt);
|
|
638
|
+
const ok = matchExpected(task.expected, output, run);
|
|
639
|
+
results.push({
|
|
640
|
+
taskId: task.id,
|
|
641
|
+
ok,
|
|
642
|
+
score: ok ? 1 : 0,
|
|
643
|
+
output
|
|
644
|
+
});
|
|
645
|
+
} catch (e) {
|
|
646
|
+
results.push({
|
|
647
|
+
taskId: task.id,
|
|
648
|
+
ok: false,
|
|
649
|
+
score: 0,
|
|
650
|
+
error: messageOf(e)
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
const passed = results.filter((r) => r.ok).length;
|
|
654
|
+
const scores = results.map((r) => r.score);
|
|
655
|
+
return {
|
|
656
|
+
id: this.#createId?.() ?? `eval-${Math.random().toString(36).slice(2, 10)}`,
|
|
657
|
+
generatedAt: this.#now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
658
|
+
results,
|
|
659
|
+
summary: {
|
|
660
|
+
total: results.length,
|
|
661
|
+
passed,
|
|
662
|
+
failed: results.length - passed,
|
|
663
|
+
averageScore: scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : void 0
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
/** 失败 trace → 回归评估任务建议(prompt 复现 + expected 错误模式) */
|
|
669
|
+
function suggestFromFailedRun(run) {
|
|
670
|
+
const errorPatterns = run.trace.filter((e) => e.type === "tool.failed" || e.type === "run.failed").map((e) => String(e.data?.["code"] ?? e.message ?? "")).filter(Boolean);
|
|
671
|
+
return {
|
|
672
|
+
id: `regression-${run.id}`,
|
|
673
|
+
prompt: run.userPrompt,
|
|
674
|
+
expected: (output, rerun) => rerun.status === "completed" && !rerun.trace.some((e) => (e.type === "tool.failed" || e.type === "run.failed") && errorPatterns.includes(String(e.data?.["code"] ?? ""))),
|
|
675
|
+
metadata: {
|
|
676
|
+
source: "test-suggestion",
|
|
677
|
+
failureReason: run.terminationReason ?? "unknown",
|
|
678
|
+
errorPatterns
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const SCORING_WEIGHTS = {
|
|
683
|
+
success: .5,
|
|
684
|
+
usage: .2,
|
|
685
|
+
eval: .2,
|
|
686
|
+
risk: .1
|
|
687
|
+
};
|
|
688
|
+
const RISK_SCORE = {
|
|
689
|
+
low: 0,
|
|
690
|
+
medium: .5,
|
|
691
|
+
high: 1
|
|
692
|
+
};
|
|
693
|
+
const clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
694
|
+
/**
|
|
695
|
+
* 综合评分:successRate*W_SUCCESS + usageNorm*W_USAGE + evalScore*W_EVAL − risk*W_RISK
|
|
696
|
+
* (clamp [0,1]);低于阈值给 improve 建议。usage 数据来自 skill:{name} memory scope。
|
|
697
|
+
*/
|
|
698
|
+
var SkillScorer = class {
|
|
699
|
+
#weights;
|
|
700
|
+
#improveThreshold;
|
|
701
|
+
constructor(weights = SCORING_WEIGHTS, improveThreshold = .5) {
|
|
702
|
+
this.#weights = weights;
|
|
703
|
+
this.#improveThreshold = improveThreshold;
|
|
704
|
+
}
|
|
705
|
+
score(input) {
|
|
706
|
+
const usageNorm = clamp01(input.usageCount / (input.usageBaseline ?? 100));
|
|
707
|
+
const value = clamp01(input.successRate * this.#weights.success + usageNorm * this.#weights.usage + (input.evalScore ?? 0) * this.#weights.eval - RISK_SCORE[input.risk] * this.#weights.risk);
|
|
708
|
+
return {
|
|
709
|
+
value,
|
|
710
|
+
...value < this.#improveThreshold ? { suggestion: `Score ${value.toFixed(2)} is below ${this.#improveThreshold}: consider improving reliability (success rate ${(input.successRate * 100).toFixed(0)}%) or reducing risk` } : {}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
const tokenize = (text) => new Set(text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1));
|
|
715
|
+
function jaccardSimilarity(a, b) {
|
|
716
|
+
const sa = tokenize(a);
|
|
717
|
+
const sb = tokenize(b);
|
|
718
|
+
if (sa.size === 0 && sb.size === 0) return 1;
|
|
719
|
+
let intersection = 0;
|
|
720
|
+
for (const t of sa) if (sb.has(t)) intersection++;
|
|
721
|
+
return intersection / (sa.size + sb.size - intersection || 1);
|
|
722
|
+
}
|
|
723
|
+
/** 描述词集 Jaccard 查重 */
|
|
724
|
+
var SimilarityDetector = class {
|
|
725
|
+
#threshold;
|
|
726
|
+
constructor(threshold = .8) {
|
|
727
|
+
this.#threshold = threshold;
|
|
728
|
+
}
|
|
729
|
+
/** 返回首个超过阈值的已有技能名;无重复返回 undefined */
|
|
730
|
+
findDuplicate(description, existing) {
|
|
731
|
+
for (const skill of existing) if (jaccardSimilarity(description, skill.description) >= this.#threshold) return skill.name;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
/** 技能引用依赖图(references/ 中引用其他技能的声明关系) */
|
|
735
|
+
var DependencyGraph = class {
|
|
736
|
+
#edges = /* @__PURE__ */ new Map();
|
|
737
|
+
addDependency(from, to) {
|
|
738
|
+
if (from === to) return;
|
|
739
|
+
const set = this.#edges.get(from) ?? /* @__PURE__ */ new Set();
|
|
740
|
+
set.add(to);
|
|
741
|
+
this.#edges.set(from, set);
|
|
742
|
+
}
|
|
743
|
+
dependenciesOf(name) {
|
|
744
|
+
return [...this.#edges.get(name) ?? []].sort();
|
|
745
|
+
}
|
|
746
|
+
dependentsOf(name) {
|
|
747
|
+
const out = [];
|
|
748
|
+
for (const [from, targets] of this.#edges) if (targets.has(name)) out.push(from);
|
|
749
|
+
return out.sort();
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
/** 读文档 + sha256 hash(变更检测用) */
|
|
753
|
+
async function readDocument(fs, path) {
|
|
754
|
+
const content = await fs.readText(path);
|
|
755
|
+
return {
|
|
756
|
+
path,
|
|
757
|
+
content,
|
|
758
|
+
hash: createHash("sha256").update(content).digest("hex")
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const EXTRACT_PROMPT = (doc, nameHint) => [
|
|
762
|
+
"Extract an executable skill from the following document as STRICT JSON only.",
|
|
763
|
+
"Schema: {\"name\": string, \"description\": string, \"risk\": \"low\"|\"medium\"|\"high\",",
|
|
764
|
+
" \"riskReasons\": string[], \"files\": [{\"path\": string, \"content\": string}]}",
|
|
765
|
+
"The candidate is ALWAYS a draft. Do NOT claim approved or published status.",
|
|
766
|
+
"Include a SKILL.md (frontmatter name/description + procedure steps as Markdown body).",
|
|
767
|
+
"Scripts (if any) go under scripts/ and must end with .ts or .js.",
|
|
768
|
+
nameHint ? `Prefer the name "${nameHint}" if appropriate.` : "",
|
|
769
|
+
"--- DOCUMENT ---",
|
|
770
|
+
doc.content.slice(0, 12e3)
|
|
771
|
+
].filter(Boolean).join("\n");
|
|
772
|
+
/** 文档 → LLM 抽取 → 完整防御管线 → draft 候选(source: document) */
|
|
773
|
+
var DocumentSkillExtractor = class {
|
|
774
|
+
#llm;
|
|
775
|
+
#audit;
|
|
776
|
+
#now;
|
|
777
|
+
#createId;
|
|
778
|
+
constructor(deps) {
|
|
779
|
+
this.#llm = deps.llm;
|
|
780
|
+
this.#audit = deps.audit;
|
|
781
|
+
this.#now = deps.now;
|
|
782
|
+
this.#createId = deps.createId;
|
|
783
|
+
}
|
|
784
|
+
async extract(input) {
|
|
785
|
+
const candidate = normalizeCandidate({
|
|
786
|
+
raw: parseJsonObject(await completeText(this.#llm, [{
|
|
787
|
+
role: "user",
|
|
788
|
+
content: EXTRACT_PROMPT(input.document, input.nameHint)
|
|
789
|
+
}])),
|
|
790
|
+
source: "document",
|
|
791
|
+
...this.#now ? { now: this.#now } : {},
|
|
792
|
+
...this.#createId ? { createId: this.#createId } : {}
|
|
793
|
+
});
|
|
794
|
+
candidate.metadata = {
|
|
795
|
+
...candidate.metadata ?? {},
|
|
796
|
+
documentPath: input.document.path,
|
|
797
|
+
documentHash: input.document.hash
|
|
798
|
+
};
|
|
799
|
+
await this.#audit?.append({
|
|
800
|
+
type: "candidate.created",
|
|
801
|
+
target: candidate.id,
|
|
802
|
+
data: {
|
|
803
|
+
name: candidate.name,
|
|
804
|
+
source: "document",
|
|
805
|
+
documentPath: input.document.path,
|
|
806
|
+
documentHash: input.document.hash
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
return candidate;
|
|
810
|
+
}
|
|
811
|
+
/** hash 变化 → 生成新候选(关联原技能名,不覆盖已发布技能);无变化返回 changed:false */
|
|
812
|
+
async checkUpdate(input) {
|
|
813
|
+
if (input.document.hash === input.previousHash) return { changed: false };
|
|
814
|
+
const candidate = await this.extract({
|
|
815
|
+
document: input.document,
|
|
816
|
+
...input.previousSkillName ? { nameHint: input.previousSkillName } : {}
|
|
817
|
+
});
|
|
818
|
+
candidate.metadata = {
|
|
819
|
+
...candidate.metadata ?? {},
|
|
820
|
+
updatesSkill: input.previousSkillName,
|
|
821
|
+
previousHash: input.previousHash
|
|
822
|
+
};
|
|
823
|
+
return {
|
|
824
|
+
changed: true,
|
|
825
|
+
candidate
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
/**
|
|
830
|
+
* runtime-miss 现成适配(opt-in):run 未激活技能时生成 draft 候选入库待审。
|
|
831
|
+
* 返回的 hook 失败会向上抛(由 runtime 降级为 warning)。
|
|
832
|
+
*/
|
|
833
|
+
function createMissHook(deps) {
|
|
834
|
+
return async ({ prompt }) => {
|
|
835
|
+
const candidate = await deps.generator.generate({
|
|
836
|
+
prompt,
|
|
837
|
+
source: "runtime-miss"
|
|
838
|
+
});
|
|
839
|
+
await deps.store.save(candidate);
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
//#endregion
|
|
844
|
+
export { AlwaysHumanApprovalPolicy, ApprovalWorkflow, CandidateStore, CompositeApprovalPolicy, DependencyGraph, DocumentSkillExtractor, EvaluationRunner, FailureAnalyzer, FsAuditLog, LlmCandidateGenerator, RepairPlanner, SCORING_WEIGHTS, SimilarityDetector, SkillScorer, SkillStatePolicy, SkillVersionStore, candidateToCatalogEntry, completeText, createMissHook, jaccardSimilarity, normalizeCandidate, normalizeCandidateFiles, normalizeRisk, parseJsonObject, readDocument, sanitizeCandidateName, suggestFromFailedRun, validateCandidate };
|