claude-telegram-bot 0.3.11 → 0.3.14
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/bot.mjs +38 -4
- package/ctb.mjs +35 -11
- package/package.json +1 -1
package/bot.mjs
CHANGED
|
@@ -794,6 +794,7 @@ async function downloadAttachment(att) {
|
|
|
794
794
|
// ── 메시지 처리 ───────────────────────────────────────────────────────────
|
|
795
795
|
let busy = false;
|
|
796
796
|
const msgQueue = []; // { msg, receivedAt } — busy 중 수신 메시지 대기열
|
|
797
|
+
const mediaGroups = new Map(); // media_group_id → { msgs, timer } — 미디어 그룹 수집 대기
|
|
797
798
|
let currentChild = null; // 실행 중인 claude child process (/stop 용)
|
|
798
799
|
let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
|
|
799
800
|
let prevSessionId; // /stop --reset 복원 대상
|
|
@@ -804,8 +805,8 @@ async function handle(msg) {
|
|
|
804
805
|
if (!chatId) return;
|
|
805
806
|
const l = langOf(msg);
|
|
806
807
|
const text = (msg.text || msg.caption || "").trim();
|
|
807
|
-
const attachment = pickAttachment(msg);
|
|
808
|
-
if (!text && !attachment) return;
|
|
808
|
+
const attachment = msg._mediaGroup ? null : pickAttachment(msg);
|
|
809
|
+
if (!text && !attachment && !msg._mediaGroup?.length) return;
|
|
809
810
|
|
|
810
811
|
// 화이트리스트
|
|
811
812
|
if (!cfg.allowedChatId) {
|
|
@@ -951,7 +952,18 @@ async function handle(msg) {
|
|
|
951
952
|
|
|
952
953
|
try {
|
|
953
954
|
let prompt = text;
|
|
954
|
-
if (
|
|
955
|
+
if (msg._mediaGroup?.length) {
|
|
956
|
+
const notes = [];
|
|
957
|
+
for (const fileId of msg._mediaGroup) {
|
|
958
|
+
try {
|
|
959
|
+
const { dest, name } = await downloadAttachment({ fileId, name: null });
|
|
960
|
+
notes.push(`[Attachment] Absolute path: ${dest} (filename: ${name}). Open it with the Read tool if needed.`);
|
|
961
|
+
} catch (e) {
|
|
962
|
+
await send(chatId, t(l, "attachFail", e.message));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (notes.length) prompt = text ? `${text}\n\n${notes.join("\n")}` : notes.join("\n");
|
|
966
|
+
} else if (attachment) {
|
|
955
967
|
try {
|
|
956
968
|
const { dest, name } = await downloadAttachment(attachment);
|
|
957
969
|
const note = `[Attachment] Absolute path: ${dest} (filename: ${name}). Open it with the Read tool if needed.`;
|
|
@@ -996,6 +1008,28 @@ function drainQueue() {
|
|
|
996
1008
|
return { ...group[group.length - 1].msg, text: merged, caption: undefined };
|
|
997
1009
|
}
|
|
998
1010
|
|
|
1011
|
+
// 미디어 그룹(여러 장 동시 전송) — 1초 대기 후 일괄 처리
|
|
1012
|
+
function mergeMediaGroup(msgs) {
|
|
1013
|
+
const captions = msgs.map((m) => m.caption || "").filter(Boolean);
|
|
1014
|
+
const fileIds = msgs
|
|
1015
|
+
.filter((m) => m.photo?.length)
|
|
1016
|
+
.map((m) => m.photo[m.photo.length - 1].file_id);
|
|
1017
|
+
return { ...msgs[0], text: captions.join("\n"), caption: undefined, _mediaGroup: fileIds };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function dispatch(msg) {
|
|
1021
|
+
const gid = msg.media_group_id;
|
|
1022
|
+
if (!gid) { handle(msg).catch((e) => console.error("Handle error:", e.message)); return; }
|
|
1023
|
+
if (!mediaGroups.has(gid)) mediaGroups.set(gid, { msgs: [], timer: null });
|
|
1024
|
+
const g = mediaGroups.get(gid);
|
|
1025
|
+
g.msgs.push(msg);
|
|
1026
|
+
clearTimeout(g.timer);
|
|
1027
|
+
g.timer = setTimeout(() => {
|
|
1028
|
+
mediaGroups.delete(gid);
|
|
1029
|
+
handle(mergeMediaGroup(g.msgs)).catch((e) => console.error("Handle error:", e.message));
|
|
1030
|
+
}, 1000);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
999
1033
|
// ── 롱폴링 루프 ───────────────────────────────────────────────────────────
|
|
1000
1034
|
async function main() {
|
|
1001
1035
|
console.log("Bot started. Polling Telegram...");
|
|
@@ -1033,7 +1067,7 @@ async function main() {
|
|
|
1033
1067
|
}
|
|
1034
1068
|
for (const upd of res.result) {
|
|
1035
1069
|
offset = upd.update_id + 1;
|
|
1036
|
-
if (upd.message)
|
|
1070
|
+
if (upd.message) dispatch(upd.message);
|
|
1037
1071
|
}
|
|
1038
1072
|
} catch (e) {
|
|
1039
1073
|
console.error("Polling error:", e.message);
|
package/ctb.mjs
CHANGED
|
@@ -17,6 +17,11 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
|
|
|
17
17
|
import { basename, dirname, join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
|
+
import dns from "node:dns";
|
|
21
|
+
import net from "node:net";
|
|
22
|
+
|
|
23
|
+
dns.setDefaultResultOrder("ipv4first");
|
|
24
|
+
if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
|
|
20
25
|
|
|
21
26
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
22
27
|
const args = process.argv.slice(2);
|
|
@@ -28,7 +33,7 @@ const VERSION = (() => {
|
|
|
28
33
|
} catch {
|
|
29
34
|
return "?";
|
|
30
35
|
}
|
|
31
|
-
})()
|
|
36
|
+
})();;
|
|
32
37
|
|
|
33
38
|
function runBot(botArgs) {
|
|
34
39
|
const child = spawn(process.execPath, [join(HERE, "bot.mjs"), ...botArgs], {
|
|
@@ -56,7 +61,7 @@ function resolveConfig(arg) {
|
|
|
56
61
|
return existsSync(join(process.cwd(), arg)) ? join(process.cwd(), arg) : join(HERE, arg);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
function main() {
|
|
64
|
+
async function main() {
|
|
60
65
|
if (a === "-h" || a === "--help") {
|
|
61
66
|
console.log(
|
|
62
67
|
`ctb v${VERSION} — claude-telegram-bot short CLI\n\n` +
|
|
@@ -122,9 +127,20 @@ function main() {
|
|
|
122
127
|
sessionId = JSON.parse(readFileSync(statePath, "utf8")).sessionId;
|
|
123
128
|
} catch {}
|
|
124
129
|
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
if (sessionId) {
|
|
131
|
+
process.stderr.write(`Resuming session: ${sessionId}\n`);
|
|
132
|
+
// 텔레그램 이전 대화와 구분하기 위해 세션에 시작 마커 삽입
|
|
133
|
+
await new Promise((resolve) => {
|
|
134
|
+
const marker = spawn("claude", [
|
|
135
|
+
"--resume", sessionId, "-p", "---ctb:start---", "--output-format", "json",
|
|
136
|
+
], { stdio: ["ignore", "ignore", "ignore"] });
|
|
137
|
+
marker.on("close", resolve);
|
|
138
|
+
marker.on("error", resolve);
|
|
139
|
+
setTimeout(() => { marker.kill(); resolve(); }, 15000);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
127
142
|
|
|
143
|
+
const finalArgs = sessionId ? ["--resume", sessionId, ...claudeArgs] : claudeArgs;
|
|
128
144
|
const child = spawn("claude", finalArgs, { stdio: "inherit" });
|
|
129
145
|
child.on("close", async (code) => {
|
|
130
146
|
cleanup();
|
|
@@ -135,8 +151,8 @@ function main() {
|
|
|
135
151
|
|
|
136
152
|
async function summarizeSession(sid, lang) {
|
|
137
153
|
const langInstruction = lang && lang.startsWith("ko")
|
|
138
|
-
? "한국어로 짧은 구문(10단어 이내)으로
|
|
139
|
-
: "
|
|
154
|
+
? "---ctb:start--- 마커 이후에 이 터미널 세션에서 한 작업을 한국어로 짧은 구문(10단어 이내)으로 요약해줘. 마크다운 없이 텍스트만. 마커 이후 중요한 작업이 없으면 정확히 이렇게만 답해: SKIP"
|
|
155
|
+
: "After the ---ctb:start--- marker in this conversation, what was accomplished in this terminal session? One short phrase, 10 words max, plain text. If nothing significant after the marker, reply: SKIP";
|
|
140
156
|
return new Promise((resolve) => {
|
|
141
157
|
const child = spawn("claude", [
|
|
142
158
|
"--resume", sid,
|
|
@@ -160,14 +176,22 @@ async function notifyTelegram(configPath, sessionId) {
|
|
|
160
176
|
try {
|
|
161
177
|
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
|
162
178
|
if (!cfg.token || !cfg.allowedChatId || cfg.ctbNotify === false) return;
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
await
|
|
179
|
+
const lang = cfg.lang || process.env.LANG || "";
|
|
180
|
+
process.stderr.write("ctb: summarizing session...\n");
|
|
181
|
+
const summary = await summarizeSession(sessionId, lang);
|
|
182
|
+
if (!summary) { process.stderr.write("ctb: nothing to summarize (SKIP)\n"); return; }
|
|
183
|
+
process.stderr.write(`ctb: sending to Telegram — ${summary}\n`);
|
|
184
|
+
const label = lang.startsWith("ko") ? "[터미널]" : "[local]";
|
|
185
|
+
const r = await fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
|
|
166
186
|
method: "POST",
|
|
167
187
|
headers: { "content-type": "application/json" },
|
|
168
|
-
body: JSON.stringify({ chat_id: cfg.allowedChatId, text: `💻 ${summary}` }),
|
|
188
|
+
body: JSON.stringify({ chat_id: cfg.allowedChatId, text: `💻 ${label} ${summary}` }),
|
|
169
189
|
});
|
|
170
|
-
|
|
190
|
+
const json = await r.json();
|
|
191
|
+
if (!json.ok) process.stderr.write(`ctb: Telegram error — ${JSON.stringify(json)}\n`);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
process.stderr.write(`ctb: notify error — ${e.message}\n`);
|
|
194
|
+
}
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-telegram-bot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "Drive Claude Code from Telegram — messages run headless `claude -p` in a project dir and replies come back to the chat. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|