@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.
- 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 +1827 -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 +1253 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +135 -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 +406 -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 +33 -95
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +116 -20
- package/src/directory.ts +60 -92
- 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 +403 -26
- package/src/media.ts +509 -132
- 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.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 +105 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +414 -95
- 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 +453 -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,20 @@
|
|
|
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
|
-
}
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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,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
|
+
});
|