@openclaw/feishu 2026.2.25 → 2026.3.2
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/index.ts +2 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +161 -0
- package/src/accounts.ts +76 -8
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +56 -1
- package/src/bot.test.ts +1271 -56
- package/src/bot.ts +499 -215
- package/src/card-action.ts +79 -0
- package/src/channel.ts +26 -4
- 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 +121 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +101 -1
- package/src/config-schema.ts +66 -11
- package/src/dedup.ts +47 -1
- package/src/doc-schema.ts +135 -0
- package/src/docx-batch-insert.ts +190 -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 +331 -9
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +227 -7
- package/src/media.ts +52 -11
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +534 -0
- package/src/monitor.reaction.test.ts +578 -0
- package/src/monitor.startup.test.ts +203 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +152 -0
- package/src/monitor.test-mocks.ts +12 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +53 -10
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.ts +144 -52
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +271 -0
- package/src/probe.ts +131 -19
- package/src/reply-dispatcher.test.ts +300 -0
- package/src/reply-dispatcher.ts +159 -46
- package/src/secret-input.ts +19 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +6 -2
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +55 -1
- package/src/targets.ts +32 -7
- 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/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +10 -1
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- package/src/wiki.ts +55 -50
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { resolveFeishuSendTarget } from "./send-target.js";
|
|
4
|
+
|
|
5
|
+
const resolveFeishuAccountMock = vi.hoisted(() => vi.fn());
|
|
6
|
+
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("./accounts.js", () => ({
|
|
9
|
+
resolveFeishuAccount: resolveFeishuAccountMock,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("./client.js", () => ({
|
|
13
|
+
createFeishuClient: createFeishuClientMock,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("resolveFeishuSendTarget", () => {
|
|
17
|
+
const cfg = {} as ClawdbotConfig;
|
|
18
|
+
const client = { id: "client" };
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
resolveFeishuAccountMock.mockReset().mockReturnValue({
|
|
22
|
+
accountId: "default",
|
|
23
|
+
enabled: true,
|
|
24
|
+
configured: true,
|
|
25
|
+
});
|
|
26
|
+
createFeishuClientMock.mockReset().mockReturnValue(client);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("keeps explicit group targets as chat_id even when ID shape is ambiguous", () => {
|
|
30
|
+
const result = resolveFeishuSendTarget({
|
|
31
|
+
cfg,
|
|
32
|
+
to: "feishu:group:group_room_alpha",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result.receiveId).toBe("group_room_alpha");
|
|
36
|
+
expect(result.receiveIdType).toBe("chat_id");
|
|
37
|
+
expect(result.client).toBe(client);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("maps dm-prefixed open IDs to open_id", () => {
|
|
41
|
+
const result = resolveFeishuSendTarget({
|
|
42
|
+
cfg,
|
|
43
|
+
to: "lark:dm:ou_123",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.receiveId).toBe("ou_123");
|
|
47
|
+
expect(result.receiveIdType).toBe("open_id");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("maps dm-prefixed non-open IDs to user_id", () => {
|
|
51
|
+
const result = resolveFeishuSendTarget({
|
|
52
|
+
cfg,
|
|
53
|
+
to: " feishu:dm:user_123 ",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.receiveId).toBe("user_123");
|
|
57
|
+
expect(result.receiveIdType).toBe("user_id");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("throws when target account is not configured", () => {
|
|
61
|
+
resolveFeishuAccountMock.mockReturnValue({
|
|
62
|
+
accountId: "default",
|
|
63
|
+
enabled: true,
|
|
64
|
+
configured: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(() =>
|
|
68
|
+
resolveFeishuSendTarget({
|
|
69
|
+
cfg,
|
|
70
|
+
to: "feishu:group:oc_123",
|
|
71
|
+
}),
|
|
72
|
+
).toThrow('Feishu account "default" not configured');
|
|
73
|
+
});
|
|
74
|
+
});
|
package/src/send-target.ts
CHANGED
|
@@ -8,18 +8,22 @@ export function resolveFeishuSendTarget(params: {
|
|
|
8
8
|
to: string;
|
|
9
9
|
accountId?: string;
|
|
10
10
|
}) {
|
|
11
|
+
const target = params.to.trim();
|
|
11
12
|
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
12
13
|
if (!account.configured) {
|
|
13
14
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
14
15
|
}
|
|
15
16
|
const client = createFeishuClient(account);
|
|
16
|
-
const receiveId = normalizeFeishuTarget(
|
|
17
|
+
const receiveId = normalizeFeishuTarget(target);
|
|
17
18
|
if (!receiveId) {
|
|
18
19
|
throw new Error(`Invalid Feishu target: ${params.to}`);
|
|
19
20
|
}
|
|
21
|
+
// Preserve explicit routing prefixes (chat/group/user/dm/open_id) when present.
|
|
22
|
+
// normalizeFeishuTarget strips these prefixes, so infer type from the raw target first.
|
|
23
|
+
const withoutProviderPrefix = target.replace(/^(feishu|lark):/i, "");
|
|
20
24
|
return {
|
|
21
25
|
client,
|
|
22
26
|
receiveId,
|
|
23
|
-
receiveIdType: resolveReceiveIdType(
|
|
27
|
+
receiveIdType: resolveReceiveIdType(withoutProviderPrefix),
|
|
24
28
|
};
|
|
25
29
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
resolveFeishuSendTargetMock.mockReturnValue({
|
|
31
|
+
client: {
|
|
32
|
+
im: {
|
|
33
|
+
message: {
|
|
34
|
+
reply: replyMock,
|
|
35
|
+
create: createMock,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
receiveId: "ou_target",
|
|
40
|
+
receiveIdType: "open_id",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("falls back to create for withdrawn post replies", async () => {
|
|
45
|
+
replyMock.mockResolvedValue({
|
|
46
|
+
code: 230011,
|
|
47
|
+
msg: "The message was withdrawn.",
|
|
48
|
+
});
|
|
49
|
+
createMock.mockResolvedValue({
|
|
50
|
+
code: 0,
|
|
51
|
+
data: { message_id: "om_new" },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await sendMessageFeishu({
|
|
55
|
+
cfg: {} as never,
|
|
56
|
+
to: "user:ou_target",
|
|
57
|
+
text: "hello",
|
|
58
|
+
replyToMessageId: "om_parent",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(replyMock).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(result.messageId).toBe("om_new");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("falls back to create for withdrawn card replies", async () => {
|
|
67
|
+
replyMock.mockResolvedValue({
|
|
68
|
+
code: 231003,
|
|
69
|
+
msg: "The message is not found",
|
|
70
|
+
});
|
|
71
|
+
createMock.mockResolvedValue({
|
|
72
|
+
code: 0,
|
|
73
|
+
data: { message_id: "om_card_new" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = await sendCardFeishu({
|
|
77
|
+
cfg: {} as never,
|
|
78
|
+
to: "user:ou_target",
|
|
79
|
+
card: { schema: "2.0" },
|
|
80
|
+
replyToMessageId: "om_parent",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(replyMock).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(result.messageId).toBe("om_card_new");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("still throws for non-withdrawn reply failures", async () => {
|
|
89
|
+
replyMock.mockResolvedValue({
|
|
90
|
+
code: 999999,
|
|
91
|
+
msg: "unknown failure",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await expect(
|
|
95
|
+
sendMessageFeishu({
|
|
96
|
+
cfg: {} as never,
|
|
97
|
+
to: "user:ou_target",
|
|
98
|
+
text: "hello",
|
|
99
|
+
replyToMessageId: "om_parent",
|
|
100
|
+
}),
|
|
101
|
+
).rejects.toThrow("Feishu reply failed");
|
|
102
|
+
|
|
103
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
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
|
+
});
|
package/src/send.ts
CHANGED
|
@@ -3,21 +3,105 @@ import { resolveFeishuAccount } from "./accounts.js";
|
|
|
3
3
|
import { createFeishuClient } from "./client.js";
|
|
4
4
|
import type { MentionTarget } from "./mention.js";
|
|
5
5
|
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
|
6
|
+
import { parsePostContent } from "./post.js";
|
|
6
7
|
import { getFeishuRuntime } from "./runtime.js";
|
|
7
8
|
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
|
|
8
9
|
import { resolveFeishuSendTarget } from "./send-target.js";
|
|
9
10
|
import type { FeishuSendResult } from "./types.js";
|
|
10
11
|
|
|
12
|
+
const WITHDRAWN_REPLY_ERROR_CODES = new Set([230011, 231003]);
|
|
13
|
+
|
|
14
|
+
function shouldFallbackFromReplyTarget(response: { code?: number; msg?: string }): boolean {
|
|
15
|
+
if (response.code !== undefined && WITHDRAWN_REPLY_ERROR_CODES.has(response.code)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const msg = response.msg?.toLowerCase() ?? "";
|
|
19
|
+
return msg.includes("withdrawn") || msg.includes("not found");
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export type FeishuMessageInfo = {
|
|
12
23
|
messageId: string;
|
|
13
24
|
chatId: string;
|
|
14
25
|
senderId?: string;
|
|
15
26
|
senderOpenId?: string;
|
|
27
|
+
senderType?: string;
|
|
16
28
|
content: string;
|
|
17
29
|
contentType: string;
|
|
18
30
|
createTime?: number;
|
|
19
31
|
};
|
|
20
32
|
|
|
33
|
+
function parseInteractiveCardContent(parsed: unknown): string {
|
|
34
|
+
if (!parsed || typeof parsed !== "object") {
|
|
35
|
+
return "[Interactive Card]";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const candidate = parsed as { elements?: unknown };
|
|
39
|
+
if (!Array.isArray(candidate.elements)) {
|
|
40
|
+
return "[Interactive Card]";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const texts: string[] = [];
|
|
44
|
+
for (const element of candidate.elements) {
|
|
45
|
+
if (!element || typeof element !== "object") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const item = element as {
|
|
49
|
+
tag?: string;
|
|
50
|
+
content?: string;
|
|
51
|
+
text?: { content?: string };
|
|
52
|
+
};
|
|
53
|
+
if (item.tag === "div" && typeof item.text?.content === "string") {
|
|
54
|
+
texts.push(item.text.content);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (item.tag === "markdown" && typeof item.content === "string") {
|
|
58
|
+
texts.push(item.content);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return texts.join("\n").trim() || "[Interactive Card]";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseQuotedMessageContent(rawContent: string, msgType: string): string {
|
|
65
|
+
if (!rawContent) {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let parsed: unknown;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(rawContent);
|
|
72
|
+
} catch {
|
|
73
|
+
return rawContent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (msgType === "text") {
|
|
77
|
+
const text = (parsed as { text?: unknown })?.text;
|
|
78
|
+
return typeof text === "string" ? text : "[Text message]";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (msgType === "post") {
|
|
82
|
+
return parsePostContent(rawContent).textContent;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (msgType === "interactive") {
|
|
86
|
+
return parseInteractiveCardContent(parsed);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof parsed === "string") {
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const genericText = (parsed as { text?: unknown; title?: unknown } | null)?.text;
|
|
94
|
+
if (typeof genericText === "string" && genericText.trim()) {
|
|
95
|
+
return genericText;
|
|
96
|
+
}
|
|
97
|
+
const genericTitle = (parsed as { title?: unknown } | null)?.title;
|
|
98
|
+
if (typeof genericTitle === "string" && genericTitle.trim()) {
|
|
99
|
+
return genericTitle;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return `[${msgType || "unknown"} message]`;
|
|
103
|
+
}
|
|
104
|
+
|
|
21
105
|
/**
|
|
22
106
|
* Get a message by its ID.
|
|
23
107
|
* Useful for fetching quoted/replied message content.
|
|
@@ -54,6 +138,16 @@ export async function getMessageFeishu(params: {
|
|
|
54
138
|
};
|
|
55
139
|
create_time?: string;
|
|
56
140
|
}>;
|
|
141
|
+
message_id?: string;
|
|
142
|
+
chat_id?: string;
|
|
143
|
+
msg_type?: string;
|
|
144
|
+
body?: { content?: string };
|
|
145
|
+
sender?: {
|
|
146
|
+
id?: string;
|
|
147
|
+
id_type?: string;
|
|
148
|
+
sender_type?: string;
|
|
149
|
+
};
|
|
150
|
+
create_time?: string;
|
|
57
151
|
};
|
|
58
152
|
};
|
|
59
153
|
|
|
@@ -61,30 +155,30 @@ export async function getMessageFeishu(params: {
|
|
|
61
155
|
return null;
|
|
62
156
|
}
|
|
63
157
|
|
|
64
|
-
|
|
158
|
+
// Support both list shape (data.items[0]) and single-object shape (data as message)
|
|
159
|
+
const rawItem = response.data?.items?.[0] ?? response.data;
|
|
160
|
+
const item =
|
|
161
|
+
rawItem &&
|
|
162
|
+
(rawItem.body !== undefined || (rawItem as { message_id?: string }).message_id !== undefined)
|
|
163
|
+
? rawItem
|
|
164
|
+
: null;
|
|
65
165
|
if (!item) {
|
|
66
166
|
return null;
|
|
67
167
|
}
|
|
68
168
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const parsed = JSON.parse(content);
|
|
73
|
-
if (item.msg_type === "text" && parsed.text) {
|
|
74
|
-
content = parsed.text;
|
|
75
|
-
}
|
|
76
|
-
} catch {
|
|
77
|
-
// Keep raw content if parsing fails
|
|
78
|
-
}
|
|
169
|
+
const msgType = item.msg_type ?? "text";
|
|
170
|
+
const rawContent = item.body?.content ?? "";
|
|
171
|
+
const content = parseQuotedMessageContent(rawContent, msgType);
|
|
79
172
|
|
|
80
173
|
return {
|
|
81
174
|
messageId: item.message_id ?? messageId,
|
|
82
175
|
chatId: item.chat_id ?? "",
|
|
83
176
|
senderId: item.sender?.id,
|
|
84
177
|
senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
|
|
178
|
+
senderType: item.sender?.sender_type,
|
|
85
179
|
content,
|
|
86
|
-
contentType:
|
|
87
|
-
createTime: item.create_time ? parseInt(item.create_time, 10) : undefined,
|
|
180
|
+
contentType: msgType,
|
|
181
|
+
createTime: item.create_time ? parseInt(String(item.create_time), 10) : undefined,
|
|
88
182
|
};
|
|
89
183
|
} catch {
|
|
90
184
|
return null;
|
|
@@ -96,6 +190,8 @@ export type SendFeishuMessageParams = {
|
|
|
96
190
|
to: string;
|
|
97
191
|
text: string;
|
|
98
192
|
replyToMessageId?: string;
|
|
193
|
+
/** When true, reply creates a Feishu topic thread instead of an inline reply */
|
|
194
|
+
replyInThread?: boolean;
|
|
99
195
|
/** Mention target users */
|
|
100
196
|
mentions?: MentionTarget[];
|
|
101
197
|
/** Account ID (optional, uses default if not specified) */
|
|
@@ -127,7 +223,7 @@ function buildFeishuPostMessagePayload(params: { messageText: string }): {
|
|
|
127
223
|
export async function sendMessageFeishu(
|
|
128
224
|
params: SendFeishuMessageParams,
|
|
129
225
|
): Promise<FeishuSendResult> {
|
|
130
|
-
const { cfg, to, text, replyToMessageId, mentions, accountId } = params;
|
|
226
|
+
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
|
|
131
227
|
const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
|
|
132
228
|
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
133
229
|
cfg,
|
|
@@ -149,8 +245,21 @@ export async function sendMessageFeishu(
|
|
|
149
245
|
data: {
|
|
150
246
|
content,
|
|
151
247
|
msg_type: msgType,
|
|
248
|
+
...(replyInThread ? { reply_in_thread: true } : {}),
|
|
152
249
|
},
|
|
153
250
|
});
|
|
251
|
+
if (shouldFallbackFromReplyTarget(response)) {
|
|
252
|
+
const fallback = await client.im.message.create({
|
|
253
|
+
params: { receive_id_type: receiveIdType },
|
|
254
|
+
data: {
|
|
255
|
+
receive_id: receiveId,
|
|
256
|
+
content,
|
|
257
|
+
msg_type: msgType,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
assertFeishuMessageApiSuccess(fallback, "Feishu send failed");
|
|
261
|
+
return toFeishuSendResult(fallback, receiveId);
|
|
262
|
+
}
|
|
154
263
|
assertFeishuMessageApiSuccess(response, "Feishu reply failed");
|
|
155
264
|
return toFeishuSendResult(response, receiveId);
|
|
156
265
|
}
|
|
@@ -172,11 +281,13 @@ export type SendFeishuCardParams = {
|
|
|
172
281
|
to: string;
|
|
173
282
|
card: Record<string, unknown>;
|
|
174
283
|
replyToMessageId?: string;
|
|
284
|
+
/** When true, reply creates a Feishu topic thread instead of an inline reply */
|
|
285
|
+
replyInThread?: boolean;
|
|
175
286
|
accountId?: string;
|
|
176
287
|
};
|
|
177
288
|
|
|
178
289
|
export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
|
|
179
|
-
const { cfg, to, card, replyToMessageId, accountId } = params;
|
|
290
|
+
const { cfg, to, card, replyToMessageId, replyInThread, accountId } = params;
|
|
180
291
|
const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
|
|
181
292
|
const content = JSON.stringify(card);
|
|
182
293
|
|
|
@@ -186,8 +297,21 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
|
|
|
186
297
|
data: {
|
|
187
298
|
content,
|
|
188
299
|
msg_type: "interactive",
|
|
300
|
+
...(replyInThread ? { reply_in_thread: true } : {}),
|
|
189
301
|
},
|
|
190
302
|
});
|
|
303
|
+
if (shouldFallbackFromReplyTarget(response)) {
|
|
304
|
+
const fallback = await client.im.message.create({
|
|
305
|
+
params: { receive_id_type: receiveIdType },
|
|
306
|
+
data: {
|
|
307
|
+
receive_id: receiveId,
|
|
308
|
+
content,
|
|
309
|
+
msg_type: "interactive",
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
assertFeishuMessageApiSuccess(fallback, "Feishu card send failed");
|
|
313
|
+
return toFeishuSendResult(fallback, receiveId);
|
|
314
|
+
}
|
|
191
315
|
assertFeishuMessageApiSuccess(response, "Feishu card reply failed");
|
|
192
316
|
return toFeishuSendResult(response, receiveId);
|
|
193
317
|
}
|
|
@@ -260,18 +384,19 @@ export async function sendMarkdownCardFeishu(params: {
|
|
|
260
384
|
to: string;
|
|
261
385
|
text: string;
|
|
262
386
|
replyToMessageId?: string;
|
|
387
|
+
/** When true, reply creates a Feishu topic thread instead of an inline reply */
|
|
388
|
+
replyInThread?: boolean;
|
|
263
389
|
/** Mention target users */
|
|
264
390
|
mentions?: MentionTarget[];
|
|
265
391
|
accountId?: string;
|
|
266
392
|
}): Promise<FeishuSendResult> {
|
|
267
|
-
const { cfg, to, text, replyToMessageId, mentions, accountId } = params;
|
|
268
|
-
// Build message content (with @mention support)
|
|
393
|
+
const { cfg, to, text, replyToMessageId, replyInThread, mentions, accountId } = params;
|
|
269
394
|
let cardText = text;
|
|
270
395
|
if (mentions && mentions.length > 0) {
|
|
271
396
|
cardText = buildMentionedCardContent(mentions, text);
|
|
272
397
|
}
|
|
273
398
|
const card = buildMarkdownCard(cardText);
|
|
274
|
-
return sendCardFeishu({ cfg, to, card, replyToMessageId, accountId });
|
|
399
|
+
return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread, accountId });
|
|
275
400
|
}
|
|
276
401
|
|
|
277
402
|
/**
|