@webskill/sdk 0.0.4 → 0.0.6
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 +47 -5
- package/dist/browser.js +225 -18
- package/dist/{dist-Bvhbxrrt.js → dist-33_zuOCm.js} +141 -13
- package/dist/{dist-RcqvzAGF.js → dist-BQruQ9Hv.js} +243 -9
- package/dist/{dist-COsE72Ct.js → dist-nXiR40hi.js} +715 -331
- package/dist/governance.d.ts +4 -3
- package/dist/governance.js +13 -4
- package/dist/{index-CqDIreSE.d.ts → index-DCifjtJx.d.ts} +176 -8
- package/dist/{index-BsbfEiaC.d.ts → index-vi7GPemR.d.ts} +13 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/ui-react.d.ts +2 -2
- package/dist/ui-react.js +13 -3
- package/dist/ui-vue.d.ts +2 -2
- package/dist/ui-vue.js +10 -3
- package/dist/ui.d.ts +17 -5
- package/dist/ui.js +3 -3
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as parseBridgeRequest, G as SKILL_PACK_FILE, H as SKILL_MANIFEST_FILE, J as WebSkillError, N as networkUrlHost, P as normalizeToolContent, V as SKILLS_LOCKFILE, X as buildManifest, a as CapabilityApproval, at as parseSkillMarkdown, c as FsMemoryStore, dt as verifyManifest, j as isNetworkAllowed, lt as resolveInsideRoot, nt as isValidSkillName, ot as parseSkillPackManifest, s as FsArtifactStore, tt as exportSkills, ut as validateSkills, w as bridgeError } from "./dist-nXiR40hi.js";
|
|
2
|
+
import { unzipSync, zipSync } from "fflate";
|
|
2
3
|
import { existsSync, promises, readFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -8,7 +9,6 @@ import { parseSync } from "oxc-parser";
|
|
|
8
9
|
import { createInterface } from "node:readline/promises";
|
|
9
10
|
import { mkdtemp } from "node:fs/promises";
|
|
10
11
|
import { tmpdir } from "node:os";
|
|
11
|
-
import { unzipSync, zipSync } from "fflate";
|
|
12
12
|
import * as tar from "tar";
|
|
13
13
|
import { execFile } from "node:child_process";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
@@ -224,6 +224,11 @@ var NodeScriptExecutor = class {
|
|
|
224
224
|
const baseName = (p) => p.split("/").pop() ?? p;
|
|
225
225
|
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
226
226
|
const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
|
|
227
|
+
/**
|
|
228
|
+
* 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
|
|
229
|
+
* runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
|
|
230
|
+
*/
|
|
231
|
+
const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
227
232
|
/** Worker 入口文件:src 为同目录 .ts;dist 为 dist/executor/ 下的 .js */
|
|
228
233
|
function workerEntryPath() {
|
|
229
234
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -245,6 +250,8 @@ var SandboxedScriptExecutor = class {
|
|
|
245
250
|
#fs;
|
|
246
251
|
#options;
|
|
247
252
|
#capabilities;
|
|
253
|
+
#networkPolicy;
|
|
254
|
+
#approval;
|
|
248
255
|
constructor(fs, options = {}) {
|
|
249
256
|
this.#fs = fs;
|
|
250
257
|
this.#options = options;
|
|
@@ -253,6 +260,11 @@ var SandboxedScriptExecutor = class {
|
|
|
253
260
|
writeArtifact: options.capabilities?.writeArtifact ?? true,
|
|
254
261
|
confirm: options.capabilities?.confirm ?? true
|
|
255
262
|
};
|
|
263
|
+
this.#networkPolicy = options.networkPolicy ?? "deny-all";
|
|
264
|
+
this.#approval = new CapabilityApproval({
|
|
265
|
+
...options.uiBridge ? { uiBridge: options.uiBridge } : {},
|
|
266
|
+
...options.approvalScope ? { scope: options.approvalScope } : {}
|
|
267
|
+
});
|
|
256
268
|
}
|
|
257
269
|
async #locateScript(skillRoot, scriptName) {
|
|
258
270
|
const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
|
|
@@ -302,7 +314,7 @@ var SandboxedScriptExecutor = class {
|
|
|
302
314
|
skillName: context.skillName,
|
|
303
315
|
runId: context.runId,
|
|
304
316
|
args
|
|
305
|
-
}, timeoutMs, (request) => this.#handleBridge(request, context));
|
|
317
|
+
}, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
306
318
|
if (!result.ok) {
|
|
307
319
|
const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
|
|
308
320
|
return {
|
|
@@ -335,12 +347,16 @@ var SandboxedScriptExecutor = class {
|
|
|
335
347
|
}
|
|
336
348
|
}
|
|
337
349
|
/** 起独立 Worker 执行任务;超时 terminate(同步死循环可杀);非零退出码 → TOOL_EXECUTION_FAILED */
|
|
338
|
-
#runWorker(workerData, timeoutMs = 3e4, onBridge) {
|
|
350
|
+
#runWorker(workerData, timeoutMs = 3e4, onBridge, onNetworkBlocked) {
|
|
339
351
|
const envWhitelist = this.#options.envWhitelist ?? [];
|
|
340
352
|
const env = Object.fromEntries(envWhitelist.filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]]));
|
|
341
353
|
return new Promise((resolve, reject) => {
|
|
342
354
|
const worker = new Worker(workerEntryPath(), {
|
|
343
|
-
workerData
|
|
355
|
+
workerData: {
|
|
356
|
+
...workerData,
|
|
357
|
+
networkPolicy: this.#networkPolicy,
|
|
358
|
+
networkPolicyLib: NETWORK_POLICY_LIB
|
|
359
|
+
},
|
|
344
360
|
env,
|
|
345
361
|
resourceLimits: this.#options.resourceLimits ?? {
|
|
346
362
|
maxOldGenerationSizeMb: 64,
|
|
@@ -361,6 +377,10 @@ var SandboxedScriptExecutor = class {
|
|
|
361
377
|
fn();
|
|
362
378
|
};
|
|
363
379
|
worker.on("message", (msg) => {
|
|
380
|
+
if (msg?.type === "network-blocked") {
|
|
381
|
+
if (typeof msg.host === "string") onNetworkBlocked?.(msg.host);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
364
384
|
if (msg?.type === "bridge" && onBridge) {
|
|
365
385
|
const request = parseBridgeRequest(msg.request);
|
|
366
386
|
const respond = (response) => worker.postMessage({
|
|
@@ -382,20 +402,36 @@ var SandboxedScriptExecutor = class {
|
|
|
382
402
|
});
|
|
383
403
|
});
|
|
384
404
|
}
|
|
385
|
-
/**
|
|
405
|
+
/** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
|
|
386
406
|
async #handleBridge(request, context) {
|
|
387
|
-
const
|
|
407
|
+
const gate = async (capability, message, details) => {
|
|
408
|
+
const decision = await this.#approval.authorize({
|
|
409
|
+
runId: context.runId,
|
|
410
|
+
capability,
|
|
411
|
+
mode: this.#capabilities[capability],
|
|
412
|
+
message,
|
|
413
|
+
...details !== void 0 ? { details } : {}
|
|
414
|
+
});
|
|
415
|
+
if (decision === "allowed") return void 0;
|
|
416
|
+
return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
|
|
417
|
+
};
|
|
388
418
|
try {
|
|
389
419
|
switch (request.kind) {
|
|
390
|
-
case "readReference":
|
|
391
|
-
|
|
420
|
+
case "readReference": {
|
|
421
|
+
const denied = await gate("readReference", `Script "${context.skillName}" wants to read reference "${request.path}"`, { path: request.path });
|
|
422
|
+
if (denied) return denied;
|
|
392
423
|
return {
|
|
393
424
|
id: request.id,
|
|
394
425
|
ok: true,
|
|
395
426
|
value: await context.readReference(request.path)
|
|
396
427
|
};
|
|
428
|
+
}
|
|
397
429
|
case "writeArtifact": {
|
|
398
|
-
|
|
430
|
+
const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
|
|
431
|
+
path: request.path,
|
|
432
|
+
mimeType: request.mimeType
|
|
433
|
+
});
|
|
434
|
+
if (denied) return denied;
|
|
399
435
|
const content = typeof request.content === "string" ? request.content : new Uint8Array(request.content);
|
|
400
436
|
const artifact = await context.writeArtifact(request.path, content, { ...request.mimeType ? { mimeType: request.mimeType } : {} });
|
|
401
437
|
return {
|
|
@@ -404,13 +440,16 @@ var SandboxedScriptExecutor = class {
|
|
|
404
440
|
value: artifact
|
|
405
441
|
};
|
|
406
442
|
}
|
|
407
|
-
case "confirm":
|
|
408
|
-
if (!
|
|
443
|
+
case "confirm": {
|
|
444
|
+
if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
|
|
445
|
+
const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`);
|
|
446
|
+
if (denied) return denied;
|
|
409
447
|
return {
|
|
410
448
|
id: request.id,
|
|
411
449
|
ok: true,
|
|
412
450
|
value: await context.confirm(request.message)
|
|
413
451
|
};
|
|
452
|
+
}
|
|
414
453
|
}
|
|
415
454
|
} catch (e) {
|
|
416
455
|
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf$5(e));
|
|
@@ -615,7 +654,8 @@ var FileMemoryStore = class extends FsMemoryStore {
|
|
|
615
654
|
};
|
|
616
655
|
/**
|
|
617
656
|
* 命令行 UiBridge:ask→问答,confirm→y/n(带默认值),
|
|
618
|
-
* form→逐字段提示(显示默认值与必填标记),select
|
|
657
|
+
* form→逐字段提示(显示默认值与必填标记),select→编号列表,
|
|
658
|
+
* authorize→授权询问(默认拒绝,仅显式 y/yes 批准)。
|
|
619
659
|
* 输入/输出流可注入(测试用 PassThrough)。
|
|
620
660
|
*/
|
|
621
661
|
var CliUiBridge = class {
|
|
@@ -659,6 +699,14 @@ var CliUiBridge = class {
|
|
|
659
699
|
value: option ? option.value : void 0
|
|
660
700
|
};
|
|
661
701
|
}
|
|
702
|
+
case "authorize": {
|
|
703
|
+
this.#write(`[authorization required: ${input.capability}]\n`);
|
|
704
|
+
const answer = (await this.#question(`${input.message} [y/N] `)).trim().toLowerCase();
|
|
705
|
+
return {
|
|
706
|
+
id: input.id,
|
|
707
|
+
value: answer === "y" || answer === "yes"
|
|
708
|
+
};
|
|
709
|
+
}
|
|
662
710
|
}
|
|
663
711
|
}
|
|
664
712
|
async progress(input) {
|
|
@@ -923,6 +971,15 @@ async function stageHttp(source, ctx, expectedSha256) {
|
|
|
923
971
|
await ctx.fs.writeBinary(archivePath, data);
|
|
924
972
|
await extractTarFile(toPlatform(archivePath), toPlatform(contentDir));
|
|
925
973
|
}
|
|
974
|
+
const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
|
|
975
|
+
if (await ctx.fs.exists(packFilePath)) return {
|
|
976
|
+
skillDir: "",
|
|
977
|
+
source,
|
|
978
|
+
pack: {
|
|
979
|
+
contentDir,
|
|
980
|
+
manifest: parseSkillPackManifest(await ctx.fs.readText(packFilePath))
|
|
981
|
+
}
|
|
982
|
+
};
|
|
926
983
|
if (await ctx.fs.exists(`${contentDir}/SKILL.md`)) return {
|
|
927
984
|
skillDir: contentDir,
|
|
928
985
|
source
|
|
@@ -1109,6 +1166,7 @@ var SkillManager = class {
|
|
|
1109
1166
|
break;
|
|
1110
1167
|
case "archive": throw new WebSkillError("TOOL_UNSUPPORTED", "The \"archive\" install source is only supported by the browser skill manager");
|
|
1111
1168
|
}
|
|
1169
|
+
if (staged.pack) return (await this.#installPack(staged.pack, staged.source, stagingRoot))[0];
|
|
1112
1170
|
let name;
|
|
1113
1171
|
let version;
|
|
1114
1172
|
try {
|
|
@@ -1150,6 +1208,61 @@ var SkillManager = class {
|
|
|
1150
1208
|
}
|
|
1151
1209
|
}
|
|
1152
1210
|
/**
|
|
1211
|
+
* 包集安装:逐技能走单技能管线(归一 → 汇总校验 → 落 managed root → manifest → digest 比对)。
|
|
1212
|
+
* 任一失败整体回滚(删除本次已落目录;lockfile 在全部成功后才写入,天然零残留)。
|
|
1213
|
+
* 包是封闭产物:跳过安装期 schema 推导(sidecar 已随包携带,保证 digest 逐字节一致)。
|
|
1214
|
+
*/
|
|
1215
|
+
async #installPack(pack, source, stagingRoot) {
|
|
1216
|
+
const fs = this.#fs;
|
|
1217
|
+
const finalRoot = `${stagingRoot}/final`;
|
|
1218
|
+
const installed = [];
|
|
1219
|
+
try {
|
|
1220
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1221
|
+
for (const entry of pack.manifest.skills) {
|
|
1222
|
+
const skillDir = `${pack.contentDir}/${entry.name}`;
|
|
1223
|
+
if (!await fs.exists(`${skillDir}/SKILL.md`)) throw new WebSkillError("INSTALL_FAILED", `Skill pack is missing a directory for skill "${entry.name}"`);
|
|
1224
|
+
let name;
|
|
1225
|
+
try {
|
|
1226
|
+
const { metadata } = parseSkillMarkdown(await fs.readText(`${skillDir}/SKILL.md`));
|
|
1227
|
+
name = metadata.name;
|
|
1228
|
+
versions.set(name, typeof metadata["version"] === "string" ? metadata["version"] : void 0);
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md of pack entry "${entry.name}": ${messageOf(e)}`, e);
|
|
1231
|
+
}
|
|
1232
|
+
if (name !== entry.name) throw new WebSkillError("INSTALL_FAILED", `Skill pack entry "${entry.name}" does not match the SKILL.md name "${name}"`);
|
|
1233
|
+
await copyDir(fs, skillDir, `${finalRoot}/${name}`);
|
|
1234
|
+
}
|
|
1235
|
+
const report = await validateSkills(fs, [finalRoot]);
|
|
1236
|
+
if (!report.ok) {
|
|
1237
|
+
const errors = report.issues.filter((i) => i.severity === "error");
|
|
1238
|
+
throw new WebSkillError("INSTALL_FAILED", `Skill pack failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
1239
|
+
}
|
|
1240
|
+
const manifests = [];
|
|
1241
|
+
for (const entry of pack.manifest.skills) {
|
|
1242
|
+
const targetDir = `${this.#managedRoot}/${entry.name}`;
|
|
1243
|
+
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
1244
|
+
await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
|
|
1245
|
+
installed.push(targetDir);
|
|
1246
|
+
const manifest = await createManifest(fs, targetDir, {
|
|
1247
|
+
name: entry.name,
|
|
1248
|
+
...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
|
|
1249
|
+
source
|
|
1250
|
+
});
|
|
1251
|
+
if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
|
|
1252
|
+
manifests.push(manifest);
|
|
1253
|
+
}
|
|
1254
|
+
for (const manifest of manifests) await upsertLockEntry(fs, this.#managedRoot, manifest.name, {
|
|
1255
|
+
digest: manifest.integrity.digest,
|
|
1256
|
+
installedAt: manifest.installedAt,
|
|
1257
|
+
source
|
|
1258
|
+
});
|
|
1259
|
+
return manifests;
|
|
1260
|
+
} catch (e) {
|
|
1261
|
+
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
1262
|
+
throw asInstallFailed(e);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1153
1266
|
* D2 安装期预推导:scripts/ 下无显式 inputSchema 导出且无 sidecar 的脚本,
|
|
1154
1267
|
* 经 OXC 推导写 sidecar(计入 manifest.files;重装重新生成)
|
|
1155
1268
|
*/
|
|
@@ -1195,6 +1308,21 @@ var SkillManager = class {
|
|
|
1195
1308
|
if (!await this.#fs.exists(skillRoot)) throw new WebSkillError("EXPORT_FAILED", `Skill "${name}" is not installed`);
|
|
1196
1309
|
return exportArchive(this.#fs, skillRoot, options);
|
|
1197
1310
|
}
|
|
1311
|
+
/** 多技能包集导出(webskill.skill-pack.json + 各技能目录含 manifest),写 outPath 并返回 */
|
|
1312
|
+
async exportPack(names, options) {
|
|
1313
|
+
if (names.length === 0) throw new WebSkillError("EXPORT_FAILED", "exportPack requires at least one skill name");
|
|
1314
|
+
const roots = names.map((name) => {
|
|
1315
|
+
if (!isValidSkillName(name)) throw new WebSkillError("EXPORT_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
|
|
1316
|
+
return `${this.#managedRoot}/${name}`;
|
|
1317
|
+
});
|
|
1318
|
+
for (const [index, root] of roots.entries()) if (!await this.#fs.exists(root)) throw new WebSkillError("EXPORT_FAILED", `Skill "${names[index]}" is not installed`);
|
|
1319
|
+
const bytes = await exportSkills(this.#fs, {
|
|
1320
|
+
roots,
|
|
1321
|
+
manifestBuilder: (root) => readManifest(this.#fs, root)
|
|
1322
|
+
});
|
|
1323
|
+
await this.#fs.writeBinary(options.outPath, bytes);
|
|
1324
|
+
return options.outPath;
|
|
1325
|
+
}
|
|
1198
1326
|
};
|
|
1199
1327
|
/**
|
|
1200
1328
|
* 解析 .env(简单 KEY=VALUE 行解析,不引依赖),
|
|
@@ -5,10 +5,11 @@ function shapeInteractionValue(model, values) {
|
|
|
5
5
|
case "ask": return values["answer"];
|
|
6
6
|
case "confirm": return values["confirmed"] === true;
|
|
7
7
|
case "select": return values["selected"];
|
|
8
|
+
case "authorize": return true;
|
|
8
9
|
case "form": return values;
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
|
-
/**
|
|
12
|
+
/** 五类 InteractionRequest → 统一中间模型(框架无关) */
|
|
12
13
|
function interactionToFormModel(request) {
|
|
13
14
|
switch (request.type) {
|
|
14
15
|
case "ask": return {
|
|
@@ -63,6 +64,14 @@ function interactionToFormModel(request) {
|
|
|
63
64
|
submitLabel: "Select",
|
|
64
65
|
cancelLabel: "Cancel"
|
|
65
66
|
};
|
|
67
|
+
case "authorize": return {
|
|
68
|
+
kind: "authorize",
|
|
69
|
+
title: "Authorization required",
|
|
70
|
+
message: request.message,
|
|
71
|
+
controls: [],
|
|
72
|
+
submitLabel: "Allow",
|
|
73
|
+
cancelLabel: "Deny"
|
|
74
|
+
};
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
const isEmpty = (v) => v === void 0 || v === "";
|
|
@@ -181,9 +190,219 @@ function renderMiniMarkdown(text, doc) {
|
|
|
181
190
|
}
|
|
182
191
|
return root;
|
|
183
192
|
}
|
|
184
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* 轻量 SVG 图表自绘(bar/line/pie,零依赖,三端复用单一来源)。
|
|
195
|
+
* 全部经 doc.createElementNS 构建(内容来自技能输出,不拼 innerHTML)。
|
|
196
|
+
*/
|
|
197
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
198
|
+
/** 固定 8 色循环调色板 */
|
|
199
|
+
const CHART_PALETTE = [
|
|
200
|
+
"#4e79a7",
|
|
201
|
+
"#f28e2b",
|
|
202
|
+
"#e15759",
|
|
203
|
+
"#76b7b2",
|
|
204
|
+
"#59a14f",
|
|
205
|
+
"#edc948",
|
|
206
|
+
"#b07aa1",
|
|
207
|
+
"#ff9da7"
|
|
208
|
+
];
|
|
209
|
+
const WIDTH = 320;
|
|
210
|
+
const HEIGHT = 200;
|
|
211
|
+
function el(doc, name, attrs) {
|
|
212
|
+
const node = doc.createElementNS(SVG_NS, name);
|
|
213
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
214
|
+
return node;
|
|
215
|
+
}
|
|
216
|
+
function text$1(doc, x, y, content, attrs = {}) {
|
|
217
|
+
const node = el(doc, "text", {
|
|
218
|
+
x: String(x),
|
|
219
|
+
y: String(y),
|
|
220
|
+
"font-size": "9",
|
|
221
|
+
fill: "#555",
|
|
222
|
+
...attrs
|
|
223
|
+
});
|
|
224
|
+
node.textContent = content;
|
|
225
|
+
return node;
|
|
226
|
+
}
|
|
227
|
+
const fmt = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
228
|
+
function maxValue(chart) {
|
|
229
|
+
let max = 0;
|
|
230
|
+
for (const s of chart.series) for (const v of s.data) if (v > max) max = v;
|
|
231
|
+
return max;
|
|
232
|
+
}
|
|
233
|
+
function renderBar(chart, svg, doc) {
|
|
234
|
+
const plot = {
|
|
235
|
+
x0: 30,
|
|
236
|
+
y0: 24,
|
|
237
|
+
x1: 312,
|
|
238
|
+
y1: 164
|
|
239
|
+
};
|
|
240
|
+
const max = maxValue(chart);
|
|
241
|
+
svg.appendChild(el(doc, "line", {
|
|
242
|
+
x1: String(plot.x0),
|
|
243
|
+
y1: String(plot.y0),
|
|
244
|
+
x2: String(plot.x0),
|
|
245
|
+
y2: String(plot.y1),
|
|
246
|
+
stroke: "#999"
|
|
247
|
+
}));
|
|
248
|
+
svg.appendChild(el(doc, "line", {
|
|
249
|
+
x1: String(plot.x0),
|
|
250
|
+
y1: String(plot.y1),
|
|
251
|
+
x2: String(plot.x1),
|
|
252
|
+
y2: String(plot.y1),
|
|
253
|
+
stroke: "#999"
|
|
254
|
+
}));
|
|
255
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
256
|
+
const groupWidth = (plot.x1 - plot.x0) / chart.labels.length;
|
|
257
|
+
const barWidth = Math.max(1, groupWidth / chart.series.length * .7);
|
|
258
|
+
chart.series.forEach((series, si) => {
|
|
259
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
260
|
+
series.data.forEach((value, li) => {
|
|
261
|
+
if (li >= chart.labels.length) return;
|
|
262
|
+
const height = (plot.y1 - plot.y0) * value / max;
|
|
263
|
+
const x = plot.x0 + li * groupWidth + (groupWidth - barWidth * chart.series.length) / 2 + si * barWidth;
|
|
264
|
+
const y = plot.y1 - height;
|
|
265
|
+
svg.appendChild(el(doc, "rect", {
|
|
266
|
+
x: String(x),
|
|
267
|
+
y: String(y),
|
|
268
|
+
width: String(barWidth),
|
|
269
|
+
height: String(height),
|
|
270
|
+
fill: color,
|
|
271
|
+
"data-series": series.name ?? String(si)
|
|
272
|
+
}));
|
|
273
|
+
if (chart.series.length === 1) svg.appendChild(text$1(doc, x + barWidth / 2, y - 2, fmt(value), { "text-anchor": "middle" }));
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
chart.labels.forEach((label, li) => {
|
|
277
|
+
svg.appendChild(text$1(doc, plot.x0 + li * groupWidth + groupWidth / 2, plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function renderLine(chart, svg, doc) {
|
|
281
|
+
const plot = {
|
|
282
|
+
x0: 30,
|
|
283
|
+
y0: 24,
|
|
284
|
+
x1: 312,
|
|
285
|
+
y1: 164
|
|
286
|
+
};
|
|
287
|
+
const max = maxValue(chart);
|
|
288
|
+
svg.appendChild(el(doc, "line", {
|
|
289
|
+
x1: String(plot.x0),
|
|
290
|
+
y1: String(plot.y0),
|
|
291
|
+
x2: String(plot.x0),
|
|
292
|
+
y2: String(plot.y1),
|
|
293
|
+
stroke: "#999"
|
|
294
|
+
}));
|
|
295
|
+
svg.appendChild(el(doc, "line", {
|
|
296
|
+
x1: String(plot.x0),
|
|
297
|
+
y1: String(plot.y1),
|
|
298
|
+
x2: String(plot.x1),
|
|
299
|
+
y2: String(plot.y1),
|
|
300
|
+
stroke: "#999"
|
|
301
|
+
}));
|
|
302
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
303
|
+
const step = chart.labels.length > 1 ? (plot.x1 - plot.x0) / (chart.labels.length - 1) : 0;
|
|
304
|
+
const px = (li) => chart.labels.length > 1 ? plot.x0 + li * step : (plot.x0 + plot.x1) / 2;
|
|
305
|
+
const py = (v) => plot.y1 - (plot.y1 - plot.y0) * v / max;
|
|
306
|
+
chart.series.forEach((series, si) => {
|
|
307
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
308
|
+
const points = series.data.slice(0, chart.labels.length).map((v, li) => `${px(li)},${py(v)}`).join(" ");
|
|
309
|
+
if (points !== "") svg.appendChild(el(doc, "polyline", {
|
|
310
|
+
points,
|
|
311
|
+
fill: "none",
|
|
312
|
+
stroke: color,
|
|
313
|
+
"stroke-width": "1.5"
|
|
314
|
+
}));
|
|
315
|
+
series.data.slice(0, chart.labels.length).forEach((v, li) => {
|
|
316
|
+
svg.appendChild(el(doc, "circle", {
|
|
317
|
+
cx: String(px(li)),
|
|
318
|
+
cy: String(py(v)),
|
|
319
|
+
r: "2",
|
|
320
|
+
fill: color
|
|
321
|
+
}));
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
chart.labels.forEach((label, li) => {
|
|
325
|
+
svg.appendChild(text$1(doc, px(li), plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function renderPie(chart, svg, doc) {
|
|
329
|
+
const data = (chart.series[0]?.data ?? []).slice(0, chart.labels.length);
|
|
330
|
+
const total = data.reduce((sum, v) => sum + (v > 0 ? v : 0), 0);
|
|
331
|
+
const cx = 90;
|
|
332
|
+
const cy = 104;
|
|
333
|
+
const r = 72;
|
|
334
|
+
if (total <= 0) {
|
|
335
|
+
svg.appendChild(text$1(doc, cx, cy, "No data", { "text-anchor": "middle" }));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
let angle = -Math.PI / 2;
|
|
339
|
+
data.forEach((value, i) => {
|
|
340
|
+
const label = chart.labels[i] ?? String(i);
|
|
341
|
+
const color = CHART_PALETTE[i % CHART_PALETTE.length];
|
|
342
|
+
const slice = Math.max(value, 0) / total * Math.PI * 2;
|
|
343
|
+
const x0 = cx + r * Math.cos(angle);
|
|
344
|
+
const y0 = cy + r * Math.sin(angle);
|
|
345
|
+
const x1 = cx + r * Math.cos(angle + slice);
|
|
346
|
+
const y1 = cy + r * Math.sin(angle + slice);
|
|
347
|
+
const path = el(doc, "path", {
|
|
348
|
+
d: `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${slice > Math.PI ? "1" : "0"} 1 ${x1} ${y1} Z`,
|
|
349
|
+
fill: color,
|
|
350
|
+
"data-label": label
|
|
351
|
+
});
|
|
352
|
+
svg.appendChild(path);
|
|
353
|
+
angle += slice;
|
|
354
|
+
const ly = 40 + i * 14;
|
|
355
|
+
svg.appendChild(el(doc, "rect", {
|
|
356
|
+
x: "190",
|
|
357
|
+
y: String(ly - 7),
|
|
358
|
+
width: "8",
|
|
359
|
+
height: "8",
|
|
360
|
+
fill: color
|
|
361
|
+
}));
|
|
362
|
+
svg.appendChild(text$1(doc, 202, ly, `${label} (${fmt(value)})`));
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/** ChartSpec → SVG(bar 等宽柱 + 值标签;line 折线 + 点;pie 扇形 + 图例;空数据容错) */
|
|
366
|
+
function renderMiniChart(chart, doc) {
|
|
367
|
+
const svg = el(doc, "svg", {
|
|
368
|
+
viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
|
|
369
|
+
width: String(WIDTH),
|
|
370
|
+
height: String(HEIGHT),
|
|
371
|
+
class: `webskill-chart webskill-chart--${chart.kind}`,
|
|
372
|
+
role: "img"
|
|
373
|
+
});
|
|
374
|
+
if (chart.title) svg.appendChild(text$1(doc, WIDTH / 2, 14, chart.title, {
|
|
375
|
+
"text-anchor": "middle",
|
|
376
|
+
"font-size": "11",
|
|
377
|
+
fill: "#333"
|
|
378
|
+
}));
|
|
379
|
+
switch (chart.kind) {
|
|
380
|
+
case "bar":
|
|
381
|
+
renderBar(chart, svg, doc);
|
|
382
|
+
break;
|
|
383
|
+
case "line":
|
|
384
|
+
renderLine(chart, svg, doc);
|
|
385
|
+
break;
|
|
386
|
+
case "pie":
|
|
387
|
+
renderPie(chart, svg, doc);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
return svg;
|
|
391
|
+
}
|
|
392
|
+
/** A2UI/OpenUI 降级:chart → table(labels 为首列,series 各为一列) */
|
|
393
|
+
function chartToTable(chart) {
|
|
394
|
+
return {
|
|
395
|
+
type: "table",
|
|
396
|
+
columns: ["label", ...chart.series.map((s, i) => s.name ?? `series ${i + 1}`)],
|
|
397
|
+
rows: chart.labels.map((label, li) => [label, ...chart.series.map((s) => s.data[li] ?? null)])
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/** RenderBlock → DOM(markdown/json/table/image/file/chart 六种) */
|
|
185
401
|
function renderBlocks(container, blocks, doc) {
|
|
186
402
|
for (const block of blocks) switch (block.type) {
|
|
403
|
+
case "chart":
|
|
404
|
+
container.appendChild(renderMiniChart(block.chart, doc));
|
|
405
|
+
break;
|
|
187
406
|
case "markdown":
|
|
188
407
|
container.appendChild(renderMiniMarkdown(block.text, doc));
|
|
189
408
|
break;
|
|
@@ -551,6 +770,15 @@ function toOpenUiLang(request) {
|
|
|
551
770
|
options: request.options
|
|
552
771
|
}];
|
|
553
772
|
break;
|
|
773
|
+
case "authorize":
|
|
774
|
+
title = "Authorization required";
|
|
775
|
+
fields = [{
|
|
776
|
+
name: "approved",
|
|
777
|
+
label: request.message,
|
|
778
|
+
control: "boolean",
|
|
779
|
+
defaultValue: false
|
|
780
|
+
}];
|
|
781
|
+
break;
|
|
554
782
|
}
|
|
555
783
|
lines.push(...fieldStatements(fields));
|
|
556
784
|
const children = fields.map((_, i) => `f${i}`);
|
|
@@ -617,7 +845,11 @@ function toA2uiMessages(request) {
|
|
|
617
845
|
components.push(text("title", request.title, "h3"));
|
|
618
846
|
children.push("title");
|
|
619
847
|
}
|
|
620
|
-
|
|
848
|
+
if (request.type === "authorize") {
|
|
849
|
+
components.push(text("title", "Authorization required", "h3"));
|
|
850
|
+
children.push("title");
|
|
851
|
+
}
|
|
852
|
+
const message = request.type === "ask" || request.type === "confirm" || request.type === "select" || request.type === "authorize" ? request.message : void 0;
|
|
621
853
|
if (message) {
|
|
622
854
|
components.push(text("message", message));
|
|
623
855
|
children.push("message");
|
|
@@ -632,7 +864,7 @@ function toA2uiMessages(request) {
|
|
|
632
864
|
label: request.message,
|
|
633
865
|
type: "boolean",
|
|
634
866
|
defaultValue: request.defaultValue ?? true
|
|
635
|
-
}] : [{
|
|
867
|
+
}] : request.type === "authorize" ? [] : [{
|
|
636
868
|
name: "selected",
|
|
637
869
|
label: request.message,
|
|
638
870
|
type: "select",
|
|
@@ -675,7 +907,7 @@ function toA2uiMessages(request) {
|
|
|
675
907
|
});
|
|
676
908
|
children.push(`field_${field.name}`);
|
|
677
909
|
}
|
|
678
|
-
components.push(text("submit_label", "Submit"));
|
|
910
|
+
components.push(text("submit_label", request.type === "authorize" ? "Allow" : "Submit"));
|
|
679
911
|
components.push({
|
|
680
912
|
id: "submit",
|
|
681
913
|
component: "Button",
|
|
@@ -686,7 +918,7 @@ function toA2uiMessages(request) {
|
|
|
686
918
|
context: { requestId: request.id }
|
|
687
919
|
} }
|
|
688
920
|
});
|
|
689
|
-
components.push(text("cancel_label", "Cancel"));
|
|
921
|
+
components.push(text("cancel_label", request.type === "authorize" ? "Deny" : "Cancel"));
|
|
690
922
|
components.push({
|
|
691
923
|
id: "cancel",
|
|
692
924
|
component: "Button",
|
|
@@ -15359,13 +15591,15 @@ var LitRendererBridge = class {
|
|
|
15359
15591
|
const surfaces = processor.getClientDataModel("v0.9.1")?.surfaces;
|
|
15360
15592
|
const formValues = typeof surfaces?.[surfaceId] === "object" && surfaces[surfaceId] !== null ? surfaces[surfaceId]["form"] ?? action.context : action.context;
|
|
15361
15593
|
cleanup(surfaceEl);
|
|
15362
|
-
|
|
15594
|
+
const response = fromA2uiAction({
|
|
15363
15595
|
...action,
|
|
15364
15596
|
context: {
|
|
15365
15597
|
...action.context,
|
|
15366
15598
|
values: formValues
|
|
15367
15599
|
}
|
|
15368
|
-
})
|
|
15600
|
+
});
|
|
15601
|
+
if (input.type === "authorize" && !response.cancelled) response.value = true;
|
|
15602
|
+
resolve(response);
|
|
15369
15603
|
}, { version: "v0.9.1" });
|
|
15370
15604
|
processor.onSurfaceCreated((surface) => {
|
|
15371
15605
|
const el = this.#doc.createElement("a2ui-surface");
|
|
@@ -15379,4 +15613,4 @@ var LitRendererBridge = class {
|
|
|
15379
15613
|
};
|
|
15380
15614
|
|
|
15381
15615
|
//#endregion
|
|
15382
|
-
export {
|
|
15616
|
+
export { renderRenderResult as C, toVercelToolInvocation as D, toOpenUiLang as E, renderMiniMarkdown as S, toA2uiMessages as T, fromOpenUiAction as _, CHART_PALETTE as a, renderBlocks as b, OPENUI_SUBMIT_ACTION as c, WEBSKILL_STYLES_CSS as d, WebFormBridge as f, fromA2uiAction as g, ensureStyles as h, A2UI_VERSION as i, VERCEL_INTERACTION_TOOL_NAME as l, collectValues as m, A2UI_CANCEL_ACTION as n, LitRendererBridge as o, chartToTable as p, A2UI_SUBMIT_ACTION as r, OPENUI_CANCEL_ACTION as s, A2UI_BASIC_CATALOG_ID as t, VercelUiBridge as u, fromVercelToolResult as v, shapeInteractionValue as w, renderMiniChart as x, interactionToFormModel as y };
|