@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.
@@ -1,4 +1,5 @@
1
- import { A as parseBridgeRequest, C as bridgeError, H as WebSkillError, I as SKILLS_LOCKFILE, J as isValidSkillName, L as SKILL_MANIFEST_FILE, W as buildManifest, Z as parseSkillMarkdown, et as resolveInsideRoot, k as normalizeToolContent, nt as verifyManifest, o as FsArtifactStore, s as FsMemoryStore, tt as validateSkills } from "./dist-COsE72Ct.js";
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
- /** 能力桥宿主侧处理:能力关闭即 TOOL_UNSUPPORTED;委托 context */
405
+ /** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
386
406
  async #handleBridge(request, context) {
387
- const unsupported = (capability) => bridgeError(request.id, "TOOL_UNSUPPORTED", `Capability "${capability}" is disabled`);
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
- if (!this.#capabilities.readReference) return unsupported("readReference");
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
- if (!this.#capabilities.writeArtifact) return unsupported("writeArtifact");
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 (!this.#capabilities.confirm || !context.confirm) return unsupported("confirm");
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
- /** 四类 InteractionRequest → 统一中间模型(框架无关) */
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
- /** RenderBlock → DOM(markdown/json/table/image/file 五种) */
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
- const message = request.type === "ask" || request.type === "confirm" || request.type === "select" ? request.message : void 0;
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
- resolve(fromA2uiAction({
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 { toOpenUiLang as C, toA2uiMessages as S, interactionToFormModel as _, LitRendererBridge as a, renderRenderResult as b, VERCEL_INTERACTION_TOOL_NAME as c, WebFormBridge as d, collectValues as f, fromVercelToolResult as g, fromOpenUiAction as h, A2UI_VERSION as i, VercelUiBridge as l, fromA2uiAction as m, A2UI_CANCEL_ACTION as n, OPENUI_CANCEL_ACTION as o, ensureStyles as p, A2UI_SUBMIT_ACTION as r, OPENUI_SUBMIT_ACTION as s, A2UI_BASIC_CATALOG_ID as t, WEBSKILL_STYLES_CSS as u, renderBlocks as v, toVercelToolInvocation as w, shapeInteractionValue as x, renderMiniMarkdown as y };
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 };