@openclaw/feishu 2026.3.13 → 2026.5.1-beta.1

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 (188) hide show
  1. package/api.ts +31 -0
  2. package/channel-entry.ts +20 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +16 -0
  5. package/index.ts +70 -53
  6. package/openclaw.plugin.json +1653 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1252 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +84 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +365 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +32 -94
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +119 -20
  72. package/src/directory.ts +61 -91
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +375 -26
  91. package/src/media.ts +434 -88
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
  114. package/src/monitor.startup.test.ts +11 -9
  115. package/src/monitor.startup.ts +26 -16
  116. package/src/monitor.state.ts +20 -5
  117. package/src/monitor.synthetic-error.ts +18 -0
  118. package/src/monitor.test-mocks.ts +2 -2
  119. package/src/monitor.transport.ts +220 -60
  120. package/src/monitor.ts +15 -10
  121. package/src/monitor.webhook-e2e.test.ts +65 -7
  122. package/src/monitor.webhook-security.test.ts +122 -0
  123. package/src/monitor.webhook.test-helpers.ts +44 -26
  124. package/src/outbound-runtime-api.ts +1 -0
  125. package/src/outbound.test.ts +616 -37
  126. package/src/outbound.ts +623 -81
  127. package/src/perm-schema.ts +1 -1
  128. package/src/perm.ts +1 -7
  129. package/src/pins.ts +108 -0
  130. package/src/policy.test.ts +297 -117
  131. package/src/policy.ts +142 -29
  132. package/src/post.ts +7 -6
  133. package/src/probe.test.ts +14 -9
  134. package/src/probe.ts +26 -16
  135. package/src/processing-claims.ts +59 -0
  136. package/src/qr-terminal.ts +1 -0
  137. package/src/reactions.ts +4 -34
  138. package/src/reasoning-preview.test.ts +59 -0
  139. package/src/reasoning-preview.ts +20 -0
  140. package/src/reply-dispatcher-runtime-api.ts +7 -0
  141. package/src/reply-dispatcher.test.ts +660 -29
  142. package/src/reply-dispatcher.ts +407 -154
  143. package/src/runtime.ts +6 -3
  144. package/src/secret-contract.ts +145 -0
  145. package/src/secret-input.ts +1 -13
  146. package/src/security-audit-shared.ts +69 -0
  147. package/src/security-audit.test.ts +61 -0
  148. package/src/security-audit.ts +1 -0
  149. package/src/send-result.ts +1 -1
  150. package/src/send-target.test.ts +9 -3
  151. package/src/send-target.ts +10 -4
  152. package/src/send.reply-fallback.test.ts +77 -2
  153. package/src/send.test.ts +386 -4
  154. package/src/send.ts +399 -86
  155. package/src/sequential-key.test.ts +72 -0
  156. package/src/sequential-key.ts +28 -0
  157. package/src/sequential-queue.test.ts +92 -0
  158. package/src/sequential-queue.ts +16 -0
  159. package/src/session-conversation.ts +42 -0
  160. package/src/session-route.ts +48 -0
  161. package/src/setup-core.ts +51 -0
  162. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  163. package/src/setup-surface.ts +581 -0
  164. package/src/streaming-card.test.ts +138 -2
  165. package/src/streaming-card.ts +134 -18
  166. package/src/subagent-hooks.test.ts +603 -0
  167. package/src/subagent-hooks.ts +397 -0
  168. package/src/targets.ts +3 -13
  169. package/src/test-support/lifecycle-test-support.ts +479 -0
  170. package/src/thread-bindings.test.ts +143 -0
  171. package/src/thread-bindings.ts +330 -0
  172. package/src/tool-account-routing.test.ts +66 -8
  173. package/src/tool-account.test.ts +44 -0
  174. package/src/tool-account.ts +40 -17
  175. package/src/tool-factory-test-harness.ts +11 -8
  176. package/src/tool-result.ts +3 -1
  177. package/src/tools-config.ts +1 -1
  178. package/src/types.ts +16 -15
  179. package/src/typing.ts +10 -6
  180. package/src/wiki-schema.ts +1 -1
  181. package/src/wiki.ts +1 -7
  182. package/subagent-hooks-api.ts +31 -0
  183. package/tsconfig.json +16 -0
  184. package/src/feishu-command-handler.ts +0 -59
  185. package/src/onboarding.status.test.ts +0 -25
  186. package/src/onboarding.ts +0 -489
  187. package/src/send-message.ts +0 -71
  188. package/src/targets.test.ts +0 -70
package/src/docx.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { existsSync, promises as fs } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { isAbsolute } from "node:path";
3
+ import { isAbsolute, resolve } from "node:path";
4
4
  import { basename } from "node:path";
5
5
  import type * as Lark from "@larksuiteoapi/node-sdk";
6
- import { Type } from "@sinclair/typebox";
7
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
6
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
8
+ import { Type } from "typebox";
9
+ import type { OpenClawPluginApi } from "../runtime-api.js";
8
10
  import { listEnabledFeishuAccounts } from "./accounts.js";
9
11
  import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
10
12
  import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js";
@@ -17,6 +19,7 @@ import {
17
19
  deleteTableColumns,
18
20
  mergeTableCells,
19
21
  } from "./docx-table-ops.js";
