@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.
- package/README.md +280 -7
- package/index.ts +6 -6
- package/package.json +13 -3
- package/skills/feishu-doc/SKILL.md +64 -2
- package/skills/feishu-task/SKILL.md +210 -0
- package/src/bitable-tools/index.ts +1 -0
- package/src/bot.ts +182 -71
- package/src/channel.ts +2 -0
- package/src/config-schema.ts +4 -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} +39 -1
- package/src/{docx.ts → doc-write-service.ts} +204 -351
- 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 +5 -3
- package/src/onboarding.ts +14 -4
- 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 +6 -4
- package/src/send.ts +33 -2
- package/src/task-tools/actions.ts +466 -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 +433 -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/drive.ts +0 -264
- 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,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,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> {
|
|
@@ -495,12 +496,13 @@ export async function sendMediaFeishu(params: {
|
|
|
495
496
|
fileType,
|
|
496
497
|
accountId,
|
|
497
498
|
});
|
|
498
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
197
|
+
const accountId = resolveDmPolicyAccountId(cfg);
|
|
193
198
|
return resolveFeishuAccount({ cfg, accountId }).config.dmPolicy ?? "pairing";
|
|
194
199
|
},
|
|
195
|
-
setPolicy: (cfg, policy
|
|
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,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";
|
|
@@ -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;
|