231 lines
11 KiB
Markdown
231 lines
11 KiB
Markdown
---
|
|
phase: 38-telegram-bridge
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- server/package.json
|
|
- server/src/services/telegram.ts
|
|
- server/src/routes/telegram.ts
|
|
- server/src/app.ts
|
|
autonomous: true
|
|
requirements: [TGRAM-01, TGRAM-02, TGRAM-05, TGRAM-06]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "A text message sent to the Telegram bot produces an agent reply prefixed with the agent name"
|
|
- "The bot runs via long polling with no public HTTPS endpoint"
|
|
- "The telegram.ts service file is under 500 lines"
|
|
artifacts:
|
|
- path: "server/src/services/telegram.ts"
|
|
provides: "grammY bot lifecycle, text message relay, session map, agent prefix"
|
|
exports: ["telegramService"]
|
|
- path: "server/src/routes/telegram.ts"
|
|
provides: "POST /api/telegram/token validation, GET /api/telegram/status"
|
|
exports: ["telegramRoutes"]
|
|
key_links:
|
|
- from: "server/src/services/telegram.ts"
|
|
to: "server/src/services/chat.ts"
|
|
via: "chatService(db).createConversation, addMessage"
|
|
pattern: "chatService.*createConversation|addMessage"
|
|
- from: "server/src/services/telegram.ts"
|
|
to: "server/src/services/puter-proxy.ts"
|
|
via: "puterProxyService(db).chatStream async generator collection"
|
|
pattern: "puterProxy.*chatStream"
|
|
- from: "server/src/app.ts"
|
|
to: "server/src/services/telegram.ts"
|
|
via: "conditional start on telegramToken presence"
|
|
pattern: "telegramService|tg\\.start"
|
|
---
|
|
|
|
<objective>
|
|
Create the Telegram bridge service and management routes. Install grammY, implement the telegramService factory function with text message relay (user -> agent -> reply with [AgentName] prefix), session mapping, long polling lifecycle, and token validation endpoint.
|
|
|
|
Purpose: This is the core Telegram relay infrastructure. Text messaging must work end-to-end before voice can be layered on (Plan 02).
|
|
Output: `server/src/services/telegram.ts`, `server/src/routes/telegram.ts`, updated `server/src/app.ts`
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/38-telegram-bridge/38-CONTEXT.md
|
|
@.planning/phases/38-telegram-bridge/38-RESEARCH.md
|
|
@.planning/REQUIREMENTS.md
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs -->
|
|
|
|
From server/src/services/chat.ts:
|
|
```typescript
|
|
export function chatService(db: Db) {
|
|
return {
|
|
async createConversation(companyId: string, data: { title?: string; agentId?: string }),
|
|
async addMessage(conversationId: string, data: { role: string; content: string; agentId?: string; ... }),
|
|
async listMessages(conversationId: string, opts: { cursor?: string; limit?: number }),
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
From server/src/services/puter-proxy.ts:
|
|
```typescript
|
|
export function puterProxyService(db: Db) {
|
|
return {
|
|
async* chatStream(
|
|
companyId: string,
|
|
agentId: string | null | undefined,
|
|
messages: unknown[],
|
|
model: string | undefined,
|
|
signal: AbortSignal | undefined,
|
|
): AsyncGenerator<string>,
|
|
async resolveToken(companyId: string): Promise<string>,
|
|
}
|
|
}
|
|
```
|
|
|
|
From server/src/services/nexus-settings.ts:
|
|
```typescript
|
|
export function nexusSettingsService() {
|
|
return {
|
|
async get(): Promise<NexusSettings>, // includes telegramToken?: string
|
|
async set(patch: Partial<NexusSettings>): Promise<NexusSettings>,
|
|
}
|
|
}
|
|
```
|
|
|
|
From server/src/services/agents.ts:
|
|
```typescript
|
|
// agentService(db).list(companyId) returns agents with .name property
|
|
```
|
|
|
|
From server/src/services/companies.ts:
|
|
```typescript
|
|
// companyService(db).list() returns companies array
|
|
```
|
|
|
|
Route mounting pattern in app.ts:
|
|
```typescript
|
|
api.use(someRoutes(db)); // mounts under /api prefix
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Install grammY and create telegramService + telegramRoutes</name>
|
|
<files>server/package.json, server/src/services/telegram.ts, server/src/routes/telegram.ts</files>
|
|
<read_first>
|
|
- server/src/services/chat.ts (createConversation, addMessage signatures)
|
|
- server/src/services/puter-proxy.ts (chatStream async generator pattern)
|
|
- server/src/services/agents.ts (list method, agent.name field)
|
|
- server/src/services/companies.ts (list method)
|
|
- server/src/services/nexus-settings.ts (get/set, telegramToken field)
|
|
- server/src/routes/nexus-settings.ts (route pattern reference)
|
|
- server/src/routes/authz.ts (assertBoard helper)
|
|
</read_first>
|
|
<action>
|
|
1. Install grammY: `cd server && pnpm add grammy`
|
|
|
|
2. Create `server/src/services/telegram.ts` — factory function `telegramService(db: Db)`:
|
|
- Import `Bot, InputFile` from "grammy", plus chatService, agentService, companyService, puterProxyService, nexusSettingsService, logger
|
|
- In-memory `sessionMap: Map<string, string>` for `${chatId}:${agentId}` -> conversationId
|
|
- `getOrCreateConversation(chatId, agentId, db)` — looks up Map, creates via chatService if missing
|
|
- `resolveDefaultAgent(db)` — gets first company via companyService(db).list(), then first agent via agentService(db).list(companyId). Returns `{ companyId, agentId, agentName }`. If no agents, returns null.
|
|
- `bot.on("message:text", async (ctx) => { ... })` handler:
|
|
- Resolve agent via `resolveDefaultAgent(db)`. If null, reply "No agents configured" and return.
|
|
- Get/create conversation via `getOrCreateConversation`
|
|
- Persist user message via `chatSvc.addMessage(convId, { role: "user", content: ctx.message.text })`
|
|
- Build messages array from `chatSvc.listMessages` (last 20 messages, map to `{role, content}` format for LLM)
|
|
- Collect full response from `puterProxyService(db).chatStream(companyId, agentId, messages, undefined, undefined)` by iterating the async generator
|
|
- Prefix reply with `[AgentName]: ` (TGRAM-02)
|
|
- Split at 4000 chars if reply exceeds 4096 (Telegram limit) — send multiple messages
|
|
- Persist assistant message via `chatSvc.addMessage`
|
|
- Send reply via `ctx.reply(text, { parse_mode: "Markdown" })`
|
|
- Wrap in try/catch — on error reply "Sorry, something went wrong."
|
|
- `bot.catch((err) => logger.error({ err }, "Telegram bot error"))` before start
|
|
- `start(token: string)`: create Bot, call `bot.api.deleteWebhook()` first (Pitfall 4), register handlers, call `bot.start()` (do NOT await — never-resolving promise). Save bot reference.
|
|
- `stop()`: await `bot?.stop()`, set bot to null
|
|
- `isRunning()`: returns `bot !== null`
|
|
- Return `{ start, stop, isRunning }`
|
|
- CRITICAL: Keep under 500 lines total (TGRAM-06). Target ~250 lines for text-only; voice handlers will add ~150 in Plan 02.
|
|
|
|
3. Create `server/src/routes/telegram.ts` — `telegramRoutes(db: Db)`:
|
|
- `POST /telegram/token` — assertBoard, validate token string from body, create temp `new Bot(token)`, call `bot.api.getMe()` to validate (catch -> 400 "Invalid Telegram bot token"). On success save via `nexusSettingsService().set({ telegramToken: token })`. Return `{ ok: true, botUsername: me.username }`.
|
|
- `GET /telegram/status` — assertBoard, return `{ running: boolean }` from the telegram service instance. To access the service instance, accept it as a second parameter: `telegramRoutes(db, telegramSvc)`.
|
|
|
|
</action>
|
|
<verify>
|
|
<automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep -q "grammy" server/package.json
|
|
- grep -q "export function telegramService" server/src/services/telegram.ts
|
|
- grep -q "export function telegramRoutes" server/src/routes/telegram.ts
|
|
- grep -q "bot.on.*message:text" server/src/services/telegram.ts
|
|
- grep -q "AgentName\|agentName" server/src/services/telegram.ts
|
|
- grep -q "bot.start()" server/src/services/telegram.ts
|
|
- grep -q "deleteWebhook" server/src/services/telegram.ts
|
|
- grep -q "getMe" server/src/routes/telegram.ts
|
|
- wc -l < server/src/services/telegram.ts | awk '{exit ($1 > 500)}'
|
|
</acceptance_criteria>
|
|
<done>telegramService factory function exists with text relay handler, agent prefix, long polling lifecycle. telegramRoutes has token validation and status endpoints. grammY installed. File under 500 lines.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire telegram service and routes into app.ts startup</name>
|
|
<files>server/src/app.ts</files>
|
|
<read_first>
|
|
- server/src/app.ts (full file — understand createApp structure, route mounting, return value)
|
|
- server/src/services/telegram.ts (the file created in Task 1)
|
|
- server/src/routes/telegram.ts (the file created in Task 1)
|
|
</read_first>
|
|
<action>
|
|
1. In `server/src/app.ts`:
|
|
- Add imports: `import { telegramService } from "./services/telegram.js"` and `import { telegramRoutes } from "./routes/telegram.js"`
|
|
- Inside `createApp` function, after all route mounting but before the catch-all 404:
|
|
- Create telegram service instance: `const tg = telegramService(db);`
|
|
- Mount routes: `api.use(telegramRoutes(db, tg));`
|
|
- Read settings and conditionally start: `const settings = await nexusSettingsService().get(); if (settings.telegramToken) { tg.start(settings.telegramToken).catch(err => logger.error({ err }, "Failed to start Telegram bot")); }`
|
|
- Add `nexusSettingsService` import if not already present
|
|
- Add `tg` to the return value of createApp (or store as module-level ref) so the token route can restart it after saving a new token
|
|
|
|
2. In `server/src/routes/telegram.ts`, update the POST /telegram/token handler to also (re)start the telegram service after saving the token. The route receives the service instance as parameter — call `await svc.stop()` then `await svc.start(token)`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep -q "telegramService" server/src/app.ts
|
|
- grep -q "telegramRoutes" server/src/app.ts
|
|
- grep -q "telegramToken" server/src/app.ts
|
|
- grep -q "tg.start\|tg\.stop\|svc.start\|svc\.stop" server/src/routes/telegram.ts
|
|
</acceptance_criteria>
|
|
<done>Telegram service starts automatically on app boot when token is configured. Token validation endpoint restarts the bot after saving new token. TypeScript compiles clean.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `cd server && pnpm exec tsc --noEmit` — zero errors
|
|
- `wc -l server/src/services/telegram.ts` — under 500 lines
|
|
- `grep -c "bot.on\|bot.start\|bot.catch" server/src/services/telegram.ts` — at least 3 matches
|
|
- `grep "telegramRoutes\|telegramService" server/src/app.ts` — both present
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- grammY installed in server/package.json
|
|
- telegramService factory function with text relay, agent prefix, session map, long polling
|
|
- telegramRoutes with POST /telegram/token (validates + saves + restarts) and GET /telegram/status
|
|
- app.ts conditionally starts telegram bot on boot
|
|
- TypeScript compiles without errors
|
|
- telegram.ts under 500 lines
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/38-telegram-bridge/38-01-SUMMARY.md`
|
|
</output>
|