@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.
Files changed (99) hide show
  1. package/README.md +185 -0
  2. package/dist/cli/commands/add.js +29 -0
  3. package/dist/cli/commands/context.js +29 -0
  4. package/dist/cli/commands/doctor.js +62 -0
  5. package/dist/cli/commands/edit.js +85 -0
  6. package/dist/cli/commands/help.js +16 -0
  7. package/dist/cli/commands/list.js +25 -0
  8. package/dist/cli/commands/log.js +63 -0
  9. package/dist/cli/commands/memory.js +241 -0
  10. package/dist/cli/commands/report.js +35 -0
  11. package/dist/cli/commands/session.js +74 -0
  12. package/dist/cli/commands/setup.js +86 -0
  13. package/dist/cli/commands/status.js +50 -0
  14. package/dist/cli/commands/telegram.js +701 -0
  15. package/dist/cli/commands/use.js +108 -0
  16. package/dist/cli/commands/version.js +12 -0
  17. package/dist/cli/index.js +276 -0
  18. package/dist/cli/output/table.js +22 -0
  19. package/dist/cli/prompts/prompt.js +37 -0
  20. package/dist/cli/registry.js +16 -0
  21. package/dist/core/config/cache-store.js +193 -0
  22. package/dist/core/config/json-store.js +114 -0
  23. package/dist/core/config/paths.js +85 -0
  24. package/dist/core/config/providers-store.js +89 -0
  25. package/dist/core/config/schema.js +60 -0
  26. package/dist/core/config/session-store.js +30 -0
  27. package/dist/core/config/usage-store.js +18 -0
  28. package/dist/core/context/context-service.js +186 -0
  29. package/dist/core/integrations/integration-state.js +105 -0
  30. package/dist/core/logs/log-service.js +56 -0
  31. package/dist/core/memory/embedding-check.js +121 -0
  32. package/dist/core/memory/memory-config.js +122 -0
  33. package/dist/core/models/model-discovery.js +430 -0
  34. package/dist/core/models/model-filter.js +13 -0
  35. package/dist/core/providers/provider-service.js +212 -0
  36. package/dist/core/reports/report-service.js +166 -0
  37. package/dist/core/runner/command-resolver.js +60 -0
  38. package/dist/core/runner/engine-registry.js +93 -0
  39. package/dist/core/runner/fallback.js +114 -0
  40. package/dist/core/runner/openai-usage-http.js +82 -0
  41. package/dist/core/runner/openai-usage-proxy.js +1 -0
  42. package/dist/core/runner/openai-usage-recording.js +172 -0
  43. package/dist/core/runner/openai-usage-responses.js +469 -0
  44. package/dist/core/runner/openai-usage-server.js +319 -0
  45. package/dist/core/runner/openai-usage-types.js +1 -0
  46. package/dist/core/runner/tool-runner.js +138 -0
  47. package/dist/core/sessions/session-service.js +47 -0
  48. package/dist/core/status/doctor-service.js +391 -0
  49. package/dist/core/status/status-service.js +60 -0
  50. package/dist/core/types.js +1 -0
  51. package/dist/core/usage/pricing.js +113 -0
  52. package/dist/core/usage/usage-service.js +30 -0
  53. package/dist/core/utils/is-record.js +3 -0
  54. package/dist/server/index.js +28 -0
  55. package/dist/server/runtime-server.js +430 -0
  56. package/dist/telegram/bot-registry.js +80 -0
  57. package/dist/telegram/bot.js +128 -0
  58. package/dist/telegram/config.js +235 -0
  59. package/dist/telegram/engine/claude-engine.js +240 -0
  60. package/dist/telegram/engine/codex-engine.js +437 -0
  61. package/dist/telegram/engine/engine-utils.js +67 -0
  62. package/dist/telegram/engine/process-utils.js +132 -0
  63. package/dist/telegram/engine/registry.js +31 -0
  64. package/dist/telegram/engine/types.js +1 -0
  65. package/dist/telegram/handler-registry.js +28 -0
  66. package/dist/telegram/handlers/callback.js +311 -0
  67. package/dist/telegram/handlers/command.js +272 -0
  68. package/dist/telegram/handlers/document.js +108 -0
  69. package/dist/telegram/handlers/memory.js +305 -0
  70. package/dist/telegram/handlers/message.js +701 -0
  71. package/dist/telegram/handlers/provider.js +332 -0
  72. package/dist/telegram/handlers/setup.js +527 -0
  73. package/dist/telegram/handlers/usage.js +124 -0
  74. package/dist/telegram/index.js +93 -0
  75. package/dist/telegram/interaction/approval.js +108 -0
  76. package/dist/telegram/interaction/command-menu.js +253 -0
  77. package/dist/telegram/interaction/formatter.js +487 -0
  78. package/dist/telegram/interaction/keyboards.js +145 -0
  79. package/dist/telegram/interaction/progress-reporter.js +160 -0
  80. package/dist/telegram/interaction/prompt-middleware.js +168 -0
  81. package/dist/telegram/interaction/result-store.js +41 -0
  82. package/dist/telegram/interaction/token-budget.js +21 -0
  83. package/dist/telegram/interaction/tool-name.js +41 -0
  84. package/dist/telegram/lifecycle-registry.js +47 -0
  85. package/dist/telegram/log.js +46 -0
  86. package/dist/telegram/memory/memory-inject.js +52 -0
  87. package/dist/telegram/memory/memory-service.js +413 -0
  88. package/dist/telegram/memory/memory-store.js +216 -0
  89. package/dist/telegram/memory/types.js +1 -0
  90. package/dist/telegram/network-retry.js +22 -0
  91. package/dist/telegram/network.js +53 -0
  92. package/dist/telegram/session/manager.js +229 -0
  93. package/dist/telegram/session/store.js +363 -0
  94. package/dist/telegram/session/types.js +1 -0
  95. package/dist/telegram/supervisor.js +57 -0
  96. package/dist/telegram/templates/messages.js +1 -0
  97. package/docs/README.md +98 -0
  98. package/docs/USAGE.html +853 -0
  99. package/package.json +57 -0
