@m1heng-clawd/feishu 0.1.10 → 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 (47) hide show
  1. package/README.md +280 -7
  2. package/index.ts +6 -6
  3. package/package.json +13 -3
  4. package/skills/feishu-doc/SKILL.md +64 -2
  5. package/skills/feishu-task/SKILL.md +210 -0
  6. package/src/bitable-tools/index.ts +1 -0
  7. package/src/bot.ts +182 -71
  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} +39 -1
  15. package/src/{docx.ts → doc-write-service.ts} +204 -351
  16. package/src/drive-tools/actions.ts +182 -0
  17. package/src/drive-tools/common.ts +18 -0
  18. package/src/drive-tools/index.ts +2 -0
  19. package/src/drive-tools/register.ts +71 -0
  20. package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
  21. package/src/media.ts +5 -3
  22. package/src/onboarding.ts +14 -4
  23. package/src/perm-tools/actions.ts +111 -0
  24. package/src/perm-tools/common.ts +18 -0
  25. package/src/perm-tools/index.ts +2 -0
  26. package/src/perm-tools/register.ts +65 -0
  27. package/src/policy.ts +13 -0
  28. package/src/reply-dispatcher.ts +6 -4
  29. package/src/send.ts +33 -2
  30. package/src/task-tools/actions.ts +466 -13
  31. package/src/task-tools/constants.ts +13 -0
  32. package/src/task-tools/index.ts +1 -0
  33. package/src/task-tools/register.ts +175 -13
  34. package/src/task-tools/schemas.ts +433 -4
  35. package/src/text/markdown-links.ts +104 -0
  36. package/src/types.ts +1 -0
  37. package/src/wiki-tools/actions.ts +166 -0
  38. package/src/wiki-tools/common.ts +18 -0
  39. package/src/wiki-tools/index.ts +2 -0
  40. package/src/wiki-tools/register.ts +66 -0
  41. package/src/bitable.ts +0 -1
  42. package/src/drive.ts +0 -264
  43. package/src/perm.ts +0 -165
  44. package/src/task.ts +0 -1
  45. package/src/wiki.ts +0 -223
  46. /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
  47. /package/src/{wiki-schema.ts → wiki-tools/schemas.ts} +0 -0
