feat: telegram platform

This commit is contained in:
Acbox
2026-01-11 19:15:18 +08:00
parent f783457160
commit a7e07d7467
18 changed files with 2324 additions and 2 deletions
Binary file not shown.
+7 -2
View File
@@ -4,7 +4,12 @@ import { treaty } from '@elysiajs/eden'
export type ApiClient = typeof app
export const createClient = (
baseUrl: string = process.env.API_BASE_URL ?? 'http://localhost:7002'
baseUrl: string = process.env.API_BASE_URL ?? 'http://localhost:7002',
token?: string,
) => {
return treaty<ApiClient>(baseUrl)
return treaty<ApiClient>(baseUrl, {
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
})
}
+6
View File
@@ -0,0 +1,6 @@
.env
node_modules/
dist/
*.log
.DS_Store
+213
View File
@@ -0,0 +1,213 @@
# Telegram Bot 快速开始
## 5 分钟启动指南
### 1. 获取 Bot Token
1. 在 Telegram 搜索 `@BotFather`
2. 发送 `/newbot`
3. 按提示输入 bot 名称和用户名
4. 复制获得的 token(格式类似:`123456789:ABCdefGHIjklMNOpqrsTUVwxyz`
### 2. 准备环境
```bash
# 安装依赖
cd packages/platform-telegram
pnpm install
# 创建配置文件
cat > .env << EOF
BOT_TOKEN=你的_bot_token
REDIS_URL=redis://localhost:6379
API_BASE_URL=http://localhost:7002
EOF
```
### 3. 启动服务
**Terminal 1 - 启动 Redis:**
```bash
redis-server
```
**Terminal 2 - 启动 MemoHome API:**
```bash
cd packages/api
pnpm start
```
**Terminal 3 - 启动 Telegram Bot:**
```bash
cd packages/platform-telegram
pnpm start
```
### 4. 测试 Bot
在 Telegram 中找到你的 bot,然后:
```
你: /start
Bot: 👋 Welcome to MemoHome Bot!
Available commands:
/login <username> <password> - Login to your account
/logout - Logout from your account
/whoami - Show current user info
/chat <message> - Chat with AI agent
/help - Show this help message
```
```
你: /login admin password
Bot: ✅ Login successful!
👤 Username: admin
🎭 Role: admin
🔑 User ID: xxx
You can now use the bot to interact with MemoHome.
```
```
你: /chat 你好,介绍一下你自己
Bot: 🤖 你好!我是 MemoHome AI 助手...
```
```
你: 今天天气怎么样?
Bot: 🤖 很抱歉,我没有实时天气信息...
```
## 多用户测试
用两个不同的 Telegram 账号测试:
**账号 A:**
```
A: /login userA passwordA
Bot: ✅ Login successful! Username: userA
A: 我是用户A
Bot: 🤖 你好 userA...
```
**账号 B:**
```
B: /login userB passwordB
Bot: ✅ Login successful! Username: userB
B: 我是用户B
Bot: 🤖 你好 userB...
```
两个用户的对话是完全独立的!
## 常见问题
### Q: Bot 没有响应?
**A:** 检查以下几点:
1. Bot token 是否正确
2. Bot 是否在运行:`pnpm start`
3. 查看控制台是否有错误
4. 在 BotFather 中确认 bot 已创建
### Q: 登录失败?
**A:**
1. 确认 API 是否在运行:`curl http://localhost:7002`
2. 检查用户名密码是否正确
3. 查看 API 日志
### Q: Redis 连接错误?
**A:**
1. 确认 Redis 在运行:`redis-cli ping` 应返回 `PONG`
2. 检查 `.env` 中的 `REDIS_URL`
3. 如果用 Docker`docker run -d -p 6379:6379 redis`
### Q: 如何查看存储的数据?
**A:**
```bash
# 连接 Redis
redis-cli
# 查看所有 token
KEYS memohome:tg:token:*
# 查看特定用户的 token
GET memohome:tg:token:123456789
# 查看用户信息
GET memohome:tg:user:123456789
# 查看过期时间
TTL memohome:tg:token:123456789
```
## 下一步
- 📖 阅读 [README.md](./README.md) 了解更多功能
- 🛠 查看 [SETUP.md](./SETUP.md) 了解详细配置
- 🎯 参考 [example.ts](./example.ts) 学习编程使用
- 📚 阅读项目根目录的 [REFACTORING_SUMMARY.md](../../REFACTORING_SUMMARY.md) 了解整体架构
## 添加自定义命令
编辑 `src/index.ts`
```typescript
// 添加一个新命令
this.bot.command('hello', requireAuth(storage), async (ctx) => {
await ctx.reply('Hello! 你好!')
})
```
重启 bot,然后在 Telegram 中:
```
你: /hello
Bot: Hello! 你好!
```
## 生产部署提示
1. **使用环境变量** 而不是 `.env` 文件
2. **使用 HTTPS** 作为 API endpoint
3. **使用 PM2** 或 Docker 保持进程运行
4. **添加日志** 监控和调试
5. **设置 Webhook** 而不是 polling(更高效)
示例 PM2 配置:
```json
{
"apps": [{
"name": "memohome-tg-bot",
"script": "bun",
"args": "run src/bot.ts",
"env": {
"BOT_TOKEN": "your_token",
"REDIS_URL": "redis://localhost:6379",
"API_BASE_URL": "https://api.yourdomain.com"
}
}]
}
```
启动:
```bash
pm2 start ecosystem.json
pm2 logs memohome-tg-bot
```
祝你使用愉快!🎉
+241
View File
@@ -0,0 +1,241 @@
# MemoHome Telegram Platform
Telegram bot platform for MemoHome, supporting multi-user authentication with Redis storage.
## Features
- 🔐 **Multi-user Authentication**: Each Telegram user can login to their own MemoHome account
- 💾 **Redis Storage**: Token and user info stored in Redis
- 💬 **AI Chat**: Stream responses from MemoHome AI agent
- 🔄 **Real-time Updates**: Live message editing during streaming responses
- 🛡️ **Auth Middleware**: Protected commands require login
## Installation
```bash
cd packages/platform-telegram
pnpm install
```
## Configuration
1. Create a `.env` file from the example:
```bash
cp .env.example .env
```
2. Configure your environment variables:
```env
# Get your bot token from @BotFather on Telegram
BOT_TOKEN=your_telegram_bot_token_here
# Redis connection string
REDIS_URL=redis://localhost:6379
# MemoHome API URL
API_BASE_URL=http://localhost:7002
```
## Usage
### Standalone Bot
Run the bot as a standalone process:
```bash
pnpm start
```
Or in development mode with auto-reload:
```bash
pnpm dev
```
### As a Platform Module
```typescript
import { TelegramPlatform } from '@memohome/platform-telegram'
const platform = new TelegramPlatform()
await platform.start({
botToken: process.env.BOT_TOKEN,
redisUrl: process.env.REDIS_URL,
apiUrl: process.env.API_BASE_URL,
})
```
## Bot Commands
### Authentication
- `/start` - Welcome message and command list
- `/login <username> <password>` - Login to your MemoHome account
- `/logout` - Logout from your account
- `/whoami` - Show current user information
### Chat
- `/chat <message>` - Send a message to the AI agent
- Or just send a message directly (requires login)
### Help
- `/help` - Show all available commands
## Architecture
### Storage Structure
Redis keys follow this pattern:
```
memohome:tg:token:{telegram_user_id} -> token (30 days TTL)
memohome:tg:user:{telegram_user_id} -> { username, role, userId } (30 days TTL)
```
### Multi-user Support
Each Telegram user ID is mapped to their own MemoHome account token:
```typescript
// User 123456 logs in
telegram_user_id: "123456" -> token: "abc..."
telegram_user_id: "123456" -> userInfo: { username: "user1", ... }
// User 789012 logs in
telegram_user_id: "789012" -> token: "def..."
telegram_user_id: "789012" -> userInfo: { username: "user2", ... }
```
### Authentication Flow
1. User sends `/login username password`
2. Bot validates credentials with MemoHome API
3. Token is stored in Redis with Telegram user ID as key
4. User info is cached in Redis
5. Subsequent messages use the stored token
### Middleware
The `requireAuth` middleware checks if the user is logged in before allowing access to protected commands:
```typescript
bot.command('chat', requireAuth(storage), async (ctx) => {
// User is guaranteed to be logged in
})
```
## Development
### Custom Commands
Add new commands to `src/index.ts`:
```typescript
this.bot.command('mycommand', requireAuth(storage), async (ctx) => {
const memoContext = getMemoContext(ctx, storage)
// Your command logic here
})
```
### Using Core Functions
```typescript
import { chatStreamAsync, listModels } from '@memohome/cli/core'
import { getMemoContext } from './auth'
// In a command handler
const memoContext = getMemoContext(ctx, storage)
// Chat with AI
await chatStreamAsync({
message: 'Hello',
language: 'Chinese'
}, async (event) => {
if (event.type === 'text-delta') {
// Handle streaming text
}
}, memoContext)
// Or use other core functions
const models = await listModels()
```
## Testing
### Test Login
```
/login admin password
```
### Test Chat
```
/chat 你好,今天天气怎么样?
```
Or send a message directly:
```
介绍一下 TypeScript
```
## Troubleshooting
### Bot not responding
1. Check if the bot token is correct
2. Ensure MemoHome API is running
3. Check Redis connection
### Login failed
1. Verify the API URL is correct
2. Check if the username/password is valid
3. Ensure the API server is accessible
### Redis errors
1. Make sure Redis is running: `redis-server`
2. Check the Redis URL in `.env`
3. Test connection: `redis-cli ping`
## Security Notes
- Never commit your `.env` file or bot token
- Tokens are stored with 30-day expiration
- Use HTTPS for production API endpoints
- Consider rate limiting for production use
## Production Deployment
### Docker
```dockerfile
FROM oven/bun:latest
WORKDIR /app
COPY package.json ./
RUN bun install
COPY . .
CMD ["bun", "run", "src/bot.ts"]
```
### Environment Variables
Set these in your deployment environment:
- `BOT_TOKEN` - Your Telegram bot token
- `REDIS_URL` - Redis connection string
- `API_BASE_URL` - MemoHome API URL
## License
ISC
+423
View File
@@ -0,0 +1,423 @@
# Telegram Bot Setup Guide
## Prerequisites
1. **Telegram Bot Token**
- Open Telegram and search for `@BotFather`
- Send `/newbot` and follow instructions
- Copy the bot token provided
2. **Redis Server**
```bash
# Install Redis (macOS)
brew install redis
# Start Redis
redis-server
# Or with Docker
docker run -d -p 6379:6379 redis:latest
```
3. **MemoHome API**
- Ensure the API server is running on `http://localhost:7002`
- Or update `API_BASE_URL` to your API endpoint
## Quick Start
### 1. Create Environment File
Create `.env` file in `packages/platform-telegram/`:
```env
BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
REDIS_URL=redis://localhost:6379
API_BASE_URL=http://localhost:7002
```
### 2. Install Dependencies
```bash
cd packages/platform-telegram
pnpm install
```
### 3. Start the Bot
```bash
pnpm start
```
You should see:
```
🚀 Starting Telegram bot...
📡 API URL: http://localhost:7002
💾 Redis URL: redis://localhost:6379
✅ Telegram bot started successfully
✅ Bot is running...
Press Ctrl+C to stop
```
### 4. Test the Bot
Open Telegram and find your bot, then:
1. **Start conversation**
```
/start
```
2. **Login to MemoHome**
```
/login admin password
```
3. **Check your user**
```
/whoami
```
4. **Chat with AI**
```
/chat 你好,介绍一下你自己
```
Or just send a message directly:
```
今天天气怎么样?
```
## Architecture Overview
### Storage Model
```
┌─────────────────────┐
│ Telegram User 1 │──► telegram_id: 123456
│ @user1 │ ├─ token: "abc..."
└─────────────────────┘ └─ userInfo: { username: "user1", ... }
┌─────────────────────┐
│ Telegram User 2 │──► telegram_id: 789012
│ @user2 │ ├─ token: "def..."
└─────────────────────┘ └─ userInfo: { username: "user2", ... }
```
### Data Flow
```
1. User: /login admin password
├──► Bot validates with MemoHome API
├──► API returns token + user info
└──► Redis stores:
├─ memohome:tg:token:123456 = "token_abc..."
└─ memohome:tg:user:123456 = { username, role, userId }
2. User: /chat Hello
├──► Middleware checks: is logged in?
├──► Get token from Redis by telegram_id
├──► Call MemoHome API with token
└──► Stream response back to user
```
## Common Use Cases
### Multiple Users
Each Telegram user can have their own account:
```
# User A logs in
@userA: /login adminA passwordA
Bot: ✅ Login successful! Username: adminA
# User B logs in
@userB: /login userB passwordB
Bot: ✅ Login successful! Username: userB
# Both can chat independently
@userA: /chat What's the weather?
@userB: /chat Tell me a joke
```
### Session Management
Tokens expire after 30 days. To re-login:
```
# Logout
/logout
# Login again
/login username password
```
### Check Authentication
```
/whoami
Response:
👤 Current User:
Username: admin
Role: admin
User ID: user-id-xxx
Telegram ID: 123456789
```
## Development
### Project Structure
```
packages/platform-telegram/
├── src/
│ ├── index.ts # Main platform class
│ ├── bot.ts # Standalone entry point
│ ├── auth.ts # Auth handlers & middleware
│ └── storage.ts # Redis storage implementation
├── package.json
└── README.md
```
### Adding Custom Commands
Edit `src/index.ts`:
```typescript
// Add a new command
this.bot.command('mycommand', requireAuth(storage), async (ctx) => {
const memoContext = getMemoContext(ctx, storage)
// Your logic here
await ctx.reply('Command executed!')
})
```
### Using Core Functions
```typescript
import {
chatStreamAsync,
listModels,
searchMemory
} from '@memohome/cli/core'
// In command handler
const memoContext = getMemoContext(ctx, storage)
// List models
const models = await listModels()
// Search memory
const memories = await searchMemory({
query: 'TypeScript',
limit: 5
})
// Chat with streaming
await chatStreamAsync({
message: 'Hello'
}, async (event) => {
if (event.type === 'text-delta') {
console.log(event.text)
}
}, memoContext)
```
## Debugging
### Enable Verbose Logging
```bash
DEBUG=telegraf:* pnpm start
```
### Check Redis Data
```bash
# Connect to Redis CLI
redis-cli
# List all keys
KEYS memohome:tg:*
# Get a token
GET memohome:tg:token:123456
# Get user info
GET memohome:tg:user:123456
# Check TTL (time to live)
TTL memohome:tg:token:123456
```
### Test API Connection
```bash
# Test if API is accessible
curl http://localhost:7002/
# Test login endpoint
curl -X POST http://localhost:7002/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}'
```
## Production Deployment
### Using Docker Compose
Create `docker-compose.yml`:
```yaml
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
telegram-bot:
build: .
environment:
- BOT_TOKEN=${BOT_TOKEN}
- REDIS_URL=redis://redis:6379
- API_BASE_URL=${API_BASE_URL}
depends_on:
- redis
restart: unless-stopped
volumes:
redis-data:
```
Start with:
```bash
docker-compose up -d
```
### Environment Variables for Production
```env
BOT_TOKEN=your_production_bot_token
REDIS_URL=redis://your-redis-host:6379
API_BASE_URL=https://api.yourdomain.com
```
### Security Considerations
1. **Never commit** `.env` files or bot tokens
2. **Use HTTPS** for production API endpoints
3. **Rate limiting**: Consider adding rate limits to prevent abuse
4. **Token rotation**: Implement token refresh mechanism
5. **Monitoring**: Add logging and error tracking (e.g., Sentry)
## Troubleshooting
### Bot not responding
**Issue**: Bot doesn't respond to commands
**Solutions**:
1. Check bot is running: `pnpm start`
2. Verify bot token is correct
3. Check if bot is blocked by user
4. Review logs for errors
### Authentication failures
**Issue**: `/login` fails
**Solutions**:
1. Verify API URL is accessible
2. Check username/password are correct
3. Ensure MemoHome API is running
4. Test API endpoint directly with curl
### Redis connection errors
**Issue**: Cannot connect to Redis
**Solutions**:
1. Check Redis is running: `redis-cli ping` should return `PONG`
2. Verify REDIS_URL in `.env`
3. Check firewall settings
4. For Docker: ensure containers are on same network
### Token expiration
**Issue**: Commands fail after some time
**Solutions**:
1. Tokens expire after 30 days
2. User needs to `/logout` and `/login` again
3. Check token TTL in Redis: `TTL memohome:tg:token:123456`
## Advanced Usage
### Custom Storage Backend
You can implement your own storage:
```typescript
import type { TokenStorage } from '@memohome/cli/core'
class MyCustomStorage implements TokenStorage {
async getApiUrl(): Promise<string> { /* ... */ }
async setApiUrl(url: string): Promise<void> { /* ... */ }
async getToken(userId?: string): Promise<string | null> { /* ... */ }
async setToken(token: string, userId?: string): Promise<void> { /* ... */ }
async clearToken(userId?: string): Promise<void> { /* ... */ }
}
// Use it
const storage = new MyCustomStorage()
const platform = new TelegramPlatform()
// ... modify platform to use custom storage
```
### Webhook Mode (instead of polling)
For production, use webhooks instead of long polling:
```typescript
import { Telegraf } from 'telegraf'
const bot = new Telegraf(botToken)
// Set webhook
bot.telegram.setWebhook('https://yourdomain.com/bot')
// Express.js example
import express from 'express'
const app = express()
app.use(bot.webhookCallback('/bot'))
app.listen(3000)
```
## Support
For issues or questions:
1. Check the main [README.md](./README.md)
2. Review [ARCHITECTURE.md](../../ARCHITECTURE.md) in CLI package
3. Open an issue on GitHub
## License
ISC
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bun
/**
* Example: Running Telegram Bot
*
* This example shows how to start the Telegram bot programmatically
*/
import { TelegramPlatform } from './src/index'
async function main() {
// Configuration
const config = {
botToken: process.env.BOT_TOKEN || '',
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
apiUrl: process.env.API_BASE_URL || 'http://localhost:7002',
}
console.log('Starting Telegram bot with config:', {
apiUrl: config.apiUrl,
redisUrl: config.redisUrl,
botToken: config.botToken.substring(0, 10) + '...',
})
// Create and start platform
const platform = new TelegramPlatform()
try {
await platform.start(config)
console.log('Bot is running!')
console.log('\nAvailable commands:')
console.log(' /start - Welcome message')
console.log(' /login <username> <password> - Login to MemoHome')
console.log(' /whoami - Show current user')
console.log(' /chat <message> - Chat with AI')
console.log(' /logout - Logout')
// Handle shutdown
process.once('SIGINT', async () => {
console.log('\nStopping bot...')
await platform.stop()
process.exit(0)
})
} catch (error) {
console.error('Failed to start bot:', error)
process.exit(1)
}
}
// Only run if executed directly
if (import.meta.main) {
main()
}
export { main }
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@memohome/platform-telegram",
"version": "1.0.0",
"description": "Telegram platform for MemoHome",
"exports": {
".": "./src/index.ts"
},
"main": "src/index.ts",
"bin": {
"memohome-tg-bot": "./src/bot.ts"
},
"scripts": {
"start": "bun run src/bot.ts",
"dev": "bun run --watch src/bot.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.27.0",
"dependencies": {
"@memohome/client": "workspace:*",
"@memohome/platform": "workspace:*",
"dotenv": "^16.4.7",
"ioredis": "^5.9.1",
"telegraf": "^4.16.3"
},
"devDependencies": {
"@types/node": "^22.10.5"
}
}
+147
View File
@@ -0,0 +1,147 @@
import type { Context } from 'telegraf'
import { login, logout, isLoggedIn, getCurrentUser } from '@memohome/client'
import { getTokenStorage } from './storage'
/**
* Login command handler for Telegram bot
* Usage: /login username password
*/
export async function handleLogin(ctx: Context) {
const telegramUserId = ctx.from?.id.toString()
if (!telegramUserId) {
await ctx.reply('❌ Unable to identify user')
return
}
console.log('telegramUserId', telegramUserId)
// Parse command arguments
const args = ctx.message && 'text' in ctx.message
? ctx.message.text.split(' ').slice(1)
: []
if (args.length !== 2) {
await ctx.reply(
'❌ Invalid format\n\n' +
'Usage: /login <username> <password>\n' +
'Example: /login admin password'
)
return
}
const [username, password] = args
try {
const storage = await getTokenStorage(telegramUserId)
// Attempt login
const result = await login({ username, password }, { storage })
if (result.success && result.user) {
await ctx.reply(
'✅ Login successful!\n\n' +
`👤 Username: ${result.user.username}\n` +
`🎭 Role: ${result.user.role}\n` +
`🔑 User ID: ${result.user.id}\n\n` +
'You can now use the bot to interact with MemoHome.'
)
} else {
await ctx.reply('❌ Login failed: Invalid response from server')
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
await ctx.reply(`❌ Login failed: ${message}`)
}
}
/**
* Logout command handler for Telegram bot
* Usage: /logout
*/
export async function handleLogout(ctx: Context) {
const telegramUserId = ctx.from?.id.toString()
if (!telegramUserId) {
await ctx.reply('❌ Unable to identify user')
return
}
try {
const storage = await getTokenStorage(telegramUserId)
await logout({ storage })
await ctx.reply('✅ Logged out successfully')
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
await ctx.reply(`❌ Logout failed: ${message}`)
}
}
/**
* Whoami command handler - show current logged in user
* Usage: /whoami
*/
export async function handleWhoami(ctx: Context) {
const telegramUserId = ctx.from?.id.toString()
if (!telegramUserId) {
await ctx.reply('❌ Unable to identify user')
return
}
try {
const storage = await getTokenStorage(telegramUserId)
const isLogged = await isLoggedIn({ storage })
if (!isLogged) {
await ctx.reply(
'❌ You are not logged in\n\n' +
'Use /login <username> <password> to login'
)
return
}
const user = await getCurrentUser({ storage })
await ctx.reply(
'👤 Current User:\n\n' +
`Username: ${user.username}\n` +
`Role: ${user.role}\n` +
`User ID: ${user.id}\n` +
`Telegram ID: ${telegramUserId}`
)
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
await ctx.reply(`❌ Error: ${message}`)
}
}
/**
* Middleware to require authentication
* Add this middleware to commands that require login
*/
export function requireAuth() {
return async (ctx: Context, next: () => Promise<void>) => {
const telegramUserId = ctx.from?.id.toString()
if (!telegramUserId) {
await ctx.reply('❌ Unable to identify user')
return
}
const storage = await getTokenStorage(telegramUserId)
const isLogged = await isLoggedIn({ storage })
if (!isLogged) {
await ctx.reply(
'❌ You need to login first\n\n' +
'Use /login <username> <password> to login'
)
return
}
// User is authenticated, continue to next handler
await next()
}
}
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bun
/**
* Telegram Bot Standalone Entry Point
*
* This file allows running the Telegram bot as a standalone process
*/
import { TelegramPlatform } from './index'
async function main() {
const botToken = process.env.BOT_TOKEN
const redisUrl = process.env.REDIS_URL
const apiUrl = process.env.API_BASE_URL
if (!botToken) {
console.error('❌ BOT_TOKEN environment variable is required')
process.exit(1)
}
console.log('🚀 Starting Telegram bot...')
console.log(`📡 API URL: ${apiUrl || 'http://localhost:7002'}`)
console.log(`💾 Redis URL: ${redisUrl || 'redis://localhost:6379'}`)
const platform = new TelegramPlatform()
try {
await platform.start({
botToken,
redisUrl,
apiUrl,
})
console.log('✅ Bot is running...')
console.log('Press Ctrl+C to stop')
// Graceful shutdown
process.once('SIGINT', async () => {
console.log('\n🛑 Stopping bot...')
await platform.stop()
process.exit(0)
})
process.once('SIGTERM', async () => {
console.log('\n🛑 Stopping bot...')
await platform.stop()
process.exit(0)
})
} catch (error) {
console.error('❌ Failed to start bot:', error)
process.exit(1)
}
}
main()
+207
View File
@@ -0,0 +1,207 @@
import { Telegraf, type Context } from 'telegraf'
import { BasePlatform } from '@memohome/platform'
import { handleLogin, handleLogout, handleWhoami, requireAuth } from './auth'
import { chatStreamAsync, type StreamEvent } from '@memohome/client'
export interface TelegramPlatformConfig {
botToken: string
redisUrl?: string
apiUrl?: string
}
export class TelegramPlatform extends BasePlatform {
name = 'telegram'
description = 'Telegram Bot platform for MemoHome'
private bot?: Telegraf
// private storage?: TelegramRedisStorage
async start(config: Record<string, unknown>): Promise<void> {
const botToken = config.botToken as string
if (!botToken) {
throw new Error('Bot token is required')
}
// // Initialize storage
// this.storage = new TelegramRedisStorage({
// redisUrl: config.redisUrl as string,
// apiUrl: config.apiUrl as string,
// })
// Initialize bot
this.bot = new Telegraf(botToken)
// Register commands
this.registerCommands()
// Start bot
await this.bot.launch()
console.log('✅ Telegram bot started successfully')
}
async stop(): Promise<void> {
if (this.bot) {
this.bot.stop('SIGTERM')
console.log('🛑 Telegram bot stopped')
}
// if (this.storage) {
// await this.storage.close()
// console.log('🛑 Redis connection closed')
// }
}
private registerCommands(): void {
if (!this.bot) {
throw new Error('Bot or storage not initialized')
}
// Start command
this.bot.command('start', async (ctx) => {
await ctx.reply(
'👋 Welcome to MemoHome Bot!\n\n' +
'Available commands:\n' +
'/login <username> <password> - Login to your account\n' +
'/logout - Logout from your account\n' +
'/whoami - Show current user info\n' +
'/chat <message> - Chat with AI agent\n' +
'/help - Show this help message'
)
})
// Help command
this.bot.command('help', async (ctx) => {
await ctx.reply(
'📚 MemoHome Bot Help\n\n' +
'🔐 Authentication:\n' +
'/login <username> <password> - Login\n' +
'/logout - Logout\n' +
'/whoami - Show current user\n\n' +
'💬 Chat:\n' +
'/chat <message> - Talk to AI\n' +
'Or just send a message directly\n\n' +
'❓ Help:\n' +
'/help - Show this message'
)
})
// Auth commands
this.bot.command('login', (ctx) => handleLogin(ctx))
this.bot.command('logout', (ctx) => handleLogout(ctx))
this.bot.command('whoami', (ctx) => handleWhoami(ctx))
// Chat command (requires auth)
this.bot.command('chat', requireAuth(), async (ctx) => {
const args = ctx.message.text.split(' ').slice(1)
if (args.length === 0) {
await ctx.reply('❌ Please provide a message\n\nUsage: /chat <message>')
return
}
const message = args.join(' ')
await this.handleChat(ctx, message)
})
// Handle direct messages (requires auth)
this.bot.on('text', requireAuth(), async (ctx) => {
// Skip if it's a command
if (ctx.message.text.startsWith('/')) {
return
}
await this.handleChat(ctx, ctx.message.text)
})
// Error handling
this.bot.catch((err, ctx) => {
console.error('Bot error:', err)
ctx.reply('❌ An error occurred. Please try again.')
})
}
private async handleChat(ctx: Context, message: string): Promise<void> {
try {
// Send typing indicator
await ctx.sendChatAction('typing')
let responseText = ''
let lastUpdateTime = Date.now()
let messageId: number | undefined
await chatStreamAsync(
{
message,
language: 'Chinese',
maxContextLoadTime: 60,
},
async (event: StreamEvent) => {
if (event.type === 'text-delta' && event.text) {
responseText += event.text
// Update message every 1 second or when response is complete
const now = Date.now()
if (now - lastUpdateTime > 1000) {
lastUpdateTime = now
if (messageId && ctx.chat) {
// Edit existing message
try {
await ctx.telegram.editMessageText(
ctx.chat.id,
messageId,
undefined,
`🤖 ${responseText}`
)
} catch {
// Ignore if message is not modified
}
} else {
// Send first message
const sent = await ctx.reply(`🤖 ${responseText}`)
messageId = sent.message_id
}
}
} else if (event.type === 'tool-call') {
// Show tool usage
if (messageId && ctx.chat) {
try {
await ctx.telegram.editMessageText(
ctx.chat.id,
messageId,
undefined,
`🤖 ${responseText}\n\n🔧 Using tool: ${event.toolName}...`
)
} catch {
// Ignore
}
}
} else if (event.type === 'error') {
await ctx.reply(`❌ Error: ${event.error}`)
} else if (event.type === 'done') {
// Final update
if (messageId && responseText && ctx.chat) {
try {
await ctx.telegram.editMessageText(
ctx.chat.id,
messageId,
undefined,
`🤖 ${responseText}`
)
} catch {
// Ignore
}
} else if (!messageId && responseText) {
await ctx.reply(`🤖 ${responseText}`)
}
}
},
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
await ctx.reply(`❌ Error: ${errorMessage}`)
}
}
}
// Export for easy use
export { handleLogin, handleLogout, handleWhoami, requireAuth } from './auth'
+19
View File
@@ -0,0 +1,19 @@
import type { TokenStorage } from '@memohome/client'
import Redis from 'ioredis'
export const getTokenStorage = async (telegramUserId: string): Promise<TokenStorage> => {
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379')
const token = await redis.get(`memohome:telegram:${telegramUserId}:token`)
return {
getApiUrl: () => process.env.API_URL || 'http://localhost:7002',
setApiUrl: () => {},
getToken: () => token,
setToken: (token: string) => {
redis.set(`memohome:telegram:${telegramUserId}:token`, token)
},
clearToken: () => {
redis.del(`memohome:telegram:${telegramUserId}:token`)
},
}
}
+1
View File
@@ -0,0 +1 @@
# @memohome/platform
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@memohome/platform",
"version": "1.0.0",
"description": "",
"exports": {
".": "./src/index.ts"
},
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.27.0",
"dependencies": {
"elysia": "^1.4.21"
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Elysia } from 'elysia'
export class BasePlatform {
name: string = 'base'
description: string = 'Base platform'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async start(config: Record<string, unknown>): Promise<void> {}
async stop(): Promise<void> {}
async send(): Promise<void> {}
// serve(): void {
// const app = new Elysia()
// }
}