@openclaw-china/shared 2026.3.22 → 2026.3.29
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/package.json +1 -1
- package/src/cli/index.ts +2 -2
- package/src/cron/index.ts +16 -16
- package/src/file/file-utils.test.ts +141 -141
- package/src/file/file-utils.ts +284 -284
- package/src/file/index.ts +10 -10
- package/src/index.ts +3 -3
- package/src/logger/index.ts +1 -1
- package/src/logger/logger.ts +51 -51
- package/src/media/index.ts +22 -22
- package/vitest.config.ts +8 -8
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./china-setup.js";
|
|
2
|
-
export * from "./install-hint.js";
|
|
1
|
+
export * from "./china-setup.js";
|
|
2
|
+
export * from "./install-hint.js";
|
package/src/cron/index.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
const CRON_HIDDEN_PROMPT = `
|
|
2
|
-
When creating or updating a cron task, always store a fixed delivery target in the job itself.
|
|
3
|
-
- Use the built-in cron tool (action=add/update). Do not run shell commands.
|
|
4
|
-
- Must use sessionTarget="isolated" for reminder jobs.
|
|
5
|
-
- payload.kind="agentTurn"
|
|
6
|
-
- payload.message must be plain user-visible reminder text only.
|
|
7
|
-
- You must encode runtime guardrails directly into payload.message so the cron run can follow them without extra context.
|
|
8
|
-
- Runtime guardrails to encode in payload.message:
|
|
9
|
-
- return plain text only
|
|
10
|
-
- never call any tool
|
|
11
|
-
- never call the message tool
|
|
12
|
-
- never send manually; delivery is handled by cron delivery settings
|
|
13
|
-
- Do not include tool directives, "NO_REPLY", or heartbeat markers in payload.message.
|
|
14
|
-
- Job name is never a message target.
|
|
15
|
-
- During cron run, must return plain text only and never call the message tool.
|
|
16
|
-
- Use top-level delivery with announce mode:
|
|
1
|
+
const CRON_HIDDEN_PROMPT = `
|
|
2
|
+
When creating or updating a cron task, always store a fixed delivery target in the job itself.
|
|
3
|
+
- Use the built-in cron tool (action=add/update). Do not run shell commands.
|
|
4
|
+
- Must use sessionTarget="isolated" for reminder jobs.
|
|
5
|
+
- payload.kind="agentTurn"
|
|
6
|
+
- payload.message must be plain user-visible reminder text only.
|
|
7
|
+
- You must encode runtime guardrails directly into payload.message so the cron run can follow them without extra context.
|
|
8
|
+
- Runtime guardrails to encode in payload.message:
|
|
9
|
+
- return plain text only
|
|
10
|
+
- never call any tool
|
|
11
|
+
- never call the message tool
|
|
12
|
+
- never send manually; delivery is handled by cron delivery settings
|
|
13
|
+
- Do not include tool directives, "NO_REPLY", or heartbeat markers in payload.message.
|
|
14
|
+
- Job name is never a message target.
|
|
15
|
+
- During cron run, must return plain text only and never call the message tool.
|
|
16
|
+
- Use top-level delivery with announce mode:
|
|
17
17
|
delivery.mode="announce"
|
|
18
18
|
delivery.channel=<OriginatingChannel> (example: "qqbot")
|
|
19
19
|
delivery.to=<OriginatingTo> (examples: "user:<openid>" / "group:<group_openid>")
|
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit Tests for File Utilities
|
|
3
|
-
*
|
|
4
|
-
* Feature: dingtalk-media-receive
|
|
5
|
-
* Validates: Requirements 5.1-5.8, 6.1-6.6
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from "vitest";
|
|
9
|
-
import { resolveFileCategory, resolveExtension } from "./file-utils.js";
|
|
10
|
-
|
|
11
|
-
describe("resolveFileCategory", () => {
|
|
12
|
-
// Image categorization (Requirement 5.1)
|
|
13
|
-
it("should categorize image MIME types", () => {
|
|
14
|
-
expect(resolveFileCategory("image/jpeg")).toBe("image");
|
|
15
|
-
expect(resolveFileCategory("image/png")).toBe("image");
|
|
16
|
-
expect(resolveFileCategory("image/gif")).toBe("image");
|
|
17
|
-
expect(resolveFileCategory("image/webp")).toBe("image");
|
|
18
|
-
expect(resolveFileCategory("image/bmp")).toBe("image");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// Audio categorization (Requirement 5.2)
|
|
22
|
-
it("should categorize audio MIME types", () => {
|
|
23
|
-
expect(resolveFileCategory("audio/mpeg")).toBe("audio");
|
|
24
|
-
expect(resolveFileCategory("audio/wav")).toBe("audio");
|
|
25
|
-
expect(resolveFileCategory("audio/ogg")).toBe("audio");
|
|
26
|
-
expect(resolveFileCategory("audio/amr")).toBe("audio");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Video categorization (Requirement 5.3)
|
|
30
|
-
it("should categorize video MIME types", () => {
|
|
31
|
-
expect(resolveFileCategory("video/mp4")).toBe("video");
|
|
32
|
-
expect(resolveFileCategory("video/quicktime")).toBe("video");
|
|
33
|
-
expect(resolveFileCategory("video/webm")).toBe("video");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Document categorization (Requirement 5.4)
|
|
37
|
-
it("should categorize document MIME types", () => {
|
|
38
|
-
expect(resolveFileCategory("application/pdf")).toBe("document");
|
|
39
|
-
expect(resolveFileCategory("application/msword")).toBe("document");
|
|
40
|
-
expect(resolveFileCategory("text/plain")).toBe("document");
|
|
41
|
-
expect(resolveFileCategory("text/markdown")).toBe("document");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Archive categorization (Requirement 5.5)
|
|
45
|
-
it("should categorize archive MIME types", () => {
|
|
46
|
-
expect(resolveFileCategory("application/zip")).toBe("archive");
|
|
47
|
-
expect(resolveFileCategory("application/x-rar-compressed")).toBe("archive");
|
|
48
|
-
expect(resolveFileCategory("application/x-7z-compressed")).toBe("archive");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Code categorization (Requirement 5.6)
|
|
52
|
-
it("should categorize code MIME types", () => {
|
|
53
|
-
expect(resolveFileCategory("application/json")).toBe("code");
|
|
54
|
-
expect(resolveFileCategory("text/html")).toBe("code");
|
|
55
|
-
expect(resolveFileCategory("text/css")).toBe("code");
|
|
56
|
-
expect(resolveFileCategory("text/javascript")).toBe("code");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Extension fallback (Requirement 5.8)
|
|
60
|
-
it("should use extension fallback when MIME type is unknown", () => {
|
|
61
|
-
expect(resolveFileCategory("application/octet-stream", "photo.jpg")).toBe("image");
|
|
62
|
-
expect(resolveFileCategory("application/octet-stream", "song.mp3")).toBe("audio");
|
|
63
|
-
expect(resolveFileCategory("application/octet-stream", "movie.mp4")).toBe("video");
|
|
64
|
-
expect(resolveFileCategory("application/octet-stream", "doc.pdf")).toBe("document");
|
|
65
|
-
expect(resolveFileCategory("application/octet-stream", "archive.zip")).toBe("archive");
|
|
66
|
-
expect(resolveFileCategory("application/octet-stream", "script.py")).toBe("code");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Other category (Requirement 5.7)
|
|
70
|
-
it("should return 'other' for unknown types", () => {
|
|
71
|
-
expect(resolveFileCategory("application/octet-stream")).toBe("other");
|
|
72
|
-
expect(resolveFileCategory("application/unknown")).toBe("other");
|
|
73
|
-
expect(resolveFileCategory("application/octet-stream", "file.xyz")).toBe("other");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// MIME type normalization
|
|
77
|
-
it("should handle MIME types with parameters", () => {
|
|
78
|
-
expect(resolveFileCategory("image/jpeg; charset=utf-8")).toBe("image");
|
|
79
|
-
expect(resolveFileCategory("text/plain; charset=utf-8")).toBe("document");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("resolveExtension", () => {
|
|
84
|
-
// Image extensions (Requirement 6.1)
|
|
85
|
-
it("should resolve image MIME types to extensions", () => {
|
|
86
|
-
expect(resolveExtension("image/jpeg")).toBe(".jpg");
|
|
87
|
-
expect(resolveExtension("image/png")).toBe(".png");
|
|
88
|
-
expect(resolveExtension("image/gif")).toBe(".gif");
|
|
89
|
-
expect(resolveExtension("image/webp")).toBe(".webp");
|
|
90
|
-
expect(resolveExtension("image/bmp")).toBe(".bmp");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Audio extensions (Requirement 6.2)
|
|
94
|
-
it("should resolve audio MIME types to extensions", () => {
|
|
95
|
-
expect(resolveExtension("audio/mpeg")).toBe(".mp3");
|
|
96
|
-
expect(resolveExtension("audio/wav")).toBe(".wav");
|
|
97
|
-
expect(resolveExtension("audio/ogg")).toBe(".ogg");
|
|
98
|
-
expect(resolveExtension("audio/amr")).toBe(".amr");
|
|
99
|
-
expect(resolveExtension("audio/x-m4a")).toBe(".m4a");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Video extensions (Requirement 6.3)
|
|
103
|
-
it("should resolve video MIME types to extensions", () => {
|
|
104
|
-
expect(resolveExtension("video/mp4")).toBe(".mp4");
|
|
105
|
-
expect(resolveExtension("video/quicktime")).toBe(".mov");
|
|
106
|
-
expect(resolveExtension("video/x-msvideo")).toBe(".avi");
|
|
107
|
-
expect(resolveExtension("video/webm")).toBe(".webm");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Document extensions (Requirement 6.4)
|
|
111
|
-
it("should resolve document MIME types to extensions", () => {
|
|
112
|
-
expect(resolveExtension("application/pdf")).toBe(".pdf");
|
|
113
|
-
expect(resolveExtension("application/msword")).toBe(".doc");
|
|
114
|
-
expect(resolveExtension("application/vnd.openxmlformats-officedocument.wordprocessingml.document")).toBe(".docx");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Default extension (Requirement 6.5)
|
|
118
|
-
it("should return .bin for unknown MIME types", () => {
|
|
119
|
-
expect(resolveExtension("application/unknown")).toBe(".bin");
|
|
120
|
-
expect(resolveExtension("application/octet-stream")).toBe(".bin");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// fileName precedence (Requirement 6.6)
|
|
124
|
-
it("should use fileName extension when provided", () => {
|
|
125
|
-
expect(resolveExtension("application/octet-stream", "photo.jpg")).toBe(".jpg");
|
|
126
|
-
expect(resolveExtension("image/png", "custom.jpeg")).toBe(".jpeg");
|
|
127
|
-
expect(resolveExtension("application/unknown", "document.pdf")).toBe(".pdf");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// MIME type normalization
|
|
131
|
-
it("should handle MIME types with parameters", () => {
|
|
132
|
-
expect(resolveExtension("image/jpeg; charset=utf-8")).toBe(".jpg");
|
|
133
|
-
expect(resolveExtension("audio/mpeg; bitrate=320")).toBe(".mp3");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Edge cases
|
|
137
|
-
it("should handle fileName without extension", () => {
|
|
138
|
-
expect(resolveExtension("image/jpeg", "photo")).toBe(".jpg");
|
|
139
|
-
expect(resolveExtension("application/unknown", "noext")).toBe(".bin");
|
|
140
|
-
});
|
|
141
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for File Utilities
|
|
3
|
+
*
|
|
4
|
+
* Feature: dingtalk-media-receive
|
|
5
|
+
* Validates: Requirements 5.1-5.8, 6.1-6.6
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { resolveFileCategory, resolveExtension } from "./file-utils.js";
|
|
10
|
+
|
|
11
|
+
describe("resolveFileCategory", () => {
|
|
12
|
+
// Image categorization (Requirement 5.1)
|
|
13
|
+
it("should categorize image MIME types", () => {
|
|
14
|
+
expect(resolveFileCategory("image/jpeg")).toBe("image");
|
|
15
|
+
expect(resolveFileCategory("image/png")).toBe("image");
|
|
16
|
+
expect(resolveFileCategory("image/gif")).toBe("image");
|
|
17
|
+
expect(resolveFileCategory("image/webp")).toBe("image");
|
|
18
|
+
expect(resolveFileCategory("image/bmp")).toBe("image");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Audio categorization (Requirement 5.2)
|
|
22
|
+
it("should categorize audio MIME types", () => {
|
|
23
|
+
expect(resolveFileCategory("audio/mpeg")).toBe("audio");
|
|
24
|
+
expect(resolveFileCategory("audio/wav")).toBe("audio");
|
|
25
|
+
expect(resolveFileCategory("audio/ogg")).toBe("audio");
|
|
26
|
+
expect(resolveFileCategory("audio/amr")).toBe("audio");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Video categorization (Requirement 5.3)
|
|
30
|
+
it("should categorize video MIME types", () => {
|
|
31
|
+
expect(resolveFileCategory("video/mp4")).toBe("video");
|
|
32
|
+
expect(resolveFileCategory("video/quicktime")).toBe("video");
|
|
33
|
+
expect(resolveFileCategory("video/webm")).toBe("video");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Document categorization (Requirement 5.4)
|
|
37
|
+
it("should categorize document MIME types", () => {
|
|
38
|
+
expect(resolveFileCategory("application/pdf")).toBe("document");
|
|
39
|
+
expect(resolveFileCategory("application/msword")).toBe("document");
|
|
40
|
+
expect(resolveFileCategory("text/plain")).toBe("document");
|
|
41
|
+
expect(resolveFileCategory("text/markdown")).toBe("document");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Archive categorization (Requirement 5.5)
|
|
45
|
+
it("should categorize archive MIME types", () => {
|
|
46
|
+
expect(resolveFileCategory("application/zip")).toBe("archive");
|
|
47
|
+
expect(resolveFileCategory("application/x-rar-compressed")).toBe("archive");
|
|
48
|
+
expect(resolveFileCategory("application/x-7z-compressed")).toBe("archive");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Code categorization (Requirement 5.6)
|
|
52
|
+
it("should categorize code MIME types", () => {
|
|
53
|
+
expect(resolveFileCategory("application/json")).toBe("code");
|
|
54
|
+
expect(resolveFileCategory("text/html")).toBe("code");
|
|
55
|
+
expect(resolveFileCategory("text/css")).toBe("code");
|
|
56
|
+
expect(resolveFileCategory("text/javascript")).toBe("code");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Extension fallback (Requirement 5.8)
|
|
60
|
+
it("should use extension fallback when MIME type is unknown", () => {
|
|
61
|
+
expect(resolveFileCategory("application/octet-stream", "photo.jpg")).toBe("image");
|
|
62
|
+
expect(resolveFileCategory("application/octet-stream", "song.mp3")).toBe("audio");
|
|
63
|
+
expect(resolveFileCategory("application/octet-stream", "movie.mp4")).toBe("video");
|
|
64
|
+
expect(resolveFileCategory("application/octet-stream", "doc.pdf")).toBe("document");
|
|
65
|
+
expect(resolveFileCategory("application/octet-stream", "archive.zip")).toBe("archive");
|
|
66
|
+
expect(resolveFileCategory("application/octet-stream", "script.py")).toBe("code");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Other category (Requirement 5.7)
|
|
70
|
+
it("should return 'other' for unknown types", () => {
|
|
71
|
+
expect(resolveFileCategory("application/octet-stream")).toBe("other");
|
|
72
|
+
expect(resolveFileCategory("application/unknown")).toBe("other");
|
|
73
|
+
expect(resolveFileCategory("application/octet-stream", "file.xyz")).toBe("other");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// MIME type normalization
|
|
77
|
+
it("should handle MIME types with parameters", () => {
|
|
78
|
+
expect(resolveFileCategory("image/jpeg; charset=utf-8")).toBe("image");
|
|
79
|
+
expect(resolveFileCategory("text/plain; charset=utf-8")).toBe("document");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("resolveExtension", () => {
|
|
84
|
+
// Image extensions (Requirement 6.1)
|
|
85
|
+
it("should resolve image MIME types to extensions", () => {
|
|
86
|
+
expect(resolveExtension("image/jpeg")).toBe(".jpg");
|
|
87
|
+
expect(resolveExtension("image/png")).toBe(".png");
|
|
88
|
+
expect(resolveExtension("image/gif")).toBe(".gif");
|
|
89
|
+
expect(resolveExtension("image/webp")).toBe(".webp");
|
|
90
|
+
expect(resolveExtension("image/bmp")).toBe(".bmp");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Audio extensions (Requirement 6.2)
|
|
94
|
+
it("should resolve audio MIME types to extensions", () => {
|
|
95
|
+
expect(resolveExtension("audio/mpeg")).toBe(".mp3");
|
|
96
|
+
expect(resolveExtension("audio/wav")).toBe(".wav");
|
|
97
|
+
expect(resolveExtension("audio/ogg")).toBe(".ogg");
|
|
98
|
+
expect(resolveExtension("audio/amr")).toBe(".amr");
|
|
99
|
+
expect(resolveExtension("audio/x-m4a")).toBe(".m4a");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Video extensions (Requirement 6.3)
|
|
103
|
+
it("should resolve video MIME types to extensions", () => {
|
|
104
|
+
expect(resolveExtension("video/mp4")).toBe(".mp4");
|
|
105
|
+
expect(resolveExtension("video/quicktime")).toBe(".mov");
|
|
106
|
+
expect(resolveExtension("video/x-msvideo")).toBe(".avi");
|
|
107
|
+
expect(resolveExtension("video/webm")).toBe(".webm");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Document extensions (Requirement 6.4)
|
|
111
|
+
it("should resolve document MIME types to extensions", () => {
|
|
112
|
+
expect(resolveExtension("application/pdf")).toBe(".pdf");
|
|
113
|
+
expect(resolveExtension("application/msword")).toBe(".doc");
|
|
114
|
+
expect(resolveExtension("application/vnd.openxmlformats-officedocument.wordprocessingml.document")).toBe(".docx");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Default extension (Requirement 6.5)
|
|
118
|
+
it("should return .bin for unknown MIME types", () => {
|
|
119
|
+
expect(resolveExtension("application/unknown")).toBe(".bin");
|
|
120
|
+
expect(resolveExtension("application/octet-stream")).toBe(".bin");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// fileName precedence (Requirement 6.6)
|
|
124
|
+
it("should use fileName extension when provided", () => {
|
|
125
|
+
expect(resolveExtension("application/octet-stream", "photo.jpg")).toBe(".jpg");
|
|
126
|
+
expect(resolveExtension("image/png", "custom.jpeg")).toBe(".jpeg");
|
|
127
|
+
expect(resolveExtension("application/unknown", "document.pdf")).toBe(".pdf");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// MIME type normalization
|
|
131
|
+
it("should handle MIME types with parameters", () => {
|
|
132
|
+
expect(resolveExtension("image/jpeg; charset=utf-8")).toBe(".jpg");
|
|
133
|
+
expect(resolveExtension("audio/mpeg; bitrate=320")).toBe(".mp3");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Edge cases
|
|
137
|
+
it("should handle fileName without extension", () => {
|
|
138
|
+
expect(resolveExtension("image/jpeg", "photo")).toBe(".jpg");
|
|
139
|
+
expect(resolveExtension("application/unknown", "noext")).toBe(".bin");
|
|
140
|
+
});
|
|
141
|
+
});
|
package/src/file/file-utils.ts
CHANGED
|
@@ -1,284 +1,284 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File utilities for categorizing and resolving file extensions
|
|
3
|
-
* @module @openclaw-china/shared/file
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* File category for processing strategy
|
|
8
|
-
*/
|
|
9
|
-
export type FileCategory =
|
|
10
|
-
| "image"
|
|
11
|
-
| "audio"
|
|
12
|
-
| "video"
|
|
13
|
-
| "document"
|
|
14
|
-
| "archive"
|
|
15
|
-
| "code"
|
|
16
|
-
| "other";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* MIME type to extension mapping
|
|
20
|
-
*/
|
|
21
|
-
const MIME_TO_EXTENSION: Record<string, string> = {
|
|
22
|
-
// Images
|
|
23
|
-
"image/jpeg": ".jpg",
|
|
24
|
-
"image/png": ".png",
|
|
25
|
-
"image/gif": ".gif",
|
|
26
|
-
"image/webp": ".webp",
|
|
27
|
-
"image/bmp": ".bmp",
|
|
28
|
-
|
|
29
|
-
// Audio
|
|
30
|
-
"audio/mpeg": ".mp3",
|
|
31
|
-
"audio/wav": ".wav",
|
|
32
|
-
"audio/ogg": ".ogg",
|
|
33
|
-
"audio/amr": ".amr",
|
|
34
|
-
"audio/x-m4a": ".m4a",
|
|
35
|
-
|
|
36
|
-
// Video
|
|
37
|
-
"video/mp4": ".mp4",
|
|
38
|
-
"video/quicktime": ".mov",
|
|
39
|
-
"video/x-msvideo": ".avi",
|
|
40
|
-
"video/webm": ".webm",
|
|
41
|
-
|
|
42
|
-
// Documents
|
|
43
|
-
"application/pdf": ".pdf",
|
|
44
|
-
"application/msword": ".doc",
|
|
45
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
46
|
-
".docx",
|
|
47
|
-
"application/vnd.ms-excel": ".xls",
|
|
48
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
49
|
-
"application/vnd.ms-powerpoint": ".ppt",
|
|
50
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
|
51
|
-
".pptx",
|
|
52
|
-
"application/rtf": ".rtf",
|
|
53
|
-
"application/vnd.oasis.opendocument.text": ".odt",
|
|
54
|
-
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
|
55
|
-
"text/plain": ".txt",
|
|
56
|
-
"text/markdown": ".md",
|
|
57
|
-
"text/csv": ".csv",
|
|
58
|
-
|
|
59
|
-
// Archives
|
|
60
|
-
"application/zip": ".zip",
|
|
61
|
-
"application/x-rar-compressed": ".rar",
|
|
62
|
-
"application/vnd.rar": ".rar",
|
|
63
|
-
"application/x-7z-compressed": ".7z",
|
|
64
|
-
"application/x-tar": ".tar",
|
|
65
|
-
"application/gzip": ".gz",
|
|
66
|
-
"application/x-gzip": ".gz",
|
|
67
|
-
"application/x-bzip2": ".bz2",
|
|
68
|
-
|
|
69
|
-
// Code
|
|
70
|
-
"application/json": ".json",
|
|
71
|
-
"application/xml": ".xml",
|
|
72
|
-
"text/xml": ".xml",
|
|
73
|
-
"text/html": ".html",
|
|
74
|
-
"text/css": ".css",
|
|
75
|
-
"text/javascript": ".js",
|
|
76
|
-
"application/javascript": ".js",
|
|
77
|
-
"text/x-python": ".py",
|
|
78
|
-
"text/x-java-source": ".java",
|
|
79
|
-
"text/x-c": ".c",
|
|
80
|
-
"text/x-yaml": ".yaml",
|
|
81
|
-
"application/x-yaml": ".yaml",
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* MIME type to category mapping for non-prefix-based types
|
|
86
|
-
*/
|
|
87
|
-
const CATEGORY_BY_MIME: Record<string, FileCategory> = {
|
|
88
|
-
// Documents
|
|
89
|
-
"application/pdf": "document",
|
|
90
|
-
"application/msword": "document",
|
|
91
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
92
|
-
"document",
|
|
93
|
-
"application/vnd.ms-excel": "document",
|
|
94
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
95
|
-
"document",
|
|
96
|
-
"application/vnd.ms-powerpoint": "document",
|
|
97
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
|
98
|
-
"document",
|
|
99
|
-
"application/rtf": "document",
|
|
100
|
-
"application/vnd.oasis.opendocument.text": "document",
|
|
101
|
-
"application/vnd.oasis.opendocument.spreadsheet": "document",
|
|
102
|
-
"text/plain": "document",
|
|
103
|
-
"text/markdown": "document",
|
|
104
|
-
"text/csv": "document",
|
|
105
|
-
// Archives
|
|
106
|
-
"application/zip": "archive",
|
|
107
|
-
"application/x-rar-compressed": "archive",
|
|
108
|
-
"application/vnd.rar": "archive",
|
|
109
|
-
"application/x-7z-compressed": "archive",
|
|
110
|
-
"application/x-tar": "archive",
|
|
111
|
-
"application/gzip": "archive",
|
|
112
|
-
"application/x-gzip": "archive",
|
|
113
|
-
"application/x-bzip2": "archive",
|
|
114
|
-
// Code
|
|
115
|
-
"application/json": "code",
|
|
116
|
-
"application/xml": "code",
|
|
117
|
-
"text/xml": "code",
|
|
118
|
-
"text/html": "code",
|
|
119
|
-
"text/css": "code",
|
|
120
|
-
"text/javascript": "code",
|
|
121
|
-
"application/javascript": "code",
|
|
122
|
-
"text/x-python": "code",
|
|
123
|
-
"text/x-java-source": "code",
|
|
124
|
-
"text/x-c": "code",
|
|
125
|
-
"text/x-yaml": "code",
|
|
126
|
-
"application/x-yaml": "code",
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Extension to category mapping
|
|
131
|
-
*/
|
|
132
|
-
const CATEGORY_BY_EXTENSION: Record<string, FileCategory> = {
|
|
133
|
-
// Images
|
|
134
|
-
".jpg": "image",
|
|
135
|
-
".jpeg": "image",
|
|
136
|
-
".png": "image",
|
|
137
|
-
".gif": "image",
|
|
138
|
-
".webp": "image",
|
|
139
|
-
".bmp": "image",
|
|
140
|
-
// Audio
|
|
141
|
-
".mp3": "audio",
|
|
142
|
-
".wav": "audio",
|
|
143
|
-
".ogg": "audio",
|
|
144
|
-
".m4a": "audio",
|
|
145
|
-
".amr": "audio",
|
|
146
|
-
// Video
|
|
147
|
-
".mp4": "video",
|
|
148
|
-
".mov": "video",
|
|
149
|
-
".avi": "video",
|
|
150
|
-
".mkv": "video",
|
|
151
|
-
".webm": "video",
|
|
152
|
-
// Documents
|
|
153
|
-
".pdf": "document",
|
|
154
|
-
".doc": "document",
|
|
155
|
-
".docx": "document",
|
|
156
|
-
".txt": "document",
|
|
157
|
-
".md": "document",
|
|
158
|
-
".rtf": "document",
|
|
159
|
-
".odt": "document",
|
|
160
|
-
".xls": "document",
|
|
161
|
-
".xlsx": "document",
|
|
162
|
-
".csv": "document",
|
|
163
|
-
".ods": "document",
|
|
164
|
-
".ppt": "document",
|
|
165
|
-
".pptx": "document",
|
|
166
|
-
// Archives
|
|
167
|
-
".zip": "archive",
|
|
168
|
-
".rar": "archive",
|
|
169
|
-
".7z": "archive",
|
|
170
|
-
".tar": "archive",
|
|
171
|
-
".gz": "archive",
|
|
172
|
-
".bz2": "archive",
|
|
173
|
-
// Code
|
|
174
|
-
".py": "code",
|
|
175
|
-
".js": "code",
|
|
176
|
-
".ts": "code",
|
|
177
|
-
".jsx": "code",
|
|
178
|
-
".tsx": "code",
|
|
179
|
-
".java": "code",
|
|
180
|
-
".cpp": "code",
|
|
181
|
-
".c": "code",
|
|
182
|
-
".go": "code",
|
|
183
|
-
".rs": "code",
|
|
184
|
-
".json": "code",
|
|
185
|
-
".xml": "code",
|
|
186
|
-
".yaml": "code",
|
|
187
|
-
".yml": "code",
|
|
188
|
-
".html": "code",
|
|
189
|
-
".css": "code",
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Extract file extension from a file name
|
|
194
|
-
* @param fileName - The file name to extract extension from
|
|
195
|
-
* @returns The extension with leading dot (e.g., ".jpg") or empty string if none
|
|
196
|
-
*/
|
|
197
|
-
function extractExtension(fileName: string): string {
|
|
198
|
-
const lastDot = fileName.lastIndexOf(".");
|
|
199
|
-
if (lastDot === -1 || lastDot === fileName.length - 1) {
|
|
200
|
-
return "";
|
|
201
|
-
}
|
|
202
|
-
return fileName.slice(lastDot).toLowerCase();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Categorize a file based on MIME type and extension
|
|
207
|
-
*
|
|
208
|
-
* Priority:
|
|
209
|
-
* 1. Check MIME type prefix (image/, audio/, video/)
|
|
210
|
-
* 2. Check exact MIME type mapping (document, archive, code)
|
|
211
|
-
* 3. Check file extension from fileName
|
|
212
|
-
* 4. Return 'other' if no match
|
|
213
|
-
*
|
|
214
|
-
* @param contentType - MIME type string
|
|
215
|
-
* @param fileName - Optional file name for extension-based fallback
|
|
216
|
-
* @returns File category
|
|
217
|
-
*/
|
|
218
|
-
export function resolveFileCategory(
|
|
219
|
-
contentType: string,
|
|
220
|
-
fileName?: string
|
|
221
|
-
): FileCategory {
|
|
222
|
-
// Normalize content type (remove parameters like charset)
|
|
223
|
-
const mimeType = contentType.split(";")[0].trim().toLowerCase();
|
|
224
|
-
|
|
225
|
-
// Check MIME type prefix first (image/, audio/, video/)
|
|
226
|
-
if (mimeType.startsWith("image/")) {
|
|
227
|
-
return "image";
|
|
228
|
-
}
|
|
229
|
-
if (mimeType.startsWith("audio/")) {
|
|
230
|
-
return "audio";
|
|
231
|
-
}
|
|
232
|
-
if (mimeType.startsWith("video/")) {
|
|
233
|
-
return "video";
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Check exact MIME type mapping (document, archive, code)
|
|
237
|
-
if (mimeType in CATEGORY_BY_MIME) {
|
|
238
|
-
return CATEGORY_BY_MIME[mimeType];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Check file extension if fileName is provided
|
|
242
|
-
if (fileName) {
|
|
243
|
-
const ext = extractExtension(fileName);
|
|
244
|
-
if (ext && ext in CATEGORY_BY_EXTENSION) {
|
|
245
|
-
return CATEGORY_BY_EXTENSION[ext];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return "other";
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Resolve file extension from MIME type or fileName
|
|
254
|
-
*
|
|
255
|
-
* Priority:
|
|
256
|
-
* 1. fileName extension (if provided and has extension)
|
|
257
|
-
* 2. MIME type mapping
|
|
258
|
-
* 3. ".bin" default
|
|
259
|
-
*
|
|
260
|
-
* @param contentType - MIME type string
|
|
261
|
-
* @param fileName - Optional file name to extract extension from (takes precedence)
|
|
262
|
-
* @returns Extension with leading dot (e.g., ".jpg") or ".bin" if unknown
|
|
263
|
-
*/
|
|
264
|
-
export function resolveExtension(
|
|
265
|
-
contentType: string,
|
|
266
|
-
fileName?: string
|
|
267
|
-
): string {
|
|
268
|
-
// Priority 1: fileName extension
|
|
269
|
-
if (fileName) {
|
|
270
|
-
const ext = extractExtension(fileName);
|
|
271
|
-
if (ext) {
|
|
272
|
-
return ext;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Priority 2: MIME type mapping
|
|
277
|
-
const mimeType = contentType.split(";")[0].trim().toLowerCase();
|
|
278
|
-
if (mimeType in MIME_TO_EXTENSION) {
|
|
279
|
-
return MIME_TO_EXTENSION[mimeType];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Priority 3: Default
|
|
283
|
-
return ".bin";
|
|
284
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* File utilities for categorizing and resolving file extensions
|
|
3
|
+
* @module @openclaw-china/shared/file
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* File category for processing strategy
|
|
8
|
+
*/
|
|
9
|
+
export type FileCategory =
|
|
10
|
+
| "image"
|
|
11
|
+
| "audio"
|
|
12
|
+
| "video"
|
|
13
|
+
| "document"
|
|
14
|
+
| "archive"
|
|
15
|
+
| "code"
|
|
16
|
+
| "other";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* MIME type to extension mapping
|
|
20
|
+
*/
|
|
21
|
+
const MIME_TO_EXTENSION: Record<string, string> = {
|
|
22
|
+
// Images
|
|
23
|
+
"image/jpeg": ".jpg",
|
|
24
|
+
"image/png": ".png",
|
|
25
|
+
"image/gif": ".gif",
|
|
26
|
+
"image/webp": ".webp",
|
|
27
|
+
"image/bmp": ".bmp",
|
|
28
|
+
|
|
29
|
+
// Audio
|
|
30
|
+
"audio/mpeg": ".mp3",
|
|
31
|
+
"audio/wav": ".wav",
|
|
32
|
+
"audio/ogg": ".ogg",
|
|
33
|
+
"audio/amr": ".amr",
|
|
34
|
+
"audio/x-m4a": ".m4a",
|
|
35
|
+
|
|
36
|
+
// Video
|
|
37
|
+
"video/mp4": ".mp4",
|
|
38
|
+
"video/quicktime": ".mov",
|
|
39
|
+
"video/x-msvideo": ".avi",
|
|
40
|
+
"video/webm": ".webm",
|
|
41
|
+
|
|
42
|
+
// Documents
|
|
43
|
+
"application/pdf": ".pdf",
|
|
44
|
+
"application/msword": ".doc",
|
|
45
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
46
|
+
".docx",
|
|
47
|
+
"application/vnd.ms-excel": ".xls",
|
|
48
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
49
|
+
"application/vnd.ms-powerpoint": ".ppt",
|
|
50
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
|
51
|
+
".pptx",
|
|
52
|
+
"application/rtf": ".rtf",
|
|
53
|
+
"application/vnd.oasis.opendocument.text": ".odt",
|
|
54
|
+
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
|
55
|
+
"text/plain": ".txt",
|
|
56
|
+
"text/markdown": ".md",
|
|
57
|
+
"text/csv": ".csv",
|
|
58
|
+
|
|
59
|
+
// Archives
|
|
60
|
+
"application/zip": ".zip",
|
|
61
|
+
"application/x-rar-compressed": ".rar",
|
|
62
|
+
"application/vnd.rar": ".rar",
|
|
63
|
+
"application/x-7z-compressed": ".7z",
|
|
64
|
+
"application/x-tar": ".tar",
|
|
65
|
+
"application/gzip": ".gz",
|
|
66
|
+
"application/x-gzip": ".gz",
|
|
67
|
+
"application/x-bzip2": ".bz2",
|
|
68
|
+
|
|
69
|
+
// Code
|
|
70
|
+
"application/json": ".json",
|
|
71
|
+
"application/xml": ".xml",
|
|
72
|
+
"text/xml": ".xml",
|
|
73
|
+
"text/html": ".html",
|
|
74
|
+
"text/css": ".css",
|
|
75
|
+
"text/javascript": ".js",
|
|
76
|
+
"application/javascript": ".js",
|
|
77
|
+
"text/x-python": ".py",
|
|
78
|
+
"text/x-java-source": ".java",
|
|
79
|
+
"text/x-c": ".c",
|
|
80
|
+
"text/x-yaml": ".yaml",
|
|
81
|
+
"application/x-yaml": ".yaml",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* MIME type to category mapping for non-prefix-based types
|
|
86
|
+
*/
|
|
87
|
+
const CATEGORY_BY_MIME: Record<string, FileCategory> = {
|
|
88
|
+
// Documents
|
|
89
|
+
"application/pdf": "document",
|
|
90
|
+
"application/msword": "document",
|
|
91
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
92
|
+
"document",
|
|
93
|
+
"application/vnd.ms-excel": "document",
|
|
94
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
95
|
+
"document",
|
|
96
|
+
"application/vnd.ms-powerpoint": "document",
|
|
97
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
|
98
|
+
"document",
|
|
99
|
+
"application/rtf": "document",
|
|
100
|
+
"application/vnd.oasis.opendocument.text": "document",
|
|
101
|
+
"application/vnd.oasis.opendocument.spreadsheet": "document",
|
|
102
|
+
"text/plain": "document",
|
|
103
|
+
"text/markdown": "document",
|
|
104
|
+
"text/csv": "document",
|
|
105
|
+
// Archives
|
|
106
|
+
"application/zip": "archive",
|
|
107
|
+
"application/x-rar-compressed": "archive",
|
|
108
|
+
"application/vnd.rar": "archive",
|
|
109
|
+
"application/x-7z-compressed": "archive",
|
|
110
|
+
"application/x-tar": "archive",
|
|
111
|
+
"application/gzip": "archive",
|
|
112
|
+
"application/x-gzip": "archive",
|
|
113
|
+
"application/x-bzip2": "archive",
|
|
114
|
+
// Code
|
|
115
|
+
"application/json": "code",
|
|
116
|
+
"application/xml": "code",
|
|
117
|
+
"text/xml": "code",
|
|
118
|
+
"text/html": "code",
|
|
119
|
+
"text/css": "code",
|
|
120
|
+
"text/javascript": "code",
|
|
121
|
+
"application/javascript": "code",
|
|
122
|
+
"text/x-python": "code",
|
|
123
|
+
"text/x-java-source": "code",
|
|
124
|
+
"text/x-c": "code",
|
|
125
|
+
"text/x-yaml": "code",
|
|
126
|
+
"application/x-yaml": "code",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extension to category mapping
|
|
131
|
+
*/
|
|
132
|
+
const CATEGORY_BY_EXTENSION: Record<string, FileCategory> = {
|
|
133
|
+
// Images
|
|
134
|
+
".jpg": "image",
|
|
135
|
+
".jpeg": "image",
|
|
136
|
+
".png": "image",
|
|
137
|
+
".gif": "image",
|
|
138
|
+
".webp": "image",
|
|
139
|
+
".bmp": "image",
|
|
140
|
+
// Audio
|
|
141
|
+
".mp3": "audio",
|
|
142
|
+
".wav": "audio",
|
|
143
|
+
".ogg": "audio",
|
|
144
|
+
".m4a": "audio",
|
|
145
|
+
".amr": "audio",
|
|
146
|
+
// Video
|
|
147
|
+
".mp4": "video",
|
|
148
|
+
".mov": "video",
|
|
149
|
+
".avi": "video",
|
|
150
|
+
".mkv": "video",
|
|
151
|
+
".webm": "video",
|
|
152
|
+
// Documents
|
|
153
|
+
".pdf": "document",
|
|
154
|
+
".doc": "document",
|
|
155
|
+
".docx": "document",
|
|
156
|
+
".txt": "document",
|
|
157
|
+
".md": "document",
|
|
158
|
+
".rtf": "document",
|
|
159
|
+
".odt": "document",
|
|
160
|
+
".xls": "document",
|
|
161
|
+
".xlsx": "document",
|
|
162
|
+
".csv": "document",
|
|
163
|
+
".ods": "document",
|
|
164
|
+
".ppt": "document",
|
|
165
|
+
".pptx": "document",
|
|
166
|
+
// Archives
|
|
167
|
+
".zip": "archive",
|
|
168
|
+
".rar": "archive",
|
|
169
|
+
".7z": "archive",
|
|
170
|
+
".tar": "archive",
|
|
171
|
+
".gz": "archive",
|
|
172
|
+
".bz2": "archive",
|
|
173
|
+
// Code
|
|
174
|
+
".py": "code",
|
|
175
|
+
".js": "code",
|
|
176
|
+
".ts": "code",
|
|
177
|
+
".jsx": "code",
|
|
178
|
+
".tsx": "code",
|
|
179
|
+
".java": "code",
|
|
180
|
+
".cpp": "code",
|
|
181
|
+
".c": "code",
|
|
182
|
+
".go": "code",
|
|
183
|
+
".rs": "code",
|
|
184
|
+
".json": "code",
|
|
185
|
+
".xml": "code",
|
|
186
|
+
".yaml": "code",
|
|
187
|
+
".yml": "code",
|
|
188
|
+
".html": "code",
|
|
189
|
+
".css": "code",
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Extract file extension from a file name
|
|
194
|
+
* @param fileName - The file name to extract extension from
|
|
195
|
+
* @returns The extension with leading dot (e.g., ".jpg") or empty string if none
|
|
196
|
+
*/
|
|
197
|
+
function extractExtension(fileName: string): string {
|
|
198
|
+
const lastDot = fileName.lastIndexOf(".");
|
|
199
|
+
if (lastDot === -1 || lastDot === fileName.length - 1) {
|
|
200
|
+
return "";
|
|
201
|
+
}
|
|
202
|
+
return fileName.slice(lastDot).toLowerCase();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Categorize a file based on MIME type and extension
|
|
207
|
+
*
|
|
208
|
+
* Priority:
|
|
209
|
+
* 1. Check MIME type prefix (image/, audio/, video/)
|
|
210
|
+
* 2. Check exact MIME type mapping (document, archive, code)
|
|
211
|
+
* 3. Check file extension from fileName
|
|
212
|
+
* 4. Return 'other' if no match
|
|
213
|
+
*
|
|
214
|
+
* @param contentType - MIME type string
|
|
215
|
+
* @param fileName - Optional file name for extension-based fallback
|
|
216
|
+
* @returns File category
|
|
217
|
+
*/
|
|
218
|
+
export function resolveFileCategory(
|
|
219
|
+
contentType: string,
|
|
220
|
+
fileName?: string
|
|
221
|
+
): FileCategory {
|
|
222
|
+
// Normalize content type (remove parameters like charset)
|
|
223
|
+
const mimeType = contentType.split(";")[0].trim().toLowerCase();
|
|
224
|
+
|
|
225
|
+
// Check MIME type prefix first (image/, audio/, video/)
|
|
226
|
+
if (mimeType.startsWith("image/")) {
|
|
227
|
+
return "image";
|
|
228
|
+
}
|
|
229
|
+
if (mimeType.startsWith("audio/")) {
|
|
230
|
+
return "audio";
|
|
231
|
+
}
|
|
232
|
+
if (mimeType.startsWith("video/")) {
|
|
233
|
+
return "video";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check exact MIME type mapping (document, archive, code)
|
|
237
|
+
if (mimeType in CATEGORY_BY_MIME) {
|
|
238
|
+
return CATEGORY_BY_MIME[mimeType];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check file extension if fileName is provided
|
|
242
|
+
if (fileName) {
|
|
243
|
+
const ext = extractExtension(fileName);
|
|
244
|
+
if (ext && ext in CATEGORY_BY_EXTENSION) {
|
|
245
|
+
return CATEGORY_BY_EXTENSION[ext];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return "other";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Resolve file extension from MIME type or fileName
|
|
254
|
+
*
|
|
255
|
+
* Priority:
|
|
256
|
+
* 1. fileName extension (if provided and has extension)
|
|
257
|
+
* 2. MIME type mapping
|
|
258
|
+
* 3. ".bin" default
|
|
259
|
+
*
|
|
260
|
+
* @param contentType - MIME type string
|
|
261
|
+
* @param fileName - Optional file name to extract extension from (takes precedence)
|
|
262
|
+
* @returns Extension with leading dot (e.g., ".jpg") or ".bin" if unknown
|
|
263
|
+
*/
|
|
264
|
+
export function resolveExtension(
|
|
265
|
+
contentType: string,
|
|
266
|
+
fileName?: string
|
|
267
|
+
): string {
|
|
268
|
+
// Priority 1: fileName extension
|
|
269
|
+
if (fileName) {
|
|
270
|
+
const ext = extractExtension(fileName);
|
|
271
|
+
if (ext) {
|
|
272
|
+
return ext;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Priority 2: MIME type mapping
|
|
277
|
+
const mimeType = contentType.split(";")[0].trim().toLowerCase();
|
|
278
|
+
if (mimeType in MIME_TO_EXTENSION) {
|
|
279
|
+
return MIME_TO_EXTENSION[mimeType];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Priority 3: Default
|
|
283
|
+
return ".bin";
|
|
284
|
+
}
|
package/src/file/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File utilities module
|
|
3
|
-
* @module @openclaw-china/shared/file
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
type FileCategory,
|
|
8
|
-
resolveFileCategory,
|
|
9
|
-
resolveExtension,
|
|
10
|
-
} from "./file-utils.js";
|
|
1
|
+
/**
|
|
2
|
+
* File utilities module
|
|
3
|
+
* @module @openclaw-china/shared/file
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
type FileCategory,
|
|
8
|
+
resolveFileCategory,
|
|
9
|
+
resolveExtension,
|
|
10
|
+
} from "./file-utils.js";
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,6 @@ export * from "./http/index.js";
|
|
|
7
7
|
export * from "./types/common.js";
|
|
8
8
|
export * from "./file/index.js";
|
|
9
9
|
export * from "./media/index.js";
|
|
10
|
-
export * from "./cron/index.js";
|
|
11
|
-
export * from "./asr/index.js";
|
|
12
|
-
export * from "./cli/index.js";
|
|
10
|
+
export * from "./cron/index.js";
|
|
11
|
+
export * from "./asr/index.js";
|
|
12
|
+
export * from "./cli/index.js";
|
package/src/logger/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./logger.js";
|
|
1
|
+
export * from "./logger.js";
|
package/src/logger/logger.ts
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 通用日志工具
|
|
3
|
-
*
|
|
4
|
-
* 提供分级日志功能:
|
|
5
|
-
* - info: 关键业务日志(默认显示)
|
|
6
|
-
* - debug: 调试日志(带 [DEBUG] 标记)
|
|
7
|
-
* - error: 错误日志
|
|
8
|
-
* - warn: 警告日志
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
12
|
-
|
|
13
|
-
export interface Logger {
|
|
14
|
-
debug: (msg: string) => void;
|
|
15
|
-
info: (msg: string) => void;
|
|
16
|
-
warn: (msg: string) => void;
|
|
17
|
-
error: (msg: string) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface LoggerOptions {
|
|
21
|
-
log?: (msg: string) => void;
|
|
22
|
-
error?: (msg: string) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 创建带前缀的日志器
|
|
27
|
-
*
|
|
28
|
-
* @param prefix 日志前缀(如 "dingtalk", "feishu")
|
|
29
|
-
* @param opts 可选的日志输出函数
|
|
30
|
-
* @returns Logger 实例
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* const logger = createLogger("dingtalk");
|
|
35
|
-
* logger.debug("connecting..."); // [dingtalk] [DEBUG] connecting...
|
|
36
|
-
* logger.info("connected"); // [dingtalk] connected
|
|
37
|
-
* logger.warn("slow response"); // [dingtalk] [WARN] slow response
|
|
38
|
-
* logger.error("failed"); // [dingtalk] [ERROR] failed
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
export function createLogger(prefix: string, opts?: LoggerOptions): Logger {
|
|
42
|
-
const logFn = opts?.log ?? console.log;
|
|
43
|
-
const errorFn = opts?.error ?? console.error;
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
debug: (msg: string) => logFn(`[${prefix}] [DEBUG] ${msg}`),
|
|
47
|
-
info: (msg: string) => logFn(`[${prefix}] ${msg}`),
|
|
48
|
-
warn: (msg: string) => logFn(`[${prefix}] [WARN] ${msg}`),
|
|
49
|
-
error: (msg: string) => errorFn(`[${prefix}] [ERROR] ${msg}`),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 通用日志工具
|
|
3
|
+
*
|
|
4
|
+
* 提供分级日志功能:
|
|
5
|
+
* - info: 关键业务日志(默认显示)
|
|
6
|
+
* - debug: 调试日志(带 [DEBUG] 标记)
|
|
7
|
+
* - error: 错误日志
|
|
8
|
+
* - warn: 警告日志
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
12
|
+
|
|
13
|
+
export interface Logger {
|
|
14
|
+
debug: (msg: string) => void;
|
|
15
|
+
info: (msg: string) => void;
|
|
16
|
+
warn: (msg: string) => void;
|
|
17
|
+
error: (msg: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoggerOptions {
|
|
21
|
+
log?: (msg: string) => void;
|
|
22
|
+
error?: (msg: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 创建带前缀的日志器
|
|
27
|
+
*
|
|
28
|
+
* @param prefix 日志前缀(如 "dingtalk", "feishu")
|
|
29
|
+
* @param opts 可选的日志输出函数
|
|
30
|
+
* @returns Logger 实例
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const logger = createLogger("dingtalk");
|
|
35
|
+
* logger.debug("connecting..."); // [dingtalk] [DEBUG] connecting...
|
|
36
|
+
* logger.info("connected"); // [dingtalk] connected
|
|
37
|
+
* logger.warn("slow response"); // [dingtalk] [WARN] slow response
|
|
38
|
+
* logger.error("failed"); // [dingtalk] [ERROR] failed
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function createLogger(prefix: string, opts?: LoggerOptions): Logger {
|
|
42
|
+
const logFn = opts?.log ?? console.log;
|
|
43
|
+
const errorFn = opts?.error ?? console.error;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
debug: (msg: string) => logFn(`[${prefix}] [DEBUG] ${msg}`),
|
|
47
|
+
info: (msg: string) => logFn(`[${prefix}] ${msg}`),
|
|
48
|
+
warn: (msg: string) => logFn(`[${prefix}] [WARN] ${msg}`),
|
|
49
|
+
error: (msg: string) => errorFn(`[${prefix}] [ERROR] ${msg}`),
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/media/index.ts
CHANGED
|
@@ -35,31 +35,31 @@ export {
|
|
|
35
35
|
} from "./media-parser.js";
|
|
36
36
|
|
|
37
37
|
// 媒体 IO
|
|
38
|
-
export {
|
|
39
|
-
// 类型
|
|
40
|
-
type MediaReadResult,
|
|
41
|
-
type MediaReadOptions,
|
|
42
|
-
type DownloadToTempFileResult,
|
|
43
|
-
type DownloadToTempFileOptions,
|
|
44
|
-
type FinalizeInboundMediaOptions,
|
|
45
|
-
type PruneInboundMediaDirOptions,
|
|
46
|
-
type PathSecurityOptions,
|
|
47
|
-
// 错误类
|
|
48
|
-
FileSizeLimitError,
|
|
49
|
-
MediaTimeoutError,
|
|
50
|
-
PathSecurityError,
|
|
38
|
+
export {
|
|
39
|
+
// 类型
|
|
40
|
+
type MediaReadResult,
|
|
41
|
+
type MediaReadOptions,
|
|
42
|
+
type DownloadToTempFileResult,
|
|
43
|
+
type DownloadToTempFileOptions,
|
|
44
|
+
type FinalizeInboundMediaOptions,
|
|
45
|
+
type PruneInboundMediaDirOptions,
|
|
46
|
+
type PathSecurityOptions,
|
|
47
|
+
// 错误类
|
|
48
|
+
FileSizeLimitError,
|
|
49
|
+
MediaTimeoutError,
|
|
50
|
+
PathSecurityError,
|
|
51
51
|
// 路径安全
|
|
52
52
|
validatePathSecurity,
|
|
53
53
|
getDefaultAllowedPrefixes,
|
|
54
54
|
// MIME 类型
|
|
55
55
|
getMimeType,
|
|
56
56
|
// 媒体读取函数
|
|
57
|
-
fetchMediaFromUrl,
|
|
58
|
-
readMediaFromLocal,
|
|
59
|
-
readMedia,
|
|
60
|
-
readMediaBatch,
|
|
61
|
-
downloadToTempFile,
|
|
62
|
-
finalizeInboundMediaFile,
|
|
63
|
-
pruneInboundMediaDir,
|
|
64
|
-
cleanupFileSafe,
|
|
65
|
-
} from "./media-io.js";
|
|
57
|
+
fetchMediaFromUrl,
|
|
58
|
+
readMediaFromLocal,
|
|
59
|
+
readMedia,
|
|
60
|
+
readMediaBatch,
|
|
61
|
+
downloadToTempFile,
|
|
62
|
+
finalizeInboundMediaFile,
|
|
63
|
+
pruneInboundMediaDir,
|
|
64
|
+
cleanupFileSafe,
|
|
65
|
+
} from "./media-io.js";
|
package/vitest.config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
include: ["src/**/*.test.ts"],
|
|
6
|
-
globals: false,
|
|
7
|
-
},
|
|
8
|
-
});
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
include: ["src/**/*.test.ts"],
|
|
6
|
+
globals: false,
|
|
7
|
+
},
|
|
8
|
+
});
|