agentp 0.11.2 โ 0.11.3
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/bin/tgagentp +344 -59
- package/docs/specification.md +1 -1
- package/lib/file-share.js +59 -0
- package/package.json +1 -1
package/bin/tgagentp
CHANGED
|
@@ -310,6 +310,8 @@ async function sendLongMessage(token, chatIdOrState, text, replyTo, threadId) {
|
|
|
310
310
|
// Returns empty string if the message isn't a reply or has no text.
|
|
311
311
|
// Uses a dynamic code-fence length (max consecutive backticks + 1)
|
|
312
312
|
// so nested backticks never break the structure.
|
|
313
|
+
const { ensureSharedDir, saveUploadedFile, formatFileSize } = require('../lib/file-share.js');
|
|
314
|
+
|
|
313
315
|
function quoteReply(msg) {
|
|
314
316
|
const rm = msg.reply_to_message;
|
|
315
317
|
if (!rm || !rm.text) return '';
|
|
@@ -326,6 +328,58 @@ function quoteReply(msg) {
|
|
|
326
328
|
return `In response to ${replyFrom} (${senderType}):\n\n${fence}\n${rm.text}\n${fence}\n\n`;
|
|
327
329
|
}
|
|
328
330
|
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// File upload helpers
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
// Download a file from Telegram's file storage.
|
|
336
|
+
// Returns a Buffer of the file contents.
|
|
337
|
+
async function downloadTelegramFile(token, fileId) {
|
|
338
|
+
const fileInfo = await telegramRequest(token, 'getFile', { file_id: fileId });
|
|
339
|
+
if (!fileInfo.ok || !fileInfo.result?.file_path) {
|
|
340
|
+
throw new Error('Failed to get file info from Telegram');
|
|
341
|
+
}
|
|
342
|
+
const filePath = fileInfo.result.file_path;
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
const req = https.get(`https://api.telegram.org/file/bot${token}/${filePath}`, (res) => {
|
|
345
|
+
const chunks = [];
|
|
346
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
347
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
348
|
+
});
|
|
349
|
+
req.on('error', reject);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Handle an uploaded file from Telegram.
|
|
354
|
+
// Downloads the file, saves it, and returns a notification message.
|
|
355
|
+
async function handleFileUpload(token, msg, projectDir) {
|
|
356
|
+
const doc = msg.document;
|
|
357
|
+
const photo = msg.photo;
|
|
358
|
+
if (!doc && !photo) return null;
|
|
359
|
+
|
|
360
|
+
let fileId;
|
|
361
|
+
let fileName;
|
|
362
|
+
let fileSize;
|
|
363
|
+
|
|
364
|
+
if (doc) {
|
|
365
|
+
fileId = doc.file_id;
|
|
366
|
+
fileName = doc.file_name || 'upload.bin';
|
|
367
|
+
fileSize = doc.file_size || 0;
|
|
368
|
+
} else if (photo) {
|
|
369
|
+
// Use the largest photo
|
|
370
|
+
const largest = photo[photo.length - 1];
|
|
371
|
+
fileId = largest.file_id;
|
|
372
|
+
fileName = 'photo.jpg';
|
|
373
|
+
fileSize = largest.file_size || 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const fileData = await downloadTelegramFile(token, fileId);
|
|
377
|
+
ensureSharedDir(projectDir);
|
|
378
|
+
const relativePath = saveUploadedFile(projectDir, fileName, fileData);
|
|
379
|
+
const sizeStr = fileSize > 0 ? ` (${formatFileSize(fileSize)})` : '';
|
|
380
|
+
return `๐ File uploaded: \`${relativePath}\`${sizeStr}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
329
383
|
// ---------------------------------------------------------------------------
|
|
330
384
|
// Telegram command handlers
|
|
331
385
|
// ---------------------------------------------------------------------------
|
|
@@ -604,7 +658,17 @@ async function cmdServers(token, chatId, args, chatState, setServerForChat, setA
|
|
|
604
658
|
}
|
|
605
659
|
const activeUrl = chatState.serverBase ? normalizeUrl(chatState.serverBase) : null;
|
|
606
660
|
const lines = servers.map(s => {
|
|
607
|
-
const
|
|
661
|
+
const normalizedUrl = normalizeUrl(s.url);
|
|
662
|
+
const isActive = activeUrl && normalizedUrl === activeUrl;
|
|
663
|
+
const isOwned = Object.keys(serverOwners).some(url => normalizeUrl(url) === normalizedUrl);
|
|
664
|
+
let bullet;
|
|
665
|
+
if (isActive) {
|
|
666
|
+
bullet = 'โถ ';
|
|
667
|
+
} else if (isOwned) {
|
|
668
|
+
bullet = ' ยท ';
|
|
669
|
+
} else {
|
|
670
|
+
bullet = '๐ ';
|
|
671
|
+
}
|
|
608
672
|
return bullet + `${s.url} โ ${s.dir} (${s.status})`;
|
|
609
673
|
});
|
|
610
674
|
const prefix = chatState.serverBase ? 'Active servers:' : 'Available servers:';
|
|
@@ -809,7 +873,7 @@ async function cmdSessions(token, chatId, args, chatState, setServerForChat, set
|
|
|
809
873
|
|
|
810
874
|
const name = sessionName(s);
|
|
811
875
|
const pos = idx + 1;
|
|
812
|
-
const bullet = s.id === activeId ? 'โถ ' : '
|
|
876
|
+
const bullet = s.id === activeId ? 'โถ ' : '๐ ';
|
|
813
877
|
lines.push('`' + bullet + pos + ' ' + timeLabel + ' โ ' + name.replace(/`/g, "'") + '`');
|
|
814
878
|
});
|
|
815
879
|
|
|
@@ -938,7 +1002,7 @@ function startAgentpGateway(token, serverBaseResolver) {
|
|
|
938
1002
|
return new Promise((resolve) => {
|
|
939
1003
|
const server = http.createServer((req, res) => {
|
|
940
1004
|
try {
|
|
941
|
-
if (req.method !== 'POST'
|
|
1005
|
+
if (req.method !== 'POST') {
|
|
942
1006
|
res.writeHead(405);
|
|
943
1007
|
res.end('Method Not Allowed');
|
|
944
1008
|
return;
|
|
@@ -948,59 +1012,14 @@ function startAgentpGateway(token, serverBaseResolver) {
|
|
|
948
1012
|
res.end('Unauthorized');
|
|
949
1013
|
return;
|
|
950
1014
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
req.
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
const { text, server } = data;
|
|
962
|
-
if (typeof text !== 'string' || typeof server !== 'string') {
|
|
963
|
-
res.writeHead(400);
|
|
964
|
-
res.end('Bad Request: text and server are required');
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Find the chat that owns this server
|
|
969
|
-
const targetUrl = normalizeUrl(server);
|
|
970
|
-
const owningChatId = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === targetUrl)?.[1];
|
|
971
|
-
|
|
972
|
-
// Return recorded conversation buffer for the owning chat
|
|
973
|
-
const buffered = owningChatId ? flushRecorded(owningChatId.chatId, owningChatId.threadId) : [];
|
|
974
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
975
|
-
res.end(JSON.stringify({ buffered }));
|
|
976
|
-
|
|
977
|
-
if (!owningChatId) return;
|
|
978
|
-
const activeUrl = normalizeUrl(serverBaseResolver());
|
|
979
|
-
|
|
980
|
-
if (targetUrl === activeUrl) {
|
|
981
|
-
sendLongMessage(token, owningChatId, text.trim()).catch(() => {});
|
|
982
|
-
} else {
|
|
983
|
-
if (!agentpQueues[server]) {
|
|
984
|
-
agentpQueues[server] = { messages: [], notifyTimer: null, notified: false };
|
|
985
|
-
}
|
|
986
|
-
const q = agentpQueues[server];
|
|
987
|
-
q.messages.push(text);
|
|
988
|
-
if (!q.notifyTimer) {
|
|
989
|
-
q.notifyTimer = setTimeout(() => {
|
|
990
|
-
q.notifyTimer = null;
|
|
991
|
-
if (q.messages.length > 0 && !q.notified) {
|
|
992
|
-
q.notified = true;
|
|
993
|
-
sendLongMessage(token, owningChatId,
|
|
994
|
-
`๐ฅ ${q.messages.length} agentp message(s) queued for server \`${server}\`. Switch servers to view them.`)
|
|
995
|
-
.catch(() => {});
|
|
996
|
-
}
|
|
997
|
-
}, TGAGENTP_DEBOUNCE_MS);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
} catch (err) {
|
|
1001
|
-
log.error(`Agentp gateway handler error: ${err.message}`);
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1015
|
+
if (req.url === '/send') {
|
|
1016
|
+
handleSendRequest(req, res, token, serverBaseResolver);
|
|
1017
|
+
} else if (req.url === '/send-file') {
|
|
1018
|
+
handleSendFileRequest(req, res, token, serverBaseResolver);
|
|
1019
|
+
} else {
|
|
1020
|
+
res.writeHead(404);
|
|
1021
|
+
res.end('Not Found');
|
|
1022
|
+
}
|
|
1004
1023
|
} catch (err) {
|
|
1005
1024
|
log.error(`Agentp gateway handler error: ${err.message}`);
|
|
1006
1025
|
}
|
|
@@ -1017,6 +1036,193 @@ function startAgentpGateway(token, serverBaseResolver) {
|
|
|
1017
1036
|
});
|
|
1018
1037
|
}
|
|
1019
1038
|
|
|
1039
|
+
function handleSendRequest(req, res, token, serverBaseResolver) {
|
|
1040
|
+
let body = '';
|
|
1041
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
1042
|
+
req.on('end', () => {
|
|
1043
|
+
try {
|
|
1044
|
+
let data;
|
|
1045
|
+
try { data = JSON.parse(body); } catch {
|
|
1046
|
+
res.writeHead(400);
|
|
1047
|
+
res.end('Bad Request');
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const { text, server } = data;
|
|
1051
|
+
if (typeof text !== 'string' || typeof server !== 'string') {
|
|
1052
|
+
res.writeHead(400);
|
|
1053
|
+
res.end('Bad Request: text and server are required');
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Find the chat that owns this server
|
|
1058
|
+
const targetUrl = normalizeUrl(server);
|
|
1059
|
+
const owningChatId = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === targetUrl)?.[1];
|
|
1060
|
+
|
|
1061
|
+
// Return recorded conversation buffer for the owning chat
|
|
1062
|
+
const buffered = owningChatId ? flushRecorded(owningChatId.chatId, owningChatId.threadId) : [];
|
|
1063
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1064
|
+
res.end(JSON.stringify({ buffered }));
|
|
1065
|
+
|
|
1066
|
+
if (!owningChatId) return;
|
|
1067
|
+
const activeUrl = normalizeUrl(serverBaseResolver());
|
|
1068
|
+
|
|
1069
|
+
if (targetUrl === activeUrl) {
|
|
1070
|
+
sendLongMessage(token, owningChatId, text.trim()).catch(() => {});
|
|
1071
|
+
} else {
|
|
1072
|
+
if (!agentpQueues[server]) {
|
|
1073
|
+
agentpQueues[server] = { messages: [], notifyTimer: null, notified: false };
|
|
1074
|
+
}
|
|
1075
|
+
const q = agentpQueues[server];
|
|
1076
|
+
q.messages.push(text);
|
|
1077
|
+
if (!q.notifyTimer) {
|
|
1078
|
+
q.notifyTimer = setTimeout(() => {
|
|
1079
|
+
q.notifyTimer = null;
|
|
1080
|
+
if (q.messages.length > 0 && !q.notified) {
|
|
1081
|
+
q.notified = true;
|
|
1082
|
+
sendLongMessage(token, owningChatId,
|
|
1083
|
+
`๐ฅ ${q.messages.length} agentp message(s) queued for server \`${server}\`. Switch servers to view them.`)
|
|
1084
|
+
.catch(() => {});
|
|
1085
|
+
}
|
|
1086
|
+
}, TGAGENTP_DEBOUNCE_MS);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
log.error(`Agentp gateway handler error: ${err.message}`);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function handleSendFileRequest(req, res, token, serverBaseResolver) {
|
|
1096
|
+
let body = '';
|
|
1097
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
1098
|
+
req.on('end', () => {
|
|
1099
|
+
try {
|
|
1100
|
+
let data;
|
|
1101
|
+
try { data = JSON.parse(body); } catch {
|
|
1102
|
+
res.writeHead(400);
|
|
1103
|
+
res.end('Bad Request');
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const { filePath, server, message } = data;
|
|
1107
|
+
if (typeof filePath !== 'string' || typeof server !== 'string') {
|
|
1108
|
+
res.writeHead(400);
|
|
1109
|
+
res.end('Bad Request: filePath and server are required');
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Find the chat that owns this server
|
|
1114
|
+
const targetUrl = normalizeUrl(server);
|
|
1115
|
+
const owningChatId = Object.entries(serverOwners).find(([url]) => normalizeUrl(url) === targetUrl)?.[1];
|
|
1116
|
+
|
|
1117
|
+
if (!owningChatId) {
|
|
1118
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1119
|
+
res.end(JSON.stringify({ sent: false, reason: 'no owner' }));
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Resolve the file path relative to the server directory
|
|
1124
|
+
const dir = serverDir(server);
|
|
1125
|
+
if (!dir) {
|
|
1126
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1127
|
+
res.end(JSON.stringify({ sent: false, reason: 'no server dir' }));
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const fullPath = path.resolve(dir, filePath);
|
|
1131
|
+
if (!fs.existsSync(fullPath)) {
|
|
1132
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1133
|
+
res.end(JSON.stringify({ sent: false, reason: 'file not found' }));
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const fileData = fs.readFileSync(fullPath);
|
|
1138
|
+
const fileName = path.basename(fullPath);
|
|
1139
|
+
|
|
1140
|
+
// Send file to Telegram
|
|
1141
|
+
sendTelegramDocument(token, owningChatId, fileData, fileName, message).then(() => {
|
|
1142
|
+
// Clean up: only delete files from telegram-shared/downloads/
|
|
1143
|
+
// to prevent accidental deletion of project files
|
|
1144
|
+
const downloadsDir = path.join(dir, 'telegram-shared', 'downloads');
|
|
1145
|
+
if (fullPath.startsWith(downloadsDir + path.sep)) {
|
|
1146
|
+
try { fs.unlinkSync(fullPath); } catch (_) {}
|
|
1147
|
+
}
|
|
1148
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1149
|
+
res.end(JSON.stringify({ sent: true }));
|
|
1150
|
+
}).catch((err) => {
|
|
1151
|
+
log.error(`Failed to send file: ${err.message}`);
|
|
1152
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1153
|
+
res.end(JSON.stringify({ sent: false, reason: err.message }));
|
|
1154
|
+
});
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
log.error(`Agentp gateway handler error: ${err.message}`);
|
|
1157
|
+
res.writeHead(500);
|
|
1158
|
+
res.end('Internal Server Error');
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Send a document to Telegram using multipart/form-data.
|
|
1164
|
+
async function sendTelegramDocument(token, chatIdOrState, fileData, fileName, caption) {
|
|
1165
|
+
const chatId = typeof chatIdOrState === 'object' && chatIdOrState.chatId != null
|
|
1166
|
+
? chatIdOrState.chatId : chatIdOrState;
|
|
1167
|
+
const threadId = typeof chatIdOrState === 'object' ? chatIdOrState.threadId : undefined;
|
|
1168
|
+
|
|
1169
|
+
const boundary = '----tgagentpBoundary' + Math.random().toString(36).slice(2);
|
|
1170
|
+
const parts = [];
|
|
1171
|
+
|
|
1172
|
+
// chat_id field
|
|
1173
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n${chatId}\r\n`));
|
|
1174
|
+
|
|
1175
|
+
// message_thread_id field (if present)
|
|
1176
|
+
if (threadId) {
|
|
1177
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="message_thread_id"\r\n\r\n${threadId}\r\n`));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// document field (the file)
|
|
1181
|
+
const mimeType = 'application/octet-stream';
|
|
1182
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="document"; filename="${fileName}"\r\nContent-Type: ${mimeType}\r\n\r\n`));
|
|
1183
|
+
parts.push(fileData);
|
|
1184
|
+
parts.push(Buffer.from('\r\n'));
|
|
1185
|
+
|
|
1186
|
+
// caption field (optional)
|
|
1187
|
+
if (caption) {
|
|
1188
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\n${caption}\r\n`));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// closing boundary
|
|
1192
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`));
|
|
1193
|
+
|
|
1194
|
+
const body = Buffer.concat(parts);
|
|
1195
|
+
|
|
1196
|
+
return new Promise((resolve, reject) => {
|
|
1197
|
+
const options = {
|
|
1198
|
+
hostname: 'api.telegram.org',
|
|
1199
|
+
port: 443,
|
|
1200
|
+
path: `/bot${token}/sendDocument`,
|
|
1201
|
+
method: 'POST',
|
|
1202
|
+
headers: {
|
|
1203
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
1204
|
+
'Content-Length': body.length,
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
const req = https.request(options, (res) => {
|
|
1208
|
+
let data = '';
|
|
1209
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
1210
|
+
res.on('end', () => {
|
|
1211
|
+
try {
|
|
1212
|
+
const result = JSON.parse(data);
|
|
1213
|
+
if (result.ok) resolve(result);
|
|
1214
|
+
else reject(new Error(result.description || 'Telegram API error'));
|
|
1215
|
+
} catch (e) {
|
|
1216
|
+
reject(new Error(`Failed to parse Telegram response: ${data}`));
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
req.on('error', reject);
|
|
1221
|
+
req.write(body);
|
|
1222
|
+
req.end();
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1020
1226
|
async function cmdAllow(token, chatId, args, chatState, setServerForChat, setActiveSessionForChat) {
|
|
1021
1227
|
const st = getState(chatState.serverBase);
|
|
1022
1228
|
if (!st.pendingPermission) {
|
|
@@ -2001,12 +2207,9 @@ async function main() {
|
|
|
2001
2207
|
continue;
|
|
2002
2208
|
}
|
|
2003
2209
|
|
|
2004
|
-
if (typeof msg.text !== 'string') continue;
|
|
2005
|
-
|
|
2006
2210
|
const chatId = String(msg.chat.id);
|
|
2007
2211
|
const threadId = msg.message_thread_id || null;
|
|
2008
2212
|
const replyTo = msg.message_id;
|
|
2009
|
-
const text = msg.text;
|
|
2010
2213
|
const from = msg.from?.username
|
|
2011
2214
|
? `@${msg.from.username}`
|
|
2012
2215
|
: [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(' ') || 'unknown';
|
|
@@ -2027,6 +2230,88 @@ async function main() {
|
|
|
2027
2230
|
// Per-chat state: get or create for this chat (thread-aware)
|
|
2028
2231
|
const cs = getChatState(chatId, threadId);
|
|
2029
2232
|
|
|
2233
|
+
// Handle file uploads (documents and photos)
|
|
2234
|
+
if (msg.document || msg.photo) {
|
|
2235
|
+
if (!cs.serverBase) {
|
|
2236
|
+
await sendLongMessage(token, cs, '๐ Not connected to any server. Upload ignored.');
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const projectDir = serverDir(cs.serverBase);
|
|
2240
|
+
if (!projectDir) {
|
|
2241
|
+
await sendLongMessage(token, cs, 'โ Could not find project directory for this server.');
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
try {
|
|
2245
|
+
const notification = await handleFileUpload(token, msg, projectDir);
|
|
2246
|
+
// Verify the file is actually on disk before notifying
|
|
2247
|
+
const relativePath = notification.match(/`([^`]+)`/)?.[1];
|
|
2248
|
+
if (relativePath) {
|
|
2249
|
+
const fullPath = path.join(projectDir, relativePath);
|
|
2250
|
+
let retries = 5;
|
|
2251
|
+
while (!fs.existsSync(fullPath) && retries > 0) {
|
|
2252
|
+
await new Promise(r => setTimeout(r, 100));
|
|
2253
|
+
retries--;
|
|
2254
|
+
}
|
|
2255
|
+
if (!fs.existsSync(fullPath)) {
|
|
2256
|
+
log.error(`File not found after upload: ${fullPath}`);
|
|
2257
|
+
await sendLongMessage(token, cs, `โ File upload failed: file not saved to disk.`);
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
const userText = msg.caption || msg.text || '';
|
|
2262
|
+
const fullMessage = notification + (userText ? '\n\nUser message: ' + userText : '');
|
|
2263
|
+
log.debug(`[chat ${chatId}] ${from}: [file upload]`);
|
|
2264
|
+
msgLog('in', chatId, from, '[file upload]');
|
|
2265
|
+
|
|
2266
|
+
// Buffer the upload notification for /record
|
|
2267
|
+
bufferMessage(chatId, threadId, 'user', fullMessage);
|
|
2268
|
+
|
|
2269
|
+
const st = getState(cs.serverBase);
|
|
2270
|
+
if (st.serverDead) {
|
|
2271
|
+
const alive = await isServerAlive(cs.serverBase);
|
|
2272
|
+
if (alive) st.serverDead = false;
|
|
2273
|
+
}
|
|
2274
|
+
if (st.serverDead) {
|
|
2275
|
+
if (!st.messageQueue) st.messageQueue = [];
|
|
2276
|
+
st.messageQueue.push({ text: fullMessage, replyTo });
|
|
2277
|
+
const qlen = st.messageQueue.length;
|
|
2278
|
+
await sendLongMessage(token, cs,
|
|
2279
|
+
`โ ๏ธ Server \`${cs.serverBase}\` is not reachable.\n\n` +
|
|
2280
|
+
`Your file upload has been queued (#${qlen}). It will be sent when the server becomes available.\n\n` +
|
|
2281
|
+
`**Options:**\n` +
|
|
2282
|
+
`โข Start the server: \`ocmux serve\`\n` +
|
|
2283
|
+
`โข Switch to another server: \`/servers switch <name>\`\n` +
|
|
2284
|
+
`โข Flush the queue: \`/flush\``);
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
if (st.busy) {
|
|
2289
|
+
if (!st.messageQueue) st.messageQueue = [];
|
|
2290
|
+
st.messageQueue.push({ text: fullMessage, replyTo });
|
|
2291
|
+
await sendLongMessage(token, cs, `โณ Server is busy. File upload queued (#${st.messageQueue.length}).`);
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// Auto-switch tmux window
|
|
2296
|
+
const srvList = listServers();
|
|
2297
|
+
const srvMatch = srvList.find(s => normalizeUrl(s.url) === normalizeUrl(cs.serverBase));
|
|
2298
|
+
if (srvMatch) activateServer(srvMatch.dir, srvMatch.index, srvMatch.url);
|
|
2299
|
+
|
|
2300
|
+
st.busy = true;
|
|
2301
|
+
const myTaskId = ++st.taskId;
|
|
2302
|
+
log.debug(` โ processing file upload notification (task ${myTaskId})`);
|
|
2303
|
+
processMessageAsync(chatId, fullMessage, cs.serverBase, !!st.activeSessionId, st.activeSessionId, myTaskId, replyTo, threadId);
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
log.error(`File upload error: ${err.message}`);
|
|
2306
|
+
await sendLongMessage(token, cs, `โ File upload failed: ${err.message}`);
|
|
2307
|
+
}
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (typeof msg.text !== 'string') continue;
|
|
2312
|
+
|
|
2313
|
+
const text = msg.text;
|
|
2314
|
+
|
|
2030
2315
|
log.debug(`[chat ${chatId}] ${from}: ${text}`);
|
|
2031
2316
|
msgLog('in', chatId, from, text);
|
|
2032
2317
|
|
package/docs/specification.md
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Ensure telegram-shared/{uploads,downloads}/ directories exist
|
|
7
|
+
// and telegram-shared/ is in .gitignore.
|
|
8
|
+
function ensureSharedDir(projectDir) {
|
|
9
|
+
const sharedDir = path.join(projectDir, 'telegram-shared');
|
|
10
|
+
const uploadsDir = path.join(sharedDir, 'uploads');
|
|
11
|
+
const downloadsDir = path.join(sharedDir, 'downloads');
|
|
12
|
+
if (!fs.existsSync(uploadsDir)) {
|
|
13
|
+
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
if (!fs.existsSync(downloadsDir)) {
|
|
16
|
+
fs.mkdirSync(downloadsDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
// Ensure .gitignore has telegram-shared/
|
|
19
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
20
|
+
let gitignoreContent = '';
|
|
21
|
+
try {
|
|
22
|
+
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
23
|
+
} catch (_) {
|
|
24
|
+
// .gitignore doesn't exist, will create it
|
|
25
|
+
}
|
|
26
|
+
if (!gitignoreContent.split('\n').some(line => line.trim() === 'telegram-shared/' || line.trim() === 'telegram-shared')) {
|
|
27
|
+
const append = gitignoreContent.endsWith('\n') || gitignoreContent === '' ? '' : '\n';
|
|
28
|
+
fs.writeFileSync(gitignorePath, gitignoreContent + append + 'telegram-shared/\n');
|
|
29
|
+
}
|
|
30
|
+
return sharedDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Save an uploaded file to the project's telegram-shared/uploads/ directory.
|
|
34
|
+
// Returns the relative path (e.g., 'telegram-shared/uploads/20250610_143022_notes.md').
|
|
35
|
+
function saveUploadedFile(projectDir, fileName, fileData) {
|
|
36
|
+
const uploadsDir = path.join(projectDir, 'telegram-shared', 'uploads');
|
|
37
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
38
|
+
const safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
39
|
+
const uniqueName = `${timestamp}_${safeName}`;
|
|
40
|
+
const filePath = path.join(uploadsDir, uniqueName);
|
|
41
|
+
fs.writeFileSync(filePath, fileData);
|
|
42
|
+
// Ensure data is flushed to disk so the file is immediately visible
|
|
43
|
+
const fd = fs.openSync(filePath, 'r');
|
|
44
|
+
fs.fsyncSync(fd);
|
|
45
|
+
fs.closeSync(fd);
|
|
46
|
+
return path.join('telegram-shared', 'uploads', uniqueName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatFileSize(bytes) {
|
|
50
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
51
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
52
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
ensureSharedDir,
|
|
57
|
+
saveUploadedFile,
|
|
58
|
+
formatFileSize,
|
|
59
|
+
};
|