dsh-sql 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +17 -2
- package/README.md +17 -2
- package/cordis.patch.yml +2 -0
- package/lib/config.d.ts +4 -0
- package/lib/config.js +13 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/tools.d.ts +3 -1
- package/lib/tools.js +214 -27
- package/package.json +67 -68
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stardustlc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
CHANGED
|
@@ -10,12 +10,25 @@ DSH (DeepSeek Harness) engineer-grade database plugin: four tools covering conne
|
|
|
10
10
|
|
|
11
11
|
[](https://awesome-dsh-plugin.com)
|
|
12
12
|
|
|
13
|
+
## Compatibility
|
|
14
|
+
|
|
15
|
+
Verified against `@deepseek-ai/dsh@0.1.1-rc.2` on 2026-08-26. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
|
|
16
|
+
|
|
13
17
|
## Installation
|
|
14
18
|
|
|
15
19
|
```bash
|
|
16
20
|
dsh plugin --profile web add dsh-sql
|
|
17
21
|
```
|
|
18
22
|
|
|
23
|
+
## Uninstall
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh plugin --profile web remove dsh-sql
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
30
|
+
|
|
31
|
+
|
|
19
32
|
## Configuration
|
|
20
33
|
|
|
21
34
|
```yaml
|
|
@@ -38,6 +51,8 @@ dsh plugin --profile web add dsh-sql
|
|
|
38
51
|
user: root
|
|
39
52
|
database: legacy
|
|
40
53
|
maxRows: 1000 # query row cap (1-10000)
|
|
54
|
+
queryTimeoutMs: 60000 # per-query timeout (default 60s, 5s - 10min)
|
|
55
|
+
execTimeoutMs: 120000 # per-write timeout (default 120s, 5s - 10min)
|
|
41
56
|
readOnly: false # true disables sql_exec
|
|
42
57
|
writeApproval: true # approve write operations first (default true)
|
|
43
58
|
```
|
|
@@ -80,9 +95,9 @@ sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
|
80
95
|
|
|
81
96
|
```bash
|
|
82
97
|
pnpm install
|
|
83
|
-
pnpm test # build +
|
|
98
|
+
pnpm test # build + 35 tests, including a real SQLite integration suite
|
|
84
99
|
```
|
|
85
100
|
|
|
86
101
|
## License
|
|
87
102
|
|
|
88
|
-
MIT
|
|
103
|
+
MIT
|
package/README.md
CHANGED
|
@@ -10,12 +10,25 @@ DSH(DeepSeek Harness)工程师级数据库插件:四个工具覆盖连接
|
|
|
10
10
|
|
|
11
11
|
[](https://awesome-dsh-plugin.com)
|
|
12
12
|
|
|
13
|
+
## 兼容性
|
|
14
|
+
|
|
15
|
+
在 `@deepseek-ai/dsh@0.1.1-rc.2` 上验证(2026-08-26)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
|
|
16
|
+
|
|
13
17
|
## 安装
|
|
14
18
|
|
|
15
19
|
```bash
|
|
16
20
|
dsh plugin --profile web add dsh-sql
|
|
17
21
|
```
|
|
18
22
|
|
|
23
|
+
## 卸载
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
dsh plugin --profile web remove dsh-sql
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
|
|
30
|
+
|
|
31
|
+
|
|
19
32
|
## 配置
|
|
20
33
|
|
|
21
34
|
```yaml
|
|
@@ -38,6 +51,8 @@ dsh plugin --profile web add dsh-sql
|
|
|
38
51
|
user: root
|
|
39
52
|
database: legacy
|
|
40
53
|
maxRows: 1000 # 查询返回行数上限(1-10000)
|
|
54
|
+
queryTimeoutMs: 60000 # 单次查询超时(默认 60 秒,5 秒 - 10 分钟)
|
|
55
|
+
execTimeoutMs: 120000 # 单次写操作超时(默认 120 秒,5 秒 - 10 分钟)
|
|
41
56
|
readOnly: false # true 时禁用 sql_exec
|
|
42
57
|
writeApproval: true # 写操作先弹审批(默认 true)
|
|
43
58
|
```
|
|
@@ -80,9 +95,9 @@ sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
|
80
95
|
|
|
81
96
|
```bash
|
|
82
97
|
pnpm install
|
|
83
|
-
pnpm test # 构建 +
|
|
98
|
+
pnpm test # 构建 + 35 个测试(含真实 SQLite 集成)
|
|
84
99
|
```
|
|
85
100
|
|
|
86
101
|
## License
|
|
87
102
|
|
|
88
|
-
MIT
|
|
103
|
+
MIT
|
package/cordis.patch.yml
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
# engine: sqlite
|
|
13
13
|
# file: E:\data\app.db
|
|
14
14
|
# maxRows: 1000 # sql_query 返回行数上限(1-10000,默认 1000)
|
|
15
|
+
# queryTimeoutMs: 60000 # 单次查询超时(5 秒 - 10 分钟)
|
|
16
|
+
# execTimeoutMs: 120000 # 单次写操作超时(5 秒 - 10 分钟)
|
|
15
17
|
# readOnly: false # true 时 sql_exec 被禁用
|
|
16
18
|
# writeApproval: true # 写操作先弹审批(默认 true)
|
|
17
19
|
#
|
package/lib/config.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface SqlConfig {
|
|
|
20
20
|
maxRows?: number;
|
|
21
21
|
readOnly?: boolean;
|
|
22
22
|
writeApproval?: boolean;
|
|
23
|
+
queryTimeoutMs?: number;
|
|
24
|
+
execTimeoutMs?: number;
|
|
23
25
|
}
|
|
24
26
|
/** 解析后的配置。 */
|
|
25
27
|
export interface ResolvedSqlConfig {
|
|
@@ -27,6 +29,8 @@ export interface ResolvedSqlConfig {
|
|
|
27
29
|
maxRows: number;
|
|
28
30
|
readOnly: boolean;
|
|
29
31
|
writeApproval: boolean;
|
|
32
|
+
queryTimeoutMs: number;
|
|
33
|
+
execTimeoutMs: number;
|
|
30
34
|
}
|
|
31
35
|
/** 连接密码环境变量名:DSH_SQL_PASSWORD_<NAME 大写>。 */
|
|
32
36
|
export declare function passwordEnvName(name: string): string;
|
package/lib/config.js
CHANGED
|
@@ -53,9 +53,21 @@ export function resolveConfig(config, env = process.env) {
|
|
|
53
53
|
throw new Error('maxRows 必须是大于 0 的整数。');
|
|
54
54
|
maxRows = Math.min(10000, cfg.maxRows);
|
|
55
55
|
}
|
|
56
|
+
let queryTimeoutMs = 60000;
|
|
57
|
+
if (cfg.queryTimeoutMs !== undefined) {
|
|
58
|
+
if (typeof cfg.queryTimeoutMs !== 'number' || !Number.isFinite(cfg.queryTimeoutMs) || cfg.queryTimeoutMs <= 0)
|
|
59
|
+
throw new Error('queryTimeoutMs 必须是大于 0 的数字(毫秒)。');
|
|
60
|
+
queryTimeoutMs = Math.min(600000, Math.max(5000, Math.round(cfg.queryTimeoutMs)));
|
|
61
|
+
}
|
|
62
|
+
let execTimeoutMs = 120000;
|
|
63
|
+
if (cfg.execTimeoutMs !== undefined) {
|
|
64
|
+
if (typeof cfg.execTimeoutMs !== 'number' || !Number.isFinite(cfg.execTimeoutMs) || cfg.execTimeoutMs <= 0)
|
|
65
|
+
throw new Error('execTimeoutMs 必须是大于 0 的数字(毫秒)。');
|
|
66
|
+
execTimeoutMs = Math.min(600000, Math.max(5000, Math.round(cfg.execTimeoutMs)));
|
|
67
|
+
}
|
|
56
68
|
const readOnly = cfg.readOnly === true;
|
|
57
69
|
const writeApproval = cfg.writeApproval !== false;
|
|
58
|
-
return { connections, maxRows, readOnly, writeApproval };
|
|
70
|
+
return { connections, maxRows, readOnly, writeApproval, queryTimeoutMs, execTimeoutMs };
|
|
59
71
|
}
|
|
60
72
|
/** 校验表名/标识符,防注入到 schema 语句。 */
|
|
61
73
|
export function assertIdentifier(name, label) {
|
package/lib/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { type SqlConfig } from './config.js';
|
|
11
11
|
import { type SqlToolDefinition } from './tools.js';
|
|
12
12
|
/** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明。 */
|
|
13
|
+
export declare const name = "sql";
|
|
13
14
|
export declare const inject: string[];
|
|
14
15
|
/** 审批服务最小面(对齐 dsh-email)。 */
|
|
15
16
|
export interface SqlApproval {
|
package/lib/index.js
CHANGED
package/lib/tools.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 六个面向模型的数据库工具:sql_list / sql_query / sql_exec / sql_schema / sql_stats / sql_health。
|
|
3
3
|
*
|
|
4
4
|
* @module dsh-sql/tools
|
|
5
5
|
*/
|
|
@@ -29,6 +29,8 @@ export interface SqlToolDefinition {
|
|
|
29
29
|
}
|
|
30
30
|
/** 校验只读查询:词法去噪后白名单开头 + 写关键字扫描 + 单语句。 */
|
|
31
31
|
export declare function assertReadQuery(sql: string): string;
|
|
32
|
+
/** 查询结果转 CSV 文本(RFC 4180 风格转义)。 */
|
|
33
|
+
export declare function toCsv(columns: string[], rows: unknown[][]): string;
|
|
32
34
|
/** 审批执行上下文的最小面。 */
|
|
33
35
|
export interface SqlExecGateContext {
|
|
34
36
|
agent?: unknown;
|
package/lib/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 六个面向模型的数据库工具:sql_list / sql_query / sql_exec / sql_schema / sql_stats / sql_health。
|
|
3
3
|
*
|
|
4
4
|
* @module dsh-sql/tools
|
|
5
5
|
*/
|
|
@@ -166,6 +166,8 @@ const querySchema = {
|
|
|
166
166
|
rowCount: { type: 'integer' },
|
|
167
167
|
truncated: { type: 'boolean' },
|
|
168
168
|
maxRows: { type: 'integer' },
|
|
169
|
+
format: { type: 'string' },
|
|
170
|
+
formatted: { type: 'string' },
|
|
169
171
|
},
|
|
170
172
|
additionalProperties: true,
|
|
171
173
|
};
|
|
@@ -208,6 +210,75 @@ const schemaToolSchema = {
|
|
|
208
210
|
},
|
|
209
211
|
additionalProperties: true,
|
|
210
212
|
};
|
|
213
|
+
// ---------- 输出格式与统计辅助 ----------
|
|
214
|
+
/** 按引擎给标识符加引号,防止表名破坏 SQL。 */
|
|
215
|
+
function quoteIdent(engine, name) {
|
|
216
|
+
if (engine === 'mysql')
|
|
217
|
+
return '`' + name.replace(/`/g, '``') + '`';
|
|
218
|
+
return '"' + name.replace(/"/g, '""') + '"';
|
|
219
|
+
}
|
|
220
|
+
/** 查询结果转 CSV 文本(RFC 4180 风格转义)。 */
|
|
221
|
+
export function toCsv(columns, rows) {
|
|
222
|
+
const escape = (cell) => {
|
|
223
|
+
if (cell === null || cell === undefined)
|
|
224
|
+
return '';
|
|
225
|
+
const text = Array.isArray(cell) || typeof cell === 'object' ? JSON.stringify(cell) : String(cell);
|
|
226
|
+
return /[",\n\r]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text;
|
|
227
|
+
};
|
|
228
|
+
const lines = [columns.map(escape).join(',')];
|
|
229
|
+
for (const row of rows)
|
|
230
|
+
lines.push(row.map(escape).join(','));
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
/** 人类可读的字节数。 */
|
|
234
|
+
function formatBytes(bytes) {
|
|
235
|
+
if (bytes < 1024)
|
|
236
|
+
return bytes + ' B';
|
|
237
|
+
if (bytes < 1024 * 1024)
|
|
238
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
239
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
240
|
+
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
|
241
|
+
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
242
|
+
}
|
|
243
|
+
/** 库体积:SQLite 用页数×页大小;MySQL/PostgreSQL 走系统函数;失败抛错由调用方兜底。 */
|
|
244
|
+
async function databaseSize(adapter, engine) {
|
|
245
|
+
if (engine === 'sqlite') {
|
|
246
|
+
const pageCount = await adapter.query('PRAGMA page_count', 1);
|
|
247
|
+
const pageSize = await adapter.query('PRAGMA page_size', 1);
|
|
248
|
+
return Number(pageCount.rows[0]?.[0] ?? 0) * Number(pageSize.rows[0]?.[0] ?? 0);
|
|
249
|
+
}
|
|
250
|
+
if (engine === 'mysql') {
|
|
251
|
+
const result = await adapter.query('SELECT SUM(DATA_LENGTH + INDEX_LENGTH) FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE()', 1);
|
|
252
|
+
return Number(result.rows[0]?.[0] ?? -1);
|
|
253
|
+
}
|
|
254
|
+
const result = await adapter.query('SELECT pg_database_size(current_database())', 1);
|
|
255
|
+
return Number(result.rows[0]?.[0] ?? -1);
|
|
256
|
+
}
|
|
257
|
+
const statsSchema = {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
connection: { type: 'string' },
|
|
261
|
+
engine: { type: 'string' },
|
|
262
|
+
tableCount: { type: 'integer' },
|
|
263
|
+
tables: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
264
|
+
sizeBytes: { type: 'integer' },
|
|
265
|
+
},
|
|
266
|
+
additionalProperties: true,
|
|
267
|
+
};
|
|
268
|
+
const healthSchema = {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
ok: { type: 'boolean' },
|
|
272
|
+
plugin: { type: 'string' },
|
|
273
|
+
connections: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
274
|
+
readOnly: { type: 'boolean' },
|
|
275
|
+
writeApproval: { type: 'boolean' },
|
|
276
|
+
maxRows: { type: 'integer' },
|
|
277
|
+
queryTimeoutMs: { type: 'integer' },
|
|
278
|
+
execTimeoutMs: { type: 'integer' },
|
|
279
|
+
},
|
|
280
|
+
additionalProperties: true,
|
|
281
|
+
};
|
|
211
282
|
/** 构建四个工具定义;adapters 惰性创建并按连接名缓存。 */
|
|
212
283
|
export function buildSqlTools(config) {
|
|
213
284
|
const cfg = config;
|
|
@@ -225,6 +296,30 @@ export function buildSqlTools(config) {
|
|
|
225
296
|
}
|
|
226
297
|
return { adapter, name: connection.name };
|
|
227
298
|
};
|
|
299
|
+
const pingAllConnections = async () => {
|
|
300
|
+
const rows = [];
|
|
301
|
+
for (const connection of cfg.connections) {
|
|
302
|
+
const entry = { name: connection.name, engine: connection.engine };
|
|
303
|
+
if (connection.engine === 'sqlite')
|
|
304
|
+
entry.file = connection.file ?? ':memory:';
|
|
305
|
+
else {
|
|
306
|
+
entry.host = connection.host ?? '';
|
|
307
|
+
entry.database = connection.database ?? '';
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const { adapter } = getAdapter(connection.name);
|
|
311
|
+
await adapter.ping();
|
|
312
|
+
entry.ok = true;
|
|
313
|
+
entry.error = '';
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
entry.ok = false;
|
|
317
|
+
entry.error = error instanceof Error ? error.message : String(error);
|
|
318
|
+
}
|
|
319
|
+
rows.push(entry);
|
|
320
|
+
}
|
|
321
|
+
return rows;
|
|
322
|
+
};
|
|
228
323
|
const sqlList = {
|
|
229
324
|
name: 'sql_list',
|
|
230
325
|
description: '列出配置的数据库连接并逐一做连通性测试(SELECT 1)。返回连接名、引擎、目标与健康状态。',
|
|
@@ -244,28 +339,7 @@ export function buildSqlTools(config) {
|
|
|
244
339
|
},
|
|
245
340
|
},
|
|
246
341
|
async execute() {
|
|
247
|
-
|
|
248
|
-
for (const connection of cfg.connections) {
|
|
249
|
-
const entry = { name: connection.name, engine: connection.engine };
|
|
250
|
-
if (connection.engine === 'sqlite')
|
|
251
|
-
entry.file = connection.file ?? ':memory:';
|
|
252
|
-
else {
|
|
253
|
-
entry.host = connection.host ?? '';
|
|
254
|
-
entry.database = connection.database ?? '';
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
const { adapter } = getAdapter(connection.name);
|
|
258
|
-
await adapter.ping();
|
|
259
|
-
entry.ok = true;
|
|
260
|
-
entry.error = '';
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
entry.ok = false;
|
|
264
|
-
entry.error = error instanceof Error ? error.message : String(error);
|
|
265
|
-
}
|
|
266
|
-
rows.push(entry);
|
|
267
|
-
}
|
|
268
|
-
return { connections: rows };
|
|
342
|
+
return { connections: await pingAllConnections() };
|
|
269
343
|
},
|
|
270
344
|
timeoutMs: 30000,
|
|
271
345
|
};
|
|
@@ -275,11 +349,16 @@ export function buildSqlTools(config) {
|
|
|
275
349
|
parameters: compileParameters({
|
|
276
350
|
sql: { type: 'string', required: true, description: '只读 SQL 语句(必填,单条)。' },
|
|
277
351
|
connection: { type: 'string', description: '连接名(可选,缺省第一个连接)。' },
|
|
352
|
+
format: { type: 'string', description: '输出格式:table(默认表格)/ csv / json。csv 与 json 会额外返回 formatted 文本,便于落盘或转存。' },
|
|
278
353
|
}),
|
|
279
354
|
output: {
|
|
280
355
|
schema: querySchema,
|
|
281
356
|
render: (_args, value) => {
|
|
282
357
|
const rec = asRecord(value);
|
|
358
|
+
if (typeof rec.formatted === 'string' && rec.formatted !== '') {
|
|
359
|
+
const preview = rec.formatted.length > 4000 ? rec.formatted.slice(0, 4000) + '\n…(预览已截断)' : rec.formatted;
|
|
360
|
+
return [{ type: 'text', text: '查询返回 ' + rec.rowCount + ' 行(' + String(rec.format) + ' 格式):\n' + preview }];
|
|
361
|
+
}
|
|
283
362
|
const rows = Array.isArray(rec.rows) ? rec.rows : [];
|
|
284
363
|
const lines = ['查询返回 ' + rec.rowCount + ' 行' + (rec.truncated === true ? '(截断到 ' + rec.maxRows + ' 行)' : '') + ',列:' + (Array.isArray(rec.columns) ? rec.columns.join(', ') : '')];
|
|
285
364
|
for (const row of rows.slice(0, 20)) {
|
|
@@ -297,7 +376,8 @@ export function buildSqlTools(config) {
|
|
|
297
376
|
const result = await adapter.query(sql, cfg.maxRows + 1);
|
|
298
377
|
const total = result.rows.length;
|
|
299
378
|
const rows = result.rows.slice(0, cfg.maxRows);
|
|
300
|
-
|
|
379
|
+
const format = optionalString(args, 'format')?.toLowerCase() ?? 'table';
|
|
380
|
+
const base = {
|
|
301
381
|
connection: name,
|
|
302
382
|
columns: result.columns,
|
|
303
383
|
rows,
|
|
@@ -305,8 +385,15 @@ export function buildSqlTools(config) {
|
|
|
305
385
|
truncated: total > cfg.maxRows,
|
|
306
386
|
maxRows: cfg.maxRows,
|
|
307
387
|
};
|
|
388
|
+
if (format === 'csv')
|
|
389
|
+
return { ...base, format, formatted: toCsv(result.columns, rows) };
|
|
390
|
+
if (format === 'json') {
|
|
391
|
+
const objects = rows.map((row) => Object.fromEntries(result.columns.map((column, i) => [column, row[i]])));
|
|
392
|
+
return { ...base, format, formatted: JSON.stringify(objects, null, 2) };
|
|
393
|
+
}
|
|
394
|
+
return base;
|
|
308
395
|
},
|
|
309
|
-
timeoutMs:
|
|
396
|
+
timeoutMs: cfg.queryTimeoutMs,
|
|
310
397
|
};
|
|
311
398
|
const sqlExec = {
|
|
312
399
|
name: 'sql_exec',
|
|
@@ -334,7 +421,7 @@ export function buildSqlTools(config) {
|
|
|
334
421
|
gate(exec, next) {
|
|
335
422
|
return next(); // 审批门由 index.ts 注入(需要宿主 ctx)
|
|
336
423
|
},
|
|
337
|
-
timeoutMs:
|
|
424
|
+
timeoutMs: cfg.execTimeoutMs,
|
|
338
425
|
};
|
|
339
426
|
const sqlSchema = {
|
|
340
427
|
name: 'sql_schema',
|
|
@@ -373,5 +460,105 @@ export function buildSqlTools(config) {
|
|
|
373
460
|
},
|
|
374
461
|
timeoutMs: 30000,
|
|
375
462
|
};
|
|
376
|
-
|
|
463
|
+
const sqlStats = {
|
|
464
|
+
name: 'sql_stats',
|
|
465
|
+
description: '数据库概览统计:表数量、每张表的行数、库体积(SQLite 按页计算,MySQL/PostgreSQL 走系统表)。connection 为连接名(缺省第一个连接)。适合在写查询前先了解数据规模。',
|
|
466
|
+
parameters: compileParameters({
|
|
467
|
+
connection: { type: 'string', description: '连接名(可选,缺省第一个连接)。' },
|
|
468
|
+
}),
|
|
469
|
+
output: {
|
|
470
|
+
schema: statsSchema,
|
|
471
|
+
render: (_args, value) => {
|
|
472
|
+
const rec = asRecord(value);
|
|
473
|
+
const tables = Array.isArray(rec.tables) ? rec.tables : [];
|
|
474
|
+
const lines = ['连接 ' + rec.connection + '(' + rec.engine + '):共 ' + tables.length + ' 张表' + (typeof rec.sizeBytes === 'number' && rec.sizeBytes >= 0 ? ',库体积 ' + formatBytes(rec.sizeBytes) : '') + '。'];
|
|
475
|
+
for (const item of tables.slice(0, 30)) {
|
|
476
|
+
const t = asRecord(item);
|
|
477
|
+
lines.push('- ' + t.name + ':' + (typeof t.rowCount === 'number' ? t.rowCount + ' 行' : '行数未知' + (t.error ? '(' + t.error + ')' : '')));
|
|
478
|
+
}
|
|
479
|
+
if (tables.length > 30)
|
|
480
|
+
lines.push('…仅展示前 30 张表');
|
|
481
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
async execute(rawArgs) {
|
|
485
|
+
const args = asRecord(rawArgs);
|
|
486
|
+
const { adapter, name } = getAdapter(optionalString(args, 'connection'));
|
|
487
|
+
const engine = adapter.engine;
|
|
488
|
+
let sizeBytes = -1;
|
|
489
|
+
try {
|
|
490
|
+
sizeBytes = await databaseSize(adapter, engine);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
sizeBytes = -1;
|
|
494
|
+
}
|
|
495
|
+
const tables = [];
|
|
496
|
+
if (engine === 'mysql' || engine === 'postgres') {
|
|
497
|
+
try {
|
|
498
|
+
const sql = engine === 'mysql'
|
|
499
|
+
? 'SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE()'
|
|
500
|
+
: 'SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY relname';
|
|
501
|
+
const result = await adapter.query(sql);
|
|
502
|
+
for (const row of result.rows) {
|
|
503
|
+
tables.push({ name: String(row[0]), rowCount: typeof row[1] === 'number' ? row[1] : Number(row[1] ?? 0) });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
tables.push({ name: '', error: error instanceof Error ? error.message : String(error) });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
const names = await adapter.listTables();
|
|
512
|
+
for (const table of names) {
|
|
513
|
+
try {
|
|
514
|
+
const result = await adapter.query('SELECT COUNT(*) FROM ' + quoteIdent(engine, table), 1);
|
|
515
|
+
tables.push({ name: table, rowCount: Number(result.rows[0]?.[0] ?? 0) });
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
tables.push({ name: table, error: error instanceof Error ? error.message : String(error) });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return { connection: name, engine, tableCount: tables.filter((t) => t.name !== '').length, tables, sizeBytes };
|
|
523
|
+
},
|
|
524
|
+
timeoutMs: cfg.queryTimeoutMs,
|
|
525
|
+
};
|
|
526
|
+
const sqlHealth = {
|
|
527
|
+
name: 'sql_health',
|
|
528
|
+
description: 'dsh-sql 自检:逐个连接做连通性测试,并汇总安全配置(只读模式、写审批门、行数上限、超时)。遇到问题时先运行本工具定位。',
|
|
529
|
+
parameters: compileParameters({}),
|
|
530
|
+
output: {
|
|
531
|
+
schema: healthSchema,
|
|
532
|
+
render: (_args, value) => {
|
|
533
|
+
const rec = asRecord(value);
|
|
534
|
+
const connections = Array.isArray(rec.connections) ? rec.connections : [];
|
|
535
|
+
const bad = connections.filter((c) => asRecord(c).ok !== true);
|
|
536
|
+
const lines = ['dsh-sql 自检' + (bad.length === 0 ? ':全部连接正常。' : ':' + bad.length + ' 个连接异常。')];
|
|
537
|
+
for (const item of connections) {
|
|
538
|
+
const c = asRecord(item);
|
|
539
|
+
lines.push('- ' + c.name + '(' + c.engine + ')' + (c.ok === true ? ' ✅' : ' ❌ ' + String(c.error ?? '')));
|
|
540
|
+
}
|
|
541
|
+
lines.push('- 只读模式:' + (rec.readOnly === true ? '开(sql_exec 已禁用)' : '关'));
|
|
542
|
+
lines.push('- 写审批门:' + (rec.writeApproval === true ? '开' : '关'));
|
|
543
|
+
lines.push('- 行数上限:' + String(rec.maxRows) + ';查询超时 ' + String(rec.queryTimeoutMs) + 'ms;写超时 ' + String(rec.execTimeoutMs) + 'ms');
|
|
544
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
async execute() {
|
|
548
|
+
const connections = await pingAllConnections();
|
|
549
|
+
const bad = connections.filter((c) => c.ok !== true);
|
|
550
|
+
return {
|
|
551
|
+
ok: bad.length === 0,
|
|
552
|
+
plugin: 'dsh-sql',
|
|
553
|
+
connections,
|
|
554
|
+
readOnly: cfg.readOnly,
|
|
555
|
+
writeApproval: cfg.writeApproval,
|
|
556
|
+
maxRows: cfg.maxRows,
|
|
557
|
+
queryTimeoutMs: cfg.queryTimeoutMs,
|
|
558
|
+
execTimeoutMs: cfg.execTimeoutMs,
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
timeoutMs: 30000,
|
|
562
|
+
};
|
|
563
|
+
return { tools: [sqlList, sqlQuery, sqlExec, sqlSchema, sqlStats, sqlHealth], adapters };
|
|
377
564
|
}
|
package/package.json
CHANGED
|
@@ -1,68 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-sql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 工程师级数据库插件:sql_list/query/exec/schema 四工具,SQLite/MySQL/PostgreSQL
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"types": "lib/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./lib/index.d.ts",
|
|
11
|
-
"default": "./lib/index.js"
|
|
12
|
-
},
|
|
13
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
-
"./package.json": "./package.json"
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"lib",
|
|
18
|
-
"cordis.patch.yml",
|
|
19
|
-
"README.md",
|
|
20
|
-
"README.en.md"
|
|
21
|
-
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"build": "tsc -p tsconfig.json",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"@types/
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-sql",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "DSH 工程师级数据库插件:sql_list/query/exec/schema 四工具,SQLite/MySQL/PostgreSQL 三引擎、多连接、只读模式与写操作审批门、词法级只读保护、流式行数钳制与可配置查询/写超时。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README.en.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
26
|
+
"prepublishOnly": "pnpm run build"
|
|
27
|
+
},
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"dsh",
|
|
35
|
+
"deepseek-harness",
|
|
36
|
+
"plugin",
|
|
37
|
+
"sql",
|
|
38
|
+
"sqlite",
|
|
39
|
+
"mysql",
|
|
40
|
+
"postgresql",
|
|
41
|
+
"database",
|
|
42
|
+
"dsh-plugin"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22.13"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"mysql2": "^3.11.0",
|
|
50
|
+
"pg": "^8.13.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"@types/pg": "^8.21.0",
|
|
55
|
+
"typescript": "^5.6.0"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/STARDUSTLC666/dsh-sql.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/STARDUSTLC666/dsh-sql/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/STARDUSTLC666/dsh-sql#readme",
|
|
65
|
+
"author": "stardustlc",
|
|
66
|
+
"packageManager": "pnpm@11.7.0"
|
|
67
|
+
}
|