@webskill/sdk 0.1.4 → 0.2.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/dist/browser.d.ts +9 -4
- package/dist/browser.js +25 -22
- package/dist/{dist-MTc2KB03.js → dist-BJobG0i-.js} +30 -7
- package/dist/{dist-DAn07zHu.js → dist-BS5OpedX.js} +279 -63
- package/dist/{dist-DvoBsChX.js → dist-D0saNPi_.js} +154 -10
- package/dist/{dist-BVXZF7H_.js → dist-Dj6QjHBn.js} +148 -200
- package/dist/env--jJB-TSX-04klhTYi.js +143 -0
- package/dist/env-BPUBZCwJ-4jat_SVG.d.ts +38 -0
- package/dist/governance.d.ts +38 -6
- package/dist/governance.js +171 -11
- package/dist/{index-BSS3EWY-.d.ts → index-CySxIvRz.d.ts} +68 -3
- package/dist/{index-npFMt2Zw.d.ts → index-Vn63HJRO.d.ts} +15 -38
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +35 -9
- package/dist/node.d.ts +5 -4
- package/dist/node.js +5 -4
- package/dist/sandboxWorkerEntry.js +20 -3
- package/dist/{testing-S8SN9Lc6.js → testing-B4pq6JYa.js} +1 -1
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +3 -2
- package/dist/{types-CgNC-oQu-DYrxhD3z.d.ts → types-CKm5G_eQ-8W8FnP4u.d.ts} +41 -4
- package/dist/ui-react.d.ts +4 -2
- package/dist/ui-react.js +25 -4
- package/dist/ui-vue.d.ts +3 -2
- package/dist/ui-vue.js +44 -15
- package/dist/ui.d.ts +5 -3
- package/dist/ui.js +2 -2
- package/package.json +21 -9
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { A as unzipWithLimits, C as parseSkillMarkdown, M as verifyManifest, O as resolveArchiveLimits, T as readResponseWithLimit, b as isValidSkillName, f as atomicWriteText, i as SKILL_MANIFEST_FILE, j as validateSkills, k as resolveInsideRoot, m as buildManifest, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, w as parseSkillPackManifest, y as exportSkills } from "./dist-D0saNPi_.js";
|
|
2
|
+
import { A as isNetworkAllowed, C as bridgeError, M as networkUrlHost, N as normalizeToolContent, P as parseBridgeRequest, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-BS5OpedX.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { unzipSync, zipSync } from "fflate";
|
|
4
|
-
import { existsSync, promises
|
|
5
|
+
import { existsSync, promises } from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
6
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
9
|
import { format, promisify } from "node:util";
|
|
8
10
|
import { Worker } from "node:worker_threads";
|
|
9
|
-
import { parseSync } from "oxc-parser";
|
|
10
11
|
import { createInterface } from "node:readline/promises";
|
|
11
12
|
import { mkdtemp } from "node:fs/promises";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import * as tar from "tar";
|
|
14
13
|
import { execFile } from "node:child_process";
|
|
15
14
|
import { createHash } from "node:crypto";
|
|
16
15
|
|
|
17
16
|
//#region ../node/dist/index.js
|
|
18
|
-
const toPlatform$
|
|
17
|
+
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
19
18
|
const toPosix = (p) => p.split(path.sep).join("/");
|
|
20
19
|
const isEnoent = (e) => typeof e === "object" && e !== null && e.code === "ENOENT";
|
|
21
20
|
/** realpath 最近现存祖先并拼回剩余段(写入目标尚不存在时的等价判定) */
|
|
@@ -47,41 +46,41 @@ var NodeFS = class {
|
|
|
47
46
|
/** root 模式:目标 realpath 必须落在 root realpath 前缀内(符号链接逃逸防护) */
|
|
48
47
|
async #assertContained(p) {
|
|
49
48
|
if (!this.#root) return;
|
|
50
|
-
const rootReal = await promises.realpath(toPlatform$
|
|
51
|
-
const targetReal = await realpathNearest(toPlatform$
|
|
49
|
+
const rootReal = await promises.realpath(toPlatform$3(this.#root));
|
|
50
|
+
const targetReal = await realpathNearest(toPlatform$3(p));
|
|
52
51
|
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + path.sep)) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path escapes root via symbolic link: ${p} (resolves outside ${this.#root})`);
|
|
53
52
|
}
|
|
54
53
|
async readText(p) {
|
|
55
54
|
await this.#assertContained(p);
|
|
56
55
|
try {
|
|
57
|
-
return await promises.readFile(toPlatform$
|
|
56
|
+
return await promises.readFile(toPlatform$3(p), "utf8");
|
|
58
57
|
} catch (e) {
|
|
59
58
|
throw this.#mapError(e, p);
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
async writeText(p, content) {
|
|
63
62
|
await this.#assertContained(p);
|
|
64
|
-
const target = toPlatform$
|
|
63
|
+
const target = toPlatform$3(p);
|
|
65
64
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
66
65
|
await promises.writeFile(target, content, "utf8");
|
|
67
66
|
}
|
|
68
67
|
async readBinary(p) {
|
|
69
68
|
await this.#assertContained(p);
|
|
70
69
|
try {
|
|
71
|
-
return await promises.readFile(toPlatform$
|
|
70
|
+
return await promises.readFile(toPlatform$3(p));
|
|
72
71
|
} catch (e) {
|
|
73
72
|
throw this.#mapError(e, p);
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
async writeBinary(p, content) {
|
|
77
76
|
await this.#assertContained(p);
|
|
78
|
-
const target = toPlatform$
|
|
77
|
+
const target = toPlatform$3(p);
|
|
79
78
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
80
79
|
await promises.writeFile(target, content);
|
|
81
80
|
}
|
|
82
81
|
async exists(p) {
|
|
83
82
|
try {
|
|
84
|
-
await promises.access(toPlatform$
|
|
83
|
+
await promises.access(toPlatform$3(p));
|
|
85
84
|
return true;
|
|
86
85
|
} catch {
|
|
87
86
|
return false;
|
|
@@ -89,7 +88,7 @@ var NodeFS = class {
|
|
|
89
88
|
}
|
|
90
89
|
async stat(p) {
|
|
91
90
|
try {
|
|
92
|
-
const s = await promises.stat(toPlatform$
|
|
91
|
+
const s = await promises.stat(toPlatform$3(p));
|
|
93
92
|
return {
|
|
94
93
|
path: toPosix(p),
|
|
95
94
|
type: s.isDirectory() ? "directory" : "file",
|
|
@@ -103,32 +102,39 @@ var NodeFS = class {
|
|
|
103
102
|
async list(p) {
|
|
104
103
|
let dirents;
|
|
105
104
|
try {
|
|
106
|
-
dirents = await promises.readdir(toPlatform$
|
|
105
|
+
dirents = await promises.readdir(toPlatform$3(p), { withFileTypes: true });
|
|
107
106
|
} catch (e) {
|
|
108
107
|
throw this.#mapError(e, p);
|
|
109
108
|
}
|
|
110
109
|
return dirents.map((d) => ({
|
|
111
|
-
path: toPosix(path.join(toPlatform$
|
|
110
|
+
path: toPosix(path.join(toPlatform$3(p), d.name)),
|
|
112
111
|
type: d.isDirectory() ? "directory" : "file"
|
|
113
112
|
}));
|
|
114
113
|
}
|
|
115
114
|
async mkdir(p) {
|
|
116
|
-
await promises.mkdir(toPlatform$
|
|
115
|
+
await promises.mkdir(toPlatform$3(p), { recursive: true });
|
|
117
116
|
}
|
|
118
117
|
async remove(p, options) {
|
|
119
118
|
try {
|
|
120
|
-
await promises.rm(toPlatform$
|
|
119
|
+
await promises.rm(toPlatform$3(p), { recursive: options?.recursive ?? false });
|
|
121
120
|
} catch (e) {
|
|
122
121
|
throw this.#mapError(e, p);
|
|
123
122
|
}
|
|
124
123
|
}
|
|
124
|
+
async rename(from, to) {
|
|
125
|
+
await promises.mkdir(path.dirname(toPlatform$3(to)), { recursive: true });
|
|
126
|
+
try {
|
|
127
|
+
await promises.rename(toPlatform$3(from), toPlatform$3(to));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
throw this.#mapError(e, from);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
125
132
|
#mapError(e, p) {
|
|
126
133
|
if (isEnoent(e)) return new WebSkillError("FS_NOT_FOUND", `Path not found: ${p}`);
|
|
127
134
|
return e;
|
|
128
135
|
}
|
|
129
136
|
};
|
|
130
137
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
131
|
-
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
132
138
|
/**
|
|
133
139
|
* 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
|
|
134
140
|
* worker_threads 沙箱见 deferred-items D1。
|
|
@@ -148,7 +154,22 @@ var NodeScriptExecutor = class {
|
|
|
148
154
|
return hasTs ? tsPath : jsPath;
|
|
149
155
|
}
|
|
150
156
|
async #loadModule(scriptPath) {
|
|
151
|
-
|
|
157
|
+
if (scriptPath.endsWith(".ts")) {
|
|
158
|
+
const source = await this.#fs.readText(scriptPath);
|
|
159
|
+
const dir = await promises.mkdtemp(path.join(tmpdir(), "webskill-ts-"));
|
|
160
|
+
try {
|
|
161
|
+
const file = path.join(dir, "script.ts");
|
|
162
|
+
await promises.writeFile(file, source);
|
|
163
|
+
return await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
164
|
+
} finally {
|
|
165
|
+
await promises.rm(dir, {
|
|
166
|
+
recursive: true,
|
|
167
|
+
force: true
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const source = await this.#fs.readText(scriptPath);
|
|
172
|
+
return await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(`${source}\n//# ${Date.now()}`)}`);
|
|
152
173
|
}
|
|
153
174
|
async loadDefinition(skillRoot, scriptName) {
|
|
154
175
|
const scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
@@ -319,7 +340,8 @@ var SandboxedScriptExecutor = class {
|
|
|
319
340
|
const result = await this.#runWorker({
|
|
320
341
|
mode: "load",
|
|
321
342
|
scriptPath: toPlatform$2(scriptPath),
|
|
322
|
-
scriptName
|
|
343
|
+
scriptName,
|
|
344
|
+
scriptSource: await this.#fs.readText(scriptPath)
|
|
323
345
|
});
|
|
324
346
|
if (!result.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${result.error?.message ?? "unknown error"}`);
|
|
325
347
|
return {
|
|
@@ -352,7 +374,8 @@ var SandboxedScriptExecutor = class {
|
|
|
352
374
|
scriptName,
|
|
353
375
|
skillName: context.skillName,
|
|
354
376
|
runId: context.runId,
|
|
355
|
-
args
|
|
377
|
+
args,
|
|
378
|
+
scriptSource: await this.#fs.readText(scriptPath)
|
|
356
379
|
}, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
357
380
|
if (!result.ok) {
|
|
358
381
|
const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
|
|
@@ -496,6 +519,16 @@ var SandboxedScriptExecutor = class {
|
|
|
496
519
|
}
|
|
497
520
|
}
|
|
498
521
|
};
|
|
522
|
+
const require_ = createRequire(import.meta.url);
|
|
523
|
+
let parseSyncCache;
|
|
524
|
+
function loadParseSync() {
|
|
525
|
+
if (!parseSyncCache) try {
|
|
526
|
+
parseSyncCache = require_("oxc-parser").parseSync;
|
|
527
|
+
} catch (e) {
|
|
528
|
+
throw new WebSkillError("TOOL_UNSUPPORTED", "The \"oxc-parser\" package is required for schema inference; install it first (npm i oxc-parser)", e);
|
|
529
|
+
}
|
|
530
|
+
return parseSyncCache;
|
|
531
|
+
}
|
|
499
532
|
const PLACEHOLDER_PREFIX = "Inferred placeholder (unsupported type: ";
|
|
500
533
|
const placeholder = (text) => ({
|
|
501
534
|
type: "string",
|
|
@@ -545,7 +578,7 @@ var TypeMapper = class {
|
|
|
545
578
|
};
|
|
546
579
|
return placeholder(this.#text(t));
|
|
547
580
|
}
|
|
548
|
-
#union(t,
|
|
581
|
+
#union(t, _seen) {
|
|
549
582
|
const values = [];
|
|
550
583
|
for (const member of t.types ?? []) {
|
|
551
584
|
const value = member.literal?.value ?? member.value;
|
|
@@ -645,7 +678,7 @@ var OxcSchemaInferer = class {
|
|
|
645
678
|
let program;
|
|
646
679
|
let comments;
|
|
647
680
|
try {
|
|
648
|
-
const result =
|
|
681
|
+
const result = loadParseSync()(options?.fileName ?? "script.ts", source);
|
|
649
682
|
if (result.errors?.length > 0) return void 0;
|
|
650
683
|
program = result.program;
|
|
651
684
|
comments = result.comments ?? [];
|
|
@@ -661,7 +694,7 @@ var OxcSchemaInferer = class {
|
|
|
661
694
|
const jsdoc = comments.filter((c) => c.type === "Block" && typeof c.value === "string" && c.value.includes("@param") && typeof c.end === "number" && c.end <= runStart).sort((a, b) => b.end - a.end)[0];
|
|
662
695
|
if (!jsdoc) return void 0;
|
|
663
696
|
const reparse = (typeText) => {
|
|
664
|
-
const r =
|
|
697
|
+
const r = loadParseSync()("__t.ts", `type __T = ${typeText};`);
|
|
665
698
|
if (r.errors?.length > 0) return void 0;
|
|
666
699
|
return r.program.body.find((n) => n.type === "TSTypeAliasDeclaration")?.typeAnnotation;
|
|
667
700
|
};
|
|
@@ -817,38 +850,46 @@ var CliUiBridge = class {
|
|
|
817
850
|
const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
|
|
818
851
|
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
819
852
|
const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
|
|
853
|
+
let tarModule$1;
|
|
854
|
+
async function loadTar$1() {
|
|
855
|
+
tarModule$1 ??= await import("tar").catch((e) => {
|
|
856
|
+
throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
|
|
857
|
+
});
|
|
858
|
+
return tarModule$1;
|
|
859
|
+
}
|
|
820
860
|
/**
|
|
821
|
-
* zip 解包(fflate
|
|
861
|
+
* zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
|
|
822
862
|
* 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
|
|
823
863
|
*/
|
|
824
|
-
async function extractZipData(fs, data, destRoot) {
|
|
825
|
-
|
|
826
|
-
try {
|
|
827
|
-
entries = unzipSync(data);
|
|
828
|
-
} catch (e) {
|
|
829
|
-
throw new WebSkillError("INSTALL_FAILED", `Failed to read zip archive: ${messageOf$4(e)}`, e);
|
|
830
|
-
}
|
|
831
|
-
for (const [entryPath, content] of Object.entries(entries)) {
|
|
864
|
+
async function extractZipData(fs, data, destRoot, limits) {
|
|
865
|
+
for (const [entryPath, content] of await unzipWithLimits(data, limits)) {
|
|
832
866
|
const target = resolveInsideRoot(destRoot, entryPath);
|
|
833
867
|
if (entryPath.endsWith("/")) await fs.mkdir(target);
|
|
834
868
|
else await fs.writeBinary(target, content);
|
|
835
869
|
}
|
|
836
870
|
}
|
|
837
871
|
/**
|
|
838
|
-
* tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot
|
|
839
|
-
*
|
|
872
|
+
* tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot 校验并累计
|
|
873
|
+
* 单条目/总体积上限(声明 size 预检),再解包;符号链接/硬链接条目不跟随(跳过)。
|
|
840
874
|
*/
|
|
841
|
-
async function extractTarFile(archivePath, destRoot) {
|
|
875
|
+
async function extractTarFile(archivePath, destRoot, limits) {
|
|
876
|
+
const { maxEntryBytes, maxTotalBytes } = resolveArchiveLimits(limits);
|
|
842
877
|
try {
|
|
843
878
|
const entryPaths = [];
|
|
844
|
-
|
|
879
|
+
let total = 0;
|
|
880
|
+
const tarMod = await loadTar$1();
|
|
881
|
+
await tarMod.t({
|
|
845
882
|
file: toPlatform$1(archivePath),
|
|
846
883
|
onentry: (entry) => {
|
|
847
884
|
entryPaths.push(entry.path);
|
|
885
|
+
const size = typeof entry.size === "number" ? entry.size : 0;
|
|
886
|
+
if (size > maxEntryBytes) throw new WebSkillError("INSTALL_FAILED", `Archive entry exceeds the ${maxEntryBytes}-byte limit: ${entry.path}`);
|
|
887
|
+
total += size;
|
|
888
|
+
if (total > maxTotalBytes) throw new WebSkillError("INSTALL_FAILED", `Archive contents exceed the ${maxTotalBytes}-byte total limit`);
|
|
848
889
|
}
|
|
849
890
|
});
|
|
850
891
|
for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
|
|
851
|
-
await
|
|
892
|
+
await tarMod.x({
|
|
852
893
|
file: toPlatform$1(archivePath),
|
|
853
894
|
cwd: toPlatform$1(destRoot),
|
|
854
895
|
filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
|
|
@@ -900,6 +941,13 @@ async function removeDirQuiet(fs, dir) {
|
|
|
900
941
|
} catch {}
|
|
901
942
|
}
|
|
902
943
|
const messageOf$3 = (e) => e instanceof Error ? e.message : String(e);
|
|
944
|
+
let tarModule;
|
|
945
|
+
async function loadTar() {
|
|
946
|
+
tarModule ??= await import("tar").catch((e) => {
|
|
947
|
+
throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
|
|
948
|
+
});
|
|
949
|
+
return tarModule;
|
|
950
|
+
}
|
|
903
951
|
/** 导出技能目录全部文件(含 manifest)为 zip/tar 归档,返回 outPath */
|
|
904
952
|
async function exportArchive(fs, skillRoot, options) {
|
|
905
953
|
try {
|
|
@@ -907,7 +955,7 @@ async function exportArchive(fs, skillRoot, options) {
|
|
|
907
955
|
const files = {};
|
|
908
956
|
for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
|
|
909
957
|
await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
|
|
910
|
-
} else await
|
|
958
|
+
} else await (await loadTar()).c({
|
|
911
959
|
file: toPlatform$1(options.outPath),
|
|
912
960
|
cwd: toPlatform$1(skillRoot)
|
|
913
961
|
}, ["."]);
|
|
@@ -929,7 +977,7 @@ async function readArchiveManifest(fs, archivePath) {
|
|
|
929
977
|
}
|
|
930
978
|
let manifestText;
|
|
931
979
|
try {
|
|
932
|
-
await
|
|
980
|
+
await (await loadTar()).t({
|
|
933
981
|
file: toPlatform$1(archivePath),
|
|
934
982
|
onentry: (entry) => {
|
|
935
983
|
if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
|
|
@@ -951,28 +999,39 @@ const _execFileP = promisify(execFile);
|
|
|
951
999
|
const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
952
1000
|
/**
|
|
953
1001
|
* 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
|
|
954
|
-
* git 等原生 exe
|
|
1002
|
+
* git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
|
|
955
1003
|
*/
|
|
956
|
-
async function execFileP(command, args) {
|
|
1004
|
+
async function execFileP(command, args, options = {}) {
|
|
1005
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
957
1006
|
if (process.platform === "win32" && command === "npm") return _execFileP("cmd", [
|
|
958
1007
|
"/c",
|
|
959
1008
|
command,
|
|
960
1009
|
...args
|
|
961
|
-
]);
|
|
962
|
-
return _execFileP(command, args);
|
|
1010
|
+
], { timeout: timeoutMs });
|
|
1011
|
+
return _execFileP(command, args, { timeout: timeoutMs });
|
|
963
1012
|
}
|
|
964
1013
|
/** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
|
|
965
1014
|
function commandFailed(command, e) {
|
|
966
1015
|
return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf$2(e)}`, e);
|
|
967
1016
|
}
|
|
968
|
-
/** git
|
|
1017
|
+
/** git url 协议白名单:https:// 与 git@ SCP 形式;无 scheme 的本地路径放行(dev 工作流);
|
|
1018
|
+
* 其余显式协议(ext::/file:// 等)一律拒绝 */
|
|
1019
|
+
function assertGitUrlSafe(url) {
|
|
1020
|
+
if (url.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git url must not start with "--": ${JSON.stringify(url)}`);
|
|
1021
|
+
if (url.startsWith("https://") || url.startsWith("git@")) return;
|
|
1022
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) throw new WebSkillError("INSTALL_FAILED", `Git url protocol is not allowed (only https://, git@ SCP form, or plain local paths): ${JSON.stringify(url)}`);
|
|
1023
|
+
}
|
|
1024
|
+
/** git 源:clone --depth 1 + rev-parse HEAD 记录 commit;url/ref 前插 `--` 防选项注入 */
|
|
969
1025
|
async function stageGit(source, ctx) {
|
|
1026
|
+
assertGitUrlSafe(source.url);
|
|
1027
|
+
if (source.ref?.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git ref must not start with "--": ${JSON.stringify(source.ref)}`);
|
|
970
1028
|
const skillDir = `${ctx.stagingRoot}/repo`;
|
|
971
1029
|
const args = [
|
|
972
1030
|
"clone",
|
|
973
1031
|
"--depth",
|
|
974
1032
|
"1",
|
|
975
1033
|
...source.ref ? ["--branch", source.ref] : [],
|
|
1034
|
+
"--",
|
|
976
1035
|
source.url,
|
|
977
1036
|
skillDir
|
|
978
1037
|
];
|
|
@@ -1003,7 +1062,7 @@ function sha256Hex(data) {
|
|
|
1003
1062
|
return createHash("sha256").update(data).digest("hex");
|
|
1004
1063
|
}
|
|
1005
1064
|
const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
1006
|
-
/** http
|
|
1065
|
+
/** http 源:下载归档(Content-Length + 流式累计上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
|
|
1007
1066
|
async function stageHttp(source, ctx, expectedSha256) {
|
|
1008
1067
|
const fetchImpl = ctx.fetchImpl ?? fetch;
|
|
1009
1068
|
let res;
|
|
@@ -1013,18 +1072,18 @@ async function stageHttp(source, ctx, expectedSha256) {
|
|
|
1013
1072
|
throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
|
|
1014
1073
|
}
|
|
1015
1074
|
if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
|
|
1016
|
-
const data =
|
|
1075
|
+
const data = await readResponseWithLimit(res, ctx.archiveLimits);
|
|
1017
1076
|
if (expectedSha256 !== void 0) {
|
|
1018
1077
|
const actual = sha256Hex(data);
|
|
1019
1078
|
if (actual !== expectedSha256) throw new WebSkillError("INSTALL_FAILED", `Archive checksum mismatch: expected ${expectedSha256}, got ${actual}`);
|
|
1020
1079
|
}
|
|
1021
1080
|
const contentDir = `${ctx.stagingRoot}/content`;
|
|
1022
1081
|
await ctx.fs.mkdir(contentDir);
|
|
1023
|
-
if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir);
|
|
1082
|
+
if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir, ctx.archiveLimits);
|
|
1024
1083
|
else {
|
|
1025
1084
|
const archivePath = `${ctx.stagingRoot}/archive.tar`;
|
|
1026
1085
|
await ctx.fs.writeBinary(archivePath, data);
|
|
1027
|
-
await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir));
|
|
1086
|
+
await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir), ctx.archiveLimits);
|
|
1028
1087
|
}
|
|
1029
1088
|
const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
|
|
1030
1089
|
if (await ctx.fs.exists(packFilePath)) return {
|
|
@@ -1056,16 +1115,27 @@ async function stageLocal(source, ctx) {
|
|
|
1056
1115
|
source
|
|
1057
1116
|
};
|
|
1058
1117
|
}
|
|
1059
|
-
/** npm
|
|
1118
|
+
/** npm 包名(@scope/name 或 name,小写字母数字 . _ - /)与版本(semver 或 dist-tag)正则 */
|
|
1119
|
+
const PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
1120
|
+
const VERSION_RE = /^[a-z0-9._~^>=<x*+-]+$/i;
|
|
1121
|
+
function assertNpmSpecSafe(source) {
|
|
1122
|
+
const name = source.packageName;
|
|
1123
|
+
const isLocalPath = name.startsWith("/") || name.startsWith("./") || name.startsWith("../");
|
|
1124
|
+
if (name.startsWith("--") || !PACKAGE_NAME_RE.test(name) && !isLocalPath) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package name: ${JSON.stringify(name)}`);
|
|
1125
|
+
if (source.version !== void 0 && (!VERSION_RE.test(source.version) || source.version.startsWith("--"))) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package version: ${JSON.stringify(source.version)}`);
|
|
1126
|
+
}
|
|
1127
|
+
/** npm 源:npm pack → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
|
|
1060
1128
|
async function stageNpm(source, ctx) {
|
|
1129
|
+
assertNpmSpecSafe(source);
|
|
1061
1130
|
const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
|
|
1062
1131
|
let stdout;
|
|
1063
1132
|
try {
|
|
1064
1133
|
({stdout} = await execFileP("npm", [
|
|
1065
1134
|
"pack",
|
|
1066
|
-
spec,
|
|
1067
1135
|
"--pack-destination",
|
|
1068
|
-
ctx.stagingRoot
|
|
1136
|
+
ctx.stagingRoot,
|
|
1137
|
+
"--",
|
|
1138
|
+
spec
|
|
1069
1139
|
]));
|
|
1070
1140
|
} catch (e) {
|
|
1071
1141
|
throw commandFailed("npm", e);
|
|
@@ -1113,15 +1183,19 @@ async function readLockfile(fs, managedRoot) {
|
|
|
1113
1183
|
async function upsertLockEntry(fs, managedRoot, name, entry) {
|
|
1114
1184
|
const lockfile = await readLockfile(fs, managedRoot);
|
|
1115
1185
|
lockfile.skills[name] = entry;
|
|
1116
|
-
await fs
|
|
1186
|
+
await writeLockfileAtomic(fs, managedRoot, lockfile);
|
|
1117
1187
|
return lockfile;
|
|
1118
1188
|
}
|
|
1119
1189
|
async function removeLockEntry(fs, managedRoot, name) {
|
|
1120
1190
|
const lockfile = await readLockfile(fs, managedRoot);
|
|
1121
1191
|
delete lockfile.skills[name];
|
|
1122
|
-
await fs
|
|
1192
|
+
await writeLockfileAtomic(fs, managedRoot, lockfile);
|
|
1123
1193
|
return lockfile;
|
|
1124
1194
|
}
|
|
1195
|
+
/** 原子写:先写临时文件再 rename(进程崩溃不留下半写 lockfile) */
|
|
1196
|
+
async function writeLockfileAtomic(fs, managedRoot, lockfile) {
|
|
1197
|
+
await atomicWriteText(fs, `${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
|
|
1198
|
+
}
|
|
1125
1199
|
const EXCLUDED_FILES = /* @__PURE__ */ new Set([SKILL_MANIFEST_FILE, SKILLS_LOCKFILE]);
|
|
1126
1200
|
/** 递归收集技能目录内的文件(相对路径,posix 分隔) */
|
|
1127
1201
|
async function walkFiles(fs, root, prefix = "") {
|
|
@@ -1166,15 +1240,13 @@ async function readManifest(fs, skillRoot) {
|
|
|
1166
1240
|
throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
1167
1241
|
}
|
|
1168
1242
|
}
|
|
1169
|
-
/**
|
|
1243
|
+
/** walk 实际目录与 manifest 比对:hash 不一致 / 清单外新增 / 清单内缺失三类全空才 ok */
|
|
1170
1244
|
async function verifyIntegrity(fs, skillRoot) {
|
|
1171
1245
|
const manifest = await readManifest(fs, skillRoot);
|
|
1172
1246
|
const actualHashes = /* @__PURE__ */ new Map();
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
1177
|
-
return verifyManifest(manifest, actualHashes);
|
|
1247
|
+
const actualFiles = await walkFiles(fs, skillRoot);
|
|
1248
|
+
for (const rel of actualFiles) actualHashes.set(rel, sha256Hex(await fs.readBinary(`${skillRoot}/${rel}`)));
|
|
1249
|
+
return verifyManifest(manifest, actualHashes, actualFiles);
|
|
1178
1250
|
}
|
|
1179
1251
|
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
1180
1252
|
const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
|
|
@@ -1188,12 +1260,20 @@ var SkillManager = class {
|
|
|
1188
1260
|
#fs;
|
|
1189
1261
|
#fetchImpl;
|
|
1190
1262
|
#schemaInference;
|
|
1263
|
+
#archiveLimits;
|
|
1264
|
+
#onChanged;
|
|
1191
1265
|
#schemaInferer = new OxcSchemaInferer();
|
|
1192
1266
|
constructor(deps) {
|
|
1193
1267
|
this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
|
|
1194
1268
|
this.#fs = deps.fs ?? new NodeFS();
|
|
1195
1269
|
this.#fetchImpl = deps.fetchImpl;
|
|
1196
1270
|
this.#schemaInference = deps.schemaInference ?? true;
|
|
1271
|
+
this.#archiveLimits = deps.archiveLimits;
|
|
1272
|
+
this.#onChanged = deps.onChanged;
|
|
1273
|
+
}
|
|
1274
|
+
/** 托管根目录(治理发布归档捕获等只读场景) */
|
|
1275
|
+
get managedRoot() {
|
|
1276
|
+
return this.#managedRoot;
|
|
1197
1277
|
}
|
|
1198
1278
|
async install(source, options) {
|
|
1199
1279
|
const fs = this.#fs;
|
|
@@ -1204,7 +1284,8 @@ var SkillManager = class {
|
|
|
1204
1284
|
const ctx = {
|
|
1205
1285
|
fs,
|
|
1206
1286
|
stagingRoot,
|
|
1207
|
-
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
1287
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {},
|
|
1288
|
+
...this.#archiveLimits ? { archiveLimits: this.#archiveLimits } : {}
|
|
1208
1289
|
};
|
|
1209
1290
|
let staged;
|
|
1210
1291
|
switch (source.type) {
|
|
@@ -1268,6 +1349,7 @@ var SkillManager = class {
|
|
|
1268
1349
|
throw e;
|
|
1269
1350
|
}
|
|
1270
1351
|
targetDir = void 0;
|
|
1352
|
+
this.#onChanged?.();
|
|
1271
1353
|
return manifest;
|
|
1272
1354
|
} catch (e) {
|
|
1273
1355
|
if (targetDir) await removeDirQuiet(fs, targetDir);
|
|
@@ -1346,6 +1428,7 @@ var SkillManager = class {
|
|
|
1346
1428
|
installed.length = 0;
|
|
1347
1429
|
throw e;
|
|
1348
1430
|
}
|
|
1431
|
+
this.#onChanged?.();
|
|
1349
1432
|
return manifests;
|
|
1350
1433
|
} catch (e) {
|
|
1351
1434
|
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
@@ -1381,6 +1464,7 @@ var SkillManager = class {
|
|
|
1381
1464
|
try {
|
|
1382
1465
|
await this.#fs.remove(targetDir, { recursive: true });
|
|
1383
1466
|
await removeLockEntry(this.#fs, this.#managedRoot, name);
|
|
1467
|
+
this.#onChanged?.();
|
|
1384
1468
|
} catch (e) {
|
|
1385
1469
|
throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf(e)}`, e);
|
|
1386
1470
|
}
|
|
@@ -1414,142 +1498,6 @@ var SkillManager = class {
|
|
|
1414
1498
|
return options.outPath;
|
|
1415
1499
|
}
|
|
1416
1500
|
};
|
|
1417
|
-
/** 解析 .env(简单 KEY=VALUE 行解析,不引依赖);文件缺失返回 undefined */
|
|
1418
|
-
function readEnvVars(dotenvPath) {
|
|
1419
|
-
let raw;
|
|
1420
|
-
try {
|
|
1421
|
-
raw = readFileSync(dotenvPath, "utf8");
|
|
1422
|
-
} catch {
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
const vars = /* @__PURE__ */ new Map();
|
|
1426
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
1427
|
-
const trimmed = line.trim();
|
|
1428
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1429
|
-
const eq = trimmed.indexOf("=");
|
|
1430
|
-
if (eq <= 0) continue;
|
|
1431
|
-
const key = trimmed.slice(0, eq).trim();
|
|
1432
|
-
let value = trimmed.slice(eq + 1).trim();
|
|
1433
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1434
|
-
vars.set(key, value);
|
|
1435
|
-
}
|
|
1436
|
-
return vars;
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* 读取 VITE_BASE_URL / VITE_API_KEY / VITE_MODEL_NAME;缺项返回 undefined 供测试 skip 判断。
|
|
1440
|
-
*/
|
|
1441
|
-
function loadLlmConfigFromEnv(dotenvPath = ".env") {
|
|
1442
|
-
const vars = readEnvVars(dotenvPath);
|
|
1443
|
-
if (!vars) return void 0;
|
|
1444
|
-
const baseUrl = vars.get("VITE_BASE_URL");
|
|
1445
|
-
const apiKey = vars.get("VITE_API_KEY");
|
|
1446
|
-
const model = vars.get("VITE_MODEL_NAME");
|
|
1447
|
-
if (!baseUrl || !apiKey || !model) return void 0;
|
|
1448
|
-
return {
|
|
1449
|
-
baseUrl,
|
|
1450
|
-
apiKey,
|
|
1451
|
-
model
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
/** ANTHROPIC_API_KEY / ANTHROPIC_MODEL(可选 ANTHROPIC_BASE_URL);缺项返回 undefined */
|
|
1455
|
-
function loadAnthropicConfigFromEnv(dotenvPath = ".env") {
|
|
1456
|
-
const vars = readEnvVars(dotenvPath);
|
|
1457
|
-
if (!vars) return void 0;
|
|
1458
|
-
const apiKey = vars.get("ANTHROPIC_API_KEY");
|
|
1459
|
-
const model = vars.get("ANTHROPIC_MODEL");
|
|
1460
|
-
if (!apiKey || !model) return void 0;
|
|
1461
|
-
const baseUrl = vars.get("ANTHROPIC_BASE_URL");
|
|
1462
|
-
return {
|
|
1463
|
-
apiKey,
|
|
1464
|
-
model,
|
|
1465
|
-
...baseUrl ? { baseUrl } : {}
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
/** GOOGLE_API_KEY / GOOGLE_MODEL(可选 GOOGLE_BASE_URL);缺项返回 undefined */
|
|
1469
|
-
function loadGoogleConfigFromEnv(dotenvPath = ".env") {
|
|
1470
|
-
const vars = readEnvVars(dotenvPath);
|
|
1471
|
-
if (!vars) return void 0;
|
|
1472
|
-
const apiKey = vars.get("GOOGLE_API_KEY");
|
|
1473
|
-
const model = vars.get("GOOGLE_MODEL");
|
|
1474
|
-
if (!apiKey || !model) return void 0;
|
|
1475
|
-
const baseUrl = vars.get("GOOGLE_BASE_URL");
|
|
1476
|
-
return {
|
|
1477
|
-
apiKey,
|
|
1478
|
-
model,
|
|
1479
|
-
...baseUrl ? { baseUrl } : {}
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
let capabilitiesCache;
|
|
1483
|
-
/**
|
|
1484
|
-
* 探测 LLM 能力(结果模块级缓存):
|
|
1485
|
-
* - available:GET /models 可达(与 checkAvailability 同语义)
|
|
1486
|
-
* - nonStreaming:最小非流式 chat completion(max_tokens=1,短超时);
|
|
1487
|
-
* HTTP 400/404 或明确不支持非流式的错误 → false
|
|
1488
|
-
* - streaming 能力由 available 推断(SSE 解析在 SDK 侧实现)
|
|
1489
|
-
* 环境变量 VITE_LLM_NON_STREAMING=0 可显式强制 nonStreaming=false(模拟流式-only 回归)。
|
|
1490
|
-
*/
|
|
1491
|
-
function probeLlmCapabilities(config) {
|
|
1492
|
-
capabilitiesCache ??= doProbeLlmCapabilities(config);
|
|
1493
|
-
return capabilitiesCache;
|
|
1494
|
-
}
|
|
1495
|
-
async function doProbeLlmCapabilities(config) {
|
|
1496
|
-
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1497
|
-
const headers = { authorization: `Bearer ${config.apiKey}` };
|
|
1498
|
-
let available = false;
|
|
1499
|
-
try {
|
|
1500
|
-
available = (await fetch(`${baseUrl}/models`, {
|
|
1501
|
-
headers,
|
|
1502
|
-
signal: AbortSignal.timeout(1e4)
|
|
1503
|
-
})).ok;
|
|
1504
|
-
} catch {
|
|
1505
|
-
available = false;
|
|
1506
|
-
}
|
|
1507
|
-
if (!available) return {
|
|
1508
|
-
available: false,
|
|
1509
|
-
nonStreaming: false,
|
|
1510
|
-
reason: "endpoint unreachable or unauthorized"
|
|
1511
|
-
};
|
|
1512
|
-
if (process.env["VITE_LLM_NON_STREAMING"] === "0") return {
|
|
1513
|
-
available: true,
|
|
1514
|
-
nonStreaming: false,
|
|
1515
|
-
reason: "non-streaming disabled via VITE_LLM_NON_STREAMING=0 (simulated)"
|
|
1516
|
-
};
|
|
1517
|
-
try {
|
|
1518
|
-
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
1519
|
-
method: "POST",
|
|
1520
|
-
headers: {
|
|
1521
|
-
...headers,
|
|
1522
|
-
"content-type": "application/json"
|
|
1523
|
-
},
|
|
1524
|
-
body: JSON.stringify({
|
|
1525
|
-
model: config.model,
|
|
1526
|
-
messages: [{
|
|
1527
|
-
role: "user",
|
|
1528
|
-
content: "OK"
|
|
1529
|
-
}],
|
|
1530
|
-
max_tokens: 1
|
|
1531
|
-
}),
|
|
1532
|
-
signal: AbortSignal.timeout(15e3)
|
|
1533
|
-
});
|
|
1534
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
1535
|
-
if (res.ok && !contentType.includes("text/event-stream")) return {
|
|
1536
|
-
available: true,
|
|
1537
|
-
nonStreaming: true
|
|
1538
|
-
};
|
|
1539
|
-
const detail = (await res.text().catch(() => "")).slice(0, 200);
|
|
1540
|
-
return {
|
|
1541
|
-
available: true,
|
|
1542
|
-
nonStreaming: false,
|
|
1543
|
-
reason: `endpoint rejects non-streaming chat completions (HTTP ${res.status})${detail ? `: ${detail}` : ""}`
|
|
1544
|
-
};
|
|
1545
|
-
} catch (e) {
|
|
1546
|
-
return {
|
|
1547
|
-
available: true,
|
|
1548
|
-
nonStreaming: false,
|
|
1549
|
-
reason: `non-streaming probe failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
1501
|
|
|
1554
1502
|
//#endregion
|
|
1555
|
-
export { NodeScriptExecutor as a, SkillManager as c,
|
|
1503
|
+
export { NodeScriptExecutor as a, SkillManager as c, NodeFS as i, exportArchive as l, FileArtifactStore as n, OxcSchemaInferer as o, FileMemoryStore as r, SandboxedScriptExecutor as s, CliUiBridge as t, readArchiveManifest as u };
|