@larksuite/openclaw-lark 2026.4.8 → 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.
Files changed (36) hide show
  1. package/package.json +2 -1
  2. package/skills/feishu-task/SKILL.md +79 -32
  3. package/src/channel/event-handlers.d.ts +1 -0
  4. package/src/channel/event-handlers.js +73 -0
  5. package/src/channel/monitor.js +1 -0
  6. package/src/core/raw-request.js +26 -2
  7. package/src/core/synthetic-target.d.ts +33 -0
  8. package/src/core/synthetic-target.js +40 -0
  9. package/src/core/targets.js +0 -1
  10. package/src/core/tool-scopes.d.ts +2 -2
  11. package/src/core/tool-scopes.js +6 -1
  12. package/src/messaging/inbound/dispatch.d.ts +2 -0
  13. package/src/messaging/inbound/dispatch.js +76 -0
  14. package/src/messaging/inbound/vc-meeting-invited-handler.d.ts +20 -0
  15. package/src/messaging/inbound/vc-meeting-invited-handler.js +226 -0
  16. package/src/messaging/inbound/vc-sender.d.ts +41 -0
  17. package/src/messaging/inbound/vc-sender.js +53 -0
  18. package/src/messaging/outbound/outbound.js +18 -0
  19. package/src/messaging/types.d.ts +63 -0
  20. package/src/tools/oapi/index.js +2 -0
  21. package/src/tools/oapi/task/attachment.d.ts +18 -0
  22. package/src/tools/oapi/task/attachment.js +107 -0
  23. package/src/tools/oapi/task/comment.d.ts +1 -1
  24. package/src/tools/oapi/task/comment.js +11 -5
  25. package/src/tools/oapi/task/index.d.ts +2 -0
  26. package/src/tools/oapi/task/index.js +5 -1
  27. package/src/tools/oapi/task/section.d.ts +1 -1
  28. package/src/tools/oapi/task/section.js +15 -7
  29. package/src/tools/oapi/task/subtask.d.ts +1 -1
  30. package/src/tools/oapi/task/subtask.js +12 -6
  31. package/src/tools/oapi/task/task.d.ts +3 -0
  32. package/src/tools/oapi/task/task.js +174 -9
  33. package/src/tools/oapi/task/task_agent.d.ts +14 -0
  34. package/src/tools/oapi/task/task_agent.js +108 -0
  35. package/src/tools/oapi/task/tasklist.d.ts +1 -1
  36. package/src/tools/oapi/task/tasklist.js +30 -13
@@ -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';
@@ -39,9 +39,11 @@ function registerOapiTools(api) {
39
39
  // Task tools
40
40
  (0, index_3.registerFeishuTaskTaskTool)(api);
41
41
  (0, index_3.registerFeishuTaskTasklistTool)(api);
42
+ (0, index_3.registerFeishuTaskAttachmentTool)(api);
42
43
  (0, index_3.registerFeishuTaskSectionTool)(api);
43
44
  (0, index_3.registerFeishuTaskCommentTool)(api);
44
45
  (0, index_3.registerFeishuTaskSubtaskTool)(api);
46
+ (0, index_3.registerFeishuTaskAgentTool)(api);
45
47
  // Bitable tools
46
48
  (0, index_4.registerFeishuBitableAppTool)(api);
47
49
  (0, index_4.registerFeishuBitableAppTableTool)(api);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * feishu_task_attachment tool -- Manage task attachments.
6
+ *
7
+ * Actions:
8
+ * - upload: Upload task attachment (tenant identity)
9
+ */
10
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
11
+ export interface FeishuTaskAttachmentParams {
12
+ action: 'upload';
13
+ resource_type?: 'task' | 'task_delivery';
14
+ resource_id: string;
15
+ file: string;
16
+ name?: string;
17
+ }
18
+ export declare function registerFeishuTaskAttachmentTool(api: OpenClawPluginApi): void;