@yejiming/dsh-data-agent 0.0.9 → 0.0.11
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/README.en.md +32 -14
- package/README.md +32 -14
- package/cordis.patch.yml +8 -9
- package/lib/client.js +576 -593
- package/lib/client.js.map +1 -1
- package/lib/command-DuCpwVbl.js +875 -0
- package/lib/command.js +1 -874
- package/lib/{connections-DeauhaZi.js → connections-5sfdEDsG.js} +344 -34
- package/lib/index.js +104 -36
- package/lib/routes.js +6 -2
- package/lib/tool-DVh61An-.js +1128 -0
- package/lib/tool.js +1 -1127
- package/lib/types/client/DataAgentWorkbench.d.ts +2 -2
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/locales.d.ts +24 -0
- package/lib/types/client/persistence.d.ts +1 -1
- package/lib/types/client-discovery.d.ts +45 -0
- package/lib/types/clients.d.ts +7 -1
- package/lib/types/connections.d.ts +10 -0
- package/lib/types/defaults.d.ts +2 -0
- package/lib/types/index.d.ts +42 -16
- package/lib/types/routes.d.ts +0 -14
- package/lib/types/storage.d.ts +5 -0
- package/lib/types/tool.d.ts +4 -0
- package/package.json +5 -1
- package/preset/data-agent/agent.cordis.yml +6 -25
- package/preset/data-agent/preset.yml +1 -1
package/lib/tool.js
CHANGED
|
@@ -1,1128 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, r as DEFAULT_MAX_QUERY_CHARS } from "./defaults-DP4RyRh1.js";
|
|
3
|
-
import z from "schemastery";
|
|
4
|
-
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
|
-
const VIEW_KINDS = [
|
|
6
|
-
"metric",
|
|
7
|
-
"line",
|
|
8
|
-
"bar",
|
|
9
|
-
"pie",
|
|
10
|
-
"scatter",
|
|
11
|
-
"table"
|
|
12
|
-
];
|
|
13
|
-
const AXIS_TYPES = ["category", "time"];
|
|
14
|
-
const WIDTHS = ["full", "half"];
|
|
15
|
-
const METRIC_FORMATS = ["number", "percent"];
|
|
16
|
-
function fail(message) {
|
|
17
|
-
throw new Error(message);
|
|
18
|
-
}
|
|
19
|
-
function isRecord(value) {
|
|
20
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
-
}
|
|
22
|
-
/** Reject keys outside the allowed set (additionalProperties=false semantics). */
|
|
23
|
-
function assertOnlyKeys(record, allowed, label) {
|
|
24
|
-
for (const key of Object.keys(record)) if (!allowed.includes(key)) fail(label + ": 不支持的字段 \"" + key + "\"");
|
|
25
|
-
}
|
|
26
|
-
function requireNonEmptyString(value, label) {
|
|
27
|
-
if (typeof value !== "string" || value.trim().length === 0) fail(label + ": 必须是非空字符串");
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
function optionalString(record, key, label) {
|
|
31
|
-
const value = record[key];
|
|
32
|
-
if (value === void 0) return void 0;
|
|
33
|
-
if (typeof value !== "string") fail(label + "." + key + ": 必须是字符串");
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
function optionalEnum(record, key, allowed, label) {
|
|
37
|
-
const value = record[key];
|
|
38
|
-
if (value === void 0) return void 0;
|
|
39
|
-
if (typeof value !== "string" || !allowed.includes(value)) fail(label + "." + key + ": 必须是 " + allowed.join("/") + " 之一");
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
/** Read a required non-empty string field with a concrete error path. */
|
|
43
|
-
function requiredStringField(record, key, label) {
|
|
44
|
-
return requireNonEmptyString(record[key], label + "." + key);
|
|
45
|
-
}
|
|
46
|
-
/** Read an optional array of unique non-empty strings (the table whitelist). */
|
|
47
|
-
function optionalStringArray(record, key, label) {
|
|
48
|
-
const value = record[key];
|
|
49
|
-
if (value === void 0) return void 0;
|
|
50
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) fail(label + "." + key + ": 必须是非空字符串数组");
|
|
51
|
-
const items = value.map((item) => item);
|
|
52
|
-
if (new Set(items).size !== items.length) fail(label + "." + key + ": 列名不能重复");
|
|
53
|
-
return items;
|
|
54
|
-
}
|
|
55
|
-
function parseXAxis(value, label) {
|
|
56
|
-
if (!isRecord(value)) fail(label + ".x: 必须是对象 { field, type, label? }");
|
|
57
|
-
assertOnlyKeys(value, [
|
|
58
|
-
"field",
|
|
59
|
-
"type",
|
|
60
|
-
"label"
|
|
61
|
-
], label + ".x");
|
|
62
|
-
const field = requiredStringField(value, "field", label + ".x");
|
|
63
|
-
const type = optionalEnum(value, "type", AXIS_TYPES, label + ".x");
|
|
64
|
-
if (type === void 0) fail(label + ".x.type: 必须是 category/time 之一");
|
|
65
|
-
const axisLabel = optionalString(value, "label", label + ".x");
|
|
66
|
-
const axis = {
|
|
67
|
-
field,
|
|
68
|
-
type
|
|
69
|
-
};
|
|
70
|
-
if (axisLabel !== void 0) axis.label = axisLabel;
|
|
71
|
-
return axis;
|
|
72
|
-
}
|
|
73
|
-
function parseView(value, label, datasetIds) {
|
|
74
|
-
if (!isRecord(value)) fail(label + ": 必须是对象");
|
|
75
|
-
const kind = value["kind"];
|
|
76
|
-
if (typeof kind !== "string" || !VIEW_KINDS.includes(kind)) fail(label + ": kind 必须是 metric/line/bar/pie/scatter/table 之一");
|
|
77
|
-
const viewKind = kind;
|
|
78
|
-
const id = requiredStringField(value, "id", label);
|
|
79
|
-
const datasetId = requiredStringField(value, "datasetId", label);
|
|
80
|
-
if (!datasetIds.has(datasetId)) fail(label + ": 引用了不存在的 dataset id \"" + datasetId + "\"");
|
|
81
|
-
if (viewKind === "metric") {
|
|
82
|
-
assertOnlyKeys(value, [
|
|
83
|
-
"id",
|
|
84
|
-
"kind",
|
|
85
|
-
"datasetId",
|
|
86
|
-
"field",
|
|
87
|
-
"label",
|
|
88
|
-
"format"
|
|
89
|
-
], label);
|
|
90
|
-
const metric = {
|
|
91
|
-
id,
|
|
92
|
-
kind: "metric",
|
|
93
|
-
datasetId,
|
|
94
|
-
field: requiredStringField(value, "field", label),
|
|
95
|
-
label: requiredStringField(value, "label", label)
|
|
96
|
-
};
|
|
97
|
-
const format = optionalEnum(value, "format", METRIC_FORMATS, label);
|
|
98
|
-
if (format !== void 0) metric.format = format;
|
|
99
|
-
return metric;
|
|
100
|
-
}
|
|
101
|
-
const width = optionalEnum(value, "width", WIDTHS, label);
|
|
102
|
-
const viewLabel = optionalString(value, "label", label);
|
|
103
|
-
switch (viewKind) {
|
|
104
|
-
case "line":
|
|
105
|
-
case "bar": {
|
|
106
|
-
assertOnlyKeys(value, [
|
|
107
|
-
"id",
|
|
108
|
-
"kind",
|
|
109
|
-
"datasetId",
|
|
110
|
-
"label",
|
|
111
|
-
"width",
|
|
112
|
-
"x",
|
|
113
|
-
"y",
|
|
114
|
-
"seriesField"
|
|
115
|
-
], label);
|
|
116
|
-
const x = parseXAxis(value["x"], label);
|
|
117
|
-
const y = value["y"];
|
|
118
|
-
if (!Array.isArray(y) || y.length < 1 || y.length > 4 || y.some((item) => typeof item !== "string" || item.trim().length === 0)) fail(label + ".y: 必须是 1-4 个非空字段名");
|
|
119
|
-
const seriesField = optionalString(value, "seriesField", label);
|
|
120
|
-
if (seriesField !== void 0 && y.length >= 2) fail(label + ": seriesField 与多个 y 字段互斥,只能二选一");
|
|
121
|
-
const view = {
|
|
122
|
-
id,
|
|
123
|
-
kind: viewKind,
|
|
124
|
-
datasetId,
|
|
125
|
-
x,
|
|
126
|
-
y: y.map((item) => item)
|
|
127
|
-
};
|
|
128
|
-
if (viewLabel !== void 0) view.label = viewLabel;
|
|
129
|
-
if (width !== void 0) view.width = width;
|
|
130
|
-
if (seriesField !== void 0) view.seriesField = seriesField;
|
|
131
|
-
return view;
|
|
132
|
-
}
|
|
133
|
-
case "pie": {
|
|
134
|
-
assertOnlyKeys(value, [
|
|
135
|
-
"id",
|
|
136
|
-
"kind",
|
|
137
|
-
"datasetId",
|
|
138
|
-
"label",
|
|
139
|
-
"width",
|
|
140
|
-
"categoryField",
|
|
141
|
-
"valueField"
|
|
142
|
-
], label);
|
|
143
|
-
const view = {
|
|
144
|
-
id,
|
|
145
|
-
kind: "pie",
|
|
146
|
-
datasetId,
|
|
147
|
-
categoryField: requiredStringField(value, "categoryField", label),
|
|
148
|
-
valueField: requiredStringField(value, "valueField", label)
|
|
149
|
-
};
|
|
150
|
-
if (viewLabel !== void 0) view.label = viewLabel;
|
|
151
|
-
if (width !== void 0) view.width = width;
|
|
152
|
-
return view;
|
|
153
|
-
}
|
|
154
|
-
case "scatter": {
|
|
155
|
-
assertOnlyKeys(value, [
|
|
156
|
-
"id",
|
|
157
|
-
"kind",
|
|
158
|
-
"datasetId",
|
|
159
|
-
"label",
|
|
160
|
-
"width",
|
|
161
|
-
"xField",
|
|
162
|
-
"yField"
|
|
163
|
-
], label);
|
|
164
|
-
const view = {
|
|
165
|
-
id,
|
|
166
|
-
kind: "scatter",
|
|
167
|
-
datasetId,
|
|
168
|
-
xField: requiredStringField(value, "xField", label),
|
|
169
|
-
yField: requiredStringField(value, "yField", label)
|
|
170
|
-
};
|
|
171
|
-
if (viewLabel !== void 0) view.label = viewLabel;
|
|
172
|
-
if (width !== void 0) view.width = width;
|
|
173
|
-
return view;
|
|
174
|
-
}
|
|
175
|
-
case "table": {
|
|
176
|
-
assertOnlyKeys(value, [
|
|
177
|
-
"id",
|
|
178
|
-
"kind",
|
|
179
|
-
"datasetId",
|
|
180
|
-
"label",
|
|
181
|
-
"width",
|
|
182
|
-
"columns"
|
|
183
|
-
], label);
|
|
184
|
-
const view = {
|
|
185
|
-
id,
|
|
186
|
-
kind: "table",
|
|
187
|
-
datasetId
|
|
188
|
-
};
|
|
189
|
-
const columns = optionalStringArray(value, "columns", label);
|
|
190
|
-
if (viewLabel !== void 0) view.label = viewLabel;
|
|
191
|
-
if (width !== void 0) view.width = width;
|
|
192
|
-
if (columns !== void 0) view.columns = columns;
|
|
193
|
-
return view;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Strictly parse a model-supplied analysis request. Every structural
|
|
199
|
-
* violation (unknown fields, duplicate ids, dangling references, count or
|
|
200
|
-
* union constraints) throws with a message naming the offending view/dataset.
|
|
201
|
-
*/
|
|
202
|
-
function parseAnalysisRequest(input, prefix = "render-analysis") {
|
|
203
|
-
if (!isRecord(input)) fail(prefix + ": 请求必须是对象");
|
|
204
|
-
assertOnlyKeys(input, [
|
|
205
|
-
"title",
|
|
206
|
-
"summary",
|
|
207
|
-
"datasets",
|
|
208
|
-
"views"
|
|
209
|
-
], prefix);
|
|
210
|
-
const title = requireNonEmptyString(input["title"], prefix + ".title");
|
|
211
|
-
const summary = optionalString(input, "summary", prefix);
|
|
212
|
-
const datasets = input["datasets"];
|
|
213
|
-
if (!Array.isArray(datasets) || datasets.length < 1 || datasets.length > 6) fail(prefix + ": datasets 必须是 1-6 个");
|
|
214
|
-
const parsedDatasets = [];
|
|
215
|
-
const datasetIds = /* @__PURE__ */ new Set();
|
|
216
|
-
for (let index = 0; index < datasets.length; index += 1) {
|
|
217
|
-
const label = prefix + ".datasets[" + index + "]";
|
|
218
|
-
const item = datasets[index];
|
|
219
|
-
if (!isRecord(item)) fail(label + ": 必须是对象");
|
|
220
|
-
assertOnlyKeys(item, ["id", "sql"], label);
|
|
221
|
-
const id = requiredStringField(item, "id", label);
|
|
222
|
-
if (datasetIds.has(id)) fail(label + ": dataset id \"" + id + "\" 重复");
|
|
223
|
-
datasetIds.add(id);
|
|
224
|
-
parsedDatasets.push({
|
|
225
|
-
id,
|
|
226
|
-
sql: requiredStringField(item, "sql", label)
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
const views = input["views"];
|
|
230
|
-
if (!Array.isArray(views) || views.length < 1 || views.length > 8) fail(prefix + ": views 必须是 1-8 个");
|
|
231
|
-
const parsedViews = [];
|
|
232
|
-
const viewIds = /* @__PURE__ */ new Set();
|
|
233
|
-
for (let index = 0; index < views.length; index += 1) {
|
|
234
|
-
const label = prefix + ".views[" + index + "]";
|
|
235
|
-
const view = parseView(views[index], label, datasetIds);
|
|
236
|
-
if (viewIds.has(view.id)) fail(label + ": view id \"" + view.id + "\" 重复");
|
|
237
|
-
viewIds.add(view.id);
|
|
238
|
-
parsedViews.push(view);
|
|
239
|
-
}
|
|
240
|
-
const request = {
|
|
241
|
-
title,
|
|
242
|
-
datasets: parsedDatasets,
|
|
243
|
-
views: parsedViews
|
|
244
|
-
};
|
|
245
|
-
if (summary !== void 0) request.summary = summary;
|
|
246
|
-
return request;
|
|
247
|
-
}
|
|
248
|
-
/** Whether one string parses to a finite number. */
|
|
249
|
-
function isFiniteNumberText(value) {
|
|
250
|
-
return value.trim() !== "" && Number.isFinite(Number(value));
|
|
251
|
-
}
|
|
252
|
-
/** Whether one string parses as a time value. */
|
|
253
|
-
function isParseableTimeText(value) {
|
|
254
|
-
return value.trim() !== "" && !Number.isNaN(Date.parse(value));
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Validate view→dataset semantics AFTER all queries succeeded and BEFORE any
|
|
258
|
-
* meta is built: field existence, finite numerics, pie non-negativity, time
|
|
259
|
-
* parseability, and table whitelist existence. The client is never asked to
|
|
260
|
-
* aggregate, sort, or treat null as zero — validation happens here.
|
|
261
|
-
*/
|
|
262
|
-
function validateViewSemantics(views, datasets, prefix = "render-analysis") {
|
|
263
|
-
for (const view of views) {
|
|
264
|
-
const dataset = datasets.get(view.datasetId);
|
|
265
|
-
if (dataset === void 0) fail(prefix + ": view \"" + view.id + "\" 引用了未知 dataset \"" + view.datasetId + "\"");
|
|
266
|
-
const columns = new Set(dataset.columns);
|
|
267
|
-
const requireColumn = (field) => {
|
|
268
|
-
if (!columns.has(field)) fail(prefix + ": view \"" + view.id + "\" 引用了 dataset \"" + view.datasetId + "\" 中不存在的字段 \"" + field + "\"");
|
|
269
|
-
};
|
|
270
|
-
const requireNumeric = (field) => {
|
|
271
|
-
requireColumn(field);
|
|
272
|
-
for (const row of dataset.rows) {
|
|
273
|
-
const value = row[field] ?? null;
|
|
274
|
-
if (value !== null && !isFiniteNumberText(value)) fail(prefix + ": view \"" + view.id + "\" 的字段 \"" + field + "\" 含有非数值 \"" + value + "\"(不能转换为有限数)");
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
switch (view.kind) {
|
|
278
|
-
case "metric":
|
|
279
|
-
requireNumeric(view.field);
|
|
280
|
-
break;
|
|
281
|
-
case "line":
|
|
282
|
-
case "bar":
|
|
283
|
-
if (view.x.type === "time") {
|
|
284
|
-
requireColumn(view.x.field);
|
|
285
|
-
for (const row of dataset.rows) {
|
|
286
|
-
const value = row[view.x.field] ?? null;
|
|
287
|
-
if (value !== null && !isParseableTimeText(value)) fail(prefix + ": view \"" + view.id + "\" 的 x 字段 \"" + view.x.field + "\" 含有不可解析的时间值 \"" + value + "\"");
|
|
288
|
-
}
|
|
289
|
-
} else requireColumn(view.x.field);
|
|
290
|
-
if (view.seriesField !== void 0) requireColumn(view.seriesField);
|
|
291
|
-
for (const field of view.y) requireNumeric(field);
|
|
292
|
-
break;
|
|
293
|
-
case "pie":
|
|
294
|
-
requireColumn(view.categoryField);
|
|
295
|
-
requireNumeric(view.valueField);
|
|
296
|
-
for (const row of dataset.rows) {
|
|
297
|
-
const value = row[view.valueField] ?? null;
|
|
298
|
-
if (value !== null && Number(value) < 0) fail(prefix + ": view \"" + view.id + "\" 的 valueField \"" + view.valueField + "\" 含负数 \"" + value + "\"(饼图值必须非负)");
|
|
299
|
-
}
|
|
300
|
-
break;
|
|
301
|
-
case "scatter":
|
|
302
|
-
requireNumeric(view.xField);
|
|
303
|
-
requireNumeric(view.yField);
|
|
304
|
-
break;
|
|
305
|
-
case "table": for (const column of view.columns ?? []) requireColumn(column);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
/** Compress object rows into column-aligned two-dimensional arrays (D2). */
|
|
310
|
-
function rowsToArrays(columns, rows) {
|
|
311
|
-
return rows.map((row) => columns.map((column) => row[column] ?? null));
|
|
312
|
-
}
|
|
313
|
-
/** JSON-encoded UTF-8 size of the normalized report (the 512 KiB bound). */
|
|
314
|
-
function reportJsonBytes(report) {
|
|
315
|
-
return new TextEncoder().encode(JSON.stringify(report)).length;
|
|
316
|
-
}
|
|
317
|
-
/** One-line model-facing summary; never re-injects rows into model context (D5). */
|
|
318
|
-
function formatAnalysisSummary(report) {
|
|
319
|
-
const emptyIds = report.datasets.filter((dataset) => dataset.rows.length === 0).map((dataset) => dataset.id);
|
|
320
|
-
let text = "已生成分析报告《" + report.title + "》:" + report.datasets.length + " 个数据集、" + report.views.length + " 个视图(version 1)。";
|
|
321
|
-
if (emptyIds.length > 0) text += "其中 " + emptyIds.length + " 个数据集无数据:" + emptyIds.join("、") + "。";
|
|
322
|
-
return text;
|
|
323
|
-
}
|
|
324
|
-
const BASE_VIEW_PROPERTIES = {
|
|
325
|
-
id: {
|
|
326
|
-
type: "string",
|
|
327
|
-
required: true,
|
|
328
|
-
description: "视图唯一 id(本报告内不重复)"
|
|
329
|
-
},
|
|
330
|
-
kind: {
|
|
331
|
-
type: "string",
|
|
332
|
-
required: true,
|
|
333
|
-
description: "视图类型"
|
|
334
|
-
},
|
|
335
|
-
datasetId: {
|
|
336
|
-
type: "string",
|
|
337
|
-
required: true,
|
|
338
|
-
description: "引用本次请求中的一个 dataset id"
|
|
339
|
-
},
|
|
340
|
-
label: {
|
|
341
|
-
type: "string",
|
|
342
|
-
description: "可选视图标题,用于图表可访问名称与空态"
|
|
343
|
-
},
|
|
344
|
-
width: {
|
|
345
|
-
type: "string",
|
|
346
|
-
enum: ["full", "half"],
|
|
347
|
-
description: "可选宽度:full 整行 / half 半行(缺省由系统决定)"
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
const METRIC_VIEW_SCHEMA = {
|
|
351
|
-
type: "object",
|
|
352
|
-
properties: {
|
|
353
|
-
id: BASE_VIEW_PROPERTIES.id,
|
|
354
|
-
kind: {
|
|
355
|
-
type: "string",
|
|
356
|
-
const: "metric",
|
|
357
|
-
required: true
|
|
358
|
-
},
|
|
359
|
-
datasetId: BASE_VIEW_PROPERTIES.datasetId,
|
|
360
|
-
field: {
|
|
361
|
-
type: "string",
|
|
362
|
-
required: true,
|
|
363
|
-
description: "数值字段名(来自 dataset 查询结果的列)"
|
|
364
|
-
},
|
|
365
|
-
label: {
|
|
366
|
-
type: "string",
|
|
367
|
-
required: true,
|
|
368
|
-
description: "指标名称,如「本月营收」"
|
|
369
|
-
},
|
|
370
|
-
format: {
|
|
371
|
-
type: "string",
|
|
372
|
-
enum: ["number", "percent"],
|
|
373
|
-
description: "可选数值格式:number(默认)或 percent(值×100 后加 %)"
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
additionalProperties: false
|
|
377
|
-
};
|
|
378
|
-
const LINE_BAR_VIEW_SCHEMA = (kind) => ({
|
|
379
|
-
type: "object",
|
|
380
|
-
properties: {
|
|
381
|
-
id: BASE_VIEW_PROPERTIES.id,
|
|
382
|
-
kind: {
|
|
383
|
-
type: "string",
|
|
384
|
-
const: kind,
|
|
385
|
-
required: true
|
|
386
|
-
},
|
|
387
|
-
datasetId: BASE_VIEW_PROPERTIES.datasetId,
|
|
388
|
-
label: BASE_VIEW_PROPERTIES.label,
|
|
389
|
-
width: BASE_VIEW_PROPERTIES.width,
|
|
390
|
-
x: {
|
|
391
|
-
type: "object",
|
|
392
|
-
properties: {
|
|
393
|
-
field: {
|
|
394
|
-
type: "string",
|
|
395
|
-
required: true,
|
|
396
|
-
description: "x 轴字段名"
|
|
397
|
-
},
|
|
398
|
-
type: {
|
|
399
|
-
type: "string",
|
|
400
|
-
enum: ["category", "time"],
|
|
401
|
-
required: true,
|
|
402
|
-
description: "category 分类轴 / time 时间轴(数据需可由 Date 解析,SQL 请 ORDER BY)"
|
|
403
|
-
},
|
|
404
|
-
label: {
|
|
405
|
-
type: "string",
|
|
406
|
-
description: "可选 x 轴名称"
|
|
407
|
-
}
|
|
408
|
-
},
|
|
409
|
-
additionalProperties: false,
|
|
410
|
-
required: true
|
|
411
|
-
},
|
|
412
|
-
y: {
|
|
413
|
-
type: "array",
|
|
414
|
-
required: true,
|
|
415
|
-
items: { type: "string" },
|
|
416
|
-
description: "1-4 个数值 y 字段名;声明多个 y 时不得同时声明 seriesField"
|
|
417
|
-
},
|
|
418
|
-
seriesField: {
|
|
419
|
-
type: "string",
|
|
420
|
-
description: "可选分组字段:按该字段取值拆成多个系列(与多个 y 字段互斥)"
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
additionalProperties: false
|
|
424
|
-
});
|
|
425
|
-
const PIE_VIEW_SCHEMA = {
|
|
426
|
-
type: "object",
|
|
427
|
-
properties: {
|
|
428
|
-
id: BASE_VIEW_PROPERTIES.id,
|
|
429
|
-
kind: {
|
|
430
|
-
type: "string",
|
|
431
|
-
const: "pie",
|
|
432
|
-
required: true
|
|
433
|
-
},
|
|
434
|
-
datasetId: BASE_VIEW_PROPERTIES.datasetId,
|
|
435
|
-
label: BASE_VIEW_PROPERTIES.label,
|
|
436
|
-
width: BASE_VIEW_PROPERTIES.width,
|
|
437
|
-
categoryField: {
|
|
438
|
-
type: "string",
|
|
439
|
-
required: true,
|
|
440
|
-
description: "分类字段名"
|
|
441
|
-
},
|
|
442
|
-
valueField: {
|
|
443
|
-
type: "string",
|
|
444
|
-
required: true,
|
|
445
|
-
description: "非负数值字段名"
|
|
446
|
-
}
|
|
447
|
-
},
|
|
448
|
-
additionalProperties: false
|
|
449
|
-
};
|
|
450
|
-
const SCATTER_VIEW_SCHEMA = {
|
|
451
|
-
type: "object",
|
|
452
|
-
properties: {
|
|
453
|
-
id: BASE_VIEW_PROPERTIES.id,
|
|
454
|
-
kind: {
|
|
455
|
-
type: "string",
|
|
456
|
-
const: "scatter",
|
|
457
|
-
required: true
|
|
458
|
-
},
|
|
459
|
-
datasetId: BASE_VIEW_PROPERTIES.datasetId,
|
|
460
|
-
label: BASE_VIEW_PROPERTIES.label,
|
|
461
|
-
width: BASE_VIEW_PROPERTIES.width,
|
|
462
|
-
xField: {
|
|
463
|
-
type: "string",
|
|
464
|
-
required: true,
|
|
465
|
-
description: "数值 x 字段名"
|
|
466
|
-
},
|
|
467
|
-
yField: {
|
|
468
|
-
type: "string",
|
|
469
|
-
required: true,
|
|
470
|
-
description: "数值 y 字段名"
|
|
471
|
-
}
|
|
472
|
-
},
|
|
473
|
-
additionalProperties: false
|
|
474
|
-
};
|
|
475
|
-
const TABLE_VIEW_SCHEMA = {
|
|
476
|
-
type: "object",
|
|
477
|
-
properties: {
|
|
478
|
-
id: BASE_VIEW_PROPERTIES.id,
|
|
479
|
-
kind: {
|
|
480
|
-
type: "string",
|
|
481
|
-
const: "table",
|
|
482
|
-
required: true
|
|
483
|
-
},
|
|
484
|
-
datasetId: BASE_VIEW_PROPERTIES.datasetId,
|
|
485
|
-
label: BASE_VIEW_PROPERTIES.label,
|
|
486
|
-
width: BASE_VIEW_PROPERTIES.width,
|
|
487
|
-
columns: {
|
|
488
|
-
type: "array",
|
|
489
|
-
items: { type: "string" },
|
|
490
|
-
description: "可选列白名单;省略时按 dataset 列顺序显示"
|
|
491
|
-
}
|
|
492
|
-
},
|
|
493
|
-
additionalProperties: false
|
|
494
|
-
};
|
|
495
|
-
/** The view union: exactly the six supported kinds, nothing else. */
|
|
496
|
-
const ANALYSIS_VIEWS_SCHEMA = { oneOf: [
|
|
497
|
-
METRIC_VIEW_SCHEMA,
|
|
498
|
-
LINE_BAR_VIEW_SCHEMA("line"),
|
|
499
|
-
LINE_BAR_VIEW_SCHEMA("bar"),
|
|
500
|
-
PIE_VIEW_SCHEMA,
|
|
501
|
-
SCATTER_VIEW_SCHEMA,
|
|
502
|
-
TABLE_VIEW_SCHEMA
|
|
503
|
-
] };
|
|
504
|
-
/** Wire parameter schema of the render-analysis tool. */
|
|
505
|
-
const RENDER_ANALYSIS_PARAMETERS = {
|
|
506
|
-
title: {
|
|
507
|
-
type: "string",
|
|
508
|
-
required: true,
|
|
509
|
-
description: "报告标题,如「月度经营分析」"
|
|
510
|
-
},
|
|
511
|
-
summary: {
|
|
512
|
-
type: "string",
|
|
513
|
-
description: "可选一句话结论/摘要,显示在报告头部"
|
|
514
|
-
},
|
|
515
|
-
datasets: {
|
|
516
|
-
type: "array",
|
|
517
|
-
required: true,
|
|
518
|
-
items: {
|
|
519
|
-
type: "object",
|
|
520
|
-
properties: {
|
|
521
|
-
id: {
|
|
522
|
-
type: "string",
|
|
523
|
-
required: true,
|
|
524
|
-
description: "数据集唯一 id(供 views 引用)"
|
|
525
|
-
},
|
|
526
|
-
sql: {
|
|
527
|
-
type: "string",
|
|
528
|
-
required: true,
|
|
529
|
-
description: "一条只读 SQL(SELECT/SHOW/DESCRIBE/EXPLAIN;聚合、Top N、排序都写在 SQL 中)"
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
additionalProperties: false
|
|
533
|
-
},
|
|
534
|
-
description: "1-6 个数据集;每个按顺序恰好执行一次,同一数据集可被多个视图复用"
|
|
535
|
-
},
|
|
536
|
-
views: {
|
|
537
|
-
type: "array",
|
|
538
|
-
required: true,
|
|
539
|
-
items: ANALYSIS_VIEWS_SCHEMA,
|
|
540
|
-
description: "1-8 个视图;每个视图必须回答一个不同子问题,多个视图可共享同一 dataset"
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
/** Canonical output schema of the render-analysis tool. */
|
|
544
|
-
const ANALYSIS_REPORT_OUTPUT_SCHEMA = {
|
|
545
|
-
type: "object",
|
|
546
|
-
properties: {
|
|
547
|
-
version: {
|
|
548
|
-
type: "integer",
|
|
549
|
-
const: 1,
|
|
550
|
-
required: true
|
|
551
|
-
},
|
|
552
|
-
title: {
|
|
553
|
-
type: "string",
|
|
554
|
-
required: true
|
|
555
|
-
},
|
|
556
|
-
summary: { type: "string" },
|
|
557
|
-
datasets: {
|
|
558
|
-
type: "array",
|
|
559
|
-
required: true,
|
|
560
|
-
items: {
|
|
561
|
-
type: "object",
|
|
562
|
-
properties: {
|
|
563
|
-
id: {
|
|
564
|
-
type: "string",
|
|
565
|
-
required: true
|
|
566
|
-
},
|
|
567
|
-
columns: {
|
|
568
|
-
type: "array",
|
|
569
|
-
required: true,
|
|
570
|
-
items: { type: "string" }
|
|
571
|
-
},
|
|
572
|
-
rows: {
|
|
573
|
-
type: "array",
|
|
574
|
-
required: true,
|
|
575
|
-
items: {
|
|
576
|
-
type: "array",
|
|
577
|
-
items: { oneOf: [{ type: "string" }, { type: "null" }] }
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
additionalProperties: false
|
|
582
|
-
}
|
|
583
|
-
},
|
|
584
|
-
views: {
|
|
585
|
-
type: "array",
|
|
586
|
-
required: true,
|
|
587
|
-
items: ANALYSIS_VIEWS_SCHEMA
|
|
588
|
-
}
|
|
589
|
-
},
|
|
590
|
-
additionalProperties: false
|
|
591
|
-
};
|
|
592
|
-
//#endregion
|
|
593
|
-
//#region src/structured.ts
|
|
594
|
-
function normalizeNewlines(text) {
|
|
595
|
-
return text.replace(/\r\n?/g, "\n");
|
|
596
|
-
}
|
|
597
|
-
function splitLine(line, delimiter) {
|
|
598
|
-
return line.split(delimiter);
|
|
599
|
-
}
|
|
600
|
-
/** Make column names valid unique JSON object keys. */
|
|
601
|
-
function uniqueColumns(columns) {
|
|
602
|
-
const used = /* @__PURE__ */ new Set();
|
|
603
|
-
return columns.map((raw, index) => {
|
|
604
|
-
let name = raw.trim();
|
|
605
|
-
if (name.length === 0) name = `column_${index + 1}`;
|
|
606
|
-
if (used.has(name)) {
|
|
607
|
-
let suffix = 2;
|
|
608
|
-
while (used.has(`${name}_${suffix}`)) suffix += 1;
|
|
609
|
-
name = `${name}_${suffix}`;
|
|
610
|
-
}
|
|
611
|
-
used.add(name);
|
|
612
|
-
return name;
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
function rowObject(columns, fields) {
|
|
616
|
-
const row = {};
|
|
617
|
-
for (let index = 0; index < columns.length; index += 1) row[columns[index]] = fields[index] ?? null;
|
|
618
|
-
return row;
|
|
619
|
-
}
|
|
620
|
-
function emptyOutput() {
|
|
621
|
-
return {
|
|
622
|
-
columns: [],
|
|
623
|
-
rows: [],
|
|
624
|
-
rowLimitExceeded: false
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
function skipLeadingBlank(lines) {
|
|
628
|
-
let index = 0;
|
|
629
|
-
while (index < lines.length && lines[index].trim().length === 0) index += 1;
|
|
630
|
-
return index;
|
|
631
|
-
}
|
|
632
|
-
/** PostgreSQL `-A` appends a `(N rows)` / `(N row)` footer after SELECT output. */
|
|
633
|
-
function isPostgresFooter(line) {
|
|
634
|
-
return /^\(\d+ rows?\)$/.test(line.trim());
|
|
635
|
-
}
|
|
636
|
-
function parseDelimited(stdout, delimiter, maxRows, skipFooter = false) {
|
|
637
|
-
const lines = normalizeNewlines(stdout).split("\n");
|
|
638
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
639
|
-
const headerIndex = skipLeadingBlank(lines);
|
|
640
|
-
if (headerIndex >= lines.length) return emptyOutput();
|
|
641
|
-
const columns = uniqueColumns(splitLine(lines[headerIndex], delimiter));
|
|
642
|
-
const rows = [];
|
|
643
|
-
let rowLimitExceeded = false;
|
|
644
|
-
for (let index = headerIndex + 1; index < lines.length; index += 1) {
|
|
645
|
-
const line = lines[index];
|
|
646
|
-
if (skipFooter && isPostgresFooter(line)) continue;
|
|
647
|
-
if (rows.length >= maxRows) {
|
|
648
|
-
rowLimitExceeded = true;
|
|
649
|
-
break;
|
|
650
|
-
}
|
|
651
|
-
rows.push(rowObject(columns, splitLine(line, delimiter)));
|
|
652
|
-
}
|
|
653
|
-
return {
|
|
654
|
-
columns,
|
|
655
|
-
rows,
|
|
656
|
-
rowLimitExceeded
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
/** Minimal RFC-4180-style parser for sqlite3 `-csv` output. */
|
|
660
|
-
function parseCsv(text) {
|
|
661
|
-
const records = [];
|
|
662
|
-
let record = [];
|
|
663
|
-
let field = "";
|
|
664
|
-
let quoted = false;
|
|
665
|
-
let index = 0;
|
|
666
|
-
const pushField = () => {
|
|
667
|
-
record.push(field);
|
|
668
|
-
field = "";
|
|
669
|
-
};
|
|
670
|
-
const pushRecord = () => {
|
|
671
|
-
pushField();
|
|
672
|
-
records.push(record);
|
|
673
|
-
record = [];
|
|
674
|
-
};
|
|
675
|
-
while (index < text.length) {
|
|
676
|
-
const char = text[index];
|
|
677
|
-
if (quoted) {
|
|
678
|
-
if (char === "\"") {
|
|
679
|
-
if (text[index + 1] === "\"") {
|
|
680
|
-
field += "\"";
|
|
681
|
-
index += 2;
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
quoted = false;
|
|
685
|
-
index += 1;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
field += char;
|
|
689
|
-
index += 1;
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
if (char === "\"" && field.length === 0) {
|
|
693
|
-
quoted = true;
|
|
694
|
-
index += 1;
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
if (char === ",") {
|
|
698
|
-
pushField();
|
|
699
|
-
index += 1;
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
if (char === "\n") {
|
|
703
|
-
pushRecord();
|
|
704
|
-
index += 1;
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
if (char === "\r") {
|
|
708
|
-
if (text[index + 1] === "\n") index += 1;
|
|
709
|
-
pushRecord();
|
|
710
|
-
index += 1;
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
field += char;
|
|
714
|
-
index += 1;
|
|
715
|
-
}
|
|
716
|
-
if (field.length > 0 || record.length > 0) pushRecord();
|
|
717
|
-
return records;
|
|
718
|
-
}
|
|
719
|
-
function parseCsvOutput(stdout, maxRows) {
|
|
720
|
-
const records = parseCsv(normalizeNewlines(stdout)).filter((record) => !(record.length === 1 && record[0] === ""));
|
|
721
|
-
if (records.length === 0) return emptyOutput();
|
|
722
|
-
const columns = uniqueColumns(records[0]);
|
|
723
|
-
const rows = [];
|
|
724
|
-
let rowLimitExceeded = false;
|
|
725
|
-
for (let index = 1; index < records.length; index += 1) {
|
|
726
|
-
if (rows.length >= maxRows) {
|
|
727
|
-
rowLimitExceeded = true;
|
|
728
|
-
break;
|
|
729
|
-
}
|
|
730
|
-
rows.push(rowObject(columns, records[index]));
|
|
731
|
-
}
|
|
732
|
-
return {
|
|
733
|
-
columns,
|
|
734
|
-
rows,
|
|
735
|
-
rowLimitExceeded
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Parse one database type's structured-query stdout. The matching template is
|
|
740
|
-
* `buildStructuredQueryTemplate`: mysql tab-separated with a header, postgres
|
|
741
|
-
* pipe-separated with a header and row-count footer, sqlite CSV with a header,
|
|
742
|
-
* oracle pipe-separated with heading on, hive/impala tsv with a header.
|
|
743
|
-
*/
|
|
744
|
-
function parseStructuredQueryOutput(type, stdout, maxRows) {
|
|
745
|
-
switch (type) {
|
|
746
|
-
case "mysql": return parseDelimited(stdout, " ", maxRows);
|
|
747
|
-
case "postgres": return parseDelimited(stdout, "|", maxRows, true);
|
|
748
|
-
case "sqlite": return parseCsvOutput(stdout, maxRows);
|
|
749
|
-
case "oracle": return parseDelimited(stdout, "|", maxRows);
|
|
750
|
-
case "hive":
|
|
751
|
-
case "impala": return parseDelimited(stdout, " ", maxRows);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
//#endregion
|
|
755
|
-
//#region src/structured-read.ts
|
|
756
|
-
/** Look up the session connection, failing with the same message for every tool. */
|
|
757
|
-
async function requireToolConnection(ctx, exec, toolName) {
|
|
758
|
-
const sessionId = exec.agent?.id;
|
|
759
|
-
if (sessionId === void 0) throw new Error(toolName + ": 缺少会话上下文(agent loop 未注入)");
|
|
760
|
-
try {
|
|
761
|
-
return await ctx.dataAgentConnections.resolveForExecution(sessionId);
|
|
762
|
-
} catch (error) {
|
|
763
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
764
|
-
throw new Error(toolName + ": " + message);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
/** Run and redact a client result/error before it reaches tool/session output. */
|
|
768
|
-
async function runRedactedClientQuery(ctx, connection, sql, options, signal) {
|
|
769
|
-
try {
|
|
770
|
-
const result = await runClientQuery(ctx, connection, sql, options, signal);
|
|
771
|
-
return redactQueryResult(result, connection);
|
|
772
|
-
} catch (error) {
|
|
773
|
-
const message = redactSecretText(error instanceof Error ? error.message : String(error), [connection.password]);
|
|
774
|
-
throw new Error(message, error instanceof Error ? { cause: error } : void 0);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
/** Query runner options with the deployment overrides applied. */
|
|
778
|
-
function runnerOptions(resolved, mode) {
|
|
779
|
-
return {
|
|
780
|
-
clients: resolved.clients,
|
|
781
|
-
timeoutMs: resolved.queryTimeoutMs,
|
|
782
|
-
maxResultChars: resolved.maxResultChars,
|
|
783
|
-
...mode !== void 0 ? { mode } : {}
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
/**
|
|
787
|
-
* Execute one read-only SQL through the structured client template and parse
|
|
788
|
-
* it into the canonical { columns, rows } shape, with maxRows enforced at both
|
|
789
|
-
* the SQL level (LIMIT injection) and the parse level.
|
|
790
|
-
*/
|
|
791
|
-
async function runStructuredReadQuery(ctx, connection, sql, resolved, toolName, signal) {
|
|
792
|
-
if (sql.trim().length === 0) throw new Error(toolName + ": sql 不能为空");
|
|
793
|
-
if (sql.length > resolved.maxQueryChars) throw new Error(toolName + ": sql 超过长度上限(" + resolved.maxQueryChars + " 字符)");
|
|
794
|
-
assertSingleStatement(sql, toolName);
|
|
795
|
-
if (classifyStatement(sql, connection.type) !== "read") throw new Error(toolName + " 只执行读语句(SELECT/SHOW/DESCRIBE/EXPLAIN,SQLite 还含查询型 PRAGMA);写语句请使用 sql-write");
|
|
796
|
-
const limitedSql = enforceReadRowLimit(sql, connection.type, resolved.maxRows);
|
|
797
|
-
const startedAt = Date.now();
|
|
798
|
-
const result = await runRedactedClientQuery(ctx, connection, limitedSql, runnerOptions(resolved, "structured"), signal);
|
|
799
|
-
const elapsedMs = Date.now() - startedAt;
|
|
800
|
-
if (result.exitCode !== 0) {
|
|
801
|
-
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
802
|
-
throw new Error(toolName + " 执行失败(exit " + result.exitCode + "):" + detail);
|
|
803
|
-
}
|
|
804
|
-
const parsed = parseStructuredQueryOutput(connection.type, result.stdout, resolved.maxRows);
|
|
805
|
-
return {
|
|
806
|
-
columns: parsed.columns,
|
|
807
|
-
rows: parsed.rows,
|
|
808
|
-
elapsedMs,
|
|
809
|
-
truncated: result.truncated || parsed.rowLimitExceeded
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
//#endregion
|
|
813
|
-
//#region src/tool.ts
|
|
814
|
-
/** Cordis plugin name (diagnostics only). */
|
|
815
|
-
const name = "data-agent-tool";
|
|
816
|
-
/** Services required before the tool can register. */
|
|
817
|
-
const inject = [
|
|
818
|
-
"tools",
|
|
819
|
-
"subprocess",
|
|
820
|
-
"dataAgentConnections"
|
|
821
|
-
];
|
|
822
|
-
/** Loader schema with deployment defaults (no library defaults). */
|
|
823
|
-
const Config = z.object({
|
|
824
|
-
queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS),
|
|
825
|
-
maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS),
|
|
826
|
-
maxRows: z.number().step(1).min(1).default(100),
|
|
827
|
-
maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS),
|
|
828
|
-
readonly: z.boolean().default(false),
|
|
829
|
-
clients: clientsSchema
|
|
830
|
-
});
|
|
831
|
-
/** One-line tool-call label (newlines collapsed). */
|
|
832
|
-
function oneLine(sql) {
|
|
833
|
-
const line = sql.replace(/\s+/g, " ").trim();
|
|
834
|
-
return line.length > 80 ? `${line.slice(0, 77)}...` : line;
|
|
835
|
-
}
|
|
836
|
-
/** Format the raw terminal result. */
|
|
837
|
-
function formatResult(value) {
|
|
838
|
-
const parts = [];
|
|
839
|
-
if (value.stdout.length > 0) parts.push(value.stdout);
|
|
840
|
-
if (value.stderr.length > 0) parts.push(`[stderr]\n${value.stderr}`);
|
|
841
|
-
if (value.truncated) parts.push("… 输出超过上限,已截断(可缩小查询或增加 maxResultChars)");
|
|
842
|
-
if (value.exitCode !== 0) parts.push(`[exit code: ${value.exitCode ?? "signal"}]`);
|
|
843
|
-
return parts.join("\n");
|
|
844
|
-
}
|
|
845
|
-
/** Format the structured result as JSON text (the canonical value stays JSON). */
|
|
846
|
-
function formatStructuredResult(value) {
|
|
847
|
-
return "```json\n" + JSON.stringify(value, null, 2) + "\n```";
|
|
848
|
-
}
|
|
849
|
-
/** Empty and multi-statement checks shared by the write/raw tools. */
|
|
850
|
-
function validateSingleSql(sql, toolName) {
|
|
851
|
-
if (sql.trim().length === 0) throw new Error(toolName + ": sql 不能为空");
|
|
852
|
-
assertSingleStatement(sql, toolName);
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* The Web-only render-analysis tool (D1-D5): one call builds one versioned
|
|
856
|
-
* analysis report from 1-6 read-only datasets and 1-8 views. The full report
|
|
857
|
-
* is persisted as presentationMeta; the model only receives a short summary
|
|
858
|
-
* (output.render), never the rows themselves.
|
|
859
|
-
*/
|
|
860
|
-
function defineRenderAnalysisTool(ctx, resolved) {
|
|
861
|
-
return defineTool({
|
|
862
|
-
name: "render-analysis",
|
|
863
|
-
description: "Web only: render one versioned analysis report (v1) from 1-6 read-only datasets and 1-8 metric, line, bar, pie, scatter, or table views. First use sql-query to inspect and verify data, then call this tool only when visualization adds value. Use one primary chart for a simple relationship or 3-6 complementary views for multi-metric, time-series, or segmented analysis. Put aggregation, Top N, and sorting in SQL, and add ORDER BY for line or time datasets. Reuse a dataset across views via datasetId; each dataset runs once. Arbitrary chart options, scripts, HTML, CSS, and URLs are not accepted. Empty datasets are valid and render as no-data states.",
|
|
864
|
-
parameters: RENDER_ANALYSIS_PARAMETERS,
|
|
865
|
-
output: {
|
|
866
|
-
schema: ANALYSIS_REPORT_OUTPUT_SCHEMA,
|
|
867
|
-
render: (_args, value) => [{
|
|
868
|
-
type: "text",
|
|
869
|
-
text: formatAnalysisSummary(value)
|
|
870
|
-
}],
|
|
871
|
-
presentationMeta: (_args, value) => value
|
|
872
|
-
},
|
|
873
|
-
presentCall: (args) => ({
|
|
874
|
-
card: "generic",
|
|
875
|
-
kind: "read",
|
|
876
|
-
title: "render-analysis《" + args.title + "》",
|
|
877
|
-
rawInput: args.title
|
|
878
|
-
}),
|
|
879
|
-
presentResult: (args, result) => ({
|
|
880
|
-
card: "generic",
|
|
881
|
-
title: "render-analysis《" + args.title + "》",
|
|
882
|
-
content: result.content
|
|
883
|
-
}),
|
|
884
|
-
async execute(args, exec) {
|
|
885
|
-
const request = parseAnalysisRequest(args);
|
|
886
|
-
const connection = await requireToolConnection(ctx, exec, "render-analysis");
|
|
887
|
-
const planned = request.datasets.map((dataset) => {
|
|
888
|
-
const sql = dataset.sql;
|
|
889
|
-
if (sql.trim().length === 0) throw new Error("render-analysis: dataset \"" + dataset.id + "\" 的 sql 不能为空");
|
|
890
|
-
if (sql.length > resolved.maxQueryChars) throw new Error("render-analysis: dataset \"" + dataset.id + "\" 的 sql 超过长度上限(" + resolved.maxQueryChars + " 字符)");
|
|
891
|
-
assertSingleStatement(sql, "render-analysis");
|
|
892
|
-
if (classifyStatement(sql, connection.type) !== "read") throw new Error("render-analysis: dataset \"" + dataset.id + "\" 必须是读语句(SELECT/SHOW/DESCRIBE/EXPLAIN,SQLite 还含查询型 PRAGMA)");
|
|
893
|
-
return {
|
|
894
|
-
id: dataset.id,
|
|
895
|
-
sql: enforceReadRowLimit(sql, connection.type, resolved.maxRows)
|
|
896
|
-
};
|
|
897
|
-
});
|
|
898
|
-
const results = /* @__PURE__ */ new Map();
|
|
899
|
-
for (const item of planned) {
|
|
900
|
-
let read;
|
|
901
|
-
try {
|
|
902
|
-
read = await runStructuredReadQuery(ctx, connection, item.sql, resolved, "render-analysis", exec.signal);
|
|
903
|
-
} catch (error) {
|
|
904
|
-
if (exec.signal.aborted) throw error;
|
|
905
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
906
|
-
throw new Error("render-analysis: dataset \"" + item.id + "\" 执行失败:" + message);
|
|
907
|
-
}
|
|
908
|
-
if (read.truncated) throw new Error("render-analysis: dataset \"" + item.id + "\" 的查询结果被截断(超过 maxRows/maxResultChars);请缩小、聚合或拆分查询");
|
|
909
|
-
results.set(item.id, {
|
|
910
|
-
columns: read.columns,
|
|
911
|
-
rows: read.rows
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
validateViewSemantics(request.views, results);
|
|
915
|
-
const report = {
|
|
916
|
-
version: 1,
|
|
917
|
-
title: request.title,
|
|
918
|
-
...request.summary !== void 0 ? { summary: request.summary } : {},
|
|
919
|
-
datasets: request.datasets.map((dataset) => {
|
|
920
|
-
const data = results.get(dataset.id);
|
|
921
|
-
return {
|
|
922
|
-
id: dataset.id,
|
|
923
|
-
columns: data.columns,
|
|
924
|
-
rows: rowsToArrays(data.columns, data.rows)
|
|
925
|
-
};
|
|
926
|
-
}),
|
|
927
|
-
views: request.views
|
|
928
|
-
};
|
|
929
|
-
const bytes = reportJsonBytes(report);
|
|
930
|
-
if (bytes > 524288) throw new Error("render-analysis: 报告 JSON 超过 524288 字节上限(当前 " + bytes + " 字节);请聚合、筛选或拆分报告,不得静默删减数据");
|
|
931
|
-
return report;
|
|
932
|
-
}
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Mount the data-agent database tools: `sql-query` (structured read-only),
|
|
937
|
-
* `sql-write` (explicit write semantics), and `sql-cmd` (raw compatibility).
|
|
938
|
-
* @param ctx - the preset-scoped agent context.
|
|
939
|
-
* @param config - validated loader configuration.
|
|
940
|
-
*/
|
|
941
|
-
function apply(ctx, config) {
|
|
942
|
-
const resolved = {
|
|
943
|
-
queryTimeoutMs: config.queryTimeoutMs,
|
|
944
|
-
maxResultChars: config.maxResultChars,
|
|
945
|
-
maxRows: config.maxRows,
|
|
946
|
-
maxQueryChars: config.maxQueryChars,
|
|
947
|
-
readonly: config.readonly,
|
|
948
|
-
clients: config.clients
|
|
949
|
-
};
|
|
950
|
-
ctx.tools.register(defineTool({
|
|
951
|
-
name: "sql-query",
|
|
952
|
-
description: `Execute exactly one read-only SQL statement (SELECT, SHOW, DESCRIBE, EXPLAIN, or a read-only SQLite PRAGMA) on the connected database. Returns structured JSON with columns, rows, affectedRows, elapsedMs, and truncated. An unbounded SELECT is limited automatically, and every result is capped at ${resolved.maxRows} rows. Use sql-write for write operations and sql-cmd when raw database-client output is required.`,
|
|
953
|
-
parameters: { sql: {
|
|
954
|
-
type: "string",
|
|
955
|
-
required: true,
|
|
956
|
-
description: "一条只读 SQL,如 \"SELECT * FROM orders LIMIT 5;\"、\"SHOW TABLES;\"、\"DESCRIBE users;\""
|
|
957
|
-
} },
|
|
958
|
-
output: {
|
|
959
|
-
schema: {
|
|
960
|
-
type: "object",
|
|
961
|
-
properties: {
|
|
962
|
-
columns: {
|
|
963
|
-
type: "array",
|
|
964
|
-
items: { type: "string" },
|
|
965
|
-
required: true
|
|
966
|
-
},
|
|
967
|
-
rows: {
|
|
968
|
-
type: "array",
|
|
969
|
-
items: {
|
|
970
|
-
type: "object",
|
|
971
|
-
properties: {},
|
|
972
|
-
additionalProperties: true
|
|
973
|
-
},
|
|
974
|
-
required: true
|
|
975
|
-
},
|
|
976
|
-
affectedRows: {
|
|
977
|
-
type: "integer",
|
|
978
|
-
required: true
|
|
979
|
-
},
|
|
980
|
-
elapsedMs: {
|
|
981
|
-
type: "integer",
|
|
982
|
-
required: true
|
|
983
|
-
},
|
|
984
|
-
truncated: {
|
|
985
|
-
type: "boolean",
|
|
986
|
-
required: true
|
|
987
|
-
}
|
|
988
|
-
},
|
|
989
|
-
additionalProperties: false
|
|
990
|
-
},
|
|
991
|
-
render: (_args, value) => [{
|
|
992
|
-
type: "text",
|
|
993
|
-
text: formatStructuredResult(value)
|
|
994
|
-
}]
|
|
995
|
-
},
|
|
996
|
-
presentCall: (args) => ({
|
|
997
|
-
card: "generic",
|
|
998
|
-
kind: "read",
|
|
999
|
-
title: `sql-query ${oneLine(args.sql)}`,
|
|
1000
|
-
rawInput: args.sql
|
|
1001
|
-
}),
|
|
1002
|
-
presentResult: (args, result) => ({
|
|
1003
|
-
card: "generic",
|
|
1004
|
-
title: `sql-query ${oneLine(args.sql)}`,
|
|
1005
|
-
content: result.content
|
|
1006
|
-
}),
|
|
1007
|
-
async execute(args, exec) {
|
|
1008
|
-
const read = await runStructuredReadQuery(ctx, await requireToolConnection(ctx, exec, "sql-query"), args.sql, resolved, "sql-query", exec.signal);
|
|
1009
|
-
return {
|
|
1010
|
-
columns: read.columns,
|
|
1011
|
-
rows: read.rows,
|
|
1012
|
-
affectedRows: 0,
|
|
1013
|
-
elapsedMs: read.elapsedMs,
|
|
1014
|
-
truncated: read.truncated
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
}));
|
|
1018
|
-
ctx.tools.register(defineTool({
|
|
1019
|
-
name: "sql-write",
|
|
1020
|
-
description: "Execute exactly one write or administrative SQL statement, such as INSERT, UPDATE, DELETE, or DDL, on the connected database. Each call starts an independent database-client process and auto-commits. Multi-statement transactions cannot span calls; use one atomic statement such as INSERT ... SELECT, or a database-side script or stored procedure. Use sql-query for read-only queries.",
|
|
1021
|
-
parameters: { sql: {
|
|
1022
|
-
type: "string",
|
|
1023
|
-
required: true,
|
|
1024
|
-
description: "一条写/管理 SQL,如 \"INSERT INTO t VALUES (1);\"、\"UPDATE t SET x=1;\"、\"CREATE INDEX idx_t_x ON t(x);\""
|
|
1025
|
-
} },
|
|
1026
|
-
output: {
|
|
1027
|
-
schema: {
|
|
1028
|
-
type: "object",
|
|
1029
|
-
properties: {
|
|
1030
|
-
exitCode: {
|
|
1031
|
-
oneOf: [{ type: "integer" }, { type: "null" }],
|
|
1032
|
-
required: true
|
|
1033
|
-
},
|
|
1034
|
-
stdout: {
|
|
1035
|
-
type: "string",
|
|
1036
|
-
required: true
|
|
1037
|
-
},
|
|
1038
|
-
stderr: {
|
|
1039
|
-
type: "string",
|
|
1040
|
-
required: true
|
|
1041
|
-
},
|
|
1042
|
-
truncated: {
|
|
1043
|
-
type: "boolean",
|
|
1044
|
-
required: true
|
|
1045
|
-
}
|
|
1046
|
-
},
|
|
1047
|
-
additionalProperties: false
|
|
1048
|
-
},
|
|
1049
|
-
render: (_args, value) => [{
|
|
1050
|
-
type: "text",
|
|
1051
|
-
text: formatResult(value)
|
|
1052
|
-
}]
|
|
1053
|
-
},
|
|
1054
|
-
presentCall: (args) => ({
|
|
1055
|
-
card: "terminal",
|
|
1056
|
-
title: `sql-write ${oneLine(args.sql)}`,
|
|
1057
|
-
description: "执行一条写/管理 SQL(自动提交)"
|
|
1058
|
-
}),
|
|
1059
|
-
presentResult: (args, result) => ({
|
|
1060
|
-
card: "terminal",
|
|
1061
|
-
title: `sql-write ${oneLine(args.sql)}`,
|
|
1062
|
-
content: result.content
|
|
1063
|
-
}),
|
|
1064
|
-
async execute(args, exec) {
|
|
1065
|
-
const connection = await requireToolConnection(ctx, exec, "sql-write");
|
|
1066
|
-
validateSingleSql(args.sql, "sql-write");
|
|
1067
|
-
if (classifyStatement(args.sql, connection.type) === "read") throw new Error("sql-write 只执行写/管理语句;只读查询请使用 sql-query");
|
|
1068
|
-
if (connection.readonly ?? resolved.readonly) throw new Error("当前连接为只读模式,sql-write 拒绝执行写/管理语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/查询型 PRAGMA 等)");
|
|
1069
|
-
return runRedactedClientQuery(ctx, connection, args.sql, runnerOptions(resolved), exec.signal);
|
|
1070
|
-
}
|
|
1071
|
-
}));
|
|
1072
|
-
ctx.tools.register(defineTool({
|
|
1073
|
-
name: "sql-cmd",
|
|
1074
|
-
description: `Execute exactly one SQL statement or database-client command, such as SHOW TABLES or DESCRIBE users, on the connected database and return raw exitCode, stdout, stderr, and truncated fields. Prefer sql-query for structured read results and sql-write for explicit write semantics. Read SELECT results are limited to ${resolved.maxRows} rows. Each call starts an independent database-client process and auto-commits.`,
|
|
1075
|
-
parameters: { sql: {
|
|
1076
|
-
type: "string",
|
|
1077
|
-
required: true,
|
|
1078
|
-
description: "一条 SQL 文本(或客户端命令),如 \"SHOW TABLES;\"、\"DESCRIBE users;\"、\"SELECT * FROM orders LIMIT 5;\""
|
|
1079
|
-
} },
|
|
1080
|
-
output: {
|
|
1081
|
-
schema: {
|
|
1082
|
-
type: "object",
|
|
1083
|
-
properties: {
|
|
1084
|
-
exitCode: {
|
|
1085
|
-
oneOf: [{ type: "integer" }, { type: "null" }],
|
|
1086
|
-
required: true
|
|
1087
|
-
},
|
|
1088
|
-
stdout: {
|
|
1089
|
-
type: "string",
|
|
1090
|
-
required: true
|
|
1091
|
-
},
|
|
1092
|
-
stderr: {
|
|
1093
|
-
type: "string",
|
|
1094
|
-
required: true
|
|
1095
|
-
},
|
|
1096
|
-
truncated: {
|
|
1097
|
-
type: "boolean",
|
|
1098
|
-
required: true
|
|
1099
|
-
}
|
|
1100
|
-
},
|
|
1101
|
-
additionalProperties: false
|
|
1102
|
-
},
|
|
1103
|
-
render: (_args, value) => [{
|
|
1104
|
-
type: "text",
|
|
1105
|
-
text: formatResult(value)
|
|
1106
|
-
}]
|
|
1107
|
-
},
|
|
1108
|
-
presentCall: (args) => ({
|
|
1109
|
-
card: "terminal",
|
|
1110
|
-
title: `sql-cmd ${oneLine(args.sql)}`,
|
|
1111
|
-
description: "在数据库客户端执行一条 SQL"
|
|
1112
|
-
}),
|
|
1113
|
-
presentResult: (args, result) => ({
|
|
1114
|
-
card: "terminal",
|
|
1115
|
-
title: `sql-cmd ${oneLine(args.sql)}`,
|
|
1116
|
-
content: result.content
|
|
1117
|
-
}),
|
|
1118
|
-
async execute(args, exec) {
|
|
1119
|
-
const connection = await requireToolConnection(ctx, exec, "sql-cmd");
|
|
1120
|
-
validateSingleSql(args.sql, "sql-cmd");
|
|
1121
|
-
if ((connection.readonly ?? resolved.readonly) && classifyStatement(args.sql, connection.type) === "write") throw new Error("当前连接为只读模式,sql-cmd 拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/查询型 PRAGMA 等)");
|
|
1122
|
-
return runRedactedClientQuery(ctx, connection, classifyStatement(args.sql, connection.type) === "read" ? enforceReadRowLimit(args.sql, connection.type, resolved.maxRows) : args.sql, runnerOptions(resolved), exec.signal);
|
|
1123
|
-
}
|
|
1124
|
-
}));
|
|
1125
|
-
if (ctx.get("webServer") !== void 0) ctx.tools.register(defineRenderAnalysisTool(ctx, resolved));
|
|
1126
|
-
}
|
|
1127
|
-
//#endregion
|
|
1
|
+
import { i as name, n as apply, r as inject, t as Config } from "./tool-DVh61An-.js";
|
|
1128
2
|
export { Config, apply, inject, name };
|