@zhin.js/adapter-lark 1.1.21 → 1.1.23

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,70 @@
1
1
  # @zhin.js/adapter-lark
2
2
 
3
+ ## 1.1.23
4
+
5
+ ### Patch Changes
6
+
7
+ - bc6ccb1: Deep audit bug fixes and 7 architectural improvements:
8
+ - Sink Agent Turn ingress pipeline from `@zhin.js/cli` into `@zhin.js/agent/runtime` (`plugin-runtime/ingress/`) and decompose `ingress-route.ts` into modular pipeline stages.
9
+ - Bind Session State Plane (`JsxButtonRegistry`, `InteractionRuntime`, `RuntimeInteractiveRouter`) to `PluginIncarnationRef` so active buttons and interactions survive unrelated-plugin HMR while expiring when the owning plugin reloads, and support per-conversation fallback numbering (`numberingScope: 'conversation'`).
10
+ - Establish `conversation_events` as the canonical message SSOT and reference `conversation_event_id`, `conversation_key`, `platform_message_id`, and `message_key` in `agent_messages` (`AGENT_MESSAGE_QUERY_VERSION = 2`), threading `conversationSequence` across inbound and outbound delivery receipts.
11
+ - Push down `DatabaseContextRepository` and `DatabaseScheduleJobStorage` full-session/full-table memory scans to indexed SQL queries.
12
+ - Add kernel-level inbound `replyTo` and `mentioned` auto-promotion (`promoteInboundMessagePayload`) plus reusable `InboundDeduper` and `TtlLookupCache` primitives in `@zhin.js/adapter`.
13
+ - Add `MiddlewareContext.dispatch` (`InboundDispatchSummary`) and `onAfterDispatch` hooks for `after-dispatch` middlewares, and support incremental rendering of in-place `envelope.payload` array mutations in `OutboundDeliveryRuntime`.
14
+ - Unify `HistoryWindowTracker` and `AgentCompactionRuntime` token/message budgets (`resolveUnifiedHistoryAndCompactionBudget`).
15
+ - Fix schema validation, database transaction connection cleanup and SQLite WAL mode, schedule timer wheel head-of-line blocking, security policy bash read paths and subagent guard propagation, group merge window permission carrier handoff, micro-compact namespaced tool matching, and WebSocket force-close / inbound segment mapping across adapters.
16
+ - Updated dependencies [3a8504b]
17
+ - Updated dependencies [6163abc]
18
+ - Updated dependencies [fed2c27]
19
+ - Updated dependencies [bc6ccb1]
20
+ - Updated dependencies [72b5a12]
21
+ - Updated dependencies [aa41889]
22
+ - Updated dependencies [4427e72]
23
+ - Updated dependencies [22bd14a]
24
+ - @zhin.js/agent-feature@1.1.14
25
+ - @zhin.js/core@1.1.57
26
+ - @zhin.js/client@1.1.25
27
+ - @zhin.js/page@1.1.15
28
+ - @zhin.js/im-contract@1.1.12
29
+ - @zhin.js/adapter@1.1.29
30
+ - @zhin.js/host-http@1.1.15
31
+ - @zhin.js/feature-kit@1.1.13
32
+ - @zhin.js/tool@1.1.16
33
+ - @zhin.js/skill@1.1.14
34
+
35
+ ## 1.1.22
36
+
37
+ ### Patch Changes
38
+
39
+ - 41ff1a5: ADR 0044 C5: every official adapter now declares a capability `profile` (its `segments` moved into `profile.default.messaging.segments`). icqq declares the complete profile (per conversation kind: scene, all 14 actions with real limits, events, feedback, delivery) and implements the seven new management ports (`pokeUser`, `unmuteGroupMember`, `muteAllGroupMembers`, `setGroupCard`, `setGroupTitle`, `setGroupEssence`, `inviteGroupMember`) with fresh bot-role reads. qq official declares its passive reply windows; undeclared capabilities stay unsupported.
40
+ - cbd3f14: ADR 0044 integration and C6 guard. `delivery.spool` now reproduces the pre-0044 media rule (icqq `false`, every other official adapter `true`). icqq narrows `actions.recall.others` unless the Bot is admin or owner, and declares recall for groups. Endpoint kind capabilities merge the Endpoint's `operations` (Discord reactions keep working; Discord declares reaction feedback). Adapters in the `@zhin.js/` scope are trusted: their custom prompt text is not labelled `external_content`. Core no longer keeps a telegram/discord interactive-policy table (adapters declare `segments.interactive`). New `pnpm check:no-platform-branches` blocks platform-name branches in runtime code.
41
+ - 4fe49b5: 支持配置按需生效:分离保存与有效版本,独立变更先应用,账号资料更新保留连接,模型按回合使用确定版本;Console 返回结构化应用结果。监听改为仅显式 --watch 启用,开发模板显式选择源码和配置监听。
42
+
43
+ 自变更消费配置应用回执,避免已处理配置被二次重载;持久化本次写入修订和应用结果,未知结果或修订冲突时停止自动续作并保留现场,恢复不会覆盖未经本次变更确认的配置。
44
+
45
+ - Updated dependencies [b249e5e]
46
+ - Updated dependencies [57661f6]
47
+ - Updated dependencies [cbd3f14]
48
+ - Updated dependencies [299863a]
49
+ - Updated dependencies [aaa93d7]
50
+ - Updated dependencies [bd524d1]
51
+ - Updated dependencies [126ae99]
52
+ - Updated dependencies [4fe49b5]
53
+ - Updated dependencies [cce07db]
54
+ - Updated dependencies [b652db3]
55
+ - Updated dependencies [0315ba0]
56
+ - Updated dependencies [00be764]
57
+ - @zhin.js/core@1.1.56
58
+ - @zhin.js/adapter@1.1.28
59
+ - @zhin.js/im-contract@1.1.11
60
+ - @zhin.js/tool@1.1.15
61
+ - @zhin.js/skill@1.1.13
62
+ - @zhin.js/host-http@1.1.14
63
+ - @zhin.js/feature-kit@1.1.12
64
+ - @zhin.js/client@1.1.24
65
+ - @zhin.js/page@1.1.14
66
+ - @zhin.js/agent-feature@1.1.13
67
+
3
68
  ## 1.1.21
