@larksuite/openclaw-lark 2026.4.10 → 2026.5.7-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/openclaw.plugin.json +50 -3
- package/package.json +1 -1
- package/src/channel/event-handlers.js +15 -0
- package/src/channel/monitor.js +1 -1
- package/src/core/config-schema.d.ts +7 -0
- package/src/core/config-schema.js +3 -0
- package/src/core/tool-scopes.d.ts +1 -1
- package/src/core/tool-scopes.js +1 -1
- package/src/messaging/inbound/dispatch-builders.js +1 -1
- package/src/messaging/inbound/enrich.js +13 -10
- package/src/messaging/inbound/gate.d.ts +16 -1
- package/src/messaging/inbound/gate.js +121 -30
- package/src/messaging/inbound/parse.js +4 -0
- package/src/messaging/inbound/user-name-cache.d.ts +12 -0
- package/src/messaging/inbound/user-name-cache.js +46 -0
- package/src/messaging/types.d.ts +8 -0
- package/src/tools/oapi/task/task.js +1 -1
package/openclaw.plugin.json
CHANGED
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-lark",
|
|
3
|
-
"channels": [
|
|
4
|
-
|
|
3
|
+
"channels": [
|
|
4
|
+
"feishu"
|
|
5
|
+
],
|
|
6
|
+
"skills": [
|
|
7
|
+
"./skills"
|
|
8
|
+
],
|
|
5
9
|
"configSchema": {
|
|
6
10
|
"type": "object",
|
|
7
11
|
"additionalProperties": false,
|
|
8
12
|
"properties": {}
|
|
9
13
|
},
|
|
14
|
+
"contracts": {
|
|
15
|
+
"tools": [
|
|
16
|
+
"feishu_bitable_app",
|
|
17
|
+
"feishu_bitable_app_table",
|
|
18
|
+
"feishu_bitable_app_table_field",
|
|
19
|
+
"feishu_bitable_app_table_record",
|
|
20
|
+
"feishu_bitable_app_table_view",
|
|
21
|
+
"feishu_calendar_calendar",
|
|
22
|
+
"feishu_calendar_event",
|
|
23
|
+
"feishu_calendar_event_attendee",
|
|
24
|
+
"feishu_calendar_freebusy",
|
|
25
|
+
"feishu_chat",
|
|
26
|
+
"feishu_chat_members",
|
|
27
|
+
"feishu_create_doc",
|
|
28
|
+
"feishu_doc_comments",
|
|
29
|
+
"feishu_doc_media",
|
|
30
|
+
"feishu_drive_file",
|
|
31
|
+
"feishu_fetch_doc",
|
|
32
|
+
"feishu_get_user",
|
|
33
|
+
"feishu_im_bot_image",
|
|
34
|
+
"feishu_im_user_fetch_resource",
|
|
35
|
+
"feishu_im_user_get_messages",
|
|
36
|
+
"feishu_im_user_get_thread_messages",
|
|
37
|
+
"feishu_im_user_message",
|
|
38
|
+
"feishu_im_user_search_messages",
|
|
39
|
+
"feishu_oauth",
|
|
40
|
+
"feishu_oauth_batch_auth",
|
|
41
|
+
"feishu_search_doc_wiki",
|
|
42
|
+
"feishu_search_user",
|
|
43
|
+
"feishu_sheet",
|
|
44
|
+
"feishu_task_comment",
|
|
45
|
+
"feishu_task_subtask",
|
|
46
|
+
"feishu_task_task",
|
|
47
|
+
"feishu_task_agent",
|
|
48
|
+
"feishu_task_attachment",
|
|
49
|
+
"feishu_task_tasklist",
|
|
50
|
+
"feishu_update_doc",
|
|
51
|
+
"feishu_wiki_space",
|
|
52
|
+
"feishu_wiki_space_node",
|
|
53
|
+
"feishu_task_section",
|
|
54
|
+
"feishu_ask_user_question"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
10
57
|
"channelConfigs": {
|
|
11
58
|
"feishu": {
|
|
12
59
|
"schema": {
|
|
@@ -14,4 +61,4 @@
|
|
|
14
61
|
}
|
|
15
62
|
}
|
|
16
63
|
}
|
|
17
|
-
}
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -69,6 +69,21 @@ async function handleMessageEvent(ctx, data) {
|
|
|
69
69
|
const { accountId, log, error } = ctx;
|
|
70
70
|
try {
|
|
71
71
|
const event = data;
|
|
72
|
+
// Self-echo hard filter — drop messages authored by this very bot before
|
|
73
|
+
// dedup and enqueue. Prevents self-reply loops; the primary guardrail
|
|
74
|
+
// against bot-to-bot ping-pong.
|
|
75
|
+
//
|
|
76
|
+
// NOTE: if botOpenId is not yet populated (startup race before probe
|
|
77
|
+
// resolves), this filter is skipped. The downstream bot-sender gate
|
|
78
|
+
// (checkBotSenderGate) acts as fallback — bot messages default to
|
|
79
|
+
// `allowBots='mentions'`, so in groups they require an explicit @-mention
|
|
80
|
+
// of this bot to pass; DMs are pass-through under the default.
|
|
81
|
+
const senderOpenId = event.sender?.sender_id?.open_id;
|
|
82
|
+
const botOpenId = ctx.lark.botOpenId;
|
|
83
|
+
if (botOpenId && senderOpenId && senderOpenId === botOpenId) {
|
|
84
|
+
log(`feishu[${accountId}]: drop self-echo message ${event.message?.message_id ?? 'unknown'}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
72
87
|
const msgId = event.message?.message_id ?? 'unknown';
|
|
73
88
|
const chatId = event.message?.chat_id ?? '';
|
|
74
89
|
// In topic groups, reply events carry root_id but not thread_id.
|
package/src/channel/monitor.js
CHANGED
|
@@ -13,9 +13,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
exports.monitorFeishuProvider = monitorFeishuProvider;
|
|
14
14
|
const accounts_1 = require("../core/accounts.js");
|
|
15
15
|
const lark_client_1 = require("../core/lark-client.js");
|
|
16
|
-
const dedup_1 = require("../messaging/inbound/dedup.js");
|
|
17
16
|
const lark_logger_1 = require("../core/lark-logger.js");
|
|
18
17
|
const shutdown_hooks_1 = require("../core/shutdown-hooks.js");
|
|
18
|
+
const dedup_1 = require("../messaging/inbound/dedup.js");
|
|
19
19
|
const event_handlers_1 = require("./event-handlers.js");
|
|
20
20
|
const mlog = (0, lark_logger_1.larkLogger)('channel/monitor');
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
@@ -30,6 +30,7 @@ export declare const FeishuGroupSchema: z.ZodObject<{
|
|
|
30
30
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
31
31
|
allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
|
|
32
32
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
33
|
+
allowBots: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"mentions">]>>;
|
|
33
34
|
}, z.core.$strip>;
|
|
34
35
|
export declare const FeishuAccountConfigSchema: z.ZodObject<{
|
|
35
36
|
appId: z.ZodOptional<z.ZodString>;
|
|
@@ -76,6 +77,7 @@ export declare const FeishuAccountConfigSchema: z.ZodObject<{
|
|
|
76
77
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
77
78
|
allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
|
|
78
79
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
80
|
+
allowBots: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"mentions">]>>;
|
|
79
81
|
}, z.core.$strip>>>;
|
|
80
82
|
historyLimit: z.ZodOptional<z.ZodNumber>;
|
|
81
83
|
dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
|
|
@@ -170,6 +172,7 @@ export declare const FeishuAccountConfigSchema: z.ZodObject<{
|
|
|
170
172
|
all: "all";
|
|
171
173
|
}>>;
|
|
172
174
|
threadSession: z.ZodOptional<z.ZodBoolean>;
|
|
175
|
+
allowBots: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"mentions">]>>;
|
|
173
176
|
uat: z.ZodOptional<z.ZodObject<{
|
|
174
177
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
175
178
|
allowedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -221,6 +224,7 @@ export declare const FeishuConfigSchema: z.ZodObject<{
|
|
|
221
224
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
222
225
|
allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
|
|
223
226
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
227
|
+
allowBots: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"mentions">]>>;
|
|
224
228
|
}, z.core.$strip>>>;
|
|
225
229
|
historyLimit: z.ZodOptional<z.ZodNumber>;
|
|
226
230
|
dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
|
|
@@ -315,6 +319,7 @@ export declare const FeishuConfigSchema: z.ZodObject<{
|
|
|
315
319
|
all: "all";
|
|
316
320
|
}>>;
|
|
317
321
|
threadSession: z.ZodOptional<z.ZodBoolean>;
|
|
322
|
+
allowBots: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"mentions">]>>;
|
|
318
323
|
uat: z.ZodOptional<z.ZodObject<{
|
|
319
324
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
320
325
|
allowedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -365,6 +370,7 @@ export declare const FeishuConfigSchema: z.ZodObject<{
|
|
|
365
370
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
366
371
|
allowFrom: z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>, z.ZodTransform<string[] | undefined, string | string[] | undefined>>;
|
|
367
372
|
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
373
|
+
allowBots: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"mentions">]>>;
|
|
368
374
|
}, z.core.$strip>>>;
|
|
369
375
|
historyLimit: z.ZodOptional<z.ZodNumber>;
|
|
370
376
|
dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
|
|
@@ -459,6 +465,7 @@ export declare const FeishuConfigSchema: z.ZodObject<{
|
|
|
459
465
|
all: "all";
|
|
460
466
|
}>>;
|
|
461
467
|
threadSession: z.ZodOptional<z.ZodBoolean>;
|
|
468
|
+
allowBots: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"mentions">]>>;
|
|
462
469
|
uat: z.ZodOptional<z.ZodObject<{
|
|
463
470
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
464
471
|
allowedScopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -105,6 +105,7 @@ const DedupSchema = zod_1.z
|
|
|
105
105
|
maxEntries: zod_1.z.number().optional(), // default 5000
|
|
106
106
|
})
|
|
107
107
|
.optional();
|
|
108
|
+
const AllowBotsSchema = zod_1.z.union([zod_1.z.boolean(), zod_1.z.literal('mentions')]).optional();
|
|
108
109
|
const ReactionNotificationModeSchema = zod_1.z.enum(['off', 'own', 'all']).optional();
|
|
109
110
|
exports.UATConfigSchema = zod_1.z
|
|
110
111
|
.object({
|
|
@@ -130,6 +131,7 @@ exports.FeishuGroupSchema = zod_1.z.object({
|
|
|
130
131
|
enabled: zod_1.z.boolean().optional(),
|
|
131
132
|
allowFrom: AllowFromSchema,
|
|
132
133
|
systemPrompt: zod_1.z.string().optional(),
|
|
134
|
+
allowBots: AllowBotsSchema,
|
|
133
135
|
});
|
|
134
136
|
// ---------------------------------------------------------------------------
|
|
135
137
|
// Account config schema (same shape as top-level minus `accounts`)
|
|
@@ -176,6 +178,7 @@ exports.FeishuAccountConfigSchema = zod_1.z.object({
|
|
|
176
178
|
dedup: DedupSchema,
|
|
177
179
|
reactionNotifications: ReactionNotificationModeSchema,
|
|
178
180
|
threadSession: zod_1.z.boolean().optional(),
|
|
181
|
+
allowBots: AllowBotsSchema,
|
|
179
182
|
uat: exports.UATConfigSchema,
|
|
180
183
|
});
|
|
181
184
|
// ---------------------------------------------------------------------------
|
package/src/core/tool-scopes.js
CHANGED
|
@@ -35,7 +35,7 @@ function buildMentionAnnotation(ctx) {
|
|
|
35
35
|
if (mentions.length === 0)
|
|
36
36
|
return undefined;
|
|
37
37
|
const mentionDetails = mentions.map((t) => `${t.name} (open_id: ${t.openId})`).join(', ');
|
|
38
|
-
return `[System: This message @mentions the following users: ${mentionDetails}. Use these open_ids when performing actions involving these users.]`;
|
|
38
|
+
return `[System: This message @mentions the following users: ${mentionDetails}. Use these open_ids when performing actions involving these users. To @mention in a reply, use \`<at user_id="ou_xxx">Name</at>\`; plain "@Name" won't notify.]`;
|
|
39
39
|
}
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
41
|
// Message body builders
|
|
@@ -44,18 +44,21 @@ const media_resolver_1 = require("./media-resolver.js");
|
|
|
44
44
|
async function resolveSenderInfo(params) {
|
|
45
45
|
const { account, log } = params;
|
|
46
46
|
let ctx = params.ctx;
|
|
47
|
-
//
|
|
48
|
-
// does not return
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
// Bots and users have separate name-resolution endpoints. The contact API
|
|
48
|
+
// does not return bot info, so dispatch on senderIsBot. Both endpoints
|
|
49
|
+
// populate the same account-scoped cache (keyed by openId).
|
|
50
|
+
//
|
|
51
|
+
// Skip resolution for unknown sender_types (e.g. anonymous, missing) — the
|
|
52
|
+
// contact API would 4xx and the bot API would not match. This preserves the
|
|
53
|
+
// pre-bot-support behavior of only resolving names for `sender_type === 'user'`.
|
|
54
|
+
const senderType = ctx.rawSender?.sender_type;
|
|
55
|
+
if (!ctx.senderIsBot && senderType !== 'user') {
|
|
56
|
+
log(`sender_type is "${senderType ?? 'undefined'}", skipping name resolution`);
|
|
51
57
|
return { ctx };
|
|
52
58
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
account,
|
|
56
|
-
openId: ctx.senderId,
|
|
57
|
-
log,
|
|
58
|
-
});
|
|
59
|
+
const senderResult = ctx.senderIsBot
|
|
60
|
+
? await (0, user_name_cache_1.resolveBotName)({ account, openId: ctx.senderId, log })
|
|
61
|
+
: await (0, user_name_cache_1.resolveUserName)({ account, openId: ctx.senderId, log });
|
|
59
62
|
if (senderResult.name) {
|
|
60
63
|
ctx = { ...ctx, senderName: senderResult.name };
|
|
61
64
|
log(`sender resolved: ${senderResult.name}`);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import type { ClawdbotConfig } from 'openclaw/plugin-sdk';
|
|
26
26
|
import type { HistoryEntry } from 'openclaw/plugin-sdk/reply-history';
|
|
27
27
|
import type { MessageContext } from '../types';
|
|
28
|
-
import type { FeishuConfig, LarkAccount } from '../../core/types';
|
|
28
|
+
import type { FeishuConfig, FeishuGroupConfig, LarkAccount } from '../../core/types';
|
|
29
29
|
/**
|
|
30
30
|
* Resolve the effective `respondToMentionAll` setting.
|
|
31
31
|
*
|
|
@@ -42,6 +42,21 @@ export declare function resolveRespondToMentionAll(params: {
|
|
|
42
42
|
respondToMentionAll?: boolean;
|
|
43
43
|
};
|
|
44
44
|
}): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the effective allowBots setting.
|
|
47
|
+
*
|
|
48
|
+
* Precedence: per-group > default ("*") > account > 'mentions'.
|
|
49
|
+
*
|
|
50
|
+
* The `'mentions'` default lets bot-to-bot interaction work out of the box
|
|
51
|
+
* while still requiring an explicit @-mention in groups; DMs treat it as
|
|
52
|
+
* pass-through. Operators can opt into fully-open (`true`) or fully-closed
|
|
53
|
+
* (`false`) explicitly.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveAllowBots(params: {
|
|
56
|
+
groupConfig?: FeishuGroupConfig;
|
|
57
|
+
defaultConfig?: FeishuGroupConfig;
|
|
58
|
+
accountFeishuCfg?: FeishuConfig;
|
|
59
|
+
}): boolean | 'mentions';
|
|
45
60
|
/**
|
|
46
61
|
* Read the pairing allowFrom store for the Feishu channel via the SDK runtime.
|
|
47
62
|
*/
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
27
|
exports.resolveRespondToMentionAll = resolveRespondToMentionAll;
|
|
28
|
+
exports.resolveAllowBots = resolveAllowBots;
|
|
28
29
|
exports.readFeishuAllowFromStore = readAllowFromStore;
|
|
29
30
|
exports.checkMessageGate = checkMessageGate;
|
|
30
31
|
const lark_client_1 = require("../../core/lark-client.js");
|
|
@@ -42,6 +43,22 @@ function resolveRespondToMentionAll(params) {
|
|
|
42
43
|
params.accountFeishuCfg?.respondToMentionAll ??
|
|
43
44
|
false);
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the effective allowBots setting.
|
|
48
|
+
*
|
|
49
|
+
* Precedence: per-group > default ("*") > account > 'mentions'.
|
|
50
|
+
*
|
|
51
|
+
* The `'mentions'` default lets bot-to-bot interaction work out of the box
|
|
52
|
+
* while still requiring an explicit @-mention in groups; DMs treat it as
|
|
53
|
+
* pass-through. Operators can opt into fully-open (`true`) or fully-closed
|
|
54
|
+
* (`false`) explicitly.
|
|
55
|
+
*/
|
|
56
|
+
function resolveAllowBots(params) {
|
|
57
|
+
return (params.groupConfig?.allowBots ??
|
|
58
|
+
params.defaultConfig?.allowBots ??
|
|
59
|
+
params.accountFeishuCfg?.allowBots ??
|
|
60
|
+
'mentions');
|
|
61
|
+
}
|
|
45
62
|
/** Prevent spamming the legacy groupAllowFrom migration warning. */
|
|
46
63
|
let legacyGroupAllowFromWarned = false;
|
|
47
64
|
// ---------------------------------------------------------------------------
|
|
@@ -64,23 +81,34 @@ async function readAllowFromStore(accountId) {
|
|
|
64
81
|
* and send pairing request messages.
|
|
65
82
|
*/
|
|
66
83
|
async function checkMessageGate(params) {
|
|
67
|
-
const { ctx
|
|
84
|
+
const { ctx } = params;
|
|
85
|
+
if (ctx.senderIsBot) {
|
|
86
|
+
return checkBotSenderGate(params);
|
|
87
|
+
}
|
|
68
88
|
const isGroup = ctx.chatType === 'group';
|
|
69
89
|
if (isGroup) {
|
|
70
|
-
return checkGroupGate(
|
|
90
|
+
return checkGroupGate(params);
|
|
71
91
|
}
|
|
72
|
-
return checkDmGate(
|
|
92
|
+
return checkDmGate(params);
|
|
73
93
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Layer 1 group-level admission check, shared between human and bot sender paths.
|
|
96
|
+
*
|
|
97
|
+
* Computes:
|
|
98
|
+
* - `groupPolicy` access via SDK (`resolveGroupPolicy`)
|
|
99
|
+
* - Legacy chat-id-in-`groupAllowFrom` compat
|
|
100
|
+
* - Per-group `enabled === false` kill switch
|
|
101
|
+
*
|
|
102
|
+
* Returns `rejected` non-null when the caller should reject with that result;
|
|
103
|
+
* otherwise the resolved per-group config is returned for downstream use.
|
|
104
|
+
*
|
|
105
|
+
* Bot senders go through the same Layer 1 as humans — `allowBots` only governs
|
|
106
|
+
* sender-axis admission, not which groups the account responds in.
|
|
107
|
+
*/
|
|
108
|
+
function resolveFeishuGroupAccess(params) {
|
|
78
109
|
const { ctx, accountFeishuCfg, account, accountScopedCfg, log } = params;
|
|
79
110
|
const core = lark_client_1.LarkClient.runtime;
|
|
80
|
-
//
|
|
81
|
-
// Older Feishu configs used groupAllowFrom with chat_ids (oc_xxx) to
|
|
82
|
-
// control which groups are allowed. The correct semantic (aligned with
|
|
83
|
-
// Telegram) is sender_ids. Detect and split so both layers still work.
|
|
111
|
+
// Legacy compat: groupAllowFrom with chat_id entries.
|
|
84
112
|
const rawGroupAllowFrom = accountFeishuCfg?.groupAllowFrom ?? [];
|
|
85
113
|
const { legacyChatIds, senderAllowFrom: senderGroupAllowFrom } = (0, policy_1.splitLegacyGroupAllowFrom)(rawGroupAllowFrom);
|
|
86
114
|
if (legacyChatIds.length > 0 && !legacyGroupAllowFromWarned) {
|
|
@@ -92,11 +120,9 @@ function checkGroupGate(params) {
|
|
|
92
120
|
legacyChatIds.map((id) => ` "${id}": {},`).join('\n') +
|
|
93
121
|
`\n }`);
|
|
94
122
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// -
|
|
98
|
-
// - groupPolicy "allowlist" (or groups configured) → only listed groups pass
|
|
99
|
-
// - groupPolicy "disabled" → all groups blocked
|
|
123
|
+
const groupConfig = (0, policy_1.resolveFeishuGroupConfig)({ cfg: accountFeishuCfg, groupId: ctx.chatId });
|
|
124
|
+
const defaultConfig = accountFeishuCfg?.groups?.['*'];
|
|
125
|
+
// SDK group-level policy (groupPolicy disabled / allowlist / open).
|
|
100
126
|
const groupAccess = core.channel.groups.resolveGroupPolicy({
|
|
101
127
|
cfg: accountScopedCfg ?? {},
|
|
102
128
|
channel: 'feishu',
|
|
@@ -105,33 +131,98 @@ function checkGroupGate(params) {
|
|
|
105
131
|
groupIdCaseInsensitive: true,
|
|
106
132
|
hasGroupAllowFrom: senderGroupAllowFrom.length > 0,
|
|
107
133
|
});
|
|
108
|
-
// Legacy compat: if SDK rejects the group but the chat_id is in the
|
|
109
|
-
// old-style groupAllowFrom, allow it (backward compatibility).
|
|
110
|
-
// Track whether this group was admitted via legacy path so we can skip
|
|
111
|
-
// sender filtering below (old semantic: chat_id in groupAllowFrom meant
|
|
112
|
-
// "allow this group for any sender").
|
|
113
134
|
let legacyGroupAdmit = false;
|
|
114
135
|
if (!groupAccess.allowed) {
|
|
115
136
|
const chatIdLower = ctx.chatId.toLowerCase();
|
|
116
137
|
const legacyMatch = legacyChatIds.some((id) => String(id).toLowerCase() === chatIdLower);
|
|
117
138
|
if (!legacyMatch) {
|
|
118
139
|
log(`feishu[${account.accountId}]: group ${ctx.chatId} blocked by group-level policy`);
|
|
119
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
rejected: { allowed: false, reason: 'group_not_allowed' },
|
|
142
|
+
legacyGroupAdmit: false,
|
|
143
|
+
senderGroupAllowFrom,
|
|
144
|
+
groupConfig,
|
|
145
|
+
defaultConfig,
|
|
146
|
+
};
|
|
120
147
|
}
|
|
121
148
|
legacyGroupAdmit = true;
|
|
122
149
|
}
|
|
123
|
-
// ---- Per-group config (Feishu-specific fields) ----
|
|
124
|
-
const groupConfig = (0, policy_1.resolveFeishuGroupConfig)({
|
|
125
|
-
cfg: accountFeishuCfg,
|
|
126
|
-
groupId: ctx.chatId,
|
|
127
|
-
});
|
|
128
|
-
const defaultConfig = accountFeishuCfg?.groups?.['*'];
|
|
129
|
-
// Per-group enabled flag
|
|
130
150
|
const enabled = groupConfig?.enabled ?? defaultConfig?.enabled;
|
|
131
151
|
if (enabled === false) {
|
|
132
152
|
log(`feishu[${account.accountId}]: group ${ctx.chatId} disabled by per-group config`);
|
|
133
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
rejected: { allowed: false, reason: 'group_disabled' },
|
|
155
|
+
legacyGroupAdmit,
|
|
156
|
+
senderGroupAllowFrom,
|
|
157
|
+
groupConfig,
|
|
158
|
+
defaultConfig,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { rejected: null, legacyGroupAdmit, senderGroupAllowFrom, groupConfig, defaultConfig };
|
|
162
|
+
}
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Internal: bot sender gate
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
function checkBotSenderGate(params) {
|
|
167
|
+
const { ctx, accountFeishuCfg, account, log } = params;
|
|
168
|
+
const isGroup = ctx.chatType === 'group';
|
|
169
|
+
// 1. Layer 1 group access — bot senders are subject to the same group-level
|
|
170
|
+
// admission as humans. `allowBots` is a sender-axis filter, not a group-axis
|
|
171
|
+
// filter; an account configured to ignore a group must ignore bots there too.
|
|
172
|
+
let groupConfig;
|
|
173
|
+
let defaultConfig;
|
|
174
|
+
if (isGroup) {
|
|
175
|
+
const access = resolveFeishuGroupAccess(params);
|
|
176
|
+
if (access.rejected)
|
|
177
|
+
return access.rejected;
|
|
178
|
+
groupConfig = access.groupConfig;
|
|
179
|
+
defaultConfig = access.defaultConfig;
|
|
134
180
|
}
|
|
181
|
+
// 2. Resolve allowBots (per-group > default > account > 'mentions')
|
|
182
|
+
const allowBots = resolveAllowBots({ groupConfig, defaultConfig, accountFeishuCfg });
|
|
183
|
+
// 3. allowBots === false → drop
|
|
184
|
+
if (allowBots === false) {
|
|
185
|
+
log(`feishu[${account.accountId}]: drop bot sender ${ctx.senderId} in ${ctx.chatId} (allowBots=false)`);
|
|
186
|
+
return { allowed: false, reason: 'bot_sender_disabled' };
|
|
187
|
+
}
|
|
188
|
+
// 4. allowBots === 'mentions' + bot not mentioned → drop (group only;
|
|
189
|
+
// DMs have no @-mention concept, so mention-mode is a pass-through there).
|
|
190
|
+
if (isGroup && allowBots === 'mentions' && !(0, mention_1.mentionedBot)(ctx)) {
|
|
191
|
+
log(`feishu[${account.accountId}]: drop bot sender ${ctx.senderId} in ${ctx.chatId} (allowBots=mentions, not mentioned)`);
|
|
192
|
+
return { allowed: false, reason: 'bot_sender_not_mentioned' };
|
|
193
|
+
}
|
|
194
|
+
// 5. Group requireMention check — redundant with allowBots='mentions' but
|
|
195
|
+
// necessary for the explicit `allowBots=true + requireMention=true` combo.
|
|
196
|
+
//
|
|
197
|
+
// NOTE: this intentionally diverges from the human-sender path (checkGroupGate),
|
|
198
|
+
// which delegates to SDK's resolveRequireMention that defaults to true.
|
|
199
|
+
// For bot senders, `requireMention` must be explicitly set to true — the
|
|
200
|
+
// rationale being: if the operator opts into `allowBots=true`, they want
|
|
201
|
+
// bot traffic through by default. Holding bots to a true-default mention
|
|
202
|
+
// requirement would silently negate `allowBots=true` in most configs.
|
|
203
|
+
if (isGroup) {
|
|
204
|
+
const requireMention = groupConfig?.requireMention ??
|
|
205
|
+
defaultConfig?.requireMention ??
|
|
206
|
+
accountFeishuCfg?.requireMention;
|
|
207
|
+
if (requireMention === true && !(0, mention_1.mentionedBot)(ctx)) {
|
|
208
|
+
log(`feishu[${account.accountId}]: drop bot sender ${ctx.senderId} (no_mention)`);
|
|
209
|
+
// Intentionally NO historyEntry — bot messages never enter chat history.
|
|
210
|
+
return { allowed: false, reason: 'no_mention' };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { allowed: true };
|
|
214
|
+
}
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Internal: group gate
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
function checkGroupGate(params) {
|
|
219
|
+
const { ctx, accountFeishuCfg, account, accountScopedCfg, log } = params;
|
|
220
|
+
const core = lark_client_1.LarkClient.runtime;
|
|
221
|
+
// ---- Layer 1: Group-level admission (shared with bot path) ----
|
|
222
|
+
const access = resolveFeishuGroupAccess(params);
|
|
223
|
+
if (access.rejected)
|
|
224
|
+
return access.rejected;
|
|
225
|
+
const { legacyGroupAdmit, senderGroupAllowFrom, groupConfig, defaultConfig } = access;
|
|
135
226
|
// ---- Layer 2: Sender-level access ----
|
|
136
227
|
// Per-group groupPolicy overrides the global groupPolicy for sender filtering.
|
|
137
228
|
// senderGroupAllowFrom (global, oc_ entries excluded) + per-group allowFrom.
|
|
@@ -117,6 +117,10 @@ async function parseMessageEvent(event, botOpenId, expandCtx) {
|
|
|
117
117
|
resources,
|
|
118
118
|
mentions: mentionList,
|
|
119
119
|
mentionAll,
|
|
120
|
+
// Per Feishu docs, im.message.receive_v1 sets sender_type to 'user' or
|
|
121
|
+
// 'bot'. We also accept 'app' defensively for any SDK/legacy variant that
|
|
122
|
+
// surfaces the older value.
|
|
123
|
+
senderIsBot: event.sender.sender_type === 'bot' || event.sender.sender_type === 'app',
|
|
120
124
|
createTime: Number.isNaN(createTime) ? undefined : createTime,
|
|
121
125
|
rawMessage: effectiveContent !== event.message.content ? { ...event.message, content: effectiveContent } : event.message,
|
|
122
126
|
rawSender: event.sender,
|
|
@@ -41,6 +41,18 @@ export interface ResolveUserNameResult {
|
|
|
41
41
|
name?: string;
|
|
42
42
|
permissionError?: PermissionError;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a single bot's display name via `/open-apis/bot/v3/bots/basic_batch`.
|
|
46
|
+
*
|
|
47
|
+
* Bots are not returned by the contact API, so they have their own endpoint.
|
|
48
|
+
* Names share the same account-scoped cache (keyed by openId) since both
|
|
49
|
+
* bots and users have `ou_` prefixed openIds and a single display name.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveBotName(params: {
|
|
52
|
+
account: LarkAccount;
|
|
53
|
+
openId: string;
|
|
54
|
+
log: (...args: unknown[]) => void;
|
|
55
|
+
}): Promise<ResolveUserNameResult>;
|
|
44
56
|
/**
|
|
45
57
|
* Resolve a single user's display name.
|
|
46
58
|
*
|
|
@@ -16,6 +16,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.getUserNameCache = exports.clearUserNameCache = exports.UserNameCache = void 0;
|
|
17
17
|
exports.batchResolveUserNames = batchResolveUserNames;
|
|
18
18
|
exports.createBatchResolveNames = createBatchResolveNames;
|
|
19
|
+
exports.resolveBotName = resolveBotName;
|
|
19
20
|
exports.resolveUserName = resolveUserName;
|
|
20
21
|
const lark_client_1 = require("../../core/lark-client.js");
|
|
21
22
|
const user_name_cache_store_1 = require("./user-name-cache-store.js");
|
|
@@ -100,6 +101,51 @@ function createBatchResolveNames(account, log) {
|
|
|
100
101
|
await batchResolveUserNames({ account, openIds, log });
|
|
101
102
|
};
|
|
102
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a single bot's display name via `/open-apis/bot/v3/bots/basic_batch`.
|
|
106
|
+
*
|
|
107
|
+
* Bots are not returned by the contact API, so they have their own endpoint.
|
|
108
|
+
* Names share the same account-scoped cache (keyed by openId) since both
|
|
109
|
+
* bots and users have `ou_` prefixed openIds and a single display name.
|
|
110
|
+
*/
|
|
111
|
+
async function resolveBotName(params) {
|
|
112
|
+
const { account, openId, log } = params;
|
|
113
|
+
if (!account.configured || !openId)
|
|
114
|
+
return {};
|
|
115
|
+
const cache = (0, user_name_cache_store_1.getUserNameCache)(account.accountId);
|
|
116
|
+
if (cache.has(openId))
|
|
117
|
+
return { name: cache.get(openId) ?? '' };
|
|
118
|
+
try {
|
|
119
|
+
const client = lark_client_1.LarkClient.fromAccount(account).sdk;
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
121
|
+
const res = await client.request({
|
|
122
|
+
method: 'GET',
|
|
123
|
+
url: '/open-apis/bot/v3/bots/basic_batch',
|
|
124
|
+
params: { bot_ids: [openId] },
|
|
125
|
+
});
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
const bot = res?.data?.bots?.[openId];
|
|
128
|
+
const name = bot?.name || bot?.i18n_names?.zh_cn || bot?.i18n_names?.en_us || '';
|
|
129
|
+
// Cache even empty names to avoid repeated API calls for bots
|
|
130
|
+
// whose names we cannot resolve.
|
|
131
|
+
cache.set(openId, name);
|
|
132
|
+
return { name: name || undefined };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// Bot name resolution is best-effort: missing `bot:basic_info` scope
|
|
136
|
+
// should not surface as a permission notification to the agent. Log
|
|
137
|
+
// and cache an empty name so we don't retry, then fall back to openId.
|
|
138
|
+
const permErr = (0, permission_1.extractPermissionError)(err);
|
|
139
|
+
if (permErr) {
|
|
140
|
+
log(`feishu: permission error resolving bot name (best-effort, ignored): code=${permErr.code}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
log(`feishu: failed to resolve bot name for ${openId}: ${String(err)}`);
|
|
144
|
+
}
|
|
145
|
+
cache.set(openId, '');
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
103
149
|
/**
|
|
104
150
|
* Resolve a single user's display name.
|
|
105
151
|
*
|
package/src/messaging/types.d.ts
CHANGED
|
@@ -270,6 +270,14 @@ export interface MessageContext {
|
|
|
270
270
|
mentions: MentionInfo[];
|
|
271
271
|
/** Whether an @all / @所有人 mention was detected in the message. */
|
|
272
272
|
mentionAll: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* True when the event sender is a bot/app (sender_type === 'app').
|
|
275
|
+
*
|
|
276
|
+
* Set by parseMessageEvent. Optional because synthetic construction sites
|
|
277
|
+
* (comment / reaction / vc-invited handlers) omit it — absence is treated
|
|
278
|
+
* as `false` since those paths only originate from human actors today.
|
|
279
|
+
*/
|
|
280
|
+
senderIsBot?: boolean;
|
|
273
281
|
rootId?: string;
|
|
274
282
|
parentId?: string;
|
|
275
283
|
threadId?: string;
|
|
@@ -324,7 +324,7 @@ function registerFeishuTaskTaskTool(api) {
|
|
|
324
324
|
page_token: p.page_token,
|
|
325
325
|
completed: p.completed,
|
|
326
326
|
agent_task_status: p.agent_task_status,
|
|
327
|
-
user_id_type:
|
|
327
|
+
user_id_type: p.user_id_type || 'open_id',
|
|
328
328
|
},
|
|
329
329
|
}, opts), { as: authType });
|
|
330
330
|
(0, helpers_1.assertLarkOk)(res);
|