@m1heng-clawd/feishu 0.1.12 → 0.1.14
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/bot.ts +181 -15
- 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/bot.ts
CHANGED
|
@@ -99,6 +99,137 @@ type SenderNameResult = {
|
|
|
99
99
|
permissionError?: PermissionError;
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
+
type FeishuPairingApiMode = "legacy" | "scoped";
|
|
103
|
+
let detectedFeishuPairingApiMode: FeishuPairingApiMode | null = null;
|
|
104
|
+
|
|
105
|
+
async function resolveFeishuPairingApiMode(params: {
|
|
106
|
+
readAllowFromStore: (...args: any[]) => Promise<Array<string | number>>;
|
|
107
|
+
}): Promise<FeishuPairingApiMode> {
|
|
108
|
+
if (detectedFeishuPairingApiMode) return detectedFeishuPairingApiMode;
|
|
109
|
+
|
|
110
|
+
let channelObserved = false;
|
|
111
|
+
const probe = new Proxy<Record<PropertyKey, unknown>>(
|
|
112
|
+
{},
|
|
113
|
+
{
|
|
114
|
+
get(_target, prop) {
|
|
115
|
+
if (prop === "channel") {
|
|
116
|
+
channelObserved = true;
|
|
117
|
+
return "__feishu_pairing_probe__";
|
|
118
|
+
}
|
|
119
|
+
if (prop === "accountId") return "__feishu_pairing_probe__";
|
|
120
|
+
if (prop === "env") return undefined;
|
|
121
|
+
if (prop === Symbol.toPrimitive) return () => "__feishu_pairing_probe__";
|
|
122
|
+
if (prop === "toString") return () => "__feishu_pairing_probe__";
|
|
123
|
+
return undefined;
|
|
124
|
+
},
|
|
125
|
+
has(_target, prop) {
|
|
126
|
+
if (prop === "channel") {
|
|
127
|
+
channelObserved = true;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Probe once to detect whether runtime reads object-style params (scoped API)
|
|
137
|
+
// or positional params (legacy API).
|
|
138
|
+
await params.readAllowFromStore(probe as any, undefined, "__feishu_pairing_probe__");
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore probe errors; fallback below still keeps behavior safe.
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
detectedFeishuPairingApiMode = channelObserved ? "scoped" : "legacy";
|
|
144
|
+
return detectedFeishuPairingApiMode;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function readFeishuPairingAllowFrom(params: {
|
|
148
|
+
core: ReturnType<typeof getFeishuRuntime>;
|
|
149
|
+
accountId: string;
|
|
150
|
+
}): Promise<Array<string | number>> {
|
|
151
|
+
const pairing = params.core.channel.pairing as {
|
|
152
|
+
readAllowFromStore: (...args: any[]) => Promise<Array<string | number>>;
|
|
153
|
+
};
|
|
154
|
+
const pairingMode = await resolveFeishuPairingApiMode({
|
|
155
|
+
readAllowFromStore: pairing.readAllowFromStore,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (pairingMode === "scoped") {
|
|
159
|
+
try {
|
|
160
|
+
return await pairing.readAllowFromStore({
|
|
161
|
+
channel: "feishu",
|
|
162
|
+
accountId: params.accountId,
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
// Compatibility fallback for older OpenClaw implementations.
|
|
166
|
+
return await pairing.readAllowFromStore("feishu", undefined, params.accountId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// OpenClaw <= 2026.2.19 signature: readAllowFromStore(channel, env?, accountId?)
|
|
172
|
+
return await pairing.readAllowFromStore("feishu", undefined, params.accountId);
|
|
173
|
+
} catch {
|
|
174
|
+
// Compatibility fallback for newer OpenClaw implementations.
|
|
175
|
+
return await pairing.readAllowFromStore({
|
|
176
|
+
channel: "feishu",
|
|
177
|
+
accountId: params.accountId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function upsertFeishuPairingRequest(params: {
|
|
183
|
+
core: ReturnType<typeof getFeishuRuntime>;
|
|
184
|
+
accountId: string;
|
|
185
|
+
senderId: string;
|
|
186
|
+
senderName?: string;
|
|
187
|
+
}): Promise<{ code: string; created: boolean }> {
|
|
188
|
+
const pairing = params.core.channel.pairing as {
|
|
189
|
+
upsertPairingRequest: (...args: any[]) => Promise<{ code: string; created: boolean }>;
|
|
190
|
+
};
|
|
191
|
+
const meta = params.senderName ? { name: params.senderName } : undefined;
|
|
192
|
+
const pairingMode = await resolveFeishuPairingApiMode({
|
|
193
|
+
readAllowFromStore: params.core.channel.pairing.readAllowFromStore as (...args: any[]) => Promise<
|
|
194
|
+
Array<string | number>
|
|
195
|
+
>,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (pairingMode === "scoped") {
|
|
199
|
+
try {
|
|
200
|
+
return await pairing.upsertPairingRequest({
|
|
201
|
+
channel: "feishu",
|
|
202
|
+
id: params.senderId,
|
|
203
|
+
accountId: params.accountId,
|
|
204
|
+
meta,
|
|
205
|
+
});
|
|
206
|
+
} catch {
|
|
207
|
+
// Compatibility fallback for older OpenClaw implementations.
|
|
208
|
+
return await pairing.upsertPairingRequest({
|
|
209
|
+
channel: "feishu",
|
|
210
|
+
id: params.senderId,
|
|
211
|
+
meta,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
return await pairing.upsertPairingRequest({
|
|
218
|
+
channel: "feishu",
|
|
219
|
+
id: params.senderId,
|
|
220
|
+
meta,
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
// Compatibility fallback for newer OpenClaw implementations.
|
|
224
|
+
return await pairing.upsertPairingRequest({
|
|
225
|
+
channel: "feishu",
|
|
226
|
+
id: params.senderId,
|
|
227
|
+
accountId: params.accountId,
|
|
228
|
+
meta,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
102
233
|
async function resolveFeishuSenderName(params: {
|
|
103
234
|
account: ResolvedFeishuAccount;
|
|
104
235
|
senderOpenId: string;
|
|
@@ -226,20 +357,48 @@ export type FeishuBotAddedEvent = {
|
|
|
226
357
|
function parseMessageContent(content: string, messageType: string): string {
|
|
227
358
|
try {
|
|
228
359
|
const parsed = JSON.parse(content);
|
|
229
|
-
|
|
360
|
+
const normalizedMessageType = normalizeFeishuInboundMessageType(messageType);
|
|
361
|
+
if (normalizedMessageType === "text") {
|
|
230
362
|
return parsed.text || "";
|
|
231
363
|
}
|
|
232
|
-
if (
|
|
364
|
+
if (normalizedMessageType === "post") {
|
|
233
365
|
// Extract text content from rich text post
|
|
234
366
|
const { textContent } = parsePostContent(content);
|
|
235
367
|
return textContent;
|
|
236
368
|
}
|
|
369
|
+
if (["image", "file", "audio", "video", "sticker"].includes(normalizedMessageType)) {
|
|
370
|
+
return inferPlaceholder(normalizedMessageType);
|
|
371
|
+
}
|
|
237
372
|
return content;
|
|
238
373
|
} catch {
|
|
239
374
|
return content;
|
|
240
375
|
}
|
|
241
376
|
}
|
|
242
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Feishu may use "media" for inbound video messages.
|
|
380
|
+
* Normalize to "video" so downstream media handling is consistent.
|
|
381
|
+
*/
|
|
382
|
+
export function normalizeFeishuInboundMessageType(messageType: string): string {
|
|
383
|
+
return messageType === "media" ? "video" : messageType;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Select the correct resource key for messageResource download.
|
|
388
|
+
* - image message: prefer image_key
|
|
389
|
+
* - other media (video/audio/file/sticker): prefer file_key
|
|
390
|
+
*/
|
|
391
|
+
export function selectFeishuMessageResourceKey(
|
|
392
|
+
messageType: string,
|
|
393
|
+
keys: { imageKey?: string; fileKey?: string },
|
|
394
|
+
): string | undefined {
|
|
395
|
+
const normalized = normalizeFeishuInboundMessageType(messageType);
|
|
396
|
+
if (normalized === "image") {
|
|
397
|
+
return keys.imageKey ?? keys.fileKey;
|
|
398
|
+
}
|
|
399
|
+
return keys.fileKey ?? keys.imageKey;
|
|
400
|
+
}
|
|
401
|
+
|
|
243
402
|
function checkBotMentioned(
|
|
244
403
|
event: FeishuMessageEvent,
|
|
245
404
|
botOpenId?: string,
|
|
@@ -288,6 +447,7 @@ function parseMediaKeys(
|
|
|
288
447
|
case "audio":
|
|
289
448
|
return { fileKey: parsed.file_key };
|
|
290
449
|
case "video":
|
|
450
|
+
case "media":
|
|
291
451
|
// Video has both file_key (video) and image_key (thumbnail)
|
|
292
452
|
return { fileKey: parsed.file_key, imageKey: parsed.image_key };
|
|
293
453
|
case "sticker":
|
|
@@ -363,6 +523,7 @@ function inferPlaceholder(messageType: string): string {
|
|
|
363
523
|
case "audio":
|
|
364
524
|
return "<media:audio>";
|
|
365
525
|
case "video":
|
|
526
|
+
case "media":
|
|
366
527
|
return "<media:video>";
|
|
367
528
|
case "sticker":
|
|
368
529
|
return "<media:sticker>";
|
|
@@ -385,10 +546,11 @@ async function resolveFeishuMediaList(params: {
|
|
|
385
546
|
accountId?: string;
|
|
386
547
|
}): Promise<FeishuMediaInfo[]> {
|
|
387
548
|
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
|
|
549
|
+
const normalizedMessageType = normalizeFeishuInboundMessageType(messageType);
|
|
388
550
|
|
|
389
551
|
// Only process media message types (including post for embedded images)
|
|
390
552
|
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
|
|
391
|
-
if (!mediaTypes.includes(
|
|
553
|
+
if (!mediaTypes.includes(normalizedMessageType)) {
|
|
392
554
|
return [];
|
|
393
555
|
}
|
|
394
556
|
|
|
@@ -396,7 +558,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
396
558
|
const core = getFeishuRuntime();
|
|
397
559
|
|
|
398
560
|
// Handle post (rich text) messages with embedded images
|
|
399
|
-
if (
|
|
561
|
+
if (normalizedMessageType === "post") {
|
|
400
562
|
const { imageKeys } = parsePostContent(content);
|
|
401
563
|
if (imageKeys.length === 0) {
|
|
402
564
|
return [];
|
|
@@ -443,7 +605,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
443
605
|
}
|
|
444
606
|
|
|
445
607
|
// Handle other media types
|
|
446
|
-
const mediaKeys = parseMediaKeys(content,
|
|
608
|
+
const mediaKeys = parseMediaKeys(content, normalizedMessageType);
|
|
447
609
|
if (!mediaKeys.imageKey && !mediaKeys.fileKey) {
|
|
448
610
|
return [];
|
|
449
611
|
}
|
|
@@ -455,12 +617,12 @@ async function resolveFeishuMediaList(params: {
|
|
|
455
617
|
|
|
456
618
|
// For message media, always use messageResource API
|
|
457
619
|
// The image.get API is only for images uploaded via im/v1/images, not for message attachments
|
|
458
|
-
const fileKey =
|
|
620
|
+
const fileKey = selectFeishuMessageResourceKey(normalizedMessageType, mediaKeys);
|
|
459
621
|
if (!fileKey) {
|
|
460
622
|
return [];
|
|
461
623
|
}
|
|
462
624
|
|
|
463
|
-
const resourceType =
|
|
625
|
+
const resourceType = normalizedMessageType === "image" ? "image" : "file";
|
|
464
626
|
const result = await downloadMessageResourceFeishu({
|
|
465
627
|
cfg,
|
|
466
628
|
messageId,
|
|
@@ -489,12 +651,12 @@ async function resolveFeishuMediaList(params: {
|
|
|
489
651
|
out.push({
|
|
490
652
|
path: saved.path,
|
|
491
653
|
contentType: saved.contentType,
|
|
492
|
-
placeholder: inferPlaceholder(
|
|
654
|
+
placeholder: inferPlaceholder(normalizedMessageType),
|
|
493
655
|
});
|
|
494
656
|
|
|
495
|
-
log?.(`feishu: downloaded ${
|
|
657
|
+
log?.(`feishu: downloaded ${normalizedMessageType} media, saved to ${saved.path}`);
|
|
496
658
|
} catch (err) {
|
|
497
|
-
log?.(`feishu: failed to download ${
|
|
659
|
+
log?.(`feishu: failed to download ${normalizedMessageType} media: ${String(err)}`);
|
|
498
660
|
}
|
|
499
661
|
|
|
500
662
|
return out;
|
|
@@ -648,7 +810,10 @@ export async function handleFeishuMessage(params: {
|
|
|
648
810
|
const hasControlCommand = core.channel.text.hasControlCommand(ctx.content, cfg);
|
|
649
811
|
const storeAllowFrom =
|
|
650
812
|
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
|
|
651
|
-
? await
|
|
813
|
+
? await readFeishuPairingAllowFrom({
|
|
814
|
+
core,
|
|
815
|
+
accountId: account.accountId,
|
|
816
|
+
}).catch(() => [])
|
|
652
817
|
: [];
|
|
653
818
|
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
654
819
|
const dmAllowed = resolveFeishuAllowlistMatch({
|
|
@@ -659,10 +824,11 @@ export async function handleFeishuMessage(params: {
|
|
|
659
824
|
|
|
660
825
|
if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
|
|
661
826
|
if (dmPolicy === "pairing") {
|
|
662
|
-
const { code, created } = await
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
827
|
+
const { code, created } = await upsertFeishuPairingRequest({
|
|
828
|
+
core,
|
|
829
|
+
accountId: account.accountId,
|
|
830
|
+
senderId: ctx.senderOpenId,
|
|
831
|
+
senderName: ctx.senderName,
|
|
666
832
|
});
|
|
667
833
|
if (created) {
|
|
668
834
|
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
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" }),
|