@hedeqiang/dsh-nl2sql 0.14.3 → 0.14.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.14.4] - 2026-11-13
9
+
10
+ ### Added
11
+ - Data freshness can be switched off, or hidden from the page only (**Manage → Data health → Settings**). When off, nothing is shown under results or in Schema, no scheduled checks run and the agent's `nl2sql_freshness` tool reports that it is off; "hide on page" keeps checks and notifications but removes the warning bars and the Schema panel. The Data health page keeps showing the data either way.
12
+
8
13
  ## [0.14.3] - 2026-11-13
9
14
 
10
15
  ### Changed
@@ -159,7 +164,8 @@ These features are tested with local databases and fake IM / Lark servers; real
159
164
 
160
165
  - Redesigned UI; Lark push (Markdown and native charts); access control, feedback loop, dashboard global filters; PostgreSQL / Elasticsearch, SSH tunnels, privacy settings, automatic overviews, scheduled reports; semantic layer, ER graph, scan guard, history and audit, dashboards; first version.
161
166
 
162
- [Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.3...HEAD
167
+ [Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.4...HEAD
168
+ [0.14.4]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.3...v0.14.4
163
169
  [0.14.3]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.2...v0.14.3
164
170
  [0.14.2]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.1...v0.14.2
165
171
  [0.14.1]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.0...v0.14.1
package/lib/client.js CHANGED
@@ -17,6 +17,7 @@ window.__ModuleLoader__.load({
17
17
  // ------------------------------------------------------------ locale
18
18
 
19
19
  const zh = {
20
+ 'fq.set.showInUi': "在页面上显示数据新鲜度(查询结果下的滞后提示、表结构页的新鲜度面板)",
20
21
  'mcp.client.claudeCode': 'Claude Code', 'mcp.client.cline': 'Cline', 'mcp.client.windsurf': 'Windsurf', 'mcp.client.codex': 'Codex CLI', 'mcp.client.gemini': 'Gemini CLI', 'mcp.client.zed': 'Zed', 'mcp.client.continue': 'Continue',
21
22
  'mcp.where.claudeCode': '在终端运行(HTTP 直连;下方为 stdio 桥接写法)。', 'mcp.where.cline': '写入 cline_mcp_settings.json(Cline 面板 → MCP Servers → Configure)。', 'mcp.where.windsurf': '写入 Windsurf 的 mcp_config.json(Cascade → MCP → 打开配置文件)。', 'mcp.where.codex': '写入 ~/.codex/config.toml(或项目的 .codex/config.toml)。', 'mcp.where.gemini': '写入 ~/.gemini/settings.json 或项目的 .gemini/settings.json。', 'mcp.where.zed': '写入 Zed 的 settings.json。', 'mcp.where.continue': '保存为工作区 .continue/mcpServers/nl2sql.yaml。',
22
23
  'mcp.test': '测试连接', 'mcp.testOk': '连接正常:发现 {n} 个工具。', 'mcp.testFail': '连接失败:{error}', 'mcp.testFailSteps': '连接测试未通过:',
@@ -80,7 +81,7 @@ window.__ModuleLoader__.load({
80
81
  "fq.admin.qOff": "每日质量巡检已关闭",
81
82
  "fq.admin.noDs": "请先添加数据源",
82
83
  "fq.set.title": "设置",
83
- "fq.set.fEnabled": "定时检查数据新鲜度",
84
+ "fq.set.fEnabled": "启用数据新鲜度(定时检查、滞后通知、Agent 回答近期数据时的提醒);关闭后页面上也不再显示",
84
85
  "fq.set.qEnabled": "每日数据质量巡检",
85
86
  "fq.set.interval": "新鲜度检查间隔(分钟)",
86
87
  "fq.set.time": "质量巡检时间",
@@ -1178,6 +1179,7 @@ window.__ModuleLoader__.load({
1178
1179
  'policy.tablesLabel': '可访问的表',
1179
1180
  }
1180
1181
  const en = {
1182
+ 'fq.set.showInUi': "Show data freshness on the page (stale-data bars under results, the freshness panel in Schema)",
1181
1183
  'mcp.client.claudeCode': 'Claude Code', 'mcp.client.cline': 'Cline', 'mcp.client.windsurf': 'Windsurf', 'mcp.client.codex': 'Codex CLI', 'mcp.client.gemini': 'Gemini CLI', 'mcp.client.zed': 'Zed', 'mcp.client.continue': 'Continue',
1182
1184
  'mcp.where.claudeCode': 'Run in a terminal (HTTP; the stdio bridge variant is below).', 'mcp.where.cline': 'Add to cline_mcp_settings.json (Cline panel → MCP Servers → Configure).', 'mcp.where.windsurf': 'Add to Windsurf\'s mcp_config.json (Cascade → MCP → open config file).', 'mcp.where.codex': 'Add to ~/.codex/config.toml (or the project\'s .codex/config.toml).', 'mcp.where.gemini': 'Add to ~/.gemini/settings.json or the project\'s .gemini/settings.json.', 'mcp.where.zed': 'Add to Zed\'s settings.json.', 'mcp.where.continue': 'Save as .continue/mcpServers/nl2sql.yaml in the workspace.',
1183
1185
  'mcp.test': 'Test connection', 'mcp.testOk': 'Connected: {n} tools discovered.', 'mcp.testFail': 'Connection failed: {error}', 'mcp.testFailSteps': 'Connection test failed:',
@@ -1241,7 +1243,7 @@ window.__ModuleLoader__.load({
1241
1243
  "fq.admin.qOff": "Daily quality patrol is off",
1242
1244
  "fq.admin.noDs": "Add a data source first",
1243
1245
  "fq.set.title": "Settings",
1244
- "fq.set.fEnabled": "Check data freshness on a schedule",
1246
+ "fq.set.fEnabled": "Enable data freshness (scheduled checks, stale-data notifications, warnings in answers about recent data); when off, nothing is shown on the page either",
1245
1247
  "fq.set.qEnabled": "Daily data quality patrol",
1246
1248
  "fq.set.interval": "Freshness check interval (min)",
1247
1249
  "fq.set.time": "Quality patrol time",
@@ -6630,19 +6632,21 @@ window.__ModuleLoader__.load({
6630
6632
  const key = `${datasourceId}\u0000${sql ?? ''}`
6631
6633
  const hit = map.get(key)
6632
6634
  if (hit && Date.now() - hit.at < TTL) return hit.promise
6633
- const promise = call('freshnessFor', { datasourceId, ...(sql ? { sql } : {}) })
6635
+ const promise = call('freshnessFor', { datasourceId, panel: true, ...(sql ? { sql } : {}) })
6634
6636
  map.set(key, { at: Date.now(), promise })
6635
6637
  promise.catch(() => map.delete(key))
6636
6638
  return promise
6637
6639
  },
6638
6640
  async refresh(datasourceId) {
6639
- const v = await call('freshnessFor', { datasourceId, refresh: true })
6641
+ const v = await call('freshnessFor', { datasourceId, refresh: true, panel: true })
6640
6642
  for (const k of [...map.keys()]) if (k.startsWith(`${datasourceId}\u0000`)) map.delete(k)
6641
6643
  map.set(`${datasourceId}\u0000`, { at: Date.now(), promise: Promise.resolve(v) })
6642
6644
  rev++
6643
6645
  for (const f of subs) f()
6644
6646
  return v
6645
6647
  },
6648
+ /** Drop everything (e.g. after the show / enable settings change) and re-render subscribers. */
6649
+ reset() { map.clear(); rev++; for (const f of subs) f() },
6646
6650
  subscribe(f) { subs.add(f); return () => subs.delete(f) },
6647
6651
  rev: () => rev,
6648
6652
  }
@@ -6691,6 +6695,7 @@ window.__ModuleLoader__.load({
6691
6695
  fqCache.get(dsId, sql).then(v => alive && setView(v), () => {})
6692
6696
  return () => { alive = false }
6693
6697
  }, [dsId, sql, rev])
6698
+ if (view?.disabled) return null
6694
6699
  const stale = (view?.tables ?? []).filter(x => x.stale)
6695
6700
  if (!stale.length) return null
6696
6701
  return h('div', { className: 'n2s-fq-bar', role: 'status' },
@@ -6719,7 +6724,7 @@ window.__ModuleLoader__.load({
6719
6724
  return () => { alive = false }
6720
6725
  }, [ds?.id, rev])
6721
6726
  const v = state.value
6722
- if (!ds || state.error || !v || !v.tables?.length) return null
6727
+ if (!ds || state.error || !v || v.disabled || !v.tables?.length) return null
6723
6728
  const stale = v.tables.filter(x => x.stale).length
6724
6729
  const warn = v.tables.filter(x => x.status === 'warn').length
6725
6730
  const expanded = open ?? stale > 0
@@ -6786,6 +6791,7 @@ window.__ModuleLoader__.load({
6786
6791
  const { overrides: _o, ...freshness } = form.freshness
6787
6792
  const out = await call('saveHealthSettings', { freshness: { ...freshness, intervalMin: Number(freshness.intervalMin), staleHours: Number(freshness.staleHours), channelId: freshness.channelId || null }, quality: { ...form.quality, dropPoints: Number(form.quality.dropPoints), channelId: form.quality.channelId || null } })
6788
6793
  setForm(structuredClone(out))
6794
+ fqCache.reset()
6789
6795
  notify(t('fq.saved'), 'ok')
6790
6796
  onSaved?.()
6791
6797
  } catch (e) { notifyError(e) }
@@ -6793,6 +6799,7 @@ window.__ModuleLoader__.load({
6793
6799
  return h('div', { className: 'n2s-card' },
6794
6800
  h('div', { className: 'n2s-form' },
6795
6801
  h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: Boolean(form.freshness.enabled), onChange: e => setF('enabled', e.target.checked) }), t('fq.set.fEnabled')),
6802
+ h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: form.freshness.showInUi !== false && Boolean(form.freshness.enabled), disabled: !form.freshness.enabled, onChange: e => setF('showInUi', e.target.checked) }), t('fq.set.showInUi')),
6796
6803
  h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: Boolean(form.quality.enabled), onChange: e => setQ('enabled', e.target.checked) }), t('fq.set.qEnabled')),
6797
6804
  h('label', { className: 'n2s-field' }, h('span', null, t('fq.set.interval')), h('input', { className: 'n2s-input', type: 'number', min: 5, max: 1440, value: form.freshness.intervalMin, onChange: e => setF('intervalMin', e.target.value) })),
6798
6805
  h('label', { className: 'n2s-field' }, h('span', null, t('fq.set.time')), h('input', { className: 'n2s-input', type: 'time', value: form.quality.time, onChange: e => setQ('time', e.target.value) })),
@@ -6807,7 +6814,7 @@ window.__ModuleLoader__.load({
6807
6814
 
6808
6815
  function HealthFreshnessTable({ t, dsId }) {
6809
6816
  const rev = useFqRev()
6810
- const fr = useAsync(() => (dsId ? fqCache.get(dsId) : null), [dsId, rev])
6817
+ const fr = useAsync(() => (dsId ? call('freshnessFor', { datasourceId: dsId }) : null), [dsId, rev])
6811
6818
  const settings = useAsync(() => call('healthSettings'), [dsId, rev])
6812
6819
  const events = useAsync(() => (dsId ? call('freshnessEvents', { datasourceId: dsId, limit: 10 }) : []), [dsId, rev])
6813
6820
  const [busy, setBusy] = useState(false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hedeqiang/dsh-nl2sql",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "Ask MySQL, PostgreSQL, Elasticsearch and CSV/Excel in plain language inside DeepSeek Harness: charts, dashboards, alerts, and IM push and bots for Lark/Feishu, Telegram, WeCom and DingTalk.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,8 @@ import { UserError, lastDailySlot, newId } from './shared.js'
19
19
 
20
20
  // ------------------------------------------------------------------ constants
21
21
 
22
- export const FRESHNESS_DEFAULTS = { enabled: true, intervalMin: 30, staleHours: 24, overrides: {}, channelId: null }
22
+ // enabled: scheduled checks + notifications + the agent tool; showInUi: warning bars under results and the schema panel.
23
+ export const FRESHNESS_DEFAULTS = { enabled: true, showInUi: true, intervalMin: 30, staleHours: 24, overrides: {}, channelId: null }
23
24
  export const QUALITY_DEFAULTS = { enabled: false, time: '06:30', channelId: null, dropPoints: 10, maxTableRows: 5000000, maxTables: 60 }
24
25
  const DATE_TYPES = new Set(['date', 'datetime', 'timestamp', 'timestamptz'])
25
26
  const NUM_TYPES = new Set(['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint', 'decimal', 'numeric', 'float', 'double', 'real'])
@@ -271,6 +272,7 @@ export const methods = {
271
272
  if (freshness) {
272
273
  const s = this.freshnessState().settings
273
274
  if (freshness.enabled !== undefined) s.enabled = Boolean(freshness.enabled)
275
+ if (freshness.showInUi !== undefined) s.showInUi = Boolean(freshness.showInUi)
274
276
  if (freshness.intervalMin !== undefined) s.intervalMin = Math.round(intIn(freshness.intervalMin, 5, 1440, 'intervalMin'))
275
277
  if (freshness.staleHours !== undefined) s.staleHours = intIn(freshness.staleHours, 0.25, 24 * 90, 'staleHours')
276
278
  if (freshness.channelId !== undefined) s.channelId = channel(freshness.channelId)
@@ -422,9 +424,12 @@ export const methods = {
422
424
  },
423
425
 
424
426
  /** RPC: stored freshness (computed on first use or when refresh); `sql` narrows to referenced tables. */
425
- async freshnessFor({ datasourceId, refresh = false, sql } = {}) {
427
+ async freshnessFor({ datasourceId, refresh = false, sql, panel = false } = {}) {
426
428
  const ds = this.requireDatasource(datasourceId ?? this.state.current)
427
429
  const f = this.freshnessState()
430
+ // Page panels (result bars, schema panel) ask with panel: true; when the feature or its
431
+ // page display is off they get nothing and no query runs.
432
+ if (panel && (!f.settings.enabled || f.settings.showInUi === false)) return { datasourceId: ds.id, disabled: true, tables: [] }
428
433
  let store = f[ds.id]
429
434
  if (refresh || !store) {
430
435
  if (refresh) store = await this.withJobLock(`freshness:${ds.id}`, () => this.refreshFreshness(ds))
@@ -737,6 +742,7 @@ export const FEATURE = {
737
742
  presentCall: (args) => ({ card: 'generic', title: args?.tables?.length ? `数据新鲜度 ${args.tables.join(', ')}` : '数据新鲜度', kind: 'read' }),
738
743
  execute: async (args, exec) => {
739
744
  try {
745
+ if (!service.freshnessState().settings.enabled) return { disabled: true, message: 'Data freshness checks are turned off by the administrator (Data Q&A → Manage → Data health). Answer without freshness warnings.' }
740
746
  return await service.freshnessForSession({ sessionId: exec?.agent?.id, datasource: args?.datasource, tables: args?.tables })
741
747
  } catch (error) {
742
748
  throw error instanceof UserError ? new Error(error.message) : error