@openclaw/feishu 2026.3.13 → 2026.5.2-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 (187) 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 +1827 -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 +1253 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +135 -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 +406 -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 +33 -95
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +116 -20
  72. package/src/directory.ts +60 -92
  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 +403 -26
  91. package/src/media.ts +509 -132
  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.startup.test.ts +11 -9
  114. package/src/monitor.startup.ts +26 -16
  115. package/src/monitor.state.ts +20 -5
  116. package/src/monitor.synthetic-error.ts +18 -0
  117. package/src/monitor.test-mocks.ts +2 -2
  118. package/src/monitor.transport.ts +220 -60
  119. package/src/monitor.ts +15 -10
  120. package/src/monitor.webhook-e2e.test.ts +65 -7
  121. package/src/monitor.webhook-security.test.ts +122 -0
  122. package/src/monitor.webhook.test-helpers.ts +44 -26
  123. package/src/outbound-runtime-api.ts +1 -0
  124. package/src/outbound.test.ts +616 -37
  125. package/src/outbound.ts +623 -81
  126. package/src/perm-schema.ts +1 -1
  127. package/src/perm.ts +1 -7
  128. package/src/pins.ts +108 -0
  129. package/src/policy.test.ts +297 -117
  130. package/src/policy.ts +142 -29
  131. package/src/post.ts +7 -6
  132. package/src/probe.test.ts +14 -9
  133. package/src/probe.ts +26 -16
  134. package/src/processing-claims.ts +59 -0
  135. package/src/qr-terminal.ts +1 -0
  136. package/src/reactions.ts +4 -34
  137. package/src/reasoning-preview.test.ts +59 -0
  138. package/src/reasoning-preview.ts +20 -0
  139. package/src/reply-dispatcher-runtime-api.ts +7 -0
  140. package/src/reply-dispatcher.test.ts +660 -29
  141. package/src/reply-dispatcher.ts +407 -154
  142. package/src/runtime.ts +6 -3
  143. package/src/secret-contract.ts +145 -0
  144. package/src/secret-input.ts +1 -13
  145. package/src/security-audit-shared.ts +69 -0
  146. package/src/security-audit.test.ts +61 -0
  147. package/src/security-audit.ts +1 -0
  148. package/src/send-result.ts +1 -1
  149. package/src/send-target.test.ts +9 -3
  150. package/src/send-target.ts +10 -4
  151. package/src/send.reply-fallback.test.ts +105 -2
  152. package/src/send.test.ts +386 -4
  153. package/src/send.ts +414 -95
  154. package/src/sequential-key.test.ts +72 -0
  155. package/src/sequential-key.ts +28 -0
  156. package/src/sequential-queue.test.ts +92 -0
  157. package/src/sequential-queue.ts +16 -0
  158. package/src/session-conversation.ts +42 -0
  159. package/src/session-route.ts +48 -0
  160. package/src/setup-core.ts +51 -0
  161. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  162. package/src/setup-surface.ts +581 -0
  163. package/src/streaming-card.test.ts +138 -2
  164. package/src/streaming-card.ts +134 -18
  165. package/src/subagent-hooks.test.ts +603 -0
  166. package/src/subagent-hooks.ts +397 -0
  167. package/src/targets.ts +3 -13
  168. package/src/test-support/lifecycle-test-support.ts +453 -0
  169. package/src/thread-bindings.test.ts +143 -0
  170. package/src/thread-bindings.ts +330 -0
  171. package/src/tool-account-routing.test.ts +66 -8
  172. package/src/tool-account.test.ts +44 -0
  173. package/src/tool-account.ts +40 -17
  174. package/src/tool-factory-test-harness.ts +11 -8
  175. package/src/tool-result.ts +3 -1
  176. package/src/tools-config.ts +1 -1
  177. package/src/types.ts +16 -15
  178. package/src/typing.ts +10 -6
  179. package/src/wiki-schema.ts +1 -1
  180. package/src/wiki.ts +1 -7
  181. package/subagent-hooks-api.ts +31 -0
  182. package/tsconfig.json +16 -0
  183. package/src/feishu-command-handler.ts +0 -59
  184. package/src/onboarding.status.test.ts +0 -25
  185. package/src/onboarding.ts +0 -489
  186. package/src/send-message.ts +0 -71
  187. package/src/targets.test.ts +0 -70
