dsh-dbhub-live 3.0.0 → 3.1.3

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/lib/adhoc.mjs CHANGED
@@ -1,81 +1,97 @@
1
- // dsh-dbhub-live: ad-hoc temporary connections.
2
- //
3
- // Each call spawns a fresh throwaway `dbhub --transport stdio --dsn <dsn>`,
4
- // runs one MCP tools/call against an ad-hoc target (ip/account/password/db
5
- // supplied by the model per request), then kills it. Independent per call, so
6
- // two parallel calls can query two different databases at once. Nothing is
7
- // persisted and the persistent multi-source server is untouched.
8
-
9
- import { DATA_DIR, maskDsn } from './config.mjs'
10
- import { buildSpawnArgv, resolveDbhubExe } from './runtime.mjs'
11
- import { createMcpClient, disabledMessage } from './mcp.mjs'
12
- import * as state from './state.mjs'
13
-
14
- export async function runAdhoc(subprocess, dsn, rawName, mcpArgs, exec) {
15
- if (!state.isEnabled()) {
16
- return { ok: false, text: disabledMessage() }
17
- }
18
- if (!dsn || typeof dsn !== 'string' || !dsn.trim()) {
19
- return { ok: false, text: '缺少 dsn 参数(如 mysql://user:pass@host:3306/db)' }
20
- }
21
- let exe
22
- try {
23
- exe = await resolveDbhubExe(subprocess, exec.signal)
24
- } catch (e) {
25
- return { ok: false, text: '无法获取 dbhub: ' + String((e && e.message) || e) }
26
- }
27
- let handle
28
- try {
29
- handle = subprocess.spawn({
30
- argv: buildSpawnArgv(subprocess, exe, ['--transport', 'stdio', '--dsn', dsn.trim()]),
31
- cwd: DATA_DIR,
32
- stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
33
- graceMs: 3000,
34
- signal: exec.signal,
35
- })
36
- } catch (e) {
37
- return { ok: false, text: '启动临时 dbhub 失败: ' + String((e && e.message) || e) }
38
- }
39
- let stderrTail = ''
40
- try {
41
- const client = createMcpClient(handle, (c) => {
42
- stderrTail = (stderrTail + String(c)).slice(-2000)
43
- })
44
- await client.request('initialize', {
45
- protocolVersion: '2025-03-26',
46
- capabilities: {},
47
- clientInfo: { name: 'dsh-dbhub-adhoc', version: '3.0.0' },
48
- })
49
- client.notify('notifications/initialized')
50
- const res = await client.request('tools/call', { name: rawName, arguments: mcpArgs })
51
- const textOf = (r) => {
52
- if (r && Array.isArray(r.content)) {
53
- const parts = []
54
- for (const b of r.content) if (b && typeof b.text === 'string') parts.push(b.text)
55
- return parts.join('\n')
56
- }
57
- return JSON.stringify(r)
58
- }
59
- const prefix = '{临时连接: ' + maskDsn(dsn) + '}\n'
60
- if (res && res.isError) return { ok: false, text: '临时连接执行错误: ' + textOf(res) }
61
- if (res && res.structuredContent !== undefined) {
62
- return { ok: true, text: prefix + JSON.stringify(res.structuredContent, null, 2) }
63
- }
64
- return { ok: true, text: prefix + textOf(res) }
65
- } catch (e) {
66
- let detail = String((e && e.message) || e)
67
- if (stderrTail) detail += '\n[dbhub stderr] ' + stderrTail
68
- return { ok: false, text: detail }
69
- } finally {
70
- try {
71
- handle.terminate()
72
- } catch (e) {
73
- /* ignore */
74
- }
75
- try {
76
- await handle.waitForExit(exec.signal)
77
- } catch (e) {
78
- /* ignore */
79
- }
80
- }
1
+ // dsh-dbhub-live: ad-hoc temporary connections.
2
+ //
3
+ // Each call spawns a fresh throwaway `dbhub --transport stdio --dsn <dsn>`,
4
+ // runs one MCP tools/call against an ad-hoc target (ip/account/password/db
5
+ // supplied by the model per request), then kills it. Independent per call, so
6
+ // two parallel calls can query two different databases at once. Nothing is
7
+ // persisted and the persistent multi-source server is untouched.
8
+
9
+ import { DATA_DIR, maskDsn } from './config.mjs'
10
+ import { buildSpawnArgv, resolveDbhubExe } from './runtime.mjs'
11
+ import { createMcpClient, disabledMessage } from './mcp.mjs'
12
+ import { currentT } from './i18n.mjs'
13
+ import * as state from './state.mjs'
14
+
15
+ /**
16
+ * Detect a surfaced DSN whose password was masked (as in the settings-card
17
+ * summaries). The model sometimes copies a masked DSN into `dbhub_query`;
18
+ * connecting with `****` always fails, so refuse loudly with the right path
19
+ * instead of burning an auth round-trip.
20
+ * @param dsn - the ad-hoc DSN string.
21
+ * @returns whether the password position looks masked (`:**…` before @).
22
+ */
23
+ export function isMaskedDsn(dsn) {
24
+ return typeof dsn === 'string' && /\/\/[^/@]*:\*{2,}@/.test(dsn)
25
+ }
26
+
27
+ export async function runAdhoc(subprocess, dsn, rawName, mcpArgs, exec) {
28
+ if (!state.isEnabled()) {
29
+ return { ok: false, text: disabledMessage() }
30
+ }
31
+ if (!dsn || typeof dsn !== 'string' || !dsn.trim()) {
32
+ return { ok: false, text: currentT('result.dsnMissing') }
33
+ }
34
+ if (isMaskedDsn(dsn)) {
35
+ return { ok: false, text: currentT('result.masked') }
36
+ }
37
+ let exe
38
+ try {
39
+ exe = await resolveDbhubExe(subprocess, exec.signal)
40
+ } catch (e) {
41
+ return { ok: false, text: '无法获取 dbhub: ' + String((e && e.message) || e) }
42
+ }
43
+ let handle
44
+ try {
45
+ handle = subprocess.spawn({
46
+ argv: buildSpawnArgv(subprocess, exe, ['--transport', 'stdio', '--dsn', dsn.trim()]),
47
+ cwd: DATA_DIR,
48
+ stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
49
+ graceMs: 3000,
50
+ signal: exec.signal,
51
+ })
52
+ } catch (e) {
53
+ return { ok: false, text: '启动临时 dbhub 失败: ' + String((e && e.message) || e) }
54
+ }
55
+ let stderrTail = ''
56
+ try {
57
+ const client = createMcpClient(handle, (c) => {
58
+ stderrTail = (stderrTail + String(c)).slice(-2000)
59
+ })
60
+ await client.request('initialize', {
61
+ protocolVersion: '2025-03-26',
62
+ capabilities: {},
63
+ clientInfo: { name: 'dsh-dbhub-adhoc', version: '3.0.0' },
64
+ })
65
+ client.notify('notifications/initialized')
66
+ const res = await client.request('tools/call', { name: rawName, arguments: mcpArgs })
67
+ const textOf = (r) => {
68
+ if (r && Array.isArray(r.content)) {
69
+ const parts = []
70
+ for (const b of r.content) if (b && typeof b.text === 'string') parts.push(b.text)
71
+ return parts.join('\n')
72
+ }
73
+ return JSON.stringify(r)
74
+ }
75
+ const prefix = currentT('label.adhocConn', { dsn: maskDsn(dsn) }) + '\n'
76
+ if (res && res.isError) return { ok: false, text: textOf(res) }
77
+ if (res && res.structuredContent !== undefined) {
78
+ return { ok: true, text: prefix + JSON.stringify(res.structuredContent, null, 2) }
79
+ }
80
+ return { ok: true, text: prefix + textOf(res) }
81
+ } catch (e) {
82
+ let detail = String((e && e.message) || e)
83
+ if (stderrTail) detail += '\n[dbhub stderr] ' + stderrTail
84
+ return { ok: false, text: detail }
85
+ } finally {
86
+ try {
87
+ handle.terminate()
88
+ } catch (e) {
89
+ /* ignore */
90
+ }
91
+ try {
92
+ await handle.waitForExit(exec.signal)
93
+ } catch (e) {
94
+ /* ignore */
95
+ }
96
+ }
81
97
  }