@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.
@@ -0,0 +1,565 @@
1
+ import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
2
+
3
+ //#region ../agent/src/frame.ts
4
+ /**
5
+ * 帧指称的展示形式(分册 18 FR-18.2)。
6
+ *
7
+ * 感知与操作两侧各自声明自己的 scope 类型(那是刻意的,见各自 types.ts),
8
+ * 但「一条帧路径写成给人看的字符串」只能有一份实现——
9
+ * 两处各写一遍,审计里的帧名和确认卡里的帧名就会慢慢对不上。
10
+ */
11
+ /**
12
+ * 帧路径的稳定展示串:`'self'` / `'#a'` / `'#a >>> #b'`。
13
+ *
14
+ * `>>>` **只是展示分隔符**:CSS 选择器里可以合法出现任意字符,
15
+ * 反向解析这个串取回路径是不成立的,实现层一律传原始形状(D-18-1)。
16
+ * @experimental
17
+ */
18
+ function frameLabel(frame) {
19
+ if (typeof frame === "string") return frame;
20
+ if (frame.length === 0) return "self";
21
+ return frame.join(" >>> ");
22
+ }
23
+ /** 把两种形状归一成逐层选择器数组;`'self'` 与空数组都归一成空数组 @experimental */
24
+ function frameSteps(frame) {
25
+ if (typeof frame === "string") return frame === "self" ? [] : [frame];
26
+ return frame;
27
+ }
28
+
29
+ //#endregion
30
+ //#region ../agent/src/perception/types.ts
31
+ /**
32
+ * 两种形状归一成帧列表;旧形状等价于一条 `frame:'self'`(FR-24.1)。
33
+ *
34
+ * **这是判别的单一来源。** 散在各处写 `'frames' in scope` 会让
35
+ * 「旧配置还等价吗」这个问题没有唯一答案。
36
+ * @experimental
37
+ */
38
+ function toFrameScopes(scope) {
39
+ if ("frames" in scope) return scope.frames;
40
+ return [{
41
+ frame: "self",
42
+ include: scope.include,
43
+ ...scope.exclude ? { exclude: scope.exclude } : {}
44
+ }];
45
+ }
46
+
47
+ //#endregion
48
+ //#region ../agent/src/pageAction/types.ts
49
+ /**
50
+ * 两种形状归一成帧列表;旧形状等价于一条 `frame:'self'`。
51
+ * 与感知侧的 `toFrameScopes` 一样,是本侧判别的**单一来源**。
52
+ * @experimental
53
+ */
54
+ function toActionFrameScopes(scope) {
55
+ if ("frames" in scope) return scope.frames;
56
+ return [{
57
+ frame: "self",
58
+ include: scope.include,
59
+ ...scope.exclude ? { exclude: scope.exclude } : {}
60
+ }];
61
+ }
62
+ /**
63
+ * 本版的操作集(FR-25.1 / 0.16.0 FR-13.6 / 0.18.0 FR-10.1)。拖拽仍不在内(D64);
64
+ * 导航只有 `back` 一个,且它**不接受地址**——可达面被浏览器历史封死(SC-1)。
65
+ *
66
+ * `select`/`set`/`attach`/`scroll` 与既有几个走**同一条** policy 路径,
67
+ * 范围白名单、逐次确认、审计三条硬约束因此天然覆盖到它们——
68
+ * 前提是新动作不绕过 policy 直调执行器(AC-25.4 守这一点)。
69
+ * @experimental
70
+ */
71
+ const PAGE_ACTION_KINDS = [
72
+ "click",
73
+ "fill",
74
+ "submit",
75
+ "select",
76
+ "set",
77
+ "attach",
78
+ "scroll",
79
+ "back"
80
+ ];
81
+ /**
82
+ * `back` 的目标占位(0.16.0 分册 13 §4.1)。
83
+ *
84
+ * **模型不能表达它**:工具源在 `action === 'back'` 时拒绝任何 `ref` 入参。
85
+ * 它存在只为了让 `back` 不必改 `PageActionRequest.ref` 的类型——
86
+ * 改它会波及所有实现过 executor 的宓主。
87
+ * @experimental
88
+ */
89
+ const PAGE_DOCUMENT_REF = "@document";
90
+
91
+ //#endregion
92
+ //#region ../agent/src/document/budget.ts
93
+ /** @experimental */
94
+ const DOCUMENT_READ_BUDGET = {
95
+ textChars: 96e3,
96
+ visionImages: 8
97
+ };
98
+
99
+ //#endregion
100
+ //#region ../agent/src/document/format.ts
101
+ const PDF_MAGIC = [
102
+ 37,
103
+ 80,
104
+ 68,
105
+ 70,
106
+ 45
107
+ ];
108
+ const ZIP_MAGIC = [
109
+ 80,
110
+ 75,
111
+ 3,
112
+ 4
113
+ ];
114
+ function startsWith(bytes, magic) {
115
+ return magic.every((b, i) => bytes[i] === b);
116
+ }
117
+ /** zip 的本地文件头把条目名以明文 ASCII 存着,扫一遍即可,不必解压 */
118
+ function containsEntry(bytes, name) {
119
+ const needle = new TextEncoder().encode(name);
120
+ const find = (from, to) => {
121
+ outer: for (let i = from; i + needle.length <= to; i += 1) {
122
+ for (let j = 0; j < needle.length; j += 1) if (bytes[i + j] !== needle[j]) continue outer;
123
+ return true;
124
+ }
125
+ return false;
126
+ };
127
+ const WINDOW = 64 * 1024;
128
+ if (find(0, Math.min(bytes.length, WINDOW))) return true;
129
+ return find(Math.max(0, bytes.length - WINDOW * 2), bytes.length);
130
+ }
131
+ /**
132
+ * 格式以字节为准,文件名只作次要线索:改扩展名不该改变我们如何解析它。
133
+ * 认不出返回 `undefined`,由调用方归因。
134
+ * @experimental
135
+ */
136
+ function sniffDocumentFormat(bytes, name) {
137
+ if (startsWith(bytes, PDF_MAGIC)) return "pdf";
138
+ if (!startsWith(bytes, ZIP_MAGIC)) return void 0;
139
+ const lower = (name ?? "").toLowerCase();
140
+ if (lower.endsWith(".docx")) return "docx";
141
+ if (lower.endsWith(".xlsx")) return "xlsx";
142
+ if (containsEntry(bytes, "word/document.xml")) return "docx";
143
+ if (containsEntry(bytes, "xl/workbook.xml")) return "xlsx";
144
+ }
145
+
146
+ //#endregion
147
+ //#region ../agent/src/document/pdfQuality.ts
148
+ /**
149
+ * 「这一页抽出来的是不是乱码」的判定(0.21.0 分册 22 · FR-22.2)。
150
+ *
151
+ * 判定必须是纯函数:它决定一页走文本还是走渲染,而渲染要烧一次多模态请求。
152
+ * 靠人眼看一遍不叫判据。
153
+ */
154
+ /** 坏字符占可打印字符的比例超过它即判文本层不可信 @experimental */
155
+ const PDF_GARBLED_RATIO = .2;
156
+ /** 短于它的页不参与比例判定:三五个字符里有一个私用区字符说明不了任何事 */
157
+ const MIN_SAMPLE = 16;
158
+ /**
159
+ * 坏字符有三类,来源各不相同:
160
+ * - 私用区 U+E000–U+F8FF:字体子集没有 `ToUnicode` 时,pdfjs 把 CID 原样落在这里
161
+ * - U+FFFD:解码失败的替换字符
162
+ * - C0/C1 控制字符:把二进制当文本读出来的残留
163
+ */
164
+ function isGarbledChar(code) {
165
+ if (code >= 57344 && code <= 63743) return true;
166
+ if (code === 65533) return true;
167
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13) return true;
168
+ return code >= 127 && code <= 159;
169
+ }
170
+ /**
171
+ * 空白页一律判不可信:它要么是扫描页、要么什么都没有,
172
+ * 两种情况都该让模型自己看一眼,而不是把一页空气交上去。
173
+ * @experimental
174
+ */
175
+ function measurePdfTextQuality(text) {
176
+ let printable = 0;
177
+ let garbled = 0;
178
+ for (const ch of text) {
179
+ const code = ch.codePointAt(0) ?? 0;
180
+ if (/\s/u.test(ch)) continue;
181
+ printable += 1;
182
+ if (isGarbledChar(code)) garbled += 1;
183
+ }
184
+ if (printable === 0) return {
185
+ printable,
186
+ garbled,
187
+ trustworthy: false
188
+ };
189
+ if (printable < MIN_SAMPLE) return {
190
+ printable,
191
+ garbled,
192
+ trustworthy: garbled === 0
193
+ };
194
+ return {
195
+ printable,
196
+ garbled,
197
+ trustworthy: garbled / printable <= PDF_GARBLED_RATIO
198
+ };
199
+ }
200
+ /** `measurePdfTextQuality(text).trustworthy` 的简写 @experimental */
201
+ function isPdfTextTrustworthy(text) {
202
+ return measurePdfTextQuality(text).trustworthy;
203
+ }
204
+
205
+ //#endregion
206
+ //#region ../agent/src/document/deferral.ts
207
+ /** @experimental */
208
+ function takesImage(mode, role) {
209
+ if (mode === "vision") return true;
210
+ if (mode === "images") return role === "illustration";
211
+ return role === "content";
212
+ }
213
+ /** 本次没取、但回头取得到的图(进返回值的 `deferred`) @experimental */
214
+ function defersImage(mode, role) {
215
+ return mode === "auto" && role === "illustration";
216
+ }
217
+ /** 入参里的 `mode` 只认这三个字面量,其余一律当 `auto` @experimental */
218
+ function asReadMode(raw) {
219
+ return raw === "vision" || raw === "images" ? raw : "auto";
220
+ }
221
+
222
+ //#endregion
223
+ //#region ../agent/src/document/toolSource.ts
224
+ /** @experimental */
225
+ const READ_DOCUMENT_TOOL = "read_document";
226
+ /** 大图的门槛:低于它的是页眉 logo、印章、项目符号,投它们等于白烧一次多模态请求 @experimental */
227
+ const PDF_LARGE_IMAGE_RATIO = .15;
228
+ const DESCRIPTION = "Read a PDF, Word (.docx) or Excel (.xlsx) document the user attached or downloaded, one batch of units at a time. Pages that have no usable text layer (scans) are rendered as images, so their content is never lost. Pictures on pages whose text is readable are listed under \"deferred\" instead of being returned, because reading them costs an extra round trip; call again with mode=\"images\" to fetch those.";
229
+ const INPUT_SCHEMA = {
230
+ type: "object",
231
+ properties: {
232
+ id: {
233
+ type: "string",
234
+ description: "The attachment id, or the id from list_downloaded_files."
235
+ },
236
+ from: {
237
+ type: "object",
238
+ description: "Where to continue. Omit to start at the beginning; otherwise pass the \"next\" of the previous call.",
239
+ properties: {
240
+ page: {
241
+ type: "number",
242
+ description: "PDF only: 0-based page index."
243
+ },
244
+ block: {
245
+ type: "number",
246
+ description: "Word only: 0-based block index."
247
+ },
248
+ sheet: {
249
+ type: "number",
250
+ description: "Excel only: 0-based sheet index."
251
+ },
252
+ row: {
253
+ type: "number",
254
+ description: "Excel only: 0-based row index within the sheet."
255
+ }
256
+ },
257
+ additionalProperties: false
258
+ },
259
+ mode: {
260
+ type: "string",
261
+ enum: [
262
+ "auto",
263
+ "images",
264
+ "vision"
265
+ ],
266
+ description: "\"auto\" (default) returns text plus the pictures that cannot be replaced by text, and lists the rest under \"deferred\". \"images\" returns only those deferred pictures and no text. \"vision\" (PDF only) renders every page as an image and ignores the text layer."
267
+ }
268
+ },
269
+ required: ["id"],
270
+ additionalProperties: false
271
+ };
272
+ const SYSTEM_PROMPT = "read_document reads an attached or downloaded PDF/Word/Excel document in batches. It returns a \"next\" cursor whenever the document is not finished: keep calling read_document with that cursor until the result has no \"next\", otherwise you have only seen part of the document. Read the text first and answer from it; a result that carries \"deferred\" means those units hold pictures that were not read yet. Fetch them with mode=\"images\" only when the answer depends on what they show, and tell the user the pictures have not been looked at when you answer without them. Never guess an id, and never claim you read a document you only read part of.";
273
+ function toolError(e) {
274
+ return {
275
+ ok: false,
276
+ content: [],
277
+ error: {
278
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
279
+ message: messageOf(e)
280
+ }
281
+ };
282
+ }
283
+ /** 包一层:直写 `signal?.aborted` 会被 TS 按第一次检查永久收窄成 false */
284
+ function aborted(signal) {
285
+ return signal !== void 0 && signal.aborted;
286
+ }
287
+ /**
288
+ * 逐单元读文档,读不出字的地方出图(0.21.0 分册 22)。
289
+ *
290
+ * 三个端口一个都没接时 `listToolSpecs()` 返回空——模型连这个工具的存在都看不到,
291
+ * 而不是看得到再被拒(AC-22.2)。
292
+ * @experimental
293
+ */
294
+ function createDocumentToolSource(options) {
295
+ const { files } = options;
296
+ const available = () => options.pdf !== void 0 || options.docx !== void 0 || options.xlsx !== void 0;
297
+ const budgetOf = () => options.budget?.() ?? DOCUMENT_READ_BUDGET;
298
+ return {
299
+ kind: "document-reader",
300
+ listToolSpecs: () => Promise.resolve(available() ? [{
301
+ name: READ_DOCUMENT_TOOL,
302
+ description: DESCRIPTION,
303
+ inputSchema: INPUT_SCHEMA
304
+ }] : []),
305
+ systemPrompt: () => Promise.resolve(available() ? SYSTEM_PROMPT : void 0),
306
+ canHandle: (name) => name === READ_DOCUMENT_TOOL,
307
+ argCaptureTrust: (name) => name === "read_document" ? { tier: "reviewed" } : void 0,
308
+ call: async (name, args, context) => {
309
+ try {
310
+ if (name !== "read_document") throw new WebSkillError("TOOL_NOT_FOUND", `Unknown tool ${name}.`);
311
+ return await read(args, context?.signal);
312
+ } catch (e) {
313
+ return toolError(e);
314
+ }
315
+ }
316
+ };
317
+ async function read(args, signal) {
318
+ const id = args["id"];
319
+ if (typeof id !== "string" || id === "") throw new WebSkillError("TOOL_UNSUPPORTED", "read_document requires the \"id\" of an attachment or download.");
320
+ const file = await files.read(id);
321
+ if (file === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", `No readable document with id "${id}". It is either not an attachment of the current message, or a download id that has since expired — list the downloads again to get a fresh one.`);
322
+ const format = sniffDocumentFormat(file.bytes, file.name);
323
+ if (format === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", `"${file.name}" is not a PDF, Word (.docx) or Excel (.xlsx) document, so read_document cannot read it.`);
324
+ const reader = readerFor(format);
325
+ if (reader === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", `This environment cannot read ${format.toUpperCase()} documents.`);
326
+ const from = asCursor(args["from"]);
327
+ const mode = asReadMode(args["mode"]);
328
+ const budget = budgetOf();
329
+ const batch = format === "pdf" ? await readPdf(reader, file.bytes, from, mode, budget, signal) : format === "docx" ? await readDocx(reader, file.bytes, from, mode, budget, signal) : await readXlsx(reader, file.bytes, from, mode, budget, signal);
330
+ return {
331
+ ok: true,
332
+ content: describe(file.name, format, batch, aborted(signal))
333
+ };
334
+ }
335
+ function readerFor(format) {
336
+ if (format === "pdf") return options.pdf;
337
+ if (format === "docx") return options.docx;
338
+ return options.xlsx;
339
+ }
340
+ }
341
+ function asCursor(raw) {
342
+ if (typeof raw !== "object" || raw === null) return {};
343
+ const out = {};
344
+ for (const key of [
345
+ "page",
346
+ "block",
347
+ "sheet",
348
+ "row"
349
+ ]) {
350
+ const value = raw[key];
351
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) out[key] = value;
352
+ }
353
+ return out;
354
+ }
355
+ /**
356
+ * 预算是「加上这一个还超不超」,不是「已经超了没有」:
357
+ * 后者会把一个超大单元整个吞下去再喊停,等于预算形同虚设。
358
+ * 但**至少读一个单元**,否则一个超预算的单元会让续读原地打转。
359
+ */
360
+ function wouldExceed(budget, taken, chars, images) {
361
+ if (taken === 0) return false;
362
+ return chars > budget.textChars || images > budget.visionImages;
363
+ }
364
+ async function readPdf(reader, bytes, from, mode, budget, signal) {
365
+ const handle = await reader.open(bytes);
366
+ try {
367
+ const start = from["page"] ?? 0;
368
+ const units = [];
369
+ const deferred = [];
370
+ let chars = 0;
371
+ let images = 0;
372
+ let page = start;
373
+ for (; page < handle.pageCount; page += 1) {
374
+ if (aborted(signal)) break;
375
+ const text = mode === "vision" ? "" : await handle.text(page);
376
+ const trustworthy = mode !== "vision" && isPdfTextTrustworthy(text);
377
+ const role = trustworthy ? "illustration" : "content";
378
+ const hasImage = role === "content" || await hasLargeImage(handle, page);
379
+ const needsImage = hasImage && takesImage(mode, role);
380
+ if (mode === "images" && !needsImage) continue;
381
+ const withText = trustworthy && mode !== "images";
382
+ const nextChars = chars + (withText ? text.length : 0);
383
+ const nextImages = images + (needsImage ? 1 : 0);
384
+ if (wouldExceed(budget, units.length, nextChars, nextImages)) break;
385
+ if (aborted(signal)) break;
386
+ const rendered = needsImage ? await handle.render(page) : void 0;
387
+ if (hasImage && defersImage(mode, role)) deferred.push({ page });
388
+ units.push({
389
+ unit: { page },
390
+ ...withText ? { text } : {},
391
+ ...rendered ? { image: rendered } : {}
392
+ });
393
+ chars = nextChars;
394
+ images = nextImages;
395
+ }
396
+ return {
397
+ units,
398
+ total: handle.pageCount,
399
+ done: page,
400
+ skippedImages: 0,
401
+ deferred,
402
+ ...page < handle.pageCount ? { next: { page } } : {}
403
+ };
404
+ } finally {
405
+ await handle.close();
406
+ }
407
+ }
408
+ async function hasLargeImage(handle, page) {
409
+ return (await handle.images(page)).some((image) => image.areaRatio >= PDF_LARGE_IMAGE_RATIO);
410
+ }
411
+ async function readDocx(reader, bytes, from, mode, budget, signal) {
412
+ const { blocks } = await reader.read(bytes);
413
+ const taken = takeBlocks(blocks, from["block"] ?? 0, mode, budget, signal, (index) => ({ block: index }));
414
+ return {
415
+ units: taken.units,
416
+ total: blocks.length,
417
+ done: taken.stopped,
418
+ skippedImages: taken.skippedImages,
419
+ deferred: taken.deferred,
420
+ ...taken.stopped < blocks.length ? { next: { block: taken.stopped } } : {}
421
+ };
422
+ }
423
+ async function readXlsx(reader, bytes, from, mode, budget, signal) {
424
+ const { sheets } = await reader.read(bytes);
425
+ const flat = [];
426
+ sheets.forEach((sheet, sheetIndex) => {
427
+ sheet.rows.forEach((block, rowIndex) => {
428
+ flat.push({
429
+ sheet: sheetIndex,
430
+ sheetName: sheet.name,
431
+ row: rowIndex,
432
+ block
433
+ });
434
+ });
435
+ });
436
+ const startSheet = from["sheet"] ?? 0;
437
+ const startRow = from["row"] ?? 0;
438
+ const found = flat.findIndex((entry) => entry.sheet > startSheet || entry.sheet === startSheet && entry.row >= startRow);
439
+ const start = found === -1 ? flat.length : found;
440
+ const taken = takeBlocks(flat.map((entry) => entry.block), start, mode, budget, signal, (index) => ({
441
+ sheet: flat[index].sheet,
442
+ sheetName: flat[index].sheetName,
443
+ row: flat[index].row
444
+ }));
445
+ const stop = flat[taken.stopped];
446
+ return {
447
+ units: taken.units,
448
+ total: flat.length,
449
+ done: taken.stopped,
450
+ skippedImages: taken.skippedImages,
451
+ deferred: taken.deferred,
452
+ ...stop ? { next: {
453
+ sheet: stop.sheet,
454
+ row: stop.row
455
+ } } : {}
456
+ };
457
+ }
458
+ function takeBlocks(blocks, start, mode, budget, signal, locate) {
459
+ const units = [];
460
+ const deferred = [];
461
+ let chars = 0;
462
+ let images = 0;
463
+ let skippedImages = 0;
464
+ let index = start;
465
+ for (; index < blocks.length; index += 1) {
466
+ if (aborted(signal)) break;
467
+ const block = blocks[index];
468
+ if (block.kind === "skipped") {
469
+ skippedImages += 1;
470
+ continue;
471
+ }
472
+ if (!(block.kind === "text" ? mode !== "images" : takesImage(mode, "illustration"))) {
473
+ if (block.kind === "image" && defersImage(mode, "illustration")) deferred.push(locate(index));
474
+ continue;
475
+ }
476
+ const nextChars = chars + (block.kind === "text" ? block.text.length : 0);
477
+ const nextImages = images + (block.kind === "image" ? 1 : 0);
478
+ if (wouldExceed(budget, units.length, nextChars, nextImages)) break;
479
+ units.push({
480
+ unit: locate(index),
481
+ ...block.kind === "text" ? { text: block.text } : {},
482
+ ...block.kind === "image" ? { image: {
483
+ mimeType: block.mimeType,
484
+ data: block.data
485
+ } } : {}
486
+ });
487
+ chars = nextChars;
488
+ images = nextImages;
489
+ }
490
+ return {
491
+ units,
492
+ stopped: index,
493
+ skippedImages,
494
+ deferred
495
+ };
496
+ }
497
+ /**
498
+ * 图只能走 user 消息,出处只能随同一结果里的 json 分片走——
499
+ * 与下载工具源同款(引擎把图转成 carried 时会丢掉 id 与文件名)。
500
+ */
501
+ function describe(name, format, batch, cancelled) {
502
+ const images = [];
503
+ const units = batch.units.map((entry) => {
504
+ const located = {
505
+ ...entry.unit,
506
+ ...entry.text === void 0 ? {} : { text: entry.text }
507
+ };
508
+ if (entry.image === void 0) return located;
509
+ const imageId = imageIdOf(entry.unit);
510
+ images.push({
511
+ type: "image",
512
+ id: imageId,
513
+ mimeType: entry.image.mimeType,
514
+ data: entry.image.data
515
+ });
516
+ return {
517
+ ...located,
518
+ imageId
519
+ };
520
+ });
521
+ const withText = batch.units.filter((entry) => entry.text !== void 0).length;
522
+ const withImage = images.length;
523
+ const remaining = Math.max(0, batch.total - batch.done);
524
+ const json = {
525
+ file: name,
526
+ format,
527
+ source: withImage === 0 ? "text" : withText === 0 ? "vision" : "mixed",
528
+ total: batch.total,
529
+ units,
530
+ remaining,
531
+ note: continuationNote(format, batch, remaining, cancelled)
532
+ };
533
+ if (batch.next !== void 0) {
534
+ json["next"] = batch.next;
535
+ json["truncated"] = true;
536
+ }
537
+ if (batch.skippedImages > 0) json["skipped"] = {
538
+ images: batch.skippedImages,
539
+ reason: "EMF/WMF images cannot be decoded in a browser and were left out."
540
+ };
541
+ if (batch.deferred.length > 0) json["deferred"] = {
542
+ images: batch.deferred.length,
543
+ units: batch.deferred,
544
+ note: `${batch.deferred.length} unit(s) above hold pictures that were not read, because their text is readable on its own. Answer from the text when you can. If the answer depends on what a picture shows, call read_document again with mode="images" and from = ${JSON.stringify(batch.deferred[0])}, and say so if you answer without looking at them.`
545
+ };
546
+ return [{
547
+ type: "json",
548
+ data: json
549
+ }, ...images];
550
+ }
551
+ function continuationNote(format, batch, remaining, cancelled) {
552
+ const noun = format === "pdf" ? "page" : format === "docx" ? "block" : "row";
553
+ if (batch.next === void 0) return `Reached the end of the document; all ${batch.total} ${noun}s have been read.`;
554
+ const cursor = JSON.stringify(batch.next);
555
+ return `${cancelled ? "Stopped because the user cancelled the run" : `${remaining} ${noun}(s) still unread`}. Call read_document again with from = ${cursor} to continue.`;
556
+ }
557
+ /** 由单元坐标推出,因此与 `units[].imageId` 天然对得上,不依赖批次内的序号 */
558
+ function imageIdOf(unit) {
559
+ if (typeof unit["page"] === "number") return `doc-p${unit["page"]}`;
560
+ if (typeof unit["block"] === "number") return `doc-b${unit["block"]}`;
561
+ return `doc-s${unit["sheet"]}r${unit["row"]}`;
562
+ }
563
+
564
+ //#endregion
565
+ export { frameSteps as _, defersImage as a, isPdfTextTrustworthy as c, DOCUMENT_READ_BUDGET as d, PAGE_ACTION_KINDS as f, frameLabel as g, toFrameScopes as h, asReadMode as i, measurePdfTextQuality as l, toActionFrameScopes as m, READ_DOCUMENT_TOOL as n, takesImage as o, PAGE_DOCUMENT_REF as p, createDocumentToolSource as r, PDF_GARBLED_RATIO as s, PDF_LARGE_IMAGE_RATIO as t, sniffDocumentFormat as u };
@@ -28,6 +28,12 @@ type WebSkillErrorCode = 'FS_NOT_FOUND' | 'FS_PATH_OUTSIDE_ROOT' | 'SKILL_NOT_FO
28
28
  'UPLOAD_FILE_NOT_FOUND' |
29
29
  /** 上传文件超预算。与 DATA_SOURCE_TOO_LARGE 同口径:拒绝而不截断 */
30
30
  'UPLOAD_FILE_TOO_LARGE' |
31
+ /** 页面上没有可读的 WebOffice 实例,或指定的 handle 已失效(0.21.0 分册 11) */
32
+ 'WEBOFFICE_UNAVAILABLE' |
33
+ /** 认出来的不是我们支持的 WebOffice SDK 版本;不降级去猜(0.21.0 FR-11.6b) */
34
+ 'WEBOFFICE_UNSUPPORTED_VERSION' |
35
+ /** `api.ready` 描述符里没有这个方法。与「调用失败」分开:用户的下一步不同 */
36
+ 'WEBOFFICE_CAPABILITY_ABSENT' | 'WEBOFFICE_CALL_FAILED' | 'WEBOFFICE_CALL_TIMEOUT' |
31
37
  /** 用户拒绝了这次工具调用(0.14.0 分册 20) */
32
38
  'TOOL_DENIED';
33
39
  /**
@@ -189,6 +195,20 @@ interface FileStat {
189
195
  size?: number;
190
196
  mtimeMs?: number;
191
197
  }
198
+ /**
199
+ * 分块写入的句柄(0.21.0 分册 18 · FR-18.2)。
200
+ *
201
+ * 语义与 `writeBinary` 对齐:`close()` 之前这份内容不得被当成完整文件读到;
202
+ * 中途失败调 `abort()`,实现必须保证不留下会被误当成完整文件的残件(FR-18.2e)。
203
+ */
204
+ interface FileWriteStream {
205
+ /** 追加一块。`close()`/`abort()` 之后再调即为错误。 */
206
+ write(chunk: Uint8Array): Promise<void>;
207
+ /** 提交。此后文件才对 `readBinary` / `exists` 可见为完整内容。 */
208
+ close(): Promise<void>;
209
+ /** 放弃。已写入的部分必须被丢弃。 */
210
+ abort(): Promise<void>;
211
+ }
192
212
  /**
193
213
  * 文件系统抽象:core 只依赖此接口,Node/浏览器各自提供实现。
194
214
  * 路径一律使用 `/` 分隔的 POSIX 风格。
@@ -201,6 +221,14 @@ interface FileSystemProvider {
201
221
  appendText(path: string, content: string): Promise<void>;
202
222
  readBinary(path: string): Promise<Uint8Array>;
203
223
  writeBinary(path: string, content: Uint8Array): Promise<void>;
224
+ /**
225
+ * 可选:分块写入,避免整份文件进内存(0.21.0 分册 18 · FR-18.2)。
226
+ *
227
+ * **必须是可选的**:`FileSystemProvider` 有宿主自己的实现,加必选成员等于让它们
228
+ * 一夜之间不满足接口。缺省时调用方回落到 `writeBinary`——功能不降级,
229
+ * 只是内存峰值回到全量读取的水平(FR-18.2b)。
230
+ */
231
+ createWriteStream?(path: string): Promise<FileWriteStream>;
204
232
  exists(path: string): Promise<boolean>;
205
233
  stat(path: string): Promise<FileStat>;
206
234
  list(path: string): Promise<FileStat[]>;
@@ -345,6 +373,11 @@ declare class MemoryFS implements FileSystemProvider {
345
373
  appendText(path: string, content: string): Promise<void>;
346
374
  readBinary(path: string): Promise<Uint8Array>;
347
375
  writeBinary(path: string, content: Uint8Array): Promise<void>;
376
+ /**
377
+ * 分块写入(FR-18.2c)。内存实现只能先攒着,`close()` 时才合并落表——
378
+ * 这样中途 `abort()` 或抛错都不会在表里留下半份内容(FR-18.2e)。
379
+ */
380
+ createWriteStream(path: string): Promise<FileWriteStream>;
348
381
  exists(path: string): Promise<boolean>;
349
382
  stat(path: string): Promise<FileStat>;
350
383
  list(path: string): Promise<FileStat[]>;
@@ -469,6 +502,12 @@ interface AttachmentTextInput {
469
502
  kind: 'text' | 'document-text';
470
503
  /** 缺省即用户当场上传;`'download'` 时标签里额外标出本机下载目录(0.14.0 FR-20.5) */
471
504
  origin?: 'upload' | 'download';
505
+ /**
506
+ * 正文截断的字符上限,缺省 `ATTACHMENT_TEXT_LIMIT`。
507
+ * 调用方自己有预算、且**按预算拒绝而不是截断**时传自己的值——
508
+ * 不传就会被这里 32 KiB 的兜底静默切掉,长文档看起来完整、实则只剩开头。
509
+ */
510
+ limit?: number;
472
511
  }
473
512
  /**
474
513
  * 文本类附件进模型上下文的最终形态。两条入口共用,差别只有出处那一段(0.14.0 D-20-6)。
@@ -716,7 +755,7 @@ declare function escapeXml(text: string): string;
716
755
  declare function renderAvailableSkillsXml(catalog: SkillCatalog): string;
717
756
  declare const xmlRenderer: CatalogRenderer;
718
757
  //#endregion
719
- //#region ../runtime/dist/types-Btpdd1y-.d.ts
758
+ //#region ../runtime/dist/types-BvTV_05-.d.ts
720
759
  //#region src/llm/streamTypes.d.ts
721
760
  /** 流式 LLM 事件(OpenAI SSE / Vercel fullStream 统一映射) */
722
761
  type LlmStreamEvent = {
@@ -941,7 +980,19 @@ type InteractionRequest = {
941
980
  /** 读取本机下载目录(0.14.0 分册 20) */
942
981
  'readDownloadedFile' |
943
982
  /** 读取本轮上传的文件(0.15.0 分册 17;0.17.0 分册 19 从 `confirm` 改到这里) */
944
- 'readUploadFile';
983
+ 'readUploadFile' |
984
+ /**
985
+ * 读取页面里嵌的 WPS WebOffice 文档(0.21.0 分册 13)。
986
+ * 只有一个字面量,三种动作(列出 / 读取 / 截屏)靠 `details.action` 分——
987
+ * 分成三个能力会让宿主的能力开关变成三个,而用户心里它是一件事。
988
+ */
989
+ 'readWebOfficeDocument' |
990
+ /**
991
+ * 把工具产出的图交给第二个模型端点认(0.21.0 分册 17)。
992
+ * 这是一次**数据出境**:用户选 A 家模型时并没有同意发给 B 家,
993
+ * 所以它必须是一个独立能力位,而不是并进任何一个「读」的授权里。
994
+ */
995
+ 'delegateVision';
945
996
  message: string;
946
997
  /**
947
998
  * 结构化载荷。`message` 是无字典层的英文兜底,持有字典的宿主据本字段重写正文
@@ -1159,4 +1210,4 @@ interface MemoryStore {
1159
1210
  transaction?<T>(scope: string, fn: (inner: MemoryStore) => Promise<T>): Promise<T>;
1160
1211
  }
1161
1212
  //#endregion
1162
- export { SKILLS_LOCKFILE as $, messageOf as $t, extractSkillCandidate as A, ValidationReport as At, DiscoveryResult as B, checkDependencyCycles as Bt, UiSpecActionCapability as C, SkillsLockfile as Ct, UiSpecSnapshot as D, UNTRUSTED_LINE_LIMIT as Dt, UiSpecPatch as E, TrustedKeyStore as Et, CatalogRenderer as F, assertRemoteUrlAllowed as Ft, IMAGE_MIME_TYPES as G, detectSkillArchiveShapeFromFs as Gt, FileStat as H, classifyAttachment as Ht, ChatAttachmentKind as I, assertSafePathSegment as It, MemoryFS as J, formatAttachmentText as Jt, JsonSchema as K, escapeXml as Kt, CryptoKeyLike as L, atomicWriteText as Lt, ATTACHMENT_TEXT_LIMIT as M, WebSkillError as Mt, ArchiveLimits as N, WebSkillErrorCode as Nt, UiSurfaceActionRequest as O, UiSpecNode as Ot, AttachmentTextInput as P, XLSX_MIME as Pt, SIGNATURE_SCHEMA_VERSION as Q, keyIdOf as Qt, DEFAULT_ARCHIVE_LIMITS as R, buildCatalog as Rt, UiBridge as S, SkillSource as St, UiSpecEvent as T, TrustedKey as Tt, FileSystemProvider as U, computeDigest as Ut, FILE_MIME_TYPES as V, checkSkillRules as Vt, FsTrustedKeyStore as W, detectSkillArchiveShape as Wt, PageQuery as X, isValidSkillName as Xt, Page as Y, isAtomicTempPath as Yt, RemoteUrlPolicy as Z, jsonRenderer as Zt, LlmToolSpec as _, xmlRenderer as _n, SkillManifest as _t, InteractionOrigin as a, renderAvailableSkillsXml as an, SignatureAuditSink as at, RenderResultRequest as b, SkillReader as bt, InteractionResponse as c, resolveInsideRoot as cn, SkillArchiveShape as ct, LlmContentPart as d, signaturePayloadBytes as dn, SkillDiscovery as dt, normalizePath as en, SKILL_MANIFEST_FILE as et, LlmMessage as f, stripArchiveRoot as fn, SkillDocument as ft, LlmToolCall as g, verifySkillSignature as gn, SkillManagerPort as gt, LlmTokenUsage as h, verifyManifest as hn, SkillLocation as ht, FormField as i, readSkillSignature as in, SKILL_SIGNATURE_FILE as it, ATOMIC_TMP_SUFFIX_PATTERN as j, VerifyResult as jt, UiSurfaceActionResponse as k, UnsignedPolicy as kt, LlmClient as l, sanitizeUntrustedLine as ln, SkillCatalog as lt, LlmStreamEvent as m, validateSkills as mn, SkillIssue as mt, ArtifactStore as n, parseSkillPackManifest as nn, SKILL_NAME_PATTERN as nt, InteractionPolicy as o, renderCatalogJson as on, SignatureVerdict as ot, LlmResponse as p, unzipWithLimits as pn, SkillInstallSource as pt, MANIFEST_EXCLUDED_FILES as q, exportSkills as qt, ChartSpec as r, readResponseWithLimit as rn, SKILL_PACK_FILE as rt, InteractionRequest as s, resolveArchiveLimits as sn, SkillArchiveDetection as st, Artifact as t, parseSkillMarkdown as tn, SKILL_NAME_MAX_LENGTH as tt, LlmCompleteInput as u, signSkill as un, SkillCatalogEntry as ut, MemoryStore as v, SkillMetadata as vt, UiSpecDrafts as w, TEXT_EXTENSIONS as wt, SkillCandidateMarker as x, SkillSignature as xt, RenderBlock as y, SkillPackManifest as yt, DOCX_MIME as z, buildManifest as zt };
1213
+ export { SIGNATURE_SCHEMA_VERSION as $, keyIdOf as $t, extractSkillCandidate as A, UnsignedPolicy as At, DiscoveryResult as B, buildManifest as Bt, UiSpecActionCapability as C, SkillSource as Ct, UiSpecSnapshot as D, TrustedKeyStore as Dt, UiSpecPatch as E, TrustedKey as Et, CatalogRenderer as F, XLSX_MIME as Ft, FsTrustedKeyStore as G, detectSkillArchiveShape as Gt, FileStat as H, checkSkillRules as Ht, ChatAttachmentKind as I, assertRemoteUrlAllowed as It, MANIFEST_EXCLUDED_FILES as J, exportSkills as Jt, IMAGE_MIME_TYPES as K, detectSkillArchiveShapeFromFs as Kt, CryptoKeyLike as L, assertSafePathSegment as Lt, ATTACHMENT_TEXT_LIMIT as M, VerifyResult as Mt, ArchiveLimits as N, WebSkillError as Nt, UiSurfaceActionRequest as O, UNTRUSTED_LINE_LIMIT as Ot, AttachmentTextInput as P, WebSkillErrorCode as Pt, RemoteUrlPolicy as Q, jsonRenderer as Qt, DEFAULT_ARCHIVE_LIMITS as R, atomicWriteText as Rt, UiBridge as S, SkillSignature as St, UiSpecEvent as T, TEXT_EXTENSIONS as Tt, FileSystemProvider as U, classifyAttachment as Ut, FILE_MIME_TYPES as V, checkDependencyCycles as Vt, FileWriteStream as W, computeDigest as Wt, Page as X, isAtomicTempPath as Xt, MemoryFS as Y, formatAttachmentText as Yt, PageQuery as Z, isValidSkillName as Zt, LlmToolSpec as _, verifySkillSignature as _n, SkillManagerPort as _t, InteractionOrigin as a, readSkillSignature as an, SKILL_SIGNATURE_FILE as at, RenderResultRequest as b, SkillPackManifest as bt, InteractionResponse as c, resolveArchiveLimits as cn, SkillArchiveDetection as ct, LlmContentPart as d, signSkill as dn, SkillCatalogEntry as dt, messageOf as en, SKILLS_LOCKFILE as et, LlmMessage as f, signaturePayloadBytes as fn, SkillDiscovery as ft, LlmToolCall as g, verifyManifest as gn, SkillLocation as gt, LlmTokenUsage as h, validateSkills as hn, SkillIssue as ht, FormField as i, readResponseWithLimit as in, SKILL_PACK_FILE as it, ATOMIC_TMP_SUFFIX_PATTERN as j, ValidationReport as jt, UiSurfaceActionResponse as k, UiSpecNode as kt, LlmClient as l, resolveInsideRoot as ln, SkillArchiveShape as lt, LlmStreamEvent as m, unzipWithLimits as mn, SkillInstallSource as mt, ArtifactStore as n, parseSkillMarkdown as nn, SKILL_NAME_MAX_LENGTH as nt, InteractionPolicy as o, renderAvailableSkillsXml as on, SignatureAuditSink as ot, LlmResponse as p, stripArchiveRoot as pn, SkillDocument as pt, JsonSchema as q, escapeXml as qt, ChartSpec as r, parseSkillPackManifest as rn, SKILL_NAME_PATTERN as rt, InteractionRequest as s, renderCatalogJson as sn, SignatureVerdict as st, Artifact as t, normalizePath as tn, SKILL_MANIFEST_FILE as tt, LlmCompleteInput as u, sanitizeUntrustedLine as un, SkillCatalog as ut, MemoryStore as v, xmlRenderer as vn, SkillManifest as vt, UiSpecDrafts as w, SkillsLockfile as wt, SkillCandidateMarker as x, SkillReader as xt, RenderBlock as y, SkillMetadata as yt, DOCX_MIME as z, buildCatalog as zt };