@larksuite/openclaw-lark 2026.4.9-beta.0 → 2026.4.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larksuite/openclaw-lark",
3
- "version": "2026.4.9-beta.0",
3
+ "version": "2026.4.9",
4
4
  "description": "OpenClaw Lark/Feishu channel plugin",
5
5
  "exports": {
6
6
  ".": {
@@ -9,6 +9,9 @@ description: |
9
9
  (3) 需要查看任务列表或清单内的任务
10
10
  (4) 用户提到"任务"、"待办"、"to-do"、"清单"、"task"
11
11
  (5) 需要设置任务负责人、关注人、截止时间、添加成员
12
+ (6) 需要追加任务步骤记录(Task 的 steps)
13
+ (7) 需要上传任务附件(支持 task / task_delivery)
14
+ (8) 需要注册 Agent / 更新 Agent 信息(register / update_profile)
12
15
  ---
13
16
 
14
17
  # 飞书任务管理
@@ -17,11 +20,13 @@ description: |
17
20
 
18
21
  - ✅ **时间格式**:ISO 8601 / RFC 3339(带时区),例如 `2026-02-28T17:00:00+08:00`
19
22
  - ✅ **身份授权**:工具支持 `auth_type` 为 `user`(默认,用户身份)或 `tenant`(应用身份)。
23
+ - ✅ **任务 Agent(feishu_task_agent)**:仅支持应用身份(tenant),不支持 user 身份
20
24
  - ✅ **current_user_id 强烈建议**:从消息上下文的 SenderId 获取(ou_...),工具会自动添加为 follower(如不在 members 中),确保创建者可以编辑任务
21
25
  - ✅ **patch/get 必须**:task_guid
22
26
  - ✅ **tasklist.tasks 必须**:tasklist_guid
23
27
  - ✅ **完成任务**:completed_at = "2026-02-26 15:00:00"
24
28
  - ✅ **反完成(恢复未完成)**:completed_at = "0"
29
+ - ✅ **append_steps 的 task_steps[].timestamp**:秒级 Unix 时间戳(10 位),不要用毫秒(13 位)
25
30
 
26
31
  ---
27
32
 
@@ -30,15 +35,19 @@ description: |
30
35
  | 用户意图 | 工具 | action | 必填参数 | 强烈建议 | 常用可选 |
31
36
  |---------|------|--------|---------|---------|---------|
32
37
  | 新建待办 | feishu_task_task | create | summary | current_user_id(SenderId) | members, due, description, auth_type |
33
- | 查未完成任务 | feishu_task_task | list | - | completed=false | page_size, auth_type |
38
+ | 查未完成任务 | feishu_task_task | list | - | completed=false | page_size, auth_type, agent_task_status |
34
39
  | 获取任务详情 | feishu_task_task | get | task_guid | - | auth_type |
35
40
  | 完成任务 | feishu_task_task | patch | task_guid, completed_at | - | auth_type |
36
41
  | 反完成任务 | feishu_task_task | patch | task_guid, completed_at="0" | - | auth_type |
37
42
  | 改截止时间 | feishu_task_task | patch | task_guid, due | - | auth_type |
38
43
  | 添加任务成员 | feishu_task_task | add_members | task_guid, members[] | - | auth_type |
44
+ | 追加任务步骤记录 | feishu_task_task | append_steps | task_guid, idempotent_key, task_steps[] | task_steps[].timestamp 用秒级(10 位) | - |
39
45
  | 创建清单 | feishu_task_tasklist | create | name | - | members |
40
46
  | 查看清单任务 | feishu_task_tasklist | tasks | tasklist_guid | - | completed |
41
47
  | 添加清单成员 | feishu_task_tasklist | add_members | tasklist_guid, members[] | - | - |
48
+ | 上传任务附件 | feishu_task_attachment | upload | resource_id, file(base64) | name | resource_type |
49
+ | 注册任务 Agent | feishu_task_agent | register | - | 仅支持 tenant(应用身份) | - |
50
+ | 更新任务 Agent Profile | feishu_task_agent | update_profile | profile_content | 仅支持 tenant(应用身份) | - |
42
51
 
43
52
  ---
44
53
 
@@ -233,6 +242,25 @@ description: |
233
242
  }
234
243
  ```
235
244
 
245
+ ### 场景 9: 注册任务 Agent(仅应用身份)
246
+
247
+ ```json
248
+ {
249
+ "action": "register",
250
+ "auth_type": "tenant"
251
+ }
252
+ ```
253
+
254
+ ### 场景 10: 更新任务 Agent Profile(仅应用身份)
255
+
256
+ ```json
257
+ {
258
+ "action": "update_profile",
259
+ "auth_type": "tenant",
260
+ "profile_content": "some profile content"
261
+ }
262
+ ```
263
+
236
264
  ---
237
265
 
238
266
  ## 🔍 常见错误与排查
@@ -12,5 +12,6 @@ import type { MonitorContext } from './types';
12
12
  export declare function handleMessageEvent(ctx: MonitorContext, data: unknown): Promise<void>;
13
13
  export declare function handleReactionEvent(ctx: MonitorContext, data: unknown): Promise<void>;
14
14
  export declare function handleBotMembershipEvent(ctx: MonitorContext, data: unknown, action: 'added' | 'removed'): Promise<void>;
15
+ export declare function handleVcMeetingInvitedEvent(ctx: MonitorContext, data: unknown): Promise<void>;
15
16
  export declare function handleCommentEvent(ctx: MonitorContext, data: unknown): Promise<void>;
16
17
  export declare function handleCardActionEvent(ctx: MonitorContext, data: unknown): Promise<unknown>;
@@ -13,11 +13,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.handleMessageEvent = handleMessageEvent;
14
14
  exports.handleReactionEvent = handleReactionEvent;
15
15
  exports.handleBotMembershipEvent = handleBotMembershipEvent;
16
+ exports.handleVcMeetingInvitedEvent = handleVcMeetingInvitedEvent;
16
17
  exports.handleCommentEvent = handleCommentEvent;
17
18
  exports.handleCardActionEvent = handleCardActionEvent;
18
19
  const handler_1 = require("../messaging/inbound/handler.js");
19
20
  const reaction_handler_1 = require("../messaging/inbound/reaction-handler.js");
20
21
  const comment_handler_1 = require("../messaging/inbound/comment-handler.js");
22
+ const vc_meeting_invited_handler_1 = require("../messaging/inbound/vc-meeting-invited-handler.js");
23
+ const vc_sender_1 = require("../messaging/inbound/vc-sender.js");
21
24
  const comment_context_1 = require("../messaging/inbound/comment-context.js");
22
25
  const dedup_1 = require("../messaging/inbound/dedup.js");
23
26
  const lark_ticket_1 = require("../core/lark-ticket.js");
@@ -223,6 +226,76 @@ async function handleBotMembershipEvent(ctx, data, action) {
223
226
  }
224
227
  }
225
228
  // ---------------------------------------------------------------------------
229
+ // VC meeting invited handler
230
+ // ---------------------------------------------------------------------------
231
+ async function handleVcMeetingInvitedEvent(ctx, data) {
232
+ if (!isEventOwnershipValid(ctx, data))
233
+ return;
234
+ const { accountId, log, error } = ctx;
235
+ try {
236
+ const event = data;
237
+ const meetingNo = event.meeting?.meeting_no?.trim() ?? '';
238
+ const eventId = event.event_id?.trim() ?? '';
239
+ // Resolve the inviter identity through the shared helper so the
240
+ // diagnostics log and the dispatch handler always agree on the
241
+ // same sender semantics.
242
+ const sender = (0, vc_sender_1.resolveVcSender)(event);
243
+ const senderId = sender.senderId;
244
+ const invitedBotOpenId = event.bot?.id?.open_id?.trim() ?? '';
245
+ // VC invited origin/ownership diagnostics:
246
+ // - This handler is only reachable from the WebSocket monitor path.
247
+ // - We still log app_id/bot_open_id so operators can confirm the event
248
+ // is delivered to the expected bot/account, and see which required
249
+ // fields are missing when we skip.
250
+ const expectedAppId = ctx.lark.account.appId ?? '';
251
+ const eventAppId = event.app_id?.trim() ?? '';
252
+ log(`feishu[${accountId}]: vc invited event received (ingress=websocket)` +
253
+ `${eventId ? ` event_id=${eventId}` : ''}` +
254
+ `${eventAppId ? ` app_id=${eventAppId}` : ' app_id=<missing>'}` +
255
+ `${expectedAppId ? ` expected_app_id=${expectedAppId}` : ''}` +
256
+ `${invitedBotOpenId ? ` bot_open_id=${invitedBotOpenId}` : ' bot_open_id=<missing>'}` +
257
+ `${ctx.lark.botOpenId ? ` expected_bot_open_id=${ctx.lark.botOpenId}` : ''}` +
258
+ `${event.invite_time ? ` invite_time=${event.invite_time}` : ''}` +
259
+ ` meeting_no_present=${meetingNo ? 'true' : 'false'}` +
260
+ ` sender_present=${senderId ? 'true' : 'false'}` +
261
+ ` sender_from=${sender.fromFallback}`);
262
+ if (!meetingNo) {
263
+ log(`feishu[${accountId}]: vc invited event missing meeting_no, skipping`);
264
+ return;
265
+ }
266
+ if (!senderId) {
267
+ log(`feishu[${accountId}]: vc invited event missing inviter identity, skipping`);
268
+ return;
269
+ }
270
+ if (ctx.lark.botOpenId && invitedBotOpenId && invitedBotOpenId !== ctx.lark.botOpenId) {
271
+ log(`feishu[${accountId}]: vc invited event for another bot, expected=${ctx.lark.botOpenId}, got=${invitedBotOpenId}, skipping`);
272
+ return;
273
+ }
274
+ // Prefer event_id when the SDK exposes it: historical raw payload logs
275
+ // show WebSocket reconnect replays reuse the same event_id, while a real
276
+ // second invitation yields a new event_id even for the same meeting/bot.
277
+ // Fallback to (meeting_no, bot) only when event_id is absent so older
278
+ // payload shapes still remain deduplicated.
279
+ const dedupBotKey = ctx.lark.botOpenId ?? invitedBotOpenId ?? 'no-bot';
280
+ const dedupKey = eventId ? `vc-invited:by-event:${eventId}` : `vc-invited:by-meeting:${meetingNo}:${dedupBotKey}`;
281
+ if (!ctx.messageDedup.tryRecord(dedupKey, accountId)) {
282
+ log(`feishu[${accountId}]: duplicate vc invited event detected, skipping`);
283
+ return;
284
+ }
285
+ log(`feishu[${accountId}]: vc invited event accepted for synthetic dispatch`);
286
+ await (0, vc_meeting_invited_handler_1.handleFeishuVcMeetingInvited)({
287
+ cfg: ctx.cfg,
288
+ event,
289
+ runtime: ctx.runtime,
290
+ chatHistories: ctx.chatHistories,
291
+ accountId,
292
+ });
293
+ }
294
+ catch (err) {
295
+ error(`feishu[${accountId}]: error handling vc invited event: ${String(err)}`);
296
+ }
297
+ }
298
+ // ---------------------------------------------------------------------------
226
299
  // Drive comment handler
227
300
  // ---------------------------------------------------------------------------
228
301
  async function handleCommentEvent(ctx, data) {
@@ -77,6 +77,7 @@ async function monitorSingleAccount(params) {
77
77
  'im.chat.access_event.bot_p2p_chat_entered_v1': async () => { },
78
78
  'im.chat.member.bot.added_v1': (data) => (0, event_handlers_1.handleBotMembershipEvent)(ctx, data, 'added'),
79
79
  'im.chat.member.bot.deleted_v1': (data) => (0, event_handlers_1.handleBotMembershipEvent)(ctx, data, 'removed'),
80
+ 'vc.bot.meeting_invited_v1': (data) => (0, event_handlers_1.handleVcMeetingInvitedEvent)(ctx, data),
80
81
  // Drive comment event — fires when a user adds a comment or reply on a document.
81
82
  'drive.notice.comment_add_v1': (data) => (0, event_handlers_1.handleCommentEvent)(ctx, data),
82
83
  // 飞书 SDK EventDispatcher.register 不支持带返回值的处理器,此处 as any 是 SDK 类型限制的变通
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Synthetic IM target utilities.
6
+ *
7
+ * Some inbound flows (VC meeting-invited, other service-triggered events)
8
+ * do not map to a real IM chat — there is no chat_id / open_id the agent
9
+ * should send messages into. To keep the dispatch pipeline uniform we give
10
+ * these flows a sentinel chatId ("synthetic:<kind>") and teach outbound
11
+ * deliverers to short-circuit whenever they see this prefix. This is the
12
+ * same pattern used by `core/comment-target.ts` for Drive comment threads.
13
+ */
14
+ /** Sentinel chatId for VC `vc.bot.meeting_invited_v1` synthetic inbound. */
15
+ export declare const SYNTHETIC_VC_CHAT_ID = "synthetic:vc-invited";
16
+ /**
17
+ * The `chatType` stamped on synthetic VC contexts.
18
+ *
19
+ * `MessageContext.chatType` is currently typed as `'p2p' | 'group'` across
20
+ * the plugin and widening that union touches every downstream signature.
21
+ * 'p2p' is the closest match (single-peer, non-group) and the outbound
22
+ * short-circuit gates on the sentinel chatId — not the chatType — so this
23
+ * choice does not produce any DMs on its own.
24
+ *
25
+ * TODO(synthetic-target): widen MessageContext.chatType to include
26
+ * `synthetic` once downstream signatures are audited.
27
+ */
28
+ export declare const SYNTHETIC_VC_CHAT_TYPE: "p2p";
29
+ /**
30
+ * Return `true` when `target` is a synthetic sentinel that outbound
31
+ * deliverers should not try to send an IM message to.
32
+ */
33
+ export declare function isSyntheticTarget(target: string | undefined | null): boolean;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Synthetic IM target utilities.
7
+ *
8
+ * Some inbound flows (VC meeting-invited, other service-triggered events)
9
+ * do not map to a real IM chat — there is no chat_id / open_id the agent
10
+ * should send messages into. To keep the dispatch pipeline uniform we give
11
+ * these flows a sentinel chatId ("synthetic:<kind>") and teach outbound
12
+ * deliverers to short-circuit whenever they see this prefix. This is the
13
+ * same pattern used by `core/comment-target.ts` for Drive comment threads.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.SYNTHETIC_VC_CHAT_TYPE = exports.SYNTHETIC_VC_CHAT_ID = void 0;
17
+ exports.isSyntheticTarget = isSyntheticTarget;
18
+ const SYNTHETIC_PREFIX = 'synthetic:';
19
+ /** Sentinel chatId for VC `vc.bot.meeting_invited_v1` synthetic inbound. */
20
+ exports.SYNTHETIC_VC_CHAT_ID = 'synthetic:vc-invited';
21
+ /**
22
+ * The `chatType` stamped on synthetic VC contexts.
23
+ *
24
+ * `MessageContext.chatType` is currently typed as `'p2p' | 'group'` across
25
+ * the plugin and widening that union touches every downstream signature.
26
+ * 'p2p' is the closest match (single-peer, non-group) and the outbound
27
+ * short-circuit gates on the sentinel chatId — not the chatType — so this
28
+ * choice does not produce any DMs on its own.
29
+ *
30
+ * TODO(synthetic-target): widen MessageContext.chatType to include
31
+ * `synthetic` once downstream signatures are audited.
32
+ */
33
+ exports.SYNTHETIC_VC_CHAT_TYPE = 'p2p';
34
+ /**
35
+ * Return `true` when `target` is a synthetic sentinel that outbound
36
+ * deliverers should not try to send an IM message to.
37
+ */
38
+ function isSyntheticTarget(target) {
39
+ return Boolean(target && target.startsWith(SYNTHETIC_PREFIX));
40
+ }
@@ -145,7 +145,6 @@ function resolveReceiveIdType(id) {
145
145
  return 'chat_id';
146
146
  if (id.startsWith(OPEN_ID_PREFIX))
147
147
  return 'open_id';
148
- // Default to open_id for any other pattern (safer for outbound API calls).
149
148
  return 'open_id';
150
149
  }
151
150
  function normalizeMessageId(messageId) {
@@ -22,6 +22,8 @@ export declare function dispatchToAgent(params: {
22
22
  ctx: MessageContext;
23
23
  permissionError?: PermissionError;
24
24
  mediaPayload: Record<string, unknown>;
25
+ /** Additional structured metadata for synthetic or event-driven inbound flows. */
26
+ extraInboundFields?: Record<string, unknown>;
25
27
  quotedContent?: string;
26
28
  account: LarkAccount;
27
29
  /** account 级别的 ClawdbotConfig(channels.feishu 已替换为 per-account 合并后的配置) */
@@ -26,6 +26,7 @@ const tool_use_trace_store_1 = require("../../card/tool-use-trace-store.js");
26
26
  const abort_detect_1 = require("../../channel/abort-detect.js");
27
27
  const chat_info_cache_1 = require("../../core/chat-info-cache.js");
28
28
  const comment_target_1 = require("../../core/comment-target.js");
29
+ const synthetic_target_1 = require("../../core/synthetic-target.js");
29
30
  const targets_1 = require("../../core/targets.js");
30
31
  const deliver_1 = require("../outbound/deliver.js");
31
32
  const doctor_1 = require("../../commands/doctor.js");
@@ -94,7 +95,81 @@ async function dispatchCommentMessage(dc, ctxPayload, skillFilter) {
94
95
  dc.log(`feishu[${dc.account.accountId}]: comment dispatch complete (delivered=${delivered})`);
95
96
  log.info(`comment dispatch complete (delivered=${delivered}, elapsed=${(0, lark_ticket_1.ticketElapsed)()}ms)`);
96
97
  }
98
+ /**
99
+ * Dispatch a synthetic-target message via the buffered block dispatcher
100
+ * while discarding every delivered payload.
101
+ *
102
+ * Synthetic contexts (e.g. VC meeting-invited) trigger the agent for its
103
+ * side-effects (tool calls) — they do not correspond to a real IM chat,
104
+ * so any text / card the agent emits must be dropped instead of being
105
+ * sent as a DM to whatever open_id happens to be in ctx.chatId.
106
+ */
107
+ async function dispatchSyntheticMessage(dc, ctxPayload, skillFilter) {
108
+ const effectiveSessionKey = dc.threadSessionKey ?? dc.route.sessionKey;
109
+ const isVcSynthetic = dc.ctx.chatId === synthetic_target_1.SYNTHETIC_VC_CHAT_ID;
110
+ let deliveredFinalToSender = false;
111
+ dc.log(`feishu[${dc.account.accountId}]: dispatching synthetic reply (session=${effectiveSessionKey}, target=${dc.ctx.chatId})`);
112
+ log.info(`dispatching synthetic reply (session=${effectiveSessionKey})`);
113
+ await dc.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
114
+ ctx: ctxPayload,
115
+ cfg: dc.accountScopedCfg,
116
+ dispatcherOptions: {
117
+ deliver: async (payload, info) => {
118
+ const text = payload.text?.trim() ?? '';
119
+ const preview = text.slice(0, 120);
120
+ // VC invited flows intentionally keep the synthetic target to avoid
121
+ // leaking intermediate tool output to IM, but the final business
122
+ // result should be explicitly notified to the inviter.
123
+ //
124
+ // Important: this DM is only a transport bridge for the final text.
125
+ // It does not rebind the inviter's DM conversation to the synthetic
126
+ // meeting-scoped session; later DM replies will still be routed by the
127
+ // normal OpenClaw DM session rules.
128
+ if (isVcSynthetic && info.kind === 'final' && text && text !== 'NO_REPLY' && !deliveredFinalToSender) {
129
+ deliveredFinalToSender = true;
130
+ try {
131
+ await (0, send_1.sendMessageFeishu)({
132
+ cfg: dc.accountScopedCfg,
133
+ to: dc.ctx.senderId,
134
+ text,
135
+ accountId: dc.account.accountId,
136
+ });
137
+ dc.log(`feishu[${dc.account.accountId}]: synthetic VC final delivered explicitly to sender=${dc.ctx.senderId}, preview="${preview}"`);
138
+ return;
139
+ }
140
+ catch (err) {
141
+ deliveredFinalToSender = false;
142
+ dc.error(`feishu[${dc.account.accountId}]: synthetic VC final delivery failed to sender=${dc.ctx.senderId}: ${String(err)}`);
143
+ }
144
+ }
145
+ if (info.kind === 'final') {
146
+ dc.log(`feishu[${dc.account.accountId}]: synthetic final payload dropped (target=${dc.ctx.chatId})`);
147
+ }
148
+ },
149
+ onSkip: (_payload, info) => {
150
+ if (info.reason !== 'silent') {
151
+ dc.log(`feishu[${dc.account.accountId}]: synthetic reply skipped (reason=${info.reason})`);
152
+ }
153
+ },
154
+ onError: (err, info) => {
155
+ dc.error(`feishu[${dc.account.accountId}]: synthetic ${info.kind} reply failed: ${String(err)}`);
156
+ },
157
+ },
158
+ replyOptions: {
159
+ ...(skillFilter ? { skillFilter } : {}),
160
+ },
161
+ });
162
+ dc.log(`feishu[${dc.account.accountId}]: synthetic dispatch complete (elapsed=${(0, lark_ticket_1.ticketElapsed)()}ms)`);
163
+ }
97
164
  async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey, historyLimit, replyToMessageId, skillFilter, skipTyping) {
165
+ // Synthetic targets (e.g. VC meeting-invited) have no real IM peer to
166
+ // deliver replies to. Route them through the buffered block dispatcher
167
+ // with a deliver() that drops every payload — the agent still runs
168
+ // (tool calls, side-effects) but produces no outbound IM traffic.
169
+ if ((0, synthetic_target_1.isSyntheticTarget)(dc.ctx.chatId)) {
170
+ await dispatchSyntheticMessage(dc, ctxPayload, skillFilter);
171
+ return;
172
+ }
98
173
  // Comment targets bypass the streaming card / IM flow entirely —
99
174
  // route through the Drive comment reply API.
100
175
  if ((0, comment_target_1.isCommentTarget)(dc.ctx.chatId)) {
@@ -277,6 +352,7 @@ async function dispatchToAgent(params) {
277
352
  inboundHistory,
278
353
  extraFields: {
279
354
  ...params.mediaPayload,
355
+ ...(params.extraInboundFields ?? {}),
280
356
  ...(groupSystemPrompt ? { GroupSystemPrompt: groupSystemPrompt } : {}),
281
357
  ...(dc.ctx.threadId ? { MessageThreadId: dc.ctx.threadId } : {}),
282
358
  },
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * VC meeting invited event handler for the Lark/Feishu channel plugin.
6
+ *
7
+ * Handles `vc.bot.meeting_invited_v1` by converting the event into a
8
+ * synthetic natural-language inbound and dispatching it through the
9
+ * standard OpenClaw agent pipeline.
10
+ */
11
+ import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk';
12
+ import type { HistoryEntry } from 'openclaw/plugin-sdk/reply-history';
13
+ import type { FeishuVcMeetingInvitedEvent } from '../types';
14
+ export declare function handleFeishuVcMeetingInvited(params: {
15
+ cfg: ClawdbotConfig;
16
+ event: FeishuVcMeetingInvitedEvent;
17
+ runtime?: RuntimeEnv;
18
+ chatHistories?: Map<string, HistoryEntry[]>;
19
+ accountId?: string;
20
+ }): Promise<void>;
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * VC meeting invited event handler for the Lark/Feishu channel plugin.
7
+ *
8
+ * Handles `vc.bot.meeting_invited_v1` by converting the event into a
9
+ * synthetic natural-language inbound and dispatching it through the
10
+ * standard OpenClaw agent pipeline.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.handleFeishuVcMeetingInvited = handleFeishuVcMeetingInvited;
47
+ const crypto = __importStar(require("node:crypto"));
48
+ const synthetic_target_1 = require("../../core/synthetic-target.js");
49
+ const accounts_1 = require("../../core/accounts.js");
50
+ const lark_logger_1 = require("../../core/lark-logger.js");
51
+ const dispatch_1 = require("./dispatch.js");
52
+ const gate_effects_1 = require("./gate-effects.js");
53
+ const gate_1 = require("./gate.js");
54
+ const policy_1 = require("./policy.js");
55
+ const vc_sender_1 = require("./vc-sender.js");
56
+ const logger = (0, lark_logger_1.larkLogger)('inbound/vc-meeting-invited-handler');
57
+ function buildSyntheticEvent(event) {
58
+ const meetingNo = event.meeting?.meeting_no?.trim() ?? '';
59
+ // Both meeting_no and inviter identity are required for this event.
60
+ if (!meetingNo) {
61
+ return null;
62
+ }
63
+ const sender = (0, vc_sender_1.resolveVcSender)(event);
64
+ if (!sender.senderId) {
65
+ return null;
66
+ }
67
+ return {
68
+ eventType: 'vc.bot.meeting_invited_v1',
69
+ source: 'feishu-vc-event',
70
+ eventId: event.event_id?.trim() || undefined,
71
+ meetingId: event.meeting?.id?.trim() || undefined,
72
+ meetingNo,
73
+ topic: event.meeting?.topic?.trim() || undefined,
74
+ senderId: sender.senderId,
75
+ senderOpenId: sender.senderOpenId,
76
+ senderUserId: sender.senderUserId,
77
+ senderUnionId: sender.senderUnionId,
78
+ senderName: sender.senderName,
79
+ inviteTime: event.invite_time?.trim() || undefined,
80
+ };
81
+ }
82
+ function buildSyntheticContext(event) {
83
+ // Keep the synthetic inbound prompt in English for now: it is an
84
+ // agent-facing intent string rather than user-visible copy, and the final
85
+ // reply language is still governed by the agent/session prompt stack.
86
+ // If we later need locale-aware synthetic prompts, this is the single place
87
+ // to introduce a template or config-based language switch.
88
+ const syntheticText = `Join the meeting with meeting number ${event.meetingNo}.`;
89
+ const syntheticMessageId = event.eventId
90
+ ? `vc-invited:event:${event.eventId}`
91
+ : `vc-invited:${event.meetingNo}:${event.inviteTime ?? crypto.randomUUID()}`;
92
+ // VC-invited events have no real chat/thread — they are service-to-service
93
+ // triggers. Using the inviter's open_id as chatId would cause downstream
94
+ // senders (reply / card / media) to fire off unsolicited DMs to the inviter
95
+ // whenever the agent produced any output. Use a synthetic sentinel instead
96
+ // and let IM-facing deliverers short-circuit on it (see SYNTHETIC_VC_CHAT_ID).
97
+ return {
98
+ chatId: synthetic_target_1.SYNTHETIC_VC_CHAT_ID,
99
+ messageId: syntheticMessageId,
100
+ senderId: event.senderId,
101
+ senderName: event.senderName,
102
+ chatType: synthetic_target_1.SYNTHETIC_VC_CHAT_TYPE,
103
+ content: syntheticText,
104
+ contentType: 'text',
105
+ resources: [],
106
+ mentions: [],
107
+ mentionAll: false,
108
+ rawMessage: {
109
+ message_id: syntheticMessageId,
110
+ chat_id: synthetic_target_1.SYNTHETIC_VC_CHAT_ID,
111
+ chat_type: synthetic_target_1.SYNTHETIC_VC_CHAT_TYPE,
112
+ message_type: 'text',
113
+ content: JSON.stringify({ text: syntheticText }),
114
+ create_time: event.inviteTime ?? String(Date.now()),
115
+ },
116
+ rawSender: {
117
+ sender_id: {
118
+ ...(event.senderOpenId ? { open_id: event.senderOpenId } : {}),
119
+ ...(event.senderUserId ? { user_id: event.senderUserId } : {}),
120
+ ...(event.senderUnionId ? { union_id: event.senderUnionId } : {}),
121
+ },
122
+ sender_type: 'user',
123
+ },
124
+ };
125
+ }
126
+ function matchesAnySenderId(params) {
127
+ const candidates = [...new Set(params.senderIds.map((id) => id?.trim()).filter(Boolean))];
128
+ return candidates.some((candidate) => (0, policy_1.resolveFeishuAllowlistMatch)({
129
+ allowFrom: params.allowFrom,
130
+ senderId: candidate,
131
+ }).allowed);
132
+ }
133
+ async function handleFeishuVcMeetingInvited(params) {
134
+ const { cfg, event, runtime, chatHistories, accountId } = params;
135
+ const log = runtime?.log ?? ((...args) => logger.info(args.map(String).join(' ')));
136
+ const error = runtime?.error ?? ((...args) => logger.error(args.map(String).join(' ')));
137
+ const syntheticEvent = buildSyntheticEvent(event);
138
+ if (!syntheticEvent) {
139
+ log(`feishu[${accountId}]: vc invited event missing meeting_no or inviter identity, skipping`);
140
+ return;
141
+ }
142
+ const account = (0, accounts_1.getLarkAccount)(cfg, accountId);
143
+ const accountScopedCfg = {
144
+ ...cfg,
145
+ channels: { ...cfg.channels, feishu: account.config },
146
+ };
147
+ const accountFeishuCfg = account.config;
148
+ // ---- Access policy enforcement (DM-style) ----
149
+ // VC invited events are user-triggered service events. Align their access
150
+ // semantics with direct-message/comment flows so unpaired users cannot
151
+ // trigger agent behavior through event ingress.
152
+ const dmPolicy = accountFeishuCfg?.dmPolicy ?? 'pairing';
153
+ if (dmPolicy === 'disabled') {
154
+ log(`feishu[${accountId}]: vc invited event rejected (dmPolicy=disabled)`);
155
+ return;
156
+ }
157
+ if (dmPolicy !== 'open') {
158
+ const configAllowFrom = accountFeishuCfg?.allowFrom ?? [];
159
+ const storeAllowFrom = await (0, gate_1.readFeishuAllowFromStore)(account.accountId).catch(() => []);
160
+ const combinedAllowFrom = [...configAllowFrom, ...storeAllowFrom];
161
+ const allowed = matchesAnySenderId({
162
+ allowFrom: combinedAllowFrom,
163
+ senderIds: [
164
+ syntheticEvent.senderOpenId,
165
+ syntheticEvent.senderUserId,
166
+ syntheticEvent.senderUnionId,
167
+ ],
168
+ });
169
+ if (!allowed) {
170
+ if (dmPolicy === 'pairing') {
171
+ if (syntheticEvent.senderOpenId) {
172
+ log(`feishu[${accountId}]: vc inviter not paired, creating pairing request`);
173
+ try {
174
+ await (0, gate_effects_1.sendPairingReply)({
175
+ senderId: syntheticEvent.senderOpenId,
176
+ chatId: syntheticEvent.senderOpenId,
177
+ accountId: account.accountId,
178
+ accountScopedCfg,
179
+ });
180
+ }
181
+ catch (pairingErr) {
182
+ log(`feishu[${accountId}]: failed to create pairing request for vc inviter: ${String(pairingErr)}`);
183
+ }
184
+ }
185
+ else {
186
+ log(`feishu[${accountId}]: vc inviter not paired and has no open_id for pairing reply, rejecting`);
187
+ }
188
+ }
189
+ else {
190
+ log(`feishu[${accountId}]: vc invited event rejected (dmPolicy=${dmPolicy}, inviter not in allowlist)`);
191
+ }
192
+ return;
193
+ }
194
+ }
195
+ const ctx = buildSyntheticContext(syntheticEvent);
196
+ log(`feishu[${accountId}]: vc meeting invited, dispatching synthetic inbound` +
197
+ ` sender=${syntheticEvent.senderId} meeting_no=${syntheticEvent.meetingNo}`);
198
+ try {
199
+ await (0, dispatch_1.dispatchToAgent)({
200
+ ctx,
201
+ permissionError: undefined,
202
+ mediaPayload: {},
203
+ extraInboundFields: {
204
+ SyntheticEventType: syntheticEvent.eventType,
205
+ VcMeetingId: syntheticEvent.meetingId,
206
+ VcMeetingNo: syntheticEvent.meetingNo,
207
+ VcMeetingTopic: syntheticEvent.topic,
208
+ VcInviterOpenId: syntheticEvent.senderOpenId,
209
+ VcInviteTime: syntheticEvent.inviteTime,
210
+ },
211
+ quotedContent: undefined,
212
+ account,
213
+ accountScopedCfg,
214
+ runtime,
215
+ chatHistories,
216
+ historyLimit: 0,
217
+ // VC events do not originate from a real IM message.
218
+ replyToMessageId: undefined,
219
+ commandAuthorized: false,
220
+ skipTyping: true,
221
+ });
222
+ }
223
+ catch (err) {
224
+ error(`feishu[${accountId}]: error dispatching vc invited synthetic inbound: ${String(err)}`);
225
+ }
226
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Shared sender-identity resolution for the VC meeting-invited event.
6
+ *
7
+ * Both the raw event handler (for diagnostics / dedup / ownership check)
8
+ * and the synthetic inbound builder (for dispatchToAgent) need a single,
9
+ * deterministic fallback chain. Keeping a dedicated module avoids drift
10
+ * between the two code paths when the event schema changes again.
11
+ */
12
+ import type { FeishuVcMeetingInvitedEvent } from '../types';
13
+ /** Which bucket the final senderId was picked from. */
14
+ export type VcSenderFallback = 'inviter' | 'none';
15
+ export interface ResolvedVcSender {
16
+ /**
17
+ * Final sender id used for logging / extraInboundFields. Sender is defined
18
+ * as the real inviter only; if inviter identity is missing, the event
19
+ * should be skipped instead of degrading to bot/config ids.
20
+ */
21
+ senderId: string;
22
+ /** Raw inviter-level open_id (if present); useful for agent "at inviter" use-cases. */
23
+ senderOpenId?: string;
24
+ /** Raw inviter-level user_id (if present). */
25
+ senderUserId?: string;
26
+ /** Raw inviter-level union_id (if present). */
27
+ senderUnionId?: string;
28
+ /** Human-readable name from inviter.user_name. */
29
+ senderName?: string;
30
+ /** Which bucket the senderId fell back to. */
31
+ fromFallback: VcSenderFallback;
32
+ }
33
+ /**
34
+ * Resolve the effective sender identity for a `vc.bot.meeting_invited_v1`
35
+ * event. See {@link ResolvedVcSender} for the field contract.
36
+ *
37
+ * Sender resolution order (first non-empty wins):
38
+ * 1. inviter.id.open_id → user_id → union_id
39
+ * 2. empty string + fromFallback='none'
40
+ */
41
+ export declare function resolveVcSender(event: FeishuVcMeetingInvitedEvent): ResolvedVcSender;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * Shared sender-identity resolution for the VC meeting-invited event.
7
+ *
8
+ * Both the raw event handler (for diagnostics / dedup / ownership check)
9
+ * and the synthetic inbound builder (for dispatchToAgent) need a single,
10
+ * deterministic fallback chain. Keeping a dedicated module avoids drift
11
+ * between the two code paths when the event schema changes again.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.resolveVcSender = resolveVcSender;
15
+ /**
16
+ * Trim a possibly-null identifier and treat the empty string as missing.
17
+ *
18
+ * The event schema marks open_id / user_id / union_id as nullable and we
19
+ * have observed tenants returning empty strings in practice, so `||`/`??`
20
+ * alone are not enough.
21
+ */
22
+ function pickId(value) {
23
+ const trimmed = value?.trim();
24
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
25
+ }
26
+ /**
27
+ * Resolve the effective sender identity for a `vc.bot.meeting_invited_v1`
28
+ * event. See {@link ResolvedVcSender} for the field contract.
29
+ *
30
+ * Sender resolution order (first non-empty wins):
31
+ * 1. inviter.id.open_id → user_id → union_id
32
+ * 2. empty string + fromFallback='none'
33
+ */
34
+ function resolveVcSender(event) {
35
+ const inviterId = event.inviter?.id;
36
+ const inviterOpenId = pickId(inviterId?.open_id);
37
+ const inviterUserId = pickId(inviterId?.user_id);
38
+ const inviterUnionId = pickId(inviterId?.union_id);
39
+ let senderId = '';
40
+ let fromFallback = 'none';
41
+ if (inviterOpenId ?? inviterUserId ?? inviterUnionId) {
42
+ senderId = inviterOpenId ?? inviterUserId ?? inviterUnionId ?? '';
43
+ fromFallback = 'inviter';
44
+ }
45
+ return {
46
+ senderId,
47
+ senderOpenId: inviterOpenId,
48
+ senderUserId: inviterUserId,
49
+ senderUnionId: inviterUnionId,
50
+ senderName: pickId(event.inviter?.user_name) ?? undefined,
51
+ fromFallback,
52
+ };
53
+ }
@@ -6,6 +6,7 @@ const lark_client_1 = require("../../core/lark-client.js");
6
6
  const lark_logger_1 = require("../../core/lark-logger.js");
7
7
  const targets_1 = require("../../core/targets.js");
8
8
  const comment_target_1 = require("../../core/comment-target.js");
9
+ const synthetic_target_1 = require("../../core/synthetic-target.js");
9
10
  const deliver_1 = require("./deliver.js");
10
11
  const log = (0, lark_logger_1.larkLogger)('outbound/outbound');
11
12
  /**
@@ -41,6 +42,13 @@ exports.feishuOutbound = {
41
42
  textChunkLimit: 15000,
42
43
  sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
43
44
  log.info(`sendText: target=${to}, textLength=${text.length}`);
45
+ // Synthetic targets (e.g. VC meeting-invited) have no real IM peer —
46
+ // drop the send silently so the agent pipeline stays uniform without
47
+ // producing unsolicited DMs. See core/synthetic-target.ts.
48
+ if ((0, synthetic_target_1.isSyntheticTarget)(to)) {
49
+ log.debug(`sendText: synthetic target ${to}, dropping outbound IM send`);
50
+ return { channel: 'feishu', messageId: '', chatId: to };
51
+ }
44
52
  // Comment thread routing — route replies through Drive comment API
45
53
  if ((0, comment_target_1.isCommentTarget)(to)) {
46
54
  log.info(`sendText: detected comment target, routing through Drive comment API`);
@@ -53,6 +61,11 @@ exports.feishuOutbound = {
53
61
  },
54
62
  sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, replyToId, threadId }) => {
55
63
  log.info(`sendMedia: target=${to}, ` + `hasText=${Boolean(text?.trim())}, mediaUrl=${mediaUrl ?? '(none)'}`);
64
+ // Synthetic targets — drop silently (see sendText for rationale).
65
+ if ((0, synthetic_target_1.isSyntheticTarget)(to)) {
66
+ log.debug(`sendMedia: synthetic target ${to}, dropping outbound IM send`);
67
+ return { channel: 'feishu', messageId: '', chatId: to };
68
+ }
56
69
  // Comment thread routing — send text (with media URL appended) via Drive comment API
57
70
  if ((0, comment_target_1.isCommentTarget)(to)) {
58
71
  log.info(`sendMedia: detected comment target, routing through Drive comment API`);
@@ -85,6 +98,11 @@ exports.feishuOutbound = {
85
98
  };
86
99
  },
87
100
  sendPayload: async ({ cfg, to, payload, mediaLocalRoots, accountId, replyToId, threadId }) => {
101
+ // Synthetic targets — drop silently (see sendText for rationale).
102
+ if ((0, synthetic_target_1.isSyntheticTarget)(to)) {
103
+ log.debug(`sendPayload: synthetic target ${to}, dropping outbound IM send`);
104
+ return { channel: 'feishu', messageId: '', chatId: to };
105
+ }
88
106
  const ctx = resolveFeishuSendContext({ cfg, to, accountId, replyToId, threadId });
89
107
  // --- channelData.feishu: card message support ---
90
108
  const feishuData = payload.channelData?.feishu;
@@ -135,6 +135,69 @@ export interface FeishuBotAddedEvent {
135
135
  ja_jp?: string;
136
136
  };
137
137
  }
138
+ /**
139
+ * Raw event shape for `vc.bot.meeting_invited_v1`.
140
+ *
141
+ * Fired when a user invites the bot to join a meeting.
142
+ *
143
+ * Identity fields (for bot / inviter / host_user) are nested under an
144
+ * `id` object; individual id variants (open_id / user_id / union_id)
145
+ * may be null depending on the tenant configuration.
146
+ */
147
+ export interface FeishuVcMeetingInvitedEvent {
148
+ app_id?: string;
149
+ event_id?: string;
150
+ meeting?: {
151
+ id?: string;
152
+ meeting_no?: string;
153
+ topic?: string;
154
+ host_user?: {
155
+ id?: {
156
+ open_id?: string | null;
157
+ user_id?: string | null;
158
+ union_id?: string | null;
159
+ };
160
+ user_name?: string;
161
+ };
162
+ };
163
+ bot?: {
164
+ id?: {
165
+ open_id?: string | null;
166
+ user_id?: string | null;
167
+ union_id?: string | null;
168
+ };
169
+ user_name?: string;
170
+ };
171
+ inviter?: {
172
+ id?: {
173
+ open_id?: string | null;
174
+ user_id?: string | null;
175
+ union_id?: string | null;
176
+ };
177
+ user_name?: string;
178
+ };
179
+ invite_time?: string;
180
+ }
181
+ /**
182
+ * Internal synthetic event model for VC meeting-invited flows.
183
+ *
184
+ * This preserves the business semantics before the event is converted into a
185
+ * synthetic MessageContext for dispatchToAgent().
186
+ */
187
+ export interface VcMeetingInvitedSyntheticEvent {
188
+ eventType: 'vc.bot.meeting_invited_v1';
189
+ source: 'feishu-vc-event';
190
+ eventId?: string;
191
+ meetingId?: string;
192
+ meetingNo: string;
193
+ topic?: string;
194
+ senderId: string;
195
+ senderOpenId?: string;
196
+ senderUserId?: string;
197
+ senderUnionId?: string;
198
+ senderName?: string;
199
+ inviteTime?: string;
200
+ }
138
201
  /** Metadata describing a media resource in a message (no binary data). */
139
202
  export interface ResourceDescriptor {
140
203
  type: 'image' | 'file' | 'audio' | 'video' | 'sticker';