@yejiming/dsh-data-agent 0.0.5 → 0.0.9
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 +127 -128
- package/README.md +123 -127
- package/lib/client.js +42071 -114
- package/lib/client.js.map +1 -1
- package/lib/command.js +875 -0
- package/lib/{defaults-Bac6QvNt.js → connections-DeauhaZi.js} +427 -19
- package/lib/defaults-DP4RyRh1.js +21 -0
- package/lib/index.js +185 -51
- package/lib/routes.js +94 -170
- package/lib/tool.js +751 -50
- package/lib/types/analysis.d.ts +1071 -0
- package/lib/types/client/AnalysisChart.d.ts +26 -0
- package/lib/types/client/AnalysisDashboard.d.ts +30 -0
- package/lib/types/client/analysis-charts.d.ts +40 -0
- package/lib/types/client/analysis-view-model.d.ts +44 -0
- package/lib/types/client/locales.d.ts +48 -0
- package/lib/types/client/persistence.d.ts +6 -1
- package/lib/types/clients.d.ts +10 -9
- package/lib/types/command.d.ts +41 -0
- package/lib/types/connections.d.ts +115 -40
- package/lib/types/index.d.ts +67 -47
- package/lib/types/routes.d.ts +25 -77
- package/lib/types/sql.d.ts +1 -1
- package/lib/types/storage.d.ts +70 -0
- package/lib/types/structured-read.d.ts +50 -0
- package/lib/types/tool.d.ts +25 -20
- package/lib/types/tui-connection-form.d.ts +98 -0
- package/package.json +61 -4
- package/preset/data-agent/agent.cordis.yml +34 -18
- package/preset/data-agent/preset.yml +2 -2
- package/lib/query-CmhTFklw.js +0 -86
package/lib/tool.js
CHANGED
|
@@ -1,7 +1,595 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { a as classifyStatement, c as assertSingleStatement, i as runClientQuery, n as redactQueryResult, o as clientsSchema, r as redactSecretText, s as enforceReadRowLimit } from "./connections-DeauhaZi.js";
|
|
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
3
|
import z from "schemastery";
|
|
4
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
|
|
5
593
|
//#region src/structured.ts
|
|
6
594
|
function normalizeNewlines(text) {
|
|
7
595
|
return text.replace(/\r\n?/g, "\n");
|
|
@@ -164,6 +752,64 @@ function parseStructuredQueryOutput(type, stdout, maxRows) {
|
|
|
164
752
|
}
|
|
165
753
|
}
|
|
166
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
|
|
167
813
|
//#region src/tool.ts
|
|
168
814
|
/** Cordis plugin name (diagnostics only). */
|
|
169
815
|
const name = "data-agent-tool";
|
|
@@ -178,6 +824,7 @@ const Config = z.object({
|
|
|
178
824
|
queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS),
|
|
179
825
|
maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS),
|
|
180
826
|
maxRows: z.number().step(1).min(1).default(100),
|
|
827
|
+
maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS),
|
|
181
828
|
readonly: z.boolean().default(false),
|
|
182
829
|
clients: clientsSchema
|
|
183
830
|
});
|
|
@@ -199,31 +846,95 @@ function formatResult(value) {
|
|
|
199
846
|
function formatStructuredResult(value) {
|
|
200
847
|
return "```json\n" + JSON.stringify(value, null, 2) + "\n```";
|
|
201
848
|
}
|
|
202
|
-
/**
|
|
203
|
-
function requireToolConnection(ctx, exec, toolName) {
|
|
204
|
-
const sessionId = exec.agent?.id;
|
|
205
|
-
if (sessionId === void 0) throw new Error(`${toolName}: 缺少会话上下文(agent loop 未注入)`);
|
|
206
|
-
const connection = ctx.dataAgentConnections.getWithSecret(sessionId);
|
|
207
|
-
if (connection === void 0) throw new Error(`请先在「数据库」标签页连接数据库,再使用 ${toolName}(未找到当前会话的连接)`);
|
|
208
|
-
return connection;
|
|
209
|
-
}
|
|
210
|
-
/** Empty and multi-statement checks shared by all three tools. */
|
|
849
|
+
/** Empty and multi-statement checks shared by the write/raw tools. */
|
|
211
850
|
function validateSingleSql(sql, toolName) {
|
|
212
|
-
if (sql.trim().length === 0) throw new Error(
|
|
851
|
+
if (sql.trim().length === 0) throw new Error(toolName + ": sql 不能为空");
|
|
213
852
|
assertSingleStatement(sql, toolName);
|
|
214
853
|
}
|
|
215
|
-
/**
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
});
|
|
223
934
|
}
|
|
224
935
|
/**
|
|
225
936
|
* Mount the data-agent database tools: `sql-query` (structured read-only),
|
|
226
|
-
* `sql-write` (explicit write semantics), and `
|
|
937
|
+
* `sql-write` (explicit write semantics), and `sql-cmd` (raw compatibility).
|
|
227
938
|
* @param ctx - the preset-scoped agent context.
|
|
228
939
|
* @param config - validated loader configuration.
|
|
229
940
|
*/
|
|
@@ -232,12 +943,13 @@ function apply(ctx, config) {
|
|
|
232
943
|
queryTimeoutMs: config.queryTimeoutMs,
|
|
233
944
|
maxResultChars: config.maxResultChars,
|
|
234
945
|
maxRows: config.maxRows,
|
|
946
|
+
maxQueryChars: config.maxQueryChars,
|
|
235
947
|
readonly: config.readonly,
|
|
236
948
|
clients: config.clients
|
|
237
949
|
};
|
|
238
950
|
ctx.tools.register(defineTool({
|
|
239
951
|
name: "sql-query",
|
|
240
|
-
description:
|
|
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.`,
|
|
241
953
|
parameters: { sql: {
|
|
242
954
|
type: "string",
|
|
243
955
|
required: true,
|
|
@@ -293,30 +1005,19 @@ function apply(ctx, config) {
|
|
|
293
1005
|
content: result.content
|
|
294
1006
|
}),
|
|
295
1007
|
async execute(args, exec) {
|
|
296
|
-
const
|
|
297
|
-
validateSingleSql(args.sql, "sql-query");
|
|
298
|
-
if (classifyStatement(args.sql, connection.type) !== "read") throw new Error("sql-query 只执行读语句(SELECT/SHOW/DESCRIBE/EXPLAIN,SQLite 还含查询型 PRAGMA);写语句请使用 sql-write");
|
|
299
|
-
const limitedSql = enforceReadRowLimit(args.sql, connection.type, resolved.maxRows);
|
|
300
|
-
const startedAt = Date.now();
|
|
301
|
-
const result = await runClientQuery(ctx, connection, limitedSql, runnerOptions(resolved, "structured"), exec.signal);
|
|
302
|
-
const elapsedMs = Date.now() - startedAt;
|
|
303
|
-
if (result.exitCode !== 0) {
|
|
304
|
-
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
305
|
-
throw new Error(`sql-query 执行失败(exit ${result.exitCode}):${detail}`);
|
|
306
|
-
}
|
|
307
|
-
const parsed = parseStructuredQueryOutput(connection.type, result.stdout, resolved.maxRows);
|
|
1008
|
+
const read = await runStructuredReadQuery(ctx, await requireToolConnection(ctx, exec, "sql-query"), args.sql, resolved, "sql-query", exec.signal);
|
|
308
1009
|
return {
|
|
309
|
-
columns:
|
|
310
|
-
rows:
|
|
1010
|
+
columns: read.columns,
|
|
1011
|
+
rows: read.rows,
|
|
311
1012
|
affectedRows: 0,
|
|
312
|
-
elapsedMs,
|
|
313
|
-
truncated:
|
|
1013
|
+
elapsedMs: read.elapsedMs,
|
|
1014
|
+
truncated: read.truncated
|
|
314
1015
|
};
|
|
315
1016
|
}
|
|
316
1017
|
}));
|
|
317
1018
|
ctx.tools.register(defineTool({
|
|
318
1019
|
name: "sql-write",
|
|
319
|
-
description: "
|
|
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.",
|
|
320
1021
|
parameters: { sql: {
|
|
321
1022
|
type: "string",
|
|
322
1023
|
required: true,
|
|
@@ -361,16 +1062,16 @@ function apply(ctx, config) {
|
|
|
361
1062
|
content: result.content
|
|
362
1063
|
}),
|
|
363
1064
|
async execute(args, exec) {
|
|
364
|
-
const connection = requireToolConnection(ctx, exec, "sql-write");
|
|
1065
|
+
const connection = await requireToolConnection(ctx, exec, "sql-write");
|
|
365
1066
|
validateSingleSql(args.sql, "sql-write");
|
|
366
1067
|
if (classifyStatement(args.sql, connection.type) === "read") throw new Error("sql-write 只执行写/管理语句;只读查询请使用 sql-query");
|
|
367
1068
|
if (connection.readonly ?? resolved.readonly) throw new Error("当前连接为只读模式,sql-write 拒绝执行写/管理语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/查询型 PRAGMA 等)");
|
|
368
|
-
return
|
|
1069
|
+
return runRedactedClientQuery(ctx, connection, args.sql, runnerOptions(resolved), exec.signal);
|
|
369
1070
|
}
|
|
370
1071
|
}));
|
|
371
1072
|
ctx.tools.register(defineTool({
|
|
372
|
-
name: "
|
|
373
|
-
description:
|
|
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.`,
|
|
374
1075
|
parameters: { sql: {
|
|
375
1076
|
type: "string",
|
|
376
1077
|
required: true,
|
|
@@ -406,22 +1107,22 @@ function apply(ctx, config) {
|
|
|
406
1107
|
},
|
|
407
1108
|
presentCall: (args) => ({
|
|
408
1109
|
card: "terminal",
|
|
409
|
-
title: `
|
|
1110
|
+
title: `sql-cmd ${oneLine(args.sql)}`,
|
|
410
1111
|
description: "在数据库客户端执行一条 SQL"
|
|
411
1112
|
}),
|
|
412
1113
|
presentResult: (args, result) => ({
|
|
413
1114
|
card: "terminal",
|
|
414
|
-
title: `
|
|
1115
|
+
title: `sql-cmd ${oneLine(args.sql)}`,
|
|
415
1116
|
content: result.content
|
|
416
1117
|
}),
|
|
417
1118
|
async execute(args, exec) {
|
|
418
|
-
const connection = requireToolConnection(ctx, exec, "
|
|
419
|
-
validateSingleSql(args.sql, "
|
|
420
|
-
if ((connection.readonly ?? resolved.readonly) && classifyStatement(args.sql, connection.type) === "write") throw new Error("当前连接为只读模式,
|
|
421
|
-
|
|
422
|
-
return runClientQuery(ctx, connection, sql, runnerOptions(resolved), exec.signal);
|
|
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);
|
|
423
1123
|
}
|
|
424
1124
|
}));
|
|
1125
|
+
if (ctx.get("webServer") !== void 0) ctx.tools.register(defineRenderAnalysisTool(ctx, resolved));
|
|
425
1126
|
}
|
|
426
1127
|
//#endregion
|
|
427
1128
|
export { Config, apply, inject, name };
|