@m1heng-clawd/feishu 0.1.12 → 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 +60 -10
- package/package.json +1 -1
- package/src/channel.ts +2 -0
- package/src/config-schema.ts +2 -0
- package/src/media.ts +24 -4
- package/src/reply-dispatcher.ts +1 -1
- package/src/streaming-card.ts +11 -8
- package/src/task-tools/actions.ts +4 -2
- package/src/task-tools/schemas.ts +24 -35
package/README.md
CHANGED
|
@@ -27,14 +27,33 @@ openclaw plugins install @m1heng-clawd/feishu
|
|
|
27
27
|
> [!IMPORTANT]
|
|
28
28
|
> **Windows Troubleshooting (`spawn npm ENOENT`)**
|
|
29
29
|
>
|
|
30
|
-
> If `openclaw plugins install` fails, install manually:
|
|
30
|
+
> If `openclaw plugins install` fails, install manually with the latest tarball:
|
|
31
31
|
>
|
|
32
32
|
> ```bash
|
|
33
|
-
> #
|
|
34
|
-
>
|
|
33
|
+
> # Option A (recommended): download latest package tarball
|
|
34
|
+
> npm pack @m1heng-clawd/feishu
|
|
35
|
+
> openclaw plugins install ./m1heng-clawd-feishu-<version>.tgz
|
|
36
|
+
> ```
|
|
37
|
+
>
|
|
38
|
+
> ```bash
|
|
39
|
+
> # Option B (keep curl flow): resolve latest tarball URL, then download/install
|
|
40
|
+
> TARBALL_URL="$(npm view @m1heng-clawd/feishu dist.tarball)"
|
|
41
|
+
> curl -L -o feishu-latest.tgz "$TARBALL_URL"
|
|
42
|
+
> openclaw plugins install ./feishu-latest.tgz
|
|
43
|
+
> ```
|
|
35
44
|
>
|
|
36
|
-
>
|
|
37
|
-
>
|
|
45
|
+
> ```powershell
|
|
46
|
+
> # Windows PowerShell (Option B)
|
|
47
|
+
> $tarball = npm view @m1heng-clawd/feishu dist.tarball
|
|
48
|
+
> curl.exe -L $tarball -o feishu-latest.tgz
|
|
49
|
+
> openclaw plugins install .\feishu-latest.tgz
|
|
50
|
+
> ```
|
|
51
|
+
>
|
|
52
|
+
> ```bash
|
|
53
|
+
> # Option C (no npm command): use URL template with latest version from npm Versions tab
|
|
54
|
+
> # https://www.npmjs.com/package/@m1heng-clawd/feishu?activeTab=versions
|
|
55
|
+
> curl -L -o feishu-latest.tgz https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-<version>.tgz
|
|
56
|
+
> openclaw plugins install ./feishu-latest.tgz
|
|
38
57
|
> ```
|
|
39
58
|
|
|
40
59
|
### Upgrade
|
|
@@ -43,6 +62,12 @@ openclaw plugins install @m1heng-clawd/feishu
|
|
|
43
62
|
openclaw plugins update feishu
|
|
44
63
|
```
|
|
45
64
|
|
|
65
|
+
Check installed version:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
openclaw plugins list | rg -i feishu
|
|
69
|
+
```
|
|
70
|
+
|
|
46
71
|
### Configuration
|
|
47
72
|
|
|
48
73
|
1. Create a self-built app on [Feishu Open Platform](https://open.feishu.cn)
|
|
@@ -478,14 +503,33 @@ openclaw plugins install @m1heng-clawd/feishu
|
|
|
478
503
|
> [!IMPORTANT]
|
|
479
504
|
> **Windows 排错(`spawn npm ENOENT`)**
|
|
480
505
|
>
|
|
481
|
-
> 如果 `openclaw plugins install`
|
|
506
|
+
> 如果 `openclaw plugins install` 失败,可通过最新 tarball 手动安装:
|
|
482
507
|
>
|
|
483
508
|
> ```bash
|
|
484
|
-
> #
|
|
485
|
-
>
|
|
509
|
+
> # 方案 A(推荐):下载最新插件 tarball
|
|
510
|
+
> npm pack @m1heng-clawd/feishu
|
|
511
|
+
> openclaw plugins install ./m1heng-clawd-feishu-<version>.tgz
|
|
512
|
+
> ```
|
|
513
|
+
>
|
|
514
|
+
> ```bash
|
|
515
|
+
> # 方案 B(保留 curl 路径):先解析最新 tarball 地址,再下载安装
|
|
516
|
+
> TARBALL_URL="$(npm view @m1heng-clawd/feishu dist.tarball)"
|
|
517
|
+
> curl -L -o feishu-latest.tgz "$TARBALL_URL"
|
|
518
|
+
> openclaw plugins install ./feishu-latest.tgz
|
|
519
|
+
> ```
|
|
486
520
|
>
|
|
487
|
-
>
|
|
488
|
-
>
|
|
521
|
+
> ```powershell
|
|
522
|
+
> # Windows PowerShell(方案 B)
|
|
523
|
+
> $tarball = npm view @m1heng-clawd/feishu dist.tarball
|
|
524
|
+
> curl.exe -L $tarball -o feishu-latest.tgz
|
|
525
|
+
> openclaw plugins install .\feishu-latest.tgz
|
|
526
|
+
> ```
|
|
527
|
+
>
|
|
528
|
+
> ```bash
|
|
529
|
+
> # 方案 C(无 npm 命令):先在 npm Versions 页查到最新版本号,再套用 URL 模板
|
|
530
|
+
> # https://www.npmjs.com/package/@m1heng-clawd/feishu?activeTab=versions
|
|
531
|
+
> curl -L -o feishu-latest.tgz https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-<version>.tgz
|
|
532
|
+
> openclaw plugins install ./feishu-latest.tgz
|
|
489
533
|
> ```
|
|
490
534
|
|
|
491
535
|
### 升级
|
|
@@ -494,6 +538,12 @@ openclaw plugins install @m1heng-clawd/feishu
|
|
|
494
538
|
openclaw plugins update feishu
|
|
495
539
|
```
|
|
496
540
|
|
|
541
|
+
查看已安装版本:
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
openclaw plugins list | rg -i feishu
|
|
545
|
+
```
|
|
546
|
+
|
|
497
547
|
### 配置
|
|
498
548
|
|
|
499
549
|
1. 在 [飞书开放平台](https://open.feishu.cn) 创建自建应用
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -97,6 +97,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
97
97
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
98
98
|
chunkMode: { type: "string", enum: ["length", "newline"] },
|
|
99
99
|
mediaMaxMb: { type: "number", minimum: 0 },
|
|
100
|
+
mediaLocalRoots: { type: "array", items: { type: "string" } },
|
|
100
101
|
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
|
101
102
|
accounts: {
|
|
102
103
|
type: "object",
|
|
@@ -111,6 +112,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
111
112
|
verificationToken: { type: "string" },
|
|
112
113
|
domain: { type: "string", enum: ["feishu", "lark"] },
|
|
113
114
|
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
115
|
+
mediaLocalRoots: { type: "array", items: { type: "string" } },
|
|
114
116
|
groupCommandMentionBypass: { type: "string", enum: ["never", "single_bot", "always"] },
|
|
115
117
|
},
|
|
116
118
|
},
|
package/src/config-schema.ts
CHANGED
|
@@ -148,6 +148,7 @@ export const FeishuAccountConfigSchema = z
|
|
|
148
148
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
149
149
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
150
150
|
mediaMaxMb: z.number().positive().optional(),
|
|
151
|
+
mediaLocalRoots: z.array(z.string()).optional(),
|
|
151
152
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
152
153
|
renderMode: RenderModeSchema,
|
|
153
154
|
streaming: StreamingModeSchema,
|
|
@@ -185,6 +186,7 @@ export const FeishuConfigSchema = z
|
|
|
185
186
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
186
187
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
187
188
|
mediaMaxMb: z.number().positive().optional(),
|
|
189
|
+
mediaLocalRoots: z.array(z.string()).optional(),
|
|
188
190
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
189
191
|
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
|
190
192
|
streaming: StreamingModeSchema,
|
package/src/media.ts
CHANGED
|
@@ -470,10 +470,30 @@ export async function sendMediaFeishu(params: {
|
|
|
470
470
|
buffer = mediaBuffer;
|
|
471
471
|
name = fileName ?? "file";
|
|
472
472
|
} else if (mediaUrl) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
+
})();
|
|
477
497
|
buffer = loaded.buffer;
|
|
478
498
|
name = fileName ?? loaded.fileName ?? "file";
|
|
479
499
|
} else {
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -103,7 +103,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
103
103
|
params.runtime.log?.(`feishu[${account.accountId}] ${message}`),
|
|
104
104
|
);
|
|
105
105
|
try {
|
|
106
|
-
await streaming.start(chatId, resolveReceiveIdType(chatId));
|
|
106
|
+
await streaming.start(chatId, resolveReceiveIdType(chatId), replyToMessageId);
|
|
107
107
|
} catch (error) {
|
|
108
108
|
params.runtime.error?.(`feishu: streaming start failed: ${String(error)}`);
|
|
109
109
|
streaming = null;
|
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
|
}
|
|
@@ -553,13 +553,15 @@ export async function uploadTaskAttachment(
|
|
|
553
553
|
let tempCleanup: (() => Promise<void>) | undefined;
|
|
554
554
|
let filePath: string;
|
|
555
555
|
|
|
556
|
-
if (
|
|
556
|
+
if (params.file_path) {
|
|
557
557
|
filePath = params.file_path;
|
|
558
558
|
await ensureUploadableLocalFile(filePath, maxBytes);
|
|
559
|
-
} else {
|
|
559
|
+
} else if (params.file_url) {
|
|
560
560
|
const download = await downloadToTempFile(params.file_url, params.filename, maxBytes);
|
|
561
561
|
filePath = download.tempPath;
|
|
562
562
|
tempCleanup = download.cleanup;
|
|
563
|
+
} else {
|
|
564
|
+
throw new Error("Either file_path or file_url is required");
|
|
563
565
|
}
|
|
564
566
|
|
|
565
567
|
try {
|
|
@@ -156,18 +156,13 @@ export type DeleteTaskCommentParams = {
|
|
|
156
156
|
comment_id: TaskCommentDeletePayload["path"]["comment_id"];
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
-
export type UploadTaskAttachmentParams =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
task_guid: string;
|
|
167
|
-
file_url: string;
|
|
168
|
-
filename?: string;
|
|
169
|
-
user_id_type?: NonNullable<TaskAttachmentUploadPayload["params"]>["user_id_type"];
|
|
170
|
-
};
|
|
159
|
+
export type UploadTaskAttachmentParams = {
|
|
160
|
+
task_guid: string;
|
|
161
|
+
file_path?: string;
|
|
162
|
+
file_url?: string;
|
|
163
|
+
filename?: string;
|
|
164
|
+
user_id_type?: NonNullable<TaskAttachmentUploadPayload["params"]>["user_id_type"];
|
|
165
|
+
};
|
|
171
166
|
|
|
172
167
|
export type ListTaskAttachmentsParams = {
|
|
173
168
|
task_guid: NonNullable<TaskAttachmentListPayload["params"]>["resource_id"];
|
|
@@ -300,12 +295,10 @@ export const CreateTaskSchema = Type.Object({
|
|
|
300
295
|
),
|
|
301
296
|
});
|
|
302
297
|
|
|
303
|
-
export const CreateSubtaskSchema = Type.
|
|
304
|
-
Type.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
CreateTaskSchema,
|
|
308
|
-
]);
|
|
298
|
+
export const CreateSubtaskSchema = Type.Object({
|
|
299
|
+
task_guid: Type.String({ description: "Parent task GUID" }),
|
|
300
|
+
...CreateTaskSchema.properties,
|
|
301
|
+
});
|
|
309
302
|
|
|
310
303
|
export const DeleteTaskSchema = Type.Object({
|
|
311
304
|
task_guid: Type.String({ description: "Task GUID to delete" }),
|
|
@@ -521,23 +514,19 @@ export const RemoveTasklistMembersSchema = Type.Object({
|
|
|
521
514
|
),
|
|
522
515
|
});
|
|
523
516
|
|
|
524
|
-
export const UploadTaskAttachmentSchema = Type.
|
|
525
|
-
Type.
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
),
|
|
531
|
-
|
|
532
|
-
Type.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
Type.String({ description: "User ID type for returned uploader" }),
|
|
538
|
-
),
|
|
539
|
-
}),
|
|
540
|
-
]);
|
|
517
|
+
export const UploadTaskAttachmentSchema = Type.Object({
|
|
518
|
+
task_guid: Type.String({ description: "Task GUID to upload attachment to" }),
|
|
519
|
+
file_path: Type.Optional(
|
|
520
|
+
Type.String({ description: "Local file path on the OpenClaw host (provide either file_path or file_url)" }),
|
|
521
|
+
),
|
|
522
|
+
file_url: Type.Optional(
|
|
523
|
+
Type.String({ description: "Remote file URL to download and upload (provide either file_path or file_url)" }),
|
|
524
|
+
),
|
|
525
|
+
filename: Type.Optional(Type.String({ description: "Override filename for uploaded attachment (only with file_url)" })),
|
|
526
|
+
user_id_type: Type.Optional(
|
|
527
|
+
Type.String({ description: "User ID type for returned uploader" }),
|
|
528
|
+
),
|
|
529
|
+
});
|
|
541
530
|
|
|
542
531
|
export const ListTaskAttachmentsSchema = Type.Object({
|
|
543
532
|
task_guid: Type.String({ description: "Task GUID to list attachments for" }),
|