4
69
 
5
70
  ### Patch Changes
@@ -8,15 +8,27 @@ import {} from "../../lib/protocol.js";
8
8
  export { LarkEndpoint } from "../../lib/endpoint.js";
9
9
  export default defineAdapter({
10
10
  capabilities: ['inbound', 'outbound'],
11
- operations: ['recall'],
12
11
  // image 段 canonical MediaRef 经 /im/v1/images 物化为 image_key
13
12
  // (base64 解码 / path 读盘 / url 下载后上传,kind=file 平台引用直通);
14
13
  // canonical Markdown 由 endpoint codec 转为 lark_md 消息卡片;
15
14
  // 卡片交互未接入出站通道,交互段降级纯文本。
16
- segments: {
17
- outboundMedia: ['url', 'upload'],
18
- interactive: 'native',
19
- markdown: 'native',
15
+ profile: {
16
+ default: {
17
+ messaging: {
18
+ segments: {
19
+ outboundMedia: ['url', 'upload'],
20
+ interactive: 'native',
21
+ markdown: 'native',
22
+ },
23
+ quote: true,
24
+ recall: {},
25
+ },
26
+ actions: {
27
+ recall: { scopes: ['private', 'group', 'channel'] },
28
+ quoteReply: { scopes: ['private', 'group', 'channel'] },
29
+ },
30
+ delivery: { send: true, proactive: true, spool: true },
31
+ },
20
32
  },
21
33
  createAdapter: context => new LarkAdapter(context),
22
34
  });
@@ -12,15 +12,27 @@ export type { LarkEndpointOptions, LarkFetch } from '../../src/endpoint.js';
12
12
 
13
13
  export default defineAdapter<LarkEndpointConfig>({
14
14
  capabilities: ['inbound', 'outbound'],
15
- operations: ['recall'],
16
15
  // image 段 canonical MediaRef 经 /im/v1/images 物化为 image_key
17
16
  // (base64 解码 / path 读盘 / url 下载后上传,kind=file 平台引用直通);
18
17
  // canonical Markdown 由 endpoint codec 转为 lark_md 消息卡片;
19
18
  // 卡片交互未接入出站通道,交互段降级纯文本。
20
- segments: {
21
- outboundMedia: ['url', 'upload'],
22
- interactive: 'native',
23
- markdown: 'native',
19
+ profile: {
20
+ default: {
21
+ messaging: {
22
+ segments: {
23
+ outboundMedia: ['url', 'upload'],
24
+ interactive: 'native',
25
+ markdown: 'native',
26
+ },
27
+ quote: true,
28
+ recall: {},
29
+ },
30
+ actions: {
31
+ recall: { scopes: ['private', 'group', 'channel'] },
32
+ quoteReply: { scopes: ['private', 'group', 'channel'] },
33
+ },
34
+ delivery: { send: true, proactive: true, spool: true },
35
+ },
24
36
  },
25
37
  createAdapter: context => new LarkAdapter(context),
26
38
  });
package/lib/client.js CHANGED
@@ -7,10 +7,10 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
7
7
  */
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { basename } from 'node:path';
10
- import { createEndpointLifecycle, createEndpointOperation, createRecallEndpointControl, } from 'zhin.js/adapter';
10
+ import { createEndpointLifecycle, createEndpointOperation, createRecallEndpointControl, InboundDeduper, TtlLookupCache, } from 'zhin.js/adapter';
11
11
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
12
12
  import { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
13
- import { formatInboundContent, formatOutboundBody, generateMessageId, larkInboundConversation, resolveChatType, resolveSender, } from './protocol.js';
13
+ import { formatInboundContent, formatOutboundBody, generateMessageId, isLarkBotMentioned, larkInboundConversation, resolveChatType, resolveSender, } from './protocol.js';
14
14
  import { normalizeLarkSenderForPermit } from './platform-permit.js';
15
15
  import { registerLarkWebhookRoutes } from './webhook.js';
16
16
  import { createLarkLongConnection } from './long-connection.js';
@@ -61,10 +61,10 @@ export class LarkClient extends SdkClient {
61
61
  #started = false;
62
62
  #lifecycle;
63
63
  #cardSources = new Map();
64
- #seen = new Map();
65
- #chatManagers = new Map();
66
- /** One chat.get per chat at a time: concurrent messages on a cache miss share the lookup. */
67
- #chatManagerLookups = new Map();
64
+ #seen = new InboundDeduper({ ttlMs: 300_000, maxEntries: 10_000 });
65
+ #chatManagers = new TtlLookupCache({
66
+ ttlMs: LARK_CHAT_MANAGER_CACHE_TTL_MS,
67
+ });
68
68
  #startupGeneration = 0;
69
69
  #pendingInbound = [];
70
70
  #bindSignal;
@@ -304,17 +304,8 @@ export class LarkClient extends SdkClient {
304
304
  if (!this.#open)
305
305
  return;
306
306
  void this.receiveInboundCallback(async () => {
307
- if (msg.message_id) {
308
- const now = Date.now();
309
- for (const [id, time] of this.#seen)
310
- if (now - time > 300_000)
311
- this.#seen.delete(id);
312
- if (this.#seen.has(msg.message_id))
313
- return;
314
- this.#seen.set(msg.message_id, now);
315
- if (this.#seen.size > 10_000)
316
- this.#seen.delete(this.#seen.keys().next().value);
317
- }
307
+ if (!this.#seen.shouldProcess(msg.message_id))
308
+ return;
318
309
  await this.#emitInbound(msg, larkInboundConversation(String(this.#options.id), msg), nativeEvent);
319
310
  }).catch((err) => this.#logger.warn(formatCompact({ op: 'lark_gateway_receive_failed',
320
311
  conversation: msg.chat_id, error: err instanceof Error ? err.message : String(err) })));
@@ -327,6 +318,8 @@ export class LarkClient extends SdkClient {
327
318
  : undefined;
328
319
  // 规范角色 owner / admin 进 roles;细粒度能力放 senderPermissions
329
320
  const role = permit?.role === 'owner' || permit?.role === 'admin' ? permit.role : undefined;
321
+ const mentioned = isLarkBotMentioned(msg);
322
+ const replyToId = msg.parent_id || msg.root_id;
330
323
  await this.publishEvent('message.receive', {
331
324
  event: nativeEvent,
332
325
  conversation,
@@ -334,6 +327,8 @@ export class LarkClient extends SdkClient {
334
327
  content: formatInboundContent(msg),
335
328
  ...(senderId ? { sender: { id: senderId, ...(role ? { roles: [role] } : {}) } } : {}),
336
329
  endpointId: this.#options.config.id,
330
+ ...(mentioned ? { mentioned: true } : {}),
331
+ ...(replyToId ? { replyTo: { id: replyToId } } : {}),
337
332
  metadata: Object.freeze({
338
333
  messageType: msg.message_type,
339
334
  chatType,
@@ -344,29 +339,14 @@ export class LarkClient extends SdkClient {
344
339
  }
345
340
  /** 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存;失败时保守地视为普通成员。 */
346
341
  async #resolveChatRole(chatId, senderId) {
347
- const now = Date.now();
348
- for (const [id, entry] of this.#chatManagers)
349
- if (now - entry.at > LARK_CHAT_MANAGER_CACHE_TTL_MS)
350
- this.#chatManagers.delete(id);
351
- let entry = this.#chatManagers.get(chatId);
352
- if (!entry) {
353
- let lookup = this.#chatManagerLookups.get(chatId);
354
- if (!lookup) {
355
- lookup = (async () => {
356
- const info = await this.getChatInfo(chatId);
357
- const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
358
- const fetched = {
359
- at: Date.now(),
360
- ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
361
- admins: new Set(admins),
362
- };
363
- this.#chatManagers.set(chatId, fetched);
364
- return fetched;
365
- })().finally(() => this.#chatManagerLookups.delete(chatId));
366
- this.#chatManagerLookups.set(chatId, lookup);
367
- }
368
- entry = await lookup;
369
- }
342
+ const entry = await this.#chatManagers.getOrLoad(chatId, async () => {
343
+ const info = await this.getChatInfo(chatId);
344
+ const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
345
+ return {
346
+ ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
347
+ admins: new Set(admins),
348
+ };
349
+ });
370
350
  return {
371
351
  isOwner: entry.owner !== undefined && entry.owner === senderId,
372
352
  isAdmin: entry.admins.has(senderId),
@@ -391,11 +371,8 @@ export class LarkClient extends SdkClient {
391
371
  if (!source || source.conversation.id !== chatId || Date.now() - source.time > 300_000)
392
372
  return;
393
373
  const dedup = `card:${event.token ?? `${sourceId}:${sender}:${payload}`}`;
394
- if (this.#seen.has(dedup))
374
+ if (!this.#seen.shouldProcess(dedup))
395
375
  return;
396
- this.#seen.set(dedup, Date.now());
397
- if (this.#seen.size > 10_000)
398
- this.#seen.delete(this.#seen.keys().next().value);
399
376
  const conversation = source.conversation;
400
377
  void this.publishEvent('message.receive', {
401
378
  event,
package/lib/protocol.d.ts CHANGED
@@ -111,6 +111,8 @@ export declare function resolveChatType(chatId?: string, chatType?: string): 'gr
111
111
  export declare function larkInboundConversation(endpointKey: string, msg: LarkMessage): ConversationRef;
112
112
  export declare function resolveSender(msg: LarkMessage): string | undefined;
113
113
  export declare function generateMessageId(msg: LarkMessage): string;
114
+ /** 判断入站消息是否 @ 了非全员目标(Lark 群消息 @ 机器人在 mentions 列表中,key 不为 `@_all`)。 */
115
+ export declare function isLarkBotMentioned(msg: LarkMessage): boolean;
114
116
  /** Build inbound text for OutboundMessageService.receive. */
115
117
  export declare function formatInboundContent(msg: LarkMessage): string;
116
118
  /** 验签时间戳时效窗口(防重放)。 */
package/lib/protocol.js CHANGED
@@ -89,6 +89,12 @@ export function resolveSender(msg) {
89
89
  export function generateMessageId(msg) {
90
90
  return msg.message_id || `${msg.create_time ?? Date.now()}`;
91
91
  }
92
+ /** 判断入站消息是否 @ 了非全员目标(Lark 群消息 @ 机器人在 mentions 列表中,key 不为 `@_all`)。 */
93
+ export function isLarkBotMentioned(msg) {
94
+ if (!Array.isArray(msg.mentions) || msg.mentions.length === 0)
95
+ return false;
96
+ return msg.mentions.some((m) => m && m.key !== '@_all');
97
+ }
92
98
  /** Build inbound text for OutboundMessageService.receive. */
93
99
  export function formatInboundContent(msg) {
94
100
  if (!msg.content || !msg.message_type)
package/lib/webhook.js CHANGED
@@ -56,9 +56,10 @@ export async function handleLarkWebhookRequest(request, response, handler) {
56
56
  response.end(JSON.stringify({ challenge: event.challenge }));
57
57
  return;
58
58
  }
59
- if (event.header?.event_type === 'card.action.trigger' && handler.isOpen)
59
+ const headerEventType = event.header?.event_type;
60
+ if (headerEventType === 'card.action.trigger' && handler.isOpen)
60
61
  handler.admitCardAction?.(event.event);
61
- if (event.type === 'event_callback' && event.event?.message && handler.isOpen) {
62
+ if ((event.type === 'event_callback' || headerEventType === 'im.message.receive_v1') && event.event?.message && handler.isOpen) {
62
63
  const message = {
63
64
  ...event.event.message,
64
65
  sender: event.event.message.sender ?? event.event.sender,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-lark",
3
- "version": "1.1.21",
3
+ "version": "1.1.23",
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,27 +35,27 @@
35
35
  "dependencies": {
36
36
  "@larksuiteoapi/node-sdk": "^1.67.0",
37
37
  "undici": "6.29.1",
38
- "@zhin.js/adapter": "1.1.27",
39
- "@zhin.js/agent-feature": "1.1.12",
40
- "@zhin.js/client": "1.1.23",
38
+ "@zhin.js/adapter": "1.1.29",
39
+ "@zhin.js/agent-feature": "1.1.14",
40
+ "@zhin.js/client": "1.1.25",
41
41
  "@zhin.js/console-contract": "1.1.3",
42
- "@zhin.js/core": "1.1.55",
43
- "@zhin.js/feature-kit": "1.1.11",
44
- "@zhin.js/host-http": "1.1.13",
45
- "@zhin.js/im-contract": "1.1.10",
42
+ "@zhin.js/core": "1.1.57",
43
+ "@zhin.js/feature-kit": "1.1.13",
44
+ "@zhin.js/host-http": "1.1.15",
45
+ "@zhin.js/im-contract": "1.1.12",
46
46
  "@zhin.js/logger": "1.1.2",
47
- "@zhin.js/page": "1.1.13",
48
- "@zhin.js/skill": "1.1.12",
49
- "@zhin.js/tool": "1.1.14"
47
+ "@zhin.js/page": "1.1.15",
48
+ "@zhin.js/skill": "1.1.14",
49
+ "@zhin.js/tool": "1.1.16"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "zod": "^4.0.0",
53
- "@zhin.js/adapter": "^1.1.27",
54
- "@zhin.js/command": "^1.1.13",
55
- "@zhin.js/core": "^1.1.55",
56
- "@zhin.js/host-http": "^1.1.13",
57
- "@zhin.js/permission": "^1.1.11",
58
- "zhin.js": "^1.1.20"
53
+ "@zhin.js/adapter": "^1.1.29",
54
+ "@zhin.js/command": "^1.1.15",
55
+ "@zhin.js/core": "^1.1.57",
56
+ "@zhin.js/host-http": "^1.1.15",
57
+ "@zhin.js/permission": "^1.1.13",
58
+ "zhin.js": "^1.1.22"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@zhin.js/command": {
@@ -73,7 +73,7 @@
73
73
  "typescript": "^6.0.3",
74
74
  "vitest": "^4.1.10",
75
75
  "zod": "^4.4.3",
76
- "zhin.js": "1.1.20"
76
+ "zhin.js": "1.1.22"
77
77
  },
78
78
  "files": [
79
79
  "adapters",
package/plugin.js CHANGED
@@ -5,6 +5,7 @@ import { checkLarkPlatformPermit } from "./lib/platform-permit.js";
5
5
  import { createLarkConsoleRpc } from "./lib/console.js";
6
6
  export default definePlugin({
7
7
  name: 'lark',
8
+ configUpdate: { featurePaths: [['nickname'], ['profile'], ['endpoints']] },
8
9
  metadata: {
9
10
  displayName: 'Lark/Feishu (飞书) Adapter',
10
11
  },
package/src/client.ts CHANGED
@@ -20,6 +20,8 @@ import {
20
20
  type EndpointSendRequest,
21
21
  type EndpointManagementRequest,
22
22
  type EndpointGroupRequest,
23
+ InboundDeduper,
24
+ TtlLookupCache,
23
25
  } from 'zhin.js/adapter';
24
26
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
25
27
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
@@ -33,6 +35,7 @@ import {
33
35
  formatInboundContent,
34
36
  formatOutboundBody,
35
37
  generateMessageId,
38
+ isLarkBotMentioned,
36
39
  larkInboundConversation,
37
40
  resolveChatType,
38
41
  resolveSender,
@@ -137,10 +140,10 @@ export class LarkClient extends SdkClient {
137
140
  #started = false;
138
141
  readonly #lifecycle: EndpointLifecycle;
139
142
  readonly #cardSources = new Map<string, { conversation: ConversationRef; time: number }>();
140
- readonly #seen = new Map<string, number>();
141
- readonly #chatManagers = new Map<string, { at: number; owner?: string; admins: ReadonlySet<string> }>();
142
- /** One chat.get per chat at a time: concurrent messages on a cache miss share the lookup. */
143
- readonly #chatManagerLookups = new Map<string, Promise<{ at: number; owner?: string; admins: ReadonlySet<string> }>>();
143
+ readonly #seen = new InboundDeduper({ ttlMs: 300_000, maxEntries: 10_000 });
144
+ readonly #chatManagers = new TtlLookupCache<string, { owner?: string; admins: ReadonlySet<string> }>({
145
+ ttlMs: LARK_CHAT_MANAGER_CACHE_TTL_MS,
146
+ });
144
147
  #startupGeneration = 0;
145
148
  readonly #pendingInbound: { message: LarkMessage; event: unknown }[] = [];
146
149
 
@@ -358,13 +361,7 @@ export class LarkClient extends SdkClient {
358
361
  admit(msg: LarkMessage, nativeEvent: unknown = msg): void {
359
362
  if (!this.#open) return;
360
363
  void this.receiveInboundCallback(async () => {
361
- if (msg.message_id) {
362
- const now = Date.now();
363
- for (const [id, time] of this.#seen) if (now - time > 300_000) this.#seen.delete(id);
364
- if (this.#seen.has(msg.message_id)) return;
365
- this.#seen.set(msg.message_id, now);
366
- if (this.#seen.size > 10_000) this.#seen.delete(this.#seen.keys().next().value!);
367
- }
364
+ if (!this.#seen.shouldProcess(msg.message_id)) return;
368
365
  await this.#emitInbound(msg, larkInboundConversation(String(this.#options.id), msg), nativeEvent);
369
366
  }).catch((err) => this.#logger.warn(formatCompact({ op: 'lark_gateway_receive_failed',
370
367
  conversation: msg.chat_id, error: err instanceof Error ? err.message : String(err) })));
@@ -378,6 +375,8 @@ export class LarkClient extends SdkClient {
378
375
  : undefined;
379
376
  // 规范角色 owner / admin 进 roles;细粒度能力放 senderPermissions
380
377
  const role = permit?.role === 'owner' || permit?.role === 'admin' ? permit.role : undefined;
378
+ const mentioned = isLarkBotMentioned(msg);
379
+ const replyToId = msg.parent_id || msg.root_id;
381
380
  await this.publishEvent('message.receive', {
382
381
  event: nativeEvent,
383
382
  conversation,
@@ -385,6 +384,8 @@ export class LarkClient extends SdkClient {
385
384
  content: formatInboundContent(msg),
386
385
  ...(senderId ? { sender: { id: senderId, ...(role ? { roles: [role] } : {}) } } : {}),
387
386
  endpointId: this.#options.config.id,
387
+ ...(mentioned ? { mentioned: true } : {}),
388
+ ...(replyToId ? { replyTo: { id: replyToId } } : {}),
388
389
  metadata: Object.freeze({
389
390
  messageType: msg.message_type,
390
391
  chatType,
@@ -396,30 +397,17 @@ export class LarkClient extends SdkClient {
396
397
 
397
398
  /** 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存;失败时保守地视为普通成员。 */
398
399
  async #resolveChatRole(chatId: string, senderId: string): Promise<{ isOwner: boolean; isAdmin: boolean }> {
399
- const now = Date.now();
400
- for (const [id, entry] of this.#chatManagers) if (now - entry.at > LARK_CHAT_MANAGER_CACHE_TTL_MS) this.#chatManagers.delete(id);
401
- let entry = this.#chatManagers.get(chatId);
402
- if (!entry) {
403
- let lookup = this.#chatManagerLookups.get(chatId);
404
- if (!lookup) {
405
- lookup = (async () => {
406
- const info = await this.getChatInfo(chatId) as {
407
- owner_id?: unknown;
408
- user_manager_id_list?: unknown;
409
- } | null;
410
- const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
411
- const fetched = {
412
- at: Date.now(),
413
- ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
414
- admins: new Set(admins) as ReadonlySet<string>,
415
- };
416
- this.#chatManagers.set(chatId, fetched);
417
- return fetched;
418
- })().finally(() => this.#chatManagerLookups.delete(chatId));
419
- this.#chatManagerLookups.set(chatId, lookup);
420
- }
421
- entry = await lookup;
422
- }
400
+ const entry = await this.#chatManagers.getOrLoad(chatId, async () => {
401
+ const info = await this.getChatInfo(chatId) as {
402
+ owner_id?: unknown;
403
+ user_manager_id_list?: unknown;
404
+ } | null;
405
+ const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
406
+ return {
407
+ ...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
408
+ admins: new Set(admins) as ReadonlySet<string>,
409
+ };
410
+ });
423
411
  return {
424
412
  isOwner: entry.owner !== undefined && entry.owner === senderId,
425
413
  isAdmin: entry.admins.has(senderId),
@@ -442,9 +430,7 @@ export class LarkClient extends SdkClient {
442
430
  const source = this.#cardSources.get(sourceId);
443
431
  if (!source || source.conversation.id !== chatId || Date.now() - source.time > 300_000) return;
444
432
  const dedup = `card:${event.token ?? `${sourceId}:${sender}:${payload}`}`;
445
- if (this.#seen.has(dedup)) return;
446
- this.#seen.set(dedup, Date.now());
447
- if (this.#seen.size > 10_000) this.#seen.delete(this.#seen.keys().next().value!);
433
+ if (!this.#seen.shouldProcess(dedup)) return;
448
434
  const conversation = source.conversation;
449
435
  void this.publishEvent('message.receive', {
450
436
  event,
package/src/protocol.ts CHANGED
@@ -206,6 +206,12 @@ export function generateMessageId(msg: LarkMessage): string {
206
206
  return msg.message_id || `${msg.create_time ?? Date.now()}`;
207
207
  }
208
208
 
209
+ /** 判断入站消息是否 @ 了非全员目标(Lark 群消息 @ 机器人在 mentions 列表中,key 不为 `@_all`)。 */
210
+ export function isLarkBotMentioned(msg: LarkMessage): boolean {
211
+ if (!Array.isArray(msg.mentions) || msg.mentions.length === 0) return false;
212
+ return msg.mentions.some((m) => m && m.key !== '@_all');
213
+ }
214
+
209
215
  /** Build inbound text for OutboundMessageService.receive. */
210
216
  export function formatInboundContent(msg: LarkMessage): string {
211
217
  if (!msg.content || !msg.message_type) return '';
package/src/webhook.ts CHANGED
@@ -96,9 +96,10 @@ export async function handleLarkWebhookRequest(
96
96
  return;
97
97
  }
98
98
 
99
- if ((event as LarkEventBody & { header?: { event_type?: string } }).header?.event_type === 'card.action.trigger' && handler.isOpen) handler.admitCardAction?.(event.event as LarkCardAction);
99
+ const headerEventType = (event as LarkEventBody & { header?: { event_type?: string } }).header?.event_type;
100
+ if (headerEventType === 'card.action.trigger' && handler.isOpen) handler.admitCardAction?.(event.event as LarkCardAction);
100
101
 
101
- if (event.type === 'event_callback' && event.event?.message && handler.isOpen) {
102
+ if ((event.type === 'event_callback' || headerEventType === 'im.message.receive_v1') && event.event?.message && handler.isOpen) {
102
103
  const message: LarkMessage = {
103
104
  ...event.event.message,
104
105
  sender: event.event.message.sender ?? event.event.sender,