chatccc 0.2.61 → 0.2.63
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 +3 -2
- package/im-skills/feishu-skill/download-video.mjs +17 -10
- package/im-skills/wechat-file-skill/receive-send-file.md +42 -0
- package/im-skills/wechat-file-skill/send-file.mjs +101 -0
- package/im-skills/wechat-file-skill/skill.md +11 -0
- package/im-skills/{wechat-skill → wechat-image-skill}/receive-send-image.md +39 -39
- package/im-skills/{wechat-skill → wechat-image-skill}/send-image.mjs +84 -80
- package/im-skills/{wechat-skill → wechat-image-skill}/skill.md +11 -11
- package/im-skills/wechat-video-skill/receive-send-video.md +41 -0
- package/im-skills/wechat-video-skill/send-video.mjs +84 -0
- package/im-skills/wechat-video-skill/skill.md +11 -0
- package/package.json +59 -59
- package/src/__tests__/im-skills.test.ts +57 -2
- package/src/__tests__/orchestrator.test.ts +1 -1
- package/src/__tests__/wechat-platform.test.ts +65 -0
- package/src/config.ts +24 -2
- package/src/index.ts +5 -2
- package/src/orchestrator.ts +4 -3
- package/src/session.ts +7 -3
- package/src/web-ui.ts +26 -3
- package/src/wechat-platform.ts +170 -30
package/config.sample.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"feishu": {
|
|
3
3
|
"appId": "",
|
|
4
|
-
"appSecret": ""
|
|
4
|
+
"appSecret": "",
|
|
5
|
+
"domain": "feishu"
|
|
5
6
|
},
|
|
6
7
|
"platforms": {
|
|
7
8
|
"feishu": { "enabled": true },
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
"claude": {
|
|
14
15
|
"enabled": false,
|
|
15
16
|
"defaultAgent": true,
|
|
16
|
-
"model": "",
|
|
17
|
+
"model": "claude-sonnet-4-6",
|
|
17
18
|
"subagentModel": "",
|
|
18
19
|
"effort": "",
|
|
19
20
|
"apiKey": "",
|
|
@@ -30,6 +30,12 @@ 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
|
+
|
|
33
39
|
async function findConfig() {
|
|
34
40
|
const paths = [
|
|
35
41
|
join(homedir(), ".chatccc", "config.json"),
|
|
@@ -44,7 +50,7 @@ async function findConfig() {
|
|
|
44
50
|
const appSecret = cfg.feishu?.appSecret || "";
|
|
45
51
|
if (appId && appSecret) {
|
|
46
52
|
console.error(`Using config: ${path}`);
|
|
47
|
-
return { appId, appSecret };
|
|
53
|
+
return { appId, appSecret, baseUrl: getBaseUrl(cfg.feishu?.domain) };
|
|
48
54
|
}
|
|
49
55
|
} catch {
|
|
50
56
|
// Try the next candidate.
|
|
@@ -53,8 +59,8 @@ async function findConfig() {
|
|
|
53
59
|
throw new Error(`Could not find Feishu config. Tried: ${paths.slice(0, 3).join(", ")}...`);
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
async function getTenantAccessToken(appId, appSecret) {
|
|
57
|
-
const response = await fetch(
|
|
62
|
+
async function getTenantAccessToken(appId, appSecret, baseUrl) {
|
|
63
|
+
const response = await fetch(`${baseUrl}/auth/v3/tenant_access_token/internal`, {
|
|
58
64
|
method: "POST",
|
|
59
65
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
60
66
|
body: Buffer.from(JSON.stringify({ app_id: appId, app_secret: appSecret }), "utf8"),
|
|
@@ -66,10 +72,10 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
66
72
|
return data.tenant_access_token;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
async function findMessageId(token, chatId, fileKey) {
|
|
75
|
+
async function findMessageId(token, baseUrl, chatId, fileKey) {
|
|
70
76
|
let pageToken = "";
|
|
71
77
|
for (let page = 0; page < 10; page++) {
|
|
72
|
-
const url = new URL(
|
|
78
|
+
const url = new URL(`${baseUrl}/im/v1/messages`);
|
|
73
79
|
url.searchParams.set("receive_id_type", "chat_id");
|
|
74
80
|
url.searchParams.set("receive_id", chatId);
|
|
75
81
|
url.searchParams.set("page_size", "50");
|
|
@@ -101,8 +107,8 @@ function safeFileName(name) {
|
|
|
101
107
|
return (name || "download.bin").replace(/[\\/:*?"<>|]/g, "_");
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
async function downloadResource(token, messageId, fileKey, fileName) {
|
|
105
|
-
const url =
|
|
110
|
+
async function downloadResource(token, baseUrl, messageId, fileKey, fileName) {
|
|
111
|
+
const url = `${baseUrl}/im/v1/messages/${encodeURIComponent(messageId)}/resources/${encodeURIComponent(fileKey)}?type=file`;
|
|
106
112
|
console.error(`Downloading: ${url}`);
|
|
107
113
|
|
|
108
114
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -131,12 +137,12 @@ async function main() {
|
|
|
131
137
|
process.exit(1);
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
const { appId, appSecret } = await findConfig();
|
|
135
|
-
const token = await getTenantAccessToken(appId, appSecret);
|
|
140
|
+
const { appId, appSecret, baseUrl } = await findConfig();
|
|
141
|
+
const token = await getTenantAccessToken(appId, appSecret, baseUrl);
|
|
136
142
|
|
|
137
143
|
let messageId = args["message-id"] || "";
|
|
138
144
|
if (!messageId && args["chat-id"]) {
|
|
139
|
-
messageId = await findMessageId(token, args["chat-id"], args["file-key"]);
|
|
145
|
+
messageId = await findMessageId(token, baseUrl, args["chat-id"], args["file-key"]);
|
|
140
146
|
if (!messageId) {
|
|
141
147
|
throw new Error(`No message found for file_key=${args["file-key"]}`);
|
|
142
148
|
}
|
|
@@ -149,6 +155,7 @@ async function main() {
|
|
|
149
155
|
|
|
150
156
|
const localPath = await downloadResource(
|
|
151
157
|
token,
|
|
158
|
+
baseUrl,
|
|
152
159
|
messageId,
|
|
153
160
|
args["file-key"],
|
|
154
161
|
args.name || "download.bin",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Receiving & Sending Files (WeChat)
|
|
2
|
+
|
|
3
|
+
## Receive Files
|
|
4
|
+
|
|
5
|
+
Files sent to the bot are automatically downloaded to `~/.chatccc/files/downloads/` with the `wx_` filename prefix.
|
|
6
|
+
|
|
7
|
+
The message text you receive will include the downloaded path in the format:
|
|
8
|
+
```text
|
|
9
|
+
[文件] C:\Users\<user>\.chatccc\files\downloads\wx_<key>_<filename>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
You can read the file at that path to inspect, parse, transform, or forward it.
|
|
13
|
+
|
|
14
|
+
## Send Files
|
|
15
|
+
|
|
16
|
+
### Script (recommended)
|
|
17
|
+
```bash
|
|
18
|
+
node "{{wechat_send_file_script}}" --path "<absolute file 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 file 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, fileBuffer, "report.pdf", "caption");
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For non-image and non-video MIME types, the SDK routes the upload as a file attachment.
|
|
33
|
+
|
|
34
|
+
### Rules
|
|
35
|
+
|
|
36
|
+
- Save or choose a local file first.
|
|
37
|
+
- Use an absolute local path.
|
|
38
|
+
- Supported formats: .txt, .pdf, .doc, .docx, .xls, .xlsx, .csv, .ppt, .pptx, .zip, .tar, .gz.
|
|
39
|
+
- Max file size: 30MB.
|
|
40
|
+
- Use the dedicated image or video skill for images and videos.
|
|
41
|
+
- Only send a file when the user asked for one or when it materially helps the answer.
|
|
42
|
+
- File sending counts as an outgoing WeChat message. Avoid repeated unsolicited sends.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, extname, isAbsolute, 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_FILE_BYTES = 30 * 1024 * 1024;
|
|
10
|
+
const ALLOWED_EXTS = new Set([
|
|
11
|
+
".txt",
|
|
12
|
+
".pdf",
|
|
13
|
+
".doc",
|
|
14
|
+
".docx",
|
|
15
|
+
".xls",
|
|
16
|
+
".xlsx",
|
|
17
|
+
".csv",
|
|
18
|
+
".ppt",
|
|
19
|
+
".pptx",
|
|
20
|
+
".zip",
|
|
21
|
+
".tar",
|
|
22
|
+
".gz",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const result = {};
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const key = argv[i];
|
|
29
|
+
if (!key.startsWith("--")) continue;
|
|
30
|
+
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
31
|
+
result[key.slice(2)] = value;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usage() {
|
|
37
|
+
console.error(`Usage:
|
|
38
|
+
node ${basename(process.argv[1])} --path <absolute file path> [--caption <text>]`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const args = parseArgs(process.argv.slice(2));
|
|
43
|
+
const filePath = args.path;
|
|
44
|
+
const caption = args.caption || "";
|
|
45
|
+
|
|
46
|
+
if (!filePath) {
|
|
47
|
+
usage();
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (!isAbsolute(filePath)) {
|
|
51
|
+
console.error("File path must be absolute.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ext = extname(filePath).toLowerCase();
|
|
56
|
+
if (!ALLOWED_EXTS.has(ext)) {
|
|
57
|
+
console.error(`Unsupported file extension: ${ext || "(none)"}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const st = statSync(filePath);
|
|
62
|
+
if (!st.isFile()) {
|
|
63
|
+
console.error("File path is not a file");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
if (st.size <= 0) {
|
|
67
|
+
console.error("File is empty");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
71
|
+
console.error("File exceeds 30MB limit");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!existsSync(ILINK_AUTH_PATH)) {
|
|
76
|
+
console.error(`Auth snapshot not found: ${ILINK_AUTH_PATH}`);
|
|
77
|
+
console.error("Make sure the WeChat iLink platform is logged in.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const snap = JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
|
|
82
|
+
if (!snap.token || !snap.lastChatId || !snap.contextToken) {
|
|
83
|
+
console.error("Auth snapshot missing token, lastChatId, or contextToken.");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fileData = readFileSync(filePath);
|
|
88
|
+
const fileName = basename(filePath);
|
|
89
|
+
|
|
90
|
+
const wire = new OpenIlinkWire(snap.token, {
|
|
91
|
+
base_url: snap.baseUrl,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await wire.sendMediaFile(snap.lastChatId, snap.contextToken, fileData, fileName, caption);
|
|
95
|
+
console.log(JSON.stringify({ ok: true, sentTo: 1 }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch((err) => {
|
|
99
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wechat-file-skill
|
|
3
|
+
description: WeChat iLink local skill for sending and receiving files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Current working directory: {{cwd}}
|
|
7
|
+
|
|
8
|
+
WeChat files are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper script below instead.
|
|
9
|
+
|
|
10
|
+
- **Receive files**: Files sent to the bot are automatically downloaded to `~/.chatccc/files/downloads/` with the filename prefix `wx_`. The message text will include the local path as `[文件] <absolute path>`.
|
|
11
|
+
- **Send files**: Use the send-file helper script — read `{{im_skills_cache_dir}}/wechat-file-skill/receive-send-file.md`
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
# Receiving & Sending Images (WeChat)
|
|
2
|
-
|
|
3
|
-
## Receive Images
|
|
4
|
-
|
|
5
|
-
Images sent to the bot are
|
|
6
|
-
|
|
7
|
-
The message text you receive will include the downloaded path in the format:
|
|
8
|
-
```
|
|
9
|
-
[图片] C:\Users
|
|
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
|
|
38
|
-
- Only send an image when the user asked for one or when it materially helps the answer.
|
|
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
|
+
```text
|
|
9
|
+
[图片] C:\Users\<user>\.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.
|
|
38
|
+
- Only send an image when the user asked for one or when it materially helps the answer.
|
|
39
|
+
- Image sending counts as an outgoing WeChat message. Avoid repeated unsolicited sends.
|
|
@@ -1,80 +1,84 @@
|
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (st.
|
|
50
|
-
console.error("Image
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.
|
|
79
|
-
|
|
80
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, extname, isAbsolute, 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
|
+
if (!isAbsolute(imagePath)) {
|
|
38
|
+
console.error("Image path must be absolute.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ext = extname(imagePath).toLowerCase();
|
|
43
|
+
if (!ALLOWED_EXTS.has(ext)) {
|
|
44
|
+
console.error(`Unsupported image extension: ${ext || "(none)"}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const st = statSync(imagePath);
|
|
49
|
+
if (!st.isFile()) {
|
|
50
|
+
console.error("Image path is not a file");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
if (st.size > MAX_IMAGE_BYTES) {
|
|
54
|
+
console.error("Image file exceeds 10MB limit");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!existsSync(ILINK_AUTH_PATH)) {
|
|
59
|
+
console.error(`Auth snapshot not found: ${ILINK_AUTH_PATH}`);
|
|
60
|
+
console.error("Make sure the WeChat iLink platform is logged in.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const snap = JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
|
|
65
|
+
if (!snap.token || !snap.lastChatId || !snap.contextToken) {
|
|
66
|
+
console.error("Auth snapshot missing token, lastChatId, or contextToken.");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const imgData = readFileSync(imagePath);
|
|
71
|
+
const fileName = basename(imagePath);
|
|
72
|
+
|
|
73
|
+
const wire = new OpenIlinkWire(snap.token, {
|
|
74
|
+
base_url: snap.baseUrl,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await wire.sendMediaFile(snap.lastChatId, snap.contextToken, imgData, fileName, caption);
|
|
78
|
+
console.log(JSON.stringify({ ok: true, sentTo: 1 }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch((err) => {
|
|
82
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wechat-skill
|
|
3
|
-
description: WeChat iLink local
|
|
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
|
|
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
|
-
- **Send images**: Use the send-image helper script — read `{{im_skills_cache_dir}}/wechat-skill/receive-send-image.md`
|
|
1
|
+
---
|
|
2
|
+
name: wechat-image-skill
|
|
3
|
+
description: WeChat iLink local skill 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 script 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
|
+
- **Send images**: Use the send-image helper script — read `{{im_skills_cache_dir}}/wechat-image-skill/receive-send-image.md`
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Receiving & Sending Videos (WeChat)
|
|
2
|
+
|
|
3
|
+
## Receive Videos
|
|
4
|
+
|
|
5
|
+
Videos sent to the bot are automatically downloaded to `~/.chatccc/videos/downloads/` with the `wx_` filename prefix.
|
|
6
|
+
|
|
7
|
+
The message text you receive will include the downloaded path in the format:
|
|
8
|
+
```text
|
|
9
|
+
[视频] C:\Users\<user>\.chatccc\videos\downloads\wx_<key>.mp4
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
You can read the video file at that path to inspect, transcode, trim, or forward it.
|
|
13
|
+
|
|
14
|
+
## Send Videos
|
|
15
|
+
|
|
16
|
+
### Script (recommended)
|
|
17
|
+
```bash
|
|
18
|
+
node "{{wechat_send_video_script}}" --path "<absolute video 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 video 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, videoBuffer, "filename.mp4", "caption");
|
|
30
|
+
```
|
|
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
|
+
### Rules
|
|
35
|
+
|
|
36
|
+
- Save or choose a local video file first.
|
|
37
|
+
- Use an absolute local path.
|
|
38
|
+
- Supported formats: .mp4, .mov, .avi, .mkv, .webm, .flv.
|
|
39
|
+
- Max video size: 30MB.
|
|
40
|
+
- Only send a video when the user asked for one or when it materially helps the answer.
|
|
41
|
+
- Video sending counts as an outgoing WeChat message. Avoid repeated unsolicited sends.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, extname, isAbsolute, 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_VIDEO_BYTES = 30 * 1024 * 1024;
|
|
10
|
+
const ALLOWED_EXTS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv"]);
|
|
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 video path> [--caption <text>]`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
const args = parseArgs(process.argv.slice(2));
|
|
30
|
+
const videoPath = args.path;
|
|
31
|
+
const caption = args.caption || "";
|
|
32
|
+
|
|
33
|
+
if (!videoPath) {
|
|
34
|
+
usage();
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (!isAbsolute(videoPath)) {
|
|
38
|
+
console.error("Video path must be absolute.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ext = extname(videoPath).toLowerCase();
|
|
43
|
+
if (!ALLOWED_EXTS.has(ext)) {
|
|
44
|
+
console.error(`Unsupported video extension: ${ext || "(none)"}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const st = statSync(videoPath);
|
|
49
|
+
if (!st.isFile()) {
|
|
50
|
+
console.error("Video path is not a file");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
if (st.size > MAX_VIDEO_BYTES) {
|
|
54
|
+
console.error("Video file exceeds 30MB limit");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!existsSync(ILINK_AUTH_PATH)) {
|
|
59
|
+
console.error(`Auth snapshot not found: ${ILINK_AUTH_PATH}`);
|
|
60
|
+
console.error("Make sure the WeChat iLink platform is logged in.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const snap = JSON.parse(readFileSync(ILINK_AUTH_PATH, "utf8"));
|
|
65
|
+
if (!snap.token || !snap.lastChatId || !snap.contextToken) {
|
|
66
|
+
console.error("Auth snapshot missing token, lastChatId, or contextToken.");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const videoData = readFileSync(videoPath);
|
|
71
|
+
const fileName = basename(videoPath);
|
|
72
|
+
|
|
73
|
+
const wire = new OpenIlinkWire(snap.token, {
|
|
74
|
+
base_url: snap.baseUrl,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await wire.sendMediaFile(snap.lastChatId, snap.contextToken, videoData, fileName, caption);
|
|
78
|
+
console.log(JSON.stringify({ ok: true, sentTo: 1 }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch((err) => {
|
|
82
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wechat-video-skill
|
|
3
|
+
description: WeChat iLink local skill for sending and receiving videos.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Current working directory: {{cwd}}
|
|
7
|
+
|
|
8
|
+
WeChat videos are handled through the iLink SDK. The local server does NOT have HTTP RPC endpoints for WeChat; use the helper script below instead.
|
|
9
|
+
|
|
10
|
+
- **Receive videos**: Videos sent to the bot are automatically downloaded to `~/.chatccc/videos/downloads/` with the filename prefix `wx_`. The message text will include the local path as `[视频] <absolute path>`.
|
|
11
|
+
- **Send videos**: Use the send-video helper script — read `{{im_skills_cache_dir}}/wechat-video-skill/receive-send-video.md`
|