@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,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Runtime store for structured tool-use steps.
|
|
7
|
+
*
|
|
8
|
+
* The Feishu card renderer reads from this store by session key so it can
|
|
9
|
+
* render observable, replayable tool execution without relying purely on
|
|
10
|
+
* reply payload text.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.startToolUseTraceRun = startToolUseTraceRun;
|
|
14
|
+
exports.clearToolUseTraceRun = clearToolUseTraceRun;
|
|
15
|
+
exports.hasToolUseTraceRun = hasToolUseTraceRun;
|
|
16
|
+
exports.recordToolUseStart = recordToolUseStart;
|
|
17
|
+
exports.recordToolUseEnd = recordToolUseEnd;
|
|
18
|
+
exports.getToolUseTraceSteps = getToolUseTraceSteps;
|
|
19
|
+
exports.sanitizeTraceValue = sanitizeTraceValue;
|
|
20
|
+
exports._resetForTesting = _resetForTesting;
|
|
21
|
+
const reasoning_utils_1 = require("./reasoning-utils.js");
|
|
22
|
+
const TRACE_TTL_MS = 30 * 60 * 1000;
|
|
23
|
+
const MAX_SESSION_TRACES = 128;
|
|
24
|
+
const MAX_STEPS_PER_SESSION = 256;
|
|
25
|
+
const STEP_RUNNING_TIMEOUT_MS = 5 * 60 * 1000;
|
|
26
|
+
const GENERIC_STRING_LIMIT = 512;
|
|
27
|
+
const RESULT_STRING_LIMIT = 1024;
|
|
28
|
+
const COMMAND_STRING_LIMIT = 4096;
|
|
29
|
+
const PATH_STRING_LIMIT = 2048;
|
|
30
|
+
const sessionTraces = new Map();
|
|
31
|
+
function startToolUseTraceRun(sessionKey) {
|
|
32
|
+
if (!sessionKey)
|
|
33
|
+
return;
|
|
34
|
+
pruneTraceStore();
|
|
35
|
+
sessionTraces.set(sessionKey, {
|
|
36
|
+
nextSeq: 1,
|
|
37
|
+
updatedAt: Date.now(),
|
|
38
|
+
steps: [],
|
|
39
|
+
currentRunId: undefined,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function clearToolUseTraceRun(sessionKey) {
|
|
43
|
+
if (!sessionKey)
|
|
44
|
+
return;
|
|
45
|
+
sessionTraces.delete(sessionKey);
|
|
46
|
+
}
|
|
47
|
+
function hasToolUseTraceRun(sessionKey) {
|
|
48
|
+
if (!sessionKey)
|
|
49
|
+
return false;
|
|
50
|
+
return sessionTraces.has(sessionKey);
|
|
51
|
+
}
|
|
52
|
+
function recordToolUseStart(params) {
|
|
53
|
+
const { sessionKey, toolName, toolParams, toolCallId, runId } = params;
|
|
54
|
+
if (!sessionKey || !toolName)
|
|
55
|
+
return;
|
|
56
|
+
const state = sessionTraces.get(sessionKey);
|
|
57
|
+
if (!state)
|
|
58
|
+
return;
|
|
59
|
+
if (runId) {
|
|
60
|
+
if (state.currentRunId === undefined) {
|
|
61
|
+
state.currentRunId = runId;
|
|
62
|
+
}
|
|
63
|
+
else if (state.currentRunId !== runId) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (state.steps.length >= MAX_STEPS_PER_SESSION) {
|
|
69
|
+
state.steps.splice(0, state.steps.length - MAX_STEPS_PER_SESSION + 1);
|
|
70
|
+
}
|
|
71
|
+
state.steps.push({
|
|
72
|
+
id: `${state.nextSeq}`,
|
|
73
|
+
seq: state.nextSeq,
|
|
74
|
+
toolName,
|
|
75
|
+
toolCallId: toolCallId || undefined,
|
|
76
|
+
runId: runId || undefined,
|
|
77
|
+
params: sanitizeTraceValue(toolParams, 0, { source: 'params' }),
|
|
78
|
+
status: 'running',
|
|
79
|
+
startedAt: now,
|
|
80
|
+
});
|
|
81
|
+
state.nextSeq += 1;
|
|
82
|
+
state.updatedAt = now;
|
|
83
|
+
}
|
|
84
|
+
function recordToolUseEnd(params) {
|
|
85
|
+
const { sessionKey, toolName, toolParams, toolCallId, runId, result, error, durationMs } = params;
|
|
86
|
+
if (!sessionKey || !toolName)
|
|
87
|
+
return;
|
|
88
|
+
const state = sessionTraces.get(sessionKey);
|
|
89
|
+
if (!state)
|
|
90
|
+
return;
|
|
91
|
+
if (runId && state.currentRunId !== undefined && state.currentRunId !== runId) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const sanitizedParams = sanitizeTraceValue(toolParams, 0, { source: 'params' });
|
|
96
|
+
const pendingIndex = findPendingStepIndex(state.steps, toolName, sanitizedParams, toolCallId);
|
|
97
|
+
if (pendingIndex >= 0) {
|
|
98
|
+
const step = state.steps[pendingIndex];
|
|
99
|
+
if (!step)
|
|
100
|
+
return;
|
|
101
|
+
step.status = error ? 'error' : 'success';
|
|
102
|
+
step.result = sanitizeTraceValue(result, 0, { source: 'result' });
|
|
103
|
+
step.error = error ? (0, reasoning_utils_1.truncateText)(error, 160) : undefined;
|
|
104
|
+
step.durationMs = durationMs;
|
|
105
|
+
step.finishedAt = now;
|
|
106
|
+
if (!step.params && sanitizedParams) {
|
|
107
|
+
step.params = sanitizedParams;
|
|
108
|
+
}
|
|
109
|
+
state.updatedAt = now;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
state.steps.push({
|
|
113
|
+
id: `${state.nextSeq}`,
|
|
114
|
+
seq: state.nextSeq,
|
|
115
|
+
toolName,
|
|
116
|
+
toolCallId: toolCallId || undefined,
|
|
117
|
+
runId: runId || undefined,
|
|
118
|
+
params: sanitizedParams,
|
|
119
|
+
result: sanitizeTraceValue(result, 0, { source: 'result' }),
|
|
120
|
+
error: error ? (0, reasoning_utils_1.truncateText)(error, 160) : undefined,
|
|
121
|
+
durationMs,
|
|
122
|
+
status: error ? 'error' : 'success',
|
|
123
|
+
startedAt: now,
|
|
124
|
+
finishedAt: now,
|
|
125
|
+
});
|
|
126
|
+
state.nextSeq += 1;
|
|
127
|
+
state.updatedAt = now;
|
|
128
|
+
}
|
|
129
|
+
function getToolUseTraceSteps(sessionKey) {
|
|
130
|
+
if (!sessionKey)
|
|
131
|
+
return [];
|
|
132
|
+
const state = sessionTraces.get(sessionKey);
|
|
133
|
+
if (!state)
|
|
134
|
+
return [];
|
|
135
|
+
if (Date.now() - state.updatedAt > TRACE_TTL_MS) {
|
|
136
|
+
sessionTraces.delete(sessionKey);
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
return state.steps.map((step) => {
|
|
141
|
+
if (step.status === 'running' && now - step.startedAt > STEP_RUNNING_TIMEOUT_MS) {
|
|
142
|
+
return { ...step, status: 'error', error: 'timed out', finishedAt: now };
|
|
143
|
+
}
|
|
144
|
+
return { ...step };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function findPendingStepIndex(steps, toolName, params, toolCallId) {
|
|
148
|
+
if (toolCallId) {
|
|
149
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
150
|
+
const step = steps[index];
|
|
151
|
+
if (!step || step.status !== 'running')
|
|
152
|
+
continue;
|
|
153
|
+
if (step.toolCallId === toolCallId)
|
|
154
|
+
return index;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const normalizedToolName = (0, reasoning_utils_1.normalizeToolName)(toolName);
|
|
158
|
+
const paramsKey = fingerprintTraceValue(params);
|
|
159
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
160
|
+
const step = steps[index];
|
|
161
|
+
if (!step || step.status !== 'running')
|
|
162
|
+
continue;
|
|
163
|
+
if ((0, reasoning_utils_1.normalizeToolName)(step.toolName) !== normalizedToolName)
|
|
164
|
+
continue;
|
|
165
|
+
if (fingerprintTraceValue(step.params) !== paramsKey)
|
|
166
|
+
continue;
|
|
167
|
+
return index;
|
|
168
|
+
}
|
|
169
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
170
|
+
const step = steps[index];
|
|
171
|
+
if (!step || step.status !== 'running')
|
|
172
|
+
continue;
|
|
173
|
+
if ((0, reasoning_utils_1.normalizeToolName)(step.toolName) !== normalizedToolName)
|
|
174
|
+
continue;
|
|
175
|
+
return index;
|
|
176
|
+
}
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
function pruneTraceStore() {
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
for (const [sessionKey, state] of sessionTraces) {
|
|
182
|
+
if (now - state.updatedAt > TRACE_TTL_MS) {
|
|
183
|
+
sessionTraces.delete(sessionKey);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (sessionTraces.size <= MAX_SESSION_TRACES)
|
|
187
|
+
return;
|
|
188
|
+
const overflow = sessionTraces.size - MAX_SESSION_TRACES;
|
|
189
|
+
const entries = [...sessionTraces.entries()].sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
|
190
|
+
for (const [sessionKey] of entries.slice(0, overflow)) {
|
|
191
|
+
sessionTraces.delete(sessionKey);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function sanitizeTraceValue(value, depth = 0, context = {}) {
|
|
195
|
+
if (value == null)
|
|
196
|
+
return undefined;
|
|
197
|
+
if (typeof value === 'string') {
|
|
198
|
+
const limit = resolveStringLimit(context);
|
|
199
|
+
return (0, reasoning_utils_1.truncateText)(sanitizeTraceString(value, context), limit);
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
202
|
+
return value;
|
|
203
|
+
if (depth >= 2)
|
|
204
|
+
return '[truncated]';
|
|
205
|
+
if (Array.isArray(value)) {
|
|
206
|
+
return value.slice(0, 8).map((item) => sanitizeTraceValue(item, depth + 1, { source: context.source }));
|
|
207
|
+
}
|
|
208
|
+
if (typeof value === 'object') {
|
|
209
|
+
const input = value;
|
|
210
|
+
const output = {};
|
|
211
|
+
for (const [key, entryValue] of Object.entries(input).slice(0, 12)) {
|
|
212
|
+
output[key] = isSensitiveKey(key)
|
|
213
|
+
? '[redacted]'
|
|
214
|
+
: sanitizeTraceValue(entryValue, depth + 1, { source: context.source, key });
|
|
215
|
+
}
|
|
216
|
+
return output;
|
|
217
|
+
}
|
|
218
|
+
return (0, reasoning_utils_1.truncateText)(String(value), 180);
|
|
219
|
+
}
|
|
220
|
+
function sanitizeTraceString(value, context) {
|
|
221
|
+
const redactedUrl = redactUrlParams(value);
|
|
222
|
+
if (isCommandLikeKey(context.key)) {
|
|
223
|
+
return (0, reasoning_utils_1.redactInlineSecrets)(redactedUrl);
|
|
224
|
+
}
|
|
225
|
+
return redactedUrl;
|
|
226
|
+
}
|
|
227
|
+
function resolveStringLimit(context) {
|
|
228
|
+
const key = context.key?.toLowerCase() ?? '';
|
|
229
|
+
if (/(?:^|_)(?:command|script|description|prompt|task)(?:$|_)/.test(key)) {
|
|
230
|
+
return COMMAND_STRING_LIMIT;
|
|
231
|
+
}
|
|
232
|
+
if (/(?:^|_)(?:path|file|url|uri|cwd|folder|dir)(?:$|_)/.test(key)) {
|
|
233
|
+
return PATH_STRING_LIMIT;
|
|
234
|
+
}
|
|
235
|
+
if (context.source === 'result') {
|
|
236
|
+
return RESULT_STRING_LIMIT;
|
|
237
|
+
}
|
|
238
|
+
return GENERIC_STRING_LIMIT;
|
|
239
|
+
}
|
|
240
|
+
function isCommandLikeKey(key) {
|
|
241
|
+
const normalized = key?.toLowerCase() ?? '';
|
|
242
|
+
return /(?:^|_)(?:command|script)(?:$|_)/.test(normalized);
|
|
243
|
+
}
|
|
244
|
+
const SENSITIVE_KEY_RE = /secret|token|password|authorization|cookie|api[-_]?key|credential|private[-_]?key|access[-_]?key|database[-_]?url|connection[-_]?string|bearer|signing[-_]?key|encryption[-_]?key|session[-_]?id|client[-_]?secret|auth[-_]?token/i;
|
|
245
|
+
function isSensitiveKey(key) {
|
|
246
|
+
return SENSITIVE_KEY_RE.test(key);
|
|
247
|
+
}
|
|
248
|
+
function redactUrlParams(url) {
|
|
249
|
+
return url.replace(/([?&])(api_key|token|secret|key)=[^&]*/gi, '$1$2=[redacted]');
|
|
250
|
+
}
|
|
251
|
+
function fingerprintTraceValue(value) {
|
|
252
|
+
if (value == null)
|
|
253
|
+
return '';
|
|
254
|
+
if (typeof value !== 'object')
|
|
255
|
+
return String(value);
|
|
256
|
+
return JSON.stringify(sortTraceValue(value));
|
|
257
|
+
}
|
|
258
|
+
function sortTraceValue(value) {
|
|
259
|
+
if (Array.isArray(value))
|
|
260
|
+
return value.map((item) => sortTraceValue(item));
|
|
261
|
+
if (value && typeof value === 'object') {
|
|
262
|
+
return Object.fromEntries(Object.entries(value)
|
|
263
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
264
|
+
.map(([key, entryValue]) => [key, sortTraceValue(entryValue)]));
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
/** @internal — test-only helper to reset module-level state between test cases. */
|
|
269
|
+
function _resetForTesting() {
|
|
270
|
+
sessionTraces.clear();
|
|
271
|
+
}
|
|
@@ -12,4 +12,5 @@ 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 handleCommentEvent(ctx: MonitorContext, data: unknown): Promise<void>;
|
|
15
16
|
export declare function handleCardActionEvent(ctx: MonitorContext, data: unknown): Promise<unknown>;
|
|
@@ -13,9 +13,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
exports.handleMessageEvent = handleMessageEvent;
|
|
14
14
|
exports.handleReactionEvent = handleReactionEvent;
|
|
15
15
|
exports.handleBotMembershipEvent = handleBotMembershipEvent;
|
|
16
|
+
exports.handleCommentEvent = handleCommentEvent;
|
|
16
17
|
exports.handleCardActionEvent = handleCardActionEvent;
|
|
17
18
|
const handler_1 = require("../messaging/inbound/handler.js");
|
|
18
19
|
const reaction_handler_1 = require("../messaging/inbound/reaction-handler.js");
|
|
20
|
+
const comment_handler_1 = require("../messaging/inbound/comment-handler.js");
|
|
21
|
+
const comment_context_1 = require("../messaging/inbound/comment-context.js");
|
|
19
22
|
const dedup_1 = require("../messaging/inbound/dedup.js");
|
|
20
23
|
const lark_ticket_1 = require("../core/lark-ticket.js");
|
|
21
24
|
const lark_logger_1 = require("../core/lark-logger.js");
|
|
@@ -219,6 +222,54 @@ async function handleBotMembershipEvent(ctx, data, action) {
|
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
224
|
// ---------------------------------------------------------------------------
|
|
225
|
+
// Drive comment handler
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
async function handleCommentEvent(ctx, data) {
|
|
228
|
+
if (!isEventOwnershipValid(ctx, data))
|
|
229
|
+
return;
|
|
230
|
+
const { accountId, log, error } = ctx;
|
|
231
|
+
try {
|
|
232
|
+
const parsed = (0, comment_context_1.parseFeishuDriveCommentNoticeEventPayload)(data);
|
|
233
|
+
if (!parsed) {
|
|
234
|
+
log(`feishu[${accountId}]: invalid comment event payload, skipping`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const commentId = parsed.comment_id ?? '';
|
|
238
|
+
const replyId = parsed.reply_id ?? '';
|
|
239
|
+
// Parser has normalized notice_meta fields into canonical top-level fields
|
|
240
|
+
const _senderOpenId = parsed.user_id?.open_id ?? '';
|
|
241
|
+
const isMentioned = parsed.is_mention ?? false;
|
|
242
|
+
const eventTimestamp = parsed.action_time;
|
|
243
|
+
log(`feishu[${accountId}]: drive comment event: ` +
|
|
244
|
+
`type=${parsed.file_type}, comment=${commentId}` +
|
|
245
|
+
`${replyId ? `, reply=${replyId}` : ''}` +
|
|
246
|
+
`${isMentioned ? ', @bot' : ''}`);
|
|
247
|
+
// Dedup: build a deterministic key from the comment/reply IDs
|
|
248
|
+
const dedupKey = replyId ? `comment:${commentId}:reply:${replyId}` : `comment:${commentId}`;
|
|
249
|
+
if (!ctx.messageDedup.tryRecord(dedupKey, accountId)) {
|
|
250
|
+
log(`feishu[${accountId}]: duplicate comment event ${dedupKey}, skipping`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// Expiry check
|
|
254
|
+
if ((0, dedup_1.isMessageExpired)(eventTimestamp)) {
|
|
255
|
+
log(`feishu[${accountId}]: comment event expired, discarding`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Dispatch the comment event (no queue serialization needed for comment threads)
|
|
259
|
+
await (0, comment_handler_1.handleFeishuCommentEvent)({
|
|
260
|
+
cfg: ctx.cfg,
|
|
261
|
+
event: parsed,
|
|
262
|
+
botOpenId: ctx.lark.botOpenId,
|
|
263
|
+
runtime: ctx.runtime,
|
|
264
|
+
chatHistories: ctx.chatHistories,
|
|
265
|
+
accountId,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
error(`feishu[${accountId}]: error handling comment event: ${String(err)}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
222
273
|
// Card action handler
|
|
223
274
|
// ---------------------------------------------------------------------------
|
|
224
275
|
async function handleCardActionEvent(ctx, data) {
|
package/src/channel/monitor.js
CHANGED
|
@@ -77,6 +77,8 @@ 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
|
+
// Drive comment event — fires when a user adds a comment or reply on a document.
|
|
81
|
+
'drive.notice.comment_add_v1': (data) => (0, event_handlers_1.handleCommentEvent)(ctx, data),
|
|
80
82
|
// 飞书 SDK EventDispatcher.register 不支持带返回值的处理器,此处 as any 是 SDK 类型限制的变通
|
|
81
83
|
'card.action.trigger': ((data) =>
|
|
82
84
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Feishu Drive comment target ID parsing and formatting utilities.
|
|
6
|
+
*
|
|
7
|
+
* Comment targets use one of these formats:
|
|
8
|
+
* - `comment:<fileType>:<fileToken>:<commentId>` (legacy/default reply mode)
|
|
9
|
+
* - `comment:<deliveryMode>:<fileType>:<fileToken>:<commentId>`
|
|
10
|
+
*
|
|
11
|
+
* This enables the outbound routing layer to distinguish comment-thread
|
|
12
|
+
* replies from normal IM messages and route them through the Drive
|
|
13
|
+
* comment API instead.
|
|
14
|
+
*/
|
|
15
|
+
/** Document types that support Drive comments. */
|
|
16
|
+
export type CommentFileType = 'doc' | 'docx' | 'file' | 'sheet' | 'slides';
|
|
17
|
+
/** Delivery mode for a comment target. */
|
|
18
|
+
export type CommentDeliveryMode = 'reply' | 'create_whole';
|
|
19
|
+
/** Parsed comment target components. */
|
|
20
|
+
export interface CommentTarget {
|
|
21
|
+
deliveryMode: CommentDeliveryMode;
|
|
22
|
+
fileType: CommentFileType;
|
|
23
|
+
fileToken: string;
|
|
24
|
+
commentId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Construct a comment target string from its components.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* buildFeishuCommentTarget({ fileType: 'docx', fileToken: 'abc123', commentId: '789' })
|
|
32
|
+
* // => 'comment:docx:abc123:789'
|
|
33
|
+
*
|
|
34
|
+
* buildFeishuCommentTarget({ deliveryMode: 'create_whole', fileType: 'docx', fileToken: 'abc123', commentId: '789' })
|
|
35
|
+
* // => 'comment:create_whole:docx:abc123:789'
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildFeishuCommentTarget(params: {
|
|
39
|
+
deliveryMode?: CommentDeliveryMode;
|
|
40
|
+
fileType: CommentFileType;
|
|
41
|
+
fileToken: string;
|
|
42
|
+
commentId: string;
|
|
43
|
+
}): string;
|
|
44
|
+
/**
|
|
45
|
+
* Parse a comment target string into its components.
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` when the string is not a valid comment target.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* parseFeishuCommentTarget('comment:docx:abc123:789')
|
|
52
|
+
* // => { deliveryMode: 'reply', fileType: 'docx', fileToken: 'abc123', commentId: '789' }
|
|
53
|
+
*
|
|
54
|
+
* parseFeishuCommentTarget('comment:create_whole:docx:abc123:789')
|
|
55
|
+
* // => { deliveryMode: 'create_whole', fileType: 'docx', fileToken: 'abc123', commentId: '789' }
|
|
56
|
+
*
|
|
57
|
+
* parseFeishuCommentTarget('oc_xxx')
|
|
58
|
+
* // => null
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseFeishuCommentTarget(target: string): CommentTarget | null;
|
|
62
|
+
/**
|
|
63
|
+
* Return `true` when a target string looks like a comment target.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isCommentTarget(target: string): boolean;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Feishu Drive comment target ID parsing and formatting utilities.
|
|
7
|
+
*
|
|
8
|
+
* Comment targets use one of these formats:
|
|
9
|
+
* - `comment:<fileType>:<fileToken>:<commentId>` (legacy/default reply mode)
|
|
10
|
+
* - `comment:<deliveryMode>:<fileType>:<fileToken>:<commentId>`
|
|
11
|
+
*
|
|
12
|
+
* This enables the outbound routing layer to distinguish comment-thread
|
|
13
|
+
* replies from normal IM messages and route them through the Drive
|
|
14
|
+
* comment API instead.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.buildFeishuCommentTarget = buildFeishuCommentTarget;
|
|
18
|
+
exports.parseFeishuCommentTarget = parseFeishuCommentTarget;
|
|
19
|
+
exports.isCommentTarget = isCommentTarget;
|
|
20
|
+
const VALID_FILE_TYPES = new Set(['doc', 'docx', 'file', 'sheet', 'slides']);
|
|
21
|
+
const VALID_DELIVERY_MODES = new Set(['reply', 'create_whole']);
|
|
22
|
+
const COMMENT_PREFIX = 'comment:';
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Build
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Construct a comment target string from its components.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* buildFeishuCommentTarget({ fileType: 'docx', fileToken: 'abc123', commentId: '789' })
|
|
32
|
+
* // => 'comment:docx:abc123:789'
|
|
33
|
+
*
|
|
34
|
+
* buildFeishuCommentTarget({ deliveryMode: 'create_whole', fileType: 'docx', fileToken: 'abc123', commentId: '789' })
|
|
35
|
+
* // => 'comment:create_whole:docx:abc123:789'
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function buildFeishuCommentTarget(params) {
|
|
39
|
+
const deliveryMode = params.deliveryMode ?? 'reply';
|
|
40
|
+
if (deliveryMode === 'reply') {
|
|
41
|
+
return `${COMMENT_PREFIX}${params.fileType}:${params.fileToken}:${params.commentId}`;
|
|
42
|
+
}
|
|
43
|
+
return `${COMMENT_PREFIX}${deliveryMode}:${params.fileType}:${params.fileToken}:${params.commentId}`;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Parse
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Parse a comment target string into its components.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` when the string is not a valid comment target.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* parseFeishuCommentTarget('comment:docx:abc123:789')
|
|
56
|
+
* // => { deliveryMode: 'reply', fileType: 'docx', fileToken: 'abc123', commentId: '789' }
|
|
57
|
+
*
|
|
58
|
+
* parseFeishuCommentTarget('comment:create_whole:docx:abc123:789')
|
|
59
|
+
* // => { deliveryMode: 'create_whole', fileType: 'docx', fileToken: 'abc123', commentId: '789' }
|
|
60
|
+
*
|
|
61
|
+
* parseFeishuCommentTarget('oc_xxx')
|
|
62
|
+
* // => null
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
function parseFeishuCommentTarget(target) {
|
|
66
|
+
if (!target || !target.startsWith(COMMENT_PREFIX))
|
|
67
|
+
return null;
|
|
68
|
+
const rest = target.slice(COMMENT_PREFIX.length);
|
|
69
|
+
const parts = rest.split(':');
|
|
70
|
+
let deliveryMode = 'reply';
|
|
71
|
+
let fileType;
|
|
72
|
+
let fileToken;
|
|
73
|
+
let commentId;
|
|
74
|
+
if (parts.length === 3) {
|
|
75
|
+
[fileType, fileToken, commentId] = parts;
|
|
76
|
+
}
|
|
77
|
+
else if (parts.length === 4 && VALID_DELIVERY_MODES.has(parts[0])) {
|
|
78
|
+
[deliveryMode, fileType, fileToken, commentId] = parts;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!VALID_FILE_TYPES.has(fileType) || !fileToken || !commentId)
|
|
84
|
+
return null;
|
|
85
|
+
return {
|
|
86
|
+
deliveryMode,
|
|
87
|
+
fileType: fileType,
|
|
88
|
+
fileToken,
|
|
89
|
+
commentId,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Detection
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
/**
|
|
96
|
+
* Return `true` when a target string looks like a comment target.
|
|
97
|
+
*/
|
|
98
|
+
function isCommentTarget(target) {
|
|
99
|
+
return Boolean(target && target.startsWith(COMMENT_PREFIX));
|
|
100
|
+
}
|
|
@@ -129,6 +129,9 @@ export declare const FeishuAccountConfigSchema: z.ZodObject<{
|
|
|
129
129
|
}, z.core.$strip>]>>;
|
|
130
130
|
streaming: z.ZodOptional<z.ZodBoolean>;
|
|
131
131
|
blockStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
132
|
+
toolUseDisplay: z.ZodOptional<z.ZodObject<{
|
|
133
|
+
showFullPaths: z.ZodOptional<z.ZodBoolean>;
|
|
134
|
+
}, z.core.$strip>>;
|
|
132
135
|
tools: z.ZodOptional<z.ZodObject<{
|
|
133
136
|
doc: z.ZodOptional<z.ZodBoolean>;
|
|
134
137
|
wiki: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -271,6 +274,9 @@ export declare const FeishuConfigSchema: z.ZodObject<{
|
|
|
271
274
|
}, z.core.$strip>]>>;
|
|
272
275
|
streaming: z.ZodOptional<z.ZodBoolean>;
|
|
273
276
|
blockStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
277
|
+
toolUseDisplay: z.ZodOptional<z.ZodObject<{
|
|
278
|
+
showFullPaths: z.ZodOptional<z.ZodBoolean>;
|
|
279
|
+
}, z.core.$strip>>;
|
|
274
280
|
tools: z.ZodOptional<z.ZodObject<{
|
|
275
281
|
doc: z.ZodOptional<z.ZodBoolean>;
|
|
276
282
|
wiki: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -412,6 +418,9 @@ export declare const FeishuConfigSchema: z.ZodObject<{
|
|
|
412
418
|
}, z.core.$strip>]>>;
|
|
413
419
|
streaming: z.ZodOptional<z.ZodBoolean>;
|
|
414
420
|
blockStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
421
|
+
toolUseDisplay: z.ZodOptional<z.ZodObject<{
|
|
422
|
+
showFullPaths: z.ZodOptional<z.ZodBoolean>;
|
|
423
|
+
}, z.core.$strip>>;
|
|
415
424
|
tools: z.ZodOptional<z.ZodObject<{
|
|
416
425
|
doc: z.ZodOptional<z.ZodBoolean>;
|
|
417
426
|
wiki: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -163,6 +163,11 @@ exports.FeishuAccountConfigSchema = zod_1.z.object({
|
|
|
163
163
|
replyMode: ReplyModeSchema,
|
|
164
164
|
streaming: zod_1.z.boolean().optional(),
|
|
165
165
|
blockStreaming: zod_1.z.boolean().optional(),
|
|
166
|
+
toolUseDisplay: zod_1.z
|
|
167
|
+
.object({
|
|
168
|
+
showFullPaths: zod_1.z.boolean().optional(),
|
|
169
|
+
})
|
|
170
|
+
.optional(),
|
|
166
171
|
tools: FeishuToolsFlagSchema,
|
|
167
172
|
footer: FeishuFooterSchema,
|
|
168
173
|
markdown: MarkdownConfigSchema,
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
*
|
|
54
54
|
* 总计:96 个工具动作
|
|
55
55
|
*/
|
|
56
|
-
export type ToolActionKey = 'feishu_bitable_app.copy' | 'feishu_bitable_app.create' | 'feishu_bitable_app.get' | 'feishu_bitable_app.list' | 'feishu_bitable_app.patch' | 'feishu_bitable_app_table.batch_create' | 'feishu_bitable_app_table.create' | 'feishu_bitable_app_table.list' | 'feishu_bitable_app_table.patch' | 'feishu_bitable_app_table_field.create' | 'feishu_bitable_app_table_field.delete' | 'feishu_bitable_app_table_field.list' | 'feishu_bitable_app_table_field.update' | 'feishu_bitable_app_table_record.batch_create' | 'feishu_bitable_app_table_record.batch_delete' | 'feishu_bitable_app_table_record.batch_update' | 'feishu_bitable_app_table_record.create' | 'feishu_bitable_app_table_record.delete' | 'feishu_bitable_app_table_record.list' | 'feishu_bitable_app_table_record.update' | 'feishu_bitable_app_table_view.create' | 'feishu_bitable_app_table_view.get' | 'feishu_bitable_app_table_view.list' | 'feishu_bitable_app_table_view.patch' | 'feishu_calendar_calendar.get' | 'feishu_calendar_calendar.list' | 'feishu_calendar_calendar.primary' | 'feishu_calendar_event.create' | 'feishu_calendar_event.delete' | 'feishu_calendar_event.get' | 'feishu_calendar_event.instance_view' | 'feishu_calendar_event.instances' | 'feishu_calendar_event.list' | 'feishu_calendar_event.patch' | 'feishu_calendar_event.reply' | 'feishu_calendar_event.search' | 'feishu_calendar_event_attendee.create' | 'feishu_calendar_event_attendee.list' | 'feishu_calendar_freebusy.list' | 'feishu_chat.get' | 'feishu_chat.search' | 'feishu_chat_members.default' | 'feishu_create_doc.default' | 'feishu_doc_comments.create' | 'feishu_doc_comments.list' | 'feishu_doc_comments.patch' | 'feishu_doc_media.download' | 'feishu_doc_media.insert' | 'feishu_drive_file.copy' | 'feishu_drive_file.delete' | 'feishu_drive_file.download' | 'feishu_drive_file.get_meta' | 'feishu_drive_file.list' | 'feishu_drive_file.move' | 'feishu_drive_file.upload' | 'feishu_fetch_doc.default' | 'feishu_get_user.basic_batch' | 'feishu_get_user.default' | 'feishu_im_user_fetch_resource.default' | 'feishu_im_user_get_messages.default' | 'feishu_im_user_message.reply' | 'feishu_im_user_message.send' | 'feishu_im_user_search_messages.default' | 'feishu_search_doc_wiki.search' | 'feishu_search_user.default' | 'feishu_task_comment.create' | 'feishu_task_comment.get' | 'feishu_task_comment.list' | 'feishu_task_subtask.create' | 'feishu_task_subtask.list' | 'feishu_task_task.create' | 'feishu_task_task.get' | 'feishu_task_task.list' | 'feishu_task_task.patch' | 'feishu_task_tasklist.add_members' | 'feishu_task_tasklist.create' | 'feishu_task_tasklist.get' | 'feishu_task_tasklist.list' | 'feishu_task_tasklist.patch' | 'feishu_task_tasklist.tasks' | 'feishu_update_doc.default' | 'feishu_wiki_space.create' | 'feishu_wiki_space.get' | 'feishu_wiki_space.list' | 'feishu_wiki_space_node.copy' | 'feishu_wiki_space_node.create' | 'feishu_wiki_space_node.get' | 'feishu_wiki_space_node.list' | 'feishu_wiki_space_node.move' | 'feishu_sheet.info' | 'feishu_sheet.read' | 'feishu_sheet.write' | 'feishu_sheet.append' | 'feishu_sheet.find' | 'feishu_sheet.create' | 'feishu_sheet.export';
|
|
56
|
+
export type ToolActionKey = 'feishu_bitable_app.copy' | 'feishu_bitable_app.create' | 'feishu_bitable_app.get' | 'feishu_bitable_app.list' | 'feishu_bitable_app.patch' | 'feishu_bitable_app_table.batch_create' | 'feishu_bitable_app_table.create' | 'feishu_bitable_app_table.list' | 'feishu_bitable_app_table.patch' | 'feishu_bitable_app_table_field.create' | 'feishu_bitable_app_table_field.delete' | 'feishu_bitable_app_table_field.list' | 'feishu_bitable_app_table_field.update' | 'feishu_bitable_app_table_record.batch_create' | 'feishu_bitable_app_table_record.batch_delete' | 'feishu_bitable_app_table_record.batch_update' | 'feishu_bitable_app_table_record.create' | 'feishu_bitable_app_table_record.delete' | 'feishu_bitable_app_table_record.list' | 'feishu_bitable_app_table_record.update' | 'feishu_bitable_app_table_view.create' | 'feishu_bitable_app_table_view.get' | 'feishu_bitable_app_table_view.list' | 'feishu_bitable_app_table_view.patch' | 'feishu_calendar_calendar.get' | 'feishu_calendar_calendar.list' | 'feishu_calendar_calendar.primary' | 'feishu_calendar_event.create' | 'feishu_calendar_event.delete' | 'feishu_calendar_event.get' | 'feishu_calendar_event.instance_view' | 'feishu_calendar_event.instances' | 'feishu_calendar_event.list' | 'feishu_calendar_event.patch' | 'feishu_calendar_event.reply' | 'feishu_calendar_event.search' | 'feishu_calendar_event_attendee.create' | 'feishu_calendar_event_attendee.list' | 'feishu_calendar_freebusy.list' | 'feishu_chat.get' | 'feishu_chat.search' | 'feishu_chat_members.default' | 'feishu_create_doc.default' | 'feishu_doc_comments.create' | 'feishu_doc_comments.list' | 'feishu_doc_comments.list_replies' | 'feishu_doc_comments.patch' | 'feishu_doc_comments.reply' | 'feishu_doc_media.download' | 'feishu_doc_media.insert' | 'feishu_drive_file.copy' | 'feishu_drive_file.delete' | 'feishu_drive_file.download' | 'feishu_drive_file.get_meta' | 'feishu_drive_file.list' | 'feishu_drive_file.move' | 'feishu_drive_file.upload' | 'feishu_fetch_doc.default' | 'feishu_get_user.basic_batch' | 'feishu_get_user.default' | 'feishu_im_user_fetch_resource.default' | 'feishu_im_user_get_messages.default' | 'feishu_im_user_message.reply' | 'feishu_im_user_message.send' | 'feishu_im_user_search_messages.default' | 'feishu_search_doc_wiki.search' | 'feishu_search_user.default' | 'feishu_task_comment.create' | 'feishu_task_comment.get' | 'feishu_task_comment.list' | 'feishu_task_section.create' | 'feishu_task_section.get' | 'feishu_task_section.list' | 'feishu_task_section.patch' | 'feishu_task_section.tasks' | 'feishu_task_subtask.create' | 'feishu_task_subtask.list' | 'feishu_task_task.create' | 'feishu_task_task.get' | 'feishu_task_task.list' | 'feishu_task_task.patch' | 'feishu_task_tasklist.add_members' | 'feishu_task_tasklist.create' | 'feishu_task_tasklist.get' | 'feishu_task_tasklist.list' | 'feishu_task_tasklist.patch' | 'feishu_task_tasklist.tasks' | 'feishu_update_doc.default' | 'feishu_wiki_space.create' | 'feishu_wiki_space.get' | 'feishu_wiki_space.list' | 'feishu_wiki_space_node.copy' | 'feishu_wiki_space_node.create' | 'feishu_wiki_space_node.get' | 'feishu_wiki_space_node.list' | 'feishu_wiki_space_node.move' | 'feishu_sheet.info' | 'feishu_sheet.read' | 'feishu_sheet.write' | 'feishu_sheet.append' | 'feishu_sheet.find' | 'feishu_sheet.create' | 'feishu_sheet.export';
|
|
57
57
|
/**
|
|
58
58
|
* Tool Scope 映射类型
|
|
59
59
|
*
|
package/src/core/tool-scopes.js
CHANGED
|
@@ -120,6 +120,11 @@ exports.TOOL_SCOPES = {
|
|
|
120
120
|
'feishu_task_comment.create': ['task:comment:write'],
|
|
121
121
|
'feishu_task_comment.list': ['task:comment:read', 'task:comment:write'],
|
|
122
122
|
'feishu_task_comment.get': ['task:comment:read', 'task:comment:write'],
|
|
123
|
+
'feishu_task_section.create': ['task:task'],
|
|
124
|
+
'feishu_task_section.get': ['task:task'],
|
|
125
|
+
'feishu_task_section.list': ['task:task'],
|
|
126
|
+
'feishu_task_section.patch': ['task:task'],
|
|
127
|
+
'feishu_task_section.tasks': ['task:task'],
|
|
123
128
|
'feishu_task_subtask.create': ['task:task:write'],
|
|
124
129
|
'feishu_task_subtask.list': ['task:task:read', 'task:task:write'],
|
|
125
130
|
'feishu_chat.search': ['im:chat:read'],
|
|
@@ -135,7 +140,9 @@ exports.TOOL_SCOPES = {
|
|
|
135
140
|
'feishu_doc_media.download': ['board:whiteboard:node:read', 'docs:document.media:download'],
|
|
136
141
|
'feishu_doc_media.insert': ['docx:document:write_only', 'docs:document.media:upload'],
|
|
137
142
|
'feishu_doc_comments.list': ['wiki:node:read', 'docs:document.comment:read'],
|
|
143
|
+
'feishu_doc_comments.list_replies': ['wiki:node:read', 'docs:document.comment:read'],
|
|
138
144
|
'feishu_doc_comments.create': ['wiki:node:read', 'docs:document.comment:create'],
|
|
145
|
+
'feishu_doc_comments.reply': ['wiki:node:read', 'docs:document.comment:create'],
|
|
139
146
|
'feishu_doc_comments.patch': ['docs:document.comment:update'],
|
|
140
147
|
'feishu_wiki_space.list': ['wiki:space:retrieve'],
|
|
141
148
|
'feishu_wiki_space.get': ['wiki:space:read'],
|