@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.
- package/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +95 -7
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +778 -775
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +32 -94
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +375 -26
- package/src/media.ts +434 -88
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +218 -312
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +108 -48
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +11 -9
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +660 -29
- package/src/reply-dispatcher.ts +407 -154
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +77 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +399 -86
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/directory.ts
CHANGED
|
@@ -1,71 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
listFeishuDirectoryGroups,
|
|
7
|
+
listFeishuDirectoryPeers,
|
|
8
|
+
type FeishuDirectoryGroup,
|
|
9
|
+
type FeishuDirectoryPeer,
|
|
10
|
+
} from "./directory.static.js";
|
|
25
11
|
|
|
26
|
-
|
|
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
|
-
}
|
|
12
|
+
export { listFeishuDirectoryGroups, listFeishuDirectoryPeers } from "./directory.static.js";
|
|
63
13
|
|
|
64
14
|
export async function listFeishuDirectoryPeersLive(params: {
|
|
65
15
|
cfg: ClawdbotConfig;
|
|
66
16
|
query?: string;
|
|
67
17
|
limit?: number;
|
|
68
18
|
accountId?: string;
|
|
19
|
+
fallbackToStatic?: boolean;
|
|
69
20
|
}): Promise<FeishuDirectoryPeer[]> {
|
|
70
21
|
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
71
22
|
if (!account.configured) {
|
|
@@ -83,27 +34,36 @@ export async function listFeishuDirectoryPeersLive(params: {
|
|
|
83
34
|
},
|
|
84
35
|
});
|
|
85
36
|
|
|
86
|
-
if (response.code
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
37
|
+
if (response.code !== 0) {
|
|
38
|
+
throw new Error(response.msg || `code ${response.code}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const q = normalizeLowercaseStringOrEmpty(params.query);
|
|
42
|
+
for (const user of response.data?.items ?? []) {
|
|
43
|
+
if (user.open_id) {
|
|
44
|
+
const name = user.name || "";
|
|
45
|
+
if (
|
|
46
|
+
!q ||
|
|
47
|
+
normalizeLowercaseStringOrEmpty(user.open_id).includes(q) ||
|
|
48
|
+
normalizeLowercaseStringOrEmpty(name).includes(q)
|
|
49
|
+
) {
|
|
50
|
+
peers.push({
|
|
51
|
+
kind: "user",
|
|
52
|
+
id: user.open_id,
|
|
53
|
+
name: name || undefined,
|
|
54
|
+
});
|
|
101
55
|
}
|
|
102
56
|
}
|
|
57
|
+
if (peers.length >= limit) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
103
60
|
}
|
|
104
61
|
|
|
105
62
|
return peers;
|
|
106
|
-
} catch {
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (params.fallbackToStatic === false) {
|
|
65
|
+
throw err instanceof Error ? err : new Error("Feishu live peer lookup failed");
|
|
66
|
+
}
|
|
107
67
|
return listFeishuDirectoryPeers(params);
|
|
108
68
|
}
|
|
109
69
|
}
|
|
@@ -113,6 +73,7 @@ export async function listFeishuDirectoryGroupsLive(params: {
|
|
|
113
73
|
query?: string;
|
|
114
74
|
limit?: number;
|
|
115
75
|
accountId?: string;
|
|
76
|
+
fallbackToStatic?: boolean;
|
|
116
77
|
}): Promise<FeishuDirectoryGroup[]> {
|
|
117
78
|
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
118
79
|
if (!account.configured) {
|
|
@@ -130,27 +91,36 @@ export async function listFeishuDirectoryGroupsLive(params: {
|
|
|
130
91
|
},
|
|
131
92
|
});
|
|
132
93
|
|
|
133
|
-
if (response.code
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
94
|
+
if (response.code !== 0) {
|
|
95
|
+
throw new Error(response.msg || `code ${response.code}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const q = normalizeLowercaseStringOrEmpty(params.query);
|
|
99
|
+
for (const chat of response.data?.items ?? []) {
|
|
100
|
+
if (chat.chat_id) {
|
|
101
|
+
const name = chat.name || "";
|
|
102
|
+
if (
|
|
103
|
+
!q ||
|
|
104
|
+
normalizeLowercaseStringOrEmpty(chat.chat_id).includes(q) ||
|
|
105
|
+
normalizeLowercaseStringOrEmpty(name).includes(q)
|
|
106
|
+
) {
|
|
107
|
+
groups.push({
|
|
108
|
+
kind: "group",
|
|
109
|
+
id: chat.chat_id,
|
|
110
|
+
name: name || undefined,
|
|
111
|
+
});
|
|
148
112
|
}
|
|
149
113
|
}
|
|
114
|
+
if (groups.length >= limit) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
150
117
|
}
|
|
151
118
|
|
|
152
119
|
return groups;
|
|
153
|
-
} catch {
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (params.fallbackToStatic === false) {
|
|
122
|
+
throw err instanceof Error ? err : new Error("Feishu live group lookup failed");
|
|
123
|
+
}
|
|
154
124
|
return listFeishuDirectoryGroups(params);
|
|
155
125
|
}
|
|
156
126
|
}
|
package/src/doc-schema.ts
CHANGED
|
@@ -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 =
|
|
26
|
-
|
|
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
|
|
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 =
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
});
|
package/src/docx-batch-insert.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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))
|
|
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)
|
|
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:
|
|
90
|
+
blocks: FeishuDocxBlock[],
|
|
61
91
|
firstLevelBlockIds: string[],
|
|
62
92
|
parentBlockId: string = docToken,
|
|
63
93
|
index: number = -1,
|
|
64
|
-
): Promise<
|
|
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:
|
|
136
|
+
blocks: FeishuDocxBlock[],
|
|
108
137
|
firstLevelBlockIds: string[],
|
|
109
138
|
logger?: Logger,
|
|
110
139
|
parentBlockId: string = docToken,
|
|
111
140
|
startIndex: number = -1,
|
|
112
|
-
): Promise<{ children:
|
|
113
|
-
const allChildren:
|
|
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:
|
|
117
|
-
let currentBatch: { firstLevelIds: string[]; 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,
|
|
151
|
+
const blockMap = new Map<string, FeishuDocxBlock>();
|
|
120
152
|
for (const block of blocks) {
|
|
121
|
-
|
|
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
|
-
|
|
185
|
+
if (block.block_id) {
|
|
186
|
+
usedBlockIds.add(block.block_id);
|
|
187
|
+
}
|
|
152
188
|
}
|
|
153
189
|
}
|
|
154
190
|
|
package/src/docx-color-text.ts
CHANGED
|
@@ -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
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
+
});
|