chatccc 0.2.51 → 0.2.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -217
- package/bin/chatccc.mjs +23 -23
- package/config.sample.json +34 -30
- package/demo/ilink_echo_probe.ts +222 -222
- package/im-skills/feishu-skill/download-video.mjs +162 -162
- package/im-skills/feishu-skill/receive-send-file.md +66 -66
- package/im-skills/feishu-skill/receive-send-image.md +27 -27
- package/im-skills/feishu-skill/send-file.mjs +50 -50
- package/im-skills/feishu-skill/send-image.mjs +50 -50
- package/im-skills/feishu-skill/skill.md +10 -10
- package/package.json +59 -59
- package/src/__tests__/agent-image-rpc.test.ts +33 -33
- package/src/__tests__/agent-rpc-body.test.ts +41 -41
- package/src/__tests__/card-plain-text.test.ts +42 -0
- package/src/__tests__/claude-adapter.test.ts +54 -1
- package/src/__tests__/codex-adapter.test.ts +304 -304
- package/src/__tests__/config-reload.test.ts +58 -41
- package/src/__tests__/config-sample.test.ts +19 -0
- package/src/__tests__/crash-logging.test.ts +62 -62
- package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -4
- package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -6
- package/src/__tests__/im-skills.test.ts +46 -46
- package/src/__tests__/session.test.ts +95 -2
- package/src/__tests__/sim-agent.test.ts +173 -173
- package/src/__tests__/sim-platform.test.ts +76 -76
- package/src/__tests__/sim-store.test.ts +213 -213
- package/src/__tests__/stream-state.test.ts +134 -134
- package/src/__tests__/wechat-platform.test.ts +57 -0
- package/src/adapters/claude-adapter.ts +23 -2
- package/src/adapters/codex-adapter.ts +294 -294
- package/src/adapters/codex-session-meta-store.ts +130 -130
- package/src/agent-file-rpc.ts +156 -156
- package/src/agent-image-rpc.ts +152 -152
- package/src/agent-rpc-body.ts +48 -48
- package/src/card-plain-text.ts +108 -0
- package/src/cards.ts +4 -4
- package/src/config.ts +775 -742
- package/src/feishu-api.ts +6 -0
- package/src/im-skills.ts +109 -109
- package/src/index.ts +155 -788
- package/src/orchestrator.ts +1032 -0
- package/src/platform-adapter.ts +61 -0
- package/src/session-chat-binding.ts +7 -0
- package/src/session.ts +278 -113
- package/src/sim-agent.ts +167 -156
- package/src/trace.ts +50 -50
- package/src/web-ui.ts +1891 -1718
- package/src/wechat-platform.ts +517 -0
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const DOWNLOAD_DIR = join(homedir(), ".chatccc", "videos", "downloads");
|
|
9
|
-
|
|
10
|
-
function parseArgs(argv) {
|
|
11
|
-
const result = {};
|
|
12
|
-
for (let i = 0; i < argv.length; i++) {
|
|
13
|
-
const key = argv[i];
|
|
14
|
-
if (!key.startsWith("--")) continue;
|
|
15
|
-
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
16
|
-
result[key.slice(2)] = value;
|
|
17
|
-
}
|
|
18
|
-
return result;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function walkUpForConfig(startDir) {
|
|
22
|
-
const result = [];
|
|
23
|
-
let dir = resolve(startDir);
|
|
24
|
-
for (let i = 0; i < 6; i++) {
|
|
25
|
-
result.push(join(dir, "config.json"));
|
|
26
|
-
const parent = dirname(dir);
|
|
27
|
-
if (parent === dir) break;
|
|
28
|
-
dir = parent;
|
|
29
|
-
}
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function findConfig() {
|
|
34
|
-
const paths = [
|
|
35
|
-
join(homedir(), ".chatccc", "config.json"),
|
|
36
|
-
...walkUpForConfig(SCRIPT_DIR),
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
for (const path of paths) {
|
|
40
|
-
try {
|
|
41
|
-
const raw = await readFile(path, "utf-8");
|
|
42
|
-
const cfg = JSON.parse(raw);
|
|
43
|
-
const appId = cfg.feishu?.appId || "";
|
|
44
|
-
const appSecret = cfg.feishu?.appSecret || "";
|
|
45
|
-
if (appId && appSecret) {
|
|
46
|
-
console.error(`Using config: ${path}`);
|
|
47
|
-
return { appId, appSecret };
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
// Try the next candidate.
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
throw new Error(`Could not find Feishu config. Tried: ${paths.slice(0, 3).join(", ")}...`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function getTenantAccessToken(appId, appSecret) {
|
|
57
|
-
const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
|
58
|
-
method: "POST",
|
|
59
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
60
|
-
body: Buffer.from(JSON.stringify({ app_id: appId, app_secret: appSecret }), "utf8"),
|
|
61
|
-
});
|
|
62
|
-
const data = await response.json();
|
|
63
|
-
if (data.code !== 0) {
|
|
64
|
-
throw new Error(`Failed to get tenant_access_token: [${data.code}] ${data.msg}`);
|
|
65
|
-
}
|
|
66
|
-
return data.tenant_access_token;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function findMessageId(token, chatId, fileKey) {
|
|
70
|
-
let pageToken = "";
|
|
71
|
-
for (let page = 0; page < 10; page++) {
|
|
72
|
-
const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
|
|
73
|
-
url.searchParams.set("receive_id_type", "chat_id");
|
|
74
|
-
url.searchParams.set("receive_id", chatId);
|
|
75
|
-
url.searchParams.set("page_size", "50");
|
|
76
|
-
if (pageToken) url.searchParams.set("page_token", pageToken);
|
|
77
|
-
|
|
78
|
-
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
79
|
-
const data = await response.json();
|
|
80
|
-
if (data.code !== 0) {
|
|
81
|
-
throw new Error(`Failed to list messages: [${data.code}] ${data.msg}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (const item of data.data?.items ?? []) {
|
|
85
|
-
try {
|
|
86
|
-
if (JSON.parse(item.body?.content ?? "{}").file_key === fileKey) {
|
|
87
|
-
return item.message_id;
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
// Ignore non-file messages.
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!data.data?.has_more) break;
|
|
95
|
-
pageToken = data.data?.page_token || "";
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function safeFileName(name) {
|
|
101
|
-
return (name || "download.bin").replace(/[\\/:*?"<>|]/g, "_");
|
|
102
|
-
}
|
|
103
|
-
|
|
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`;
|
|
106
|
-
console.error(`Downloading: ${url}`);
|
|
107
|
-
|
|
108
|
-
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
109
|
-
if (!response.ok) {
|
|
110
|
-
const text = await response.text().catch(() => "");
|
|
111
|
-
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
await mkdir(DOWNLOAD_DIR, { recursive: true });
|
|
115
|
-
const localPath = resolve(DOWNLOAD_DIR, safeFileName(fileName));
|
|
116
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
117
|
-
await writeFile(localPath, buffer);
|
|
118
|
-
return localPath;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function usage() {
|
|
122
|
-
console.error(`Usage:
|
|
123
|
-
node download-video.mjs --message-id <message_id> --file-key <file_key> [--name <file_name>]
|
|
124
|
-
node download-video.mjs --chat-id <chat_id> --file-key <file_key> [--name <file_name>]`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function main() {
|
|
128
|
-
const args = parseArgs(process.argv.slice(2));
|
|
129
|
-
if (!args["file-key"]) {
|
|
130
|
-
usage();
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const { appId, appSecret } = await findConfig();
|
|
135
|
-
const token = await getTenantAccessToken(appId, appSecret);
|
|
136
|
-
|
|
137
|
-
let messageId = args["message-id"] || "";
|
|
138
|
-
if (!messageId && args["chat-id"]) {
|
|
139
|
-
messageId = await findMessageId(token, args["chat-id"], args["file-key"]);
|
|
140
|
-
if (!messageId) {
|
|
141
|
-
throw new Error(`No message found for file_key=${args["file-key"]}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!messageId) {
|
|
146
|
-
usage();
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const localPath = await downloadResource(
|
|
151
|
-
token,
|
|
152
|
-
messageId,
|
|
153
|
-
args["file-key"],
|
|
154
|
-
args.name || "download.bin",
|
|
155
|
-
);
|
|
156
|
-
console.log(localPath);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
main().catch((err) => {
|
|
160
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
161
|
-
process.exit(1);
|
|
162
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const DOWNLOAD_DIR = join(homedir(), ".chatccc", "videos", "downloads");
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const key = argv[i];
|
|
14
|
+
if (!key.startsWith("--")) continue;
|
|
15
|
+
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
16
|
+
result[key.slice(2)] = value;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function walkUpForConfig(startDir) {
|
|
22
|
+
const result = [];
|
|
23
|
+
let dir = resolve(startDir);
|
|
24
|
+
for (let i = 0; i < 6; i++) {
|
|
25
|
+
result.push(join(dir, "config.json"));
|
|
26
|
+
const parent = dirname(dir);
|
|
27
|
+
if (parent === dir) break;
|
|
28
|
+
dir = parent;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function findConfig() {
|
|
34
|
+
const paths = [
|
|
35
|
+
join(homedir(), ".chatccc", "config.json"),
|
|
36
|
+
...walkUpForConfig(SCRIPT_DIR),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const path of paths) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = await readFile(path, "utf-8");
|
|
42
|
+
const cfg = JSON.parse(raw);
|
|
43
|
+
const appId = cfg.feishu?.appId || "";
|
|
44
|
+
const appSecret = cfg.feishu?.appSecret || "";
|
|
45
|
+
if (appId && appSecret) {
|
|
46
|
+
console.error(`Using config: ${path}`);
|
|
47
|
+
return { appId, appSecret };
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Try the next candidate.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Could not find Feishu config. Tried: ${paths.slice(0, 3).join(", ")}...`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function getTenantAccessToken(appId, appSecret) {
|
|
57
|
+
const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
60
|
+
body: Buffer.from(JSON.stringify({ app_id: appId, app_secret: appSecret }), "utf8"),
|
|
61
|
+
});
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
if (data.code !== 0) {
|
|
64
|
+
throw new Error(`Failed to get tenant_access_token: [${data.code}] ${data.msg}`);
|
|
65
|
+
}
|
|
66
|
+
return data.tenant_access_token;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function findMessageId(token, chatId, fileKey) {
|
|
70
|
+
let pageToken = "";
|
|
71
|
+
for (let page = 0; page < 10; page++) {
|
|
72
|
+
const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
|
|
73
|
+
url.searchParams.set("receive_id_type", "chat_id");
|
|
74
|
+
url.searchParams.set("receive_id", chatId);
|
|
75
|
+
url.searchParams.set("page_size", "50");
|
|
76
|
+
if (pageToken) url.searchParams.set("page_token", pageToken);
|
|
77
|
+
|
|
78
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
if (data.code !== 0) {
|
|
81
|
+
throw new Error(`Failed to list messages: [${data.code}] ${data.msg}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const item of data.data?.items ?? []) {
|
|
85
|
+
try {
|
|
86
|
+
if (JSON.parse(item.body?.content ?? "{}").file_key === fileKey) {
|
|
87
|
+
return item.message_id;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Ignore non-file messages.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!data.data?.has_more) break;
|
|
95
|
+
pageToken = data.data?.page_token || "";
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeFileName(name) {
|
|
101
|
+
return (name || "download.bin").replace(/[\\/:*?"<>|]/g, "_");
|
|
102
|
+
}
|
|
103
|
+
|
|
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`;
|
|
106
|
+
console.error(`Downloading: ${url}`);
|
|
107
|
+
|
|
108
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const text = await response.text().catch(() => "");
|
|
111
|
+
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await mkdir(DOWNLOAD_DIR, { recursive: true });
|
|
115
|
+
const localPath = resolve(DOWNLOAD_DIR, safeFileName(fileName));
|
|
116
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
117
|
+
await writeFile(localPath, buffer);
|
|
118
|
+
return localPath;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function usage() {
|
|
122
|
+
console.error(`Usage:
|
|
123
|
+
node download-video.mjs --message-id <message_id> --file-key <file_key> [--name <file_name>]
|
|
124
|
+
node download-video.mjs --chat-id <chat_id> --file-key <file_key> [--name <file_name>]`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function main() {
|
|
128
|
+
const args = parseArgs(process.argv.slice(2));
|
|
129
|
+
if (!args["file-key"]) {
|
|
130
|
+
usage();
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { appId, appSecret } = await findConfig();
|
|
135
|
+
const token = await getTenantAccessToken(appId, appSecret);
|
|
136
|
+
|
|
137
|
+
let messageId = args["message-id"] || "";
|
|
138
|
+
if (!messageId && args["chat-id"]) {
|
|
139
|
+
messageId = await findMessageId(token, args["chat-id"], args["file-key"]);
|
|
140
|
+
if (!messageId) {
|
|
141
|
+
throw new Error(`No message found for file_key=${args["file-key"]}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!messageId) {
|
|
146
|
+
usage();
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const localPath = await downloadResource(
|
|
151
|
+
token,
|
|
152
|
+
messageId,
|
|
153
|
+
args["file-key"],
|
|
154
|
+
args.name || "download.bin",
|
|
155
|
+
);
|
|
156
|
+
console.log(localPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((err) => {
|
|
160
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
# Sending & Downloading Files/Videos
|
|
2
|
-
|
|
3
|
-
## Send Files or Videos
|
|
4
|
-
|
|
5
|
-
Videos are sent as regular files (not media), which looks cleaner in Feishu.
|
|
6
|
-
|
|
7
|
-
### Script (recommended)
|
|
8
|
-
```bash
|
|
9
|
-
node "{{send_file_script}}" --url "{{send_file_url}}" --session-id "{{session_id}}" --path "<absolute file path>" --caption "<optional caption>"
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
### Direct HTTP
|
|
13
|
-
```http
|
|
14
|
-
POST {{send_file_url}}
|
|
15
|
-
Content-Type: application/json; charset=utf-8
|
|
16
|
-
|
|
17
|
-
{"session_id":"{{session_id}}","path":"<absolute file path>","caption":"<optional caption>"}
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### Rules
|
|
21
|
-
|
|
22
|
-
- Save or choose a local file first.
|
|
23
|
-
- Use an absolute local path.
|
|
24
|
-
- Max file size: 30MB.
|
|
25
|
-
- Supported formats: .mp4 .mov .avi .mkv .webm .flv .mp3 .wav .ogg .aac .m4a .pdf .doc .docx .xls .xlsx .csv .ppt .pptx .txt .zip .tar .gz.
|
|
26
|
-
- Only send a file/video when the user asked for one or when it materially helps the answer.
|
|
27
|
-
|
|
28
|
-
### Video Compression (when file > 30MB)
|
|
29
|
-
|
|
30
|
-
If the video exceeds 30MB, compress it with ffmpeg before sending.
|
|
31
|
-
|
|
32
|
-
**Ensure ffmpeg is available** (install if missing):
|
|
33
|
-
|
|
34
|
-
| OS | Install command |
|
|
35
|
-
|----|----------------|
|
|
36
|
-
| macOS | `brew install ffmpeg` |
|
|
37
|
-
| Linux (Debian/Ubuntu) | `sudo apt install ffmpeg` |
|
|
38
|
-
| Linux (RHEL/Fedora) | `sudo dnf install ffmpeg` |
|
|
39
|
-
| Windows | `winget install Gyan.FFmpeg` |
|
|
40
|
-
|
|
41
|
-
**Two-pass compression** (target ~28MB for 30s video, adjust `b:v` for other durations):
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 1 -f mp4 NUL
|
|
45
|
-
ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 2 -c:a aac -b:a 128k "<output>"
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Bitrate formula: `bitrate = 28 × 8 × 1000 ÷ duration_seconds - 128` (target ~28MB, safe under 30MB).
|
|
49
|
-
On Windows replace `NUL` with `NUL` (same); on Linux/macOS use `/dev/null`.
|
|
50
|
-
|
|
51
|
-
If the compressed file still exceeds 30MB, explain to the user that automatic compression wasn't enough and suggest they manually trim or re-encode the source.
|
|
52
|
-
|
|
53
|
-
## Download Files or Videos
|
|
54
|
-
|
|
55
|
-
When the user sends a file or video to the bot, the message contains `message_id` and `file_key`. Download it with:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
node "{{download_video_script}}" --message-id <message_id> --file-key <file_key> --name <file_name>
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
If only `chat_id` and `file_key` are available:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
node "{{download_video_script}}" --chat-id <chat_id> --file-key <file_key> --name <file_name>
|
|
65
|
-
```
|
|
66
|
-
|
|
1
|
+
# Sending & Downloading Files/Videos
|
|
2
|
+
|
|
3
|
+
## Send Files or Videos
|
|
4
|
+
|
|
5
|
+
Videos are sent as regular files (not media), which looks cleaner in Feishu.
|
|
6
|
+
|
|
7
|
+
### Script (recommended)
|
|
8
|
+
```bash
|
|
9
|
+
node "{{send_file_script}}" --url "{{send_file_url}}" --session-id "{{session_id}}" --path "<absolute file path>" --caption "<optional caption>"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Direct HTTP
|
|
13
|
+
```http
|
|
14
|
+
POST {{send_file_url}}
|
|
15
|
+
Content-Type: application/json; charset=utf-8
|
|
16
|
+
|
|
17
|
+
{"session_id":"{{session_id}}","path":"<absolute file path>","caption":"<optional caption>"}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Rules
|
|
21
|
+
|
|
22
|
+
- Save or choose a local file first.
|
|
23
|
+
- Use an absolute local path.
|
|
24
|
+
- Max file size: 30MB.
|
|
25
|
+
- Supported formats: .mp4 .mov .avi .mkv .webm .flv .mp3 .wav .ogg .aac .m4a .pdf .doc .docx .xls .xlsx .csv .ppt .pptx .txt .zip .tar .gz.
|
|
26
|
+
- Only send a file/video when the user asked for one or when it materially helps the answer.
|
|
27
|
+
|
|
28
|
+
### Video Compression (when file > 30MB)
|
|
29
|
+
|
|
30
|
+
If the video exceeds 30MB, compress it with ffmpeg before sending.
|
|
31
|
+
|
|
32
|
+
**Ensure ffmpeg is available** (install if missing):
|
|
33
|
+
|
|
34
|
+
| OS | Install command |
|
|
35
|
+
|----|----------------|
|
|
36
|
+
| macOS | `brew install ffmpeg` |
|
|
37
|
+
| Linux (Debian/Ubuntu) | `sudo apt install ffmpeg` |
|
|
38
|
+
| Linux (RHEL/Fedora) | `sudo dnf install ffmpeg` |
|
|
39
|
+
| Windows | `winget install Gyan.FFmpeg` |
|
|
40
|
+
|
|
41
|
+
**Two-pass compression** (target ~28MB for 30s video, adjust `b:v` for other durations):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 1 -f mp4 NUL
|
|
45
|
+
ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 2 -c:a aac -b:a 128k "<output>"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Bitrate formula: `bitrate = 28 × 8 × 1000 ÷ duration_seconds - 128` (target ~28MB, safe under 30MB).
|
|
49
|
+
On Windows replace `NUL` with `NUL` (same); on Linux/macOS use `/dev/null`.
|
|
50
|
+
|
|
51
|
+
If the compressed file still exceeds 30MB, explain to the user that automatic compression wasn't enough and suggest they manually trim or re-encode the source.
|
|
52
|
+
|
|
53
|
+
## Download Files or Videos
|
|
54
|
+
|
|
55
|
+
When the user sends a file or video to the bot, the message contains `message_id` and `file_key`. Download it with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node "{{download_video_script}}" --message-id <message_id> --file-key <file_key> --name <file_name>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If only `chat_id` and `file_key` are available:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
node "{{download_video_script}}" --chat-id <chat_id> --file-key <file_key> --name <file_name>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
67
|
Downloads are saved under `~/.chatccc/videos/downloads/`.
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
# Sending & Receiving Images
|
|
2
|
-
|
|
3
|
-
## Send Images
|
|
4
|
-
|
|
5
|
-
### Script (recommended)
|
|
6
|
-
```bash
|
|
7
|
-
node "{{send_image_script}}" --url "{{send_image_url}}" --session-id "{{session_id}}" --path "<absolute image path>" --caption "<optional caption>"
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
### Direct HTTP
|
|
11
|
-
```http
|
|
12
|
-
POST {{send_image_url}}
|
|
13
|
-
Content-Type: application/json; charset=utf-8
|
|
14
|
-
|
|
15
|
-
{"session_id":"{{session_id}}","path":"<absolute image path>","caption":"<optional caption>"}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### Rules
|
|
19
|
-
|
|
20
|
-
- Save or choose a local image file first.
|
|
21
|
-
- Use an absolute local path.
|
|
22
|
-
- Supported formats: .png, .jpg, .jpeg, .webp, .gif, .bmp.
|
|
23
|
-
- Max image size: 10MB.
|
|
24
|
-
- Only send an image when the user asked for one or when it materially helps the answer.
|
|
25
|
-
|
|
26
|
-
## Receive Images
|
|
27
|
-
|
|
1
|
+
# Sending & Receiving Images
|
|
2
|
+
|
|
3
|
+
## Send Images
|
|
4
|
+
|
|
5
|
+
### Script (recommended)
|
|
6
|
+
```bash
|
|
7
|
+
node "{{send_image_script}}" --url "{{send_image_url}}" --session-id "{{session_id}}" --path "<absolute image path>" --caption "<optional caption>"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
### Direct HTTP
|
|
11
|
+
```http
|
|
12
|
+
POST {{send_image_url}}
|
|
13
|
+
Content-Type: application/json; charset=utf-8
|
|
14
|
+
|
|
15
|
+
{"session_id":"{{session_id}}","path":"<absolute image path>","caption":"<optional caption>"}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Rules
|
|
19
|
+
|
|
20
|
+
- Save or choose a local image file first.
|
|
21
|
+
- Use an absolute local path.
|
|
22
|
+
- Supported formats: .png, .jpg, .jpeg, .webp, .gif, .bmp.
|
|
23
|
+
- Max image size: 10MB.
|
|
24
|
+
- Only send an image when the user asked for one or when it materially helps the answer.
|
|
25
|
+
|
|
26
|
+
## Receive Images
|
|
27
|
+
|
|
28
28
|
Images sent to the bot are automatically downloaded to `~/.chatccc/images/downloads/`. The message contains an `image_key` that maps to the cached file.
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { basename } from "node:path";
|
|
3
|
-
|
|
4
|
-
function parseArgs(argv) {
|
|
5
|
-
const result = {};
|
|
6
|
-
for (let i = 0; i < argv.length; i++) {
|
|
7
|
-
const key = argv[i];
|
|
8
|
-
if (!key.startsWith("--")) continue;
|
|
9
|
-
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
10
|
-
result[key.slice(2)] = value;
|
|
11
|
-
}
|
|
12
|
-
return result;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function usage() {
|
|
16
|
-
console.error(`Usage:
|
|
17
|
-
node ${basename(process.argv[1])} --url <url> --session-id <session_id> --path <absolute file path> [--caption <text>]`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function main() {
|
|
21
|
-
const args = parseArgs(process.argv.slice(2));
|
|
22
|
-
const url = args.url || process.env.CHATCCC_SEND_FILE_URL;
|
|
23
|
-
const sessionId = args["session-id"] || args.session_id || process.env.CHATCCC_SESSION_ID;
|
|
24
|
-
const path = args.path;
|
|
25
|
-
const caption = args.caption || "";
|
|
26
|
-
|
|
27
|
-
if (!url || !sessionId || !path) {
|
|
28
|
-
usage();
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const body = Buffer.from(JSON.stringify({ session_id: sessionId, path, caption }), "utf8");
|
|
33
|
-
const response = await fetch(url, {
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: {
|
|
36
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
37
|
-
},
|
|
38
|
-
body,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const text = await response.text();
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
44
|
-
}
|
|
45
|
-
console.log(text);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
main().catch((err) => {
|
|
49
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
50
|
-
process.exit(1);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
const result = {};
|
|
6
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7
|
+
const key = argv[i];
|
|
8
|
+
if (!key.startsWith("--")) continue;
|
|
9
|
+
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
10
|
+
result[key.slice(2)] = value;
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function usage() {
|
|
16
|
+
console.error(`Usage:
|
|
17
|
+
node ${basename(process.argv[1])} --url <url> --session-id <session_id> --path <absolute file path> [--caption <text>]`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const args = parseArgs(process.argv.slice(2));
|
|
22
|
+
const url = args.url || process.env.CHATCCC_SEND_FILE_URL;
|
|
23
|
+
const sessionId = args["session-id"] || args.session_id || process.env.CHATCCC_SESSION_ID;
|
|
24
|
+
const path = args.path;
|
|
25
|
+
const caption = args.caption || "";
|
|
26
|
+
|
|
27
|
+
if (!url || !sessionId || !path) {
|
|
28
|
+
usage();
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const body = Buffer.from(JSON.stringify({ session_id: sessionId, path, caption }), "utf8");
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
37
|
+
},
|
|
38
|
+
body,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
44
|
+
}
|
|
45
|
+
console.log(text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main().catch((err) => {
|
|
49
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
50
|
+
process.exit(1);
|
|
51
51
|
});
|