@@ -0,0 +1,182 @@
1
+ import { createAndWriteDoc } from "../doc-write-service.js";
2
+ import { runDriveApiCall, type DriveClient } from "./common.js";
3
+ import type { FeishuDriveParams } from "./schemas.js";
4
+
5
+ type DriveMoveType = "doc" | "docx" | "sheet" | "bitable" | "folder" | "file" | "mindnote" | "slides";
6
+ type DriveDeleteType = DriveMoveType | "shortcut";
7
+
8
+ async function getRootFolderToken(client: DriveClient): Promise<string> {
9
+ // Use generic HTTP client to call the root folder meta API
10
+ // as it's not directly exposed in the SDK.
11
+ const domain = (client as any).domain ?? "https://open.feishu.cn";
12
+ const res = await runDriveApiCall("drive.explorer.v2.root_folder.meta", () =>
13
+ (client as any).httpInstance.get(`${domain}/open-apis/drive/explorer/v2/root_folder/meta`) as Promise<{
14
+ code?: number;
15
+ msg?: string;
16
+ data?: { token?: string };
17
+ }>,
18
+ );
19
+ const token = res.data?.token;
20
+ if (!token) throw new Error("Root folder token not found");
21
+ return token;
22
+ }
23
+
24
+ async function listFolder(client: DriveClient, folderToken?: string) {
25
+ // Filter out invalid folder_token values (empty, "0", etc.)
26
+ const validFolderToken = folderToken && folderToken !== "0" ? folderToken : undefined;
27
+ const res = await runDriveApiCall("drive.file.list", () =>
28
+ client.drive.file.list({
29
+ params: validFolderToken ? { folder_token: validFolderToken } : {},
30
+ }),
31
+ );
32
+
33
+ return {
34
+ files:
35
+ res.data?.files?.map((f) => ({
36
+ token: f.token,
37
+ name: f.name,
38
+ type: f.type,
39
+ url: f.url,
40
+ created_time: f.created_time,
41
+ modified_time: f.modified_time,
42
+ owner_id: f.owner_id,
43
+ })) ?? [],
44
+ next_page_token: res.data?.next_page_token,
45
+ };
46
+ }
47
+
48
+ async function getFileInfo(client: DriveClient, fileToken: string, folderToken?: string) {
49
+ // Use list with folder_token to find file info.
50
+ const res = await runDriveApiCall("drive.file.list", () =>
51
+ client.drive.file.list({
52
+ params: folderToken ? { folder_token: folderToken } : {},
53
+ }),
54
+ );
55
+
56
+ const file = res.data?.files?.find((f) => f.token === fileToken);
57
+ if (!file) {
58
+ throw new Error(`File not found: ${fileToken}`);
59
+ }
60
+
61
+ return {
62
+ token: file.token,
63
+ name: file.name,
64
+ type: file.type,
65
+ url: file.url,
66
+ created_time: file.created_time,
67
+ modified_time: file.modified_time,
68
+ owner_id: file.owner_id,
69
+ };
70
+ }
71
+
72
+ async function createFolder(client: DriveClient, name: string, folderToken?: string) {
73
+ // Feishu supports using folder_token="0" as the root folder.
74
+ // We try to resolve the real root token (explorer API), but fall back to "0"
75
+ // because some tenants/apps return 400 for that explorer endpoint.
76
+ let effectiveToken = folderToken && folderToken !== "0" ? folderToken : "0";
77
+ if (effectiveToken === "0") {
78
+ try {
79
+ effectiveToken = await getRootFolderToken(client);
80
+ } catch {
81
+ // ignore and keep "0"
82
+ }
83
+ }
84
+
85
+ const res = await runDriveApiCall("drive.file.createFolder", () =>
86
+ client.drive.file.createFolder({
87
+ data: {
88
+ name,
89
+ folder_token: effectiveToken,
90
+ },
91
+ }),
92
+ );
93
+
94
+ return {
95
+ token: res.data?.token,
96
+ url: res.data?.url,
97
+ };
98
+ }
99
+
100
+ async function moveFile(
101
+ client: DriveClient,
102
+ fileToken: string,
103
+ type: string,
104
+ folderToken: string,
105
+ ) {
106
+ const res = await runDriveApiCall("drive.file.move", () =>
107
+ client.drive.file.move({
108
+ path: { file_token: fileToken },
109
+ data: {
110
+ type: type as DriveMoveType,
111
+ folder_token: folderToken,
112
+ },
113
+ }),
114
+ );
115
+
116
+ return {
117
+ success: true,
118
+ task_id: res.data?.task_id,
119
+ };
120
+ }
121
+
122
+ async function deleteFile(client: DriveClient, fileToken: string, type: string) {
123
+ const res = await runDriveApiCall("drive.file.delete", () =>
124
+ client.drive.file.delete({
125
+ path: { file_token: fileToken },
126
+ params: {
127
+ type: type as DriveDeleteType,
128
+ },
129
+ }),
130
+ );
131
+
132
+ return {
133
+ success: true,
134
+ task_id: res.data?.task_id,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Import markdown content as a new Feishu document.
140
+ * Uses create + write approach for reliable content import.
141
+ * Note: docType parameter is accepted for API compatibility but docx is always used.
142
+ */
143
+ async function importDocument(
144
+ client: DriveClient,
145
+ title: string,
146
+ content: string,
147
+ mediaMaxBytes: number,
148
+ folderToken?: string,
149
+ _docType?: "docx" | "doc",
150
+ ) {
151
+ return createAndWriteDoc(client, title, content, mediaMaxBytes, folderToken);
152
+ }
153
+
154
+ export async function runDriveAction(
155
+ client: DriveClient,
156
+ params: FeishuDriveParams,
157
+ mediaMaxBytes: number,
158
+ ) {
159
+ switch (params.action) {
160
+ case "list":
161
+ return listFolder(client, params.folder_token);
162
+ case "info":
163
+ return getFileInfo(client, params.file_token);
164
+ case "create_folder":
165
+ return createFolder(client, params.name, params.folder_token);
166
+ case "move":
167
+ return moveFile(client, params.file_token, params.type, params.folder_token);
168
+ case "delete":
169
+ return deleteFile(client, params.file_token, params.type);
170
+ case "import_document":
171
+ return importDocument(
172
+ client,
173
+ params.title,
174
+ params.content,
175
+ mediaMaxBytes,
176
+ params.folder_token,
177
+ params.doc_type || "docx",
178
+ );
179
+ default:
180
+ return { error: `Unknown action: ${(params as any).action}` };
181
+ }
182
+ }
@@ -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
  });
