@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.
- package/package.json +2 -1
- package/skills/feishu-task/SKILL.md +79 -32
- package/src/channel/event-handlers.d.ts +1 -0
- package/src/channel/event-handlers.js +73 -0
- package/src/channel/monitor.js +1 -0
- package/src/core/raw-request.js +26 -2
- package/src/core/synthetic-target.d.ts +33 -0
- package/src/core/synthetic-target.js +40 -0
- package/src/core/targets.js +0 -1
- package/src/core/tool-scopes.d.ts +2 -2
- package/src/core/tool-scopes.js +6 -1
- package/src/messaging/inbound/dispatch.d.ts +2 -0
- package/src/messaging/inbound/dispatch.js +76 -0
- package/src/messaging/inbound/vc-meeting-invited-handler.d.ts +20 -0
- package/src/messaging/inbound/vc-meeting-invited-handler.js +226 -0
- package/src/messaging/inbound/vc-sender.d.ts +41 -0
- package/src/messaging/inbound/vc-sender.js +53 -0
- package/src/messaging/outbound/outbound.js +18 -0
- package/src/messaging/types.d.ts +63 -0
- package/src/tools/oapi/index.js +2 -0
- package/src/tools/oapi/task/attachment.d.ts +18 -0
- package/src/tools/oapi/task/attachment.js +107 -0
- package/src/tools/oapi/task/comment.d.ts +1 -1
- package/src/tools/oapi/task/comment.js +11 -5
- package/src/tools/oapi/task/index.d.ts +2 -0
- package/src/tools/oapi/task/index.js +5 -1
- package/src/tools/oapi/task/section.d.ts +1 -1
- package/src/tools/oapi/task/section.js +15 -7
- package/src/tools/oapi/task/subtask.d.ts +1 -1
- package/src/tools/oapi/task/subtask.js +12 -6
- package/src/tools/oapi/task/task.d.ts +3 -0
- package/src/tools/oapi/task/task.js +174 -9
- package/src/tools/oapi/task/task_agent.d.ts +14 -0
- package/src/tools/oapi/task/task_agent.js +108 -0
- package/src/tools/oapi/task/tasklist.d.ts +1 -1
- 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;
|
package/src/messaging/types.d.ts
CHANGED
|
@@ -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';
|
package/src/tools/oapi/index.js
CHANGED
|
@@ -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;
|