Skip to content

OpenCode 通知系统:Bun + Telegram Bot 集成

执行摘要

本研究探讨了为 OpenCode 实现一个通知系统,当以下情况时通过 Telegram 发送实时状态更新:

  1. OpenCode 成功完成执行
  2. OpenCode 执行失败
  3. OpenCode 需要用户确认/交互

建议的方案利用 Bun 作为运行时,因其性能和 WebSocket 能力,结合 Telegram Bot API 实现可靠、跨平台的消息传递。这种方法提供了清晰的关注点分离,允许 OpenCode 发出事件,而独立的通知服务处理消息传递。

技术分析

1. 架构概述

┌─────────────────┐         ┌──────────────────┐         ┌─────────────┐
│   OpenCode      │────────▶│ Notification     │────────▶│  Telegram   │
│   (事件发射器)   │  事件   │  服务 (Bun)       │  HTTP   │   Bot API   │
└─────────────────┘         └──────────────────┘         └─────────────┘


                             ┌──────────────┐
                             │  本地 IPC/   │
                             │  WebSocket   │
                             └──────────────┘

核心组件:

  • OpenCode:发出生命周期事件(开始、成功、失败、需要确认)
  • 通知服务 (Bun):订阅事件、格式化消息、处理 Telegram API
  • Telegram Bot:向用户设备传递格式化的通知

2. 事件类型

事件名称触发条件载荷结构
opencode:start任务执行开始{ taskId, command, timestamp }
opencode:success任务成功完成{ taskId, duration, outputSummary }
opencode:failure任务失败{ taskId, error, stackTrace, exitCode }
opencode:confirm需要用户确认{ taskId, message, options }

3. Telegram Bot API 集成

端点: https://api.telegram.org/bot<token>/<method>

关键方法:

  • sendMessage:发送文本消息
  • sendMessagereply_markup:用于确认的交互式按钮
  • sendDocument:用于大型输出/日志(可选)

消息格式:

markdown
✅ 任务完成
任务: <command>
耗时: <time>
输出: <summary>

[查看详情]

4. Bun 优势

  1. 性能:I/O 操作比 Node.js 快 3 倍
  2. 内置 WebSocket:原生支持实时事件流
  3. TypeScript 支持:一流的 TypeScript 集成
  4. 打包器:内置打包功能,便于部署

5. 配置管理

环境变量:

env
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_id
OPENCODE_SOCKET_PATH=/tmp/opencode.sock
LOG_LEVEL=info

实现指南 / 概念验证

1. Telegram Bot 设置

步骤 1:通过 BotFather 创建 Bot

bash
# 在 Telegram 上向 @BotFather 发送 /newbot
# 按照提示获取您的 bot token

步骤 2:获取 Chat ID

bash
# 向您的 bot 发送一条消息,然后:
curl https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates

2. 通知服务 (Bun)

typescript
// 示例:notification-service.ts
import { Serve } from 'bun';

const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID!;

interface OpenCodeEvent {
  type: 'success' | 'failure' | 'confirm' | 'start';
  taskId: string;
  timestamp: number;
  data: any;
}

async function sendTelegramMessage(text: string, replyMarkup?: any) {
  const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
  
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      chat_id: TELEGRAM_CHAT_ID,
      text,
      parse_mode: 'Markdown',
      reply_markup: replyMarkup
    })
  });
  
  return response.json();
}

function formatEventMessage(event: OpenCodeEvent): string {
  const emoji = {
    success: '✅',
    failure: '❌',
    confirm: '❓',
    start: '🚀'
  };

  switch (event.type) {
    case 'success':
      return `${emoji.success} *OpenCode 任务完成*\n\n` +
             `任务 ID: \`${event.taskId}\`\n` +
             `耗时: ${event.data.duration}s\n` +
             `输出预览: \`${event.data.output.substring(0, 100)}...\``;

    case 'failure':
      return `${emoji.failure} *OpenCode 任务失败*\n\n` +
             `任务 ID: \`${event.taskId}\`\n` +
             `错误: \`${event.data.error}\`\n` +
             `退出码: ${event.data.exitCode}`;

    case 'confirm':
      return `${emoji.confirm} *需要操作*\n\n` +
             `任务 ID: \`${event.taskId}\`\n` +
             `${event.data.message}`;

    case 'start':
      return `${emoji.start} *OpenCode 任务开始*\n\n` +
             `任务 ID: \`${event.taskId}\`\n` +
             `命令: \`${event.data.command}\``;
  }
}

function createConfirmationButtons(taskId: string, options: string[]) {
  return {
    inline_keyboard: options.map((opt, idx) => [
      {
        text: opt,
        callback_data: JSON.stringify({ action: 'confirm', taskId, choice: idx })
      }
    ])
  };
}

// 用于 OpenCode 事件的 WebSocket 服务器
const server = Bun.serve({
  port: 3000,
  fetch(req) {
    if (req.url.endsWith('/events')) {
      return new Response('OpenCode 通知服务运行中', {
        headers: { 'Content-Type': 'text/plain' }
      });
    }
    return new Response('未找到', { status: 404 });
  }
});

console.log(`通知服务运行在端口 ${server.port}`);

// 事件处理程序(概念验证)
async function handleOpenCodeEvent(event: OpenCodeEvent) {
  const message = formatEventMessage(event);
  
  if (event.type === 'confirm' && event.data.options) {
    const keyboard = createConfirmationButtons(event.taskId, event.data.options);
    await sendTelegramMessage(message, keyboard);
  } else {
    await sendTelegramMessage(message);
  }
}

