dsh-dbhub-live 2.0.0 → 3.1.2
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/AGENTS.md +97 -0
- package/LICENSE +21 -21
- package/README.en.md +162 -0
- package/README.md +162 -110
- package/cordis.patch.yml +8 -8
- package/doc/REQUIREMENTS.md +65 -0
- package/lib/adhoc.mjs +97 -0
- package/lib/client.js +604 -0
- package/lib/collect.mjs +118 -0
- package/lib/config.mjs +492 -0
- package/lib/i18n.mjs +139 -0
- package/lib/index.mjs +345 -1417
- package/lib/mcp.mjs +462 -0
- package/lib/options.mjs +94 -0
- package/lib/runtime.mjs +255 -0
- package/lib/state.mjs +108 -0
- package/lib/tools.mjs +462 -0
- package/package.json +61 -39
- package/test/adhoc.test.mjs +29 -0
- package/test/client-format.test.mjs +88 -0
- package/test/i18n.test.mjs +27 -0
- package/test/init.test.mjs +163 -0
- package/test/options.test.mjs +41 -0
- package/test/state.test.mjs +85 -0
- package/test/util.test.mjs +96 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# dsh-dbhub-live 需求文档(业务层面)
|
|
2
|
+
|
|
3
|
+
> 说明:本文只描述业务需求,不涉及实现原理;详细开发说明见 `AGENTS.md`,用户安装使用见 `README.md`。
|
|
4
|
+
|
|
5
|
+
## 产品定位
|
|
6
|
+
|
|
7
|
+
为 DSH(DeepSeek Harness)提供数据库操作能力:模型在对话中可以直接查询、搜索、管理数据库连接;支持按工作区配置专属连接,也支持临时连接任意数据库做一次性排查。
|
|
8
|
+
|
|
9
|
+
## 一、既有功能
|
|
10
|
+
|
|
11
|
+
### 1. 数据库连接管理
|
|
12
|
+
- **支持的数据库**:MySQL、PostgreSQL、MariaDB、SQLite、SQL Server。
|
|
13
|
+
- **按工作区配置**:每个工作区可配置并持久化专属连接,一次配置长期复用。
|
|
14
|
+
- **同一工作区多环境**:可为一个工作区添加多个环境连接(默认环境 `default` 与命名环境如 prod / dev / test),各环境有独立连接标识与 `source` 值,互不混淆;可在设置卡片中直接查看、新增、修改、删除。
|
|
15
|
+
- **三种配置方式**:
|
|
16
|
+
1. 直接填写完整连接串(DSN);
|
|
17
|
+
2. 按分项填写(类型 / 主机 / 端口 / 账号 / 密码 / 库名);
|
|
18
|
+
3. 授权扫描项目配置文件(`.env`、`application*.yml`、`docker-compose`、`jdbc.properties` 等),列出候选项供确认后采用。
|
|
19
|
+
- **自动发现**:工作区已有 `mise env` 或 `.env`(`DSN` / `DB_*`)时自动识别,无需手动配置。
|
|
20
|
+
- **安全约束**:凭据仅存本机用户目录;任何展示/结果中的密码一律打码;扫描敏感文件必须先经用户授权。
|
|
21
|
+
|
|
22
|
+
### 2. 查询与对象搜索
|
|
23
|
+
- **按数据源查询**:通过 `dbhub_execute_sql(source, …)` / `dbhub_search_objects(source, …)` 选择工作区与环境(`source` 值见 `dbhub_list_sources`),默认环境不带后缀、命名环境带 `_<环境>` 后缀。**工具声明数量恒定**:不随工作区/环境数量增长,避免占用模型上下文。
|
|
24
|
+
- **临时连接查询**:对任意未配置的库传入连接串即可查询/搜索(多语句用 `;` 分隔),每次调用独立连接,可同时连多个不同库,便于跨环境对比排查。
|
|
25
|
+
- **环境提示**:工具描述和结果中标注目标连接环境(主机/端口/库,密码打码),避免误操作生产库。
|
|
26
|
+
|
|
27
|
+
### 3. 运行时保障
|
|
28
|
+
- 常驻连接复用,提升多次查询效率;空闲一段时间自动回收。
|
|
29
|
+
- 首次使用若缺少依赖则自动安装,之后定期自动更新;用户自行安装的版本不受影响。
|
|
30
|
+
- 服务异常退出后,下次调用自动恢复,无需人工干预。
|
|
31
|
+
|
|
32
|
+
## 二、本次新增功能
|
|
33
|
+
|
|
34
|
+
### 1. 懒加载启动优化
|
|
35
|
+
- 插件启动不阻塞 GUI,工具立即可用。
|
|
36
|
+
- 环境初始化推迟到首次调用时进行;初始化期间界面提示「初始化中」,查询请求自动等待就绪。
|
|
37
|
+
|
|
38
|
+
### 2. 插件状态管理
|
|
39
|
+
- **启用 / 禁用开关**:关闭后所有 dbhub 相关工具立即返回「插件已禁用」友好提示,无需重启;再次开启立即可用。
|
|
40
|
+
- **运行状态**:运行中 / 初始化中 / 异常 三态,实时反映插件可用性。
|
|
41
|
+
- **工具数量**:当前已注册工具总数实时展示。
|
|
42
|
+
- **错误摘要**:最近一次初始化或运行错误简要信息,异常时展示。
|
|
43
|
+
|
|
44
|
+
### 3. 浏览器状态卡片(设置在插件的状态面板)
|
|
45
|
+
- **整行折叠样式**(与其余插件设置卡一致):折叠态一行展示运行状态徽章(🟢 / 🟡 / 🔴 / ⚪)、环境数量与 **启用 / 禁用开关**(不展开即可看状态、切开关);展开后分「状态 / 配置 / 工作区连接」三块。
|
|
46
|
+
- 状态块为辅助信息:常驻进程状态、工作模式(懒加载)、**工具声明数(固定 6,非重点)**、环境计数(N 个 · 已保存 M)、最近错误(红色展示)。
|
|
47
|
+
- 工作区连接块逐行展示:工作区名、环境名、**source 值**(等宽字体,供 `dbhub_execute_sql` 使用)、打码连接串、来源徽章(已保存 / 自动),支持修改 / 删除 / 新增。
|
|
48
|
+
- 启用/禁用开关即时生效。
|
|
49
|
+
|
|
50
|
+
### 4. 可配置参数(插件配置面板内直接编辑)
|
|
51
|
+
- 自动更新间隔天数(0 关闭)、空闲回收时长(分钟):在「配置」分区编辑后点保存,即时生效并持久化。
|
|
52
|
+
- 优先级:用户设置 > 进程环境变量(默认值种子)> 内置默认。
|
|
53
|
+
- 自动安装包不进入 UI:由环境变量 `DSH_DBHUB_PACKAGE` 单独控制(默认 `@bytebase/dbhub`);`DSH_DBHUB_UPDATE_DAYS` 继续作为更新间隔默认值种子。
|
|
54
|
+
|
|
55
|
+
### 5. 工作区连接管理(插件配置面板内查看与增删改)
|
|
56
|
+
- 「工作区连接」分区列出每个工作区 × 环境的连接:工作区名、环境名、打码连接串、来源徽章(已保存 / 自动)。
|
|
57
|
+
- 已保存:经 `dbhub_configure` 或卡片添加持久化于本机。
|
|
58
|
+
- 自动:来自 mise env / .env 自动发现,不持久化、随源文件变化。
|
|
59
|
+
- 支持:新增连接(工作区 + 环境名 + 连接串)、修改连接串(对自动项执行修改即转为已保存的手动覆盖)、删除已保存项。
|
|
60
|
+
- **同一工作区可添加多个环境**:默认环境 `default` 与命名环境(prod / dev / test 等)并存,均通过 `dbhub_execute_sql` / `dbhub_search_objects` 的 `source` 参数选择(source 值见 `dbhub_list_sources`),互不混淆。
|
|
61
|
+
- **上下文占用恒定**:**不为每个工作区 × 环境单独注册工具**——常驻工具声明固定为 6 个,环境再多也不膨胀模型上下文;`source` 在调用时由 Host 解析到真实连接。
|
|
62
|
+
- **变更即时生效**:卡片新增/修改/删除连接后,常驻 dbhub 服务立即重启,下一次查询即可使用新环境的 `source` 值,无需重启实例。
|
|
63
|
+
- **连接源枚举工具**:`dbhub_list_sources` 列出全部已注册连接源(工作区 × 环境、打码连接串、来源与对应 source 值),避免把「测试环境」误当成默认(生产)库。
|
|
64
|
+
- **脱敏连接串不可直连**:`dbhub_list_sources` 中展示的连接串密码为 `****`(仅识别用途);`dbhub_query` / `dbhub_query_objects` 检测到脱敏密码时会直接拒绝并引导使用常驻工具(内置真实凭据),避免用 `****` 白跑一次失败连接。
|
|
65
|
+
- 自动配置与已保存配置并存展示;自动发现与实际不符时可手动修改覆盖。
|
package/lib/adhoc.mjs
ADDED
|
@@ -0,0 +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 { 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
|
+
}
|
|
97
|
+
}
|