chatccc 0.2.63 → 0.2.64
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/config.sample.json +1 -2
- package/im-skills/feishu-skill/download-video.mjs +10 -17
- package/im-skills/wechat-file-skill/receive-send-file.md +8 -11
- package/im-skills/wechat-file-skill/send-file.mjs +7 -24
- package/im-skills/wechat-file-skill/skill.md +4 -4
- package/im-skills/wechat-video-skill/receive-send-video.md +7 -9
- package/im-skills/wechat-video-skill/send-video.mjs +4 -8
- package/im-skills/wechat-video-skill/skill.md +4 -4
- package/package.json +59 -59
- package/src/__tests__/agent-platform-routing.test.ts +26 -0
- package/src/__tests__/claude-adapter.test.ts +48 -48
- package/src/__tests__/config-reload.test.ts +14 -14
- package/src/__tests__/config-sample.test.ts +22 -22
- package/src/__tests__/im-skills.test.ts +56 -2
- package/src/__tests__/privacy.test.ts +142 -142
- package/src/__tests__/simplify.test.ts +282 -282
- package/src/__tests__/web-ui.test.ts +23 -23
- package/src/adapters/claude-adapter.ts +40 -40
- package/src/agent-file-rpc.ts +19 -4
- package/src/agent-image-rpc.ts +19 -4
- package/src/agent-platform-routing.ts +28 -0
- package/src/config.ts +2 -24
- package/src/im-skills.ts +9 -0
- package/src/index.ts +11 -6
- package/src/privacy.ts +67 -67
- package/src/session.ts +14 -2
- package/src/simplify.ts +119 -119
- package/src/web-ui.ts +3 -26
package/config.sample.json
CHANGED
|
@@ -30,12 +30,6 @@ function walkUpForConfig(startDir) {
|
|
|
30
30
|
return result;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function getBaseUrl(domain) {
|
|
34
|
-
return domain === "lark"
|
|
35
|
-
? "https://open.larksuite.com/open-apis"
|
|
36
|
-
: "https://open.feishu.cn/open-apis";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
33
|
async function findConfig() {
|
|
40
34
|
const paths = [
|
|
41
35
|
join(homedir(), ".chatccc", "config.json"),
|
|
@@ -50,7 +44,7 @@ async function findConfig() {
|
|
|
50
44
|
const appSecret = cfg.feishu?.appSecret || "";
|
|
51
45
|
if (appId && appSecret) {
|
|
52
46
|
console.error(`Using config: ${path}`);
|
|
53
|
-
return { appId, appSecret
|
|
47
|
+
return { appId, appSecret };
|
|
54
48
|
}
|
|
55
49
|
} catch {
|
|
56
50
|
// Try the next candidate.
|
|
@@ -59,8 +53,8 @@ async function findConfig() {
|
|
|
59
53
|
throw new Error(`Could not find Feishu config. Tried: ${paths.slice(0, 3).join(", ")}...`);
|
|
60
54
|
}
|
|
61
55
|
|
|
62
|
-
async function getTenantAccessToken(appId, appSecret
|
|
63
|
-
const response = await fetch(
|
|
56
|
+
async function getTenantAccessToken(appId, appSecret) {
|
|
57
|
+
const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
|
64
58
|
method: "POST",
|
|
65
59
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
66
60
|
body: Buffer.from(JSON.stringify({ app_id: appId, app_secret: appSecret }), "utf8"),
|
|
@@ -72,10 +66,10 @@ async function getTenantAccessToken(appId, appSecret, baseUrl) {
|
|
|
72
66
|
return data.tenant_access_token;
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
async function findMessageId(token,
|
|
69
|
+
async function findMessageId(token, chatId, fileKey) {
|
|
76
70
|
let pageToken = "";
|
|
77
71
|
for (let page = 0; page < 10; page++) {
|
|
78
|
-
const url = new URL(
|
|
72
|
+
const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
|
|
79
73
|
url.searchParams.set("receive_id_type", "chat_id");
|
|
80
74
|
url.searchParams.set("receive_id", chatId);
|
|
81
75
|
url.searchParams.set("page_size", "50");
|
|
@@ -107,8 +101,8 @@ function safeFileName(name) {
|
|
|
107
101
|
return (name || "download.bin").replace(/[\\/:*?"<>|]/g, "_");
|
|
108
102
|
}
|
|
109
103
|
|
|
110
|
-
async function downloadResource(token,
|
|
111
|
-
const url =
|
|
104
|
+
async function downloadResource(token, messageId, fileKey, fileName) {
|
|
105
|
+
const url = `https://open.feishu.cn/open-apis/im/v1/messages/${encodeURIComponent(messageId)}/resources/${encodeURIComponent(fileKey)}?type=file`;
|
|
112
106
|
console.error(`Downloading: ${url}`);
|
|
113
107
|
|
|
114
108
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -137,12 +131,12 @@ async function main() {
|
|
|
137
131
|
process.exit(1);
|
|
138
132
|
}
|
|
139
133
|
|
|
140
|
-
const { appId, appSecret
|
|
141
|
-
const token = await getTenantAccessToken(appId, appSecret
|
|
134
|
+
const { appId, appSecret } = await findConfig();
|
|
135
|
+
const token = await getTenantAccessToken(appId, appSecret);
|
|
142
136
|
|
|
143
137
|
let messageId = args["message-id"] || "";
|
|
144
138
|
if (!messageId && args["chat-id"]) {
|
|
145
|
-
messageId = await findMessageId(token,
|
|
139
|
+
messageId = await findMessageId(token, args["chat-id"], args["file-key"]);
|
|
146
140
|
if (!messageId) {
|
|
147
141
|
throw new Error(`No message found for file_key=${args["file-key"]}`);
|
|
148
142
|
}
|
|
@@ -155,7 +149,6 @@ async function main() {
|
|
|
155
149
|
|
|
156
150
|
const localPath = await downloadResource(
|
|
157
151
|
token,
|
|
158
|
-
baseUrl,
|
|
159
152
|
messageId,
|
|
160
153
|
args["file-key"],
|
|
161
154
|
args.name || "download.bin",
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Receive Files
|
|
4
4
|
|
|
5
|
-
Files sent to the bot are automatically downloaded to `~/.chatccc/
|
|
5
|
+
Files sent to the bot are **automatically downloaded** to `~/.chatccc/images/downloads/` with the `wx_` filename prefix.
|
|
6
6
|
|
|
7
7
|
The message text you receive will include the downloaded path in the format:
|
|
8
|
-
```
|
|
9
|
-
[文件] C:\Users
|
|
8
|
+
```
|
|
9
|
+
[文件] C:\Users\<用户名>\.chatccc\images\downloads\wx_<key>_<filename>
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
You can read the file at that path to
|
|
12
|
+
You can read the file at that path to understand what the user sent.
|
|
13
13
|
|
|
14
14
|
## Send Files
|
|
15
15
|
|
|
@@ -26,17 +26,14 @@ The underlying SDK call is:
|
|
|
26
26
|
```ts
|
|
27
27
|
import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
|
|
28
28
|
const wire = new OpenIlinkWire(token, { base_url: baseUrl });
|
|
29
|
-
await wire.sendMediaFile(chatId, contextToken, fileBuffer, "
|
|
29
|
+
await wire.sendMediaFile(chatId, contextToken, fileBuffer, "filename.pdf", "caption");
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
For non-image and non-video MIME types, the SDK routes the upload as a file attachment.
|
|
33
|
-
|
|
34
32
|
### Rules
|
|
35
33
|
|
|
36
34
|
- Save or choose a local file first.
|
|
37
35
|
- Use an absolute local path.
|
|
38
|
-
- Supported formats: .
|
|
39
|
-
- Max file size:
|
|
40
|
-
- Use the dedicated image or video skill for images and videos.
|
|
36
|
+
- Supported formats: .pdf, .doc, .docx, .xls, .xlsx, .csv, .ppt, .pptx, .txt, .zip, .tar, .gz, .rar, .7z, .mp3, .wav, .ogg, .aac, .m4a.
|
|
37
|
+
- Max file size: 100MB.
|
|
41
38
|
- Only send a file when the user asked for one or when it materially helps the answer.
|
|
42
|
-
-
|
|
39
|
+
- **Claw 限制**: 文件发送也计入微信 claw 连发计数,连续 10 条未回复会截断。
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { basename, extname,
|
|
3
|
+
import { basename, extname, join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
6
|
import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
|
|
7
7
|
|
|
8
8
|
const ILINK_AUTH_PATH = join(homedir(), ".chatccc", "state", "ilink-auth.json");
|
|
9
|
-
const MAX_FILE_BYTES =
|
|
9
|
+
const MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
10
10
|
const ALLOWED_EXTS = new Set([
|
|
11
|
-
".
|
|
12
|
-
".
|
|
13
|
-
".
|
|
14
|
-
".docx",
|
|
15
|
-
".xls",
|
|
16
|
-
".xlsx",
|
|
17
|
-
".csv",
|
|
18
|
-
".ppt",
|
|
19
|
-
".pptx",
|
|
20
|
-
".zip",
|
|
21
|
-
".tar",
|
|
22
|
-
".gz",
|
|
11
|
+
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".csv", ".ppt", ".pptx",
|
|
12
|
+
".txt", ".zip", ".tar", ".gz", ".rar", ".7z",
|
|
13
|
+
".mp3", ".wav", ".ogg", ".aac", ".m4a",
|
|
23
14
|
]);
|
|
24
15
|
|
|
25
16
|
function parseArgs(argv) {
|
|
@@ -47,10 +38,6 @@ async function main() {
|
|
|
47
38
|
usage();
|
|
48
39
|
process.exit(1);
|
|
49
40
|
}
|
|
50
|
-
if (!isAbsolute(filePath)) {
|
|
51
|
-
console.error("File path must be absolute.");
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
41
|
|
|
55
42
|
const ext = extname(filePath).toLowerCase();
|
|
56
43
|
if (!ALLOWED_EXTS.has(ext)) {
|
|
@@ -63,12 +50,8 @@ async function main() {
|
|
|
63
50
|
console.error("File path is not a file");
|
|
64
51
|
process.exit(1);
|
|
65
52
|
}
|
|
66
|
-
if (st.size <= 0) {
|
|
67
|
-
console.error("File is empty");
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
53
|
if (st.size > MAX_FILE_BYTES) {
|
|
71
|
-
console.error("File exceeds
|
|
54
|
+
console.error("File exceeds 100MB limit");
|
|
72
55
|
process.exit(1);
|
|
73
56
|
}
|
|
74
57
|
|
|
@@ -98,4 +81,4 @@ async function main() {
|
|
|
98
81
|
main().catch((err) => {
|
|
99
82
|
console.error(err instanceof Error ? err.message : String(err));
|
|
100
83
|
process.exit(1);
|
|
101
|
-
});
|
|
84
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wechat-file-skill
|
|
3
|
-
description: WeChat iLink local
|
|
3
|
+
description: WeChat iLink local skills for sending and receiving files.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Current working directory: {{cwd}}
|
|
7
7
|
|
|
8
|
-
WeChat files are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper
|
|
8
|
+
WeChat files are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper scripts below instead.
|
|
9
9
|
|
|
10
|
-
- **Receive files**: Files sent to the bot are automatically downloaded to `~/.chatccc/
|
|
11
|
-
- **Send files**: Use the send-file helper script — read `{{im_skills_cache_dir}}/wechat-file-skill/receive-send-file.md`
|
|
10
|
+
- **Receive files**: Files 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>` — read `{{im_skills_cache_dir}}/wechat-file-skill/receive-send-file.md`
|
|
11
|
+
- **Send files**: Use the send-file helper script — read `{{im_skills_cache_dir}}/wechat-file-skill/receive-send-file.md`
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Receive Videos
|
|
4
4
|
|
|
5
|
-
Videos sent to the bot are automatically downloaded to `~/.chatccc/
|
|
5
|
+
Videos sent to the bot are **automatically downloaded** to `~/.chatccc/images/downloads/` with the `wx_` filename prefix.
|
|
6
6
|
|
|
7
7
|
The message text you receive will include the downloaded path in the format:
|
|
8
|
-
```
|
|
9
|
-
[视频] C:\Users
|
|
8
|
+
```
|
|
9
|
+
[视频] C:\Users\<用户名>\.chatccc\images\downloads\wx_<key>.mp4
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
You can read the video file at that path to
|
|
12
|
+
You can read the video file at that path to understand what the user sent.
|
|
13
13
|
|
|
14
14
|
## Send Videos
|
|
15
15
|
|
|
@@ -26,16 +26,14 @@ The underlying SDK call is:
|
|
|
26
26
|
```ts
|
|
27
27
|
import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
|
|
28
28
|
const wire = new OpenIlinkWire(token, { base_url: baseUrl });
|
|
29
|
-
await wire.sendMediaFile(chatId, contextToken, videoBuffer, "
|
|
29
|
+
await wire.sendMediaFile(chatId, contextToken, videoBuffer, "video.mp4", "caption");
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The SDK detects video MIME types from the file name and routes `.mp4`, `.mov`, `.avi`, `.mkv`, `.webm`, and `.flv` as videos.
|
|
33
|
-
|
|
34
32
|
### Rules
|
|
35
33
|
|
|
36
34
|
- Save or choose a local video file first.
|
|
37
35
|
- Use an absolute local path.
|
|
38
36
|
- Supported formats: .mp4, .mov, .avi, .mkv, .webm, .flv.
|
|
39
|
-
- Max video size:
|
|
37
|
+
- Max video size: 100MB.
|
|
40
38
|
- Only send a video when the user asked for one or when it materially helps the answer.
|
|
41
|
-
-
|
|
39
|
+
- **Claw 限制**: 视频发送也计入微信 claw 连发计数,连续 10 条未回复会截断。
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { basename, extname,
|
|
3
|
+
import { basename, extname, join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
6
|
import { Client as OpenIlinkWire } from "@openilink/openilink-sdk-node";
|
|
7
7
|
|
|
8
8
|
const ILINK_AUTH_PATH = join(homedir(), ".chatccc", "state", "ilink-auth.json");
|
|
9
|
-
const MAX_VIDEO_BYTES =
|
|
9
|
+
const MAX_VIDEO_BYTES = 100 * 1024 * 1024;
|
|
10
10
|
const ALLOWED_EXTS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv"]);
|
|
11
11
|
|
|
12
12
|
function parseArgs(argv) {
|
|
@@ -34,10 +34,6 @@ async function main() {
|
|
|
34
34
|
usage();
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
|
-
if (!isAbsolute(videoPath)) {
|
|
38
|
-
console.error("Video path must be absolute.");
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
37
|
|
|
42
38
|
const ext = extname(videoPath).toLowerCase();
|
|
43
39
|
if (!ALLOWED_EXTS.has(ext)) {
|
|
@@ -51,7 +47,7 @@ async function main() {
|
|
|
51
47
|
process.exit(1);
|
|
52
48
|
}
|
|
53
49
|
if (st.size > MAX_VIDEO_BYTES) {
|
|
54
|
-
console.error("Video file exceeds
|
|
50
|
+
console.error("Video file exceeds 100MB limit");
|
|
55
51
|
process.exit(1);
|
|
56
52
|
}
|
|
57
53
|
|
|
@@ -81,4 +77,4 @@ async function main() {
|
|
|
81
77
|
main().catch((err) => {
|
|
82
78
|
console.error(err instanceof Error ? err.message : String(err));
|
|
83
79
|
process.exit(1);
|
|
84
|
-
});
|
|
80
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wechat-video-skill
|
|
3
|
-
description: WeChat iLink local
|
|
3
|
+
description: WeChat iLink local skills for sending and receiving videos.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Current working directory: {{cwd}}
|
|
7
7
|
|
|
8
|
-
WeChat videos are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper
|
|
8
|
+
WeChat videos are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper scripts below instead.
|
|
9
9
|
|
|
10
|
-
- **Receive videos**: Videos sent to the bot are automatically downloaded to `~/.chatccc/
|
|
11
|
-
- **Send videos**: Use the send-video helper script — read `{{im_skills_cache_dir}}/wechat-video-skill/receive-send-video.md`
|
|
10
|
+
- **Receive videos**: Videos 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>` — read `{{im_skills_cache_dir}}/wechat-video-skill/receive-send-video.md`
|
|
11
|
+
- **Send videos**: Use the send-video helper script — read `{{im_skills_cache_dir}}/wechat-video-skill/receive-send-video.md`
|
package/package.json
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
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.64",
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { splitFeishuTargetChats } from "../agent-platform-routing.ts";
|
|
4
|
+
|
|
5
|
+
describe("agent platform routing", () => {
|
|
6
|
+
it("keeps Feishu-compatible chats and skips WeChat chats for Feishu RPC", () => {
|
|
7
|
+
const result = splitFeishuTargetChats(["fs-1", "wx-1", "unknown-1"], (chatId) => {
|
|
8
|
+
if (chatId.startsWith("wx")) return "wechat";
|
|
9
|
+
if (chatId.startsWith("fs")) return "feishu";
|
|
10
|
+
return undefined;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(result.targetChatIds).toEqual(["fs-1", "unknown-1"]);
|
|
14
|
+
expect(result.skippedUnsupported).toEqual([{ chatId: "wx-1", platformKind: "wechat" }]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("reports no Feishu targets when every bound chat is WeChat", () => {
|
|
18
|
+
const result = splitFeishuTargetChats(["wx-1", "wx-2"], () => "wechat");
|
|
19
|
+
|
|
20
|
+
expect(result.targetChatIds).toEqual([]);
|
|
21
|
+
expect(result.skippedUnsupported).toEqual([
|
|
22
|
+
{ chatId: "wx-1", platformKind: "wechat" },
|
|
23
|
+
{ chatId: "wx-2", platformKind: "wechat" },
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -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");
|