@webskill/sdk 0.19.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.
- package/dist/agent.d.ts +3 -3
- package/dist/agent.js +277 -61
- package/dist/{approval-Bbh_Apwg.js → approval-DwN2o2QG.js} +433 -21
- package/dist/browser.d.ts +689 -4
- package/dist/browser.js +1637 -33
- package/dist/{eventTypes-CwciaqCU.js → eventTypes-DcTQXSV1.js} +7 -1
- package/dist/governance.d.ts +9 -3
- package/dist/governance.js +1 -1
- package/dist/{index-ad11wflI.d.ts → index-B8Tgmpwh.d.ts} +2 -2
- package/dist/{index-DQ3-6GFu.d.ts → index-CYLt5u4L.d.ts} +252 -3
- package/dist/{index-BDyXe-a5.d.ts → index-DHopiH_m.d.ts} +261 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +47 -41
- package/dist/{kind-DxgS8LM-.js → kind-DaKqLX2F.js} +3 -17
- package/dist/linkedDocument-C0gj1sMq.js +245 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/node.d.ts +8 -3
- package/dist/node.js +79 -4
- package/dist/processSandboxEntry.js +5 -0
- package/dist/sandboxWorkerEntry.js +5 -0
- package/dist/{skillVersionStore-B24Jjjry-v2CCU0Xo.d.ts → skillVersionStore-B24Jjjry-BkOk9wI_.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/toolSource-C9oNuKmr.js +565 -0
- package/dist/{types-Btpdd1y--BcxQ10Fa.d.ts → types-BvTV_05--BW8zDuEk.d.ts} +54 -3
- package/dist/ui-react.d.ts +3 -3
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/untrustedText-BIaRvPZK.js +16 -0
- package/dist/{webSkillApi-CsvA69qk.js → webSkillApi-Cib6G-94.js} +1 -1
- package/package.json +6 -5
- package/dist/linkedDocument-xNXF-z8n.js +0 -94
- package/dist/types-DOJI5YC3.js +0 -90
|
@@ -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-
|
|
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 {
|
|
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 };
|