package/src/onboarding.ts CHANGED
@@ -23,6 +23,7 @@ import { probeFeishu } from "./probe.js";
23
23
  import type { FeishuConfig } from "./types.js";
24
24
 
25
25
  const channel = "feishu" as const;
26
+ let onboardingDmPolicyAccountId: string | undefined;
26
27
 
27
28
  function resolveOnboardingAccountId(cfg: ClawdbotConfig, accountId?: string | null): string {
28
29
  const raw = accountId?.trim();
@@ -32,6 +33,10 @@ function resolveOnboardingAccountId(cfg: ClawdbotConfig, accountId?: string | nu
32
33
  return resolveDefaultFeishuAccountId(cfg);
33
34
  }
34
35
 
36
+ function resolveDmPolicyAccountId(cfg: ClawdbotConfig, accountId?: string | null): string {
37
+ return resolveOnboardingAccountId(cfg, accountId ?? onboardingDmPolicyAccountId);
38
+ }
39
+
35
40
  function upsertFeishuAccountConfig(
36
41
  cfg: ClawdbotConfig,
37
42
  accountId: string,
@@ -79,7 +84,7 @@ function setFeishuDmPolicy(
79
84
  dmPolicy: DmPolicy,
80
85
  accountId?: string,
81
86
  ): ClawdbotConfig {
82
- const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
87
+ const resolvedAccountId = resolveDmPolicyAccountId(cfg, accountId);
83
88
  const account = resolveFeishuAccount({ cfg, accountId: resolvedAccountId });
84
89
  // Feishu channel config does not support "disabled" as a dmPolicy value.
85
90
  const effectiveDmPolicy = dmPolicy === "disabled" ? "pairing" : dmPolicy;
@@ -110,7 +115,7 @@ async function promptFeishuAllowFrom(params: {
110
115
  prompter: WizardPrompter;
111
116
  accountId?: string;
112
117
  }): Promise<ClawdbotConfig> {
113
- const accountId = resolveOnboardingAccountId(params.cfg, params.accountId);
118
+ const accountId = resolveDmPolicyAccountId(params.cfg, params.accountId);
114
119
  const existing = resolveFeishuAccount({ cfg: params.cfg, accountId }).config.allowFrom ?? [];
115
120
  const accountLabel = accountId === DEFAULT_ACCOUNT_ID ? "default" : accountId;
116
121
  await params.prompter.note(
@@ -189,10 +194,10 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
189
194
  policyKey: "channels.feishu.dmPolicy",
190
195
  allowFromKey: "channels.feishu.allowFrom",
191
196
  getCurrent: (cfg) => {
192
- const accountId = resolveDefaultFeishuAccountId(cfg);
197
+ const accountId = resolveDmPolicyAccountId(cfg);
193
198
  return resolveFeishuAccount({ cfg, accountId }).config.dmPolicy ?? "pairing";
194
199
  },
195
- setPolicy: (cfg, policy, accountId?: string) => setFeishuDmPolicy(cfg, policy, accountId),
200
+ setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy, onboardingDmPolicyAccountId),
196
201
  promptAllowFrom: promptFeishuAllowFrom,
197
202
  };
198
203
 
@@ -260,6 +265,7 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
260
265
  defaultAccountId: defaultFeishuAccountId,
261
266
  });
262
267
  }
268
+ onboardingDmPolicyAccountId = feishuAccountId;
263
269
  const accountLabel = feishuAccountId === DEFAULT_ACCOUNT_ID ? "default" : feishuAccountId;
264
270
  const currentAccount = resolveFeishuAccount({ cfg, accountId: feishuAccountId });
265
271
  const resolved = resolveFeishuCredentials(currentAccount.config);
@@ -429,6 +435,10 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
429
435
 
430
436
  dmPolicy,
431
437
 
438
+ onAccountRecorded: (accountId) => {
439
+ onboardingDmPolicyAccountId = normalizeAccountId(accountId);
440
+ },
441
+
432
442
  disable: (cfg) => ({
433
443
  ...cfg,
434
444
  channels: {
@@ -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;