dsh-db-tool 1.8.0 → 1.8.1

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/client/client.js CHANGED
@@ -105,6 +105,7 @@ window.__ModuleLoader__.load({
105
105
  urlUser: "用户名(可选,自动注入 URL)",
106
106
  urlPassword: "密码(可选,自动注入 URL;留空保持已存)",
107
107
  fieldsHost: "主机地址",
108
+ fieldsHostInstance: "主机地址(命名实例填 主机\\实例名,无需端口)",
108
109
  fieldsPort: "端口",
109
110
  fieldsUser: "用户名",
110
111
  fieldsPassword: "密码",
@@ -323,6 +324,7 @@ window.__ModuleLoader__.load({
323
324
  urlUser: "Username (optional, injected into URL)",
324
325
  urlPassword: "Password (optional, injected into URL; blank keeps saved)",
325
326
  fieldsHost: "Host",
327
+ fieldsHostInstance: "Host (named instance: host\\instance, no port needed)",
326
328
  fieldsPort: "Port",
327
329
  fieldsUser: "User",
328
330
  fieldsPassword: "Password",
@@ -1251,7 +1253,7 @@ window.__ModuleLoader__.load({
1251
1253
  mongodb: { host: "127.0.0.1", port: 27017, database: "test" },
1252
1254
  oracle: { host: "127.0.0.1", port: 1521, user: "system" },
1253
1255
  dmdb: { host: "127.0.0.1", port: 5236, user: "SYSDBA" },
1254
- sqlserver: { host: "127.0.0.1", port: 1433, user: "sa" },
1256
+ sqlserver: { host: "127.0.0.1", user: "sa" },
1255
1257
  };
1256
1258
 
1257
1259
  /**
@@ -1268,6 +1270,24 @@ window.__ModuleLoader__.load({
1268
1270
  return next;
1269
1271
  }
1270
1272
 
1273
+ /** URL→分字段的字段反解:只取可信部分。safeUrl 仅脱敏密码段(用户名/主机/端口为真值),
1274
+ * 密码不可反解——留空走编辑语义,clearUrl 时由服务端迁移 URL 内嵌密码。非法 URL 返回 null。 */
1275
+ function urlToFieldsPatch(url) {
1276
+ if (!url) return null;
1277
+ try {
1278
+ const u = new URL(url);
1279
+ const decoded = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
1280
+ return {
1281
+ host: u.hostname,
1282
+ port: u.port,
1283
+ user: decoded(u.username),
1284
+ database: decoded(u.pathname.replace(/^\//, "")),
1285
+ };
1286
+ } catch {
1287
+ return null;
1288
+ }
1289
+ }
1290
+
1271
1291
  /** 新建表单初始态:分字段模式 + 当前 kind 的官方默认值 */
1272
1292
  const freshForm = () => withKindDefaults({ ...EMPTY_FORM, mode: "fields" });
1273
1293
 
@@ -1322,12 +1342,20 @@ window.__ModuleLoader__.load({
1322
1342
  setDirty(new Set());
1323
1343
  setDraftTest(null);
1324
1344
  }
1325
- /** 从 URL 模式切回分字段:保留表单里已有值(编辑回填场景),只对空字段补官方默认值 */
1345
+ /** 从 URL 模式切回分字段:表单已有值保留;空字段先从已填 URL 反解真实值,仍空的才补官方默认值——
1346
+ * 直接补默认会把 URL 里的真实主机/端口悄悄换成 127.0.0.1/1433(丢端口 bug)。 */
1326
1347
  function reenterFields() {
1327
1348
  setForm((prev) => {
1328
1349
  const keep = new Set(dirty);
1329
1350
  for (const k of ["host", "port", "user", "database"]) if (prev[k]) keep.add(k);
1330
- return withKindDefaults(Object.assign({}, prev, { mode: "fields" }), keep);
1351
+ const next = Object.assign({}, prev, { mode: "fields" });
1352
+ const fromUrl = urlToFieldsPatch(prev.url);
1353
+ if (fromUrl) {
1354
+ for (const k of ["host", "port", "user", "database"]) {
1355
+ if (!prev[k] && fromUrl[k]) { next[k] = fromUrl[k]; keep.add(k); }
1356
+ }
1357
+ }
1358
+ return withKindDefaults(next, keep);
1331
1359
  });
1332
1360
  setDraftTest(null);
1333
1361
  }
@@ -1348,7 +1376,8 @@ window.__ModuleLoader__.load({
1348
1376
  if (up) body.urlPassword = up;
1349
1377
  } else {
1350
1378
  body.fields = { host: form.host, user: form.user, database: form.database || undefined };
1351
- if (form.port) body.fields.port = Number(form.port);
1379
+ // 端口原样发字符串:Number("abc")=NaN 会被 JSON 序列化成 null,服务端按「未提供端口」静默回退默认值,配置错误被掩盖
1380
+ if (form.port) body.fields.port = form.port;
1352
1381
  if (form.password) body.fields.password = form.password;
1353
1382
  // 从 url 方式切到分字段保存 → 服务端清除已存 url(否则连接仍走旧 url)
1354
1383
  if (props.initial && props.initial.mode === "url") body.clearUrl = true;
@@ -1406,7 +1435,8 @@ window.__ModuleLoader__.load({
1406
1435
  "div",
1407
1436
  { style: { display: "flex", flexDirection: "column", gap: 6 } },
1408
1437
  React.createElement("div", { className: "dbt-row" },
1409
- React.createElement("input", { placeholder: t("fieldsHost"), "aria-label": t("fieldsHost") + t("requiredMark"), "aria-required": "true", ref: firstInputRef, value: form.host, onChange: (e) => patch({ host: e.target.value }) }),
1438
+ // sqlserver:官方模型 port 可选(默认 1433)且与命名实例互斥——host 直接填 主机\实例名,端口留空
1439
+ React.createElement("input", { placeholder: form.kind === "sqlserver" ? t("fieldsHostInstance") : t("fieldsHost"), "aria-label": t("fieldsHost") + t("requiredMark"), "aria-required": "true", ref: firstInputRef, value: form.host, onChange: (e) => patch({ host: e.target.value }) }),
1410
1440
  React.createElement("input", { placeholder: t("fieldsPort"), "aria-label": t("fieldsPort"), value: form.port, onChange: (e) => patch({ port: e.target.value }) }),
1411
1441
  React.createElement("input", { placeholder: t("fieldsDatabase"), "aria-label": t("fieldsDatabase"), value: form.database, onChange: (e) => patch({ database: e.target.value }) })),
1412
1442
  React.createElement("div", { className: "dbt-row" },
@@ -3685,6 +3715,7 @@ window.__ModuleLoader__.load({
3685
3715
  sortRows, slicePage, ResultSetGrid,
3686
3716
  classifyHead, ConsoleView,
3687
3717
  Panel, TransferView, TransferLogView,
3718
+ urlToFieldsPatch,
3688
3719
  };
3689
3720
  return module.exports;
3690
3721
  },
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import db from 'dmdb';
22
22
  import { sqlHead, SQL_READ_HEADS } from '../../guard/index.js';
23
+ import { validPort } from '../sql-shared/common.js';
23
24
  const ROWS_MAX = 500;
24
25
  const CELL_TRUNC = 1000;
25
26
  const DDL_KEYWORDS = new Set([
@@ -69,6 +70,10 @@ export function resolveDmConn(conn) {
69
70
  throw new Error('DM URL 应以 dm:// 开头,如 dm://user:pass@host:5236');
70
71
  }
71
72
  const u = new URL(conn.url);
73
+ // 端口段校验:'dm://host:0' 等非法值抛中文错误,不再透传驱动报晦涩的 EADDRNOTAVAIL
74
+ // (connectString 仍用原 URL 原样透传,不重组)
75
+ if (u.port)
76
+ validPort(u.port, 5236, 'dmdb');
72
77
  return {
73
78
  connectString: conn.url,
74
79
  user: decodeURIComponent(u.username),
@@ -77,7 +82,9 @@ export function resolveDmConn(conn) {
77
82
  }
78
83
  const f = conn.fields ?? {};
79
84
  const host = typeof f.host === 'string' ? f.host : '';
80
- const port = typeof f.port === 'number' ? f.port : 5236;
85
+ // 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
86
+ // 静默丢弃回退 5236,且驱动侧 parseInt 对非法形态还有第二重静默回退
87
+ const port = validPort(f.port, 5236, 'dmdb');
81
88
  if (host === '') {
82
89
  throw new Error('DM 连接需要 url(dm://user:pass@host:5236)或 fields(host/port)');
83
90
  }
@@ -17,6 +17,7 @@
17
17
  * bson 保持 dependencies(bundle 的 external),保证 BSON 类实例与外部一致。
18
18
  */
19
19
  import { createRequire } from 'node:module';
20
+ import { validPort } from '../sql-shared/common.js';
20
21
  let vendorCache;
21
22
  /** 容错加载 vendor bundle:缺失时返回 undefined(调用方降级处理),成功后缓存。
22
23
  * 失败不缓存 → vendor 构建完成后无需重启即可恢复。 */
@@ -574,7 +575,9 @@ export function reviveEjson(v) {
574
575
  }
575
576
  export function buildUri(fields, ssl) {
576
577
  const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
577
- const port = typeof fields?.port === 'number' ? fields.port : 27017;
578
+ // 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
579
+ // 静默丢弃回退 27017,分字段模式用户所填端口实际永不生效
580
+ const port = validPort(fields?.port, 27017, 'mongodb');
578
581
  const user = typeof fields?.user === 'string' ? encodeURIComponent(fields.user) : '';
579
582
  const pass = typeof fields?.password === 'string' ? `:${encodeURIComponent(fields.password)}` : '';
580
583
  const auth = user !== '' ? `${user}${pass}@` : '';
@@ -6,7 +6,7 @@
6
6
  * TRANSACTION READ ONLY)+ query 首词只读白名单双保险;服务层拦截 execute。
7
7
  */
8
8
  import mysql from 'mysql2/promise';
9
- import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent } from '../sql-shared/common.js';
9
+ import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent, validPort } from '../sql-shared/common.js';
10
10
  /** ro 模式下 query 允许的读语句首词白名单 */
11
11
  const RO_READ_PREFIX = /^(select|show|desc|describe|explain|use|help|table)\b/i;
12
12
  /**
@@ -43,7 +43,8 @@ function connOptions(conn) {
43
43
  if (conn.url) {
44
44
  const u = new URL(conn.url);
45
45
  base.host = u.hostname;
46
- base.port = Number(u.port) || 3306;
46
+ // 端口段校验:':0' 等非法值抛中文错误,不再被 Number()|| 静默改写 3306
47
+ base.port = u.port ? validPort(u.port, 3306, 'mysql') : 3306;
47
48
  try {
48
49
  if (u.username)
49
50
  base.user = decodeURIComponent(u.username);
@@ -63,7 +64,8 @@ function connOptions(conn) {
63
64
  else if (conn.fields) {
64
65
  const f = conn.fields;
65
66
  base.host = String(f.host ?? '127.0.0.1');
66
- base.port = Number(f.port ?? 3306);
67
+ // 前端原样发字符串端口:显式校验,NaN/0 等不再被驱动 `|| 3306` 静默改写后连错服务
68
+ base.port = validPort(f.port, 3306, 'mysql');
67
69
  base.user = String(f.user ?? f.username ?? 'root');
68
70
  if (f.password != null)
69
71
  base.password = String(f.password);
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import oracledb from 'oracledb';
20
20
  import { sqlHead, SQL_READ_HEADS } from '../../guard/index.js';
21
+ import { validPort } from '../sql-shared/common.js';
21
22
  // CLOB 统一按字符串返回;BLOB 统一按 Buffer 返回。均为官方全局按类型转换
22
23
  // (fetchInfo 的键是列名而非类型名,`{ BLOB: … }` 只会匹配恰好名为 "BLOB" 的列,
23
24
  // 按类型统一转换须用全局 fetchAsString/fetchAsBuffer——官方 connection.rst
@@ -111,12 +112,15 @@ export function resolveOracleConn(conn) {
111
112
  return {
112
113
  user,
113
114
  password: decodeURIComponent(u.password),
114
- connectString: `${u.hostname}:${u.port || 1521}/${service}`,
115
+ // 端口段校验:':0' 等非法值抛中文错误,不再 host:0 直传驱动
116
+ connectString: `${u.hostname}:${u.port ? validPort(u.port, 1521, 'oracle') : 1521}/${service}`,
115
117
  };
116
118
  }
117
119
  const f = conn.fields ?? {};
118
120
  const host = typeof f.host === 'string' ? f.host : '';
119
- const port = typeof f.port === 'number' ? f.port : 1521;
121
+ // 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
122
+ // 静默丢弃回退 1521
123
+ const port = validPort(f.port, 1521, 'oracle');
120
124
  const service = typeof f.service === 'string' ? f.service : typeof f.database === 'string' ? f.database : '';
121
125
  if (host === '' || service === '') {
122
126
  throw new Error('Oracle 连接需要 url(oracle://user:pass@host:1521/SERVICE)或 fields(host/port/service)');
@@ -10,6 +10,7 @@
10
10
  * - 只读账号建议由用户在服务端配置 ACL 只读用户(见 README/文档)。
11
11
  */
12
12
  import { createClient } from 'redis';
13
+ import { validPort } from '../sql-shared/common.js';
13
14
  /** 危险命令:服务层拦截器在用户确认前拒绝执行(KEYS 同时被 query 白名单禁用)。对齐官方 ACL dangerous 类。 */
14
15
  export const DANGEROUS_COMMANDS = new Set([
15
16
  'FLUSHALL', 'FLUSHDB', 'SWAPDB', 'CONFIG', 'DEBUG', 'SAVE', 'SHUTDOWN',
@@ -111,9 +112,11 @@ function parseInfo(raw) {
111
112
  return rows;
112
113
  }
113
114
  /** 从 fields 拼接连接 URL(url 缺省时) */
114
- function buildUrl(fields, ssl) {
115
+ export function buildUrl(fields, ssl) {
115
116
  const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
116
- const port = typeof fields?.port === 'number' ? fields.port : 6379;
117
+ // 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
118
+ // 静默丢弃回退 6379,属静默错连
119
+ const port = validPort(fields?.port, 6379, 'redis');
117
120
  const user = typeof fields?.user === 'string' && fields.user !== '' ? encodeURIComponent(fields.user) : '';
118
121
  const pass = typeof fields?.password === 'string' ? `:${encodeURIComponent(fields.password)}@` : '';
119
122
  const db = typeof fields?.database === 'number' && fields.database !== 0 ? `/${fields.database}` : '';
@@ -27,6 +27,21 @@ export function clampOffset(offset) {
27
27
  const n = Math.floor(Number(offset ?? 0));
28
28
  return Number.isFinite(n) && n > 0 ? n : 0;
29
29
  }
30
+ /**
31
+ * 端口校验(fail-closed,sqlserver validPort 同口径,各适配器 fields/URL 两路径共用):
32
+ * 仅「未提供」(undefined/null/'',表单留空的官方语义)回退该库默认端口;提供了但
33
+ * Number() 后不是 1-65535 的整数则抛中文错误——绝不静默改写用户配置(各驱动普遍对
34
+ * falsy 端口 `|| 默认` 静默回退、对非整数静默截断,连错服务/端口后排障会被误导)。
35
+ */
36
+ export function validPort(v, dflt, label) {
37
+ if (v === undefined || v === null || v === '')
38
+ return dflt;
39
+ const n = Number(v);
40
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
41
+ throw new Error(`${label} 连接的端口取值非法:${JSON.stringify(v)}(须为 1-65535 的整数;留空才使用默认端口 ${dflt})`);
42
+ }
43
+ return n;
44
+ }
30
45
  /** 任意单元格 → NormalizedCell(string | number | null) */
31
46
  export function normalizeCell(v) {
32
47
  if (v === null || v === undefined)
@@ -1,4 +1,4 @@
1
- import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent } from './common.js';
1
+ import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent, validPort } from './common.js';
2
2
  /** 多主机 authority 逐台重组段(host[:port]) */
3
3
  function joinHostSeg(h) {
4
4
  return `${h.host}${h.port != null ? ':' + h.port : ''}`;
@@ -20,7 +20,9 @@ function poolConfig(conn, overrideUrl, max = 10) {
20
20
  // devg-dist/gaussdb-12-0319)、默认管理用户 gaussdb;postgresql 维持社区默认 5432/postgres 不变。
21
21
  const gb = conn.meta.kind === 'gaussdb';
22
22
  base.host = String(f.host ?? '127.0.0.1');
23
- base.port = Number(f.port ?? (gb ? 8000 : 5432));
23
+ // 前端原样发字符串端口:显式校验,NaN/'' 经 Number() 变 0 后会被驱动 truthy 检查
24
+ // 静默回退 defaults 5432(连 gaussdb 想兜底的 8000 都落不到)、小数被 parseInt 截断
25
+ base.port = validPort(f.port, gb ? 8000 : 5432, conn.meta.kind);
24
26
  base.user = String(f.user ?? f.username ?? (gb ? 'gaussdb' : 'postgres'));
25
27
  if (f.password != null)
26
28
  base.password = String(f.password);
@@ -42,7 +44,7 @@ function poolConfig(conn, overrideUrl, max = 10) {
42
44
  * authority(host:port,host:port)端口段含逗号非数字 → new URL 直接抛 Invalid URL。
43
45
  * 故自行按文本解析 authority,不经 WHATWG 解析。
44
46
  */
45
- export function normalizeUrl(url) {
47
+ export function normalizeUrl(url, label = 'postgres') {
46
48
  const stripped = url.replace(/^jdbc:/i, '');
47
49
  const schemeEnd = stripped.indexOf('://');
48
50
  // 无 authority(非 URL 形态):整串原样保留,交由驱动按既有逻辑处理
@@ -60,8 +62,14 @@ export function normalizeUrl(url) {
60
62
  const ci = seg.lastIndexOf(':');
61
63
  if (ci < 0)
62
64
  return { host: seg };
63
- const port = Number.parseInt(seg.slice(ci + 1), 10);
64
- return Number.isFinite(port) ? { host: seg.slice(0, ci), port } : { host: seg };
65
+ const host = seg.slice(0, ci);
66
+ const rawPort = seg.slice(ci + 1);
67
+ // 空端口段视同未提供(驱动按默认端口探测);非空则必须为 1-65535 整数——历史 parseInt
68
+ // 会静默截断 '1433.5'→1433、NaN 时把整段当主机名,均属静默改写端口。dflt 传 0 仅占位:
69
+ // rawPort 非空不会走到回退分支。
70
+ if (rawPort === '')
71
+ return { host };
72
+ return { host, port: validPort(rawPort, 0, label) };
65
73
  });
66
74
  const prefix = `${stripped.slice(0, schemeEnd)}://${userinfo}`;
67
75
  // 单台不设 hosts(调用方只在多台时探测)
@@ -118,7 +126,7 @@ export async function pickHost(driver, base, hosts, masterOnly) {
118
126
  * 返回最终驱动池配置与最终连接 URL。
119
127
  */
120
128
  export async function pgLikePoolCfg(kind, driver, conn, max) {
121
- const norm = conn.url ? normalizeUrl(conn.url) : null;
129
+ const norm = conn.url ? normalizeUrl(conn.url, kind) : null;
122
130
  const cleanUrl = norm ? norm.prefix + (norm.hosts ?? []).map(joinHostSeg).join(',') + norm.suffix : undefined;
123
131
  let finalUrl = cleanUrl;
124
132
  if (norm?.hosts && norm.hosts.length > 1) {
@@ -81,9 +81,19 @@ function decodeCred(raw) {
81
81
  throw new Error('sqlserver 连接 URL 的用户名或密码 percent 编码非法(如含未编码的 %),请 URL 编码后重试');
82
82
  }
83
83
  }
84
+ /**
85
+ * 端口校验(fail-closed,对齐官方语义):仅「未提供」回退默认端口(tedious 对未提供 port
86
+ * 默认 1433);提供了但不是 1-65535 的整数则抛中文错误——与 tedious 对非法 port 抛
87
+ * RangeError 的行为一致,绝不静默改写用户配置(排障会被假端口误导)。
88
+ */
84
89
  function validPort(v, dflt) {
90
+ if (v === undefined || v === null || v === '')
91
+ return dflt;
85
92
  const n = Number(v);
86
- return Number.isFinite(n) && n > 0 && n <= 65535 ? Math.floor(n) : dflt;
93
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
94
+ throw new Error(`sqlserver 连接的端口取值非法:${JSON.stringify(v)}(须为 1-65535 的整数;留空才使用默认端口 ${dflt})`);
95
+ }
96
+ return n;
87
97
  }
88
98
  /**
89
99
  * URL 脱敏(错误消息可能进 audit.jsonl 与模型上下文,绝不能回显明文密码):
@@ -116,7 +126,7 @@ function poolConfig(conn) {
116
126
  }
117
127
  catch {
118
128
  // 脱敏:错误消息会进 audit.jsonl 与模型上下文,绝不回显 userinfo 明文密码
119
- throw new Error(`sqlserver 连接 URL 无法解析:${redactUrl(conn.url)}(期望形态 sqlserver://user:pass@host:1433/db?encrypt=true;请检查端口是否在 1-65535、是否有多余字符)`);
129
+ throw new Error(`sqlserver 连接 URL 无法解析:${redactUrl(conn.url)}(期望形态 sqlserver://user:pass@host:1433/db?encrypt=true;请检查端口是否在 1-65535、是否有多余字符;命名实例请改用 ?instanceName=实例名 参数)`);
120
130
  }
121
131
  if (!u.hostname)
122
132
  throw new Error('sqlserver 连接 URL 缺少主机名(期望形态 sqlserver://user:pass@host:1433/db)');
@@ -140,7 +150,8 @@ function poolConfig(conn) {
140
150
  options.tdsVersion = tds;
141
151
  }
142
152
  const instance = queryParam(u.searchParams, 'instanceName');
143
- // 指定命名实例时 tedious 会忽略 port,改由 SQL Server Browser(UDP 1434)解析
153
+ // instanceName 与 port 互斥:mssql 层在建连前会 delete 掉 port(tedious 对两者共存直接抛
154
+ // 互斥 Error),端口改由 SQL Server Browser(UDP 1434)按实例名解析
144
155
  if (instance !== undefined && instance !== '')
145
156
  options.instanceName = instance;
146
157
  // fields.database 覆盖 URL 库名:传输端跨库定位(sqlserverLocate)按 pg/mysql 惯例、
@@ -396,7 +407,7 @@ function mssqlReason(e) {
396
407
  base = `服务器要求加密连接:当前 encrypt=false 被服务器拒绝,请改用 ?encrypt=true 后重试(${detail})`;
397
408
  break;
398
409
  case 'EINSTLOOKUP':
399
- base = `实例名解析失败:SQL Server Browser 服务可能未运行或 UDP 1434 不可达(命名实例可改用「主机:端口」直连,如 host,1433)(${detail})`;
410
+ base = `实例名解析失败:SQL Server Browser 服务可能未运行或 UDP 1434 不可达(命名实例可改用「主机,端口」直连,如 host,1433)(${detail})`;
400
411
  break;
401
412
  case 'ENOTOPEN':
402
413
  case 'ECONNCLOSED':
@@ -492,8 +503,10 @@ function rowsOf(res) {
492
503
  function makePoolBox(Driver, cfg) {
493
504
  const pool = new Driver.ConnectionPool(cfg);
494
505
  // 无条件兜底:mssql 的 ConnectionPool 在 acquire 失败(空闲连接被服务器断开等)时会
495
- // emit('error', err),EventEmitter 无监听器时直接抛 → 崩掉宿主进程。
496
- pool.on('error', () => { });
506
+ // emit('error', err),EventEmitter 无监听器时直接抛 → 崩掉宿主进程。这里只留痕不中断请求。
507
+ pool.on('error', (e) => {
508
+ console.error('[sqlserver] 连接池错误(不影响后续请求,连接将按需重建):', e instanceof Error ? e.message : e);
509
+ });
497
510
  let connecting;
498
511
  const ready = () => {
499
512
  if (connecting === undefined) {
@@ -264,6 +264,9 @@ export class ConnectionStore {
264
264
  if (finalUrl !== undefined) {
265
265
  rec.urlSafe = redactUrl(finalUrl);
266
266
  this.secrets.set(id, { url: finalUrl });
267
+ // 对称清理(clearUrl 的反方向):切到 URL 方式保存即放弃分字段配置;残留 fields 会被
268
+ // sqlserver 等适配器的 fields.database 幽灵覆盖 URL 库名(safeUrl 显示新库、实际连旧库的静默错库)
269
+ delete rec.fields;
267
270
  }
268
271
  if (patch.clearUrl && rec.urlSafe !== undefined) {
269
272
  delete rec.urlSafe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
5
5
  "type": "module",
6
6
  "license": "MIT",