22
+ import type { FeishuDocxBlock, FeishuDocxBlockChild } from "./docx-types.js";
20
23
  import { getFeishuRuntime } from "./runtime.js";
21
24
  import {
22
25
  createFeishuToolClient,
@@ -33,6 +36,24 @@ function json(data: unknown) {
33
36
  };
34
37
  }
35
38
 
39
+ function resolveDocToolLocalRoots(ctx: {
40
+ workspaceDir?: string;
41
+ fsPolicy?: { workspaceOnly: boolean };
42
+ }): string[] | undefined {
43
+ if (ctx.fsPolicy?.workspaceOnly !== true) {
44
+ return undefined;
45
+ }
46
+ const workspaceDir = ctx.workspaceDir?.trim();
47
+ // Fail closed: workspace-only with no resolved workspace must not fall back
48
+ // to default managed roots.
49
+ if (!workspaceDir) {
50
+ return [];
51
+ }
52
+ // Workspace paths are expected to be absolute; resolve() normalizes any
53
+ // accidental relative input before passing roots to loadWebMedia.
54
+ return [resolve(workspaceDir)];
55
+ }
56
+
36
57
  /** Extract image URLs from markdown content */
37
58
  function extractImageUrls(markdown: string): string[] {
38
59
  const regex = /!\[[^\]]*\]\(([^)]+)\)/g;
