@hedeqiang/dsh-nl2sql 0.10.0 → 0.11.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/index.js CHANGED
@@ -18,12 +18,12 @@ import { methods as m2 } from './service/schema.js'
18
18
  import { methods as m3 } from './service/query.js'
19
19
  import { methods as m4 } from './service/dashboards.js'
20
20
  import { methods as m5 } from './service/push.js'
21
- import { methods as m6 } from './service/lark.js'
21
+ import { methods as m6 } from './service/bots.js'
22
+ import { methods as mChannels } from './service/channels.js'
22
23
  import { methods as m7 } from './service/evaluation.js'
23
24
  import { methods as m8 } from './service/alerts.js'
24
25
  import { methods as m9 } from './service/uploads.js'
25
26
  import { methods as m10 } from './service/admin.js'
26
- import { methods as mLarkActions } from './service/lark-actions.js'
27
27
  import { methods as mSuggest } from './service/suggestions.js'
28
28
  const { CHART_TYPES, DEFAULT_SETTINGS, RECORD_KINDS, RPC_PATH, Records, UserError, newId, pick, toUserError } = shared
29
29
 
@@ -36,7 +36,7 @@ class Nl2SqlService {
36
36
  this.config = config ?? {}
37
37
  this.dir = this.config.dataDir ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'nl2sql')
38
38
  this.file = join(this.dir, 'state.json')
39
- this.state = { version: 1, datasources: [], settings: structuredClone(DEFAULT_SETTINGS), current: null, sessions: {}, dashboard: [], knowledge: {}, reports: [], dashboardFilters: [], channels: [], dashboards: [], currentDashboard: null, evals: {}, alerts: [], users: [], larkBot: null }
39
+ this.state = { version: 1, datasources: [], settings: structuredClone(DEFAULT_SETTINGS), current: null, sessions: {}, dashboard: [], knowledge: {}, reports: [], dashboardFilters: [], channels: [], dashboards: [], currentDashboard: null, evals: {}, alerts: [], users: [], botConversations: {} }
40
40
  this.drivers = new Map()
41
41
  this.schemas = new Map()
42
42
  this.results = new Map()
@@ -58,6 +58,8 @@ class Nl2SqlService {
58
58
  this.state.currentDashboard = 'db-default'
59
59
  }
60
60
  this.syncLegacyDashboard()
61
+ // Channels (adapter registry), bots on channels, member identities, bot settings.
62
+ const migrated = [await this.initChannels(), this.migrateUsers(), this.migrateSettings()].some(Boolean)
61
63
  try {
62
64
  this.store = new ResultStore(join(this.dir, 'results.sqlite'))
63
65
  } catch (error) {
@@ -82,8 +84,10 @@ class Nl2SqlService {
82
84
  if (changed) {
83
85
  if (Object.keys(moved).length) console.warn('nl2sql: migrated logs to SQLite: %s', JSON.stringify(moved))
84
86
  await this.save()
87
+ return
85
88
  }
86
89
  }
90
+ if (migrated) await this.save()
87
91
  }
88
92
 
89
93
  /** Keep the legacy `dashboard` / `dashboardFilters` fields pointing at the current dashboard. */
@@ -104,8 +108,7 @@ class Nl2SqlService {
104
108
  if (ids.length > keep) for (const id of ids.slice(0, ids.length - keep)) { delete this.state.sessions[id]; delete this.state.sessionActors?.[id]; delete this.state.evalExclusions?.[id] }
105
109
  for (const id of Object.keys(this.state.sessionActors ?? {})) if (!(id in (this.state.sessions ?? {}))) delete this.state.sessionActors[id]
106
110
  for (const id of Object.keys(this.state.evalExclusions ?? {})) if (!(id in (this.state.sessions ?? {}))) delete this.state.evalExclusions[id]
107
- const convs = Object.values(this.state.larkSessions ?? {}).sort((a, b) => (b.lastAt ?? 0) - (a.lastAt ?? 0))
108
- if (convs.length > 500) for (const c of convs.slice(500)) delete this.state.larkSessions[c.key]
111
+ this.pruneBotConversations()
109
112
  }
110
113
 
