@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 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
- > # 1. Download the package
34
- > curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
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
- > # 2. Install from local file
37
- > openclaw plugins install ./feishu-0.1.3.tgz
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
- > # 1. 下载插件包
485
- > curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
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
- > # 2. 从本地安装
488
- > openclaw plugins install ./feishu-0.1.3.tgz
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m1heng-clawd/feishu",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "scripts": {
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
  },
@@ -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 loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
474
- maxBytes: mediaMaxBytes,
475
- optimizeImages: false,
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 {
@@ -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;
@@ -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 sendRes = await this.client.im.message.create({
112
- params: { receive_id_type: receiveIdType },
113
- data: {
114
- receive_id: receiveId,
115
- msg_type: "interactive",
116
- content: JSON.stringify({ type: "card", data: { card_id: cardId } }),
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 ("file_path" in params) {
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
- task_guid: string;
162
- file_path: string;
163
- user_id_type?: NonNullable<TaskAttachmentUploadPayload["params"]>["user_id_type"];
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.Intersect([
304
- Type.Object({
305
- task_guid: Type.String({ description: "Parent task GUID" }),
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.Union([
525
- Type.Object({
526
- task_guid: Type.String({ description: "Task GUID to upload attachment to" }),
527
- file_path: Type.String({ description: "Local file path on the OpenClaw host" }),
528
- user_id_type: Type.Optional(
529
- Type.String({ description: "User ID type for returned uploader" }),
530
- ),
531
- }),
532
- Type.Object({
533
- task_guid: Type.String({ description: "Task GUID to upload attachment to" }),
534
- file_url: Type.String({ description: "Remote file URL to download and upload" }),
535
- filename: Type.Optional(Type.String({ description: "Override filename for uploaded attachment" })),
536
- user_id_type: Type.Optional(
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" }),