@yejiming/dsh-data-agent 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1128 @@
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-WmjuUrDj.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
+ 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
1128
+ export { name as i, apply as n, inject as r, Config as t };