@m1heng-clawd/feishu 0.1.11 → 0.1.13
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 +183 -14
- package/index.ts +6 -6
- package/package.json +13 -3
- package/skills/feishu-doc/SKILL.md +40 -2
- package/skills/feishu-task/SKILL.md +210 -0
- package/src/bitable-tools/index.ts +1 -0
- package/src/bot.ts +181 -70
- package/src/channel.ts +4 -0
- package/src/config-schema.ts +6 -0
- package/src/doc-tools/actions.ts +341 -0
- package/src/doc-tools/common.ts +33 -0
- package/src/doc-tools/index.ts +2 -0
- package/src/doc-tools/register.ts +90 -0
- package/src/{doc-schema.ts → doc-tools/schemas.ts} +31 -1
- package/src/drive-tools/actions.ts +182 -0
- package/src/drive-tools/common.ts +18 -0
- package/src/drive-tools/index.ts +2 -0
- package/src/drive-tools/register.ts +71 -0
- package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
- package/src/media.ts +29 -7
- package/src/perm-tools/actions.ts +111 -0
- package/src/perm-tools/common.ts +18 -0
- package/src/perm-tools/index.ts +2 -0
- package/src/perm-tools/register.ts +65 -0
- package/src/policy.ts +13 -0
- package/src/reply-dispatcher.ts +7 -5
- package/src/send.ts +33 -2
- package/src/streaming-card.ts +11 -8
- package/src/task-tools/actions.ts +468 -13
- package/src/task-tools/constants.ts +13 -0
- package/src/task-tools/index.ts +1 -0
- package/src/task-tools/register.ts +175 -13
- package/src/task-tools/schemas.ts +422 -4
- package/src/text/markdown-links.ts +104 -0
- package/src/types.ts +1 -0
- package/src/wiki-tools/actions.ts +166 -0
- package/src/wiki-tools/common.ts +18 -0
- package/src/wiki-tools/index.ts +2 -0
- package/src/wiki-tools/register.ts +66 -0
- package/src/bitable.ts +0 -1
- package/src/docx.ts +0 -276
- package/src/drive.ts +0 -237
- package/src/perm.ts +0 -165
- package/src/task.ts +0 -1
- package/src/wiki.ts +0 -223
- /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
- /package/src/{wiki-schema.ts → wiki-tools/schemas.ts} +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFeishuClient } from "../client.js";
|
|
2
|
+
import {
|
|
3
|
+
errorResult,
|
|
4
|
+
json,
|
|
5
|
+
runFeishuApiCall,
|
|
6
|
+
type FeishuApiResponse,
|
|
7
|
+
} from "../tools-common/feishu-api.js";
|
|
8
|
+
|
|
9
|
+
export type DriveClient = ReturnType<typeof createFeishuClient>;
|
|
10
|
+
|
|
11
|
+
export { json, errorResult };
|
|
12
|
+
|
|
13
|
+
export async function runDriveApiCall<T extends FeishuApiResponse>(
|
|
14
|
+
context: string,
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
return runFeishuApiCall(context, fn);
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ResolvedFeishuAccount } from "../types.js";
|
|
4
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
5
|
+
import { runDriveAction } from "./actions.js";
|
|
6
|
+
import { errorResult, json, type DriveClient } from "./common.js";
|
|
7
|
+
import { FeishuDriveSchema, type FeishuDriveParams } from "./schemas.js";
|
|
8
|
+
|
|
9
|
+
type DriveToolSpec<P> = {
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
parameters: TSchema;
|
|
14
|
+
run: (args: { client: DriveClient; account: ResolvedFeishuAccount }, params: P) => Promise<unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function registerDriveTool<P>(api: OpenClawPluginApi, spec: DriveToolSpec<P>) {
|
|
18
|
+
api.registerTool(
|
|
19
|
+
{
|
|
20
|
+
name: spec.name,
|
|
21
|
+
label: spec.label,
|
|
22
|
+
description: spec.description,
|
|
23
|
+
parameters: spec.parameters,
|
|
24
|
+
async execute(_toolCallId, params) {
|
|
25
|
+
try {
|
|
26
|
+
return await withFeishuToolClient({
|
|
27
|
+
api,
|
|
28
|
+
toolName: spec.name,
|
|
29
|
+
requiredTool: "drive",
|
|
30
|
+
run: async ({ client, account }) =>
|
|
31
|
+
json(await spec.run({ client: client as DriveClient, account }, params as P)),
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return errorResult(err);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{ name: spec.name },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
43
|
+
if (!api.config) {
|
|
44
|
+
api.logger.debug?.("feishu_drive: No config available, skipping drive tools");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
49
|
+
api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "drive")) {
|
|
54
|
+
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
registerDriveTool<FeishuDriveParams>(api, {
|
|
59
|
+
name: "feishu_drive",
|
|
60
|
+
label: "Feishu Drive",
|
|
61
|
+
description:
|
|
62
|
+
"Feishu cloud storage operations. Actions: list, info, create_folder, move, delete, import_document. Use 'import_document' to create documents from Markdown with better structure preservation than block-by-block writing.",
|
|
63
|
+
parameters: FeishuDriveSchema,
|
|
64
|
+
run: async ({ client, account }, params) => {
|
|
65
|
+
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
66
|
+
return runDriveAction(client, params, mediaMaxBytes);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
api.logger.debug?.("feishu_drive: Registered feishu_drive tool");
|
|
71
|
+
}
|
|
@@ -52,7 +52,8 @@ export const FeishuDriveSchema = Type.Union([
|
|
|
52
52
|
description: "Document title",
|
|
53
53
|
}),
|
|
54
54
|
content: Type.String({
|
|
55
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
"Markdown content to import. Supports full Markdown syntax including tables, lists, code blocks, etc.",
|
|
56
57
|
}),
|
|
57
58
|
folder_token: Type.Optional(
|
|
58
59
|
Type.String({
|
package/src/media.ts
CHANGED
|
@@ -354,7 +354,8 @@ export async function sendFileFeishu(params: {
|
|
|
354
354
|
cfg: ClawdbotConfig;
|
|
355
355
|
to: string;
|
|
356
356
|
fileKey: string;
|
|
357
|
-
|
|
357
|
+
/** Use "audio" for audio, "media" for video, "file" for documents */
|
|
358
|
+
msgType?: "file" | "audio" | "media";
|
|
358
359
|
replyToMessageId?: string;
|
|
359
360
|
accountId?: string;
|
|
360
361
|
}): Promise<SendMediaResult> {
|
|
@@ -469,10 +470,30 @@ export async function sendMediaFeishu(params: {
|
|
|
469
470
|
buffer = mediaBuffer;
|
|
470
471
|
name = fileName ?? "file";
|
|
471
472
|
} else if (mediaUrl) {
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
const mediaLocalRoots = (account.config?.mediaLocalRoots ?? [])
|
|
474
|
+
.map((root) => root.trim())
|
|
475
|
+
.filter((root) => root.length > 0);
|
|
476
|
+
const loaded = await (async () => {
|
|
477
|
+
try {
|
|
478
|
+
return await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
|
|
479
|
+
maxBytes: mediaMaxBytes,
|
|
480
|
+
optimizeImages: false,
|
|
481
|
+
});
|
|
482
|
+
} catch (err) {
|
|
483
|
+
const shouldRetryWithCustomRoots =
|
|
484
|
+
mediaLocalRoots.length > 0 &&
|
|
485
|
+
err instanceof Error &&
|
|
486
|
+
err.message.includes("Local media path is not under an allowed directory");
|
|
487
|
+
if (!shouldRetryWithCustomRoots) {
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
490
|
+
return await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
|
|
491
|
+
maxBytes: mediaMaxBytes,
|
|
492
|
+
optimizeImages: false,
|
|
493
|
+
localRoots: mediaLocalRoots,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
})();
|
|
476
497
|
buffer = loaded.buffer;
|
|
477
498
|
name = fileName ?? loaded.fileName ?? "file";
|
|
478
499
|
} else {
|
|
@@ -495,12 +516,13 @@ export async function sendMediaFeishu(params: {
|
|
|
495
516
|
fileType,
|
|
496
517
|
accountId,
|
|
497
518
|
});
|
|
498
|
-
|
|
519
|
+
// Feishu requires msg_type "audio" for audio, "media" for video, "file" for documents
|
|
520
|
+
const msgType = fileType === "opus" ? "audio" : fileType === "mp4" ? "media" : "file";
|
|
499
521
|
return sendFileFeishu({
|
|
500
522
|
cfg,
|
|
501
523
|
to,
|
|
502
524
|
fileKey,
|
|
503
|
-
msgType
|
|
525
|
+
msgType,
|
|
504
526
|
replyToMessageId,
|
|
505
527
|
accountId,
|
|
506
528
|
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { runPermApiCall, type PermClient } from "./common.js";
|
|
2
|
+
import type { FeishuPermParams } from "./schemas.js";
|
|
3
|
+
|
|
4
|
+
type ListTokenType =
|
|
5
|
+
| "doc"
|
|
6
|
+
| "sheet"
|
|
7
|
+
| "file"
|
|
8
|
+
| "wiki"
|
|
9
|
+
| "bitable"
|
|
10
|
+
| "docx"
|
|
11
|
+
| "mindnote"
|
|
12
|
+
| "minutes"
|
|
13
|
+
| "slides";
|
|
14
|
+
type CreateTokenType =
|
|
15
|
+
| "doc"
|
|
16
|
+
| "sheet"
|
|
17
|
+
| "file"
|
|
18
|
+
| "wiki"
|
|
19
|
+
| "bitable"
|
|
20
|
+
| "docx"
|
|
21
|
+
| "folder"
|
|
22
|
+
| "mindnote"
|
|
23
|
+
| "minutes"
|
|
24
|
+
| "slides";
|
|
25
|
+
type MemberType =
|
|
26
|
+
| "email"
|
|
27
|
+
| "openid"
|
|
28
|
+
| "unionid"
|
|
29
|
+
| "openchat"
|
|
30
|
+
| "opendepartmentid"
|
|
31
|
+
| "userid"
|
|
32
|
+
| "groupid"
|
|
33
|
+
| "wikispaceid";
|
|
34
|
+
type PermType = "view" | "edit" | "full_access";
|
|
35
|
+
|
|
36
|
+
async function listMembers(client: PermClient, token: string, type: string) {
|
|
37
|
+
const res = await runPermApiCall("drive.permissionMember.list", () =>
|
|
38
|
+
client.drive.permissionMember.list({
|
|
39
|
+
path: { token },
|
|
40
|
+
params: { type: type as ListTokenType },
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
members:
|
|
46
|
+
res.data?.items?.map((m) => ({
|
|
47
|
+
member_type: m.member_type,
|
|
48
|
+
member_id: m.member_id,
|
|
49
|
+
perm: m.perm,
|
|
50
|
+
name: m.name,
|
|
51
|
+
})) ?? [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function addMember(
|
|
56
|
+
client: PermClient,
|
|
57
|
+
token: string,
|
|
58
|
+
type: string,
|
|
59
|
+
memberType: string,
|
|
60
|
+
memberId: string,
|
|
61
|
+
perm: string,
|
|
62
|
+
) {
|
|
63
|
+
const res = await runPermApiCall("drive.permissionMember.create", () =>
|
|
64
|
+
client.drive.permissionMember.create({
|
|
65
|
+
path: { token },
|
|
66
|
+
params: { type: type as CreateTokenType, need_notification: false },
|
|
67
|
+
data: {
|
|
68
|
+
member_type: memberType as MemberType,
|
|
69
|
+
member_id: memberId,
|
|
70
|
+
perm: perm as PermType,
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
member: res.data?.member,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function removeMember(
|
|
82
|
+
client: PermClient,
|
|
83
|
+
token: string,
|
|
84
|
+
type: string,
|
|
85
|
+
memberType: string,
|
|
86
|
+
memberId: string,
|
|
87
|
+
) {
|
|
88
|
+
await runPermApiCall("drive.permissionMember.delete", () =>
|
|
89
|
+
client.drive.permissionMember.delete({
|
|
90
|
+
path: { token, member_id: memberId },
|
|
91
|
+
params: { type: type as CreateTokenType, member_type: memberType as MemberType },
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function runPermAction(client: PermClient, params: FeishuPermParams) {
|
|
101
|
+
switch (params.action) {
|
|
102
|
+
case "list":
|
|
103
|
+
return listMembers(client, params.token, params.type);
|
|
104
|
+
case "add":
|
|
105
|
+
return addMember(client, params.token, params.type, params.member_type, params.member_id, params.perm);
|
|
106
|
+
case "remove":
|
|
107
|
+
return removeMember(client, params.token, params.type, params.member_type, params.member_id);
|
|
108
|
+
default:
|
|
109
|
+
return { error: `Unknown action: ${(params as any).action}` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFeishuClient } from "../client.js";
|
|
2
|
+
import {
|
|
3
|
+
errorResult,
|
|
4
|
+
json,
|
|
5
|
+
runFeishuApiCall,
|
|
6
|
+
type FeishuApiResponse,
|
|
7
|
+
} from "../tools-common/feishu-api.js";
|
|
8
|
+
|
|
9
|
+
export type PermClient = ReturnType<typeof createFeishuClient>;
|
|
10
|
+
|
|
11
|
+
export { json, errorResult };
|
|
12
|
+
|
|
13
|
+
export async function runPermApiCall<T extends FeishuApiResponse>(
|
|
14
|
+
context: string,
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
return runFeishuApiCall(context, fn);
|
|
18
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
4
|
+
import { runPermAction } from "./actions.js";
|
|
5
|
+
import { errorResult, json, type PermClient } from "./common.js";
|
|
6
|
+
import { FeishuPermSchema, type FeishuPermParams } from "./schemas.js";
|
|
7
|
+
|
|
8
|
+
type PermToolSpec<P> = {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: TSchema;
|
|
13
|
+
run: (client: PermClient, params: P) => Promise<unknown>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function registerPermTool<P>(api: OpenClawPluginApi, spec: PermToolSpec<P>) {
|
|
17
|
+
api.registerTool(
|
|
18
|
+
{
|
|
19
|
+
name: spec.name,
|
|
20
|
+
label: spec.label,
|
|
21
|
+
description: spec.description,
|
|
22
|
+
parameters: spec.parameters,
|
|
23
|
+
async execute(_toolCallId, params) {
|
|
24
|
+
try {
|
|
25
|
+
return await withFeishuToolClient({
|
|
26
|
+
api,
|
|
27
|
+
toolName: spec.name,
|
|
28
|
+
requiredTool: "perm",
|
|
29
|
+
run: async ({ client }) => json(await spec.run(client as PermClient, params as P)),
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return errorResult(err);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{ name: spec.name },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
41
|
+
if (!api.config) {
|
|
42
|
+
api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
47
|
+
api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "perm")) {
|
|
52
|
+
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
registerPermTool<FeishuPermParams>(api, {
|
|
57
|
+
name: "feishu_perm",
|
|
58
|
+
label: "Feishu Perm",
|
|
59
|
+
description: "Feishu permission management. Actions: list, add, remove",
|
|
60
|
+
parameters: FeishuPermSchema,
|
|
61
|
+
run: (client, params) => runPermAction(client, params),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
api.logger.debug?.("feishu_perm: Registered feishu_perm tool");
|
|
65
|
+
}
|
package/src/policy.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
|
|
3
3
|
|
|
4
|
+
export type FeishuGroupCommandMentionBypass = "never" | "single_bot" | "always";
|
|
5
|
+
|
|
4
6
|
export type FeishuAllowlistMatch = {
|
|
5
7
|
allowed: boolean;
|
|
6
8
|
matchKey?: string;
|
|
@@ -90,3 +92,14 @@ export function resolveFeishuReplyPolicy(params: {
|
|
|
90
92
|
|
|
91
93
|
return { requireMention };
|
|
92
94
|
}
|
|
95
|
+
|
|
96
|
+
export function resolveFeishuGroupCommandMentionBypass(params: {
|
|
97
|
+
globalConfig?: FeishuConfig;
|
|
98
|
+
groupConfig?: FeishuGroupConfig;
|
|
99
|
+
}): FeishuGroupCommandMentionBypass {
|
|
100
|
+
return (
|
|
101
|
+
params.groupConfig?.groupCommandMentionBypass ??
|
|
102
|
+
params.globalConfig?.groupCommandMentionBypass ??
|
|
103
|
+
"single_bot"
|
|
104
|
+
);
|
|
105
|
+
}
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
10
10
|
import { createFeishuClient } from "./client.js";
|
|
11
11
|
import { buildMentionedCardContent, type MentionTarget } from "./mention.js";
|
|
12
|
+
import { normalizeFeishuMarkdownLinks } from "./text/markdown-links.js";
|
|
12
13
|
import { getFeishuRuntime } from "./runtime.js";
|
|
13
14
|
import { sendMarkdownCardFeishu, sendMessageFeishu } from "./send.js";
|
|
14
15
|
import { FeishuStreamingSession } from "./streaming-card.js";
|
|
@@ -102,7 +103,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
102
103
|
params.runtime.log?.(`feishu[${account.accountId}] ${message}`),
|
|
103
104
|
);
|
|
104
105
|
try {
|
|
105
|
-
await streaming.start(chatId, resolveReceiveIdType(chatId));
|
|
106
|
+
await streaming.start(chatId, resolveReceiveIdType(chatId), replyToMessageId);
|
|
106
107
|
} catch (error) {
|
|
107
108
|
params.runtime.error?.(`feishu: streaming start failed: ${String(error)}`);
|
|
108
109
|
streaming = null;
|
|
@@ -120,7 +121,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
120
121
|
if (mentionTargets?.length) {
|
|
121
122
|
text = buildMentionedCardContent(mentionTargets, text);
|
|
122
123
|
}
|
|
123
|
-
await streaming.close(text);
|
|
124
|
+
await streaming.close(normalizeFeishuMarkdownLinks(text));
|
|
124
125
|
}
|
|
125
126
|
streaming = null;
|
|
126
127
|
streamingStartPromise = null;
|
|
@@ -217,11 +218,12 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
217
218
|
onModelSelected: prefixContext.onModelSelected,
|
|
218
219
|
onPartialReply: streamingEnabled
|
|
219
220
|
? (payload: ReplyPayload) => {
|
|
220
|
-
|
|
221
|
+
const partialText = normalizeFeishuMarkdownLinks(payload.text ?? "");
|
|
222
|
+
if (!partialText || partialText === lastPartial) {
|
|
221
223
|
return;
|
|
222
224
|
}
|
|
223
|
-
lastPartial =
|
|
224
|
-
streamText =
|
|
225
|
+
lastPartial = partialText;
|
|
226
|
+
streamText = partialText;
|
|
225
227
|
partialUpdateQueue = partialUpdateQueue.then(async () => {
|
|
226
228
|
if (streamingStartPromise) {
|
|
227
229
|
await streamingStartPromise;
|
package/src/send.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { FeishuSendResult, ResolvedFeishuAccount } from "./types.js";
|
|
|
3
3
|
import type { MentionTarget } from "./mention.js";
|
|
4
4
|
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
|
5
5
|
import { createFeishuClient } from "./client.js";
|
|
6
|
+
import { normalizeFeishuMarkdownLinks } from "./text/markdown-links.js";
|
|
6
7
|
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
|
7
8
|
import { getFeishuRuntime } from "./runtime.js";
|
|
8
9
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
@@ -71,6 +72,31 @@ export async function getMessageFeishu(params: {
|
|
|
71
72
|
const parsed = JSON.parse(content);
|
|
72
73
|
if (item.msg_type === "text" && parsed.text) {
|
|
73
74
|
content = parsed.text;
|
|
75
|
+
} else if (parsed.content || parsed.elements) {
|
|
76
|
+
// Extract plain text from rich text (post) or interactive (card) format.
|
|
77
|
+
// Both use nested arrays: Array<Array<{tag, text?, href?, ...}>>
|
|
78
|
+
const blocks = parsed.content ?? parsed.elements ?? [];
|
|
79
|
+
const lines: string[] = [];
|
|
80
|
+
for (const paragraph of blocks) {
|
|
81
|
+
if (!Array.isArray(paragraph)) continue;
|
|
82
|
+
const line = paragraph
|
|
83
|
+
.map((node: { tag?: string; text?: string; href?: string }) => {
|
|
84
|
+
if (node.tag === "text") return node.text ?? "";
|
|
85
|
+
if (node.tag === "a") return node.text ?? node.href ?? "";
|
|
86
|
+
if (node.tag === "at") return "";
|
|
87
|
+
if (node.tag === "img") return "[图片]";
|
|
88
|
+
return node.text ?? "";
|
|
89
|
+
})
|
|
90
|
+
.join("");
|
|
91
|
+
if (line.trim()) lines.push(line);
|
|
92
|
+
}
|
|
93
|
+
const extracted = (parsed.title ? parsed.title + "\n" : "") + lines.join("\n");
|
|
94
|
+
// Filter out Feishu's degraded card placeholder text
|
|
95
|
+
if (extracted.trim() && !extracted.includes("请升级至最新版本客户端")) {
|
|
96
|
+
content = extracted;
|
|
97
|
+
} else if (extracted.includes("请升级至最新版本客户端")) {
|
|
98
|
+
content = "[卡片消息]";
|
|
99
|
+
}
|
|
74
100
|
}
|
|
75
101
|
} catch {
|
|
76
102
|
// Keep raw content if parsing fails
|
|
@@ -147,7 +173,9 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
|
|
|
147
173
|
if (mentions && mentions.length > 0) {
|
|
148
174
|
rawText = buildMentionedMessage(mentions, rawText);
|
|
149
175
|
}
|
|
150
|
-
const messageText =
|
|
176
|
+
const messageText = normalizeFeishuMarkdownLinks(
|
|
177
|
+
getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode),
|
|
178
|
+
);
|
|
151
179
|
|
|
152
180
|
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
|
153
181
|
|
|
@@ -317,6 +345,7 @@ export async function sendMarkdownCardFeishu(params: {
|
|
|
317
345
|
if (mentions && mentions.length > 0) {
|
|
318
346
|
cardText = buildMentionedCardContent(mentions, text);
|
|
319
347
|
}
|
|
348
|
+
cardText = normalizeFeishuMarkdownLinks(cardText);
|
|
320
349
|
const card = buildMarkdownCard(cardText);
|
|
321
350
|
return sendCardFeishu({ cfg, to, card, replyToMessageId, accountId });
|
|
322
351
|
}
|
|
@@ -342,7 +371,9 @@ export async function editMessageFeishu(params: {
|
|
|
342
371
|
cfg,
|
|
343
372
|
channel: "feishu",
|
|
344
373
|
});
|
|
345
|
-
const messageText =
|
|
374
|
+
const messageText = normalizeFeishuMarkdownLinks(
|
|
375
|
+
getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode),
|
|
376
|
+
);
|
|
346
377
|
|
|
347
378
|
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
|
348
379
|
|
package/src/streaming-card.ts
CHANGED
|
@@ -72,6 +72,7 @@ export class FeishuStreamingSession {
|
|
|
72
72
|
async start(
|
|
73
73
|
receiveId: string,
|
|
74
74
|
receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id",
|
|
75
|
+
replyToMessageId?: string,
|
|
75
76
|
): Promise<void> {
|
|
76
77
|
if (this.state) {
|
|
77
78
|
return;
|
|
@@ -108,14 +109,16 @@ export class FeishuStreamingSession {
|
|
|
108
109
|
}
|
|
109
110
|
const cardId = createData.data.card_id;
|
|
110
111
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
const cardContent = JSON.stringify({ type: "card", data: { card_id: cardId } });
|
|
113
|
+
const sendRes = replyToMessageId
|
|
114
|
+
? await this.client.im.message.reply({
|
|
115
|
+
path: { message_id: replyToMessageId },
|
|
116
|
+
data: { msg_type: "interactive", content: cardContent },
|
|
117
|
+
})
|
|
118
|
+
: await this.client.im.message.create({
|
|
119
|
+
params: { receive_id_type: receiveIdType },
|
|
120
|
+
data: { receive_id: receiveId, msg_type: "interactive", content: cardContent },
|
|
121
|
+
});
|
|
119
122
|
if (sendRes.code !== 0 || !sendRes.data?.message_id) {
|
|
120
123
|
throw new Error(`Send card failed: ${sendRes.msg}`);
|
|
121
124
|
}
|