@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/CHANGELOG.md +22 -2
- package/README.en.md +2 -1
- package/README.md +2 -2
- package/channels/dingtalk.js +574 -0
- package/channels/http.js +63 -0
- package/channels/index.js +138 -0
- package/channels/lark.js +1180 -0
- package/channels/message.js +267 -0
- package/channels/telegram.js +663 -0
- package/channels/wecom.js +585 -0
- package/cordis.patch.yml +3 -3
- package/docs/CHANNELS.md +184 -0
- package/im.js +6 -509
- package/index.js +25 -17
- package/lark-bot.js +7 -604
- package/lib/client.js +1097 -185
- package/package.json +14 -9
- package/service/access.js +116 -22
- package/service/admin.js +29 -13
- package/service/bots.js +665 -0
- package/service/channels.js +363 -0
- package/service/datasources.js +31 -8
- package/service/push.js +5 -156
- package/service/shared.js +1 -1
- package/service/lark-actions.js +0 -223
- package/service/lark.js +0 -250
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hedeqiang/dsh-nl2sql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Data Q&A (NL2SQL) for DeepSeek Harness: ask MySQL / PostgreSQL / Elasticsearch / CSV in plain language, charts, dashboards, alerts,
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"description": "Data Q&A (NL2SQL) for DeepSeek Harness: ask MySQL / PostgreSQL / Elasticsearch / CSV in plain language, charts, dashboards, alerts, IM push and bots (Lark/Feishu, Telegram, WeCom, DingTalk), access control.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"index.js",
|
|
14
14
|
"service/*.js",
|
|
15
|
+
"channels/*.js",
|
|
15
16
|
"drivers.js",
|
|
16
17
|
"policy.js",
|
|
17
18
|
"filters.js",
|
|
@@ -30,7 +31,8 @@
|
|
|
30
31
|
"README.md",
|
|
31
32
|
"README.en.md",
|
|
32
33
|
"LICENSE",
|
|
33
|
-
"CHANGELOG.md"
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"docs/CHANNELS.md"
|
|
34
36
|
],
|
|
35
37
|
"dependencies": {
|
|
36
38
|
"@larksuiteoapi/node-sdk": "1.76.0",
|
|
@@ -67,17 +69,20 @@
|
|
|
67
69
|
"license": "MIT",
|
|
68
70
|
"author": "hedeqiang <laravel_code@163.com>",
|
|
69
71
|
"keywords": [
|
|
72
|
+
"bi",
|
|
70
73
|
"deepseek-harness",
|
|
74
|
+
"dingtalk",
|
|
71
75
|
"dsh",
|
|
72
76
|
"dsh-bundle",
|
|
73
|
-
"nl2sql",
|
|
74
|
-
"text-to-sql",
|
|
75
|
-
"bi",
|
|
76
|
-
"mysql",
|
|
77
|
-
"postgresql",
|
|
78
77
|
"elasticsearch",
|
|
78
|
+
"feishu",
|
|
79
79
|
"lark",
|
|
80
|
-
"
|
|
80
|
+
"mysql",
|
|
81
|
+
"nl2sql",
|
|
82
|
+
"postgresql",
|
|
83
|
+
"telegram",
|
|
84
|
+
"text-to-sql",
|
|
85
|
+
"wecom"
|
|
81
86
|
],
|
|
82
87
|
"engines": {
|
|
83
88
|
"node": ">=24"
|
package/service/access.js
CHANGED
|
@@ -4,6 +4,58 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { ROLE_RANK, UserError, newId, oneOf, requireText, upsert } from './shared.js'
|
|
6
6
|
|
|
7
|
+
export const PLATFORM_LABEL = { lark: '飞书', telegram: 'Telegram', wecom: '企业微信', dingtalk: '钉钉' }
|
|
8
|
+
const PLATFORM_RE = /^[a-z][\w-]{0,31}$/
|
|
9
|
+
|
|
10
|
+
/** Validate and dedupe an identities array: [{ platform, id, email? }]. */
|
|
11
|
+
export function normalizeIdentities(list) {
|
|
12
|
+
if (!Array.isArray(list)) throw new UserError('nl2sql/invalid', 'identities 必须是数组')
|
|
13
|
+
const out = []
|
|
14
|
+
for (const raw of list.slice(0, 20)) {
|
|
15
|
+
if (!raw || typeof raw !== 'object') continue
|
|
16
|
+
const platform = String(raw.platform ?? '').trim().toLowerCase()
|
|
17
|
+
const id = String(raw.id ?? '').trim().slice(0, 200)
|
|
18
|
+
const email = String(raw.email ?? '').trim().slice(0, 200)
|
|
19
|
+
if (!id && !email) continue
|
|
20
|
+
if (!PLATFORM_RE.test(platform)) throw new UserError('nl2sql/invalid', `无效的平台:${raw.platform ?? ''}`)
|
|
21
|
+
if (!id) throw new UserError('nl2sql/invalid', `请填写 ${PLATFORM_LABEL[platform] ?? platform} 账号 ID`)
|
|
22
|
+
if (out.some(i => i.platform === platform && i.id === id)) continue
|
|
23
|
+
out.push({ platform, id, ...(email ? { email } : {}) })
|
|
24
|
+
}
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Keep the legacy larkOpenId / larkChatId fields mirroring the first Lark identity. */
|
|
29
|
+
function mirrorLegacyIds(u) {
|
|
30
|
+
const lark = u.identities.find(i => i.platform === 'lark')?.id
|
|
31
|
+
delete u.larkOpenId
|
|
32
|
+
delete u.larkChatId
|
|
33
|
+
if (lark) { if (u.kind === 'chat') u.larkChatId = lark; else u.larkOpenId = lark }
|
|
34
|
+
return u
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Legacy member fields → identities, in place (idempotent). */
|
|
38
|
+
export function normalizeUserRecord(u) {
|
|
39
|
+
if (!u || typeof u !== 'object') return u
|
|
40
|
+
if (!Array.isArray(u.identities)) {
|
|
41
|
+
u.identities = []
|
|
42
|
+
const legacy = u.kind === 'chat' ? (u.larkChatId ?? u.larkOpenId) : (u.larkOpenId ?? u.larkChatId)
|
|
43
|
+
if (legacy) u.identities.push({ platform: 'lark', id: String(legacy) })
|
|
44
|
+
}
|
|
45
|
+
return mirrorLegacyIds(u)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Another member already owning one of u's identities (same platform + id) or its email. */
|
|
49
|
+
export function findIdentityClash(users, u) {
|
|
50
|
+
const mail = u.email?.toLowerCase()
|
|
51
|
+
return users.find(x => {
|
|
52
|
+
if (x.id === u.id) return false
|
|
53
|
+
const ids = normalizeUserRecord({ ...x, identities: x.identities ? [...x.identities] : undefined }).identities
|
|
54
|
+
if (u.identities.some(a => ids.some(b => a.platform === b.platform && a.id === b.id))) return true
|
|
55
|
+
return Boolean(mail && x.email && x.email.toLowerCase() === mail)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
7
59
|
export const methods = {
|
|
8
60
|
// ------------------------------------------------------------ access
|
|
9
61
|
|
|
@@ -45,26 +97,38 @@ export const methods = {
|
|
|
45
97
|
},
|
|
46
98
|
|
|
47
99
|
/**
|
|
48
|
-
* Members: people
|
|
49
|
-
* (admin | analyst | viewer)
|
|
50
|
-
*
|
|
51
|
-
*
|
|
100
|
+
* Members: people and group chats on IM platforms, each with a role
|
|
101
|
+
* (admin | analyst | viewer), a data source scope and platform-scoped
|
|
102
|
+
* identities [{ platform, id, email? }] (see docs/CHANNELS.md). The local
|
|
103
|
+
* owner is implicit. Bot messages from unknown people fall back to
|
|
104
|
+
* settings.botDefaultRole (none = refuse).
|
|
52
105
|
*/
|
|
53
106
|
listUsers() {
|
|
54
|
-
return (this.state.users ?? []).map(u => ({ ...u, datasourceNames: (u.datasourceIds ?? []).map(id => this.findDatasource(id)?.name ?? id) }))
|
|
107
|
+
return (this.state.users ?? []).map(u => ({ ...normalizeUserRecord(u), datasourceNames: (u.datasourceIds ?? []).map(id => this.findDatasource(id)?.name ?? id) }))
|
|
55
108
|
},
|
|
56
109
|
|
|
57
110
|
async saveUser(input) {
|
|
58
111
|
const existing = input.id ? (this.state.users ?? []).find(u => u.id === input.id) : undefined
|
|
59
|
-
const u = existing ? { ...existing } : { id: newId('u'), createdAt: Date.now() }
|
|
112
|
+
const u = existing ? normalizeUserRecord({ ...existing, identities: structuredClone(existing.identities ?? []) }) : { id: newId('u'), createdAt: Date.now(), identities: [] }
|
|
60
113
|
u.kind = oneOf(input.kind ?? u.kind ?? 'person', ['person', 'chat'], 'kind')
|
|
61
114
|
u.name = requireText(input.name ?? u.name, 'name', 60)
|
|
62
115
|
u.role = oneOf(input.role ?? u.role ?? 'analyst', ['admin', 'analyst', 'viewer'], 'role')
|
|
63
|
-
u.larkOpenId = String(input.larkOpenId ?? u.larkOpenId ?? '').trim().slice(0, 100) || undefined
|
|
64
116
|
u.email = String(input.email ?? u.email ?? '').trim().slice(0, 200) || undefined
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
117
|
+
if (input.identities !== undefined) {
|
|
118
|
+
u.identities = normalizeIdentities(input.identities)
|
|
119
|
+
} else {
|
|
120
|
+
// Legacy single Lark ids: replace the Lark identity (an empty value removes it).
|
|
121
|
+
const legacy = u.kind === 'chat' ? input.larkChatId : input.larkOpenId
|
|
122
|
+
const other = u.kind === 'chat' ? input.larkOpenId : input.larkChatId
|
|
123
|
+
if (legacy !== undefined || other !== undefined) {
|
|
124
|
+
const value = String(legacy ?? '').trim().slice(0, 100)
|
|
125
|
+
u.identities = u.identities.filter(i => i.platform !== 'lark')
|
|
126
|
+
if (value) u.identities.unshift({ platform: 'lark', id: value })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
mirrorLegacyIds(u)
|
|
130
|
+
if (u.kind === 'person' && !u.identities.length && !u.email) throw new UserError('nl2sql/invalid', '成员需要填写至少一个 IM 账号(如飞书 open_id、Telegram 用户 ID)或邮箱,用于识别身份')
|
|
131
|
+
if (u.kind === 'chat' && !u.identities.length) throw new UserError('nl2sql/invalid', '群成员需要填写群 ID(如飞书 chat_id、Telegram chat id)')
|
|
68
132
|
const allDs = input.allDatasources ?? (input.datasourceIds === undefined ? u.datasourceIds === null : false)
|
|
69
133
|
u.datasourceIds = allDs ? null : (input.datasourceIds ?? u.datasourceIds ?? []).filter(id => this.findDatasource(id))
|
|
70
134
|
u.defaultDatasourceId = input.defaultDatasourceId && this.findDatasource(input.defaultDatasourceId) ? input.defaultDatasourceId : (u.datasourceIds?.[0] ?? null)
|
|
@@ -78,7 +142,7 @@ export const methods = {
|
|
|
78
142
|
})).filter(r => r.values.length).slice(0, 20)
|
|
79
143
|
if (u.rowRules.some(r => !/^[A-Za-z_\u4e00-\u9fa5][\w$\u4e00-\u9fa5]*$/.test(r.column))) throw new UserError('nl2sql/invalid', '行级规则的字段名只能包含字母、数字、下划线或中文')
|
|
80
144
|
}
|
|
81
|
-
const dup = (this.state.users ?? []
|
|
145
|
+
const dup = findIdentityClash(this.state.users ?? [], u)
|
|
82
146
|
if (dup) throw new UserError('nl2sql/duplicate', `该身份已属于成员「${dup.name}」`)
|
|
83
147
|
u.updatedAt = Date.now()
|
|
84
148
|
this.state.users = upsert(this.state.users ?? [], u)
|
|
@@ -95,34 +159,64 @@ export const methods = {
|
|
|
95
159
|
return { deleted: id }
|
|
96
160
|
},
|
|
97
161
|
|
|
162
|
+
/** Migrate legacy member fields to identities (idempotent). Returns true when something changed. */
|
|
163
|
+
migrateUsers() {
|
|
164
|
+
let changed = false
|
|
165
|
+
for (const u of this.state.users ?? []) {
|
|
166
|
+
const before = JSON.stringify(u)
|
|
167
|
+
normalizeUserRecord(u)
|
|
168
|
+
if (JSON.stringify(u) !== before) changed = true
|
|
169
|
+
}
|
|
170
|
+
return changed
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/** Default role / data sources for unknown bot users (new keys, falling back to the legacy lark* keys). */
|
|
174
|
+
botDefaults() {
|
|
175
|
+
const s = this.state.settings
|
|
176
|
+
return {
|
|
177
|
+
role: s.botDefaultRole ?? s.larkDefaultRole ?? 'none',
|
|
178
|
+
datasourceIds: s.botDefaultDatasourceIds !== undefined ? s.botDefaultDatasourceIds : s.larkDefaultDatasourceIds,
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
98
182
|
/**
|
|
99
|
-
* Resolve the effective actor for a
|
|
100
|
-
* narrowed by the chat's scope in group chats.
|
|
183
|
+
* Resolve the effective actor for a bot message or action: the person's own
|
|
184
|
+
* scope, narrowed by the chat's scope in group chats. People match by
|
|
185
|
+
* (platform, userId) or email; chats by (platform, chatId).
|
|
186
|
+
* Returns { actor } or { refused }.
|
|
101
187
|
*/
|
|
102
|
-
|
|
103
|
-
const users = (this.state.users ?? []).filter(u => !u.disabled)
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
188
|
+
resolveActor({ platform = 'lark', userId, email, name, chatId, chatType }) {
|
|
189
|
+
const users = (this.state.users ?? []).filter(u => !u.disabled).map(u => normalizeUserRecord(u))
|
|
190
|
+
const mail = email ? String(email).toLowerCase() : ''
|
|
191
|
+
const person = users.find(u => u.kind === 'person' && userId && u.identities.some(i => i.platform === platform && i.id === String(userId)))
|
|
192
|
+
?? (mail ? users.find(u => u.kind === 'person' && ((u.email && u.email.toLowerCase() === mail) || u.identities.some(i => i.email && i.email.toLowerCase() === mail))) : undefined)
|
|
193
|
+
const chat = chatType === 'group' && chatId ? users.find(u => u.kind === 'chat' && u.identities.some(i => i.platform === platform && i.id === String(chatId))) : undefined
|
|
194
|
+
const defaults = this.botDefaults()
|
|
195
|
+
const fallbackRole = defaults.role
|
|
196
|
+
if (!person && !chat && fallbackRole === 'none') return { refused: `你还没有开通数据问答权限,请联系管理员在「数据问答 → 成员」中添加你的${PLATFORM_LABEL[platform] ?? platform}账号。` }
|
|
108
197
|
const scopes = [person?.datasourceIds, chat?.datasourceIds].filter(x => x !== undefined)
|
|
109
198
|
let datasourceIds = null
|
|
110
199
|
for (const sc of scopes) if (Array.isArray(sc)) datasourceIds = datasourceIds === null ? [...sc] : datasourceIds.filter(id => sc.includes(id))
|
|
111
|
-
if (!person && !chat) datasourceIds =
|
|
200
|
+
if (!person && !chat) datasourceIds = defaults.datasourceIds ?? []
|
|
112
201
|
const role = person?.role ?? chat?.role ?? fallbackRole
|
|
113
202
|
if (Array.isArray(datasourceIds) && datasourceIds.length === 0) return { refused: '你在当前群里没有可访问的数据源。' }
|
|
114
203
|
return {
|
|
115
204
|
actor: {
|
|
116
|
-
kind:
|
|
205
|
+
kind: platform, platform, id: person?.id ?? `${platform}:${userId}`, name: person?.name ?? name ?? userId,
|
|
117
206
|
// Group chats: the narrower of the person's and the group's role.
|
|
118
207
|
role: [person?.role, chat?.role, (!person && !chat) ? fallbackRole : undefined].filter(Boolean).sort((a, b) => ROLE_RANK[a] - ROLE_RANK[b])[0] ?? role,
|
|
119
|
-
openId, chatId, chatName: chat?.name, datasourceIds,
|
|
208
|
+
userId, ...(platform === 'lark' ? { openId: userId } : {}), chatId, chatName: chat?.name, datasourceIds,
|
|
120
209
|
rowRules: [...(person?.rowRules ?? []), ...(chat?.rowRules ?? [])],
|
|
121
210
|
defaultDatasourceId: chat?.defaultDatasourceId ?? person?.defaultDatasourceId ?? (datasourceIds?.[0] ?? null),
|
|
122
211
|
},
|
|
123
212
|
}
|
|
124
213
|
},
|
|
125
214
|
|
|
215
|
+
/** Back-compat alias of resolveActor for Lark. */
|
|
216
|
+
resolveLarkActor({ openId, email, name, chatId, chatType }) {
|
|
217
|
+
return this.resolveActor({ platform: 'lark', userId: openId, email, name, chatId, chatType })
|
|
218
|
+
},
|
|
219
|
+
|
|
126
220
|
auditAdmin(action, detail, actor) {
|
|
127
221
|
this.records.put('admin', { id: newId('ad'), at: Date.now(), action, detail: String(detail).slice(0, 300), ...(actor ? { actor: { id: actor.id, name: actor.name, kind: actor.kind } } : {}) })
|
|
128
222
|
},
|
package/service/admin.js
CHANGED
|
@@ -9,6 +9,9 @@ import { join } from 'node:path'
|
|
|
9
9
|
import { DatabaseSync } from 'node:sqlite'
|
|
10
10
|
import { DEFAULT_SETTINGS, RECORD_LIMITS, UserError, newId, pick } from './shared.js'
|
|
11
11
|
import { widgetWidth } from './dashboards.js'
|
|
12
|
+
import { kindOf } from '../channels/index.js'
|
|
13
|
+
import { findIdentityClash, normalizeIdentities, normalizeUserRecord } from './access.js'
|
|
14
|
+
import { migrateChannelRecord } from './channels.js'
|
|
12
15
|
|
|
13
16
|
const PKG = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) } catch { return {} } })()
|
|
14
17
|
|
|
@@ -18,7 +21,7 @@ const SECTIONS = ['settings', 'datasources', 'knowledge', 'dashboards', 'alerts'
|
|
|
18
21
|
const DS_FIELDS = ['id', 'name', 'type', 'host', 'port', 'database', 'user', 'charset', 'timezone', 'ssl', 'schemas', 'description', 'privacy', 'policy']
|
|
19
22
|
const ALERT_RUNTIME = ['state', 'lastCheckedAt', 'lastValue', 'lastResultId', 'lastFiredAt', 'lastError']
|
|
20
23
|
const REPORT_FIELDS = ['id', 'title', 'scope', 'question', 'instructions', 'period', 'time', 'weekdays', 'timeZone', 'target']
|
|
21
|
-
const CHANNEL_FIELDS = ['id', 'name', 'type', 'domain', 'appId', 'receiveIdType', 'receiveId', 'description', 'isDefault', 'chartMode']
|
|
24
|
+
const CHANNEL_FIELDS = ['id', 'name', 'type', 'platform', 'domain', 'appId', 'receiveIdType', 'receiveId', 'description', 'isDefault', 'chartMode', 'config', 'bot']
|
|
22
25
|
|
|
23
26
|
async function fileBytes(file) {
|
|
24
27
|
try { return (await stat(file)).size } catch { return 0 }
|
|
@@ -199,8 +202,10 @@ export const methods = {
|
|
|
199
202
|
const dsName = (id) => this.findDatasource(id)?.name ?? null
|
|
200
203
|
const chName = (id) => this.state.channels.find(c => c.id === id)?.name ?? null
|
|
201
204
|
const settings = structuredClone(this.state.settings)
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
const botDs = settings.botDefaultDatasourceIds !== undefined ? settings.botDefaultDatasourceIds : settings.larkDefaultDatasourceIds
|
|
206
|
+
if (Array.isArray(botDs)) settings.botDefaultDatasources = botDs.map(dsName).filter(Boolean)
|
|
207
|
+
if (settings.larkDefaultRole !== undefined && settings.botDefaultRole === undefined) settings.botDefaultRole = settings.larkDefaultRole
|
|
208
|
+
delete settings.larkDefaultDatasourceIds; delete settings.botDefaultDatasourceIds; delete settings.larkDefaultRole
|
|
204
209
|
if (settings.schemaNotifyChannel) settings.schemaNotifyChannelName = chName(settings.schemaNotifyChannel)
|
|
205
210
|
delete settings.schemaNotifyChannel
|
|
206
211
|
return {
|
|
@@ -234,9 +239,9 @@ export const methods = {
|
|
|
234
239
|
channelName: chName(r.channelId),
|
|
235
240
|
dashboardName: this.state.dashboards.find(d => d.id === r.dashboardId)?.name ?? null,
|
|
236
241
|
})),
|
|
237
|
-
channels: this.state.channels.map(c => ({ ...pick(c, CHANNEL_FIELDS), hasSecret: Boolean(c.hasSecret), hasSign: Boolean(c.hasSign) })
|
|
242
|
+
channels: this.state.channels.map(c => { migrateChannelRecord(c); return { ...structuredClone(pick(c, CHANNEL_FIELDS)), secretsSet: { ...(c.secretsSet ?? {}) }, hasSecret: Boolean(c.hasSecret ?? Object.keys(c.secretsSet ?? {}).length), hasSign: Boolean(c.hasSign) } }),
|
|
238
243
|
users: (this.state.users ?? []).map(u => {
|
|
239
|
-
const out = structuredClone(u)
|
|
244
|
+
const out = normalizeUserRecord(structuredClone(u))
|
|
240
245
|
out.datasources = Array.isArray(u.datasourceIds) ? u.datasourceIds.map(dsName).filter(Boolean) : null
|
|
241
246
|
out.defaultDatasource = dsName(u.defaultDatasourceId)
|
|
242
247
|
out.rowRules = (u.rowRules ?? []).map(r => ({ ...r, datasource: r.datasourceId ? dsName(r.datasourceId) : null, datasourceId: undefined }))
|
|
@@ -337,18 +342,24 @@ export const methods = {
|
|
|
337
342
|
if (mode === 'replace') chMap.clear()
|
|
338
343
|
for (const src of doc.channels ?? []) {
|
|
339
344
|
const name = String(src?.name ?? '').trim().slice(0, 64)
|
|
340
|
-
|
|
345
|
+
const found = kindOf(src?.type)
|
|
346
|
+
if (!name || !found) { r.skipped++; warn('channels', `渠道「${name || '?'}」无效${src?.type && !found ? `(不支持的类型 ${src.type})` : ''},已跳过`); continue }
|
|
341
347
|
const current = mode === 'replace' ? this.state.channels.find(c => c.name === name) : next.find(c => c.name === name)
|
|
342
|
-
const fields = pick(src, CHANNEL_FIELDS.filter(k => k !== 'id' && k !== 'isDefault'))
|
|
348
|
+
const fields = structuredClone(pick(src, CHANNEL_FIELDS.filter(k => k !== 'id' && k !== 'isDefault')))
|
|
349
|
+
fields.platform = found.adapter.id
|
|
350
|
+
if (fields.bot) fields.bot = this.normalizeBotConfig(fields.bot, {})
|
|
351
|
+
if (current && current.type !== src.type) { delete current.config; delete current.secretsSet }
|
|
343
352
|
if (current) {
|
|
344
|
-
const c = { ...current, ...fields, name, updatedAt: Date.now() }
|
|
353
|
+
const c = migrateChannelRecord({ ...current, ...fields, name, ...(src.config ? {} : { config: undefined }), updatedAt: Date.now() })
|
|
345
354
|
const i = next.findIndex(x => x.id === current.id)
|
|
346
355
|
if (i >= 0) next[i] = c; else next.push(c)
|
|
347
356
|
chMap.set(name, c.id)
|
|
348
357
|
r.update++
|
|
349
358
|
} else {
|
|
350
359
|
const id = src.id && /^ch-[\w-]+$/.test(src.id) && !next.some(c => c.id === src.id) && !this.state.channels.some(c => c.id === src.id) ? src.id : newId('ch')
|
|
351
|
-
const c = { id, ...fields, name, hasSecret: false, hasSign: false, needsSecret: true, createdAt: Date.now(), updatedAt: Date.now(), isDefault: false }
|
|
360
|
+
const c = migrateChannelRecord({ id, ...fields, name, hasSecret: false, hasSign: false, secretsSet: {}, needsSecret: true, createdAt: Date.now(), updatedAt: Date.now(), isDefault: false })
|
|
361
|
+
// Bots need their secret back first.
|
|
362
|
+
if (c.bot) c.bot.enabled = false
|
|
352
363
|
next.push(c)
|
|
353
364
|
chMap.set(name, id)
|
|
354
365
|
r.add++
|
|
@@ -496,14 +507,17 @@ export const methods = {
|
|
|
496
507
|
if (!name || !['person', 'chat'].includes(src.kind ?? 'person') || !['admin', 'analyst', 'viewer'].includes(src.role ?? 'analyst')) { r.skipped++; continue }
|
|
497
508
|
const ids = src.datasources === null || src.datasources === undefined ? null : (src.datasources ?? []).map(resolveDs).filter(Boolean)
|
|
498
509
|
if (Array.isArray(src.datasources) && ids.length < src.datasources.length) warn('users', `成员「${name}」的部分数据源不存在,已忽略`)
|
|
499
|
-
|
|
510
|
+
let identities
|
|
511
|
+
try { identities = Array.isArray(src.identities) ? normalizeIdentities(src.identities) : undefined } catch (error) { r.skipped++; warn('users', `成员「${name}」:${error.message}`); continue }
|
|
512
|
+
const u = normalizeUserRecord({
|
|
500
513
|
...pick(src, ['kind', 'role', 'larkOpenId', 'email', 'larkChatId', 'disabled', 'note']),
|
|
514
|
+
...(identities ? { identities } : {}),
|
|
501
515
|
kind: src.kind ?? 'person', role: src.role ?? 'analyst', name,
|
|
502
516
|
datasourceIds: ids, defaultDatasourceId: resolveDs(src.defaultDatasource) ?? ids?.[0] ?? null,
|
|
503
517
|
rowRules: (src.rowRules ?? []).map(x => ({ datasourceId: x.datasource ? resolveDs(x.datasource) ?? null : null, column: x.column, values: x.values ?? [] })).filter(x => x.column && x.values.length),
|
|
504
518
|
updatedAt: Date.now(),
|
|
505
|
-
}
|
|
506
|
-
const existing = next
|
|
519
|
+
})
|
|
520
|
+
const existing = findIdentityClash(next, { ...u, id: undefined }) ?? next.find(x => x.name === name)
|
|
507
521
|
if (existing) { next[next.indexOf(existing)] = { ...u, id: existing.id, createdAt: existing.createdAt }; r.update++ }
|
|
508
522
|
else {
|
|
509
523
|
const id = src.id && /^u-[\w-]+$/.test(src.id) && !next.some(x => x.id === src.id) ? src.id : newId('u')
|
|
@@ -518,7 +532,9 @@ export const methods = {
|
|
|
518
532
|
if (want.has('settings') && doc.settings && typeof doc.settings === 'object') {
|
|
519
533
|
const r = sec('settings')
|
|
520
534
|
const input = pick(doc.settings, Object.keys(DEFAULT_SETTINGS).concat(['schemaCheckDaily']))
|
|
521
|
-
if (
|
|
535
|
+
if (doc.settings.larkDefaultRole !== undefined && input.botDefaultRole === undefined) input.botDefaultRole = doc.settings.larkDefaultRole
|
|
536
|
+
const botDs = doc.settings.botDefaultDatasources ?? doc.settings.larkDefaultDatasources
|
|
537
|
+
if (Array.isArray(botDs)) input.botDefaultDatasourceIds = botDs.map(resolveDs).filter(Boolean)
|
|
522
538
|
if (doc.settings.schemaNotifyChannelName) input.schemaNotifyChannel = resolveCh(doc.settings.schemaNotifyChannelName) ?? ''
|
|
523
539
|
if (apply) {
|
|
524
540
|
const before = structuredClone(this.state.settings)
|