@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.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/README.md +83 -57
- package/docs/access-control-design.md +8 -5
- package/docs/attachment-pipeline-v2.md +33 -41
- package/docs/feishu-onboarding.md +36 -33
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +1 -1
- package/docs/workspace-isolation-design.md +7 -8
- package/lib/client.js +444 -288
- package/lib/client.js.map +1 -1
- package/lib/index.js +9017 -6794
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/cards.ts +9 -9
- package/src/access/controller.ts +10 -10
- package/src/access/index.ts +1 -0
- package/src/access/policy.ts +10 -10
- package/src/access/requests.ts +32 -31
- package/src/access/store.ts +43 -13
- package/src/access/targets.ts +56 -0
- package/src/cards/approval.ts +96 -26
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +1 -0
- package/src/cards/picker.ts +108 -0
- package/src/cards/streaming.ts +56 -7
- package/src/cards/wiki.ts +124 -0
- package/src/client/api.ts +5 -25
- package/src/client/panel/access-card.tsx +24 -15
- package/src/client/panel/access-tab.tsx +3 -3
- package/src/client/panel/connection-tab.tsx +70 -1
- package/src/client/panel/index.tsx +11 -62
- package/src/client/panel/shared.tsx +96 -24
- package/src/client/panel/workspace-tab.tsx +2 -18
- package/src/client/string-list.ts +139 -0
- package/src/client/styles.ts +5 -0
- package/src/commands/registry.ts +8 -5
- package/src/config.ts +55 -54
- package/src/domain.ts +36 -123
- package/src/feishu/delivery.ts +34 -1
- package/src/feishu/inbound-attachments.ts +1 -1
- package/src/feishu/manifest.ts +8 -31
- package/src/feishu/message-read.ts +97 -24
- package/src/feishu/scopes.ts +194 -0
- package/src/feishu/send-file.ts +543 -0
- package/src/feishu/slash-commands.ts +134 -0
- package/src/feishu/tools.ts +72 -112
- package/src/feishu/wiki/api.ts +346 -0
- package/src/feishu/wiki/consent.ts +183 -0
- package/src/feishu/wiki/links.ts +122 -0
- package/src/feishu/wiki/scope.ts +66 -0
- package/src/feishu/wiki/tools.ts +984 -0
- package/src/feishu-channel.ts +280 -9
- package/src/index.ts +5 -4
- package/src/interactions/approvals.ts +187 -7
- package/src/interactions/pending.ts +3 -3
- package/src/routing/inbound.ts +73 -106
- package/src/service.ts +545 -440
- package/src/session/manager.ts +1 -1
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +110 -89
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +144 -94
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +21 -29
- package/src/cleanup.ts +0 -546
- package/src/client/panel/cleanup-card.tsx +0 -106
- package/src/feishu/pending-attachments.ts +0 -562
package/src/feishu-channel.ts
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createLarkChannel, LoggerLevel } from '@larksuiteoapi/node-sdk'
|
|
12
|
+
import { SCOPE_GRANTED_STATUS } from './feishu/scopes.ts'
|
|
13
|
+
import type { ScopeGrant } from './feishu/scopes.ts'
|
|
14
|
+
import type { RemoteSlashCommand, SlashCommandApi, SlashCommandEntry } from './feishu/slash-commands.ts'
|
|
15
|
+
import { createWikiApi } from './feishu/wiki/api.ts'
|
|
16
|
+
import type { WikiApi, WikiRawRequest } from './feishu/wiki/api.ts'
|
|
12
17
|
import type {
|
|
13
18
|
BotAddedEvent,
|
|
14
19
|
ChatInfo,
|
|
@@ -21,12 +26,18 @@ import type {
|
|
|
21
26
|
export type { BotAddedEvent, ChatInfo, NormalizedMessage, SendResult } from '@larksuiteoapi/node-sdk'
|
|
22
27
|
export type FeishuConnectionStatus = WSConnectionStatus
|
|
23
28
|
|
|
24
|
-
/** Outbound payloads used by this plugin, including Buffer-backed file
|
|
29
|
+
/** Outbound payloads used by this plugin, including Buffer-backed image and file uploads. */
|
|
25
30
|
export type FeishuSendInput =
|
|
26
31
|
| { card: object }
|
|
27
32
|
| { text: string }
|
|
33
|
+
| { image: { source: Buffer } }
|
|
28
34
|
| { file: { source: string | Buffer; fileName: string } }
|
|
29
35
|
|
|
36
|
+
/** One outbound image or file uploaded from memory. */
|
|
37
|
+
export type OutboundMedia =
|
|
38
|
+
| { readonly kind: 'image'; readonly data: Buffer }
|
|
39
|
+
| { readonly kind: 'file'; readonly data: Buffer; readonly fileName: string }
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* One normalized card action (button click) event, as delivered by the
|
|
32
43
|
* official SDK's `card.action.trigger` handler (card JSON 2.0 callback
|
|
@@ -58,18 +69,51 @@ export interface FeishuChannel {
|
|
|
58
69
|
getChatInfo(chatId: string): Promise<ChatInfo>
|
|
59
70
|
/** Bot open_id, available after `connect()` resolves. */
|
|
60
71
|
botOpenId(): string | undefined
|
|
72
|
+
/**
|
|
73
|
+
* The app owner's `user_id` (`application.v6.application.get`), used as the
|
|
74
|
+
* administrator when none is configured. Resolves `undefined` when the owner
|
|
75
|
+
* is not a tenant member; rejects with an {@link AppOwnerLookupError} when
|
|
76
|
+
* the API call fails (typically a missing app-info scope). Optional so stubs
|
|
77
|
+
* and older channels can omit it; absence means no owner fallback.
|
|
78
|
+
*/
|
|
79
|
+
appOwnerUserId?(): Promise<string | undefined>
|
|
80
|
+
/**
|
|
81
|
+
* Scopes the app requested from the tenant and their grant state
|
|
82
|
+
* (`application.v6.scope.list`). Rejects with a {@link FeishuApiError} when
|
|
83
|
+
* the call fails. Optional so stubs can omit it; absence skips the check.
|
|
84
|
+
*/
|
|
85
|
+
grantedScopes?(): Promise<readonly ScopeGrant[]>
|
|
86
|
+
/**
|
|
87
|
+
* The app's native slash-command panel endpoints
|
|
88
|
+
* (`application/v7/app_slash_commands`). Optional so stubs can omit it;
|
|
89
|
+
* absence skips the panel sync.
|
|
90
|
+
*/
|
|
91
|
+
readonly slashCommands?: SlashCommandApi
|
|
92
|
+
/**
|
|
93
|
+
* Wiki and docx endpoints used by the wiki tools, called with the app
|
|
94
|
+
* identity. Optional so stubs can omit it; absence fails wiki calls with
|
|
95
|
+
* a channel-unavailable result.
|
|
96
|
+
*/
|
|
97
|
+
readonly wiki?: WikiApi
|
|
61
98
|
/** Current official-SDK WebSocket lifecycle snapshot, when initialized. */
|
|
62
99
|
getConnectionStatus(): FeishuConnectionStatus | undefined
|
|
63
100
|
/** Start the WebSocket long connection (resolves after the handshake). */
|
|
64
101
|
connect(): Promise<void>
|
|
65
102
|
/** Stop the connection. */
|
|
66
103
|
disconnect(): Promise<void>
|
|
67
|
-
/** Send one message (text or
|
|
104
|
+
/** Send one message (text, card, image or file) with optional reply targeting. */
|
|
68
105
|
send(
|
|
69
106
|
chatId: string,
|
|
70
107
|
input: FeishuSendInput,
|
|
71
108
|
options?: { replyTo?: string; replyInThread?: boolean },
|
|
72
109
|
): Promise<SendResult>
|
|
110
|
+
/**
|
|
111
|
+
* Upload one image or file and send it as a reply to `replyTo`. Unlike
|
|
112
|
+
* {@link FeishuChannel.send}, a recalled or deleted reply target is NOT
|
|
113
|
+
* retried as a fresh chat message: the call rejects with a
|
|
114
|
+
* {@link FeishuApiError}, so topic output never lands on the main timeline.
|
|
115
|
+
*/
|
|
116
|
+
replyMedia(replyTo: string, media: OutboundMedia, options: { replyInThread: boolean }): Promise<SendResult>
|
|
73
117
|
/** Full-card replacement of a previously sent card message. */
|
|
74
118
|
updateCard(messageId: string, card: object): Promise<void>
|
|
75
119
|
/** Add an emoji reaction; resolves with the reaction id needed for removal. */
|
|
@@ -181,6 +225,136 @@ export async function* downloadResourceInRanges(
|
|
|
181
225
|
}
|
|
182
226
|
}
|
|
183
227
|
|
|
228
|
+
/** Owner type `2` = tenant member; `0`/`1` are Feishu itself or an ISV partner. */
|
|
229
|
+
const APP_OWNER_TYPE_TENANT_MEMBER = 2
|
|
230
|
+
|
|
231
|
+
/** Failed Feishu OpenAPI call, carrying the Feishu `code`/`msg` when present. */
|
|
232
|
+
export class FeishuApiError extends Error {
|
|
233
|
+
/**
|
|
234
|
+
* @param detail Feishu `code=<n> <msg>` or the transport error text.
|
|
235
|
+
* @param code Feishu API error code, when the response carried one.
|
|
236
|
+
*/
|
|
237
|
+
constructor(readonly detail: string, readonly code?: number) {
|
|
238
|
+
super(`feishu api call failed: ${detail}`)
|
|
239
|
+
this.name = 'FeishuApiError'
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Failed app-owner lookup, carrying the Feishu `code`/`msg` when present. */
|
|
244
|
+
export class AppOwnerLookupError extends FeishuApiError {
|
|
245
|
+
/**
|
|
246
|
+
* @param detail Feishu `code=<n> <msg>` or the transport error text.
|
|
247
|
+
* @param code Feishu API error code, when the response carried one.
|
|
248
|
+
*/
|
|
249
|
+
constructor(detail: string, code?: number) {
|
|
250
|
+
super(detail, code)
|
|
251
|
+
this.message = `app owner lookup failed: ${detail}`
|
|
252
|
+
this.name = 'AppOwnerLookupError'
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** `detail`/`code` of a thrown SDK/Axios error, preferring the Feishu error body. */
|
|
257
|
+
function thrownErrorFields(error: unknown): { detail: string; code?: number } {
|
|
258
|
+
const data = (error as { response?: { data?: { code?: unknown; msg?: unknown } } } | undefined)?.response?.data
|
|
259
|
+
const code = typeof data?.code === 'number' ? data.code : undefined
|
|
260
|
+
const msg = typeof data?.msg === 'string' ? data.msg : undefined
|
|
261
|
+
if (code === undefined && msg === undefined) return { detail: error instanceof Error ? error.message : String(error) }
|
|
262
|
+
return { detail: [code === undefined ? undefined : `code=${code}`, msg].filter(Boolean).join(' '), code }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Normalize a thrown SDK/Axios error into an {@link AppOwnerLookupError}. */
|
|
266
|
+
/** Throw a {@link FeishuApiError} for a non-zero Feishu `code`. */
|
|
267
|
+
function assertFeishuOk(response: { code?: number; msg?: string }): void {
|
|
268
|
+
if (response.code !== undefined && response.code !== 0) {
|
|
269
|
+
throw new FeishuApiError(`code=${response.code}${response.msg === undefined ? '' : ` ${response.msg}`}`, response.code)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Run one raw request, mapping thrown and non-zero-code failures to {@link FeishuApiError}. */
|
|
274
|
+
async function feishuCall<T extends { code?: number; msg?: string }>(call: () => Promise<T>): Promise<T> {
|
|
275
|
+
let response: T
|
|
276
|
+
try {
|
|
277
|
+
response = await call()
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const { detail, code } = thrownErrorFields(error)
|
|
280
|
+
throw new FeishuApiError(detail, code)
|
|
281
|
+
}
|
|
282
|
+
assertFeishuOk(response)
|
|
283
|
+
return response
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Run one upload. The typed upload methods return the `data` object directly
|
|
288
|
+
* (or `null`) and throw on a non-zero code, so only thrown errors need mapping.
|
|
289
|
+
*/
|
|
290
|
+
async function feishuUpload<T>(call: () => Promise<T>): Promise<T> {
|
|
291
|
+
try {
|
|
292
|
+
return await call()
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const { detail, code } = thrownErrorFields(error)
|
|
295
|
+
throw new FeishuApiError(detail, code)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Base path of the app slash-command endpoints (absent from SDK 1.76's typed client). */
|
|
300
|
+
const SLASH_COMMANDS_PATH = '/open-apis/application/v7/app_slash_commands'
|
|
301
|
+
|
|
302
|
+
/** One `app_slash_commands` list response (unpaginated). */
|
|
303
|
+
interface SlashCommandListResponse {
|
|
304
|
+
code?: number
|
|
305
|
+
msg?: string
|
|
306
|
+
data?: {
|
|
307
|
+
items?: Array<{ command_id?: string; command?: string; description?: { default_value?: string } }>
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** `rawClient.request` as the slash-command endpoints use it. */
|
|
312
|
+
type RawRequest = <T>(input: { method: string; url: string; data?: object }) => Promise<T>
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create/update body. The panel icon stays at Feishu's default; PATCH
|
|
316
|
+
* replaces the whole `description` object.
|
|
317
|
+
*/
|
|
318
|
+
function slashCommandDescription(entry: SlashCommandEntry): object {
|
|
319
|
+
return { default_value: entry.description, i18n: { zh_cn: entry.description } }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** {@link SlashCommandApi} over the SDK's generic authenticated request. */
|
|
323
|
+
function slashCommandApi(request: RawRequest): SlashCommandApi {
|
|
324
|
+
const commandUrl = (id: string) => `${SLASH_COMMANDS_PATH}/${encodeURIComponent(id)}`
|
|
325
|
+
return {
|
|
326
|
+
async list(): Promise<readonly RemoteSlashCommand[]> {
|
|
327
|
+
const response = await feishuCall(() => request<SlashCommandListResponse>({ method: 'GET', url: SLASH_COMMANDS_PATH }))
|
|
328
|
+
return (response.data?.items ?? []).flatMap(item => item.command_id === undefined || item.command === undefined
|
|
329
|
+
? []
|
|
330
|
+
: [{ id: item.command_id, command: item.command, description: item.description?.default_value ?? '' }])
|
|
331
|
+
},
|
|
332
|
+
async create(entry: SlashCommandEntry): Promise<void> {
|
|
333
|
+
await feishuCall(() => request<{ code?: number; msg?: string }>({
|
|
334
|
+
method: 'POST',
|
|
335
|
+
url: SLASH_COMMANDS_PATH,
|
|
336
|
+
data: { command: entry.command, description: slashCommandDescription(entry) },
|
|
337
|
+
}))
|
|
338
|
+
},
|
|
339
|
+
async update(id: string, entry: SlashCommandEntry): Promise<void> {
|
|
340
|
+
await feishuCall(() => request<{ code?: number; msg?: string }>({
|
|
341
|
+
method: 'PATCH',
|
|
342
|
+
url: commandUrl(id),
|
|
343
|
+
data: { description: slashCommandDescription(entry) },
|
|
344
|
+
}))
|
|
345
|
+
},
|
|
346
|
+
async delete(id: string): Promise<void> {
|
|
347
|
+
await feishuCall(() => request<{ code?: number; msg?: string }>({ method: 'DELETE', url: commandUrl(id) }))
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function appOwnerLookupError(error: unknown): AppOwnerLookupError {
|
|
353
|
+
if (error instanceof AppOwnerLookupError) return error
|
|
354
|
+
const { detail, code } = thrownErrorFields(error)
|
|
355
|
+
return new AppOwnerLookupError(detail, code)
|
|
356
|
+
}
|
|
357
|
+
|
|
184
358
|
export interface ChannelConnectOptions {
|
|
185
359
|
appId: string
|
|
186
360
|
appSecret: string
|
|
@@ -191,17 +365,19 @@ export interface ChannelConnectOptions {
|
|
|
191
365
|
|
|
192
366
|
class LarkChannelAdapter implements FeishuChannel {
|
|
193
367
|
private readonly channel: LarkChannel
|
|
368
|
+
private readonly appId: string
|
|
194
369
|
private readonly requireMention: boolean
|
|
195
370
|
private readonly dmMode: 'open' | 'disabled'
|
|
371
|
+
readonly slashCommands: SlashCommandApi
|
|
372
|
+
readonly wiki: WikiApi
|
|
196
373
|
|
|
197
374
|
constructor(options: ChannelConnectOptions) {
|
|
198
375
|
this.channel = createLarkChannel({
|
|
199
376
|
appId: options.appId,
|
|
200
377
|
appSecret: options.appSecret,
|
|
201
378
|
loggerLevel: LOGGER_LEVELS[options.loggerLevel],
|
|
202
|
-
// Keep the raw event body:
|
|
203
|
-
//
|
|
204
|
-
// intake recovers them from `msg.raw` instead.
|
|
379
|
+
// Keep the raw event body: the sender `user_id` and the top-level
|
|
380
|
+
// rich-text `files` array are only recoverable from `msg.raw`.
|
|
205
381
|
includeRawEvent: true,
|
|
206
382
|
policy: {
|
|
207
383
|
// The plugin routes mentions itself (thread routing must work without
|
|
@@ -210,13 +386,39 @@ class LarkChannelAdapter implements FeishuChannel {
|
|
|
210
386
|
dmMode: options.dmMode === 'open' ? 'open' : 'disabled',
|
|
211
387
|
respondToMentionAll: false,
|
|
212
388
|
},
|
|
389
|
+
safety: {
|
|
390
|
+
// The SDK debounces messages per chatId and merges a burst into one
|
|
391
|
+
// message carrying the LAST sender/thread/message id. In a group that
|
|
392
|
+
// attributes one user's text and files to another, bypassing admission
|
|
393
|
+
// and topic ownership. Flush every message on its own; per-chat
|
|
394
|
+
// serialization and dedup stay in place.
|
|
395
|
+
batch: { text: { delayMs: 0 } },
|
|
396
|
+
},
|
|
213
397
|
})
|
|
398
|
+
this.appId = options.appId
|
|
214
399
|
this.requireMention = options.requireMention
|
|
215
400
|
this.dmMode = options.dmMode
|
|
401
|
+
const rawClient = this.channel.rawClient
|
|
402
|
+
this.slashCommands = slashCommandApi(async <T,>(input: Parameters<RawRequest>[0]) => await rawClient.request<T>(input))
|
|
403
|
+
this.wiki = createWikiApi(async <T,>(input: Parameters<WikiRawRequest>[0]) => await rawClient.request<T>(input))
|
|
216
404
|
}
|
|
217
405
|
|
|
218
406
|
onMessage(handler: (message: NormalizedMessage) => void | Promise<void>): () => void {
|
|
219
|
-
return this.channel.on('message',
|
|
407
|
+
return this.channel.on('message', message => {
|
|
408
|
+
const userId = senderUserIdOf(message.raw)
|
|
409
|
+
if (userId === undefined) {
|
|
410
|
+
// Bots and other apps never carry a user_id; neither do external
|
|
411
|
+
// users or a tenant app without the user ID scope. None of them can
|
|
412
|
+
// be identified, so none of them may reach admission or routing.
|
|
413
|
+
console.warn(
|
|
414
|
+
'[dsh-channels] dropped message %s: sender has no user_id '
|
|
415
|
+
+ '(bot/app sender, external user, or the app lacks contact:user.employee_id:readonly)',
|
|
416
|
+
message.messageId,
|
|
417
|
+
)
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
return handler({ ...message, senderId: userId })
|
|
421
|
+
})
|
|
220
422
|
}
|
|
221
423
|
|
|
222
424
|
onCardAction(handler: (event: CardActionEvent) => void | Promise<void>): () => void {
|
|
@@ -243,6 +445,34 @@ class LarkChannelAdapter implements FeishuChannel {
|
|
|
243
445
|
return this.channel.getConnectionStatus()
|
|
244
446
|
}
|
|
245
447
|
|
|
448
|
+
async appOwnerUserId(): Promise<string | undefined> {
|
|
449
|
+
let response
|
|
450
|
+
try {
|
|
451
|
+
response = await this.channel.rawClient.application.v6.application.get({
|
|
452
|
+
path: { app_id: this.appId },
|
|
453
|
+
params: { lang: 'zh_cn', user_id_type: 'user_id' },
|
|
454
|
+
})
|
|
455
|
+
} catch (error) {
|
|
456
|
+
throw appOwnerLookupError(error)
|
|
457
|
+
}
|
|
458
|
+
if (response.code !== undefined && response.code !== 0) {
|
|
459
|
+
throw new AppOwnerLookupError(`code=${response.code}${response.msg === undefined ? '' : ` ${response.msg}`}`, response.code)
|
|
460
|
+
}
|
|
461
|
+
const owner = response.data?.app?.owner
|
|
462
|
+
if (owner?.type !== APP_OWNER_TYPE_TENANT_MEMBER) return undefined
|
|
463
|
+
const userId = owner.owner_id?.trim()
|
|
464
|
+
return userId === undefined || userId === '' ? undefined : userId
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async grantedScopes(): Promise<readonly ScopeGrant[]> {
|
|
468
|
+
const response = await feishuCall(() => this.channel.rawClient.application.v6.scope.list())
|
|
469
|
+
return (response.data?.scopes ?? []).map(scope => ({
|
|
470
|
+
name: scope.scope_name,
|
|
471
|
+
granted: scope.grant_status === SCOPE_GRANTED_STATUS,
|
|
472
|
+
...(scope.scope_type === undefined ? {} : { type: scope.scope_type }),
|
|
473
|
+
}))
|
|
474
|
+
}
|
|
475
|
+
|
|
246
476
|
async connect(): Promise<void> {
|
|
247
477
|
await this.channel.connect()
|
|
248
478
|
}
|
|
@@ -259,6 +489,31 @@ class LarkChannelAdapter implements FeishuChannel {
|
|
|
259
489
|
return this.channel.send(chatId, input, options)
|
|
260
490
|
}
|
|
261
491
|
|
|
492
|
+
async replyMedia(replyTo: string, media: OutboundMedia, options: { replyInThread: boolean }): Promise<SendResult> {
|
|
493
|
+
const im = this.channel.rawClient.im.v1
|
|
494
|
+
let content: string
|
|
495
|
+
if (media.kind === 'image') {
|
|
496
|
+
const uploaded = await feishuUpload(() => im.image.create({ data: { image_type: 'message', image: media.data } }))
|
|
497
|
+
const imageKey = uploaded?.image_key
|
|
498
|
+
if (imageKey === undefined) throw new FeishuApiError('image_key missing in upload response')
|
|
499
|
+
content = JSON.stringify({ image_key: imageKey })
|
|
500
|
+
} else {
|
|
501
|
+
const uploaded = await feishuUpload(() => im.file.create({
|
|
502
|
+
data: { file_type: 'stream', file_name: media.fileName, file: media.data },
|
|
503
|
+
}))
|
|
504
|
+
const fileKey = uploaded?.file_key
|
|
505
|
+
if (fileKey === undefined) throw new FeishuApiError('file_key missing in upload response')
|
|
506
|
+
content = JSON.stringify({ file_key: fileKey })
|
|
507
|
+
}
|
|
508
|
+
const sent = await feishuCall(() => im.message.reply({
|
|
509
|
+
path: { message_id: replyTo },
|
|
510
|
+
data: { msg_type: media.kind, content, reply_in_thread: options.replyInThread },
|
|
511
|
+
}))
|
|
512
|
+
const messageId = sent.data?.message_id
|
|
513
|
+
if (messageId === undefined) throw new FeishuApiError('message_id missing in reply response')
|
|
514
|
+
return { messageId }
|
|
515
|
+
}
|
|
516
|
+
|
|
262
517
|
async updateCard(messageId: string, card: object): Promise<void> {
|
|
263
518
|
await this.channel.updateCard(messageId, card)
|
|
264
519
|
}
|
|
@@ -278,9 +533,9 @@ class LarkChannelAdapter implements FeishuChannel {
|
|
|
278
533
|
|
|
279
534
|
async messageGet(messageId: string): Promise<unknown> {
|
|
280
535
|
const message = this.channel.rawClient.im.v1.message as unknown as {
|
|
281
|
-
get(input: { path: { message_id: string } }): Promise<unknown>
|
|
536
|
+
get(input: { path: { message_id: string }; params: { user_id_type: 'user_id' } }): Promise<unknown>
|
|
282
537
|
}
|
|
283
|
-
return await message.get({ path: { message_id: messageId } })
|
|
538
|
+
return await message.get({ path: { message_id: messageId }, params: { user_id_type: 'user_id' } })
|
|
284
539
|
}
|
|
285
540
|
|
|
286
541
|
async messageMget(messageIds: readonly string[]): Promise<unknown> {
|
|
@@ -300,7 +555,7 @@ class LarkChannelAdapter implements FeishuChannel {
|
|
|
300
555
|
}
|
|
301
556
|
return await client.request({
|
|
302
557
|
method: 'GET',
|
|
303
|
-
url: `/open-apis/im/v1/messages/mget?${query}&card_msg_content_type=raw_card_content`,
|
|
558
|
+
url: `/open-apis/im/v1/messages/mget?${query}&card_msg_content_type=raw_card_content&user_id_type=user_id`,
|
|
304
559
|
})
|
|
305
560
|
}
|
|
306
561
|
|
|
@@ -381,6 +636,22 @@ export function sniffImageMediaType(data: Uint8Array): 'image/png' | 'image/jpeg
|
|
|
381
636
|
return undefined
|
|
382
637
|
}
|
|
383
638
|
|
|
639
|
+
/**
|
|
640
|
+
* The sender `user_id` of one raw `im.message.receive_v1` event body. The SDK
|
|
641
|
+
* normalizer fills `senderId` with the open_id; the plugin identifies users by
|
|
642
|
+
* the tenant-wide `user_id`, which Feishu only sends to tenant-internal apps
|
|
643
|
+
* granted `contact:user.employee_id:readonly`.
|
|
644
|
+
*
|
|
645
|
+
* @param raw - `NormalizedMessage.raw` (requires `includeRawEvent`).
|
|
646
|
+
* @returns the non-empty `user_id`, or `undefined` when the event has none.
|
|
647
|
+
*/
|
|
648
|
+
export function senderUserIdOf(raw: unknown): string | undefined {
|
|
649
|
+
if (raw === null || typeof raw !== 'object') return undefined
|
|
650
|
+
const sender = (raw as { sender?: { sender_id?: { user_id?: unknown } } }).sender
|
|
651
|
+
const userId = sender?.sender_id?.user_id
|
|
652
|
+
return typeof userId === 'string' && userId !== '' ? userId : undefined
|
|
653
|
+
}
|
|
654
|
+
|
|
384
655
|
const LOGGER_LEVELS: Record<ChannelConnectOptions['loggerLevel'], LoggerLevel> = {
|
|
385
656
|
debug: LoggerLevel.debug,
|
|
386
657
|
info: LoggerLevel.info,
|
package/src/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type * from './questions.ts'
|
|
|
41
41
|
export type * from './settings/contract.ts'
|
|
42
42
|
export * from './access/index.ts'
|
|
43
43
|
export type { AccessGroupRecord, AccessRequestRecord, AccessUserRecord } from './domain.ts'
|
|
44
|
-
export { CATALOG_ROUTE,
|
|
44
|
+
export { CATALOG_ROUTE, CONFIG_ROUTE, CREDENTIALS_ROUTE, EDITABLE_KEYS, PANEL_COPY, PANEL_ROUTE, PICK_DIRECTORY_ROUTE, RECONNECT_ROUTE, RESTART_KEYS, STATUS_ROUTE } from './settings/contract.ts'
|
|
45
45
|
export type { PickDirectoryResponse } from './settings/contract.ts'
|
|
46
46
|
export { defaultOverlayPath, mergeOverlay, normalizeOverlay, OverlayStore, parseOverlay, pickEditable, resolveDshHome } from './settings/overlay.ts'
|
|
47
47
|
export { applyCatalog, emptyCatalog, FIELD_DESCRIPTORS, normalizeCredentialValue, readOnlyFields, SettingsSurface, withDirectoryBrowse } from './settings/surface.ts'
|
|
@@ -63,7 +63,7 @@ export { createLogicalSessionExport, DSH_CHANNELS_VERSION, LOGICAL_EXPORT_FORMAT
|
|
|
63
63
|
export { addUsage, applyProjectionViews, averageDecodeTokensPerSecond, averageTtftMs, cacheHitPct, EMPTY_TOTALS, foldAll, foldEvent, foldSessionStats, measureSessionContext, readProjectionViews, usageFromBuckets } from './stats.ts'
|
|
64
64
|
export { FeishuService } from './service.ts'
|
|
65
65
|
export { TurnStreamer } from './streamer.ts'
|
|
66
|
-
export { ensureWorkspace,
|
|
66
|
+
export { ensureWorkspace, stagingPathFor, stagingRoot, topicDirFor, topicTokenFor, workspacePathFor, workspaceTitleFor, worktreeBranchFor } from './workspaces.ts'
|
|
67
67
|
export { accessGroupRecordSchema, accessRequestRecordSchema, accessUserRecordSchema, appendSessionHistory, chatRouteKey, expiredRouteRecordSchema, feishuDomain, normalizeRouteRecord, normalizeSessionHistory, routeRecordSchema, sessionDirOf, sessionHistoryEntrySchema, inflightSnapshotSchema, processedRecordSchema } from './domain.ts'
|
|
68
68
|
|
|
69
69
|
/** Cordis function-plugin name. */
|
|
@@ -105,9 +105,10 @@ export function apply(ctx: HostContext, config?: unknown): void {
|
|
|
105
105
|
const resolved = resolveConfig(config)
|
|
106
106
|
const factory: ChannelFactory = (config as FeishuPluginOptions | undefined)?.channelFactory
|
|
107
107
|
?? defaultChannelFactory
|
|
108
|
-
// Web settings overlay (optional
|
|
108
|
+
// Web settings overlay at `$DSH_HOME/dsh-channels/settings.json` (optional
|
|
109
|
+
// file; degrades to "no overrides").
|
|
109
110
|
const overlay = new OverlayStore(
|
|
110
|
-
|
|
111
|
+
defaultOverlayPath(),
|
|
111
112
|
{ warn: (message, ...args) => ctx.logger?.warn?.(message, ...args) },
|
|
112
113
|
)
|
|
113
114
|
const service = new FeishuService(ctx, resolved, factory, undefined, overlay)
|