@webskill/sdk 0.24.0 → 0.26.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 +1105 -67
- package/dist/browser.d.ts +33 -3
- package/dist/browser.js +67 -21
- package/dist/governance.d.ts +1 -1
- package/dist/{index-mKgZU7cG.d.ts → index-0R-7x8Rp.d.ts} +1 -1
- package/dist/{index-B3rMAUWB.d.ts → index-Bb9kBiF0.d.ts} +83 -2
- package/dist/{index-Bq299VnF.d.ts → index-DRkI7M3z.d.ts} +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/ui-react.d.ts +2 -2
- package/dist/ui.d.ts +2 -2
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -135,7 +135,7 @@ const STATUS_ENUM = [
|
|
|
135
135
|
"completed"
|
|
136
136
|
];
|
|
137
137
|
const TODO_TOOL_DESCRIPTION = "Maintain the visible task list for a multi-step request. The rules are in the system prompt.";
|
|
138
|
-
const INPUT_SCHEMA$
|
|
138
|
+
const INPUT_SCHEMA$7 = {
|
|
139
139
|
type: "object",
|
|
140
140
|
properties: {
|
|
141
141
|
action: {
|
|
@@ -188,7 +188,7 @@ const INPUT_SCHEMA$6 = {
|
|
|
188
188
|
required: ["action"],
|
|
189
189
|
additionalProperties: false
|
|
190
190
|
};
|
|
191
|
-
function toolError$
|
|
191
|
+
function toolError$9(code, message) {
|
|
192
192
|
return {
|
|
193
193
|
ok: false,
|
|
194
194
|
content: [],
|
|
@@ -237,15 +237,15 @@ function createTodoToolSource(options = {}) {
|
|
|
237
237
|
try {
|
|
238
238
|
if (action === "create") {
|
|
239
239
|
const items = parseItems(args["items"]);
|
|
240
|
-
if (typeof items === "string") return Promise.resolve(toolError$
|
|
240
|
+
if (typeof items === "string") return Promise.resolve(toolError$9("VALIDATION_FAILED", items));
|
|
241
241
|
const list = store.create(items);
|
|
242
242
|
return Promise.resolve(ok(events, progressText(list.items)));
|
|
243
243
|
}
|
|
244
244
|
if (action === "update") {
|
|
245
245
|
const id = args["id"];
|
|
246
246
|
const status = args["status"];
|
|
247
|
-
if (typeof id !== "string") return Promise.resolve(toolError$
|
|
248
|
-
if (status !== void 0 && typeof status !== "string") return Promise.resolve(toolError$
|
|
247
|
+
if (typeof id !== "string") return Promise.resolve(toolError$9("VALIDATION_FAILED", "The \"id\" argument is required for action \"update\""));
|
|
248
|
+
if (status !== void 0 && typeof status !== "string") return Promise.resolve(toolError$9("VALIDATION_FAILED", "The \"status\" argument must be a string"));
|
|
249
249
|
const title = args["title"];
|
|
250
250
|
const list = store.update(id, {
|
|
251
251
|
...status !== void 0 ? { status } : {},
|
|
@@ -257,10 +257,10 @@ function createTodoToolSource(options = {}) {
|
|
|
257
257
|
store.clear();
|
|
258
258
|
return Promise.resolve(ok(events, progressText([])));
|
|
259
259
|
}
|
|
260
|
-
return Promise.resolve(toolError$
|
|
260
|
+
return Promise.resolve(toolError$9("VALIDATION_FAILED", "The \"action\" argument must be one of create, update, clear"));
|
|
261
261
|
} catch (e) {
|
|
262
262
|
const code = e instanceof Error && "code" in e ? String(e.code) : "VALIDATION_FAILED";
|
|
263
|
-
return Promise.resolve(toolError$
|
|
263
|
+
return Promise.resolve(toolError$9(code, e instanceof Error ? e.message : String(e)));
|
|
264
264
|
} finally {
|
|
265
265
|
unsubscribe();
|
|
266
266
|
}
|
|
@@ -272,7 +272,7 @@ function createTodoToolSource(options = {}) {
|
|
|
272
272
|
listToolSpecs: () => Promise.resolve([{
|
|
273
273
|
name: MANAGE_TODO_TOOL,
|
|
274
274
|
description: TODO_TOOL_DESCRIPTION,
|
|
275
|
-
inputSchema: INPUT_SCHEMA$
|
|
275
|
+
inputSchema: INPUT_SCHEMA$7
|
|
276
276
|
}]),
|
|
277
277
|
canHandle: (name) => name === MANAGE_TODO_TOOL,
|
|
278
278
|
argCaptureTrust: (name) => name === "manage_todo" ? { tier: "reviewed" } : void 0,
|
|
@@ -304,7 +304,7 @@ const SCRIPT_EXTENSIONS = [".ts", ".js"];
|
|
|
304
304
|
function defaultConfirmSkill(draft) {
|
|
305
305
|
return `Save this conversation as the skill "${draft.name}"? ${draft.description}\nIt will be submitted for review, not activated. Please check the preview below for anything that must not be stored.`;
|
|
306
306
|
}
|
|
307
|
-
function fail(message, details) {
|
|
307
|
+
function fail$2(message, details) {
|
|
308
308
|
throw new WebSkillError("SKILL_GENERATION_VALIDATION_FAILED", message, details);
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
@@ -312,21 +312,21 @@ function fail(message, details) {
|
|
|
312
312
|
* 再去打扰用户——不让用户确认一个注定会失败的技能。
|
|
313
313
|
*/
|
|
314
314
|
function validateDraft(draft) {
|
|
315
|
-
if (!isValidSkillName(draft.name)) fail(`"${draft.name}" is not a valid skill name; use lowercase letters, digits and hyphens`);
|
|
316
|
-
if (draft.description.trim() === "") fail("The skill description must not be empty");
|
|
317
|
-
if (typeof draft.content !== "string" || draft.content.trim() === "") fail("The SKILL.md content must not be empty");
|
|
315
|
+
if (!isValidSkillName(draft.name)) fail$2(`"${draft.name}" is not a valid skill name; use lowercase letters, digits and hyphens`);
|
|
316
|
+
if (draft.description.trim() === "") fail$2("The skill description must not be empty");
|
|
317
|
+
if (typeof draft.content !== "string" || draft.content.trim() === "") fail$2("The SKILL.md content must not be empty");
|
|
318
318
|
let metadata;
|
|
319
319
|
try {
|
|
320
320
|
metadata = parseSkillMarkdown(draft.content).metadata;
|
|
321
321
|
} catch (e) {
|
|
322
|
-
fail(`The SKILL.md content is not a valid skill document: ${e instanceof Error ? e.message : String(e)}`);
|
|
322
|
+
fail$2(`The SKILL.md content is not a valid skill document: ${e instanceof Error ? e.message : String(e)}`);
|
|
323
323
|
}
|
|
324
|
-
if (metadata.name !== draft.name) fail(`The SKILL.md frontmatter declares name "${metadata.name}" but the draft name is "${draft.name}"`);
|
|
324
|
+
if (metadata.name !== draft.name) fail$2(`The SKILL.md frontmatter declares name "${metadata.name}" but the draft name is "${draft.name}"`);
|
|
325
325
|
for (const file of draft.files ?? []) {
|
|
326
326
|
const path = file.path;
|
|
327
|
-
if (path.trim() === "") fail("Every attached file needs a path");
|
|
328
|
-
if (path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) fail(`The attached file path "${path}" must be relative and must not escape the skill folder`);
|
|
329
|
-
if (path.startsWith("scripts/") && !SCRIPT_EXTENSIONS.some((ext) => path.endsWith(ext))) fail(`The script "${path}" must end with .ts or .js`);
|
|
327
|
+
if (path.trim() === "") fail$2("Every attached file needs a path");
|
|
328
|
+
if (path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) fail$2(`The attached file path "${path}" must be relative and must not escape the skill folder`);
|
|
329
|
+
if (path.startsWith("scripts/") && !SCRIPT_EXTENSIONS.some((ext) => path.endsWith(ext))) fail$2(`The script "${path}" must end with .ts or .js`);
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
/** 确认面板正文:完整 SKILL.md + 附带文件,供用户逐字检查(FR-9.4 风险缓解) */
|
|
@@ -358,25 +358,25 @@ async function materializeBinaries(draft, options) {
|
|
|
358
358
|
const out = /* @__PURE__ */ new Map();
|
|
359
359
|
if (claimed.length === 0) return out;
|
|
360
360
|
const resolve = options.resolveAttachment;
|
|
361
|
-
if (resolve === void 0) fail("This host cannot put uploaded files into a skill. Remove \"attachmentId\" from the file entries and write their contents instead.");
|
|
361
|
+
if (resolve === void 0) fail$2("This host cannot put uploaded files into a skill. Remove \"attachmentId\" from the file entries and write their contents instead.");
|
|
362
362
|
const maxOne = options.policy?.maxBinaryBytes ?? DEFAULT_MAX_BINARY_BYTES;
|
|
363
363
|
const maxTotal = options.policy?.maxTotalBinaryBytes ?? DEFAULT_MAX_TOTAL_BINARY_BYTES;
|
|
364
364
|
let total = 0;
|
|
365
365
|
for (const file of claimed) {
|
|
366
366
|
const id = file.attachmentId;
|
|
367
367
|
const attachment = await resolve(id);
|
|
368
|
-
if (attachment === void 0) fail(`No file with attachment id "${id}" was uploaded in this conversation, so "${file.path}" cannot be added to the skill.`, {
|
|
368
|
+
if (attachment === void 0) fail$2(`No file with attachment id "${id}" was uploaded in this conversation, so "${file.path}" cannot be added to the skill.`, {
|
|
369
369
|
attachmentId: id,
|
|
370
370
|
path: file.path
|
|
371
371
|
});
|
|
372
372
|
const size = attachment.bytes.byteLength;
|
|
373
|
-
if (size > maxOne) fail(`The attached file "${attachment.name}" is ${size} bytes, above the ${maxOne} byte limit for one file.`, {
|
|
373
|
+
if (size > maxOne) fail$2(`The attached file "${attachment.name}" is ${size} bytes, above the ${maxOne} byte limit for one file.`, {
|
|
374
374
|
path: file.path,
|
|
375
375
|
size,
|
|
376
376
|
limit: maxOne
|
|
377
377
|
});
|
|
378
378
|
total += size;
|
|
379
|
-
if (total > maxTotal) fail(`The attached files add up to ${total} bytes, above the ${maxTotal} byte limit for one skill.`, {
|
|
379
|
+
if (total > maxTotal) fail$2(`The attached files add up to ${total} bytes, above the ${maxTotal} byte limit for one skill.`, {
|
|
380
380
|
path: file.path,
|
|
381
381
|
total,
|
|
382
382
|
limit: maxTotal
|
|
@@ -403,7 +403,7 @@ function assertStepsAreBackedByTrace(draft, records) {
|
|
|
403
403
|
const executed = new Set(records.map((r) => r.tool));
|
|
404
404
|
const unknown = [...new Set(claimed.map((s) => s.tool).filter((tool) => !executed.has(tool)))];
|
|
405
405
|
if (unknown.length === 0) return;
|
|
406
|
-
fail(`The skill claims tools that were never called in this session: ${unknown.join(", ")}. Tools actually called: ${executed.size === 0 ? "(none)" : [...executed].sort().join(", ")}. Rewrite the skill using only the tools above.`, {
|
|
406
|
+
fail$2(`The skill claims tools that were never called in this session: ${unknown.join(", ")}. Tools actually called: ${executed.size === 0 ? "(none)" : [...executed].sort().join(", ")}. Rewrite the skill using only the tools above.`, {
|
|
407
407
|
claimed: unknown,
|
|
408
408
|
executed: [...executed].sort()
|
|
409
409
|
});
|
|
@@ -421,7 +421,7 @@ function assertStepsAreNotTruncated(draft, records) {
|
|
|
421
421
|
const usable = new Set(records.filter((r) => r.truncated !== true).map((r) => r.tool));
|
|
422
422
|
const truncated = [...new Set(claimed.map((s) => s.tool).filter((tool) => !usable.has(tool)))];
|
|
423
423
|
if (truncated.length === 0) return;
|
|
424
|
-
fail(`The arguments of these tool calls were too large to record, so the skill cannot state them: ${truncated.join(", ")}. Rewrite the skill without these steps.`, { truncated });
|
|
424
|
+
fail$2(`The arguments of these tool calls were too large to record, so the skill cannot state them: ${truncated.join(", ")}. Rewrite the skill without these steps.`, { truncated });
|
|
425
425
|
}
|
|
426
426
|
/**
|
|
427
427
|
* 确认卡证据(AC-30.12 / AC-30.13):逐条列出模型**写进技能的**步骤,
|
|
@@ -569,7 +569,7 @@ const SKILL_GENERATION_SYSTEM_PROMPT = [
|
|
|
569
569
|
//#region ../agent/src/skillGeneration/toolSource.ts
|
|
570
570
|
const GENERATE_SKILL_TOOL = "generate_skill";
|
|
571
571
|
const TOOL_DESCRIPTION = "Turn the procedure just demonstrated in this conversation into a skill candidate. The user must confirm it, and a reviewer must approve it before anyone can use it.";
|
|
572
|
-
const INPUT_SCHEMA$
|
|
572
|
+
const INPUT_SCHEMA$6 = {
|
|
573
573
|
type: "object",
|
|
574
574
|
properties: {
|
|
575
575
|
name: {
|
|
@@ -634,7 +634,7 @@ const INPUT_SCHEMA$5 = {
|
|
|
634
634
|
],
|
|
635
635
|
additionalProperties: false
|
|
636
636
|
};
|
|
637
|
-
function toolError$
|
|
637
|
+
function toolError$8(code, message) {
|
|
638
638
|
return {
|
|
639
639
|
ok: false,
|
|
640
640
|
content: [],
|
|
@@ -715,7 +715,7 @@ function createSkillGenerationToolSource(options) {
|
|
|
715
715
|
const generator = new SkillGenerator(options);
|
|
716
716
|
const call = async (_name, args) => {
|
|
717
717
|
const draft = readDraft(args);
|
|
718
|
-
if (typeof draft === "string") return toolError$
|
|
718
|
+
if (typeof draft === "string") return toolError$8("SKILL_GENERATION_VALIDATION_FAILED", draft);
|
|
719
719
|
try {
|
|
720
720
|
const outcome = await generator.generate(draft);
|
|
721
721
|
if (outcome.status === "declined") return {
|
|
@@ -739,7 +739,7 @@ function createSkillGenerationToolSource(options) {
|
|
|
739
739
|
}]
|
|
740
740
|
};
|
|
741
741
|
} catch (e) {
|
|
742
|
-
return toolError$
|
|
742
|
+
return toolError$8(e instanceof Error && "code" in e ? String(e.code) : "SKILL_GENERATION_VALIDATION_FAILED", e instanceof Error ? e.message : String(e));
|
|
743
743
|
}
|
|
744
744
|
};
|
|
745
745
|
return {
|
|
@@ -748,7 +748,7 @@ function createSkillGenerationToolSource(options) {
|
|
|
748
748
|
listToolSpecs: () => Promise.resolve([{
|
|
749
749
|
name: GENERATE_SKILL_TOOL,
|
|
750
750
|
description: TOOL_DESCRIPTION,
|
|
751
|
-
inputSchema: INPUT_SCHEMA$
|
|
751
|
+
inputSchema: INPUT_SCHEMA$6
|
|
752
752
|
}]),
|
|
753
753
|
canHandle: (name) => name === GENERATE_SKILL_TOOL,
|
|
754
754
|
argCaptureTrust: (name) => name === "generate_skill" ? { tier: "reviewed" } : void 0,
|
|
@@ -887,7 +887,7 @@ const DELEGATION_SYSTEM_PROMPT = [
|
|
|
887
887
|
//#region ../agent/src/delegation/toolSource.ts
|
|
888
888
|
const DELEGATE_TASK_TOOL = "delegate_task";
|
|
889
889
|
const DELEGATE_TOOL_DESCRIPTION = "Hand one self-contained sub-task to a sub-agent and wait for its summary. Several delegations may run in parallel, up to a limit the user picks.";
|
|
890
|
-
const INPUT_SCHEMA$
|
|
890
|
+
const INPUT_SCHEMA$5 = {
|
|
891
891
|
type: "object",
|
|
892
892
|
properties: {
|
|
893
893
|
todoId: {
|
|
@@ -912,7 +912,7 @@ const INPUT_SCHEMA$4 = {
|
|
|
912
912
|
required: ["todoId", "task"],
|
|
913
913
|
additionalProperties: false
|
|
914
914
|
};
|
|
915
|
-
function toolError$
|
|
915
|
+
function toolError$7(code, message) {
|
|
916
916
|
return {
|
|
917
917
|
ok: false,
|
|
918
918
|
content: [],
|
|
@@ -947,12 +947,12 @@ function createDelegationToolSource(options) {
|
|
|
947
947
|
const call = async (_name, args) => {
|
|
948
948
|
const todoId = args["todoId"];
|
|
949
949
|
const task = args["task"];
|
|
950
|
-
if (typeof todoId !== "string" || todoId === "") return toolError$
|
|
951
|
-
if (typeof task !== "string" || task.trim() === "") return toolError$
|
|
950
|
+
if (typeof todoId !== "string" || todoId === "") return toolError$7("VALIDATION_FAILED", "The \"todoId\" argument is required and must be a string");
|
|
951
|
+
if (typeof task !== "string" || task.trim() === "") return toolError$7("VALIDATION_FAILED", "The \"task\" argument is required and must be a non-empty string");
|
|
952
952
|
const rawTools = args["allowedTools"];
|
|
953
953
|
let allowedTools;
|
|
954
954
|
if (rawTools !== void 0) {
|
|
955
|
-
if (!Array.isArray(rawTools) || rawTools.some((entry) => typeof entry !== "string")) return toolError$
|
|
955
|
+
if (!Array.isArray(rawTools) || rawTools.some((entry) => typeof entry !== "string")) return toolError$7("VALIDATION_FAILED", "The \"allowedTools\" argument must be an array of tool names");
|
|
956
956
|
allowedTools = rawTools;
|
|
957
957
|
}
|
|
958
958
|
try {
|
|
@@ -984,7 +984,7 @@ function createDelegationToolSource(options) {
|
|
|
984
984
|
}]
|
|
985
985
|
};
|
|
986
986
|
} catch (e) {
|
|
987
|
-
return toolError$
|
|
987
|
+
return toolError$7(e instanceof Error && "code" in e ? String(e.code) : "DELEGATION_FAILED", e instanceof Error ? e.message : String(e));
|
|
988
988
|
}
|
|
989
989
|
};
|
|
990
990
|
return {
|
|
@@ -994,7 +994,7 @@ function createDelegationToolSource(options) {
|
|
|
994
994
|
listToolSpecs: () => Promise.resolve([{
|
|
995
995
|
name: DELEGATE_TASK_TOOL,
|
|
996
996
|
description: DELEGATE_TOOL_DESCRIPTION,
|
|
997
|
-
inputSchema: INPUT_SCHEMA$
|
|
997
|
+
inputSchema: INPUT_SCHEMA$5
|
|
998
998
|
}]),
|
|
999
999
|
canHandle: (name) => name === DELEGATE_TASK_TOOL,
|
|
1000
1000
|
argCaptureTrust: (name) => name === "delegate_task" ? { tier: "reviewed" } : void 0,
|
|
@@ -1397,7 +1397,7 @@ const PERCEPTION_SYSTEM_PROMPT = [
|
|
|
1397
1397
|
//#endregion
|
|
1398
1398
|
//#region ../agent/src/perception/toolSource.ts
|
|
1399
1399
|
const PERCEIVE_PAGE_TOOL = "perceive_page";
|
|
1400
|
-
const DESCRIPTION$
|
|
1400
|
+
const DESCRIPTION$5 = "Read the parts of the current page the host made readable, as a structured accessibility outline. Read-only: it cannot click, type, submit or navigate.";
|
|
1401
1401
|
/**
|
|
1402
1402
|
* 两个参数,且它们都扩不出一寸新范围(AC-10.8 的判据由 0.14.0 分册 22 §3 修订)。
|
|
1403
1403
|
*
|
|
@@ -1410,7 +1410,7 @@ const DESCRIPTION$4 = "Read the parts of the current page the host made readable
|
|
|
1410
1410
|
* 「模型看到的工具契约」随宿主配置漂移,同一段对话里换个设置就变一次形状,
|
|
1411
1411
|
* 而模型的历史消息里还留着旧形状。宁可多一个恒定存在、关闭时明确拒绝的参数。
|
|
1412
1412
|
*/
|
|
1413
|
-
const INPUT_SCHEMA$
|
|
1413
|
+
const INPUT_SCHEMA$4 = {
|
|
1414
1414
|
type: "object",
|
|
1415
1415
|
properties: {
|
|
1416
1416
|
cursor: {
|
|
@@ -1431,7 +1431,7 @@ const SUBTREE_CODES = /* @__PURE__ */ new Set([
|
|
|
1431
1431
|
"PERCEPTION_SUBTREE_OUT_OF_SCOPE",
|
|
1432
1432
|
"PERCEPTION_SUBTREE_UNKNOWN_REF"
|
|
1433
1433
|
]);
|
|
1434
|
-
function toolError$
|
|
1434
|
+
function toolError$6(code, message) {
|
|
1435
1435
|
return {
|
|
1436
1436
|
ok: false,
|
|
1437
1437
|
content: [],
|
|
@@ -1523,8 +1523,8 @@ function createPagePerceptionToolSource(options) {
|
|
|
1523
1523
|
kind: "page-perception",
|
|
1524
1524
|
listToolSpecs: () => Promise.resolve(policy.enabled ? [{
|
|
1525
1525
|
name: PERCEIVE_PAGE_TOOL,
|
|
1526
|
-
description: DESCRIPTION$
|
|
1527
|
-
inputSchema: INPUT_SCHEMA$
|
|
1526
|
+
description: DESCRIPTION$5,
|
|
1527
|
+
inputSchema: INPUT_SCHEMA$4
|
|
1528
1528
|
}] : []),
|
|
1529
1529
|
systemPrompt: () => Promise.resolve(policy.enabled ? PERCEPTION_SYSTEM_PROMPT : void 0),
|
|
1530
1530
|
canHandle: (name) => name === PERCEIVE_PAGE_TOOL,
|
|
@@ -1538,11 +1538,11 @@ function createPagePerceptionToolSource(options) {
|
|
|
1538
1538
|
};
|
|
1539
1539
|
const cursor = args["cursor"];
|
|
1540
1540
|
const ref = args["ref"];
|
|
1541
|
-
if (ref !== void 0 && typeof ref !== "string") return toolError$
|
|
1541
|
+
if (ref !== void 0 && typeof ref !== "string") return toolError$6("PERCEPTION_REF_INVALID", "perceive_page needs \"ref\" to be a string when it is present.");
|
|
1542
1542
|
if (cursor !== void 0) {
|
|
1543
1543
|
const index = typeof cursor === "string" ? pending?.cursors.get(cursor) : void 0;
|
|
1544
|
-
if (!paged || pending === void 0 || index === void 0 || pending.runId !== context?.runId) return toolError$
|
|
1545
|
-
if (ref !== void 0 && ref !== pending.subtreeRef) return toolError$
|
|
1544
|
+
if (!paged || pending === void 0 || index === void 0 || pending.runId !== context?.runId) return toolError$6("PERCEPTION_CURSOR_INVALID", CURSOR_REJECTED);
|
|
1545
|
+
if (ref !== void 0 && ref !== pending.subtreeRef) return toolError$6("PERCEPTION_CURSOR_INVALID", CURSOR_REJECTED);
|
|
1546
1546
|
return resultOf(pending, index);
|
|
1547
1547
|
}
|
|
1548
1548
|
try {
|
|
@@ -1604,8 +1604,8 @@ function createPagePerceptionToolSource(options) {
|
|
|
1604
1604
|
return resultOf(pending, 0);
|
|
1605
1605
|
} catch (e) {
|
|
1606
1606
|
const code = e instanceof WebSkillError ? e.code : void 0;
|
|
1607
|
-
if (code !== void 0 && SUBTREE_CODES.has(code)) return toolError$
|
|
1608
|
-
return toolError$
|
|
1607
|
+
if (code !== void 0 && SUBTREE_CODES.has(code)) return toolError$6(code, messageOf(e));
|
|
1608
|
+
return toolError$6("PERCEPTION_FAILED", `perceive_page failed: ${messageOf(e)}`);
|
|
1609
1609
|
}
|
|
1610
1610
|
}
|
|
1611
1611
|
};
|
|
@@ -2233,13 +2233,13 @@ const PAGE_ACTION_SYSTEM_PROMPT = [
|
|
|
2233
2233
|
//#endregion
|
|
2234
2234
|
//#region ../agent/src/pageAction/toolSource.ts
|
|
2235
2235
|
const PAGE_ACTION_TOOL = "act_on_page";
|
|
2236
|
-
const DESCRIPTION$
|
|
2236
|
+
const DESCRIPTION$4 = "Act on one element of the current page. The target must be a \"ref\" from a fresh perceive_page result; selectors are rejected. Each call asks the user to confirm before anything happens.";
|
|
2237
2237
|
/**
|
|
2238
2238
|
* 入参里**没有** `scope` / `selector` / `include` / `frame` 一类字段(AC-23.2):
|
|
2239
2239
|
* 工具一旦接受它们,「能操作哪里」就有了一条从模型输出流进来的路径。
|
|
2240
2240
|
* 模型能表达的只有「在哪个句柄上做哪个动作」。
|
|
2241
2241
|
*/
|
|
2242
|
-
const INPUT_SCHEMA$
|
|
2242
|
+
const INPUT_SCHEMA$3 = {
|
|
2243
2243
|
type: "object",
|
|
2244
2244
|
properties: {
|
|
2245
2245
|
ref: {
|
|
@@ -2258,7 +2258,7 @@ const INPUT_SCHEMA$2 = {
|
|
|
2258
2258
|
required: ["action"],
|
|
2259
2259
|
additionalProperties: false
|
|
2260
2260
|
};
|
|
2261
|
-
function toolError$
|
|
2261
|
+
function toolError$5(code, message) {
|
|
2262
2262
|
return {
|
|
2263
2263
|
ok: false,
|
|
2264
2264
|
content: [],
|
|
@@ -2281,8 +2281,8 @@ function createPageActionToolSource(options) {
|
|
|
2281
2281
|
kind: "page-action",
|
|
2282
2282
|
listToolSpecs: () => Promise.resolve(policy.enabled ? [{
|
|
2283
2283
|
name: PAGE_ACTION_TOOL,
|
|
2284
|
-
description: DESCRIPTION$
|
|
2285
|
-
inputSchema: INPUT_SCHEMA$
|
|
2284
|
+
description: DESCRIPTION$4,
|
|
2285
|
+
inputSchema: INPUT_SCHEMA$3
|
|
2286
2286
|
}] : []),
|
|
2287
2287
|
systemPrompt: () => Promise.resolve(policy.enabled ? PAGE_ACTION_SYSTEM_PROMPT : void 0),
|
|
2288
2288
|
canHandle: (name) => name === PAGE_ACTION_TOOL,
|
|
@@ -2308,7 +2308,7 @@ function createPageActionToolSource(options) {
|
|
|
2308
2308
|
},
|
|
2309
2309
|
call: async (_name, args, context) => {
|
|
2310
2310
|
const action = args["action"];
|
|
2311
|
-
if (typeof action !== "string" || !PAGE_ACTION_KINDS.includes(action)) return toolError$
|
|
2311
|
+
if (typeof action !== "string" || !PAGE_ACTION_KINDS.includes(action)) return toolError$5("VALIDATION_FAILED", `act_on_page needs one of ${PAGE_ACTION_KINDS.join(" / ")}.`);
|
|
2312
2312
|
const runContext = { ...context?.runId !== void 0 ? { runId: context.runId } : {} };
|
|
2313
2313
|
const run = async (request) => {
|
|
2314
2314
|
try {
|
|
@@ -2325,25 +2325,25 @@ function createPageActionToolSource(options) {
|
|
|
2325
2325
|
} }
|
|
2326
2326
|
};
|
|
2327
2327
|
} catch (e) {
|
|
2328
|
-
return toolError$
|
|
2328
|
+
return toolError$5(e instanceof WebSkillError ? e.code : "PAGE_ACTION_FAILED", messageOf(e));
|
|
2329
2329
|
}
|
|
2330
2330
|
};
|
|
2331
2331
|
if (action === "back") {
|
|
2332
|
-
if ("ref" in args || "value" in args) return toolError$
|
|
2332
|
+
if ("ref" in args || "value" in args) return toolError$5("VALIDATION_FAILED", "act_on_page with \"back\" takes no \"ref\" and no \"value\".");
|
|
2333
2333
|
return await run({
|
|
2334
2334
|
ref: PAGE_DOCUMENT_REF,
|
|
2335
2335
|
action: "back"
|
|
2336
2336
|
});
|
|
2337
2337
|
}
|
|
2338
2338
|
const ref = args["ref"];
|
|
2339
|
-
if (typeof ref !== "string") return toolError$
|
|
2339
|
+
if (typeof ref !== "string") return toolError$5("VALIDATION_FAILED", `act_on_page needs a "ref" string when the action is "${action}".`);
|
|
2340
2340
|
const value = args["value"];
|
|
2341
2341
|
if ([
|
|
2342
2342
|
"fill",
|
|
2343
2343
|
"select",
|
|
2344
2344
|
"set"
|
|
2345
|
-
].includes(action) && typeof value !== "string") return toolError$
|
|
2346
|
-
if (action === "scroll" && !SCROLL_DIRECTIONS.includes(value)) return toolError$
|
|
2345
|
+
].includes(action) && typeof value !== "string") return toolError$5("VALIDATION_FAILED", `act_on_page needs a "value" string when the action is "${action}".`);
|
|
2346
|
+
if (action === "scroll" && !SCROLL_DIRECTIONS.includes(value)) return toolError$5("VALIDATION_FAILED", `act_on_page with "scroll" needs a "value" of ${SCROLL_DIRECTIONS.map((d) => `"${d}"`).join(" / ")}.`);
|
|
2347
2347
|
return await run({
|
|
2348
2348
|
ref,
|
|
2349
2349
|
action,
|
|
@@ -2585,7 +2585,7 @@ const SPECS = [
|
|
|
2585
2585
|
inputSchema: TAB_SCHEMA
|
|
2586
2586
|
}
|
|
2587
2587
|
];
|
|
2588
|
-
function toolError$
|
|
2588
|
+
function toolError$4(code, message) {
|
|
2589
2589
|
return {
|
|
2590
2590
|
ok: false,
|
|
2591
2591
|
content: [],
|
|
@@ -2620,7 +2620,7 @@ function createTabWorksetToolSource(options) {
|
|
|
2620
2620
|
}]
|
|
2621
2621
|
};
|
|
2622
2622
|
const handle = args["tab"];
|
|
2623
|
-
if (typeof handle !== "string" || handle === "") return toolError$
|
|
2623
|
+
if (typeof handle !== "string" || handle === "") return toolError$4("VALIDATION_FAILED", `${name} needs a "tab" handle from list_page_tabs.`);
|
|
2624
2624
|
return {
|
|
2625
2625
|
ok: true,
|
|
2626
2626
|
content: [{
|
|
@@ -2629,7 +2629,7 @@ function createTabWorksetToolSource(options) {
|
|
|
2629
2629
|
}]
|
|
2630
2630
|
};
|
|
2631
2631
|
} catch (e) {
|
|
2632
|
-
return toolError$
|
|
2632
|
+
return toolError$4(e instanceof WebSkillError ? e.code : "PAGE_TAB_FAILED", messageOf(e));
|
|
2633
2633
|
}
|
|
2634
2634
|
}
|
|
2635
2635
|
};
|
|
@@ -2788,6 +2788,18 @@ var DownloadedFilePolicy = class {
|
|
|
2788
2788
|
}
|
|
2789
2789
|
async #materialize(entry, contentType, kind, bytes) {
|
|
2790
2790
|
if (kind === "text" || kind === "document-text") {
|
|
2791
|
+
if (kind === "document-text" && extractedDocumentFormat(contentType, entry.name) === "xlsx") {
|
|
2792
|
+
const routed = await this.#options.sheetRouting?.({
|
|
2793
|
+
entry,
|
|
2794
|
+
bytes
|
|
2795
|
+
});
|
|
2796
|
+
if (routed !== void 0) return {
|
|
2797
|
+
entry,
|
|
2798
|
+
contentType,
|
|
2799
|
+
kind,
|
|
2800
|
+
text: routed
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2791
2803
|
const body = kind === "text" ? new TextDecoder().decode(bytes) : await this.#extract(contentType, entry.name)(bytes);
|
|
2792
2804
|
return {
|
|
2793
2805
|
entry,
|
|
@@ -2958,8 +2970,8 @@ const READ_INPUT_SCHEMA = {
|
|
|
2958
2970
|
required: ["id"],
|
|
2959
2971
|
additionalProperties: false
|
|
2960
2972
|
};
|
|
2961
|
-
const SYSTEM_PROMPT$
|
|
2962
|
-
function toolError$
|
|
2973
|
+
const SYSTEM_PROMPT$4 = "You can list and read files the user downloaded in this browser with list_downloaded_files and read_downloaded_file. They cover the browser download folder and nothing else. A file the user attached to a message is already in this conversation and is reachable by the id given with it, so never go looking for an attachment here. Every call asks the user for permission, so only use them when the task actually needs a local file. Never guess an id: always list first, and never treat the listing as evidence about files you were not shown.";
|
|
2974
|
+
function toolError$3(e) {
|
|
2963
2975
|
return {
|
|
2964
2976
|
ok: false,
|
|
2965
2977
|
content: [],
|
|
@@ -2989,7 +3001,7 @@ function createDownloadedFileToolSource(options) {
|
|
|
2989
3001
|
description: READ_DESCRIPTION,
|
|
2990
3002
|
inputSchema: READ_INPUT_SCHEMA
|
|
2991
3003
|
}] : []),
|
|
2992
|
-
systemPrompt: () => Promise.resolve(policy.enabled ? SYSTEM_PROMPT$
|
|
3004
|
+
systemPrompt: () => Promise.resolve(policy.enabled ? SYSTEM_PROMPT$4 : void 0),
|
|
2993
3005
|
canHandle: (name) => name === "list_downloaded_files" || name === "read_downloaded_file",
|
|
2994
3006
|
argCaptureTrust: (name) => name === "list_downloaded_files" || name === "read_downloaded_file" ? { tier: "reviewed" } : void 0,
|
|
2995
3007
|
call: async (name, args) => {
|
|
@@ -3008,7 +3020,7 @@ function createDownloadedFileToolSource(options) {
|
|
|
3008
3020
|
content: readContent(await policy.read(id))
|
|
3009
3021
|
};
|
|
3010
3022
|
} catch (e) {
|
|
3011
|
-
return toolError$
|
|
3023
|
+
return toolError$3(e);
|
|
3012
3024
|
}
|
|
3013
3025
|
}
|
|
3014
3026
|
};
|
|
@@ -3051,7 +3063,7 @@ function readContent(result) {
|
|
|
3051
3063
|
//#endregion
|
|
3052
3064
|
//#region ../agent/src/spreadsheet/toolSource.ts
|
|
3053
3065
|
const EXPORT_SPREADSHEET_TOOL = "export_spreadsheet";
|
|
3054
|
-
const DESCRIPTION$
|
|
3066
|
+
const DESCRIPTION$3 = "Write tabular data — and, when they belong in the sheet, pictures — to an Excel (.xlsx) file the user can download.";
|
|
3055
3067
|
const CELL_SCHEMA = {
|
|
3056
3068
|
anyOf: [
|
|
3057
3069
|
{ type: "string" },
|
|
@@ -3168,7 +3180,7 @@ function inputSchema(withImages) {
|
|
|
3168
3180
|
function systemPrompt$1(withImages) {
|
|
3169
3181
|
return "Use export_spreadsheet when the user asks for a downloadable Excel file. The tool returns only a summary; the file itself is delivered to the user as a card. Do not re-read the file you just wrote. Cell values are written literally — the tool never turns text into a number or a date, and it writes no styling, formulas, merged cells or charts." + (withImages ? " Pictures do go in: put them in the sheet's \"images\" with a ref and an anchor cell. A picture the user attached to a message is already in this conversation — reference it as \"upload:<id or file name>\" and do not go looking for it on the page or in the download list first. When the user asks for a workbook of pictures, put the pictures in it — do not deliver the images separately and ask the user to insert them." : "");
|
|
3170
3182
|
}
|
|
3171
|
-
function toolError$
|
|
3183
|
+
function toolError$2(e) {
|
|
3172
3184
|
return {
|
|
3173
3185
|
ok: false,
|
|
3174
3186
|
content: [],
|
|
@@ -3221,7 +3233,7 @@ function createSpreadsheetToolSource(options) {
|
|
|
3221
3233
|
kind: "spreadsheet",
|
|
3222
3234
|
listToolSpecs: () => Promise.resolve(writeArtifact ? [{
|
|
3223
3235
|
name: EXPORT_SPREADSHEET_TOOL,
|
|
3224
|
-
description: DESCRIPTION$
|
|
3236
|
+
description: DESCRIPTION$3,
|
|
3225
3237
|
inputSchema: schema
|
|
3226
3238
|
}] : []),
|
|
3227
3239
|
systemPrompt: () => Promise.resolve(writeArtifact ? prompt : void 0),
|
|
@@ -3254,11 +3266,1037 @@ function createSpreadsheetToolSource(options) {
|
|
|
3254
3266
|
}
|
|
3255
3267
|
}]
|
|
3256
3268
|
};
|
|
3269
|
+
} catch (e) {
|
|
3270
|
+
return toolError$2(e);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
//#endregion
|
|
3277
|
+
//#region ../agent/src/table/columns.ts
|
|
3278
|
+
const NUMBER_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
3279
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2})?$/;
|
|
3280
|
+
/** 前导零的整数(工号、手机号、邮编)是标识不是数量:当文本,免得被求和 */
|
|
3281
|
+
const LEADING_ZERO_RE = /^[+-]?0\d/;
|
|
3282
|
+
/** 入参是**已去首尾空白**的文本 */
|
|
3283
|
+
function kindOf(trimmed) {
|
|
3284
|
+
if (trimmed === "") return "empty";
|
|
3285
|
+
if (NUMBER_RE.test(trimmed)) return LEADING_ZERO_RE.test(trimmed) ? "text" : "number";
|
|
3286
|
+
if (DATE_RE.test(trimmed)) return "date";
|
|
3287
|
+
return "text";
|
|
3288
|
+
}
|
|
3289
|
+
/** 0 → A,25 → Z,26 → AA */
|
|
3290
|
+
function columnLetter(index) {
|
|
3291
|
+
let n = index + 1;
|
|
3292
|
+
let out = "";
|
|
3293
|
+
while (n > 0) {
|
|
3294
|
+
const rem = (n - 1) % 26;
|
|
3295
|
+
out = String.fromCharCode(65 + rem) + out;
|
|
3296
|
+
n = Math.floor((n - 1) / 26);
|
|
3297
|
+
}
|
|
3298
|
+
return out;
|
|
3299
|
+
}
|
|
3300
|
+
const blank = (cells) => cells.every((cell) => cell.trim() === "");
|
|
3301
|
+
function fail$1(message) {
|
|
3302
|
+
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3303
|
+
}
|
|
3304
|
+
/** 先按序号(1 基)、再按表名定位;缺省取第一张有内容的表 */
|
|
3305
|
+
function resolveSheet(workbook, ref) {
|
|
3306
|
+
const { sheets } = workbook;
|
|
3307
|
+
if (sheets.length === 0) fail$1("This workbook has no sheets.");
|
|
3308
|
+
const names = () => sheets.map((sheet, i) => `${i + 1}: "${sheet.name}"`).join(", ");
|
|
3309
|
+
if (ref === void 0) {
|
|
3310
|
+
const found = sheets.findIndex((sheet) => sheet.rows.some((row) => !blank(row)));
|
|
3311
|
+
const index = found === -1 ? 0 : found;
|
|
3312
|
+
return {
|
|
3313
|
+
sheet: sheets[index],
|
|
3314
|
+
index
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
if (typeof ref === "number") {
|
|
3318
|
+
const sheet = Number.isInteger(ref) ? sheets[ref - 1] : void 0;
|
|
3319
|
+
if (sheet === void 0) fail$1(`There is no sheet number ${String(ref)}. Sheets: ${names()}.`);
|
|
3320
|
+
return {
|
|
3321
|
+
sheet,
|
|
3322
|
+
index: ref - 1
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
if (typeof ref === "string") {
|
|
3326
|
+
const index = sheets.findIndex((sheet) => sheet.name === ref);
|
|
3327
|
+
if (index === -1) fail$1(`There is no sheet named "${ref}". Sheets: ${names()}.`);
|
|
3328
|
+
return {
|
|
3329
|
+
sheet: sheets[index],
|
|
3330
|
+
index
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
return fail$1("\"sheet\" must be a sheet name or a 1-based sheet number.");
|
|
3334
|
+
}
|
|
3335
|
+
/** 表头行(缺省为第一个有内容的行)+ 其后有内容的行;行号保持工作表的 1 基行号 */
|
|
3336
|
+
function buildView(workbook, sheetRef, headerRowArg) {
|
|
3337
|
+
const { sheet, index } = resolveSheet(workbook, sheetRef);
|
|
3338
|
+
const rows = sheet.rows;
|
|
3339
|
+
let headerIndex;
|
|
3340
|
+
if (headerRowArg === void 0) headerIndex = rows.findIndex((row) => !blank(row));
|
|
3341
|
+
else {
|
|
3342
|
+
if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$1("\"headerRow\" must be a positive integer (1-based worksheet row number).");
|
|
3343
|
+
headerIndex = headerRowArg - 1;
|
|
3344
|
+
if (headerIndex >= rows.length) fail$1(`"headerRow" ${headerRowArg} is past the last row (${rows.length}) of the sheet.`);
|
|
3345
|
+
}
|
|
3346
|
+
if (headerIndex === -1) return {
|
|
3347
|
+
sheetName: sheet.name,
|
|
3348
|
+
sheetIndex: index,
|
|
3349
|
+
headerRow: 0,
|
|
3350
|
+
columns: [],
|
|
3351
|
+
data: []
|
|
3352
|
+
};
|
|
3353
|
+
const header = rows[headerIndex] ?? [];
|
|
3354
|
+
const data = [];
|
|
3355
|
+
let width = header.length;
|
|
3356
|
+
for (let i = headerIndex + 1; i < rows.length; i += 1) {
|
|
3357
|
+
const cells = rows[i];
|
|
3358
|
+
if (blank(cells)) continue;
|
|
3359
|
+
if (cells.length > width) width = cells.length;
|
|
3360
|
+
data.push({
|
|
3361
|
+
row: i + 1,
|
|
3362
|
+
cells
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
const columns = [];
|
|
3366
|
+
for (let c = 0; c < width; c += 1) {
|
|
3367
|
+
const text = (header[c] ?? "").trim();
|
|
3368
|
+
const letter = columnLetter(c);
|
|
3369
|
+
columns.push({
|
|
3370
|
+
index: c,
|
|
3371
|
+
letter,
|
|
3372
|
+
header: text,
|
|
3373
|
+
label: text === "" ? letter : `${letter} ${text}`,
|
|
3374
|
+
name: text === "" ? letter : text
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
return {
|
|
3378
|
+
sheetName: sheet.name,
|
|
3379
|
+
sheetIndex: index,
|
|
3380
|
+
headerRow: headerIndex + 1,
|
|
3381
|
+
columns,
|
|
3382
|
+
data
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
const LETTER_RE = /^[A-Za-z]{1,3}$/;
|
|
3386
|
+
function listColumns(view) {
|
|
3387
|
+
return view.columns.map((column) => column.label).join("; ");
|
|
3388
|
+
}
|
|
3389
|
+
/** 表头文字优先,其次列字母;表头重名时拒绝按表头引用,并点出同名各列的列字母 */
|
|
3390
|
+
function resolveColumn(view, ref, what) {
|
|
3391
|
+
if (typeof ref !== "string" || ref.trim() === "") fail$1(`${what} must name a column (header text or column letter).`);
|
|
3392
|
+
const text = ref.trim();
|
|
3393
|
+
const byHeader = view.columns.filter((column) => column.header === text);
|
|
3394
|
+
if (byHeader.length === 1) return byHeader[0];
|
|
3395
|
+
if (byHeader.length > 1) fail$1(`${what}: the header "${text}" is shared by columns ${byHeader.map((c) => c.letter).join(", ")}. Refer to the column by its letter instead.`);
|
|
3396
|
+
if (LETTER_RE.test(text)) {
|
|
3397
|
+
const byLetter = view.columns.find((column) => column.letter === text.toUpperCase());
|
|
3398
|
+
if (byLetter !== void 0) return byLetter;
|
|
3399
|
+
}
|
|
3400
|
+
return fail$1(`${what}: no column "${text}" on sheet "${view.sheetName}". Columns: ${listColumns(view)}.`);
|
|
3401
|
+
}
|
|
3402
|
+
/** Neumaier 补偿求和:几万个小数相加不累积漂移 */
|
|
3403
|
+
var Summer = class {
|
|
3404
|
+
#sum = 0;
|
|
3405
|
+
#comp = 0;
|
|
3406
|
+
add(x) {
|
|
3407
|
+
const t = this.#sum + x;
|
|
3408
|
+
this.#comp += Math.abs(this.#sum) >= Math.abs(x) ? this.#sum - t + x : x - t + this.#sum;
|
|
3409
|
+
this.#sum = t;
|
|
3410
|
+
}
|
|
3411
|
+
get total() {
|
|
3412
|
+
return this.#sum + this.#comp;
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
/** 12 位有效数字:消掉 `0.30000000000000004` 一类尾巴,又不损失财务级精度 */
|
|
3416
|
+
const tidy = (x) => Number(x.toPrecision(12));
|
|
3417
|
+
|
|
3418
|
+
//#endregion
|
|
3419
|
+
//#region ../agent/src/table/profile.ts
|
|
3420
|
+
const PROFILE_MAX_CHARS = 8e3;
|
|
3421
|
+
const DISTINCT_CAP = 1e4;
|
|
3422
|
+
const SAMPLE_ROWS = 5;
|
|
3423
|
+
function statsOf(view, column) {
|
|
3424
|
+
let filled = 0;
|
|
3425
|
+
let numbers = 0;
|
|
3426
|
+
let dates = 0;
|
|
3427
|
+
let texts = 0;
|
|
3428
|
+
const summer = new Summer();
|
|
3429
|
+
let min;
|
|
3430
|
+
let max;
|
|
3431
|
+
let earliest;
|
|
3432
|
+
let latest;
|
|
3433
|
+
const freq = /* @__PURE__ */ new Map();
|
|
3434
|
+
let capped = false;
|
|
3435
|
+
for (const { cells } of view.data) {
|
|
3436
|
+
const text = (cells[column.index] ?? "").trim();
|
|
3437
|
+
const kind = kindOf(text);
|
|
3438
|
+
if (kind === "empty") continue;
|
|
3439
|
+
filled += 1;
|
|
3440
|
+
if (kind === "number") {
|
|
3441
|
+
numbers += 1;
|
|
3442
|
+
const x = Number(text);
|
|
3443
|
+
summer.add(x);
|
|
3444
|
+
if (min === void 0 || x < min) min = x;
|
|
3445
|
+
if (max === void 0 || x > max) max = x;
|
|
3446
|
+
} else if (kind === "date") {
|
|
3447
|
+
dates += 1;
|
|
3448
|
+
if (earliest === void 0 || text < earliest) earliest = text;
|
|
3449
|
+
if (latest === void 0 || text > latest) latest = text;
|
|
3450
|
+
} else texts += 1;
|
|
3451
|
+
const seen = freq.get(text);
|
|
3452
|
+
if (seen !== void 0) freq.set(text, seen + 1);
|
|
3453
|
+
else if (freq.size < DISTINCT_CAP) freq.set(text, 1);
|
|
3454
|
+
else capped = true;
|
|
3455
|
+
}
|
|
3456
|
+
let type;
|
|
3457
|
+
if (filled === 0) type = "empty";
|
|
3458
|
+
else if (numbers >= filled * .95) type = "number";
|
|
3459
|
+
else if (dates >= filled * .95) type = "date";
|
|
3460
|
+
else if (numbers + dates <= filled * .05) type = "text";
|
|
3461
|
+
else type = "mixed";
|
|
3462
|
+
const top = capped || type !== "text" && type !== "mixed" ? [] : [...freq.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 3);
|
|
3463
|
+
return {
|
|
3464
|
+
filled,
|
|
3465
|
+
numbers,
|
|
3466
|
+
dates,
|
|
3467
|
+
texts,
|
|
3468
|
+
type,
|
|
3469
|
+
distinct: capped ? void 0 : freq.size,
|
|
3470
|
+
sum: summer.total,
|
|
3471
|
+
min,
|
|
3472
|
+
max,
|
|
3473
|
+
earliest,
|
|
3474
|
+
latest,
|
|
3475
|
+
top
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
const clip$2 = (text, max) => {
|
|
3479
|
+
const chars = [...text.replace(/[\r\n\t]+/g, " ")];
|
|
3480
|
+
return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
|
|
3481
|
+
};
|
|
3482
|
+
/** 超出字符预算时的收缩阶梯:先少表、再少列、再收样本、最后去掉 top */
|
|
3483
|
+
const LADDER = [
|
|
3484
|
+
{
|
|
3485
|
+
maxSheets: 8,
|
|
3486
|
+
maxColumns: 40,
|
|
3487
|
+
sampleRows: SAMPLE_ROWS,
|
|
3488
|
+
sampleCell: 40,
|
|
3489
|
+
withTop: true,
|
|
3490
|
+
note: void 0
|
|
3491
|
+
},
|
|
3492
|
+
{
|
|
3493
|
+
maxSheets: 3,
|
|
3494
|
+
maxColumns: 40,
|
|
3495
|
+
sampleRows: SAMPLE_ROWS,
|
|
3496
|
+
sampleCell: 40,
|
|
3497
|
+
withTop: true,
|
|
3498
|
+
note: "sheets"
|
|
3499
|
+
},
|
|
3500
|
+
{
|
|
3501
|
+
maxSheets: 3,
|
|
3502
|
+
maxColumns: 20,
|
|
3503
|
+
sampleRows: SAMPLE_ROWS,
|
|
3504
|
+
sampleCell: 40,
|
|
3505
|
+
withTop: true,
|
|
3506
|
+
note: "sheets, columns"
|
|
3507
|
+
},
|
|
3508
|
+
{
|
|
3509
|
+
maxSheets: 3,
|
|
3510
|
+
maxColumns: 20,
|
|
3511
|
+
sampleRows: 3,
|
|
3512
|
+
sampleCell: 16,
|
|
3513
|
+
withTop: true,
|
|
3514
|
+
note: "sheets, columns, sample"
|
|
3515
|
+
},
|
|
3516
|
+
{
|
|
3517
|
+
maxSheets: 2,
|
|
3518
|
+
maxColumns: 12,
|
|
3519
|
+
sampleRows: 2,
|
|
3520
|
+
sampleCell: 12,
|
|
3521
|
+
withTop: false,
|
|
3522
|
+
note: "sheets, columns, sample, top values"
|
|
3523
|
+
}
|
|
3524
|
+
];
|
|
3525
|
+
function columnLine(column, stats, withTop) {
|
|
3526
|
+
const parts = [
|
|
3527
|
+
`${column.letter} ${clip$2(column.header, 40)}`.trimEnd(),
|
|
3528
|
+
stats.type,
|
|
3529
|
+
`${stats.filled} filled`
|
|
3530
|
+
];
|
|
3531
|
+
parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP} distinct` : `${stats.distinct} distinct`);
|
|
3532
|
+
if (stats.min !== void 0 && stats.max !== void 0) parts.push(`min ${tidy(stats.min)}`, `max ${tidy(stats.max)}`, `sum ${tidy(stats.sum)}`);
|
|
3533
|
+
if (stats.earliest !== void 0 && stats.latest !== void 0) parts.push(`${stats.earliest} … ${stats.latest}`);
|
|
3534
|
+
if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$2(value, 24)} ×${count}`).join(", ")}`);
|
|
3535
|
+
return parts.join(" | ");
|
|
3536
|
+
}
|
|
3537
|
+
function sheetSection(view, rung, expanded, statsFor) {
|
|
3538
|
+
const head = `Sheet ${view.sheetIndex + 1} "${view.sheetName}": ${view.data.length} data rows × ${view.columns.length} columns` + (view.headerRow > 0 ? `, header at row ${view.headerRow}` : ", empty");
|
|
3539
|
+
if (!expanded || view.columns.length === 0) return [head];
|
|
3540
|
+
const lines = [head, "Columns:"];
|
|
3541
|
+
const shown = view.columns.slice(0, rung.maxColumns);
|
|
3542
|
+
for (const column of shown) lines.push(columnLine(column, statsFor(column), rung.withTop));
|
|
3543
|
+
if (view.columns.length > shown.length) lines.push(`+${view.columns.length - shown.length} more columns — call query_table with op "describe" and this sheet to list them`);
|
|
3544
|
+
const sample = view.data.slice(0, rung.sampleRows);
|
|
3545
|
+
if (sample.length > 0) {
|
|
3546
|
+
lines.push(`Sample (sheet rows ${sample[0].row}–${sample[sample.length - 1].row}):`);
|
|
3547
|
+
for (const { cells } of sample) lines.push(shown.map((column) => clip$2((cells[column.index] ?? "").trim(), rung.sampleCell)).join(" "));
|
|
3548
|
+
}
|
|
3549
|
+
return lines;
|
|
3550
|
+
}
|
|
3551
|
+
/**
|
|
3552
|
+
* 表格画像:每张表的行列数、表头位置,每列的类型 / 非空数 / 去重数 / 数值与日期范围 / 高频取值,
|
|
3553
|
+
* 以及前几行样本(0.26.0 分册 10 · FR-10.3)。
|
|
3554
|
+
*
|
|
3555
|
+
* 分流进消息与 `query_table` 的 `describe` 调同一个函数,模型看到的是同一份口径。
|
|
3556
|
+
* 整份不超过 8000 字符;超出时按阶梯收缩,并在末尾写明收缩了什么。
|
|
3557
|
+
* @experimental
|
|
3558
|
+
*/
|
|
3559
|
+
function profileTable(input) {
|
|
3560
|
+
const { workbook } = input;
|
|
3561
|
+
const total = workbook.sheets.length;
|
|
3562
|
+
const only = input.sheet === void 0 ? void 0 : buildView(workbook, input.sheet, input.headerRow).sheetIndex;
|
|
3563
|
+
const header = `--- Spreadsheet profile: ${input.name} (id: ${input.id}) · ${total} sheet${total === 1 ? "" : "s"} ---`;
|
|
3564
|
+
const views = workbook.sheets.map((_, index) => buildView(workbook, index + 1, only === index ? input.headerRow : void 0));
|
|
3565
|
+
const memo = /* @__PURE__ */ new Map();
|
|
3566
|
+
const statsFor = (view) => (column) => {
|
|
3567
|
+
let perView = memo.get(view);
|
|
3568
|
+
if (perView === void 0) memo.set(view, perView = /* @__PURE__ */ new Map());
|
|
3569
|
+
let stats = perView.get(column.index);
|
|
3570
|
+
if (stats === void 0) perView.set(column.index, stats = statsOf(view, column));
|
|
3571
|
+
return stats;
|
|
3572
|
+
};
|
|
3573
|
+
let result = "";
|
|
3574
|
+
for (const rung of LADDER) {
|
|
3575
|
+
const lines = [header];
|
|
3576
|
+
let expandedCount = 0;
|
|
3577
|
+
for (const [index, view] of views.entries()) {
|
|
3578
|
+
const expanded = only === void 0 ? expandedCount < rung.maxSheets : index === only;
|
|
3579
|
+
if (expanded && view.data.length > 0) expandedCount += 1;
|
|
3580
|
+
lines.push(...sheetSection(view, rung, expanded && (only !== void 0 || view.data.length > 0), statsFor(view)));
|
|
3581
|
+
}
|
|
3582
|
+
if (rung.note !== void 0) lines.push(`Profile shortened to fit: ${rung.note}. Call query_table with op "describe" for the rest.`);
|
|
3583
|
+
result = lines.join("\n");
|
|
3584
|
+
if (result.length <= PROFILE_MAX_CHARS) return result;
|
|
3585
|
+
}
|
|
3586
|
+
return `${result.slice(0, PROFILE_MAX_CHARS - 80)}\nProfile cut at ${PROFILE_MAX_CHARS} characters.`;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
//#endregion
|
|
3590
|
+
//#region ../agent/src/table/query.ts
|
|
3591
|
+
const QUERY_LIMITS = {
|
|
3592
|
+
defaultRows: 50,
|
|
3593
|
+
maxRows: 500,
|
|
3594
|
+
maxFilters: 20,
|
|
3595
|
+
maxGroupBy: 3,
|
|
3596
|
+
maxMetrics: 8,
|
|
3597
|
+
maxOrderBy: 3,
|
|
3598
|
+
maxIn: 200,
|
|
3599
|
+
/** 返回体的硬上限,远低于 `loop.toolResultMaxBytes` 的 256 KB(FR-10.12) */
|
|
3600
|
+
maxBytes: 64 * 1024,
|
|
3601
|
+
cellChars: 200
|
|
3602
|
+
};
|
|
3603
|
+
const METRIC_FNS = [
|
|
3604
|
+
"count",
|
|
3605
|
+
"countDistinct",
|
|
3606
|
+
"sum",
|
|
3607
|
+
"avg",
|
|
3608
|
+
"min",
|
|
3609
|
+
"max",
|
|
3610
|
+
"median"
|
|
3611
|
+
];
|
|
3612
|
+
const FILTER_OPS = [
|
|
3613
|
+
"eq",
|
|
3614
|
+
"ne",
|
|
3615
|
+
"gt",
|
|
3616
|
+
"gte",
|
|
3617
|
+
"lt",
|
|
3618
|
+
"lte",
|
|
3619
|
+
"contains",
|
|
3620
|
+
"in",
|
|
3621
|
+
"empty",
|
|
3622
|
+
"notEmpty"
|
|
3623
|
+
];
|
|
3624
|
+
const BUCKETS = [
|
|
3625
|
+
"value",
|
|
3626
|
+
"year",
|
|
3627
|
+
"month",
|
|
3628
|
+
"day"
|
|
3629
|
+
];
|
|
3630
|
+
function fail(message) {
|
|
3631
|
+
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3632
|
+
}
|
|
3633
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3634
|
+
function list(v, what, max) {
|
|
3635
|
+
if (v === void 0) return [];
|
|
3636
|
+
if (!Array.isArray(v)) fail(`"${what}" must be an array.`);
|
|
3637
|
+
if (v.length > max) fail(`"${what}" has ${v.length} entries; at most ${max} are allowed.`);
|
|
3638
|
+
return v;
|
|
3639
|
+
}
|
|
3640
|
+
function oneOf(v, allowed, what) {
|
|
3641
|
+
if (typeof v === "string" && allowed.includes(v)) return v;
|
|
3642
|
+
return fail(`${what} must be one of: ${allowed.join(", ")}.`);
|
|
3643
|
+
}
|
|
3644
|
+
function noOtherKeys(obj, allowed, what) {
|
|
3645
|
+
const extra = Object.keys(obj).filter((key) => !allowed.includes(key));
|
|
3646
|
+
if (extra.length > 0) fail(`${what} has unknown field${extra.length > 1 ? "s" : ""}: ${extra.join(", ")}.`);
|
|
3647
|
+
}
|
|
3648
|
+
function scalar(v, what) {
|
|
3649
|
+
if (typeof v === "string") return v;
|
|
3650
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
3651
|
+
return fail(`${what} must be a string or a finite number.`);
|
|
3652
|
+
}
|
|
3653
|
+
function compileFilter(view, raw, index, uncomparable) {
|
|
3654
|
+
const what = `where[${index}]`;
|
|
3655
|
+
if (!isObject(raw)) fail(`${what} must be an object.`);
|
|
3656
|
+
noOtherKeys(raw, [
|
|
3657
|
+
"column",
|
|
3658
|
+
"op",
|
|
3659
|
+
"value"
|
|
3660
|
+
], what);
|
|
3661
|
+
const column = resolveColumn(view, raw["column"], `${what}.column`);
|
|
3662
|
+
const op = oneOf(raw["op"], FILTER_OPS, `${what}.op`);
|
|
3663
|
+
const at = column.index;
|
|
3664
|
+
const cellOf = (cells) => (cells[at] ?? "").trim();
|
|
3665
|
+
if (op === "empty") return {
|
|
3666
|
+
column,
|
|
3667
|
+
test: (cells) => cellOf(cells) === ""
|
|
3668
|
+
};
|
|
3669
|
+
if (op === "notEmpty") return {
|
|
3670
|
+
column,
|
|
3671
|
+
test: (cells) => cellOf(cells) !== ""
|
|
3672
|
+
};
|
|
3673
|
+
if (op === "in") {
|
|
3674
|
+
const values = raw["value"];
|
|
3675
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > QUERY_LIMITS.maxIn) fail(`${what}.value must be a non-empty array of at most ${QUERY_LIMITS.maxIn} strings or numbers for op "in".`);
|
|
3676
|
+
const texts = /* @__PURE__ */ new Set();
|
|
3677
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
3678
|
+
for (const item of values) {
|
|
3679
|
+
const text = String(scalar(item, `${what}.value[]`)).trim();
|
|
3680
|
+
texts.add(text);
|
|
3681
|
+
if (kindOf(text) === "number") numbers.add(Number(text));
|
|
3682
|
+
}
|
|
3683
|
+
return {
|
|
3684
|
+
column,
|
|
3685
|
+
test: (cells) => {
|
|
3686
|
+
const text = cellOf(cells);
|
|
3687
|
+
return texts.has(text) || numbers.size > 0 && kindOf(text) === "number" && numbers.has(Number(text));
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
if (raw["value"] === void 0) fail(`${what}.value is required for op "${op}".`);
|
|
3692
|
+
const value = String(scalar(raw["value"], `${what}.value`)).trim();
|
|
3693
|
+
const valueKind = kindOf(value);
|
|
3694
|
+
if (op === "contains") {
|
|
3695
|
+
const needle = value.toLowerCase();
|
|
3696
|
+
return {
|
|
3697
|
+
column,
|
|
3698
|
+
test: (cells) => cellOf(cells).toLowerCase().includes(needle)
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
if (op === "eq" || op === "ne") {
|
|
3702
|
+
const numeric = valueKind === "number" ? Number(value) : void 0;
|
|
3703
|
+
const equal = (cells) => {
|
|
3704
|
+
const text = cellOf(cells);
|
|
3705
|
+
if (text === value) return true;
|
|
3706
|
+
return numeric !== void 0 && kindOf(text) === "number" && Number(text) === numeric;
|
|
3707
|
+
};
|
|
3708
|
+
return {
|
|
3709
|
+
column,
|
|
3710
|
+
test: op === "eq" ? equal : (cells) => !equal(cells)
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
if (valueKind !== "number" && valueKind !== "date") fail(`${what}.value must be a number or an ISO date (YYYY-MM-DD) for op "${op}", received "${value}".`);
|
|
3714
|
+
const numericValue = valueKind === "number" ? Number(value) : 0;
|
|
3715
|
+
const satisfied = (order) => op === "gt" ? order > 0 : op === "gte" ? order >= 0 : op === "lt" ? order < 0 : order <= 0;
|
|
3716
|
+
return {
|
|
3717
|
+
column,
|
|
3718
|
+
test: (cells) => {
|
|
3719
|
+
const text = cellOf(cells);
|
|
3720
|
+
if (text === "") return false;
|
|
3721
|
+
const kind = kindOf(text);
|
|
3722
|
+
if (kind === valueKind) {
|
|
3723
|
+
const order = kind === "number" ? Number(text) - numericValue : text < value ? -1 : text > value ? 1 : 0;
|
|
3724
|
+
return satisfied(order);
|
|
3725
|
+
}
|
|
3726
|
+
uncomparable.set(column.label, (uncomparable.get(column.label) ?? 0) + 1);
|
|
3727
|
+
return false;
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
function parseLimit(v) {
|
|
3732
|
+
if (v === void 0) return {
|
|
3733
|
+
limit: QUERY_LIMITS.defaultRows,
|
|
3734
|
+
clamped: false
|
|
3735
|
+
};
|
|
3736
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) fail("\"limit\" must be a positive integer.");
|
|
3737
|
+
return v > QUERY_LIMITS.maxRows ? {
|
|
3738
|
+
limit: QUERY_LIMITS.maxRows,
|
|
3739
|
+
clamped: true
|
|
3740
|
+
} : {
|
|
3741
|
+
limit: v,
|
|
3742
|
+
clamped: false
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
function parseQuery(args, view) {
|
|
3746
|
+
const op = oneOf(args["op"], ["aggregate", "rows"], "\"op\"");
|
|
3747
|
+
const uncomparable = /* @__PURE__ */ new Map();
|
|
3748
|
+
const filters = list(args["where"], "where", QUERY_LIMITS.maxFilters).map((raw, i) => compileFilter(view, raw, i, uncomparable));
|
|
3749
|
+
const { limit, clamped } = parseLimit(args["limit"]);
|
|
3750
|
+
const orderRaw = list(args["orderBy"], "orderBy", QUERY_LIMITS.maxOrderBy).map((raw, i) => {
|
|
3751
|
+
if (!isObject(raw)) fail(`orderBy[${i}] must be an object.`);
|
|
3752
|
+
noOtherKeys(raw, ["key", "desc"], `orderBy[${i}]`);
|
|
3753
|
+
if (typeof raw["key"] !== "string" || raw["key"].trim() === "") fail(`orderBy[${i}].key must be a string.`);
|
|
3754
|
+
if (raw["desc"] !== void 0 && typeof raw["desc"] !== "boolean") fail(`orderBy[${i}].desc must be a boolean.`);
|
|
3755
|
+
return {
|
|
3756
|
+
key: raw["key"].trim(),
|
|
3757
|
+
desc: raw["desc"] === true
|
|
3758
|
+
};
|
|
3759
|
+
});
|
|
3760
|
+
if (op === "rows") {
|
|
3761
|
+
if (args["groupBy"] !== void 0 || args["metrics"] !== void 0) fail("\"groupBy\" and \"metrics\" belong to op \"aggregate\". Use op \"rows\" with \"columns\" to list rows.");
|
|
3762
|
+
const columns = list(args["columns"], "columns", view.columns.length || 1).map((ref, i) => resolveColumn(view, ref, `columns[${i}]`));
|
|
3763
|
+
for (const entry of orderRaw) resolveColumn(view, entry.key, "orderBy.key");
|
|
3764
|
+
const offset = args["offset"];
|
|
3765
|
+
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) fail("\"offset\" must be a non-negative integer.");
|
|
3766
|
+
return {
|
|
3767
|
+
op,
|
|
3768
|
+
filters,
|
|
3769
|
+
uncomparable,
|
|
3770
|
+
groups: [],
|
|
3771
|
+
metrics: [],
|
|
3772
|
+
columns: columns.length > 0 ? columns : [...view.columns],
|
|
3773
|
+
order: orderRaw,
|
|
3774
|
+
limit,
|
|
3775
|
+
limitClamped: clamped,
|
|
3776
|
+
offset: typeof offset === "number" ? offset : 0
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
if (args["columns"] !== void 0 || args["offset"] !== void 0) fail("\"columns\" and \"offset\" belong to op \"rows\". Use op \"aggregate\" with \"groupBy\" and \"metrics\".");
|
|
3780
|
+
const groups = list(args["groupBy"], "groupBy", QUERY_LIMITS.maxGroupBy).map((raw, i) => {
|
|
3781
|
+
if (!isObject(raw)) fail(`groupBy[${i}] must be an object.`);
|
|
3782
|
+
noOtherKeys(raw, ["column", "bucket"], `groupBy[${i}]`);
|
|
3783
|
+
const column = resolveColumn(view, raw["column"], `groupBy[${i}].column`);
|
|
3784
|
+
return {
|
|
3785
|
+
column,
|
|
3786
|
+
bucket: raw["bucket"] === void 0 ? "value" : oneOf(raw["bucket"], BUCKETS, `groupBy[${i}].bucket`),
|
|
3787
|
+
name: column.name
|
|
3788
|
+
};
|
|
3789
|
+
});
|
|
3790
|
+
if (new Set(groups.map((g) => g.column.index)).size !== groups.length) fail("\"groupBy\" names the same column twice.");
|
|
3791
|
+
if (new Set(groups.map((g) => g.name)).size !== groups.length) for (const g of groups) g.name = g.column.label;
|
|
3792
|
+
const metricsRaw = list(args["metrics"], "metrics", QUERY_LIMITS.maxMetrics);
|
|
3793
|
+
const metrics = (metricsRaw.length > 0 ? metricsRaw : [{ fn: "count" }]).map((raw, i) => {
|
|
3794
|
+
if (!isObject(raw)) fail(`metrics[${i}] must be an object.`);
|
|
3795
|
+
noOtherKeys(raw, [
|
|
3796
|
+
"fn",
|
|
3797
|
+
"column",
|
|
3798
|
+
"as"
|
|
3799
|
+
], `metrics[${i}]`);
|
|
3800
|
+
const fn = oneOf(raw["fn"], METRIC_FNS, `metrics[${i}].fn`);
|
|
3801
|
+
if (raw["column"] === void 0 && fn !== "count") fail(`metrics[${i}].column is required for "${fn}".`);
|
|
3802
|
+
const column = raw["column"] === void 0 ? void 0 : resolveColumn(view, raw["column"], `metrics[${i}].column`);
|
|
3803
|
+
if (raw["as"] !== void 0 && (typeof raw["as"] !== "string" || raw["as"].trim() === "")) fail(`metrics[${i}].as must be a non-empty string.`);
|
|
3804
|
+
const alias = typeof raw["as"] === "string" ? raw["as"].trim() : column === void 0 ? fn : `${fn}(${column.name})`;
|
|
3805
|
+
return column === void 0 ? {
|
|
3806
|
+
fn,
|
|
3807
|
+
alias
|
|
3808
|
+
} : {
|
|
3809
|
+
fn,
|
|
3810
|
+
column,
|
|
3811
|
+
alias
|
|
3812
|
+
};
|
|
3813
|
+
});
|
|
3814
|
+
if (new Set(metrics.map((m) => m.alias)).size !== metrics.length) fail("Two metrics have the same name; give one of them a distinct \"as\".");
|
|
3815
|
+
const available = () => [...metrics.map((m) => m.alias), ...groups.map((g) => g.name)].join(", ");
|
|
3816
|
+
for (const entry of orderRaw) {
|
|
3817
|
+
if (metrics.some((m) => m.alias === entry.key)) continue;
|
|
3818
|
+
const column = resolveColumn(view, entry.key, "orderBy.key");
|
|
3819
|
+
if (!groups.some((g) => g.column.index === column.index)) fail(`orderBy.key "${entry.key}" is neither a metric nor a grouped column. Available: ${available()}.`);
|
|
3820
|
+
}
|
|
3821
|
+
return {
|
|
3822
|
+
op,
|
|
3823
|
+
filters,
|
|
3824
|
+
uncomparable,
|
|
3825
|
+
groups,
|
|
3826
|
+
metrics,
|
|
3827
|
+
columns: [],
|
|
3828
|
+
order: orderRaw,
|
|
3829
|
+
limit,
|
|
3830
|
+
limitClamped: clamped,
|
|
3831
|
+
offset: 0
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
function compareValues(a, b) {
|
|
3835
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
3836
|
+
const sa = String(a);
|
|
3837
|
+
const sb = String(b);
|
|
3838
|
+
if (kindOf(sa) === "number" && kindOf(sb) === "number") return Number(sa) - Number(sb);
|
|
3839
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
3840
|
+
}
|
|
3841
|
+
/** 空值永远排在最后,与升降序无关 */
|
|
3842
|
+
function compareNullable(a, b, desc) {
|
|
3843
|
+
if (a === null && b === null) return 0;
|
|
3844
|
+
if (a === null) return 1;
|
|
3845
|
+
if (b === null) return -1;
|
|
3846
|
+
const order = compareValues(a, b);
|
|
3847
|
+
return desc ? -order : order;
|
|
3848
|
+
}
|
|
3849
|
+
const clip$1 = (text) => {
|
|
3850
|
+
const chars = [...text];
|
|
3851
|
+
return chars.length <= QUERY_LIMITS.cellChars ? {
|
|
3852
|
+
text,
|
|
3853
|
+
clipped: false
|
|
3854
|
+
} : {
|
|
3855
|
+
text: `${chars.slice(0, QUERY_LIMITS.cellChars).join("")}…`,
|
|
3856
|
+
clipped: true
|
|
3857
|
+
};
|
|
3858
|
+
};
|
|
3859
|
+
function newAcc(metric) {
|
|
3860
|
+
return {
|
|
3861
|
+
rows: 0,
|
|
3862
|
+
numbers: 0,
|
|
3863
|
+
dates: 0,
|
|
3864
|
+
sum: new Summer(),
|
|
3865
|
+
...metric.fn === "countDistinct" ? { distinct: /* @__PURE__ */ new Set() } : {},
|
|
3866
|
+
...metric.fn === "median" ? { values: [] } : {}
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
function finish(metric, acc) {
|
|
3870
|
+
switch (metric.fn) {
|
|
3871
|
+
case "count": return acc.rows;
|
|
3872
|
+
case "countDistinct": return acc.distinct?.size ?? 0;
|
|
3873
|
+
case "sum": return acc.numbers > 0 ? tidy(acc.sum.total) : null;
|
|
3874
|
+
case "avg": return acc.numbers > 0 ? tidy(acc.sum.total / acc.numbers) : null;
|
|
3875
|
+
case "median": {
|
|
3876
|
+
const values = acc.values ?? [];
|
|
3877
|
+
if (values.length === 0) return null;
|
|
3878
|
+
const sorted = Float64Array.from(values).sort();
|
|
3879
|
+
const mid = Math.floor(sorted.length / 2);
|
|
3880
|
+
return tidy(sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2);
|
|
3881
|
+
}
|
|
3882
|
+
case "min":
|
|
3883
|
+
case "max":
|
|
3884
|
+
if (acc.numbers === 0 && acc.dates === 0) return null;
|
|
3885
|
+
if (acc.numbers >= acc.dates) return (metric.fn === "min" ? acc.minN : acc.maxN) ?? null;
|
|
3886
|
+
return (metric.fn === "min" ? acc.minD : acc.maxD) ?? null;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
function runAggregate(view, query) {
|
|
3890
|
+
const { groups: groupSpecs, metrics } = query;
|
|
3891
|
+
const skipped = new Map(metrics.map((m) => [m.alias, {
|
|
3892
|
+
empty: 0,
|
|
3893
|
+
nonNumeric: 0
|
|
3894
|
+
}]));
|
|
3895
|
+
const unbucketed = /* @__PURE__ */ new Map();
|
|
3896
|
+
const table = /* @__PURE__ */ new Map();
|
|
3897
|
+
let matched = 0;
|
|
3898
|
+
const groupOf = (key) => {
|
|
3899
|
+
const id = key.map((part) => part === null ? "\0" : part).join("");
|
|
3900
|
+
let group = table.get(id);
|
|
3901
|
+
if (group === void 0) {
|
|
3902
|
+
group = {
|
|
3903
|
+
key,
|
|
3904
|
+
accs: metrics.map(newAcc)
|
|
3905
|
+
};
|
|
3906
|
+
table.set(id, group);
|
|
3907
|
+
}
|
|
3908
|
+
return group;
|
|
3909
|
+
};
|
|
3910
|
+
if (groupSpecs.length === 0) groupOf([]);
|
|
3911
|
+
for (const { cells } of view.data) {
|
|
3912
|
+
if (!query.filters.every((filter) => filter.test(cells))) continue;
|
|
3913
|
+
matched += 1;
|
|
3914
|
+
const group = groupOf(groupSpecs.map((spec) => {
|
|
3915
|
+
const text = (cells[spec.column.index] ?? "").trim();
|
|
3916
|
+
if (text === "") return null;
|
|
3917
|
+
if (spec.bucket === "value") return text;
|
|
3918
|
+
if (kindOf(text) === "date") return text.slice(0, spec.bucket === "year" ? 4 : spec.bucket === "month" ? 7 : 10);
|
|
3919
|
+
unbucketed.set(spec.name, (unbucketed.get(spec.name) ?? 0) + 1);
|
|
3920
|
+
return null;
|
|
3921
|
+
}));
|
|
3922
|
+
for (const [i, metric] of metrics.entries()) {
|
|
3923
|
+
const acc = group.accs[i];
|
|
3924
|
+
if (metric.column === void 0) {
|
|
3925
|
+
acc.rows += 1;
|
|
3926
|
+
continue;
|
|
3927
|
+
}
|
|
3928
|
+
const text = (cells[metric.column.index] ?? "").trim();
|
|
3929
|
+
if (metric.fn === "count") {
|
|
3930
|
+
if (text !== "") acc.rows += 1;
|
|
3931
|
+
continue;
|
|
3932
|
+
}
|
|
3933
|
+
if (metric.fn === "countDistinct") {
|
|
3934
|
+
if (text !== "") acc.distinct.add(text);
|
|
3935
|
+
continue;
|
|
3936
|
+
}
|
|
3937
|
+
const skip = skipped.get(metric.alias);
|
|
3938
|
+
const kind = kindOf(text);
|
|
3939
|
+
if (kind === "empty") skip.empty += 1;
|
|
3940
|
+
else if (kind === "number") {
|
|
3941
|
+
const x = Number(text);
|
|
3942
|
+
acc.numbers += 1;
|
|
3943
|
+
if (metric.fn === "sum" || metric.fn === "avg") acc.sum.add(x);
|
|
3944
|
+
else if (metric.fn === "median") acc.values.push(x);
|
|
3945
|
+
else if (metric.fn === "min") acc.minN = acc.minN === void 0 || x < acc.minN ? x : acc.minN;
|
|
3946
|
+
else acc.maxN = acc.maxN === void 0 || x > acc.maxN ? x : acc.maxN;
|
|
3947
|
+
} else if (kind === "date" && (metric.fn === "min" || metric.fn === "max")) {
|
|
3948
|
+
acc.dates += 1;
|
|
3949
|
+
if (metric.fn === "min") acc.minD = acc.minD === void 0 || text < acc.minD ? text : acc.minD;
|
|
3950
|
+
else acc.maxD = acc.maxD === void 0 || text > acc.maxD ? text : acc.maxD;
|
|
3951
|
+
} else skip.nonNumeric += 1;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
const rows = [...table.values()].map((group) => ({
|
|
3955
|
+
key: group.key,
|
|
3956
|
+
values: metrics.map((metric, i) => finish(metric, group.accs[i]))
|
|
3957
|
+
}));
|
|
3958
|
+
const keyIndexOf = (key) => {
|
|
3959
|
+
const column = metrics.some((m) => m.alias === key) ? void 0 : resolveColumn(view, key, "orderBy.key");
|
|
3960
|
+
return column === void 0 ? -1 : groupSpecs.findIndex((g) => g.column.index === column.index);
|
|
3961
|
+
};
|
|
3962
|
+
const comparators = [];
|
|
3963
|
+
for (const entry of query.order) {
|
|
3964
|
+
const metricIndex = metrics.findIndex((m) => m.alias === entry.key);
|
|
3965
|
+
const groupIndex = metricIndex === -1 ? keyIndexOf(entry.key) : -1;
|
|
3966
|
+
comparators.push((a, b) => metricIndex !== -1 ? compareNullable(a.values[metricIndex] ?? null, b.values[metricIndex] ?? null, entry.desc) : compareNullable(a.key[groupIndex] ?? null, b.key[groupIndex] ?? null, entry.desc));
|
|
3967
|
+
}
|
|
3968
|
+
if (comparators.length === 0 && groupSpecs.length > 0) {
|
|
3969
|
+
comparators.push((a, b) => compareNullable(a.values[0] ?? null, b.values[0] ?? null, true));
|
|
3970
|
+
for (let i = 0; i < groupSpecs.length; i += 1) comparators.push((a, b) => compareNullable(a.key[i] ?? null, b.key[i] ?? null, false));
|
|
3971
|
+
}
|
|
3972
|
+
rows.sort((a, b) => {
|
|
3973
|
+
for (const compare of comparators) {
|
|
3974
|
+
const order = compare(a, b);
|
|
3975
|
+
if (order !== 0) return order;
|
|
3976
|
+
}
|
|
3977
|
+
return 0;
|
|
3978
|
+
});
|
|
3979
|
+
let cellsTruncated = false;
|
|
3980
|
+
const names = groupSpecs.map((g) => g.name);
|
|
3981
|
+
const out = rows.slice(0, query.limit).map((row) => ({
|
|
3982
|
+
key: Object.fromEntries(row.key.map((part, i) => {
|
|
3983
|
+
if (part === null) return [names[i], null];
|
|
3984
|
+
const cut = clip$1(part);
|
|
3985
|
+
if (cut.clipped) cellsTruncated = true;
|
|
3986
|
+
return [names[i], cut.text];
|
|
3987
|
+
})),
|
|
3988
|
+
values: Object.fromEntries(metrics.map((metric, i) => [metric.alias, row.values[i] ?? null]))
|
|
3989
|
+
}));
|
|
3990
|
+
const skippedOut = {};
|
|
3991
|
+
for (const [alias, { empty, nonNumeric }] of skipped) {
|
|
3992
|
+
if (empty === 0 && nonNumeric === 0) continue;
|
|
3993
|
+
skippedOut[alias] = {
|
|
3994
|
+
...empty > 0 ? { empty } : {},
|
|
3995
|
+
...nonNumeric > 0 ? { nonNumeric } : {}
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
return {
|
|
3999
|
+
matchedRows: matched,
|
|
4000
|
+
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4001
|
+
groups: out,
|
|
4002
|
+
totalGroups: rows.length,
|
|
4003
|
+
...Object.keys(skippedOut).length > 0 ? { skipped: skippedOut } : {},
|
|
4004
|
+
...unbucketed.size > 0 ? { unbucketed: Object.fromEntries(unbucketed) } : {},
|
|
4005
|
+
cellsTruncated
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
function runRows(view, query) {
|
|
4009
|
+
const matched = view.data.filter((entry) => query.filters.every((filter) => filter.test(entry.cells)));
|
|
4010
|
+
if (query.order.length > 0) {
|
|
4011
|
+
const keys = query.order.map((entry) => ({
|
|
4012
|
+
index: resolveColumn(view, entry.key, "orderBy.key").index,
|
|
4013
|
+
desc: entry.desc
|
|
4014
|
+
}));
|
|
4015
|
+
const valueOf = (cells, index) => {
|
|
4016
|
+
const text = (cells[index] ?? "").trim();
|
|
4017
|
+
return text === "" ? null : text;
|
|
4018
|
+
};
|
|
4019
|
+
matched.sort((a, b) => {
|
|
4020
|
+
for (const { index, desc } of keys) {
|
|
4021
|
+
const order = compareNullable(valueOf(a.cells, index), valueOf(b.cells, index), desc);
|
|
4022
|
+
if (order !== 0) return order;
|
|
4023
|
+
}
|
|
4024
|
+
return a.row - b.row;
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
let cellsTruncated = false;
|
|
4028
|
+
const rows = matched.slice(query.offset, query.offset + query.limit).map((entry) => ({
|
|
4029
|
+
row: entry.row,
|
|
4030
|
+
cells: query.columns.map((column) => {
|
|
4031
|
+
const cut = clip$1((entry.cells[column.index] ?? "").trim());
|
|
4032
|
+
if (cut.clipped) cellsTruncated = true;
|
|
4033
|
+
return cut.text;
|
|
4034
|
+
})
|
|
4035
|
+
}));
|
|
4036
|
+
return {
|
|
4037
|
+
matchedRows: matched.length,
|
|
4038
|
+
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4039
|
+
rows,
|
|
4040
|
+
columns: query.columns.map((column) => column.label),
|
|
4041
|
+
cellsTruncated
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
//#endregion
|
|
4046
|
+
//#region ../agent/src/table/toolSource.ts
|
|
4047
|
+
/** @experimental */
|
|
4048
|
+
const QUERY_TABLE_TOOL = "query_table";
|
|
4049
|
+
const DESCRIPTION$2 = "Run exact statistics over an Excel (.xlsx) workbook the user attached, without reading it row by row: describe its sheets and columns, aggregate (filter, group, count, distinct count, sum, average, min, max, median), or list matching rows. Every query covers all rows and is computed on the user’s device.";
|
|
4050
|
+
const DESCRIPTION_WITH_DOWNLOADS = DESCRIPTION$2.replace("the user attached,", "the user attached or downloaded,");
|
|
4051
|
+
const SYSTEM_PROMPT$3 = "query_table computes over every row of an attached .xlsx workbook. Use it for any count, total, average, extreme, median, ranking or breakdown, and quote its numbers as returned — they are exact. Do not estimate statistics from sample rows, and do not page through a whole sheet with read_document to compute them. When a result reports skipped or uncomparable cells, tell the user how many and why. Attachment ids from earlier messages of this conversation stay valid for this tool.";
|
|
4052
|
+
const SYSTEM_PROMPT_WITH_DOWNLOADS = `${SYSTEM_PROMPT$3} A downloaded workbook\u2019s id from list_downloaded_files works too.`;
|
|
4053
|
+
const ID_DESCRIPTION = "The attachment id from the message that carried the workbook.";
|
|
4054
|
+
const ID_DESCRIPTION_WITH_DOWNLOADS = `${ID_DESCRIPTION} For a downloaded file, use the id from list_downloaded_files; the user is asked for permission to read it.`;
|
|
4055
|
+
const COLUMN = {
|
|
4056
|
+
type: "string",
|
|
4057
|
+
description: "A column, by its header text or its column letter (A, B, …). Use the letter when headers repeat."
|
|
4058
|
+
};
|
|
4059
|
+
const INPUT_SCHEMA$2 = {
|
|
4060
|
+
type: "object",
|
|
4061
|
+
properties: {
|
|
4062
|
+
id: {
|
|
4063
|
+
type: "string",
|
|
4064
|
+
description: ID_DESCRIPTION
|
|
4065
|
+
},
|
|
4066
|
+
op: {
|
|
4067
|
+
type: "string",
|
|
4068
|
+
enum: [
|
|
4069
|
+
"describe",
|
|
4070
|
+
"aggregate",
|
|
4071
|
+
"rows"
|
|
4072
|
+
],
|
|
4073
|
+
description: "\"describe\": sheets, columns, types and a sample. \"aggregate\": filter, group and compute metrics. \"rows\": list matching rows."
|
|
4074
|
+
},
|
|
4075
|
+
sheet: {
|
|
4076
|
+
anyOf: [{ type: "string" }, {
|
|
4077
|
+
type: "integer",
|
|
4078
|
+
minimum: 1
|
|
4079
|
+
}],
|
|
4080
|
+
description: "Sheet name or 1-based sheet number. Defaults to the first sheet that has data."
|
|
4081
|
+
},
|
|
4082
|
+
headerRow: {
|
|
4083
|
+
type: "integer",
|
|
4084
|
+
minimum: 1,
|
|
4085
|
+
description: "1-based worksheet row that holds the column headers. Defaults to the first non-empty row."
|
|
4086
|
+
},
|
|
4087
|
+
where: {
|
|
4088
|
+
type: "array",
|
|
4089
|
+
maxItems: QUERY_LIMITS.maxFilters,
|
|
4090
|
+
description: "Row filters; a row must satisfy all of them.",
|
|
4091
|
+
items: {
|
|
4092
|
+
type: "object",
|
|
4093
|
+
properties: {
|
|
4094
|
+
column: COLUMN,
|
|
4095
|
+
op: {
|
|
4096
|
+
type: "string",
|
|
4097
|
+
enum: [...FILTER_OPS]
|
|
4098
|
+
},
|
|
4099
|
+
value: {
|
|
4100
|
+
anyOf: [
|
|
4101
|
+
{ type: "string" },
|
|
4102
|
+
{ type: "number" },
|
|
4103
|
+
{
|
|
4104
|
+
type: "array",
|
|
4105
|
+
items: { anyOf: [{ type: "string" }, { type: "number" }] }
|
|
4106
|
+
}
|
|
4107
|
+
],
|
|
4108
|
+
description: "Comparison value. Use an array for \"in\". Ignored by \"empty\" and \"notEmpty\". gt/gte/lt/lte compare numbers with numbers and ISO dates (YYYY-MM-DD) with dates."
|
|
4109
|
+
}
|
|
4110
|
+
},
|
|
4111
|
+
required: ["column", "op"],
|
|
4112
|
+
additionalProperties: false
|
|
4113
|
+
}
|
|
4114
|
+
},
|
|
4115
|
+
groupBy: {
|
|
4116
|
+
type: "array",
|
|
4117
|
+
maxItems: QUERY_LIMITS.maxGroupBy,
|
|
4118
|
+
description: "aggregate only. Group rows by these columns. Omit for a single overall result.",
|
|
4119
|
+
items: {
|
|
4120
|
+
type: "object",
|
|
4121
|
+
properties: {
|
|
4122
|
+
column: COLUMN,
|
|
4123
|
+
bucket: {
|
|
4124
|
+
type: "string",
|
|
4125
|
+
enum: [...BUCKETS],
|
|
4126
|
+
description: "\"value\" (default) groups by the cell text; year/month/day bucket ISO date cells."
|
|
4127
|
+
}
|
|
4128
|
+
},
|
|
4129
|
+
required: ["column"],
|
|
4130
|
+
additionalProperties: false
|
|
4131
|
+
}
|
|
4132
|
+
},
|
|
4133
|
+
metrics: {
|
|
4134
|
+
type: "array",
|
|
4135
|
+
maxItems: QUERY_LIMITS.maxMetrics,
|
|
4136
|
+
description: "aggregate only. Defaults to a single row count. \"count\" without a column counts rows; with a column it counts non-empty cells.",
|
|
4137
|
+
items: {
|
|
4138
|
+
type: "object",
|
|
4139
|
+
properties: {
|
|
4140
|
+
fn: {
|
|
4141
|
+
type: "string",
|
|
4142
|
+
enum: [...METRIC_FNS]
|
|
4143
|
+
},
|
|
4144
|
+
column: COLUMN,
|
|
4145
|
+
as: {
|
|
4146
|
+
type: "string",
|
|
4147
|
+
description: "Name for this metric in the result."
|
|
4148
|
+
}
|
|
4149
|
+
},
|
|
4150
|
+
required: ["fn"],
|
|
4151
|
+
additionalProperties: false
|
|
4152
|
+
}
|
|
4153
|
+
},
|
|
4154
|
+
columns: {
|
|
4155
|
+
type: "array",
|
|
4156
|
+
items: COLUMN,
|
|
4157
|
+
description: "rows only. Which columns to return. Defaults to all."
|
|
4158
|
+
},
|
|
4159
|
+
orderBy: {
|
|
4160
|
+
type: "array",
|
|
4161
|
+
maxItems: QUERY_LIMITS.maxOrderBy,
|
|
4162
|
+
description: "aggregate: a metric name or a grouped column; default is the first metric, largest first. rows: any column; default is sheet order.",
|
|
4163
|
+
items: {
|
|
4164
|
+
type: "object",
|
|
4165
|
+
properties: {
|
|
4166
|
+
key: { type: "string" },
|
|
4167
|
+
desc: { type: "boolean" }
|
|
4168
|
+
},
|
|
4169
|
+
required: ["key"],
|
|
4170
|
+
additionalProperties: false
|
|
4171
|
+
}
|
|
4172
|
+
},
|
|
4173
|
+
limit: {
|
|
4174
|
+
type: "integer",
|
|
4175
|
+
minimum: 1,
|
|
4176
|
+
description: `Groups or rows to return. Default ${QUERY_LIMITS.defaultRows}, at most ${QUERY_LIMITS.maxRows}.`
|
|
4177
|
+
},
|
|
4178
|
+
offset: {
|
|
4179
|
+
type: "integer",
|
|
4180
|
+
minimum: 0,
|
|
4181
|
+
description: "rows only. Skip this many matching rows."
|
|
4182
|
+
}
|
|
4183
|
+
},
|
|
4184
|
+
required: ["id", "op"],
|
|
4185
|
+
additionalProperties: false
|
|
4186
|
+
};
|
|
4187
|
+
function toolError$1(e) {
|
|
4188
|
+
return {
|
|
4189
|
+
ok: false,
|
|
4190
|
+
content: [],
|
|
4191
|
+
error: {
|
|
4192
|
+
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
4193
|
+
message: messageOf(e)
|
|
4194
|
+
}
|
|
4195
|
+
};
|
|
4196
|
+
}
|
|
4197
|
+
const byteLength = (text) => new TextEncoder().encode(text).length;
|
|
4198
|
+
/** 返回体超过上限时从尾部减条数。`more` = 条件匹配的比应给的多(limit/offset 已经截过一次),减过也如实标 `truncated` */
|
|
4199
|
+
function fit(head, field, items, more) {
|
|
4200
|
+
let count = items.length;
|
|
4201
|
+
for (;;) {
|
|
4202
|
+
const truncated = more || count < items.length;
|
|
4203
|
+
const json = JSON.stringify({
|
|
4204
|
+
...head,
|
|
4205
|
+
[field]: items.slice(0, count),
|
|
4206
|
+
returned: count,
|
|
4207
|
+
...truncated ? { truncated: true } : {}
|
|
4208
|
+
});
|
|
4209
|
+
if (count <= 1 || byteLength(json) <= QUERY_LIMITS.maxBytes) return json;
|
|
4210
|
+
count = Math.max(1, Math.floor(count * .7));
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* 在本机对整张表精确计算(0.26.0 分册 10)。
|
|
4215
|
+
*
|
|
4216
|
+
* 结构化入参,**不执行**任何模型给的表达式或代码;工具只认 `TableSource` 一个取件出口,
|
|
4217
|
+
* 不认识附件、下载与会话。
|
|
4218
|
+
* @experimental
|
|
4219
|
+
*/
|
|
4220
|
+
function createTableQueryToolSource(options) {
|
|
4221
|
+
const schemaWithDownloads = {
|
|
4222
|
+
...INPUT_SCHEMA$2,
|
|
4223
|
+
properties: {
|
|
4224
|
+
...INPUT_SCHEMA$2.properties,
|
|
4225
|
+
id: {
|
|
4226
|
+
type: "string",
|
|
4227
|
+
description: ID_DESCRIPTION_WITH_DOWNLOADS
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
};
|
|
4231
|
+
const downloads = () => options.downloadedFiles?.() === true;
|
|
4232
|
+
return {
|
|
4233
|
+
kind: "table-query",
|
|
4234
|
+
listToolSpecs: () => Promise.resolve([downloads() ? {
|
|
4235
|
+
name: QUERY_TABLE_TOOL,
|
|
4236
|
+
description: DESCRIPTION_WITH_DOWNLOADS,
|
|
4237
|
+
inputSchema: schemaWithDownloads
|
|
4238
|
+
} : {
|
|
4239
|
+
name: QUERY_TABLE_TOOL,
|
|
4240
|
+
description: DESCRIPTION$2,
|
|
4241
|
+
inputSchema: INPUT_SCHEMA$2
|
|
4242
|
+
}]),
|
|
4243
|
+
systemPrompt: () => Promise.resolve(downloads() ? SYSTEM_PROMPT_WITH_DOWNLOADS : SYSTEM_PROMPT$3),
|
|
4244
|
+
canHandle: (name) => name === QUERY_TABLE_TOOL,
|
|
4245
|
+
argCaptureTrust: (name) => name === "query_table" ? { tier: "reviewed" } : void 0,
|
|
4246
|
+
call: async (name, args, context) => {
|
|
4247
|
+
try {
|
|
4248
|
+
if (name !== "query_table") throw new WebSkillError("TOOL_NOT_FOUND", `Unknown tool ${name}.`);
|
|
4249
|
+
return await run(args, context);
|
|
3257
4250
|
} catch (e) {
|
|
3258
4251
|
return toolError$1(e);
|
|
3259
4252
|
}
|
|
3260
4253
|
}
|
|
3261
4254
|
};
|
|
4255
|
+
async function run(args, context) {
|
|
4256
|
+
const id = args["id"];
|
|
4257
|
+
if (typeof id !== "string" || id === "") throw new WebSkillError("TOOL_UNSUPPORTED", "query_table requires the \"id\" of an attached Excel workbook.");
|
|
4258
|
+
const loaded = await options.tables.load(id, context);
|
|
4259
|
+
if (loaded === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", `No readable workbook with id "${id}". Use the id of an Excel file attached in this conversation; files from other conversations are not available.`);
|
|
4260
|
+
const { workbook, name } = loaded;
|
|
4261
|
+
if (args["op"] === "describe") return {
|
|
4262
|
+
ok: true,
|
|
4263
|
+
content: [{
|
|
4264
|
+
type: "text",
|
|
4265
|
+
text: profileTable({
|
|
4266
|
+
name,
|
|
4267
|
+
id,
|
|
4268
|
+
workbook,
|
|
4269
|
+
...args["sheet"] !== void 0 ? { sheet: args["sheet"] } : {},
|
|
4270
|
+
...args["headerRow"] !== void 0 ? { headerRow: args["headerRow"] } : {}
|
|
4271
|
+
})
|
|
4272
|
+
}]
|
|
4273
|
+
};
|
|
4274
|
+
const view = buildView(workbook, args["sheet"], args["headerRow"]);
|
|
4275
|
+
const query = parseQuery(args, view);
|
|
4276
|
+
const output = query.op === "aggregate" ? runAggregate(view, query) : runRows(view, query);
|
|
4277
|
+
const head = {
|
|
4278
|
+
sheet: view.sheetName,
|
|
4279
|
+
headerRow: view.headerRow,
|
|
4280
|
+
dataRows: view.data.length,
|
|
4281
|
+
matchedRows: output.matchedRows,
|
|
4282
|
+
...output.totalGroups !== void 0 ? { totalGroups: output.totalGroups } : {},
|
|
4283
|
+
...output.columns !== void 0 ? { columns: output.columns } : {},
|
|
4284
|
+
...output.skipped !== void 0 ? { skipped: output.skipped } : {},
|
|
4285
|
+
...output.unbucketed !== void 0 ? { unbucketed: output.unbucketed } : {},
|
|
4286
|
+
...Object.keys(output.uncomparable).length > 0 ? { uncomparable: output.uncomparable } : {},
|
|
4287
|
+
...output.cellsTruncated ? { cellsTruncated: true } : {},
|
|
4288
|
+
...query.limitClamped ? { limitClamped: QUERY_LIMITS.maxRows } : {}
|
|
4289
|
+
};
|
|
4290
|
+
const items = output.groups ?? output.rows ?? [];
|
|
4291
|
+
const more = output.groups !== void 0 ? (output.totalGroups ?? 0) > output.groups.length : query.offset + items.length < output.matchedRows;
|
|
4292
|
+
return {
|
|
4293
|
+
ok: true,
|
|
4294
|
+
content: [{
|
|
4295
|
+
type: "text",
|
|
4296
|
+
text: fit(head, output.groups !== void 0 ? "groups" : "rows", items, more)
|
|
4297
|
+
}]
|
|
4298
|
+
};
|
|
4299
|
+
}
|
|
3262
4300
|
}
|
|
3263
4301
|
|
|
3264
4302
|
//#endregion
|
|
@@ -4091,4 +5129,4 @@ function createDwgToolSource(options) {
|
|
|
4091
5129
|
}
|
|
4092
5130
|
|
|
4093
5131
|
//#endregion
|
|
4094
|
-
export { CAPTURE_PAGE_IMAGE_TOOL, CLOSE_PAGE_TAB_TOOL, DEFAULT_MAX_DATA_SOURCE_BYTES, DELEGATE_TASK_TOOL, DELEGATION_SYSTEM_PROMPT, DOCUMENT_READ_BUDGET, DOWNLOAD_WAIT_DEFAULT_MS, DOWNLOAD_WAIT_MAX_MS, DOWNLOAD_WAIT_MIN_MS, DWG_MODEL_VIEW_ID, DataSourceCandidatePolicy, DataSourcePolicy, DelegationOrchestrator, DownloadedFilePolicy, EMU_PER_PIXEL, EMU_PER_POINT, EMU_PER_TWIP, EXPORT_SPREADSHEET_TOOL, FETCH_REMOTE_IMAGE_TOOL, FILL_TEMPLATE_TOOL, FOCUS_PAGE_TAB_TOOL, GENERATE_SKILL_TOOL, INSPECT_TEMPLATE_TOOL, LIST_DOWNLOADED_FILES_TOOL, LIST_PAGE_TABS_TOOL, MANAGE_TODO_TOOL, MAX_DIGIT_WIDTH, PAGE_ACTION_KINDS, PAGE_ACTION_SYSTEM_PROMPT, PAGE_ACTION_TOOL, PAGE_DOCUMENT_REF, PDF_GARBLED_RATIO, PDF_LARGE_IMAGE_RATIO, PERCEIVE_PAGE_TOOL, PERCEPTION_PAGING_DEFAULTS, PERCEPTION_SYSTEM_PROMPT, PageActionPolicy, PagePerceptionPolicy, READ_DOCUMENT_TOOL, READ_DOWNLOADED_FILE_TOOL, SCROLL_DIRECTIONS, SKILL_GENERATION_SYSTEM_PROMPT, SkillGenerator, TAB_WORKSET_DEFAULT_MAX, TAB_WORKSET_MAX_MAX, TAB_WORKSET_MIN_MAX, TAB_WORKSET_SYSTEM_PROMPT, TEMPLATE_INSPECT_BUDGET, TODO_SYSTEM_PROMPT, TabWorksetPolicy, TodoStore, VIEW_DWG_TOOL, asReadMode, clampDownloadWaitMs, clampWorksetMax, columnWidthToPixels, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createHttpDataSourceTransport, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTemplateToolSource, createTodoToolSource, defersImage, dwgPickView, dwgViewsOf, fitWithin, formatCellAddress, frameLabel, frameSteps, isPdfTextTrustworthy, isRangeTopLeft, measurePdfTextQuality, parseCellAddress, parseCellRange, rangeContains, readImageSize, sniffDocumentFormat, takesImage, toActionFrameScopes, toFrameScopes, withDelegationOrigin };
|
|
5132
|
+
export { CAPTURE_PAGE_IMAGE_TOOL, CLOSE_PAGE_TAB_TOOL, DEFAULT_MAX_DATA_SOURCE_BYTES, DELEGATE_TASK_TOOL, DELEGATION_SYSTEM_PROMPT, DOCUMENT_READ_BUDGET, DOWNLOAD_WAIT_DEFAULT_MS, DOWNLOAD_WAIT_MAX_MS, DOWNLOAD_WAIT_MIN_MS, DWG_MODEL_VIEW_ID, DataSourceCandidatePolicy, DataSourcePolicy, DelegationOrchestrator, DownloadedFilePolicy, EMU_PER_PIXEL, EMU_PER_POINT, EMU_PER_TWIP, EXPORT_SPREADSHEET_TOOL, FETCH_REMOTE_IMAGE_TOOL, FILL_TEMPLATE_TOOL, FOCUS_PAGE_TAB_TOOL, GENERATE_SKILL_TOOL, INSPECT_TEMPLATE_TOOL, LIST_DOWNLOADED_FILES_TOOL, LIST_PAGE_TABS_TOOL, MANAGE_TODO_TOOL, MAX_DIGIT_WIDTH, PAGE_ACTION_KINDS, PAGE_ACTION_SYSTEM_PROMPT, PAGE_ACTION_TOOL, PAGE_DOCUMENT_REF, PDF_GARBLED_RATIO, PDF_LARGE_IMAGE_RATIO, PERCEIVE_PAGE_TOOL, PERCEPTION_PAGING_DEFAULTS, PERCEPTION_SYSTEM_PROMPT, PageActionPolicy, PagePerceptionPolicy, QUERY_TABLE_TOOL, READ_DOCUMENT_TOOL, READ_DOWNLOADED_FILE_TOOL, SCROLL_DIRECTIONS, SKILL_GENERATION_SYSTEM_PROMPT, SkillGenerator, TAB_WORKSET_DEFAULT_MAX, TAB_WORKSET_MAX_MAX, TAB_WORKSET_MIN_MAX, TAB_WORKSET_SYSTEM_PROMPT, TEMPLATE_INSPECT_BUDGET, TODO_SYSTEM_PROMPT, TabWorksetPolicy, TodoStore, VIEW_DWG_TOOL, asReadMode, clampDownloadWaitMs, clampWorksetMax, columnWidthToPixels, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createHttpDataSourceTransport, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTableQueryToolSource, createTemplateToolSource, createTodoToolSource, defersImage, dwgPickView, dwgViewsOf, fitWithin, formatCellAddress, frameLabel, frameSteps, isPdfTextTrustworthy, isRangeTopLeft, measurePdfTextQuality, parseCellAddress, parseCellRange, profileTable, rangeContains, readImageSize, sniffDocumentFormat, takesImage, toActionFrameScopes, toFrameScopes, withDelegationOrigin };
|