@blunking/codexlink 0.1.15 → 0.1.17
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/package.json +5 -4
- package/start-codex-agent.ps1 +39 -18
- 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 +244 -8
- package/telegram-plugin/lib/env.js +3 -0
- package/telegram-plugin/lib/paths.js +2 -1
- package/telegram-plugin/lib/sidecars.js +3 -0
- 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")
|
|
@@ -120,16 +176,34 @@ export function isAddressOnlyPing(config, text) {
|
|
|
120
176
|
return names.includes(normalizedText);
|
|
121
177
|
}
|
|
122
178
|
|
|
179
|
+
function buildAgentRuntimeContext(config) {
|
|
180
|
+
const name = repairMojibake(String(config.displayName || config.agentName || "CodexLink")).trim() || "CodexLink";
|
|
181
|
+
const lane = repairMojibake(String(config.lane || "")).trim();
|
|
182
|
+
const customPrompt = repairMojibake(String(config.agentPrompt || "")).trim();
|
|
183
|
+
const lines = [
|
|
184
|
+
`[CodexLink Agent Context: You are ${name}.`,
|
|
185
|
+
lane ? `Assigned lane: ${lane}. Stay inside this lane unless the user explicitly redirects you.` : "Stay inside your assigned profile scope.",
|
|
186
|
+
"Treat short greetings or name-only pings as reachability checks, not translation/correction tasks.",
|
|
187
|
+
"For a greeting, reply briefly and naturally as this agent. Do not ask whether to translate, correct, or rewrite unless the user asks for that."
|
|
188
|
+
];
|
|
189
|
+
if (customPrompt) {
|
|
190
|
+
lines.push(customPrompt);
|
|
191
|
+
}
|
|
192
|
+
lines[lines.length - 1] = `${lines[lines.length - 1]}]`;
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
123
196
|
function buildPrompt(config, message) {
|
|
124
197
|
const compactText = compactInboundText(message);
|
|
125
198
|
const isBriefSummary = compactText.startsWith("Brief von ") || compactText.startsWith("Mnemo Idle");
|
|
126
199
|
const label = isBriefSummary ? "" : compactInboundLabel(message);
|
|
127
|
-
const header = [];
|
|
200
|
+
const header = [buildAgentRuntimeContext(config), ""];
|
|
128
201
|
|
|
129
202
|
if (label) {
|
|
130
203
|
header.push(label);
|
|
131
204
|
}
|
|
132
205
|
header.push(compactText);
|
|
206
|
+
header.push(...formatAttachmentInstructions(message));
|
|
133
207
|
|
|
134
208
|
if (message.intent === "continue_nudge") {
|
|
135
209
|
header.push(
|
|
@@ -148,12 +222,172 @@ function buildPrompt(config, message) {
|
|
|
148
222
|
return header.join("\n");
|
|
149
223
|
}
|
|
150
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
|
+
if (String(process.env.BLUN_TELEGRAM_VISIBLE_CONSOLE_INJECT || "0").trim() !== "1") {
|
|
276
|
+
return "env_disabled";
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(message.attachments) && message.attachments.some((attachment) => attachment?.isImage && attachment?.localPath && !attachment.error)) {
|
|
279
|
+
return "image_attachment";
|
|
280
|
+
}
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function injectVisibleConsole(config, message) {
|
|
285
|
+
const skipReason = getVisibleConsoleSkipReason(config, message);
|
|
286
|
+
if (skipReason) {
|
|
287
|
+
return { ok: false, skipped: true, reason: skipReason };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const runtime = readRuntime(config);
|
|
291
|
+
const frontendPid = Number.parseInt(String(runtime?.frontend_host_pid || "0"), 10) || 0;
|
|
292
|
+
if (!isPidAlive(frontendPid)) {
|
|
293
|
+
return { ok: false, skipped: true, reason: "frontend_offline" };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const scriptPath = join(runtimeRoot, "telegram-console-input.ps1");
|
|
297
|
+
if (!existsSync(scriptPath)) {
|
|
298
|
+
return { ok: false, skipped: true, reason: "script_missing" };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const visibleText = buildVisibleConsoleText(config, message);
|
|
302
|
+
if (!visibleText) {
|
|
303
|
+
return { ok: false, skipped: true, reason: "empty" };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const result = spawnSync("powershell.exe", [
|
|
307
|
+
"-NoProfile",
|
|
308
|
+
"-ExecutionPolicy",
|
|
309
|
+
"Bypass",
|
|
310
|
+
"-File",
|
|
311
|
+
scriptPath,
|
|
312
|
+
"-TargetPid",
|
|
313
|
+
String(frontendPid),
|
|
314
|
+
"-Text",
|
|
315
|
+
visibleText,
|
|
316
|
+
"-ClearBefore",
|
|
317
|
+
"-Submit"
|
|
318
|
+
], {
|
|
319
|
+
cwd: runtimeRoot,
|
|
320
|
+
encoding: "utf8",
|
|
321
|
+
windowsHide: true,
|
|
322
|
+
timeout: 20000
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (result.status === 0) {
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
frontendPid,
|
|
329
|
+
visibleText
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
skipped: false,
|
|
336
|
+
reason: "script_failed",
|
|
337
|
+
stderr: String(result.stderr || result.error || "").trim(),
|
|
338
|
+
stdout: String(result.stdout || "").trim()
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildTurnInput(config, message) {
|
|
343
|
+
const prompt = buildPrompt(config, message);
|
|
344
|
+
const input = [
|
|
345
|
+
{
|
|
346
|
+
type: "text",
|
|
347
|
+
text: prompt,
|
|
348
|
+
text_elements: []
|
|
349
|
+
}
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
const attachments = Array.isArray(message.attachments) ? message.attachments : [];
|
|
353
|
+
for (const attachment of attachments) {
|
|
354
|
+
if (!attachment?.isImage || !attachment.localPath || attachment.error) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
input.push({
|
|
358
|
+
type: "localImage",
|
|
359
|
+
path: attachment.localPath
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
prompt,
|
|
365
|
+
input
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
151
369
|
export async function injectIntoThread(config, message, threadId) {
|
|
370
|
+
const turnInput = buildTurnInput(config, message);
|
|
152
371
|
if (config.appServerWsUrl) {
|
|
153
|
-
const
|
|
372
|
+
const consoleResult = injectVisibleConsole(config, message);
|
|
373
|
+
if (consoleResult.ok) {
|
|
374
|
+
return {
|
|
375
|
+
ok: true,
|
|
376
|
+
busy: false,
|
|
377
|
+
turnId: "",
|
|
378
|
+
code: 0,
|
|
379
|
+
signal: null,
|
|
380
|
+
responseText: `console_injected thread=${threadId} frontend_pid=${consoleResult.frontendPid}`,
|
|
381
|
+
stdout: "",
|
|
382
|
+
stderr: ""
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const result = await startOrSteerTextTurnOverWs({
|
|
154
387
|
wsUrl: config.appServerWsUrl,
|
|
155
388
|
threadId,
|
|
156
|
-
text:
|
|
389
|
+
text: turnInput.prompt,
|
|
390
|
+
input: turnInput.input,
|
|
157
391
|
model: config.model || null,
|
|
158
392
|
effort: config.reasoningEffort || null,
|
|
159
393
|
personality: config.personality || null,
|
|
@@ -166,7 +400,9 @@ export async function injectIntoThread(config, message, threadId) {
|
|
|
166
400
|
turnId: result.turnId || "",
|
|
167
401
|
code: result.ok ? 0 : null,
|
|
168
402
|
signal: null,
|
|
169
|
-
responseText: result.ok
|
|
403
|
+
responseText: result.ok
|
|
404
|
+
? `${result.steered ? "turn_steered" : "turn_started"} thread=${threadId} console_${consoleResult.skipped ? "skip" : "fail"}=${consoleResult.reason || "unknown"}`
|
|
405
|
+
: "",
|
|
170
406
|
stdout: "",
|
|
171
407
|
stderr: result.error ? String(result.error.message || result.error) : ""
|
|
172
408
|
};
|
|
@@ -175,7 +411,7 @@ export async function injectIntoThread(config, message, threadId) {
|
|
|
175
411
|
const safeKey = `${message.chatId}_${message.messageId}`.replace(/[^0-9A-Za-z_-]/g, "_");
|
|
176
412
|
const promptFile = `${config.paths.promptsDir}\\${safeKey}.md`;
|
|
177
413
|
const responseFile = `${config.paths.responsesDir}\\${safeKey}.txt`;
|
|
178
|
-
writeFileSync(promptFile,
|
|
414
|
+
writeFileSync(promptFile, turnInput.prompt, "utf8");
|
|
179
415
|
|
|
180
416
|
return await new Promise((resolve) => {
|
|
181
417
|
let timedOut = false;
|
|
@@ -60,7 +60,9 @@ export function loadConfig() {
|
|
|
60
60
|
return {
|
|
61
61
|
paths,
|
|
62
62
|
agentName: env.BLUN_TELEGRAM_AGENT_NAME?.trim() || env.TELEGRAM_AGENT_NAME?.trim() || "default",
|
|
63
|
+
displayName: env.BLUN_CODEX_DISPLAY_NAME?.trim() || env.BLUN_TELEGRAM_AGENT_NAME?.trim() || env.TELEGRAM_AGENT_NAME?.trim() || "CodexLink",
|
|
63
64
|
lane: env.BLUN_CODEX_LANE?.trim() || "",
|
|
65
|
+
agentPrompt: env.BLUN_CODEX_AGENT_PROMPT?.trim() || "",
|
|
64
66
|
botToken: env.BLUN_TELEGRAM_BOT_TOKEN?.trim() || env.TELEGRAM_BOT_TOKEN?.trim() || "",
|
|
65
67
|
allowedChatId: allowedChatIds[0] || "",
|
|
66
68
|
allowedChatIds,
|
|
@@ -73,6 +75,7 @@ export function loadConfig() {
|
|
|
73
75
|
idleCooldownMs: Number.parseInt(env.BLUN_TELEGRAM_IDLE_COOLDOWN_MS || "3000", 10) || 3000,
|
|
74
76
|
ambientQueueTtlMs: Number.parseInt(env.BLUN_TELEGRAM_AMBIENT_QUEUE_TTL_MS || "600000", 10) || 600000,
|
|
75
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,
|
|
76
79
|
progressFallbackMs: Number.parseInt(env.BLUN_TELEGRAM_PROGRESS_FALLBACK_MS || "20000", 10) || 20000,
|
|
77
80
|
progressRelayMode: env.BLUN_TELEGRAM_PROGRESS_RELAY?.trim().toLowerCase() || "status",
|
|
78
81
|
queueNoticeEnabled: /^(1|true|yes|on)$/i.test(env.BLUN_TELEGRAM_QUEUE_NOTICE || ""),
|