chatccc 0.2.59 → 0.2.60

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.
@@ -10,14 +10,14 @@
10
10
  "port": 18080,
11
11
  "gitTimeoutSeconds": 180,
12
12
  "allowInterrupt": false,
13
- "claude": {
14
- "enabled": false,
15
- "defaultAgent": true,
16
- "model": "claude-sonnet-4-6",
17
- "subagentModel": "",
18
- "effort": "",
19
- "apiKey": "",
20
- "baseUrl": ""
13
+ "claude": {
14
+ "enabled": false,
15
+ "defaultAgent": true,
16
+ "model": "claude-sonnet-4-6",
17
+ "subagentModel": "",
18
+ "effort": "",
19
+ "apiKey": "",
20
+ "baseUrl": ""
21
21
  },
22
22
  "cursor": {
23
23
  "enabled": false,
@@ -1,39 +1,39 @@
1
- # Receiving & Sending Images (WeChat)
2
-
3
- ## Receive Images
4
-
5
- Images sent to the bot are **automatically downloaded** to `~/.chatccc/images/downloads/` with the `wx_` filename prefix.
6
-
7
- The message text you receive will include the downloaded path in the format:
8
- ```
9
- [图片] C:\Users\<用户名>\.chatccc\images\downloads\wx_<key>.png
10
- ```
11
-
12
- You can read the image file at that path to understand what the user sent.
13
-
14
- ## Send Images
15
-
16
- ### Script (recommended)
17
- ```bash
18
- node "{{wechat_send_image_script}}" --path "<absolute image path>" --caption "<optional caption>"
19
- ```
20
-
21
- The script reads the current WeChat session's auth token, chat ID, and context token from `~/.chatccc/state/ilink-auth.json`, then sends the image to the most recent chat.
22
-
23
- ### Direct usage in test scripts
24
-
25
- The underlying SDK call is:
26
- ```ts
27
- import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
28
- const wire = new OpenIlinkWire(token, { base_url: baseUrl });
29
- await wire.sendMediaFile(chatId, contextToken, imageBuffer, "filename.png", "caption");
30
- ```
31
-
32
- ### Rules
33
-
34
- - Save or choose a local image file first.
35
- - Use an absolute local path.
36
- - Supported formats: .png, .jpg, .jpeg, .webp, .gif, .bmp.
37
- - Max image size: 10MB (SDK guideline; larger files upload slower).
38
- - Only send an image when the user asked for one or when it materially helps the answer.
1
+ # Receiving & Sending Images (WeChat)
2
+
3
+ ## Receive Images
4
+
5
+ Images sent to the bot are **automatically downloaded** to `~/.chatccc/images/downloads/` with the `wx_` filename prefix.
6
+
7
+ The message text you receive will include the downloaded path in the format:
8
+ ```
9
+ [图片] C:\Users\<用户名>\.chatccc\images\downloads\wx_<key>.png
10
+ ```
11
+
12
+ You can read the image file at that path to understand what the user sent.
13
+
14
+ ## Send Images
15
+
16
+ ### Script (recommended)
17
+ ```bash
18
+ node "{{wechat_send_image_script}}" --path "<absolute image path>" --caption "<optional caption>"
19
+ ```
20
+
21
+ The script reads the current WeChat session's auth token, chat ID, and context token from `~/.chatccc/state/ilink-auth.json`, then sends the image to the most recent chat.
22
+
23
+ ### Direct usage in test scripts
24
+
25
+ The underlying SDK call is:
26
+ ```ts
27
+ import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
28
+ const wire = new OpenIlinkWire(token, { base_url: baseUrl });
29
+ await wire.sendMediaFile(chatId, contextToken, imageBuffer, "filename.png", "caption");
30
+ ```
31
+
32
+ ### Rules
33
+
34
+ - Save or choose a local image file first.
35
+ - Use an absolute local path.
36
+ - Supported formats: .png, .jpg, .jpeg, .webp, .gif, .bmp.
37
+ - Max image size: 10MB (SDK guideline; larger files upload slower).
38
+ - Only send an image when the user asked for one or when it materially helps the answer.
39
39
  - **Claw 限制**: 图片发送也计入微信 claw 连发计数,连续 10 条未回复会截断。
@@ -1,80 +1,80 @@
1
- #!/usr/bin/env node
2
- import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { basename, extname, join } from "node:path";
4
- import { homedir } from "node:os";
5
-
6
- import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
7
-
8
- const ILINK_AUTH_PATH = join(homedir(), ".chatccc", "state", "ilink-auth.json");
9
- const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
10
- const ALLOWED_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"]);
11
-
12
- function parseArgs(argv) {
13
- const result = {};
14
- for (let i = 0; i < argv.length; i++) {
15
- const key = argv[i];
16
- if (!key.startsWith("--")) continue;
17
- const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
18
- result[key.slice(2)] = value;
19
- }
20
- return result;
21
- }
22
-
23
- function usage() {
24
- console.error(`Usage:
25
- node ${basename(process.argv[1])} --path <absolute image path> [--caption <text>]`);
26
- }
27
-
28
- async function main() {
29
- const args = parseArgs(process.argv.slice(2));
30
- const imagePath = args.path;
31
- const caption = args.caption || "";
32
-
33
- if (!imagePath) {
34
- usage();
35
- process.exit(1);
36
- }
37
-
38
- const ext = extname(imagePath).toLowerCase();
39
- if (!ALLOWED_EXTS.has(ext)) {
40
- console.error(`Unsupported image extension: ${ext || "(none)"}`);
41
- process.exit(1);
42
- }
43
-
44
- const st = statSync(imagePath);
45
- if (!st.isFile()) {
46
- console.error("Image path is not a file");
47
- process.exit(1);
48
- }
49
- if (st.size > MAX_IMAGE_BYTES) {
50
- console.error("Image file exceeds 10MB limit");
51
- process.exit(1);
52
- }
53
-
54
- if (!existsSync(ILINK_AUTH_PATH)) {
55
- console.error(`Auth snapshot not found: ${ILINK_AUTH_PATH}`);
56
- console.error("Make sure the WeChat iLink platform is logged in.");
57
- process.exit(1);
58
- }
59
-
60
- const snap = JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
61
- if (!snap.token || !snap.lastChatId || !snap.contextToken) {
62
- console.error("Auth snapshot missing token, lastChatId, or contextToken.");
63
- process.exit(1);
64
- }
65
-
66
- const imgData = readFileSync(imagePath);
67
- const fileName = basename(imagePath);
68
-
69
- const wire = new OpenIlinkWire(snap.token, {
70
- base_url: snap.baseUrl,
71
- });
72
-
73
- await wire.sendMediaFile(snap.lastChatId, snap.contextToken, imgData, fileName, caption);
74
- console.log(JSON.stringify({ ok: true, sentTo: 1 }));
75
- }
76
-
77
- main().catch((err) => {
78
- console.error(err instanceof Error ? err.message : String(err));
79
- process.exit(1);
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { basename, extname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
7
+
8
+ const ILINK_AUTH_PATH = join(homedir(), ".chatccc", "state", "ilink-auth.json");
9
+ const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
10
+ const ALLOWED_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"]);
11
+
12
+ function parseArgs(argv) {
13
+ const result = {};
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const key = argv[i];
16
+ if (!key.startsWith("--")) continue;
17
+ const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
18
+ result[key.slice(2)] = value;
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function usage() {
24
+ console.error(`Usage:
25
+ node ${basename(process.argv[1])} --path <absolute image path> [--caption <text>]`);
26
+ }
27
+
28
+ async function main() {
29
+ const args = parseArgs(process.argv.slice(2));
30
+ const imagePath = args.path;
31
+ const caption = args.caption || "";
32
+
33
+ if (!imagePath) {
34
+ usage();
35
+ process.exit(1);
36
+ }
37
+
38
+ const ext = extname(imagePath).toLowerCase();
39
+ if (!ALLOWED_EXTS.has(ext)) {
40
+ console.error(`Unsupported image extension: ${ext || "(none)"}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ const st = statSync(imagePath);
45
+ if (!st.isFile()) {
46
+ console.error("Image path is not a file");
47
+ process.exit(1);
48
+ }
49
+ if (st.size > MAX_IMAGE_BYTES) {
50
+ console.error("Image file exceeds 10MB limit");
51
+ process.exit(1);
52
+ }
53
+
54
+ if (!existsSync(ILINK_AUTH_PATH)) {
55
+ console.error(`Auth snapshot not found: ${ILINK_AUTH_PATH}`);
56
+ console.error("Make sure the WeChat iLink platform is logged in.");
57
+ process.exit(1);
58
+ }
59
+
60
+ const snap = JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
61
+ if (!snap.token || !snap.lastChatId || !snap.contextToken) {
62
+ console.error("Auth snapshot missing token, lastChatId, or contextToken.");
63
+ process.exit(1);
64
+ }
65
+
66
+ const imgData = readFileSync(imagePath);
67
+ const fileName = basename(imagePath);
68
+
69
+ const wire = new OpenIlinkWire(snap.token, {
70
+ base_url: snap.baseUrl,
71
+ });
72
+
73
+ await wire.sendMediaFile(snap.lastChatId, snap.contextToken, imgData, fileName, caption);
74
+ console.log(JSON.stringify({ ok: true, sentTo: 1 }));
75
+ }
76
+
77
+ main().catch((err) => {
78
+ console.error(err instanceof Error ? err.message : String(err));
79
+ process.exit(1);
80
80
  });
@@ -1,11 +1,11 @@
1
- ---
2
- name: wechat-skill
3
- description: WeChat iLink local skills for sending and receiving images.
4
- ---
5
-
6
- Current working directory: {{cwd}}
7
-
8
- WeChat images are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper scripts below instead.
9
-
10
- - **Receive images**: Images sent to the bot are automatically downloaded to `~/.chatccc/images/downloads/` with the filename prefix `wx_`. The message text will include the local path as `[图片] <absolute path>`.
1
+ ---
2
+ name: wechat-skill
3
+ description: WeChat iLink local skills for sending and receiving images.
4
+ ---
5
+
6
+ Current working directory: {{cwd}}
7
+
8
+ WeChat images are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper scripts below instead.
9
+
10
+ - **Receive images**: Images sent to the bot are automatically downloaded to `~/.chatccc/images/downloads/` with the filename prefix `wx_`. The message text will include the local path as `[图片] <absolute path>`.
11
11
  - **Send images**: Use the send-image helper script — read `{{im_skills_cache_dir}}/wechat-skill/receive-send-image.md`
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "chatccc",
3
- "version": "0.2.59",
4
- "description": "Feishu bot bridge for Claude Code",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "main": "./src/index.ts",
8
- "bin": {
9
- "chatccc": "bin/chatccc.mjs"
10
- },
11
- "files": [
12
- "src/",
13
- "bin/",
14
- "demo/ilink_echo_probe.ts",
15
- "im-skills/",
16
- "images/img_readme_*.jpg",
17
- "images/img_readme_*.png",
18
- "images/avatars/status_*.png",
19
- "images/avatars/badges/",
20
- "package.json",
21
- "README.md",
22
- "config.sample.json"
23
- ],
24
- "scripts": {
25
- "dev": "tsx src/index.ts",
26
- "chatccc": "tsx src/index.ts",
27
- "start": "tsx src/index.ts",
28
- "demo:bot-test": "tsx demo/bot_test.ts",
29
- "demo:bot-test:local": "tsx demo/bot_test.ts --local",
30
- "demo:create-group": "tsx src/index.ts",
31
- "demo:create-group:local": "tsx src/index.ts --local",
32
- "demo:permission-check": "tsx demo/permission_check.ts",
33
- "demo:claude-hi": "tsx demo/claude_say_hi.ts",
34
- "demo:codex-hi": "tsx demo/codex_say_hi.ts",
35
- "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
36
- "test": "vitest run",
37
- "test:watch": "vitest"
38
- },
39
- "dependencies": {
40
- "@anthropic-ai/claude-agent-sdk": "0.2.133",
41
- "@larksuiteoapi/node-sdk": "^1.59.0",
42
- "@openilink/openilink-sdk-node": "^0.6.0",
43
- "nodemailer": "^8.0.7",
44
- "qrcode-terminal": "^0.12.0",
45
- "sharp": "^0.34.5",
46
- "tsx": "^4.0.0",
47
- "ws": "^8.18.0"
48
- },
49
- "devDependencies": {
50
- "@types/node": "^20.0.0",
51
- "@types/qrcode-terminal": "^0.12.2",
52
- "@types/ws": "^8.18.1",
53
- "typescript": "^5.0.0",
54
- "vitest": "^4.1.5"
55
- },
56
- "engines": {
57
- "node": ">=20"
58
- }
59
- }
1
+ {
2
+ "name": "chatccc",
3
+ "version": "0.2.60",
4
+ "description": "Feishu bot bridge for Claude Code",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "bin": {
9
+ "chatccc": "bin/chatccc.mjs"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "bin/",
14
+ "demo/ilink_echo_probe.ts",
15
+ "im-skills/",
16
+ "images/img_readme_*.jpg",
17
+ "images/img_readme_*.png",
18
+ "images/avatars/status_*.png",
19
+ "images/avatars/badges/",
20
+ "package.json",
21
+ "README.md",
22
+ "config.sample.json"
23
+ ],
24
+ "scripts": {
25
+ "dev": "tsx src/index.ts",
26
+ "chatccc": "tsx src/index.ts",
27
+ "start": "tsx src/index.ts",
28
+ "demo:bot-test": "tsx demo/bot_test.ts",
29
+ "demo:bot-test:local": "tsx demo/bot_test.ts --local",
30
+ "demo:create-group": "tsx src/index.ts",
31
+ "demo:create-group:local": "tsx src/index.ts --local",
32
+ "demo:permission-check": "tsx demo/permission_check.ts",
33
+ "demo:claude-hi": "tsx demo/claude_say_hi.ts",
34
+ "demo:codex-hi": "tsx demo/codex_say_hi.ts",
35
+ "demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest"
38
+ },
39
+ "dependencies": {
40
+ "@anthropic-ai/claude-agent-sdk": "0.2.133",
41
+ "@larksuiteoapi/node-sdk": "^1.59.0",
42
+ "@openilink/openilink-sdk-node": "^0.6.0",
43
+ "nodemailer": "^8.0.7",
44
+ "qrcode-terminal": "^0.12.0",
45
+ "sharp": "^0.34.5",
46
+ "tsx": "^4.0.0",
47
+ "ws": "^8.18.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "@types/qrcode-terminal": "^0.12.2",
52
+ "@types/ws": "^8.18.1",
53
+ "typescript": "^5.0.0",
54
+ "vitest": "^4.1.5"
55
+ },
56
+ "engines": {
57
+ "node": ">=20"
58
+ }
59
+ }
@@ -771,10 +771,10 @@ describe("createClaudeAdapter — env 注入", () => {
771
771
  expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://api.deepseek.com/anthropic");
772
772
  });
773
773
 
774
- it("apiKey + baseUrl 都设置 → 同时覆盖", async () => {
775
- setupMockCreateSession();
776
- const adapter = createClaudeAdapter({
777
- model: "",
774
+ it("apiKey + baseUrl 都设置 → 同时覆盖", async () => {
775
+ setupMockCreateSession();
776
+ const adapter = createClaudeAdapter({
777
+ model: "",
778
778
  effort: "",
779
779
  isEmpty: (v) => v.trim() === "",
780
780
  apiKey: "sk-x",
@@ -784,50 +784,50 @@ describe("createClaudeAdapter — env 注入", () => {
784
784
  await adapter.createSession("/cwd");
785
785
 
786
786
  const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
787
- expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-x");
788
- expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
789
- });
790
-
791
- it("官方 API 模式下 subagentModel 不注入 env,也不覆盖 Claude 登录环境", async () => {
792
- setupMockCreateSession();
793
- process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
794
- const adapter = createClaudeAdapter({
795
- model: "claude-sonnet-4-6",
796
- subagentModel: "claude-haiku-4-5-20251001",
797
- effort: "",
798
- isEmpty: (v) => v.trim() === "",
799
- apiKey: "",
800
- baseUrl: "",
801
- });
802
-
803
- await adapter.createSession("/cwd");
804
-
805
- const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
806
- expect(opts).not.toHaveProperty("env");
807
- });
808
-
809
- it("第三方 API 模式下 subagentModel 通过 CLAUDE_CODE_SUBAGENT_MODEL 覆盖 Haiku", async () => {
810
- setupMockCreateSession();
811
- process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
812
- const adapter = createClaudeAdapter({
813
- model: "claude-sonnet-4-6",
814
- subagentModel: "claude-haiku-4-5-20251001",
815
- effort: "",
816
- isEmpty: (v) => v.trim() === "",
817
- apiKey: "sk-thirdparty",
818
- baseUrl: "https://gateway.example/anthropic",
819
- });
820
-
821
- await adapter.createSession("/cwd");
822
-
823
- const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
824
- expect(opts.model).toBe("claude-sonnet-4-6");
825
- expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-thirdparty");
826
- expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
827
- expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
828
- });
829
-
830
- it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
787
+ expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-x");
788
+ expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
789
+ });
790
+
791
+ it("官方 API 模式下 subagentModel 不注入 env,也不覆盖 Claude 登录环境", async () => {
792
+ setupMockCreateSession();
793
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
794
+ const adapter = createClaudeAdapter({
795
+ model: "claude-sonnet-4-6",
796
+ subagentModel: "claude-haiku-4-5-20251001",
797
+ effort: "",
798
+ isEmpty: (v) => v.trim() === "",
799
+ apiKey: "",
800
+ baseUrl: "",
801
+ });
802
+
803
+ await adapter.createSession("/cwd");
804
+
805
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
806
+ expect(opts).not.toHaveProperty("env");
807
+ });
808
+
809
+ it("第三方 API 模式下 subagentModel 通过 CLAUDE_CODE_SUBAGENT_MODEL 覆盖 Haiku", async () => {
810
+ setupMockCreateSession();
811
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
812
+ const adapter = createClaudeAdapter({
813
+ model: "claude-sonnet-4-6",
814
+ subagentModel: "claude-haiku-4-5-20251001",
815
+ effort: "",
816
+ isEmpty: (v) => v.trim() === "",
817
+ apiKey: "sk-thirdparty",
818
+ baseUrl: "https://gateway.example/anthropic",
819
+ });
820
+
821
+ await adapter.createSession("/cwd");
822
+
823
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
824
+ expect(opts.model).toBe("claude-sonnet-4-6");
825
+ expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-thirdparty");
826
+ expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
827
+ expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
828
+ });
829
+
830
+ it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
831
831
  setupMockCreateSession();
832
832
  process.env.ANTHROPIC_AUTH_TOKEN = "token-from-claude-settings";
833
833
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-claude-settings";
@@ -10,9 +10,9 @@ import {
10
10
  CHATCCC_PORT,
11
11
  CLAUDE_API_KEY,
12
12
  CLAUDE_BASE_URL,
13
- CLAUDE_EFFORT,
14
- CLAUDE_MODEL,
15
- CLAUDE_SUBAGENT_MODEL,
13
+ CLAUDE_EFFORT,
14
+ CLAUDE_MODEL,
15
+ CLAUDE_SUBAGENT_MODEL,
16
16
  CURSOR_AGENT_ARGS,
17
17
  CURSOR_AGENT_COMMAND,
18
18
  FEISHU_ENABLED,
@@ -44,10 +44,10 @@ const baseAppConfig: AppConfig = {
44
44
  allowInterrupt: false,
45
45
  claude: {
46
46
  enabled: true,
47
- defaultAgent: true,
48
- model: "initial-model",
49
- subagentModel: "initial-subagent-model",
50
- effort: "initial-effort",
47
+ defaultAgent: true,
48
+ model: "initial-model",
49
+ subagentModel: "initial-subagent-model",
50
+ effort: "initial-effort",
51
51
  apiKey: "sk-initial",
52
52
  baseUrl: "https://initial.gw/anthropic",
53
53
  },
@@ -80,22 +80,22 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
80
80
  expect(APP_SECRET).toBe("NEW_APP_SECRET");
81
81
  });
82
82
 
83
- it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
83
+ it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
84
84
  applyLoadedConfig({
85
85
  ...structuredClone(baseAppConfig),
86
86
  claude: {
87
87
  enabled: true,
88
- defaultAgent: true,
89
- model: "claude-sonnet-4-6",
90
- subagentModel: "claude-haiku-4-5-20251001",
91
- effort: "high",
88
+ defaultAgent: true,
89
+ model: "claude-sonnet-4-6",
90
+ subagentModel: "claude-haiku-4-5-20251001",
91
+ effort: "high",
92
92
  apiKey: "sk-newkey",
93
93
  baseUrl: "https://gw2.example/anthropic",
94
94
  },
95
95
  });
96
96
 
97
- expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
98
- expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
97
+ expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
98
+ expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
99
99
  expect(CLAUDE_EFFORT).toBe("high");
100
100
  expect(CLAUDE_API_KEY).toBe("sk-newkey");
101
101
  expect(CLAUDE_BASE_URL).toBe("https://gw2.example/anthropic");
@@ -6,25 +6,25 @@ import { describe, expect, it } from "vitest";
6
6
  describe("config.sample.json", () => {
7
7
  it("enables Feishu and WeChat iLink by default", () => {
8
8
  const configSamplePath = join(process.cwd(), "config.sample.json");
9
- const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
10
- platforms?: {
11
- feishu?: { enabled?: unknown };
12
- ilink?: { enabled?: unknown };
13
- };
14
- claude?: { model?: unknown; subagentModel?: unknown };
15
- };
16
-
17
- expect(sample.platforms?.feishu?.enabled).toBe(true);
18
- expect(sample.platforms?.ilink?.enabled).toBe(true);
19
- });
20
-
21
- it("uses an Anthropic official main model and leaves subagent override empty by default", () => {
22
- const configSamplePath = join(process.cwd(), "config.sample.json");
23
- const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
24
- claude?: { model?: unknown; subagentModel?: unknown };
25
- };
26
-
27
- expect(sample.claude?.model).toBe("claude-sonnet-4-6");
28
- expect(sample.claude?.subagentModel).toBe("");
29
- });
30
- });
9
+ const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
10
+ platforms?: {
11
+ feishu?: { enabled?: unknown };
12
+ ilink?: { enabled?: unknown };
13
+ };
14
+ claude?: { model?: unknown; subagentModel?: unknown };
15
+ };
16
+
17
+ expect(sample.platforms?.feishu?.enabled).toBe(true);
18
+ expect(sample.platforms?.ilink?.enabled).toBe(true);
19
+ });
20
+
21
+ it("uses an Anthropic official main model and leaves subagent override empty by default", () => {
22
+ const configSamplePath = join(process.cwd(), "config.sample.json");
23
+ const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
24
+ claude?: { model?: unknown; subagentModel?: unknown };
25
+ };
26
+
27
+ expect(sample.claude?.model).toBe("claude-sonnet-4-6");
28
+ expect(sample.claude?.subagentModel).toBe("");
29
+ });
30
+ });
@@ -1,10 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import {
3
- applyClaudeApiMode,
4
- chooseStartPath,
5
- detectClaudeApiMode,
6
- unflattenConfig,
7
- } from "../web-ui.ts";
3
+ applyClaudeApiMode,
4
+ chooseStartPath,
5
+ detectClaudeApiMode,
6
+ unflattenConfig,
7
+ } from "../web-ui.ts";
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
10
  // detectClaudeApiMode — 加载已有 config 时如何判定 UI 初始模式
@@ -57,7 +57,7 @@ describe("detectClaudeApiMode", () => {
57
57
  // - mode 未传 默认按 official 兌底(不保留可能误填的密钥)
58
58
  // ---------------------------------------------------------------------------
59
59
 
60
- describe("applyClaudeApiMode", () => {
60
+ describe("applyClaudeApiMode", () => {
61
61
  it("mode=official 时清空 CLAUDE_API_KEY / CLAUDE_BASE_URL(即使 vars 没传)", () => {
62
62
  const out = applyClaudeApiMode({ CHATCCC_APP_ID: "x" }, "official");
63
63
  expect(out).toEqual({
@@ -128,23 +128,23 @@ describe("applyClaudeApiMode", () => {
128
128
  expect(input).toEqual({ CLAUDE_API_KEY: "sk-x" }); // 原对象未变
129
129
  expect(out).not.toBe(input);
130
130
  });
131
- });
132
-
133
- describe("unflattenConfig", () => {
134
- it("maps Claude subagent model into claude.subagentModel", () => {
135
- expect(
136
- unflattenConfig({
137
- CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
138
- CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
139
- }),
140
- ).toEqual({
141
- claude: {
142
- model: "claude-sonnet-4-6",
143
- subagentModel: "claude-haiku-4-5-20251001",
144
- },
145
- });
146
- });
147
- });
131
+ });
132
+
133
+ describe("unflattenConfig", () => {
134
+ it("maps Claude subagent model into claude.subagentModel", () => {
135
+ expect(
136
+ unflattenConfig({
137
+ CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
138
+ CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
139
+ }),
140
+ ).toEqual({
141
+ claude: {
142
+ model: "claude-sonnet-4-6",
143
+ subagentModel: "claude-haiku-4-5-20251001",
144
+ },
145
+ });
146
+ });
147
+ });
148
148
 
149
149
  // ---------------------------------------------------------------------------
150
150
  // chooseStartPath — /api/start 的路径选择
@@ -1,18 +1,18 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- import { buildHelpCard } from "../cards.ts";
4
- import {
5
- _flushPendingClawFinalTextForTest,
6
- _resetWechatClawStateForTest,
7
- createWechatAdapter,
8
- } from "../wechat-platform.ts";
9
-
10
- describe("createWechatAdapter", () => {
11
- beforeEach(() => {
12
- _resetWechatClawStateForTest();
13
- });
14
-
15
- it("degrades raw cards to plain text messages", async () => {
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { buildHelpCard } from "../cards.ts";
4
+ import {
5
+ _flushPendingClawFinalTextForTest,
6
+ _resetWechatClawStateForTest,
7
+ createWechatAdapter,
8
+ } from "../wechat-platform.ts";
9
+
10
+ describe("createWechatAdapter", () => {
11
+ beforeEach(() => {
12
+ _resetWechatClawStateForTest();
13
+ });
14
+
15
+ it("degrades raw cards to plain text messages", async () => {
16
16
  const wire = {
17
17
  push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
18
18
  sendText: vi.fn(
@@ -38,7 +38,7 @@ describe("createWechatAdapter", () => {
38
38
  expect(log).toHaveBeenCalledWith("[WECHAT] sendRawCard degraded to text");
39
39
  });
40
40
 
41
- it("reports unsent non-final messages after the claw limit", async () => {
41
+ it("reports unsent non-final messages after the claw limit", async () => {
42
42
  const wire = {
43
43
  push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
44
44
  sendText: vi.fn(
@@ -59,41 +59,41 @@ describe("createWechatAdapter", () => {
59
59
  await expect(platform.sendText("wx-chat-limit", "chunk 10")).resolves.toBe(false);
60
60
  expect(wire.push).toHaveBeenCalledTimes(10);
61
61
  expect(log).toHaveBeenCalledWith(
62
- "[WECHAT] sendText skipped (claw limit): chatId=wx-chat-limit count=11",
63
- );
64
- });
65
-
66
- it("queues final messages after the claw limit until the user wakes the chat", async () => {
67
- const wire = {
68
- push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
69
- sendText: vi.fn(
70
- async (_chatId: string, _text: string, _contextToken?: string) =>
71
- "msg-id",
72
- ),
73
- };
74
- const log = vi.fn();
75
- const platform = createWechatAdapter({
76
- getWire: () => wire,
77
- log,
78
- });
79
- const chatId = "wx-chat-final-limit";
80
- const finalText = "done\n━━━ 回答结束 ━━━";
81
-
82
- for (let i = 0; i < 10; i++) {
83
- await expect(platform.sendText(chatId, `chunk ${i}`)).resolves.toBe(true);
84
- }
85
-
86
- await expect(platform.sendText(chatId, finalText)).resolves.toBe(true);
87
- expect(wire.push).toHaveBeenCalledTimes(10);
88
- expect(log).toHaveBeenCalledWith(
89
- `[WECHAT] final queued (claw limit): chatId=${chatId} count=11 len=${finalText.length}`,
90
- );
91
-
92
- await expect(_flushPendingClawFinalTextForTest(chatId, wire, log)).resolves.toBe(true);
93
- expect(wire.push).toHaveBeenCalledTimes(11);
94
- expect(wire.push).toHaveBeenLastCalledWith(chatId, finalText);
95
- expect(log).toHaveBeenCalledWith(
96
- `[WECHAT] pending final sent after claw wake: chatId=${chatId} len=${finalText.length}`,
97
- );
98
- });
99
- });
62
+ "[WECHAT] sendText skipped (claw limit): chatId=wx-chat-limit count=11",
63
+ );
64
+ });
65
+
66
+ it("queues final messages after the claw limit until the user wakes the chat", async () => {
67
+ const wire = {
68
+ push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
69
+ sendText: vi.fn(
70
+ async (_chatId: string, _text: string, _contextToken?: string) =>
71
+ "msg-id",
72
+ ),
73
+ };
74
+ const log = vi.fn();
75
+ const platform = createWechatAdapter({
76
+ getWire: () => wire,
77
+ log,
78
+ });
79
+ const chatId = "wx-chat-final-limit";
80
+ const finalText = "done\n━━━ 回答结束 ━━━";
81
+
82
+ for (let i = 0; i < 10; i++) {
83
+ await expect(platform.sendText(chatId, `chunk ${i}`)).resolves.toBe(true);
84
+ }
85
+
86
+ await expect(platform.sendText(chatId, finalText)).resolves.toBe(true);
87
+ expect(wire.push).toHaveBeenCalledTimes(10);
88
+ expect(log).toHaveBeenCalledWith(
89
+ `[WECHAT] final queued (claw limit): chatId=${chatId} count=11 len=${finalText.length}`,
90
+ );
91
+
92
+ await expect(_flushPendingClawFinalTextForTest(chatId, wire, log)).resolves.toBe(true);
93
+ expect(wire.push).toHaveBeenCalledTimes(11);
94
+ expect(wire.push).toHaveBeenLastCalledWith(chatId, finalText);
95
+ expect(log).toHaveBeenCalledWith(
96
+ `[WECHAT] pending final sent after claw wake: chatId=${chatId} len=${finalText.length}`,
97
+ );
98
+ });
99
+ });
@@ -86,33 +86,33 @@ function buildSdkEnv(
86
86
  baseUrl: string | undefined,
87
87
  subagentModel: string | undefined,
88
88
  ): Record<string, string | undefined> | undefined {
89
- const apiKeyTrim = (apiKey ?? "").trim();
90
- const baseUrlTrim = (baseUrl ?? "").trim();
91
- const subagentModelTrim = (subagentModel ?? "").trim();
92
- const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
93
- if (!hasApiOverride) return undefined;
94
-
95
- const env: Record<string, string | undefined> = { ...process.env };
96
- // ChatCCC's third-party Claude API config is authoritative when present.
97
- // Remove Claude Code/user settings env that can silently override gateway/auth/model choice.
98
- delete env.ANTHROPIC_AUTH_TOKEN;
99
- delete env.CLAUDE_CODE_OAUTH_TOKEN;
100
- delete env.ANTHROPIC_MODEL;
101
- delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
102
- delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
103
- delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
104
- delete env.CLAUDE_CODE_SUBAGENT_MODEL;
105
- delete env.CLAUDE_CODE_EFFORT_LEVEL;
106
-
107
- if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
108
- if (baseUrlTrim) {
109
- env.ANTHROPIC_BASE_URL = baseUrlTrim;
110
- } else {
111
- delete env.ANTHROPIC_BASE_URL;
112
- }
113
- if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
114
- return env;
115
- }
89
+ const apiKeyTrim = (apiKey ?? "").trim();
90
+ const baseUrlTrim = (baseUrl ?? "").trim();
91
+ const subagentModelTrim = (subagentModel ?? "").trim();
92
+ const hasApiOverride = Boolean(apiKeyTrim || baseUrlTrim);
93
+ if (!hasApiOverride) return undefined;
94
+
95
+ const env: Record<string, string | undefined> = { ...process.env };
96
+ // ChatCCC's third-party Claude API config is authoritative when present.
97
+ // Remove Claude Code/user settings env that can silently override gateway/auth/model choice.
98
+ delete env.ANTHROPIC_AUTH_TOKEN;
99
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
100
+ delete env.ANTHROPIC_MODEL;
101
+ delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
102
+ delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
103
+ delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
104
+ delete env.CLAUDE_CODE_SUBAGENT_MODEL;
105
+ delete env.CLAUDE_CODE_EFFORT_LEVEL;
106
+
107
+ if (apiKeyTrim) env.ANTHROPIC_API_KEY = apiKeyTrim;
108
+ if (baseUrlTrim) {
109
+ env.ANTHROPIC_BASE_URL = baseUrlTrim;
110
+ } else {
111
+ delete env.ANTHROPIC_BASE_URL;
112
+ }
113
+ if (subagentModelTrim) env.CLAUDE_CODE_SUBAGENT_MODEL = subagentModelTrim;
114
+ return env;
115
+ }
116
116
 
117
117
  function resolveSettingSources(
118
118
  _apiKey: string | undefined,
@@ -291,19 +291,19 @@ class ClaudeAdapter implements ToolAdapter {
291
291
  this.baseUrl,
292
292
  this.subagentModel,
293
293
  );
294
- const session = unstable_v2_resumeSession(
295
- sessionId,
296
- sessionOpts as any,
297
- );
298
-
299
- if (signal?.aborted) {
300
- session.close();
301
- return;
302
- }
303
- const onAbort = () => { session.close(); };
304
- signal?.addEventListener("abort", onAbort, { once: true });
305
-
306
- await session.send(userText);
294
+ const session = unstable_v2_resumeSession(
295
+ sessionId,
296
+ sessionOpts as any,
297
+ );
298
+
299
+ if (signal?.aborted) {
300
+ session.close();
301
+ return;
302
+ }
303
+ const onAbort = () => { session.close(); };
304
+ signal?.addEventListener("abort", onAbort, { once: true });
305
+
306
+ await session.send(userText);
307
307
 
308
308
  const stream = session.stream();
309
309
 
@@ -224,9 +224,11 @@ export function createWechatAdapter(
224
224
  }
225
225
 
226
226
  try {
227
- await sendWechatTextRaw(wire, chatId, text);
228
- const preview = text.length > 60 ? text.slice(0, 60) + "..." : text;
229
- log(`[WECHAT] sendText OK: chatId=${chatId} len=${text.length} count=${count} text="${preview}"`);
227
+ // 最后一步:非最终回复压缩行数(最多11行:头5 + ... + 尾5)
228
+ const sendText = isFinal ? text : compressGeneratingText(text);
229
+ await sendWechatTextRaw(wire, chatId, sendText);
230
+ const preview = sendText.length > 60 ? sendText.slice(0, 60) + "..." : sendText;
231
+ log(`[WECHAT] sendText OK: chatId=${chatId} len=${sendText.length} count=${count} text="${preview}"`);
230
232
  return true;
231
233
  } catch (err) {
232
234
  log(`[WECHAT] sendText failed: ${(err as Error).message}`);