@webskill/sdk 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -10
- package/dist/browser.d.ts +37 -3
- package/dist/browser.js +106 -8
- package/dist/{dist-CfBOjJ4p.js → dist-BJobG0i-.js} +30 -7
- package/dist/{dist-LIFS3ZjI.js → dist-BRGQcLqR.js} +405 -179
- package/dist/{dist-fZFZaf43.js → dist-D0saNPi_.js} +22 -4
- package/dist/{dist-DuGN5x0v.js → dist-DrySSQ5R.js} +185 -50
- package/dist/env--jJB-TSX-04klhTYi.js +143 -0
- package/dist/env-BPUBZCwJ-4jat_SVG.d.ts +38 -0
- package/dist/governance.d.ts +43 -7
- package/dist/governance.js +168 -11
- package/dist/{index-Bun1aEf4.d.ts → index-BmzysJX5.d.ts} +48 -38
- package/dist/{index-COuOu6gw.d.ts → index-gjFuBevI.d.ts} +53 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +28 -8
- package/dist/node.d.ts +5 -4
- package/dist/node.js +5 -4
- package/dist/processSandboxEntry.d.ts +1 -0
- package/dist/processSandboxEntry.js +202 -0
- package/dist/{testing-BmR48Pn1.js → testing-B4pq6JYa.js} +1 -1
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +3 -2
- package/dist/{types-CgNC-oQu-DEcIBJKG.d.ts → types-CKm5G_eQ-8W8FnP4u.d.ts} +13 -2
- 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 { A as
|
|
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-DrySSQ5R.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { unzipSync, zipSync } from "fflate";
|
|
4
|
-
import { existsSync, promises,
|
|
5
|
+
import { existsSync, promises, realpathSync } from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { tmpdir } from "node:os";
|
|
7
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
9
|
import { format, promisify } from "node:util";
|
|
9
10
|
import { Worker } from "node:worker_threads";
|
|
10
|
-
import {
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import { execFile, fork } from "node:child_process";
|
|
11
13
|
import { createInterface } from "node:readline/promises";
|
|
12
|
-
import { mkdtemp } from "node:fs/promises";
|
|
13
|
-
import * as tar from "tar";
|
|
14
|
-
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$4 = (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$4(this.#root));
|
|
50
|
+
const targetReal = await realpathNearest(toPlatform$4(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$4(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$4(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$4(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$4(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$4(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$4(p));
|
|
93
92
|
return {
|
|
94
93
|
path: toPosix(p),
|
|
95
94
|
type: s.isDirectory() ? "directory" : "file",
|
|
@@ -103,31 +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$4(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$4(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$4(p), { recursive: true });
|
|
117
116
|
}
|
|
118
117
|
async remove(p, options) {
|
|
119
118
|
try {
|
|
120
|
-
await promises.rm(toPlatform$
|
|
119
|
+
await promises.rm(toPlatform$4(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$4(to)), { recursive: true });
|
|
126
|
+
try {
|
|
127
|
+
await promises.rename(toPlatform$4(from), toPlatform$4(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
|
-
const baseName$
|
|
137
|
+
const baseName$2 = (p) => p.split("/").pop() ?? p;
|
|
131
138
|
/**
|
|
132
139
|
* 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
|
|
133
140
|
* worker_threads 沙箱见 deferred-items D1。
|
|
@@ -166,7 +173,7 @@ var NodeScriptExecutor = class {
|
|
|
166
173
|
}
|
|
167
174
|
async loadDefinition(skillRoot, scriptName) {
|
|
168
175
|
const scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
169
|
-
const skillName = baseName$
|
|
176
|
+
const skillName = baseName$2(skillRoot);
|
|
170
177
|
let module;
|
|
171
178
|
try {
|
|
172
179
|
module = await this.#loadModule(scriptPath);
|
|
@@ -270,14 +277,14 @@ var NodeScriptExecutor = class {
|
|
|
270
277
|
};
|
|
271
278
|
}
|
|
272
279
|
};
|
|
273
|
-
const baseName = (p) => p.split("/").pop() ?? p;
|
|
274
|
-
const toPlatform$
|
|
275
|
-
const messageOf$
|
|
280
|
+
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
281
|
+
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
282
|
+
const messageOf$6 = (e) => e instanceof Error ? e.message : String(e);
|
|
276
283
|
/**
|
|
277
284
|
* 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
|
|
278
285
|
* runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
|
|
279
286
|
*/
|
|
280
|
-
const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
287
|
+
const NETWORK_POLICY_LIB$1 = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
281
288
|
/** Worker 入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
|
|
282
289
|
function workerEntryPath() {
|
|
283
290
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -329,10 +336,10 @@ var SandboxedScriptExecutor = class {
|
|
|
329
336
|
}
|
|
330
337
|
async loadDefinition(skillRoot, scriptName) {
|
|
331
338
|
const scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
332
|
-
const skillName = baseName(skillRoot);
|
|
339
|
+
const skillName = baseName$1(skillRoot);
|
|
333
340
|
const result = await this.#runWorker({
|
|
334
341
|
mode: "load",
|
|
335
|
-
scriptPath: toPlatform$
|
|
342
|
+
scriptPath: toPlatform$3(scriptPath),
|
|
336
343
|
scriptName,
|
|
337
344
|
scriptSource: await this.#fs.readText(scriptPath)
|
|
338
345
|
});
|
|
@@ -356,14 +363,14 @@ var SandboxedScriptExecutor = class {
|
|
|
356
363
|
content: [],
|
|
357
364
|
error: {
|
|
358
365
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
359
|
-
message: messageOf$
|
|
366
|
+
message: messageOf$6(e)
|
|
360
367
|
}
|
|
361
368
|
};
|
|
362
369
|
}
|
|
363
370
|
try {
|
|
364
371
|
const result = await this.#runWorker({
|
|
365
372
|
mode: "execute",
|
|
366
|
-
scriptPath: toPlatform$
|
|
373
|
+
scriptPath: toPlatform$3(scriptPath),
|
|
367
374
|
scriptName,
|
|
368
375
|
skillName: context.skillName,
|
|
369
376
|
runId: context.runId,
|
|
@@ -396,7 +403,7 @@ var SandboxedScriptExecutor = class {
|
|
|
396
403
|
content: [],
|
|
397
404
|
error: {
|
|
398
405
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
399
|
-
message: messageOf$
|
|
406
|
+
message: messageOf$6(e)
|
|
400
407
|
}
|
|
401
408
|
};
|
|
402
409
|
}
|
|
@@ -410,7 +417,7 @@ var SandboxedScriptExecutor = class {
|
|
|
410
417
|
workerData: {
|
|
411
418
|
...workerData,
|
|
412
419
|
networkPolicy: this.#networkPolicy,
|
|
413
|
-
networkPolicyLib: NETWORK_POLICY_LIB,
|
|
420
|
+
networkPolicyLib: NETWORK_POLICY_LIB$1,
|
|
414
421
|
allowedModules: this.#options.allowedModules ?? []
|
|
415
422
|
},
|
|
416
423
|
env,
|
|
@@ -447,7 +454,7 @@ var SandboxedScriptExecutor = class {
|
|
|
447
454
|
respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
|
|
448
455
|
return;
|
|
449
456
|
}
|
|
450
|
-
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$
|
|
457
|
+
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$6(e))));
|
|
451
458
|
return;
|
|
452
459
|
}
|
|
453
460
|
if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
|
|
@@ -459,6 +466,328 @@ var SandboxedScriptExecutor = class {
|
|
|
459
466
|
});
|
|
460
467
|
}
|
|
461
468
|
/** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
|
|
469
|
+
async #handleBridge(request, context) {
|
|
470
|
+
const gate = async (capability, message, details) => {
|
|
471
|
+
const decision = await this.#approval.authorize({
|
|
472
|
+
runId: context.runId,
|
|
473
|
+
capability,
|
|
474
|
+
mode: this.#capabilities[capability],
|
|
475
|
+
message,
|
|
476
|
+
...details !== void 0 ? { details } : {}
|
|
477
|
+
});
|
|
478
|
+
if (decision === "allowed") return void 0;
|
|
479
|
+
return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
switch (request.kind) {
|
|
483
|
+
case "readReference": {
|
|
484
|
+
const denied = await gate("readReference", `Script "${context.skillName}" wants to read reference "${request.path}"`, { path: request.path });
|
|
485
|
+
if (denied) return denied;
|
|
486
|
+
return {
|
|
487
|
+
id: request.id,
|
|
488
|
+
ok: true,
|
|
489
|
+
value: await context.readReference(request.path)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
case "writeArtifact": {
|
|
493
|
+
const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
|
|
494
|
+
path: request.path,
|
|
495
|
+
mimeType: request.mimeType
|
|
496
|
+
});
|
|
497
|
+
if (denied) return denied;
|
|
498
|
+
const content = typeof request.content === "string" ? request.content : new Uint8Array(request.content);
|
|
499
|
+
const artifact = await context.writeArtifact(request.path, content, { ...request.mimeType ? { mimeType: request.mimeType } : {} });
|
|
500
|
+
return {
|
|
501
|
+
id: request.id,
|
|
502
|
+
ok: true,
|
|
503
|
+
value: artifact
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
case "confirm": {
|
|
507
|
+
if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
|
|
508
|
+
const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`);
|
|
509
|
+
if (denied) return denied;
|
|
510
|
+
return {
|
|
511
|
+
id: request.id,
|
|
512
|
+
ok: true,
|
|
513
|
+
value: await context.confirm(request.message)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch (e) {
|
|
518
|
+
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf$6(e));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const baseName = (p) => p.split("/").pop() ?? p;
|
|
523
|
+
const toPlatform$2 = (p) => p.split("/").join(path.sep);
|
|
524
|
+
const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
|
|
525
|
+
const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
526
|
+
const readAllow = (p) => [`--allow-fs-read=${p}`, `--allow-fs-read=${realpathSync(p)}`];
|
|
527
|
+
const writeAllow = (p) => [`--allow-fs-write=${p}`, `--allow-fs-write=${realpathSync(p)}`];
|
|
528
|
+
/** 子进程入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
|
|
529
|
+
function processEntryPath() {
|
|
530
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
531
|
+
const candidates = [
|
|
532
|
+
path.join(dir, "processSandboxEntry.js"),
|
|
533
|
+
path.join(dir, "processSandboxEntry.ts"),
|
|
534
|
+
path.join(dir, "executor", "processSandboxEntry.js")
|
|
535
|
+
];
|
|
536
|
+
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
537
|
+
throw new WebSkillError("TOOL_EXECUTION_FAILED", `Process sandbox entry not found (tried: ${candidates.join(", ")})`);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* child_process.fork + --permission 进程沙箱(真实进程隔离)。
|
|
541
|
+
* 每个子进程以 `--permission --allow-fs-read=<入口目录> --allow-fs-read=<skillRoot>
|
|
542
|
+
* --allow-fs-write=<artifactDir>` 启动:fs 维度由权限模型管控(experimental),
|
|
543
|
+
* 子进程/Worker/addons 默认禁;网络无权限维度仍靠 fetch/WebSocket patch。
|
|
544
|
+
* 能力桥协议与 worker_threads 沙箱同一形态(readReference/writeArtifact/confirm + 授权)。
|
|
545
|
+
* 温池(默认 2):同 skillRoot 复用子进程;执行后 kill 并补位重生;并发上限即池大小。
|
|
546
|
+
* 超时 kill(同步死循环可杀);非零退出码 → TOOL_EXECUTION_FAILED。
|
|
547
|
+
*/
|
|
548
|
+
var ProcessSandboxExecutor = class {
|
|
549
|
+
#fs;
|
|
550
|
+
#options;
|
|
551
|
+
#capabilities;
|
|
552
|
+
#networkPolicy;
|
|
553
|
+
#approval;
|
|
554
|
+
#slots = [];
|
|
555
|
+
#waiters = [];
|
|
556
|
+
constructor(fs, options = {}) {
|
|
557
|
+
this.#fs = fs;
|
|
558
|
+
this.#options = options;
|
|
559
|
+
this.#capabilities = {
|
|
560
|
+
readReference: options.capabilities?.readReference ?? true,
|
|
561
|
+
writeArtifact: options.capabilities?.writeArtifact ?? true,
|
|
562
|
+
confirm: options.capabilities?.confirm ?? true
|
|
563
|
+
};
|
|
564
|
+
this.#networkPolicy = options.networkPolicy ?? "deny-all";
|
|
565
|
+
this.#approval = new CapabilityApproval({
|
|
566
|
+
...options.uiBridge ? { uiBridge: options.uiBridge } : {},
|
|
567
|
+
...options.approvalScope ? { scope: options.approvalScope } : {}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
get poolSize() {
|
|
571
|
+
return this.#options.poolSize ?? 2;
|
|
572
|
+
}
|
|
573
|
+
/** 池全部子进程销毁(测试收尾/进程退出前调用) */
|
|
574
|
+
async dispose() {
|
|
575
|
+
for (const slot of this.#slots) {
|
|
576
|
+
slot.child.kill();
|
|
577
|
+
await rm(slot.artifactDir, {
|
|
578
|
+
recursive: true,
|
|
579
|
+
force: true
|
|
580
|
+
}).catch(() => void 0);
|
|
581
|
+
}
|
|
582
|
+
this.#slots = [];
|
|
583
|
+
this.#waiters = [];
|
|
584
|
+
}
|
|
585
|
+
async #locateScript(skillRoot, scriptName) {
|
|
586
|
+
const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
|
|
587
|
+
const jsPath = `${skillRoot}/scripts/${scriptName}.js`;
|
|
588
|
+
const [hasTs, hasJs] = await Promise.all([this.#fs.exists(tsPath), this.#fs.exists(jsPath)]);
|
|
589
|
+
if (hasTs && hasJs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
|
|
590
|
+
if (!hasTs && !hasJs) throw new WebSkillError("TOOL_NOT_FOUND", `Script "${scriptName}" not found under ${skillRoot}/scripts`);
|
|
591
|
+
return hasTs ? tsPath : jsPath;
|
|
592
|
+
}
|
|
593
|
+
/** 取一个可用子进程(同 key 复用 / 淘汰 idle 重生 / 扩容 / 排队) */
|
|
594
|
+
async #acquire(key) {
|
|
595
|
+
for (;;) {
|
|
596
|
+
const idle = this.#slots.filter((s) => !s.busy);
|
|
597
|
+
const match = idle.find((s) => s.key === key);
|
|
598
|
+
if (match) {
|
|
599
|
+
match.busy = true;
|
|
600
|
+
return match;
|
|
601
|
+
}
|
|
602
|
+
if (idle.length > 0) {
|
|
603
|
+
const victim = idle[0];
|
|
604
|
+
victim.child.kill();
|
|
605
|
+
await rm(victim.artifactDir, {
|
|
606
|
+
recursive: true,
|
|
607
|
+
force: true
|
|
608
|
+
}).catch(() => void 0);
|
|
609
|
+
this.#slots.splice(this.#slots.indexOf(victim), 1);
|
|
610
|
+
}
|
|
611
|
+
if (this.#slots.length < this.poolSize) {
|
|
612
|
+
const slot = await this.#spawnSlot(key);
|
|
613
|
+
slot.busy = true;
|
|
614
|
+
this.#slots.push(slot);
|
|
615
|
+
return slot;
|
|
616
|
+
}
|
|
617
|
+
await new Promise((resolve) => this.#waiters.push(resolve));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/** 执行后回收:kill 旧子进程,补位重生同 key 新子进程(温池保持),唤醒排队 */
|
|
621
|
+
#recycle(slot) {
|
|
622
|
+
slot.child.kill();
|
|
623
|
+
const index = this.#slots.indexOf(slot);
|
|
624
|
+
if (index >= 0) this.#slots.splice(index, 1);
|
|
625
|
+
rm(slot.artifactDir, {
|
|
626
|
+
recursive: true,
|
|
627
|
+
force: true
|
|
628
|
+
}).catch(() => void 0);
|
|
629
|
+
this.#spawnSlot(slot.key).then((fresh) => {
|
|
630
|
+
this.#slots.push(fresh);
|
|
631
|
+
}).catch(() => void 0).finally(() => {
|
|
632
|
+
const waiter = this.#waiters.shift();
|
|
633
|
+
if (waiter) waiter();
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
async #spawnSlot(key) {
|
|
637
|
+
const artifactDir = await mkdtemp(path.join(tmpdir(), "webskill-psbx-out-")).then((d) => d.split(path.sep).join("/"));
|
|
638
|
+
const entry = processEntryPath();
|
|
639
|
+
return {
|
|
640
|
+
key,
|
|
641
|
+
child: fork(entry, [], {
|
|
642
|
+
execArgv: [
|
|
643
|
+
"--permission",
|
|
644
|
+
...readAllow(path.dirname(entry)),
|
|
645
|
+
...readAllow(toPlatform$2(key)),
|
|
646
|
+
...readAllow(artifactDir),
|
|
647
|
+
...writeAllow(artifactDir)
|
|
648
|
+
],
|
|
649
|
+
silent: false
|
|
650
|
+
}),
|
|
651
|
+
busy: false,
|
|
652
|
+
artifactDir
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
async loadDefinition(skillRoot, scriptName) {
|
|
656
|
+
const scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
657
|
+
const skillName = baseName(skillRoot);
|
|
658
|
+
const slot = await this.#acquire(toPlatform$2(skillRoot));
|
|
659
|
+
try {
|
|
660
|
+
const response = await this.#runTask(slot, {
|
|
661
|
+
mode: "load",
|
|
662
|
+
scriptPath: toPlatform$2(scriptPath),
|
|
663
|
+
scriptName,
|
|
664
|
+
scriptSource: await this.#fs.readText(scriptPath),
|
|
665
|
+
scratchDir: slot.artifactDir
|
|
666
|
+
}, 3e4);
|
|
667
|
+
if (!response.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${response.error?.message ?? "unknown error"}`);
|
|
668
|
+
return {
|
|
669
|
+
name: `${skillName}__${scriptName}`,
|
|
670
|
+
description: response.definition?.description,
|
|
671
|
+
inputSchema: response.definition?.inputSchema,
|
|
672
|
+
source: "script",
|
|
673
|
+
skillName
|
|
674
|
+
};
|
|
675
|
+
} finally {
|
|
676
|
+
this.#recycle(slot);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async execute(input) {
|
|
680
|
+
const { skillRoot, scriptName, args, context, timeoutMs } = input;
|
|
681
|
+
let scriptPath;
|
|
682
|
+
try {
|
|
683
|
+
scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
content: [],
|
|
688
|
+
error: {
|
|
689
|
+
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
690
|
+
message: messageOf$5(e)
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const slot = await this.#acquire(toPlatform$2(skillRoot));
|
|
695
|
+
try {
|
|
696
|
+
const result = await this.#runTask(slot, {
|
|
697
|
+
mode: "execute",
|
|
698
|
+
scriptPath: toPlatform$2(scriptPath),
|
|
699
|
+
scriptName,
|
|
700
|
+
skillName: context.skillName,
|
|
701
|
+
runId: context.runId,
|
|
702
|
+
args,
|
|
703
|
+
scriptSource: await this.#fs.readText(scriptPath),
|
|
704
|
+
scratchDir: slot.artifactDir
|
|
705
|
+
}, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
706
|
+
if (!result.ok) {
|
|
707
|
+
const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
content: [],
|
|
711
|
+
error: {
|
|
712
|
+
code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
|
|
713
|
+
message: `${result.error?.message ?? "script execution failed"}${stderrSummary}`
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const content = normalizeToolContent(result.value);
|
|
718
|
+
if (result.stdout?.length) content.push({
|
|
719
|
+
type: "text",
|
|
720
|
+
text: result.stdout.join("\n")
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
ok: true,
|
|
724
|
+
content
|
|
725
|
+
};
|
|
726
|
+
} catch (e) {
|
|
727
|
+
return {
|
|
728
|
+
ok: false,
|
|
729
|
+
content: [],
|
|
730
|
+
error: {
|
|
731
|
+
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
732
|
+
message: messageOf$5(e)
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
} finally {
|
|
736
|
+
this.#recycle(slot);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/** 在子进程上跑一个任务;超时 kill;非零退出码 → TOOL_EXECUTION_FAILED */
|
|
740
|
+
#runTask(slot, task, timeoutMs, onBridge, onNetworkBlocked) {
|
|
741
|
+
const { child } = slot;
|
|
742
|
+
return new Promise((resolve, reject) => {
|
|
743
|
+
let settled = false;
|
|
744
|
+
const timer = setTimeout(() => {
|
|
745
|
+
if (settled) return;
|
|
746
|
+
settled = true;
|
|
747
|
+
child.kill();
|
|
748
|
+
reject(new WebSkillError("RUN_TIMEOUT", `Process sandbox task timed out after ${timeoutMs}ms (child killed)`));
|
|
749
|
+
}, timeoutMs);
|
|
750
|
+
const done = (fn) => {
|
|
751
|
+
if (settled) return;
|
|
752
|
+
settled = true;
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
fn();
|
|
755
|
+
};
|
|
756
|
+
child.on("message", (msg) => {
|
|
757
|
+
if (msg?.type === "network-blocked") {
|
|
758
|
+
if (typeof msg.host === "string") onNetworkBlocked?.(msg.host);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (msg?.type === "bridge" && onBridge) {
|
|
762
|
+
const request = parseBridgeRequest(msg.request);
|
|
763
|
+
const respond = (response) => child.send({
|
|
764
|
+
type: "bridge-response",
|
|
765
|
+
response
|
|
766
|
+
});
|
|
767
|
+
if (!request) {
|
|
768
|
+
respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$5(e))));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
|
|
775
|
+
});
|
|
776
|
+
child.on("error", (e) => done(() => reject(e)));
|
|
777
|
+
child.on("exit", (code) => {
|
|
778
|
+
if (!settled) done(() => reject(new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox child exited ${code === null ? "by signal" : `with code ${code}`} without producing a result`)));
|
|
779
|
+
});
|
|
780
|
+
child.send({
|
|
781
|
+
type: "task",
|
|
782
|
+
task: {
|
|
783
|
+
...task,
|
|
784
|
+
networkPolicy: this.#networkPolicy,
|
|
785
|
+
networkPolicyLib: NETWORK_POLICY_LIB
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
/** 能力桥宿主侧处理:与 worker_threads 沙箱同一判定(runtime/sandbox/approval 单一来源) */
|
|
462
791
|
async #handleBridge(request, context) {
|
|
463
792
|
const gate = async (capability, message, details) => {
|
|
464
793
|
const decision = await this.#approval.authorize({
|
|
@@ -512,6 +841,16 @@ var SandboxedScriptExecutor = class {
|
|
|
512
841
|
}
|
|
513
842
|
}
|
|
514
843
|
};
|
|
844
|
+
const require_ = createRequire(import.meta.url);
|
|
845
|
+
let parseSyncCache;
|
|
846
|
+
function loadParseSync() {
|
|
847
|
+
if (!parseSyncCache) try {
|
|
848
|
+
parseSyncCache = require_("oxc-parser").parseSync;
|
|
849
|
+
} catch (e) {
|
|
850
|
+
throw new WebSkillError("TOOL_UNSUPPORTED", "The \"oxc-parser\" package is required for schema inference; install it first (npm i oxc-parser)", e);
|
|
851
|
+
}
|
|
852
|
+
return parseSyncCache;
|
|
853
|
+
}
|
|
515
854
|
const PLACEHOLDER_PREFIX = "Inferred placeholder (unsupported type: ";
|
|
516
855
|
const placeholder = (text) => ({
|
|
517
856
|
type: "string",
|
|
@@ -561,7 +900,7 @@ var TypeMapper = class {
|
|
|
561
900
|
};
|
|
562
901
|
return placeholder(this.#text(t));
|
|
563
902
|
}
|
|
564
|
-
#union(t,
|
|
903
|
+
#union(t, _seen) {
|
|
565
904
|
const values = [];
|
|
566
905
|
for (const member of t.types ?? []) {
|
|
567
906
|
const value = member.literal?.value ?? member.value;
|
|
@@ -661,7 +1000,7 @@ var OxcSchemaInferer = class {
|
|
|
661
1000
|
let program;
|
|
662
1001
|
let comments;
|
|
663
1002
|
try {
|
|
664
|
-
const result =
|
|
1003
|
+
const result = loadParseSync()(options?.fileName ?? "script.ts", source);
|
|
665
1004
|
if (result.errors?.length > 0) return void 0;
|
|
666
1005
|
program = result.program;
|
|
667
1006
|
comments = result.comments ?? [];
|
|
@@ -677,7 +1016,7 @@ var OxcSchemaInferer = class {
|
|
|
677
1016
|
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];
|
|
678
1017
|
if (!jsdoc) return void 0;
|
|
679
1018
|
const reparse = (typeText) => {
|
|
680
|
-
const r =
|
|
1019
|
+
const r = loadParseSync()("__t.ts", `type __T = ${typeText};`);
|
|
681
1020
|
if (r.errors?.length > 0) return void 0;
|
|
682
1021
|
return r.program.body.find((n) => n.type === "TSTypeAliasDeclaration")?.typeAnnotation;
|
|
683
1022
|
};
|
|
@@ -833,6 +1172,13 @@ var CliUiBridge = class {
|
|
|
833
1172
|
const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
|
|
834
1173
|
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
835
1174
|
const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
|
|
1175
|
+
let tarModule$1;
|
|
1176
|
+
async function loadTar$1() {
|
|
1177
|
+
tarModule$1 ??= await import("tar").catch((e) => {
|
|
1178
|
+
throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
|
|
1179
|
+
});
|
|
1180
|
+
return tarModule$1;
|
|
1181
|
+
}
|
|
836
1182
|
/**
|
|
837
1183
|
* zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
|
|
838
1184
|
* 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
|
|
@@ -853,7 +1199,8 @@ async function extractTarFile(archivePath, destRoot, limits) {
|
|
|
853
1199
|
try {
|
|
854
1200
|
const entryPaths = [];
|
|
855
1201
|
let total = 0;
|
|
856
|
-
await
|
|
1202
|
+
const tarMod = await loadTar$1();
|
|
1203
|
+
await tarMod.t({
|
|
857
1204
|
file: toPlatform$1(archivePath),
|
|
858
1205
|
onentry: (entry) => {
|
|
859
1206
|
entryPaths.push(entry.path);
|
|
@@ -864,7 +1211,7 @@ async function extractTarFile(archivePath, destRoot, limits) {
|
|
|
864
1211
|
}
|
|
865
1212
|
});
|
|
866
1213
|
for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
|
|
867
|
-
await
|
|
1214
|
+
await tarMod.x({
|
|
868
1215
|
file: toPlatform$1(archivePath),
|
|
869
1216
|
cwd: toPlatform$1(destRoot),
|
|
870
1217
|
filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
|
|
@@ -916,6 +1263,13 @@ async function removeDirQuiet(fs, dir) {
|
|
|
916
1263
|
} catch {}
|
|
917
1264
|
}
|
|
918
1265
|
const messageOf$3 = (e) => e instanceof Error ? e.message : String(e);
|
|
1266
|
+
let tarModule;
|
|
1267
|
+
async function loadTar() {
|
|
1268
|
+
tarModule ??= await import("tar").catch((e) => {
|
|
1269
|
+
throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
|
|
1270
|
+
});
|
|
1271
|
+
return tarModule;
|
|
1272
|
+
}
|
|
919
1273
|
/** 导出技能目录全部文件(含 manifest)为 zip/tar 归档,返回 outPath */
|
|
920
1274
|
async function exportArchive(fs, skillRoot, options) {
|
|
921
1275
|
try {
|
|
@@ -923,7 +1277,7 @@ async function exportArchive(fs, skillRoot, options) {
|
|
|
923
1277
|
const files = {};
|
|
924
1278
|
for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
|
|
925
1279
|
await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
|
|
926
|
-
} else await
|
|
1280
|
+
} else await (await loadTar()).c({
|
|
927
1281
|
file: toPlatform$1(options.outPath),
|
|
928
1282
|
cwd: toPlatform$1(skillRoot)
|
|
929
1283
|
}, ["."]);
|
|
@@ -945,7 +1299,7 @@ async function readArchiveManifest(fs, archivePath) {
|
|
|
945
1299
|
}
|
|
946
1300
|
let manifestText;
|
|
947
1301
|
try {
|
|
948
|
-
await
|
|
1302
|
+
await (await loadTar()).t({
|
|
949
1303
|
file: toPlatform$1(archivePath),
|
|
950
1304
|
onentry: (entry) => {
|
|
951
1305
|
if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
|
|
@@ -1151,15 +1505,19 @@ async function readLockfile(fs, managedRoot) {
|
|
|
1151
1505
|
async function upsertLockEntry(fs, managedRoot, name, entry) {
|
|
1152
1506
|
const lockfile = await readLockfile(fs, managedRoot);
|
|
1153
1507
|
lockfile.skills[name] = entry;
|
|
1154
|
-
await fs
|
|
1508
|
+
await writeLockfileAtomic(fs, managedRoot, lockfile);
|
|
1155
1509
|
return lockfile;
|
|
1156
1510
|
}
|
|
1157
1511
|
async function removeLockEntry(fs, managedRoot, name) {
|
|
1158
1512
|
const lockfile = await readLockfile(fs, managedRoot);
|
|
1159
1513
|
delete lockfile.skills[name];
|
|
1160
|
-
await fs
|
|
1514
|
+
await writeLockfileAtomic(fs, managedRoot, lockfile);
|
|
1161
1515
|
return lockfile;
|
|
1162
1516
|
}
|
|
1517
|
+
/** 原子写:先写临时文件再 rename(进程崩溃不留下半写 lockfile) */
|
|
1518
|
+
async function writeLockfileAtomic(fs, managedRoot, lockfile) {
|
|
1519
|
+
await atomicWriteText(fs, `${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
|
|
1520
|
+
}
|
|
1163
1521
|
const EXCLUDED_FILES = /* @__PURE__ */ new Set([SKILL_MANIFEST_FILE, SKILLS_LOCKFILE]);
|
|
1164
1522
|
/** 递归收集技能目录内的文件(相对路径,posix 分隔) */
|
|
1165
1523
|
async function walkFiles(fs, root, prefix = "") {
|
|
@@ -1235,6 +1593,10 @@ var SkillManager = class {
|
|
|
1235
1593
|
this.#archiveLimits = deps.archiveLimits;
|
|
1236
1594
|
this.#onChanged = deps.onChanged;
|
|
1237
1595
|
}
|
|
1596
|
+
/** 托管根目录(治理发布归档捕获等只读场景) */
|
|
1597
|
+
get managedRoot() {
|
|
1598
|
+
return this.#managedRoot;
|
|
1599
|
+
}
|
|
1238
1600
|
async install(source, options) {
|
|
1239
1601
|
const fs = this.#fs;
|
|
1240
1602
|
const stagingRoot = (await mkdtemp(path.join(tmpdir(), "webskill-install-"))).split(path.sep).join("/");
|
|
@@ -1458,142 +1820,6 @@ var SkillManager = class {
|
|
|
1458
1820
|
return options.outPath;
|
|
1459
1821
|
}
|
|
1460
1822
|
};
|
|
1461
|
-
/** 解析 .env(简单 KEY=VALUE 行解析,不引依赖);文件缺失返回 undefined */
|
|
1462
|
-
function readEnvVars(dotenvPath) {
|
|
1463
|
-
let raw;
|
|
1464
|
-
try {
|
|
1465
|
-
raw = readFileSync(dotenvPath, "utf8");
|
|
1466
|
-
} catch {
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
const vars = /* @__PURE__ */ new Map();
|
|
1470
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
1471
|
-
const trimmed = line.trim();
|
|
1472
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1473
|
-
const eq = trimmed.indexOf("=");
|
|
1474
|
-
if (eq <= 0) continue;
|
|
1475
|
-
const key = trimmed.slice(0, eq).trim();
|
|
1476
|
-
let value = trimmed.slice(eq + 1).trim();
|
|
1477
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1478
|
-
vars.set(key, value);
|
|
1479
|
-
}
|
|
1480
|
-
return vars;
|
|
1481
|
-
}
|
|
1482
|
-
/**
|
|
1483
|
-
* 读取 VITE_BASE_URL / VITE_API_KEY / VITE_MODEL_NAME;缺项返回 undefined 供测试 skip 判断。
|
|
1484
|
-
*/
|
|
1485
|
-
function loadLlmConfigFromEnv(dotenvPath = ".env") {
|
|
1486
|
-
const vars = readEnvVars(dotenvPath);
|
|
1487
|
-
if (!vars) return void 0;
|
|
1488
|
-
const baseUrl = vars.get("VITE_BASE_URL");
|
|
1489
|
-
const apiKey = vars.get("VITE_API_KEY");
|
|
1490
|
-
const model = vars.get("VITE_MODEL_NAME");
|
|
1491
|
-
if (!baseUrl || !apiKey || !model) return void 0;
|
|
1492
|
-
return {
|
|
1493
|
-
baseUrl,
|
|
1494
|
-
apiKey,
|
|
1495
|
-
model
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
/** ANTHROPIC_API_KEY / ANTHROPIC_MODEL(可选 ANTHROPIC_BASE_URL);缺项返回 undefined */
|
|
1499
|
-
function loadAnthropicConfigFromEnv(dotenvPath = ".env") {
|
|
1500
|
-
const vars = readEnvVars(dotenvPath);
|
|
1501
|
-
if (!vars) return void 0;
|
|
1502
|
-
const apiKey = vars.get("ANTHROPIC_API_KEY");
|
|
1503
|
-
const model = vars.get("ANTHROPIC_MODEL");
|
|
1504
|
-
if (!apiKey || !model) return void 0;
|
|
1505
|
-
const baseUrl = vars.get("ANTHROPIC_BASE_URL");
|
|
1506
|
-
return {
|
|
1507
|
-
apiKey,
|
|
1508
|
-
model,
|
|
1509
|
-
...baseUrl ? { baseUrl } : {}
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
/** GOOGLE_API_KEY / GOOGLE_MODEL(可选 GOOGLE_BASE_URL);缺项返回 undefined */
|
|
1513
|
-
function loadGoogleConfigFromEnv(dotenvPath = ".env") {
|
|
1514
|
-
const vars = readEnvVars(dotenvPath);
|
|
1515
|
-
if (!vars) return void 0;
|
|
1516
|
-
const apiKey = vars.get("GOOGLE_API_KEY");
|
|
1517
|
-
const model = vars.get("GOOGLE_MODEL");
|
|
1518
|
-
if (!apiKey || !model) return void 0;
|
|
1519
|
-
const baseUrl = vars.get("GOOGLE_BASE_URL");
|
|
1520
|
-
return {
|
|
1521
|
-
apiKey,
|
|
1522
|
-
model,
|
|
1523
|
-
...baseUrl ? { baseUrl } : {}
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
|
-
let capabilitiesCache;
|
|
1527
|
-
/**
|
|
1528
|
-
* 探测 LLM 能力(结果模块级缓存):
|
|
1529
|
-
* - available:GET /models 可达(与 checkAvailability 同语义)
|
|
1530
|
-
* - nonStreaming:最小非流式 chat completion(max_tokens=1,短超时);
|
|
1531
|
-
* HTTP 400/404 或明确不支持非流式的错误 → false
|
|
1532
|
-
* - streaming 能力由 available 推断(SSE 解析在 SDK 侧实现)
|
|
1533
|
-
* 环境变量 VITE_LLM_NON_STREAMING=0 可显式强制 nonStreaming=false(模拟流式-only 回归)。
|
|
1534
|
-
*/
|
|
1535
|
-
function probeLlmCapabilities(config) {
|
|
1536
|
-
capabilitiesCache ??= doProbeLlmCapabilities(config);
|
|
1537
|
-
return capabilitiesCache;
|
|
1538
|
-
}
|
|
1539
|
-
async function doProbeLlmCapabilities(config) {
|
|
1540
|
-
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1541
|
-
const headers = { authorization: `Bearer ${config.apiKey}` };
|
|
1542
|
-
let available = false;
|
|
1543
|
-
try {
|
|
1544
|
-
available = (await fetch(`${baseUrl}/models`, {
|
|
1545
|
-
headers,
|
|
1546
|
-
signal: AbortSignal.timeout(1e4)
|
|
1547
|
-
})).ok;
|
|
1548
|
-
} catch {
|
|
1549
|
-
available = false;
|
|
1550
|
-
}
|
|
1551
|
-
if (!available) return {
|
|
1552
|
-
available: false,
|
|
1553
|
-
nonStreaming: false,
|
|
1554
|
-
reason: "endpoint unreachable or unauthorized"
|
|
1555
|
-
};
|
|
1556
|
-
if (process.env["VITE_LLM_NON_STREAMING"] === "0") return {
|
|
1557
|
-
available: true,
|
|
1558
|
-
nonStreaming: false,
|
|
1559
|
-
reason: "non-streaming disabled via VITE_LLM_NON_STREAMING=0 (simulated)"
|
|
1560
|
-
};
|
|
1561
|
-
try {
|
|
1562
|
-
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
1563
|
-
method: "POST",
|
|
1564
|
-
headers: {
|
|
1565
|
-
...headers,
|
|
1566
|
-
"content-type": "application/json"
|
|
1567
|
-
},
|
|
1568
|
-
body: JSON.stringify({
|
|
1569
|
-
model: config.model,
|
|
1570
|
-
messages: [{
|
|
1571
|
-
role: "user",
|
|
1572
|
-
content: "OK"
|
|
1573
|
-
}],
|
|
1574
|
-
max_tokens: 1
|
|
1575
|
-
}),
|
|
1576
|
-
signal: AbortSignal.timeout(15e3)
|
|
1577
|
-
});
|
|
1578
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
1579
|
-
if (res.ok && !contentType.includes("text/event-stream")) return {
|
|
1580
|
-
available: true,
|
|
1581
|
-
nonStreaming: true
|
|
1582
|
-
};
|
|
1583
|
-
const detail = (await res.text().catch(() => "")).slice(0, 200);
|
|
1584
|
-
return {
|
|
1585
|
-
available: true,
|
|
1586
|
-
nonStreaming: false,
|
|
1587
|
-
reason: `endpoint rejects non-streaming chat completions (HTTP ${res.status})${detail ? `: ${detail}` : ""}`
|
|
1588
|
-
};
|
|
1589
|
-
} catch (e) {
|
|
1590
|
-
return {
|
|
1591
|
-
available: true,
|
|
1592
|
-
nonStreaming: false,
|
|
1593
|
-
reason: `non-streaming probe failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1594
|
-
};
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
1823
|
|
|
1598
1824
|
//#endregion
|
|
1599
|
-
export { NodeScriptExecutor as a,
|
|
1825
|
+
export { NodeScriptExecutor as a, SandboxedScriptExecutor as c, readArchiveManifest as d, NodeFS as i, SkillManager as l, FileArtifactStore as n, OxcSchemaInferer as o, FileMemoryStore as r, ProcessSandboxExecutor as s, CliUiBridge as t, exportArchive as u };
|