@larksuite/openclaw-lark 2026.4.1 → 2026.4.7
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/index.js +25 -6
- package/package.json +1 -1
- package/src/card/builder.d.ts +29 -7
- package/src/card/builder.js +241 -29
- package/src/card/reasoning-utils.d.ts +14 -0
- package/src/card/reasoning-utils.js +64 -0
- package/src/card/reply-dispatcher-types.d.ts +12 -15
- package/src/card/reply-dispatcher-types.js +1 -0
- package/src/card/reply-dispatcher.js +63 -11
- package/src/card/streaming-card-controller.d.ts +15 -0
- package/src/card/streaming-card-controller.js +188 -65
- package/src/card/tool-use-config.d.ts +26 -0
- package/src/card/tool-use-config.js +76 -0
- package/src/card/tool-use-display.d.ts +29 -0
- package/src/card/tool-use-display.js +438 -0
- package/src/card/tool-use-trace-store.d.ts +51 -0
- package/src/card/tool-use-trace-store.js +271 -0
- package/src/channel/event-handlers.d.ts +1 -0
- package/src/channel/event-handlers.js +51 -0
- package/src/channel/monitor.js +2 -0
- package/src/core/comment-target.d.ts +65 -0
- package/src/core/comment-target.js +100 -0
- package/src/core/config-schema.d.ts +9 -0
- package/src/core/config-schema.js +5 -0
- package/src/core/tool-scopes.d.ts +1 -1
- package/src/core/tool-scopes.js +7 -0
- package/src/messaging/inbound/comment-context.d.ts +82 -0
- package/src/messaging/inbound/comment-context.js +353 -0
- package/src/messaging/inbound/comment-handler.d.ts +30 -0
- package/src/messaging/inbound/comment-handler.js +269 -0
- package/src/messaging/inbound/dispatch-commands.js +23 -2
- package/src/messaging/inbound/dispatch-context.js +19 -7
- package/src/messaging/inbound/dispatch.js +87 -8
- package/src/messaging/outbound/deliver.d.ts +29 -0
- package/src/messaging/outbound/deliver.js +94 -0
- package/src/messaging/outbound/outbound.js +19 -0
- package/src/messaging/types.d.ts +63 -0
- package/src/tools/oapi/drive/doc-comments.js +93 -24
- package/src/tools/oapi/index.js +1 -0
- package/src/tools/oapi/task/index.d.ts +1 -0
- package/src/tools/oapi/task/index.js +3 -1
- package/src/tools/oapi/task/section.d.ts +17 -0
- package/src/tools/oapi/task/section.js +285 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Drive comment event handler for the Lark/Feishu channel plugin.
|
|
7
|
+
*
|
|
8
|
+
* Handles `drive.notice.comment_add_v1` events by resolving comment
|
|
9
|
+
* context, enforcing access policies, building a synthetic
|
|
10
|
+
* {@link MessageContext}, and dispatching to the agent.
|
|
11
|
+
*
|
|
12
|
+
* Modeled after the reaction handler pattern (reaction-handler.ts):
|
|
13
|
+
* bypasses the 7-stage message pipeline and dispatches directly.
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.handleFeishuCommentEvent = handleFeishuCommentEvent;
|
|
50
|
+
const crypto = __importStar(require("node:crypto"));
|
|
51
|
+
const reply_history_1 = require("openclaw/plugin-sdk/reply-history");
|
|
52
|
+
const accounts_1 = require("../../core/accounts.js");
|
|
53
|
+
const comment_target_1 = require("../../core/comment-target.js");
|
|
54
|
+
const lark_client_1 = require("../../core/lark-client.js");
|
|
55
|
+
const lark_logger_1 = require("../../core/lark-logger.js");
|
|
56
|
+
const user_name_cache_1 = require("./user-name-cache.js");
|
|
57
|
+
const dispatch_1 = require("./dispatch.js");
|
|
58
|
+
const gate_1 = require("./gate.js");
|
|
59
|
+
const policy_1 = require("./policy.js");
|
|
60
|
+
const gate_effects_1 = require("./gate-effects.js");
|
|
61
|
+
const comment_context_1 = require("./comment-context.js");
|
|
62
|
+
const logger = (0, lark_logger_1.larkLogger)('inbound/comment-handler');
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Public API
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Handle a Drive comment event.
|
|
68
|
+
*
|
|
69
|
+
* Resolves the comment context, checks access policies, builds a
|
|
70
|
+
* synthetic MessageContext, and dispatches to the agent.
|
|
71
|
+
*/
|
|
72
|
+
async function handleFeishuCommentEvent(params) {
|
|
73
|
+
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
|
|
74
|
+
const log = runtime?.log ?? ((...args) => logger.info(args.map(String).join(' ')));
|
|
75
|
+
const error = runtime?.error ?? ((...args) => logger.error(args.map(String).join(' ')));
|
|
76
|
+
// The parser has already normalized all fields from notice_meta into
|
|
77
|
+
// canonical top-level fields, so we just read the canonical shape.
|
|
78
|
+
const senderOpenId = event.user_id?.open_id ?? '';
|
|
79
|
+
const senderUserId = event.user_id?.user_id;
|
|
80
|
+
const senderUnionId = event.user_id?.union_id;
|
|
81
|
+
const fileToken = event.file_token ?? '';
|
|
82
|
+
const fileType = (event.file_type ?? 'docx');
|
|
83
|
+
const commentId = event.comment_id ?? '';
|
|
84
|
+
if (!senderOpenId || !fileToken || !commentId) {
|
|
85
|
+
log(`feishu[${accountId}]: comment event missing required fields, skipping`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// ---- Self-comment filter ----
|
|
89
|
+
// Ignore comments/replies authored by the bot itself.
|
|
90
|
+
if (senderOpenId === botOpenId) {
|
|
91
|
+
log(`feishu[${accountId}]: ignoring self-authored comment on ${fileToken}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// ---- Account resolution ----
|
|
95
|
+
const account = (0, accounts_1.getLarkAccount)(cfg, accountId);
|
|
96
|
+
const accountFeishuCfg = account.config;
|
|
97
|
+
const accountScopedCfg = {
|
|
98
|
+
...cfg,
|
|
99
|
+
channels: { ...cfg.channels, feishu: accountFeishuCfg },
|
|
100
|
+
};
|
|
101
|
+
// ---- Access policy enforcement (DM-style) ----
|
|
102
|
+
// Comment events are user-to-bot interactions outside of IM, so we apply
|
|
103
|
+
// the same dmPolicy (open/allowlist/pairing) as DM messages.
|
|
104
|
+
// This mirrors the logic in gate.ts:checkDmGate.
|
|
105
|
+
const dmPolicy = accountFeishuCfg?.dmPolicy ?? 'pairing';
|
|
106
|
+
if (dmPolicy === 'disabled') {
|
|
107
|
+
log(`feishu[${accountId}]: comment event rejected (dmPolicy=disabled)`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (dmPolicy !== 'open') {
|
|
111
|
+
// Read both config allowlist and pairing store (matches gate.ts behavior)
|
|
112
|
+
const configAllowFrom = accountFeishuCfg?.allowFrom ?? [];
|
|
113
|
+
const storeAllowFrom = await (0, gate_1.readFeishuAllowFromStore)(account.accountId).catch(() => []);
|
|
114
|
+
const combinedAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
115
|
+
const match = (0, policy_1.resolveFeishuAllowlistMatch)({
|
|
116
|
+
allowFrom: combinedAllowFrom,
|
|
117
|
+
senderId: senderOpenId,
|
|
118
|
+
});
|
|
119
|
+
// Also check user_id if available
|
|
120
|
+
const userIdMatch = senderUserId
|
|
121
|
+
? (0, policy_1.resolveFeishuAllowlistMatch)({
|
|
122
|
+
allowFrom: combinedAllowFrom,
|
|
123
|
+
senderId: senderUserId,
|
|
124
|
+
})
|
|
125
|
+
: { allowed: false };
|
|
126
|
+
if (!match.allowed && !userIdMatch.allowed) {
|
|
127
|
+
if (dmPolicy === 'pairing') {
|
|
128
|
+
// Create pairing request and send challenge (mirrors gate.ts:334).
|
|
129
|
+
// Prefer replying in the comment thread so the user sees the
|
|
130
|
+
// challenge in context; fall back to DM if comment reply fails.
|
|
131
|
+
log(`feishu[${accountId}]: comment sender not paired, creating pairing request`);
|
|
132
|
+
try {
|
|
133
|
+
const core = lark_client_1.LarkClient.runtime;
|
|
134
|
+
const { code } = await core.channel.pairing.upsertPairingRequest({
|
|
135
|
+
channel: 'feishu',
|
|
136
|
+
id: senderOpenId,
|
|
137
|
+
accountId: account.accountId,
|
|
138
|
+
});
|
|
139
|
+
const pairingText = core.channel.pairing.buildPairingReply({
|
|
140
|
+
channel: 'feishu',
|
|
141
|
+
idLine: senderOpenId,
|
|
142
|
+
code,
|
|
143
|
+
});
|
|
144
|
+
// Try comment thread reply first
|
|
145
|
+
let sentInThread = false;
|
|
146
|
+
try {
|
|
147
|
+
const client = lark_client_1.LarkClient.fromCfg(accountScopedCfg, accountId);
|
|
148
|
+
await client.sdk.request({
|
|
149
|
+
method: 'POST',
|
|
150
|
+
url: `/open-apis/drive/v1/files/${fileToken}/comments/${commentId}/replies`,
|
|
151
|
+
params: { file_type: fileType, user_id_type: 'open_id' },
|
|
152
|
+
data: {
|
|
153
|
+
content: {
|
|
154
|
+
elements: [{ type: 'text_run', text_run: { text: pairingText } }],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
sentInThread = true;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Comment reply failed — fall through to DM
|
|
162
|
+
}
|
|
163
|
+
// Fallback: send to DM
|
|
164
|
+
if (!sentInThread) {
|
|
165
|
+
await (0, gate_effects_1.sendPairingReply)({
|
|
166
|
+
senderId: senderOpenId,
|
|
167
|
+
chatId: senderOpenId,
|
|
168
|
+
accountId: account.accountId,
|
|
169
|
+
accountScopedCfg,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (pairingErr) {
|
|
174
|
+
log(`feishu[${accountId}]: pairing request failed: ${String(pairingErr)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
log(`feishu[${accountId}]: comment event rejected (dmPolicy=${dmPolicy}, not in allowlist)`);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ---- Resolve comment context ----
|
|
184
|
+
const turn = await (0, comment_context_1.resolveDriveCommentEventTurn)({ cfg, event, accountId });
|
|
185
|
+
if (!turn) {
|
|
186
|
+
log(`feishu[${accountId}]: failed to resolve comment context, skipping`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// ---- Mention filter ----
|
|
190
|
+
// The event-level is_mentioned flag is not stable for Drive comments.
|
|
191
|
+
// Fall back to the resolved comment text, where @mentions are normalized
|
|
192
|
+
// into "@<open_id>" by extractElementText().
|
|
193
|
+
const eventMentioned = event.notice_meta?.is_mentioned ?? event.is_mention;
|
|
194
|
+
const textMentioned = Boolean(botOpenId) && Boolean(turn.commentText?.includes(`@${botOpenId}`));
|
|
195
|
+
if (eventMentioned !== true && !textMentioned) {
|
|
196
|
+
log(`feishu[${accountId}]: comment event not mentioning bot, skipping` +
|
|
197
|
+
` (eventFlag=${String(eventMentioned)}, textMention=${textMentioned})`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// ---- Build synthetic MessageContext ----
|
|
201
|
+
const commentTarget = (0, comment_target_1.buildFeishuCommentTarget)({
|
|
202
|
+
// Whole-document comment threads do not support in-thread replies
|
|
203
|
+
// through the replies API. For any event in that thread, fall back
|
|
204
|
+
// to creating a new whole-document comment instead.
|
|
205
|
+
deliveryMode: turn.isWholeComment ? 'create_whole' : 'reply',
|
|
206
|
+
fileType,
|
|
207
|
+
fileToken,
|
|
208
|
+
commentId,
|
|
209
|
+
});
|
|
210
|
+
const syntheticMessageId = `comment:${commentId}:${event.reply_id ?? 'root'}:${crypto.randomUUID()}`;
|
|
211
|
+
const syntheticText = turn.prompt;
|
|
212
|
+
let ctx = {
|
|
213
|
+
chatId: commentTarget, // Use comment target as the "chat" identifier
|
|
214
|
+
messageId: syntheticMessageId,
|
|
215
|
+
senderId: senderOpenId,
|
|
216
|
+
chatType: 'p2p', // Comment events are treated as direct interactions
|
|
217
|
+
content: syntheticText,
|
|
218
|
+
contentType: 'text',
|
|
219
|
+
resources: [],
|
|
220
|
+
mentions: [],
|
|
221
|
+
mentionAll: false,
|
|
222
|
+
rawMessage: {
|
|
223
|
+
message_id: syntheticMessageId,
|
|
224
|
+
chat_id: commentTarget,
|
|
225
|
+
chat_type: 'p2p',
|
|
226
|
+
message_type: 'text',
|
|
227
|
+
content: JSON.stringify({ text: syntheticText }),
|
|
228
|
+
create_time: event.action_time ?? String(Date.now()),
|
|
229
|
+
},
|
|
230
|
+
rawSender: {
|
|
231
|
+
sender_id: {
|
|
232
|
+
open_id: senderOpenId,
|
|
233
|
+
user_id: senderUserId,
|
|
234
|
+
union_id: senderUnionId,
|
|
235
|
+
},
|
|
236
|
+
sender_type: 'user',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
// ---- Sender name resolution ----
|
|
240
|
+
const senderResult = await (0, user_name_cache_1.resolveUserName)({ account, openId: senderOpenId, log });
|
|
241
|
+
if (senderResult.name) {
|
|
242
|
+
ctx = { ...ctx, senderName: senderResult.name };
|
|
243
|
+
}
|
|
244
|
+
log(`feishu[${accountId}]: comment event on ${commentId}` +
|
|
245
|
+
`${event.reply_id ? ` (reply ${event.reply_id})` : ''}, dispatching to agent`);
|
|
246
|
+
logger.info(`comment event on ${commentId}` +
|
|
247
|
+
`${event.reply_id ? ` (reply ${event.reply_id})` : ''}`);
|
|
248
|
+
const historyLimit = Math.max(0, accountFeishuCfg?.historyLimit ?? accountScopedCfg.messages?.groupChat?.historyLimit ?? reply_history_1.DEFAULT_GROUP_HISTORY_LIMIT);
|
|
249
|
+
// ---- Dispatch to agent ----
|
|
250
|
+
try {
|
|
251
|
+
await (0, dispatch_1.dispatchToAgent)({
|
|
252
|
+
ctx,
|
|
253
|
+
permissionError: undefined,
|
|
254
|
+
mediaPayload: {},
|
|
255
|
+
quotedContent: undefined,
|
|
256
|
+
account,
|
|
257
|
+
accountScopedCfg,
|
|
258
|
+
runtime,
|
|
259
|
+
chatHistories,
|
|
260
|
+
historyLimit,
|
|
261
|
+
replyToMessageId: undefined, // No IM message to reply to
|
|
262
|
+
commandAuthorized: false,
|
|
263
|
+
skipTyping: true, // No IM typing indicator for comment events
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
error(`feishu[${accountId}]: error dispatching comment event: ${String(err)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -14,6 +14,7 @@ exports.dispatchSystemCommand = dispatchSystemCommand;
|
|
|
14
14
|
const lark_logger_1 = require("../../core/lark-logger.js");
|
|
15
15
|
const lark_ticket_1 = require("../../core/lark-ticket.js");
|
|
16
16
|
const reply_dispatcher_1 = require("../../card/reply-dispatcher.js");
|
|
17
|
+
const tool_use_trace_store_1 = require("../../card/tool-use-trace-store.js");
|
|
17
18
|
const send_1 = require("../outbound/send.js");
|
|
18
19
|
const dispatch_builders_1 = require("./dispatch-builders.js");
|
|
19
20
|
const log = (0, lark_logger_1.larkLogger)('inbound/dispatch-commands');
|
|
@@ -44,15 +45,22 @@ async function dispatchPermissionNotification(dc, permissionError, replyToMessag
|
|
|
44
45
|
messageSid: `${dc.ctx.messageId}:permission-error`,
|
|
45
46
|
wasMentioned: false,
|
|
46
47
|
});
|
|
48
|
+
(0, tool_use_trace_store_1.startToolUseTraceRun)(dc.threadSessionKey ?? dc.route.sessionKey);
|
|
47
49
|
const { dispatcher: permDispatcher, replyOptions: permReplyOptions, markDispatchIdle: markPermIdle, markFullyComplete: markPermComplete, } = (0, reply_dispatcher_1.createFeishuReplyDispatcher)({
|
|
48
50
|
cfg: dc.accountScopedCfg,
|
|
49
51
|
agentId: dc.route.agentId,
|
|
50
|
-
sessionKey: dc.threadSessionKey ?? dc.route.sessionKey,
|
|
51
52
|
chatId: dc.ctx.chatId,
|
|
53
|
+
sessionKey: dc.threadSessionKey ?? dc.route.sessionKey,
|
|
52
54
|
replyToMessageId: replyToMessageId ?? dc.ctx.messageId,
|
|
53
55
|
accountId: dc.account.accountId,
|
|
54
56
|
chatType: dc.ctx.chatType,
|
|
55
57
|
replyInThread: dc.isThread,
|
|
58
|
+
toolUseDisplay: {
|
|
59
|
+
mode: 'off',
|
|
60
|
+
showToolUse: false,
|
|
61
|
+
showToolResultDetails: false,
|
|
62
|
+
showFullPaths: false,
|
|
63
|
+
},
|
|
56
64
|
});
|
|
57
65
|
dc.log(`feishu[${dc.account.accountId}]: dispatching permission error notification to agent`);
|
|
58
66
|
await dc.core.channel.reply.dispatchReplyFromConfig({
|
|
@@ -74,13 +82,17 @@ async function dispatchPermissionNotification(dc, permissionError, replyToMessag
|
|
|
74
82
|
*/
|
|
75
83
|
async function dispatchSystemCommand(dc, ctxPayload, replyToMessageId) {
|
|
76
84
|
let delivered = false;
|
|
85
|
+
const suppressToolDetails = isLifecycleSessionCommand(dc.ctx.content);
|
|
77
86
|
dc.log(`feishu[${dc.account.accountId}]: detected system command, using plain-text dispatch`);
|
|
78
87
|
log.info('system command detected, plain-text dispatch');
|
|
79
88
|
await dc.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
80
89
|
ctx: ctxPayload,
|
|
81
90
|
cfg: dc.accountScopedCfg,
|
|
82
91
|
dispatcherOptions: {
|
|
83
|
-
deliver: async (payload) => {
|
|
92
|
+
deliver: async (payload, info) => {
|
|
93
|
+
if (suppressToolDetails && info.kind === 'tool') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
84
96
|
const text = payload.text?.trim() ?? '';
|
|
85
97
|
if (!text)
|
|
86
98
|
return;
|
|
@@ -108,3 +120,12 @@ async function dispatchSystemCommand(dc, ctxPayload, replyToMessageId) {
|
|
|
108
120
|
dc.log(`feishu[${dc.account.accountId}]: system command dispatched (delivered=${delivered})`);
|
|
109
121
|
log.info(`system command dispatched (delivered=${delivered}, elapsed=${(0, lark_ticket_1.ticketElapsed)()}ms)`);
|
|
110
122
|
}
|
|
123
|
+
function isLifecycleSessionCommand(text) {
|
|
124
|
+
if (!text)
|
|
125
|
+
return false;
|
|
126
|
+
const match = text.trim().match(/^\/([^\s@]+)/);
|
|
127
|
+
if (!match)
|
|
128
|
+
return false;
|
|
129
|
+
const command = match[1]?.toLowerCase();
|
|
130
|
+
return command === 'new' || command === 'reset';
|
|
131
|
+
}
|
|
@@ -17,6 +17,7 @@ const routing_1 = require("openclaw/plugin-sdk/routing");
|
|
|
17
17
|
const lark_client_1 = require("../../core/lark-client.js");
|
|
18
18
|
const lark_logger_1 = require("../../core/lark-logger.js");
|
|
19
19
|
const chat_info_cache_1 = require("../../core/chat-info-cache.js");
|
|
20
|
+
const comment_target_1 = require("../../core/comment-target.js");
|
|
20
21
|
const log = (0, lark_logger_1.larkLogger)('inbound/dispatch-context');
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// RuntimeEnv fallback
|
|
@@ -46,26 +47,37 @@ function buildDispatchContext(params) {
|
|
|
46
47
|
const runtime = ensureRuntime(params.runtime);
|
|
47
48
|
const log = runtime.log;
|
|
48
49
|
const error = runtime.error;
|
|
49
|
-
const
|
|
50
|
+
const isComment = (0, comment_target_1.isCommentTarget)(ctx.chatId);
|
|
51
|
+
const isGroup = !isComment && ctx.chatType === 'group';
|
|
50
52
|
const isThread = isGroup && Boolean(ctx.threadId);
|
|
51
53
|
const core = lark_client_1.LarkClient.runtime;
|
|
52
54
|
const feishuFrom = `feishu:${ctx.senderId}`;
|
|
53
|
-
|
|
55
|
+
// Comment targets use the comment target string directly as the "To"
|
|
56
|
+
// so the outbound routing layer can detect it and route through Drive API.
|
|
57
|
+
const feishuTo = isComment
|
|
58
|
+
? ctx.chatId
|
|
59
|
+
: isGroup
|
|
60
|
+
? `chat:${ctx.chatId}`
|
|
61
|
+
: `user:${ctx.senderId}`;
|
|
54
62
|
const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderId}` : ctx.senderId;
|
|
55
63
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(accountScopedCfg);
|
|
56
64
|
// ---- Route resolution ----
|
|
65
|
+
// Comment targets use the comment target as the peer ID so each
|
|
66
|
+
// comment thread gets its own session key.
|
|
57
67
|
const route = core.channel.routing.resolveAgentRoute({
|
|
58
68
|
cfg: accountScopedCfg,
|
|
59
69
|
channel: 'feishu',
|
|
60
70
|
accountId: account.accountId,
|
|
61
|
-
peer:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
peer: isComment
|
|
72
|
+
? { kind: 'direct', id: ctx.chatId }
|
|
73
|
+
: {
|
|
74
|
+
kind: isGroup ? 'group' : 'direct',
|
|
75
|
+
id: isGroup ? ctx.chatId : ctx.senderId,
|
|
76
|
+
},
|
|
65
77
|
});
|
|
66
78
|
// ---- System event ----
|
|
67
79
|
const sender = ctx.senderName ? `${ctx.senderName} (${ctx.senderId})` : ctx.senderId;
|
|
68
|
-
const location = isGroup ? `group ${ctx.chatId}` : 'DM';
|
|
80
|
+
const location = isComment ? `comment ${ctx.chatId}` : isGroup ? `group ${ctx.chatId}` : 'DM';
|
|
69
81
|
const tags = [];
|
|
70
82
|
tags.push(`msg:${ctx.messageId}`);
|
|
71
83
|
if (ctx.parentId)
|
|
@@ -21,9 +21,13 @@ const lark_logger_1 = require("../../core/lark-logger.js");
|
|
|
21
21
|
const lark_ticket_1 = require("../../core/lark-ticket.js");
|
|
22
22
|
const reply_dispatcher_1 = require("../../card/reply-dispatcher.js");
|
|
23
23
|
const chat_queue_1 = require("../../channel/chat-queue.js");
|
|
24
|
+
const tool_use_config_1 = require("../../card/tool-use-config.js");
|
|
25
|
+
const tool_use_trace_store_1 = require("../../card/tool-use-trace-store.js");
|
|
24
26
|
const abort_detect_1 = require("../../channel/abort-detect.js");
|
|
25
27
|
const chat_info_cache_1 = require("../../core/chat-info-cache.js");
|
|
28
|
+
const comment_target_1 = require("../../core/comment-target.js");
|
|
26
29
|
const targets_1 = require("../../core/targets.js");
|
|
30
|
+
const deliver_1 = require("../outbound/deliver.js");
|
|
27
31
|
const doctor_1 = require("../../commands/doctor.js");
|
|
28
32
|
const auth_1 = require("../../commands/auth.js");
|
|
29
33
|
const index_1 = require("../../commands/index.js");
|
|
@@ -45,7 +49,58 @@ const log = (0, lark_logger_1.larkLogger)('inbound/dispatch');
|
|
|
45
49
|
* system-command path — command handlers don't consume history context,
|
|
46
50
|
* so the entries should be preserved for the next normal message.
|
|
47
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Dispatch a comment-target message via the buffered block dispatcher.
|
|
54
|
+
*
|
|
55
|
+
* Comment targets cannot use the streaming card flow (IM APIs don't
|
|
56
|
+
* understand comment:... targets). Instead we use the SDK's buffered
|
|
57
|
+
* block dispatcher with a deliver callback that sends via the Drive
|
|
58
|
+
* comment reply API.
|
|
59
|
+
*/
|
|
60
|
+
async function dispatchCommentMessage(dc, ctxPayload, skillFilter) {
|
|
61
|
+
const effectiveSessionKey = dc.threadSessionKey ?? dc.route.sessionKey;
|
|
62
|
+
dc.log(`feishu[${dc.account.accountId}]: dispatching comment reply (session=${effectiveSessionKey})`);
|
|
63
|
+
log.info(`dispatching comment reply (session=${effectiveSessionKey})`);
|
|
64
|
+
let delivered = false;
|
|
65
|
+
await dc.core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
66
|
+
ctx: ctxPayload,
|
|
67
|
+
cfg: dc.accountScopedCfg,
|
|
68
|
+
dispatcherOptions: {
|
|
69
|
+
deliver: async (payload) => {
|
|
70
|
+
const text = payload.text?.trim() ?? '';
|
|
71
|
+
if (!text || text === 'NO_REPLY')
|
|
72
|
+
return;
|
|
73
|
+
await (0, deliver_1.sendCommentReplyLark)({
|
|
74
|
+
cfg: dc.accountScopedCfg,
|
|
75
|
+
to: dc.ctx.chatId,
|
|
76
|
+
text,
|
|
77
|
+
accountId: dc.account.accountId,
|
|
78
|
+
});
|
|
79
|
+
delivered = true;
|
|
80
|
+
},
|
|
81
|
+
onSkip: (_payload, info) => {
|
|
82
|
+
if (info.reason !== 'silent') {
|
|
83
|
+
dc.log(`feishu[${dc.account.accountId}]: comment reply skipped (reason=${info.reason})`);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
onError: (err, info) => {
|
|
87
|
+
dc.error(`feishu[${dc.account.accountId}]: comment ${info.kind} reply failed: ${String(err)}`);
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
replyOptions: {
|
|
91
|
+
...(skillFilter ? { skillFilter } : {}),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
dc.log(`feishu[${dc.account.accountId}]: comment dispatch complete (delivered=${delivered})`);
|
|
95
|
+
log.info(`comment dispatch complete (delivered=${delivered}, elapsed=${(0, lark_ticket_1.ticketElapsed)()}ms)`);
|
|
96
|
+
}
|
|
48
97
|
async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey, historyLimit, replyToMessageId, skillFilter, skipTyping) {
|
|
98
|
+
// Comment targets bypass the streaming card / IM flow entirely —
|
|
99
|
+
// route through the Drive comment reply API.
|
|
100
|
+
if ((0, comment_target_1.isCommentTarget)(dc.ctx.chatId)) {
|
|
101
|
+
await dispatchCommentMessage(dc, ctxPayload, skillFilter);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
49
104
|
// Abort messages should never create streaming cards — dispatch via the
|
|
50
105
|
// plain-text system-command path so the SDK's abort handler can reply
|
|
51
106
|
// without touching CardKit.
|
|
@@ -55,16 +110,31 @@ async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey,
|
|
|
55
110
|
await (0, dispatch_commands_1.dispatchSystemCommand)(dc, ctxPayload, replyToMessageId);
|
|
56
111
|
return;
|
|
57
112
|
}
|
|
113
|
+
const effectiveSessionKey = dc.threadSessionKey ?? dc.route.sessionKey;
|
|
114
|
+
const toolUseDisplay = (0, tool_use_config_1.resolveToolUseDisplayConfig)({
|
|
115
|
+
cfg: dc.accountScopedCfg,
|
|
116
|
+
feishuCfg: dc.account.config,
|
|
117
|
+
agentId: dc.route.agentId,
|
|
118
|
+
sessionKey: effectiveSessionKey,
|
|
119
|
+
body: dc.ctx.content,
|
|
120
|
+
});
|
|
121
|
+
if (toolUseDisplay.showToolUse) {
|
|
122
|
+
(0, tool_use_trace_store_1.startToolUseTraceRun)(effectiveSessionKey);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
(0, tool_use_trace_store_1.clearToolUseTraceRun)(effectiveSessionKey);
|
|
126
|
+
}
|
|
58
127
|
const { dispatcher, replyOptions, markDispatchIdle, markFullyComplete, abortCard } = (0, reply_dispatcher_1.createFeishuReplyDispatcher)({
|
|
59
128
|
cfg: dc.accountScopedCfg,
|
|
60
129
|
agentId: dc.route.agentId,
|
|
61
|
-
sessionKey: dc.threadSessionKey ?? dc.route.sessionKey,
|
|
62
130
|
chatId: dc.ctx.chatId,
|
|
131
|
+
sessionKey: effectiveSessionKey,
|
|
63
132
|
replyToMessageId: replyToMessageId ?? dc.ctx.messageId,
|
|
64
133
|
accountId: dc.account.accountId,
|
|
65
134
|
chatType: dc.ctx.chatType,
|
|
66
135
|
skipTyping,
|
|
67
136
|
replyInThread: dc.isThread,
|
|
137
|
+
toolUseDisplay,
|
|
68
138
|
});
|
|
69
139
|
// Create an AbortController so the abort fast-path can cancel the
|
|
70
140
|
// underlying LLM request (not just the streaming card UI).
|
|
@@ -73,7 +143,6 @@ async function dispatchNormalMessage(dc, ctxPayload, chatHistories, historyKey,
|
|
|
73
143
|
// terminate the streaming card before this task completes.
|
|
74
144
|
const queueKey = (0, chat_queue_1.buildQueueKey)(dc.account.accountId, dc.ctx.chatId, dc.ctx.threadId);
|
|
75
145
|
(0, chat_queue_1.registerActiveDispatcher)(queueKey, { abortCard, abortController });
|
|
76
|
-
const effectiveSessionKey = dc.threadSessionKey ?? dc.route.sessionKey;
|
|
77
146
|
dc.log(`feishu[${dc.account.accountId}]: dispatching to agent (session=${effectiveSessionKey})`);
|
|
78
147
|
log.info(`dispatching to agent (session=${effectiveSessionKey})`);
|
|
79
148
|
try {
|
|
@@ -148,7 +217,10 @@ async function dispatchToAgent(params) {
|
|
|
148
217
|
const messageBody = (0, dispatch_builders_1.buildMessageBody)(params.ctx, params.quotedContent);
|
|
149
218
|
// 3. Permission-error notification (optional side-effect).
|
|
150
219
|
// Isolated so a failure here does not block the main message dispatch.
|
|
151
|
-
|
|
220
|
+
// Skipped for comment targets: the streaming card dispatcher inside
|
|
221
|
+
// dispatchPermissionNotification sends via IM APIs which don't
|
|
222
|
+
// understand comment:... targets.
|
|
223
|
+
if (params.permissionError && !(0, comment_target_1.isCommentTarget)(dc.ctx.chatId)) {
|
|
152
224
|
try {
|
|
153
225
|
await (0, dispatch_commands_1.dispatchPermissionNotification)(dc, params.permissionError, params.replyToMessageId);
|
|
154
226
|
}
|
|
@@ -213,11 +285,15 @@ async function dispatchToAgent(params) {
|
|
|
213
285
|
// Must run BEFORE the SDK command check — the SDK does not recognise
|
|
214
286
|
// plugin-registered commands via isControlCommandMessage, so
|
|
215
287
|
// /feishu_* falls through to the AI agent otherwise.
|
|
288
|
+
// Skipped for comment targets: comment text won't match /feishu_*
|
|
289
|
+
// patterns in practice, and sendCardFeishu/sendMessageFeishu can't
|
|
290
|
+
// deliver to comment:... targets.
|
|
216
291
|
const contentTrimmed = (params.ctx.content ?? '').trim();
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
const
|
|
292
|
+
const isCommentFlow = (0, comment_target_1.isCommentTarget)(dc.ctx.chatId);
|
|
293
|
+
const isDoctorCommand = !isCommentFlow && /^\/feishu[_ ]doctor\s*$/i.test(contentTrimmed);
|
|
294
|
+
const isAuthCommand = !isCommentFlow && /^\/feishu[_ ](?:auth|onboarding)\s*$/i.test(contentTrimmed);
|
|
295
|
+
const isStartCommand = !isCommentFlow && /^\/feishu[_ ]start\s*$/i.test(contentTrimmed);
|
|
296
|
+
const isHelpCommand = !isCommentFlow && /^\/feishu(?:[_ ]help)?\s*$/i.test(contentTrimmed);
|
|
221
297
|
const i18nCommandName = isDoctorCommand
|
|
222
298
|
? 'doctor'
|
|
223
299
|
: isAuthCommand
|
|
@@ -269,7 +345,10 @@ async function dispatchToAgent(params) {
|
|
|
269
345
|
return;
|
|
270
346
|
}
|
|
271
347
|
// 8. Dispatch: system command vs. normal message
|
|
272
|
-
|
|
348
|
+
// Comment targets always go to normal dispatch — system command
|
|
349
|
+
// delivery uses sendMessageFeishu which can't reach comment threads.
|
|
350
|
+
const isCommand = !isCommentFlow &&
|
|
351
|
+
dc.core.channel.commands.isControlCommandMessage(params.ctx.content, params.accountScopedCfg);
|
|
273
352
|
// Resolve per-group skill filter (per-group > default "*")
|
|
274
353
|
const skillFilter = dc.isGroup ? (params.groupConfig?.skills ?? params.defaultGroupConfig?.skills) : undefined;
|
|
275
354
|
if (isCommand) {
|
|
@@ -153,3 +153,32 @@ export interface SendMediaLarkParams {
|
|
|
153
153
|
* ```
|
|
154
154
|
*/
|
|
155
155
|
export declare function sendMediaLark(params: SendMediaLarkParams): Promise<FeishuSendResult>;
|
|
156
|
+
/**
|
|
157
|
+
* Parameters for sending a reply to a Drive comment thread.
|
|
158
|
+
*/
|
|
159
|
+
export interface SendCommentReplyLarkParams {
|
|
160
|
+
/** Plugin configuration. */
|
|
161
|
+
cfg: ClawdbotConfig;
|
|
162
|
+
/**
|
|
163
|
+
* Target in comment format: `comment:<fileType>:<fileToken>:<commentId>`.
|
|
164
|
+
* Parsed via `parseFeishuCommentTarget`.
|
|
165
|
+
*/
|
|
166
|
+
to: string;
|
|
167
|
+
/** Reply text content. */
|
|
168
|
+
text: string;
|
|
169
|
+
/** Optional account identifier for multi-account setups. */
|
|
170
|
+
accountId?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Send a text message to a Feishu Drive comment surface.
|
|
174
|
+
*
|
|
175
|
+
* Parses the comment target from `to`, then chooses one of two delivery
|
|
176
|
+
* strategies:
|
|
177
|
+
* - `reply` → reply in the existing comment thread
|
|
178
|
+
* - `create_whole` → create a new whole-document comment
|
|
179
|
+
*
|
|
180
|
+
* Returns a synthetic FeishuSendResult (no IM messageId).
|
|
181
|
+
*
|
|
182
|
+
* @throws {Error} When the target is not a valid comment target or API fails.
|
|
183
|
+
*/
|
|
184
|
+
export declare function sendCommentReplyLark(params: SendCommentReplyLarkParams): Promise<FeishuSendResult>;
|