111
114
  save() {
@@ -132,9 +135,9 @@ class Nl2SqlService {
132
135
  await Promise.race([Promise.allSettled(jobs), new Promise(r => { timer = setTimeout(r, 3000) })])
133
136
  clearTimeout(timer)
134
137
  }
135
- if (this.larkBot) { try { await this.larkBot.stop() } catch {} }
136
- // Card-button analyses still running in the background (service/lark-actions.js).
137
- if (this.larkActionJobs?.size) await Promise.race([Promise.allSettled([...this.larkActionJobs]), new Promise(r => setTimeout(r, 3000).unref?.())])
138
+ await this.stopAllBots()
139
+ // Button analyses still running in the background (service/bots.js).
140
+ if (this.botActionJobs?.size) await Promise.race([Promise.allSettled([...this.botActionJobs]), new Promise(r => setTimeout(r, 3000).unref?.())])
138
141
  await Promise.allSettled([...this.drivers.values()].map(p => p.then(d => d.close())))
139
142
  this.drivers.clear()
140
143
  await this.writing
@@ -142,7 +145,7 @@ class Nl2SqlService {
142
145
  }
143
146
 
144
147
  }
145
- for (const m of [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, mSuggest, mLarkActions]) Object.defineProperties(Nl2SqlService.prototype, Object.getOwnPropertyDescriptors(m))
148
+ for (const m of [m0, m1, m2, m3, m4, m5, mChannels, m6, m7, m8, m9, m10, mSuggest]) Object.defineProperties(Nl2SqlService.prototype, Object.getOwnPropertyDescriptors(m))
146
149
  const DATASOURCE_PARAM = { type: 'string', description: 'Data source id or name. Omit to use the one bound to this session.' }
147
150
 
