@nextclaw/channel-plugin-feishu 0.2.13 → 0.2.15
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/README.md +3 -1
- package/index.ts +65 -0
- package/openclaw.plugin.json +3 -7
- package/package.json +32 -9
- package/skills/feishu-doc/SKILL.md +211 -0
- package/skills/feishu-doc/references/block-types.md +103 -0
- package/skills/feishu-drive/SKILL.md +97 -0
- package/skills/feishu-perm/SKILL.md +119 -0
- package/skills/feishu-wiki/SKILL.md +111 -0
- package/src/accounts.test.ts +371 -0
- package/src/accounts.ts +244 -0
- package/src/async.ts +62 -0
- package/src/bitable.ts +725 -0
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +193 -0
- package/src/bot.stripBotMention.test.ts +134 -0
- package/src/bot.test.ts +2107 -0
- package/src/bot.ts +1556 -0
- package/src/card-action.ts +79 -0
- package/src/channel.test.ts +48 -0
- package/src/channel.ts +369 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +324 -0
- package/src/client.ts +196 -0
- package/src/config-schema.test.ts +247 -0
- package/src/config-schema.ts +306 -0
- package/src/dedup.ts +203 -0
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +156 -0
- package/src/doc-schema.ts +182 -0
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +187 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +70 -0
- package/src/docx.test.ts +445 -0
- package/src/docx.ts +1460 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +228 -0
- package/src/dynamic-agent.ts +131 -0
- package/src/external-keys.test.ts +20 -0
- package/src/external-keys.ts +19 -0
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +523 -0
- package/src/media.ts +484 -0
- package/src/mention.ts +133 -0
- package/src/monitor.account.ts +562 -0
- package/src/monitor.reaction.test.ts +653 -0
- package/src/monitor.startup.test.ts +190 -0
- package/src/monitor.startup.ts +64 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +155 -0
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +264 -0
- package/src/monitor.ts +95 -0
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +142 -0
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/nextclaw-sdk/account-id.ts +31 -0
- package/src/nextclaw-sdk/compat.ts +8 -0
- package/src/nextclaw-sdk/core-channel.ts +296 -0
- package/src/nextclaw-sdk/core-pairing.ts +224 -0
- package/src/nextclaw-sdk/core.ts +26 -0
- package/src/nextclaw-sdk/dedupe.ts +246 -0
- package/src/nextclaw-sdk/feishu.ts +77 -0
- package/src/nextclaw-sdk/history.ts +127 -0
- package/src/nextclaw-sdk/network-body.ts +245 -0
- package/src/nextclaw-sdk/network-fetch.ts +129 -0
- package/src/nextclaw-sdk/network-webhook.ts +182 -0
- package/src/nextclaw-sdk/network.ts +13 -0
- package/src/nextclaw-sdk/runtime-store.ts +26 -0
- package/src/nextclaw-sdk/secrets-config.ts +109 -0
- package/src/nextclaw-sdk/secrets-core.ts +170 -0
- package/src/nextclaw-sdk/secrets-prompt.ts +305 -0
- package/src/nextclaw-sdk/secrets.ts +18 -0
- package/src/nextclaw-sdk/types.ts +300 -0
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +489 -0
- package/src/outbound.test.ts +356 -0
- package/src/outbound.ts +176 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +176 -0
- package/src/policy.test.ts +154 -0
- package/src/policy.ts +123 -0
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +270 -0
- package/src/probe.ts +156 -0
- package/src/reactions.ts +153 -0
- package/src/reply-dispatcher.test.ts +513 -0
- package/src/reply-dispatcher.ts +397 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-result.ts +29 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +29 -0
- package/src/send.reply-fallback.test.ts +189 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +481 -0
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +374 -0
- package/src/targets.test.ts +70 -0
- package/src/targets.ts +107 -0
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +103 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +210 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +233 -0
- package/index.js +0 -27
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const resolveFeishuSendTargetMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
const resolveMarkdownTableModeMock = vi.hoisted(() => vi.fn(() => "preserve"));
|
|
5
|
+
const convertMarkdownTablesMock = vi.hoisted(() => vi.fn((text: string) => text));
|
|
6
|
+
|
|
7
|
+
vi.mock("./send-target.js", () => ({
|
|
8
|
+
resolveFeishuSendTarget: resolveFeishuSendTargetMock,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./runtime.js", () => ({
|
|
12
|
+
getFeishuRuntime: () => ({
|
|
13
|
+
channel: {
|
|
14
|
+
text: {
|
|
15
|
+
resolveMarkdownTableMode: resolveMarkdownTableModeMock,
|
|
16
|
+
convertMarkdownTables: convertMarkdownTablesMock,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { sendCardFeishu, sendMessageFeishu } from "./send.js";
|
|
23
|
+
|
|
24
|
+
describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
25
|
+
const replyMock = vi.fn();
|
|
26
|
+
const createMock = vi.fn();
|
|
27
|
+
|
|
28
|
+
async function expectFallbackResult(
|
|
29
|
+
send: () => Promise<{ messageId?: string }>,
|
|
30
|
+
expectedMessageId: string,
|
|
31
|
+
) {
|
|
32
|
+
const result = await send();
|
|
33
|
+
expect(replyMock).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(result.messageId).toBe(expectedMessageId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
resolveFeishuSendTargetMock.mockReturnValue({
|
|
41
|
+
client: {
|
|
42
|
+
im: {
|
|
43
|
+
message: {
|
|
44
|
+
reply: replyMock,
|
|
45
|
+
create: createMock,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
receiveId: "ou_target",
|
|
50
|
+
receiveIdType: "open_id",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("falls back to create for withdrawn post replies", async () => {
|
|
55
|
+
replyMock.mockResolvedValue({
|
|
56
|
+
code: 230011,
|
|
57
|
+
msg: "The message was withdrawn.",
|
|
58
|
+
});
|
|
59
|
+
createMock.mockResolvedValue({
|
|
60
|
+
code: 0,
|
|
61
|
+
data: { message_id: "om_new" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await expectFallbackResult(
|
|
65
|
+
() =>
|
|
66
|
+
sendMessageFeishu({
|
|
67
|
+
cfg: {} as never,
|
|
68
|
+
to: "user:ou_target",
|
|
69
|
+
text: "hello",
|
|
70
|
+
replyToMessageId: "om_parent",
|
|
71
|
+
}),
|
|
72
|
+
"om_new",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("falls back to create for withdrawn card replies", async () => {
|
|
77
|
+
replyMock.mockResolvedValue({
|
|
78
|
+
code: 231003,
|
|
79
|
+
msg: "The message is not found",
|
|
80
|
+
});
|
|
81
|
+
createMock.mockResolvedValue({
|
|
82
|
+
code: 0,
|
|
83
|
+
data: { message_id: "om_card_new" },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await expectFallbackResult(
|
|
87
|
+
() =>
|
|
88
|
+
sendCardFeishu({
|
|
89
|
+
cfg: {} as never,
|
|
90
|
+
to: "user:ou_target",
|
|
91
|
+
card: { schema: "2.0" },
|
|
92
|
+
replyToMessageId: "om_parent",
|
|
93
|
+
}),
|
|
94
|
+
"om_card_new",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("still throws for non-withdrawn reply failures", async () => {
|
|
99
|
+
replyMock.mockResolvedValue({
|
|
100
|
+
code: 999999,
|
|
101
|
+
msg: "unknown failure",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await expect(
|
|
105
|
+
sendMessageFeishu({
|
|
106
|
+
cfg: {} as never,
|
|
107
|
+
to: "user:ou_target",
|
|
108
|
+
text: "hello",
|
|
109
|
+
replyToMessageId: "om_parent",
|
|
110
|
+
}),
|
|
111
|
+
).rejects.toThrow("Feishu reply failed");
|
|
112
|
+
|
|
113
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("falls back to create when reply throws a withdrawn SDK error", async () => {
|
|
117
|
+
const sdkError = Object.assign(new Error("request failed"), { code: 230011 });
|
|
118
|
+
replyMock.mockRejectedValue(sdkError);
|
|
119
|
+
createMock.mockResolvedValue({
|
|
120
|
+
code: 0,
|
|
121
|
+
data: { message_id: "om_thrown_fallback" },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await expectFallbackResult(
|
|
125
|
+
() =>
|
|
126
|
+
sendMessageFeishu({
|
|
127
|
+
cfg: {} as never,
|
|
128
|
+
to: "user:ou_target",
|
|
129
|
+
text: "hello",
|
|
130
|
+
replyToMessageId: "om_parent",
|
|
131
|
+
}),
|
|
132
|
+
"om_thrown_fallback",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("falls back to create when card reply throws a not-found AxiosError", async () => {
|
|
137
|
+
const axiosError = Object.assign(new Error("Request failed"), {
|
|
138
|
+
response: { status: 200, data: { code: 231003, msg: "The message is not found" } },
|
|
139
|
+
});
|
|
140
|
+
replyMock.mockRejectedValue(axiosError);
|
|
141
|
+
createMock.mockResolvedValue({
|
|
142
|
+
code: 0,
|
|
143
|
+
data: { message_id: "om_axios_fallback" },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await expectFallbackResult(
|
|
147
|
+
() =>
|
|
148
|
+
sendCardFeishu({
|
|
149
|
+
cfg: {} as never,
|
|
150
|
+
to: "user:ou_target",
|
|
151
|
+
card: { schema: "2.0" },
|
|
152
|
+
replyToMessageId: "om_parent",
|
|
153
|
+
}),
|
|
154
|
+
"om_axios_fallback",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("re-throws non-withdrawn thrown errors for text messages", async () => {
|
|
159
|
+
const sdkError = Object.assign(new Error("rate limited"), { code: 99991400 });
|
|
160
|
+
replyMock.mockRejectedValue(sdkError);
|
|
161
|
+
|
|
162
|
+
await expect(
|
|
163
|
+
sendMessageFeishu({
|
|
164
|
+
cfg: {} as never,
|
|
165
|
+
to: "user:ou_target",
|
|
166
|
+
text: "hello",
|
|
167
|
+
replyToMessageId: "om_parent",
|
|
168
|
+
}),
|
|
169
|
+
).rejects.toThrow("rate limited");
|
|
170
|
+
|
|
171
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("re-throws non-withdrawn thrown errors for card messages", async () => {
|
|
175
|
+
const sdkError = Object.assign(new Error("permission denied"), { code: 99991401 });
|
|
176
|
+
replyMock.mockRejectedValue(sdkError);
|
|
177
|
+
|
|
178
|
+
await expect(
|
|
179
|
+
sendCardFeishu({
|
|
180
|
+
cfg: {} as never,
|
|
181
|
+
to: "user:ou_target",
|
|
182
|
+
card: { schema: "2.0" },
|
|
183
|
+
replyToMessageId: "om_parent",
|
|
184
|
+
}),
|
|
185
|
+
).rejects.toThrow("permission denied");
|
|
186
|
+
|
|
187
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
});
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "./nextclaw-sdk/feishu.js";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { getMessageFeishu } from "./send.js";
|
|
4
|
+
|
|
5
|
+
const { mockClientGet, mockCreateFeishuClient, mockResolveFeishuAccount } = vi.hoisted(() => ({
|
|
6
|
+
mockClientGet: vi.fn(),
|
|
7
|
+
mockCreateFeishuClient: vi.fn(),
|
|
8
|
+
mockResolveFeishuAccount: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./client.js", () => ({
|
|
12
|
+
createFeishuClient: mockCreateFeishuClient,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("./accounts.js", () => ({
|
|
16
|
+
resolveFeishuAccount: mockResolveFeishuAccount,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe("getMessageFeishu", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
mockResolveFeishuAccount.mockReturnValue({
|
|
23
|
+
accountId: "default",
|
|
24
|
+
configured: true,
|
|
25
|
+
});
|
|
26
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
27
|
+
im: {
|
|
28
|
+
message: {
|
|
29
|
+
get: mockClientGet,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("extracts text content from interactive card elements", async () => {
|
|
36
|
+
mockClientGet.mockResolvedValueOnce({
|
|
37
|
+
code: 0,
|
|
38
|
+
data: {
|
|
39
|
+
items: [
|
|
40
|
+
{
|
|
41
|
+
message_id: "om_1",
|
|
42
|
+
chat_id: "oc_1",
|
|
43
|
+
msg_type: "interactive",
|
|
44
|
+
body: {
|
|
45
|
+
content: JSON.stringify({
|
|
46
|
+
elements: [
|
|
47
|
+
{ tag: "markdown", content: "hello markdown" },
|
|
48
|
+
{ tag: "div", text: { content: "hello div" } },
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const result = await getMessageFeishu({
|
|
58
|
+
cfg: {} as ClawdbotConfig,
|
|
59
|
+
messageId: "om_1",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual(
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
messageId: "om_1",
|
|
65
|
+
chatId: "oc_1",
|
|
66
|
+
contentType: "interactive",
|
|
67
|
+
content: "hello markdown\nhello div",
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("extracts text content from post messages", async () => {
|
|
73
|
+
mockClientGet.mockResolvedValueOnce({
|
|
74
|
+
code: 0,
|
|
75
|
+
data: {
|
|
76
|
+
items: [
|
|
77
|
+
{
|
|
78
|
+
message_id: "om_post",
|
|
79
|
+
chat_id: "oc_post",
|
|
80
|
+
msg_type: "post",
|
|
81
|
+
body: {
|
|
82
|
+
content: JSON.stringify({
|
|
83
|
+
zh_cn: {
|
|
84
|
+
title: "Summary",
|
|
85
|
+
content: [[{ tag: "text", text: "post body" }]],
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const result = await getMessageFeishu({
|
|
95
|
+
cfg: {} as ClawdbotConfig,
|
|
96
|
+
messageId: "om_post",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual(
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
messageId: "om_post",
|
|
102
|
+
chatId: "oc_post",
|
|
103
|
+
contentType: "post",
|
|
104
|
+
content: "Summary\n\npost body",
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns text placeholder instead of raw JSON for unsupported message types", async () => {
|
|
110
|
+
mockClientGet.mockResolvedValueOnce({
|
|
111
|
+
code: 0,
|
|
112
|
+
data: {
|
|
113
|
+
items: [
|
|
114
|
+
{
|
|
115
|
+
message_id: "om_file",
|
|
116
|
+
chat_id: "oc_file",
|
|
117
|
+
msg_type: "file",
|
|
118
|
+
body: {
|
|
119
|
+
content: JSON.stringify({ file_key: "file_v3_123" }),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = await getMessageFeishu({
|
|
127
|
+
cfg: {} as ClawdbotConfig,
|
|
128
|
+
messageId: "om_file",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result).toEqual(
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
messageId: "om_file",
|
|
134
|
+
chatId: "oc_file",
|
|
135
|
+
contentType: "file",
|
|
136
|
+
content: "[file message]",
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("supports single-object response shape from Feishu API", async () => {
|
|
142
|
+
mockClientGet.mockResolvedValueOnce({
|
|
143
|
+
code: 0,
|
|
144
|
+
data: {
|
|
145
|
+
message_id: "om_single",
|
|
146
|
+
chat_id: "oc_single",
|
|
147
|
+
msg_type: "text",
|
|
148
|
+
body: {
|
|
149
|
+
content: JSON.stringify({ text: "single payload" }),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await getMessageFeishu({
|
|
155
|
+
cfg: {} as ClawdbotConfig,
|
|
156
|
+
messageId: "om_single",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual(
|
|
160
|
+
expect.objectContaining({
|
|
161
|
+
messageId: "om_single",
|
|
162
|
+
chatId: "oc_single",
|
|
163
|
+
contentType: "text",
|
|
164
|
+
content: "single payload",
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|