@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.
Files changed (43) hide show
  1. package/index.js +25 -6
  2. package/package.json +1 -1
  3. package/src/card/builder.d.ts +29 -7
  4. package/src/card/builder.js +241 -29
  5. package/src/card/reasoning-utils.d.ts +14 -0
  6. package/src/card/reasoning-utils.js +64 -0
  7. package/src/card/reply-dispatcher-types.d.ts +12 -15
  8. package/src/card/reply-dispatcher-types.js +1 -0
  9. package/src/card/reply-dispatcher.js +63 -11
  10. package/src/card/streaming-card-controller.d.ts +15 -0
  11. package/src/card/streaming-card-controller.js +188 -65
  12. package/src/card/tool-use-config.d.ts +26 -0
  13. package/src/card/tool-use-config.js +76 -0
  14. package/src/card/tool-use-display.d.ts +29 -0
  15. package/src/card/tool-use-display.js +438 -0
  16. package/src/card/tool-use-trace-store.d.ts +51 -0
  17. package/src/card/tool-use-trace-store.js +271 -0
  18. package/src/channel/event-handlers.d.ts +1 -0
  19. package/src/channel/event-handlers.js +51 -0
  20. package/src/channel/monitor.js +2 -0
  21. package/src/core/comment-target.d.ts +65 -0
  22. package/src/core/comment-target.js +100 -0
  23. package/src/core/config-schema.d.ts +9 -0
  24. package/src/core/config-schema.js +5 -0
  25. package/src/core/tool-scopes.d.ts +1 -1
  26. package/src/core/tool-scopes.js +7 -0
  27. package/src/messaging/inbound/comment-context.d.ts +82 -0
  28. package/src/messaging/inbound/comment-context.js +353 -0
  29. package/src/messaging/inbound/comment-handler.d.ts +30 -0
  30. package/src/messaging/inbound/comment-handler.js +269 -0
  31. package/src/messaging/inbound/dispatch-commands.js +23 -2
  32. package/src/messaging/inbound/dispatch-context.js +19 -7
  33. package/src/messaging/inbound/dispatch.js +87 -8
  34. package/src/messaging/outbound/deliver.d.ts +29 -0
  35. package/src/messaging/outbound/deliver.js +94 -0
  36. package/src/messaging/outbound/outbound.js +19 -0
  37. package/src/messaging/types.d.ts +63 -0
  38. package/src/tools/oapi/drive/doc-comments.js +93 -24
  39. package/src/tools/oapi/index.js +1 -0
  40. package/src/tools/oapi/task/index.d.ts +1 -0
  41. package/src/tools/oapi/task/index.js +3 -1
  42. package/src/tools/oapi/task/section.d.ts +17 -0
  43. 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) {
@@ -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
  *
@@ -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'],