chatccc 0.2.63 → 0.2.65

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.
Files changed (35) hide show
  1. package/config.sample.json +2 -3
  2. package/im-skills/feishu-skill/download-video.mjs +10 -17
  3. package/im-skills/wechat-file-skill/receive-send-file.md +39 -42
  4. package/im-skills/wechat-file-skill/send-file.mjs +84 -101
  5. package/im-skills/wechat-file-skill/skill.md +11 -11
  6. package/im-skills/wechat-image-skill/receive-send-image.md +5 -5
  7. package/im-skills/wechat-image-skill/send-image.mjs +2 -6
  8. package/im-skills/wechat-image-skill/skill.md +4 -4
  9. package/im-skills/wechat-video-skill/receive-send-video.md +39 -41
  10. package/im-skills/wechat-video-skill/send-video.mjs +80 -84
  11. package/im-skills/wechat-video-skill/skill.md +11 -11
  12. package/package.json +59 -59
  13. package/src/__tests__/agent-platform-routing.test.ts +26 -0
  14. package/src/__tests__/claude-adapter.test.ts +48 -48
  15. package/src/__tests__/config-reload.test.ts +14 -14
  16. package/src/__tests__/config-sample.test.ts +22 -22
  17. package/src/__tests__/im-skills.test.ts +49 -50
  18. package/src/__tests__/orchestrator.test.ts +1 -1
  19. package/src/__tests__/privacy.test.ts +142 -142
  20. package/src/__tests__/simplify.test.ts +282 -282
  21. package/src/__tests__/web-ui.test.ts +23 -23
  22. package/src/__tests__/wechat-platform.test.ts +0 -65
  23. package/src/adapters/claude-adapter.ts +40 -40
  24. package/src/agent-file-rpc.ts +19 -4
  25. package/src/agent-image-rpc.ts +19 -4
  26. package/src/agent-platform-routing.ts +28 -0
  27. package/src/config.ts +2 -24
  28. package/src/im-skills.ts +9 -0
  29. package/src/index.ts +11 -6
  30. package/src/orchestrator.ts +3 -4
  31. package/src/privacy.ts +67 -67
  32. package/src/session.ts +15 -3
  33. package/src/simplify.ts +119 -119
  34. package/src/web-ui.ts +3 -26
  35. package/src/wechat-platform.ts +68 -151
@@ -1,4 +1,4 @@
1
- import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { afterEach, describe, expect, it } from "vitest";
@@ -44,58 +44,57 @@ describe("IM skills prompt rendering", () => {
44
44
  expect(prompt).not.toContain("{{");
45
45
  });
46
46
 
