@m1heng-clawd/feishu 0.1.11 → 0.1.12

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.
Files changed (46) hide show
  1. package/README.md +123 -4
  2. package/index.ts +6 -6
  3. package/package.json +13 -3
  4. package/skills/feishu-doc/SKILL.md +40 -2
  5. package/skills/feishu-task/SKILL.md +210 -0
  6. package/src/bitable-tools/index.ts +1 -0
  7. package/src/bot.ts +181 -70
  8. package/src/channel.ts +2 -0
  9. package/src/config-schema.ts +4 -0
  10. package/src/doc-tools/actions.ts +341 -0
  11. package/src/doc-tools/common.ts +33 -0
  12. package/src/doc-tools/index.ts +2 -0
  13. package/src/doc-tools/register.ts +90 -0
  14. package/src/{doc-schema.ts → doc-tools/schemas.ts} +31 -1
  15. package/src/drive-tools/actions.ts +182 -0
  16. package/src/drive-tools/common.ts +18 -0
  17. package/src/drive-tools/index.ts +2 -0
  18. package/src/drive-tools/register.ts +71 -0
  19. package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
  20. package/src/media.ts +5 -3
  21. package/src/perm-tools/actions.ts +111 -0
  22. package/src/perm-tools/common.ts +18 -0
  23. package/src/perm-tools/index.ts +2 -0
  24. package/src/perm-tools/register.ts +65 -0
  25. package/src/policy.ts +13 -0
  26. package/src/reply-dispatcher.ts +6 -4
  27. package/src/send.ts +33 -2
  28. package/src/task-tools/actions.ts +466 -13
  29. package/src/task-tools/constants.ts +13 -0
  30. package/src/task-tools/index.ts +1 -0
  31. package/src/task-tools/register.ts +175 -13
  32. package/src/task-tools/schemas.ts +433 -4
  33. package/src/text/markdown-links.ts +104 -0
  34. package/src/types.ts +1 -0
  35. package/src/wiki-tools/actions.ts +166 -0
  36. package/src/wiki-tools/common.ts +18 -0
  37. package/src/wiki-tools/index.ts +2 -0
  38. package/src/wiki-tools/register.ts +66 -0
  39. package/src/bitable.ts +0 -1
  40. package/src/docx.ts +0 -276
  41. package/src/drive.ts +0 -237
  42. package/src/perm.ts +0 -165
  43. package/src/task.ts +0 -1
  44. package/src/wiki.ts +0 -223
  45. /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
  46. /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,2 @@
1
+ export { registerFeishuDriveTools } from "./register.js";
2
+ export { FeishuDriveSchema, type FeishuDriveParams } from "./schemas.js";
@@ -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: "Markdown content to import. Supports full Markdown syntax including tables, lists, code blocks, etc.",
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
- msgType?: "file" | "media";
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> {
@@ -495,12 +496,13 @@ export async function sendMediaFeishu(params: {
495
496
  fileType,
496
497
  accountId,
497
498
  });
498
- const isMedia = fileType === "mp4" || fileType === "opus";
499
+ // Feishu requires msg_type "audio" for audio, "media" for video, "file" for documents
500
+ const msgType = fileType === "opus" ? "audio" : fileType === "mp4" ? "media" : "file";
499
501
  return sendFileFeishu({
500
502
  cfg,
501
503
  to,
502
504
  fileKey,
503
- msgType: isMedia ? "media" : "file",
505
+ msgType,
504
506
  replyToMessageId,
505
507
  accountId,
506
508
  });
@@ -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,2 @@
1
+ export { registerFeishuPermTools } from "./register.js";
2
+ export { FeishuPermSchema, type FeishuPermParams } from "./schemas.js";
@@ -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
+ }
@@ -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";
@@ -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
- if (!payload.text || payload.text === lastPartial) {
221
+ const partialText = normalizeFeishuMarkdownLinks(payload.text ?? "");
222
+ if (!partialText || partialText === lastPartial) {
221
223
  return;
222
224
  }
223
- lastPartial = payload.text;
224
- streamText = payload.text;
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 = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
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 = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
374
+ const messageText = normalizeFeishuMarkdownLinks(
375
+ getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode),
376
+ );
346
377
 
347
378
  const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
348
379