@zhin.js/adapter-lark 1.1.11 → 1.1.13

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # @zhin.js/adapter-lark
2
2
 
3
+ ## 1.1.13
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [db06de8]
8
+ - @zhin.js/core@1.1.48
9
+
10
+ ## 1.1.12
11
+
12
+ ### Patch Changes
13
+
14
+ - e40899f: Adapters now project group owners and admins onto the canonical `sender.roles` names `owner` / `admin` (Telegram creator/administrator, KOOK guild owner and admin-permission roles, Lark chat owner/managers, QQ official guild owner/admin identities). KOOK numeric role ids moved from `roles` to `permissions` (`role:<id>`). `@zhin.js/command` exports `canManageConversation` ("private chat or owner/admin/master"), now shared by the Bilibili and earthquake subscription commands; Roulette lets owners and admins toggle the mute penalty and cancel a game.
15
+ - 9e1d1a6: 飞书 / KOOK 查询群主与管理员时,缓存失效那一刻同一个群(服务器)的并发消息共用一次查询,不再各自调用一次接口。
16
+ - Updated dependencies [1a11207]
17
+ - Updated dependencies [9943df6]
18
+ - Updated dependencies [5be09ae]
19
+ - Updated dependencies [a3ab153]
20
+ - Updated dependencies [e8f7309]
21
+ - Updated dependencies [5afc18f]
22
+ - Updated dependencies [d3563d9]
23
+ - Updated dependencies [c618eb7]
24
+ - Updated dependencies [fbfefb7]
25
+ - @zhin.js/tool@1.1.8
26
+ - @zhin.js/core@1.1.47
27
+ - @zhin.js/adapter@1.1.21
28
+ - @zhin.js/feature-kit@1.1.7
29
+ - @zhin.js/im-contract@1.1.5
30
+ - @zhin.js/agent-feature@1.1.8
31
+ - @zhin.js/skill@1.1.8
32
+ - @zhin.js/host-http@1.1.8
33
+
3
34
  ## 1.1.11
4
35
 
5
36
  ### Patch Changes