package/src/directory.ts CHANGED
@@ -1,71 +1,20 @@
1
- import {
2
- listDirectoryGroupEntriesFromMapKeysAndAllowFrom,
3
- listDirectoryUserEntriesFromAllowFromAndMapKeys,
4
- } from "openclaw/plugin-sdk/compat";
5
- import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
1
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
2
+ import type { ClawdbotConfig } from "../runtime-api.js";
6
3
  import { resolveFeishuAccount } from "./accounts.js";
7
4
  import { createFeishuClient } from "./client.js";
8
- import { normalizeFeishuTarget } from "./targets.js";
9
-
10
- export type FeishuDirectoryPeer = {
11
- kind: "user";
12
- id: string;
13
- name?: string;
14
- };
15
-
16
- export type FeishuDirectoryGroup = {
17
- kind: "group";
18
- id: string;
19
- name?: string;
20
- };
21
-
22
- function toFeishuDirectoryPeers(ids: string[]): FeishuDirectoryPeer[] {
23
- return ids.map((id) => ({ kind: "user", id }));
24
- }
25
-
26
- function toFeishuDirectoryGroups(ids: string[]): FeishuDirectoryGroup[] {
27
- return ids.map((id) => ({ kind: "group", id }));
28
- }
29
-
30
- export async function listFeishuDirectoryPeers(params: {
31
- cfg: ClawdbotConfig;
32
- query?: string;
33
- limit?: number;
34
- accountId?: string;
35
- }): Promise<FeishuDirectoryPeer[]> {
36
- const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
37
- const entries = listDirectoryUserEntriesFromAllowFromAndMapKeys({
38
- allowFrom: account.config.allowFrom,
39
- map: account.config.dms,
40
- query: params.query,
41
- limit: params.limit,
42
- normalizeAllowFromId: (entry) => normalizeFeishuTarget(entry) ?? entry,
43
- normalizeMapKeyId: (entry) => normalizeFeishuTarget(entry) ?? entry,
44
- });
45
- return toFeishuDirectoryPeers(entries.map((entry) => entry.id));
46
- }
47
-
48
- export async function listFeishuDirectoryGroups(params: {
49
- cfg: ClawdbotConfig;
50
- query?: string;
51
- limit?: number;
52
- accountId?: string;
53
- }): Promise<FeishuDirectoryGroup[]> {
54
- const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
55
- const entries = listDirectoryGroupEntriesFromMapKeysAndAllowFrom({
56
- groups: account.config.groups,
57
- allowFrom: account.config.groupAllowFrom,
58
- query: params.query,
59
- limit: params.limit,
60
- });
61
- return toFeishuDirectoryGroups(entries.map((entry) => entry.id));
62
- }
5
+ import {
6
+ listFeishuDirectoryGroups,
7
+ listFeishuDirectoryPeers,
8
+ type FeishuDirectoryGroup,
9
+ type FeishuDirectoryPeer,
10
+ } from "./directory.static.js";
63
11
 
