@hedeqiang/dsh-nl2sql 0.10.0 → 0.11.2

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/lark-bot.js CHANGED
@@ -1,606 +1,9 @@
1
- // Lark / Feishu inbound messages over the official long connection (WebSocket)
2
- // plus small fetch-based OpenAPI helpers for replying.
3
- //
4
- // Inbound: @larksuiteoapi/node-sdk WSClient + EventDispatcher subscribed to
5
- // `im.message.receive_v1` and the card callback `card.action.trigger`. No public
6
- // URL is needed; the app must be configured with "使用长连接接收事件" (and the
7
- // callback "卡片回传交互" in long-connection mode) in the developer console.
8
- //
9
- // Outbound helpers use plain `fetch` (no SDK) so they can be tested against a
10
- // mock server through the optional `baseUrl` parameter.
11
-
12
- import { createHash } from 'node:crypto'
13
-
14
- const HOSTS = {
15
- feishu: 'https://open.feishu.cn',
16
- lark: 'https://open.larksuite.com',
17
- }
18
- const DEFAULT_TIMEOUT_MS = 15000
19
- // Feishu waits 3 s for a card callback response; answer well before that.
20
- const CARD_ACTION_TIMEOUT_MS = 2500
21
- const APP_ID_RE = /^cli_[0-9a-fA-F]{16}$/
22
- // Lark codes meaning "the tenant token is invalid/expired" → drop cache, retry once.
23
- const TOKEN_INVALID_CODES = new Set([99991661, 99991663, 99991664, 99991668])
24
-
25
- // ---------------------------------------------------------------------------
26
- // HTTP helpers (plain fetch)
27
- // ---------------------------------------------------------------------------
28
-
29
- export function resolveBaseUrl({ domain, baseUrl } = {}) {
30
- if (baseUrl) return String(baseUrl).replace(/\/+$/, '')
31
- return domain === 'lark' ? HOSTS.lark : HOSTS.feishu
32
- }
33
-
34
- function larkError(path, body, status) {
35
- const code = body && typeof body.code === 'number' ? body.code : undefined
36
- const msg = (body && body.msg) || (status ? `HTTP ${status}` : 'unknown error')
37
- const err = new Error(`Lark API ${path} failed: ${code !== undefined ? `code=${code} ` : ''}msg=${msg}`)
38
- err.code = code
39
- err.larkMsg = msg
40
- err.status = status
41
- return err
42
- }
43
-
44
- async function rawRequest(base, path, { method = 'GET', token, body, signal, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
45
- const timeout = AbortSignal.timeout(timeoutMs)
46
- const sig = signal ? AbortSignal.any([signal, timeout]) : timeout
47
- const headers = { 'Content-Type': 'application/json; charset=utf-8' }
48
- if (token) headers.Authorization = `Bearer ${token}`
49
- let res
50
- try {
51
- res = await fetch(base + path, {
52
- method,
53
- headers,
54
- body: body === undefined ? undefined : JSON.stringify(body),
55
- signal: sig,
56
- })
57
- } catch (e) {
58
- const reason = e?.name === 'TimeoutError' ? `timeout after ${timeoutMs}ms`
59
- : e?.name === 'AbortError' ? 'aborted'
60
- : (e?.cause?.code || e?.cause?.message || e?.message || String(e))
61
- const err = new Error(`Lark API ${path} request failed: ${reason}`)
62
- err.cause = e
63
- throw err
64
- }
65
- const text = await res.text()
66
- let json
67
- try { json = text ? JSON.parse(text) : {} } catch { json = undefined }
68
- if (!json || typeof json !== 'object') throw larkError(path, { msg: `non-JSON response (HTTP ${res.status})` }, res.status)
69
- if (json.code !== 0) throw larkError(path, json, res.status)
70
- return json
71
- }
72
-
73
- const tokenCache = new Map() // key -> { token, expiresAt } | { pending: Promise }
74
-
75
- function tokenKey(base, appId, appSecret) {
76
- const h = createHash('sha256').update(String(appSecret)).digest('hex').slice(0, 16)
77
- return `${base}|${appId}|${h}`
78
- }
79
-
80
1
  /**
81
- * getTenantToken({ appId, appSecret, domain, baseUrl?, signal?, force? }) → Promise<string>
82
- * tenant_access_token for an internal (self-built) app, cached in memory until
83
- * `expire - 60s`. Concurrent calls share one request.
2
+ * Compatibility shim: the Lark long connection and OpenAPI helpers moved to
3
+ * channels/lark.js (see docs/CHANNELS.md).
84
4
  */
85
- export async function getTenantToken({ appId, appSecret, domain, baseUrl, signal, force = false } = {}) {
86
- if (!appId || !appSecret) throw new Error('Lark appId and appSecret are required')
87
- const base = resolveBaseUrl({ domain, baseUrl })
88
- const key = tokenKey(base, appId, appSecret)
89
- const hit = tokenCache.get(key)
90
- if (!force && hit) {
91
- if (hit.pending) return hit.pending
92
- if (hit.expiresAt > Date.now()) return hit.token
93
- }
94
- const pending = (async () => {
95
- const json = await rawRequest(base, '/open-apis/auth/v3/tenant_access_token/internal', {
96
- method: 'POST',
97
- body: { app_id: appId, app_secret: appSecret },
98
- signal,
99
- })
100
- const token = json.tenant_access_token
101
- if (!token) throw larkError('/open-apis/auth/v3/tenant_access_token/internal', { msg: 'no tenant_access_token in response' })
102
- const expireSec = Number(json.expire) || 7200
103
- tokenCache.set(key, { token, expiresAt: Date.now() + Math.max(0, expireSec - 60) * 1000 })
104
- return token
105
- })()
106
- tokenCache.set(key, { pending })
107
- try {
108
- return await pending
109
- } catch (e) {
110
- if (tokenCache.get(key)?.pending === pending) tokenCache.delete(key)
111
- throw e
112
- }
113
- }
114
-
115
- /** Drop cached tenant tokens (all, or for one app). Mostly for tests. */
116
- export function clearTenantTokenCache(appId) {
117
- if (!appId) return tokenCache.clear()
118
- for (const k of tokenCache.keys()) if (k.split('|')[1] === appId) tokenCache.delete(k)
119
- }
120
-
121
- /** Authenticated OpenAPI call; retries once with a fresh token on token-invalid codes. */
122
- export async function larkApi(opts, path, { method = 'GET', body, signal } = {}) {
123
- const base = resolveBaseUrl(opts)
124
- const call = async (force) => {
125
- const token = await getTenantToken({ ...opts, signal: signal || opts.signal, force })
126
- return rawRequest(base, path, { method, token, body, signal: signal || opts.signal })
127
- }
128
- try {
129
- return await call(false)
130
- } catch (e) {
131
- if (TOKEN_INVALID_CODES.has(e.code)) return call(true)
132
- throw e
133
- }
134
- }
135
-
136
- const enc = encodeURIComponent
137
-
138
- /**
139
- * replyMessage({ appId, appSecret, domain, baseUrl?, messageId, card, inThread = false, uuid? })
140
- * → Promise<string> reply message_id. Throws Error (err.code = Lark code) on failure.
141
- */
142
- export async function replyMessage({ messageId, card, inThread = false, uuid, ...opts }) {
143
- if (!messageId) throw new Error('messageId is required')
144
- const body = { msg_type: 'interactive', content: typeof card === 'string' ? card : JSON.stringify(card), reply_in_thread: !!inThread }
145
- if (uuid) body.uuid = uuid
146
- const json = await larkApi(opts, `/open-apis/im/v1/messages/${enc(messageId)}/reply`, { method: 'POST', body })
147
- return json.data?.message_id
148
- }
149
-
150
- /** replyText({ …, messageId, text, inThread = false }) → Promise<string> reply message_id. */
151
- export async function replyText({ messageId, text, inThread = false, uuid, ...opts }) {
152
- if (!messageId) throw new Error('messageId is required')
153
- const body = { msg_type: 'text', content: JSON.stringify({ text: String(text ?? '') }), reply_in_thread: !!inThread }
154
- if (uuid) body.uuid = uuid
155
- const json = await larkApi(opts, `/open-apis/im/v1/messages/${enc(messageId)}/reply`, { method: 'POST', body })
156
- return json.data?.message_id
157
- }
158
-
159
- /**
160
- * sendText({ …, chatId, text }) → Promise<string> message_id. Sends a new text
161
- * message to a chat. If only `messageId` is given it behaves like replyText.
162
- */
163
- export async function sendText({ chatId, messageId, text, uuid, ...opts }) {
164
- if (!chatId && messageId) return replyText({ messageId, text, uuid, ...opts })
165
- if (!chatId) throw new Error('chatId (or messageId) is required')
166
- const body = { receive_id: chatId, msg_type: 'text', content: JSON.stringify({ text: String(text ?? '') }) }
167
- if (uuid) body.uuid = uuid
168
- const json = await larkApi(opts, '/open-apis/im/v1/messages?receive_id_type=chat_id', { method: 'POST', body })
169
- return json.data?.message_id
170
- }
171
-
172
- /** sendCard({ …, chatId, card }) → Promise<string> message_id (new interactive message in a chat). */
173
- export async function sendCard({ chatId, card, uuid, ...opts }) {
174
- if (!chatId) throw new Error('chatId is required')
175
- const body = { receive_id: chatId, msg_type: 'interactive', content: typeof card === 'string' ? card : JSON.stringify(card) }
176
- if (uuid) body.uuid = uuid
177
- const json = await larkApi(opts, '/open-apis/im/v1/messages?receive_id_type=chat_id', { method: 'POST', body })
178
- return json.data?.message_id
179
- }
180
-
181
- /**
182
- * updateCard({ …, messageId, card }) → Promise<void>. PATCH an interactive
183
- * message previously sent by this bot (e.g. replace "正在查询…" with the result).
184
- * The card must contain `config.update_multi: true` to be updatable for everyone.
185
- */
186
- export async function updateCard({ messageId, card, ...opts }) {
187
- if (!messageId) throw new Error('messageId is required')
188
- await larkApi(opts, `/open-apis/im/v1/messages/${enc(messageId)}`, {
189
- method: 'PATCH',
190
- body: { content: typeof card === 'string' ? card : JSON.stringify(card) },
191
- })
192
- }
193
-
194
- /** addReaction({ …, messageId, emojiType }) → Promise<string|null> reaction_id. Best effort, never throws. */
195
- export async function addReaction({ messageId, emojiType, logger, ...opts }) {
196
- try {
197
- const json = await larkApi(opts, `/open-apis/im/v1/messages/${enc(messageId)}/reactions`, {
198
- method: 'POST',
199
- body: { reaction_type: { emoji_type: emojiType } },
200
- })
201
- return json.data?.reaction_id ?? null
202
- } catch (e) {
203
- logger?.debug?.(`[lark] addReaction ${emojiType} failed: ${e.message}`)
204
- return null
205
- }
206
- }
207
-
208
- /** getChatInfo({ …, chatId }) → Promise<{ name? }>. Best effort ({} on failure). */
209
- export async function getChatInfo({ chatId, logger, ...opts }) {
210
- try {
211
- const json = await larkApi(opts, `/open-apis/im/v1/chats/${enc(chatId)}`)
212
- const d = json.data || {}
213
- return d.name !== undefined ? { name: d.name } : {}
214
- } catch (e) {
215
- logger?.debug?.(`[lark] getChatInfo failed: ${e.message}`)
216
- return {}
217
- }
218
- }
219
-
220
- /** getUserInfo({ …, openId }) → Promise<{ name?, email? }>. Needs contact permission; {} on failure. */
221
- export async function getUserInfo({ openId, logger, ...opts }) {
222
- try {
223
- const json = await larkApi(opts, `/open-apis/contact/v3/users/${enc(openId)}?user_id_type=open_id`)
224
- const u = json.data?.user || {}
225
- const out = {}
226
- if (u.name) out.name = u.name
227
- if (u.email || u.enterprise_email) out.email = u.email || u.enterprise_email
228
- return out
229
- } catch (e) {
230
- logger?.debug?.(`[lark] getUserInfo failed: ${e.message}`)
231
- return {}
232
- }
233
- }
234
-
235
- /** getBotInfo({ … }) → Promise<{ openId, name }>. Throws on failure. */
236
- export async function getBotInfo(opts) {
237
- const json = await larkApi(opts, '/open-apis/bot/v3/info')
238
- const bot = json.bot || json.data?.bot || {}
239
- return { openId: bot.open_id, name: bot.app_name }
240
- }
241
-
242
- // ---------------------------------------------------------------------------
243
- // Event normalization
244
- // ---------------------------------------------------------------------------
245
-
246
- const MENTION_RE = /@_user_\d+/g
247
-
248
- function stripMentions(s) {
249
- return String(s ?? '').replace(MENTION_RE, '').replace(/[ \t]{2,}/g, ' ').replace(/[ \t]+\n/g, '\n').replace(/\n[ \t]+/g, '\n').trim()
250
- }
251
-
252
- function postToText(content) {
253
- if (!content || typeof content !== 'object') return ''
254
- // Received posts are { title, content: [[el…]…] }; tolerate the send-side
255
- // locale wrapper { zh_cn: { title, content } } too.
256
- let post = content
257
- if (!Array.isArray(post.content)) {
258
- const inner = Object.values(content).find((v) => v && typeof v === 'object' && Array.isArray(v.content))
259
- if (!inner) return ''
260
- post = inner
261
- }
262
- const lines = []
263
- if (post.title) lines.push(String(post.title))
264
- for (const line of post.content) {
265
- if (!Array.isArray(line)) continue
266
- let s = ''
267
- for (const el of line) {
268
- if (!el || typeof el !== 'object') continue
269
- if (el.tag === 'text' || el.tag === 'a' || el.tag === 'md' || el.tag === 'code_block') s += el.text ?? ''
270
- else if (el.tag === 'at') s += el.text ?? el.user_id ?? '' // placeholder (@_user_N) is stripped below
271
- }
272
- lines.push(s)
273
- }
274
- return stripMentions(lines.join('\n')).replace(/\n{3,}/g, '\n\n')
275
- }
276
-
277
- /**
278
- * normalizeMessageEvent(data, { botOpenId? }) → normalized message | null
279
- * `data` may be the flattened object the SDK EventDispatcher passes to handlers
280
- * ({ message, sender, event_id, … }) or a raw v2 envelope ({ schema, header, event }).
281
- * Returns null for events that must be ignored (sent by a bot/app, malformed).
282
- */
283
- export function normalizeMessageEvent(data, { botOpenId } = {}) {
284
- const ev = data && data.event && !data.message ? data.event : data
285
- const message = ev?.message
286
- const sender = ev?.sender
287
- if (!message || !message.message_id) return null
288
- if (sender && (sender.sender_type === 'app' || sender.sender_type === 'bot')) return null
289
-
290
- const mentions = (Array.isArray(message.mentions) ? message.mentions : []).map((m) => {
291
- const openId = m?.id?.open_id
292
- const isBot = botOpenId ? openId === botOpenId : m?.mentioned_type === 'bot'
293
- return { key: m?.key, name: m?.name, openId, userId: m?.id?.user_id, unionId: m?.id?.union_id, isBot: !!isBot }
294
- })
295
-
296
- let content = {}
297
- try { content = typeof message.content === 'string' ? JSON.parse(message.content) : (message.content || {}) } catch { content = {} }
298
-
299
- const messageType = message.message_type
300
- let text = ''
301
- if (messageType === 'text') text = stripMentions(content.text)
302
- else if (messageType === 'post') text = postToText(content)
303
-
304
- const chatType = message.chat_type === 'p2p' ? 'p2p' : 'group'
305
- const sid = sender?.sender_id || {}
306
- const out = {
307
- messageId: message.message_id,
308
- chatId: message.chat_id,
309
- chatType,
310
- messageType,
311
- text,
312
- mentionedBot: chatType === 'p2p' || mentions.some((m) => m.isBot),
313
- mentions,
314
- sender: { openId: sid.open_id, userId: sid.user_id, unionId: sid.union_id },
315
- rootId: message.root_id || undefined,
316
- parentId: message.parent_id || undefined,
317
- threadId: message.thread_id || undefined,
318
- createTime: message.create_time ? Number(message.create_time) : undefined,
319
- eventId: data?.event_id ?? data?.header?.event_id,
320
- }
321
- return out
322
- }
323
-
324
- /**
325
- * normalizeCardActionEvent(data) → normalized card callback | null
326
- * `card.action.trigger` (card JSON 2.0 callback) as delivered over the long
327
- * connection: the SDK EventDispatcher flattens { schema, header, event } into
328
- * one object; a raw envelope is accepted too. Message/chat ids live under
329
- * `event.context` (older surfaces put them at the root).
330
- * Everything here comes from the client and is untrusted: `value`, `option`
331
- * and `formValue` must only be used as lookup keys, never for authorization.
332
- */
333
- export function normalizeCardActionEvent(data) {
334
- if (!data || typeof data !== 'object') return null
335
- const ev = data.event && !data.action ? data.event : data
336
- const header = data.header ?? data
337
- const op = ev?.operator
338
- const action = ev?.action
339
- if (!op?.open_id || !action || typeof action !== 'object') return null
340
- let value = action.value
341
- if (typeof value === 'string') { try { value = JSON.parse(value) } catch { /* keep string */ } }
342
- const ctx = ev.context ?? {}
343
- return {
344
- eventId: header.event_id ?? data.event_id,
345
- operator: { openId: op.open_id, userId: op.user_id || undefined, unionId: op.union_id || undefined, name: op.name || undefined },
346
- tag: action.tag ?? 'unknown',
347
- name: action.name || undefined,
348
- value: value && typeof value === 'object' && !Array.isArray(value) ? value : (value === undefined ? {} : { raw: value }),
349
- option: typeof action.option === 'string' ? action.option : undefined,
350
- options: Array.isArray(action.options) ? action.options.map(String) : undefined,
351
- formValue: action.form_value && typeof action.form_value === 'object' ? action.form_value : undefined,
352
- inputValue: typeof action.input_value === 'string' ? action.input_value : undefined,
353
- messageId: ctx.open_message_id ?? ev.open_message_id,
354
- chatId: ctx.open_chat_id ?? ev.open_chat_id,
355
- host: ev.host,
356
- }
357
- }
358
-
359
- /** createDedupe(max = 500) → (id) => boolean (true = first time seen, false = duplicate). LRU. */
360
- export function createDedupe(max = 500) {
361
- const seen = new Map()
362
- return (id) => {
363
- if (!id) return true
364
- if (seen.has(id)) {
365
- seen.delete(id)
366
- seen.set(id, true)
367
- return false
368
- }
369
- seen.set(id, true)
370
- if (seen.size > max) seen.delete(seen.keys().next().value)
371
- return true
372
- }
373
- }
374
-
375
- // ---------------------------------------------------------------------------
376
- // Bot (WebSocket long connection)
377
- // ---------------------------------------------------------------------------
378
-
379
- const LEVELS = { fatal: 0, error: 1, warn: 2, info: 3, debug: 4, trace: 5 }
380
-
381
- function fmtArgs(args) {
382
- const flat = args.length === 1 && Array.isArray(args[0]) ? args[0] : args
383
- return flat.map((a) => (a instanceof Error ? a.message : typeof a === 'string' ? a : (() => { try { return JSON.stringify(a) } catch { return String(a) } })())).join(' ')
384
- }
385
-
386
- let sdkPromise
387
- function loadSdk() {
388
- sdkPromise ??= import('@larksuiteoapi/node-sdk').then((m) => (m.WSClient ? m : m.default))
389
- return sdkPromise
390
- }
391
-
392
- /**
393
- * createLarkBot({ appId, appSecret, domain: 'feishu'|'lark', onMessage, onStatus, logger, logLevel = 'warn', baseUrl? })
394
- * → { start(): Promise<void>, stop(): Promise<void>, status(): Status, botInfo(): { openId?, name? } }
395
- *
396
- * Status: { state: 'idle'|'connecting'|'connected'|'error'|'stopped', error?, since, lastEventAt? }
397
- *
398
- * start() never rejects: it resolves once the first connection attempt has
399
- * settled (connected, or failed → state 'error'). The SDK keeps retrying
400
- * transient (network) failures in the background; credential/config failures
401
- * are terminal until start() is called again.
402
- */
403
- export function createLarkBot({ appId, appSecret, domain = 'feishu', onMessage, onCardAction, onStatus, logger, logLevel = 'warn', baseUrl, cardActionTimeoutMs = CARD_ACTION_TIMEOUT_MS } = {}) {
404
- const log = {
405
- error: (m) => logger?.error?.(m),
406
- warn: (m) => logger?.warn?.(m),
407
- info: (m) => logger?.info?.(m),
408
- debug: (m) => logger?.debug?.(m),
409
- }
410
- const http = { appId, appSecret, domain, baseUrl }
411
- const dedupe = createDedupe(500)
412
- const cardDedupe = createDedupe(500)
413
- let st = { state: 'idle', since: Date.now() }
414
- let lastEventAt
415
- let wsClient = null
416
- let abort = null
417
- let gen = 0
418
- let starting = null
419
- let settleFirst = null
420
- let bot = {}
421
-
422
- function setStatus(state, error) {
423
- if (st.state === state && st.error === error) return
424
- st = { state, since: Date.now(), ...(error ? { error } : {}) }
425
- try { onStatus?.(status()) } catch (e) { log.error(`[lark] onStatus threw: ${e?.message || e}`) }
426
- }
427
- function status() {
428
- return { ...st, ...(lastEventAt ? { lastEventAt } : {}) }
429
- }
430
-
431
- const userLevel = LEVELS[logLevel] ?? LEVELS.warn
432
- function sdkLogger(myGen) {
433
- // The SDK calls logger.<level>(argsArray). Errors logged while (re)connecting
434
- // are the only signal for retryable failures, so surface them in status.
435
- const forward = (level) => (...args) => {
436
- if (LEVELS[level] > userLevel && level !== 'error') return
437
- const msg = `[lark-sdk] ${fmtArgs(args)}`
438
- if (level === 'error') {
439
- if (myGen === gen && (st.state === 'connecting' || st.state === 'error') && /\[ws\]/.test(msg) && !/discarded socket/.test(msg)) {
440
- const reason = msg.replace(/^\[lark-sdk\]\s*\[ws\]\s*/, '')
441
- if (reason !== 'connect failed' || !st.error) setStatus('error', `Lark long connection failed: ${reason} (retrying)`)
442
- settleFirst?.()
443
- }
444
- if (userLevel >= LEVELS.error) log.error(msg)
445
- } else if (level === 'warn') log.warn(msg)
446
- else if (level === 'info') log.info(msg)
447
- else log.debug(msg)
448
- }
449
- return { error: forward('error'), warn: forward('warn'), info: forward('info'), debug: forward('debug'), trace: forward('trace') }
450
- }
451
-
452
- function handleEvent(data) {
453
- // Must return quickly: the SDK acks the frame after this resolves.
454
- try {
455
- lastEventAt = Date.now()
456
- const msg = normalizeMessageEvent(data, { botOpenId: bot.openId })
457
- if (!msg) return
458
- if (!dedupe(msg.messageId)) {
459
- log.debug(`[lark] duplicate event for ${msg.messageId} ignored`)
460
- return
461
- }
462
- if (!onMessage) return
463
- Promise.resolve()
464
- .then(() => onMessage(msg))
465
- .catch((e) => log.error(`[lark] onMessage handler failed: ${e?.stack || e?.message || e}`))
466
- } catch (e) {
467
- log.error(`[lark] failed to handle im.message.receive_v1: ${e?.message || e}`)
468
- }
469
- }
470
-
471
- /**
472
- * card.action.trigger: the value returned here is what the SDK sends back
473
- * as the callback response (WSClient.handleEventData base64-encodes the
474
- * handler result into the ack frame). Feishu needs it within 3 s, so the
475
- * handler is raced against a timeout and falls back to a neutral toast.
476
- */
477
- async function handleCardAction(data) {
478
- lastEventAt = Date.now()
479
- let evt
480
- try { evt = normalizeCardActionEvent(data) } catch (e) { log.error(`[lark] bad card.action.trigger: ${e?.message || e}`) }
481
- if (!evt) return undefined
482
- if (evt.eventId && !cardDedupe(evt.eventId)) return { toast: { type: 'info', content: '已收到,正在处理…' } }
483
- if (!onCardAction) return undefined
484
- let timer
485
- try {
486
- const res = await Promise.race([
487
- Promise.resolve().then(() => onCardAction(evt)),
488
- new Promise((resolve) => { timer = setTimeout(() => resolve({ toast: { type: 'info', content: '已收到,正在处理…' } }), cardActionTimeoutMs) }),
489
- ])
490
- return res ?? undefined
491
- } catch (e) {
492
- log.error(`[lark] onCardAction handler failed: ${e?.stack || e?.message || e}`)
493
- return { toast: { type: 'error', content: '处理失败,请稍后重试' } }
494
- } finally {
495
- clearTimeout(timer)
496
- }
497
- }
498
-
499
- async function doStart(myGen) {
500
- setStatus('connecting')
501
- if (!appId || !appSecret) return setStatus('error', 'Lark App ID and App Secret are required')
502
- if (!APP_ID_RE.test(appId)) return setStatus('error', `Invalid Lark App ID "${appId}" (expected cli_ followed by 16 hex chars)`)
503
- abort = new AbortController()
504
- const signal = abort.signal
505
- try {
506
- // Validates credentials with a readable error before opening the socket.
507
- await getTenantToken({ ...http, signal })
508
- } catch (e) {
509
- if (myGen !== gen) return
510
- return setStatus('error', `Lark credentials check failed: ${e.message}`)
511
- }
512
- if (myGen !== gen) return
513
- try {
514
- bot = await getBotInfo({ ...http, signal })
515
- if (!bot.openId) log.warn('[lark] bot info has no open_id; @bot detection falls back to mentioned_type')
516
- } catch (e) {
517
- if (myGen !== gen) return
518
- log.warn(`[lark] could not fetch bot info (is the bot capability enabled?): ${e.message}`)
519
- }
520
- if (myGen !== gen) return
521
-
522
- let lark
523
- try {
524
- lark = await loadSdk()
525
- } catch (e) {
526
- return setStatus('error', `Failed to load @larksuiteoapi/node-sdk: ${e?.message || e}`)
527
- }
528
- if (myGen !== gen) return
529
-
530
- // The SDK must always emit errors to us (they drive status); we filter by userLevel.
531
- const level = Math.max(userLevel, LEVELS.error)
532
- const sdkLog = sdkLogger(myGen)
533
- const firstSettled = new Promise((resolve) => { settleFirst = resolve })
534
- let client
535
- try {
536
- const params = {
537
- appId,
538
- appSecret,
539
- domain: baseUrl || (domain === 'lark' ? lark.Domain.Lark : lark.Domain.Feishu),
540
- loggerLevel: level,
541
- logger: sdkLog,
542
- autoReconnect: true,
543
- handshakeTimeoutMs: DEFAULT_TIMEOUT_MS,
544
- source: 'dsh-nl2sql',
545
- onReady: () => { if (myGen === gen) { setStatus('connected'); settleFirst?.() } },
546
- onReconnecting: () => { if (myGen === gen) setStatus('connecting') },
547
- onReconnected: () => { if (myGen === gen) setStatus('connected') },
548
- onError: (err) => { if (myGen === gen) { setStatus('error', `Lark long connection failed: ${err?.message || err}`); settleFirst?.() } },
549
- }
550
- client = new lark.WSClient(params)
551
- const dispatcher = new lark.EventDispatcher({ loggerLevel: level, logger: sdkLog }).register({
552
- 'im.message.receive_v1': (data) => { handleEvent(data) },
553
- // Card callbacks arrive over the same long connection as event frames
554
- // whose header.event_type is card.action.trigger (requires subscribing to
555
- // the callback in the developer console, long-connection mode).
556
- 'card.action.trigger': (data) => handleCardAction(data),
557
- })
558
- wsClient = client
559
- await client.start({ eventDispatcher: dispatcher })
560
- } catch (e) {
561
- if (client) { try { client.close({ force: true }) } catch {} }
562
- if (wsClient === client) wsClient = null
563
- if (myGen === gen) setStatus('error', `Failed to start Lark long connection: ${e?.message || e}`)
564
- return
565
- }
566
- await firstSettled
567
- settleFirst = null
568
- }
569
-
570
- function start() {
571
- if (starting) return starting
572
- if (wsClient && (st.state === 'connected' || st.state === 'connecting')) return Promise.resolve()
573
- if (wsClient) closeClient()
574
- const myGen = ++gen
575
- starting = doStart(myGen)
576
- .catch((e) => { if (myGen === gen) setStatus('error', `Lark bot start failed: ${e?.message || e}`) })
577
- .finally(() => { if (myGen === gen) starting = null })
578
- return starting
579
- }
580
-
581
- function closeClient() {
582
- const c = wsClient
583
- wsClient = null
584
- if (c) {
585
- // close({ force: true }) sets the SDK's `closed` flag, bumps its reconnect
586
- // generation, clears ping/reconnect/liveness timers and the fragment-cache
587
- // interval, and terminates the socket immediately (no 30s close handshake).
588
- try { c.close({ force: true }) } catch (e) { log.debug(`[lark] close failed: ${e?.message || e}`) }
589
- }
590
- }
591
-
592
- async function stop() {
593
- gen++
594
- starting = null
595
- abort?.abort()
596
- abort = null
597
- settleFirst?.()
598
- settleFirst = null
599
- closeClient()
600
- setStatus('stopped')
601
- }
602
-
603
- return { start, stop, status, botInfo: () => ({ ...bot }) }
604
- }
605
-
606
- export default createLarkBot
5
+ export {
6
+ addReaction, clearTenantTokenCache, createDedupe, createLarkBot, getBotInfo, getChatInfo, getTenantToken, getUserInfo,
7
+ larkApi, normalizeCardActionEvent, normalizeMessageEvent, replyMessage, replyText, resolveBaseUrl, sendCard, sendText, updateCard,
8
+ } from './channels/lark.js'
9
+ export { createLarkBot as default } from './channels/lark.js'