@openclaw-china/shared 2026.3.22 → 2026.4.23
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/china-setup.ts +68 -59
- package/src/cli/index.ts +2 -2
- package/src/cli/install-hint.ts +16 -16
- package/src/cron/index.ts +17 -17
- 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/china-setup.ts
CHANGED
|
@@ -51,7 +51,14 @@ type ConfigRoot = {
|
|
|
51
51
|
[key: string]: unknown;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
export type ChannelId =
|
|
54
|
+
export type ChannelId =
|
|
55
|
+
| "dingtalk"
|
|
56
|
+
| "feishu-china"
|
|
57
|
+
| "wecom"
|
|
58
|
+
| "wecom-app"
|
|
59
|
+
| "wecom-kf"
|
|
60
|
+
| "qqbot-china"
|
|
61
|
+
| "wechat-mp";
|
|
55
62
|
|
|
56
63
|
export type RegisterChinaSetupCliOptions = {
|
|
57
64
|
channels?: readonly ChannelId[];
|
|
@@ -68,37 +75,38 @@ const GUIDES_BASE = "https://github.com/BytePioneer-AI/openclaw-china/tree/main/
|
|
|
68
75
|
const OPENCLAW_HOME = join(homedir(), ".openclaw");
|
|
69
76
|
const DEFAULT_PLUGIN_PATH = join(OPENCLAW_HOME, "extensions");
|
|
70
77
|
const LEGACY_PLUGIN_PATH = join(OPENCLAW_HOME, "plugins");
|
|
71
|
-
const CONFIG_FILE_PATH = join(OPENCLAW_HOME, "openclaw.json");
|
|
72
|
-
const ANSI_RESET = "\u001b[0m";
|
|
73
|
-
const ANSI_LINK = "\u001b[1;4;96m";
|
|
74
|
-
const ANSI_BORDER = "\u001b[92m";
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
"wecom
|
|
80
|
-
"wecom-
|
|
78
|
+
const CONFIG_FILE_PATH = join(OPENCLAW_HOME, "openclaw.json");
|
|
79
|
+
const ANSI_RESET = "\u001b[0m";
|
|
80
|
+
const ANSI_LINK = "\u001b[1;4;96m";
|
|
81
|
+
const ANSI_BORDER = "\u001b[92m";
|
|
82
|
+
const QQBOT_CHANNEL_ID = "qqbot-china" as const;
|
|
83
|
+
const CHANNEL_ORDER: readonly ChannelId[] = [
|
|
84
|
+
"dingtalk",
|
|
85
|
+
QQBOT_CHANNEL_ID,
|
|
86
|
+
"wecom",
|
|
87
|
+
"wecom-app",
|
|
88
|
+
"wecom-kf",
|
|
81
89
|
"wechat-mp",
|
|
82
90
|
"feishu-china",
|
|
83
91
|
];
|
|
84
92
|
const CHANNEL_DISPLAY_LABELS: Record<ChannelId, string> = {
|
|
85
93
|
dingtalk: "DingTalk(钉钉)",
|
|
86
|
-
"feishu-china": "Feishu(飞书)",
|
|
87
|
-
wecom: "WeCom(企业微信-智能机器人)",
|
|
88
|
-
"wecom-app": "WeCom App(自建应用-可接入微信)",
|
|
89
|
-
"wecom-kf": "WeCom KF(微信客服)",
|
|
90
|
-
"wechat-mp": "WeChat MP(微信公众号)",
|
|
91
|
-
qqbot: "QQBot(QQ 机器人)",
|
|
92
|
-
};
|
|
93
|
-
const CHANNEL_GUIDE_LINKS: Record<ChannelId, string> = {
|
|
94
|
+
"feishu-china": "Feishu(飞书)",
|
|
95
|
+
wecom: "WeCom(企业微信-智能机器人)",
|
|
96
|
+
"wecom-app": "WeCom App(自建应用-可接入微信)",
|
|
97
|
+
"wecom-kf": "WeCom KF(微信客服)",
|
|
98
|
+
"wechat-mp": "WeChat MP(微信公众号)",
|
|
99
|
+
"qqbot-china": "QQBot(QQ 机器人)",
|
|
100
|
+
};
|
|
101
|
+
const CHANNEL_GUIDE_LINKS: Record<ChannelId, string> = {
|
|
94
102
|
dingtalk: `${GUIDES_BASE}/dingtalk/configuration.md`,
|
|
95
103
|
"feishu-china": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/README.md",
|
|
96
104
|
wecom: `${GUIDES_BASE}/wecom/configuration.md`,
|
|
97
|
-
"wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
|
|
98
|
-
"wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
|
|
99
|
-
"wechat-mp": `${GUIDES_BASE}/wechat-mp/configuration.md`,
|
|
100
|
-
qqbot: `${GUIDES_BASE}/qqbot/configuration.md`,
|
|
101
|
-
};
|
|
105
|
+
"wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
|
|
106
|
+
"wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
|
|
107
|
+
"wechat-mp": `${GUIDES_BASE}/wechat-mp/configuration.md`,
|
|
108
|
+
"qqbot-china": `${GUIDES_BASE}/qqbot/configuration.md`,
|
|
109
|
+
};
|
|
102
110
|
const CHINA_CLI_STATE_KEY = Symbol.for("@openclaw-china/china-cli-state");
|
|
103
111
|
|
|
104
112
|
type ChinaCliState = {
|
|
@@ -272,11 +280,11 @@ function cloneConfig(cfg: ConfigRoot): ConfigRoot {
|
|
|
272
280
|
}
|
|
273
281
|
}
|
|
274
282
|
|
|
275
|
-
function getChannelConfig(cfg: ConfigRoot, channelId: ChannelId): ConfigRecord {
|
|
276
|
-
const channels = isRecord(cfg.channels) ? cfg.channels : {};
|
|
277
|
-
const existing = channels[channelId];
|
|
278
|
-
return isRecord(existing) ? existing : {};
|
|
279
|
-
}
|
|
283
|
+
function getChannelConfig(cfg: ConfigRoot, channelId: ChannelId): ConfigRecord {
|
|
284
|
+
const channels = isRecord(cfg.channels) ? cfg.channels : {};
|
|
285
|
+
const existing = channels[channelId];
|
|
286
|
+
return isRecord(existing) ? existing : {};
|
|
287
|
+
}
|
|
280
288
|
|
|
281
289
|
function getGatewayAuthToken(cfg: ConfigRoot): string | undefined {
|
|
282
290
|
if (!isRecord(cfg.gateway)) {
|
|
@@ -328,15 +336,15 @@ function hasWecomWsCredentialPair(channelCfg: ConfigRecord): boolean {
|
|
|
328
336
|
return hasCredentialPair(channelCfg, "botId", "secret");
|
|
329
337
|
}
|
|
330
338
|
|
|
331
|
-
function isChannelConfigured(cfg: ConfigRoot, channelId: ChannelId): boolean {
|
|
332
|
-
const channelCfg = getChannelConfig(cfg, channelId);
|
|
333
|
-
switch (channelId) {
|
|
339
|
+
function isChannelConfigured(cfg: ConfigRoot, channelId: ChannelId): boolean {
|
|
340
|
+
const channelCfg = getChannelConfig(cfg, channelId);
|
|
341
|
+
switch (channelId) {
|
|
334
342
|
case "dingtalk":
|
|
335
343
|
return hasNonEmptyString(channelCfg.clientId) && hasNonEmptyString(channelCfg.clientSecret);
|
|
336
344
|
case "feishu-china":
|
|
337
345
|
return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.appSecret);
|
|
338
|
-
case "qqbot":
|
|
339
|
-
return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.clientSecret);
|
|
346
|
+
case "qqbot-china":
|
|
347
|
+
return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.clientSecret);
|
|
340
348
|
case "wecom":
|
|
341
349
|
return hasWecomWsCredentialPair(channelCfg);
|
|
342
350
|
case "wecom-app":
|
|
@@ -359,21 +367,22 @@ function withConfiguredSuffix(cfg: ConfigRoot, channelId: ChannelId): string {
|
|
|
359
367
|
return isChannelConfigured(cfg, channelId) ? `${base}(已配置)` : base;
|
|
360
368
|
}
|
|
361
369
|
|
|
362
|
-
function mergeChannelConfig(
|
|
363
|
-
cfg: ConfigRoot,
|
|
364
|
-
channelId: ChannelId,
|
|
365
|
-
patch: ConfigRecord
|
|
366
|
-
): ConfigRoot {
|
|
367
|
-
const channels = isRecord(cfg.channels) ? { ...cfg.channels } : {};
|
|
368
|
-
const existing = getChannelConfig(cfg, channelId);
|
|
369
|
-
|
|
370
|
-
...existing,
|
|
371
|
-
...patch,
|
|
372
|
-
enabled: true,
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
370
|
+
function mergeChannelConfig(
|
|
371
|
+
cfg: ConfigRoot,
|
|
372
|
+
channelId: ChannelId,
|
|
373
|
+
patch: ConfigRecord
|
|
374
|
+
): ConfigRoot {
|
|
375
|
+
const channels = isRecord(cfg.channels) ? { ...cfg.channels } : {};
|
|
376
|
+
const existing = getChannelConfig(cfg, channelId);
|
|
377
|
+
const nextChannelConfig = {
|
|
378
|
+
...existing,
|
|
379
|
+
...patch,
|
|
380
|
+
enabled: true,
|
|
381
|
+
};
|
|
382
|
+
channels[channelId] = nextChannelConfig;
|
|
383
|
+
return {
|
|
384
|
+
...cfg,
|
|
385
|
+
channels,
|
|
377
386
|
};
|
|
378
387
|
}
|
|
379
388
|
|
|
@@ -795,10 +804,10 @@ async function configureWechatMp(prompter: SetupPrompter, cfg: ConfigRoot): Prom
|
|
|
795
804
|
});
|
|
796
805
|
}
|
|
797
806
|
|
|
798
|
-
async function configureQQBot(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
799
|
-
section("配置 QQBot(QQ 机器人)");
|
|
800
|
-
showGuideLink(
|
|
801
|
-
const existing = getChannelConfig(cfg,
|
|
807
|
+
async function configureQQBot(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
808
|
+
section("配置 QQBot(QQ 机器人)");
|
|
809
|
+
showGuideLink(QQBOT_CHANNEL_ID);
|
|
810
|
+
const existing = getChannelConfig(cfg, QQBOT_CHANNEL_ID);
|
|
802
811
|
const existingAsr = isRecord(existing.asr) ? existing.asr : {};
|
|
803
812
|
|
|
804
813
|
const appId = await prompter.askText({
|
|
@@ -838,10 +847,10 @@ async function configureQQBot(prompter: SetupPrompter, cfg: ConfigRoot): Promise
|
|
|
838
847
|
});
|
|
839
848
|
}
|
|
840
849
|
|
|
841
|
-
return mergeChannelConfig(cfg,
|
|
842
|
-
appId,
|
|
843
|
-
clientSecret,
|
|
844
|
-
asr,
|
|
850
|
+
return mergeChannelConfig(cfg, QQBOT_CHANNEL_ID, {
|
|
851
|
+
appId,
|
|
852
|
+
clientSecret,
|
|
853
|
+
asr,
|
|
845
854
|
});
|
|
846
855
|
}
|
|
847
856
|
|
|
@@ -863,8 +872,8 @@ async function configureSingleChannel(
|
|
|
863
872
|
return configureWecomKf(prompter, cfg);
|
|
864
873
|
case "wechat-mp":
|
|
865
874
|
return configureWechatMp(prompter, cfg);
|
|
866
|
-
case "qqbot":
|
|
867
|
-
return configureQQBot(prompter, cfg);
|
|
875
|
+
case "qqbot-china":
|
|
876
|
+
return configureQQBot(prompter, cfg);
|
|
868
877
|
default:
|
|
869
878
|
return cfg;
|
|
870
879
|
}
|
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/cli/install-hint.ts
CHANGED
|
@@ -18,16 +18,16 @@ const ANSI_RESET = "\u001b[0m";
|
|
|
18
18
|
const ANSI_BOLD = "\u001b[1m";
|
|
19
19
|
const ANSI_LINK = "\u001b[1;4;96m";
|
|
20
20
|
const ANSI_BORDER = "\u001b[92m";
|
|
21
|
-
const SUPPORTED_CHANNELS: readonly ChannelId[] = [
|
|
22
|
-
"dingtalk",
|
|
23
|
-
"feishu-china",
|
|
24
|
-
"wecom",
|
|
25
|
-
"wecom-app",
|
|
26
|
-
"wecom-kf",
|
|
27
|
-
"wechat-mp",
|
|
28
|
-
"qqbot",
|
|
29
|
-
];
|
|
30
|
-
const CHINA_INSTALL_HINT_SHOWN_KEY = Symbol.for("@openclaw-china/china-install-hint-shown");
|
|
21
|
+
const SUPPORTED_CHANNELS: readonly ChannelId[] = [
|
|
22
|
+
"dingtalk",
|
|
23
|
+
"feishu-china",
|
|
24
|
+
"wecom",
|
|
25
|
+
"wecom-app",
|
|
26
|
+
"wecom-kf",
|
|
27
|
+
"wechat-mp",
|
|
28
|
+
"qqbot-china",
|
|
29
|
+
];
|
|
30
|
+
const CHINA_INSTALL_HINT_SHOWN_KEY = Symbol.for("@openclaw-china/china-install-hint-shown");
|
|
31
31
|
|
|
32
32
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
33
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -42,12 +42,12 @@ function hasAnyEnabledChinaChannel(config: unknown): boolean {
|
|
|
42
42
|
if (!isRecord(channels)) {
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
return SUPPORTED_CHANNELS.some((channelId) => {
|
|
47
|
-
const channelConfig = channels[channelId];
|
|
48
|
-
return isRecord(channelConfig) && channelConfig.enabled === true;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
45
|
+
|
|
46
|
+
return SUPPORTED_CHANNELS.some((channelId) => {
|
|
47
|
+
const channelConfig = channels[channelId];
|
|
48
|
+
return isRecord(channelConfig) && channelConfig.enabled === true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
51
|
|
|
52
52
|
function hasShownInstallHint(): boolean {
|
|
53
53
|
const root = globalThis as Record<PropertyKey, unknown>;
|
package/src/cron/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
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.
|
|
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
16
|
- Use top-level delivery with announce mode:
|
|
17
|
-
delivery.mode="announce"
|
|
18
|
-
delivery.channel=<OriginatingChannel> (example: "qqbot")
|
|
17
|
+
delivery.mode="announce"
|
|
18
|
+
delivery.channel=<OriginatingChannel> (example: "qqbot")
|
|
19
19
|
delivery.to=<OriginatingTo> (examples: "user:<openid>" / "group:<group_openid>")
|
|
20
20
|
delivery.accountId=<AccountId> when available
|
|
21
21
|
- Never set delivery.channel="last" for multi-channel environments.
|
|
@@ -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
|
+
});
|