47
- it("renders separate WeChat image, file, and video capability docs", async () => {
48
- tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-cache-"));
49
- const prompt = await buildImSkillsPrompt({
50
- variables: {
51
- cwd: "C:/work",
52
- im_skills_cache_dir: tempRoot,
53
- wechat_send_image_script: "C:/work/im-skills/wechat-image-skill/send-image.mjs",
54
- wechat_send_file_script: "C:/work/im-skills/wechat-file-skill/send-file.mjs",
55
- wechat_send_video_script: "C:/work/im-skills/wechat-video-skill/send-video.mjs",
56
- },
57
- });
47
+ it("filters skills by enabledSkillNames", async () => {
48
+ tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-filter-"));
49
+ const feishuDir = join(tempRoot, "feishu-skill");
50
+ const wechatDir = join(tempRoot, "wechat-skill");
51
+ const outDir = join(tempRoot, "out");
52
+ await mkdir(feishuDir);
53
+ await mkdir(wechatDir);
54
+ await writeFile(join(feishuDir, "skill.md"), "Feishu {{cwd}}", "utf-8");
55
+ await writeFile(join(feishuDir, "receive-send-image.md"), "Feishu doc", "utf-8");
56
+ await writeFile(join(wechatDir, "skill.md"), "WeChat {{cwd}}", "utf-8");
57
+ await writeFile(join(wechatDir, "receive-send-image.md"), "WeChat doc", "utf-8");
58
58
 
59
- const exported = await exportSkillSubDocs(
60
- {
61
- variables: {
62
- cwd: "C:/work",
63
- im_skills_cache_dir: tempRoot,
64
- wechat_send_image_script: "C:/work/im-skills/wechat-image-skill/send-image.mjs",
65
- wechat_send_file_script: "C:/work/im-skills/wechat-file-skill/send-file.mjs",
66
- wechat_send_video_script: "C:/work/im-skills/wechat-video-skill/send-video.mjs",
67
- },
68
- },
69
- tempRoot,
70
- );
59
+ const input = {
60
+ skillsDir: tempRoot,
61
+ enabledSkillNames: ["feishu-skill"],
62
+ variables: { cwd: "C:/work" },
63
+ };
64
+ const prompt = await buildImSkillsPrompt(input);
65
+ const exported = await exportSkillSubDocs(input, outDir);
71
66
 
72
- expect(prompt).toContain("[ChatCCC IM skill: wechat-image-skill]");
73
- expect(prompt).toContain("[ChatCCC IM skill: wechat-file-skill]");
74
- expect(prompt).toContain("[ChatCCC IM skill: wechat-video-skill]");
67
+ expect(prompt).toContain("[ChatCCC IM skill: feishu-skill]");
68
+ expect(prompt).toContain("Feishu C:/work");
75
69
  expect(prompt).not.toContain("[ChatCCC IM skill: wechat-skill]");
76
- expect(prompt).toContain("Receive images");
77
- expect(prompt).toContain("Send images");
78
- expect(prompt).toContain("Receive files");
79
- expect(prompt).toContain("Send files");
80
- expect(prompt).toContain("Receive videos");
81
- expect(prompt).toContain("Send videos");
82
- expect(prompt).toContain("receive-send-image.md");
83
- expect(prompt).toContain("receive-send-file.md");
84
- expect(prompt).toContain("receive-send-video.md");
85
- expect(prompt).not.toContain("{{wechat_send_image_script}}");
86
- expect(prompt).not.toContain("{{wechat_send_file_script}}");
87
- expect(prompt).not.toContain("{{wechat_send_video_script}}");
88
- expect(exported.some((file) => file.endsWith(join("wechat-image-skill", "receive-send-image.md")))).toBe(true);
89
- expect(exported.some((file) => file.endsWith(join("wechat-file-skill", "receive-send-file.md")))).toBe(true);
90
- expect(exported.some((file) => file.endsWith(join("wechat-video-skill", "receive-send-video.md")))).toBe(true);
91
- await expect(readFile(join(tempRoot, "wechat-image-skill", "receive-send-image.md"), "utf8")).resolves.toContain(
92
- "send-image.mjs",
93
- );
94
- await expect(readFile(join(tempRoot, "wechat-file-skill", "receive-send-file.md"), "utf8")).resolves.toContain(
95
- "send-file.mjs",
96
- );
97
- await expect(readFile(join(tempRoot, "wechat-video-skill", "receive-send-video.md"), "utf8")).resolves.toContain(
98
- "send-video.mjs",
70
+ expect(prompt).not.toContain("WeChat C:/work");
71
+ expect(exported).toHaveLength(1);
72
+ expect(exported[0]).toContain(join("feishu-skill", "receive-send-image.md"));
73
+ await expect(readFile(join(outDir, "feishu-skill", "receive-send-image.md"), "utf8")).resolves.toContain(
74
+ "Feishu doc",
99
75
  );
100
76
  });
77
+
78
+ it("skips filtering when enabledSkillNames is omitted", async () => {
79
+ tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-all-"));
80
+ const feishuDir = join(tempRoot, "feishu-skill");
81
+ const wechatDir = join(tempRoot, "wechat-skill");
82
+ const outDir = join(tempRoot, "out");
83
+ await mkdir(feishuDir);
84
+ await mkdir(wechatDir);
85
+ await writeFile(join(feishuDir, "skill.md"), "Feishu {{cwd}}", "utf-8");
86
+ await writeFile(join(feishuDir, "receive-send-image.md"), "Feishu doc", "utf-8");
87
+ await writeFile(join(wechatDir, "skill.md"), "WeChat {{cwd}}", "utf-8");
88
+ await writeFile(join(wechatDir, "receive-send-image.md"), "WeChat doc", "utf-8");
89
+
90
+ const prompt = await buildImSkillsPrompt({
91
+ skillsDir: tempRoot,
92
+ variables: { cwd: "C:/work" },
93
+ });
94
+ const exported = await exportSkillSubDocs({ skillsDir: tempRoot, variables: { cwd: "C:/work" } }, outDir);
95
+
96
+ expect(prompt).toContain("[ChatCCC IM skill: feishu-skill]");
97
+ expect(prompt).toContain("[ChatCCC IM skill: wechat-skill]");
98
+ expect(exported.length).toBeGreaterThanOrEqual(2);
99
+ });
101
100
  });
@@ -146,7 +146,7 @@ describe("handleCommand WeChat processing ack", () => {
146
146
  expect(platform.sendCard).toHaveBeenCalledWith(
147
147
  "wx-chat",
148
148
  "生成中",
149
- "该会话正在生成回复中,请等待完成后再发送新消息。如需中断生成,请发送 /stop 指令。",
149
+ "该会话正在生成回复中,请等待完成后再发送新消息。",
150
150
  "yellow",
151
151
  );
152
152
  });
@@ -1,143 +1,143 @@
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
- // 在 import privacy 之前 mock config,让 USER_DATA_DIR 指向临时目录
7
- const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
8
- vi.mock("../config.ts", async () => {
9
- const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
10
- return {
11
- ...actual,
12
- USER_DATA_DIR: TEST_DATA_DIR,
13
- ts: () => "test-ts",
14
- };
15
- });
16
-
17
- let applyPrivacy: (text: string) => string;
18
- let reloadPrivacyRules: () => void;
19
- let getPrivacyRules: () => Record<string, string>;
20
-
21
- beforeEach(async () => {
22
- vi.resetModules();
23
- // 清理临时目录中的 privacy.json
24
- try {
25
- await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
26
- } catch {}
27
- const mod = await import("../privacy.ts");
28
- applyPrivacy = mod.applyPrivacy;
29
- reloadPrivacyRules = mod.reloadPrivacyRules;
30
- getPrivacyRules = mod.getPrivacyRules;
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("无 privacy.json 时返回原文", () => {
47
- reloadPrivacyRules();
48
- expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
49
- });
50
-
51
- it("privacy.json 存在时按规则替换", 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("多规则替换多次出现", async () => {
65
- await writeFile(
66
- join(TEST_DATA_DIR, "privacy.json"),
67
- JSON.stringify({ a: "A", b: "B" }),
68
- "utf-8",
69
- );
70
- reloadPrivacyRules();
71
-
72
- expect(applyPrivacy("a b a b")).toBe("A B A B");
73
- });
74
-
75
- it("空文本直接返回", async () => {
76
- await writeFile(
77
- join(TEST_DATA_DIR, "privacy.json"),
78
- JSON.stringify({ x: "y" }),
79
- "utf-8",
80
- );
81
- reloadPrivacyRules();
82
-
83
- expect(applyPrivacy("")).toBe("");
84
- });
85
-
86
- it("规则中的特殊字符不会被当作正则", async () => {
87
- await writeFile(
88
- join(TEST_DATA_DIR, "privacy.json"),
89
- JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
90
- "utf-8",
91
- );
92
- reloadPrivacyRules();
93
-
94
- expect(applyPrivacy("hello a.b world")).toBe("hello X world");
95
- expect(applyPrivacy("text (test) here")).toBe("text Y here");
96
- expect(applyPrivacy("a *star shines")).toBe("a Z shines");
97
- });
98
-
99
- it("reloadPrivacyRules 强制重新加载", async () => {
100
- await writeFile(
101
- join(TEST_DATA_DIR, "privacy.json"),
102
- JSON.stringify({ old: "OLD" }),
103
- "utf-8",
104
- );
105
- reloadPrivacyRules();
106
-
107
- expect(applyPrivacy("old")).toBe("OLD");
108
- expect(getPrivacyRules()).toEqual({ old: "OLD" });
109
-
110
- // 变更磁盘内容后 reload
111
- await writeFile(
112
- join(TEST_DATA_DIR, "privacy.json"),
113
- JSON.stringify({ new: "NEW" }),
114
- "utf-8",
115
- );
116
- reloadPrivacyRules();
117
-
118
- expect(applyPrivacy("new")).toBe("NEW");
119
- expect(getPrivacyRules()).toEqual({ new: "NEW" });
120
- });
121
-
122
- it("格式错误的 JSON 不抛异常,返回原文", async () => {
123
- await writeFile(
124
- join(TEST_DATA_DIR, "privacy.json"),
125
- "not json",
126
- "utf-8",
127
- );
128
- reloadPrivacyRules();
129
-
130
- expect(applyPrivacy("hello")).toBe("hello");
131
- });
132
-
133
- it("数组格式的 JSON 不抛异常,返回原文", async () => {
134
- await writeFile(
135
- join(TEST_DATA_DIR, "privacy.json"),
136
- JSON.stringify(["a", "b"]),
137
- "utf-8",
138
- );
139
- reloadPrivacyRules();
140
-
141
- expect(applyPrivacy("hello")).toBe("hello");
142
- });
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
+ // 在 import privacy 之前 mock config,让 USER_DATA_DIR 指向临时目录
7
+ const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
8
+ vi.mock("../config.ts", async () => {
9
+ const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
10
+ return {
11
+ ...actual,
12
+ USER_DATA_DIR: TEST_DATA_DIR,
13
+ ts: () => "test-ts",
14
+ };
15
+ });
16
+
17
+ let applyPrivacy: (text: string) => string;
18
+ let reloadPrivacyRules: () => void;
19
+ let getPrivacyRules: () => Record<string, string>;
20
+
21
+ beforeEach(async () => {
22
+ vi.resetModules();
23
+ // 清理临时目录中的 privacy.json
24
+ try {
25
+ await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
26
+ } catch {}
27
+ const mod = await import("../privacy.ts");
28
+ applyPrivacy = mod.applyPrivacy;
29
+ reloadPrivacyRules = mod.reloadPrivacyRules;
30
+ getPrivacyRules = mod.getPrivacyRules;
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("无 privacy.json 时返回原文", () => {
47
+ reloadPrivacyRules();
48
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
49
+ });
50
+
51
+ it("privacy.json 存在时按规则替换", 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("多规则替换多次出现", async () => {
65
+ await writeFile(
66
+ join(TEST_DATA_DIR, "privacy.json"),
67
+ JSON.stringify({ a: "A", b: "B" }),
68
+ "utf-8",
69
+ );
70
+ reloadPrivacyRules();
71
+
72
+ expect(applyPrivacy("a b a b")).toBe("A B A B");
73
+ });
74
+
75
+ it("空文本直接返回", async () => {
76
+ await writeFile(
77
+ join(TEST_DATA_DIR, "privacy.json"),
78
+ JSON.stringify({ x: "y" }),
79
+ "utf-8",
80
+ );
81
+ reloadPrivacyRules();
82
+
83
+ expect(applyPrivacy("")).toBe("");
84
+ });
85
+
86
+ it("规则中的特殊字符不会被当作正则", async () => {
87
+ await writeFile(
88
+ join(TEST_DATA_DIR, "privacy.json"),
89
+ JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
90
+ "utf-8",
91
+ );
92
+ reloadPrivacyRules();
93
+
94
+ expect(applyPrivacy("hello a.b world")).toBe("hello X world");
95
+ expect(applyPrivacy("text (test) here")).toBe("text Y here");
96
+ expect(applyPrivacy("a *star shines")).toBe("a Z shines");
97
+ });
98
+
99
+ it("reloadPrivacyRules 强制重新加载", async () => {
100
+ await writeFile(
101
+ join(TEST_DATA_DIR, "privacy.json"),
102
+ JSON.stringify({ old: "OLD" }),
103
+ "utf-8",
104
+ );
105
+ reloadPrivacyRules();
106
+
107
+ expect(applyPrivacy("old")).toBe("OLD");
108
+ expect(getPrivacyRules()).toEqual({ old: "OLD" });
109
+
110
+ // 变更磁盘内容后 reload
111
+ await writeFile(
112
+ join(TEST_DATA_DIR, "privacy.json"),
113
+ JSON.stringify({ new: "NEW" }),
114
+ "utf-8",
115
+ );
116
+ reloadPrivacyRules();
117
+
118
+ expect(applyPrivacy("new")).toBe("NEW");
119
+ expect(getPrivacyRules()).toEqual({ new: "NEW" });
120
+ });
121
+
122
+ it("格式错误的 JSON 不抛异常,返回原文", async () => {
123
+ await writeFile(
124
+ join(TEST_DATA_DIR, "privacy.json"),
125
+ "not json",
126
+ "utf-8",
127
+ );
128
+ reloadPrivacyRules();
129
+
130
+ expect(applyPrivacy("hello")).toBe("hello");
131
+ });
132
+
133
+ it("数组格式的 JSON 不抛异常,返回原文", async () => {
134
+ await writeFile(
135
+ join(TEST_DATA_DIR, "privacy.json"),
136
+ JSON.stringify(["a", "b"]),
137
+ "utf-8",
138
+ );
139
+ reloadPrivacyRules();
140
+
141
+ expect(applyPrivacy("hello")).toBe("hello");
142
+ });
143
143
  });