mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: telegram platform
This commit is contained in:
Binary file not shown.
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.env
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
祝你使用愉快!🎉
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
@@ -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`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# @memohome/platform
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user