@gonzih/cc-tg 0.2.2 → 0.2.4
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/dist/bot.d.ts +2 -0
- package/dist/bot.js +83 -2
- package/dist/claude.d.ts +5 -0
- package/dist/claude.js +25 -0
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
package/dist/bot.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* One ClaudeProcess per chat_id — sessions are isolated per user.
|
|
4
4
|
*/
|
|
5
5
|
import TelegramBot from "node-telegram-bot-api";
|
|
6
|
-
import { existsSync } from "fs";
|
|
7
|
-
import { resolve, basename } from "path";
|
|
6
|
+
import { existsSync, createWriteStream, mkdirSync } from "fs";
|
|
7
|
+
import { resolve, basename, join } from "path";
|
|
8
|
+
import https from "https";
|
|
9
|
+
import http from "http";
|
|
8
10
|
import { ClaudeProcess, extractText } from "./claude.js";
|
|
9
11
|
import { transcribeVoice, isVoiceAvailable } from "./voice.js";
|
|
10
12
|
import { CronManager } from "./cron.js";
|
|
@@ -53,6 +55,16 @@ export class CcTgBot {
|
|
|
53
55
|
await this.handleVoice(chatId, msg);
|
|
54
56
|
return;
|
|
55
57
|
}
|
|
58
|
+
// Photo — send as base64 image content block to Claude
|
|
59
|
+
if (msg.photo?.length) {
|
|
60
|
+
await this.handlePhoto(chatId, msg);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Document — download to CWD/.cc-tg/uploads/, tell Claude the path
|
|
64
|
+
if (msg.document) {
|
|
65
|
+
await this.handleDocument(chatId, msg);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
56
68
|
const text = msg.text?.trim();
|
|
57
69
|
if (!text)
|
|
58
70
|
return;
|
|
@@ -120,6 +132,51 @@ export class CcTgBot {
|
|
|
120
132
|
await this.bot.sendMessage(chatId, `Voice transcription failed: ${err.message}`);
|
|
121
133
|
}
|
|
122
134
|
}
|
|
135
|
+
async handlePhoto(chatId, msg) {
|
|
136
|
+
// Pick highest resolution photo
|
|
137
|
+
const photos = msg.photo;
|
|
138
|
+
const best = photos[photos.length - 1];
|
|
139
|
+
const caption = msg.caption?.trim();
|
|
140
|
+
console.log(`[photo:${chatId}] received image file_id=${best.file_id}`);
|
|
141
|
+
this.bot.sendChatAction(chatId, "typing").catch(() => { });
|
|
142
|
+
try {
|
|
143
|
+
const fileLink = await this.bot.getFileLink(best.file_id);
|
|
144
|
+
const imageData = await fetchAsBase64(fileLink);
|
|
145
|
+
// Telegram photos are always JPEG
|
|
146
|
+
const session = this.getOrCreateSession(chatId);
|
|
147
|
+
session.claude.sendImage(imageData, "image/jpeg", caption);
|
|
148
|
+
this.startTyping(chatId, session);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error(`[photo:${chatId}] error:`, err.message);
|
|
152
|
+
await this.bot.sendMessage(chatId, `Failed to process image: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async handleDocument(chatId, msg) {
|
|
156
|
+
const doc = msg.document;
|
|
157
|
+
const caption = msg.caption?.trim();
|
|
158
|
+
const fileName = doc.file_name ?? `file_${doc.file_id}`;
|
|
159
|
+
console.log(`[doc:${chatId}] received document file_name=${fileName} mime=${doc.mime_type}`);
|
|
160
|
+
this.bot.sendChatAction(chatId, "typing").catch(() => { });
|
|
161
|
+
try {
|
|
162
|
+
const uploadsDir = join(this.opts.cwd ?? process.cwd(), ".cc-tg", "uploads");
|
|
163
|
+
mkdirSync(uploadsDir, { recursive: true });
|
|
164
|
+
const destPath = join(uploadsDir, fileName);
|
|
165
|
+
const fileLink = await this.bot.getFileLink(doc.file_id);
|
|
166
|
+
await downloadToFile(fileLink, destPath);
|
|
167
|
+
console.log(`[doc:${chatId}] saved to ${destPath}`);
|
|
168
|
+
const prompt = caption
|
|
169
|
+
? `${caption}\n\nATTACHMENTS: [${fileName}](${destPath})`
|
|
170
|
+
: `ATTACHMENTS: [${fileName}](${destPath})`;
|
|
171
|
+
const session = this.getOrCreateSession(chatId);
|
|
172
|
+
session.claude.sendPrompt(prompt);
|
|
173
|
+
this.startTyping(chatId, session);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
console.error(`[doc:${chatId}] error:`, err.message);
|
|
177
|
+
await this.bot.sendMessage(chatId, `Failed to receive document: ${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
123
180
|
getOrCreateSession(chatId) {
|
|
124
181
|
const existing = this.sessions.get(chatId);
|
|
125
182
|
if (existing && !existing.claude.exited)
|
|
@@ -356,6 +413,30 @@ export class CcTgBot {
|
|
|
356
413
|
}
|
|
357
414
|
}
|
|
358
415
|
}
|
|
416
|
+
/** Download a URL and return its contents as a base64 string */
|
|
417
|
+
function fetchAsBase64(url) {
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
const client = url.startsWith("https") ? https : http;
|
|
420
|
+
client.get(url, (res) => {
|
|
421
|
+
const chunks = [];
|
|
422
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
423
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("base64")));
|
|
424
|
+
res.on("error", reject);
|
|
425
|
+
}).on("error", reject);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/** Download a URL to a local file path */
|
|
429
|
+
function downloadToFile(url, destPath) {
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const client = url.startsWith("https") ? https : http;
|
|
432
|
+
const file = createWriteStream(destPath);
|
|
433
|
+
client.get(url, (res) => {
|
|
434
|
+
res.pipe(file);
|
|
435
|
+
file.on("finish", () => file.close(() => resolve()));
|
|
436
|
+
file.on("error", reject);
|
|
437
|
+
}).on("error", reject);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
359
440
|
function splitMessage(text, maxLen = 4096) {
|
|
360
441
|
if (text.length <= maxLen)
|
|
361
442
|
return [text];
|
package/dist/claude.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare class ClaudeProcess extends EventEmitter {
|
|
|
30
30
|
private _exited;
|
|
31
31
|
constructor(opts?: ClaudeOptions);
|
|
32
32
|
sendPrompt(text: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Send an image (with optional text caption) to Claude via stream-json content blocks.
|
|
35
|
+
* mediaType: image/jpeg | image/png | image/gif | image/webp
|
|
36
|
+
*/
|
|
37
|
+
sendImage(base64Data: string, mediaType: string, caption?: string): void;
|
|
33
38
|
kill(): void;
|
|
34
39
|
get exited(): boolean;
|
|
35
40
|
private drainBuffer;
|
package/dist/claude.js
CHANGED
|
@@ -69,6 +69,31 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
69
69
|
});
|
|
70
70
|
this.proc.stdin.write(payload + "\n");
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Send an image (with optional text caption) to Claude via stream-json content blocks.
|
|
74
|
+
* mediaType: image/jpeg | image/png | image/gif | image/webp
|
|
75
|
+
*/
|
|
76
|
+
sendImage(base64Data, mediaType, caption) {
|
|
77
|
+
if (this._exited)
|
|
78
|
+
throw new Error("Claude process has exited");
|
|
79
|
+
const content = [];
|
|
80
|
+
if (caption) {
|
|
81
|
+
content.push({ type: "text", text: caption });
|
|
82
|
+
}
|
|
83
|
+
content.push({
|
|
84
|
+
type: "image",
|
|
85
|
+
source: {
|
|
86
|
+
type: "base64",
|
|
87
|
+
media_type: mediaType,
|
|
88
|
+
data: base64Data,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const payload = JSON.stringify({
|
|
92
|
+
type: "user",
|
|
93
|
+
message: { role: "user", content },
|
|
94
|
+
});
|
|
95
|
+
this.proc.stdin.write(payload + "\n");
|
|
96
|
+
}
|
|
72
97
|
kill() {
|
|
73
98
|
this.proc.kill();
|
|
74
99
|
}
|