@larksuite/openclaw-lark 2026.5.7-beta.0 → 2026.5.12-beta.0
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/package.json +1 -1
- package/src/card/reply-dispatcher-types.d.ts +2 -0
- package/src/card/reply-dispatcher.js +4 -1
- package/src/messaging/inbound/dispatch-builders.d.ts +6 -4
- package/src/messaging/inbound/dispatch-builders.js +36 -9
- package/src/messaging/inbound/dispatch.js +16 -9
- package/src/messaging/inbound/handler.d.ts +8 -6
- package/src/messaging/inbound/handler.js +24 -13
- package/src/messaging/inbound/mention.d.ts +2 -2
- package/src/messaging/inbound/mention.js +2 -2
- package/src/messaging/inbound/sentinel-store.d.ts +37 -0
- package/src/messaging/inbound/sentinel-store.js +98 -0
- package/src/messaging/inbound/user-name-cache-store.d.ts +42 -2
- package/src/messaging/inbound/user-name-cache-store.js +155 -18
- package/src/messaging/inbound/user-name-cache.d.ts +12 -0
- package/src/messaging/inbound/user-name-cache.js +122 -6
- package/src/messaging/outbound/deliver.d.ts +2 -0
- package/src/messaging/outbound/deliver.js +46 -24
- package/src/messaging/outbound/media.js +69 -9
- package/src/messaging/outbound/normalize-mentions.d.ts +50 -0
- package/src/messaging/outbound/normalize-mentions.js +166 -0
- package/src/messaging/outbound/outbound.js +10 -2
- package/src/messaging/outbound/send.d.ts +2 -0
- package/src/messaging/outbound/send.js +50 -1
- package/src/messaging/types.d.ts +1 -1
package/package.json
CHANGED
|
@@ -90,6 +90,8 @@ export interface CreateFeishuReplyDispatcherParams {
|
|
|
90
90
|
skipTyping?: boolean;
|
|
91
91
|
/** When true, replies are sent into the thread instead of main chat. */
|
|
92
92
|
replyInThread?: boolean;
|
|
93
|
+
/** Thread root id when the reply lives inside a thread; used for sentinel keying. */
|
|
94
|
+
threadId?: string;
|
|
93
95
|
toolUseDisplay: ToolUseDisplayConfig;
|
|
94
96
|
}
|
|
95
97
|
/**
|
|
@@ -33,7 +33,7 @@ const log = (0, lark_logger_1.larkLogger)('card/reply-dispatcher');
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
function createFeishuReplyDispatcher(params) {
|
|
35
35
|
const core = lark_client_1.LarkClient.runtime;
|
|
36
|
-
const { cfg, agentId, chatId, sessionKey, replyToMessageId, accountId, replyInThread } = params;
|
|
36
|
+
const { cfg, agentId, chatId, sessionKey, replyToMessageId, accountId, replyInThread, threadId } = params;
|
|
37
37
|
// Resolve account so we can read per-account config (e.g. replyMode)
|
|
38
38
|
const account = (0, accounts_1.getLarkAccount)(cfg, accountId);
|
|
39
39
|
const feishuCfg = account.config;
|
|
@@ -243,6 +243,7 @@ function createFeishuReplyDispatcher(params) {
|
|
|
243
243
|
replyToMessageId,
|
|
244
244
|
replyInThread,
|
|
245
245
|
accountId,
|
|
246
|
+
threadId,
|
|
246
247
|
});
|
|
247
248
|
}
|
|
248
249
|
catch (fallbackErr) {
|
|
@@ -277,6 +278,7 @@ function createFeishuReplyDispatcher(params) {
|
|
|
277
278
|
replyToMessageId,
|
|
278
279
|
replyInThread,
|
|
279
280
|
accountId,
|
|
281
|
+
threadId,
|
|
280
282
|
});
|
|
281
283
|
}
|
|
282
284
|
catch (fallbackErr) {
|
|
@@ -306,6 +308,7 @@ function createFeishuReplyDispatcher(params) {
|
|
|
306
308
|
replyToMessageId,
|
|
307
309
|
replyInThread,
|
|
308
310
|
accountId,
|
|
311
|
+
threadId,
|
|
309
312
|
});
|
|
310
313
|
}
|
|
311
314
|
catch (err) {
|
|
@@ -12,16 +12,18 @@ import type { HistoryEntry } from 'openclaw/plugin-sdk/reply-history';
|
|
|
12
12
|
import type { MessageContext } from '../types';
|
|
13
13
|
import type { LarkClient } from '../../core/lark-client';
|
|
14
14
|
import type { DispatchContext } from './dispatch-context';
|
|
15
|
+
import type { SentinelEntry } from './sentinel-store';
|
|
15
16
|
/**
|
|
16
17
|
* Build a `[System: ...]` mention annotation when the message @-mentions
|
|
17
|
-
* non-bot users
|
|
18
|
+
* non-self-bot users or when the previous reply had unresolved mentions.
|
|
19
|
+
* Returns `undefined` when there is nothing to report.
|
|
18
20
|
*
|
|
19
21
|
* Sender identity / chat metadata are handled by the SDK's own
|
|
20
22
|
* `buildInboundUserContextPrefix` (via SenderId, SenderName, ReplyToBody,
|
|
21
23
|
* InboundHistory, etc.), so we only inject the mention data that the SDK
|
|
22
24
|
* does not natively support.
|
|
23
25
|
*/
|
|
24
|
-
export declare function buildMentionAnnotation(ctx: MessageContext): string | undefined;
|
|
26
|
+
export declare function buildMentionAnnotation(ctx: MessageContext, sentinels?: SentinelEntry[]): string | undefined;
|
|
25
27
|
/**
|
|
26
28
|
* Pure function: build the annotated message body with optional quote,
|
|
27
29
|
* speaker prefix, and mention annotation (for the envelope Body).
|
|
@@ -31,7 +33,7 @@ export declare function buildMentionAnnotation(ctx: MessageContext): string | un
|
|
|
31
33
|
* the body cleaner and avoiding misleading heuristics for non-text
|
|
32
34
|
* message types (merge_forward, interactive cards, etc.).
|
|
33
35
|
*/
|
|
34
|
-
export declare function buildMessageBody(ctx: MessageContext, quotedContent?: string): string;
|
|
36
|
+
export declare function buildMessageBody(ctx: MessageContext, quotedContent?: string, sentinels?: SentinelEntry[]): string;
|
|
35
37
|
/**
|
|
36
38
|
* Build the BodyForAgent value: the clean message content plus an
|
|
37
39
|
* optional mention annotation.
|
|
@@ -51,7 +53,7 @@ export declare function buildMessageBody(ctx: MessageContext, quotedContent?: st
|
|
|
51
53
|
* The SDK's `detectAndLoadPromptImages` will discover image paths from
|
|
52
54
|
* the text and inject them as multimodal content blocks.
|
|
53
55
|
*/
|
|
54
|
-
export declare function buildBodyForAgent(ctx: MessageContext): string;
|
|
56
|
+
export declare function buildBodyForAgent(ctx: MessageContext, sentinels?: SentinelEntry[]): string;
|
|
55
57
|
/**
|
|
56
58
|
* Unified call to `finalizeInboundContext`, eliminating the duplicated
|
|
57
59
|
* field-mapping between permission notification and main message paths.
|
|
@@ -21,21 +21,48 @@ const mention_1 = require("./mention.js");
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Mention annotation
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
|
+
const MENTION_USAGE_HINT = 'To @mention in a reply, use `<at user_id="ou_xxx">Name</at>`; plain "@Name" won\'t notify.';
|
|
24
25
|
/**
|
|
25
26
|
* Build a `[System: ...]` mention annotation when the message @-mentions
|
|
26
|
-
* non-bot users
|
|
27
|
+
* non-self-bot users or when the previous reply had unresolved mentions.
|
|
28
|
+
* Returns `undefined` when there is nothing to report.
|
|
27
29
|
*
|
|
28
30
|
* Sender identity / chat metadata are handled by the SDK's own
|
|
29
31
|
* `buildInboundUserContextPrefix` (via SenderId, SenderName, ReplyToBody,
|
|
30
32
|
* InboundHistory, etc.), so we only inject the mention data that the SDK
|
|
31
33
|
* does not natively support.
|
|
32
34
|
*/
|
|
33
|
-
function buildMentionAnnotation(ctx) {
|
|
34
|
-
const
|
|
35
|
+
function buildMentionAnnotation(ctx, sentinels) {
|
|
36
|
+
const sections = [
|
|
37
|
+
formatMentionList((0, mention_1.nonBotMentions)(ctx)),
|
|
38
|
+
formatSentinelFeedback(sentinels),
|
|
39
|
+
].filter((s) => !!s);
|
|
40
|
+
if (sections.length === 0)
|
|
41
|
+
return undefined;
|
|
42
|
+
sections.push(MENTION_USAGE_HINT);
|
|
43
|
+
return `[System: ${sections.join(' ')}]`;
|
|
44
|
+
}
|
|
45
|
+
function formatMentionList(mentions) {
|
|
35
46
|
if (mentions.length === 0)
|
|
36
47
|
return undefined;
|
|
37
|
-
const
|
|
38
|
-
return `
|
|
48
|
+
const details = mentions.map((t) => `${t.name} (open_id: ${t.openId})`).join(', ');
|
|
49
|
+
return (`This message @mentions the following users: ${details}. ` +
|
|
50
|
+
`Use these open_ids when performing actions involving these users.`);
|
|
51
|
+
}
|
|
52
|
+
function formatSentinelFeedback(sentinels) {
|
|
53
|
+
if (!sentinels || sentinels.length === 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
const lines = sentinels.map((s) => {
|
|
56
|
+
if (s.reason === 'not_found') {
|
|
57
|
+
return `"@${s.name}" was not recognized in the chat`;
|
|
58
|
+
}
|
|
59
|
+
if (s.reason === 'ambiguous' && s.candidates && s.candidates.length > 0) {
|
|
60
|
+
const ids = s.candidates.map((c) => c.openId).join(' / ');
|
|
61
|
+
return `"@${s.name}" matched multiple users (${ids}); use explicit <at user_id="...">`;
|
|
62
|
+
}
|
|
63
|
+
return `"@${s.name}" failed to resolve`;
|
|
64
|
+
});
|
|
65
|
+
return `Previous reply had unresolved mentions: ${lines.join('; ')}.`;
|
|
39
66
|
}
|
|
40
67
|
// ---------------------------------------------------------------------------
|
|
41
68
|
// Message body builders
|
|
@@ -49,14 +76,14 @@ function buildMentionAnnotation(ctx) {
|
|
|
49
76
|
* the body cleaner and avoiding misleading heuristics for non-text
|
|
50
77
|
* message types (merge_forward, interactive cards, etc.).
|
|
51
78
|
*/
|
|
52
|
-
function buildMessageBody(ctx, quotedContent) {
|
|
79
|
+
function buildMessageBody(ctx, quotedContent, sentinels) {
|
|
53
80
|
let messageBody = ctx.content;
|
|
54
81
|
if (quotedContent) {
|
|
55
82
|
messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
|
|
56
83
|
}
|
|
57
84
|
const speaker = ctx.senderName ?? ctx.senderId;
|
|
58
85
|
messageBody = `${speaker}: ${messageBody}`;
|
|
59
|
-
const mentionAnnotation = buildMentionAnnotation(ctx);
|
|
86
|
+
const mentionAnnotation = buildMentionAnnotation(ctx, sentinels);
|
|
60
87
|
if (mentionAnnotation) {
|
|
61
88
|
messageBody += `\n\n${mentionAnnotation}`;
|
|
62
89
|
}
|
|
@@ -81,8 +108,8 @@ function buildMessageBody(ctx, quotedContent) {
|
|
|
81
108
|
* The SDK's `detectAndLoadPromptImages` will discover image paths from
|
|
82
109
|
* the text and inject them as multimodal content blocks.
|
|
83
110
|
*/
|
|
84
|
-
function buildBodyForAgent(ctx) {
|
|
85
|
-
const mentionAnnotation = buildMentionAnnotation(ctx);
|
|
111
|
+
function buildBodyForAgent(ctx, sentinels) {
|
|
112
|
+
const mentionAnnotation = buildMentionAnnotation(ctx, sentinels);
|
|
86
113
|
if (mentionAnnotation) {
|
|
87
114
|
return `${ctx.content}\n\n${mentionAnnotation}`;
|
|
88
115
|
}
|
|
@@ -35,6 +35,7 @@ const index_1 = require("../../commands/index.js");
|
|
|
35
35
|
const send_1 = require("../outbound/send.js");
|
|
36
36
|
const dispatch_commands_1 = require("./dispatch-commands.js");
|
|
37
37
|
const dispatch_builders_1 = require("./dispatch-builders.js");
|
|
38
|
+
const sentinel_store_1 = require("./sentinel-store.js");
|
|
38
39
|
const dispatch_context_1 = require("./dispatch-context.js");
|
|
39
40
|
const mention_1 = require("./mention.js");
|
|
40
41
|
const gate_1 = require("./gate.js");
|
|
@@ -209,6 +210,7 @@ async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey,
|
|
|
209
210
|
chatType: dc.ctx.chatType,
|
|
210
211
|
skipTyping,
|
|
211
212
|
replyInThread: dc.isThread,
|
|
213
|
+
threadId: dc.isThread ? dc.ctx.threadId : undefined,
|
|
212
214
|
toolUseDisplay,
|
|
213
215
|
});
|
|
214
216
|
// Create an AbortController so the abort fast-path can cancel the
|
|
@@ -288,9 +290,14 @@ async function dispatchToAgent(params) {
|
|
|
288
290
|
baseSessionKey: dc.route.sessionKey,
|
|
289
291
|
});
|
|
290
292
|
}
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
//
|
|
293
|
+
// Consume any pending mention sentinels for this thread. Take and
|
|
294
|
+
// delete is one shot per inbound — capture once, hand to both body
|
|
295
|
+
// builders below.
|
|
296
|
+
const sentinelKey = (0, chat_queue_1.threadScopedKey)(dc.ctx.chatId, dc.isThread ? dc.ctx.threadId : undefined);
|
|
297
|
+
const sentinels = (0, sentinel_store_1.getSentinelStore)(dc.account.accountId).consumeSentinels(sentinelKey);
|
|
298
|
+
// 3. Build annotated message body
|
|
299
|
+
const messageBody = (0, dispatch_builders_1.buildMessageBody)(params.ctx, params.quotedContent, sentinels);
|
|
300
|
+
// 4. Permission-error notification (optional side-effect).
|
|
294
301
|
// Isolated so a failure here does not block the main message dispatch.
|
|
295
302
|
// Skipped for comment targets: the streaming card dispatcher inside
|
|
296
303
|
// dispatchPermissionNotification sends via IM APIs which don't
|
|
@@ -303,13 +310,13 @@ async function dispatchToAgent(params) {
|
|
|
303
310
|
dc.error(`feishu[${dc.account.accountId}]: permission notification failed, continuing: ${String(err)}`);
|
|
304
311
|
}
|
|
305
312
|
}
|
|
306
|
-
//
|
|
313
|
+
// 5. Build main envelope (with group chat history)
|
|
307
314
|
const { combinedBody, historyKey } = (0, dispatch_builders_1.buildEnvelopeWithHistory)(dc, messageBody, params.chatHistories, params.historyLimit);
|
|
308
|
-
//
|
|
315
|
+
// 6. Build BodyForAgent with mention annotation (if any).
|
|
309
316
|
// SDK >= 2026.2.10 no longer falls back to Body for BodyForAgent,
|
|
310
317
|
// so we must set it explicitly to preserve the annotation.
|
|
311
|
-
const bodyForAgent = (0, dispatch_builders_1.buildBodyForAgent)(params.ctx);
|
|
312
|
-
//
|
|
318
|
+
const bodyForAgent = (0, dispatch_builders_1.buildBodyForAgent)(params.ctx, sentinels);
|
|
319
|
+
// 7. Build InboundHistory for SDK metadata injection (>= 2026.2.10).
|
|
313
320
|
// The SDK's buildInboundUserContextPrefix renders these as structured
|
|
314
321
|
// JSON blocks; earlier SDK versions simply ignore unknown fields.
|
|
315
322
|
const threadHistoryKey = (0, chat_queue_1.threadScopedKey)(dc.ctx.chatId, dc.isThread ? dc.ctx.threadId : undefined);
|
|
@@ -320,7 +327,7 @@ async function dispatchToAgent(params) {
|
|
|
320
327
|
timestamp: entry.timestamp ?? Date.now(),
|
|
321
328
|
}))
|
|
322
329
|
: undefined;
|
|
323
|
-
//
|
|
330
|
+
// 8. Build inbound context payload
|
|
324
331
|
const isBareNewOrReset = /^\/(?:new|reset)\s*$/i.test((params.ctx.content ?? '').trim());
|
|
325
332
|
const groupSystemPrompt = dc.isGroup
|
|
326
333
|
? params.groupConfig?.systemPrompt?.trim() || params.defaultGroupConfig?.systemPrompt?.trim() || undefined
|
|
@@ -357,7 +364,7 @@ async function dispatchToAgent(params) {
|
|
|
357
364
|
...(dc.ctx.threadId ? { MessageThreadId: dc.ctx.threadId } : {}),
|
|
358
365
|
},
|
|
359
366
|
});
|
|
360
|
-
//
|
|
367
|
+
// 9a. Intercept /feishu commands for i18n multi-locale card dispatch
|
|
361
368
|
// Must run BEFORE the SDK command check — the SDK does not recognise
|
|
362
369
|
// plugin-registered commands via isControlCommandMessage, so
|
|
363
370
|
// /feishu_* falls through to the AI agent otherwise.
|
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Inbound message handling pipeline for the Lark/Feishu channel plugin.
|
|
6
6
|
*
|
|
7
|
-
* Orchestrates a
|
|
7
|
+
* Orchestrates a nine-stage pipeline:
|
|
8
8
|
* 1. Account resolution
|
|
9
9
|
* 2. Event parsing → parse.ts (merge_forward expanded in-place)
|
|
10
|
-
* 3.
|
|
11
|
-
* 4.
|
|
12
|
-
* 5.
|
|
13
|
-
* 6.
|
|
14
|
-
* 7.
|
|
10
|
+
* 3. Empty-message guard → early return for text-less, media-less messages
|
|
11
|
+
* 4. Sender enrichment → enrich.ts (lightweight, before gate)
|
|
12
|
+
* 5. Policy gate → gate.ts
|
|
13
|
+
* 6. User name prefetch → enrich.ts (batch cache warm-up)
|
|
14
|
+
* 7. Content resolution → enrich.ts (media / quote, parallel)
|
|
15
|
+
* 8. Command authorization → plugin-sdk/command-auth
|
|
16
|
+
* 9. Agent dispatch → dispatch.ts
|
|
15
17
|
*/
|
|
16
18
|
import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk';
|
|
17
19
|
import type { HistoryEntry } from 'openclaw/plugin-sdk/reply-history';
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Inbound message handling pipeline for the Lark/Feishu channel plugin.
|
|
7
7
|
*
|
|
8
|
-
* Orchestrates a
|
|
8
|
+
* Orchestrates a nine-stage pipeline:
|
|
9
9
|
* 1. Account resolution
|
|
10
10
|
* 2. Event parsing → parse.ts (merge_forward expanded in-place)
|
|
11
|
-
* 3.
|
|
12
|
-
* 4.
|
|
13
|
-
* 5.
|
|
14
|
-
* 6.
|
|
15
|
-
* 7.
|
|
11
|
+
* 3. Empty-message guard → early return for text-less, media-less messages
|
|
12
|
+
* 4. Sender enrichment → enrich.ts (lightweight, before gate)
|
|
13
|
+
* 5. Policy gate → gate.ts
|
|
14
|
+
* 6. User name prefetch → enrich.ts (batch cache warm-up)
|
|
15
|
+
* 7. Content resolution → enrich.ts (media / quote, parallel)
|
|
16
|
+
* 8. Command authorization → plugin-sdk/command-auth
|
|
17
|
+
* 9. Agent dispatch → dispatch.ts
|
|
16
18
|
*/
|
|
17
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
20
|
exports.handleFeishuMessage = handleFeishuMessage;
|
|
@@ -60,7 +62,16 @@ async function handleFeishuMessage(params) {
|
|
|
60
62
|
cfg: accountScopedCfg,
|
|
61
63
|
accountId: account.accountId,
|
|
62
64
|
});
|
|
63
|
-
// 3.
|
|
65
|
+
// 3. Early reject: skip empty-text messages with no media resources.
|
|
66
|
+
// OpenClaw 2026.4.29 adds a core-side guard for this (##74634), but
|
|
67
|
+
// rejecting here avoids wasting cycles on enrichment, gate, and
|
|
68
|
+
// dispatch for messages that would be silently dropped at the deliver
|
|
69
|
+
// callback anyway.
|
|
70
|
+
if (!ctx.content.trim() && ctx.resources.length === 0) {
|
|
71
|
+
log(`feishu[${account.accountId}]: empty message ${ctx.messageId} (no text, no media), skipping`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// 4. Enrich (lightweight): sender name + permission error tracking
|
|
64
75
|
const { ctx: enrichedCtx, permissionError } = await (0, enrich_1.resolveSenderInfo)({
|
|
65
76
|
ctx,
|
|
66
77
|
account,
|
|
@@ -70,7 +81,7 @@ async function handleFeishuMessage(params) {
|
|
|
70
81
|
log(`feishu[${account.accountId}]: received message from ${ctx.senderId} in ${ctx.chatId} (${ctx.chatType})`);
|
|
71
82
|
logger.info(`received from ${ctx.senderId} in ${ctx.chatId} (${ctx.chatType})`);
|
|
72
83
|
const historyLimit = Math.max(0, accountFeishuCfg?.historyLimit ?? accountScopedCfg.messages?.groupChat?.historyLimit ?? reply_history_1.DEFAULT_GROUP_HISTORY_LIMIT);
|
|
73
|
-
//
|
|
84
|
+
// 5. Gate: policy / access-control checks (skipped for synthetic messages)
|
|
74
85
|
const gate = forceMention
|
|
75
86
|
? { allowed: true }
|
|
76
87
|
: await (0, gate_1.checkMessageGate)({ ctx, accountFeishuCfg, account, accountScopedCfg, log });
|
|
@@ -90,15 +101,15 @@ async function handleFeishuMessage(params) {
|
|
|
90
101
|
}
|
|
91
102
|
return;
|
|
92
103
|
}
|
|
93
|
-
//
|
|
104
|
+
// 6. Batch pre-warm user name cache (sender + mentions)
|
|
94
105
|
await (0, enrich_1.prefetchUserNames)({ ctx, account, log });
|
|
95
|
-
//
|
|
106
|
+
// 7. Enrich (heavyweight, after gate — parallel where possible)
|
|
96
107
|
const enrichParams = { ctx, accountScopedCfg, account, log };
|
|
97
108
|
const [mediaResult, quotedContent] = await Promise.all([
|
|
98
109
|
(0, enrich_1.resolveMedia)(enrichParams),
|
|
99
110
|
(0, enrich_1.resolveQuotedContent)(enrichParams),
|
|
100
111
|
]);
|
|
101
|
-
//
|
|
112
|
+
// 7b. Replace Feishu file-key placeholders in content with local
|
|
102
113
|
// file paths so the SDK can detect images for native vision and
|
|
103
114
|
// the AI receives meaningful file references.
|
|
104
115
|
if (mediaResult.mediaList.length > 0) {
|
|
@@ -107,7 +118,7 @@ async function handleFeishuMessage(params) {
|
|
|
107
118
|
content: (0, enrich_1.substituteMediaPaths)(ctx.content, mediaResult.mediaList),
|
|
108
119
|
};
|
|
109
120
|
}
|
|
110
|
-
//
|
|
121
|
+
// 8. Compute commandAuthorized via SDK access group command gating
|
|
111
122
|
const core = lark_client_1.LarkClient.runtime;
|
|
112
123
|
const isGroup = ctx.chatType === 'group';
|
|
113
124
|
const dmPolicy = accountFeishuCfg?.dmPolicy ?? 'pairing';
|
|
@@ -152,7 +163,7 @@ async function handleFeishuMessage(params) {
|
|
|
152
163
|
shouldComputeCommandAuthorized: core.channel.commands.shouldComputeCommandAuthorized,
|
|
153
164
|
resolveCommandAuthorizedFromAuthorizers: core.channel.commands.resolveCommandAuthorizedFromAuthorizers,
|
|
154
165
|
});
|
|
155
|
-
//
|
|
166
|
+
// 9. Dispatch to agent
|
|
156
167
|
// groupConfig and defaultGroupConfig are already resolved above.
|
|
157
168
|
try {
|
|
158
169
|
await (0, dispatch_1.dispatchToAgent)({
|
|
@@ -20,9 +20,9 @@ export type { MentionInfo } from '../types';
|
|
|
20
20
|
export declare function isMentionAll(mention: {
|
|
21
21
|
key: string;
|
|
22
22
|
}): boolean;
|
|
23
|
-
/** Whether the bot was @-mentioned. */
|
|
23
|
+
/** Whether the receiving bot itself was @-mentioned. */
|
|
24
24
|
export declare function mentionedBot(ctx: MessageContext): boolean;
|
|
25
|
-
/** All
|
|
25
|
+
/** All mentions excluding the receiving bot itself. */
|
|
26
26
|
export declare function nonBotMentions(ctx: MessageContext): MentionInfo[];
|
|
27
27
|
/**
|
|
28
28
|
* Remove all @mention placeholder keys from the message text.
|
|
@@ -34,11 +34,11 @@ const utils_1 = require("../converters/utils.js");
|
|
|
34
34
|
function isMentionAll(mention) {
|
|
35
35
|
return mention.key === '@_all';
|
|
36
36
|
}
|
|
37
|
-
/** Whether the bot was @-mentioned. */
|
|
37
|
+
/** Whether the receiving bot itself was @-mentioned. */
|
|
38
38
|
function mentionedBot(ctx) {
|
|
39
39
|
return ctx.mentions.some((m) => m.isBot);
|
|
40
40
|
}
|
|
41
|
-
/** All
|
|
41
|
+
/** All mentions excluding the receiving bot itself. */
|
|
42
42
|
function nonBotMentions(ctx) {
|
|
43
43
|
return ctx.mentions.filter((m) => !m.isBot);
|
|
44
44
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Per-thread store for unresolved mention feedback. The outbound
|
|
6
|
+
* normalizer records a SentinelEntry whenever an `@Name` cannot be
|
|
7
|
+
* resolved; the next inbound message on the same thread consumes
|
|
8
|
+
* (take and delete) the entries, which buildMentionAnnotation surfaces
|
|
9
|
+
* as a system note so the next reply can disambiguate.
|
|
10
|
+
*
|
|
11
|
+
* Kept separate from UserNameCache because the lifecycle differs:
|
|
12
|
+
* 10-minute TTL, per-thread keying, and take-and-delete consumption.
|
|
13
|
+
*/
|
|
14
|
+
export interface SentinelEntry {
|
|
15
|
+
/** Literal name as it appeared in the outbound text. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Why parsing failed. */
|
|
18
|
+
reason: 'not_found' | 'ambiguous';
|
|
19
|
+
/** Candidate open_ids when reason === 'ambiguous'. */
|
|
20
|
+
candidates?: Array<{
|
|
21
|
+
openId: string;
|
|
22
|
+
kind?: 'user' | 'bot';
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare class SentinelStore {
|
|
26
|
+
private byThread;
|
|
27
|
+
private maxThreads;
|
|
28
|
+
private ttlMs;
|
|
29
|
+
constructor(maxThreads?: number, ttlMs?: number);
|
|
30
|
+
recordSentinels(threadKey: string, sentinels: SentinelEntry[]): void;
|
|
31
|
+
consumeSentinels(threadKey: string): SentinelEntry[];
|
|
32
|
+
clear(): void;
|
|
33
|
+
private evict;
|
|
34
|
+
}
|
|
35
|
+
export declare function getSentinelStore(accountId: string, maxThreads?: number, ttlMs?: number): SentinelStore;
|
|
36
|
+
export declare function clearSentinelStore(accountId?: string): void;
|
|
37
|
+
export declare function clearAllSentinelStores(): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Per-thread store for unresolved mention feedback. The outbound
|
|
7
|
+
* normalizer records a SentinelEntry whenever an `@Name` cannot be
|
|
8
|
+
* resolved; the next inbound message on the same thread consumes
|
|
9
|
+
* (take and delete) the entries, which buildMentionAnnotation surfaces
|
|
10
|
+
* as a system note so the next reply can disambiguate.
|
|
11
|
+
*
|
|
12
|
+
* Kept separate from UserNameCache because the lifecycle differs:
|
|
13
|
+
* 10-minute TTL, per-thread keying, and take-and-delete consumption.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.SentinelStore = void 0;
|
|
17
|
+
exports.getSentinelStore = getSentinelStore;
|
|
18
|
+
exports.clearSentinelStore = clearSentinelStore;
|
|
19
|
+
exports.clearAllSentinelStores = clearAllSentinelStores;
|
|
20
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 min — short, avoid stale feedback
|
|
21
|
+
const DEFAULT_MAX_THREADS = 200; // per-account
|
|
22
|
+
function dedup(entries) {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const e of entries) {
|
|
26
|
+
const key = `${e.reason}:${e.name}`;
|
|
27
|
+
if (seen.has(key))
|
|
28
|
+
continue;
|
|
29
|
+
seen.add(key);
|
|
30
|
+
out.push(e);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
class SentinelStore {
|
|
35
|
+
byThread = new Map();
|
|
36
|
+
maxThreads;
|
|
37
|
+
ttlMs;
|
|
38
|
+
constructor(maxThreads = DEFAULT_MAX_THREADS, ttlMs = DEFAULT_TTL_MS) {
|
|
39
|
+
this.maxThreads = maxThreads;
|
|
40
|
+
this.ttlMs = ttlMs;
|
|
41
|
+
}
|
|
42
|
+
recordSentinels(threadKey, sentinels) {
|
|
43
|
+
if (sentinels.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
const existing = this.byThread.get(threadKey);
|
|
46
|
+
const merged = existing ? [...existing.entries, ...sentinels] : sentinels;
|
|
47
|
+
this.byThread.delete(threadKey); // bump LRU
|
|
48
|
+
this.byThread.set(threadKey, {
|
|
49
|
+
entries: dedup(merged),
|
|
50
|
+
expireAt: Date.now() + this.ttlMs,
|
|
51
|
+
});
|
|
52
|
+
this.evict();
|
|
53
|
+
}
|
|
54
|
+
consumeSentinels(threadKey) {
|
|
55
|
+
const stored = this.byThread.get(threadKey);
|
|
56
|
+
if (!stored)
|
|
57
|
+
return [];
|
|
58
|
+
this.byThread.delete(threadKey);
|
|
59
|
+
if (stored.expireAt <= Date.now())
|
|
60
|
+
return [];
|
|
61
|
+
return stored.entries;
|
|
62
|
+
}
|
|
63
|
+
clear() {
|
|
64
|
+
this.byThread.clear();
|
|
65
|
+
}
|
|
66
|
+
evict() {
|
|
67
|
+
while (this.byThread.size > this.maxThreads) {
|
|
68
|
+
const oldest = this.byThread.keys().next().value;
|
|
69
|
+
if (oldest === undefined)
|
|
70
|
+
break;
|
|
71
|
+
this.byThread.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.SentinelStore = SentinelStore;
|
|
76
|
+
const registry = new Map();
|
|
77
|
+
function getSentinelStore(accountId, maxThreads, ttlMs) {
|
|
78
|
+
let store = registry.get(accountId);
|
|
79
|
+
if (!store) {
|
|
80
|
+
store = new SentinelStore(maxThreads, ttlMs);
|
|
81
|
+
registry.set(accountId, store);
|
|
82
|
+
}
|
|
83
|
+
return store;
|
|
84
|
+
}
|
|
85
|
+
function clearSentinelStore(accountId) {
|
|
86
|
+
if (accountId !== undefined) {
|
|
87
|
+
registry.get(accountId)?.clear();
|
|
88
|
+
registry.delete(accountId);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
clearAllSentinelStores();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function clearAllSentinelStores() {
|
|
95
|
+
for (const s of registry.values())
|
|
96
|
+
s.clear();
|
|
97
|
+
registry.clear();
|
|
98
|
+
}
|
|
@@ -3,19 +3,59 @@
|
|
|
3
3
|
* SPDX-License-Identifier: MIT
|
|
4
4
|
*
|
|
5
5
|
* Account-scoped cache registry for Feishu user display names.
|
|
6
|
+
*
|
|
7
|
+
* Stores forward (openId → name + kind) and reverse (normalizedName → Set<openId>)
|
|
8
|
+
* indexes for mention resolution. Per-account, LRU + TTL.
|
|
6
9
|
*/
|
|
10
|
+
export type PrincipalKind = 'user' | 'bot';
|
|
11
|
+
export interface MentionMatch {
|
|
12
|
+
openId: string;
|
|
13
|
+
name: string;
|
|
14
|
+
kind?: PrincipalKind;
|
|
15
|
+
}
|
|
16
|
+
export interface ChatMember {
|
|
17
|
+
openId: string;
|
|
18
|
+
name: string;
|
|
19
|
+
kind: PrincipalKind;
|
|
20
|
+
}
|
|
21
|
+
export interface ChatMembersEntry {
|
|
22
|
+
members: ChatMember[];
|
|
23
|
+
expireAt: number;
|
|
24
|
+
}
|
|
7
25
|
export declare class UserNameCache {
|
|
8
|
-
private
|
|
26
|
+
private nameByOpenId;
|
|
27
|
+
private openIdsByName;
|
|
9
28
|
private maxSize;
|
|
10
29
|
private ttlMs;
|
|
11
|
-
|
|
30
|
+
private chatBots;
|
|
31
|
+
private chatMembers;
|
|
32
|
+
private inFlight;
|
|
33
|
+
private maxChats;
|
|
34
|
+
constructor(maxSize?: number, ttlMs?: number, maxChats?: number);
|
|
12
35
|
has(openId: string): boolean;
|
|
13
36
|
get(openId: string): string | undefined;
|
|
14
37
|
set(openId: string, name: string): void;
|
|
38
|
+
setWithKind(openId: string, name: string, kind: PrincipalKind): void;
|
|
39
|
+
lookupByName(name: string): MentionMatch[];
|
|
15
40
|
setMany(entries: Iterable<[string, string]>): void;
|
|
16
41
|
filterMissing(openIds: string[]): string[];
|
|
17
42
|
getMany(openIds: string[]): Map<string, string>;
|
|
43
|
+
recordChatBots(chatId: string, members: Array<{
|
|
44
|
+
openId: string;
|
|
45
|
+
name: string;
|
|
46
|
+
}>): void;
|
|
47
|
+
recordChatMembers(chatId: string, members: ChatMember[]): void;
|
|
48
|
+
getChatBots(chatId: string): ChatMembersEntry | null;
|
|
49
|
+
getChatMembers(chatId: string): ChatMembersEntry | null;
|
|
50
|
+
getInflight(key: string): Promise<void> | undefined;
|
|
51
|
+
setInflight(key: string, promise: Promise<void>): void;
|
|
52
|
+
clearInflight(key: string): void;
|
|
18
53
|
clear(): void;
|
|
54
|
+
private writeEntry;
|
|
55
|
+
private writeEntryNoEvict;
|
|
56
|
+
private deleteOpenId;
|
|
57
|
+
private removeFromReverse;
|
|
58
|
+
private evictChats;
|
|
19
59
|
private evict;
|
|
20
60
|
}
|
|
21
61
|
export declare function getUserNameCache(accountId: string): UserNameCache;
|