64
12
  export async function listFeishuDirectoryPeersLive(params: {
65
13
  cfg: ClawdbotConfig;
66
14
  query?: string;
67
15
  limit?: number;
68
16
  accountId?: string;
17
+ fallbackToStatic?: boolean;
69
18
  }): Promise<FeishuDirectoryPeer[]> {
70
19
  const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
71
20
  if (!account.configured) {
@@ -83,27 +32,36 @@ export async function listFeishuDirectoryPeersLive(params: {
83
32
  },
84
33
  });
85
34
 
86
- if (response.code === 0 && response.data?.items) {
87
- for (const user of response.data.items) {
88
- if (user.open_id) {
89
- const q = params.query?.trim().toLowerCase() || "";
90
- const name = user.name || "";
91
- if (!q || user.open_id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) {
92
- peers.push({
93
- kind: "user",
94
- id: user.open_id,
95
- name: name || undefined,
96
- });
97
- }
98
- }
99
- if (peers.length >= limit) {
100
- break;
35
+ if (response.code !== 0) {
36
+ throw new Error(response.msg || `code ${response.code}`);
37
+ }
38
+
39
+ const q = normalizeLowercaseStringOrEmpty(params.query);
40
+ for (const user of response.data?.items ?? []) {
41
+ if (user.open_id) {
42
+ const name = user.name || "";
43
+ if (
44
+ !q ||
45
+ normalizeLowercaseStringOrEmpty(user.open_id).includes(q) ||
46
+ normalizeLowercaseStringOrEmpty(name).includes(q)
47
+ ) {
48
+ peers.push({
49
+ kind: "user",
50
+ id: user.open_id,
51
+ name: name || undefined,
52
+ });
101
53
  }
102
54
  }
55
+ if (peers.length >= limit) {
56
+ break;
57
+ }
103
58
  }
104
59
 
105
60
  return peers;
106
- } catch {
61
+ } catch (err) {
62
+ if (params.fallbackToStatic === false) {
63
+ throw err instanceof Error ? err : new Error("Feishu live peer lookup failed");
64
+ }
107
65
  return listFeishuDirectoryPeers(params);
108
66
  }
109
67
  }
@@ -113,6 +71,7 @@ export async function listFeishuDirectoryGroupsLive(params: {
113
71
  query?: string;
114
72
  limit?: number;
115
73
  accountId?: string;
74
+ fallbackToStatic?: boolean;
116
75
  }): Promise<FeishuDirectoryGroup[]> {
117
76
  const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
118
77
  if (!account.configured) {
@@ -130,27 +89,36 @@ export async function listFeishuDirectoryGroupsLive(params: {
130
89
  },
131
90
  });
132
91
 
133
- if (response.code === 0 && response.data?.items) {
134
- for (const chat of response.data.items) {
135
- if (chat.chat_id) {
136
- const q = params.query?.trim().toLowerCase() || "";
137
- const name = chat.name || "";
138
- if (!q || chat.chat_id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) {
139
- groups.push({
140
- kind: "group",
141
- id: chat.chat_id,
142
- name: name || undefined,
143
- });
144
- }
145
- }
146
- if (groups.length >= limit) {
147
- break;
92
+ if (response.code !== 0) {
93
+ throw new Error(response.msg || `code ${response.code}`);
94
+ }
95
+
96
+ const q = normalizeLowercaseStringOrEmpty(params.query);
97
+ for (const chat of response.data?.items ?? []) {
98
+ if (chat.chat_id) {
99
+ const name = chat.name || "";
100
+ if (
101
+ !q ||
102
+ normalizeLowercaseStringOrEmpty(chat.chat_id).includes(q) ||
103
+ normalizeLowercaseStringOrEmpty(name).includes(q)
104
+ ) {
105
+ groups.push({
106
+ kind: "group",
107
+ id: chat.chat_id,
108
+ name: name || undefined,
109
+ });
148
110
  }
149
111
  }
112
+ if (groups.length >= limit) {
113
+ break;
114
+ }
150
115
  }
151
116
 
152
117
  return groups;
153
- } catch {
118
+ } catch (err) {
119
+ if (params.fallbackToStatic === false) {
120
+ throw err instanceof Error ? err : new Error("Feishu live group lookup failed");
121
+ }
154
122
  return listFeishuDirectoryGroups(params);
155
123
  }
156
124
  }
package/src/doc-schema.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from "@sinclair/typebox";
1
+ import { Type, type Static } from "typebox";
2
2
 
3
3
  const tableCreationProperties = {
4
4
  doc_token: Type.String({ description: "Document token" }),
@@ -1,5 +1,22 @@
1
+ import type * as Lark from "@larksuiteoapi/node-sdk";
1
2
  import { describe, expect, it, vi } from "vitest";
2
3
  import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js";
4
+ import type { FeishuDocxBlock } from "./docx-types.js";
5
+
6
+ type InsertBlocksClient = Parameters<typeof insertBlocksInBatches>[0];
7
+ type DocxDescendantCreate = Lark.Client["docx"]["documentBlockDescendant"]["create"];
8
+ type DocxDescendantCreateParams = Parameters<DocxDescendantCreate>[0];
9
+ type DocxDescendantCreateResponse = Awaited<ReturnType<DocxDescendantCreate>>;
10
+
11
+ function createDocxDescendantClient(create: DocxDescendantCreate): InsertBlocksClient {
12
+ return {
13
+ docx: {
14
+ documentBlockDescendant: {
15
+ create,
16
+ },
17
+ },
18
+ } as InsertBlocksClient;
19
+ }
3
20
 
4
21
  function createCountingIterable<T>(values: T[]) {
5
22
  let iterations = 0;
@@ -14,6 +31,20 @@ function createCountingIterable<T>(values: T[]) {
14
31
  };
15
32
  }
16
33
 
34
+ function createSuccessfulDocxDescendantCreateMock() {
35
+ return vi.fn(
36
+ async (params?: DocxDescendantCreateParams): Promise<DocxDescendantCreateResponse> => ({
37
+ code: 0,
38
+ data: {
39
+ children: (params?.data?.children_id ?? []).map((id) => ({
40
+ block_id: id,
41
+ block_type: 2,
42
+ })),
43
+ },
44
+ }),
45
+ );
46
+ }
47
+
17
48
  describe("insertBlocksInBatches", () => {
18
49
  it("builds the source block map once for large flat trees", async () => {
19
50
  const blockCount = BATCH_SIZE + 200;
@@ -22,24 +53,13 @@ describe("insertBlocksInBatches", () => {
22
53
  block_type: 2,
23
54
  }));
24
55
  const counting = createCountingIterable(blocks);
25
- const createMock = vi.fn(async ({ data }: { data: { children_id: string[] } }) => ({
26
- code: 0,
27
- data: {
28
- children: data.children_id.map((id) => ({ block_id: id })),
29
- },
30
- }));
31
- const client = {
32
- docx: {
33
- documentBlockDescendant: {
34
- create: createMock,
35
- },
36
- },
37
- } as any;
56
+ const createMock = createSuccessfulDocxDescendantCreateMock();
57
+ const client = createDocxDescendantClient((params) => createMock(params));
38
58
 
39
59
  const result = await insertBlocksInBatches(
40
60
  client,
41
61
  "doc_1",
42
- counting.values as any[],
62
+ Array.from(counting.values),
43
63
  blocks.map((block) => block.block_id),
44
64
  );
45
65
 
@@ -51,40 +71,21 @@ describe("insertBlocksInBatches", () => {
51
71
  });
52
72
 
53
73
  it("keeps nested descendants grouped with their root blocks", async () => {
54
- const createMock = vi.fn(
55
- async ({
56
- data,
57
- }: {
58
- data: { children_id: string[]; descendants: Array<{ block_id: string }> };
59
- }) => ({
60
- code: 0,
61
- data: {
62
- children: data.children_id.map((id) => ({ block_id: id })),
63
- },
64
- }),
65
- );
66
- const client = {
67
- docx: {
68
- documentBlockDescendant: {
69
- create: createMock,
70
- },
71
- },
72
- } as any;
73
- const blocks = [
74
+ const createMock = createSuccessfulDocxDescendantCreateMock();
75
+ const client = createDocxDescendantClient((params) => createMock(params));
76
+ const blocks: FeishuDocxBlock[] = [
74
77
  { block_id: "root_a", block_type: 1, children: ["child_a"] },
75
78
  { block_id: "child_a", block_type: 2 },
76
79
  { block_id: "root_b", block_type: 1, children: ["child_b"] },
77
80
  { block_id: "child_b", block_type: 2 },
78
81
  ];
79
82
 
80
- await insertBlocksInBatches(client, "doc_1", blocks as any[], ["root_a", "root_b"]);
83
+ await insertBlocksInBatches(client, "doc_1", blocks, ["root_a", "root_b"]);
81
84
 
82
85
  expect(createMock).toHaveBeenCalledTimes(1);
83
86
  expect(createMock.mock.calls[0]?.[0]?.data.children_id).toEqual(["root_a", "root_b"]);
84
87
  expect(
85
- createMock.mock.calls[0]?.[0]?.data.descendants.map(
86
- (block: { block_id: string }) => block.block_id,
87
- ),
88
+ createMock.mock.calls[0]?.[0]?.data.descendants.map((block) => block.block_id ?? ""),
88
89
  ).toEqual(["root_a", "child_a", "root_b", "child_b"]);
89
90
  });
90
91
  });
@@ -7,27 +7,58 @@
7
7
  */
8
8
 
9
9
  import type * as Lark from "@larksuiteoapi/node-sdk";
10
+ import { readStringValue } from "openclaw/plugin-sdk/text-runtime";
10
11
  import { cleanBlocksForDescendant } from "./docx-table-ops.js";
12
+ import type { FeishuDocxBlock, FeishuDocxBlockChild } from "./docx-types.js";
11
13
 
12
14
  export const BATCH_SIZE = 1000; // Feishu API limit per request
13
15
 
14
16
  type Logger = { info?: (msg: string) => void };
15
17
 
18
+ type DocxDescendantCreatePayload = NonNullable<
19
+ Parameters<Lark.Client["docx"]["documentBlockDescendant"]["create"]>[0]
20
+ >;
21
+ type DocxDescendantCreateBlock = NonNullable<
22
+ NonNullable<DocxDescendantCreatePayload["data"]>["descendants"]
23
+ >[number];
24
+
25
+ function normalizeChildIds(children: string[] | string | undefined): string[] | undefined {
26
+ if (Array.isArray(children)) {
27
+ return children;
28
+ }
29
+ const child = readStringValue(children);
30
+ return child ? [child] : undefined;
31
+ }
32
+
33
+ function toDescendantBlock(block: FeishuDocxBlock): DocxDescendantCreateBlock {
34
+ const children = normalizeChildIds(block.children);
35
+ return {
36
+ ...block,
37
+ ...(children ? { children } : {}),
38
+ } as DocxDescendantCreateBlock;
39
+ }
40
+
16
41
  /**
17
42
  * Collect all descendant blocks for a given first-level block ID.
18
43
  * Recursively traverses the block tree to gather all children.
19
44
  */
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
21
- function collectDescendants(blockMap: Map<string, any>, rootId: string): any[] {
22
- const result: any[] = [];
45
+ function collectDescendants(
46
+ blockMap: Map<string, FeishuDocxBlock>,
47
+ rootId: string,
48
+ ): FeishuDocxBlock[] {
49
+ const result: FeishuDocxBlock[] = [];
23
50
  const visited = new Set<string>();
24
51
 
25
52
  function collect(blockId: string) {
26
- if (visited.has(blockId)) return;
53
+ if (visited.has(blockId)) {
54
+ return;
55
+ }
27
56
  visited.add(blockId);
28
57
 
29
58
  const block = blockMap.get(blockId);
30
- if (!block) return;
59
+ if (!block) {
60
+ return;
61
+ }
31
62
 
32
63
  result.push(block);
33
64
 
@@ -53,15 +84,14 @@ function collectDescendants(blockMap: Map<string, any>, rootId: string): any[] {
53
84
  * @param parentBlockId - Parent block to insert into (defaults to docToken)
54
85
  * @param index - Position within parent's children (-1 = end)
55
86
  */
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
57
87
  async function insertBatch(
58
88
  client: Lark.Client,
59
89
  docToken: string,
60
- blocks: any[],
90
+ blocks: FeishuDocxBlock[],
61
91
  firstLevelBlockIds: string[],
62
92
  parentBlockId: string = docToken,
63
93
  index: number = -1,
64
- ): Promise<any[]> {
94
+ ): Promise<FeishuDocxBlockChild[]> {
65
95
  const descendants = cleanBlocksForDescendant(blocks);
66
96
 
67
97
  if (descendants.length === 0) {
@@ -72,7 +102,7 @@ async function insertBatch(
72
102
  path: { document_id: docToken, block_id: parentBlockId },
73
103
  data: {
74
104
  children_id: firstLevelBlockIds,
75
- descendants,
105
+ descendants: descendants.map(toDescendantBlock),
76
106
  index,
77
107
  },
78
108
  });
@@ -100,30 +130,34 @@ async function insertBatch(
100
130
  * each batch advances this by the number of first-level IDs inserted so far.
101
131
  * @returns Inserted children blocks and any skipped block IDs
102
132
  */
103
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
104
133
  export async function insertBlocksInBatches(
105
134
  client: Lark.Client,
106
135
  docToken: string,
107
- blocks: any[],
136
+ blocks: FeishuDocxBlock[],
108
137
  firstLevelBlockIds: string[],
109
138
  logger?: Logger,
110
139
  parentBlockId: string = docToken,
111
140
  startIndex: number = -1,
112
- ): Promise<{ children: any[]; skipped: string[] }> {
113
- const allChildren: any[] = [];
141
+ ): Promise<{ children: FeishuDocxBlockChild[]; skipped: string[] }> {
142
+ const allChildren: FeishuDocxBlockChild[] = [];
114
143
 
115
144
  // Build batches ensuring each batch has ≤1000 total descendants
116
- const batches: { firstLevelIds: string[]; blocks: any[] }[] = [];
117
- let currentBatch: { firstLevelIds: string[]; blocks: any[] } = { firstLevelIds: [], blocks: [] };
145
+ const batches: Array<{ firstLevelIds: string[]; blocks: FeishuDocxBlock[] }> = [];
146
+ let currentBatch: { firstLevelIds: string[]; blocks: FeishuDocxBlock[] } = {
147
+ firstLevelIds: [],
148
+ blocks: [],
149
+ };
118
150
  const usedBlockIds = new Set<string>();
119
- const blockMap = new Map<string, any>();
151
+ const blockMap = new Map<string, FeishuDocxBlock>();
120
152
  for (const block of blocks) {
121
- blockMap.set(block.block_id, block);
153
+ if (block.block_id) {
154
+ blockMap.set(block.block_id, block);
155
+ }
122
156
  }
123
157
 
124
158
  for (const firstLevelId of firstLevelBlockIds) {
125
159
  const descendants = collectDescendants(blockMap, firstLevelId);
126
- const newBlocks = descendants.filter((b) => !usedBlockIds.has(b.block_id));
160
+ const newBlocks = descendants.filter((b) => b.block_id && !usedBlockIds.has(b.block_id));
127
161
 
128
162
  // A single block whose subtree exceeds the API limit cannot be split
129
163
  // (a table or other compound block must be inserted atomically).
@@ -148,7 +182,9 @@ export async function insertBlocksInBatches(
148
182
  currentBatch.firstLevelIds.push(firstLevelId);
149
183
  for (const block of newBlocks) {
150
184
  currentBatch.blocks.push(block);
151
- usedBlockIds.add(block.block_id);
185
+ if (block.block_id) {
186
+ usedBlockIds.add(block.block_id);
187
+ }
152
188
  }
153
189
  }
154
190
 
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type * as Lark from "@larksuiteoapi/node-sdk";
15
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
15
16
 
16
17
  // Feishu text_color values (1-7)
17
18
  const TEXT_COLOR: Record<string, number> = {
@@ -44,6 +45,11 @@ interface Segment {
44
45
  bold?: boolean;
45
46
  }
46
47
 
48
+ type DocxPatchPayload = NonNullable<Parameters<Lark.Client["docx"]["documentBlock"]["patch"]>[0]>;
49
+ type DocxTextElement = NonNullable<
50
+ NonNullable<NonNullable<DocxPatchPayload["data"]>["update_text_elements"]>["elements"]
51
+ >[number];
52
+
47
53
  /**
48
54
  * Parse color markup into segments.
49
55
  *
@@ -53,7 +59,7 @@ interface Segment {
53
59
  * [bold]text[/bold] → bold
54
60
  * [green bold]text[/green] → green + bold
55
61
  */
56
- export function parseColorMarkup(content: string): Segment[] {
62
+ function parseColorMarkup(content: string): Segment[] {
57
63
  const segments: Segment[] = [];
58
64
  // Only [known_tag]...[/...] pairs are treated as markup. Using an open
59
65
  // pattern like \[([^\]]+)\] would match any bracket token — e.g. [Q1] —
@@ -81,7 +87,7 @@ export function parseColorMarkup(content: string): Segment[] {
81
87
  }
82
88
  } else {
83
89
  // Tagged segment
84
- const tagStr = match[1].toLowerCase().trim();
90
+ const tagStr = normalizeLowercaseStringOrEmpty(match[1]);
85
91
  const text = match[2];
86
92
  const tags = tagStr.split(/\s+/);
87
93
 
@@ -120,8 +126,7 @@ export async function updateColorText(
120
126
  ) {
121
127
  const segments = parseColorMarkup(content);
122
128
 
123
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK type
124
- const elements: any[] = segments.map((seg) => ({
129
+ const elements: DocxTextElement[] = segments.map((seg) => ({
125
130
  text_run: {
126
131
  content: seg.text,
127
132
  text_element_style: {
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { cleanBlocksForDescendant } from "./docx-table-ops.js";
3
+
4
+ describe("cleanBlocksForDescendant", () => {
5
+ it("removes parent links and read-only table fields while normalizing table cells", () => {
6
+ const blocks = [
7
+ {
8
+ block_id: "table-1",
9
+ parent_id: "parent-1",
10
+ block_type: 31,
11
+ children: "cell-1",
12
+ table: {
13
+ property: {
14
+ row_size: 1,
15
+ column_size: 1,
16
+ column_width: [240],
17
+ },
18
+ cells: ["cell-1"],
19
+ merge_info: [{ row_span: 1, col_span: 1 }],
20
+ },
21
+ },
22
+ {
23
+ block_id: "cell-1",
24
+ parent_id: "table-1",
25
+ block_type: 32,
26
+ children: "text-1",
27
+ },
28
+ {
29
+ block_id: "text-1",
30
+ parent_id: "cell-1",
31
+ block_type: 2,
32
+ text: {
33
+ elements: [{ text_run: { content: "hello" } }],
34
+ },
35
+ },
36
+ ];
37
+
38
+ const cleaned = cleanBlocksForDescendant(blocks);
39
+
40
+ expect(cleaned[0]).not.toHaveProperty("parent_id");
41
+ expect(cleaned[1]).not.toHaveProperty("parent_id");
42
+ expect(cleaned[2]).not.toHaveProperty("parent_id");
43
+
44
+ expect(cleaned[0]?.table).toEqual({
45
+ property: {
46
+ row_size: 1,
47
+ column_size: 1,
48
+ column_width: [240],
49
+ },
50
+ });
51
+ expect(cleaned[1]?.children).toEqual(["text-1"]);
52
+ });
53
+ });