@offerpilot/axiomruntime 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -0
- package/dist/cli/commands/add.js +29 -0
- package/dist/cli/commands/context.js +29 -0
- package/dist/cli/commands/doctor.js +62 -0
- package/dist/cli/commands/edit.js +85 -0
- package/dist/cli/commands/help.js +16 -0
- package/dist/cli/commands/list.js +25 -0
- package/dist/cli/commands/log.js +63 -0
- package/dist/cli/commands/memory.js +241 -0
- package/dist/cli/commands/report.js +35 -0
- package/dist/cli/commands/session.js +74 -0
- package/dist/cli/commands/setup.js +86 -0
- package/dist/cli/commands/status.js +50 -0
- package/dist/cli/commands/telegram.js +701 -0
- package/dist/cli/commands/use.js +108 -0
- package/dist/cli/commands/version.js +12 -0
- package/dist/cli/index.js +276 -0
- package/dist/cli/output/table.js +22 -0
- package/dist/cli/prompts/prompt.js +37 -0
- package/dist/cli/registry.js +16 -0
- package/dist/core/config/cache-store.js +193 -0
- package/dist/core/config/json-store.js +114 -0
- package/dist/core/config/paths.js +85 -0
- package/dist/core/config/providers-store.js +89 -0
- package/dist/core/config/schema.js +60 -0
- package/dist/core/config/session-store.js +30 -0
- package/dist/core/config/usage-store.js +18 -0
- package/dist/core/context/context-service.js +186 -0
- package/dist/core/integrations/integration-state.js +105 -0
- package/dist/core/logs/log-service.js +56 -0
- package/dist/core/memory/embedding-check.js +121 -0
- package/dist/core/memory/memory-config.js +122 -0
- package/dist/core/models/model-discovery.js +430 -0
- package/dist/core/models/model-filter.js +13 -0
- package/dist/core/providers/provider-service.js +212 -0
- package/dist/core/reports/report-service.js +166 -0
- package/dist/core/runner/command-resolver.js +60 -0
- package/dist/core/runner/engine-registry.js +93 -0
- package/dist/core/runner/fallback.js +114 -0
- package/dist/core/runner/openai-usage-http.js +82 -0
- package/dist/core/runner/openai-usage-proxy.js +1 -0
- package/dist/core/runner/openai-usage-recording.js +172 -0
- package/dist/core/runner/openai-usage-responses.js +469 -0
- package/dist/core/runner/openai-usage-server.js +319 -0
- package/dist/core/runner/openai-usage-types.js +1 -0
- package/dist/core/runner/tool-runner.js +138 -0
- package/dist/core/sessions/session-service.js +47 -0
- package/dist/core/status/doctor-service.js +391 -0
- package/dist/core/status/status-service.js +60 -0
- package/dist/core/types.js +1 -0
- package/dist/core/usage/pricing.js +113 -0
- package/dist/core/usage/usage-service.js +30 -0
- package/dist/core/utils/is-record.js +3 -0
- package/dist/server/index.js +28 -0
- package/dist/server/runtime-server.js +430 -0
- package/dist/telegram/bot-registry.js +80 -0
- package/dist/telegram/bot.js +128 -0
- package/dist/telegram/config.js +235 -0
- package/dist/telegram/engine/claude-engine.js +240 -0
- package/dist/telegram/engine/codex-engine.js +437 -0
- package/dist/telegram/engine/engine-utils.js +67 -0
- package/dist/telegram/engine/process-utils.js +132 -0
- package/dist/telegram/engine/registry.js +31 -0
- package/dist/telegram/engine/types.js +1 -0
- package/dist/telegram/handler-registry.js +28 -0
- package/dist/telegram/handlers/callback.js +311 -0
- package/dist/telegram/handlers/command.js +272 -0
- package/dist/telegram/handlers/document.js +108 -0
- package/dist/telegram/handlers/memory.js +305 -0
- package/dist/telegram/handlers/message.js +701 -0
- package/dist/telegram/handlers/provider.js +332 -0
- package/dist/telegram/handlers/setup.js +527 -0
- package/dist/telegram/handlers/usage.js +124 -0
- package/dist/telegram/index.js +93 -0
- package/dist/telegram/interaction/approval.js +108 -0
- package/dist/telegram/interaction/command-menu.js +253 -0
- package/dist/telegram/interaction/formatter.js +487 -0
- package/dist/telegram/interaction/keyboards.js +145 -0
- package/dist/telegram/interaction/progress-reporter.js +160 -0
- package/dist/telegram/interaction/prompt-middleware.js +168 -0
- package/dist/telegram/interaction/result-store.js +41 -0
- package/dist/telegram/interaction/token-budget.js +21 -0
- package/dist/telegram/interaction/tool-name.js +41 -0
- package/dist/telegram/lifecycle-registry.js +47 -0
- package/dist/telegram/log.js +46 -0
- package/dist/telegram/memory/memory-inject.js +52 -0
- package/dist/telegram/memory/memory-service.js +413 -0
- package/dist/telegram/memory/memory-store.js +216 -0
- package/dist/telegram/memory/types.js +1 -0
- package/dist/telegram/network-retry.js +22 -0
- package/dist/telegram/network.js +53 -0
- package/dist/telegram/session/manager.js +229 -0
- package/dist/telegram/session/store.js +363 -0
- package/dist/telegram/session/types.js +1 -0
- package/dist/telegram/supervisor.js +57 -0
- package/dist/telegram/templates/messages.js +1 -0
- package/docs/README.md +98 -0
- package/docs/USAGE.html +853 -0
- package/package.json +57 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { InlineKeyboard } from "grammy";
|
|
3
|
+
import { addProvider, deleteProvider, getCachedModels, listProviders, updateProvider } from "../../core/providers/provider-service.js";
|
|
4
|
+
import { escapeHtml, truncateText } from "../interaction/formatter.js";
|
|
5
|
+
const providerAdminService = {
|
|
6
|
+
listProviders,
|
|
7
|
+
addProvider,
|
|
8
|
+
updateProvider,
|
|
9
|
+
deleteProvider,
|
|
10
|
+
getCachedModels
|
|
11
|
+
};
|
|
12
|
+
const providerDrafts = new Map();
|
|
13
|
+
export async function showProviderAdminPanel(ctx, options = {}, service = providerAdminService) {
|
|
14
|
+
const key = providerDraftKey(ctx);
|
|
15
|
+
if (key)
|
|
16
|
+
providerDrafts.delete(key);
|
|
17
|
+
const providers = await service.listProviders();
|
|
18
|
+
const text = providers.length
|
|
19
|
+
? `<b>AI Providers</b>\n\n共 ${providers.length} 个,点击 Provider 进行编辑或删除。`
|
|
20
|
+
: "<b>AI Providers</b>\n\n还没有配置 Provider。";
|
|
21
|
+
await renderPanel(ctx, text, buildProviderListKeyboard(providers), options.edit);
|
|
22
|
+
}
|
|
23
|
+
export async function handleProviderAdminCallback(ctx, data, service = providerAdminService) {
|
|
24
|
+
if (!data.startsWith("pa:"))
|
|
25
|
+
return false;
|
|
26
|
+
await safeAnswerCallback(ctx);
|
|
27
|
+
const chatId = ctx.chat?.id;
|
|
28
|
+
if (!chatId)
|
|
29
|
+
return true;
|
|
30
|
+
const draftKey = providerDraftKey(ctx);
|
|
31
|
+
const [, action, indexValue, fingerprint, value] = data.split(":");
|
|
32
|
+
try {
|
|
33
|
+
if (action === "list" || action === "cancel") {
|
|
34
|
+
await showProviderAdminPanel(ctx, { edit: true }, service);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (action === "add") {
|
|
38
|
+
providerDrafts.set(draftKey, { kind: "add", step: "name", values: {} });
|
|
39
|
+
await renderPanel(ctx, "<b>添加 Provider</b>\n\n请输入 Provider 名称。", buildCancelKeyboard(), true);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const resolved = await resolveProvider(service, indexValue, fingerprint);
|
|
43
|
+
if (!resolved) {
|
|
44
|
+
await showProviderAdminPanel(ctx, { edit: true }, service);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const { provider, providers, index } = resolved;
|
|
48
|
+
if (action === "view") {
|
|
49
|
+
await showProviderDetails(ctx, provider, providers, index, true, service);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (action === "edit" && isEditableField(value)) {
|
|
53
|
+
providerDrafts.set(draftKey, { kind: "edit", providerName: provider.name, field: value });
|
|
54
|
+
const hint = value === "model" ? "输入 - 可清空默认模型。" : "";
|
|
55
|
+
await renderPanel(ctx, `<b>编辑 ${escapeHtml(provider.name)}</b>\n\n请输入新的 ${fieldLabel(value)}。${hint ? `\n${hint}` : ""}`, buildCancelKeyboard(), true);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (action === "mode" && isProviderMode(value)) {
|
|
59
|
+
const updated = await service.updateProvider(provider.name, { mode: value });
|
|
60
|
+
await showUpdatedProvider(ctx, updated, service, true);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (action === "level") {
|
|
64
|
+
const level = Number(value);
|
|
65
|
+
if (!Number.isInteger(level) || level < 1 || level > providers.length) {
|
|
66
|
+
await showProviderDetails(ctx, provider, providers, index, true, service);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const updated = await service.updateProvider(provider.name, { level });
|
|
70
|
+
await showUpdatedProvider(ctx, updated, service, true);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (action === "delete") {
|
|
74
|
+
await renderPanel(ctx, `<b>删除 Provider</b>\n\n确认删除 <code>${escapeHtml(provider.name)}</code>?此操作无法在 Bot 中撤销。`, buildDeleteConfirmKeyboard(index, provider), true);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (action === "confirm") {
|
|
78
|
+
await service.deleteProvider(provider.name);
|
|
79
|
+
providerDrafts.delete(draftKey);
|
|
80
|
+
const remaining = await service.listProviders();
|
|
81
|
+
await renderPanel(ctx, `<b>AI Providers</b>\n\n已删除 <code>${escapeHtml(provider.name)}</code>。`, buildProviderListKeyboard(remaining), true);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (action === "mode_picker") {
|
|
85
|
+
await renderPanel(ctx, `<b>编辑 ${escapeHtml(provider.name)}</b>\n\n选择运行模式。`, buildModeKeyboard(index, provider), true);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (action === "level_picker") {
|
|
89
|
+
await renderPanel(ctx, `<b>编辑 ${escapeHtml(provider.name)}</b>\n\n选择优先级。`, buildLevelKeyboard(index, provider, providers), true);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
await showProviderDetails(ctx, provider, providers, index, true, service);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
await ctx.reply(`Provider 操作失败:${escapeHtml(safeErrorMessage(error))}`, { parse_mode: "HTML" });
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
export async function handleProviderAdminInput(ctx, text, service = providerAdminService) {
|
|
100
|
+
const chatId = ctx.chat?.id;
|
|
101
|
+
if (!chatId)
|
|
102
|
+
return false;
|
|
103
|
+
const draftKey = providerDraftKey(ctx);
|
|
104
|
+
const draft = providerDrafts.get(draftKey);
|
|
105
|
+
if (!draft || text.startsWith("/"))
|
|
106
|
+
return false;
|
|
107
|
+
const value = text.trim();
|
|
108
|
+
if (!value) {
|
|
109
|
+
await ctx.reply("输入不能为空。", { reply_markup: buildCancelKeyboard() });
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (draft.kind === "add") {
|
|
113
|
+
if (draft.step === "name") {
|
|
114
|
+
draft.values.name = value;
|
|
115
|
+
draft.step = "baseUrl";
|
|
116
|
+
await ctx.reply("请输入 Provider Base URL。", { reply_markup: buildCancelKeyboard() });
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (draft.step === "baseUrl") {
|
|
120
|
+
if (!isHttpUrl(value)) {
|
|
121
|
+
await ctx.reply("Base URL 必须是有效的 http:// 或 https:// 地址。", { reply_markup: buildCancelKeyboard() });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
draft.values.baseUrl = value;
|
|
125
|
+
draft.step = "apiKey";
|
|
126
|
+
await ctx.reply("请输入 Provider API Key。发送后该消息会立即删除。", { reply_markup: buildCancelKeyboard() });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
await deleteSecretMessage(ctx);
|
|
130
|
+
const input = {
|
|
131
|
+
name: draft.values.name,
|
|
132
|
+
baseUrl: draft.values.baseUrl,
|
|
133
|
+
apiKey: value,
|
|
134
|
+
mode: "auto"
|
|
135
|
+
};
|
|
136
|
+
await ctx.reply("正在验证 Provider 连接并获取模型列表...");
|
|
137
|
+
try {
|
|
138
|
+
const added = await service.addProvider(input);
|
|
139
|
+
providerDrafts.delete(draftKey);
|
|
140
|
+
await showUpdatedProvider(ctx, added, service, false, "Provider 已添加。");
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
draft.values.apiKey = undefined;
|
|
144
|
+
await ctx.reply(`添加失败:${escapeHtml(safeErrorMessage(error, value))}\n\n请重新输入 API Key。`, {
|
|
145
|
+
parse_mode: "HTML",
|
|
146
|
+
reply_markup: buildCancelKeyboard()
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
const patch = buildEditPatch(draft.field, value);
|
|
152
|
+
if (draft.field === "baseUrl" && !isHttpUrl(value)) {
|
|
153
|
+
await ctx.reply("Base URL 必须是有效的 http:// 或 https:// 地址。", { reply_markup: buildCancelKeyboard() });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (draft.field === "apiKey")
|
|
157
|
+
await deleteSecretMessage(ctx);
|
|
158
|
+
try {
|
|
159
|
+
const updated = await service.updateProvider(draft.providerName, patch);
|
|
160
|
+
providerDrafts.delete(draftKey);
|
|
161
|
+
await showUpdatedProvider(ctx, updated, service, false);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const secret = draft.field === "apiKey" ? value : undefined;
|
|
165
|
+
await ctx.reply(`更新失败:${escapeHtml(safeErrorMessage(error, secret))}`, {
|
|
166
|
+
parse_mode: "HTML",
|
|
167
|
+
reply_markup: buildCancelKeyboard()
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
export function isProviderSecretInput(chatId, botId) {
|
|
173
|
+
if (!chatId)
|
|
174
|
+
return false;
|
|
175
|
+
const draft = providerDrafts.get(`${botId ?? "default"}:${chatId}`);
|
|
176
|
+
return Boolean(draft?.kind === "add" && draft.step === "apiKey"
|
|
177
|
+
|| draft?.kind === "edit" && draft.field === "apiKey");
|
|
178
|
+
}
|
|
179
|
+
function providerDraftKey(ctx) {
|
|
180
|
+
const chatId = ctx.chat?.id;
|
|
181
|
+
return chatId ? `${ctx.me?.id ?? "default"}:${chatId}` : null;
|
|
182
|
+
}
|
|
183
|
+
async function showUpdatedProvider(ctx, updated, service, edit, successMessage = "Provider 已更新。") {
|
|
184
|
+
const providers = await service.listProviders();
|
|
185
|
+
const index = providers.findIndex((provider) => provider.name === updated.name);
|
|
186
|
+
if (index < 0) {
|
|
187
|
+
await showProviderAdminPanel(ctx, { edit }, service);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await showProviderDetails(ctx, providers[index], providers, index, edit, service, `${successMessage}\n\n`);
|
|
191
|
+
}
|
|
192
|
+
async function showProviderDetails(ctx, provider, providers, index, edit, service, prefix = "") {
|
|
193
|
+
const models = await service.getCachedModels(provider.name).catch(() => []);
|
|
194
|
+
const modelCount = models.length ? `\n可用模型:<code>${models.length}</code>` : "";
|
|
195
|
+
const text = [
|
|
196
|
+
prefix,
|
|
197
|
+
`<b>${escapeHtml(truncateText(provider.name, 200))}</b>`,
|
|
198
|
+
"",
|
|
199
|
+
`Base URL:<code>${escapeHtml(truncateText(provider.baseUrl, 500))}</code>`,
|
|
200
|
+
"API Key:<code>已配置</code>",
|
|
201
|
+
`默认模型:<code>${escapeHtml(truncateText(provider.model || "未设置", 200))}</code>`,
|
|
202
|
+
`模式:<code>${provider.mode}</code>`,
|
|
203
|
+
`优先级:<code>${provider.level}</code>${modelCount}`
|
|
204
|
+
].join("\n");
|
|
205
|
+
await renderPanel(ctx, text, buildProviderDetailsKeyboard(index, provider), edit);
|
|
206
|
+
}
|
|
207
|
+
async function resolveProvider(service, indexValue, fingerprint) {
|
|
208
|
+
const index = Number(indexValue);
|
|
209
|
+
if (!Number.isInteger(index) || index < 0 || !fingerprint)
|
|
210
|
+
return null;
|
|
211
|
+
const providers = await service.listProviders();
|
|
212
|
+
const provider = providers[index];
|
|
213
|
+
if (!provider || providerFingerprint(provider.name) !== fingerprint)
|
|
214
|
+
return null;
|
|
215
|
+
return { provider, providers, index };
|
|
216
|
+
}
|
|
217
|
+
function buildProviderListKeyboard(providers) {
|
|
218
|
+
const keyboard = new InlineKeyboard();
|
|
219
|
+
for (const [index, provider] of providers.entries()) {
|
|
220
|
+
keyboard.text(`${provider.level}. ${truncateText(provider.name, 32)} · ${provider.mode}`, providerCallback("view", index, provider)).row();
|
|
221
|
+
}
|
|
222
|
+
return keyboard.text("添加 Provider", "pa:add");
|
|
223
|
+
}
|
|
224
|
+
function buildProviderDetailsKeyboard(index, provider) {
|
|
225
|
+
const callback = (action, value) => providerCallback(action, index, provider, value);
|
|
226
|
+
return new InlineKeyboard()
|
|
227
|
+
.text("名称", callback("edit", "name"))
|
|
228
|
+
.text("Base URL", callback("edit", "baseUrl"))
|
|
229
|
+
.row()
|
|
230
|
+
.text("API Key", callback("edit", "apiKey"))
|
|
231
|
+
.text("默认模型", callback("edit", "model"))
|
|
232
|
+
.row()
|
|
233
|
+
.text("模式", callback("mode_picker"))
|
|
234
|
+
.text("优先级", callback("level_picker"))
|
|
235
|
+
.row()
|
|
236
|
+
.text("删除", callback("delete"))
|
|
237
|
+
.row()
|
|
238
|
+
.text("返回", "pa:list");
|
|
239
|
+
}
|
|
240
|
+
function buildModeKeyboard(index, provider) {
|
|
241
|
+
return new InlineKeyboard()
|
|
242
|
+
.text(provider.mode === "auto" ? "auto(当前)" : "auto", providerCallback("mode", index, provider, "auto"))
|
|
243
|
+
.text(provider.mode === "ask" ? "ask(当前)" : "ask", providerCallback("mode", index, provider, "ask"))
|
|
244
|
+
.row()
|
|
245
|
+
.text("返回", providerCallback("view", index, provider));
|
|
246
|
+
}
|
|
247
|
+
function buildLevelKeyboard(index, provider, providers) {
|
|
248
|
+
const keyboard = new InlineKeyboard();
|
|
249
|
+
for (const item of providers) {
|
|
250
|
+
keyboard.text(item.level === provider.level ? `${item.level}(当前)` : String(item.level), providerCallback("level", index, provider, String(item.level)));
|
|
251
|
+
if (item.level % 4 === 0)
|
|
252
|
+
keyboard.row();
|
|
253
|
+
}
|
|
254
|
+
return keyboard.row().text("返回", providerCallback("view", index, provider));
|
|
255
|
+
}
|
|
256
|
+
function buildDeleteConfirmKeyboard(index, provider) {
|
|
257
|
+
return new InlineKeyboard()
|
|
258
|
+
.text("确认删除", providerCallback("confirm", index, provider))
|
|
259
|
+
.text("取消", providerCallback("view", index, provider));
|
|
260
|
+
}
|
|
261
|
+
function buildCancelKeyboard() {
|
|
262
|
+
return new InlineKeyboard().text("取消", "pa:cancel");
|
|
263
|
+
}
|
|
264
|
+
function providerCallback(action, index, provider, value) {
|
|
265
|
+
return ["pa", action, index, providerFingerprint(provider.name), value].filter((part) => part !== undefined).join(":");
|
|
266
|
+
}
|
|
267
|
+
function providerFingerprint(name) {
|
|
268
|
+
return createHash("sha256").update(name).digest("base64url").slice(0, 12);
|
|
269
|
+
}
|
|
270
|
+
function buildEditPatch(field, value) {
|
|
271
|
+
if (field === "model")
|
|
272
|
+
return { model: value === "-" ? "" : value };
|
|
273
|
+
return { [field]: value };
|
|
274
|
+
}
|
|
275
|
+
function isEditableField(value) {
|
|
276
|
+
return value === "name" || value === "baseUrl" || value === "apiKey" || value === "model";
|
|
277
|
+
}
|
|
278
|
+
function isProviderMode(value) {
|
|
279
|
+
return value === "auto" || value === "ask";
|
|
280
|
+
}
|
|
281
|
+
function fieldLabel(field) {
|
|
282
|
+
if (field === "baseUrl")
|
|
283
|
+
return "Base URL";
|
|
284
|
+
if (field === "apiKey")
|
|
285
|
+
return "API Key(发送后该消息会立即删除)";
|
|
286
|
+
if (field === "model")
|
|
287
|
+
return "默认模型";
|
|
288
|
+
return "Provider 名称";
|
|
289
|
+
}
|
|
290
|
+
function isHttpUrl(value) {
|
|
291
|
+
try {
|
|
292
|
+
const url = new URL(value);
|
|
293
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function safeErrorMessage(error, secret) {
|
|
300
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
301
|
+
const redacted = secret ? message.split(secret).join("[REDACTED]") : message;
|
|
302
|
+
return truncateText(redacted, 500);
|
|
303
|
+
}
|
|
304
|
+
async function deleteSecretMessage(ctx) {
|
|
305
|
+
try {
|
|
306
|
+
await ctx.deleteMessage();
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Deleting the secret message is best effort; it is still excluded from application logs.
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function safeAnswerCallback(ctx) {
|
|
313
|
+
try {
|
|
314
|
+
await ctx.answerCallbackQuery();
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// The callback may already have expired; the requested operation can still continue.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function renderPanel(ctx, text, keyboard, edit = false) {
|
|
321
|
+
const options = { parse_mode: "HTML", reply_markup: keyboard };
|
|
322
|
+
if (edit) {
|
|
323
|
+
try {
|
|
324
|
+
await ctx.editMessageText(text, options);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// The originating message may no longer be editable.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
await ctx.reply(text, options);
|
|
332
|
+
}
|