@@ -72,8 +93,10 @@ const BLOCK_TYPE_NAMES: Record<number, string> = {
72
93
  const UNSUPPORTED_CREATE_TYPES = new Set([31, 32]);
73
94
 
74
95
  /** Clean blocks for insertion (remove unsupported types and read-only fields) */
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
76
- function cleanBlocksForInsert(blocks: any[]): { cleaned: any[]; skipped: string[] } {
96
+ function cleanBlocksForInsert(blocks: FeishuDocxBlock[]): {
97
+ cleaned: FeishuDocxBlock[];
98
+ skipped: string[];
99
+ } {
77
100
  const skipped: string[] = [];
78
101
  const cleaned = blocks
79
102
  .filter((block) => {
@@ -87,7 +110,7 @@ function cleanBlocksForInsert(blocks: any[]): { cleaned: any[]; skipped: string[
87
110
  .map((block) => {
88
111
  if (block.block_type === 31 && block.table?.merge_info) {
89
112
  const { merge_info: _merge_info, ...tableRest } = block.table;
90
- return { ...block, table: tableRest };
113
+ return Object.assign({}, block, { table: tableRest });
91
114
  }
92
115
  return block;
93
116
  });
@@ -113,23 +136,148 @@ async function convertMarkdown(client: Lark.Client, markdown: string) {
113
136
  };
114
137
  }
115
138
 
116
- function sortBlocksByFirstLevel(blocks: any[], firstLevelIds: string[]): any[] {
117
- if (!firstLevelIds || firstLevelIds.length === 0) return blocks;
118
- const sorted = firstLevelIds.map((id) => blocks.find((b) => b.block_id === id)).filter(Boolean);
119
- const sortedIds = new Set(firstLevelIds);
120
- const remaining = blocks.filter((b) => !sortedIds.has(b.block_id));
121
- return [...sorted, ...remaining];
139
+ function normalizeChildIds(children: unknown): string[] {
140
+ if (Array.isArray(children)) {
141
+ return children.filter((child): child is string => typeof child === "string");
142
+ }
143
+ if (typeof children === "string") {
144
+ return [children];
145
+ }
146
+ return [];
147
+ }
148
+
149
+ type DocxChildrenCreatePayload = NonNullable<
150
+ Parameters<Lark.Client["docx"]["documentBlockChildren"]["create"]>[0]
151
+ >;
152
+ type DocxChildrenCreateChild = NonNullable<
153
+ NonNullable<DocxChildrenCreatePayload["data"]>["children"]
154
+ >[number];
155
+ type DocxDescendantCreatePayload = NonNullable<
156
+ Parameters<Lark.Client["docx"]["documentBlockDescendant"]["create"]>[0]
157
+ >;
158
+ type DocxDescendantCreateBlock = NonNullable<
159
+ NonNullable<DocxDescendantCreatePayload["data"]>["descendants"]
160
+ >[number];
161
+ type DriveMediaUploadAllPayload = NonNullable<
162
+ Parameters<Lark.Client["drive"]["media"]["uploadAll"]>[0]
163
+ >;
164
+ type DriveMediaUploadFile = NonNullable<NonNullable<DriveMediaUploadAllPayload["data"]>["file"]>;
165
+
166
+ function toCreateChildBlock(block: FeishuDocxBlock): DocxChildrenCreateChild {
167
+ return block as DocxChildrenCreateChild;
168
+ }
169
+
170
+ function toDescendantBlock(block: FeishuDocxBlock): DocxDescendantCreateBlock {
171
+ const children = normalizeChildIds(block.children);
172
+ return {
173
+ ...(block.block_id ? { block_id: block.block_id } : {}),
174
+ ...(children.length > 0 ? { children } : {}),
175
+ ...block,
176
+ } as DocxDescendantCreateBlock;
177
+ }
178
+
179
+ function normalizeInsertedChildBlocks(
180
+ children: string[] | FeishuDocxBlockChild[] | undefined,
181
+ ): FeishuDocxBlockChild[] {
182
+ if (!Array.isArray(children)) {
183
+ return [];
184
+ }
185
+ return children.filter(
186
+ (child): child is FeishuDocxBlockChild => typeof child === "object" && child !== null,
187
+ );
188
+ }
189
+
190
+ // Convert API may return `blocks` in a non-render order.
191
+ // Reconstruct the document tree using first_level_block_ids plus children/parent links,
192
+ // then emit blocks in pre-order so Descendant/Children APIs receive one normalized tree contract.
193
+ function normalizeConvertedBlockTree(
194
+ blocks: FeishuDocxBlock[],
195
+ firstLevelIds: string[],
196
+ ): { orderedBlocks: FeishuDocxBlock[]; rootIds: string[] } {
197
+ if (blocks.length <= 1) {
198
+ const rootIds =
199
+ blocks.length === 1 && typeof blocks[0]?.block_id === "string" ? [blocks[0].block_id] : [];
200
+ return { orderedBlocks: blocks, rootIds };
201
+ }
202
+
203
+ const byId = new Map<string, FeishuDocxBlock>();
204
+ const originalOrder = new Map<string, number>();
205
+ for (const [index, block] of blocks.entries()) {
206
+ if (typeof block?.block_id === "string") {
207
+ byId.set(block.block_id, block);
208
+ originalOrder.set(block.block_id, index);
209
+ }
210
+ }
211
+
212
+ const childIds = new Set<string>();
213
+ for (const block of blocks) {
214
+ for (const childId of normalizeChildIds(block?.children)) {
215
+ childIds.add(childId);
216
+ }
217
+ }
218
+
219
+ const inferredTopLevelIds = blocks
220
+ .filter((block) => {
221
+ const blockId = block?.block_id;
222
+ if (typeof blockId !== "string") {
223
+ return false;
224
+ }
225
+ const parentId = typeof block?.parent_id === "string" ? block.parent_id : "";
226
+ return !childIds.has(blockId) && (!parentId || !byId.has(parentId));
227
+ })
228
+ .toSorted(
229
+ (a, b) =>
230
+ (originalOrder.get(a.block_id ?? "__missing__") ?? 0) -
231
+ (originalOrder.get(b.block_id ?? "__missing__") ?? 0),
232
+ )
233
+ .map((block) => block.block_id)
234
+ .filter((blockId): blockId is string => typeof blockId === "string");
235
+
236
+ const rootIds = (
237
+ firstLevelIds && firstLevelIds.length > 0 ? firstLevelIds : inferredTopLevelIds
238
+ ).filter((id, index, arr) => typeof id === "string" && byId.has(id) && arr.indexOf(id) === index);
239
+
240
+ const orderedBlocks: FeishuDocxBlock[] = [];
241
+ const visited = new Set<string>();
242
+
243
+ const visit = (blockId: string) => {
244
+ if (!byId.has(blockId) || visited.has(blockId)) {
245
+ return;
246
+ }
247
+ visited.add(blockId);
248
+ const block = byId.get(blockId);
249
+ if (!block) {
250
+ return;
251
+ }
252
+ orderedBlocks.push(block);
253
+ for (const childId of normalizeChildIds(block?.children)) {
254
+ visit(childId);
255
+ }
256
+ };
257
+
258
+ for (const rootId of rootIds) {
259
+ visit(rootId);
260
+ }
261
+
262
+ // Fallback for malformed/partial trees from Convert API: keep any leftovers in original order.
263
+ for (const block of blocks) {
264
+ if (typeof block?.block_id === "string") {
265
+ visit(block.block_id);
266
+ } else {
267
+ orderedBlocks.push(block);
268
+ }
269
+ }
270
+
271
+ return { orderedBlocks, rootIds: rootIds.filter((id): id is string => typeof id === "string") };
122
272
  }
123
273
 
124
- /* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
125
274
  async function insertBlocks(
126
275
  client: Lark.Client,
127
276
  docToken: string,
128
- blocks: any[],
277
+ blocks: FeishuDocxBlock[],
129
278
  parentBlockId?: string,
130
279
  index?: number,
131
- ): Promise<{ children: any[]; skipped: string[] }> {
132
- /* eslint-enable @typescript-eslint/no-explicit-any */
280
+ ): Promise<{ children: FeishuDocxBlockChild[]; skipped: string[] }> {
133
281
  const { cleaned, skipped } = cleanBlocksForInsert(blocks);
134
282
  const blockId = parentBlockId ?? docToken;
135
283
 
@@ -141,12 +289,12 @@ async function insertBlocks(
141
289
  // The batch API (sending all children at once) does not guarantee ordering
142
290
  // because Feishu processes the batch asynchronously. Sequential single-block
143
291
  // inserts (each appended to the end) produce deterministic results.
144
- const allInserted: any[] = [];
292
+ const allInserted: FeishuDocxBlockChild[] = [];
145
293
  for (const [offset, block] of cleaned.entries()) {
146
294
  const res = await client.docx.documentBlockChildren.create({
147
295
  path: { document_id: docToken, block_id: blockId },
148
296
  data: {
149
- children: [block],
297
+ children: [toCreateChildBlock(block)],
150
298
  ...(index !== undefined ? { index: index + offset } : {}),
151
299
  },
152
300
  });
@@ -240,8 +388,7 @@ async function convertMarkdownWithFallback(client: Lark.Client, markdown: string
240
388
  throw error;
241
389
  }
242
390
 
243
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
244
- const blocks: any[] = [];
391
+ const blocks: FeishuDocxBlock[] = [];
245
392
  const firstLevelBlockIds: string[] = [];
246
393
 
247
394
  for (const chunk of chunks) {
@@ -257,29 +404,25 @@ async function convertMarkdownWithFallback(client: Lark.Client, markdown: string
257
404
  /** Convert markdown in chunks to avoid document.convert content size limits */
258
405
  async function chunkedConvertMarkdown(client: Lark.Client, markdown: string) {
259
406
  const chunks = splitMarkdownByHeadings(markdown);
260
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
261
- const allBlocks: any[] = [];
262
- const allFirstLevelBlockIds: string[] = [];
407
+ const allBlocks: FeishuDocxBlock[] = [];
408
+ const allRootIds: string[] = [];
263
409
  for (const chunk of chunks) {
264
410
  const { blocks, firstLevelBlockIds } = await convertMarkdownWithFallback(client, chunk);
265
- const sorted = sortBlocksByFirstLevel(blocks, firstLevelBlockIds);
266
- allBlocks.push(...sorted);
267
- allFirstLevelBlockIds.push(...firstLevelBlockIds);
411
+ const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
412
+ allBlocks.push(...orderedBlocks);
413
+ allRootIds.push(...rootIds);
268
414
  }
269
- return { blocks: allBlocks, firstLevelBlockIds: allFirstLevelBlockIds };
415
+ return { blocks: allBlocks, firstLevelBlockIds: allRootIds };
270
416
  }
271
417
 
272
418
  /** Insert blocks in batches of MAX_BLOCKS_PER_INSERT to avoid API 400 errors */
273
- /* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
274
- async function chunkedInsertBlocks(
419
+ async function _chunkedInsertBlocks(
275
420
  client: Lark.Client,
276
421
  docToken: string,
277
- blocks: any[],
422
+ blocks: FeishuDocxBlock[],
278
423
  parentBlockId?: string,
279
- ): Promise<{ children: any[]; skipped: string[] }> {
280
- /* eslint-enable @typescript-eslint/no-explicit-any */
281
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
282
- const allChildren: any[] = [];
424
+ ): Promise<{ children: FeishuDocxBlockChild[]; skipped: string[] }> {
425
+ const allChildren: FeishuDocxBlockChild[] = [];
283
426
  const allSkipped: string[] = [];
284
427
 
285
428
  for (let i = 0; i < blocks.length; i += MAX_BLOCKS_PER_INSERT) {
@@ -301,15 +444,13 @@ type Logger = { info?: (msg: string) => void };
301
444
  * @param parentBlockId - Parent block to insert into (defaults to docToken = document root)
302
445
  * @param index - Position within parent's children (-1 = end, 0 = first)
303
446
  */
304
- /* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
305
447
  async function insertBlocksWithDescendant(
306
448
  client: Lark.Client,
307
449
  docToken: string,
308
- blocks: any[],
450
+ blocks: FeishuDocxBlock[],
309
451
  firstLevelBlockIds: string[],
310
452
  { parentBlockId = docToken, index = -1 }: { parentBlockId?: string; index?: number } = {},
311
- ): Promise<{ children: any[] }> {
312
- /* eslint-enable @typescript-eslint/no-explicit-any */
453
+ ): Promise<{ children: FeishuDocxBlockChild[] }> {
313
454
  const descendants = cleanBlocksForDescendant(blocks);
314
455
  if (descendants.length === 0) {
315
456
  return { children: [] };
@@ -317,7 +458,11 @@ async function insertBlocksWithDescendant(
317
458
 
318
459
  const res = await client.docx.documentBlockDescendant.create({
319
460
  path: { document_id: docToken, block_id: parentBlockId },
320
- data: { children_id: firstLevelBlockIds, descendants, index },
461
+ data: {
462
+ children_id: firstLevelBlockIds,
463
+ descendants: descendants.map(toDescendantBlock),
464
+ index,
465
+ },
321
466
  });
322
467
 
323
468
  if (res.code !== 0) {
@@ -369,8 +514,7 @@ async function uploadImageToDocx(
369
514
  // Pass Buffer directly so form-data can calculate Content-Length correctly.
370
515
  // Readable.from() produces a stream with unknown length, causing Content-Length
371
516
  // mismatch that silently truncates uploads for images larger than ~1KB.
372
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK file type
373
- file: imageBuffer as any,
517
+ file: imageBuffer as DriveMediaUploadFile,
374
518
  // Required when the document block belongs to a non-default datacenter:
375
519
  // tells the drive service which document the block belongs to for routing.
376
520
  // Per API docs: certain upload scenarios require the cloud document token.
@@ -394,6 +538,7 @@ async function resolveUploadInput(
394
538
  url: string | undefined,
395
539
  filePath: string | undefined,
396
540
  maxBytes: number,
541
+ localRoots?: readonly string[],
397
542
  explicitFileName?: string,
398
543
  imageInput?: string, // data URI, plain base64, or local path
399
544
  ): Promise<{ buffer: Buffer; fileName: string }> {
@@ -458,11 +603,14 @@ async function resolveUploadInput(
458
603
  const absolutePath = isAbsolute(imageInput);
459
604
 
460
605
  if (unambiguousPath || (absolutePath && existsSync(candidate))) {
461
- const buffer = await fs.readFile(candidate);
462
- if (buffer.length > maxBytes) {
463
- throw new Error(`Local file exceeds limit: ${buffer.length} bytes > ${maxBytes} bytes`);
464
- }
465
- return { buffer, fileName: explicitFileName ?? basename(candidate) };
606
+ // Use loadWebMedia to enforce localRoots sandbox (same as sendMediaFeishu).
607
+ const resolvedPath = resolve(candidate);
608
+ const loaded = await getFeishuRuntime().media.loadWebMedia(resolvedPath, {
609
+ maxBytes,
610
+ optimizeImages: false,
611
+ localRoots,
612
+ });
613
+ return { buffer: loaded.buffer, fileName: explicitFileName ?? basename(candidate) };
466
614
  }
467
615
 
468
616
  if (absolutePath && !existsSync(candidate)) {
@@ -516,25 +664,26 @@ async function resolveUploadInput(
516
664
  };
517
665
  }
518
666
 
519
- const buffer = await fs.readFile(filePath!);
520
- if (buffer.length > maxBytes) {
521
- throw new Error(`Local file exceeds limit: ${buffer.length} bytes > ${maxBytes} bytes`);
522
- }
667
+ // Use loadWebMedia to enforce localRoots sandbox (same as sendMediaFeishu).
668
+ const resolvedFilePath = resolve(filePath!);
669
+ const loaded = await getFeishuRuntime().media.loadWebMedia(resolvedFilePath, {
670
+ maxBytes,
671
+ optimizeImages: false,
672
+ localRoots,
673
+ });
523
674
  return {
524
- buffer,
675
+ buffer: loaded.buffer,
525
676
  fileName: explicitFileName || basename(filePath!),
526
677
  };
527
678
  }
528
679
 
529
- /* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
530
680
  async function processImages(
531
681
  client: Lark.Client,
532
682
  docToken: string,
533
683
  markdown: string,
534
- insertedBlocks: any[],
684
+ insertedBlocks: FeishuDocxBlockChild[],
535
685
  maxBytes: number,
536
686
  ): Promise<number> {
537
- /* eslint-enable @typescript-eslint/no-explicit-any */
538
687
  const imageUrls = extractImageUrls(markdown);
539
688
  if (imageUrls.length === 0) {
540
689
  return 0;
@@ -545,7 +694,10 @@ async function processImages(
545
694
  let processed = 0;
546
695
  for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) {
547
696
  const url = imageUrls[i];
548
- const blockId = imageBlocks[i].block_id;
697
+ const blockId = imageBlocks[i]?.block_id;
698
+ if (!blockId) {
699
+ continue;
700
+ }
549
701
 
550
702
  try {
551
703
  const buffer = await downloadImage(url, maxBytes);
@@ -573,6 +725,7 @@ async function uploadImageBlock(
573
725
  client: Lark.Client,
574
726
  docToken: string,
575
727
  maxBytes: number,
728
+ localRoots?: readonly string[],
576
729
  url?: string,
577
730
  filePath?: string,
578
731
  parentBlockId?: string,
@@ -585,20 +738,25 @@ async function uploadImageBlock(
585
738
  const insertRes = await client.docx.documentBlockChildren.create({
586
739
  path: { document_id: docToken, block_id: parentBlockId ?? docToken },
587
740
  params: { document_revision_id: -1 },
588
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK type
589
- data: { children: [{ block_type: 27, image: {} as any }], index: index ?? -1 },
741
+ data: { children: [{ block_type: 27, image: {} }], index: index ?? -1 },
590
742
  });
591
743
  if (insertRes.code !== 0) {
592
744
  throw new Error(`Failed to create image block: ${insertRes.msg}`);
593
745
  }
594
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK return shape
595
- const imageBlockId = insertRes.data?.children?.find((b: any) => b.block_type === 27)?.block_id;
746
+ const imageBlockId = insertRes.data?.children?.find((b) => b.block_type === 27)?.block_id;
596
747
  if (!imageBlockId) {
597
748
  throw new Error("Failed to create image block");
598
749
  }
599
750
 
600
751
  // Step 2: Resolve and upload the image buffer.
601
- const upload = await resolveUploadInput(url, filePath, maxBytes, filename, imageInput);
752
+ const upload = await resolveUploadInput(
753
+ url,
754
+ filePath,
755
+ maxBytes,
756
+ localRoots,
757
+ filename,
758
+ imageInput,
759
+ );
602
760
  const fileToken = await uploadImageToDocx(
603
761
  client,
604
762
  imageBlockId,
@@ -629,6 +787,7 @@ async function uploadFileBlock(
629
787
  client: Lark.Client,
630
788
  docToken: string,
631
789
  maxBytes: number,
790
+ localRoots?: readonly string[],
632
791
  url?: string,
633
792
  filePath?: string,
634
793
  parentBlockId?: string,
@@ -639,16 +798,18 @@ async function uploadFileBlock(
639
798
  // Feishu API does not allow creating empty file blocks (block_type 23).
640
799
  // Workaround: create a placeholder text block, then replace it with file content.
641
800
  // Actually, file blocks need a different approach: use markdown link as placeholder.
642
- const upload = await resolveUploadInput(url, filePath, maxBytes, filename);
801
+ const upload = await resolveUploadInput(url, filePath, maxBytes, localRoots, filename);
643
802
 
644
803
  // Create a placeholder text block first
645
804
  const placeholderMd = `[${upload.fileName}](https://example.com/placeholder)`;
646
805
  const converted = await convertMarkdown(client, placeholderMd);
647
- const sorted = sortBlocksByFirstLevel(converted.blocks, converted.firstLevelBlockIds);
648
- const { children: inserted } = await insertBlocks(client, docToken, sorted, blockId);
806
+ const { orderedBlocks } = normalizeConvertedBlockTree(
807
+ converted.blocks,
808
+ converted.firstLevelBlockIds,
809
+ );
810
+ const { children: inserted } = await insertBlocks(client, docToken, orderedBlocks, blockId);
649
811
 
650
812
  // Get the first inserted block - we'll delete it and create the file in its place
651
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK return shape
652
813
  const placeholderBlock = inserted[0];
653
814
  if (!placeholderBlock?.block_id) {
654
815
  throw new Error("Failed to create placeholder block for file upload");
@@ -663,10 +824,7 @@ async function uploadFileBlock(
663
824
  throw new Error(childrenRes.msg);
664
825
  }
665
826
  const items = childrenRes.data?.items ?? [];
666
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
667
- const placeholderIdx = items.findIndex(
668
- (item: any) => item.block_id === placeholderBlock.block_id,
669
- );
827
+ const placeholderIdx = items.findIndex((item) => item.block_id === placeholderBlock.block_id);
670
828
  if (placeholderIdx >= 0) {
671
829
  const deleteRes = await client.docx.documentBlockChildren.batchDelete({
672
830
  path: { document_id: docToken, block_id: parentId },
@@ -684,8 +842,7 @@ async function uploadFileBlock(
684
842
  parent_type: "docx_file",
685
843
  parent_node: docToken,
686
844
  size: upload.buffer.length,
687
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK file type
688
- file: upload.buffer as any,
845
+ file: upload.buffer as DriveMediaUploadFile,
689
846
  },
690
847
  });
691
848
 
@@ -766,7 +923,7 @@ async function createDoc(
766
923
  }
767
924
  const shouldGrantToRequester = options?.grantToRequester !== false;
768
925
  const requesterOpenId = options?.requesterOpenId?.trim();
769
- const requesterPermType: "edit" = "edit";
926
+ const requesterPermType = "edit" as const;
770
927
 
771
928
  let requesterPermissionAdded = false;
772
929
  let requesterPermissionSkippedReason: string | undefined;
@@ -788,7 +945,7 @@ async function createDoc(
788
945
  });
789
946
  requesterPermissionAdded = true;
790
947
  } catch (err) {
791
- requesterPermissionError = err instanceof Error ? err.message : String(err);
948
+ requesterPermissionError = formatErrorMessage(err);
792
949
  }
793
950
  }
794
951
  }
@@ -824,11 +981,11 @@ async function writeDoc(
824
981
  }
825
982
 
826
983
  logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting...`);
827
- const sortedBlocks = sortBlocksByFirstLevel(blocks, firstLevelBlockIds);
984
+ const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
828
985
  const { children: inserted } =
829
986
  blocks.length > BATCH_SIZE
830
- ? await insertBlocksInBatches(client, docToken, sortedBlocks, firstLevelBlockIds, logger)
831
- : await insertBlocksWithDescendant(client, docToken, sortedBlocks, firstLevelBlockIds);
987
+ ? await insertBlocksInBatches(client, docToken, orderedBlocks, rootIds, logger)
988
+ : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds);
832
989
  const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
833
990
  logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
834
991
 
@@ -854,11 +1011,11 @@ async function appendDoc(
854
1011
  }
855
1012
 
856
1013
  logger?.info?.(`feishu_doc: Converted to ${blocks.length} blocks, inserting...`);
857
- const sortedBlocks = sortBlocksByFirstLevel(blocks, firstLevelBlockIds);
1014
+ const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
858
1015
  const { children: inserted } =
859
1016
  blocks.length > BATCH_SIZE
860
- ? await insertBlocksInBatches(client, docToken, sortedBlocks, firstLevelBlockIds, logger)
861
- : await insertBlocksWithDescendant(client, docToken, sortedBlocks, firstLevelBlockIds);
1017
+ ? await insertBlocksInBatches(client, docToken, orderedBlocks, rootIds, logger)
1018
+ : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds);
862
1019
  const imagesProcessed = await processImages(client, docToken, markdown, inserted, maxBytes);
863
1020
  logger?.info?.(`feishu_doc: Done (${blocks.length} blocks, ${imagesProcessed} images)`);
864
1021
 
@@ -866,8 +1023,7 @@ async function appendDoc(
866
1023
  success: true,
867
1024
  blocks_added: blocks.length,
868
1025
  images_processed: imagesProcessed,
869
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
870
- block_ids: inserted.map((b: any) => b.block_id),
1026
+ block_ids: inserted.map((b) => b.block_id),
871
1027
  };
872
1028
  }
873
1029
 
@@ -882,22 +1038,25 @@ async function insertDoc(
882
1038
  const blockInfo = await client.docx.documentBlock.get({
883
1039
  path: { document_id: docToken, block_id: afterBlockId },
884
1040
  });
885
- if (blockInfo.code !== 0) throw new Error(blockInfo.msg);
1041
+ if (blockInfo.code !== 0) {
1042
+ throw new Error(blockInfo.msg);
1043
+ }
886
1044
 
887
1045
  const parentId = blockInfo.data?.block?.parent_id ?? docToken;
888
1046
 
889
1047
  // Paginate through all children to reliably locate after_block_id.
890
1048
  // documentBlockChildren.get returns up to 200 children per page; large
891
1049
  // parents require multiple requests.
892
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
893
- const items: any[] = [];
1050
+ const items: FeishuDocxBlock[] = [];
894
1051
  let pageToken: string | undefined;
895
1052
  do {
896
1053
  const childrenRes = await client.docx.documentBlockChildren.get({
897
1054
  path: { document_id: docToken, block_id: parentId },
898
1055
  params: pageToken ? { page_token: pageToken } : {},
899
1056
  });
900
- if (childrenRes.code !== 0) throw new Error(childrenRes.msg);
1057
+ if (childrenRes.code !== 0) {
1058
+ throw new Error(childrenRes.msg);
1059
+ }
901
1060
  items.push(...(childrenRes.data?.items ?? []));
902
1061
  pageToken = childrenRes.data?.page_token ?? undefined;
903
1062
  } while (pageToken);
@@ -913,8 +1072,10 @@ async function insertDoc(
913
1072
 
914
1073
  logger?.info?.("feishu_doc: Converting markdown...");
915
1074
  const { blocks, firstLevelBlockIds } = await chunkedConvertMarkdown(client, markdown);
916
- if (blocks.length === 0) throw new Error("Content is empty");
917
- const sortedBlocks = sortBlocksByFirstLevel(blocks, firstLevelBlockIds);
1075
+ if (blocks.length === 0) {
1076
+ throw new Error("Content is empty");
1077
+ }
1078
+ const { orderedBlocks, rootIds } = normalizeConvertedBlockTree(blocks, firstLevelBlockIds);
918
1079
 
919
1080
  logger?.info?.(
920
1081
  `feishu_doc: Converted to ${blocks.length} blocks, inserting at index ${insertIndex}...`,
@@ -924,13 +1085,13 @@ async function insertDoc(
924
1085
  ? await insertBlocksInBatches(
925
1086
  client,
926
1087
  docToken,
927
- sortedBlocks,
928
- firstLevelBlockIds,
1088
+ orderedBlocks,
1089
+ rootIds,
929
1090
  logger,
930
1091
  parentId,
931
1092
  insertIndex,
932
1093
  )
933
- : await insertBlocksWithDescendant(client, docToken, sortedBlocks, firstLevelBlockIds, {
1094
+ : await insertBlocksWithDescendant(client, docToken, orderedBlocks, rootIds, {
934
1095
  parentBlockId: parentId,
935
1096
  index: insertIndex,
936
1097
  });
@@ -942,8 +1103,7 @@ async function insertDoc(
942
1103
  success: true,
943
1104
  blocks_added: blocks.length,
944
1105
  images_processed: imagesProcessed,
945
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
946
- block_ids: inserted.map((b: any) => b.block_id),
1106
+ block_ids: inserted.map((b) => b.block_id),
947
1107
  };
948
1108
  }
949
1109
 
@@ -982,10 +1142,8 @@ async function createTable(
982
1142
  throw new Error(res.msg);
983
1143
  }
984
1144
 
985
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK return type
986
- const tableBlock = (res.data?.children as any[] | undefined)?.find((b) => b.block_type === 31);
987
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK return shape may vary by version
988
- const cells = (tableBlock?.children as any[] | undefined) ?? [];
1145
+ const tableBlock = res.data?.children?.find((b) => b.block_type === 31);
1146
+ const cells = normalizeInsertedChildBlocks(tableBlock?.children);
989
1147
 
990
1148
  return {
991
1149
  success: true,
@@ -993,8 +1151,7 @@ async function createTable(
993
1151
  row_size: rowSize,
994
1152
  column_size: columnSize,
995
1153
  // row-major cell ids, if API returns them directly
996
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK return type
997
- table_cell_block_ids: cells.map((c: any) => c.block_id).filter(Boolean),
1154
+ table_cell_block_ids: cells.map((c) => c.block_id).filter(Boolean),
998
1155
  raw_children_count: res.data?.children?.length ?? 0,
999
1156
  };
1000
1157
  }
@@ -1021,13 +1178,10 @@ async function writeTableCells(
1021
1178
  throw new Error("table_block_id is not a table block");
1022
1179
  }
1023
1180
 
1024
- // SDK types are loose here across versions
1025
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block payload
1026
- const tableData = (tableBlock as any).table;
1027
- const rows = tableData?.property?.row_size as number | undefined;
1028
- const cols = tableData?.property?.column_size as number | undefined;
1029
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block payload
1030
- const cellIds = (tableData?.cells as any[] | undefined) ?? [];
1181
+ const tableData = tableBlock.table;
1182
+ const rows = tableData?.property?.row_size;
1183
+ const cols = tableData?.property?.column_size;
1184
+ const cellIds = tableData?.cells ?? [];
1031
1185
 
1032
1186
  if (!rows || !cols || !cellIds.length) {
1033
1187
  throw new Error(
@@ -1044,7 +1198,9 @@ async function writeTableCells(
1044
1198
 
1045
1199
  for (let c = 0; c < writeCols; c++) {
1046
1200
  const cellId = cellIds[r * cols + c];
1047
- if (!cellId) continue;
1201
+ if (!cellId) {
1202
+ continue;
1203
+ }
1048
1204
 
1049
1205
  // table cell is a container block: clear existing children, then create text child blocks
1050
1206
  const childrenRes = await client.docx.documentBlockChildren.get({
@@ -1067,10 +1223,13 @@ async function writeTableCells(
1067
1223
 
1068
1224
  const text = rowValues[c] ?? "";
1069
1225
  const converted = await convertMarkdown(client, text);
1070
- const sorted = sortBlocksByFirstLevel(converted.blocks, converted.firstLevelBlockIds);
1226
+ const { orderedBlocks } = normalizeConvertedBlockTree(
1227
+ converted.blocks,
1228
+ converted.firstLevelBlockIds,
1229
+ );
1071
1230
 
1072
- if (sorted.length > 0) {
1073
- await insertBlocks(client, docToken, sorted, cellId);
1231
+ if (orderedBlocks.length > 0) {
1232
+ await insertBlocks(client, docToken, orderedBlocks, cellId);
1074
1233
  }
1075
1234
 
1076
1235
  written++;
@@ -1164,8 +1323,7 @@ async function deleteBlock(client: Lark.Client, docToken: string, blockId: strin
1164
1323
  }
1165
1324
 
1166
1325
  const items = children.data?.items ?? [];
1167
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
1168
- const index = items.findIndex((item: any) => item.block_id === blockId);
1326
+ const index = items.findIndex((item) => item.block_id === blockId);
1169
1327
  if (index === -1) {
1170
1328
  throw new Error("Block not found");
1171
1329
  }
@@ -1228,14 +1386,12 @@ async function listAppScopes(client: Lark.Client) {
1228
1386
 
1229
1387
  export function registerFeishuDocTools(api: OpenClawPluginApi) {
1230
1388
  if (!api.config) {
1231
- api.logger.debug?.("feishu_doc: No config available, skipping doc tools");
1232
1389
  return;
1233
1390
  }
1234
1391
 
1235
1392
  // Check if any account is configured
1236
1393
  const accounts = listEnabledFeishuAccounts(api.config);
1237
1394
  if (accounts.length === 0) {
1238
- api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools");
1239
1395
  return;
1240
1396
  }
1241
1397
 
@@ -1262,8 +1418,11 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
1262
1418
  api.registerTool(
1263
1419
  (ctx) => {
1264
1420
  const defaultAccountId = ctx.agentAccountId;
1421
+ const mediaLocalRoots = resolveDocToolLocalRoots(ctx);
1265
1422
  const trustedRequesterOpenId =
1266
- ctx.messageChannel === "feishu" ? ctx.requesterSenderId?.trim() || undefined : undefined;
1423
+ ctx.messageChannel === "feishu"
1424
+ ? normalizeOptionalString(ctx.requesterSenderId)
1425
+ : undefined;
1267
1426
  return {
1268
1427
  name: "feishu_doc",
1269
1428
  label: "Feishu Doc",
@@ -1356,6 +1515,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
1356
1515
  client,
1357
1516
  p.doc_token,
1358
1517
  getMediaMaxBytes(p, defaultAccountId),
1518
+ mediaLocalRoots,
1359
1519
  p.url,
1360
1520
  p.file_path,
1361
1521
  p.parent_block_id,
@@ -1370,6 +1530,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
1370
1530
  client,
1371
1531
  p.doc_token,
1372
1532
  getMediaMaxBytes(p, defaultAccountId),
1533
+ mediaLocalRoots,
1373
1534
  p.url,
1374
1535
  p.file_path,
1375
1536
  p.parent_block_id,
@@ -1417,11 +1578,10 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
1417
1578
  ),
1418
1579
  );
1419
1580
  default:
1420
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
1421
- return json({ error: `Unknown action: ${(p as any).action}` });
1581
+ return json({ error: "Unknown action" });
1422
1582
  }
1423
1583
  } catch (err) {
1424
- return json({ error: err instanceof Error ? err.message : String(err) });
1584
+ return json({ error: formatErrorMessage(err) });
1425
1585
  }
1426
1586
  },
1427
1587
  };
@@ -1445,7 +1605,7 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
1445
1605
  const result = await listAppScopes(getClient(undefined, ctx.agentAccountId));
1446
1606
  return json(result);
1447
1607
  } catch (err) {
1448
- return json({ error: err instanceof Error ? err.message : String(err) });
1608
+ return json({ error: formatErrorMessage(err) });
1449
1609
  }
1450
1610
  },
1451
1611
  }),
@@ -1453,8 +1613,4 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
1453
1613
  );
1454
1614
  registered.push("feishu_app_scopes");
1455
1615
  }
1456
-
1457
- if (registered.length > 0) {
1458
- api.logger.info?.(`feishu_doc: Registered ${registered.join(", ")}`);
1459
- }
1460
1616
  }