// 示例:处理来自 OpenCode 的传入事件
// 在生产环境中,这将连接到 OpenCode 的事件发射器

3. OpenCode 事件发射器集成

概念集成模式:

typescript
// 示例:opencode-integration.ts
import { EventEmitter } from 'events';

class OpenCodeNotifier extends EventEmitter {
  private notificationServiceUrl: string;

  constructor(notificationServiceUrl: string) {
    super();
    this.notificationServiceUrl = notificationServiceUrl;
  }

  async notifySuccess(taskId: string, duration: number, output: string) {
    const event = {
      type: 'success' as const,
      taskId,
      timestamp: Date.now(),
      data: { duration, output }
    };
    
    await this.sendToNotificationService(event);
  }

  async notifyFailure(taskId: string, error: string, exitCode: number) {
    const event = {
      type: 'failure' as const,
      taskId,
      timestamp: Date.now(),
      data: { error, exitCode }
    };
    
    await this.sendToNotificationService(event);
  }

  async requestConfirmation(taskId: string, message: string, options: string[]) {
    const event = {
      type: 'confirm' as const,
      taskId,
      timestamp: Date.now(),
      data: { message, options }
    };
    
    await this.sendToNotificationService(event);
    return this.waitForConfirmation(taskId);
  }

  private async sendToNotificationService(event: any) {
    await fetch(`${this.notificationServiceUrl}/event`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event)
    });
  }

  private async waitForConfirmation(taskId: string): Promise<number> {
    return new Promise((resolve) => {
      this.once(`confirmed:${taskId}`, (choice: number) => {
        resolve(choice);
      });
    });
  }
}

4. Telegram 回调处理

typescript
// 示例:telegram-webhook.ts
import { Serve } from 'bun';

async function handleCallback(callback: any) {
  const data = JSON.parse(callback.callback_query.data);
  
  if (data.action === 'confirm') {
    // 将确认发送回 OpenCode
    await fetch('http://localhost:3000/confirm', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        taskId: data.taskId,
        choice: data.choice
      })
    });

    // 回答回调查询
    await fetch(
      `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/answerCallbackQuery`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          callback_query_id: callback.callback_query.id,
          text: '已确认!'
        })
      }
    );
  }
}

// Telegram 回调的 Webhook 服务器
const webhookServer = Bun.serve({
  port: 3001,
  async fetch(req) {
    if (req.method === 'POST' && req.url.endsWith('/webhook')) {
      const callback = await req.json();
      await handleCallback(callback);
      return new Response('OK');
    }
    return new Response('未找到', { status: 404 });
  }
});

console.log(`Telegram webhook 运行在端口 ${webhookServer.port}`);

5. 部署考虑

选项 A:本地开发

bash
# 运行通知服务
bun run notification-service.ts

# 设置 webhook(可选,用于回调)
curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://your-domain.com/webhook"

选项 B:Docker 部署

dockerfile
# 示例:Dockerfile
FROM oven/bun:1-alpine

WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install

COPY . .

EXPOSE 3000 3001
CMD ["bun", "run", "notification-service.ts"]
yaml
# 示例:docker-compose.yml
version: '3.8'
services:
  notification-service:
    build: .
    ports:
      - "3000:3000"
      - "3001:3001"
    environment:
      - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
      - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
    restart: unless-stopped

6. 安全考虑

  1. Token 安全:将 bot tokens 存储在环境变量中,永远不要提交到 git
  2. 身份验证:添加 API 密钥以验证 OpenCode → 通知服务的通信
  3. 速率限制:Telegram API 有速率限制(每秒 30 条消息)
  4. 输入清理:清理所有 markdown 输出以防止注入攻击

考虑的替代方案

方法优点缺点
Telegram Bot(推荐)跨平台、可靠、丰富的消息格式需要 bot token 设置
Discord Webhook开发人员熟悉、丰富的嵌入Discord 特定
邮件通知通用、异步传递较慢、交互性较差
推送通知 (Pushover)简单、快速需要付费服务
Slack 集成专业、面向团队需要 Slack 工作区

挑战与缓解措施

挑战 1:确认延迟

问题:Telegram 回调可能无法实时到达 OpenCode 解决方案:实施轮询回退或 WebSocket 双向通信

挑战 2:大型输出处理

问题:任务输出可能超过 Telegram 的 4096 个字符限制 解决方案

  • 使用 "..." 指示符截断
  • 使用 sendDocument 发送完整日志
  • 提供外部存储链接

挑战 3:多用户支持

问题:多个用户使用不同的 Telegram 账户 解决方案

  • 用户映射表 (userId → telegramChatId)
  • 用户特定的 bot 命令 (/register <userId>)
  • 会话管理

结论

建议的使用 BunTelegram Bot API 的通知系统为实时 OpenCode 状态更新提供了强大、高性能和用户友好的解决方案。该架构提供了:

低延迟:Bun 的性能 + WebSocket 通信 ✅ 丰富的交互性:用于确认的内联按钮 ✅ 跨平台:在 Telegram 桌面、移动端和 Web 上运行 ✅ 关注点分离:通知服务与 OpenCode 核心解耦 ✅ 可扩展性:易于添加更多事件类型或通知渠道

推荐实施路径:

  1. 从基本成功/失败通知开始 (MVP)
  2. 添加确认工作流
  3. 实施重试逻辑和错误处理
  4. 添加速率限制和输出截断
  5. 使用 Docker 进行生产部署

下一步:

  1. 设置 Telegram bot 并获取 tokens
  2. 在 Bun 中实现通知服务
  3. 将事件发射器集成到 OpenCode
  4. 使用各种任务场景进行测试
  5. 部署并监控性能