@nordbyte/nordrelay 0.4.1 → 0.5.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/.env.example +155 -64
- package/README.md +69 -59
- package/dist/access-control.js +124 -115
- package/dist/agent-updates.js +19 -1
- package/dist/bot-rendering.js +838 -0
- package/dist/bot.js +87 -1288
- package/dist/channel-runtime.js +89 -0
- package/dist/config-metadata.js +238 -0
- package/dist/config.js +0 -58
- package/dist/index.js +8 -0
- package/dist/relay-runtime.js +36 -12
- package/dist/settings-service.js +2 -117
- package/dist/telegram-access-commands.js +123 -0
- package/dist/telegram-access-middleware.js +129 -0
- package/dist/telegram-channel-runtime.js +132 -0
- package/dist/telegram-command-menu.js +54 -0
- package/dist/telegram-output.js +216 -0
- package/dist/telegram-update-commands.js +88 -0
- package/dist/user-management.js +708 -0
- package/dist/web-api-contract.js +56 -0
- package/dist/web-dashboard-assets.js +33 -2
- package/dist/web-dashboard-ui.js +14 -14
- package/dist/web-dashboard.js +595 -133
- package/dist/webui-assets/dashboard.css +919 -0
- package/dist/webui-assets/dashboard.js +1611 -0
- package/package.json +6 -3
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/commands/remote.md +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +227 -78
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +1 -1
- package/dist/web-dashboard-client.js +0 -275
- package/dist/web-dashboard-style.js +0 -9
package/dist/bot.js
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { autoRetry } from "@grammyjs/auto-retry";
|
|
6
6
|
import { Bot, InlineKeyboard, InputFile } from "grammy";
|
|
7
|
-
import {
|
|
7
|
+
import { ADMIN_GROUP_ID } from "./access-control.js";
|
|
8
8
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
9
9
|
import { collectArtifactReport, collectRecentWorkspaceArtifacts, createArtifactZipBundle, ensureOutDir, formatArtifactSummary, getArtifactTurnReport, isTelegramImagePreview, listRecentArtifactReports, persistWorkspaceArtifactReport, pruneConnectorTurnDirs, removeArtifactTurn, telegramArtifactFilename, totalArtifactSize, } from "./artifacts.js";
|
|
10
10
|
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
@@ -12,9 +12,10 @@ import { AgentUpdateManager } from "./agent-updates.js";
|
|
|
12
12
|
import { AuditLogStore } from "./audit-log.js";
|
|
13
13
|
import { formatSessionLabel, renderHelpMessage, renderWelcomeFirstTime, renderWelcomeReturning, } from "./bot-ui.js";
|
|
14
14
|
import { BotPreferencesStore, formatQuietHours, isQuietNow, parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
|
|
15
|
-
import { logTailRequests,
|
|
15
|
+
import { logTailRequests, parseLogsCommand, renderAgentUpdateJobAction, renderAgentsAction, renderArtifactReportsAction, renderChannelsAction, renderLogTailsAction, renderQueueListAction, renderQueuedPromptDetailAction, } from "./channel-actions.js";
|
|
16
16
|
import { listChannelDescriptors } from "./channel-adapter.js";
|
|
17
|
-
import {
|
|
17
|
+
import { deliverChannelAction } from "./channel-runtime.js";
|
|
18
|
+
import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
18
19
|
import { getAgentActivityLog, getAgentDiagnostics, getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
19
20
|
import { enabledAgents } from "./agent-factory.js";
|
|
20
21
|
import { checkAuthStatus, clearAuthCache, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
@@ -22,8 +23,8 @@ import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout
|
|
|
22
23
|
import { formatLaunchProfileBehavior } from "./codex-launch.js";
|
|
23
24
|
import { contextKeyFromCtx, isTelegramContextKey, isTopicContextKey, parseContextKey } from "./context-key.js";
|
|
24
25
|
import { friendlyErrorText } from "./error-messages.js";
|
|
25
|
-
import { escapeHTML
|
|
26
|
-
import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart,
|
|
26
|
+
import { escapeHTML } from "./format.js";
|
|
27
|
+
import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, } from "./operations.js";
|
|
27
28
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
28
29
|
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
29
30
|
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
@@ -34,72 +35,22 @@ import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTM
|
|
|
34
35
|
import { SessionRegistry } from "./session-registry.js";
|
|
35
36
|
import { getAvailableBackends, transcribeAudio } from "./voice.js";
|
|
36
37
|
import { getTelegramRateLimitMetrics, telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
38
|
+
import { chatBucket, downloadTelegramFile, isMessageNotModifiedError, renderMarkdownChunkWithinLimit, safeEditMessage, safeEditReplyMarkup, safeReply, sendChatActionSafe, sendTextMessage, splitMarkdownForTelegram, } from "./telegram-output.js";
|
|
39
|
+
import { NOOP_PAGE_CALLBACK_DATA, TelegramBotChannelRuntime, paginateKeyboard, telegramChannelContextFromCtx, } from "./telegram-channel-runtime.js";
|
|
40
|
+
import { createTelegramAccessMiddleware } from "./telegram-access-middleware.js";
|
|
41
|
+
import { registerTelegramAccessCommands } from "./telegram-access-commands.js";
|
|
42
|
+
import { registerTelegramUpdateCommands } from "./telegram-update-commands.js";
|
|
43
|
+
import { appendWithCap, authHelpText, buildArtifactActionsKeyboard, buildStreamingPreview, capabilitiesOf, filterActivityEvents, filterArtifactReports, filterSessions, formatAgentLaunchProfileLabel, formatAgentSettingScope, formatCliPathHTML, formatCliPathPlain, formatDurationSeconds, formatError, formatLocalDateTime, formatLockOwner, formatModelButtonLabel, formatRelativeTime, formatTelegramName, formatToolSummaryLine, formatTurnUsageLine, getWorkspaceShortName, idOf, isEmptyArtifactReport, isPromptEnvelopeLike, isQueuedPromptLike, labelOf, orderPinnedSessions, parseActivityOptions, parseFastModeArgument, parseToggle, renderActivityTimeline, renderAgentDiagnostics, renderAuditEvents, renderDiagnosticsHTML, renderDiagnosticsPlain, renderExternalMirrorEvent, renderExternalMirrorStatus, renderHealthHTML, renderHealthPlain, renderPromptFailure, renderProgressHTML, renderProgressPlain, renderSessionLocks, renderTodoList, renderToolEndMessage, renderToolStartMessage, renderVersionCheckHTML, renderVersionCheckPlain, requiresTurnApproval, trimLine, } from "./bot-rendering.js";
|
|
44
|
+
import { UserStore } from "./user-management.js";
|
|
37
45
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
|
|
38
|
-
|
|
46
|
+
export { formatToolSummaryLine, formatTurnUsageLine, summarizeToolName } from "./bot-rendering.js";
|
|
47
|
+
export { registerCommands } from "./telegram-command-menu.js";
|
|
39
48
|
const EDIT_DEBOUNCE_MS = 1500;
|
|
40
49
|
const TYPING_INTERVAL_MS = 4500;
|
|
41
50
|
const TOOL_OUTPUT_PREVIEW_LIMIT = 500;
|
|
42
|
-
const STREAMING_PREVIEW_LIMIT = 3800;
|
|
43
|
-
const FORMATTED_CHUNK_TARGET = 3000;
|
|
44
51
|
const MAX_AUDIO_FILE_SIZE = 25 * 1024 * 1024;
|
|
45
52
|
const MEDIA_GROUP_FLUSH_MS = 1200;
|
|
46
|
-
const KEYBOARD_PAGE_SIZE = 6;
|
|
47
|
-
const NOOP_PAGE_CALLBACK_DATA = "noop_page";
|
|
48
53
|
const LAUNCH_PROFILES_COMMAND = "/launch_profiles";
|
|
49
|
-
function paginateKeyboard(items, page, prefix) {
|
|
50
|
-
const totalPages = Math.max(1, Math.ceil(items.length / KEYBOARD_PAGE_SIZE));
|
|
51
|
-
const currentPage = Math.min(Math.max(page, 0), totalPages - 1);
|
|
52
|
-
const start = currentPage * KEYBOARD_PAGE_SIZE;
|
|
53
|
-
const pageItems = items.slice(start, start + KEYBOARD_PAGE_SIZE);
|
|
54
|
-
const keyboard = new InlineKeyboard();
|
|
55
|
-
pageItems.forEach((item, index) => {
|
|
56
|
-
keyboard.text(item.label, item.callbackData);
|
|
57
|
-
if (index < pageItems.length - 1 || totalPages > 1) {
|
|
58
|
-
keyboard.row();
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
if (totalPages > 1) {
|
|
62
|
-
if (currentPage > 0) {
|
|
63
|
-
keyboard.text("◀️ Prev", `${prefix}_page_${currentPage - 1}`);
|
|
64
|
-
}
|
|
65
|
-
keyboard.text(`${currentPage + 1}/${totalPages}`, NOOP_PAGE_CALLBACK_DATA);
|
|
66
|
-
if (currentPage < totalPages - 1) {
|
|
67
|
-
keyboard.text("Next ▶️", `${prefix}_page_${currentPage + 1}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return keyboard;
|
|
71
|
-
}
|
|
72
|
-
function actionKeyboard(rows) {
|
|
73
|
-
if (!rows || rows.length === 0) {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
const keyboard = new InlineKeyboard();
|
|
77
|
-
for (const row of rows) {
|
|
78
|
-
for (const button of row) {
|
|
79
|
-
keyboard.text(button.label, telegramActionData(button.action));
|
|
80
|
-
}
|
|
81
|
-
keyboard.row();
|
|
82
|
-
}
|
|
83
|
-
return keyboard;
|
|
84
|
-
}
|
|
85
|
-
function telegramActionData(action) {
|
|
86
|
-
if (action === "agent-update:jobs") {
|
|
87
|
-
return "upd_jobs";
|
|
88
|
-
}
|
|
89
|
-
const agentUpdateStart = action.match(/^agent-update:start:(.+)$/);
|
|
90
|
-
if (agentUpdateStart?.[1]) {
|
|
91
|
-
return `upd_agent:${agentUpdateStart[1]}`;
|
|
92
|
-
}
|
|
93
|
-
const agentUpdateLog = action.match(/^agent-update:log:(.+)$/);
|
|
94
|
-
if (agentUpdateLog?.[1]) {
|
|
95
|
-
return `upd_log:${agentUpdateLog[1]}`;
|
|
96
|
-
}
|
|
97
|
-
const agentUpdateCancel = action.match(/^agent-update:cancel:(.+)$/);
|
|
98
|
-
if (agentUpdateCancel?.[1]) {
|
|
99
|
-
return `upd_cancel:${agentUpdateCancel[1]}`;
|
|
100
|
-
}
|
|
101
|
-
return action;
|
|
102
|
-
}
|
|
103
54
|
export function createBot(config, registry) {
|
|
104
55
|
configureRedaction(config.telegramRedactPatterns);
|
|
105
56
|
telegramRateLimiter.configure({
|
|
@@ -109,6 +60,7 @@ export function createBot(config, registry) {
|
|
|
109
60
|
});
|
|
110
61
|
const bot = new Bot(config.telegramBotToken);
|
|
111
62
|
bot.api.config.use(autoRetry({ maxRetryAttempts: 3, maxDelaySeconds: 10 }));
|
|
63
|
+
const telegramChannelRuntime = new TelegramBotChannelRuntime(bot);
|
|
112
64
|
const contextBusy = new Map();
|
|
113
65
|
const pendingApprovals = new Map();
|
|
114
66
|
const pendingSessionPicks = new Map();
|
|
@@ -127,7 +79,10 @@ export function createBot(config, registry) {
|
|
|
127
79
|
const preferencesStore = new BotPreferencesStore(config.workspace, config.stateBackend);
|
|
128
80
|
const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
129
81
|
const lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
82
|
+
const userStore = new UserStore();
|
|
83
|
+
const contextUsers = new WeakMap();
|
|
130
84
|
const agentUpdates = new AgentUpdateManager();
|
|
85
|
+
const linkAttempts = new Map();
|
|
131
86
|
const drainingQueues = new Set();
|
|
132
87
|
const externalQueueTimers = new Map();
|
|
133
88
|
const externalMirrors = new Map();
|
|
@@ -254,6 +209,13 @@ export function createBot(config, registry) {
|
|
|
254
209
|
}
|
|
255
210
|
return checkAuthStatus(config.codexApiKey);
|
|
256
211
|
};
|
|
212
|
+
const replyChannelAction = async (ctx, rendered) => {
|
|
213
|
+
const channelContext = telegramChannelContextFromCtx(ctx);
|
|
214
|
+
if (!channelContext) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await deliverChannelAction(telegramChannelRuntime, channelContext, rendered);
|
|
218
|
+
};
|
|
257
219
|
const agentUpdateContext = () => ({
|
|
258
220
|
piCliPath: config.piCliPath,
|
|
259
221
|
hermesCliPath: config.hermesCliPath,
|
|
@@ -275,10 +237,7 @@ export function createBot(config, registry) {
|
|
|
275
237
|
});
|
|
276
238
|
}
|
|
277
239
|
const rendered = renderAgentUpdateJobAction(job);
|
|
278
|
-
await
|
|
279
|
-
fallbackText: rendered.plain,
|
|
280
|
-
replyMarkup: actionKeyboard(rendered.buttons),
|
|
281
|
-
});
|
|
240
|
+
await replyChannelAction(ctx, rendered);
|
|
282
241
|
}
|
|
283
242
|
catch (error) {
|
|
284
243
|
const message = `Failed to start ${agentLabel(agentId)} update: ${friendlyErrorText(error)}`;
|
|
@@ -416,6 +375,9 @@ export function createBot(config, registry) {
|
|
|
416
375
|
};
|
|
417
376
|
};
|
|
418
377
|
const updateQueueStatusMessage = async (contextKey, text) => {
|
|
378
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
419
381
|
const parsed = parseContextKey(contextKey);
|
|
420
382
|
const html = escapeHTML(text);
|
|
421
383
|
const state = queueStatusMessages.get(contextKey) ?? {};
|
|
@@ -458,6 +420,9 @@ export function createBot(config, registry) {
|
|
|
458
420
|
if (!isTelegramContextKey(contextKey)) {
|
|
459
421
|
return;
|
|
460
422
|
}
|
|
423
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
461
426
|
const session = await registry.getOrCreate(contextKey, { deferThreadStart: true }).catch(() => null);
|
|
462
427
|
if (!session) {
|
|
463
428
|
return;
|
|
@@ -606,7 +571,20 @@ export function createBot(config, registry) {
|
|
|
606
571
|
}
|
|
607
572
|
state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
|
|
608
573
|
};
|
|
574
|
+
const canSendSystemMessagesToContext = (contextKey) => {
|
|
575
|
+
if (!userStore.hasAdminUser()) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
const parsed = parseContextKey(contextKey);
|
|
579
|
+
if (parsed.chatId > 0) {
|
|
580
|
+
return Boolean(userStore.resolveTelegramUser(parsed.chatId));
|
|
581
|
+
}
|
|
582
|
+
return userStore.snapshot().telegramChats.some((chat) => chat.chatId === parsed.chatId && chat.enabled);
|
|
583
|
+
};
|
|
609
584
|
const deliverCliGeneratedArtifacts = async (contextKey, chatId, session, startedAt, turnId, messageThreadId) => {
|
|
585
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
610
588
|
if (!startedAt || !turnId) {
|
|
611
589
|
return;
|
|
612
590
|
}
|
|
@@ -658,6 +636,9 @@ export function createBot(config, registry) {
|
|
|
658
636
|
if (promptStore.list(contextKey).length === 0) {
|
|
659
637
|
return;
|
|
660
638
|
}
|
|
639
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
661
642
|
const busy = getBusyReason(contextKey);
|
|
662
643
|
if (busy.kind === "external") {
|
|
663
644
|
const label = busy.activity.agentLabel;
|
|
@@ -677,37 +658,12 @@ export function createBot(config, registry) {
|
|
|
677
658
|
timer.unref?.();
|
|
678
659
|
externalQueueTimers.set(contextKey, timer);
|
|
679
660
|
};
|
|
661
|
+
const getAuthenticatedUser = (ctx) => contextUsers.get(ctx) ?? null;
|
|
680
662
|
const getUserRole = (ctx) => {
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
return "admin";
|
|
684
|
-
}
|
|
685
|
-
if (fromId !== undefined && config.telegramReadOnlyUserIdSet.has(fromId)) {
|
|
686
|
-
return "readonly";
|
|
687
|
-
}
|
|
688
|
-
return "operator";
|
|
689
|
-
};
|
|
690
|
-
const getRequiredPermission = (ctx) => {
|
|
691
|
-
if (ctx.callbackQuery?.data) {
|
|
692
|
-
return permissionForCallbackData(ctx.callbackQuery.data);
|
|
693
|
-
}
|
|
694
|
-
if (ctx.message?.voice || ctx.message?.audio || ctx.message?.photo || ctx.message?.document) {
|
|
695
|
-
return "files";
|
|
696
|
-
}
|
|
697
|
-
const text = ctx.message?.text?.trim();
|
|
698
|
-
if (!text) {
|
|
699
|
-
return "inspect";
|
|
700
|
-
}
|
|
701
|
-
if (!text.startsWith("/")) {
|
|
702
|
-
return "prompt";
|
|
703
|
-
}
|
|
704
|
-
const command = extractCommandName(text);
|
|
705
|
-
if (command === "queue") {
|
|
706
|
-
const argument = text.replace(/^\/queue(?:@\w+)?\s*/i, "").trim();
|
|
707
|
-
return argument ? "prompt" : "inspect";
|
|
708
|
-
}
|
|
709
|
-
return permissionForCommand(command);
|
|
663
|
+
const authUser = getAuthenticatedUser(ctx);
|
|
664
|
+
return authUser?.groups.map((group) => group.name).join(", ") || "unauthenticated";
|
|
710
665
|
};
|
|
666
|
+
const isAdminUser = (ctx) => Boolean(getAuthenticatedUser(ctx)?.groups.some((group) => group.id === ADMIN_GROUP_ID));
|
|
711
667
|
const audit = (event) => {
|
|
712
668
|
try {
|
|
713
669
|
auditLog.append(event);
|
|
@@ -730,7 +686,7 @@ export function createBot(config, registry) {
|
|
|
730
686
|
};
|
|
731
687
|
const denyIfLocked = async (ctx, contextKey, session) => {
|
|
732
688
|
const lock = lockStore.get(contextKey);
|
|
733
|
-
const isAdmin =
|
|
689
|
+
const isAdmin = isAdminUser(ctx);
|
|
734
690
|
if (canWriteWithLock(lock, ctx.from?.id, isAdmin)) {
|
|
735
691
|
return false;
|
|
736
692
|
}
|
|
@@ -801,6 +757,9 @@ export function createBot(config, registry) {
|
|
|
801
757
|
}
|
|
802
758
|
pendingApprovals.delete(approvalId);
|
|
803
759
|
getBusyState(contextKey).approving = false;
|
|
760
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
804
763
|
const parsed = parseContextKey(contextKey);
|
|
805
764
|
void sendTextMessage(bot.api, parsed.chatId, `Approval timed out for prompt ${approvalId}.`, {
|
|
806
765
|
messageThreadId: parsed.messageThreadId,
|
|
@@ -831,6 +790,9 @@ export function createBot(config, registry) {
|
|
|
831
790
|
await safeReply(ctx, html, { fallbackText: plain, replyMarkup: keyboard });
|
|
832
791
|
};
|
|
833
792
|
const handleUserPrompt = async (ctx, contextKey, chatId, session, prompt, options = {}) => {
|
|
793
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
834
796
|
const parsed = parseContextKey(contextKey);
|
|
835
797
|
const messageThreadId = parsed.messageThreadId;
|
|
836
798
|
const envelope = isPromptEnvelopeLike(prompt) ? prompt : toPromptEnvelope(prompt);
|
|
@@ -1374,6 +1336,9 @@ export function createBot(config, registry) {
|
|
|
1374
1336
|
if (drainingQueues.has(contextKey)) {
|
|
1375
1337
|
return;
|
|
1376
1338
|
}
|
|
1339
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1377
1342
|
drainingQueues.add(contextKey);
|
|
1378
1343
|
try {
|
|
1379
1344
|
while (true) {
|
|
@@ -1536,16 +1501,25 @@ export function createBot(config, registry) {
|
|
|
1536
1501
|
clearTimeout(pending.timer);
|
|
1537
1502
|
pendingMediaGroups.delete(key);
|
|
1538
1503
|
try {
|
|
1504
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1539
1507
|
await processMediaGroup(pending);
|
|
1540
1508
|
}
|
|
1541
1509
|
catch (error) {
|
|
1542
1510
|
console.error("Failed to process media group:", error);
|
|
1511
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1543
1514
|
await safeReply(pending.ctx, `<b>Failed to process media group:</b> ${escapeHTML(friendlyErrorText(error))}`, {
|
|
1544
1515
|
fallbackText: `Failed to process media group: ${friendlyErrorText(error)}`,
|
|
1545
1516
|
});
|
|
1546
1517
|
}
|
|
1547
1518
|
};
|
|
1548
1519
|
const processMediaGroup = async (pending) => {
|
|
1520
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1549
1523
|
const busyState = getBusyState(pending.contextKey);
|
|
1550
1524
|
busyState.transcribing = true;
|
|
1551
1525
|
const turnId = randomUUID().slice(0, 12);
|
|
@@ -1596,10 +1570,16 @@ export function createBot(config, registry) {
|
|
|
1596
1570
|
busyState.transcribing = false;
|
|
1597
1571
|
}
|
|
1598
1572
|
if (stagedFiles.length === 0) {
|
|
1573
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1599
1576
|
const text = skippedCount > 0 ? "No media group files could be staged." : "Media group was empty.";
|
|
1600
1577
|
await safeReply(pending.ctx, escapeHTML(text), { fallbackText: text });
|
|
1601
1578
|
return;
|
|
1602
1579
|
}
|
|
1580
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1603
1583
|
const receivedText = `Received ${stagedFiles.length} media group file${stagedFiles.length === 1 ? "" : "s"}${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}.`;
|
|
1604
1584
|
await safeReply(pending.ctx, escapeHTML(receivedText), { fallbackText: receivedText });
|
|
1605
1585
|
await sendChatActionSafe(pending.ctx.api, pending.chatId, "typing", pending.messageThreadId).catch(() => { });
|
|
@@ -1621,35 +1601,8 @@ export function createBot(config, registry) {
|
|
|
1621
1601
|
await clearReaction(pending.ctx);
|
|
1622
1602
|
}
|
|
1623
1603
|
};
|
|
1624
|
-
bot.use(
|
|
1625
|
-
|
|
1626
|
-
const chatId = ctx.chat?.id;
|
|
1627
|
-
const authorized = config.telegramAllowAnyChat ||
|
|
1628
|
-
(fromId !== undefined && config.telegramAllowedUserIdSet.has(fromId)) ||
|
|
1629
|
-
(chatId !== undefined && config.telegramAllowedChatIdSet.has(chatId));
|
|
1630
|
-
if (!authorized) {
|
|
1631
|
-
if (ctx.callbackQuery) {
|
|
1632
|
-
await ctx.answerCallbackQuery({ text: "Unauthorized" }).catch(() => { });
|
|
1633
|
-
}
|
|
1634
|
-
else if (ctx.chat) {
|
|
1635
|
-
await safeReply(ctx, escapeHTML("Unauthorized"), { fallbackText: "Unauthorized" });
|
|
1636
|
-
}
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
const role = getUserRole(ctx);
|
|
1640
|
-
const permission = getRequiredPermission(ctx);
|
|
1641
|
-
if (!hasTelegramPermission(config.telegramRolePolicies, role, permission)) {
|
|
1642
|
-
const message = `Access denied: ${permission} permission required.`;
|
|
1643
|
-
if (ctx.callbackQuery) {
|
|
1644
|
-
await ctx.answerCallbackQuery({ text: message }).catch(() => { });
|
|
1645
|
-
}
|
|
1646
|
-
else {
|
|
1647
|
-
await safeReply(ctx, escapeHTML(message), { fallbackText: message });
|
|
1648
|
-
}
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
await next();
|
|
1652
|
-
});
|
|
1604
|
+
bot.use(createTelegramAccessMiddleware({ userStore, contextUsers, audit }));
|
|
1605
|
+
registerTelegramAccessCommands({ bot, userStore, contextUsers, linkAttempts, audit, getUserRole });
|
|
1653
1606
|
bot.command("start", async (ctx) => {
|
|
1654
1607
|
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
1655
1608
|
if (!contextSession) {
|
|
@@ -1679,11 +1632,11 @@ export function createBot(config, registry) {
|
|
|
1679
1632
|
});
|
|
1680
1633
|
bot.command("channels", async (ctx) => {
|
|
1681
1634
|
const rendered = renderChannelsAction(listChannelDescriptors());
|
|
1682
|
-
await
|
|
1635
|
+
await replyChannelAction(ctx, rendered);
|
|
1683
1636
|
});
|
|
1684
1637
|
bot.command("agents", async (ctx) => {
|
|
1685
1638
|
const rendered = renderAgentsAction(listAgentAdapterDescriptors(), enabledAgents(config));
|
|
1686
|
-
await
|
|
1639
|
+
await replyChannelAction(ctx, rendered);
|
|
1687
1640
|
});
|
|
1688
1641
|
bot.command("agent", async (ctx) => {
|
|
1689
1642
|
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
@@ -2140,7 +2093,7 @@ export function createBot(config, registry) {
|
|
|
2140
2093
|
}
|
|
2141
2094
|
const { contextKey, session } = contextSession;
|
|
2142
2095
|
const existing = lockStore.get(contextKey);
|
|
2143
|
-
if (existing && existing.ownerId !== ctx.from.id &&
|
|
2096
|
+
if (existing && existing.ownerId !== ctx.from.id && !isAdminUser(ctx)) {
|
|
2144
2097
|
const text = `Session is already locked by ${formatLockOwner(existing)}.`;
|
|
2145
2098
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2146
2099
|
return;
|
|
@@ -2161,7 +2114,7 @@ export function createBot(config, registry) {
|
|
|
2161
2114
|
}
|
|
2162
2115
|
const { contextKey, session } = contextSession;
|
|
2163
2116
|
const lock = lockStore.get(contextKey);
|
|
2164
|
-
if (lock && lock.ownerId !== ctx.from?.id &&
|
|
2117
|
+
if (lock && lock.ownerId !== ctx.from?.id && !isAdminUser(ctx)) {
|
|
2165
2118
|
const text = `Only ${formatLockOwner(lock)} or an admin can unlock this session.`;
|
|
2166
2119
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2167
2120
|
return;
|
|
@@ -2250,7 +2203,7 @@ export function createBot(config, registry) {
|
|
|
2250
2203
|
tail: await readFormattedLogTail(logRequest.lines, request.path),
|
|
2251
2204
|
})));
|
|
2252
2205
|
const rendered = renderLogTailsAction(logs);
|
|
2253
|
-
await
|
|
2206
|
+
await replyChannelAction(ctx, rendered);
|
|
2254
2207
|
});
|
|
2255
2208
|
bot.command("restart", async (ctx) => {
|
|
2256
2209
|
await safeReply(ctx, escapeHTML("Restarting connector..."), {
|
|
@@ -2260,94 +2213,7 @@ export function createBot(config, registry) {
|
|
|
2260
2213
|
spawnConnectorRestart();
|
|
2261
2214
|
}, 300);
|
|
2262
2215
|
});
|
|
2263
|
-
bot
|
|
2264
|
-
const rawText = ctx.message?.text ?? "";
|
|
2265
|
-
const argument = rawText.replace(/^\/update(?:@\w+)?\s*/i, "").trim();
|
|
2266
|
-
const tokens = argument.split(/\s+/).filter(Boolean);
|
|
2267
|
-
const subcommand = tokens[0]?.toLowerCase();
|
|
2268
|
-
if (subcommand === "agents" || subcommand === "agent") {
|
|
2269
|
-
const rendered = renderAgentUpdatePickerAction(listAgentAdapterDescriptors());
|
|
2270
|
-
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain, replyMarkup: actionKeyboard(rendered.buttons) });
|
|
2271
|
-
return;
|
|
2272
|
-
}
|
|
2273
|
-
if (subcommand === "jobs" || subcommand === "status") {
|
|
2274
|
-
const rendered = renderAgentUpdateJobsAction(agentUpdates.list());
|
|
2275
|
-
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2278
|
-
if (subcommand === "log" && tokens[1]) {
|
|
2279
|
-
const rendered = renderAgentUpdateLogAction(agentUpdates.readLog(tokens[1]));
|
|
2280
|
-
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2281
|
-
return;
|
|
2282
|
-
}
|
|
2283
|
-
if (subcommand === "cancel" && tokens[1]) {
|
|
2284
|
-
const job = agentUpdates.cancel(tokens[1]);
|
|
2285
|
-
const rendered = renderAgentUpdateJobAction(job);
|
|
2286
|
-
await safeReply(ctx, rendered.html, {
|
|
2287
|
-
fallbackText: rendered.plain,
|
|
2288
|
-
replyMarkup: actionKeyboard(rendered.buttons),
|
|
2289
|
-
});
|
|
2290
|
-
return;
|
|
2291
|
-
}
|
|
2292
|
-
if ((subcommand === "input" || subcommand === "send") && tokens[1] && tokens.slice(2).join(" ").trim()) {
|
|
2293
|
-
const job = agentUpdates.sendInput(tokens[1], tokens.slice(2).join(" "));
|
|
2294
|
-
const rendered = renderAgentUpdateJobAction(job);
|
|
2295
|
-
await safeReply(ctx, rendered.html, {
|
|
2296
|
-
fallbackText: rendered.plain,
|
|
2297
|
-
replyMarkup: actionKeyboard(rendered.buttons),
|
|
2298
|
-
});
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
const requestedAgent = parseAgentUpdateId(subcommand);
|
|
2302
|
-
if (requestedAgent) {
|
|
2303
|
-
await startTelegramAgentUpdate(ctx, requestedAgent);
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
if (subcommand) {
|
|
2307
|
-
const usage = "Unknown update target. Use /update, /update agents, /update jobs, /update <agent>, /update log <id>, /update cancel <id>, or /update input <id> <text>.";
|
|
2308
|
-
await safeReply(ctx, escapeHTML(usage), { fallbackText: usage });
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
const update = spawnSelfUpdate();
|
|
2312
|
-
const rendered = renderSelfUpdateStartedAction(update);
|
|
2313
|
-
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2314
|
-
});
|
|
2315
|
-
bot.callbackQuery("upd_jobs", async (ctx) => {
|
|
2316
|
-
await ctx.answerCallbackQuery();
|
|
2317
|
-
const rendered = renderAgentUpdateJobsAction(agentUpdates.list());
|
|
2318
|
-
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2319
|
-
});
|
|
2320
|
-
bot.callbackQuery(/^upd_agent:(codex|pi|hermes|openclaw|claude-code)$/, async (ctx) => {
|
|
2321
|
-
const agentId = ctx.match?.[1];
|
|
2322
|
-
if (!agentId) {
|
|
2323
|
-
await ctx.answerCallbackQuery();
|
|
2324
|
-
return;
|
|
2325
|
-
}
|
|
2326
|
-
await ctx.answerCallbackQuery({ text: `Starting ${agentLabel(agentId)} update...` });
|
|
2327
|
-
await startTelegramAgentUpdate(ctx, agentId);
|
|
2328
|
-
});
|
|
2329
|
-
bot.callbackQuery(/^upd_log:(.+)$/, async (ctx) => {
|
|
2330
|
-
const id = ctx.match?.[1];
|
|
2331
|
-
await ctx.answerCallbackQuery();
|
|
2332
|
-
if (!id) {
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
const rendered = renderAgentUpdateLogAction(agentUpdates.readLog(id));
|
|
2336
|
-
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2337
|
-
});
|
|
2338
|
-
bot.callbackQuery(/^upd_cancel:(.+)$/, async (ctx) => {
|
|
2339
|
-
const id = ctx.match?.[1];
|
|
2340
|
-
await ctx.answerCallbackQuery({ text: "Cancelling update..." });
|
|
2341
|
-
if (!id) {
|
|
2342
|
-
return;
|
|
2343
|
-
}
|
|
2344
|
-
const job = agentUpdates.cancel(id);
|
|
2345
|
-
const rendered = renderAgentUpdateJobAction(job);
|
|
2346
|
-
await safeReply(ctx, rendered.html, {
|
|
2347
|
-
fallbackText: rendered.plain,
|
|
2348
|
-
replyMarkup: actionKeyboard(rendered.buttons),
|
|
2349
|
-
});
|
|
2350
|
-
});
|
|
2216
|
+
registerTelegramUpdateCommands({ bot, agentUpdates, replyChannelAction, startTelegramAgentUpdate });
|
|
2351
2217
|
bot.command("new", async (ctx) => {
|
|
2352
2218
|
const chatId = ctx.chat?.id;
|
|
2353
2219
|
if (!chatId) {
|
|
@@ -3364,8 +3230,7 @@ export function createBot(config, registry) {
|
|
|
3364
3230
|
await ctx.answerCallbackQuery({ text: "Approval expired" });
|
|
3365
3231
|
return;
|
|
3366
3232
|
}
|
|
3367
|
-
|
|
3368
|
-
if (pending.requestedBy !== undefined && ctx.from?.id !== pending.requestedBy && role !== "admin") {
|
|
3233
|
+
if (pending.requestedBy !== undefined && ctx.from?.id !== pending.requestedBy && !isAdminUser(ctx)) {
|
|
3369
3234
|
await ctx.answerCallbackQuery({ text: "Only the requester or an admin can approve" });
|
|
3370
3235
|
return;
|
|
3371
3236
|
}
|
|
@@ -4081,1069 +3946,3 @@ export function createBot(config, registry) {
|
|
|
4081
3946
|
});
|
|
4082
3947
|
return bot;
|
|
4083
3948
|
}
|
|
4084
|
-
export async function registerCommands(bot) {
|
|
4085
|
-
await bot.api.setMyCommands([
|
|
4086
|
-
{ command: "start", description: "Welcome & status" },
|
|
4087
|
-
{ command: "help", description: "Command reference" },
|
|
4088
|
-
{ command: "channels", description: "Messaging adapter status" },
|
|
4089
|
-
{ command: "agents", description: "Agent adapter status" },
|
|
4090
|
-
{ command: "agent", description: "Select agent" },
|
|
4091
|
-
{ command: "new", description: "Start a new thread" },
|
|
4092
|
-
{ command: "session", description: "Current thread details" },
|
|
4093
|
-
{ command: "sessions", description: "Browse & switch threads" },
|
|
4094
|
-
{ command: "sync", description: "Sync active session from CLI state" },
|
|
4095
|
-
{ command: "pinned", description: "Show pinned threads" },
|
|
4096
|
-
{ command: "pin", description: "Pin current or given thread" },
|
|
4097
|
-
{ command: "unpin", description: "Unpin current or given thread" },
|
|
4098
|
-
{ command: "retry", description: "Resend the last prompt" },
|
|
4099
|
-
{ command: "queue", description: "Show queued prompts" },
|
|
4100
|
-
{ command: "cancel", description: "Cancel a queued prompt" },
|
|
4101
|
-
{ command: "clearqueue", description: "Clear queued prompts" },
|
|
4102
|
-
{ command: "artifacts", description: "List or resend generated files" },
|
|
4103
|
-
{ command: "workspaces", description: "List allowed workspaces" },
|
|
4104
|
-
{ command: "abort", description: "Cancel current operation" },
|
|
4105
|
-
{ command: "stop", description: "Cancel current operation" },
|
|
4106
|
-
{ command: "launch_profiles", description: "Select launch profile" },
|
|
4107
|
-
{ command: "fast", description: "Toggle fast mode" },
|
|
4108
|
-
{ command: "model", description: "View & change model" },
|
|
4109
|
-
{ command: "reasoning", description: "Set reasoning effort" },
|
|
4110
|
-
{ command: "mirror", description: "Control CLI mirroring" },
|
|
4111
|
-
{ command: "notify", description: "Control notifications" },
|
|
4112
|
-
{ command: "auth", description: "Check auth status" },
|
|
4113
|
-
{ command: "login", description: "Start authentication" },
|
|
4114
|
-
{ command: "logout", description: "Sign out" },
|
|
4115
|
-
{ command: "voice", description: "Voice transcription status" },
|
|
4116
|
-
{ command: "tasks", description: "Current turn progress" },
|
|
4117
|
-
{ command: "progress", description: "Current turn progress" },
|
|
4118
|
-
{ command: "activity", description: "Thread activity timeline" },
|
|
4119
|
-
{ command: "audit", description: "Admin: recent audit events" },
|
|
4120
|
-
{ command: "status", description: "Connector runtime status" },
|
|
4121
|
-
{ command: "health", description: "Connector health report" },
|
|
4122
|
-
{ command: "version", description: "Connector version" },
|
|
4123
|
-
{ command: "logs", description: "Admin: show connector logs" },
|
|
4124
|
-
{ command: "diagnostics", description: "Admin: connector diagnostics" },
|
|
4125
|
-
{ command: "lock", description: "Lock session writes to you" },
|
|
4126
|
-
{ command: "unlock", description: "Release session write lock" },
|
|
4127
|
-
{ command: "locks", description: "List session write locks" },
|
|
4128
|
-
{ command: "restart", description: "Admin: restart connector" },
|
|
4129
|
-
{ command: "update", description: "Admin: update connector or agents" },
|
|
4130
|
-
{ command: "handback", description: "Hand session back to CLI" },
|
|
4131
|
-
{ command: "attach", description: "Bind a session to this topic" },
|
|
4132
|
-
{ command: "switch", description: "Switch to a thread by ID" },
|
|
4133
|
-
]);
|
|
4134
|
-
}
|
|
4135
|
-
function renderVersionCheckPlain(check) {
|
|
4136
|
-
const icon = versionStatusIcon(check);
|
|
4137
|
-
const label = check.label === "NordRelay" ? "NordRelay" : `${check.label} version`;
|
|
4138
|
-
return `${label}: ${icon} ${formatVersionCheckDetailPlain(check)}`;
|
|
4139
|
-
}
|
|
4140
|
-
function renderVersionCheckHTML(check) {
|
|
4141
|
-
const icon = versionStatusIcon(check);
|
|
4142
|
-
const label = check.label === "NordRelay" ? "NordRelay" : `${check.label} version`;
|
|
4143
|
-
return `<b>${escapeHTML(label)}:</b> ${icon} ${formatVersionCheckDetailHTML(check)}`;
|
|
4144
|
-
}
|
|
4145
|
-
function formatCliPathPlain(label, cliPath, fallback) {
|
|
4146
|
-
return cliPath ? `${label} path: ${cliPath}` : `${label}: ${fallback}`;
|
|
4147
|
-
}
|
|
4148
|
-
function formatCliPathHTML(label, cliPath, fallback) {
|
|
4149
|
-
return cliPath
|
|
4150
|
-
? `<b>${escapeHTML(label)} path:</b> <code>${escapeHTML(cliPath)}</code>`
|
|
4151
|
-
: `<b>${escapeHTML(label)}:</b> <code>${escapeHTML(fallback)}</code>`;
|
|
4152
|
-
}
|
|
4153
|
-
function formatVersionCheckDetailPlain(check) {
|
|
4154
|
-
if (check.status === "not-installed") {
|
|
4155
|
-
return "not installed";
|
|
4156
|
-
}
|
|
4157
|
-
if (check.status === "outdated") {
|
|
4158
|
-
return `${check.installedLabel} (latest ${check.latestVersion ?? "unknown"})`;
|
|
4159
|
-
}
|
|
4160
|
-
if (check.status === "current") {
|
|
4161
|
-
return `${check.installedLabel} (latest)`;
|
|
4162
|
-
}
|
|
4163
|
-
return `${check.installedLabel} (latest unknown${check.detail ? `: ${check.detail}` : ""})`;
|
|
4164
|
-
}
|
|
4165
|
-
function formatVersionCheckDetailHTML(check) {
|
|
4166
|
-
if (check.status === "not-installed") {
|
|
4167
|
-
return "<code>not installed</code>";
|
|
4168
|
-
}
|
|
4169
|
-
if (check.status === "outdated") {
|
|
4170
|
-
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest ${escapeHTML(check.latestVersion ?? "unknown")})</i>`;
|
|
4171
|
-
}
|
|
4172
|
-
if (check.status === "current") {
|
|
4173
|
-
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest)</i>`;
|
|
4174
|
-
}
|
|
4175
|
-
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest unknown${check.detail ? `: ${escapeHTML(check.detail)}` : ""})</i>`;
|
|
4176
|
-
}
|
|
4177
|
-
function versionStatusIcon(check) {
|
|
4178
|
-
return check.status === "current" ? "✅" : "⚠️";
|
|
4179
|
-
}
|
|
4180
|
-
function renderAuditEvents(events) {
|
|
4181
|
-
if (events.length === 0) {
|
|
4182
|
-
return {
|
|
4183
|
-
plain: "Audit log is empty.",
|
|
4184
|
-
html: escapeHTML("Audit log is empty."),
|
|
4185
|
-
};
|
|
4186
|
-
}
|
|
4187
|
-
const lines = events.map((event) => {
|
|
4188
|
-
const time = formatLocalDateTime(new Date(event.timestamp));
|
|
4189
|
-
const actor = event.actorId ? `user ${event.actorId}` : "system";
|
|
4190
|
-
const prompt = event.promptId ? ` · ${event.promptId}` : "";
|
|
4191
|
-
const detail = event.detail ? ` · ${trimLine(event.detail, 90)}` : "";
|
|
4192
|
-
const description = event.description ? ` · ${trimLine(event.description, 90)}` : "";
|
|
4193
|
-
return `${time} · ${event.status.toUpperCase()} · ${event.action} · ${actor}${prompt}${description}${detail}`;
|
|
4194
|
-
});
|
|
4195
|
-
return {
|
|
4196
|
-
plain: ["Audit:", ...lines].join("\n"),
|
|
4197
|
-
html: [
|
|
4198
|
-
"<b>Audit:</b>",
|
|
4199
|
-
...lines.map((line) => escapeHTML(line)),
|
|
4200
|
-
].join("\n"),
|
|
4201
|
-
};
|
|
4202
|
-
}
|
|
4203
|
-
function renderSessionLocks(locks) {
|
|
4204
|
-
if (locks.length === 0) {
|
|
4205
|
-
return {
|
|
4206
|
-
plain: "No active session locks.",
|
|
4207
|
-
html: escapeHTML("No active session locks."),
|
|
4208
|
-
};
|
|
4209
|
-
}
|
|
4210
|
-
const lines = locks.map((lock) => {
|
|
4211
|
-
const expires = lock.expiresAt ? ` · expires ${formatLocalDateTime(new Date(lock.expiresAt))}` : "";
|
|
4212
|
-
return `${lock.contextKey} · ${formatLockOwner(lock)}${expires}`;
|
|
4213
|
-
});
|
|
4214
|
-
return {
|
|
4215
|
-
plain: ["Session locks:", ...lines].join("\n"),
|
|
4216
|
-
html: ["<b>Session locks:</b>", ...lines.map((line) => escapeHTML(line))].join("\n"),
|
|
4217
|
-
};
|
|
4218
|
-
}
|
|
4219
|
-
function formatLockOwner(lock) {
|
|
4220
|
-
if (!lock) {
|
|
4221
|
-
return "nobody";
|
|
4222
|
-
}
|
|
4223
|
-
return lock.ownerName ? `${lock.ownerName} (${lock.ownerId})` : `user ${lock.ownerId}`;
|
|
4224
|
-
}
|
|
4225
|
-
function formatTelegramName(ctx) {
|
|
4226
|
-
const firstName = ctx.from?.first_name?.trim();
|
|
4227
|
-
const lastName = ctx.from?.last_name?.trim();
|
|
4228
|
-
const username = ctx.from?.username?.trim();
|
|
4229
|
-
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
|
4230
|
-
return fullName || (username ? `@${username}` : undefined);
|
|
4231
|
-
}
|
|
4232
|
-
function formatLocalDateTime(date) {
|
|
4233
|
-
if (Number.isNaN(date.getTime())) {
|
|
4234
|
-
return "-";
|
|
4235
|
-
}
|
|
4236
|
-
return [
|
|
4237
|
-
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
|
|
4238
|
-
`${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`,
|
|
4239
|
-
].join(" ");
|
|
4240
|
-
}
|
|
4241
|
-
function pad2(value) {
|
|
4242
|
-
return String(value).padStart(2, "0");
|
|
4243
|
-
}
|
|
4244
|
-
function buildArtifactActionsKeyboard(reports) {
|
|
4245
|
-
const keyboard = new InlineKeyboard();
|
|
4246
|
-
for (const [index, report] of reports.slice(0, 5).entries()) {
|
|
4247
|
-
const label = `${index + 1}`;
|
|
4248
|
-
keyboard
|
|
4249
|
-
.text(`${label} Send`, `artifact_send:${report.turnId}`)
|
|
4250
|
-
.text(`${label} ZIP`, `artifact_zip:${report.turnId}`)
|
|
4251
|
-
.text(`${label} Delete`, `artifact_delete:${report.turnId}`)
|
|
4252
|
-
.row();
|
|
4253
|
-
}
|
|
4254
|
-
return keyboard;
|
|
4255
|
-
}
|
|
4256
|
-
function filterArtifactReports(reports, argument) {
|
|
4257
|
-
const normalized = argument.trim().toLowerCase();
|
|
4258
|
-
if (!normalized) {
|
|
4259
|
-
return null;
|
|
4260
|
-
}
|
|
4261
|
-
let predicate = null;
|
|
4262
|
-
if (normalized === "images" || normalized === "image" || normalized === "photos") {
|
|
4263
|
-
predicate = (artifact) => isTelegramImagePreview(artifact);
|
|
4264
|
-
}
|
|
4265
|
-
else if (normalized === "docs" || normalized === "documents" || normalized === "files") {
|
|
4266
|
-
predicate = (artifact) => !isTelegramImagePreview(artifact);
|
|
4267
|
-
}
|
|
4268
|
-
else if (normalized.startsWith("search ")) {
|
|
4269
|
-
const query = normalized.slice("search ".length).trim();
|
|
4270
|
-
if (!query) {
|
|
4271
|
-
return [];
|
|
4272
|
-
}
|
|
4273
|
-
predicate = (artifact) => artifact.name.toLowerCase().includes(query);
|
|
4274
|
-
}
|
|
4275
|
-
if (!predicate) {
|
|
4276
|
-
return null;
|
|
4277
|
-
}
|
|
4278
|
-
return reports
|
|
4279
|
-
.map((report) => ({
|
|
4280
|
-
...report,
|
|
4281
|
-
artifacts: report.artifacts.filter(predicate),
|
|
4282
|
-
}))
|
|
4283
|
-
.filter((report) => report.artifacts.length > 0);
|
|
4284
|
-
}
|
|
4285
|
-
function renderProgressPlain(progress, queueLength, busyState, info) {
|
|
4286
|
-
const busyFlags = formatBusyFlags(busyState);
|
|
4287
|
-
if (!progress) {
|
|
4288
|
-
return [
|
|
4289
|
-
"Progress:",
|
|
4290
|
-
"Status: idle",
|
|
4291
|
-
`Thread: ${info.threadId ?? "(not started yet)"}`,
|
|
4292
|
-
`Queue: ${queueLength}`,
|
|
4293
|
-
`Busy: ${busyFlags || "no"}`,
|
|
4294
|
-
].join("\n");
|
|
4295
|
-
}
|
|
4296
|
-
const lines = [
|
|
4297
|
-
"Progress:",
|
|
4298
|
-
`Status: ${progress.status}`,
|
|
4299
|
-
`Prompt: ${progress.promptDescription}`,
|
|
4300
|
-
`Elapsed: ${formatDurationSeconds(((progress.completedAt ?? Date.now()) - progress.startedAt) / 1000)}`,
|
|
4301
|
-
`Current tool: ${progress.currentTool ?? "-"}`,
|
|
4302
|
-
`Last tool: ${progress.lastTool ?? "-"}`,
|
|
4303
|
-
`Tools: ${formatToolSummaryLine(progress.toolCounts) || "-"}`,
|
|
4304
|
-
`Output chars: ${progress.textCharacters}`,
|
|
4305
|
-
`Queue: ${queueLength}`,
|
|
4306
|
-
`Busy: ${busyFlags || "no"}`,
|
|
4307
|
-
];
|
|
4308
|
-
if (progress.error) {
|
|
4309
|
-
lines.push(`Error: ${progress.error}`);
|
|
4310
|
-
}
|
|
4311
|
-
return lines.join("\n");
|
|
4312
|
-
}
|
|
4313
|
-
function renderProgressHTML(progress, queueLength, busyState, info) {
|
|
4314
|
-
const busyFlags = formatBusyFlags(busyState);
|
|
4315
|
-
if (!progress) {
|
|
4316
|
-
return [
|
|
4317
|
-
"<b>Progress:</b>",
|
|
4318
|
-
"<b>Status:</b> <code>idle</code>",
|
|
4319
|
-
`<b>Thread:</b> <code>${escapeHTML(info.threadId ?? "(not started yet)")}</code>`,
|
|
4320
|
-
`<b>Queue:</b> <code>${queueLength}</code>`,
|
|
4321
|
-
`<b>Busy:</b> <code>${escapeHTML(busyFlags || "no")}</code>`,
|
|
4322
|
-
].join("\n");
|
|
4323
|
-
}
|
|
4324
|
-
const lines = [
|
|
4325
|
-
"<b>Progress:</b>",
|
|
4326
|
-
`<b>Status:</b> <code>${escapeHTML(progress.status)}</code>`,
|
|
4327
|
-
`<b>Prompt:</b> <code>${escapeHTML(progress.promptDescription)}</code>`,
|
|
4328
|
-
`<b>Elapsed:</b> <code>${escapeHTML(formatDurationSeconds(((progress.completedAt ?? Date.now()) - progress.startedAt) / 1000))}</code>`,
|
|
4329
|
-
`<b>Current tool:</b> <code>${escapeHTML(progress.currentTool ?? "-")}</code>`,
|
|
4330
|
-
`<b>Last tool:</b> <code>${escapeHTML(progress.lastTool ?? "-")}</code>`,
|
|
4331
|
-
`<b>Tools:</b> <code>${escapeHTML(formatToolSummaryLine(progress.toolCounts) || "-")}</code>`,
|
|
4332
|
-
`<b>Output chars:</b> <code>${progress.textCharacters}</code>`,
|
|
4333
|
-
`<b>Queue:</b> <code>${queueLength}</code>`,
|
|
4334
|
-
`<b>Busy:</b> <code>${escapeHTML(busyFlags || "no")}</code>`,
|
|
4335
|
-
];
|
|
4336
|
-
if (progress.error) {
|
|
4337
|
-
lines.push(`<b>Error:</b> <code>${escapeHTML(progress.error)}</code>`);
|
|
4338
|
-
}
|
|
4339
|
-
return lines.join("\n");
|
|
4340
|
-
}
|
|
4341
|
-
function renderExternalMirrorStatus(snapshot, queueLength) {
|
|
4342
|
-
const prompt = trimLine(snapshot.latestUserMessage ?? "-", 180);
|
|
4343
|
-
const elapsed = snapshot.activity.startedAt
|
|
4344
|
-
? formatDurationSeconds((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
4345
|
-
: "-";
|
|
4346
|
-
const lines = [
|
|
4347
|
-
`${snapshot.agentLabel} CLI task running.`,
|
|
4348
|
-
`Thread: ${snapshot.threadId}`,
|
|
4349
|
-
`Elapsed: ${elapsed}`,
|
|
4350
|
-
`Prompt: ${prompt}`,
|
|
4351
|
-
`Last tool: ${snapshot.latestToolName ?? "-"}`,
|
|
4352
|
-
`Queue: ${queueLength}`,
|
|
4353
|
-
];
|
|
4354
|
-
return {
|
|
4355
|
-
plain: lines.join("\n"),
|
|
4356
|
-
html: [
|
|
4357
|
-
`<b>${escapeHTML(snapshot.agentLabel)} CLI task running.</b>`,
|
|
4358
|
-
`<b>Thread:</b> <code>${escapeHTML(snapshot.threadId)}</code>`,
|
|
4359
|
-
`<b>Elapsed:</b> <code>${escapeHTML(elapsed)}</code>`,
|
|
4360
|
-
`<b>Prompt:</b> <code>${escapeHTML(prompt)}</code>`,
|
|
4361
|
-
`<b>Last tool:</b> <code>${escapeHTML(snapshot.latestToolName ?? "-")}</code>`,
|
|
4362
|
-
`<b>Queue:</b> <code>${queueLength}</code>`,
|
|
4363
|
-
].join("\n"),
|
|
4364
|
-
};
|
|
4365
|
-
}
|
|
4366
|
-
function renderExternalMirrorEvent(event) {
|
|
4367
|
-
if (event.kind === "task") {
|
|
4368
|
-
const status = event.status ?? event.type;
|
|
4369
|
-
const plain = `CLI task: ${status}`;
|
|
4370
|
-
return {
|
|
4371
|
-
plain,
|
|
4372
|
-
html: `<b>CLI task:</b> <code>${escapeHTML(status)}</code>`,
|
|
4373
|
-
};
|
|
4374
|
-
}
|
|
4375
|
-
if (event.kind !== "tool") {
|
|
4376
|
-
return null;
|
|
4377
|
-
}
|
|
4378
|
-
const status = event.status ?? event.type;
|
|
4379
|
-
const tool = event.toolName ?? "tool";
|
|
4380
|
-
const detail = event.text ? `\n${trimLine(event.text.replace(/\s+/g, " "), 180)}` : "";
|
|
4381
|
-
const plain = `CLI tool ${status}: ${tool}${detail}`;
|
|
4382
|
-
return {
|
|
4383
|
-
plain,
|
|
4384
|
-
html: `<b>CLI tool ${escapeHTML(status)}:</b> <code>${escapeHTML(tool)}</code>${detail ? `\n<code>${escapeHTML(detail.trim())}</code>` : ""}`,
|
|
4385
|
-
};
|
|
4386
|
-
}
|
|
4387
|
-
function renderActivityTimeline(threadId, events, options = { limit: 16, filter: "all", exportFile: false }) {
|
|
4388
|
-
if (events.length === 0) {
|
|
4389
|
-
return {
|
|
4390
|
-
plain: `Activity:\nThread: ${threadId}\nFilter: ${options.filter}\nNo activity events found.`,
|
|
4391
|
-
html: `<b>Activity:</b>\n<b>Thread:</b> <code>${escapeHTML(threadId)}</code>\n<b>Filter:</b> <code>${escapeHTML(options.filter)}</code>\n<code>No activity events found.</code>`,
|
|
4392
|
-
};
|
|
4393
|
-
}
|
|
4394
|
-
const lines = events.map((event) => {
|
|
4395
|
-
const time = event.timestamp ? event.timestamp.toISOString().slice(11, 19) : "--:--:--";
|
|
4396
|
-
const label = activityEventLabel(event);
|
|
4397
|
-
const detail = event.text ? ` · ${trimLine(event.text.replace(/\s+/g, " ").trim(), 120)}` : "";
|
|
4398
|
-
const tool = event.toolName ? ` · ${event.toolName}` : "";
|
|
4399
|
-
return `${time} · ${label}${tool}${detail}`;
|
|
4400
|
-
});
|
|
4401
|
-
return {
|
|
4402
|
-
plain: ["Activity:", `Thread: ${threadId}`, `Filter: ${options.filter}`, `Events: ${events.length}`, ...lines].join("\n"),
|
|
4403
|
-
html: [
|
|
4404
|
-
"<b>Activity:</b>",
|
|
4405
|
-
`<b>Thread:</b> <code>${escapeHTML(threadId)}</code>`,
|
|
4406
|
-
`<b>Filter:</b> <code>${escapeHTML(options.filter)}</code>`,
|
|
4407
|
-
`<b>Events:</b> <code>${events.length}</code>`,
|
|
4408
|
-
...lines.map((line) => `<code>${escapeHTML(line)}</code>`),
|
|
4409
|
-
].join("\n"),
|
|
4410
|
-
};
|
|
4411
|
-
}
|
|
4412
|
-
function parseActivityOptions(argument) {
|
|
4413
|
-
const options = {
|
|
4414
|
-
limit: 16,
|
|
4415
|
-
filter: "all",
|
|
4416
|
-
exportFile: false,
|
|
4417
|
-
};
|
|
4418
|
-
const parts = argument.split(/\s+/).filter(Boolean);
|
|
4419
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
4420
|
-
const part = parts[index].toLowerCase();
|
|
4421
|
-
if (/^\d+$/.test(part)) {
|
|
4422
|
-
options.limit = Math.min(200, Math.max(1, Number(part)));
|
|
4423
|
-
continue;
|
|
4424
|
-
}
|
|
4425
|
-
if (part === "export") {
|
|
4426
|
-
options.exportFile = true;
|
|
4427
|
-
continue;
|
|
4428
|
-
}
|
|
4429
|
-
if (isActivityFilter(part)) {
|
|
4430
|
-
options.filter = part;
|
|
4431
|
-
continue;
|
|
4432
|
-
}
|
|
4433
|
-
if (part === "since" && parts[index + 1]) {
|
|
4434
|
-
options.sinceMs = parseDurationToMs(parts[index + 1]);
|
|
4435
|
-
index += 1;
|
|
4436
|
-
}
|
|
4437
|
-
}
|
|
4438
|
-
return options;
|
|
4439
|
-
}
|
|
4440
|
-
function filterActivityEvents(events, options) {
|
|
4441
|
-
const cutoff = options.sinceMs ? Date.now() - options.sinceMs : undefined;
|
|
4442
|
-
return events
|
|
4443
|
-
.filter((event) => {
|
|
4444
|
-
if (cutoff && event.timestamp && event.timestamp.getTime() < cutoff) {
|
|
4445
|
-
return false;
|
|
4446
|
-
}
|
|
4447
|
-
switch (options.filter) {
|
|
4448
|
-
case "tools":
|
|
4449
|
-
return event.kind === "tool";
|
|
4450
|
-
case "errors":
|
|
4451
|
-
return event.status === "failed" || event.status === "error" || /error|failed/i.test(event.text ?? "");
|
|
4452
|
-
case "user":
|
|
4453
|
-
return event.kind === "user";
|
|
4454
|
-
case "agent":
|
|
4455
|
-
return event.kind === "agent";
|
|
4456
|
-
case "tasks":
|
|
4457
|
-
return event.kind === "task";
|
|
4458
|
-
default:
|
|
4459
|
-
return true;
|
|
4460
|
-
}
|
|
4461
|
-
})
|
|
4462
|
-
.slice(-options.limit);
|
|
4463
|
-
}
|
|
4464
|
-
function isActivityFilter(value) {
|
|
4465
|
-
return value === "all" || value === "tools" || value === "errors" || value === "user" || value === "agent" || value === "tasks";
|
|
4466
|
-
}
|
|
4467
|
-
function formatAgentLaunchProfileLabel(profile, selected) {
|
|
4468
|
-
const prefix = selected ? "✅" : profile.unsafe ? "⚠️" : "🚀";
|
|
4469
|
-
return `${prefix} ${profile.label} · ${trimLine(profile.behavior, 24)}`;
|
|
4470
|
-
}
|
|
4471
|
-
function formatModelButtonLabel(model, selected) {
|
|
4472
|
-
const meta = [
|
|
4473
|
-
model.contextWindow ? formatCompactNumber(model.contextWindow) : undefined,
|
|
4474
|
-
model.supportsImages === true ? "img" : model.supportsImages === false ? "text" : undefined,
|
|
4475
|
-
model.supportsThinking === true ? "think" : undefined,
|
|
4476
|
-
].filter(Boolean).join(" ");
|
|
4477
|
-
return trimLine(`${selected ? "✅ " : ""}${model.displayName}${meta ? ` · ${meta}` : ""}`, 58);
|
|
4478
|
-
}
|
|
4479
|
-
function formatCompactNumber(value) {
|
|
4480
|
-
if (value >= 1_000_000_000)
|
|
4481
|
-
return `${Math.round(value / 100_000_000) / 10}B`;
|
|
4482
|
-
if (value >= 1_000_000)
|
|
4483
|
-
return `${Math.round(value / 100_000) / 10}M`;
|
|
4484
|
-
if (value >= 1_000)
|
|
4485
|
-
return `${Math.round(value / 100) / 10}K`;
|
|
4486
|
-
return String(value);
|
|
4487
|
-
}
|
|
4488
|
-
function renderAgentDiagnostics(diagnostics) {
|
|
4489
|
-
return {
|
|
4490
|
-
plain: [
|
|
4491
|
-
`${diagnostics.agentLabel} state:`,
|
|
4492
|
-
...diagnostics.lines.map((line) => `${line.label}: ${line.value}`),
|
|
4493
|
-
].join("\n"),
|
|
4494
|
-
html: [
|
|
4495
|
-
`<b>${escapeHTML(diagnostics.agentLabel)} state:</b>`,
|
|
4496
|
-
...diagnostics.lines.map((line) => `<b>${escapeHTML(line.label)}:</b> <code>${escapeHTML(line.value)}</code>`),
|
|
4497
|
-
].join("\n"),
|
|
4498
|
-
};
|
|
4499
|
-
}
|
|
4500
|
-
function activityEventLabel(event) {
|
|
4501
|
-
if (event.kind === "task") {
|
|
4502
|
-
return `task ${event.status ?? event.type}`;
|
|
4503
|
-
}
|
|
4504
|
-
if (event.kind === "user") {
|
|
4505
|
-
return "user";
|
|
4506
|
-
}
|
|
4507
|
-
if (event.kind === "agent") {
|
|
4508
|
-
return event.phase ? `agent ${event.phase}` : "agent";
|
|
4509
|
-
}
|
|
4510
|
-
return event.status ? `tool ${event.status}` : "tool";
|
|
4511
|
-
}
|
|
4512
|
-
function isEmptyArtifactReport(report) {
|
|
4513
|
-
return report.artifacts.length === 0 && report.skippedCount === 0 && !(report.omittedCount && report.omittedCount > 0);
|
|
4514
|
-
}
|
|
4515
|
-
function formatBusyFlags(state) {
|
|
4516
|
-
return Object.entries(state)
|
|
4517
|
-
.filter(([, enabled]) => enabled)
|
|
4518
|
-
.map(([name]) => name)
|
|
4519
|
-
.join(", ");
|
|
4520
|
-
}
|
|
4521
|
-
function renderDiagnosticsPlain(config, registry, health, authenticated, role, queueLength, progress, runtime) {
|
|
4522
|
-
const contexts = registry.listContexts();
|
|
4523
|
-
return [
|
|
4524
|
-
"Diagnostics:",
|
|
4525
|
-
`Status: ${health.state.status ?? "unknown"}`,
|
|
4526
|
-
`Version: ${health.version}`,
|
|
4527
|
-
`Role: ${role}`,
|
|
4528
|
-
`Auth: ${authenticated ? "yes" : "no"} (${health.state.authMethod ?? "-"})`,
|
|
4529
|
-
`PID: ${health.state.pid ?? "-"} (${health.pidRunning ? "running" : "not running"})`,
|
|
4530
|
-
`App PID: ${health.state.appPid ?? "-"} (${health.appPidRunning ? "running" : "not running"})`,
|
|
4531
|
-
`Workspace: ${config.workspace}`,
|
|
4532
|
-
`State backend: ${config.stateBackend}`,
|
|
4533
|
-
`Telegram transport: ${config.telegramTransport}`,
|
|
4534
|
-
`Codex CLI: ${health.codexCli}`,
|
|
4535
|
-
`Pi CLI: ${health.piCli}`,
|
|
4536
|
-
`Hermes CLI: ${health.hermesCli}`,
|
|
4537
|
-
`OpenClaw CLI: ${health.openClawCli}`,
|
|
4538
|
-
`Claude Code CLI: ${health.claudeCodeCli}`,
|
|
4539
|
-
`Hermes API: ${config.hermesApiBaseUrl}`,
|
|
4540
|
-
`OpenClaw Gateway: ${config.openClawGatewayUrl}`,
|
|
4541
|
-
`Enabled agents/default: ${enabledAgents(config).join(", ")} / ${config.defaultAgent}`,
|
|
4542
|
-
`State DB: ${health.databasePath ?? "-"}`,
|
|
4543
|
-
`Log file: ${health.logFile}`,
|
|
4544
|
-
`Log format: ${config.logFormat}`,
|
|
4545
|
-
`Tool verbosity: ${config.toolVerbosity}`,
|
|
4546
|
-
`Telegram rate limit queued/running/retries/429: ${runtime.rateLimit.queued}/${runtime.rateLimit.running}/${runtime.rateLimit.retries}/${runtime.rateLimit.rateLimitHits}`,
|
|
4547
|
-
`Telegram last retry_after: ${runtime.rateLimit.lastRetryAfterSeconds ?? "-"}s`,
|
|
4548
|
-
`CLI mirror mode/update: ${runtime.mirrorMode} / ${config.telegramMirrorMinUpdateMs} ms`,
|
|
4549
|
-
`Notify/quiet: ${runtime.notifyMode} / ${runtime.quietHours}`,
|
|
4550
|
-
`Voice: ${runtime.voiceBackend} / ${runtime.voiceLanguage} / transcribe-only ${runtime.voiceTranscribeOnly ? "on" : "off"}`,
|
|
4551
|
-
`Sync interval: ${config.codexSyncIntervalMs} ms`,
|
|
4552
|
-
`External busy check/stale: ${config.codexExternalBusyCheckMs} ms / ${config.codexExternalBusyStaleMs} ms`,
|
|
4553
|
-
`External mirrors/timers/status messages: ${runtime.externalMirrors}/${runtime.externalQueueTimers}/${runtime.queueStatusMessages}`,
|
|
4554
|
-
`Auto-send artifacts: ${config.telegramAutoSendArtifacts ? "yes" : "no"}`,
|
|
4555
|
-
`Artifact ignore dirs/globs: ${config.artifactIgnoreDirs.length}/${config.artifactIgnoreGlobs.length}`,
|
|
4556
|
-
`Artifact retention: ${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs`,
|
|
4557
|
-
`Workspace allowed/warn roots: ${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}`,
|
|
4558
|
-
`Allowed users/chats/admins/readonly: ${config.telegramAllowedUserIds.length}/${config.telegramAllowedChatIds.length}/${config.telegramAdminUserIds.length}/${config.telegramReadOnlyUserIds.length}`,
|
|
4559
|
-
`Session lock TTL: ${config.sessionLockTtlMs} ms`,
|
|
4560
|
-
`Audit max events: ${config.auditMaxEvents}`,
|
|
4561
|
-
`Loaded sessions: ${contexts.length}`,
|
|
4562
|
-
`Current queue: ${queueLength}`,
|
|
4563
|
-
`Current progress: ${progress?.status ?? "idle"}`,
|
|
4564
|
-
].join("\n");
|
|
4565
|
-
}
|
|
4566
|
-
function renderDiagnosticsHTML(config, registry, health, authenticated, role, queueLength, progress, runtime) {
|
|
4567
|
-
const contexts = registry.listContexts();
|
|
4568
|
-
return [
|
|
4569
|
-
"<b>Diagnostics:</b>",
|
|
4570
|
-
`<b>Status:</b> <code>${escapeHTML(health.state.status ?? "unknown")}</code>`,
|
|
4571
|
-
`<b>Version:</b> <code>${escapeHTML(health.version)}</code>`,
|
|
4572
|
-
`<b>Role:</b> <code>${escapeHTML(role)}</code>`,
|
|
4573
|
-
`<b>Auth:</b> <code>${authenticated ? "yes" : "no"} (${escapeHTML(health.state.authMethod ?? "-")})</code>`,
|
|
4574
|
-
`<b>PID:</b> <code>${escapeHTML(String(health.state.pid ?? "-"))} (${health.pidRunning ? "running" : "not running"})</code>`,
|
|
4575
|
-
`<b>App PID:</b> <code>${escapeHTML(String(health.state.appPid ?? "-"))} (${health.appPidRunning ? "running" : "not running"})</code>`,
|
|
4576
|
-
`<b>Workspace:</b> <code>${escapeHTML(config.workspace)}</code>`,
|
|
4577
|
-
`<b>State backend:</b> <code>${escapeHTML(config.stateBackend)}</code>`,
|
|
4578
|
-
`<b>Telegram transport:</b> <code>${escapeHTML(config.telegramTransport)}</code>`,
|
|
4579
|
-
`<b>Codex CLI:</b> <code>${escapeHTML(health.codexCli)}</code>`,
|
|
4580
|
-
`<b>Pi CLI:</b> <code>${escapeHTML(health.piCli)}</code>`,
|
|
4581
|
-
`<b>Hermes CLI:</b> <code>${escapeHTML(health.hermesCli)}</code>`,
|
|
4582
|
-
`<b>OpenClaw CLI:</b> <code>${escapeHTML(health.openClawCli)}</code>`,
|
|
4583
|
-
`<b>Claude Code CLI:</b> <code>${escapeHTML(health.claudeCodeCli)}</code>`,
|
|
4584
|
-
`<b>Hermes API:</b> <code>${escapeHTML(config.hermesApiBaseUrl)}</code>`,
|
|
4585
|
-
`<b>OpenClaw Gateway:</b> <code>${escapeHTML(config.openClawGatewayUrl)}</code>`,
|
|
4586
|
-
`<b>Enabled agents/default:</b> <code>${escapeHTML(`${enabledAgents(config).join(", ")} / ${config.defaultAgent}`)}</code>`,
|
|
4587
|
-
`<b>State DB:</b> <code>${escapeHTML(health.databasePath ?? "-")}</code>`,
|
|
4588
|
-
`<b>Log file:</b> <code>${escapeHTML(health.logFile)}</code>`,
|
|
4589
|
-
`<b>Log format:</b> <code>${escapeHTML(config.logFormat)}</code>`,
|
|
4590
|
-
`<b>Tool verbosity:</b> <code>${escapeHTML(config.toolVerbosity)}</code>`,
|
|
4591
|
-
`<b>Telegram rate limit queued/running/retries/429:</b> <code>${runtime.rateLimit.queued}/${runtime.rateLimit.running}/${runtime.rateLimit.retries}/${runtime.rateLimit.rateLimitHits}</code>`,
|
|
4592
|
-
`<b>Telegram last retry_after:</b> <code>${escapeHTML(String(runtime.rateLimit.lastRetryAfterSeconds ?? "-"))}s</code>`,
|
|
4593
|
-
`<b>CLI mirror mode/update:</b> <code>${escapeHTML(runtime.mirrorMode)} / ${config.telegramMirrorMinUpdateMs} ms</code>`,
|
|
4594
|
-
`<b>Notify/quiet:</b> <code>${escapeHTML(runtime.notifyMode)} / ${escapeHTML(runtime.quietHours)}</code>`,
|
|
4595
|
-
`<b>Voice:</b> <code>${escapeHTML(runtime.voiceBackend)} / ${escapeHTML(runtime.voiceLanguage)} / transcribe-only ${runtime.voiceTranscribeOnly ? "on" : "off"}</code>`,
|
|
4596
|
-
`<b>Sync interval:</b> <code>${config.codexSyncIntervalMs} ms</code>`,
|
|
4597
|
-
`<b>External busy check/stale:</b> <code>${config.codexExternalBusyCheckMs} ms / ${config.codexExternalBusyStaleMs} ms</code>`,
|
|
4598
|
-
`<b>External mirrors/timers/status messages:</b> <code>${runtime.externalMirrors}/${runtime.externalQueueTimers}/${runtime.queueStatusMessages}</code>`,
|
|
4599
|
-
`<b>Auto-send artifacts:</b> <code>${config.telegramAutoSendArtifacts ? "yes" : "no"}</code>`,
|
|
4600
|
-
`<b>Artifact ignore dirs/globs:</b> <code>${config.artifactIgnoreDirs.length}/${config.artifactIgnoreGlobs.length}</code>`,
|
|
4601
|
-
`<b>Artifact retention:</b> <code>${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs</code>`,
|
|
4602
|
-
`<b>Workspace allowed/warn roots:</b> <code>${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}</code>`,
|
|
4603
|
-
`<b>Allowed users/chats/admins/readonly:</b> <code>${config.telegramAllowedUserIds.length}/${config.telegramAllowedChatIds.length}/${config.telegramAdminUserIds.length}/${config.telegramReadOnlyUserIds.length}</code>`,
|
|
4604
|
-
`<b>Session lock TTL:</b> <code>${config.sessionLockTtlMs} ms</code>`,
|
|
4605
|
-
`<b>Audit max events:</b> <code>${config.auditMaxEvents}</code>`,
|
|
4606
|
-
`<b>Loaded sessions:</b> <code>${contexts.length}</code>`,
|
|
4607
|
-
`<b>Current queue:</b> <code>${queueLength}</code>`,
|
|
4608
|
-
`<b>Current progress:</b> <code>${escapeHTML(progress?.status ?? "idle")}</code>`,
|
|
4609
|
-
].join("\n");
|
|
4610
|
-
}
|
|
4611
|
-
function renderHealthPlain(health, authenticated, role) {
|
|
4612
|
-
return [
|
|
4613
|
-
`Status: ${health.state.status ?? "unknown"}`,
|
|
4614
|
-
`Version: ${health.version}`,
|
|
4615
|
-
`Role: ${role}`,
|
|
4616
|
-
`Auth: ${authenticated ? "yes" : "no"}`,
|
|
4617
|
-
`PID: ${health.state.pid ?? "-"} (${health.pidRunning ? "running" : "not running"})`,
|
|
4618
|
-
`App PID: ${health.state.appPid ?? "-"} (${health.appPidRunning ? "running" : "not running"})`,
|
|
4619
|
-
`Uptime: ${formatDuration(health.uptimeSeconds)}`,
|
|
4620
|
-
`Workspace: ${health.state.workspace ?? "-"}`,
|
|
4621
|
-
`Codex CLI: ${health.codexCli}`,
|
|
4622
|
-
`Pi CLI: ${health.piCli}`,
|
|
4623
|
-
`Hermes CLI: ${health.hermesCli}`,
|
|
4624
|
-
`OpenClaw CLI: ${health.openClawCli}`,
|
|
4625
|
-
`Claude Code CLI: ${health.claudeCodeCli}`,
|
|
4626
|
-
`Codex state DB: ${health.databasePath ?? "-"}`,
|
|
4627
|
-
`Log: ${health.logFile}`,
|
|
4628
|
-
].join("\n");
|
|
4629
|
-
}
|
|
4630
|
-
function renderHealthHTML(health, authenticated, role) {
|
|
4631
|
-
return [
|
|
4632
|
-
`<b>Status:</b> <code>${escapeHTML(health.state.status ?? "unknown")}</code>`,
|
|
4633
|
-
`<b>Version:</b> <code>${escapeHTML(health.version)}</code>`,
|
|
4634
|
-
`<b>Role:</b> <code>${escapeHTML(role)}</code>`,
|
|
4635
|
-
`<b>Auth:</b> <code>${authenticated ? "yes" : "no"}</code>`,
|
|
4636
|
-
`<b>PID:</b> <code>${escapeHTML(String(health.state.pid ?? "-"))} (${health.pidRunning ? "running" : "not running"})</code>`,
|
|
4637
|
-
`<b>App PID:</b> <code>${escapeHTML(String(health.state.appPid ?? "-"))} (${health.appPidRunning ? "running" : "not running"})</code>`,
|
|
4638
|
-
`<b>Uptime:</b> <code>${escapeHTML(formatDuration(health.uptimeSeconds))}</code>`,
|
|
4639
|
-
`<b>Workspace:</b> <code>${escapeHTML(health.state.workspace ?? "-")}</code>`,
|
|
4640
|
-
`<b>Codex CLI:</b> <code>${escapeHTML(health.codexCli)}</code>`,
|
|
4641
|
-
`<b>Pi CLI:</b> <code>${escapeHTML(health.piCli)}</code>`,
|
|
4642
|
-
`<b>Hermes CLI:</b> <code>${escapeHTML(health.hermesCli)}</code>`,
|
|
4643
|
-
`<b>OpenClaw CLI:</b> <code>${escapeHTML(health.openClawCli)}</code>`,
|
|
4644
|
-
`<b>Claude Code CLI:</b> <code>${escapeHTML(health.claudeCodeCli)}</code>`,
|
|
4645
|
-
`<b>Codex state DB:</b> <code>${escapeHTML(health.databasePath ?? "-")}</code>`,
|
|
4646
|
-
`<b>Log:</b> <code>${escapeHTML(health.logFile)}</code>`,
|
|
4647
|
-
].join("\n");
|
|
4648
|
-
}
|
|
4649
|
-
function parseFastModeArgument(argument, currentValue) {
|
|
4650
|
-
if (!argument) {
|
|
4651
|
-
return !currentValue;
|
|
4652
|
-
}
|
|
4653
|
-
const normalized = argument.toLowerCase();
|
|
4654
|
-
if (["on", "enable", "enabled", "true", "1"].includes(normalized)) {
|
|
4655
|
-
return true;
|
|
4656
|
-
}
|
|
4657
|
-
if (["off", "disable", "disabled", "false", "0"].includes(normalized)) {
|
|
4658
|
-
return false;
|
|
4659
|
-
}
|
|
4660
|
-
return undefined;
|
|
4661
|
-
}
|
|
4662
|
-
function parseToggle(argument) {
|
|
4663
|
-
const normalized = argument.trim().toLowerCase();
|
|
4664
|
-
if (["on", "enable", "enabled", "true", "1", "yes"].includes(normalized)) {
|
|
4665
|
-
return true;
|
|
4666
|
-
}
|
|
4667
|
-
if (["off", "disable", "disabled", "false", "0", "no"].includes(normalized)) {
|
|
4668
|
-
return false;
|
|
4669
|
-
}
|
|
4670
|
-
return undefined;
|
|
4671
|
-
}
|
|
4672
|
-
function parseDurationToMs(value) {
|
|
4673
|
-
const match = value.trim().match(/^(\d+)(s|m|h|d)?$/i);
|
|
4674
|
-
if (!match) {
|
|
4675
|
-
return undefined;
|
|
4676
|
-
}
|
|
4677
|
-
const amount = Number(match[1]);
|
|
4678
|
-
const unit = (match[2] ?? "m").toLowerCase();
|
|
4679
|
-
const multiplier = unit === "s"
|
|
4680
|
-
? 1000
|
|
4681
|
-
: unit === "h"
|
|
4682
|
-
? 60 * 60 * 1000
|
|
4683
|
-
: unit === "d"
|
|
4684
|
-
? 24 * 60 * 60 * 1000
|
|
4685
|
-
: 60 * 1000;
|
|
4686
|
-
return amount * multiplier;
|
|
4687
|
-
}
|
|
4688
|
-
function extractCommandName(text) {
|
|
4689
|
-
const match = text.trim().match(/^\/([a-zA-Z0-9_-]+)(?:@\w+)?(?:\s|$)/);
|
|
4690
|
-
return match?.[1]?.toLowerCase();
|
|
4691
|
-
}
|
|
4692
|
-
function isPromptEnvelopeLike(value) {
|
|
4693
|
-
return typeof value === "object" && value !== null && "input" in value && "description" in value;
|
|
4694
|
-
}
|
|
4695
|
-
function isQueuedPromptLike(value) {
|
|
4696
|
-
return "id" in value &&
|
|
4697
|
-
"contextKey" in value &&
|
|
4698
|
-
"createdAt" in value &&
|
|
4699
|
-
typeof value.id === "string" &&
|
|
4700
|
-
typeof value.contextKey === "string" &&
|
|
4701
|
-
typeof value.createdAt === "number";
|
|
4702
|
-
}
|
|
4703
|
-
function capabilitiesOf(info) {
|
|
4704
|
-
return info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
4705
|
-
}
|
|
4706
|
-
function labelOf(info) {
|
|
4707
|
-
return info.agentLabel ?? agentLabel(info.agentId ?? "codex");
|
|
4708
|
-
}
|
|
4709
|
-
function idOf(info) {
|
|
4710
|
-
return info.agentId ?? "codex";
|
|
4711
|
-
}
|
|
4712
|
-
function authHelpText(info) {
|
|
4713
|
-
const agentId = idOf(info);
|
|
4714
|
-
if (agentId === "pi") {
|
|
4715
|
-
return "Configure the required Pi provider environment variable on the host.";
|
|
4716
|
-
}
|
|
4717
|
-
if (agentId === "hermes") {
|
|
4718
|
-
return "Start the Hermes API Server, configure HERMES_API_KEY when required, or use /login to start Hermes CLI auth.";
|
|
4719
|
-
}
|
|
4720
|
-
if (agentId === "openclaw") {
|
|
4721
|
-
return "Start the OpenClaw Gateway and configure OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD when the gateway requires one.";
|
|
4722
|
-
}
|
|
4723
|
-
if (agentId === "claude-code") {
|
|
4724
|
-
return "Use /login to start Claude Code CLI auth, or run 'claude auth login' on the host.";
|
|
4725
|
-
}
|
|
4726
|
-
return "Use /login to start authentication, or set CODEX_API_KEY on the host.";
|
|
4727
|
-
}
|
|
4728
|
-
function formatAgentSettingScope(info, appliedToActiveThread) {
|
|
4729
|
-
const agentId = idOf(info);
|
|
4730
|
-
if (agentId === "hermes") {
|
|
4731
|
-
return appliedToActiveThread
|
|
4732
|
-
? "applies to the next Hermes run in this session"
|
|
4733
|
-
: "applies to new Hermes sessions";
|
|
4734
|
-
}
|
|
4735
|
-
if (agentId === "pi") {
|
|
4736
|
-
return appliedToActiveThread
|
|
4737
|
-
? "applied to the current idle Pi session and future turns"
|
|
4738
|
-
: "applies to new Pi sessions";
|
|
4739
|
-
}
|
|
4740
|
-
if (agentId === "openclaw") {
|
|
4741
|
-
return appliedToActiveThread
|
|
4742
|
-
? "applies to the next OpenClaw run in this session"
|
|
4743
|
-
: "applies to new OpenClaw sessions";
|
|
4744
|
-
}
|
|
4745
|
-
if (agentId === "claude-code") {
|
|
4746
|
-
return appliedToActiveThread
|
|
4747
|
-
? "applies to the next Claude Code run in this session"
|
|
4748
|
-
: "applies to new Claude Code sessions";
|
|
4749
|
-
}
|
|
4750
|
-
return appliedToActiveThread
|
|
4751
|
-
? "applied to the current idle thread and future threads"
|
|
4752
|
-
: "applies to new threads";
|
|
4753
|
-
}
|
|
4754
|
-
function requiresTurnApproval(info) {
|
|
4755
|
-
return info.unsafeLaunch || info.approvalPolicy !== "never";
|
|
4756
|
-
}
|
|
4757
|
-
function formatDuration(totalSeconds) {
|
|
4758
|
-
const seconds = Math.max(0, Math.floor(totalSeconds));
|
|
4759
|
-
const days = Math.floor(seconds / 86400);
|
|
4760
|
-
const hours = Math.floor((seconds % 86400) / 3600);
|
|
4761
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
4762
|
-
if (days > 0) {
|
|
4763
|
-
return `${days}d ${hours}h`;
|
|
4764
|
-
}
|
|
4765
|
-
if (hours > 0) {
|
|
4766
|
-
return `${hours}h ${minutes}m`;
|
|
4767
|
-
}
|
|
4768
|
-
return `${minutes}m`;
|
|
4769
|
-
}
|
|
4770
|
-
function formatDurationSeconds(totalSeconds) {
|
|
4771
|
-
const seconds = Math.max(0, Math.floor(totalSeconds));
|
|
4772
|
-
if (seconds < 60) {
|
|
4773
|
-
return `${seconds}s`;
|
|
4774
|
-
}
|
|
4775
|
-
const minutes = Math.floor(seconds / 60);
|
|
4776
|
-
const remainingSeconds = seconds % 60;
|
|
4777
|
-
if (minutes < 60) {
|
|
4778
|
-
return `${minutes}m ${remainingSeconds}s`;
|
|
4779
|
-
}
|
|
4780
|
-
const hours = Math.floor(minutes / 60);
|
|
4781
|
-
return `${hours}h ${minutes % 60}m`;
|
|
4782
|
-
}
|
|
4783
|
-
function renderToolStartMessage(toolName) {
|
|
4784
|
-
return {
|
|
4785
|
-
text: `<b>🔧 Running:</b> <code>${escapeHTML(toolName)}</code>`,
|
|
4786
|
-
fallbackText: `🔧 Running: ${toolName}`,
|
|
4787
|
-
parseMode: "HTML",
|
|
4788
|
-
};
|
|
4789
|
-
}
|
|
4790
|
-
function renderToolEndMessage(toolName, partialResult, isError) {
|
|
4791
|
-
const preview = summarizeToolOutput(partialResult);
|
|
4792
|
-
const icon = isError ? "❌" : "✅";
|
|
4793
|
-
const htmlLines = [`<b>${icon}</b> <code>${escapeHTML(toolName)}</code>`];
|
|
4794
|
-
const plainLines = [`${icon} ${toolName}`];
|
|
4795
|
-
if (preview) {
|
|
4796
|
-
htmlLines.push(`<pre>${escapeHTML(preview)}</pre>`);
|
|
4797
|
-
plainLines.push(preview);
|
|
4798
|
-
}
|
|
4799
|
-
return {
|
|
4800
|
-
text: htmlLines.join("\n"),
|
|
4801
|
-
fallbackText: plainLines.join("\n"),
|
|
4802
|
-
parseMode: "HTML",
|
|
4803
|
-
};
|
|
4804
|
-
}
|
|
4805
|
-
export function formatToolSummaryLine(toolCounts) {
|
|
4806
|
-
if (toolCounts.size === 0) {
|
|
4807
|
-
return "";
|
|
4808
|
-
}
|
|
4809
|
-
const summarizedCounts = new Map();
|
|
4810
|
-
for (const [toolName, count] of toolCounts.entries()) {
|
|
4811
|
-
const summaryName = summarizeToolName(toolName);
|
|
4812
|
-
summarizedCounts.set(summaryName, (summarizedCounts.get(summaryName) ?? 0) + count);
|
|
4813
|
-
}
|
|
4814
|
-
const entries = [...summarizedCounts.entries()].sort((left, right) => {
|
|
4815
|
-
const countDelta = right[1] - left[1];
|
|
4816
|
-
return countDelta !== 0 ? countDelta : left[0].localeCompare(right[0]);
|
|
4817
|
-
});
|
|
4818
|
-
const tools = entries
|
|
4819
|
-
.map(([name, count]) => formatSummaryEntry(name, count))
|
|
4820
|
-
.join(", ");
|
|
4821
|
-
return `Tools used: ${tools}`;
|
|
4822
|
-
}
|
|
4823
|
-
function renderTodoList(items) {
|
|
4824
|
-
const lines = items.map((item) => {
|
|
4825
|
-
const icon = item.completed ? "✅" : "⬜";
|
|
4826
|
-
return `${icon} ${escapeHTML(item.text)}`;
|
|
4827
|
-
});
|
|
4828
|
-
return `📋 <b>Plan</b>\n${lines.join("\n")}`;
|
|
4829
|
-
}
|
|
4830
|
-
export function formatTurnUsageLine(usage) {
|
|
4831
|
-
return `🪙 in: ${usage.inputTokens} · cached: ${usage.cachedInputTokens} · out: ${usage.outputTokens}`;
|
|
4832
|
-
}
|
|
4833
|
-
export function summarizeToolName(toolName) {
|
|
4834
|
-
if (toolName.startsWith("🔍 ")) {
|
|
4835
|
-
return "web_fetch";
|
|
4836
|
-
}
|
|
4837
|
-
if (toolName === "file_change") {
|
|
4838
|
-
return "file_change";
|
|
4839
|
-
}
|
|
4840
|
-
if (toolName === "⚠️ error") {
|
|
4841
|
-
return "error";
|
|
4842
|
-
}
|
|
4843
|
-
if (toolName.startsWith("mcp:")) {
|
|
4844
|
-
const tool = toolName.split("/").at(-1) ?? toolName;
|
|
4845
|
-
if (SUBAGENT_TOOL_NAMES.has(tool)) {
|
|
4846
|
-
return "subagent";
|
|
4847
|
-
}
|
|
4848
|
-
return tool;
|
|
4849
|
-
}
|
|
4850
|
-
return "bash";
|
|
4851
|
-
}
|
|
4852
|
-
function formatSummaryEntry(name, count) {
|
|
4853
|
-
if (count <= 1) {
|
|
4854
|
-
return name;
|
|
4855
|
-
}
|
|
4856
|
-
const label = name === "subagent" ? "subagents" : name;
|
|
4857
|
-
return `${count}x ${label}`;
|
|
4858
|
-
}
|
|
4859
|
-
const SUBAGENT_TOOL_NAMES = new Set(["spawn_agent", "send_input", "wait_agent", "close_agent", "resume_agent"]);
|
|
4860
|
-
async function safeReply(ctx, text, options = {}) {
|
|
4861
|
-
const chatId = ctx.chat?.id;
|
|
4862
|
-
if (!chatId) {
|
|
4863
|
-
return;
|
|
4864
|
-
}
|
|
4865
|
-
const parseMode = options.parseMode !== undefined ? options.parseMode : "HTML";
|
|
4866
|
-
const messageThreadId = options.messageThreadId ?? ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id;
|
|
4867
|
-
const chunks = splitTelegramText(redactText(text));
|
|
4868
|
-
const fallbackChunks = options.fallbackText ? splitTelegramText(redactText(options.fallbackText)) : [];
|
|
4869
|
-
for (const [index, chunk] of chunks.entries()) {
|
|
4870
|
-
await sendTextMessage(ctx.api, chatId, chunk, {
|
|
4871
|
-
parseMode,
|
|
4872
|
-
fallbackText: fallbackChunks[index] ?? chunk,
|
|
4873
|
-
replyMarkup: index === 0 ? options.replyMarkup : undefined,
|
|
4874
|
-
messageThreadId,
|
|
4875
|
-
});
|
|
4876
|
-
}
|
|
4877
|
-
}
|
|
4878
|
-
async function sendTextMessage(api, chatId, text, options = {}) {
|
|
4879
|
-
const parseMode = Object.prototype.hasOwnProperty.call(options, "parseMode") ? options.parseMode : "HTML";
|
|
4880
|
-
const safeText = redactText(text);
|
|
4881
|
-
const safeFallbackText = options.fallbackText === undefined ? undefined : redactText(options.fallbackText);
|
|
4882
|
-
const bucket = chatBucket(chatId);
|
|
4883
|
-
try {
|
|
4884
|
-
return await telegramRateLimiter.run(bucket, "sendMessage", () => api.sendMessage(chatId, safeText, {
|
|
4885
|
-
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
4886
|
-
...(options.messageThreadId ? { message_thread_id: options.messageThreadId } : {}),
|
|
4887
|
-
reply_markup: options.replyMarkup,
|
|
4888
|
-
}));
|
|
4889
|
-
}
|
|
4890
|
-
catch (error) {
|
|
4891
|
-
if (parseMode && safeFallbackText !== undefined && isTelegramParseError(error)) {
|
|
4892
|
-
return await telegramRateLimiter.run(bucket, "sendMessage", () => api.sendMessage(chatId, safeFallbackText, {
|
|
4893
|
-
...(options.messageThreadId ? { message_thread_id: options.messageThreadId } : {}),
|
|
4894
|
-
reply_markup: options.replyMarkup,
|
|
4895
|
-
}));
|
|
4896
|
-
}
|
|
4897
|
-
throw error;
|
|
4898
|
-
}
|
|
4899
|
-
}
|
|
4900
|
-
async function safeEditMessage(bot, chatId, messageId, text, options = {}) {
|
|
4901
|
-
const parseMode = Object.prototype.hasOwnProperty.call(options, "parseMode") ? options.parseMode : "HTML";
|
|
4902
|
-
const safeText = redactText(text);
|
|
4903
|
-
const safeFallbackText = options.fallbackText === undefined ? undefined : redactText(options.fallbackText);
|
|
4904
|
-
const bucket = `${chatBucket(chatId)}:${messageId}`;
|
|
4905
|
-
try {
|
|
4906
|
-
await telegramRateLimiter.run(bucket, "editMessageText", () => bot.api.editMessageText(chatId, messageId, safeText, {
|
|
4907
|
-
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
4908
|
-
reply_markup: options.replyMarkup,
|
|
4909
|
-
}));
|
|
4910
|
-
}
|
|
4911
|
-
catch (error) {
|
|
4912
|
-
if (isMessageNotModifiedError(error)) {
|
|
4913
|
-
return;
|
|
4914
|
-
}
|
|
4915
|
-
if (parseMode && safeFallbackText !== undefined && isTelegramParseError(error)) {
|
|
4916
|
-
await telegramRateLimiter.run(bucket, "editMessageText", () => bot.api.editMessageText(chatId, messageId, safeFallbackText, {
|
|
4917
|
-
reply_markup: options.replyMarkup,
|
|
4918
|
-
}));
|
|
4919
|
-
return;
|
|
4920
|
-
}
|
|
4921
|
-
throw error;
|
|
4922
|
-
}
|
|
4923
|
-
}
|
|
4924
|
-
async function safeEditReplyMarkup(bot, chatId, messageId, replyMarkup) {
|
|
4925
|
-
try {
|
|
4926
|
-
await telegramRateLimiter.run(`${chatBucket(chatId)}:${messageId}`, "editMessageReplyMarkup", () => bot.api.editMessageReplyMarkup(chatId, messageId, {
|
|
4927
|
-
reply_markup: replyMarkup ?? new InlineKeyboard(),
|
|
4928
|
-
}));
|
|
4929
|
-
}
|
|
4930
|
-
catch (error) {
|
|
4931
|
-
if (!isMessageNotModifiedError(error)) {
|
|
4932
|
-
throw error;
|
|
4933
|
-
}
|
|
4934
|
-
}
|
|
4935
|
-
}
|
|
4936
|
-
async function sendChatActionSafe(api, chatId, action, messageThreadId) {
|
|
4937
|
-
await telegramRateLimiter.run(chatBucket(chatId), "sendChatAction", () => api.sendChatAction(chatId, action, {
|
|
4938
|
-
...(messageThreadId ? { message_thread_id: messageThreadId } : {}),
|
|
4939
|
-
}));
|
|
4940
|
-
}
|
|
4941
|
-
function chatBucket(chatId) {
|
|
4942
|
-
return `chat:${String(chatId)}`;
|
|
4943
|
-
}
|
|
4944
|
-
async function downloadTelegramFile(api, token, fileId, maxBytes = MAX_AUDIO_FILE_SIZE) {
|
|
4945
|
-
const file = await api.getFile(fileId);
|
|
4946
|
-
if (!file.file_path) {
|
|
4947
|
-
throw new Error("Telegram did not return a file path");
|
|
4948
|
-
}
|
|
4949
|
-
if (file.file_size && file.file_size > maxBytes) {
|
|
4950
|
-
throw new Error(`Telegram file too large (${Math.round(file.file_size / 1024 / 1024)} MB, max ${Math.round(maxBytes / 1024 / 1024)} MB)`);
|
|
4951
|
-
}
|
|
4952
|
-
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
4953
|
-
const response = await fetch(url);
|
|
4954
|
-
if (!response.ok) {
|
|
4955
|
-
throw new Error(`Failed to download Telegram file: ${response.status}`);
|
|
4956
|
-
}
|
|
4957
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
4958
|
-
const extension = path.extname(file.file_path) || ".bin";
|
|
4959
|
-
const tempPath = path.join(tmpdir(), `nordrelay-file-${randomUUID()}${extension}`);
|
|
4960
|
-
await writeFile(tempPath, buffer);
|
|
4961
|
-
return tempPath;
|
|
4962
|
-
}
|
|
4963
|
-
function splitTelegramText(text) {
|
|
4964
|
-
if (text.length <= TELEGRAM_MESSAGE_LIMIT) {
|
|
4965
|
-
return [text];
|
|
4966
|
-
}
|
|
4967
|
-
const chunks = [];
|
|
4968
|
-
let remaining = text;
|
|
4969
|
-
while (remaining.length > TELEGRAM_MESSAGE_LIMIT) {
|
|
4970
|
-
let cut = remaining.lastIndexOf("\n", TELEGRAM_MESSAGE_LIMIT);
|
|
4971
|
-
if (cut < TELEGRAM_MESSAGE_LIMIT * 0.5) {
|
|
4972
|
-
cut = remaining.lastIndexOf(" ", TELEGRAM_MESSAGE_LIMIT);
|
|
4973
|
-
}
|
|
4974
|
-
if (cut < TELEGRAM_MESSAGE_LIMIT * 0.5) {
|
|
4975
|
-
cut = TELEGRAM_MESSAGE_LIMIT;
|
|
4976
|
-
}
|
|
4977
|
-
chunks.push(remaining.slice(0, cut).trimEnd());
|
|
4978
|
-
remaining = remaining.slice(cut).trimStart();
|
|
4979
|
-
}
|
|
4980
|
-
if (remaining) {
|
|
4981
|
-
chunks.push(remaining);
|
|
4982
|
-
}
|
|
4983
|
-
return chunks.length > 0 ? chunks : [""];
|
|
4984
|
-
}
|
|
4985
|
-
function splitMarkdownForTelegram(markdown) {
|
|
4986
|
-
if (!markdown) {
|
|
4987
|
-
return [];
|
|
4988
|
-
}
|
|
4989
|
-
const chunks = [];
|
|
4990
|
-
let remaining = markdown;
|
|
4991
|
-
while (remaining) {
|
|
4992
|
-
const maxLength = Math.min(remaining.length, FORMATTED_CHUNK_TARGET);
|
|
4993
|
-
const initialCut = findPreferredSplitIndex(remaining, maxLength);
|
|
4994
|
-
const candidate = remaining.slice(0, initialCut) || remaining.slice(0, 1);
|
|
4995
|
-
const rendered = renderMarkdownChunkWithinLimit(candidate);
|
|
4996
|
-
chunks.push(rendered);
|
|
4997
|
-
remaining = remaining.slice(rendered.sourceText.length).trimStart();
|
|
4998
|
-
}
|
|
4999
|
-
return chunks;
|
|
5000
|
-
}
|
|
5001
|
-
function renderMarkdownChunkWithinLimit(markdown) {
|
|
5002
|
-
if (!markdown) {
|
|
5003
|
-
return {
|
|
5004
|
-
text: "",
|
|
5005
|
-
fallbackText: "",
|
|
5006
|
-
parseMode: "HTML",
|
|
5007
|
-
sourceText: "",
|
|
5008
|
-
};
|
|
5009
|
-
}
|
|
5010
|
-
let sourceText = markdown;
|
|
5011
|
-
let rendered = formatMarkdownMessage(sourceText);
|
|
5012
|
-
while (rendered.text.length > TELEGRAM_MESSAGE_LIMIT && sourceText.length > 1) {
|
|
5013
|
-
const nextLength = Math.max(1, sourceText.length - Math.max(100, Math.ceil(sourceText.length * 0.1)));
|
|
5014
|
-
sourceText = sourceText.slice(0, nextLength).trimEnd() || sourceText.slice(0, nextLength);
|
|
5015
|
-
rendered = formatMarkdownMessage(sourceText);
|
|
5016
|
-
}
|
|
5017
|
-
return {
|
|
5018
|
-
...rendered,
|
|
5019
|
-
sourceText,
|
|
5020
|
-
};
|
|
5021
|
-
}
|
|
5022
|
-
function formatMarkdownMessage(markdown) {
|
|
5023
|
-
try {
|
|
5024
|
-
return {
|
|
5025
|
-
text: formatTelegramHTML(markdown),
|
|
5026
|
-
fallbackText: markdown,
|
|
5027
|
-
parseMode: "HTML",
|
|
5028
|
-
};
|
|
5029
|
-
}
|
|
5030
|
-
catch (error) {
|
|
5031
|
-
console.error("Failed to format Telegram HTML, falling back to plain text", error);
|
|
5032
|
-
return {
|
|
5033
|
-
text: markdown,
|
|
5034
|
-
fallbackText: markdown,
|
|
5035
|
-
parseMode: undefined,
|
|
5036
|
-
};
|
|
5037
|
-
}
|
|
5038
|
-
}
|
|
5039
|
-
function findPreferredSplitIndex(text, maxLength) {
|
|
5040
|
-
if (text.length <= maxLength) {
|
|
5041
|
-
return Math.max(1, text.length);
|
|
5042
|
-
}
|
|
5043
|
-
const newlineIndex = text.lastIndexOf("\n", maxLength);
|
|
5044
|
-
if (newlineIndex >= maxLength * 0.5) {
|
|
5045
|
-
return Math.max(1, newlineIndex);
|
|
5046
|
-
}
|
|
5047
|
-
const spaceIndex = text.lastIndexOf(" ", maxLength);
|
|
5048
|
-
if (spaceIndex >= maxLength * 0.5) {
|
|
5049
|
-
return Math.max(1, spaceIndex);
|
|
5050
|
-
}
|
|
5051
|
-
return Math.max(1, maxLength);
|
|
5052
|
-
}
|
|
5053
|
-
function buildStreamingPreview(text) {
|
|
5054
|
-
if (text.length <= STREAMING_PREVIEW_LIMIT) {
|
|
5055
|
-
return text;
|
|
5056
|
-
}
|
|
5057
|
-
return `${text.slice(0, STREAMING_PREVIEW_LIMIT)}\n\n… streaming (preview truncated)`;
|
|
5058
|
-
}
|
|
5059
|
-
function appendWithCap(base, addition, cap) {
|
|
5060
|
-
const combined = `${base}${addition}`;
|
|
5061
|
-
return combined.length <= cap ? combined : combined.slice(-cap);
|
|
5062
|
-
}
|
|
5063
|
-
function summarizeToolOutput(text) {
|
|
5064
|
-
const trimmed = text.trim();
|
|
5065
|
-
if (!trimmed) {
|
|
5066
|
-
return "";
|
|
5067
|
-
}
|
|
5068
|
-
return trimmed.length <= TOOL_OUTPUT_PREVIEW_LIMIT ? trimmed : `${trimmed.slice(-TOOL_OUTPUT_PREVIEW_LIMIT)}\n…`;
|
|
5069
|
-
}
|
|
5070
|
-
function trimLine(text, maxLength) {
|
|
5071
|
-
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
5072
|
-
if (singleLine.length <= maxLength) {
|
|
5073
|
-
return singleLine;
|
|
5074
|
-
}
|
|
5075
|
-
return `${singleLine.slice(0, maxLength - 1)}…`;
|
|
5076
|
-
}
|
|
5077
|
-
function getWorkspaceShortName(workspace) {
|
|
5078
|
-
return workspace.split(/[\\/]/).filter(Boolean).pop() ?? workspace;
|
|
5079
|
-
}
|
|
5080
|
-
function formatRelativeTime(date) {
|
|
5081
|
-
const deltaMs = Date.now() - date.getTime();
|
|
5082
|
-
const deltaSeconds = Math.max(0, Math.floor(deltaMs / 1000));
|
|
5083
|
-
if (deltaSeconds < 60) {
|
|
5084
|
-
return "just now";
|
|
5085
|
-
}
|
|
5086
|
-
const deltaMinutes = Math.floor(deltaSeconds / 60);
|
|
5087
|
-
if (deltaMinutes < 60) {
|
|
5088
|
-
return `${deltaMinutes}m ago`;
|
|
5089
|
-
}
|
|
5090
|
-
const deltaHours = Math.floor(deltaMinutes / 60);
|
|
5091
|
-
if (deltaHours < 48) {
|
|
5092
|
-
return `${deltaHours}h ago`;
|
|
5093
|
-
}
|
|
5094
|
-
const deltaDays = Math.floor(deltaHours / 24);
|
|
5095
|
-
if (deltaDays < 14) {
|
|
5096
|
-
return `${deltaDays}d ago`;
|
|
5097
|
-
}
|
|
5098
|
-
const deltaWeeks = Math.floor(deltaDays / 7);
|
|
5099
|
-
return `${deltaWeeks}w ago`;
|
|
5100
|
-
}
|
|
5101
|
-
function filterSessions(sessions, query) {
|
|
5102
|
-
const normalized = query.trim().toLowerCase();
|
|
5103
|
-
if (!normalized) {
|
|
5104
|
-
return sessions;
|
|
5105
|
-
}
|
|
5106
|
-
return sessions.filter((session) => [
|
|
5107
|
-
session.id,
|
|
5108
|
-
session.title ?? "",
|
|
5109
|
-
session.cwd,
|
|
5110
|
-
session.model ?? "",
|
|
5111
|
-
session.firstUserMessage ?? "",
|
|
5112
|
-
].some((value) => value.toLowerCase().includes(normalized)));
|
|
5113
|
-
}
|
|
5114
|
-
function orderPinnedSessions(sessions, pinnedThreadIds) {
|
|
5115
|
-
const pinnedIndex = new Map(pinnedThreadIds.map((threadId, index) => [threadId, index]));
|
|
5116
|
-
return [...sessions].sort((left, right) => {
|
|
5117
|
-
const leftPinned = pinnedIndex.get(left.id);
|
|
5118
|
-
const rightPinned = pinnedIndex.get(right.id);
|
|
5119
|
-
if (leftPinned !== undefined && rightPinned !== undefined) {
|
|
5120
|
-
return leftPinned - rightPinned;
|
|
5121
|
-
}
|
|
5122
|
-
if (leftPinned !== undefined) {
|
|
5123
|
-
return -1;
|
|
5124
|
-
}
|
|
5125
|
-
if (rightPinned !== undefined) {
|
|
5126
|
-
return 1;
|
|
5127
|
-
}
|
|
5128
|
-
return 0;
|
|
5129
|
-
});
|
|
5130
|
-
}
|
|
5131
|
-
function isMessageNotModifiedError(error) {
|
|
5132
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5133
|
-
return message.includes("message is not modified");
|
|
5134
|
-
}
|
|
5135
|
-
function isTelegramParseError(error) {
|
|
5136
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
5137
|
-
return (message.includes("can't parse entities") ||
|
|
5138
|
-
message.includes("unsupported start tag") ||
|
|
5139
|
-
message.includes("unexpected end tag") ||
|
|
5140
|
-
message.includes("entity name") ||
|
|
5141
|
-
message.includes("parse entities"));
|
|
5142
|
-
}
|
|
5143
|
-
function renderPromptFailure(accumulatedText, error) {
|
|
5144
|
-
const message = friendlyErrorText(error);
|
|
5145
|
-
return accumulatedText.trim() ? `${accumulatedText.trim()}\n\n⚠️ ${message}` : `⚠️ ${message}`;
|
|
5146
|
-
}
|
|
5147
|
-
function formatError(error) {
|
|
5148
|
-
return error instanceof Error ? error.message : String(error);
|
|
5149
|
-
}
|