chatccc 0.2.198 → 0.2.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +1 -1
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/cursor-adapter.test.ts +66 -7
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +47 -5
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/orchestrator.test.ts +27 -2
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/cursor-adapter.ts +39 -6
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-stop-stuck.ts +129 -129
- package/src/cards.ts +1 -1
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +41 -18
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +4 -4
- package/src/privacy.ts +118 -118
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
|
@@ -11,52 +11,52 @@ vi.mock("../feishu-platform.ts", () => ({
|
|
|
11
11
|
getMergeForwardMessages: (...args: unknown[]) => mockGetMergeForwardMessages(...args),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
-
import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
|
|
15
|
-
|
|
16
|
-
describe("formatMessageContent mixed post images", () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("includes downloaded images from mixed post messages", async () => {
|
|
23
|
-
mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
|
|
24
|
-
|
|
25
|
-
const result = await formatMessageContent({
|
|
26
|
-
message_id: "om_post",
|
|
27
|
-
message_type: "post",
|
|
28
|
-
content: JSON.stringify({
|
|
29
|
-
content: [[
|
|
30
|
-
{ tag: "text", text: "use this image" },
|
|
31
|
-
{ tag: "img", image_key: "img_001" },
|
|
32
|
-
]],
|
|
33
|
-
}),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
|
|
37
|
-
expect(mockGetTenantAccessToken).toHaveBeenCalled();
|
|
38
|
-
expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("keeps post image key when mixed post image download fails", async () => {
|
|
42
|
-
mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
|
|
43
|
-
|
|
44
|
-
const result = await formatMessageContent({
|
|
45
|
-
message_id: "om_post",
|
|
46
|
-
message_type: "post",
|
|
47
|
-
content: JSON.stringify({
|
|
48
|
-
content: [[
|
|
49
|
-
{ tag: "text", text: "caption" },
|
|
50
|
-
{ tag: "img", image_key: "img_002" },
|
|
51
|
-
]],
|
|
52
|
-
}),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(result).toBe("caption\n[图片: img_002]");
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("formatMessageContent", () => {
|
|
14
|
+
import { formatMessageContent, formatPostContent, formatMergeForward } from "../format-message.ts";
|
|
15
|
+
|
|
16
|
+
describe("formatMessageContent mixed post images", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("includes downloaded images from mixed post messages", async () => {
|
|
23
|
+
mockGetOrDownloadImage.mockResolvedValue("C:\\tmp\\img_001.png");
|
|
24
|
+
|
|
25
|
+
const result = await formatMessageContent({
|
|
26
|
+
message_id: "om_post",
|
|
27
|
+
message_type: "post",
|
|
28
|
+
content: JSON.stringify({
|
|
29
|
+
content: [[
|
|
30
|
+
{ tag: "text", text: "use this image" },
|
|
31
|
+
{ tag: "img", image_key: "img_001" },
|
|
32
|
+
]],
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result).toBe("use this image\n[图片] C:\\tmp\\img_001.png");
|
|
37
|
+
expect(mockGetTenantAccessToken).toHaveBeenCalled();
|
|
38
|
+
expect(mockGetOrDownloadImage).toHaveBeenCalledWith("mock_token", "om_post", "img_001");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps post image key when mixed post image download fails", async () => {
|
|
42
|
+
mockGetOrDownloadImage.mockRejectedValue(new Error("download failed"));
|
|
43
|
+
|
|
44
|
+
const result = await formatMessageContent({
|
|
45
|
+
message_id: "om_post",
|
|
46
|
+
message_type: "post",
|
|
47
|
+
content: JSON.stringify({
|
|
48
|
+
content: [[
|
|
49
|
+
{ tag: "text", text: "caption" },
|
|
50
|
+
{ tag: "img", image_key: "img_002" },
|
|
51
|
+
]],
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result).toBe("caption\n[图片: img_002]");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("formatMessageContent", () => {
|
|
60
60
|
beforeEach(() => {
|
|
61
61
|
vi.clearAllMocks();
|
|
62
62
|
mockGetTenantAccessToken.mockResolvedValue("mock_token");
|
|
@@ -313,4 +313,4 @@ describe("formatPostContent", () => {
|
|
|
313
313
|
const result = formatPostContent({ content: [null, undefined, "string"] });
|
|
314
314
|
expect(result).toBe("");
|
|
315
315
|
});
|
|
316
|
-
});
|
|
316
|
+
});
|
|
@@ -519,7 +519,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
519
519
|
expect(card.elements[0].text.content).toContain("**5h:** 已用 37%,剩余 63%,重置:");
|
|
520
520
|
expect(card.elements[0].text.content).toContain("约 2小时52分钟后");
|
|
521
521
|
expect(card.elements[0].text.content).toContain("[███████░░░░░░░░░░░░░]");
|
|
522
|
-
expect(card.elements[0].text.content).toContain("
|
|
522
|
+
expect(card.elements[0].text.content).toContain("**7天:** 已用 12%,剩余 88%,重置:");
|
|
523
523
|
expect(card.elements[0].text.content).toContain("约 3天18小时17分钟后");
|
|
524
524
|
expect(card.elements[0].text.content).toContain("[██░░░░░░░░░░░░░░░░░░]");
|
|
525
525
|
expect(card.elements[2].actions[0].text.content).toBe("发起重置");
|
|
@@ -527,6 +527,31 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
527
527
|
expect(platform.setChatAvatar).toHaveBeenCalledWith("feishu-p2p", "codex", "idle", { codexUsage: usage });
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
+
it("shows only the 7-day window when the 5h window is absent", async () => {
|
|
531
|
+
const platform = mockPlatform("feishu");
|
|
532
|
+
const usage = {
|
|
533
|
+
fiveHour: null,
|
|
534
|
+
weekly: {
|
|
535
|
+
usedPercent: 23,
|
|
536
|
+
remainingPercent: 77,
|
|
537
|
+
resetAtEpochSeconds: 1784510226,
|
|
538
|
+
resetAfterSeconds: 500000,
|
|
539
|
+
limitWindowSeconds: 604800,
|
|
540
|
+
},
|
|
541
|
+
rateLimitResetCreditsAvailable: 0,
|
|
542
|
+
rateLimitResetCredits: [],
|
|
543
|
+
};
|
|
544
|
+
mockGetCodexUsageSummary.mockResolvedValue(usage);
|
|
545
|
+
|
|
546
|
+
await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
|
|
547
|
+
|
|
548
|
+
const card = JSON.parse(vi.mocked(platform.sendRawCard).mock.calls[0][1]);
|
|
549
|
+
const content = card.elements[0].text.content as string;
|
|
550
|
+
expect(content).not.toContain("**5h:**");
|
|
551
|
+
expect(content).toContain("**7天:** 已用 23%,剩余 77%,重置:");
|
|
552
|
+
expect(platform.setChatAvatar).toHaveBeenCalledWith("feishu-p2p", "codex", "idle", { codexUsage: usage });
|
|
553
|
+
});
|
|
554
|
+
|
|
530
555
|
it("adds ChatGPT subscription expiry to Codex /usage when CDP lookup succeeds", async () => {
|
|
531
556
|
const platform = mockPlatform("feishu");
|
|
532
557
|
mockGetCodexUsageSummary.mockResolvedValue({
|
|
@@ -646,7 +671,7 @@ describe("handleCommand WeChat processing ack", () => {
|
|
|
646
671
|
expect(codexPlatform.sendCard).toHaveBeenCalledWith(
|
|
647
672
|
"feishu-group",
|
|
648
673
|
"Codex Session Ready",
|
|
649
|
-
expect.stringContaining("发送 **/usage** 查看 Codex 5h
|
|
674
|
+
expect.stringContaining("发送 **/usage** 查看 Codex 实际存在的 5h/7天用量窗口,以及查询/使用主动重置卡。"),
|
|
650
675
|
"green",
|
|
651
676
|
);
|
|
652
677
|
|
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
|
|
7
|
-
vi.mock("../config.ts", async () => {
|
|
8
|
-
const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
|
|
9
|
-
return {
|
|
10
|
-
...actual,
|
|
11
|
-
USER_DATA_DIR: TEST_DATA_DIR,
|
|
12
|
-
ts: () => "test-ts",
|
|
13
|
-
};
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
let applyPrivacy: (text: string) => string;
|
|
17
|
-
let reloadPrivacyRules: () => void;
|
|
18
|
-
let getPrivacyRules: () => Record<string, string>;
|
|
19
|
-
let getPrivacyConfig: () => { enabled: boolean; rules: Record<string, string> };
|
|
20
|
-
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
vi.resetModules();
|
|
23
|
-
try {
|
|
24
|
-
await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
|
|
25
|
-
} catch {}
|
|
26
|
-
const mod = await import("../privacy.ts");
|
|
27
|
-
applyPrivacy = mod.applyPrivacy;
|
|
28
|
-
reloadPrivacyRules = mod.reloadPrivacyRules;
|
|
29
|
-
getPrivacyRules = mod.getPrivacyRules;
|
|
30
|
-
getPrivacyConfig = mod.getPrivacyConfig;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterEach(async () => {
|
|
34
|
-
try {
|
|
35
|
-
await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
|
|
36
|
-
} catch {}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterAll(async () => {
|
|
40
|
-
try {
|
|
41
|
-
await rm(TEST_DATA_DIR, { recursive: true, force: true });
|
|
42
|
-
} catch {}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("applyPrivacy", () => {
|
|
46
|
-
it("returns original text when privacy.json is missing", () => {
|
|
47
|
-
reloadPrivacyRules();
|
|
48
|
-
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("supports legacy flat privacy rules", async () => {
|
|
52
|
-
await writeFile(
|
|
53
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
54
|
-
JSON.stringify({ weizhangjian: "wzj", secret: "***" }),
|
|
55
|
-
"utf-8",
|
|
56
|
-
);
|
|
57
|
-
reloadPrivacyRules();
|
|
58
|
-
|
|
59
|
-
expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
|
|
60
|
-
expect(applyPrivacy("my secret is safe")).toBe("my *** is safe");
|
|
61
|
-
expect(applyPrivacy("weizhangjian and secret")).toBe("wzj and ***");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("supports privacy.json schema with enabled=false", async () => {
|
|
65
|
-
await writeFile(
|
|
66
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
67
|
-
JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } }),
|
|
68
|
-
"utf-8",
|
|
69
|
-
);
|
|
70
|
-
reloadPrivacyRules();
|
|
71
|
-
|
|
72
|
-
expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
|
|
73
|
-
expect(getPrivacyRules()).toEqual({ weizhangjian: "wzj" });
|
|
74
|
-
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("supports privacy.json schema with enabled=true", async () => {
|
|
78
|
-
await writeFile(
|
|
79
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
80
|
-
JSON.stringify({ enabled: true, rules: { weizhangjian: "wzj" } }),
|
|
81
|
-
"utf-8",
|
|
82
|
-
);
|
|
83
|
-
reloadPrivacyRules();
|
|
84
|
-
|
|
85
|
-
expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("accepts UTF-8 BOM in privacy.json", async () => {
|
|
89
|
-
await writeFile(
|
|
90
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
91
|
-
`\uFEFF${JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } })}`,
|
|
92
|
-
"utf-8",
|
|
93
|
-
);
|
|
94
|
-
reloadPrivacyRules();
|
|
95
|
-
|
|
96
|
-
expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
|
|
97
|
-
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("auto reloads privacy.json changes without explicit reload", async () => {
|
|
101
|
-
await writeFile(
|
|
102
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
103
|
-
JSON.stringify({ weizhangjian: "wzj" }),
|
|
104
|
-
"utf-8",
|
|
105
|
-
);
|
|
106
|
-
reloadPrivacyRules();
|
|
107
|
-
|
|
108
|
-
expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
|
|
109
|
-
|
|
110
|
-
await writeFile(
|
|
111
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
112
|
-
JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj-disabled" } }),
|
|
113
|
-
"utf-8",
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
117
|
-
expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj-disabled" } });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("replaces multiple rules and repeated occurrences", async () => {
|
|
121
|
-
await writeFile(
|
|
122
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
123
|
-
JSON.stringify({ a: "A", b: "B" }),
|
|
124
|
-
"utf-8",
|
|
125
|
-
);
|
|
126
|
-
reloadPrivacyRules();
|
|
127
|
-
|
|
128
|
-
expect(applyPrivacy("a b a b")).toBe("A B A B");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("returns empty text directly", async () => {
|
|
132
|
-
await writeFile(
|
|
133
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
134
|
-
JSON.stringify({ x: "y" }),
|
|
135
|
-
"utf-8",
|
|
136
|
-
);
|
|
137
|
-
reloadPrivacyRules();
|
|
138
|
-
|
|
139
|
-
expect(applyPrivacy("")).toBe("");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("treats special characters in rule keys literally", async () => {
|
|
143
|
-
await writeFile(
|
|
144
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
145
|
-
JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
|
|
146
|
-
"utf-8",
|
|
147
|
-
);
|
|
148
|
-
reloadPrivacyRules();
|
|
149
|
-
|
|
150
|
-
expect(applyPrivacy("hello a.b world")).toBe("hello X world");
|
|
151
|
-
expect(applyPrivacy("text (test) here")).toBe("text Y here");
|
|
152
|
-
expect(applyPrivacy("a *star shines")).toBe("a Z shines");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("reloadPrivacyRules forces a reload", async () => {
|
|
156
|
-
await writeFile(
|
|
157
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
158
|
-
JSON.stringify({ old: "OLD" }),
|
|
159
|
-
"utf-8",
|
|
160
|
-
);
|
|
161
|
-
reloadPrivacyRules();
|
|
162
|
-
|
|
163
|
-
expect(applyPrivacy("old")).toBe("OLD");
|
|
164
|
-
expect(getPrivacyRules()).toEqual({ old: "OLD" });
|
|
165
|
-
|
|
166
|
-
await writeFile(
|
|
167
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
168
|
-
JSON.stringify({ new: "NEW" }),
|
|
169
|
-
"utf-8",
|
|
170
|
-
);
|
|
171
|
-
reloadPrivacyRules();
|
|
172
|
-
|
|
173
|
-
expect(applyPrivacy("new")).toBe("NEW");
|
|
174
|
-
expect(getPrivacyRules()).toEqual({ new: "NEW" });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("returns original text for malformed JSON", async () => {
|
|
178
|
-
await writeFile(
|
|
179
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
180
|
-
"not json",
|
|
181
|
-
"utf-8",
|
|
182
|
-
);
|
|
183
|
-
reloadPrivacyRules();
|
|
184
|
-
|
|
185
|
-
expect(applyPrivacy("hello")).toBe("hello");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("returns original text for array JSON", async () => {
|
|
189
|
-
await writeFile(
|
|
190
|
-
join(TEST_DATA_DIR, "privacy.json"),
|
|
191
|
-
JSON.stringify(["a", "b"]),
|
|
192
|
-
"utf-8",
|
|
193
|
-
);
|
|
194
|
-
reloadPrivacyRules();
|
|
195
|
-
|
|
196
|
-
expect(applyPrivacy("hello")).toBe("hello");
|
|
197
|
-
});
|
|
198
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
|
|
7
|
+
vi.mock("../config.ts", async () => {
|
|
8
|
+
const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
USER_DATA_DIR: TEST_DATA_DIR,
|
|
12
|
+
ts: () => "test-ts",
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
let applyPrivacy: (text: string) => string;
|
|
17
|
+
let reloadPrivacyRules: () => void;
|
|
18
|
+
let getPrivacyRules: () => Record<string, string>;
|
|
19
|
+
let getPrivacyConfig: () => { enabled: boolean; rules: Record<string, string> };
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
try {
|
|
24
|
+
await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
|
|
25
|
+
} catch {}
|
|
26
|
+
const mod = await import("../privacy.ts");
|
|
27
|
+
applyPrivacy = mod.applyPrivacy;
|
|
28
|
+
reloadPrivacyRules = mod.reloadPrivacyRules;
|
|
29
|
+
getPrivacyRules = mod.getPrivacyRules;
|
|
30
|
+
getPrivacyConfig = mod.getPrivacyConfig;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
try {
|
|
35
|
+
await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
|
|
36
|
+
} catch {}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterAll(async () => {
|
|
40
|
+
try {
|
|
41
|
+
await rm(TEST_DATA_DIR, { recursive: true, force: true });
|
|
42
|
+
} catch {}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("applyPrivacy", () => {
|
|
46
|
+
it("returns original text when privacy.json is missing", () => {
|
|
47
|
+
reloadPrivacyRules();
|
|
48
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("supports legacy flat privacy rules", async () => {
|
|
52
|
+
await writeFile(
|
|
53
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
54
|
+
JSON.stringify({ weizhangjian: "wzj", secret: "***" }),
|
|
55
|
+
"utf-8",
|
|
56
|
+
);
|
|
57
|
+
reloadPrivacyRules();
|
|
58
|
+
|
|
59
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
|
|
60
|
+
expect(applyPrivacy("my secret is safe")).toBe("my *** is safe");
|
|
61
|
+
expect(applyPrivacy("weizhangjian and secret")).toBe("wzj and ***");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("supports privacy.json schema with enabled=false", async () => {
|
|
65
|
+
await writeFile(
|
|
66
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
67
|
+
JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } }),
|
|
68
|
+
"utf-8",
|
|
69
|
+
);
|
|
70
|
+
reloadPrivacyRules();
|
|
71
|
+
|
|
72
|
+
expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
|
|
73
|
+
expect(getPrivacyRules()).toEqual({ weizhangjian: "wzj" });
|
|
74
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("supports privacy.json schema with enabled=true", async () => {
|
|
78
|
+
await writeFile(
|
|
79
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
80
|
+
JSON.stringify({ enabled: true, rules: { weizhangjian: "wzj" } }),
|
|
81
|
+
"utf-8",
|
|
82
|
+
);
|
|
83
|
+
reloadPrivacyRules();
|
|
84
|
+
|
|
85
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("accepts UTF-8 BOM in privacy.json", async () => {
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
91
|
+
`\uFEFF${JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } })}`,
|
|
92
|
+
"utf-8",
|
|
93
|
+
);
|
|
94
|
+
reloadPrivacyRules();
|
|
95
|
+
|
|
96
|
+
expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
|
|
97
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("auto reloads privacy.json changes without explicit reload", async () => {
|
|
101
|
+
await writeFile(
|
|
102
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
103
|
+
JSON.stringify({ weizhangjian: "wzj" }),
|
|
104
|
+
"utf-8",
|
|
105
|
+
);
|
|
106
|
+
reloadPrivacyRules();
|
|
107
|
+
|
|
108
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
|
|
109
|
+
|
|
110
|
+
await writeFile(
|
|
111
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
112
|
+
JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj-disabled" } }),
|
|
113
|
+
"utf-8",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
117
|
+
expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj-disabled" } });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("replaces multiple rules and repeated occurrences", async () => {
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
123
|
+
JSON.stringify({ a: "A", b: "B" }),
|
|
124
|
+
"utf-8",
|
|
125
|
+
);
|
|
126
|
+
reloadPrivacyRules();
|
|
127
|
+
|
|
128
|
+
expect(applyPrivacy("a b a b")).toBe("A B A B");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns empty text directly", async () => {
|
|
132
|
+
await writeFile(
|
|
133
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
134
|
+
JSON.stringify({ x: "y" }),
|
|
135
|
+
"utf-8",
|
|
136
|
+
);
|
|
137
|
+
reloadPrivacyRules();
|
|
138
|
+
|
|
139
|
+
expect(applyPrivacy("")).toBe("");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("treats special characters in rule keys literally", async () => {
|
|
143
|
+
await writeFile(
|
|
144
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
145
|
+
JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
|
|
146
|
+
"utf-8",
|
|
147
|
+
);
|
|
148
|
+
reloadPrivacyRules();
|
|
149
|
+
|
|
150
|
+
expect(applyPrivacy("hello a.b world")).toBe("hello X world");
|
|
151
|
+
expect(applyPrivacy("text (test) here")).toBe("text Y here");
|
|
152
|
+
expect(applyPrivacy("a *star shines")).toBe("a Z shines");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("reloadPrivacyRules forces a reload", async () => {
|
|
156
|
+
await writeFile(
|
|
157
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
158
|
+
JSON.stringify({ old: "OLD" }),
|
|
159
|
+
"utf-8",
|
|
160
|
+
);
|
|
161
|
+
reloadPrivacyRules();
|
|
162
|
+
|
|
163
|
+
expect(applyPrivacy("old")).toBe("OLD");
|
|
164
|
+
expect(getPrivacyRules()).toEqual({ old: "OLD" });
|
|
165
|
+
|
|
166
|
+
await writeFile(
|
|
167
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
168
|
+
JSON.stringify({ new: "NEW" }),
|
|
169
|
+
"utf-8",
|
|
170
|
+
);
|
|
171
|
+
reloadPrivacyRules();
|
|
172
|
+
|
|
173
|
+
expect(applyPrivacy("new")).toBe("NEW");
|
|
174
|
+
expect(getPrivacyRules()).toEqual({ new: "NEW" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("returns original text for malformed JSON", async () => {
|
|
178
|
+
await writeFile(
|
|
179
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
180
|
+
"not json",
|
|
181
|
+
"utf-8",
|
|
182
|
+
);
|
|
183
|
+
reloadPrivacyRules();
|
|
184
|
+
|
|
185
|
+
expect(applyPrivacy("hello")).toBe("hello");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("returns original text for array JSON", async () => {
|
|
189
|
+
await writeFile(
|
|
190
|
+
join(TEST_DATA_DIR, "privacy.json"),
|
|
191
|
+
JSON.stringify(["a", "b"]),
|
|
192
|
+
"utf-8",
|
|
193
|
+
);
|
|
194
|
+
reloadPrivacyRules();
|
|
195
|
+
|
|
196
|
+
expect(applyPrivacy("hello")).toBe("hello");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ABD_APPEND_PROMPT,
|
|
5
|
-
applySharedPrefix,
|
|
6
|
-
} from "../shared-prefix.ts";
|
|
7
|
-
|
|
8
|
-
describe("applySharedPrefix", () => {
|
|
9
|
-
it("leaves normal messages unchanged", () => {
|
|
10
|
-
expect(applySharedPrefix("帮我分析")).toEqual({
|
|
11
|
-
matched: false,
|
|
12
|
-
text: "帮我分析",
|
|
13
|
-
body: "帮我分析",
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("removes /abd without requiring a space", () => {
|
|
18
|
-
const result = applySharedPrefix("/abd帮我分析");
|
|
19
|
-
|
|
20
|
-
expect(result.matched).toBe(true);
|
|
21
|
-
expect(result.body).toBe("帮我分析");
|
|
22
|
-
expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("removes whitespace after /abd", () => {
|
|
26
|
-
const result = applySharedPrefix("/abd 帮我分析");
|
|
27
|
-
|
|
28
|
-
expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("keeps the appendix when the user body is empty", () => {
|
|
32
|
-
const result = applySharedPrefix("/abd ");
|
|
33
|
-
|
|
34
|
-
expect(result.text).toBe(`---\n${ABD_APPEND_PROMPT}`);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ABD_APPEND_PROMPT,
|
|
5
|
+
applySharedPrefix,
|
|
6
|
+
} from "../shared-prefix.ts";
|
|
7
|
+
|
|
8
|
+
describe("applySharedPrefix", () => {
|
|
9
|
+
it("leaves normal messages unchanged", () => {
|
|
10
|
+
expect(applySharedPrefix("帮我分析")).toEqual({
|
|
11
|
+
matched: false,
|
|
12
|
+
text: "帮我分析",
|
|
13
|
+
body: "帮我分析",
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("removes /abd without requiring a space", () => {
|
|
18
|
+
const result = applySharedPrefix("/abd帮我分析");
|
|
19
|
+
|
|
20
|
+
expect(result.matched).toBe(true);
|
|
21
|
+
expect(result.body).toBe("帮我分析");
|
|
22
|
+
expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("removes whitespace after /abd", () => {
|
|
26
|
+
const result = applySharedPrefix("/abd 帮我分析");
|
|
27
|
+
|
|
28
|
+
expect(result.text).toBe(`帮我分析\n\n---\n${ABD_APPEND_PROMPT}`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("keeps the appendix when the user body is empty", () => {
|
|
32
|
+
const result = applySharedPrefix("/abd ");
|
|
33
|
+
|
|
34
|
+
expect(result.text).toBe(`---\n${ABD_APPEND_PROMPT}`);
|
|
35
|
+
});
|
|
36
|
+
});
|