@zhin.js/adapter-lark 1.1.22 → 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,37 @@
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
+
3
35
  ## 1.1.22
4
36
 
5
37
  ### Patch Changes
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.22",
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.28",
39
- "@zhin.js/agent-feature": "1.1.13",
40
- "@zhin.js/client": "1.1.24",
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.56",
43
- "@zhin.js/feature-kit": "1.1.12",
44
- "@zhin.js/host-http": "1.1.14",
45
- "@zhin.js/im-contract": "1.1.11",
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.14",
48
- "@zhin.js/skill": "1.1.13",
49
- "@zhin.js/tool": "1.1.15"
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.28",
54
- "@zhin.js/command": "^1.1.14",
55
- "@zhin.js/core": "^1.1.56",
56
- "@zhin.js/host-http": "^1.1.14",
57
- "@zhin.js/permission": "^1.1.12",
58
- "zhin.js": "^1.1.21"
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.21"
76
+ "zhin.js": "1.1.22"
77
77
  },
78
78
  "files": [
79
79
  "adapters",
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,