@blunking/codexlink 0.1.16 → 0.1.18
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 +2 -0
- package/package.json +5 -4
- package/start-codex-agent.ps1 +28 -12
- package/telegram-console-input.ps1 +151 -0
- package/telegram-plugin/README.md +4 -3
- package/telegram-plugin/lib/app-server-client.js +141 -7
- package/telegram-plugin/lib/bridge.js +203 -15
- package/telegram-plugin/lib/codex.js +227 -7
- package/telegram-plugin/lib/env.js +1 -0
- package/telegram-plugin/lib/paths.js +2 -1
- package/telegram-plugin/lib/telegram.js +15 -0
- package/telegram-title-embed.ps1 +133 -47
- package/telegram-title-watcher.ps1 +8 -23
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join } from "node:path";
|
|
3
3
|
import { listLoadedThreadsOverWs, readThreadOverWs } from "./app-server-client.js";
|
|
4
4
|
import { loadConfig } from "./env.js";
|
|
5
5
|
import { injectIntoThread, isAddressOnlyPing } from "./codex.js";
|
|
6
|
-
import { getUpdates, sendChatAction, sendMessage } from "./telegram.js";
|
|
6
|
+
import { downloadFileBuffer, getFileInfo, getUpdates, sendChatAction, sendMessage } from "./telegram.js";
|
|
7
7
|
import { appendJsonl, appendLog, defaultState, loadJson, nowIso, readTail, saveJson } from "./storage.js";
|
|
8
8
|
|
|
9
9
|
function loadState(config) {
|
|
@@ -384,6 +384,19 @@ function escapeRegExp(value) {
|
|
|
384
384
|
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
function startsWithAgentAddress(normalized, mention) {
|
|
388
|
+
// normalizeTriggerText strips most punctuation, so this covers "Otto?",
|
|
389
|
+
// "Otto:", "/otto", "!otto", "#otto" and "[otto]" as "otto".
|
|
390
|
+
return new RegExp(`^@?${mention}\\b(?:\\s|$)`, "u").test(normalized);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function startsWithTeamAddressList(normalized, mention) {
|
|
394
|
+
// Group chats often address multiple agents in one breath: "Angel, Otto ...".
|
|
395
|
+
// Keep this conservative: only treat it as direct if Otto is in the first
|
|
396
|
+
// small address block at the beginning, not if Otto is mentioned later.
|
|
397
|
+
return new RegExp(`^(?:@?[a-z][a-z0-9_-]{1,24}\\b\\s+){1,3}@?${mention}\\b(?:\\s|$)`, "u").test(normalized);
|
|
398
|
+
}
|
|
399
|
+
|
|
387
400
|
function looksLikeStatusBroadcast(text) {
|
|
388
401
|
const normalized = foldTriggerText(text);
|
|
389
402
|
if (/^status(?:\s|$|[~:.-])/u.test(normalized)) {
|
|
@@ -408,7 +421,7 @@ function isAgentAddressed(config, text) {
|
|
|
408
421
|
|
|
409
422
|
for (const name of mentionNames) {
|
|
410
423
|
const mention = escapeRegExp(name);
|
|
411
|
-
const startsAddressed =
|
|
424
|
+
const startsAddressed = startsWithAgentAddress(normalized, mention) || startsWithTeamAddressList(normalized, mention);
|
|
412
425
|
if (startsAddressed) {
|
|
413
426
|
return true;
|
|
414
427
|
}
|
|
@@ -423,10 +436,15 @@ function isAgentAddressed(config, text) {
|
|
|
423
436
|
return true;
|
|
424
437
|
}
|
|
425
438
|
|
|
426
|
-
const imperativeAfterMention = new RegExp(`\\b@?${mention}\\b\\s+(?:bitte|please|pull|abruf|abrufen|zieh|hol|hole|pruef|pruf|teste|debugge|fix|patch|mach|setz|starte|antwort|melde)\\b`, "u").test(normalized);
|
|
439
|
+
const imperativeAfterMention = new RegExp(`\\b@?${mention}\\b\\s+(?:bitte|please|du|kannst|kann|sollst|soll|bekommst|bekommt|kriegst|erhaeltst|erhaltst|brauchst|brauche|hilf|unterstuetz|unterstuetze|sag|schick|send|pull|abruf|abrufen|zieh|hol|hole|pruef|pruf|teste|test|debugge|fix|patch|mach|setz|starte|aktivier|antwort|melde|bescheid)\\b`, "u").test(normalized);
|
|
427
440
|
if (imperativeAfterMention) {
|
|
428
441
|
return true;
|
|
429
442
|
}
|
|
443
|
+
|
|
444
|
+
const workDirective = new RegExp(`\\b@?${mention}\\b[\\s\\S]{0,120}\\b(?:bitte|please|du|kannst|kann|sollst|soll|bekommst|bekommt|kriegst|erhaeltst|erhaltst|brauchst|brauche|hilf|unterstuetz|unterstuetze|sag|schick|send|weiter|continue|mach|pull|abruf|abrufen|zieh|hol|hole|pruef|pruf|teste|test|debugge|fix|patch|setz|starte|aktivier|antwort|melde|bescheid|signal|live|stream|chunk|content)\\b|\\b(?:brauchst|brauche|hilf|unterstuetz|unterstuetze|sag|schick|send|weiter|continue|mach|pull|abruf|abrufen|zieh|hol|hole|pruef|pruf|teste|test|debugge|fix|patch|setz|starte|aktivier|antwort|melde|bescheid|signal|live|stream|chunk|content)\\b[\\s\\S]{0,120}\\b@?${mention}\\b`, "u").test(normalized);
|
|
445
|
+
if (workDirective) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
430
448
|
}
|
|
431
449
|
|
|
432
450
|
return false;
|
|
@@ -445,7 +463,7 @@ function isOtherAgentAddressed(config, text) {
|
|
|
445
463
|
|
|
446
464
|
for (const name of mentionNames) {
|
|
447
465
|
const mention = escapeRegExp(name);
|
|
448
|
-
const startsAddressed =
|
|
466
|
+
const startsAddressed = startsWithAgentAddress(normalized, mention) || startsWithTeamAddressList(normalized, mention);
|
|
449
467
|
if (startsAddressed) {
|
|
450
468
|
return true;
|
|
451
469
|
}
|
|
@@ -460,7 +478,7 @@ function isOtherAgentAddressed(config, text) {
|
|
|
460
478
|
return true;
|
|
461
479
|
}
|
|
462
480
|
|
|
463
|
-
const workDirective = new RegExp(`\\b@?${mention}\\b[\\s\\S]{0,
|
|
481
|
+
const workDirective = new RegExp(`\\b@?${mention}\\b[\\s\\S]{0,120}\\b(?:bitte|please|du|kannst|kann|sollst|soll|bekommst|bekommt|kriegst|erhaeltst|erhaltst|brauchst|brauche|hilf|unterstuetz|unterstuetze|sag|schick|send|weiter|continue|mach|pull|abruf|abrufen|zieh|hol|hole|pruef|pruf|teste|test|debugge|fix|patch|setz|starte|aktivier|antwort|melde|bescheid|signal|live|stream|chunk|content|uebersetz|ubersetz|translate)\\b|\\b(?:brauchst|brauche|hilf|unterstuetz|unterstuetze|sag|schick|send|weiter|continue|mach|pull|abruf|abrufen|zieh|hol|hole|pruef|pruf|teste|test|debugge|fix|patch|setz|starte|aktivier|antwort|melde|bescheid|signal|live|stream|chunk|content|uebersetz|ubersetz|translate)\\b[\\s\\S]{0,120}\\b@?${mention}\\b`, "u").test(normalized);
|
|
464
482
|
if (workDirective) {
|
|
465
483
|
return true;
|
|
466
484
|
}
|
|
@@ -1014,6 +1032,135 @@ function mergeStateSnapshots(currentState, incomingState) {
|
|
|
1014
1032
|
return merged;
|
|
1015
1033
|
}
|
|
1016
1034
|
|
|
1035
|
+
const IMAGE_EXTENSIONS = new Set([".avif", ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".webp"]);
|
|
1036
|
+
|
|
1037
|
+
function sanitizeAttachmentName(name) {
|
|
1038
|
+
const raw = basename(String(name || "").replace(/\0/g, "")).replace(/\.\./g, "");
|
|
1039
|
+
const cleaned = raw.replace(/[^0-9A-Za-z._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1040
|
+
return cleaned || "telegram-file.bin";
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function safePathPart(value) {
|
|
1044
|
+
return String(value || "")
|
|
1045
|
+
.replace(/[^0-9A-Za-z_-]+/g, "_")
|
|
1046
|
+
.replace(/^_+|_+$/g, "")
|
|
1047
|
+
|| "telegram";
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function isImageMimeOrName(mimeType, name) {
|
|
1051
|
+
const mime = String(mimeType || "").toLowerCase();
|
|
1052
|
+
if (mime.startsWith("image/")) {
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
return IMAGE_EXTENSIONS.has(extname(String(name || "")).toLowerCase());
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function pickTelegramAttachment(message) {
|
|
1059
|
+
const messageId = String(message?.message_id || Date.now());
|
|
1060
|
+
const photos = Array.isArray(message?.photo) ? message.photo : [];
|
|
1061
|
+
if (photos.length > 0) {
|
|
1062
|
+
const photo = [...photos].sort((left, right) => Number(left?.file_size || 0) - Number(right?.file_size || 0)).pop();
|
|
1063
|
+
if (photo?.file_id) {
|
|
1064
|
+
return {
|
|
1065
|
+
kind: "photo",
|
|
1066
|
+
fileId: String(photo.file_id),
|
|
1067
|
+
fileUniqueId: String(photo.file_unique_id || ""),
|
|
1068
|
+
originalName: `telegram-photo-${messageId}.jpg`,
|
|
1069
|
+
mimeType: "image/jpeg",
|
|
1070
|
+
sizeBytes: Number(photo.file_size || 0),
|
|
1071
|
+
isImage: true
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const document = message?.document;
|
|
1077
|
+
if (document?.file_id) {
|
|
1078
|
+
const originalName = String(document.file_name || `telegram-document-${messageId}`);
|
|
1079
|
+
const mimeType = String(document.mime_type || "application/octet-stream");
|
|
1080
|
+
return {
|
|
1081
|
+
kind: "document",
|
|
1082
|
+
fileId: String(document.file_id),
|
|
1083
|
+
fileUniqueId: String(document.file_unique_id || ""),
|
|
1084
|
+
originalName,
|
|
1085
|
+
mimeType,
|
|
1086
|
+
sizeBytes: Number(document.file_size || 0),
|
|
1087
|
+
isImage: isImageMimeOrName(mimeType, originalName)
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const video = message?.video || message?.animation;
|
|
1092
|
+
if (video?.file_id) {
|
|
1093
|
+
const mimeType = String(video.mime_type || "video/mp4");
|
|
1094
|
+
return {
|
|
1095
|
+
kind: message?.animation ? "animation" : "video",
|
|
1096
|
+
fileId: String(video.file_id),
|
|
1097
|
+
fileUniqueId: String(video.file_unique_id || ""),
|
|
1098
|
+
originalName: String(video.file_name || `telegram-video-${messageId}.mp4`),
|
|
1099
|
+
mimeType,
|
|
1100
|
+
sizeBytes: Number(video.file_size || 0),
|
|
1101
|
+
isImage: false
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function stageTelegramAttachment(config, inbound) {
|
|
1109
|
+
if (!inbound?.attachment?.fileId) {
|
|
1110
|
+
delete inbound.attachment;
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const attachment = inbound.attachment;
|
|
1115
|
+
const maxBytes = Math.max(Number(config.attachmentMaxBytes || 0), 1);
|
|
1116
|
+
try {
|
|
1117
|
+
const file = await getFileInfo(config, attachment.fileId);
|
|
1118
|
+
if (!file?.file_path) {
|
|
1119
|
+
throw new Error("Telegram returned no file path");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const fileSize = Number(file.file_size || attachment.sizeBytes || 0);
|
|
1123
|
+
if (fileSize > maxBytes) {
|
|
1124
|
+
throw new Error(`file too large (${Math.round(fileSize / 1024 / 1024)} MB, max ${Math.round(maxBytes / 1024 / 1024)} MB)`);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const buffer = await downloadFileBuffer(config, file.file_path);
|
|
1128
|
+
if (buffer.byteLength > maxBytes) {
|
|
1129
|
+
throw new Error(`file too large (${Math.round(buffer.byteLength / 1024 / 1024)} MB, max ${Math.round(maxBytes / 1024 / 1024)} MB)`);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const fallbackName = basename(file.file_path) || attachment.originalName || "telegram-file.bin";
|
|
1133
|
+
const originalName = attachment.originalName || fallbackName;
|
|
1134
|
+
const safeName = sanitizeAttachmentName(originalName.includes(".") ? originalName : fallbackName);
|
|
1135
|
+
const dir = join(
|
|
1136
|
+
config.paths.attachmentsDir,
|
|
1137
|
+
`${safePathPart(inbound.chatId)}_${safePathPart(inbound.messageId)}`
|
|
1138
|
+
);
|
|
1139
|
+
mkdirSync(dir, { recursive: true });
|
|
1140
|
+
const localPath = join(dir, safeName);
|
|
1141
|
+
writeFileSync(localPath, buffer);
|
|
1142
|
+
|
|
1143
|
+
inbound.attachments = [{
|
|
1144
|
+
...attachment,
|
|
1145
|
+
originalName,
|
|
1146
|
+
safeName,
|
|
1147
|
+
sizeBytes: buffer.byteLength,
|
|
1148
|
+
telegramFilePath: file.file_path,
|
|
1149
|
+
localPath,
|
|
1150
|
+
isImage: Boolean(attachment.isImage || isImageMimeOrName(attachment.mimeType, safeName))
|
|
1151
|
+
}];
|
|
1152
|
+
appendLog(config.paths.activityFile, `ATTACHMENT_SAVED chat=${inbound.chatId} message=${inbound.messageId} file=${safeName} bytes=${buffer.byteLength}`);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
inbound.attachments = [{
|
|
1155
|
+
...attachment,
|
|
1156
|
+
error: String(error?.message || error)
|
|
1157
|
+
}];
|
|
1158
|
+
appendLog(config.paths.activityFile, `ATTACHMENT_ERROR chat=${inbound.chatId} message=${inbound.messageId}: ${String(error?.message || error)}`);
|
|
1159
|
+
} finally {
|
|
1160
|
+
delete inbound.attachment;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1017
1164
|
function normalizeInbound(message) {
|
|
1018
1165
|
const text = message.text ?? message.caption ?? "";
|
|
1019
1166
|
const chatType = String(message.chat?.type || "unknown");
|
|
@@ -1033,6 +1180,7 @@ function normalizeInbound(message) {
|
|
|
1033
1180
|
user: message.from?.username || message.from?.first_name || "unknown",
|
|
1034
1181
|
userId: message.from?.id ? String(message.from.id) : "",
|
|
1035
1182
|
text,
|
|
1183
|
+
attachment: pickTelegramAttachment(message),
|
|
1036
1184
|
ts: nowIso(),
|
|
1037
1185
|
intent: "message",
|
|
1038
1186
|
relevance: "ambient",
|
|
@@ -1321,7 +1469,13 @@ function shouldPublishInboundUiNotice(entry) {
|
|
|
1321
1469
|
}
|
|
1322
1470
|
|
|
1323
1471
|
function formatCompactInboundUiNotice(entry) {
|
|
1324
|
-
|
|
1472
|
+
let text = normalizeWhitespace(repairMojibake(entry?.text || "")).slice(0, 180);
|
|
1473
|
+
if (!text && Array.isArray(entry?.attachments) && entry.attachments.length > 0) {
|
|
1474
|
+
const first = entry.attachments[0];
|
|
1475
|
+
text = first?.isImage ? "sendete einen Screenshot" : `sendete ${first?.originalName || "eine Datei"}`;
|
|
1476
|
+
} else if (!text && entry?.attachment) {
|
|
1477
|
+
text = entry.attachment.isImage ? "sendete einen Screenshot" : `sendete ${entry.attachment.originalName || "eine Datei"}`;
|
|
1478
|
+
}
|
|
1325
1479
|
if (!text) {
|
|
1326
1480
|
return "";
|
|
1327
1481
|
}
|
|
@@ -1592,8 +1746,17 @@ function normalizeThreadTimestampMs(value) {
|
|
|
1592
1746
|
return numeric > 100000000000 ? numeric : numeric * 1000;
|
|
1593
1747
|
}
|
|
1594
1748
|
|
|
1749
|
+
function parseRuntimeStartedAtMs(runtime) {
|
|
1750
|
+
const parsed = Date.parse(String(runtime?.started_at || ""));
|
|
1751
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1595
1754
|
function buildHistoryText(message) {
|
|
1596
|
-
|
|
1755
|
+
let text = String(message.text || "").trim();
|
|
1756
|
+
if (!text && Array.isArray(message.attachments) && message.attachments.length > 0) {
|
|
1757
|
+
const first = message.attachments[0];
|
|
1758
|
+
text = first?.isImage ? "[Telegram screenshot]" : `[Telegram file: ${first?.originalName || "attachment"}]`;
|
|
1759
|
+
}
|
|
1597
1760
|
const chatType = String(message.chatType || "");
|
|
1598
1761
|
if (chatType === "private" || !chatType) {
|
|
1599
1762
|
return text;
|
|
@@ -1702,6 +1865,7 @@ async function resolveActiveThreadId(config, state, preferredThreadId, options =
|
|
|
1702
1865
|
|
|
1703
1866
|
const runtimeOwner = getRuntimeOwner(config);
|
|
1704
1867
|
const runtimeThreadId = String(runtimeOwner?.runtime?.thread_id || "").trim();
|
|
1868
|
+
const runtimeStartedAtMs = parseRuntimeStartedAtMs(runtimeOwner?.runtime);
|
|
1705
1869
|
const pinnedThreadId = String(preferredThreadId || config.currentThreadId || runtimeThreadId || "").trim();
|
|
1706
1870
|
if (options.forcePreferred && pinnedThreadId && loadedIds.includes(pinnedThreadId)) {
|
|
1707
1871
|
if (state.currentThreadId !== pinnedThreadId) {
|
|
@@ -1732,11 +1896,14 @@ async function resolveActiveThreadId(config, state, preferredThreadId, options =
|
|
|
1732
1896
|
const source = String(thread.source || "").toLowerCase();
|
|
1733
1897
|
const statusType = String(thread.status?.type || "").toLowerCase();
|
|
1734
1898
|
if (source === "cli" && statusType === "active") {
|
|
1735
|
-
score +=
|
|
1899
|
+
score += 4000000000000000;
|
|
1736
1900
|
} else if (statusType === "active") {
|
|
1737
|
-
score +=
|
|
1901
|
+
score += 3000000000000000;
|
|
1738
1902
|
} else if (source === "cli") {
|
|
1739
|
-
score +=
|
|
1903
|
+
score += 2000000000000000;
|
|
1904
|
+
}
|
|
1905
|
+
if (runtimeStartedAtMs > 0 && createdAtMs >= runtimeStartedAtMs - 120000) {
|
|
1906
|
+
score += 1000000000000000;
|
|
1740
1907
|
}
|
|
1741
1908
|
const sessionPath = String(thread.path || "").trim();
|
|
1742
1909
|
if (sessionPath && existsSync(sessionPath)) {
|
|
@@ -1806,7 +1973,7 @@ export function bridgeStatus() {
|
|
|
1806
1973
|
lastPollAt: state.lastPollAt,
|
|
1807
1974
|
lastInjectAt: state.lastInjectAt,
|
|
1808
1975
|
stateDir: config.paths.root,
|
|
1809
|
-
note: "Telegram first lands in queue.
|
|
1976
|
+
note: "Telegram first lands in queue. Direct/private/lane messages are delivered immediately in app-server mode; active turns receive them via turn/steer. Ambient group noise stays parked."
|
|
1810
1977
|
};
|
|
1811
1978
|
}
|
|
1812
1979
|
|
|
@@ -1968,7 +2135,7 @@ export async function pollOnce() {
|
|
|
1968
2135
|
appendLog(config.paths.activityFile, `IGNORED_IDLE_BRIEF chat=${inbound.chatId} message=${inbound.messageId}`);
|
|
1969
2136
|
continue;
|
|
1970
2137
|
}
|
|
1971
|
-
if (!inbound.text.trim()) {
|
|
2138
|
+
if (!inbound.text.trim() && !inbound.attachment) {
|
|
1972
2139
|
ignored += 1;
|
|
1973
2140
|
appendLog(config.paths.activityFile, `IGNORED_EMPTY chat=${inbound.chatId} message=${inbound.messageId}`);
|
|
1974
2141
|
continue;
|
|
@@ -1993,6 +2160,7 @@ export async function pollOnce() {
|
|
|
1993
2160
|
telegramThreadId: inbound.telegramThreadId
|
|
1994
2161
|
}).catch(() => {});
|
|
1995
2162
|
}
|
|
2163
|
+
await stageTelegramAttachment(config, inbound);
|
|
1996
2164
|
state.queue.push(inbound);
|
|
1997
2165
|
state.lastInbound = inbound;
|
|
1998
2166
|
if (shouldPublishInboundUiNotice(inbound)) {
|
|
@@ -2091,6 +2259,15 @@ function isFastDispatchEntry(entry) {
|
|
|
2091
2259
|
return false;
|
|
2092
2260
|
}
|
|
2093
2261
|
|
|
2262
|
+
function isRealtimeAppServerEntry(entry) {
|
|
2263
|
+
if (!entry) {
|
|
2264
|
+
return false;
|
|
2265
|
+
}
|
|
2266
|
+
const relevance = String(entry.relevance || "").trim().toLowerCase();
|
|
2267
|
+
const chatType = String(entry.chatType || "").trim().toLowerCase();
|
|
2268
|
+
return chatType === "private" || relevance === "direct" || relevance === "lane";
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2094
2271
|
export async function injectNext(threadId, options = {}) {
|
|
2095
2272
|
const config = loadConfig();
|
|
2096
2273
|
const state = loadState(config);
|
|
@@ -2188,10 +2365,17 @@ export async function injectNext(threadId, options = {}) {
|
|
|
2188
2365
|
saveStateForConfig(config, state);
|
|
2189
2366
|
}
|
|
2190
2367
|
|
|
2191
|
-
const bypassDeferredGate = auto && (
|
|
2368
|
+
const bypassDeferredGate = auto && (
|
|
2369
|
+
next.relevance === "escalation"
|
|
2370
|
+
|| isFastDispatchEntry(next)
|
|
2371
|
+
|| (useAppServer && isRealtimeAppServerEntry(next))
|
|
2372
|
+
);
|
|
2192
2373
|
if (bypassDeferredGate && auto && isFastDispatchEntry(next)) {
|
|
2193
2374
|
appendLog(config.paths.activityFile, `FAST_TRIGGER_BYPASS chat=${next.chatId} message=${next.messageId} intent=${next.intent}`);
|
|
2194
2375
|
}
|
|
2376
|
+
if (bypassDeferredGate && auto && useAppServer && isRealtimeAppServerEntry(next) && !isFastDispatchEntry(next)) {
|
|
2377
|
+
appendLog(config.paths.activityFile, `REALTIME_BYPASS chat=${next.chatId} message=${next.messageId} relevance=${next.relevance || "-"} chat_type=${next.chatType || "-"}`);
|
|
2378
|
+
}
|
|
2195
2379
|
if (auto && !bypassDeferredGate && String(config.dispatchMode || "deferred").toLowerCase() !== "legacy") {
|
|
2196
2380
|
const openPendingReplies = countOpenPendingReplies(state, config);
|
|
2197
2381
|
if (openPendingReplies > 0) {
|
|
@@ -2296,6 +2480,10 @@ export async function injectNext(threadId, options = {}) {
|
|
|
2296
2480
|
state.lastAutoDispatchAt = auto ? state.lastInjectAt : state.lastAutoDispatchAt;
|
|
2297
2481
|
const latestState = loadState(config);
|
|
2298
2482
|
saveStateForConfig(config, mergeStateSnapshots(latestState, state));
|
|
2483
|
+
const injectPreview = normalizeWhitespace(result.responseText || result.stderr || "").slice(0, 220);
|
|
2484
|
+
if (injectPreview) {
|
|
2485
|
+
appendLog(config.paths.activityFile, `INJECT_RESULT thread=${resolvedThreadId} message=${next.messageId}: ${injectPreview}`);
|
|
2486
|
+
}
|
|
2299
2487
|
appendLog(config.paths.activityFile, `INJECT_${result.ok ? "OK" : "ERROR"} thread=${resolvedThreadId} message=${next.messageId}`);
|
|
2300
2488
|
return {
|
|
2301
2489
|
ok: result.ok,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { startOrSteerTextTurnOverWs } from "./app-server-client.js";
|
|
6
|
+
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const runtimeRoot = join(here, "..", "..");
|
|
5
9
|
|
|
6
10
|
function repairMojibake(value) {
|
|
7
11
|
const input = String(value || "");
|
|
@@ -86,6 +90,9 @@ function summarizeBrief(text) {
|
|
|
86
90
|
function compactInboundText(message) {
|
|
87
91
|
const text = String(message.text || "").trim();
|
|
88
92
|
if (!text) {
|
|
93
|
+
if (Array.isArray(message.attachments) && message.attachments.length > 0) {
|
|
94
|
+
return "hat eine Datei per Telegram gesendet.";
|
|
95
|
+
}
|
|
89
96
|
return "";
|
|
90
97
|
}
|
|
91
98
|
if (/^---\s*BRIEF\b/i.test(text)) {
|
|
@@ -94,6 +101,55 @@ function compactInboundText(message) {
|
|
|
94
101
|
return text;
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
function formatBytes(bytes) {
|
|
105
|
+
const value = Number(bytes || 0);
|
|
106
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
if (value < 1024) {
|
|
110
|
+
return `${Math.round(value)} B`;
|
|
111
|
+
}
|
|
112
|
+
if (value < 1024 * 1024) {
|
|
113
|
+
return `${(value / 1024).toFixed(1)} KB`;
|
|
114
|
+
}
|
|
115
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatAttachmentInstructions(message) {
|
|
119
|
+
const attachments = Array.isArray(message.attachments) ? message.attachments : [];
|
|
120
|
+
if (attachments.length === 0) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lines = [
|
|
125
|
+
"",
|
|
126
|
+
"Telegram-Anhang:"
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const attachment of attachments) {
|
|
130
|
+
if (attachment?.error) {
|
|
131
|
+
lines.push(`- ${attachment.kind || "Datei"} konnte nicht geladen werden: ${attachment.error}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const label = attachment.isImage ? "Bild/Screenshot" : (attachment.kind || "Datei");
|
|
135
|
+
const meta = [
|
|
136
|
+
attachment.mimeType,
|
|
137
|
+
formatBytes(attachment.sizeBytes)
|
|
138
|
+
].filter(Boolean).join(", ");
|
|
139
|
+
const name = attachment.originalName || attachment.safeName || "telegram-file";
|
|
140
|
+
lines.push(`- ${label}: ${name}${meta ? ` (${meta})` : ""}`);
|
|
141
|
+
lines.push(` Lokaler Pfad: ${attachment.localPath}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (attachments.some((attachment) => attachment?.isImage && attachment?.localPath)) {
|
|
145
|
+
lines.push("Die Bilddatei wurde als lokaler Bild-Input an diesen Turn angehaengt. Nutze sie direkt fuer Screenshot-/UI-Analyse.");
|
|
146
|
+
} else {
|
|
147
|
+
lines.push("Nutze die lokalen Pfade, wenn du den Inhalt der Datei pruefen oder weiterverarbeiten sollst.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
152
|
+
|
|
97
153
|
function normalizeAddressText(value) {
|
|
98
154
|
return repairMojibake(String(value || ""))
|
|
99
155
|
.normalize("NFKC")
|
|
@@ -147,6 +203,7 @@ function buildPrompt(config, message) {
|
|
|
147
203
|
header.push(label);
|
|
148
204
|
}
|
|
149
205
|
header.push(compactText);
|
|
206
|
+
header.push(...formatAttachmentInstructions(message));
|
|
150
207
|
|
|
151
208
|
if (message.intent === "continue_nudge") {
|
|
152
209
|
header.push(
|
|
@@ -165,12 +222,173 @@ function buildPrompt(config, message) {
|
|
|
165
222
|
return header.join("\n");
|
|
166
223
|
}
|
|
167
224
|
|
|
225
|
+
function buildVisibleConsoleText(config, message) {
|
|
226
|
+
const compactText = compactInboundText(message);
|
|
227
|
+
const isBriefSummary = compactText.startsWith("Brief von ") || compactText.startsWith("Mnemo Idle");
|
|
228
|
+
const parts = [];
|
|
229
|
+
if (!isBriefSummary) {
|
|
230
|
+
parts.push(compactInboundLabel(message));
|
|
231
|
+
}
|
|
232
|
+
parts.push(compactText);
|
|
233
|
+
parts.push(...formatAttachmentInstructions(message));
|
|
234
|
+
if (message.intent === "continue_nudge") {
|
|
235
|
+
parts.push("Weiter-Signal: Bitte den laufenden Arbeitsfluss fortsetzen und nur antworten, wenn es ein konkretes Ergebnis, einen Blocker oder eine Entscheidung gibt.");
|
|
236
|
+
}
|
|
237
|
+
return parts
|
|
238
|
+
.join("\n")
|
|
239
|
+
.replace(/\s+\n/g, "\n")
|
|
240
|
+
.replace(/\n\s+/g, "\n")
|
|
241
|
+
.trim();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readRuntime(config) {
|
|
245
|
+
try {
|
|
246
|
+
if (!config?.paths?.currentRuntimeFile || !existsSync(config.paths.currentRuntimeFile)) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return JSON.parse(readFileSync(config.paths.currentRuntimeFile, "utf8"));
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isPidAlive(pid) {
|
|
256
|
+
const parsed = Number.parseInt(String(pid || "0"), 10);
|
|
257
|
+
if (!parsed || parsed <= 0) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
process.kill(parsed, 0);
|
|
262
|
+
return true;
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getVisibleConsoleSkipReason(config, message) {
|
|
269
|
+
if (process.platform !== "win32") {
|
|
270
|
+
return "not_windows";
|
|
271
|
+
}
|
|
272
|
+
if (!config.appServerWsUrl) {
|
|
273
|
+
return "no_app_server";
|
|
274
|
+
}
|
|
275
|
+
const visibleConsoleMode = String(process.env.BLUN_TELEGRAM_VISIBLE_CONSOLE_INJECT || "0").trim().toLowerCase();
|
|
276
|
+
if (visibleConsoleMode !== "force") {
|
|
277
|
+
return "env_disabled";
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(message.attachments) && message.attachments.some((attachment) => attachment?.isImage && attachment?.localPath && !attachment.error)) {
|
|
280
|
+
return "image_attachment";
|
|
281
|
+
}
|
|
282
|
+
return "";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function injectVisibleConsole(config, message) {
|
|
286
|
+
const skipReason = getVisibleConsoleSkipReason(config, message);
|
|
287
|
+
if (skipReason) {
|
|
288
|
+
return { ok: false, skipped: true, reason: skipReason };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const runtime = readRuntime(config);
|
|
292
|
+
const frontendPid = Number.parseInt(String(runtime?.frontend_host_pid || "0"), 10) || 0;
|
|
293
|
+
if (!isPidAlive(frontendPid)) {
|
|
294
|
+
return { ok: false, skipped: true, reason: "frontend_offline" };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const scriptPath = join(runtimeRoot, "telegram-console-input.ps1");
|
|
298
|
+
if (!existsSync(scriptPath)) {
|
|
299
|
+
return { ok: false, skipped: true, reason: "script_missing" };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const visibleText = buildVisibleConsoleText(config, message);
|
|
303
|
+
if (!visibleText) {
|
|
304
|
+
return { ok: false, skipped: true, reason: "empty" };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = spawnSync("powershell.exe", [
|
|
308
|
+
"-NoProfile",
|
|
309
|
+
"-ExecutionPolicy",
|
|
310
|
+
"Bypass",
|
|
311
|
+
"-File",
|
|
312
|
+
scriptPath,
|
|
313
|
+
"-TargetPid",
|
|
314
|
+
String(frontendPid),
|
|
315
|
+
"-Text",
|
|
316
|
+
visibleText,
|
|
317
|
+
"-ClearBefore",
|
|
318
|
+
"-Submit"
|
|
319
|
+
], {
|
|
320
|
+
cwd: runtimeRoot,
|
|
321
|
+
encoding: "utf8",
|
|
322
|
+
windowsHide: true,
|
|
323
|
+
timeout: 20000
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (result.status === 0) {
|
|
327
|
+
return {
|
|
328
|
+
ok: true,
|
|
329
|
+
frontendPid,
|
|
330
|
+
visibleText
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
skipped: false,
|
|
337
|
+
reason: "script_failed",
|
|
338
|
+
stderr: String(result.stderr || result.error || "").trim(),
|
|
339
|
+
stdout: String(result.stdout || "").trim()
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildTurnInput(config, message) {
|
|
344
|
+
const prompt = buildPrompt(config, message);
|
|
345
|
+
const input = [
|
|
346
|
+
{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: prompt,
|
|
349
|
+
text_elements: []
|
|
350
|
+
}
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
const attachments = Array.isArray(message.attachments) ? message.attachments : [];
|
|
354
|
+
for (const attachment of attachments) {
|
|
355
|
+
if (!attachment?.isImage || !attachment.localPath || attachment.error) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
input.push({
|
|
359
|
+
type: "localImage",
|
|
360
|
+
path: attachment.localPath
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
prompt,
|
|
366
|
+
input
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
168
370
|
export async function injectIntoThread(config, message, threadId) {
|
|
371
|
+
const turnInput = buildTurnInput(config, message);
|
|
169
372
|
if (config.appServerWsUrl) {
|
|
170
|
-
const
|
|
373
|
+
const consoleResult = injectVisibleConsole(config, message);
|
|
374
|
+
if (consoleResult.ok) {
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
busy: false,
|
|
378
|
+
turnId: "",
|
|
379
|
+
code: 0,
|
|
380
|
+
signal: null,
|
|
381
|
+
responseText: `console_injected thread=${threadId} frontend_pid=${consoleResult.frontendPid}`,
|
|
382
|
+
stdout: "",
|
|
383
|
+
stderr: ""
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = await startOrSteerTextTurnOverWs({
|
|
171
388
|
wsUrl: config.appServerWsUrl,
|
|
172
389
|
threadId,
|
|
173
|
-
text:
|
|
390
|
+
text: turnInput.prompt,
|
|
391
|
+
input: turnInput.input,
|
|
174
392
|
model: config.model || null,
|
|
175
393
|
effort: config.reasoningEffort || null,
|
|
176
394
|
personality: config.personality || null,
|
|
@@ -183,7 +401,9 @@ export async function injectIntoThread(config, message, threadId) {
|
|
|
183
401
|
turnId: result.turnId || "",
|
|
184
402
|
code: result.ok ? 0 : null,
|
|
185
403
|
signal: null,
|
|
186
|
-
responseText: result.ok
|
|
404
|
+
responseText: result.ok
|
|
405
|
+
? `${result.steered ? "turn_steered" : "turn_started"} thread=${threadId} console_${consoleResult.skipped ? "skip" : "fail"}=${consoleResult.reason || "unknown"}`
|
|
406
|
+
: "",
|
|
187
407
|
stdout: "",
|
|
188
408
|
stderr: result.error ? String(result.error.message || result.error) : ""
|
|
189
409
|
};
|
|
@@ -192,7 +412,7 @@ export async function injectIntoThread(config, message, threadId) {
|
|
|
192
412
|
const safeKey = `${message.chatId}_${message.messageId}`.replace(/[^0-9A-Za-z_-]/g, "_");
|
|
193
413
|
const promptFile = `${config.paths.promptsDir}\\${safeKey}.md`;
|
|
194
414
|
const responseFile = `${config.paths.responsesDir}\\${safeKey}.txt`;
|
|
195
|
-
writeFileSync(promptFile,
|
|
415
|
+
writeFileSync(promptFile, turnInput.prompt, "utf8");
|
|
196
416
|
|
|
197
417
|
return await new Promise((resolve) => {
|
|
198
418
|
let timedOut = false;
|
|
@@ -75,6 +75,7 @@ export function loadConfig() {
|
|
|
75
75
|
idleCooldownMs: Number.parseInt(env.BLUN_TELEGRAM_IDLE_COOLDOWN_MS || "3000", 10) || 3000,
|
|
76
76
|
ambientQueueTtlMs: Number.parseInt(env.BLUN_TELEGRAM_AMBIENT_QUEUE_TTL_MS || "600000", 10) || 600000,
|
|
77
77
|
pendingReplyTimeoutMs: Number.parseInt(env.BLUN_TELEGRAM_PENDING_REPLY_TIMEOUT_MS || "1800000", 10) || 1800000,
|
|
78
|
+
attachmentMaxBytes: Number.parseInt(env.BLUN_TELEGRAM_ATTACHMENT_MAX_BYTES || "52428800", 10) || 52428800,
|
|
78
79
|
progressFallbackMs: Number.parseInt(env.BLUN_TELEGRAM_PROGRESS_FALLBACK_MS || "20000", 10) || 20000,
|
|
79
80
|
progressRelayMode: env.BLUN_TELEGRAM_PROGRESS_RELAY?.trim().toLowerCase() || "status",
|
|
80
81
|
queueNoticeEnabled: /^(1|true|yes|on)$/i.test(env.BLUN_TELEGRAM_QUEUE_NOTICE || ""),
|
|
@@ -25,6 +25,7 @@ export function getPaths() {
|
|
|
25
25
|
inboxFile: join(root, "inbox.jsonl"),
|
|
26
26
|
outboxFile: join(root, "outbox.jsonl"),
|
|
27
27
|
activityFile: join(root, "activity.log"),
|
|
28
|
+
attachmentsDir: join(root, "attachments"),
|
|
28
29
|
pollerPidFile: join(root, "poller.pid"),
|
|
29
30
|
dispatcherPidFile: join(root, "dispatcher.pid"),
|
|
30
31
|
responderPidFile: join(root, "responder.pid"),
|
|
@@ -42,7 +43,7 @@ export function getPaths() {
|
|
|
42
43
|
|
|
43
44
|
export function ensureStateLayout() {
|
|
44
45
|
const paths = getPaths();
|
|
45
|
-
for (const dir of [paths.root, paths.promptsDir, paths.responsesDir, paths.runtimeDir]) {
|
|
46
|
+
for (const dir of [paths.root, paths.promptsDir, paths.responsesDir, paths.runtimeDir, paths.attachmentsDir]) {
|
|
46
47
|
if (!existsSync(dir)) {
|
|
47
48
|
mkdirSync(dir, { recursive: true });
|
|
48
49
|
}
|
|
@@ -27,6 +27,21 @@ export async function getUpdates(config, offset) {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export async function getFileInfo(config, fileId) {
|
|
31
|
+
return telegramRequest(config, "getFile", {
|
|
32
|
+
file_id: fileId
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function downloadFileBuffer(config, filePath) {
|
|
37
|
+
requireToken(config);
|
|
38
|
+
const response = await fetch(`https://api.telegram.org/file/bot${config.botToken}/${filePath}`);
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`Telegram file download failed: ${response.status}`);
|
|
41
|
+
}
|
|
42
|
+
return Buffer.from(await response.arrayBuffer());
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
export async function sendMessage(config, { chatId, text, replyToMessageId, telegramThreadId }) {
|
|
31
46
|
return telegramRequest(config, "sendMessage", {
|
|
32
47
|
chat_id: chatId,
|