@soimy/dingtalk 3.3.0 → 3.4.1
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 +141 -12
- package/index.ts +71 -66
- package/package.json +6 -5
- package/src/access-control.ts +65 -0
- package/src/ack-reaction/dynamic-ack-reaction-controller.ts +271 -0
- package/src/ack-reaction/dynamic-ack-reaction-events.ts +123 -0
- package/src/ack-reaction/dynamic-ack-reaction-progress.ts +59 -0
- package/src/ack-reaction-classifier.ts +17 -4
- package/src/ack-reaction-service.ts +66 -19
- package/src/attachment-text-extractor.ts +2 -1
- package/src/card-service.ts +145 -257
- package/src/channel.ts +106 -47
- package/src/config-schema.ts +28 -6
- package/src/config.ts +30 -6
- package/src/connection-manager.ts +16 -5
- package/src/inbound-handler.ts +694 -520
- package/src/media-utils.ts +99 -36
- package/src/message-context-store.ts +787 -0
- package/src/message-utils.ts +221 -42
- package/src/messaging/quoted-context.ts +269 -0
- package/src/messaging/quoted-ref.ts +97 -0
- package/src/onboarding.ts +381 -269
- package/src/reply-strategy-card.ts +225 -0
- package/src/reply-strategy-markdown.ts +55 -0
- package/src/reply-strategy-with-reaction.ts +190 -0
- package/src/reply-strategy.ts +72 -0
- package/src/runtime.ts +5 -7
- package/src/send-service.ts +164 -62
- package/src/targeting/agent-name-matcher.ts +148 -0
- package/src/targeting/agent-routing.ts +181 -0
- package/src/targeting/target-directory-adapter.ts +152 -0
- package/src/targeting/target-directory-store.ts +396 -0
- package/src/targeting/target-input.ts +62 -0
- package/src/types.ts +124 -21
- package/src/quote-journal.ts +0 -242
- package/src/quoted-msg-cache.ts +0 -226
package/src/media-utils.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Provides functions for media type detection and file upload to DingTalk media servers.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
8
|
import { randomUUID } from "node:crypto";
|
|
10
9
|
import * as os from "node:os";
|
|
11
10
|
import * as path from "node:path";
|
|
@@ -16,17 +15,35 @@ import axios from "axios";
|
|
|
16
15
|
import FormData from "form-data";
|
|
17
16
|
import type { DingTalkConfig, Logger } from "./types";
|
|
18
17
|
import { formatDingTalkErrorPayloadLog, getProxyBypassOption } from "./utils";
|
|
18
|
+
import { getDingTalkRuntime } from "./runtime";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extended PluginRuntime with media bridge support.
|
|
22
|
+
* The `media.loadWebMedia` method resolves sandbox/container paths
|
|
23
|
+
* through the runtime bridge when direct host filesystem access fails.
|
|
24
|
+
*/
|
|
25
|
+
interface PluginRuntimeWithMedia {
|
|
26
|
+
media?: {
|
|
27
|
+
loadWebMedia(
|
|
28
|
+
mediaPath: string,
|
|
29
|
+
options?: { mediaLocalRoots?: string[] },
|
|
30
|
+
): Promise<{ buffer: Buffer | ArrayBuffer; fileName?: string; contentType?: string } | null>;
|
|
31
|
+
};
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
19
34
|
|
|
20
35
|
/**
|
|
21
36
|
* Calculate MP3 duration in seconds by parsing MPEG frame headers
|
|
22
37
|
* Supports CBR and VBR MP3 files
|
|
23
|
-
* @param
|
|
38
|
+
* @param filePathOrBuffer Path to the MP3 file, or a pre-read Buffer
|
|
24
39
|
* @param log Optional logger
|
|
25
40
|
* @returns Duration in seconds (0 if parsing fails)
|
|
26
41
|
*/
|
|
27
|
-
export async function getMp3DurationSeconds(
|
|
42
|
+
export async function getMp3DurationSeconds(filePathOrBuffer: string | Buffer, log?: Logger): Promise<number> {
|
|
28
43
|
try {
|
|
29
|
-
const buffer =
|
|
44
|
+
const buffer = typeof filePathOrBuffer === "string"
|
|
45
|
+
? await fsPromises.readFile(filePathOrBuffer)
|
|
46
|
+
: filePathOrBuffer;
|
|
30
47
|
let offset = 0;
|
|
31
48
|
|
|
32
49
|
// Skip ID3v2 tag if present
|
|
@@ -181,7 +198,7 @@ export async function getMp3DurationSeconds(filePath: string, log?: Logger): Pro
|
|
|
181
198
|
return Math.floor(duration);
|
|
182
199
|
}
|
|
183
200
|
|
|
184
|
-
log?.warn?.(`[DingTalk] Could not parse MP3 duration from ${
|
|
201
|
+
log?.warn?.(`[DingTalk] Could not parse MP3 duration from ${typeof filePathOrBuffer === "string" ? filePathOrBuffer : "<buffer>"} (found ${frameCount} frames)`);
|
|
185
202
|
return 0;
|
|
186
203
|
} catch (err: unknown) {
|
|
187
204
|
log?.error?.(`[DingTalk] Failed to get MP3 duration: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -195,6 +212,7 @@ export async function getVoiceDurationMs(
|
|
|
195
212
|
filePath: string,
|
|
196
213
|
mediaType: DingTalkMediaType,
|
|
197
214
|
log?: Logger,
|
|
215
|
+
options?: { mediaLocalRoots?: string[]; preReadBuffer?: Buffer },
|
|
198
216
|
): Promise<number> {
|
|
199
217
|
if (mediaType !== "voice") {
|
|
200
218
|
return DEFAULT_VOICE_DURATION_MS;
|
|
@@ -203,7 +221,15 @@ export async function getVoiceDurationMs(
|
|
|
203
221
|
const ext = path.extname(filePath).toLowerCase();
|
|
204
222
|
|
|
205
223
|
if (ext === ".mp3") {
|
|
206
|
-
|
|
224
|
+
let durationSec: number;
|
|
225
|
+
try {
|
|
226
|
+
// Reuse pre-read buffer from uploadMedia when available to avoid double read
|
|
227
|
+
const buffer = options?.preReadBuffer
|
|
228
|
+
?? (await readMediaBuffer(filePath, options, log)).buffer;
|
|
229
|
+
durationSec = await getMp3DurationSeconds(buffer, log);
|
|
230
|
+
} catch {
|
|
231
|
+
durationSec = 0;
|
|
232
|
+
}
|
|
207
233
|
if (durationSec > 0) {
|
|
208
234
|
return Math.max(1, Math.round(durationSec * 1000));
|
|
209
235
|
}
|
|
@@ -607,36 +633,80 @@ const FILE_SIZE_LIMITS: Record<DingTalkMediaType, number> = {
|
|
|
607
633
|
};
|
|
608
634
|
|
|
609
635
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
636
|
+
* Read a media file, resolving sandbox/container paths via the runtime bridge
|
|
637
|
+
* when direct host filesystem access fails.
|
|
612
638
|
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
* @param config DingTalk configuration
|
|
617
|
-
* @param mediaPath Local path to the media file
|
|
618
|
-
* @param mediaType Type of media: 'image' | 'voice' | 'video' | 'file'
|
|
619
|
-
* @param getAccessToken Function to get DingTalk access token
|
|
620
|
-
* @param log Optional logger
|
|
621
|
-
* @returns media_id on success, null on failure
|
|
639
|
+
* Precedence:
|
|
640
|
+
* 1. Direct fs.readFile (works for host-local paths)
|
|
641
|
+
* 2. rt.media.loadWebMedia (resolves sandbox workspace paths via bridge)
|
|
622
642
|
*/
|
|
643
|
+
async function readMediaBuffer(
|
|
644
|
+
mediaPath: string,
|
|
645
|
+
options?: { mediaLocalRoots?: string[] },
|
|
646
|
+
log?: Logger,
|
|
647
|
+
): Promise<{ buffer: Buffer; size: number }> {
|
|
648
|
+
// Try direct host filesystem first
|
|
649
|
+
try {
|
|
650
|
+
const buffer = await fsPromises.readFile(mediaPath);
|
|
651
|
+
return { buffer, size: buffer.length };
|
|
652
|
+
} catch (err: unknown) {
|
|
653
|
+
const errno = err as NodeJS.ErrnoException;
|
|
654
|
+
if (errno.code !== "ENOENT") {
|
|
655
|
+
throw err; // Permission errors etc. should propagate immediately
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// File not found on host — try runtime media bridge (sandbox/container paths)
|
|
660
|
+
log?.debug?.(`[DingTalk] File not found on host, trying runtime media bridge: ${mediaPath}`);
|
|
661
|
+
const rt = getDingTalkRuntime() as PluginRuntimeWithMedia;
|
|
662
|
+
if (!rt.media?.loadWebMedia) {
|
|
663
|
+
throw Object.assign(
|
|
664
|
+
new Error(`File not found and runtime media bridge unavailable: ${mediaPath}`),
|
|
665
|
+
{ code: "ENOENT" },
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const media = await rt.media.loadWebMedia(mediaPath, {
|
|
670
|
+
mediaLocalRoots: options?.mediaLocalRoots,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
if (!media || !media.buffer) {
|
|
674
|
+
throw Object.assign(
|
|
675
|
+
new Error(`Runtime media bridge returned no data for: ${mediaPath}`),
|
|
676
|
+
{ code: "ENOENT" },
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const buffer = Buffer.isBuffer(media.buffer)
|
|
681
|
+
? media.buffer
|
|
682
|
+
: Buffer.from(media.buffer);
|
|
683
|
+
return { buffer, size: buffer.length };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export interface UploadMediaResult {
|
|
687
|
+
mediaId: string;
|
|
688
|
+
/** The file buffer read during upload, reusable for voice duration parsing etc. */
|
|
689
|
+
buffer: Buffer;
|
|
690
|
+
}
|
|
691
|
+
|
|
623
692
|
export async function uploadMedia(
|
|
624
693
|
config: DingTalkConfig,
|
|
625
694
|
mediaPath: string,
|
|
626
695
|
mediaType: DingTalkMediaType,
|
|
627
696
|
getAccessToken: (config: DingTalkConfig, log?: Logger) => Promise<string>,
|
|
628
697
|
log?: Logger,
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
698
|
+
options?: { mediaLocalRoots?: string[] },
|
|
699
|
+
): Promise<UploadMediaResult | null> {
|
|
632
700
|
try {
|
|
633
701
|
const token = await getAccessToken(config, log);
|
|
634
702
|
|
|
635
|
-
//
|
|
636
|
-
const
|
|
703
|
+
// Read file via sandbox-aware bridge (falls back to direct fs for host paths)
|
|
704
|
+
const { buffer, size } = await readMediaBuffer(mediaPath, options, log);
|
|
705
|
+
|
|
706
|
+
// Check file size
|
|
637
707
|
const sizeLimit = FILE_SIZE_LIMITS[mediaType];
|
|
638
|
-
if (
|
|
639
|
-
const sizeMB = (
|
|
708
|
+
if (size > sizeLimit) {
|
|
709
|
+
const sizeMB = (size / (1024 * 1024)).toFixed(2);
|
|
640
710
|
const limitMB = (sizeLimit / (1024 * 1024)).toFixed(2);
|
|
641
711
|
log?.error?.(
|
|
642
712
|
`[DingTalk] Media file too large: ${sizeMB}MB exceeds ${limitMB}MB limit for ${mediaType}`,
|
|
@@ -644,17 +714,15 @@ export async function uploadMedia(
|
|
|
644
714
|
return null;
|
|
645
715
|
}
|
|
646
716
|
|
|
647
|
-
// Read file as a stream for better memory efficiency
|
|
648
|
-
fileStream = fs.createReadStream(mediaPath);
|
|
649
717
|
const filename = path.basename(mediaPath);
|
|
650
718
|
|
|
651
719
|
// Upload to DingTalk's media server using form-data
|
|
652
720
|
const form = new FormData();
|
|
653
|
-
form.append("media",
|
|
721
|
+
form.append("media", buffer, { filename });
|
|
654
722
|
|
|
655
723
|
const uploadUrl = `https://oapi.dingtalk.com/media/upload?access_token=${token}&type=${mediaType}`;
|
|
656
724
|
|
|
657
|
-
log?.debug?.(`[DingTalk] Uploading media: ${filename} (${
|
|
725
|
+
log?.debug?.(`[DingTalk] Uploading media: ${filename} (${size} bytes) as ${mediaType}`);
|
|
658
726
|
|
|
659
727
|
const response = await axios.post(uploadUrl, form, {
|
|
660
728
|
headers: form.getHeaders(),
|
|
@@ -665,9 +733,9 @@ export async function uploadMedia(
|
|
|
665
733
|
|
|
666
734
|
if (response.data?.errcode === 0 && response.data?.media_id) {
|
|
667
735
|
log?.debug?.(
|
|
668
|
-
`[DingTalk] Media uploaded successfully: ${response.data.media_id} (${
|
|
736
|
+
`[DingTalk] Media uploaded successfully: ${response.data.media_id} (${size} bytes)`,
|
|
669
737
|
);
|
|
670
|
-
return response.data.media_id;
|
|
738
|
+
return { mediaId: response.data.media_id, buffer };
|
|
671
739
|
} else {
|
|
672
740
|
log?.error?.(`[DingTalk] Media upload failed: ${JSON.stringify(response.data)}`);
|
|
673
741
|
return null;
|
|
@@ -676,7 +744,7 @@ export async function uploadMedia(
|
|
|
676
744
|
// Handle file system errors (e.g., file not found, permission denied)
|
|
677
745
|
const errno = err as NodeJS.ErrnoException;
|
|
678
746
|
if (errno.code === "ENOENT") {
|
|
679
|
-
log?.error?.(`[DingTalk] Media file not found: ${mediaPath}`);
|
|
747
|
+
log?.error?.(`[DingTalk] Media file not found (host and sandbox): ${mediaPath}`);
|
|
680
748
|
} else if (errno.code === "EACCES") {
|
|
681
749
|
log?.error?.(`[DingTalk] Permission denied accessing media file: ${mediaPath}`);
|
|
682
750
|
} else {
|
|
@@ -690,10 +758,5 @@ export async function uploadMedia(
|
|
|
690
758
|
}
|
|
691
759
|
}
|
|
692
760
|
return null;
|
|
693
|
-
} finally {
|
|
694
|
-
// Ensure file stream is closed even on error
|
|
695
|
-
if (fileStream) {
|
|
696
|
-
fileStream.destroy();
|
|
697
|
-
}
|
|
698
761
|
}
|
|
699
762
|
}
|