@@ -0,0 +1,28 @@
1
+ export class TelegramHandlerRegistry {
2
+ handlers = new Map();
3
+ register(name, registrar) {
4
+ const normalizedName = name.trim();
5
+ if (!normalizedName) {
6
+ throw new Error("Telegram handler name cannot be empty.");
7
+ }
8
+ this.handlers.set(normalizedName, registrar);
9
+ }
10
+ install(bot, deps) {
11
+ for (const registrar of this.handlers.values()) {
12
+ registrar(bot, deps);
13
+ }
14
+ }
15
+ list() {
16
+ return [...this.handlers.keys()];
17
+ }
18
+ }
19
+ const defaultTelegramHandlerRegistry = new TelegramHandlerRegistry();
20
+ export function registerTelegramHandler(name, registrar) {
21
+ defaultTelegramHandlerRegistry.register(name, registrar);
22
+ }
23
+ export function installTelegramHandlers(bot, deps) {
24
+ defaultTelegramHandlerRegistry.install(bot, deps);
25
+ }
26
+ export function listTelegramHandlers() {
27
+ return defaultTelegramHandlerRegistry.list();
28
+ }
@@ -0,0 +1,311 @@
1
+ import { InputFile } from "grammy";
2
+ import { Formatter } from "../interaction/formatter.js";
3
+ import { askCustomWorkdir, completeAgentProfile, selectCli, selectReadyCli, selectReadyMode, selectReadyModel, selectSetupModel, showAgentProfile, setWorkdir, showReadyCard, showReadyCliPicker, showReadyModePicker, showReadyModelPicker, showReadyProviderPicker, showSetupModelPicker, showSetupProviderPicker, showWorkdirList, showWorkdirStart } from "./setup.js";
4
+ import { executePrompt, runCliInit, resultStore } from "./message.js";
5
+ import { handleMemoryManageCallback, handleRememberScopeCallback } from "./memory.js";
6
+ import { handleProviderAdminCallback } from "./provider.js";
7
+ import { askUsagePrice, showUsagePanel } from "./usage.js";
8
+ import { withTelegramNetworkRetry } from "../network-retry.js";
9
+ export function registerCallbackHandler(bot, deps) {
10
+ bot.on("callback_query:data", async (ctx) => {
11
+ const data = ctx.callbackQuery.data;
12
+ const chatId = ctx.chat?.id;
13
+ if (!chatId) {
14
+ await ctx.answerCallbackQuery();
15
+ return;
16
+ }
17
+ if (await handleProviderAdminCallback(ctx, data))
18
+ return;
19
+ if (data.startsWith("perm:")) {
20
+ const [, action, id] = data.split(":");
21
+ if (!id) {
22
+ await ctx.answerCallbackQuery({ text: "Invalid permission request." });
23
+ return;
24
+ }
25
+ if (action === "approve") {
26
+ deps.approvalManager.resolve(id, true);
27
+ await ctx.answerCallbackQuery({ text: "Allowed" });
28
+ return;
29
+ }
30
+ if (action === "deny" || action === "skip") {
31
+ deps.approvalManager.resolve(id, false);
32
+ await ctx.answerCallbackQuery({ text: action === "skip" ? "Skipped" : "Denied" });
33
+ return;
34
+ }
35
+ if (action === "approve_all") {
36
+ const session = deps.sessionManager.getOrCreate(chatId);
37
+ deps.sessionManager.update(session.id, { approveAll: true });
38
+ deps.approvalManager.resolve(id, true);
39
+ await ctx.answerCallbackQuery({ text: "Allowed for this session" });
40
+ return;
41
+ }
42
+ }
43
+ if (data.startsWith("setup:")) {
44
+ const [, category, action, first, second, third] = data.split(":");
45
+ const forcedEngine = parseTelegramEngine(category === "model" ? third : second);
46
+ if (category === "provider" && action === "show") {
47
+ await ctx.answerCallbackQuery();
48
+ await showSetupProviderPicker(ctx, deps, { edit: true, forcedEngine: parseTelegramEngine(first), reselect: Boolean(first) });
49
+ return;
50
+ }
51
+ if (category === "provider" && action === "pick" && first) {
52
+ await ctx.answerCallbackQuery();
53
+ await showSetupModelPicker(ctx, deps, Number(first), { forcedEngine });
54
+ return;
55
+ }
56
+ if (category === "model" && action === "pick" && first && second) {
57
+ await ctx.answerCallbackQuery({ text: "Provider / Model 已选择" });
58
+ await selectSetupModel(ctx, deps, Number(first), Number(second), { forcedEngine });
59
+ return;
60
+ }
61
+ if (category === "workdir" && action === "list") {
62
+ await ctx.answerCallbackQuery();
63
+ await showWorkdirList(ctx, deps, { edit: true });
64
+ return;
65
+ }
66
+ if (category === "workdir" && action === "list_empty") {
67
+ await ctx.answerCallbackQuery({ text: "还没有记录过工作路径" });
68
+ await showWorkdirList(ctx, deps, { edit: true });
69
+ return;
70
+ }
71
+ if (category === "workdir" && action === "custom") {
72
+ await ctx.answerCallbackQuery();
73
+ await askCustomWorkdir(ctx, deps);
74
+ return;
75
+ }
76
+ if (category === "workdir" && action === "back") {
77
+ const session = deps.sessionManager.getOrCreate(chatId);
78
+ deps.sessionManager.update(session.id, { setupStep: "workdir" });
79
+ await ctx.answerCallbackQuery();
80
+ await showWorkdirStart(ctx, deps, { edit: true });
81
+ return;
82
+ }
83
+ if (category === "workdir" && action === "pick" && first) {
84
+ const cwd = resolveWorkdirCallbackValue(first, deps.sessionManager.getWorkdirs(chatId));
85
+ if (!cwd) {
86
+ await ctx.answerCallbackQuery({ text: "工作路径不存在" });
87
+ await showWorkdirList(ctx, deps);
88
+ return;
89
+ }
90
+ await ctx.answerCallbackQuery({ text: "工作路径已选择" });
91
+ await setWorkdir(ctx, deps, cwd, {
92
+ edit: true,
93
+ afterSet: (next) => runCliInit(ctx, deps, next.engine, { edit: true })
94
+ });
95
+ return;
96
+ }
97
+ if (category === "cli" && (action === "claude" || action === "codex")) {
98
+ await ctx.answerCallbackQuery({ text: `CLI: ${action}` });
99
+ await selectCli(ctx, deps, action);
100
+ return;
101
+ }
102
+ if (category === "agent" && action === "skip") {
103
+ await ctx.answerCallbackQuery({ text: "已跳过画像" });
104
+ await completeAgentProfile(ctx, deps, undefined, { edit: true });
105
+ return;
106
+ }
107
+ }
108
+ if (data.startsWith("ready:")) {
109
+ const [, category, action, first, second, third] = data.split(":");
110
+ const forcedEngine = parseTelegramEngine(category === "model" ? third : second);
111
+ if (category === "back") {
112
+ await ctx.answerCallbackQuery();
113
+ await showReadyCard(ctx, deps, deps.sessionManager.getOrCreate(chatId), { edit: true });
114
+ return;
115
+ }
116
+ if (category === "cli" && !action) {
117
+ await ctx.answerCallbackQuery();
118
+ await showReadyCliPicker(ctx, deps);
119
+ return;
120
+ }
121
+ if (category === "agent" && !action) {
122
+ await ctx.answerCallbackQuery();
123
+ await showAgentProfile(ctx, deps);
124
+ return;
125
+ }
126
+ if (category === "provider" && action === "show") {
127
+ await ctx.answerCallbackQuery();
128
+ await showReadyProviderPicker(ctx, deps, { forcedEngine: parseTelegramEngine(first), reselect: Boolean(first) });
129
+ return;
130
+ }
131
+ if (category === "cli" && action === "pick" && (first === "claude" || first === "codex")) {
132
+ await ctx.answerCallbackQuery({ text: `CLI: ${first}` });
133
+ await selectReadyCli(ctx, deps, first);
134
+ return;
135
+ }
136
+ if (category === "provider" && !action) {
137
+ await ctx.answerCallbackQuery();
138
+ await showReadyProviderPicker(ctx, deps);
139
+ return;
140
+ }
141
+ if (category === "provider" && action === "pick" && first) {
142
+ await ctx.answerCallbackQuery();
143
+ await showReadyModelPicker(ctx, deps, Number(first), { forcedEngine });
144
+ return;
145
+ }
146
+ if (category === "model" && action === "pick" && first && second) {
147
+ await ctx.answerCallbackQuery({ text: "Provider / Model 已更新" });
148
+ await selectReadyModel(ctx, deps, Number(first), Number(second), { forcedEngine });
149
+ return;
150
+ }
151
+ if (category === "mode" && !action) {
152
+ await ctx.answerCallbackQuery();
153
+ await showReadyModePicker(ctx);
154
+ return;
155
+ }
156
+ if (category === "mode" && action === "pick" && isTelegramPermissionMode(first)) {
157
+ await ctx.answerCallbackQuery({ text: "权限模式已更新" });
158
+ await selectReadyMode(ctx, deps, first);
159
+ return;
160
+ }
161
+ if (category === "workdir" && !action) {
162
+ const session = deps.sessionManager.getOrCreate(chatId);
163
+ deps.sessionManager.update(session.id, { setupStep: "workdir" });
164
+ await ctx.answerCallbackQuery();
165
+ await showWorkdirStart(ctx, deps, { edit: true, scope: "ready" });
166
+ return;
167
+ }
168
+ if (category === "workdir" && action === "list") {
169
+ await ctx.answerCallbackQuery();
170
+ await showWorkdirList(ctx, deps, { edit: true, scope: "ready" });
171
+ return;
172
+ }
173
+ if (category === "workdir" && action === "list_empty") {
174
+ await ctx.answerCallbackQuery({ text: "还没有记录过工作路径" });
175
+ await showWorkdirList(ctx, deps, { edit: true, scope: "ready" });
176
+ return;
177
+ }
178
+ if (category === "workdir" && action === "custom") {
179
+ await ctx.answerCallbackQuery();
180
+ await askCustomWorkdir(ctx, deps, { edit: true, returnToReady: true });
181
+ return;
182
+ }
183
+ if (category === "workdir" && action === "back") {
184
+ const session = deps.sessionManager.getOrCreate(chatId);
185
+ deps.sessionManager.update(session.id, { setupStep: "ready" });
186
+ await ctx.answerCallbackQuery();
187
+ await showReadyCard(ctx, deps, deps.sessionManager.getOrCreate(chatId), { edit: true });
188
+ return;
189
+ }
190
+ if (category === "workdir" && action === "pick" && first) {
191
+ const cwd = resolveWorkdirCallbackValue(first, deps.sessionManager.getWorkdirs(chatId));
192
+ if (!cwd) {
193
+ await ctx.answerCallbackQuery({ text: "工作路径不存在" });
194
+ await showWorkdirList(ctx, deps, { scope: "ready" });
195
+ return;
196
+ }
197
+ await ctx.answerCallbackQuery({ text: "工作路径已更新" });
198
+ await setWorkdir(ctx, deps, cwd, {
199
+ returnToReady: true,
200
+ edit: true,
201
+ afterSet: (next) => runCliInit(ctx, deps, next.engine, { edit: true })
202
+ });
203
+ return;
204
+ }
205
+ }
206
+ if (data.startsWith("usage:")) {
207
+ const [, category, action] = data.split(":");
208
+ if (category === "price" && action === "input") {
209
+ await ctx.answerCallbackQuery();
210
+ await askUsagePrice(ctx, deps, { edit: true });
211
+ return;
212
+ }
213
+ if (category === "price" && action === "unknown") {
214
+ await ctx.answerCallbackQuery({ text: "只显示 token" });
215
+ await showUsagePanel(ctx, deps, { edit: true, tokenOnly: true });
216
+ return;
217
+ }
218
+ }
219
+ if (data.startsWith("memory:remember:scope:")) {
220
+ const mode = parseRememberScopeMode(data.split(":")[3]);
221
+ if (!mode) {
222
+ await ctx.answerCallbackQuery({ text: "Invalid memory scope." });
223
+ return;
224
+ }
225
+ await ctx.answerCallbackQuery();
226
+ await handleRememberScopeCallback(ctx, deps, mode);
227
+ return;
228
+ }
229
+ if (data.startsWith("memory:manage:")) {
230
+ const [, , action, scope] = data.split(":");
231
+ await ctx.answerCallbackQuery();
232
+ await handleMemoryManageCallback(ctx, deps, parseMemoryManageAction(action), scope);
233
+ return;
234
+ }
235
+ if (data === "command:cancel") {
236
+ const session = deps.sessionManager.getOrCreate(chatId);
237
+ deps.sessionManager.update(session.id, { setupStep: "ready" });
238
+ await ctx.answerCallbackQuery({ text: "已关闭" });
239
+ await ctx.editMessageText("已关闭命令注册。", { reply_markup: { inline_keyboard: [] } });
240
+ return;
241
+ }
242
+ if (data === "command:back") {
243
+ const session = deps.sessionManager.getOrCreate(chatId);
244
+ deps.sessionManager.update(session.id, { setupStep: "command_name" });
245
+ await ctx.answerCallbackQuery();
246
+ await ctx.editMessageText(Formatter.commandNamePrompt(), {
247
+ reply_markup: { inline_keyboard: [[{ text: "关闭", callback_data: "command:cancel" }]] }
248
+ });
249
+ return;
250
+ }
251
+ if (data === "retry:last") {
252
+ const session = deps.sessionManager.getOrCreate(chatId);
253
+ const lastTurn = session.conversation[session.conversation.length - 1];
254
+ if (!lastTurn?.user) {
255
+ await ctx.answerCallbackQuery({ text: "没有可重试的消息" });
256
+ return;
257
+ }
258
+ await ctx.answerCallbackQuery({ text: "正在重新执行…" });
259
+ await executePrompt(ctx, deps, lastTurn.user);
260
+ return;
261
+ }
262
+ if (data.startsWith("result:")) {
263
+ const action = data.slice("result:".length);
264
+ const cached = resultStore.get(chatId);
265
+ if (action === "full") {
266
+ if (!cached?.response) {
267
+ await ctx.answerCallbackQuery({ text: "输出已过期" });
268
+ return;
269
+ }
270
+ await ctx.answerCallbackQuery();
271
+ await withTelegramNetworkRetry(() => ctx.replyWithDocument(new InputFile(Buffer.from(cached.response, "utf8"), "output.md"), { caption: "完整输出" }));
272
+ return;
273
+ }
274
+ if (action === "diff") {
275
+ if (!cached?.diff) {
276
+ await ctx.answerCallbackQuery({ text: "Diff 已过期" });
277
+ return;
278
+ }
279
+ await ctx.answerCallbackQuery();
280
+ await withTelegramNetworkRetry(() => ctx.replyWithDocument(new InputFile(Buffer.from(cached.diff, "utf8"), "changes.diff"), { caption: "Diff" }));
281
+ return;
282
+ }
283
+ }
284
+ await ctx.answerCallbackQuery();
285
+ });
286
+ }
287
+ function resolveWorkdirCallbackValue(value, workdirs) {
288
+ if (/^\d+$/.test(value)) {
289
+ return workdirs[Number(value)] ?? null;
290
+ }
291
+ try {
292
+ return decodeURIComponent(value);
293
+ }
294
+ catch {
295
+ return null;
296
+ }
297
+ }
298
+ function isTelegramPermissionMode(value) {
299
+ return value === "default" || value === "acceptEdits" || value === "bypassPermissions";
300
+ }
301
+ function parseTelegramEngine(value) {
302
+ return value === "claude" || value === "codex" ? value : undefined;
303
+ }
304
+ function parseRememberScopeMode(value) {
305
+ return value === "global" || value === "bot" || value === "auto" ? value : null;
306
+ }
307
+ function parseMemoryManageAction(value) {
308
+ if (value === "promote" || value === "clear" || value === "clear_confirm")
309
+ return value;
310
+ return "panel";
311
+ }
@@ -0,0 +1,272 @@
1
+ import fs from "node:fs";
2
+ import { exec } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { InputFile } from "grammy";
5
+ import { createCommandPermissionRequest } from "../engine/engine-utils.js";
6
+ import { requiresDangerousConfirmation } from "../interaction/approval.js";
7
+ import { safeResetChatCommandMenu, safeSetChatCommandMenu } from "../interaction/command-menu.js";
8
+ import { Formatter, escapeHtml } from "../interaction/formatter.js";
9
+ import { runCliInit } from "./message.js";
10
+ import { handleForgetCommand, handleMemoriesCommand, handleMemoryCommand, handleRememberCommand } from "./memory.js";
11
+ import { showProviderAdminPanel } from "./provider.js";
12
+ import { showConfigPanel, showReadyProviderPicker, showSetupProviderPicker, showWorkdirStart, startSetup } from "./setup.js";
13
+ import { showUsagePanel } from "./usage.js";
14
+ import { writeTelegramLog } from "../log.js";
15
+ const execAsync = promisify(exec);
16
+ const FRESH_DELETE_SCAN_LIMIT = 1000;
17
+ const TELEGRAM_DELETE_BATCH_SIZE = 100;
18
+ const START_COMMAND_DEDUP_MS = 10000;
19
+ const recentStartCommands = new Map();
20
+ export function registerCommands(bot, deps) {
21
+ bot.command("start", async (ctx) => {
22
+ if (!shouldHandleStartCommand(ctx.chat.id)) {
23
+ await safeDeleteCommandMessage(ctx);
24
+ return;
25
+ }
26
+ await safeResetChatCommandMenu(bot, ctx.chat.id);
27
+ await startSetup(ctx, deps);
28
+ });
29
+ bot.command("reset", async (ctx) => {
30
+ deps.engines.claude.abort(ctx.chat.id);
31
+ deps.engines.codex.abort(ctx.chat.id);
32
+ deps.approvalManager.resolveAll(ctx.chat.id, false);
33
+ await safeResetChatCommandMenu(bot, ctx.chat.id);
34
+ await startSetup(ctx, deps);
35
+ });
36
+ bot.command("stop", async (ctx) => {
37
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
38
+ if (session.status !== "running" && session.status !== "waiting_approval") {
39
+ await ctx.reply("当前没有正在运行的任务。");
40
+ return;
41
+ }
42
+ deps.engines.claude.abort(ctx.chat.id);
43
+ deps.engines.codex.abort(ctx.chat.id);
44
+ deps.approvalManager.resolveAll(ctx.chat.id, false);
45
+ deps.sessionManager.update(session.id, { status: "idle" });
46
+ const hasLastMessage = session.conversation.length > 0;
47
+ await ctx.reply("已停止当前任务。", hasLastMessage ? {
48
+ reply_markup: { inline_keyboard: [[{ text: "🔁 重新执行上一条", callback_data: "retry:last" }]] }
49
+ } : undefined);
50
+ });
51
+ bot.command("fresh", async (ctx) => {
52
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
53
+ if (session.status === "running" || session.status === "waiting_approval") {
54
+ await ctx.reply("已有任务正在运行,请等待完成。");
55
+ return;
56
+ }
57
+ await ctx.reply(Formatter.sessionCleared(session), { parse_mode: "HTML" });
58
+ await deleteRecentChatMessages(ctx, ctx.chat.id, ctx.message?.message_id);
59
+ });
60
+ bot.command("config", async (ctx) => {
61
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
62
+ await showConfigPanel(ctx, deps, session);
63
+ });
64
+ bot.command("model", async (ctx) => {
65
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
66
+ if (session.setupStep === "ready") {
67
+ await showReadyProviderPicker(ctx, deps, { reselect: true });
68
+ return;
69
+ }
70
+ await showSetupProviderPicker(ctx, deps, { reselect: true });
71
+ });
72
+ bot.command("provider", async (ctx) => {
73
+ await showProviderAdminPanel(ctx);
74
+ });
75
+ bot.command("usage", async (ctx) => {
76
+ await showUsagePanel(ctx, deps);
77
+ });
78
+ bot.command("remember", async (ctx) => {
79
+ await handleRememberCommand(ctx, deps);
80
+ });
81
+ bot.command("forget", async (ctx) => {
82
+ await handleForgetCommand(ctx, deps);
83
+ });
84
+ bot.command("memories", async (ctx) => {
85
+ await handleMemoriesCommand(ctx, deps);
86
+ });
87
+ bot.command("memory", async (ctx) => {
88
+ await handleMemoryCommand(ctx, deps);
89
+ });
90
+ bot.command("usge", async (ctx) => {
91
+ await showUsagePanel(ctx, deps);
92
+ });
93
+ bot.command("help", async (ctx) => {
94
+ await ctx.reply(Formatter.help(), { parse_mode: "HTML" });
95
+ });
96
+ bot.command("claude", async (ctx) => {
97
+ await runEngineCommand(ctx, deps, bot, "claude");
98
+ });
99
+ bot.command("codex", async (ctx) => {
100
+ await runEngineCommand(ctx, deps, bot, "codex");
101
+ });
102
+ bot.command("cwd", async (ctx) => {
103
+ const cwd = String(ctx.match ?? "").trim();
104
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
105
+ if (!cwd) {
106
+ deps.sessionManager.update(session.id, { setupStep: "ready" });
107
+ await showWorkdirStart(ctx, deps, { scope: "ready", prompt: Formatter.workdirChangePrompt() });
108
+ return;
109
+ }
110
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
111
+ await ctx.reply(`Directory not found: <code>${escapeHtml(cwd)}</code>`, { parse_mode: "HTML" });
112
+ return;
113
+ }
114
+ const next = deps.sessionManager.update(session.id, {
115
+ cwd,
116
+ engineSessionId: null,
117
+ botContextFingerprint: null
118
+ });
119
+ await runCliInit(ctx, deps, next.engine);
120
+ const customCommands = deps.sessionManager.listCustomCommands(ctx.chat.id, cwd);
121
+ await safeSetChatCommandMenu(bot, ctx.chat.id, next.engine, customCommands);
122
+ await ctx.reply(`cwd: <code>${escapeHtml(cwd)}</code>`, { parse_mode: "HTML" });
123
+ });
124
+ bot.command("command", async (ctx) => {
125
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
126
+ const next = deps.sessionManager.update(session.id, { setupStep: "command_name" });
127
+ void writeTelegramLog({ action: "command_register_start", message: `chatId=${ctx.chat.id} sessionId=${session.id} before=${session.setupStep} after=${next.setupStep}` });
128
+ await ctx.reply(Formatter.commandNamePrompt(), {
129
+ reply_markup: { inline_keyboard: [[{ text: "关闭", callback_data: "command:cancel" }]] }
130
+ });
131
+ });
132
+ bot.command("pin", async (ctx) => {
133
+ const reply = ctx.message?.reply_to_message;
134
+ if (!reply) {
135
+ await ctx.reply("用法:回复一条 bot 消息并发送 /pin 来固定该对话。");
136
+ return;
137
+ }
138
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
139
+ const replyText = "text" in reply ? reply.text : "caption" in reply ? reply.caption : undefined;
140
+ const normalizedReply = normalizePinText(replyText ?? "");
141
+ const turn = [...session.conversation].reverse().find((item) => {
142
+ const assistant = normalizePinText(item.assistant);
143
+ const sample = assistant.slice(0, Math.min(80, assistant.length));
144
+ return Boolean(sample && (normalizedReply.includes(sample) || assistant.includes(normalizedReply)));
145
+ });
146
+ if (!turn) {
147
+ await ctx.reply("未找到对应的对话记录,只能固定 bot 回复的消息。");
148
+ return;
149
+ }
150
+ deps.sessionManager.pinTurn(ctx.chat.id, reply.message_id, { user: turn.user, assistant: turn.assistant });
151
+ await ctx.reply("📌 已固定该对话,后续请求会优先携带此上下文。");
152
+ });
153
+ bot.command("unpin", async (ctx) => {
154
+ const reply = ctx.message?.reply_to_message;
155
+ if (!reply) {
156
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
157
+ if (!session.pinnedTurns.length) {
158
+ await ctx.reply("当前没有固定的对话。");
159
+ return;
160
+ }
161
+ deps.sessionManager.unpinTurn(ctx.chat.id, session.pinnedTurns[session.pinnedTurns.length - 1].messageId);
162
+ await ctx.reply("已取消最近一条固定对话。");
163
+ return;
164
+ }
165
+ deps.sessionManager.unpinTurn(ctx.chat.id, reply.message_id);
166
+ await ctx.reply("已取消固定该对话。");
167
+ });
168
+ bot.command("terminal", async (ctx) => {
169
+ const shell = String(ctx.match ?? "").trim();
170
+ if (!shell) {
171
+ await ctx.reply("用法:<code>/terminal &lt;命令&gt;</code>\n例如:<code>/terminal ls -la</code>", { parse_mode: "HTML" });
172
+ return;
173
+ }
174
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
175
+ const cwd = session.cwd || process.cwd();
176
+ if (requiresDangerousConfirmation(shell)) {
177
+ deps.sessionManager.update(session.id, { status: "waiting_approval" });
178
+ const approved = await deps.approvalManager.request(ctx.chat.id, createCommandPermissionRequest("Telegram terminal", shell));
179
+ deps.sessionManager.update(session.id, { status: "idle" });
180
+ if (!approved) {
181
+ await ctx.reply("已取消高风险终端命令。");
182
+ return;
183
+ }
184
+ }
185
+ await ctx.reply(`⚙️ 执行中:<code>${escapeHtml(shell)}</code>`, { parse_mode: "HTML" });
186
+ try {
187
+ const { stdout, stderr } = await execAsync(shell, { cwd, timeout: 60_000, maxBuffer: 10 * 1024 * 1024 });
188
+ const output = (stdout || stderr || "(无输出)").trim();
189
+ if (output.length <= 3800) {
190
+ await ctx.reply(`<pre>${escapeHtml(output)}</pre>`, { parse_mode: "HTML" });
191
+ }
192
+ else {
193
+ await ctx.replyWithDocument(new InputFile(Buffer.from(output, "utf-8"), "output.txt"));
194
+ }
195
+ }
196
+ catch (error) {
197
+ const msg = error instanceof Error ? error.message : String(error);
198
+ if (msg.length <= 3800) {
199
+ await ctx.reply(`❌ 执行失败:\n<pre>${escapeHtml(msg)}</pre>`, { parse_mode: "HTML" });
200
+ }
201
+ else {
202
+ await ctx.replyWithDocument(new InputFile(Buffer.from(msg, "utf-8"), "error.txt"));
203
+ }
204
+ }
205
+ });
206
+ bot.command("restart", async (ctx) => {
207
+ await ctx.reply("🔄 Bot 正在重启…");
208
+ setTimeout(() => process.kill(process.pid, "SIGHUP"), 500).unref?.();
209
+ });
210
+ }
211
+ function normalizePinText(value) {
212
+ return value
213
+ .replace(/^✅\s*完成(?:\s*·[^\n]+)?/u, "")
214
+ .replace(/\s+/g, " ")
215
+ .trim();
216
+ }
217
+ export function shouldHandleStartCommand(chatId, now = Date.now(), recent = recentStartCommands) {
218
+ const previous = recent.get(chatId);
219
+ recent.set(chatId, now);
220
+ return !previous || now - previous > START_COMMAND_DEDUP_MS;
221
+ }
222
+ async function safeDeleteCommandMessage(ctx) {
223
+ try {
224
+ await ctx.deleteMessage();
225
+ }
226
+ catch {
227
+ // The duplicate /start prompt is best-effort cleanup only.
228
+ }
229
+ }
230
+ async function runEngineCommand(ctx, deps, bot, engine) {
231
+ const session = deps.sessionManager.getOrCreate(ctx.chat.id);
232
+ if (session.setupStep !== "ready") {
233
+ await showSetupProviderPicker(ctx, deps, { forcedEngine: engine, reselect: true });
234
+ return;
235
+ }
236
+ const customCommands = deps.sessionManager.listCustomCommands(ctx.chat.id, session.cwd);
237
+ await safeSetChatCommandMenu(bot, ctx.chat.id, engine, customCommands);
238
+ await showReadyProviderPicker(ctx, deps, { forcedEngine: engine, reselect: true });
239
+ }
240
+ export async function deleteRecentChatMessages(ctx, chatId, fromMessageId, scanLimit = FRESH_DELETE_SCAN_LIMIT) {
241
+ if (!fromMessageId || scanLimit <= 0)
242
+ return 0;
243
+ const messageIds = [];
244
+ for (let messageId = fromMessageId; messageId > 0 && messageIds.length < scanLimit; messageId -= 1) {
245
+ messageIds.push(messageId);
246
+ }
247
+ let deleted = 0;
248
+ for (let index = 0; index < messageIds.length; index += TELEGRAM_DELETE_BATCH_SIZE) {
249
+ const batch = messageIds.slice(index, index + TELEGRAM_DELETE_BATCH_SIZE);
250
+ try {
251
+ await ctx.api.deleteMessages(chatId, batch);
252
+ deleted += batch.length;
253
+ }
254
+ catch {
255
+ deleted += await deleteMessagesIndividually(ctx, chatId, batch);
256
+ }
257
+ }
258
+ return deleted;
259
+ }
260
+ async function deleteMessagesIndividually(ctx, chatId, messageIds) {
261
+ let deleted = 0;
262
+ for (const messageId of messageIds) {
263
+ try {
264
+ await ctx.api.deleteMessage(chatId, messageId);
265
+ deleted += 1;
266
+ }
267
+ catch {
268
+ // Old, already deleted, or unauthorized messages are skipped.
269
+ }
270
+ }
271
+ return deleted;
272
+ }