148
151
  function textBlock(text) {
@@ -296,15 +299,15 @@ function registerTools(ctx, service) {
296
299
  function registerSendTool(ctx, service) {
297
300
  ctx.tools.register({
298
301
  name: 'nl2sql_send',
299
- description: 'Send a message to an IM channel (Lark / Feishu group or user) configured by the user in the Data Q&A page: a title, a Markdown body, and up to 10 query results by resultId (rendered as native interactive charts, KPI lines or tables). The Markdown supports headings, bold/italic, lists, quotes, code blocks, links, tables (small ones inline, larger ones become table components), <font color=\'green|red|grey\'>…</font> and <at id=all></at>. Only send when the user asks to share/push/send, or when a scheduled report instructs you to. Use action=list to see channels.',
302
+ description: 'Send a message to an IM channel (such as Lark/Feishu, Telegram, WeCom, DingTalk) configured by the user in the Data Q&A page: a title, a Markdown body, and up to 10 query results by resultId. Each platform renders them as natively as it can (Lark: interactive cards with native charts; others: chart images or Markdown tables — see `capabilities` in action=list). The Markdown supports headings, bold/italic, lists, quotes, code blocks, links, tables, <font color=\'green|red|grey\'>…</font> and <at id=all></at>; platforms with basic Markdown drop the <font>/<at> tags. Only send when the user asks to share/push/send, or when a scheduled report instructs you to. Use action=list to see channels.',
300
303
  parameters: {
301
304
  type: 'object',
302
305
  properties: {
303
306
  action: { type: 'string', enum: ['send', 'list'], description: 'send (default) or list channels.' },
304
307
  channel: { type: 'string', description: 'Channel id or name; omit for the default channel.' },
305
- title: { type: 'string', description: 'Card title.' },
308
+ title: { type: 'string', description: 'Message title.' },
306
309
  text: { type: 'string', description: 'Markdown body: summary, conclusions, tables. Write it for the IM reader; do not paste raw result rows that are already attached via resultIds.' },
307
- chartMode: { type: 'string', enum: ['native', 'image', 'table'], description: 'How attached results render: native card charts (default), PNG images (app bots only), or tables.' },
310
+ chartMode: { type: 'string', enum: ['native', 'image', 'table'], description: 'How attached results render: native card charts, PNG images, or tables. Omit to use the channel default; unsupported modes fall back to it.' },
308
311
  resultIds: { type: 'array', items: { type: 'string' }, description: 'resultIds from nl2sql_query / nl2sql_chart / nl2sql_overview to include.' },
309
312
  includeSql: { type: 'boolean', description: 'Also include each result\'s SQL.' },
310
313
  },
@@ -314,7 +317,7 @@ function registerSendTool(ctx, service) {
314
317
  presentCall: (args) => ({ card: 'generic', title: args?.action === 'list' ? '列出推送渠道' : `推送到 ${args?.channel ?? '默认渠道'}:${args?.title ?? ''}`, kind: 'other' }),
315
318
  execute: async (args, exec) => {
316
319
  try {
317
- if (args.action === 'list') return { channels: service.listChannels().map(c => pick(c, ['id', 'name', 'type', 'description', 'isDefault'])) }
320
+ if (args.action === 'list') return { channels: service.listChannels().map(c => ({ ...pick(c, ['id', 'name', 'type', 'platform', 'description', 'isDefault']), ...(c.capabilities ? { capabilities: pick(c.capabilities, ['markdown', 'charts', 'tables', 'chartModes']) } : {}) })) }
318
321
  return await service.sendToChannel({ channelId: args.channel, title: args.title, text: args.text, resultIds: args.resultIds ?? [], includeSql: args.includeSql, chartMode: args.chartMode, source: 'agent', sessionId: exec.agent?.id })
319
322
  } catch (error) {
320
323
  throw error instanceof UserError ? new Error(error.message) : error
@@ -452,11 +455,13 @@ function registerRpc(ctx, service) {
452
455
  deleteChannel: (p) => service.deleteChannel(p.id),
453
456
  testChannel: (p) => service.testChannel(p.id),
454
457
  listChannelChats: (p) => service.listChannelChats(p ?? {}),
458
+ channelCatalog: (p) => service.channelCatalog(p ?? {}),
459
+ listChannelTargets: (p) => service.listChannelTargets(p ?? {}),
455
460
  sendToChannel: (p) => service.sendToChannel({ ...p, source: p.source === 'dashboard' ? 'dashboard' : 'ui' }),
456
461
  sendDashboard: (p) => service.sendDashboard(p),
457
462
  previewAssistantMessage: (p) => service.previewAssistantMessage(p),
458
463
  sendAssistantMessage: (p) => service.sendAssistantMessage(p),
459
- listDeliveries: () => service.listDeliveries(),
464
+ listDeliveries: (p) => service.listDeliveries(p?.limit ?? 100),
460
465
  listHistory: (p) => service.listHistory(p ?? {}),
461
466
  clearHistory: () => service.clearHistory(),
462
467
  deleteHistory: (p) => service.deleteHistory(p ?? {}),
@@ -497,6 +502,9 @@ function registerRpc(ctx, service) {
497
502
  larkBotStatus: () => service.larkBotStatus(),
498
503
  saveLarkBot: (p) => service.saveLarkBot(p),
499
504
  restartLarkBot: async () => { await service.restartLarkBot(); return service.larkBotStatus() },
505
+ botStatus: () => service.botStatus(),
506
+ saveBot: (p) => service.saveBot(p ?? {}),
507
+ restartBot: async (p) => { await service.restartBot({ channelId: p?.channelId }); return (await service.botStatus()).find(b => b.channelId === p?.channelId) ?? null },
500
508
  queryMetric: (p) => service.queryMetric({ ...p, source: 'ui' }),
501
509
  parseUpload: (p) => service.parseUpload(p),
502
510
  importUpload: (p) => service.importUpload(p),
@@ -559,7 +567,7 @@ function registerRpc(ctx, service) {
559
567
  return Response.json({ ok: true, value: value ?? null }, { headers: { 'cache-control': 'no-store' } })
560
568
  } catch (error) {
561
569
  const e = toUserError(error)
562
- return Response.json({ ok: false, error: { code: e.code ?? 'nl2sql/error', message: e.message } }, { headers: { 'cache-control': 'no-store' } })
570
+ return Response.json({ ok: false, error: { code: e.code ?? 'nl2sql/error', message: e.message, ...(e.field ? { field: e.field } : {}) } }, { headers: { 'cache-control': 'no-store' } })
563
571
  }
564
572
  },
565
573
  })
@@ -575,8 +583,8 @@ export async function apply(ctx, config) {
575
583
  registerKnowledgeTool(ctx, service)
576
584
  registerOverviewTool(ctx, service)
577
585
  registerSendTool(ctx, service)
578
- // Inbound Lark bot (long connection), if enabled.
579
- service.restartLarkBot().catch(error => console.warn('nl2sql lark: start failed: %s', error.message))
586
+ // Inbound IM bots (Lark long connection, Telegram polling, …) for every enabled bot channel.
587
+ service.startBots().catch(error => console.warn('nl2sql bot: start failed: %s', error.message))
580
588
  service.scheduleAlerts()
581
589
  registerAnalysisTools(ctx, service)
582
590
  }