package/lib/endpoint.js CHANGED
@@ -9,6 +9,7 @@ import { Endpoint, createEndpointLifecycle, createRecallEndpointControl, } from
9
9
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
10
10
  import { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
11
11
  import { formatInboundContent, formatOutboundBody, generateMessageId, larkInboundConversation, resolveChatType, resolveSender, } from './protocol.js';
12
+ import { normalizeLarkSenderForPermit } from './platform-permit.js';
12
13
  import { registerLarkWebhookRoutes } from './webhook.js';
13
14
  import { createLarkLongConnection } from './long-connection.js';
14
15
  /** 出站 HTTP 调用统一 30s 超时。 */
@@ -31,6 +32,7 @@ export class LarkClient {
31
32
  setChatManagers = (id, users) => this.api.setChatManagers(id, users);
32
33
  removeChatManagers = (id, users) => this.api.removeChatManagers(id, users);
33
34
  }
35
+ const LARK_CHAT_MANAGER_CACHE_TTL_MS = 60_000;
34
36
  export class LarkEndpoint extends Endpoint {
35
37
  client = new LarkClient({
36
38
  getUserInfo: (id, type) => this.getUserInfo(id, type),
@@ -82,6 +84,9 @@ export class LarkEndpoint extends Endpoint {
82
84
  #lifecycle;
83
85
  #cardSources = new Map();
84
86
  #seen = new Map();
87
+ #chatManagers = new Map();
88
+ /** One chat.get per chat at a time: concurrent messages on a cache miss share the lookup. */
89
+ #chatManagerLookups = new Map();
85
90
  #startupGeneration = 0;
86
91
  #pendingInbound = [];
87
92
  constructor(options) {
@@ -301,24 +306,66 @@ export class LarkEndpoint extends Endpoint {
301
306
  this.#seen.delete(this.#seen.keys().next().value);
302
307
  }
303
308
  const conversation = larkInboundConversation(String(this.#options.id), msg);
304
- void this.emit('message.receive', {
309
+ void this.#emitInbound(msg, conversation).catch((err) => {
310
+ this.#logger.warn(formatCompact({
311
+ op: 'lark_gateway_receive_failed',
312
+ conversation: conversation.id,
313
+ error: err instanceof Error ? err.message : String(err),
314
+ }));
315
+ });
316
+ }
317
+ async #emitInbound(msg, conversation) {
318
+ const chatType = resolveChatType(msg.chat_id, msg.chat_type);
319
+ const senderId = resolveSender(msg);
320
+ const permit = chatType === 'group' && msg.chat_id
321
+ ? normalizeLarkSenderForPermit(await this.#resolveChatRole(msg.chat_id, senderId))
322
+ : undefined;
323
+ // 规范角色 owner / admin 进 roles;细粒度能力放 senderPermissions
324
+ const role = permit?.role === 'owner' || permit?.role === 'admin' ? permit.role : undefined;
325
+ await this.emit('message.receive', {
305
326
  conversation,
306
327
  message: { conversation, id: generateMessageId(msg) },
307
328
  content: formatInboundContent(msg),
308
- sender: { id: resolveSender(msg) },
329
+ sender: { id: senderId, ...(role ? { roles: [role] } : {}) },
309
330
  endpointId: this.#options.config.id,
310
331
  metadata: Object.freeze({
311
332
  messageType: msg.message_type,
312
- chatType: resolveChatType(msg.chat_id, msg.chat_type),
333
+ chatType,
334
+ ...(role ? { senderRole: role } : {}),
335
+ ...(role && permit?.permissions?.length ? { senderPermissions: [...permit.permissions] } : {}),
313
336
  }),
314
- }).catch((err) => {
315
- this.#logger.warn(formatCompact({
316
- op: 'lark_gateway_receive_failed',
317
- conversation: conversation.id,
318
- error: err instanceof Error ? err.message : String(err),
319
- }));
320
337
  });
321
338
  }
339
+ /** 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存;失败时保守地视为普通成员。 */
340
+ async #resolveChatRole(chatId, senderId) {
341
+ const now = Date.now();
342
+ for (const [id, entry] of this.#chatManagers)
343
+ if (now - entry.at > LARK_CHAT_MANAGER_CACHE_TTL_MS)
344
+ this.#chatManagers.delete(id);
345
+ let entry = this.#chatManagers.get(chatId);
346
+ if (!entry) {
347
+ let lookup = this.#chatManagerLookups.get(chatId);
348
+ if (!lookup) {
349
+ lookup = (async () => {
350
+ const info = await this.getChatInfo(chatId);
351
+ const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
352
+ const fetched = {
353
+ at: Date.now(),
354
+ ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
355
+ admins: new Set(admins),
356
+ };
357
+ this.#chatManagers.set(chatId, fetched);
358
+ return fetched;
359
+ })().finally(() => this.#chatManagerLookups.delete(chatId));
360
+ this.#chatManagerLookups.set(chatId, lookup);
361
+ }
362
+ entry = await lookup;
363
+ }
364
+ return {
365
+ isOwner: entry.owner !== undefined && entry.owner === senderId,
366
+ isAdmin: entry.admins.has(senderId),
367
+ };
368
+ }
322
369
  /** Only correlate callbacks to a card actually sent by this endpoint. */
323
370
  admitCardAction(event) {
324
371
  if (!this.#open)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-lark",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Zhin.js Lark/Feishu adapter (HTTP webhook and WebSocket long connection)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -35,24 +35,24 @@
35
35
  "dependencies": {
36
36
  "undici": "6.28.0",
37
37
  "@larksuiteoapi/node-sdk": "^1.67.0",
38
- "@zhin.js/adapter": "1.1.20",
39
- "@zhin.js/agent-feature": "1.1.7",
40
- "@zhin.js/core": "1.1.46",
41
- "@zhin.js/feature-kit": "1.1.6",
42
- "@zhin.js/host-http": "1.1.7",
43
- "@zhin.js/im-contract": "1.1.4",
44
- "@zhin.js/skill": "1.1.7",
45
- "@zhin.js/tool": "1.1.7",
46
- "@zhin.js/logger": "1.1.2"
38
+ "@zhin.js/adapter": "1.1.21",
39
+ "@zhin.js/agent-feature": "1.1.8",
40
+ "@zhin.js/core": "1.1.48",
41
+ "@zhin.js/feature-kit": "1.1.7",
42
+ "@zhin.js/host-http": "1.1.8",
43
+ "@zhin.js/im-contract": "1.1.5",
44
+ "@zhin.js/logger": "1.1.2",
45
+ "@zhin.js/skill": "1.1.8",
46
+ "@zhin.js/tool": "1.1.8"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "zod": "^4.0.0",
50
- "@zhin.js/adapter": "^1.1.20",
51
- "@zhin.js/command": "^1.1.8",
52
- "@zhin.js/core": "^1.1.46",
53
- "zhin.js": "^1.1.11",
54
- "@zhin.js/host-http": "^1.1.7",
55
- "@zhin.js/permission": "^1.1.7"
50
+ "@zhin.js/adapter": "^1.1.21",
51
+ "@zhin.js/command": "^1.1.9",
52
+ "@zhin.js/core": "^1.1.48",
53
+ "@zhin.js/host-http": "^1.1.8",
54
+ "@zhin.js/permission": "^1.1.7",
55
+ "zhin.js": "^1.1.13"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@zhin.js/command": {
@@ -70,7 +70,7 @@
70
70
  "typescript": "^6.0.3",
71
71
  "vitest": "^4.1.10",
72
72
  "zod": "^4.4.3",
73
- "zhin.js": "1.1.11"
73
+ "zhin.js": "1.1.13"
74
74
  },
75
75
  "files": [
76
76
  "adapters",
package/src/endpoint.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  type LarkMessage,
38
38
  type ResolvedLarkConfig,
39
39
  } from './protocol.js';
40
+ import { normalizeLarkSenderForPermit } from './platform-permit.js';
40
41
  import { registerLarkWebhookRoutes } from './webhook.js';
41
42
  import { createLarkLongConnection, type LarkLongConnectionFactory } from './long-connection.js';
42
43
 
@@ -100,6 +101,8 @@ export class LarkClient implements LarkClientApi {
100
101
  removeChatManagers = (id: string, users: string[]) => this.api.removeChatManagers(id, users);
101
102
  }
102
103
 
104
+ const LARK_CHAT_MANAGER_CACHE_TTL_MS = 60_000;
105
+
103
106
  export class LarkEndpoint extends Endpoint<LarkClient> {
104
107
  readonly client = new LarkClient({
105
108
  getUserInfo: (id, type) => this.getUserInfo(id, type),
@@ -154,6 +157,9 @@ export class LarkEndpoint extends Endpoint<LarkClient> {
154
157
  readonly #lifecycle: EndpointLifecycle;
155
158
  readonly #cardSources = new Map<string, { conversation: ConversationRef; time: number }>();
156
159
  readonly #seen = new Map<string, number>();
160
+ readonly #chatManagers = new Map<string, { at: number; owner?: string; admins: ReadonlySet<string> }>();
161
+ /** One chat.get per chat at a time: concurrent messages on a cache miss share the lookup. */
162
+ readonly #chatManagerLookups = new Map<string, Promise<{ at: number; owner?: string; admins: ReadonlySet<string> }>>();
157
163
  #startupGeneration = 0;
158
164
  readonly #pendingInbound: LarkMessage[] = [];
159
165
 
@@ -353,25 +359,70 @@ export class LarkEndpoint extends Endpoint<LarkClient> {
353
359
  if (this.#seen.size > 10_000) this.#seen.delete(this.#seen.keys().next().value!);
354
360
  }
355
361
  const conversation = larkInboundConversation(String(this.#options.id), msg);
356
- void this.emit('message.receive', {
362
+ void this.#emitInbound(msg, conversation).catch((err) => {
363
+ this.#logger.warn(formatCompact({
364
+ op: 'lark_gateway_receive_failed',
365
+ conversation: conversation.id,
366
+ error: err instanceof Error ? err.message : String(err),
367
+ }));
368
+ });
369
+ }
370
+
371
+ async #emitInbound(msg: LarkMessage, conversation: ConversationRef): Promise<void> {
372
+ const chatType = resolveChatType(msg.chat_id, msg.chat_type);
373
+ const senderId = resolveSender(msg);
374
+ const permit = chatType === 'group' && msg.chat_id
375
+ ? normalizeLarkSenderForPermit(await this.#resolveChatRole(msg.chat_id, senderId))
376
+ : undefined;
377
+ // 规范角色 owner / admin 进 roles;细粒度能力放 senderPermissions
378
+ const role = permit?.role === 'owner' || permit?.role === 'admin' ? permit.role : undefined;
379
+ await this.emit('message.receive', {
357
380
  conversation,
358
381
  message: { conversation, id: generateMessageId(msg) },
359
382
  content: formatInboundContent(msg),
360
- sender: { id: resolveSender(msg) },
383
+ sender: { id: senderId, ...(role ? { roles: [role] } : {}) },
361
384
  endpointId: this.#options.config.id,
362
385
  metadata: Object.freeze({
363
386
  messageType: msg.message_type,
364
- chatType: resolveChatType(msg.chat_id, msg.chat_type),
387
+ chatType,
388
+ ...(role ? { senderRole: role } : {}),
389
+ ...(role && permit?.permissions?.length ? { senderPermissions: [...permit.permissions] } : {}),
365
390
  }),
366
- }).catch((err) => {
367
- this.#logger.warn(formatCompact({
368
- op: 'lark_gateway_receive_failed',
369
- conversation: conversation.id,
370
- error: err instanceof Error ? err.message : String(err),
371
- }));
372
391
  });
373
392
  }
374
393
 
394
+ /** 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存;失败时保守地视为普通成员。 */
395
+ async #resolveChatRole(chatId: string, senderId: string): Promise<{ isOwner: boolean; isAdmin: boolean }> {
396
+ const now = Date.now();
397
+ for (const [id, entry] of this.#chatManagers) if (now - entry.at > LARK_CHAT_MANAGER_CACHE_TTL_MS) this.#chatManagers.delete(id);
398
+ let entry = this.#chatManagers.get(chatId);
399
+ if (!entry) {
400
+ let lookup = this.#chatManagerLookups.get(chatId);
401
+ if (!lookup) {
402
+ lookup = (async () => {
403
+ const info = await this.getChatInfo(chatId) as {
404
+ owner_id?: unknown;
405
+ user_manager_id_list?: unknown;
406
+ } | null;
407
+ const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
408
+ const fetched = {
409
+ at: Date.now(),
410
+ ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
411
+ admins: new Set(admins) as ReadonlySet<string>,
412
+ };
413
+ this.#chatManagers.set(chatId, fetched);
414
+ return fetched;
415
+ })().finally(() => this.#chatManagerLookups.delete(chatId));
416
+ this.#chatManagerLookups.set(chatId, lookup);
417
+ }
418
+ entry = await lookup;
419
+ }
420
+ return {
421
+ isOwner: entry.owner !== undefined && entry.owner === senderId,
422
+ isAdmin: entry.admins.has(senderId),
423
+ };
424
+ }
425
+
375
426
  /** Only correlate callbacks to a card actually sent by this endpoint. */
376
427
  admitCardAction(event: LarkCardAction): void {
377
428
  if (!this.#open) return;