@hedeqiang/dsh-nl2sql 0.11.1 → 0.12.0
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 +66 -31
- package/README.md +144 -58
- package/README.zh-CN.md +175 -0
- package/channels/dingtalk.js +3 -3
- package/channels/index.js +1 -1
- package/channels/lark.js +35 -7
- package/channels/message.js +19 -2
- package/channels/telegram.js +20 -4
- package/channels/wecom.js +2 -2
- package/im.js +1 -1
- package/index.js +3 -1
- package/lark-bot.js +1 -1
- package/lib/client.js +188 -28
- package/package.json +8 -4
- package/service/access.js +38 -1
- package/service/bots.js +77 -10
- package/service/channels.js +23 -7
- package/service/push.js +2 -2
- package/service/suggestions.js +1 -1
- package/README.en.md +0 -63
- /package/docs/{CHANNELS.md → channels.md} +0 -0
package/lib/client.js
CHANGED
|
@@ -15,6 +15,32 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
// ------------------------------------------------------------ locale
|
|
16
16
|
|
|
17
17
|
const zh = {
|
|
18
|
+
'mb.pick.people': "从和机器人说过话的人中选择",
|
|
19
|
+
'mb.pick.chats': "从机器人所在的群中选择",
|
|
20
|
+
'mb.pick.refresh': "刷新",
|
|
21
|
+
'mb.pick.noPeople': "还没有人和机器人说过话。先让对方在群里 @机器人 或私聊发一句话(会提示未开通),这里就会出现,点一下即可添加。",
|
|
22
|
+
'mb.pick.noChats': "还没有可选的群。把机器人拉进群并 @它一次,或确认应用已开通「获取群信息」权限。",
|
|
23
|
+
'mb.pick.peopleHint': "点选即可自动填入账号 ID 和名称,也可以在下方手动填写。",
|
|
24
|
+
'mb.pick.chatsHint': "点选群即可自动填入群 ID 和群名;群内所有成员按该群的权限使用。",
|
|
25
|
+
'mb.pick.via': "来自渠道:{name}",
|
|
26
|
+
'mb.pick.taken': "已是成员「{name}」",
|
|
27
|
+
'mb.pick.refused': "曾因未开通被拒绝",
|
|
28
|
+
'mb.pick.pending': "待开通",
|
|
29
|
+
'mb.pick.added': "已添加",
|
|
30
|
+
'mb.pending.title': "有 {n} 个人或群向机器人提问时因未开通被拒绝,点一下即可添加为成员:",
|
|
31
|
+
'mb.pending.add': "添加",
|
|
32
|
+
'dash.sourceOf': "数据源",
|
|
33
|
+
'dash.sourceMissing': "该图表的数据源已删除,无法刷新",
|
|
34
|
+
'dash.sourceMissingShort': "数据源已删除",
|
|
35
|
+
'dash.scope': "显示范围",
|
|
36
|
+
'dash.scopeAll': "全部数据源",
|
|
37
|
+
'dash.scopeCurrent': "仅 {name}",
|
|
38
|
+
'dash.sources': "包含数据源:{list}",
|
|
39
|
+
'dash.scopeEmpty': "这个看板里没有「{name}」的图表",
|
|
40
|
+
'dash.scopeEmptyHint': "另有 {n} 个其他数据源的图表被隐藏。",
|
|
41
|
+
'im.attachFull': "超过 {n} 行的结果附带完整数据 Excel",
|
|
42
|
+
'im.rowsLimit': "消息中每个结果最多显示 {n} 行,超出部分会注明;完整数据可在页面导出 Excel",
|
|
43
|
+
'im.sentFile': "已附带完整数据 Excel",
|
|
18
44
|
"settings.botDefaultRole": "未登记的用户",
|
|
19
45
|
"settings.larkDefaultRole": "未登记的用户",
|
|
20
46
|
"ch.savedTestFailedMsg": "已保存,但测试失败:{msg}",
|
|
@@ -1006,6 +1032,32 @@ window.__ModuleLoader__.load({
|
|
|
1006
1032
|
'error.generic': '操作失败:{message}',
|
|
1007
1033
|
}
|
|
1008
1034
|
const en = {
|
|
1035
|
+
'mb.pick.people': "Pick someone who talked to a bot",
|
|
1036
|
+
'mb.pick.chats': "Pick a group the bot is in",
|
|
1037
|
+
'mb.pick.refresh': "Refresh",
|
|
1038
|
+
'mb.pick.noPeople': "Nobody has talked to a bot yet. Ask them to @-mention the bot or DM it once (they will be told they have no access); they then appear here for one-click adding.",
|
|
1039
|
+
'mb.pick.noChats': "No groups to pick yet. Add the bot to a group and @-mention it once, or check the app can read chat info.",
|
|
1040
|
+
'mb.pick.peopleHint': "Picking fills in the account id and name; you can also type them below.",
|
|
1041
|
+
'mb.pick.chatsHint': "Picking fills in the chat id and name; everyone in the group uses its access.",
|
|
1042
|
+
'mb.pick.via': "via channel: {name}",
|
|
1043
|
+
'mb.pick.taken': "already member \"{name}\"",
|
|
1044
|
+
'mb.pick.refused': "was refused (no access)",
|
|
1045
|
+
'mb.pick.pending': "pending",
|
|
1046
|
+
'mb.pick.added': "added",
|
|
1047
|
+
'mb.pending.title': "{n} people or groups were refused by a bot because they are not members. Click to add:",
|
|
1048
|
+
'mb.pending.add': "Add",
|
|
1049
|
+
'dash.sourceOf': "Data source",
|
|
1050
|
+
'dash.sourceMissing': "This chart's data source was deleted; it cannot refresh",
|
|
1051
|
+
'dash.sourceMissingShort': "source deleted",
|
|
1052
|
+
'dash.scope': "Show",
|
|
1053
|
+
'dash.scopeAll': "All sources",
|
|
1054
|
+
'dash.scopeCurrent': "Only {name}",
|
|
1055
|
+
'dash.sources': "Sources: {list}",
|
|
1056
|
+
'dash.scopeEmpty': "No charts from \"{name}\" on this dashboard",
|
|
1057
|
+
'dash.scopeEmptyHint': "{n} charts from other sources are hidden.",
|
|
1058
|
+
'im.attachFull': "Attach full data as Excel for results over {n} rows",
|
|
1059
|
+
'im.rowsLimit': "Messages show up to {n} rows per result and say when rows are omitted; export Excel on the page for full data",
|
|
1060
|
+
'im.sentFile': "full data attached as Excel",
|
|
1009
1061
|
"settings.botDefaultRole": "Unregistered users",
|
|
1010
1062
|
"settings.larkDefaultRole": "Unregistered users",
|
|
1011
1063
|
"ch.savedTestFailedMsg": "Saved, but the test failed: {msg}",
|
|
@@ -1918,7 +1970,17 @@ window.__ModuleLoader__.load({
|
|
|
1918
1970
|
.n2s-morebtn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;height:36px;margin-top:6px;border:.5px dashed var(--n2s-line-2);border-radius:var(--n2s-radius-sm);background:transparent;color:var(--n2s-muted);font:inherit;font-size:13px;cursor:pointer}
|
|
1919
1971
|
.n2s-morebtn:hover{background:var(--n2s-hover);color:var(--dsw-alias-label-primary)}
|
|
1920
1972
|
.n2s-btn.danger-text{color:var(--n2s-err)}
|
|
1973
|
+
/* ---- member identity picker ---- */
|
|
1974
|
+
.n2s-pickchips{display:flex;flex-wrap:wrap;gap:6px}
|
|
1975
|
+
.n2s-pickchip{display:inline-flex;align-items:center;gap:6px;max-width:100%;height:30px;padding:0 10px;border:.5px solid var(--n2s-line-2);border-radius:999px;background:var(--n2s-surface);color:inherit;font:inherit;font-size:12.5px;cursor:pointer}
|
|
1976
|
+
.n2s-pickchip:hover:not(:disabled){background:var(--n2s-hover)}
|
|
1977
|
+
.n2s-pickchip[aria-pressed=true]{border-color:var(--n2s-accent);background:var(--n2s-accent-soft,rgba(51,112,255,.08));color:var(--n2s-accent)}
|
|
1978
|
+
.n2s-pickchip:disabled{opacity:.55;cursor:default}
|
|
1979
|
+
.n2s-pickchip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px}
|
|
1980
|
+
.n2s-idpicker{gap:6px}
|
|
1921
1981
|
/* ---- dashboard v2 ---- */
|
|
1982
|
+
.n2s-srcbadge{display:inline-flex;align-items:center;gap:3px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:none}
|
|
1983
|
+
.n2s-seg.small button{height:26px;padding:0 10px;font-size:12px}
|
|
1922
1984
|
.n2s-dash{grid-template-columns:repeat(6,minmax(0,1fr));align-items:start}
|
|
1923
1985
|
.n2s-widget.w-third{grid-column:span 2}
|
|
1924
1986
|
.n2s-widget.w-half{grid-column:span 3}
|
|
@@ -2096,6 +2158,10 @@ window.__ModuleLoader__.load({
|
|
|
2096
2158
|
/** Current UI language ('zh' | 'en'), updated by the page/tool views on render. */
|
|
2097
2159
|
let uiLang = 'zh'
|
|
2098
2160
|
// LocaleSnapshot is { active, locales, revision } (dsh-client-locale); older hosts passed a string.
|
|
2161
|
+
/** Join a list for display in the UI language: 「a、b」 / "a, b". */
|
|
2162
|
+
const joinList = (items, sep) => (items ?? []).filter(x => x !== undefined && x !== null && x !== '').join(sep ?? (uiLang === 'zh' ? '、' : ', '))
|
|
2163
|
+
/** Label + colon in the UI language: 「标签:」 / "Label: ". */
|
|
2164
|
+
const colon = (label) => (uiLang === 'zh' ? `${label}:` : `${label}: `)
|
|
2099
2165
|
const setUiLang = (snapshot) => {
|
|
2100
2166
|
const id = typeof snapshot === 'string' ? snapshot : (snapshot?.active ?? snapshot?.locale ?? 'zh')
|
|
2101
2167
|
uiLang = /^zh/i.test(String(id)) ? 'zh' : 'en'
|
|
@@ -3280,7 +3346,7 @@ window.__ModuleLoader__.load({
|
|
|
3280
3346
|
if (section === 'glossary') list = k.glossary.length ? k.glossary.map(g => item(g.id, h(React.Fragment, null, g.term, g.synonyms?.length ? h('span', { className: 'n2s-muted n2s-small' }, ` ${g.synonyms.join(' / ')}`) : null),
|
|
3281
3347
|
h('div', null, h('div', null, g.definition), g.sql && h('code', { className: 'n2s-md-code', style: { display: 'inline-block', marginTop: 6 } }, g.sql)), 'glossary', g)) : empty(t('kb.glossaryEmpty'))
|
|
3282
3348
|
if (section === 'metric') list = k.metrics.length ? k.metrics.map(m => item(m.id, h(React.Fragment, null, m.name, m.unit ? h('span', { className: 'n2s-badge', style: { marginLeft: 6 } }, m.unit) : null),
|
|
3283
|
-
h('div', null, h('div', { className: 'n2s-mono n2s-small' }, `${m.expression}${m.table ? ` FROM ${m.table}` : ''}${m.filter ? ` WHERE ${m.filter}` : ''}`), m.timeColumn && h('div', { className: 'n2s-muted n2s-small' }, `${t('kb.timeColumn')}: ${m.timeColumn}${m.dimensions?.length ? ` · ${t('kb.dimensions')}: ${m.dimensions.join('、')}` : ''}${m.synonyms?.length ? ` · ${t('kb.synonymsLabel')}: ${m.synonyms.join('、')}` : ''}`), m.description && h('div', { className: 'n2s-muted n2s-small' }, m.description)), 'metric', m)) : empty(t('kb.metricsEmpty'))
|
|
3349
|
+
h('div', null, h('div', { className: 'n2s-mono n2s-small' }, `${m.expression}${m.table ? ` FROM ${m.table}` : ''}${m.filter ? ` WHERE ${m.filter}` : ''}`), m.timeColumn && h('div', { className: 'n2s-muted n2s-small' }, `${t('kb.timeColumn')}: ${m.timeColumn}${m.dimensions?.length ? ` · ${t('kb.dimensions')}: ${m.dimensions.join(uiLang === 'zh' ? '、' : ', ')}` : ''}${m.synonyms?.length ? ` · ${t('kb.synonymsLabel')}: ${m.synonyms.join(uiLang === 'zh' ? '、' : ', ')}` : ''}`), m.description && h('div', { className: 'n2s-muted n2s-small' }, m.description)), 'metric', m)) : empty(t('kb.metricsEmpty'))
|
|
3284
3350
|
if (section === 'note') {
|
|
3285
3351
|
const entries = Object.entries(k.notes).sort(([a], [b]) => a.localeCompare(b))
|
|
3286
3352
|
list = h(React.Fragment, null,
|
|
@@ -3465,7 +3531,7 @@ window.__ModuleLoader__.load({
|
|
|
3465
3531
|
h('span', { className: 'n2s-tile', 'aria-hidden': true }, svg(16, [h('circle', { key: 1, cx: 8, cy: 8, r: 5.5 }), h('path', { key: 2, d: 'M8 5v3.2l2 1.3' })])),
|
|
3466
3532
|
h('div', { className: 'n2s-grow', style: { minWidth: 0 } },
|
|
3467
3533
|
h('div', { className: 'n2s-row', style: { gap: 6 } }, h('span', { className: 'n2s-list2-title' }, r.title), r.channelName && h('span', { className: 'n2s-badge' }, Icon.send(11), r.channelName)),
|
|
3468
|
-
h('div', { className: 'n2s-list2-sub' }, `${t(`rep.period.${r.period}`)} ${r.period === 'weekly' ? r.weekdays.map(d => t(`rep.wd.${d}`)).join('、') + ' ' : ''}${r.time.slice(0, 5)} · ${r.datasourceName} · ${r.scope === 'question' ? r.question : t('rep.scope.dashboard')}`)),
|
|
3534
|
+
h('div', { className: 'n2s-list2-sub' }, `${t(`rep.period.${r.period}`)} ${r.period === 'weekly' ? r.weekdays.map(d => t(`rep.wd.${d}`)).join(uiLang === 'zh' ? '、' : ', ') + ' ' : ''}${r.time.slice(0, 5)} · ${r.datasourceName} · ${r.scope === 'question' ? r.question : t('rep.scope.dashboard')}`)),
|
|
3469
3535
|
h('div', { className: 'n2s-list2-side' },
|
|
3470
3536
|
statusDot(r.status),
|
|
3471
3537
|
h('span', { className: 'n2s-faint n2s-small' }, r.scheduledAt && r.status === 'active' ? `${t('rep.next')} ${new Date(r.scheduledAt).toLocaleString([], { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })}` : '')),
|
|
@@ -3643,7 +3709,7 @@ window.__ModuleLoader__.load({
|
|
|
3643
3709
|
|
|
3644
3710
|
// ------------------------------------------- channels: catalog & shapes
|
|
3645
3711
|
// The host may be older (flat Lark-only channels, no channelCatalog /
|
|
3646
|
-
// botStatus RPCs) or newer (docs/
|
|
3712
|
+
// botStatus RPCs) or newer (docs/channels.md). Everything below normalizes
|
|
3647
3713
|
// both so the pages only deal with { platform, type, config, secretsSet,
|
|
3648
3714
|
// capabilities, bot }.
|
|
3649
3715
|
|
|
@@ -3993,7 +4059,7 @@ window.__ModuleLoader__.load({
|
|
|
3993
4059
|
/** Modal dialog sending a title, text and results to an IM channel. */
|
|
3994
4060
|
function SendDialog({ t, initial, onClose }) {
|
|
3995
4061
|
const channels = useChannels()
|
|
3996
|
-
const [form, setForm] = useState(() => ({ channelId: '', title: initial.title ?? '', text: initial.text ?? '', includeSql: false, chartMode: '' }))
|
|
4062
|
+
const [form, setForm] = useState(() => ({ channelId: '', title: initial.title ?? '', text: initial.text ?? '', includeSql: false, chartMode: '', attachFull: true }))
|
|
3997
4063
|
const [resultIds, setResultIds] = useState(initial.resultIds ?? [])
|
|
3998
4064
|
const [loading, setLoading] = useState(Boolean(initial.message))
|
|
3999
4065
|
useEffect(() => {
|
|
@@ -4021,9 +4087,9 @@ window.__ModuleLoader__.load({
|
|
|
4021
4087
|
setState({ busy: true })
|
|
4022
4088
|
try {
|
|
4023
4089
|
const res = initial.dashboard
|
|
4024
|
-
? await call('sendDashboard', { channelId: form.channelId, title: form.title, ...(form.chartMode ? { chartMode: form.chartMode } : {}) })
|
|
4025
|
-
: await call('sendToChannel', { channelId: form.channelId, title: form.title, text: form.text, resultIds, includeSql: form.includeSql, ...(form.chartMode ? { chartMode: form.chartMode } : {}), sessionId: initial.message?.sessionId })
|
|
4026
|
-
setState({ ok: fmt(t, 'im.sent', { channel: res.channel, images: res.images }) })
|
|
4090
|
+
? await call('sendDashboard', { channelId: form.channelId, title: form.title, attachFull: form.attachFull, ...(form.chartMode ? { chartMode: form.chartMode } : {}) })
|
|
4091
|
+
: await call('sendToChannel', { channelId: form.channelId, title: form.title, text: form.text, resultIds, includeSql: form.includeSql, attachFull: form.attachFull, ...(form.chartMode ? { chartMode: form.chartMode } : {}), sessionId: initial.message?.sessionId })
|
|
4092
|
+
setState({ ok: `${fmt(t, 'im.sent', { channel: res.channel, images: res.images })}${res.files ? ` · ${t('im.sentFile')}` : ''}`, warn: res.warning })
|
|
4027
4093
|
refreshChannels()
|
|
4028
4094
|
setTimeout(onClose, 1200)
|
|
4029
4095
|
} catch (e) {
|
|
@@ -4047,9 +4113,14 @@ window.__ModuleLoader__.load({
|
|
|
4047
4113
|
h('span', { className: 'n2s-small' }, t('im.chartMode')),
|
|
4048
4114
|
channel?.capabilities?.charts === 'none' && h('span', { className: 'n2s-faint n2s-small' }, t('ch.tablesOnly')),
|
|
4049
4115
|
h('div', { className: 'n2s-seg', role: 'group', 'aria-label': t('im.chartMode') }, ['', ...chartModesFor(channel?.capabilities, channel?.type)].map(v => h('button', { key: v || 'def', type: 'button', 'aria-pressed': form.chartMode === v, onClick: () => setForm(f => ({ ...f, chartMode: v })) }, t(v ? `im.cm.${v}` : 'im.cm.default'))))),
|
|
4050
|
-
!initial.dashboard && resultIds.length > 0 && h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: form.includeSql, onChange: e => setForm(f => ({ ...f, includeSql: e.target.checked })) }), t('im.includeSql'))
|
|
4116
|
+
!initial.dashboard && resultIds.length > 0 && h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: form.includeSql, onChange: e => setForm(f => ({ ...f, includeSql: e.target.checked })) }), t('im.includeSql')),
|
|
4117
|
+
(resultIds.length > 0 || initial.dashboard) && h('div', { className: 'n2s-field' },
|
|
4118
|
+
channel?.capabilities?.files
|
|
4119
|
+
? h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: form.attachFull, onChange: e => setForm(f => ({ ...f, attachFull: e.target.checked })) }), fmt(t, 'im.attachFull', { n: channel.capabilities.tableRows ?? 10 }))
|
|
4120
|
+
: h('span', { className: 'n2s-faint n2s-small' }, fmt(t, 'im.rowsLimit', { n: channel?.capabilities?.tableRows ?? 10 })))),
|
|
4051
4121
|
state?.error && h('div', { className: 'n2s-notice err', style: { marginTop: 10 } }, state.error),
|
|
4052
4122
|
state?.ok && h('div', { className: 'n2s-notice ok', style: { marginTop: 10 } }, state.ok),
|
|
4123
|
+
state?.warn && h('div', { className: 'n2s-notice warn', style: { marginTop: 8 } }, state.warn),
|
|
4053
4124
|
h('div', { className: 'n2s-row', style: { marginTop: 14, justifyContent: 'flex-end' } },
|
|
4054
4125
|
h('button', { type: 'button', className: 'n2s-btn ghost', onClick: onClose }, t('ds.cancel')),
|
|
4055
4126
|
h('button', { type: 'button', className: 'n2s-btn primary', disabled: !form.channelId || !form.title.trim() || state?.busy || loading, onClick: send }, state?.busy ? h('span', { className: 'n2s-spin' }) : null, t('im.send'))))), document.body)
|
|
@@ -4411,7 +4482,7 @@ window.__ModuleLoader__.load({
|
|
|
4411
4482
|
h('td', { title: d.title }, d.title),
|
|
4412
4483
|
h('td', { className: 'n2s-muted' }, tOpt(t, `im.src.${d.source}`) || d.source),
|
|
4413
4484
|
h('td', { title: d.error ?? '' }, d.status === 'ok'
|
|
4414
|
-
? h('span', { className: 'n2s-statusdot ok' }, [t('im.sentOk'), d.results ? fmt(t, 'im.resultsN', { count: d.results }) : ''].filter(Boolean).join(' · '))
|
|
4485
|
+
? h('span', { className: 'n2s-statusdot ok' }, [t('im.sentOk'), d.results ? fmt(t, 'im.resultsN', { count: d.results }) : '', d.files ? t('im.sentFile') : '', d.error ? d.error : ''].filter(Boolean).join(' · '))
|
|
4415
4486
|
: h('span', { className: 'n2s-statusdot err' }, d.error ?? t('im.lastFailed')))))))))
|
|
4416
4487
|
}
|
|
4417
4488
|
|
|
@@ -4465,7 +4536,7 @@ window.__ModuleLoader__.load({
|
|
|
4465
4536
|
const HIDDEN_PIN = { hidden: true }
|
|
4466
4537
|
const WIDGET_WIDTHS = ['third', 'half', 'full']
|
|
4467
4538
|
|
|
4468
|
-
function DashboardWidget({ t, widget, onRemove, onChanged, onMove, tick, first, last, filters, filterCount, otherBoards = [], onTransfer, onLoaded, drag }) {
|
|
4539
|
+
function DashboardWidget({ t, widget, onRemove, onChanged, onMove, tick, first, last, filters, filterCount, otherBoards = [], onTransfer, onLoaded, drag, source, showSource }) {
|
|
4469
4540
|
const [state, setState] = useState({ busy: true })
|
|
4470
4541
|
const [local, setLocal] = useState(0)
|
|
4471
4542
|
useEffect(() => {
|
|
@@ -4491,6 +4562,7 @@ window.__ModuleLoader__.load({
|
|
|
4491
4562
|
onKeyDown: (e) => { if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { e.preventDefault(); if (!first) onMove(-1) } if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { e.preventDefault(); if (!last) onMove(1) } },
|
|
4492
4563
|
}, svg(14, [h('circle', { key: 1, cx: 6, cy: 4, r: .9, fill: 'currentColor' }), h('circle', { key: 2, cx: 10, cy: 4, r: .9, fill: 'currentColor' }), h('circle', { key: 3, cx: 6, cy: 8, r: .9, fill: 'currentColor' }), h('circle', { key: 4, cx: 10, cy: 8, r: .9, fill: 'currentColor' }), h('circle', { key: 5, cx: 6, cy: 12, r: .9, fill: 'currentColor' }), h('circle', { key: 6, cx: 10, cy: 12, r: .9, fill: 'currentColor' })])),
|
|
4493
4564
|
h('span', { className: 'title', title: widget.title }, widget.title),
|
|
4565
|
+
showSource && h('span', { className: `n2s-badge n2s-srcbadge${source ? '' : ' warn'}`, title: source ? `${t('dash.sourceOf')}: ${source.name} · ${typeLabel(t, source.type)}` : t('dash.sourceMissing') }, Icon.db(11), source ? source.name : t('dash.sourceMissingShort')),
|
|
4494
4566
|
state.busy ? h('span', { className: 'n2s-spin n2s-faint', 'aria-label': t('dash.loading') }) : ago && h('span', { className: 'n2s-faint n2s-small n2s-hide-narrow', title: state.result?.cached ? fmt(t, 'dash.cachedAt', { time: new Date(state.result.cachedAt).toLocaleTimeString() }) : t('dash.updatedAt') }, state.result?.cached ? `${ago} · ${t('dash.cached')}` : ago),
|
|
4495
4567
|
state.result?.filters?.length > 0 && h('span', { className: 'n2s-badge blue', title: state.result.filters.map(f => f.description).join('\n') }, fmt(t, 'filter.appliedCount', { count: state.result.filters.length })),
|
|
4496
4568
|
state.result && filterCount > 0 && !(state.result.filters?.length) && h('span', { className: 'n2s-badge', title: t('filter.notApplied') }, t('filter.notAppliedShort')),
|
|
@@ -4511,7 +4583,7 @@ window.__ModuleLoader__.load({
|
|
|
4511
4583
|
state.result && h(ResultView, { result: widgetResult, t, compact: true, embedded: true, onPinned: HIDDEN_PIN }))
|
|
4512
4584
|
}
|
|
4513
4585
|
|
|
4514
|
-
function DashboardTab({ t }) {
|
|
4586
|
+
function DashboardTab({ t, ds, datasources = [] }) {
|
|
4515
4587
|
const [boardId, setBoardId] = useState(null)
|
|
4516
4588
|
const list = useAsync(() => call('listDashboard'), [boardId])
|
|
4517
4589
|
const boards = useAsync(() => call('listDashboards'), [boardId])
|
|
@@ -4526,7 +4598,17 @@ window.__ModuleLoader__.load({
|
|
|
4526
4598
|
const [order, setOrder] = useState(null)
|
|
4527
4599
|
useEffect(() => { setOrder(null) }, [list.value])
|
|
4528
4600
|
const serverWidgets = list.value ?? []
|
|
4529
|
-
const
|
|
4601
|
+
const allWidgets = order ? order.map(id => serverWidgets.find(w => w.id === id)).filter(Boolean) : serverWidgets
|
|
4602
|
+
// Widgets keep the data source they were pinned from; a board may mix sources.
|
|
4603
|
+
// Scope: all sources, or only the one selected at the top right (remembered per browser).
|
|
4604
|
+
const [scope, setScopeState] = useState(() => { try { return localStorage.getItem('nl2sql:dashScope') === 'current' ? 'current' : 'all' } catch { return 'all' } })
|
|
4605
|
+
const setScope = (v) => { setScopeState(v); try { localStorage.setItem('nl2sql:dashScope', v) } catch {} }
|
|
4606
|
+
const sourceIds = [...new Set(allWidgets.map(w => w.datasourceId))]
|
|
4607
|
+
const mixed = sourceIds.length > 1
|
|
4608
|
+
const dsById = new Map(datasources.map(d => [d.id, d]))
|
|
4609
|
+
const widgets = scope === 'current' && ds ? allWidgets.filter(w => w.datasourceId === ds.id) : allWidgets
|
|
4610
|
+
const hiddenByScope = allWidgets.length - widgets.length
|
|
4611
|
+
const showSource = mixed || (ds && sourceIds.some(id => id !== ds.id))
|
|
4530
4612
|
const saved = useAsync(() => call('getDashboardFilters'), [boardId])
|
|
4531
4613
|
const [filters, setFilters] = useState(null)
|
|
4532
4614
|
const [filtersFor, setFiltersFor] = useState(null)
|
|
@@ -4548,7 +4630,11 @@ window.__ModuleLoader__.load({
|
|
|
4548
4630
|
// Drag and drop.
|
|
4549
4631
|
const [dragId, setDragId] = useState(null)
|
|
4550
4632
|
const [over, setOver] = useState(null) // { id, pos: 'before'|'after' }
|
|
4551
|
-
const commitOrder = async (
|
|
4633
|
+
const commitOrder = async (visibleIds) => {
|
|
4634
|
+
// Hidden (other-source) widgets keep their slots; visible ones take the new order.
|
|
4635
|
+
const queue = [...visibleIds]
|
|
4636
|
+
const visible = new Set(visibleIds)
|
|
4637
|
+
const ids = allWidgets.map(w => (visible.has(w.id) ? queue.shift() : w.id))
|
|
4552
4638
|
setOrder(ids)
|
|
4553
4639
|
try { await call('reorderWidgets', { dashboardId: boards.value?.current, ids }) } catch (e) { notifyError(e); setOrder(null) }
|
|
4554
4640
|
list.reload()
|
|
@@ -4587,9 +4673,13 @@ window.__ModuleLoader__.load({
|
|
|
4587
4673
|
}
|
|
4588
4674
|
return h('div', null,
|
|
4589
4675
|
h(DashboardSwitcher, { t, value: { id: boardId }, onChange: (id) => setBoardId(`${id}:${Date.now()}`) }),
|
|
4590
|
-
|
|
4676
|
+
allWidgets.length > 0 && h('div', { className: 'n2s-toolbar n2s-dashbar' },
|
|
4591
4677
|
h('div', { className: 'n2s-grow', style: { minWidth: 0 } },
|
|
4592
|
-
h('div', { className: 'n2s-
|
|
4678
|
+
h('div', { className: 'n2s-row', style: { gap: 10 } },
|
|
4679
|
+
h('div', { className: 'n2s-h2', style: { margin: 0 } }, fmt(t, 'dash.count', { count: widgets.length })),
|
|
4680
|
+
(mixed || hiddenByScope > 0 || (ds && sourceIds.some(id => id !== ds.id))) && h('div', { className: 'n2s-seg small', role: 'group', 'aria-label': t('dash.scope') },
|
|
4681
|
+
['all', 'current'].map(v => h('button', { key: v, type: 'button', 'aria-pressed': scope === v, disabled: v === 'current' && !ds, onClick: () => setScope(v) }, v === 'all' ? t('dash.scopeAll') : fmt(t, 'dash.scopeCurrent', { name: ds?.name ?? '' }))))),
|
|
4682
|
+
showSource && h('div', { className: 'n2s-faint n2s-small' }, fmt(t, 'dash.sources', { list: sourceIds.map(id => dsById.get(id)?.name ?? t('dash.sourceMissingShort')).join(uiLang === 'zh' ? '、' : ', ') })),
|
|
4593
4683
|
h('div', { className: 'n2s-faint n2s-small', role: 'status', 'aria-live': 'polite' },
|
|
4594
4684
|
loadingN > 0 ? fmt(t, 'dash.loadingN', { n: loadingN }) : lastAt ? fmt(t, 'dash.lastRefresh', { time: new Date(lastAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) }) : '',
|
|
4595
4685
|
cachedN > 0 && loadingN === 0 ? h('span', null, ' · ', h('button', { type: 'button', className: 'n2s-link', title: t('dash.cachedHint'), onClick: () => setTick(x => x + 1) }, fmt(t, 'dash.cachedN', { n: cachedN }))) : null,
|
|
@@ -4606,11 +4696,12 @@ window.__ModuleLoader__.load({
|
|
|
4606
4696
|
widgets.length > 0 && filtersReady && h(FilterBar, { key: `f:${boardId ?? 'b'}`, t, filters, onChange: changeFilters }),
|
|
4607
4697
|
sendingDash && h(SendDialog, { t, initial: { title: t('im.dashboardTitle'), dashboard: true }, onClose: () => setSendingDash(false) }),
|
|
4608
4698
|
list.error && h('div', { className: 'n2s-notice err' }, list.error.message),
|
|
4609
|
-
!list.loading && widgets.length === 0 && h('div', { className: 'n2s-empty' }, h('h3', null, t
|
|
4699
|
+
!list.loading && widgets.length === 0 && hiddenByScope > 0 && h('div', { className: 'n2s-empty' }, h('h3', null, fmt(t, 'dash.scopeEmpty', { name: ds?.name ?? '' })), h('div', null, fmt(t, 'dash.scopeEmptyHint', { n: hiddenByScope })), h('button', { type: 'button', className: 'n2s-btn', onClick: () => setScope('all') }, t('dash.scopeAll'))),
|
|
4700
|
+
!list.loading && allWidgets.length === 0 && h('div', { className: 'n2s-empty' }, h('h3', null, t('dash.empty')), h('div', null, t('dash.emptyHint')), h('button', { type: 'button', className: 'n2s-btn', onClick: () => fbCtx.navigate?.({ tab: 'ask' }) }, t('dash.goAsk'))),
|
|
4610
4701
|
widgets.length > 1 && h('div', { className: 'n2s-faint n2s-small n2s-hide-narrow', style: { margin: '-4px 0 8px' } }, t('dash.dragTip')),
|
|
4611
4702
|
filtersReady && !list.loading && h('div', { className: `n2s-dash${dragId ? ' is-dragging' : ''}`, key: `d:${boardId ?? 'b'}` }, widgets.map((w, i) => h(DashboardWidget, {
|
|
4612
4703
|
key: w.id, t, widget: w, tick, first: i === 0, last: i === widgets.length - 1, filters: filters ?? [], filterCount: (filters ?? []).length,
|
|
4613
|
-
onChanged: () => list.reload(), onLoaded, drag: dragProps(w),
|
|
4704
|
+
onChanged: () => list.reload(), onLoaded, drag: dragProps(w), source: dsById.get(w.datasourceId), showSource,
|
|
4614
4705
|
onMove: (delta) => move(w, delta),
|
|
4615
4706
|
onRemove: async () => { if (!await confirmDialog({ title: fmt(t, 'dash.removeConfirm', { title: w.title }), danger: true })) return; try { await call('unpinWidget', { id: w.id }); notify(t('dash.removed'), 'ok') } catch (e) { notifyError(e) } list.reload() },
|
|
4616
4707
|
otherBoards, onTransfer: async (to, copy) => { try { await call('transferWidget', { id: w.id, toDashboardId: to, copy }); notify(copy ? t('dash.copied') : t('dash.moved'), 'ok') } catch (e) { notifyError(e) } list.reload(); boards.reload() },
|
|
@@ -4664,7 +4755,7 @@ window.__ModuleLoader__.load({
|
|
|
4664
4755
|
h('span', { className: 'n2s-wf-bar' }, h('span', { className: `n2s-wf-fill ${it.delta > 0 ? 'up' : it.delta < 0 ? 'down' : ''}`, style: { width: `${Math.max(2, Math.abs(it.delta) / max * 100)}%` } })),
|
|
4665
4756
|
h('span', { className: 'n2s-wf-num' }, `${it.delta >= 0 ? '+' : ''}${formatCompact(it.delta)}`),
|
|
4666
4757
|
h('span', { className: 'n2s-wf-share n2s-faint' }, it.share === null || Math.abs(it.share) > 3 ? '' : `${Math.round(it.share * 100)}%`)))),
|
|
4667
|
-
why.dimensions.filter(d => !d.attribution).length > 0 && h('div', { className: 'n2s-faint n2s-small' }, `${t('drill.failedDims')}
|
|
4758
|
+
why.dimensions.filter(d => !d.attribution).length > 0 && h('div', { className: 'n2s-faint n2s-small' }, `${colon(t('drill.failedDims'))}${why.dimensions.filter(d => !d.attribution).map(d => d.dimension).join(uiLang === 'zh' ? '、' : ', ')}`))
|
|
4668
4759
|
}
|
|
4669
4760
|
|
|
4670
4761
|
// ------------------------------------------------------------ alerts
|
|
@@ -4820,7 +4911,7 @@ window.__ModuleLoader__.load({
|
|
|
4820
4911
|
h('div', { className: 'n2s-row' }, h('strong', null, fmt(t, 'sc.title', { count: c.changes.length, time: new Date(c.at).toLocaleString([], { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) })), h('span', { className: 'n2s-grow' }),
|
|
4821
4912
|
h('button', { type: 'button', className: 'n2s-btn small', onClick: async () => { await call('ackSchemaChange', { id: c.id }); list.reload() } }, t('sc.ack'))),
|
|
4822
4913
|
h('div', { className: 'n2s-small', style: { marginTop: 4 } }, c.changes.slice(0, 8).map(x => `${kindLabel(x.kind)} ${x.table}${x.column ? `.${x.column}` : ''}${x.detail ? `(${x.detail})` : ''}`).join(';')),
|
|
4823
|
-
c.impacts.length > 0 && h('div', { className: 'n2s-small', style: { marginTop: 4 } }, h('strong', null, `${t('sc.impacts')}
|
|
4914
|
+
c.impacts.length > 0 && h('div', { className: 'n2s-small', style: { marginTop: 4 } }, h('strong', null, `${colon(t('sc.impacts'))}`), c.impacts.slice(0, 8).map(i => `${t(`sc.imp.${i.kind}`)}「${i.name}」`).join(uiLang === 'zh' ? '、' : ', ')))))
|
|
4824
4915
|
}
|
|
4825
4916
|
|
|
4826
4917
|
// -------------------------------------------------- knowledge io
|
|
@@ -4889,7 +4980,7 @@ window.__ModuleLoader__.load({
|
|
|
4889
4980
|
if (s.kind === 'example') return h('pre', { className: 'n2s-code', style: { marginTop: 6, maxHeight: 140 } }, prettySql(it.sql ?? ''))
|
|
4890
4981
|
if (s.kind === 'metric') return h('div', null,
|
|
4891
4982
|
h('div', { className: 'n2s-mono n2s-small' }, `${it.expression}${it.table ? ` FROM ${it.table}` : ''}${it.filter ? ` WHERE ${it.filter}` : ''}`),
|
|
4892
|
-
(it.timeColumn || it.dimensions?.length) && h('div', { className: 'n2s-muted n2s-small' }, [it.timeColumn && `${t('kb.timeColumn')}: ${it.timeColumn}`, it.dimensions?.length && `${t('kb.dimensions')}: ${it.dimensions.join('、')}`].filter(Boolean).join(' · ')))
|
|
4983
|
+
(it.timeColumn || it.dimensions?.length) && h('div', { className: 'n2s-muted n2s-small' }, [it.timeColumn && `${t('kb.timeColumn')}: ${it.timeColumn}`, it.dimensions?.length && `${t('kb.dimensions')}: ${it.dimensions.join(uiLang === 'zh' ? '、' : ', ')}`].filter(Boolean).join(' · ')))
|
|
4893
4984
|
if (s.kind === 'note') return h('div', { className: 'n2s-small' }, it.text)
|
|
4894
4985
|
return h('div', null, h('div', { className: 'n2s-small' }, it.definition), it.sql && h('code', { className: 'n2s-md-code', style: { display: 'inline-block', marginTop: 4 } }, it.sql))
|
|
4895
4986
|
}
|
|
@@ -4995,6 +5086,69 @@ window.__ModuleLoader__.load({
|
|
|
4995
5086
|
h('button', { type: 'button', className: 'n2s-btn small ghost', style: { justifySelf: 'start' }, onClick: () => onChange([...rows, { platform: rows[rows.length - 1]?.platform ?? platforms[0], id: '', email: '' }]) }, '+ ', t('mb.ids.add')))
|
|
4996
5087
|
}
|
|
4997
5088
|
|
|
5089
|
+
/**
|
|
5090
|
+
* One-click identity picks: people and groups that already talked to a bot
|
|
5091
|
+
* (recorded by the host, including refused ones), plus the groups a bot is
|
|
5092
|
+
* in (channels whose kind can list targets). No id lookup needed.
|
|
5093
|
+
*/
|
|
5094
|
+
function IdentityPicker({ t, kind, catalog, onPick, chosen }) {
|
|
5095
|
+
const seen = useAsync(() => call('listSeenIdentities', { kind }).catch(() => []), [kind])
|
|
5096
|
+
const channels = useChannels()
|
|
5097
|
+
const listable = channels.filter(c => (c.capabilities ?? findKind(catalog, c.type)?.kind?.capabilities)?.listTargets)
|
|
5098
|
+
const [groups, setGroups] = useState(null)
|
|
5099
|
+
const [loadingGroups, setLoadingGroups] = useState(false)
|
|
5100
|
+
const loadGroups = async () => {
|
|
5101
|
+
setLoadingGroups(true)
|
|
5102
|
+
const out = []
|
|
5103
|
+
for (const c of listable) {
|
|
5104
|
+
try { for (const g of await call('listChannelTargets', { id: c.id })) out.push({ platform: c.platform, kind: 'chat', id: String(g.id), name: g.name, via: c.name }) } catch (e) { notifyError(e) }
|
|
5105
|
+
}
|
|
5106
|
+
setGroups(out); setLoadingGroups(false)
|
|
5107
|
+
}
|
|
5108
|
+
useEffect(() => { if (kind === 'chat' && listable.length && groups === null) loadGroups() }, [kind, listable.length])
|
|
5109
|
+
const isChosen = (x) => chosen.some(r => r.platform === x.platform && r.id === x.id)
|
|
5110
|
+
const items = [...(seen.value ?? [])]
|
|
5111
|
+
if (kind === 'chat') for (const g of groups ?? []) if (!items.some(x => x.platform === g.platform && x.id === g.id)) items.push(g)
|
|
5112
|
+
const label = (x) => x.name || x.chatName || x.email || x.id
|
|
5113
|
+
return h('div', { className: 'n2s-field full n2s-idpicker' },
|
|
5114
|
+
h('div', { className: 'n2s-row' },
|
|
5115
|
+
h('span', { className: 'n2s-flabel' }, kind === 'chat' ? t('mb.pick.chats') : t('mb.pick.people')),
|
|
5116
|
+
h('span', { className: 'n2s-grow' }),
|
|
5117
|
+
kind === 'chat' && listable.length > 0 && h('button', { type: 'button', className: 'n2s-link n2s-small', disabled: loadingGroups, onClick: loadGroups }, loadingGroups ? h('span', { className: 'n2s-spin' }) : Icon.refresh(12), t('mb.pick.refresh'))),
|
|
5118
|
+
seen.loading && !seen.value ? h('div', { className: 'n2s-skel', style: { height: 34 } }) :
|
|
5119
|
+
items.length === 0
|
|
5120
|
+
? h('div', { className: 'n2s-faint n2s-small' }, kind === 'chat' ? t('mb.pick.noChats') : t('mb.pick.noPeople'))
|
|
5121
|
+
: h('div', { className: 'n2s-pickchips', role: 'list' }, items.slice(0, 30).map(x => {
|
|
5122
|
+
const taken = x.memberId && !isChosen(x)
|
|
5123
|
+
return h('button', {
|
|
5124
|
+
key: `${x.platform}:${x.id}`, type: 'button', role: 'listitem', className: 'n2s-pickchip', 'aria-pressed': isChosen(x), disabled: Boolean(taken),
|
|
5125
|
+
title: [`${platformName(t, x.platform, catalog)} · ${x.id}`, x.email, x.via ? fmt(t, 'mb.pick.via', { name: x.via }) : '', taken ? fmt(t, 'mb.pick.taken', { name: x.memberName }) : '', x.refused ? t('mb.pick.refused') : ''].filter(Boolean).join('\n'),
|
|
5126
|
+
onClick: () => onPick(x),
|
|
5127
|
+
},
|
|
5128
|
+
h(PlatformMark, { t, platform: x.platform, size: 16 }),
|
|
5129
|
+
h('span', { className: 'n2s-pickchip-name' }, label(x)),
|
|
5130
|
+
x.refused && !x.memberId && h('span', { className: 'n2s-badge warn' }, t('mb.pick.pending')),
|
|
5131
|
+
taken && h('span', { className: 'n2s-faint' }, t('mb.pick.added')),
|
|
5132
|
+
x.lastAt && h('span', { className: 'n2s-faint' }, shortTime(x.lastAt)))
|
|
5133
|
+
})),
|
|
5134
|
+
h('div', { className: 'n2s-faint n2s-small' }, kind === 'chat' ? t('mb.pick.chatsHint') : t('mb.pick.peopleHint')))
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
/** People / groups refused by a bot because they are not members yet: add with one click. */
|
|
5138
|
+
function PendingMembers({ t, onAdd }) {
|
|
5139
|
+
const seen = useAsync(() => call('listSeenIdentities', {}).catch(() => []), [])
|
|
5140
|
+
const { catalog } = useCatalog(t)
|
|
5141
|
+
const pending = (seen.value ?? []).filter(x => x.refused && !x.memberId).slice(0, 6)
|
|
5142
|
+
if (!pending.length) return null
|
|
5143
|
+
return h('div', { className: 'n2s-notice warn n2s-pending', role: 'status', style: { marginBottom: 12 } },
|
|
5144
|
+
h('div', { style: { marginBottom: 6 } }, fmt(t, 'mb.pending.title', { n: pending.length })),
|
|
5145
|
+
h('div', { className: 'n2s-pickchips' }, pending.map(x => h('button', { key: `${x.platform}:${x.kind}:${x.id}`, type: 'button', className: 'n2s-pickchip', onClick: () => onAdd(x), title: `${platformName(t, x.platform, catalog)} · ${x.id}` },
|
|
5146
|
+
h(PlatformMark, { t, platform: x.platform, size: 16 }),
|
|
5147
|
+
h('span', { className: 'n2s-pickchip-name' }, x.name || x.chatName || x.id),
|
|
5148
|
+
h('span', { className: 'n2s-faint' }, x.kind === 'chat' ? t('mb.kind2.chat') : t('mb.kind2.person')),
|
|
5149
|
+
h('span', { className: 'n2s-link' }, t('mb.pending.add'))))))
|
|
5150
|
+
}
|
|
5151
|
+
|
|
4998
5152
|
function IdentityChips({ t, u, catalog }) {
|
|
4999
5153
|
const ids = identitiesOf(u)
|
|
5000
5154
|
if (!ids.length) return h('span', { className: 'n2s-badge warn' }, t('mb.ids.none'))
|
|
@@ -5002,11 +5156,11 @@ window.__ModuleLoader__.load({
|
|
|
5002
5156
|
h(PlatformMark, { t, platform: x.platform, size: 16 }), h('span', { className: 'n2s-mono' }, x.id || x.email))))
|
|
5003
5157
|
}
|
|
5004
5158
|
|
|
5005
|
-
function UserForm({ t, initial, datasources, onDone, onCancel }) {
|
|
5159
|
+
function UserForm({ t, initial, prefill, datasources, onDone, onCancel }) {
|
|
5006
5160
|
const { catalog } = useCatalog(t)
|
|
5007
5161
|
const [form, setForm] = useState(() => {
|
|
5008
|
-
const base = { kind: 'person', name: '', role: 'analyst', allDatasources: initial ? initial.datasourceIds === null : false, datasourceIds: [], note: '', ...(initial ?? {}) }
|
|
5009
|
-
const ids = identitiesOf(initial)
|
|
5162
|
+
const base = { kind: prefill?.kind ?? 'person', name: prefill ? (prefill.name || prefill.chatName || '') : '', role: 'analyst', allDatasources: initial ? initial.datasourceIds === null : false, datasourceIds: [], note: '', ...(initial ?? {}) }
|
|
5163
|
+
const ids = prefill ? [{ platform: prefill.platform, id: prefill.id, ...(prefill.kind === 'person' && prefill.email ? { email: prefill.email } : {}) }] : identitiesOf(initial)
|
|
5010
5164
|
return { ...base, identities: ids.length ? ids : [{ platform: 'lark', id: '', email: '' }] }
|
|
5011
5165
|
})
|
|
5012
5166
|
const [error, setError] = useState(null)
|
|
@@ -5038,6 +5192,11 @@ window.__ModuleLoader__.load({
|
|
|
5038
5192
|
h('div', { className: 'n2s-field full' }, h('span', { id: 'n2s-mb-kind' }, t('mb.kind')), h('div', { className: 'n2s-seg', role: 'group', 'aria-labelledby': 'n2s-mb-kind', style: { justifySelf: 'start' } }, ['person', 'chat'].map(v => h('button', { key: v, type: 'button', 'aria-pressed': form.kind === v, disabled: Boolean(initial?.id), onClick: () => setForm(f => ({ ...f, kind: v })) }, t(`mb.kind2.${v}`))))),
|
|
5039
5193
|
h('label', { className: 'n2s-field' }, h('span', null, t('im.name')), h('input', { className: 'n2s-input', value: form.name, onChange: set('name') })),
|
|
5040
5194
|
h('label', { className: 'n2s-field' }, h('span', null, t('mb.role')), h('select', { className: 'n2s-select', value: form.role, onChange: set('role') }, ['admin', 'analyst', 'viewer'].map(r => h('option', { key: r, value: r }, t(`mb.role.${r}`)))), h('span', { className: 'n2s-faint n2s-small' }, t(`mb.roleHint.${form.role}`))),
|
|
5195
|
+
!initial?.id && h(IdentityPicker, { t, kind: form.kind, catalog, chosen: form.identities, onPick: (x) => setForm(f => {
|
|
5196
|
+
const exists = f.identities.some(r => r.platform === x.platform && r.id === x.id)
|
|
5197
|
+
const rows = exists ? f.identities.filter(r => !(r.platform === x.platform && r.id === x.id)) : [...f.identities.filter(r => r.id || r.email), { platform: x.platform, id: x.id, ...(f.kind === 'person' && x.email ? { email: x.email } : {}) }]
|
|
5198
|
+
return { ...f, identities: rows.length ? rows : [{ platform: x.platform, id: '', email: '' }], name: f.name || (exists ? f.name : (x.name || x.chatName || '')) }
|
|
5199
|
+
}) }),
|
|
5041
5200
|
h(IdentityEditor, { t, kind: form.kind, rows: form.identities, catalog, errors: idErrors, onChange: (rows) => { setForm(f => ({ ...f, identities: rows })); setIdErrors({}) } }),
|
|
5042
5201
|
h('div', { className: 'n2s-field full' }, h('span', null, t('mb.scope')),
|
|
5043
5202
|
h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: form.allDatasources, onChange: set('allDatasources') }), t('mb.allDs')),
|
|
@@ -5128,7 +5287,8 @@ window.__ModuleLoader__.load({
|
|
|
5128
5287
|
return h('div', null,
|
|
5129
5288
|
h(SectionHead, { title: t('tab.members'), desc: t('mb.hint') }, h('button', { type: 'button', className: 'n2s-btn primary', onClick: () => setEditing({}) }, '+ ', t('mb.add'))),
|
|
5130
5289
|
h(BotsOverview, { t }),
|
|
5131
|
-
editing && h(
|
|
5290
|
+
!editing && h(PendingMembers, { t, onAdd: (x) => setEditing({ prefill: x }) }),
|
|
5291
|
+
editing && h(UserForm, { key: editing.id ?? (editing.prefill ? `p:${editing.prefill.id}` : 'new'), t, datasources, prefill: editing.prefill, initial: editing.id ? editing : null, onCancel: () => setEditing(null), onDone: () => { setEditing(null); users.reload() } }),
|
|
5132
5292
|
settings.value && h('div', { className: 'n2s-setnote', style: { marginBottom: 10 } },
|
|
5133
5293
|
t('mb.default'), ' ', h('strong', null, t(`mb.defaultRole.${settings.value.botDefaultRole ?? settings.value.larkDefaultRole ?? 'none'}`)), ' · ',
|
|
5134
5294
|
h('button', { type: 'button', className: 'n2s-link', onClick: () => fbCtx.navigate?.({ tab: 'admin', section: 'settings' }) }, t('set.changeInSettings'))),
|
|
@@ -5140,7 +5300,7 @@ window.__ModuleLoader__.load({
|
|
|
5140
5300
|
h('div', { className: 'n2s-grow', style: { minWidth: 0 } },
|
|
5141
5301
|
h('div', { className: 'n2s-row', style: { gap: 6 } }, h('span', { className: 'n2s-list2-title' }, u.name), h('span', { className: 'n2s-badge' }, t(`mb.role.${u.role}`)), u.disabled && h('span', { className: 'n2s-badge err' }, t('mb.disabled'))),
|
|
5142
5302
|
h('div', { className: 'n2s-row', style: { gap: 6, marginTop: 4 } }, h(IdentityChips, { t, u, catalog }), h('span', { className: 'n2s-muted n2s-small' }, u.datasourceIds === null ? t('mb.allDs') : (u.datasourceNames ?? []).join(uiLang === 'zh' ? '、' : ', ') || t('mb.noDs'))),
|
|
5143
|
-
(u.rowRules ?? []).length > 0 && h('div', { className: 'n2s-list2-sub' }, `${t('mb.rowRules')}
|
|
5303
|
+
(u.rowRules ?? []).length > 0 && h('div', { className: 'n2s-list2-sub' }, `${colon(t('mb.rowRules'))}${u.rowRules.map(r => `${r.column} ∈ {${r.values.join(', ')}}`).join(';')}`)),
|
|
5144
5304
|
h('div', { className: 'n2s-list2-side' }, h('span', { className: 'n2s-small' }, us ? fmt(t, 'mb.queries', { n: us.queries }) : t('mb.noUsage')), us?.lastAt && h('span', { className: 'n2s-faint n2s-small' }, new Date(us.lastAt).toLocaleDateString())),
|
|
5145
5305
|
h(Dropdown, { label: fmt(t, 'ch.actionsFor', { name: u.name }), trigger: { content: Icon.more() }, items: [
|
|
5146
5306
|
{ label: t('ds.edit'), icon: Icon.edit(14), onClick: () => setEditing(u) },
|
|
@@ -5441,7 +5601,7 @@ window.__ModuleLoader__.load({
|
|
|
5441
5601
|
nav.tab === 'schema' ? h(SchemaTab, { t, ds, onAskTable: (tb) => ask(fmt(t, 'ask.tablePrompt', { name: tb.name, comment: tb.comment ? `(${tb.comment})` : '' })) }) :
|
|
5442
5602
|
nav.tab === 'knowledge' ? h(KnowledgeTab, { t, ds }) :
|
|
5443
5603
|
nav.tab === 'history' ? h(HistoryTab, { t, datasources, onOpenSession: (id) => ctx.uiWorkspace.openSession(id) }) :
|
|
5444
|
-
nav.tab === 'dashboard' ? h(DashboardTab, { t }) :
|
|
5604
|
+
nav.tab === 'dashboard' ? h(DashboardTab, { t, ds, datasources }) :
|
|
5445
5605
|
h(AdminArea, { t, ctx, section: ADMIN_SECTIONS.includes(nav.section) ? nav.section : 'sources', sub: nav.sub, onNavigate: setNav, ds, datasources }))),
|
|
5446
5606
|
h(FeedbackLayer, { t })))
|
|
5447
5607
|
}
|
|
@@ -5708,7 +5868,7 @@ window.__ModuleLoader__.load({
|
|
|
5708
5868
|
h('table', { className: 'n2s-table' }, h('thead', null, h('tr', null, [t('set.section'), t('set.toAdd'), t('set.toUpdate'), t('set.note')].map((x, i) => h('th', { key: i }, x)))),
|
|
5709
5869
|
h('tbody', null, Object.entries(importState.dry?.report ?? {}).map(([k, r]) => h('tr', { key: k },
|
|
5710
5870
|
h('td', null, t(`set.sec.${k}`)), h('td', null, r.add ?? r.added ?? 0), h('td', null, r.update ?? r.updated ?? 0),
|
|
5711
|
-
h('td', { className: 'n2s-muted n2s-small', style: { whiteSpace: 'normal' } }, [r.needsSecret?.length ? fmt(t, 'set.needsSecret', { names: r.needsSecret.join('、') }) : '', r.paused ? t('set.reportsPaused') : '', r.skipped ? fmt(t, 'set.skipped', { n: r.skipped }) : ''].filter(Boolean).join(';')))))),
|
|
5871
|
+
h('td', { className: 'n2s-muted n2s-small', style: { whiteSpace: 'normal' } }, [r.needsSecret?.length ? fmt(t, 'set.needsSecret', { names: r.needsSecret.join(uiLang === 'zh' ? '、' : ', ') }) : '', r.paused ? t('set.reportsPaused') : '', r.skipped ? fmt(t, 'set.skipped', { n: r.skipped }) : ''].filter(Boolean).join(';')))))),
|
|
5712
5872
|
h('div', { className: 'n2s-row', style: { marginTop: 14, justifyContent: 'flex-end' } },
|
|
5713
5873
|
h('button', { type: 'button', className: 'n2s-btn ghost', onClick: () => setImportState(null) }, t('ds.cancel')),
|
|
5714
5874
|
h('button', { type: 'button', className: 'n2s-btn danger', onClick: () => doImport('replace') }, t('kb.replace')),
|
|
@@ -5724,7 +5884,7 @@ window.__ModuleLoader__.load({
|
|
|
5724
5884
|
row('dataDir', v?.dataDir, true),
|
|
5725
5885
|
row('runtime', v ? `Node ${v.nodeVersion} · SQLite ${v.sqliteVersion}` : null),
|
|
5726
5886
|
row('tests', v?.testsCommand, true),
|
|
5727
|
-
row('docs', '
|
|
5887
|
+
row('docs', 'github.com/hedeqiang/dsh-nl2sql'))))
|
|
5728
5888
|
}
|
|
5729
5889
|
|
|
5730
5890
|
// ------------------------------------------------------------ apply
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hedeqiang/dsh-nl2sql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.12.0",
|
|
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
|
"exports": {
|
|
7
7
|
".": "./index.js",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"locale/*.json",
|
|
30
30
|
"icon.svg",
|
|
31
31
|
"README.md",
|
|
32
|
-
"README.en.md",
|
|
33
32
|
"LICENSE",
|
|
34
33
|
"CHANGELOG.md",
|
|
35
|
-
"
|
|
34
|
+
"README.zh-CN.md",
|
|
35
|
+
"docs/channels.md"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@larksuiteoapi/node-sdk": "1.76.0",
|
|
@@ -93,5 +93,9 @@
|
|
|
93
93
|
},
|
|
94
94
|
"publishConfig": {
|
|
95
95
|
"access": "public"
|
|
96
|
+
},
|
|
97
|
+
"homepage": "https://github.com/hedeqiang/dsh-nl2sql#readme",
|
|
98
|
+
"bugs": {
|
|
99
|
+
"url": "https://github.com/hedeqiang/dsh-nl2sql/issues"
|
|
96
100
|
}
|
|
97
101
|
}
|
package/service/access.js
CHANGED
|
@@ -99,7 +99,7 @@ export const methods = {
|
|
|
99
99
|
/**
|
|
100
100
|
* Members: people and group chats on IM platforms, each with a role
|
|
101
101
|
* (admin | analyst | viewer), a data source scope and platform-scoped
|
|
102
|
-
* identities [{ platform, id, email? }] (see docs/
|
|
102
|
+
* identities [{ platform, id, email? }] (see docs/channels.md). The local
|
|
103
103
|
* owner is implicit. Bot messages from unknown people fall back to
|
|
104
104
|
* settings.botDefaultRole (none = refuse).
|
|
105
105
|
*/
|
|
@@ -212,6 +212,43 @@ export const methods = {
|
|
|
212
212
|
}
|
|
213
213
|
},
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Remember an IM identity that talked to a bot (refused or not), so admins can
|
|
217
|
+
* add it as a member with one click instead of looking up ids. Bounded list,
|
|
218
|
+
* most recent first; keyed by (platform, kind, id).
|
|
219
|
+
*/
|
|
220
|
+
noteSeenIdentity({ platform, kind, id, name, email, chatName, channelId, refused }) {
|
|
221
|
+
if (!platform || !id) return
|
|
222
|
+
this.state.seenIdentities ??= []
|
|
223
|
+
const list = this.state.seenIdentities
|
|
224
|
+
const key = `${platform}:${kind}:${id}`
|
|
225
|
+
const i = list.findIndex(x => `${x.platform}:${x.kind}:${x.id}` === key)
|
|
226
|
+
const prev = i >= 0 ? list.splice(i, 1)[0] : {}
|
|
227
|
+
list.unshift({ ...prev, platform, kind, id: String(id), ...(name ? { name: String(name).slice(0, 60) } : {}), ...(email ? { email: String(email).slice(0, 200) } : {}), ...(chatName ? { chatName: String(chatName).slice(0, 80) } : {}), ...(channelId ? { channelId } : {}), lastAt: Date.now(), count: (prev.count ?? 0) + 1, refused: Boolean(refused) })
|
|
228
|
+
if (list.length > 200) list.length = 200
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* People and group chats that talked to a bot recently, for the member form:
|
|
233
|
+
* [{ platform, kind:'person'|'chat', id, name?, email?, chatName?, lastAt, count, refused, memberId? }].
|
|
234
|
+
* `memberId` is set when the identity already belongs to a member.
|
|
235
|
+
*/
|
|
236
|
+
listSeenIdentities({ kind, platform } = {}) {
|
|
237
|
+
const users = (this.state.users ?? []).map(u => normalizeUserRecord(u))
|
|
238
|
+
// Group chats seen in bot conversations count too (older data has no seen list).
|
|
239
|
+
const seen = [...(this.state.seenIdentities ?? [])]
|
|
240
|
+
for (const c of Object.values(this.state.botConversations ?? {})) {
|
|
241
|
+
if (c.chatType === 'group' && c.chatId && !seen.some(x => x.kind === 'chat' && x.platform === c.platform && x.id === String(c.chatId))) seen.push({ platform: c.platform ?? 'lark', kind: 'chat', id: String(c.chatId), chatName: c.chatName, name: c.chatName, lastAt: c.lastAt ?? c.createdAt, count: 1, refused: false })
|
|
242
|
+
}
|
|
243
|
+
return seen
|
|
244
|
+
.filter(x => (!kind || x.kind === kind) && (!platform || x.platform === platform))
|
|
245
|
+
.map(x => {
|
|
246
|
+
const owner = users.find(u => u.kind === x.kind && u.identities.some(i => i.platform === x.platform && i.id === x.id))
|
|
247
|
+
return { ...x, ...(owner ? { memberId: owner.id, memberName: owner.name } : {}) }
|
|
248
|
+
})
|
|
249
|
+
.sort((a, b) => (b.lastAt ?? 0) - (a.lastAt ?? 0))
|
|
250
|
+
},
|
|
251
|
+
|
|
215
252
|
/** Back-compat alias of resolveActor for Lark. */
|
|
216
253
|
resolveLarkActor({ openId, email, name, chatId, chatType }) {
|
|
217
254
|
return this.resolveActor({ platform: 'lark', userId: openId, email, name, chatId, chatType })
|