@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.10
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 +168 -371
- package/dist/adapter.d.ts +36 -12
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +12 -7
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +358 -135
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +100 -36
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +71 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +168 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +30 -24
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +620 -224
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +22 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +97 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +127 -72
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +3 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +16 -0
- package/dist/adapters/slack/session.js.map +1 -0
- package/dist/adapters/slack/tools/attach.d.ts +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +193 -147
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +58 -111
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +9 -13
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +601 -567
- package/dist/agent.js.map +1 -1
- package/dist/commands/auto-reply.d.ts +16 -0
- package/dist/commands/auto-reply.d.ts.map +1 -0
- package/dist/commands/auto-reply.js +69 -0
- package/dist/commands/auto-reply.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +76 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/model.d.ts +14 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +112 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +28 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/registry.d.ts +7 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +14 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/sandbox.d.ts +10 -0
- package/dist/commands/sandbox.d.ts.map +1 -0
- package/dist/commands/sandbox.js +88 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/session-view.d.ts +5 -0
- package/dist/commands/session-view.d.ts.map +1 -0
- package/dist/commands/session-view.js +62 -0
- package/dist/commands/session-view.js.map +1 -0
- package/dist/commands/types.d.ts +41 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +14 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/config.d.ts +49 -30
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +313 -75
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +10 -42
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +14 -127
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +13 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +118 -64
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +9 -5
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +82 -18
- package/dist/execution-resolver.js.map +1 -1
- package/dist/file-guards.d.ts +6 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +48 -0
- package/dist/file-guards.js.map +1 -0
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +4 -11
- package/dist/instrument.js.map +1 -1
- package/dist/log.d.ts +1 -5
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -38
- package/dist/log.js.map +1 -1
- package/dist/{login.d.ts → login/index.d.ts} +16 -4
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +55 -17
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +7 -4
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1453 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/{link-token.d.ts → login/session.d.ts} +4 -3
- package/dist/login/session.d.ts.map +1 -0
- package/dist/{link-token.js → login/session.js} +1 -1
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +151 -373
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +38 -52
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +212 -111
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-orchestrator.d.ts +42 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +150 -0
- package/dist/runtime/conversation-orchestrator.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +27 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +211 -0
- package/dist/runtime/session-runtime.js.map +1 -0
- package/dist/sandbox/cloudflare.d.ts +15 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -0
- package/dist/sandbox/cloudflare.js +137 -0
- package/dist/sandbox/cloudflare.js.map +1 -0
- package/dist/sandbox/container.d.ts +2 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +5 -1
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/firecracker.d.ts +2 -1
- package/dist/sandbox/firecracker.d.ts.map +1 -1
- package/dist/sandbox/firecracker.js +6 -0
- package/dist/sandbox/firecracker.js.map +1 -1
- package/dist/sandbox/host.d.ts +2 -3
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +5 -5
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sandbox/index.d.ts +6 -4
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +9 -6
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/path-context.d.ts +4 -0
- package/dist/sandbox/path-context.d.ts.map +1 -0
- package/dist/sandbox/path-context.js +20 -0
- package/dist/sandbox/path-context.js.map +1 -0
- package/dist/sandbox/types.d.ts +17 -1
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +4 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-policy.d.ts +13 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +23 -0
- package/dist/session-policy.js.map +1 -0
- package/dist/session-store.d.ts +34 -3
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +184 -22
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/command.d.ts +5 -0
- package/dist/session-view/command.d.ts.map +1 -0
- package/dist/session-view/command.js +11 -0
- package/dist/session-view/command.js.map +1 -0
- package/dist/session-view/portal.d.ts +16 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +1742 -0
- package/dist/session-view/portal.js.map +1 -0
- package/dist/session-view/service.d.ts +34 -0
- package/dist/session-view/service.d.ts.map +1 -0
- package/dist/session-view/service.js +427 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +18 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +39 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/store.d.ts +3 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +22 -48
- package/dist/store.js.map +1 -1
- package/dist/tool-diagnostics.d.ts +2 -0
- package/dist/tool-diagnostics.d.ts.map +1 -0
- package/dist/tool-diagnostics.js +7 -0
- package/dist/tool-diagnostics.js.map +1 -0
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts +43 -2
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +48 -13
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/trigger.d.ts +31 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +98 -0
- package/dist/trigger.js.map +1 -0
- package/dist/ui-copy.d.ts +1 -0
- package/dist/ui-copy.d.ts.map +1 -1
- package/dist/ui-copy.js +3 -0
- package/dist/ui-copy.js.map +1 -1
- package/dist/vault-routing.d.ts +1 -7
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +6 -48
- package/dist/vault-routing.js.map +1 -1
- package/dist/vault.d.ts +21 -55
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +138 -263
- package/dist/vault.js.map +1 -1
- package/package.json +12 -10
- package/dist/bindings.d.ts +0 -63
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js +0 -94
- package/dist/bindings.js.map +0 -1
- package/dist/link-server.d.ts.map +0 -1
- package/dist/link-server.js +0 -839
- package/dist/link-server.js.map +0 -1
- package/dist/link-token.d.ts.map +0 -1
- package/dist/link-token.js.map +0 -1
- package/dist/login.d.ts.map +0 -1
- package/dist/login.js.map +0 -1
- package/dist/vault.test.d.ts +0 -2
- package/dist/vault.test.d.ts.map +0 -1
- package/dist/vault.test.js +0 -67
- package/dist/vault.test.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAUhF,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA2DD,qBAAa,WAAY,YAAW,GAAG;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAQ7E;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB3B;IAEK,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGvE;IAEK,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWnF;IAED,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAmBrC;IAED,eAAe,IAAI,YAAY,CAQ9B;IAMK,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvF;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;IAEK,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9C;IAEK,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjF;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAI9C;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAS9D;IAED;;;;OAIG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,GAAG,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAyBhD;YAKa,mBAAmB;IAgDjC,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,kBAAkB;CA0I3B","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport { parseLoginCommand } from \"../../login.js\";\nimport * as log from \"../../log.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\ninterface MessageContext {\n msg: any;\n text: string;\n chatId: string;\n chatType: string;\n userId: string;\n userName: string;\n msgId: string;\n threadTs: string | undefined;\n sessionKey: string;\n}\n\nconst TELEGRAM_COMMANDS = [\n { command: \"start\", description: \"Show the welcome message\" },\n { command: \"help\", description: \"Show help\" },\n { command: \"stop\", description: \"Stop the current task\" },\n { command: \"new\", description: \"Start a new conversation\" },\n { command: \"login\", description: \"Open your secure login page\" },\n] as const;\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n await this.client.api.setMyCommands([...TELEGRAM_COMMANDS]);\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected();\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(conversationId: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(conversationId), text);\n return String(result);\n }\n\n async updateMessage(conversationId: string, ts: string, text: string): Promise<void> {\n try {\n await this.client.api.editMessageText(parseInt(conversationId), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"message is not modified\")) {\n throw err;\n }\n }\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const telegramEvent: TelegramEvent = {\n ...event,\n type: \"message\",\n conversationId: event.conversationId,\n };\n const adapters = createTelegramAdapters(telegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n }\n\n async postPlainMessage(chatId: number, text: string): Promise<void> {\n await this.client.api.sendMessage(chatId, text);\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n }\n\n logToFile(channel: string, entry: object): void {\n const channelDir = join(this.workingDir, channel);\n if (!existsSync(channelDir)) mkdirSync(channelDir, { recursive: true });\n appendFileSync(join(channelDir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n this.logToFile(channel, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const attachmentsDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(attachmentsDir)) mkdirSync(attachmentsDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private extractMessageContext(msg: any): MessageContext | null {\n if (!msg) return null;\n if (msg.date * 1000 < this.startupTime) return null;\n if (msg.from?.is_bot) return null;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return null;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n\n // Private chats: single session per chat (no per-message splitting)\n // Groups: per-thread sessions (use reply chain or unique message id)\n const sessionKey = chatType === \"private\" ? chatId : `${chatId}:${threadTs ?? msgId}`;\n\n return { msg, text, chatId, chatType, userId, userName, msgId, threadTs, sessionKey };\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.toLowerCase().includes(`@${this.botUsername.toLowerCase()}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"gi\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n // --- Slash commands (registered before catch-all so grammY intercepts them) ---\n\n this.client.command(\"start\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Welcome!</b>\",\n \"\",\n \"I'm an AI coding agent. Send me a message or use these commands:\",\n \"\",\n \"/new — Start a new conversation\",\n \"/stop — Stop the current task\",\n \"/help — Show help\",\n \"/login — Open your secure login page\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"help\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Available commands:</b>\",\n \"\",\n \"/start — Show the welcome message\",\n \"/help — Show help\",\n \"/stop — Stop the current task\",\n \"/new — Start a new conversation\",\n \"/login — Open your secure login page\",\n \"\",\n \"You can also send a regular message to chat with the agent.\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"stop\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n });\n\n this.client.command(\"new\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleNew(mc.sessionKey, mc.chatId, this);\n });\n\n this.client.command(\"login\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n mc.text,\n mc.chatType === \"private\",\n );\n });\n\n // --- Catch-all for regular (non-command) messages ---\n\n this.client.on(\"message\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n\n // In groups, only respond when addressed to bot\n if (!this.isAddressedToBot(mc.text, mc.chatType)) return;\n\n const cleanedText = this.cleanText(mc.text);\n\n if (parseLoginCommand(cleanedText)) {\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n cleanedText,\n mc.chatType === \"private\",\n );\n return;\n }\n\n // Process attachments\n const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);\n\n const event: TelegramEvent = {\n type: \"message\",\n conversationId: mc.chatId,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log the message\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle bare \"stop\" text (backward compat)\n if (cleanedText.toLowerCase() === \"stop\") {\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n return;\n }\n\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.postMessage(mc.chatId, formatAlreadyWorking(\"telegram\", \"/stop\"));\n } else {\n this.getQueue(mc.sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AA8BhF,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAuBD,qBAAa,WAAY,YAAW,GAAG;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAQ7E;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAwB3B;IAEK,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhE;IAEK,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB5E;IAED,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAerC;IAED,eAAe,IAAI,YAAY,CAQ9B;IAMK,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBlE;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIlE;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBvF;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;IAEK,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9C;IAEK,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjF;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE9D;IAED;;;;OAIG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,GAAG,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAyBhD;YAKa,mBAAmB;IAgDjC,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,qBAAqB;IAsC7B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,kBAAkB;CA+H3B","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { resolveChatSessionKey } from \"../../session-policy.js\";\nimport { evaluateAutoReplyPolicy } from \"../../trigger.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport {\n appendBotResponseLog,\n appendChannelLog,\n ChannelQueue,\n resolveOnlyScopedStopTarget,\n resolveStopTarget,\n withRetry,\n} from \"../shared.js\";\nimport { createTelegramAdapters } from \"./context.js\";\nimport { escapeTelegramHtml } from \"./html.js\";\n\n// grammY surfaces Telegram errors as `GrammyError` with `error_code` mirroring\n// the Bot API. 429 is the rate-limit status; the response also includes\n// `parameters.retry_after` but exponential backoff is good enough here.\nfunction telegramIsRateLimited(err: Error): boolean {\n return (err as { error_code?: number }).error_code === 429;\n}\n\nconst telegramRetry = <T>(fn: () => Promise<T>): Promise<T> =>\n withRetry(fn, { isRateLimited: telegramIsRateLimited });\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\ninterface MessageContext {\n msg: any;\n text: string;\n chatId: string;\n chatType: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n userName: string;\n msgId: string;\n threadTs: string | undefined;\n sessionKey: string;\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nfunction isTelegramHtmlParseError(message: string): boolean {\n return message.includes(\"can't parse entities\");\n}\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n await this.client.api.setMyCommands([\n { command: \"login\", description: \"Store credentials in your private vault\" },\n { command: \"session\", description: \"Open the current session in the web viewer\" },\n { command: \"model\", description: \"Switch this conversation's LLM model\" },\n { command: \"sandbox\", description: \"Show or boost sandbox limits\" },\n { command: \"stop\", description: \"Stop ongoing conversation\" },\n { command: \"new\", description: \"Reset conversation history and start fresh\" },\n ]);\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected(\"Telegram\");\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(channel: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(channel), text);\n return String(result);\n }\n\n async updateMessage(channel: string, ts: string, text: string): Promise<void> {\n return telegramRetry(async () => {\n try {\n await this.client.api.editMessageText(parseInt(channel), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"message is not modified\")) {\n return;\n }\n if (!isTelegramHtmlParseError(msg)) {\n throw err;\n }\n await this.client.api.editMessageText(\n parseInt(channel),\n parseInt(ts),\n escapeTelegramHtml(text),\n {\n parse_mode: \"HTML\",\n },\n );\n }\n });\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const conversationId = event.conversationId;\n const queue = this.getQueue(conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const adapters = createTelegramAdapters(event as TelegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n return telegramRetry(async () => {\n try {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!isTelegramHtmlParseError(msg)) {\n throw err;\n }\n const result = await this.client.api.sendMessage(chatId, escapeTelegramHtml(text), {\n parse_mode: \"HTML\",\n });\n return result.message_id;\n }\n });\n }\n\n async postPlainMessage(chatId: number, text: string): Promise<void> {\n return telegramRetry(async () => {\n await this.client.api.sendMessage(chatId, text);\n });\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n return telegramRetry(async () => {\n try {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!isTelegramHtmlParseError(msg)) {\n throw err;\n }\n const result = await this.client.api.sendMessage(chatId, escapeTelegramHtml(text), {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n });\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n return telegramRetry(async () => {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n });\n }\n\n logToFile(channel: string, entry: object): void {\n appendChannelLog(this.workingDir, channel, entry);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n appendBotResponseLog(this.workingDir, channel, text, ts);\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const fullDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(fullDir)) mkdirSync(fullDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(fullDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue(\"Telegram\");\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private extractMessageContext(msg: any): MessageContext | null {\n if (!msg) return null;\n if (msg.date * 1000 < this.startupTime) return null;\n if (msg.from?.is_bot) return null;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return null;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n const conversationKind = chatType === \"private\" ? \"direct\" : \"shared\";\n\n const sessionKey = resolveChatSessionKey({\n conversationId: chatId,\n conversationKind,\n messageId: msgId,\n threadTs,\n });\n\n return {\n msg,\n text,\n chatId,\n chatType,\n conversationKind,\n userId,\n userName,\n msgId,\n threadTs,\n sessionKey,\n };\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.toLowerCase().includes(`@${this.botUsername.toLowerCase()}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"gi\"), \"\").trim();\n }\n\n private isStopText(text: string): boolean {\n return /^\\/?stop(?:@\\w+)?$/i.test(text.trim());\n }\n\n private resolveStopTarget(mc: MessageContext): string | null {\n const directTarget = resolveStopTarget({\n handler: this.handler,\n conversationId: mc.chatId,\n sessionKey: mc.sessionKey,\n });\n if (directTarget) return directTarget;\n return resolveOnlyScopedStopTarget(this.handler, mc.chatId);\n }\n\n private setupEventHandlers(): void {\n // --- Slash commands (registered before catch-all so grammY intercepts them) ---\n\n this.client.command(\"stop\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n const stopTarget = this.resolveStopTarget(mc);\n if (stopTarget) {\n await this.handler.handleStop(stopTarget, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n });\n\n this.client.command(\"new\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleNewCommand(mc.sessionKey, mc.chatId, this);\n });\n\n this.client.command(\"sandbox\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n const cleanedText = this.cleanText(mc.text).replace(/^\\/sandbox(?:@\\w+)?/i, \"/pi-sandbox\");\n const event: TelegramEvent = {\n type: \"command\",\n conversationId: mc.chatId,\n conversationKind: mc.conversationKind,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: [],\n };\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n ...(mc.conversationKind === \"shared\" && mc.threadTs ? { threadTs: mc.threadTs } : {}),\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: [],\n isBot: false,\n });\n const adapters = createTelegramAdapters(event, this, false);\n await this.handler.handleEvent(event, this, adapters, false);\n });\n\n // --- Catch-all for regular (non-command) messages ---\n\n this.client.on(\"message\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n\n const cleanedText = this.cleanText(mc.text);\n const addressedToBot = this.isAddressedToBot(mc.text, mc.chatType);\n\n if (this.isStopText(cleanedText)) {\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: [],\n isBot: false,\n });\n\n const stopTarget = this.resolveStopTarget(mc);\n if (stopTarget) {\n await this.handler.handleStop(stopTarget, mc.chatId, this);\n } else if (addressedToBot || mc.chatType === \"private\") {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n return;\n }\n\n const isAutoReplyCandidate = mc.chatType !== \"private\" && !addressedToBot;\n\n const eventBase: TelegramEvent = {\n type: \"message\",\n conversationId: mc.chatId,\n conversationKind: mc.conversationKind,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n };\n\n const triggerResult = isAutoReplyCandidate\n ? await evaluateAutoReplyPolicy({ event: eventBase, workingDir: this.workingDir })\n : ({ trigger: true, reason: \"addressed\" } as const);\n\n const logEntryBase = {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n ...(mc.conversationKind === \"shared\" && mc.threadTs ? { threadTs: mc.threadTs } : {}),\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n isBot: false,\n };\n\n if (!triggerResult.trigger) {\n this.logToFile(mc.chatId, { ...logEntryBase, attachments: [] });\n return;\n }\n\n const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);\n const event: TelegramEvent = { ...eventBase, attachments: processedAttachments };\n\n this.logToFile(mc.chatId, { ...logEntryBase, attachments: processedAttachments });\n\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.postMessage(mc.chatId, formatAlreadyWorking(\"telegram\", \"/stop\"));\n } else {\n this.getQueue(mc.sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|
|
@@ -1,47 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { basename, join } from "path";
|
|
3
3
|
import { Bot as GrammyBot, InputFile } from "grammy";
|
|
4
|
-
import { parseLoginCommand } from "../../login.js";
|
|
5
4
|
import * as log from "../../log.js";
|
|
5
|
+
import { resolveChatSessionKey } from "../../session-policy.js";
|
|
6
|
+
import { evaluateAutoReplyPolicy } from "../../trigger.js";
|
|
6
7
|
import { formatAlreadyWorking, formatNothingRunning } from "../../ui-copy.js";
|
|
8
|
+
import { appendBotResponseLog, appendChannelLog, ChannelQueue, resolveOnlyScopedStopTarget, resolveStopTarget, withRetry, } from "../shared.js";
|
|
7
9
|
import { createTelegramAdapters } from "./context.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
];
|
|
15
|
-
class ChannelQueue {
|
|
16
|
-
constructor() {
|
|
17
|
-
this.queue = [];
|
|
18
|
-
this.processing = false;
|
|
19
|
-
}
|
|
20
|
-
enqueue(work) {
|
|
21
|
-
this.queue.push(work);
|
|
22
|
-
this.processNext();
|
|
23
|
-
}
|
|
24
|
-
size() {
|
|
25
|
-
return this.queue.length;
|
|
26
|
-
}
|
|
27
|
-
async processNext() {
|
|
28
|
-
if (this.processing || this.queue.length === 0)
|
|
29
|
-
return;
|
|
30
|
-
this.processing = true;
|
|
31
|
-
const work = this.queue.shift();
|
|
32
|
-
try {
|
|
33
|
-
await work();
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
log.logWarning("Telegram queue error", err instanceof Error ? err.message : String(err));
|
|
37
|
-
}
|
|
38
|
-
this.processing = false;
|
|
39
|
-
this.processNext();
|
|
40
|
-
}
|
|
10
|
+
import { escapeTelegramHtml } from "./html.js";
|
|
11
|
+
// grammY surfaces Telegram errors as `GrammyError` with `error_code` mirroring
|
|
12
|
+
// the Bot API. 429 is the rate-limit status; the response also includes
|
|
13
|
+
// `parameters.retry_after` but exponential backoff is good enough here.
|
|
14
|
+
function telegramIsRateLimited(err) {
|
|
15
|
+
return err.error_code === 429;
|
|
41
16
|
}
|
|
17
|
+
const telegramRetry = (fn) => withRetry(fn, { isRateLimited: telegramIsRateLimited });
|
|
42
18
|
// ============================================================================
|
|
43
19
|
// TelegramBot
|
|
44
20
|
// ============================================================================
|
|
21
|
+
function isTelegramHtmlParseError(message) {
|
|
22
|
+
return message.includes("can't parse entities");
|
|
23
|
+
}
|
|
45
24
|
export class TelegramBot {
|
|
46
25
|
constructor(handler, config) {
|
|
47
26
|
this.botUserId = null;
|
|
@@ -64,46 +43,57 @@ export class TelegramBot {
|
|
|
64
43
|
this.botUserId = String(me.id);
|
|
65
44
|
this.botUsername = me.username ?? null;
|
|
66
45
|
this.startupTime = Date.now();
|
|
67
|
-
await this.client.api.setMyCommands([
|
|
46
|
+
await this.client.api.setMyCommands([
|
|
47
|
+
{ command: "login", description: "Store credentials in your private vault" },
|
|
48
|
+
{ command: "session", description: "Open the current session in the web viewer" },
|
|
49
|
+
{ command: "model", description: "Switch this conversation's LLM model" },
|
|
50
|
+
{ command: "sandbox", description: "Show or boost sandbox limits" },
|
|
51
|
+
{ command: "stop", description: "Stop ongoing conversation" },
|
|
52
|
+
{ command: "new", description: "Reset conversation history and start fresh" },
|
|
53
|
+
]);
|
|
68
54
|
this.setupEventHandlers();
|
|
69
55
|
// Start polling in background (bot.start() runs indefinitely)
|
|
70
56
|
this.client.start().catch((err) => {
|
|
71
57
|
log.logWarning("Telegram polling error", err instanceof Error ? err.message : String(err));
|
|
72
58
|
});
|
|
73
|
-
log.logConnected();
|
|
59
|
+
log.logConnected("Telegram");
|
|
74
60
|
log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);
|
|
75
61
|
}
|
|
76
|
-
async postMessage(
|
|
77
|
-
const result = await this.postMessageRaw(parseInt(
|
|
62
|
+
async postMessage(channel, text) {
|
|
63
|
+
const result = await this.postMessageRaw(parseInt(channel), text);
|
|
78
64
|
return String(result);
|
|
79
65
|
}
|
|
80
|
-
async updateMessage(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
catch (err) {
|
|
87
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
-
if (!msg.includes("message is not modified")) {
|
|
89
|
-
throw err;
|
|
66
|
+
async updateMessage(channel, ts, text) {
|
|
67
|
+
return telegramRetry(async () => {
|
|
68
|
+
try {
|
|
69
|
+
await this.client.api.editMessageText(parseInt(channel), parseInt(ts), text, {
|
|
70
|
+
parse_mode: "HTML",
|
|
71
|
+
});
|
|
90
72
|
}
|
|
91
|
-
|
|
73
|
+
catch (err) {
|
|
74
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
if (msg.includes("message is not modified")) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!isTelegramHtmlParseError(msg)) {
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
await this.client.api.editMessageText(parseInt(channel), parseInt(ts), escapeTelegramHtml(text), {
|
|
82
|
+
parse_mode: "HTML",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
92
86
|
}
|
|
93
87
|
enqueueEvent(event) {
|
|
94
|
-
const
|
|
88
|
+
const conversationId = event.conversationId;
|
|
89
|
+
const queue = this.getQueue(conversationId);
|
|
95
90
|
if (queue.size() >= 5) {
|
|
96
|
-
log.logWarning(`Event queue full for ${
|
|
91
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
97
92
|
return false;
|
|
98
93
|
}
|
|
99
|
-
log.logInfo(`Enqueueing event for ${
|
|
94
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
100
95
|
queue.enqueue(() => {
|
|
101
|
-
const
|
|
102
|
-
...event,
|
|
103
|
-
type: "message",
|
|
104
|
-
conversationId: event.conversationId,
|
|
105
|
-
};
|
|
106
|
-
const adapters = createTelegramAdapters(telegramEvent, this, true);
|
|
96
|
+
const adapters = createTelegramAdapters(event, this, true);
|
|
107
97
|
return this.handler.handleEvent(event, this, adapters, true);
|
|
108
98
|
});
|
|
109
99
|
return true;
|
|
@@ -120,18 +110,49 @@ export class TelegramBot {
|
|
|
120
110
|
// Internal helpers (used by context.ts)
|
|
121
111
|
// ==========================================================================
|
|
122
112
|
async postMessageRaw(chatId, text) {
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
return telegramRetry(async () => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await this.client.api.sendMessage(chatId, text, { parse_mode: "HTML" });
|
|
116
|
+
return result.message_id;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
120
|
+
if (!isTelegramHtmlParseError(msg)) {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
const result = await this.client.api.sendMessage(chatId, escapeTelegramHtml(text), {
|
|
124
|
+
parse_mode: "HTML",
|
|
125
|
+
});
|
|
126
|
+
return result.message_id;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
125
129
|
}
|
|
126
130
|
async postPlainMessage(chatId, text) {
|
|
127
|
-
|
|
131
|
+
return telegramRetry(async () => {
|
|
132
|
+
await this.client.api.sendMessage(chatId, text);
|
|
133
|
+
});
|
|
128
134
|
}
|
|
129
135
|
async postReply(chatId, replyToMessageId, text) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
return telegramRetry(async () => {
|
|
137
|
+
try {
|
|
138
|
+
const result = await this.client.api.sendMessage(chatId, text, {
|
|
139
|
+
parse_mode: "HTML",
|
|
140
|
+
reply_parameters: { message_id: replyToMessageId },
|
|
141
|
+
});
|
|
142
|
+
return result.message_id;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
if (!isTelegramHtmlParseError(msg)) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
const result = await this.client.api.sendMessage(chatId, escapeTelegramHtml(text), {
|
|
150
|
+
parse_mode: "HTML",
|
|
151
|
+
reply_parameters: { message_id: replyToMessageId },
|
|
152
|
+
});
|
|
153
|
+
return result.message_id;
|
|
154
|
+
}
|
|
133
155
|
});
|
|
134
|
-
return result.message_id;
|
|
135
156
|
}
|
|
136
157
|
async deleteMessageRaw(chatId, messageId) {
|
|
137
158
|
await this.client.api.deleteMessage(chatId, messageId);
|
|
@@ -140,25 +161,17 @@ export class TelegramBot {
|
|
|
140
161
|
await this.client.api.sendChatAction(chatId, "typing");
|
|
141
162
|
}
|
|
142
163
|
async uploadFile(channel, filePath, title) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
return telegramRetry(async () => {
|
|
165
|
+
const fileName = title ?? basename(filePath);
|
|
166
|
+
const fileContent = readFileSync(filePath);
|
|
167
|
+
await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));
|
|
168
|
+
});
|
|
146
169
|
}
|
|
147
170
|
logToFile(channel, entry) {
|
|
148
|
-
|
|
149
|
-
if (!existsSync(channelDir))
|
|
150
|
-
mkdirSync(channelDir, { recursive: true });
|
|
151
|
-
appendFileSync(join(channelDir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
171
|
+
appendChannelLog(this.workingDir, channel, entry);
|
|
152
172
|
}
|
|
153
173
|
logBotResponse(channel, text, ts) {
|
|
154
|
-
this.
|
|
155
|
-
date: new Date().toISOString(),
|
|
156
|
-
ts,
|
|
157
|
-
user: "bot",
|
|
158
|
-
text,
|
|
159
|
-
attachments: [],
|
|
160
|
-
isBot: true,
|
|
161
|
-
});
|
|
174
|
+
appendBotResponseLog(this.workingDir, channel, text, ts);
|
|
162
175
|
}
|
|
163
176
|
/**
|
|
164
177
|
* Process attachments from a Telegram message
|
|
@@ -200,9 +213,9 @@ export class TelegramBot {
|
|
|
200
213
|
const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
201
214
|
const filename = `${ts}_${sanitizedName}`;
|
|
202
215
|
const localPath = `${chatId}/attachments/${filename}`;
|
|
203
|
-
const
|
|
204
|
-
if (!existsSync(
|
|
205
|
-
mkdirSync(
|
|
216
|
+
const fullDir = join(this.workingDir, chatId, "attachments");
|
|
217
|
+
if (!existsSync(fullDir))
|
|
218
|
+
mkdirSync(fullDir, { recursive: true });
|
|
206
219
|
// Construct download URL
|
|
207
220
|
const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;
|
|
208
221
|
// Download the file
|
|
@@ -211,7 +224,7 @@ export class TelegramBot {
|
|
|
211
224
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
212
225
|
}
|
|
213
226
|
const buffer = await response.arrayBuffer();
|
|
214
|
-
writeFileSync(join(
|
|
227
|
+
writeFileSync(join(fullDir, filename), Buffer.from(buffer));
|
|
215
228
|
return {
|
|
216
229
|
name: originalName,
|
|
217
230
|
localPath: localPath,
|
|
@@ -228,7 +241,7 @@ export class TelegramBot {
|
|
|
228
241
|
getQueue(channelId) {
|
|
229
242
|
let queue = this.queues.get(channelId);
|
|
230
243
|
if (!queue) {
|
|
231
|
-
queue = new ChannelQueue();
|
|
244
|
+
queue = new ChannelQueue("Telegram");
|
|
232
245
|
this.queues.set(channelId, queue);
|
|
233
246
|
}
|
|
234
247
|
return queue;
|
|
@@ -250,10 +263,25 @@ export class TelegramBot {
|
|
|
250
263
|
const msgId = String(msg.message_id);
|
|
251
264
|
const replyToId = msg.reply_to_message?.message_id;
|
|
252
265
|
const threadTs = replyToId ? String(replyToId) : undefined;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
266
|
+
const conversationKind = chatType === "private" ? "direct" : "shared";
|
|
267
|
+
const sessionKey = resolveChatSessionKey({
|
|
268
|
+
conversationId: chatId,
|
|
269
|
+
conversationKind,
|
|
270
|
+
messageId: msgId,
|
|
271
|
+
threadTs,
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
msg,
|
|
275
|
+
text,
|
|
276
|
+
chatId,
|
|
277
|
+
chatType,
|
|
278
|
+
conversationKind,
|
|
279
|
+
userId,
|
|
280
|
+
userName,
|
|
281
|
+
msgId,
|
|
282
|
+
threadTs,
|
|
283
|
+
sessionKey,
|
|
284
|
+
};
|
|
257
285
|
}
|
|
258
286
|
isAddressedToBot(text, chatType) {
|
|
259
287
|
if (chatType === "private")
|
|
@@ -267,45 +295,28 @@ export class TelegramBot {
|
|
|
267
295
|
return text.trim();
|
|
268
296
|
return text.replace(new RegExp(`@${this.botUsername}`, "gi"), "").trim();
|
|
269
297
|
}
|
|
298
|
+
isStopText(text) {
|
|
299
|
+
return /^\/?stop(?:@\w+)?$/i.test(text.trim());
|
|
300
|
+
}
|
|
301
|
+
resolveStopTarget(mc) {
|
|
302
|
+
const directTarget = resolveStopTarget({
|
|
303
|
+
handler: this.handler,
|
|
304
|
+
conversationId: mc.chatId,
|
|
305
|
+
sessionKey: mc.sessionKey,
|
|
306
|
+
});
|
|
307
|
+
if (directTarget)
|
|
308
|
+
return directTarget;
|
|
309
|
+
return resolveOnlyScopedStopTarget(this.handler, mc.chatId);
|
|
310
|
+
}
|
|
270
311
|
setupEventHandlers() {
|
|
271
312
|
// --- Slash commands (registered before catch-all so grammY intercepts them) ---
|
|
272
|
-
this.client.command("start", async (ctx) => {
|
|
273
|
-
const mc = this.extractMessageContext(ctx.message);
|
|
274
|
-
if (!mc)
|
|
275
|
-
return;
|
|
276
|
-
await this.postMessageRaw(parseInt(mc.chatId), [
|
|
277
|
-
"<b>Welcome!</b>",
|
|
278
|
-
"",
|
|
279
|
-
"I'm an AI coding agent. Send me a message or use these commands:",
|
|
280
|
-
"",
|
|
281
|
-
"/new — Start a new conversation",
|
|
282
|
-
"/stop — Stop the current task",
|
|
283
|
-
"/help — Show help",
|
|
284
|
-
"/login — Open your secure login page",
|
|
285
|
-
].join("\n"));
|
|
286
|
-
});
|
|
287
|
-
this.client.command("help", async (ctx) => {
|
|
288
|
-
const mc = this.extractMessageContext(ctx.message);
|
|
289
|
-
if (!mc)
|
|
290
|
-
return;
|
|
291
|
-
await this.postMessageRaw(parseInt(mc.chatId), [
|
|
292
|
-
"<b>Available commands:</b>",
|
|
293
|
-
"",
|
|
294
|
-
"/start — Show the welcome message",
|
|
295
|
-
"/help — Show help",
|
|
296
|
-
"/stop — Stop the current task",
|
|
297
|
-
"/new — Start a new conversation",
|
|
298
|
-
"/login — Open your secure login page",
|
|
299
|
-
"",
|
|
300
|
-
"You can also send a regular message to chat with the agent.",
|
|
301
|
-
].join("\n"));
|
|
302
|
-
});
|
|
303
313
|
this.client.command("stop", async (ctx) => {
|
|
304
314
|
const mc = this.extractMessageContext(ctx.message);
|
|
305
315
|
if (!mc)
|
|
306
316
|
return;
|
|
307
|
-
|
|
308
|
-
|
|
317
|
+
const stopTarget = this.resolveStopTarget(mc);
|
|
318
|
+
if (stopTarget) {
|
|
319
|
+
await this.handler.handleStop(stopTarget, mc.chatId, this);
|
|
309
320
|
}
|
|
310
321
|
else {
|
|
311
322
|
await this.postMessage(mc.chatId, formatNothingRunning("telegram"));
|
|
@@ -315,60 +326,95 @@ export class TelegramBot {
|
|
|
315
326
|
const mc = this.extractMessageContext(ctx.message);
|
|
316
327
|
if (!mc)
|
|
317
328
|
return;
|
|
318
|
-
await this.handler.
|
|
329
|
+
await this.handler.handleNewCommand(mc.sessionKey, mc.chatId, this);
|
|
319
330
|
});
|
|
320
|
-
this.client.command("
|
|
331
|
+
this.client.command("sandbox", async (ctx) => {
|
|
321
332
|
const mc = this.extractMessageContext(ctx.message);
|
|
322
333
|
if (!mc)
|
|
323
334
|
return;
|
|
324
|
-
|
|
335
|
+
const cleanedText = this.cleanText(mc.text).replace(/^\/sandbox(?:@\w+)?/i, "/pi-sandbox");
|
|
336
|
+
const event = {
|
|
337
|
+
type: "command",
|
|
338
|
+
conversationId: mc.chatId,
|
|
339
|
+
conversationKind: mc.conversationKind,
|
|
340
|
+
ts: mc.msgId,
|
|
341
|
+
thread_ts: mc.threadTs,
|
|
342
|
+
sessionKey: mc.sessionKey,
|
|
343
|
+
user: mc.userId,
|
|
344
|
+
userName: mc.userName,
|
|
345
|
+
text: cleanedText,
|
|
346
|
+
attachments: [],
|
|
347
|
+
};
|
|
348
|
+
this.logToFile(mc.chatId, {
|
|
349
|
+
date: new Date(mc.msg.date * 1000).toISOString(),
|
|
350
|
+
ts: mc.msgId,
|
|
351
|
+
...(mc.conversationKind === "shared" && mc.threadTs ? { threadTs: mc.threadTs } : {}),
|
|
352
|
+
user: mc.userId,
|
|
353
|
+
userName: mc.userName,
|
|
354
|
+
text: cleanedText,
|
|
355
|
+
attachments: [],
|
|
356
|
+
isBot: false,
|
|
357
|
+
});
|
|
358
|
+
const adapters = createTelegramAdapters(event, this, false);
|
|
359
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
325
360
|
});
|
|
326
361
|
// --- Catch-all for regular (non-command) messages ---
|
|
327
362
|
this.client.on("message", async (ctx) => {
|
|
328
363
|
const mc = this.extractMessageContext(ctx.message);
|
|
329
364
|
if (!mc)
|
|
330
365
|
return;
|
|
331
|
-
// In groups, only respond when addressed to bot
|
|
332
|
-
if (!this.isAddressedToBot(mc.text, mc.chatType))
|
|
333
|
-
return;
|
|
334
366
|
const cleanedText = this.cleanText(mc.text);
|
|
335
|
-
|
|
336
|
-
|
|
367
|
+
const addressedToBot = this.isAddressedToBot(mc.text, mc.chatType);
|
|
368
|
+
if (this.isStopText(cleanedText)) {
|
|
369
|
+
this.logToFile(mc.chatId, {
|
|
370
|
+
date: new Date(mc.msg.date * 1000).toISOString(),
|
|
371
|
+
ts: mc.msgId,
|
|
372
|
+
user: mc.userId,
|
|
373
|
+
userName: mc.userName,
|
|
374
|
+
text: cleanedText,
|
|
375
|
+
attachments: [],
|
|
376
|
+
isBot: false,
|
|
377
|
+
});
|
|
378
|
+
const stopTarget = this.resolveStopTarget(mc);
|
|
379
|
+
if (stopTarget) {
|
|
380
|
+
await this.handler.handleStop(stopTarget, mc.chatId, this);
|
|
381
|
+
}
|
|
382
|
+
else if (addressedToBot || mc.chatType === "private") {
|
|
383
|
+
await this.postMessage(mc.chatId, formatNothingRunning("telegram"));
|
|
384
|
+
}
|
|
337
385
|
return;
|
|
338
386
|
}
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
const event = {
|
|
387
|
+
const isAutoReplyCandidate = mc.chatType !== "private" && !addressedToBot;
|
|
388
|
+
const eventBase = {
|
|
342
389
|
type: "message",
|
|
343
390
|
conversationId: mc.chatId,
|
|
391
|
+
conversationKind: mc.conversationKind,
|
|
344
392
|
ts: mc.msgId,
|
|
345
393
|
thread_ts: mc.threadTs,
|
|
346
394
|
sessionKey: mc.sessionKey,
|
|
347
395
|
user: mc.userId,
|
|
348
396
|
userName: mc.userName,
|
|
349
397
|
text: cleanedText,
|
|
350
|
-
attachments: processedAttachments,
|
|
351
398
|
};
|
|
352
|
-
|
|
353
|
-
|
|
399
|
+
const triggerResult = isAutoReplyCandidate
|
|
400
|
+
? await evaluateAutoReplyPolicy({ event: eventBase, workingDir: this.workingDir })
|
|
401
|
+
: { trigger: true, reason: "addressed" };
|
|
402
|
+
const logEntryBase = {
|
|
354
403
|
date: new Date(mc.msg.date * 1000).toISOString(),
|
|
355
404
|
ts: mc.msgId,
|
|
405
|
+
...(mc.conversationKind === "shared" && mc.threadTs ? { threadTs: mc.threadTs } : {}),
|
|
356
406
|
user: mc.userId,
|
|
357
407
|
userName: mc.userName,
|
|
358
408
|
text: cleanedText,
|
|
359
|
-
attachments: processedAttachments,
|
|
360
409
|
isBot: false,
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (this.handler.isRunning(mc.sessionKey)) {
|
|
365
|
-
await this.handler.handleStop(mc.sessionKey, mc.chatId, this);
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
await this.postMessage(mc.chatId, formatNothingRunning("telegram"));
|
|
369
|
-
}
|
|
410
|
+
};
|
|
411
|
+
if (!triggerResult.trigger) {
|
|
412
|
+
this.logToFile(mc.chatId, { ...logEntryBase, attachments: [] });
|
|
370
413
|
return;
|
|
371
414
|
}
|
|
415
|
+
const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);
|
|
416
|
+
const event = { ...eventBase, attachments: processedAttachments };
|
|
417
|
+
this.logToFile(mc.chatId, { ...logEntryBase, attachments: processedAttachments });
|
|
372
418
|
if (this.handler.isRunning(mc.sessionKey)) {
|
|
373
419
|
await this.postMessage(mc.chatId, formatAlreadyWorking("telegram", "/stop"));
|
|
374
420
|
}
|