@blunking/codexlink 0.1.2 → 0.1.9
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 +56 -38
- package/package.json +40 -38
- package/start-codex-agent.ps1 +179 -71
- package/telegram-doctor.ps1 +34 -20
- package/telegram-plugin/.env.example +1 -0
- package/telegram-plugin/README.md +3 -0
- package/telegram-plugin/dispatcher.js +4 -0
- package/telegram-plugin/lib/bridge.js +1550 -86
- package/telegram-plugin/lib/codex.js +142 -21
- package/telegram-plugin/lib/env.js +29 -1
- package/telegram-plugin/lib/paths.js +7 -1
- package/telegram-plugin/lib/sidecars.js +12 -1
- package/telegram-plugin/lib/singleton.js +66 -0
- package/telegram-plugin/lib/storage.js +66 -25
- package/telegram-plugin/lib/telegram.js +8 -0
- package/telegram-plugin/poller.js +4 -0
- package/telegram-plugin/responder.js +4 -0
- package/telegram-setup.ps1 +55 -46
- package/telegram-status.ps1 +292 -182
- package/telegram-title-embed.ps1 +442 -0
- package/telegram-title-watcher.ps1 +454 -0
|
@@ -3,28 +3,149 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { startTextTurnOverWs } from "./app-server-client.js";
|
|
5
5
|
|
|
6
|
+
function repairMojibake(value) {
|
|
7
|
+
const input = String(value || "");
|
|
8
|
+
if (!input || !/[ÃÂâð]/.test(input)) {
|
|
9
|
+
return input;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const repaired = Buffer.from(input, "latin1").toString("utf8");
|
|
13
|
+
if (!repaired || repaired.includes("\uFFFD")) {
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
16
|
+
return repaired;
|
|
17
|
+
} catch {
|
|
18
|
+
return input;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compactInboundLabel(message) {
|
|
23
|
+
const user = repairMojibake(String(message.user || "Unbekannt")).trim() || "Unbekannt";
|
|
24
|
+
const group = repairMojibake(String(message.groupTitle || "")).trim();
|
|
25
|
+
const chatType = String(message.chatType || "").trim();
|
|
26
|
+
|
|
27
|
+
if (group || chatType === "group" || chatType === "supergroup") {
|
|
28
|
+
return `${user} @ ${group || "Gruppe"} schrieb:`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `${user} schrieb:`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeWhitespace(text) {
|
|
35
|
+
return repairMojibake(String(text || ""))
|
|
36
|
+
.replace(/\r/g, " ")
|
|
37
|
+
.replace(/\n/g, " ")
|
|
38
|
+
.replace(/\s+/g, " ")
|
|
39
|
+
.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function firstMeaningfulLine(lines) {
|
|
43
|
+
for (const raw of lines) {
|
|
44
|
+
const line = String(raw || "").trim();
|
|
45
|
+
if (!line) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (/^---\s*BRIEF/i.test(line) || /^---\s*BRIEF END/i.test(line)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (/^##\s*(Title|Project|Request|Constraints|Acceptance|Report Back)\s*$/i.test(line)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
return line;
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function summarizeBrief(text) {
|
|
60
|
+
const raw = String(text || "");
|
|
61
|
+
const lines = raw.split(/\r?\n/);
|
|
62
|
+
const briefHeader = lines.find((line) => /^---\s*BRIEF\b/i.test(String(line || "").trim())) || "";
|
|
63
|
+
const idMatch = briefHeader.match(/\bid=(\d+)/i);
|
|
64
|
+
const fromMatch = briefHeader.match(/\bfrom=([^\s]+)/i);
|
|
65
|
+
const titleIndex = lines.findIndex((line) => /^##\s*Title\s*$/i.test(String(line || "").trim()));
|
|
66
|
+
const titleLine = titleIndex >= 0 ? firstMeaningfulLine(lines.slice(titleIndex + 1, titleIndex + 4)) : "";
|
|
67
|
+
const firstLine = firstMeaningfulLine(lines);
|
|
68
|
+
let detail = titleLine || firstLine || "Neuer Brief";
|
|
69
|
+
detail = detail.replace(/^\[IDLE-CYCLE\]\s*/i, "IDLE-CYCLE: ");
|
|
70
|
+
detail = normalizeWhitespace(detail);
|
|
71
|
+
const from = fromMatch ? fromMatch[1] : "Brief";
|
|
72
|
+
const idPart = idMatch ? ` #${idMatch[1]}` : "";
|
|
73
|
+
if (from === "mnemo-idle-loop") {
|
|
74
|
+
const compactIdle = detail
|
|
75
|
+
.replace(/^IDLE-CYCLE:\s*/i, "")
|
|
76
|
+
.replace(/^Pull project_state,\s*/i, "")
|
|
77
|
+
.replace(/generate proposals via mem_propose,\s*/i, "proposals, ")
|
|
78
|
+
.replace(/ship if ship_eligible\.?/i, "ship-check")
|
|
79
|
+
.replace(/Mode:\s*autonomous\.?/i, "auto")
|
|
80
|
+
.trim();
|
|
81
|
+
return `Mnemo Idle${idPart}: ${compactIdle || "IDLE-CYCLE"}`;
|
|
82
|
+
}
|
|
83
|
+
return `Brief von ${from}${idPart}: ${detail}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compactInboundText(message) {
|
|
87
|
+
const text = String(message.text || "").trim();
|
|
88
|
+
if (!text) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
if (/^---\s*BRIEF\b/i.test(text)) {
|
|
92
|
+
return summarizeBrief(text);
|
|
93
|
+
}
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeAddressText(value) {
|
|
98
|
+
return repairMojibake(String(value || ""))
|
|
99
|
+
.normalize("NFKC")
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.replace(/^@+/, "")
|
|
102
|
+
.replace(/[^\p{L}\p{N}_-]+/gu, " ")
|
|
103
|
+
.replace(/\s+/g, " ")
|
|
104
|
+
.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isAddressOnlyPing(config, text) {
|
|
108
|
+
const normalizedText = normalizeAddressText(text);
|
|
109
|
+
if (!normalizedText || normalizedText.includes(" ")) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const names = [
|
|
114
|
+
...(Array.isArray(config.mentionNames) ? config.mentionNames : []),
|
|
115
|
+
config.agentName
|
|
116
|
+
]
|
|
117
|
+
.map((value) => normalizeAddressText(value))
|
|
118
|
+
.filter((value) => value && value !== "default");
|
|
119
|
+
|
|
120
|
+
return names.includes(normalizedText);
|
|
121
|
+
}
|
|
122
|
+
|
|
6
123
|
function buildPrompt(config, message) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
124
|
+
const compactText = compactInboundText(message);
|
|
125
|
+
const isBriefSummary = compactText.startsWith("Brief von ") || compactText.startsWith("Mnemo Idle");
|
|
126
|
+
const label = isBriefSummary ? "" : compactInboundLabel(message);
|
|
127
|
+
const header = [];
|
|
128
|
+
|
|
129
|
+
if (label) {
|
|
130
|
+
header.push(label);
|
|
131
|
+
}
|
|
132
|
+
header.push(compactText);
|
|
133
|
+
|
|
134
|
+
if (message.intent === "continue_nudge") {
|
|
135
|
+
header.push(
|
|
136
|
+
"",
|
|
137
|
+
"[Weiter-Signal: kein bloßes Ack senden. Nur antworten, wenn jetzt ein konkretes Ergebnis, Blocker oder eine Entscheidung sichtbar gemacht werden muss.]"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isAddressOnlyPing(config, compactText)) {
|
|
142
|
+
header.push(
|
|
143
|
+
"",
|
|
144
|
+
"[Ping: Der User prueft nur, ob du erreichbar bist. Antworte kurz, dass du da bist. Starte keine Suche und keinen Tool-Lauf.]"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return header.join("\n");
|
|
28
149
|
}
|
|
29
150
|
|
|
30
151
|
export async function injectIntoThread(config, message, threadId) {
|
|
@@ -25,6 +25,15 @@ function parseAllowedChatIds(rawValue) {
|
|
|
25
25
|
.filter(Boolean);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function parseMentionNames(rawValue) {
|
|
29
|
+
return Array.from(new Set(
|
|
30
|
+
String(rawValue || "")
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((value) => value.trim().toLowerCase())
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
export function loadConfig() {
|
|
29
38
|
ensureStateLayout();
|
|
30
39
|
const paths = getPaths();
|
|
@@ -36,6 +45,18 @@ export function loadConfig() {
|
|
|
36
45
|
delete fallbackEnv.BLUN_TELEGRAM_THREAD_ID;
|
|
37
46
|
const env = { ...fallbackEnv, ...process.env, ...fileEnv };
|
|
38
47
|
const allowedChatIds = parseAllowedChatIds(env.BLUN_TELEGRAM_ALLOWED_CHAT_ID || env.TELEGRAM_ALLOWED_CHAT_ID || "");
|
|
48
|
+
const mentionNames = parseMentionNames(
|
|
49
|
+
env.BLUN_TELEGRAM_MENTION_NAMES
|
|
50
|
+
|| env.BLUN_CODEX_AGENT
|
|
51
|
+
|| env.TELEGRAM_AGENT_NAME
|
|
52
|
+
|| env.BLUN_TELEGRAM_AGENT_NAME
|
|
53
|
+
|| ""
|
|
54
|
+
);
|
|
55
|
+
const otherAgentNames = parseMentionNames(
|
|
56
|
+
env.BLUN_TELEGRAM_OTHER_AGENT_NAMES
|
|
57
|
+
|| env.BLUN_CODEX_OTHER_AGENTS
|
|
58
|
+
|| ""
|
|
59
|
+
);
|
|
39
60
|
return {
|
|
40
61
|
paths,
|
|
41
62
|
agentName: env.BLUN_TELEGRAM_AGENT_NAME?.trim() || env.TELEGRAM_AGENT_NAME?.trim() || "default",
|
|
@@ -43,11 +64,18 @@ export function loadConfig() {
|
|
|
43
64
|
botToken: env.BLUN_TELEGRAM_BOT_TOKEN?.trim() || env.TELEGRAM_BOT_TOKEN?.trim() || "",
|
|
44
65
|
allowedChatId: allowedChatIds[0] || "",
|
|
45
66
|
allowedChatIds,
|
|
67
|
+
mentionNames,
|
|
68
|
+
otherAgentNames,
|
|
46
69
|
codexBin: env.BLUN_TELEGRAM_CODEX_BIN?.trim() || "codex",
|
|
47
70
|
appServerWsUrl: env.BLUN_TELEGRAM_APP_SERVER_WS_URL?.trim() || "",
|
|
48
71
|
currentThreadId: env.BLUN_TELEGRAM_THREAD_ID?.trim() || process.env.CODEX_THREAD_ID?.trim() || "",
|
|
49
72
|
resumeTimeoutMs: Number.parseInt(env.BLUN_TELEGRAM_RESUME_TIMEOUT_MS || "15000", 10) || 15000,
|
|
50
|
-
idleCooldownMs: Number.parseInt(env.BLUN_TELEGRAM_IDLE_COOLDOWN_MS || "
|
|
73
|
+
idleCooldownMs: Number.parseInt(env.BLUN_TELEGRAM_IDLE_COOLDOWN_MS || "3000", 10) || 3000,
|
|
74
|
+
ambientQueueTtlMs: Number.parseInt(env.BLUN_TELEGRAM_AMBIENT_QUEUE_TTL_MS || "600000", 10) || 600000,
|
|
75
|
+
pendingReplyTimeoutMs: Number.parseInt(env.BLUN_TELEGRAM_PENDING_REPLY_TIMEOUT_MS || "1800000", 10) || 1800000,
|
|
76
|
+
progressFallbackMs: Number.parseInt(env.BLUN_TELEGRAM_PROGRESS_FALLBACK_MS || "20000", 10) || 20000,
|
|
77
|
+
progressRelayMode: env.BLUN_TELEGRAM_PROGRESS_RELAY?.trim().toLowerCase() || "status",
|
|
78
|
+
queueNoticeEnabled: /^(1|true|yes|on)$/i.test(env.BLUN_TELEGRAM_QUEUE_NOTICE || ""),
|
|
51
79
|
dispatchMode: env.BLUN_TELEGRAM_DISPATCH_MODE?.trim() || "deferred",
|
|
52
80
|
pluginMode: env.BLUN_TELEGRAM_PLUGIN_MODE?.trim() || "inherit",
|
|
53
81
|
model: env.BLUN_CODEX_MODEL?.trim() || "",
|
|
@@ -9,10 +9,16 @@ export function getStateDir() {
|
|
|
9
9
|
export function getPaths() {
|
|
10
10
|
const root = getStateDir();
|
|
11
11
|
const codexHome = join(homedir(), ".codex");
|
|
12
|
+
const agentName = process.env.BLUN_TELEGRAM_AGENT_NAME?.trim()
|
|
13
|
+
|| process.env.TELEGRAM_AGENT_NAME?.trim()
|
|
14
|
+
|| "default";
|
|
15
|
+
const runtimeDir = join(codexHome, "runtimes", agentName);
|
|
12
16
|
return {
|
|
13
17
|
root,
|
|
14
18
|
legacyRoot: join(codexHome, "channels", "codexlink-telegram"),
|
|
15
19
|
codexHome,
|
|
20
|
+
runtimeDir,
|
|
21
|
+
currentRuntimeFile: join(runtimeDir, "current-remote-runtime.json"),
|
|
16
22
|
sessionsDir: join(codexHome, "sessions"),
|
|
17
23
|
envFile: join(root, ".env"),
|
|
18
24
|
stateFile: join(root, "state.json"),
|
|
@@ -36,7 +42,7 @@ export function getPaths() {
|
|
|
36
42
|
|
|
37
43
|
export function ensureStateLayout() {
|
|
38
44
|
const paths = getPaths();
|
|
39
|
-
for (const dir of [paths.root, paths.promptsDir, paths.responsesDir]) {
|
|
45
|
+
for (const dir of [paths.root, paths.promptsDir, paths.responsesDir, paths.runtimeDir]) {
|
|
40
46
|
if (!existsSync(dir)) {
|
|
41
47
|
mkdirSync(dir, { recursive: true });
|
|
42
48
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { appendLog } from "./storage.js";
|
|
6
6
|
|
|
7
7
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -31,6 +31,14 @@ function stopPid(pid) {
|
|
|
31
31
|
if (!isPidAlive(pid)) {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
try {
|
|
36
|
+
execFileSync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
34
42
|
try {
|
|
35
43
|
process.kill(pid, "SIGTERM");
|
|
36
44
|
} catch {
|
|
@@ -60,10 +68,13 @@ function ensureSidecar(scriptName, pidFile, stdoutFile, stderrFile, config, opti
|
|
|
60
68
|
BLUN_TELEGRAM_STATE_DIR: config.paths.root,
|
|
61
69
|
BLUN_TELEGRAM_BOT_TOKEN: config.botToken || "",
|
|
62
70
|
BLUN_TELEGRAM_ALLOWED_CHAT_ID: Array.isArray(config.allowedChatIds) ? config.allowedChatIds.join(",") : (config.allowedChatId || ""),
|
|
71
|
+
BLUN_TELEGRAM_OTHER_AGENT_NAMES: Array.isArray(config.otherAgentNames) ? config.otherAgentNames.join(",") : "",
|
|
63
72
|
BLUN_TELEGRAM_APP_SERVER_WS_URL: config.appServerWsUrl || "",
|
|
64
73
|
BLUN_TELEGRAM_CODEX_BIN: config.codexBin || "codex",
|
|
65
74
|
BLUN_TELEGRAM_RESUME_TIMEOUT_MS: String(config.resumeTimeoutMs || 15000),
|
|
66
75
|
BLUN_TELEGRAM_IDLE_COOLDOWN_MS: String(config.idleCooldownMs || 15000),
|
|
76
|
+
BLUN_TELEGRAM_PROGRESS_FALLBACK_MS: String(config.progressFallbackMs || 20000),
|
|
77
|
+
BLUN_TELEGRAM_QUEUE_NOTICE: config.queueNoticeEnabled ? "1" : "0",
|
|
67
78
|
BLUN_TELEGRAM_DISPATCH_MODE: config.dispatchMode || "deferred",
|
|
68
79
|
BLUN_TELEGRAM_PLUGIN_MODE: config.pluginMode || "plugin",
|
|
69
80
|
BLUN_CODEX_MODEL: config.model || "",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { getPaths } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
function readPid(path) {
|
|
5
|
+
try {
|
|
6
|
+
return Number.parseInt(readFileSync(path, "utf8").trim(), 10) || 0;
|
|
7
|
+
} catch {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function writePid(path) {
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(path, `${process.pid}\n`, "utf8");
|
|
15
|
+
} catch {
|
|
16
|
+
// Best-effort self-heal only; the sidecar can still keep running.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPidAlive(pid) {
|
|
21
|
+
if (!pid || pid <= 0) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isCurrentSidecarPid(kind) {
|
|
33
|
+
const paths = getPaths();
|
|
34
|
+
const pidFiles = {
|
|
35
|
+
poller: paths.pollerPidFile,
|
|
36
|
+
dispatcher: paths.dispatcherPidFile,
|
|
37
|
+
responder: paths.responderPidFile
|
|
38
|
+
};
|
|
39
|
+
const pidFile = pidFiles[kind];
|
|
40
|
+
if (!pidFile) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const currentPid = readPid(pidFile);
|
|
45
|
+
if (!currentPid) {
|
|
46
|
+
writePid(pidFile);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (currentPid === process.pid) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The parent writes the pid file just after spawn. Give a fresh child a
|
|
55
|
+
// short grace window so it does not exit before its pid has been recorded.
|
|
56
|
+
if (process.uptime() < 2) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!isPidAlive(currentPid)) {
|
|
61
|
+
writePid(pidFile);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
@@ -1,21 +1,60 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
function sleepSync(ms) {
|
|
5
|
+
const buffer = new SharedArrayBuffer(4);
|
|
6
|
+
const view = new Int32Array(buffer);
|
|
7
|
+
Atomics.wait(view, 0, 0, ms);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function nowIso() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function loadJson(path, fallback) {
|
|
15
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(path, "utf8").replace(/^\uFEFF/, "");
|
|
18
|
+
if (!raw.trim()) {
|
|
19
|
+
throw new Error("empty json file");
|
|
20
|
+
}
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
} catch {
|
|
23
|
+
if (attempt < 5) {
|
|
24
|
+
sleepSync(20 * (attempt + 1));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveJson(path, value) {
|
|
32
|
+
const text = JSON.stringify(value, null, 2);
|
|
33
|
+
const dir = dirname(path);
|
|
34
|
+
const base = basename(path);
|
|
35
|
+
let lastError = null;
|
|
36
|
+
|
|
37
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
38
|
+
const tempPath = join(dir, `.${base}.${process.pid}.${Date.now()}.${attempt}.tmp`);
|
|
39
|
+
try {
|
|
40
|
+
writeFileSync(tempPath, text, "utf8");
|
|
41
|
+
renameSync(tempPath, path);
|
|
42
|
+
return;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
lastError = error;
|
|
45
|
+
try {
|
|
46
|
+
unlinkSync(tempPath);
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore cleanup failures for temp files.
|
|
49
|
+
}
|
|
50
|
+
if (attempt < 5) {
|
|
51
|
+
sleepSync(25 * (attempt + 1));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw lastError || new Error(`Failed to save JSON file: ${path}`);
|
|
57
|
+
}
|
|
19
58
|
|
|
20
59
|
export function appendJsonl(path, value) {
|
|
21
60
|
appendFileSync(path, `${JSON.stringify(value)}\n`, "utf8");
|
|
@@ -41,10 +80,12 @@ export function defaultState() {
|
|
|
41
80
|
pendingReplies: [],
|
|
42
81
|
replyOffsets: {},
|
|
43
82
|
replyBuffers: {},
|
|
44
|
-
lastInbound: null,
|
|
45
|
-
lastOutbound: null,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
lastInbound: null,
|
|
84
|
+
lastOutbound: null,
|
|
85
|
+
lastUiNotice: null,
|
|
86
|
+
lastPollAt: null,
|
|
87
|
+
lastInjectAt: null,
|
|
88
|
+
lastAutoDispatchAt: null,
|
|
89
|
+
lastQueueNoticeAt: null
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -35,3 +35,11 @@ export async function sendMessage(config, { chatId, text, replyToMessageId, tele
|
|
|
35
35
|
...(telegramThreadId ? { message_thread_id: Number(telegramThreadId) } : {})
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
export async function sendChatAction(config, { chatId, telegramThreadId, action = "typing" }) {
|
|
40
|
+
return telegramRequest(config, "sendChatAction", {
|
|
41
|
+
chat_id: chatId,
|
|
42
|
+
action,
|
|
43
|
+
...(telegramThreadId ? { message_thread_id: Number(telegramThreadId) } : {})
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { pollOnce } from "./lib/bridge.js";
|
|
3
|
+
import { isCurrentSidecarPid } from "./lib/singleton.js";
|
|
3
4
|
|
|
4
5
|
const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_POLL_INTERVAL_MS || "1500", 10) || 1500;
|
|
5
6
|
let stopping = false;
|
|
@@ -18,6 +19,9 @@ process.on("SIGTERM", () => {
|
|
|
18
19
|
|
|
19
20
|
async function main() {
|
|
20
21
|
while (!stopping) {
|
|
22
|
+
if (!isCurrentSidecarPid("poller")) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
21
25
|
try {
|
|
22
26
|
const result = await pollOnce();
|
|
23
27
|
if (result.captured > 0 || result.ignored > 0) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { relayRepliesOnce } from "./lib/bridge.js";
|
|
3
|
+
import { isCurrentSidecarPid } from "./lib/singleton.js";
|
|
3
4
|
|
|
4
5
|
const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_REPLY_INTERVAL_MS || "1500", 10) || 1500;
|
|
5
6
|
let stopping = false;
|
|
@@ -18,6 +19,9 @@ process.on("SIGTERM", () => {
|
|
|
18
19
|
|
|
19
20
|
async function main() {
|
|
20
21
|
while (!stopping) {
|
|
22
|
+
if (!isCurrentSidecarPid("responder")) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
21
25
|
try {
|
|
22
26
|
const result = await relayRepliesOnce();
|
|
23
27
|
if (result.status !== "empty" && result.delivered > 0) {
|
package/telegram-setup.ps1
CHANGED
|
@@ -148,42 +148,50 @@ if (-not (Test-AllowedChatIdsFormat -Value $currentAllowedChatIds)) {
|
|
|
148
148
|
$currentAllowedChatIds = [string]$envValues["TELEGRAM_ALLOWED_CHAT_ID"]
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
$needsToken = -not (Test-TelegramTokenFormat -Value $currentToken)
|
|
152
|
-
$needsChatIds = -not (Test-AllowedChatIdsFormat -Value $currentAllowedChatIds)
|
|
153
|
-
$changed = $false
|
|
154
|
-
|
|
155
|
-
if ($EnsureConfigured -and -not $needsToken
|
|
156
|
-
$result = [ordered]@{
|
|
157
|
-
ok = $true
|
|
158
|
-
changed = $false
|
|
159
|
-
profile = $profileAgent
|
|
160
|
-
state_dir = $stateDir
|
|
161
|
-
env_path = $envPath
|
|
162
|
-
missing = @()
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
151
|
+
$needsToken = -not (Test-TelegramTokenFormat -Value $currentToken)
|
|
152
|
+
$needsChatIds = -not (Test-AllowedChatIdsFormat -Value $currentAllowedChatIds)
|
|
153
|
+
$changed = $false
|
|
154
|
+
|
|
155
|
+
if ($EnsureConfigured -and -not $needsToken) {
|
|
156
|
+
$result = [ordered]@{
|
|
157
|
+
ok = $true
|
|
158
|
+
changed = $false
|
|
159
|
+
profile = $profileAgent
|
|
160
|
+
state_dir = $stateDir
|
|
161
|
+
env_path = $envPath
|
|
162
|
+
missing = @()
|
|
163
|
+
optional = @(
|
|
164
|
+
"allowed_chat_ids"
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
if ($Json) {
|
|
168
|
+
$result | ConvertTo-Json -Depth 6
|
|
169
|
+
} else {
|
|
170
|
+
Write-Host "Telegram ist bereits eingerichtet fuer Profil '$profileAgent'." -ForegroundColor Green
|
|
171
|
+
Write-Host "State-Ordner: $stateDir"
|
|
172
|
+
if ($needsChatIds) {
|
|
173
|
+
Write-Host "Hinweis: keine Chat-Allowlist gesetzt. Der Bot akzeptiert aktuell alle Chats, die er sehen kann." -ForegroundColor Yellow
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exit 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if ($EnsureConfigured -and $Json -and $needsToken) {
|
|
180
|
+
$missing = @()
|
|
181
|
+
if ($needsToken) { $missing += "bot_token" }
|
|
182
|
+
[ordered]@{
|
|
183
|
+
ok = $false
|
|
184
|
+
changed = $false
|
|
185
|
+
profile = $profileAgent
|
|
186
|
+
state_dir = $stateDir
|
|
187
|
+
env_path = $envPath
|
|
188
|
+
missing = $missing
|
|
189
|
+
optional = @(
|
|
190
|
+
"allowed_chat_ids"
|
|
191
|
+
)
|
|
192
|
+
} | ConvertTo-Json -Depth 6
|
|
193
|
+
exit 2
|
|
194
|
+
}
|
|
187
195
|
|
|
188
196
|
if (-not $Json) {
|
|
189
197
|
Write-Host ""
|
|
@@ -192,9 +200,10 @@ if (-not $Json) {
|
|
|
192
200
|
Write-Host "Lokaler State-Ordner: $stateDir"
|
|
193
201
|
Write-Host ""
|
|
194
202
|
Write-Host "Ich speichere die Telegram-Werte automatisch an die richtige lokale Stelle." -ForegroundColor DarkGray
|
|
195
|
-
Write-Host "Du musst keine .env-Datei selbst suchen." -ForegroundColor DarkGray
|
|
196
|
-
Write-Host ""
|
|
197
|
-
|
|
203
|
+
Write-Host "Du musst keine .env-Datei selbst suchen." -ForegroundColor DarkGray
|
|
204
|
+
Write-Host "Chat-ID-Allowlist ist optional und blockiert den Start nicht mehr." -ForegroundColor DarkGray
|
|
205
|
+
Write-Host ""
|
|
206
|
+
}
|
|
198
207
|
|
|
199
208
|
if ($needsToken) {
|
|
200
209
|
$currentToken = Prompt-RequiredValue `
|
|
@@ -206,15 +215,15 @@ if ($needsToken) {
|
|
|
206
215
|
$changed = $true
|
|
207
216
|
}
|
|
208
217
|
|
|
209
|
-
if ($needsChatIds) {
|
|
210
|
-
$currentAllowedChatIds = Prompt-RequiredValue `
|
|
211
|
-
-Prompt "Erlaubte Chat ID(s), komma-getrennt" `
|
|
212
|
-
-CurrentValue $currentAllowedChatIds `
|
|
213
|
-
-Validator { param($v) Test-AllowedChatIdsFormat -Value $v } `
|
|
218
|
+
if (-not $EnsureConfigured -and $needsChatIds) {
|
|
219
|
+
$currentAllowedChatIds = Prompt-RequiredValue `
|
|
220
|
+
-Prompt "Erlaubte Chat ID(s), komma-getrennt" `
|
|
221
|
+
-CurrentValue $currentAllowedChatIds `
|
|
222
|
+
-Validator { param($v) Test-AllowedChatIdsFormat -Value $v } `
|
|
214
223
|
-ErrorMessage "Bitte mindestens eine numerische Chat-ID eingeben. Mehrere IDs mit Komma trennen."
|
|
215
224
|
$envValues["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = (($currentAllowedChatIds -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -join ",")
|
|
216
|
-
$changed = $true
|
|
217
|
-
}
|
|
225
|
+
$changed = $true
|
|
226
|
+
}
|
|
218
227
|
|
|
219
228
|
$envValues["BLUN_TELEGRAM_AGENT_NAME"] = $profileAgent
|
|
220
229
|
$envValues["BLUN_TELEGRAM_STATE_DIR"] = $stateDir
|