@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 +65 -0
- package/adapters/lark/index.js +17 -5
- package/adapters/lark/index.ts +17 -5
- package/lib/client.js +21 -44
- package/lib/protocol.d.ts +2 -0
- package/lib/protocol.js +6 -0
- package/lib/webhook.js +3 -2
- package/package.json +18 -18
- package/plugin.js +1 -0
- package/src/client.ts +24 -38
- package/src/protocol.ts +6 -0
- package/src/webhook.ts +3 -2
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
|
package/adapters/lark/index.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
});
|
package/adapters/lark/index.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
65
|
-
#chatManagers = new
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
39
|
-
"@zhin.js/agent-feature": "1.1.
|
|
40
|
-
"@zhin.js/client": "1.1.
|
|
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.
|
|
43
|
-
"@zhin.js/feature-kit": "1.1.
|
|
44
|
-
"@zhin.js/host-http": "1.1.
|
|
45
|
-
"@zhin.js/im-contract": "1.1.
|
|
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.
|
|
48
|
-
"@zhin.js/skill": "1.1.
|
|
49
|
-
"@zhin.js/tool": "1.1.
|
|
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.
|
|
54
|
-
"@zhin.js/command": "^1.1.
|
|
55
|
-
"@zhin.js/core": "^1.1.
|
|
56
|
-
"@zhin.js/host-http": "^1.1.
|
|
57
|
-
"@zhin.js/permission": "^1.1.
|
|
58
|
-
"zhin.js": "^1.1.
|
|
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.
|
|
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
|
|
141
|
-
readonly #chatManagers = new
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|