@webskill/sdk 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,3 @@
1
- //#region ../core/src/text/untrustedText.ts
2
- /** 一行不可信文本的长度上限;两处消费方(数据源候选说明、文档投放的来源描述)共用 */
3
- const UNTRUSTED_LINE_LIMIT = 200;
4
- /**
5
- * 把一段**由页面/技能自由书写、又会进模型上下文或授权告知**的文本压成单行并截断。
6
- *
7
- * 换行是提示注入最省力的载体:一句 `\n\n此操作已获管理员批准` 就能把告知变成反面的东西,
8
- * 控制字符同理。因此先把它们全部折成空格,再收敛连续空白,最后截断。
9
- * @experimental
10
- */
11
- function sanitizeUntrustedLine(value, limit = 200) {
12
- return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, limit);
13
- }
14
-
15
- //#endregion
16
1
  //#region ../core/src/attachment/kind.ts
17
2
  /** 可作为图片分片外发的 MIME 类型 @experimental */
18
3
  const IMAGE_MIME_TYPES = [
@@ -67,11 +52,12 @@ function classifyAttachment(contentType, fileName) {
67
52
  * @experimental
68
53
  */
69
54
  function formatAttachmentText(input) {
70
- const clipped = input.body.length > 32768 ? `${input.body.slice(0, ATTACHMENT_TEXT_LIMIT)}\n[truncated]` : input.body;
55
+ const limit = input.limit ?? 32768;
56
+ const clipped = input.body.length > limit ? `${input.body.slice(0, limit)}\n[truncated]` : input.body;
71
57
  const bodyOnly = input.kind === "document-text" ? ", body text only" : "";
72
58
  const from = input.origin === "download" ? ", from this device's downloads" : "";
73
59
  return `--- Attachment: ${input.name} (${input.contentType}${bodyOnly}${from}) ---\n${clipped}`;
74
60
  }
75
61
 
76
62
  //#endregion
77
- export { TEXT_EXTENSIONS as a, formatAttachmentText as c, IMAGE_MIME_TYPES as i, UNTRUSTED_LINE_LIMIT as l, DOCX_MIME as n, XLSX_MIME as o, FILE_MIME_TYPES as r, classifyAttachment as s, ATTACHMENT_TEXT_LIMIT as t, sanitizeUntrustedLine as u };
63
+ export { TEXT_EXTENSIONS as a, formatAttachmentText as c, IMAGE_MIME_TYPES as i, DOCX_MIME as n, XLSX_MIME as o, FILE_MIME_TYPES as r, classifyAttachment as s, ATTACHMENT_TEXT_LIMIT as t };
@@ -0,0 +1,245 @@
1
+ import { t as WebSkillError } from "./errors-BDZNpC13.js";
2
+ import writeXlsxFile from "write-excel-file/universal";
3
+
4
+ //#region ../runtime/src/tools/types.ts
5
+ /**
6
+ * 参与 `toolResultMaxBytes` 截断的分片类型(FR-23.3)。**判据的单一来源。**
7
+ *
8
+ * 白名单而不是黑名单:黑名单式下新加的分片默认「参与截断」,
9
+ * 漏改一处就会把一份 PDF 截成坏文件、把 docx 文本腰斩,而且没有任何报错。
10
+ * 白名单式下新分片默认**不参与**,要受文本预算约束必须显式加进来。
11
+ * @experimental
12
+ */
13
+ const TEXT_BUDGETED_CONTENT_TYPES = /* @__PURE__ */ new Set(["text", "json"]);
14
+ /**
15
+ * `file` 分片的上限(FR-23.4),量的是**实际上线的 base64 长度**而不是解码后的字节——
16
+ * provider 的限额算的是请求载荷。
17
+ *
18
+ * 2026-08-14 查证的三家上限:
19
+ * | provider | 上限 | 出处 |
20
+ * | --------- | --------------------------------------- | ----------------------- |
21
+ * | Anthropic | **32 MB**(**整个请求载荷**)、600 页 | PDF support / 请求大小 |
22
+ * | OpenAI | 单文件 50 MB,全部文件合计 50 MB | File inputs / 使用须知 |
23
+ * | Gemini | 50 MB 或 1000 页(内联与 Files API 同) | 文档理解 / 技术详情 |
24
+ *
25
+ * 取最小值 Anthropic 的 32 MB。它是**整个请求**的额度,不是文档单独的额度,
26
+ * 所以再留出余量给系统提示词、catalog(约 34 KB)、历史消息与其余分片。
27
+ * 25 000 KB base64 ≈ 18.75 MB 原始 PDF。取 1024 的整数倍:设置界面按 KB 展示,
28
+ * 十进制的 24 000 000 会显示成 23437.5 这种读不出来的数。
29
+ */
30
+ const DEFAULT_MAX_DOCUMENT_BYTES = 256e5;
31
+ /**
32
+ * `document-text` 分片的上限(FR-23.4)。
33
+ *
34
+ * 约束来自**上下文窗口**而不是请求大小:抽出来的文本要整段进上下文。
35
+ * 按英文约 4 字节/token 折算,500 KB ≈ 128K token,占 200K 窗口的多半,
36
+ * 给历史消息与模型的回答留下其余。
37
+ */
38
+ const DEFAULT_MAX_DOCUMENT_TEXT_BYTES = 512e3;
39
+ /**
40
+ * 单次 fetchData 结果的字节上限(分册 16,FR-16.6):1000 KB。
41
+ * 结构化数据不是文档,量级差一个数量级;超出**只拒不截**——截断的 JSON 解不出来。
42
+ */
43
+ const DEFAULT_MAX_DATA_SOURCE_BYTES = 1024e3;
44
+ /**
45
+ * 单个上传文件交给脚本的字节上限(0.15.0 分册 17,FR-17.5):20 MB。
46
+ * 取得比模型图片预算宽:字节不进模型上下文,只进脚本,护的是内存不是 token。
47
+ */
48
+ const DEFAULT_MAX_UPLOAD_FILE_BYTES = 20 * 1024 * 1024;
49
+
50
+ //#endregion
51
+ //#region ../runtime/src/spreadsheet/types.ts
52
+ /** 规模上限(FR-10.7)。越界一律 `VALIDATION_FAILED`,不静默截断 */
53
+ const SPREADSHEET_LIMITS = {
54
+ sheets: 16,
55
+ rowsPerSheet: 1e4,
56
+ columnsPerSheet: 256,
57
+ /** Excel 对单个单元格文本的硬限制 */
58
+ charsPerCell: 32767,
59
+ bytes: 16 * 1024 * 1024
60
+ };
61
+ const SPREADSHEET_MIME_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
62
+ const SPREADSHEET_EXTENSION = ".xlsx";
63
+ /**
64
+ * 本版**不支持**的 xlsx 能力(FR-10.8,备案 D75)。
65
+ *
66
+ * 导出这张表是为了让上层能如实向用户/模型声明缺什么,而不是笼统写「基础支持」——
67
+ * 与 `XLSX_UNEXTRACTED` 同一条口径。
68
+ */
69
+ const XLSX_UNSUPPORTED = [
70
+ "cell-styles",
71
+ "fonts-and-colors",
72
+ "borders",
73
+ "formulas",
74
+ "merged-cells",
75
+ "charts",
76
+ "images",
77
+ "comments",
78
+ "date-and-time-cell-types",
79
+ "column-widths",
80
+ "frozen-panes",
81
+ "auto-filter",
82
+ "incremental-edit-of-existing-file"
83
+ ];
84
+
85
+ //#endregion
86
+ //#region ../runtime/src/spreadsheet/encodeSpreadsheet.ts
87
+ /** Excel 不允许出现在工作表名里的字符 */
88
+ const ILLEGAL_SHEET_NAME_CHARS = /[:\\/?*[\]]/;
89
+ const MAX_SHEET_NAME_LENGTH = 31;
90
+ function invalid(message) {
91
+ return new WebSkillError("VALIDATION_FAILED", message);
92
+ }
93
+ /**
94
+ * 把一个 `SpreadsheetCell` 映射成库的输入。
95
+ *
96
+ * **必须显式传 `type`**:裸值会让库自己推断类型,虽然实测 `'0123'` 恰好推断正确,
97
+ * 但那是库的实现细节不是它的契约,而 FR-10.2a 要求「不做智能识别」。
98
+ *
99
+ * `''` 在这里归一成 `null`(DV-3):xlsx 里「空文本」与「空单元格」产出的字节
100
+ * 完全相同,归一让下游只有一条路径。
101
+ */
102
+ function toCell(value) {
103
+ if (value === null || value === void 0 || value === "") return null;
104
+ if (typeof value === "string") return {
105
+ value,
106
+ type: String
107
+ };
108
+ if (typeof value === "number") return {
109
+ value,
110
+ type: Number
111
+ };
112
+ return {
113
+ value,
114
+ type: Boolean
115
+ };
116
+ }
117
+ function assertSheetName(name, index, seen) {
118
+ if (typeof name !== "string" || name === "") throw invalid(`Sheet ${index + 1}: "name" must be a non-empty string.`);
119
+ if (name.length > MAX_SHEET_NAME_LENGTH) throw invalid(`Sheet "${name}" has a name of ${name.length} characters (max ${MAX_SHEET_NAME_LENGTH}); Excel would refuse to open the file.`);
120
+ if (ILLEGAL_SHEET_NAME_CHARS.test(name)) throw invalid(`Sheet "${name}" has a name containing one of : \\ / ? * [ ], which Excel does not allow.`);
121
+ const key = name.toLowerCase();
122
+ const previous = seen.get(key);
123
+ if (previous !== void 0) throw invalid(`Sheet "${name}" duplicates the name of sheet ${previous + 1}; Excel requires unique sheet names.`);
124
+ seen.set(key, index);
125
+ return name;
126
+ }
127
+ function assertCell(value, sheetName, row, column) {
128
+ if (value === null || value === void 0) return;
129
+ if (typeof value === "number") {
130
+ if (!Number.isFinite(value)) throw invalid(`Sheet "${sheetName}" row ${row} column ${column}: value must be a finite number.`);
131
+ return;
132
+ }
133
+ if (typeof value === "string") {
134
+ if (value.length > SPREADSHEET_LIMITS.charsPerCell) throw invalid(`Sheet "${sheetName}" row ${row} column ${column}: text is ${value.length} characters (max ${SPREADSHEET_LIMITS.charsPerCell}).`);
135
+ return;
136
+ }
137
+ if (typeof value === "boolean") return;
138
+ throw invalid(`Sheet "${sheetName}" row ${row} column ${column}: cell must be a string, number, boolean or null (got ${typeof value}).`);
139
+ }
140
+ /**
141
+ * 全量校验(FR-10.7)。**在映射与编码之前一次查完**,抛第一个越界项的定位。
142
+ *
143
+ * 不做边查边编码:编码到一半才失败会留下一个已经开始的 Blob,
144
+ * 而且用户看到的错误顺序取决于内部遍历顺序,不稳定。
145
+ */
146
+ function assertSpec(spec) {
147
+ const sheets = spec?.sheets;
148
+ if (!Array.isArray(sheets) || sheets.length === 0) throw invalid("Spreadsheet must contain at least one sheet.");
149
+ if (sheets.length > SPREADSHEET_LIMITS.sheets) throw invalid(`Spreadsheet has ${sheets.length} sheets (max ${SPREADSHEET_LIMITS.sheets}).`);
150
+ const seen = /* @__PURE__ */ new Map();
151
+ sheets.forEach((sheet, index) => {
152
+ const name = assertSheetName(sheet?.name, index, seen);
153
+ const { columns, rows } = sheet;
154
+ if (columns !== void 0 && !Array.isArray(columns)) throw invalid(`Sheet "${name}": "columns" must be an array of header strings when present.`);
155
+ if (!Array.isArray(rows)) throw invalid(`Sheet "${name}": "rows" must be an array of rows.`);
156
+ if (rows.length > SPREADSHEET_LIMITS.rowsPerSheet) throw invalid(`Sheet "${name}" exceeds the row limit: ${rows.length} rows (max ${SPREADSHEET_LIMITS.rowsPerSheet}).`);
157
+ const widest = rows.reduce((max, row) => Math.max(max, Array.isArray(row) ? row.length : 0), columns?.length ?? 0);
158
+ if (widest > SPREADSHEET_LIMITS.columnsPerSheet) throw invalid(`Sheet "${name}" exceeds the column limit: ${widest} columns (max ${SPREADSHEET_LIMITS.columnsPerSheet}).`);
159
+ const headerOffset = columns === void 0 ? 0 : 1;
160
+ columns?.forEach((header, column) => assertCell(header, name, 1, column + 1));
161
+ rows.forEach((row, rowIndex) => {
162
+ if (!Array.isArray(row)) throw invalid(`Sheet "${name}" row ${rowIndex + 1 + headerOffset}: row must be an array of cells.`);
163
+ row.forEach((cell, column) => assertCell(cell, name, rowIndex + 1 + headerOffset, column + 1));
164
+ });
165
+ });
166
+ return sheets;
167
+ }
168
+ /**
169
+ * 表头行。加粗写死在这里,**不向调用方暴露任何样式入参**——
170
+ * D75 延后的是样式 API,不是「产出的文件里一个样式属性都不许有」:
171
+ * 表头不加粗的表格在 Excel 里看不出哪行是表头。
172
+ */
173
+ function headerRow(columns) {
174
+ return columns.map((header) => {
175
+ const cell = toCell(header);
176
+ return cell === null ? null : {
177
+ ...cell,
178
+ fontWeight: "bold"
179
+ };
180
+ });
181
+ }
182
+ /**
183
+ * 把表格规格编码为 xlsx 字节。
184
+ *
185
+ * 纯函数——不认识产物、run 与工具,因此工具、脚本能力、测试三个消费面共用同一份
186
+ * 实现,AC-10.8「双消费面逐字节相同」是结构性成立的。
187
+ */
188
+ async function encodeSpreadsheet(spec) {
189
+ const blob = await writeXlsxFile(assertSpec(spec).map((sheet) => ({
190
+ sheet: sheet.name,
191
+ data: [...sheet.columns === void 0 ? [] : [headerRow(sheet.columns)], ...sheet.rows.map((row) => row.map(toCell))]
192
+ }))).toBlob();
193
+ const bytes = new Uint8Array(await blob.arrayBuffer());
194
+ if (bytes.length > SPREADSHEET_LIMITS.bytes) throw invalid(`The encoded spreadsheet is ${bytes.length} bytes (max ${SPREADSHEET_LIMITS.bytes}); reduce the number of rows or sheets.`);
195
+ return bytes;
196
+ }
197
+
198
+ //#endregion
199
+ //#region ../runtime/src/tools/linkedDocument.ts
200
+ const READ_LINKED_DOCUMENT_TOOL_NAME = "read_linked_document";
201
+ /**
202
+ * 能直接给模型用的 MIME。其余一律拒绝并列出这份清单——
203
+ * 「尽力而为地猜格式」会让模型收到乱码却以为读成功了。
204
+ */
205
+ const SUPPORTED_DOCUMENT_MIME = {
206
+ pdf: "application/pdf",
207
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
208
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
209
+ };
210
+ /** 引擎侧固定流程(§2.3)用到的错误文案,集中一处便于判据引用 */
211
+ const UNSUPPORTED_DOCUMENT_MESSAGE = `Only PDF (${SUPPORTED_DOCUMENT_MIME.pdf}), Word (${SUPPORTED_DOCUMENT_MIME.docx}), Excel (${SUPPORTED_DOCUMENT_MIME.xlsx}) and plain text documents can be read.`;
212
+ const READ_LINKED_DOCUMENT_INPUT_SCHEMA = {
213
+ type: "object",
214
+ properties: { url: {
215
+ type: "string",
216
+ description: "The href of a link that appeared in a perceive_page result."
217
+ } },
218
+ required: ["url"]
219
+ };
220
+ /**
221
+ * 内建工具:读页面链接指向的文档。
222
+ *
223
+ * **始终注册**(只要宿主装配了 reader):docx / xlsx 与纯文本这条路对所有模型成立。
224
+ * PDF 目标在模型不支持文档时**取数后拒绝**,而不是入口就拦 ——
225
+ * 入口拦会误伤 docx,它抽成文本后根本不需要文档能力。
226
+ */
227
+ const READ_LINKED_DOCUMENT_TOOL = {
228
+ name: READ_LINKED_DOCUMENT_TOOL_NAME,
229
+ description: "Read a document linked from the current page. Pass the exact \"href\" from a perceive_page result. Cross-origin documents need the user to confirm each time.",
230
+ inputSchema: READ_LINKED_DOCUMENT_INPUT_SCHEMA,
231
+ source: "builtin"
232
+ };
233
+ /** 二进制转 base64;`btoa` 只吃 latin1,必须逐字节喂而不是先 decode 成字符串 */
234
+ function toBase64(bytes) {
235
+ let binary = "";
236
+ for (const byte of bytes) binary += String.fromCharCode(byte);
237
+ return btoa(binary);
238
+ }
239
+ /** `toBase64` 的逆;分片里的文档要交回抽取器时用(0.13.0 FR-22.3) */
240
+ function fromBase64(data) {
241
+ return Uint8Array.from(atob(data), (char) => char.charCodeAt(0));
242
+ }
243
+
244
+ //#endregion
245
+ export { fromBase64 as a, SPREADSHEET_EXTENSION as c, XLSX_UNSUPPORTED as d, DEFAULT_MAX_DATA_SOURCE_BYTES as f, TEXT_BUDGETED_CONTENT_TYPES as g, DEFAULT_MAX_UPLOAD_FILE_BYTES as h, UNSUPPORTED_DOCUMENT_MESSAGE as i, SPREADSHEET_LIMITS as l, DEFAULT_MAX_DOCUMENT_TEXT_BYTES as m, READ_LINKED_DOCUMENT_TOOL_NAME as n, toBase64 as o, DEFAULT_MAX_DOCUMENT_BYTES as p, SUPPORTED_DOCUMENT_MIME as r, encodeSpreadsheet as s, READ_LINKED_DOCUMENT_TOOL as t, SPREADSHEET_MIME_TYPE as u };
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { K as JsonSchema, _ as LlmToolSpec, ft as SkillDocument, ut as SkillCatalogEntry } from "./types-Btpdd1y--BcxQ10Fa.js";
2
- import { R as ExternalSkillProvider, _n as ToolDisclosure, fr as mergeCatalogEntries, yn as ToolResult, z as ExternalToolSource } from "./index-BDyXe-a5.js";
1
+ import { _ as LlmToolSpec, dt as SkillCatalogEntry, pt as SkillDocument, q as JsonSchema } from "./types-BvTV_05--BW8zDuEk.js";
2
+ import { Cn as ToolDisclosure, R as ExternalSkillProvider, Tn as ToolResult, jr as mergeCatalogEntries, z as ExternalToolSource } from "./index-DHopiH_m.js";
3
3
  import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
4
  import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/dist/node.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as SKILLS_LOCKFILE, Ct as SkillsLockfile, Et as TrustedKeyStore, H as FileStat, K as JsonSchema, N as ArchiveLimits, S as UiBridge, U as FileSystemProvider, _t as SkillManifest, at as SignatureAuditSink, b as RenderResultRequest, c as InteractionResponse, et as SKILL_MANIFEST_FILE, gt as SkillManagerPort, jt as VerifyResult, kt as UnsignedPolicy, pt as SkillInstallSource, s as InteractionRequest } from "./types-Btpdd1y--BcxQ10Fa.js";
2
- import { Gn as WebSkillRuntimeDeps, Gt as SchemaInferer, H as FsMemoryStore, Kt as ScriptExecutionContext, V as FsArtifactStore, Wn as WebSkillRuntime, Zn as createScriptContext, ct as NetworkPolicy, gn as ToolDefinition, m as ApprovalScope, qt as ScriptExecutor, y as BridgeCapabilities, yn as ToolResult } from "./index-BDyXe-a5.js";
3
- import { a as AuditLog, b as SkillVersionStore, d as CandidateSkill, m as CandidateStore, r as ApprovalPolicy } from "./skillVersionStore-B24Jjjry-v2CCU0Xo.js";
1
+ import { At as UnsignedPolicy, Dt as TrustedKeyStore, H as FileStat, Mt as VerifyResult, N as ArchiveLimits, S as UiBridge, U as FileSystemProvider, W as FileWriteStream, _t as SkillManagerPort, b as RenderResultRequest, c as InteractionResponse, et as SKILLS_LOCKFILE, mt as SkillInstallSource, ot as SignatureAuditSink, q as JsonSchema, s as InteractionRequest, tt as SKILL_MANIFEST_FILE, vt as SkillManifest, wt as SkillsLockfile } from "./types-BvTV_05--BW8zDuEk.js";
2
+ import { H as FsMemoryStore, Jt as SchemaInferer, Sn as ToolDefinition, Tn as ToolResult, V as FsArtifactStore, Xt as ScriptExecutor, Yt as ScriptExecutionContext, ar as WebSkillRuntime, ct as NetworkPolicy, hr as createScriptContext, m as ApprovalScope, or as WebSkillRuntimeDeps, y as BridgeCapabilities } from "./index-DHopiH_m.js";
3
+ import { a as AuditLog, b as SkillVersionStore, d as CandidateSkill, m as CandidateStore, r as ApprovalPolicy } from "./skillVersionStore-B24Jjjry-BkOk9wI_.js";
4
4
  import { n as LlmEnvConfig, s as probeLlmCapabilities, t as LlmCapabilities } from "./env-AK3cSMEA-Dli6QU5E.js";
5
5
  import { Readable, Writable } from "node:stream";
6
6
  //#region ../node/dist/index.d.ts
@@ -23,6 +23,11 @@ declare class NodeFS implements FileSystemProvider {
23
23
  appendText(p: string, content: string): Promise<void>;
24
24
  readBinary(p: string): Promise<Uint8Array>;
25
25
  writeBinary(p: string, content: Uint8Array): Promise<void>;
26
+ /**
27
+ * 分块写入(FR-18.2c)。先写同目录临时文件,`close()` 时 rename 到位:
28
+ * 中途失败只会留下临时文件(随即删掉),目标路径要么没有、要么是完整内容(FR-18.2e)。
29
+ */
30
+ createWriteStream(p: string): Promise<FileWriteStream>;
26
31
  exists(p: string): Promise<boolean>;
27
32
  stat(p: string): Promise<FileStat>;
28
33
  list(p: string): Promise<FileStat[]>;
package/dist/node.js CHANGED
@@ -2,10 +2,11 @@ import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
2
2
  import { A as SKILL_MANIFEST_FILE, D as verifySkillSignature, M as buildManifest, O as MANIFEST_EXCLUDED_FILES, P as verifyManifest, b as parseSkillMarkdown, c as unzipWithLimits, f as SKILL_PACK_FILE, k as SKILLS_LOCKFILE, m as parseSkillPackManifest, o as readResponseWithLimit, p as exportSkills, s as resolveArchiveLimits, t as validateSkills, w as readSkillSignature, y as isValidSkillName } from "./skill-CAJMsLod.js";
3
3
  import { a as atomicWriteText, o as isAtomicTempPath, r as resolveInsideRoot, t as assertSafePathSegment } from "./pathSecurity-B1owvJAF.js";
4
4
  import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
5
- import { B as createScriptContext, a as networkPolicyLibSource, c as bridgeError, d as FsMemoryStore, f as WebSkillRuntime, l as parseBridgeRequest, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-Bbh_Apwg.js";
5
+ import { K as createScriptContext, a as networkPolicyLibSource, c as bridgeError, d as FsMemoryStore, f as WebSkillRuntime, l as parseBridgeRequest, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-DwN2o2QG.js";
6
+ import { u as SPREADSHEET_MIME_TYPE } from "./linkedDocument-C0gj1sMq.js";
6
7
  import { n as normalizeToolContent } from "./external-_ZRQe-V9.js";
7
8
  import { i as probeLlmCapabilities } from "./env-Bj1MI2Ww.js";
8
- import { t as AUDIT_EVENT_TYPES } from "./eventTypes-CwciaqCU.js";
9
+ import { t as AUDIT_EVENT_TYPES } from "./eventTypes-DcTQXSV1.js";
9
10
  import { createRequire } from "node:module";
10
11
  import { unzipSync, zipSync } from "fflate";
11
12
  import { existsSync, promises, realpathSync } from "node:fs";
@@ -93,6 +94,50 @@ var NodeFS = class NodeFS {
93
94
  await promises.mkdir(path.dirname(target), { recursive: true });
94
95
  await promises.writeFile(target, content);
95
96
  }
97
+ /**
98
+ * 分块写入(FR-18.2c)。先写同目录临时文件,`close()` 时 rename 到位:
99
+ * 中途失败只会留下临时文件(随即删掉),目标路径要么没有、要么是完整内容(FR-18.2e)。
100
+ */
101
+ async createWriteStream(p) {
102
+ await this.#assertContained(p);
103
+ const target = toPlatform$4(p);
104
+ await promises.mkdir(path.dirname(target), { recursive: true });
105
+ const temp = `${target}.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}.part`;
106
+ const handle = await promises.open(temp, "wx");
107
+ let settled = false;
108
+ const discard = async () => {
109
+ await handle.close().catch(() => void 0);
110
+ await promises.rm(temp, { force: true }).catch(() => void 0);
111
+ };
112
+ return {
113
+ write: async (chunk) => {
114
+ if (settled) throw new WebSkillError("FS_PERMISSION_DENIED", `Cannot write to a finished write stream: ${p}`);
115
+ try {
116
+ await handle.write(chunk);
117
+ } catch (e) {
118
+ settled = true;
119
+ await discard();
120
+ throw this.#mapError(e, p);
121
+ }
122
+ },
123
+ close: async () => {
124
+ if (settled) throw new WebSkillError("FS_PERMISSION_DENIED", `Cannot close a finished write stream: ${p}`);
125
+ settled = true;
126
+ try {
127
+ await handle.close();
128
+ await promises.rename(temp, target);
129
+ } catch (e) {
130
+ await promises.rm(temp, { force: true }).catch(() => void 0);
131
+ throw this.#mapError(e, p);
132
+ }
133
+ },
134
+ abort: async () => {
135
+ if (settled) return;
136
+ settled = true;
137
+ await discard();
138
+ }
139
+ };
140
+ }
96
141
  async exists(p) {
97
142
  await this.#assertContained(p);
98
143
  try {
@@ -407,7 +452,8 @@ var SandboxedScriptExecutor = class {
407
452
  runId: context.runId,
408
453
  args,
409
454
  scriptSource: await this.#fs.readText(scriptPath),
410
- ...context.readUploadFile ? { uploadFiles: true } : {}
455
+ ...context.readUploadFile ? { uploadFiles: true } : {},
456
+ ...context.writeSpreadsheet ? { writeSpreadsheet: true } : {}
411
457
  }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
412
458
  if (!result.ok) {
413
459
  const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
@@ -565,6 +611,20 @@ var SandboxedScriptExecutor = class {
565
611
  value: artifact
566
612
  };
567
613
  }
614
+ case "writeSpreadsheet": {
615
+ if (!context.writeSpreadsheet) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"writeSpreadsheet\" is unavailable");
616
+ const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
617
+ path: request.path,
618
+ mimeType: SPREADSHEET_MIME_TYPE
619
+ });
620
+ if (denied) return denied;
621
+ const artifact = await context.writeSpreadsheet(request.path, request.spec, { ...request.metadata === void 0 ? {} : { metadata: request.metadata } });
622
+ return {
623
+ id: request.id,
624
+ ok: true,
625
+ value: artifact
626
+ };
627
+ }
568
628
  case "confirm": {
569
629
  if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
570
630
  const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`, { question: request.message });
@@ -809,7 +869,8 @@ var ProcessSandboxExecutor = class {
809
869
  args,
810
870
  scriptSource: await this.#fs.readText(scriptPath),
811
871
  scratchDir: slot.artifactDir,
812
- ...context.readUploadFile ? { uploadFiles: true } : {}
872
+ ...context.readUploadFile ? { uploadFiles: true } : {},
873
+ ...context.writeSpreadsheet ? { writeSpreadsheet: true } : {}
813
874
  }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
814
875
  if (!result.ok) {
815
876
  const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
@@ -963,6 +1024,20 @@ var ProcessSandboxExecutor = class {
963
1024
  value: artifact
964
1025
  };
965
1026
  }
1027
+ case "writeSpreadsheet": {
1028
+ if (!context.writeSpreadsheet) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"writeSpreadsheet\" is unavailable");
1029
+ const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
1030
+ path: request.path,
1031
+ mimeType: SPREADSHEET_MIME_TYPE
1032
+ });
1033
+ if (denied) return denied;
1034
+ const artifact = await context.writeSpreadsheet(request.path, request.spec, { ...request.metadata === void 0 ? {} : { metadata: request.metadata } });
1035
+ return {
1036
+ id: request.id,
1037
+ ok: true,
1038
+ value: artifact
1039
+ };
1040
+ }
966
1041
  case "confirm": {
967
1042
  if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
968
1043
  const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`, { question: request.message });
@@ -178,6 +178,11 @@ async function main(task) {
178
178
  metadata: options?.metadata
179
179
  }),
180
180
  confirm: (message) => callCapability("confirm", { message }),
181
+ ...task.writeSpreadsheet ? { writeSpreadsheet: (path, spec, options) => callCapability("writeSpreadsheet", {
182
+ path,
183
+ spec,
184
+ metadata: options?.metadata
185
+ }) } : {},
181
186
  ...task.uploadFiles ? {
182
187
  listUploadFiles: () => callCapability("listUploadFiles", {}),
183
188
  readUploadFile: async (fileId) => new Uint8Array(await callCapability("readUploadFile", { fileId }))
@@ -239,6 +239,11 @@ async function main() {
239
239
  metadata: options?.metadata
240
240
  }),
241
241
  confirm: (message) => callCapability("confirm", { message }),
242
+ ...task.writeSpreadsheet ? { writeSpreadsheet: (path, spec, options) => callCapability("writeSpreadsheet", {
243
+ path,
244
+ spec,
245
+ metadata: options?.metadata
246
+ }) } : {},
242
247
  ...task.uploadFiles ? {
243
248
  listUploadFiles: () => callCapability("listUploadFiles", {}),
244
249
  readUploadFile: async (fileId) => new Uint8Array(await callCapability("readUploadFile", { fileId }))
@@ -1,4 +1,4 @@
1
- import { U as FileSystemProvider, X as PageQuery, Y as Page, _t as SkillManifest, ut as SkillCatalogEntry } from "./types-Btpdd1y--BcxQ10Fa.js";
1
+ import { U as FileSystemProvider, X as Page, Z as PageQuery, dt as SkillCatalogEntry, vt as SkillManifest } from "./types-BvTV_05--BW8zDuEk.js";
2
2
  //#region ../governance/dist/skillVersionStore-B24Jjjry.d.ts
3
3
  //#region src/types.d.ts
4
4
  type CandidateStatus = 'draft' | 'pending-review' | 'approved' | 'published' | 'rejected';
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as UiBridge, c as InteractionResponse, l as LlmClient, m as LlmStreamEvent, n as ArtifactStore, p as LlmResponse, s as InteractionRequest, t as Artifact, u as LlmCompleteInput, v as MemoryStore } from "./types-Btpdd1y--BcxQ10Fa.js";
1
+ import { S as UiBridge, c as InteractionResponse, l as LlmClient, m as LlmStreamEvent, n as ArtifactStore, p as LlmResponse, s as InteractionRequest, t as Artifact, u as LlmCompleteInput, v as MemoryStore } from "./types-BvTV_05--BW8zDuEk.js";
2
2
  import { a as loadGoogleConfigFromEnv, i as loadAnthropicConfigFromEnv, n as LlmEnvConfig, o as loadLlmConfigFromEnv, r as ProviderEnvConfig } from "./env-AK3cSMEA-Dli6QU5E.js";
3
3
  //#region ../runtime/dist/testing.d.ts
4
4
  //#region src/llm/mockLlmClient.d.ts