telecodex 0.1.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/bot/auth.js +64 -0
- package/dist/bot/commandSupport.js +239 -0
- package/dist/bot/createBot.js +51 -0
- package/dist/bot/handlerDeps.js +1 -0
- package/dist/bot/handlers/messageHandlers.js +71 -0
- package/dist/bot/handlers/operationalHandlers.js +131 -0
- package/dist/bot/handlers/projectHandlers.js +192 -0
- package/dist/bot/handlers/sessionConfigHandlers.js +319 -0
- package/dist/bot/inputService.js +372 -0
- package/dist/bot/registerHandlers.js +10 -0
- package/dist/bot/session.js +22 -0
- package/dist/bot/sessionFlow.js +51 -0
- package/dist/cli.js +14 -0
- package/dist/codex/sdkRuntime.js +165 -0
- package/dist/config.js +69 -0
- package/dist/runtime/appPaths.js +14 -0
- package/dist/runtime/bootstrap.js +213 -0
- package/dist/runtime/instanceLock.js +89 -0
- package/dist/runtime/logger.js +75 -0
- package/dist/runtime/secrets.js +45 -0
- package/dist/runtime/sessionRuntime.js +53 -0
- package/dist/runtime/startTelecodex.js +118 -0
- package/dist/store/db.js +267 -0
- package/dist/store/projects.js +47 -0
- package/dist/store/sessions.js +328 -0
- package/dist/telegram/attachments.js +67 -0
- package/dist/telegram/delivery.js +140 -0
- package/dist/telegram/messageBuffer.js +272 -0
- package/dist/telegram/renderer.js +146 -0
- package/dist/telegram/splitMessage.js +141 -0
- package/package.json +66 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getAppHome } from "../runtime/appPaths.js";
|
|
5
|
+
export async function telegramImageMessageToCodexInput(input) {
|
|
6
|
+
const source = selectImageSource(input.message);
|
|
7
|
+
if (!source)
|
|
8
|
+
return null;
|
|
9
|
+
const file = await input.bot.api.getFile(source.fileId);
|
|
10
|
+
if (!file.file_path) {
|
|
11
|
+
throw new Error("Telegram did not return a downloadable file_path.");
|
|
12
|
+
}
|
|
13
|
+
const url = `https://api.telegram.org/file/bot${input.config.telegramBotToken}/${file.file_path}`;
|
|
14
|
+
const response = await fetch(url);
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`Failed to download the Telegram image: HTTP ${response.status}`);
|
|
17
|
+
}
|
|
18
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
19
|
+
const directory = path.join(getAppHome(), "attachments");
|
|
20
|
+
await mkdir(directory, { recursive: true });
|
|
21
|
+
const localPath = path.join(directory, `${input.chatId}-${input.messageThreadId ?? "root"}-${Date.now()}-${randomUUID()}${extensionFor(source, file.file_path)}`);
|
|
22
|
+
await writeFile(localPath, bytes);
|
|
23
|
+
const caption = input.message.caption?.trim();
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: caption || "Continue based on this image.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: "local_image",
|
|
31
|
+
path: localPath,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
function selectImageSource(message) {
|
|
36
|
+
if (message.photo?.length) {
|
|
37
|
+
const sorted = [...message.photo].sort((left, right) => imageScore(right) - imageScore(left));
|
|
38
|
+
const photo = sorted[0];
|
|
39
|
+
return photo ? { fileId: photo.file_id } : null;
|
|
40
|
+
}
|
|
41
|
+
const document = message.document;
|
|
42
|
+
if (!document?.file_id)
|
|
43
|
+
return null;
|
|
44
|
+
if (!document.mime_type?.startsWith("image/"))
|
|
45
|
+
return null;
|
|
46
|
+
return {
|
|
47
|
+
fileId: document.file_id,
|
|
48
|
+
...(document.file_name ? { fileName: document.file_name } : {}),
|
|
49
|
+
...(document.mime_type ? { mimeType: document.mime_type } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function imageScore(photo) {
|
|
53
|
+
return photo.file_size ?? (photo.width ?? 0) * (photo.height ?? 0);
|
|
54
|
+
}
|
|
55
|
+
function extensionFor(source, filePath) {
|
|
56
|
+
const fromName = source.fileName ? path.extname(source.fileName) : "";
|
|
57
|
+
if (fromName)
|
|
58
|
+
return fromName;
|
|
59
|
+
const fromPath = path.extname(filePath);
|
|
60
|
+
if (fromPath)
|
|
61
|
+
return fromPath;
|
|
62
|
+
if (source.mimeType === "image/png")
|
|
63
|
+
return ".png";
|
|
64
|
+
if (source.mimeType === "image/webp")
|
|
65
|
+
return ".webp";
|
|
66
|
+
return ".jpg";
|
|
67
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { GrammyError } from "grammy";
|
|
2
|
+
import { renderPlainChunksForTelegram } from "./renderer.js";
|
|
3
|
+
import { splitTelegramHtml } from "./splitMessage.js";
|
|
4
|
+
export async function sendHtmlMessage(bot, input, logger) {
|
|
5
|
+
return retryTelegramCall(() => bot.api.sendMessage(input.chatId, input.text, {
|
|
6
|
+
...(input.messageThreadId == null ? {} : { message_thread_id: input.messageThreadId }),
|
|
7
|
+
parse_mode: "HTML",
|
|
8
|
+
link_preview_options: { is_disabled: true },
|
|
9
|
+
}), logger, "telegram send rate limited", {
|
|
10
|
+
chatId: input.chatId,
|
|
11
|
+
messageThreadId: input.messageThreadId,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function sendHtmlChunks(bot, input, logger) {
|
|
15
|
+
const messages = [];
|
|
16
|
+
for (const chunk of splitTelegramHtml(input.text)) {
|
|
17
|
+
messages.push(await sendHtmlMessage(bot, { ...input, text: chunk }, logger));
|
|
18
|
+
}
|
|
19
|
+
return messages;
|
|
20
|
+
}
|
|
21
|
+
export async function sendPlainChunks(bot, input, logger) {
|
|
22
|
+
const messages = [];
|
|
23
|
+
for (const chunk of renderPlainChunksForTelegram(input.text)) {
|
|
24
|
+
messages.push(await sendHtmlMessage(bot, { ...input, text: chunk }, logger));
|
|
25
|
+
}
|
|
26
|
+
return messages;
|
|
27
|
+
}
|
|
28
|
+
export async function sendTypingAction(bot, input, logger) {
|
|
29
|
+
await retryTelegramCall(() => bot.api.sendChatAction(input.chatId, "typing", {
|
|
30
|
+
...(input.messageThreadId == null ? {} : { message_thread_id: input.messageThreadId }),
|
|
31
|
+
}), logger, "telegram chat action rate limited", {
|
|
32
|
+
chatId: input.chatId,
|
|
33
|
+
messageThreadId: input.messageThreadId,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function replaceOrSendHtmlChunks(bot, input, logger) {
|
|
37
|
+
const [first, ...rest] = input.chunks;
|
|
38
|
+
let firstMessageId = input.messageId;
|
|
39
|
+
if (first) {
|
|
40
|
+
let firstDelivered = false;
|
|
41
|
+
if (input.messageId != null) {
|
|
42
|
+
try {
|
|
43
|
+
await editHtmlMessage(bot, {
|
|
44
|
+
chatId: input.chatId,
|
|
45
|
+
messageId: input.messageId,
|
|
46
|
+
text: first,
|
|
47
|
+
}, logger);
|
|
48
|
+
firstDelivered = true;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (isMessageNotModifiedError(error)) {
|
|
52
|
+
firstDelivered = true;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
logger?.warn("telegram final edit failed, falling back to send", {
|
|
56
|
+
chatId: input.chatId,
|
|
57
|
+
messageThreadId: input.messageThreadId,
|
|
58
|
+
messageId: input.messageId,
|
|
59
|
+
error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!firstDelivered) {
|
|
65
|
+
const message = await sendHtmlMessage(bot, {
|
|
66
|
+
chatId: input.chatId,
|
|
67
|
+
messageThreadId: input.messageThreadId,
|
|
68
|
+
text: first,
|
|
69
|
+
}, logger);
|
|
70
|
+
firstMessageId = message.message_id;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const chunk of rest) {
|
|
74
|
+
await sendHtmlMessage(bot, {
|
|
75
|
+
chatId: input.chatId,
|
|
76
|
+
messageThreadId: input.messageThreadId,
|
|
77
|
+
text: chunk,
|
|
78
|
+
}, logger);
|
|
79
|
+
}
|
|
80
|
+
return firstMessageId ?? null;
|
|
81
|
+
}
|
|
82
|
+
export async function editHtmlMessage(bot, input, logger) {
|
|
83
|
+
await retryTelegramCall(() => bot.api.editMessageText(input.chatId, input.messageId, input.text, {
|
|
84
|
+
parse_mode: "HTML",
|
|
85
|
+
link_preview_options: { is_disabled: true },
|
|
86
|
+
}), logger, "telegram edit rate limited", {
|
|
87
|
+
chatId: input.chatId,
|
|
88
|
+
messageId: input.messageId,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function isMessageNotModifiedError(error) {
|
|
92
|
+
return error instanceof GrammyError && descriptionOf(error)?.toLowerCase().includes("message is not modified") === true;
|
|
93
|
+
}
|
|
94
|
+
export function shouldFallbackToNewMessage(error) {
|
|
95
|
+
if (!(error instanceof GrammyError))
|
|
96
|
+
return false;
|
|
97
|
+
const description = descriptionOf(error)?.toLowerCase();
|
|
98
|
+
if (!description)
|
|
99
|
+
return false;
|
|
100
|
+
return description.includes("message to edit not found") || description.includes("message can't be edited");
|
|
101
|
+
}
|
|
102
|
+
function retryAfterMs(error) {
|
|
103
|
+
if (error instanceof GrammyError) {
|
|
104
|
+
const retryAfter = error.parameters?.retry_after;
|
|
105
|
+
if (typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
106
|
+
return retryAfter * 1000;
|
|
107
|
+
}
|
|
108
|
+
const match = descriptionOf(error)?.match(/retry after\s+(\d+)/i);
|
|
109
|
+
if (match) {
|
|
110
|
+
return Number(match[1]) * 1000;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function descriptionOf(error) {
|
|
116
|
+
return typeof error.description === "string" ? error.description : null;
|
|
117
|
+
}
|
|
118
|
+
export async function retryTelegramCall(operation, logger, message, context) {
|
|
119
|
+
for (let attempt = 0;; attempt += 1) {
|
|
120
|
+
try {
|
|
121
|
+
return await operation();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const waitMs = retryAfterMs(error);
|
|
125
|
+
if (waitMs == null || attempt >= 5) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
logger?.warn(message, {
|
|
129
|
+
...context,
|
|
130
|
+
attempt: attempt + 1,
|
|
131
|
+
retryAfterMs: waitMs,
|
|
132
|
+
error,
|
|
133
|
+
});
|
|
134
|
+
await sleep(waitMs + 250);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function sleep(ms) {
|
|
139
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { editHtmlMessage, isMessageNotModifiedError, replaceOrSendHtmlChunks, sendHtmlMessage, sendTypingAction, shouldFallbackToNewMessage, } from "./delivery.js";
|
|
2
|
+
import { renderMarkdownForTelegram, renderPlainChunksForTelegram, renderPlainForTelegram } from "./renderer.js";
|
|
3
|
+
const ACTIVITY_PULSE_INTERVAL_MS = 4_000;
|
|
4
|
+
export class MessageBuffer {
|
|
5
|
+
bot;
|
|
6
|
+
updateIntervalMs;
|
|
7
|
+
logger;
|
|
8
|
+
states = new Map();
|
|
9
|
+
constructor(bot, updateIntervalMs, logger) {
|
|
10
|
+
this.bot = bot;
|
|
11
|
+
this.updateIntervalMs = updateIntervalMs;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
async create(key, input) {
|
|
15
|
+
const previous = this.states.get(key);
|
|
16
|
+
if (previous) {
|
|
17
|
+
if (previous.timer)
|
|
18
|
+
clearTimeout(previous.timer);
|
|
19
|
+
this.stopActivityPulse(previous);
|
|
20
|
+
this.states.delete(key);
|
|
21
|
+
}
|
|
22
|
+
const message = await sendHtmlMessage(this.bot, {
|
|
23
|
+
chatId: input.chatId,
|
|
24
|
+
messageThreadId: input.messageThreadId,
|
|
25
|
+
text: "Codex is working...",
|
|
26
|
+
}, this.logger);
|
|
27
|
+
const state = {
|
|
28
|
+
chatId: input.chatId,
|
|
29
|
+
messageThreadId: input.messageThreadId,
|
|
30
|
+
messageId: message.message_id,
|
|
31
|
+
text: "",
|
|
32
|
+
progressLines: [],
|
|
33
|
+
planText: "",
|
|
34
|
+
reasoningSummaryText: "",
|
|
35
|
+
toolOutputText: "",
|
|
36
|
+
timer: null,
|
|
37
|
+
activityTimer: null,
|
|
38
|
+
activityInFlight: false,
|
|
39
|
+
lastSentText: "",
|
|
40
|
+
queue: Promise.resolve(),
|
|
41
|
+
};
|
|
42
|
+
this.states.set(key, state);
|
|
43
|
+
this.startActivityPulse(state);
|
|
44
|
+
return message.message_id;
|
|
45
|
+
}
|
|
46
|
+
has(key) {
|
|
47
|
+
return this.states.has(key);
|
|
48
|
+
}
|
|
49
|
+
setReplyDraft(key, text) {
|
|
50
|
+
const state = this.states.get(key);
|
|
51
|
+
if (!state)
|
|
52
|
+
return;
|
|
53
|
+
state.text = text;
|
|
54
|
+
this.scheduleFlush(key, state);
|
|
55
|
+
}
|
|
56
|
+
note(key, line) {
|
|
57
|
+
const state = this.states.get(key);
|
|
58
|
+
if (!state)
|
|
59
|
+
return;
|
|
60
|
+
const normalized = line.trim();
|
|
61
|
+
if (!normalized)
|
|
62
|
+
return;
|
|
63
|
+
const existingIndex = state.progressLines.findIndex((entry) => entry === normalized);
|
|
64
|
+
if (existingIndex >= 0) {
|
|
65
|
+
state.progressLines.splice(existingIndex, 1);
|
|
66
|
+
}
|
|
67
|
+
state.progressLines.push(normalized);
|
|
68
|
+
if (state.progressLines.length > 8) {
|
|
69
|
+
state.progressLines.splice(0, state.progressLines.length - 8);
|
|
70
|
+
}
|
|
71
|
+
this.scheduleFlush(key, state);
|
|
72
|
+
}
|
|
73
|
+
setPlan(key, text) {
|
|
74
|
+
const state = this.states.get(key);
|
|
75
|
+
if (!state)
|
|
76
|
+
return;
|
|
77
|
+
state.planText = text.trim();
|
|
78
|
+
this.scheduleFlush(key, state);
|
|
79
|
+
}
|
|
80
|
+
setReasoningSummary(key, text) {
|
|
81
|
+
const state = this.states.get(key);
|
|
82
|
+
if (!state)
|
|
83
|
+
return;
|
|
84
|
+
state.reasoningSummaryText = text.trim();
|
|
85
|
+
this.scheduleFlush(key, state);
|
|
86
|
+
}
|
|
87
|
+
setToolOutput(key, text) {
|
|
88
|
+
const state = this.states.get(key);
|
|
89
|
+
if (!state)
|
|
90
|
+
return;
|
|
91
|
+
state.toolOutputText = truncateTail(text.replace(/\r/g, "").trim(), 2000);
|
|
92
|
+
this.scheduleFlush(key, state);
|
|
93
|
+
}
|
|
94
|
+
rename(from, to) {
|
|
95
|
+
const state = this.states.get(from);
|
|
96
|
+
if (!state)
|
|
97
|
+
return;
|
|
98
|
+
this.states.delete(from);
|
|
99
|
+
this.states.set(to, state);
|
|
100
|
+
}
|
|
101
|
+
async complete(key, finalMarkdown) {
|
|
102
|
+
const state = this.states.get(key);
|
|
103
|
+
if (!state)
|
|
104
|
+
return;
|
|
105
|
+
if (state.timer)
|
|
106
|
+
clearTimeout(state.timer);
|
|
107
|
+
this.stopActivityPulse(state);
|
|
108
|
+
await this.enqueue(state, async () => {
|
|
109
|
+
const text = (finalMarkdown ?? state.text).trim();
|
|
110
|
+
const chunks = text
|
|
111
|
+
? renderMarkdownForTelegram(text)
|
|
112
|
+
: renderPlainChunksForTelegram("Codex finished, but returned no text to send.");
|
|
113
|
+
await this.replaceWithChunks(state, chunks);
|
|
114
|
+
this.states.delete(key);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async fail(key, message) {
|
|
118
|
+
const state = this.states.get(key);
|
|
119
|
+
if (!state)
|
|
120
|
+
return;
|
|
121
|
+
if (state.timer)
|
|
122
|
+
clearTimeout(state.timer);
|
|
123
|
+
this.stopActivityPulse(state);
|
|
124
|
+
await this.enqueue(state, async () => {
|
|
125
|
+
await this.replaceWithChunks(state, renderPlainChunksForTelegram(`Codex error: ${message}`));
|
|
126
|
+
this.states.delete(key);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async flush(key) {
|
|
130
|
+
const state = this.states.get(key);
|
|
131
|
+
if (!state)
|
|
132
|
+
return;
|
|
133
|
+
await this.enqueue(state, async () => {
|
|
134
|
+
const latest = this.states.get(key);
|
|
135
|
+
if (!latest)
|
|
136
|
+
return;
|
|
137
|
+
const text = renderPlainForTelegram(truncateForEdit(composePendingText(latest)));
|
|
138
|
+
if (text === latest.lastSentText)
|
|
139
|
+
return;
|
|
140
|
+
await this.safeEdit(latest, text);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
scheduleFlush(key, state) {
|
|
144
|
+
if (state.timer)
|
|
145
|
+
return;
|
|
146
|
+
state.timer = setTimeout(() => {
|
|
147
|
+
state.timer = null;
|
|
148
|
+
void this.flush(key);
|
|
149
|
+
}, this.updateIntervalMs);
|
|
150
|
+
}
|
|
151
|
+
async safeEdit(state, text) {
|
|
152
|
+
try {
|
|
153
|
+
await editHtmlMessage(this.bot, {
|
|
154
|
+
chatId: state.chatId,
|
|
155
|
+
messageId: state.messageId,
|
|
156
|
+
text,
|
|
157
|
+
}, this.logger);
|
|
158
|
+
state.lastSentText = text;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (isMessageNotModifiedError(error)) {
|
|
162
|
+
state.lastSentText = text;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (shouldFallbackToNewMessage(error)) {
|
|
166
|
+
const message = await sendHtmlMessage(this.bot, {
|
|
167
|
+
chatId: state.chatId,
|
|
168
|
+
messageThreadId: state.messageThreadId,
|
|
169
|
+
text,
|
|
170
|
+
}, this.logger);
|
|
171
|
+
state.messageId = message.message_id;
|
|
172
|
+
state.lastSentText = text;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
this.logger?.warn("telegram edit failed", {
|
|
177
|
+
message,
|
|
178
|
+
chatId: state.chatId,
|
|
179
|
+
messageThreadId: state.messageThreadId,
|
|
180
|
+
messageId: state.messageId,
|
|
181
|
+
});
|
|
182
|
+
process.stderr.write(`[telegram edit failed] ${message}\n`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
startActivityPulse(state) {
|
|
186
|
+
void this.sendActivityPulse(state);
|
|
187
|
+
const timer = setInterval(() => {
|
|
188
|
+
void this.sendActivityPulse(state);
|
|
189
|
+
}, ACTIVITY_PULSE_INTERVAL_MS);
|
|
190
|
+
timer.unref?.();
|
|
191
|
+
state.activityTimer = timer;
|
|
192
|
+
}
|
|
193
|
+
stopActivityPulse(state) {
|
|
194
|
+
if (!state.activityTimer)
|
|
195
|
+
return;
|
|
196
|
+
clearInterval(state.activityTimer);
|
|
197
|
+
state.activityTimer = null;
|
|
198
|
+
}
|
|
199
|
+
async sendActivityPulse(state) {
|
|
200
|
+
if (state.activityInFlight)
|
|
201
|
+
return;
|
|
202
|
+
state.activityInFlight = true;
|
|
203
|
+
try {
|
|
204
|
+
await sendTypingAction(this.bot, {
|
|
205
|
+
chatId: state.chatId,
|
|
206
|
+
messageThreadId: state.messageThreadId,
|
|
207
|
+
}, this.logger);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
this.logger?.warn("telegram chat action failed", {
|
|
211
|
+
chatId: state.chatId,
|
|
212
|
+
messageThreadId: state.messageThreadId,
|
|
213
|
+
message: error instanceof Error ? error.message : String(error),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
state.activityInFlight = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async replaceWithChunks(state, chunks) {
|
|
221
|
+
const messageId = await replaceOrSendHtmlChunks(this.bot, {
|
|
222
|
+
chatId: state.chatId,
|
|
223
|
+
messageThreadId: state.messageThreadId,
|
|
224
|
+
messageId: state.messageId,
|
|
225
|
+
chunks,
|
|
226
|
+
}, this.logger);
|
|
227
|
+
if (messageId != null) {
|
|
228
|
+
state.messageId = messageId;
|
|
229
|
+
}
|
|
230
|
+
const [first] = chunks;
|
|
231
|
+
if (first) {
|
|
232
|
+
state.lastSentText = first;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async enqueue(state, work) {
|
|
236
|
+
const run = state.queue.then(work, work);
|
|
237
|
+
state.queue = run.catch(() => undefined);
|
|
238
|
+
await run;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function truncateForEdit(text) {
|
|
242
|
+
if (text.length <= 3800)
|
|
243
|
+
return text || "Codex is working...";
|
|
244
|
+
return `${text.slice(0, 3800)}\n\n...`;
|
|
245
|
+
}
|
|
246
|
+
function composePendingText(state) {
|
|
247
|
+
const sections = ["Codex is working..."];
|
|
248
|
+
if (state.planText) {
|
|
249
|
+
sections.push(`[Plan]\n${state.planText}`);
|
|
250
|
+
}
|
|
251
|
+
const reasoningSummary = state.reasoningSummaryText.trim();
|
|
252
|
+
if (reasoningSummary) {
|
|
253
|
+
sections.push(`[Reasoning Summary]\n${reasoningSummary}`);
|
|
254
|
+
}
|
|
255
|
+
if (state.progressLines.length > 0) {
|
|
256
|
+
sections.push(`[Progress]\n${state.progressLines.join("\n")}`);
|
|
257
|
+
}
|
|
258
|
+
const toolOutput = state.toolOutputText.trim();
|
|
259
|
+
if (toolOutput) {
|
|
260
|
+
sections.push(`[Tool Output]\n${toolOutput}`);
|
|
261
|
+
}
|
|
262
|
+
const replyDraft = state.text.trim();
|
|
263
|
+
if (replyDraft) {
|
|
264
|
+
sections.push(`[Draft Reply]\n${replyDraft}`);
|
|
265
|
+
}
|
|
266
|
+
return sections.join("\n\n");
|
|
267
|
+
}
|
|
268
|
+
function truncateTail(text, maxLength) {
|
|
269
|
+
if (text.length <= maxLength)
|
|
270
|
+
return text;
|
|
271
|
+
return text.slice(text.length - maxLength);
|
|
272
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
import { splitTelegramHtml, splitTelegramText } from "./splitMessage.js";
|
|
3
|
+
const md = new MarkdownIt({
|
|
4
|
+
html: false,
|
|
5
|
+
linkify: true,
|
|
6
|
+
breaks: true,
|
|
7
|
+
});
|
|
8
|
+
export function escapeHtml(value) {
|
|
9
|
+
return value
|
|
10
|
+
.replaceAll("&", "&")
|
|
11
|
+
.replaceAll("<", "<")
|
|
12
|
+
.replaceAll(">", ">");
|
|
13
|
+
}
|
|
14
|
+
export function renderMarkdownForTelegram(markdown) {
|
|
15
|
+
const tokens = md.parse(markdown, {});
|
|
16
|
+
const html = renderTokens(tokens).replace(/\n{3,}/g, "\n\n").trim();
|
|
17
|
+
return splitTelegramHtml(html || escapeHtml(markdown || ""));
|
|
18
|
+
}
|
|
19
|
+
export function renderPlainForTelegram(text) {
|
|
20
|
+
return escapeHtml(text.trim() || " ");
|
|
21
|
+
}
|
|
22
|
+
export function renderPlainChunksForTelegram(text) {
|
|
23
|
+
return splitTelegramText(renderPlainForTelegram(text));
|
|
24
|
+
}
|
|
25
|
+
function renderTokens(tokens) {
|
|
26
|
+
let out = "";
|
|
27
|
+
const orderedStack = [];
|
|
28
|
+
for (const token of tokens) {
|
|
29
|
+
switch (token.type) {
|
|
30
|
+
case "heading_open":
|
|
31
|
+
out += "<b>";
|
|
32
|
+
break;
|
|
33
|
+
case "heading_close":
|
|
34
|
+
out += "</b>\n\n";
|
|
35
|
+
break;
|
|
36
|
+
case "paragraph_open":
|
|
37
|
+
break;
|
|
38
|
+
case "paragraph_close":
|
|
39
|
+
out += "\n\n";
|
|
40
|
+
break;
|
|
41
|
+
case "inline":
|
|
42
|
+
out += renderInline(token.children ?? []);
|
|
43
|
+
break;
|
|
44
|
+
case "bullet_list_open":
|
|
45
|
+
break;
|
|
46
|
+
case "bullet_list_close":
|
|
47
|
+
out += "\n";
|
|
48
|
+
break;
|
|
49
|
+
case "ordered_list_open":
|
|
50
|
+
orderedStack.push({ index: Number(token.attrGet("start") ?? "1") });
|
|
51
|
+
break;
|
|
52
|
+
case "ordered_list_close":
|
|
53
|
+
orderedStack.pop();
|
|
54
|
+
out += "\n";
|
|
55
|
+
break;
|
|
56
|
+
case "list_item_open": {
|
|
57
|
+
const current = orderedStack.at(-1);
|
|
58
|
+
if (current) {
|
|
59
|
+
out += `${current.index}. `;
|
|
60
|
+
current.index += 1;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
out += "- ";
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "list_item_close":
|
|
68
|
+
out += "\n";
|
|
69
|
+
break;
|
|
70
|
+
case "blockquote_open":
|
|
71
|
+
out += "<blockquote>";
|
|
72
|
+
break;
|
|
73
|
+
case "blockquote_close":
|
|
74
|
+
out += "</blockquote>\n\n";
|
|
75
|
+
break;
|
|
76
|
+
case "fence":
|
|
77
|
+
case "code_block":
|
|
78
|
+
out += `<pre><code>${escapeHtml(token.content)}</code></pre>\n\n`;
|
|
79
|
+
break;
|
|
80
|
+
case "hr":
|
|
81
|
+
out += "\n---\n\n";
|
|
82
|
+
break;
|
|
83
|
+
case "softbreak":
|
|
84
|
+
case "hardbreak":
|
|
85
|
+
out += "\n";
|
|
86
|
+
break;
|
|
87
|
+
case "text":
|
|
88
|
+
out += escapeHtml(token.content);
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function renderInline(tokens) {
|
|
97
|
+
let out = "";
|
|
98
|
+
for (const token of tokens) {
|
|
99
|
+
switch (token.type) {
|
|
100
|
+
case "text":
|
|
101
|
+
out += escapeHtml(token.content);
|
|
102
|
+
break;
|
|
103
|
+
case "code_inline":
|
|
104
|
+
out += `<code>${escapeHtml(token.content)}</code>`;
|
|
105
|
+
break;
|
|
106
|
+
case "strong_open":
|
|
107
|
+
out += "<b>";
|
|
108
|
+
break;
|
|
109
|
+
case "strong_close":
|
|
110
|
+
out += "</b>";
|
|
111
|
+
break;
|
|
112
|
+
case "em_open":
|
|
113
|
+
out += "<i>";
|
|
114
|
+
break;
|
|
115
|
+
case "em_close":
|
|
116
|
+
out += "</i>";
|
|
117
|
+
break;
|
|
118
|
+
case "s_open":
|
|
119
|
+
out += "<s>";
|
|
120
|
+
break;
|
|
121
|
+
case "s_close":
|
|
122
|
+
out += "</s>";
|
|
123
|
+
break;
|
|
124
|
+
case "link_open": {
|
|
125
|
+
const href = token.attrGet("href");
|
|
126
|
+
out += href ? `<a href="${escapeHtml(href)}">` : "";
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "link_close":
|
|
130
|
+
out += "</a>";
|
|
131
|
+
break;
|
|
132
|
+
case "softbreak":
|
|
133
|
+
case "hardbreak":
|
|
134
|
+
out += "\n";
|
|
135
|
+
break;
|
|
136
|
+
case "html_inline":
|
|
137
|
+
out += escapeHtml(token.content);
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
if (token.children)
|
|
141
|
+
out += renderInline(token.children);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|