@m1heng-clawd/feishu 0.1.8 → 0.1.10
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 +42 -28
- package/index.ts +3 -1
- package/package.json +5 -5
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bitable.ts +1 -441
- package/src/bot.ts +130 -68
- package/src/channel.ts +6 -8
- package/src/config-schema.ts +7 -0
- package/src/dedup.ts +31 -0
- package/src/docx.ts +440 -62
- package/src/drive-schema.ts +20 -0
- package/src/drive.ts +84 -27
- package/src/dynamic-agent.ts +8 -4
- package/src/media.ts +25 -40
- package/src/monitor.ts +29 -2
- package/src/onboarding.ts +186 -105
- package/src/perm.ts +23 -24
- package/src/probe.ts +108 -4
- package/src/reply-dispatcher.ts +137 -73
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +1 -1
- package/src/task-tools/actions.ts +137 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/register.ts +101 -0
- package/src/task-tools/schemas.ts +138 -0
- package/src/task.ts +1 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/wiki.ts +42 -43
package/src/drive.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { createFeishuClient } from "./client.js";
|
|
3
|
-
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
2
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
3
|
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
|
6
|
-
import {
|
|
4
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "./tools-common/tool-exec.js";
|
|
5
|
+
import { writeDoc } from "./docx.js";
|
|
7
6
|
|
|
8
7
|
// ============ Helpers ============
|
|
9
8
|
|
|
@@ -147,6 +146,51 @@ async function deleteFile(client: Lark.Client, fileToken: string, type: string)
|
|
|
147
146
|
};
|
|
148
147
|
}
|
|
149
148
|
|
|
149
|
+
// ============ Import Document Functions ============
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Import markdown content as a new Feishu document
|
|
153
|
+
* Uses create + write approach for reliable content import.
|
|
154
|
+
* Note: docType parameter is accepted for API compatibility but docx is always used.
|
|
155
|
+
*/
|
|
156
|
+
async function importDocument(
|
|
157
|
+
client: Lark.Client,
|
|
158
|
+
title: string,
|
|
159
|
+
content: string,
|
|
160
|
+
mediaMaxBytes: number,
|
|
161
|
+
folderToken?: string,
|
|
162
|
+
_docType?: "docx" | "doc",
|
|
163
|
+
) {
|
|
164
|
+
// Step 1: Create empty document
|
|
165
|
+
const createRes = await client.docx.document.create({
|
|
166
|
+
data: { title, folder_token: folderToken },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (createRes.code !== 0) {
|
|
170
|
+
throw new Error(`Failed to create document: ${createRes.msg}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const docId = createRes.data?.document?.document_id;
|
|
174
|
+
if (!docId) {
|
|
175
|
+
throw new Error("Document created but no document_id returned");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Step 2: Write markdown content to the document
|
|
179
|
+
// This ensures proper structure preservation using the writeDoc function
|
|
180
|
+
const writeResult = await writeDoc(client, docId, content, mediaMaxBytes);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
document_id: docId,
|
|
185
|
+
title: title,
|
|
186
|
+
url: `https://feishu.cn/docx/${docId}`,
|
|
187
|
+
import_method: "create_and_write",
|
|
188
|
+
blocks_added: writeResult.blocks_added,
|
|
189
|
+
images_processed: writeResult.images_processed,
|
|
190
|
+
...("warning" in writeResult && { warning: writeResult.warning }),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
150
194
|
// ============ Tool Registration ============
|
|
151
195
|
|
|
152
196
|
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
@@ -155,46 +199,59 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
|
155
199
|
return;
|
|
156
200
|
}
|
|
157
201
|
|
|
158
|
-
|
|
159
|
-
if (accounts.length === 0) {
|
|
202
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
160
203
|
api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
|
|
161
204
|
return;
|
|
162
205
|
}
|
|
163
206
|
|
|
164
|
-
|
|
165
|
-
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
|
166
|
-
if (!toolsCfg.drive) {
|
|
207
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "drive")) {
|
|
167
208
|
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
|
168
209
|
return;
|
|
169
210
|
}
|
|
170
211
|
|
|
171
|
-
const getClient = () => createFeishuClient(firstAccount);
|
|
172
|
-
|
|
173
212
|
api.registerTool(
|
|
174
213
|
{
|
|
175
214
|
name: "feishu_drive",
|
|
176
215
|
label: "Feishu Drive",
|
|
177
216
|
description:
|
|
178
|
-
"Feishu cloud storage operations. Actions: list, info, create_folder, move, delete",
|
|
217
|
+
"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.",
|
|
179
218
|
parameters: FeishuDriveSchema,
|
|
180
219
|
async execute(_toolCallId, params) {
|
|
181
220
|
const p = params as FeishuDriveParams;
|
|
182
221
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
222
|
+
return await withFeishuToolClient({
|
|
223
|
+
api,
|
|
224
|
+
toolName: "feishu_drive",
|
|
225
|
+
requiredTool: "drive",
|
|
226
|
+
run: async ({ client, account }) => {
|
|
227
|
+
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
228
|
+
switch (p.action) {
|
|
229
|
+
case "list":
|
|
230
|
+
return json(await listFolder(client, p.folder_token));
|
|
231
|
+
case "info":
|
|
232
|
+
return json(await getFileInfo(client, p.file_token));
|
|
233
|
+
case "create_folder":
|
|
234
|
+
return json(await createFolder(client, p.name, p.folder_token));
|
|
235
|
+
case "move":
|
|
236
|
+
return json(await moveFile(client, p.file_token, p.type, p.folder_token));
|
|
237
|
+
case "delete":
|
|
238
|
+
return json(await deleteFile(client, p.file_token, p.type));
|
|
239
|
+
case "import_document":
|
|
240
|
+
return json(
|
|
241
|
+
await importDocument(
|
|
242
|
+
client,
|
|
243
|
+
p.title,
|
|
244
|
+
p.content,
|
|
245
|
+
mediaMaxBytes,
|
|
246
|
+
p.folder_token,
|
|
247
|
+
(p as any).doc_type || "docx",
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
default:
|
|
251
|
+
return json({ error: `Unknown action: ${(p as any).action}` });
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
});
|
|
198
255
|
} catch (err) {
|
|
199
256
|
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
200
257
|
}
|
|
@@ -203,5 +260,5 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
|
203
260
|
{ name: "feishu_drive" },
|
|
204
261
|
);
|
|
205
262
|
|
|
206
|
-
api.logger.
|
|
263
|
+
api.logger.debug?.("feishu_drive: Registered feishu_drive tool");
|
|
207
264
|
}
|
package/src/dynamic-agent.ts
CHANGED
|
@@ -19,16 +19,18 @@ export async function maybeCreateDynamicAgent(params: {
|
|
|
19
19
|
runtime: PluginRuntime;
|
|
20
20
|
senderOpenId: string;
|
|
21
21
|
dynamicCfg: DynamicAgentCreationConfig;
|
|
22
|
+
accountId?: string;
|
|
22
23
|
log: (msg: string) => void;
|
|
23
24
|
}): Promise<MaybeCreateDynamicAgentResult> {
|
|
24
|
-
const { cfg, runtime, senderOpenId, dynamicCfg, log } = params;
|
|
25
|
+
const { cfg, runtime, senderOpenId, dynamicCfg, accountId, log } = params;
|
|
25
26
|
|
|
26
27
|
// Check if there's already a binding for this user
|
|
27
28
|
const existingBindings = cfg.bindings ?? [];
|
|
28
29
|
const hasBinding = existingBindings.some(
|
|
29
30
|
(b) =>
|
|
30
31
|
b.match?.channel === "feishu" &&
|
|
31
|
-
b.match?.
|
|
32
|
+
(!accountId || b.match?.accountId === accountId) &&
|
|
33
|
+
b.match?.peer?.kind === "direct" &&
|
|
32
34
|
b.match?.peer?.id === senderOpenId,
|
|
33
35
|
);
|
|
34
36
|
|
|
@@ -66,7 +68,8 @@ export async function maybeCreateDynamicAgent(params: {
|
|
|
66
68
|
agentId,
|
|
67
69
|
match: {
|
|
68
70
|
channel: "feishu",
|
|
69
|
-
|
|
71
|
+
...(accountId ? { accountId } : {}),
|
|
72
|
+
peer: { kind: "direct", id: senderOpenId },
|
|
70
73
|
},
|
|
71
74
|
},
|
|
72
75
|
],
|
|
@@ -108,7 +111,8 @@ export async function maybeCreateDynamicAgent(params: {
|
|
|
108
111
|
agentId,
|
|
109
112
|
match: {
|
|
110
113
|
channel: "feishu",
|
|
111
|
-
|
|
114
|
+
...(accountId ? { accountId } : {}),
|
|
115
|
+
peer: { kind: "direct", id: senderOpenId },
|
|
112
116
|
},
|
|
113
117
|
},
|
|
114
118
|
],
|
package/src/media.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { createFeishuClient } from "./client.js";
|
|
3
3
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
4
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
4
5
|
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
|
5
6
|
import fs from "fs";
|
|
6
7
|
import path from "path";
|
|
@@ -353,10 +354,12 @@ export async function sendFileFeishu(params: {
|
|
|
353
354
|
cfg: ClawdbotConfig;
|
|
354
355
|
to: string;
|
|
355
356
|
fileKey: string;
|
|
357
|
+
msgType?: "file" | "media";
|
|
356
358
|
replyToMessageId?: string;
|
|
357
359
|
accountId?: string;
|
|
358
360
|
}): Promise<SendMediaResult> {
|
|
359
361
|
const { cfg, to, fileKey, replyToMessageId, accountId } = params;
|
|
362
|
+
const msgType = params.msgType ?? "file";
|
|
360
363
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
361
364
|
if (!account.configured) {
|
|
362
365
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
@@ -376,7 +379,7 @@ export async function sendFileFeishu(params: {
|
|
|
376
379
|
path: { message_id: replyToMessageId },
|
|
377
380
|
data: {
|
|
378
381
|
content,
|
|
379
|
-
msg_type:
|
|
382
|
+
msg_type: msgType,
|
|
380
383
|
},
|
|
381
384
|
});
|
|
382
385
|
|
|
@@ -395,7 +398,7 @@ export async function sendFileFeishu(params: {
|
|
|
395
398
|
data: {
|
|
396
399
|
receive_id: receiveId,
|
|
397
400
|
content,
|
|
398
|
-
msg_type:
|
|
401
|
+
msg_type: msgType,
|
|
399
402
|
},
|
|
400
403
|
});
|
|
401
404
|
|
|
@@ -440,23 +443,6 @@ export function detectFileType(
|
|
|
440
443
|
}
|
|
441
444
|
}
|
|
442
445
|
|
|
443
|
-
/**
|
|
444
|
-
* Check if a string is a local file path (not a URL)
|
|
445
|
-
*/
|
|
446
|
-
function isLocalPath(urlOrPath: string): boolean {
|
|
447
|
-
// Starts with / or ~ or drive letter (Windows)
|
|
448
|
-
if (urlOrPath.startsWith("/") || urlOrPath.startsWith("~") || /^[a-zA-Z]:/.test(urlOrPath)) {
|
|
449
|
-
return true;
|
|
450
|
-
}
|
|
451
|
-
// Try to parse as URL - if it fails or has no protocol, it's likely a local path
|
|
452
|
-
try {
|
|
453
|
-
const url = new URL(urlOrPath);
|
|
454
|
-
return url.protocol === "file:";
|
|
455
|
-
} catch {
|
|
456
|
-
return true; // Not a valid URL, treat as local path
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
446
|
/**
|
|
461
447
|
* Upload and send media (image or file) from URL, local path, or buffer
|
|
462
448
|
*/
|
|
@@ -470,6 +456,11 @@ export async function sendMediaFeishu(params: {
|
|
|
470
456
|
accountId?: string;
|
|
471
457
|
}): Promise<SendMediaResult> {
|
|
472
458
|
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
|
|
459
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
460
|
+
if (!account.configured) {
|
|
461
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
462
|
+
}
|
|
463
|
+
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
473
464
|
|
|
474
465
|
let buffer: Buffer;
|
|
475
466
|
let name: string;
|
|
@@ -478,26 +469,12 @@ export async function sendMediaFeishu(params: {
|
|
|
478
469
|
buffer = mediaBuffer;
|
|
479
470
|
name = fileName ?? "file";
|
|
480
471
|
} else if (mediaUrl) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (!fs.existsSync(filePath)) {
|
|
488
|
-
throw new Error(`Local file not found: ${filePath}`);
|
|
489
|
-
}
|
|
490
|
-
buffer = fs.readFileSync(filePath);
|
|
491
|
-
name = fileName ?? path.basename(filePath);
|
|
492
|
-
} else {
|
|
493
|
-
// Remote URL - fetch
|
|
494
|
-
const response = await fetch(mediaUrl);
|
|
495
|
-
if (!response.ok) {
|
|
496
|
-
throw new Error(`Failed to fetch media from URL: ${response.status}`);
|
|
497
|
-
}
|
|
498
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
499
|
-
name = fileName ?? (path.basename(new URL(mediaUrl).pathname) || "file");
|
|
500
|
-
}
|
|
472
|
+
const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
|
|
473
|
+
maxBytes: mediaMaxBytes,
|
|
474
|
+
optimizeImages: false,
|
|
475
|
+
});
|
|
476
|
+
buffer = loaded.buffer;
|
|
477
|
+
name = fileName ?? loaded.fileName ?? "file";
|
|
501
478
|
} else {
|
|
502
479
|
throw new Error("Either mediaUrl or mediaBuffer must be provided");
|
|
503
480
|
}
|
|
@@ -518,6 +495,14 @@ export async function sendMediaFeishu(params: {
|
|
|
518
495
|
fileType,
|
|
519
496
|
accountId,
|
|
520
497
|
});
|
|
521
|
-
|
|
498
|
+
const isMedia = fileType === "mp4" || fileType === "opus";
|
|
499
|
+
return sendFileFeishu({
|
|
500
|
+
cfg,
|
|
501
|
+
to,
|
|
502
|
+
fileKey,
|
|
503
|
+
msgType: isMedia ? "media" : "file",
|
|
504
|
+
replyToMessageId,
|
|
505
|
+
accountId,
|
|
506
|
+
});
|
|
522
507
|
}
|
|
523
508
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
2
|
import * as http from "http";
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
type ClawdbotConfig,
|
|
5
|
+
type RuntimeEnv,
|
|
6
|
+
type HistoryEntry,
|
|
7
|
+
installRequestBodyLimitGuard,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
4
9
|
import type { ResolvedFeishuAccount } from "./types.js";
|
|
5
10
|
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
|
6
11
|
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
|
@@ -18,6 +23,8 @@ export type MonitorFeishuOpts = {
|
|
|
18
23
|
const wsClients = new Map<string, Lark.WSClient>();
|
|
19
24
|
const httpServers = new Map<string, http.Server>();
|
|
20
25
|
const botOpenIds = new Map<string, string>();
|
|
26
|
+
const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
27
|
+
const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
|
21
28
|
|
|
22
29
|
async function fetchBotOpenId(
|
|
23
30
|
account: ResolvedFeishuAccount,
|
|
@@ -191,7 +198,27 @@ async function monitorWebhook({ params, accountId, eventDispatcher }: Connection
|
|
|
191
198
|
log(`feishu[${accountId}]: starting Webhook server on port ${port}, path ${path}...`);
|
|
192
199
|
|
|
193
200
|
const server = http.createServer();
|
|
194
|
-
|
|
201
|
+
const webhookHandler = Lark.adaptDefault(path, eventDispatcher, { autoChallenge: true });
|
|
202
|
+
server.on("request", (req, res) => {
|
|
203
|
+
const guard = installRequestBodyLimitGuard(req, res, {
|
|
204
|
+
maxBytes: FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
|
205
|
+
timeoutMs: FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
|
|
206
|
+
responseFormat: "text",
|
|
207
|
+
});
|
|
208
|
+
if (guard.isTripped()) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void Promise.resolve(webhookHandler(req, res))
|
|
213
|
+
.catch((err) => {
|
|
214
|
+
if (!guard.isTripped()) {
|
|
215
|
+
error(`feishu[${accountId}]: webhook handler error: ${String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.finally(() => {
|
|
219
|
+
guard.dispose();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
195
222
|
httpServers.set(accountId, server);
|
|
196
223
|
|
|
197
224
|
return new Promise((resolve, reject) => {
|