@larksuite/openclaw-lark 2026.3.24 → 2026.3.26-beta.0
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/bin/openclaw-lark.js +1 -1
- package/index.d.ts +1 -1
- package/index.js +10 -9
- package/package.json +4 -2
- package/src/card/builder.d.ts +24 -0
- package/src/card/builder.js +94 -25
- package/src/card/card-error.d.ts +91 -0
- package/src/card/card-error.js +206 -0
- package/src/card/cardkit.js +19 -5
- package/src/card/reply-dispatcher-types.d.ts +12 -0
- package/src/card/reply-dispatcher.js +78 -9
- package/src/card/reply-mode.js +7 -1
- package/src/card/streaming-card-controller.d.ts +16 -1
- package/src/card/streaming-card-controller.js +187 -21
- package/src/channel/chat-queue.js +5 -5
- package/src/channel/event-handlers.js +11 -1
- package/src/core/chat-info-cache.d.ts +2 -0
- package/src/core/chat-info-cache.js +16 -2
- package/src/core/config-schema.d.ts +12 -0
- package/src/core/config-schema.js +4 -0
- package/src/core/footer-config.js +8 -0
- package/src/core/lark-client.js +4 -0
- package/src/core/sdk-compat.d.ts +4 -13
- package/src/core/sdk-compat.js +6 -10
- package/src/core/types.d.ts +4 -0
- package/src/messaging/inbound/dispatch-commands.js +1 -0
- package/src/messaging/inbound/dispatch.js +20 -1
- package/src/tools/ask-user-question.d.ts +28 -0
- package/src/tools/ask-user-question.js +766 -0
- package/src/tools/helpers.js +1 -1
- package/src/tools/mcp/doc/index.js +1 -1
- package/src/tools/oapi/chat/index.js +1 -1
- package/src/tools/oapi/drive/index.js +1 -1
- package/src/tools/oapi/im/index.js +1 -1
- package/src/tools/oapi/index.js +1 -1
- package/src/tools/oapi/search/index.js +1 -1
- package/src/tools/oapi/sheets/index.js +1 -1
- package/src/tools/oapi/wiki/index.js +1 -1
- package/src/tools/oauth-batch-auth.js +1 -1
- package/src/tools/oauth.js +1 -1
- package/src/tools/tat/im/index.js +1 -1
package/bin/openclaw-lark.js
CHANGED
|
@@ -14,7 +14,7 @@ if (vIdx !== -1) {
|
|
|
14
14
|
args.splice(vIdx, 2);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const allArgs = ["--yes", `@larksuite/openclaw-lark-tools@${version}`, ...args];
|
|
17
|
+
const allArgs = ["--yes", "--prefer-online", `@larksuite/openclaw-lark-tools@${version}`, ...args];
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
20
|
if (process.platform === "win32") {
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -10,24 +10,19 @@
|
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.isMessageExpired = exports.checkMessageGate = exports.parseMessageEvent = exports.handleFeishuReaction = exports.feishuPlugin = exports.buildMentionedCardContent = exports.buildMentionedMessage = exports.formatMentionAllForCard = exports.formatMentionAllForText = exports.formatMentionForCard = exports.formatMentionForText = exports.extractMessageBody = exports.nonBotMentions = exports.mentionedBot = exports.feishuMessageActions = exports.listChatMembersFeishu = exports.removeChatMembersFeishu = exports.addChatMembersFeishu = exports.updateChatFeishu = exports.forwardMessageFeishu = exports.VALID_FEISHU_EMOJI_TYPES = exports.FeishuEmoji = exports.listReactionsFeishu = exports.removeReactionFeishu = exports.addReactionFeishu = exports.probeFeishu = exports.sendMediaLark = exports.sendCardLark = exports.sendTextLark = exports.uploadAndSendMediaLark = exports.sendAudioLark = exports.sendFileLark = exports.sendImageLark = exports.uploadFileLark = exports.uploadImageLark = exports.getMessageFeishu = exports.editMessageFeishu = exports.updateCardFeishu = exports.sendCardFeishu = exports.sendMessageFeishu = exports.monitorFeishuProvider = void 0;
|
|
13
|
+
const plugin_sdk_1 = require("openclaw/plugin-sdk");
|
|
13
14
|
const plugin_1 = require("./src/channel/plugin.js");
|
|
14
15
|
const lark_client_1 = require("./src/core/lark-client.js");
|
|
15
16
|
const index_1 = require("./src/tools/oapi/index.js");
|
|
16
17
|
const index_2 = require("./src/tools/mcp/doc/index.js");
|
|
17
18
|
const oauth_1 = require("./src/tools/oauth.js");
|
|
18
19
|
const oauth_batch_auth_1 = require("./src/tools/oauth-batch-auth.js");
|
|
20
|
+
const ask_user_question_1 = require("./src/tools/ask-user-question.js");
|
|
19
21
|
const diagnose_1 = require("./src/commands/diagnose.js");
|
|
20
22
|
const index_3 = require("./src/commands/index.js");
|
|
21
23
|
const lark_logger_1 = require("./src/core/lark-logger.js");
|
|
22
24
|
const security_check_1 = require("./src/core/security-check.js");
|
|
23
25
|
const log = (0, lark_logger_1.larkLogger)('plugin');
|
|
24
|
-
function emptyPluginConfigSchema() {
|
|
25
|
-
return {
|
|
26
|
-
type: 'object',
|
|
27
|
-
additionalProperties: false,
|
|
28
|
-
properties: {},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
26
|
// ---------------------------------------------------------------------------
|
|
32
27
|
// Re-exports for external consumers
|
|
33
28
|
// ---------------------------------------------------------------------------
|
|
@@ -95,7 +90,7 @@ const plugin = {
|
|
|
95
90
|
id: 'openclaw-lark',
|
|
96
91
|
name: 'Feishu',
|
|
97
92
|
description: 'Lark/Feishu channel plugin with im/doc/wiki/drive/task/calendar tools',
|
|
98
|
-
configSchema: emptyPluginConfigSchema(),
|
|
93
|
+
configSchema: (0, plugin_sdk_1.emptyPluginConfigSchema)(),
|
|
99
94
|
register(api) {
|
|
100
95
|
lark_client_1.LarkClient.setRuntime(api.runtime);
|
|
101
96
|
api.registerChannel({ plugin: plugin_1.feishuPlugin });
|
|
@@ -108,11 +103,17 @@ const plugin = {
|
|
|
108
103
|
(0, oauth_1.registerFeishuOAuthTool)(api);
|
|
109
104
|
// Register OAuth batch auth tool (batch authorization for all app scopes)
|
|
110
105
|
(0, oauth_batch_auth_1.registerFeishuOAuthBatchAuthTool)(api);
|
|
111
|
-
//
|
|
106
|
+
// Register AskUserQuestion tool (interactive card-based user prompting)
|
|
107
|
+
(0, ask_user_question_1.registerAskUserQuestionTool)(api);
|
|
108
|
+
// ---- Tool call hooks (trace Feishu-owned tool invocations only) ----
|
|
112
109
|
api.on('before_tool_call', (event) => {
|
|
110
|
+
if (!event.toolName.startsWith('feishu_'))
|
|
111
|
+
return;
|
|
113
112
|
log.info(`tool call: ${event.toolName} params=${JSON.stringify(event.params)}`);
|
|
114
113
|
});
|
|
115
114
|
api.on('after_tool_call', (event) => {
|
|
115
|
+
if (!event.toolName.startsWith('feishu_'))
|
|
116
|
+
return;
|
|
116
117
|
if (event.error) {
|
|
117
118
|
log.error(`tool fail: ${event.toolName} ${event.error} (${event.durationMs ?? 0}ms)`);
|
|
118
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@larksuite/openclaw-lark",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.26-beta.0",
|
|
4
4
|
"description": "OpenClaw Lark/Feishu channel plugin",
|
|
5
5
|
"bin": {
|
|
6
6
|
"openclaw-lark": "bin/openclaw-lark.js"
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
14
14
|
"@sinclair/typebox": "0.34.48",
|
|
15
15
|
"image-size": "^2.0.2",
|
|
16
|
-
"openclaw": "^2026.3.13",
|
|
17
16
|
"zod": "^4.3.6"
|
|
18
17
|
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"openclaw": ">=2026.3.22"
|
|
20
|
+
},
|
|
19
21
|
"openclaw": {
|
|
20
22
|
"extensions": [
|
|
21
23
|
"./index.js"
|
package/src/card/builder.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Provides utilities to construct Feishu Interactive Message Cards for
|
|
8
8
|
* different agent response states (thinking, streaming, complete, confirm).
|
|
9
9
|
*/
|
|
10
|
+
import type { FooterSessionMetrics } from './reply-dispatcher-types';
|
|
10
11
|
/**
|
|
11
12
|
* Element ID used for the streaming text area in cards. The CardKit
|
|
12
13
|
* `cardElement.content()` API targets this element for typewriter-effect
|
|
@@ -79,6 +80,24 @@ export declare function formatReasoningDuration(ms: number): {
|
|
|
79
80
|
* Format milliseconds into a human-readable duration string.
|
|
80
81
|
*/
|
|
81
82
|
export declare function formatElapsed(ms: number): string;
|
|
83
|
+
export declare function compactNumber(value: number): string;
|
|
84
|
+
export declare function formatFooterRuntimeSegments(params: {
|
|
85
|
+
footer?: {
|
|
86
|
+
status?: boolean;
|
|
87
|
+
elapsed?: boolean;
|
|
88
|
+
tokens?: boolean;
|
|
89
|
+
cache?: boolean;
|
|
90
|
+
context?: boolean;
|
|
91
|
+
model?: boolean;
|
|
92
|
+
};
|
|
93
|
+
metrics?: FooterSessionMetrics;
|
|
94
|
+
elapsedMs?: number;
|
|
95
|
+
isError?: boolean;
|
|
96
|
+
isAborted?: boolean;
|
|
97
|
+
}): {
|
|
98
|
+
zh: string[];
|
|
99
|
+
en: string[];
|
|
100
|
+
};
|
|
82
101
|
/**
|
|
83
102
|
* Build a full Feishu Interactive Message Card JSON object for the
|
|
84
103
|
* given state.
|
|
@@ -95,7 +114,12 @@ export declare function buildCardContent(state: CardState, data?: {
|
|
|
95
114
|
footer?: {
|
|
96
115
|
status?: boolean;
|
|
97
116
|
elapsed?: boolean;
|
|
117
|
+
tokens?: boolean;
|
|
118
|
+
cache?: boolean;
|
|
119
|
+
context?: boolean;
|
|
120
|
+
model?: boolean;
|
|
98
121
|
};
|
|
122
|
+
footerMetrics?: FooterSessionMetrics;
|
|
99
123
|
}): FeishuCard;
|
|
100
124
|
/**
|
|
101
125
|
* Convert an old-format FeishuCard to CardKit JSON 2.0 format.
|
package/src/card/builder.js
CHANGED
|
@@ -14,6 +14,8 @@ exports.splitReasoningText = splitReasoningText;
|
|
|
14
14
|
exports.stripReasoningTags = stripReasoningTags;
|
|
15
15
|
exports.formatReasoningDuration = formatReasoningDuration;
|
|
16
16
|
exports.formatElapsed = formatElapsed;
|
|
17
|
+
exports.compactNumber = compactNumber;
|
|
18
|
+
exports.formatFooterRuntimeSegments = formatFooterRuntimeSegments;
|
|
17
19
|
exports.buildCardContent = buildCardContent;
|
|
18
20
|
exports.toCardKit2 = toCardKit2;
|
|
19
21
|
const markdown_style_1 = require("./markdown-style.js");
|
|
@@ -143,6 +145,86 @@ function buildFooter(zhText, enText, isError) {
|
|
|
143
145
|
text_size: 'notation',
|
|
144
146
|
}];
|
|
145
147
|
}
|
|
148
|
+
function compactNumber(value) {
|
|
149
|
+
const abs = Math.abs(value);
|
|
150
|
+
if (abs >= 1_000_000) {
|
|
151
|
+
const m = value / 1_000_000;
|
|
152
|
+
return Math.abs(m) >= 100 ? `${Math.round(m)}m` : `${m.toFixed(1)}m`;
|
|
153
|
+
}
|
|
154
|
+
if (abs >= 1_000) {
|
|
155
|
+
const k = value / 1_000;
|
|
156
|
+
return Math.abs(k) >= 100 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
|
|
157
|
+
}
|
|
158
|
+
return `${Math.round(value)}`;
|
|
159
|
+
}
|
|
160
|
+
function formatFooterRuntimeSegments(params) {
|
|
161
|
+
const { footer, metrics, elapsedMs, isError, isAborted } = params;
|
|
162
|
+
const zhParts = [];
|
|
163
|
+
const enParts = [];
|
|
164
|
+
if (footer?.status) {
|
|
165
|
+
if (isError) {
|
|
166
|
+
zhParts.push('出错');
|
|
167
|
+
enParts.push('Error');
|
|
168
|
+
}
|
|
169
|
+
else if (isAborted) {
|
|
170
|
+
zhParts.push('已停止');
|
|
171
|
+
enParts.push('Stopped');
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
zhParts.push('已完成');
|
|
175
|
+
enParts.push('Completed');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (footer?.elapsed && elapsedMs != null) {
|
|
179
|
+
const d = formatElapsed(elapsedMs);
|
|
180
|
+
zhParts.push(`耗时 ${d}`);
|
|
181
|
+
enParts.push(`Elapsed ${d}`);
|
|
182
|
+
}
|
|
183
|
+
if (footer?.tokens && metrics) {
|
|
184
|
+
const inTokens = typeof metrics.inputTokens === 'number' ? Math.max(0, metrics.inputTokens) : undefined;
|
|
185
|
+
const outTokens = typeof metrics.outputTokens === 'number' ? Math.max(0, metrics.outputTokens) : undefined;
|
|
186
|
+
if (inTokens != null && outTokens != null) {
|
|
187
|
+
const inLabel = compactNumber(inTokens);
|
|
188
|
+
const outLabel = compactNumber(outTokens);
|
|
189
|
+
zhParts.push(`↑ ${inLabel} ↓ ${outLabel}`);
|
|
190
|
+
enParts.push(`↑ ${inLabel} ↓ ${outLabel}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (footer?.cache && metrics) {
|
|
194
|
+
const read = typeof metrics.cacheRead === 'number' ? Math.max(0, metrics.cacheRead) : undefined;
|
|
195
|
+
const write = typeof metrics.cacheWrite === 'number' ? Math.max(0, metrics.cacheWrite) : undefined;
|
|
196
|
+
const inputVal = typeof metrics.inputTokens === 'number' ? Math.max(0, metrics.inputTokens) : undefined;
|
|
197
|
+
if (read != null && write != null && inputVal != null) {
|
|
198
|
+
const total = read + write + inputVal;
|
|
199
|
+
const hit = total > 0 ? Math.round((read / total) * 100) : 0;
|
|
200
|
+
const left = compactNumber(read);
|
|
201
|
+
const right = compactNumber(write);
|
|
202
|
+
zhParts.push(`缓存 ${left}/${right} (${hit}%)`);
|
|
203
|
+
enParts.push(`Cache ${left}/${right} (${hit}%)`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (footer?.context && metrics) {
|
|
207
|
+
const freshTotal = metrics.totalTokensFresh === false ? undefined : metrics.totalTokens;
|
|
208
|
+
const total = typeof freshTotal === 'number' ? Math.max(0, freshTotal) : undefined;
|
|
209
|
+
const ctx = typeof metrics.contextTokens === 'number' ? Math.max(0, metrics.contextTokens) : undefined;
|
|
210
|
+
if (total != null && ctx != null) {
|
|
211
|
+
const totalLabel = compactNumber(total);
|
|
212
|
+
const ctxLabel = compactNumber(ctx);
|
|
213
|
+
const pct = ctx > 0 ? Math.round((total / ctx) * 100) : 0;
|
|
214
|
+
const pctLabel = `${pct}%`;
|
|
215
|
+
zhParts.push(`上下文 ${totalLabel}/${ctxLabel} (${pctLabel})`);
|
|
216
|
+
enParts.push(`Context ${totalLabel}/${ctxLabel} (${pctLabel})`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (footer?.model && metrics?.model) {
|
|
220
|
+
const model = metrics.model.trim();
|
|
221
|
+
if (model) {
|
|
222
|
+
zhParts.push(model);
|
|
223
|
+
enParts.push(model);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { zh: zhParts, en: enParts };
|
|
227
|
+
}
|
|
146
228
|
// ---------------------------------------------------------------------------
|
|
147
229
|
// buildCardContent
|
|
148
230
|
// ---------------------------------------------------------------------------
|
|
@@ -166,6 +248,7 @@ function buildCardContent(state, data = {}) {
|
|
|
166
248
|
reasoningElapsedMs: data.reasoningElapsedMs,
|
|
167
249
|
isAborted: data.isAborted,
|
|
168
250
|
footer: data.footer,
|
|
251
|
+
footerMetrics: data.footerMetrics,
|
|
169
252
|
});
|
|
170
253
|
case 'confirm':
|
|
171
254
|
return buildConfirmCard(data.confirmData);
|
|
@@ -227,7 +310,7 @@ function buildStreamingCard(partialText, toolCalls, reasoningText) {
|
|
|
227
310
|
};
|
|
228
311
|
}
|
|
229
312
|
function buildCompleteCard(params) {
|
|
230
|
-
const { text, toolCalls, elapsedMs, isError, reasoningText, reasoningElapsedMs, isAborted, footer } = params;
|
|
313
|
+
const { text, toolCalls, elapsedMs, isError, reasoningText, reasoningElapsedMs, isAborted, footer, footerMetrics } = params;
|
|
231
314
|
const elements = [];
|
|
232
315
|
// Collapsible reasoning panel (before main content)
|
|
233
316
|
if (reasoningText) {
|
|
@@ -285,30 +368,16 @@ function buildCompleteCard(params) {
|
|
|
285
368
|
});
|
|
286
369
|
}
|
|
287
370
|
// Footer meta-info: each metadata item is independently controlled via
|
|
288
|
-
// the `footer` config.
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
enParts.push('Stopped');
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
zhParts.push('已完成');
|
|
302
|
-
enParts.push('Completed');
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (footer?.elapsed && elapsedMs != null) {
|
|
306
|
-
const d = formatElapsed(elapsedMs);
|
|
307
|
-
zhParts.push(`耗时 ${d}`);
|
|
308
|
-
enParts.push(`Elapsed ${d}`);
|
|
309
|
-
}
|
|
310
|
-
if (zhParts.length > 0) {
|
|
311
|
-
elements.push(...buildFooter(zhParts.join(' · '), enParts.join(' · '), isError));
|
|
371
|
+
// the `footer` config.
|
|
372
|
+
const footerParts = formatFooterRuntimeSegments({
|
|
373
|
+
footer,
|
|
374
|
+
metrics: footerMetrics,
|
|
375
|
+
elapsedMs,
|
|
376
|
+
isError,
|
|
377
|
+
isAborted,
|
|
378
|
+
});
|
|
379
|
+
if (footerParts.zh.length > 0) {
|
|
380
|
+
elements.push(...buildFooter(footerParts.zh.join(' · '), footerParts.en.join(' · '), isError));
|
|
312
381
|
}
|
|
313
382
|
// Use the answer text (not reasoning) as the feed preview summary.
|
|
314
383
|
// Strip markdown syntax so the preview reads as plain text.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Unified card API error handling.
|
|
6
|
+
*
|
|
7
|
+
* Provides structured error class for CardKit API responses, sub-error
|
|
8
|
+
* parsing for the generic 230099 code, and helper predicates used by
|
|
9
|
+
* reply-dispatcher and streaming-card-controller.
|
|
10
|
+
*/
|
|
11
|
+
/** 卡片 API 级别错误码。 */
|
|
12
|
+
export declare const CARD_ERROR: {
|
|
13
|
+
/** 发送频率限制 */
|
|
14
|
+
readonly RATE_LIMITED: 230020;
|
|
15
|
+
/** 卡片内容创建失败(通用码,需检查子错误) */
|
|
16
|
+
readonly CARD_CONTENT_FAILED: 230099;
|
|
17
|
+
};
|
|
18
|
+
/** 230099 子错误码,嵌套在 msg 的 ErrCode 字段中。 */
|
|
19
|
+
export declare const CARD_CONTENT_SUB_ERROR: {
|
|
20
|
+
/** 卡片元素(表格等)数量超限 */
|
|
21
|
+
readonly ELEMENT_LIMIT: 11310;
|
|
22
|
+
};
|
|
23
|
+
export declare const FEISHU_CARD_TABLE_LIMIT = 3;
|
|
24
|
+
export interface MarkdownTableMatch {
|
|
25
|
+
index: number;
|
|
26
|
+
length: number;
|
|
27
|
+
raw: string;
|
|
28
|
+
}
|
|
29
|
+
/** CardKit API 返回非零 code 时的结构化错误。 */
|
|
30
|
+
export declare class CardKitApiError extends Error {
|
|
31
|
+
readonly code: number;
|
|
32
|
+
readonly msg: string;
|
|
33
|
+
constructor(params: {
|
|
34
|
+
api: string;
|
|
35
|
+
code: number;
|
|
36
|
+
msg: string;
|
|
37
|
+
context: string;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 从 msg 字符串中提取子错误码。
|
|
42
|
+
*
|
|
43
|
+
* 示例输入: "Failed to create card content, ext=ErrCode: 11310; ErrMsg: element exceeds the limit; code:230099"
|
|
44
|
+
* 返回 11310 或 null。
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractSubCode(msg: string): number | null;
|
|
47
|
+
/**
|
|
48
|
+
* 从任意抛错对象中解析卡片 API 错误结构。
|
|
49
|
+
*
|
|
50
|
+
* 返回 { code, subCode, errMsg },如果无法提取 code 则返回 null。
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseCardApiError(err: unknown): {
|
|
53
|
+
code: number;
|
|
54
|
+
subCode: number | null;
|
|
55
|
+
errMsg: string;
|
|
56
|
+
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* 判断错误是否为卡片表格数量超限。
|
|
59
|
+
*
|
|
60
|
+
* 匹配条件:code 230099 + subCode 11310 + errMsg 含 "table number over limit"。
|
|
61
|
+
* 11310 是通用的元素超限码(也覆盖模板可见性、组件上限等),
|
|
62
|
+
* 必须同时检查 errMsg 确认是表格数量导致的。
|
|
63
|
+
*
|
|
64
|
+
* 实际错误格式(生产日志 2026-03-13):
|
|
65
|
+
* "Failed to create card content, ext=ErrCode: 11310; ErrMsg: card table number over limit; ErrorValue: table; "
|
|
66
|
+
*/
|
|
67
|
+
export declare function isCardTableLimitError(err: unknown): boolean;
|
|
68
|
+
/** 判断错误是否为卡片发送频率限制(230020)。 */
|
|
69
|
+
export declare function isCardRateLimitError(err: unknown): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* 收集正文里可被飞书卡片实际渲染的 markdown 表格。
|
|
72
|
+
*
|
|
73
|
+
* 代码块里的示例表格不会被飞书解析成卡片表格元素,因此这里要先排除,
|
|
74
|
+
* 让 shouldUseCard() 预检和 sanitizeTextForCard() 降级逻辑使用同一份结果。
|
|
75
|
+
*/
|
|
76
|
+
export declare function findMarkdownTablesOutsideCodeBlocks(text: string): MarkdownTableMatch[];
|
|
77
|
+
/**
|
|
78
|
+
* 对多段 markdown 文本共享一个表格预算。
|
|
79
|
+
*
|
|
80
|
+
* 段落按数组顺序消耗额度,适合处理“reasoning + 正文”这类会被飞书
|
|
81
|
+
* 作为同一张卡片渲染的多块文本。
|
|
82
|
+
*/
|
|
83
|
+
export declare function sanitizeTextSegmentsForCard(texts: readonly string[], tableLimit?: number): string[];
|
|
84
|
+
/**
|
|
85
|
+
* 对正文中超出 tableLimit 的 markdown 表格降级为 code block,
|
|
86
|
+
* 避免飞书卡片因表格数超限触发 230099/11310。
|
|
87
|
+
*
|
|
88
|
+
* 前 tableLimit 张表格保持原样(可正常卡片渲染);
|
|
89
|
+
* 超出部分用反引号包裹,阻止飞书将其解析为卡片表格元素。
|
|
90
|
+
*/
|
|
91
|
+
export declare function sanitizeTextForCard(text: string, tableLimit?: number): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Unified card API error handling.
|
|
7
|
+
*
|
|
8
|
+
* Provides structured error class for CardKit API responses, sub-error
|
|
9
|
+
* parsing for the generic 230099 code, and helper predicates used by
|
|
10
|
+
* reply-dispatcher and streaming-card-controller.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.CardKitApiError = exports.FEISHU_CARD_TABLE_LIMIT = exports.CARD_CONTENT_SUB_ERROR = exports.CARD_ERROR = void 0;
|
|
14
|
+
exports.extractSubCode = extractSubCode;
|
|
15
|
+
exports.parseCardApiError = parseCardApiError;
|
|
16
|
+
exports.isCardTableLimitError = isCardTableLimitError;
|
|
17
|
+
exports.isCardRateLimitError = isCardRateLimitError;
|
|
18
|
+
exports.findMarkdownTablesOutsideCodeBlocks = findMarkdownTablesOutsideCodeBlocks;
|
|
19
|
+
exports.sanitizeTextSegmentsForCard = sanitizeTextSegmentsForCard;
|
|
20
|
+
exports.sanitizeTextForCard = sanitizeTextForCard;
|
|
21
|
+
const api_error_1 = require("../core/api-error.js");
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Error code constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** 卡片 API 级别错误码。 */
|
|
26
|
+
exports.CARD_ERROR = {
|
|
27
|
+
/** 发送频率限制 */
|
|
28
|
+
RATE_LIMITED: 230020,
|
|
29
|
+
/** 卡片内容创建失败(通用码,需检查子错误) */
|
|
30
|
+
CARD_CONTENT_FAILED: 230099,
|
|
31
|
+
};
|
|
32
|
+
/** 230099 子错误码,嵌套在 msg 的 ErrCode 字段中。 */
|
|
33
|
+
exports.CARD_CONTENT_SUB_ERROR = {
|
|
34
|
+
/** 卡片元素(表格等)数量超限 */
|
|
35
|
+
ELEMENT_LIMIT: 11310,
|
|
36
|
+
};
|
|
37
|
+
// 经验性的飞书卡片表格上限 -- 4+ 张触发 230099/11310(2026-03 实测)。
|
|
38
|
+
exports.FEISHU_CARD_TABLE_LIMIT = 3;
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Error class
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/** CardKit API 返回非零 code 时的结构化错误。 */
|
|
43
|
+
class CardKitApiError extends Error {
|
|
44
|
+
code;
|
|
45
|
+
msg;
|
|
46
|
+
constructor(params) {
|
|
47
|
+
const { api, code, msg, context } = params;
|
|
48
|
+
super(`cardkit ${api} FAILED: code=${code}, msg=${msg}, ${context}`);
|
|
49
|
+
this.name = 'CardKitApiError';
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.msg = msg;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.CardKitApiError = CardKitApiError;
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Sub-error extraction
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* 从 msg 字符串中提取子错误码。
|
|
60
|
+
*
|
|
61
|
+
* 示例输入: "Failed to create card content, ext=ErrCode: 11310; ErrMsg: element exceeds the limit; code:230099"
|
|
62
|
+
* 返回 11310 或 null。
|
|
63
|
+
*/
|
|
64
|
+
function extractSubCode(msg) {
|
|
65
|
+
const match = /ErrCode:\s*(\d+)/.exec(msg);
|
|
66
|
+
if (!match)
|
|
67
|
+
return null;
|
|
68
|
+
const code = Number(match[1]);
|
|
69
|
+
return Number.isFinite(code) ? code : null;
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Structured error parsing
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
/**
|
|
75
|
+
* 从任意抛错对象中解析卡片 API 错误结构。
|
|
76
|
+
*
|
|
77
|
+
* 返回 { code, subCode, errMsg },如果无法提取 code 则返回 null。
|
|
78
|
+
*/
|
|
79
|
+
function parseCardApiError(err) {
|
|
80
|
+
const code = (0, api_error_1.extractLarkApiCode)(err);
|
|
81
|
+
if (code === undefined)
|
|
82
|
+
return null;
|
|
83
|
+
// 按优先级提取 msg 文本
|
|
84
|
+
let errMsg = '';
|
|
85
|
+
if (err && typeof err === 'object') {
|
|
86
|
+
const e = err;
|
|
87
|
+
if (typeof e.msg === 'string') {
|
|
88
|
+
errMsg = e.msg;
|
|
89
|
+
}
|
|
90
|
+
else if (typeof e.response?.data?.msg === 'string') {
|
|
91
|
+
// Axios errors: response.data.msg carries the Feishu detail with ErrCode
|
|
92
|
+
errMsg = e.response.data.msg;
|
|
93
|
+
}
|
|
94
|
+
else if (typeof e.message === 'string') {
|
|
95
|
+
// Fallback to generic Error.message (e.g. CardKitApiError)
|
|
96
|
+
errMsg = e.message;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const subCode = extractSubCode(errMsg);
|
|
100
|
+
return { code, subCode, errMsg };
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Helper predicates
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
/**
|
|
106
|
+
* 判断错误是否为卡片表格数量超限。
|
|
107
|
+
*
|
|
108
|
+
* 匹配条件:code 230099 + subCode 11310 + errMsg 含 "table number over limit"。
|
|
109
|
+
* 11310 是通用的元素超限码(也覆盖模板可见性、组件上限等),
|
|
110
|
+
* 必须同时检查 errMsg 确认是表格数量导致的。
|
|
111
|
+
*
|
|
112
|
+
* 实际错误格式(生产日志 2026-03-13):
|
|
113
|
+
* "Failed to create card content, ext=ErrCode: 11310; ErrMsg: card table number over limit; ErrorValue: table; "
|
|
114
|
+
*/
|
|
115
|
+
function isCardTableLimitError(err) {
|
|
116
|
+
const parsed = parseCardApiError(err);
|
|
117
|
+
if (!parsed)
|
|
118
|
+
return false;
|
|
119
|
+
return (parsed.code === exports.CARD_ERROR.CARD_CONTENT_FAILED &&
|
|
120
|
+
parsed.subCode === exports.CARD_CONTENT_SUB_ERROR.ELEMENT_LIMIT &&
|
|
121
|
+
/table number over limit/i.test(parsed.errMsg));
|
|
122
|
+
}
|
|
123
|
+
/** 判断错误是否为卡片发送频率限制(230020)。 */
|
|
124
|
+
function isCardRateLimitError(err) {
|
|
125
|
+
const parsed = parseCardApiError(err);
|
|
126
|
+
if (!parsed)
|
|
127
|
+
return false;
|
|
128
|
+
return parsed.code === exports.CARD_ERROR.RATE_LIMITED;
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Text sanitization
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* 收集正文里可被飞书卡片实际渲染的 markdown 表格。
|
|
135
|
+
*
|
|
136
|
+
* 代码块里的示例表格不会被飞书解析成卡片表格元素,因此这里要先排除,
|
|
137
|
+
* 让 shouldUseCard() 预检和 sanitizeTextForCard() 降级逻辑使用同一份结果。
|
|
138
|
+
*/
|
|
139
|
+
function findMarkdownTablesOutsideCodeBlocks(text) {
|
|
140
|
+
const codeBlockRanges = [];
|
|
141
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
142
|
+
let codeBlockMatch = codeBlockRegex.exec(text);
|
|
143
|
+
while (codeBlockMatch !== null) {
|
|
144
|
+
codeBlockRanges.push({
|
|
145
|
+
start: codeBlockMatch.index,
|
|
146
|
+
end: codeBlockMatch.index + codeBlockMatch[0].length,
|
|
147
|
+
});
|
|
148
|
+
codeBlockMatch = codeBlockRegex.exec(text);
|
|
149
|
+
}
|
|
150
|
+
const isInsideCodeBlock = (idx) => codeBlockRanges.some((range) => idx >= range.start && idx < range.end);
|
|
151
|
+
const tableRegex = /\|.+\|[\r\n]+\|[-:| ]+\|[\s\S]*?(?=\n\n|\n(?!\|)|$)/g;
|
|
152
|
+
const matches = [];
|
|
153
|
+
let tableMatch = tableRegex.exec(text);
|
|
154
|
+
while (tableMatch !== null) {
|
|
155
|
+
if (!isInsideCodeBlock(tableMatch.index)) {
|
|
156
|
+
matches.push({
|
|
157
|
+
index: tableMatch.index,
|
|
158
|
+
length: tableMatch[0].length,
|
|
159
|
+
raw: tableMatch[0],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
tableMatch = tableRegex.exec(text);
|
|
163
|
+
}
|
|
164
|
+
return matches;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 对多段 markdown 文本共享一个表格预算。
|
|
168
|
+
*
|
|
169
|
+
* 段落按数组顺序消耗额度,适合处理“reasoning + 正文”这类会被飞书
|
|
170
|
+
* 作为同一张卡片渲染的多块文本。
|
|
171
|
+
*/
|
|
172
|
+
function sanitizeTextSegmentsForCard(texts, tableLimit = exports.FEISHU_CARD_TABLE_LIMIT) {
|
|
173
|
+
let remainingTableBudget = tableLimit;
|
|
174
|
+
return texts.map((text) => {
|
|
175
|
+
const matches = findMarkdownTablesOutsideCodeBlocks(text);
|
|
176
|
+
if (matches.length <= remainingTableBudget) {
|
|
177
|
+
remainingTableBudget -= matches.length;
|
|
178
|
+
return text;
|
|
179
|
+
}
|
|
180
|
+
const sanitizedText = wrapTablesBeyondLimit(text, matches, Math.max(remainingTableBudget, 0));
|
|
181
|
+
remainingTableBudget = 0;
|
|
182
|
+
return sanitizedText;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 对正文中超出 tableLimit 的 markdown 表格降级为 code block,
|
|
187
|
+
* 避免飞书卡片因表格数超限触发 230099/11310。
|
|
188
|
+
*
|
|
189
|
+
* 前 tableLimit 张表格保持原样(可正常卡片渲染);
|
|
190
|
+
* 超出部分用反引号包裹,阻止飞书将其解析为卡片表格元素。
|
|
191
|
+
*/
|
|
192
|
+
function sanitizeTextForCard(text, tableLimit = exports.FEISHU_CARD_TABLE_LIMIT) {
|
|
193
|
+
return sanitizeTextSegmentsForCard([text], tableLimit)[0];
|
|
194
|
+
}
|
|
195
|
+
function wrapTablesBeyondLimit(text, matches, keepCount) {
|
|
196
|
+
if (matches.length <= keepCount)
|
|
197
|
+
return text;
|
|
198
|
+
// Back-to-front replacement keeps the original indices stable.
|
|
199
|
+
let result = text;
|
|
200
|
+
for (let i = matches.length - 1; i >= keepCount; i--) {
|
|
201
|
+
const { index, length, raw } = matches[i];
|
|
202
|
+
const replacement = `\`\`\`\n${raw}\n\`\`\``;
|
|
203
|
+
result = result.slice(0, index) + replacement + result.slice(index + length);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
package/src/card/cardkit.js
CHANGED
|
@@ -14,8 +14,9 @@ exports.sendCardByCardId = sendCardByCardId;
|
|
|
14
14
|
exports.setCardStreamingMode = setCardStreamingMode;
|
|
15
15
|
const lark_client_1 = require("../core/lark-client.js");
|
|
16
16
|
const lark_logger_1 = require("../core/lark-logger.js");
|
|
17
|
-
const targets_1 = require("../core/targets.js");
|
|
18
17
|
const message_unavailable_1 = require("../core/message-unavailable.js");
|
|
18
|
+
const targets_1 = require("../core/targets.js");
|
|
19
|
+
const card_error_1 = require("./card-error.js");
|
|
19
20
|
const log = (0, lark_logger_1.larkLogger)('card/cardkit');
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Helpers
|
|
@@ -31,8 +32,13 @@ function logCardKitResponse(params) {
|
|
|
31
32
|
const { code, msg } = resp;
|
|
32
33
|
log.info(`cardkit ${api} response`, { code, msg, context });
|
|
33
34
|
if (code && code !== 0) {
|
|
34
|
-
log.warn(`cardkit ${api} FAILED`, {
|
|
35
|
-
|
|
35
|
+
log.warn(`cardkit ${api} FAILED`, {
|
|
36
|
+
code,
|
|
37
|
+
msg,
|
|
38
|
+
context,
|
|
39
|
+
fullResponse: resp,
|
|
40
|
+
});
|
|
41
|
+
throw new card_error_1.CardKitApiError({ api, code, msg: msg ?? '', context });
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
// ---------------------------------------------------------------------------
|
|
@@ -56,7 +62,11 @@ async function createCardEntity(params) {
|
|
|
56
62
|
}));
|
|
57
63
|
// 兼容不同 SDK 包装层:优先 data.card_id,回退顶层 card_id
|
|
58
64
|
const cardId = (response.data?.card_id ?? response.card_id) ?? null;
|
|
59
|
-
logCardKitResponse({
|
|
65
|
+
logCardKitResponse({
|
|
66
|
+
resp: response,
|
|
67
|
+
api: 'card.create',
|
|
68
|
+
context: `cardId=${cardId}`,
|
|
69
|
+
});
|
|
60
70
|
return cardId;
|
|
61
71
|
}
|
|
62
72
|
/**
|
|
@@ -136,7 +146,11 @@ async function sendCardByCardId(params) {
|
|
|
136
146
|
operation: 'im.message.reply(interactive.cardkit)',
|
|
137
147
|
fn: () => client.im.message.reply({
|
|
138
148
|
path: { message_id: normalizedId },
|
|
139
|
-
data: {
|
|
149
|
+
data: {
|
|
150
|
+
content: contentPayload,
|
|
151
|
+
msg_type: 'interactive',
|
|
152
|
+
reply_in_thread: replyInThread,
|
|
153
|
+
},
|
|
140
154
|
}),
|
|
141
155
|
});
|
|
142
156
|
return {
|
|
@@ -70,6 +70,7 @@ export declare const EMPTY_REPLY_FALLBACK_TEXT = "Done.";
|
|
|
70
70
|
export interface CreateFeishuReplyDispatcherParams {
|
|
71
71
|
cfg: ClawdbotConfig;
|
|
72
72
|
agentId: string;
|
|
73
|
+
sessionKey: string;
|
|
73
74
|
chatId: string;
|
|
74
75
|
replyToMessageId?: string;
|
|
75
76
|
/** Account ID for multi-account support. */
|
|
@@ -110,8 +111,19 @@ export interface FeishuReplyDispatcherResult {
|
|
|
110
111
|
markFullyComplete: () => void;
|
|
111
112
|
abortCard: () => Promise<void>;
|
|
112
113
|
}
|
|
114
|
+
export interface FooterSessionMetrics {
|
|
115
|
+
inputTokens?: number;
|
|
116
|
+
outputTokens?: number;
|
|
117
|
+
cacheRead?: number;
|
|
118
|
+
cacheWrite?: number;
|
|
119
|
+
totalTokens?: number;
|
|
120
|
+
totalTokensFresh?: boolean;
|
|
121
|
+
contextTokens?: number;
|
|
122
|
+
model?: string;
|
|
123
|
+
}
|
|
113
124
|
export interface StreamingCardDeps {
|
|
114
125
|
cfg: ClawdbotConfig;
|
|
126
|
+
sessionKey: string;
|
|
115
127
|
accountId: string | undefined;
|
|
116
128
|
chatId: string;
|
|
117
129
|
replyToMessageId: string | undefined;
|