@openclaw-china/shared 0.1.18 → 0.1.20

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 CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@openclaw-china/shared",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
7
7
  },
8
8
  "scripts": {
9
9
  "build": "tsc",
10
- "dev": "tsc --watch"
10
+ "dev": "tsc --watch",
11
+ "test": "vitest --run"
11
12
  },
12
13
  "devDependencies": {
13
- "typescript": "^5.7.0"
14
+ "typescript": "^5.7.0",
15
+ "vitest": "^2.1.0"
14
16
  },
15
17
  "private": false
16
18
  }
@@ -0,0 +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
+ });
@@ -0,0 +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
+ }
@@ -0,0 +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";
package/src/index.ts CHANGED
@@ -5,3 +5,4 @@ export * from "./logger/index.js";
5
5
  export * from "./policy/index.js";
6
6
  export * from "./http/index.js";
7
7
  export * from "./types/common.js";
8
+ export * from "./file/index.js";
@@ -1 +1 @@
1
- export * from "./logger.js";
1
+ export * from "./logger.js";
@@ -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
+ }
@@ -0,0 +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
+ });