Files
ProjectAGiPrompt/4-ProjectNaughtyMan/1-概要详细设计/3.1-详细设计说明书.md
2025-11-20 16:18:35 +08:00

52 KiB
Raw Permalink Blame History

Telegram Bot 智能通知与交互系统 - 详细设计说明书

项目代号: NaughtyMan 文档版本: v2.0 编制日期: 2025年10月24日 更新说明: 根据PRD v2.1需求更新


系统架构设计

总体架构

系统采用分层微服务架构,由接口层、业务逻辑层、数据访问层和外部服务层构成。架构设计遵循高内聚低耦合原则,确保各模块独立演进能力。

graph TB
    subgraph "客户端层"
        A1[ProjectOctopus]
        A2[ProjectTonyStack]
        A3[Telegram用户端]
    end

    subgraph "接入层"
        B1[RESTful API网关<br/>Gin框架]
        B2[Telegram Webhook<br/>Long Polling]
    end

    subgraph "业务逻辑层"
        C1[认证服务<br/>AuthService]
        C2[消息调度服务<br/>MessageDispatcher]
        C3[提醒管理服务<br/>ReminderService]
        C4[AI交互服务<br/>AIService]
        C5[速率控制服务<br/>RateLimiter]
        C6[用户管理服务<br/>UserManagementService]
        C7[指令管理服务<br/>CommandService]
    end

    subgraph "数据访问层"
        D1[消息队列<br/>PriorityQueue]
        D2[SQLite数据库<br/>WAL模式]
        D3[缓存层<br/>sync.Map]
    end

    subgraph "外部服务层"
        E1[Telegram Bot API]
        E2[OpenRouter API]
        E3[SOCKS5/HTTP代理]
    end

    A1 --> B1
    A2 --> B1
    A3 --> B2
    B1 --> C1
    B1 --> C2
    B2 --> C3
    B2 --> C4
    C2 --> C5
    C3 --> D2
    C4 --> D3
    C2 --> D1
    C5 --> E1
    C4 --> E2
    E1 -.代理.-> E3
    C6 --> D2
    C7 --> D2
    B2 --> C6
    B2 --> C7

技术选型说明

技术组件 选型方案 选型理由
开发语言 Go 1.21+ 原生并发支持、高性能、跨平台编译、内存安全
Web框架 Gin v1.9 轻量级、路由高效、中间件生态完善、社区活跃
数据库 SQLite 3.40+ 零配置部署、事务ACID保证、跨平台兼容、适合中小规模数据
Bot SDK tgbotapi v5 官方API完整封装、长连接稳定、支持代理配置
AI SDK OpenAI Go SDK 统一接口、支持OpenRouter等多提供商、流式响应完善
并发控制 sync.Map + Channel 原生并发安全结构、无锁化设计、性能优越
日志框架 zap 结构化日志、高性能、灵活的日志等级控制

核心模块详细设计

认证与安全模块

双阶段Token认证流程

sequenceDiagram
    participant C as 客户端
    participant G as API网关
    participant A as 认证服务
    participant D as 数据库

    C->>G: POST /auth/handshake<br/>{api_key}
    G->>A: 验证API Key
    A->>D: 查询白名单
    D-->>A: 返回权限范围
    A->>A: 生成Challenge码<br/>(UUID + 时间戳)
    A->>D: 存储Challenge<br/>(60s有效期)
    A-->>C: {challenge, expire_at}

    Note over C: 计算签名<br/>HMAC-SHA256(challenge+api_secret)

    C->>G: POST /auth/token<br/>{api_key, challenge, signature}
    G->>A: 验证签名
    A->>D: 查询Challenge
    D-->>A: Challenge数据
    A->>A: 对比签名<br/>检查时效
    A->>A: 生成JWT Token<br/>(6h有效期)
    A->>D: 记录Token映射
    A-->>C: {access_token, refresh_token}

Token数据结构设计

JWT Payload规范:

type TokenClaims struct {
    ApiKey       string   `json:"api_key"`
    Whitelist    []int64  `json:"whitelist"`    // 加密的授权目标ID列表
    Permissions  []string `json:"permissions"`  // send_message, query_status
    IssuedAt     int64    `json:"iat"`
    ExpiresAt    int64    `json:"exp"`
    RefreshAfter int64    `json:"rfa"`          // 允许刷新的时间阈值
}

数据库表结构:

CREATE TABLE api_credentials (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    api_key TEXT UNIQUE NOT NULL,
    api_secret TEXT NOT NULL,          -- BCRYPT加密存储
    project TEXT NOT NULL,             -- octopus/tonystack
    whitelist_ids TEXT,                -- JSON数组存储
    status INTEGER DEFAULT 1,          -- 1:启用 0:禁用
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_used DATETIME,
    INDEX idx_api_key (api_key)
);

CREATE TABLE token_sessions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    api_key TEXT NOT NULL,
    challenge TEXT,
    challenge_expire DATETIME,
    access_token TEXT UNIQUE,
    refresh_token TEXT UNIQUE,
    token_expire DATETIME,
    client_ip TEXT,
    user_agent TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_access_token (access_token),
    INDEX idx_challenge (challenge, challenge_expire)
);

白名单验证机制

中间件实现:

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")

        // 1. 解析JWT Token
        claims, err := jwt.Parse(token, secretKey)
        if err != nil || time.Now().Unix() > claims.ExpiresAt {
            c.JSON(401, gin.H{"error": "无效或过期的Token"})
            c.Abort()
            return
        }

        // 2. 验证目标ID在白名单内
        var req MessageRequest
        c.BindJSON(&req)

        if !contains(claims.Whitelist, req.TargetID) {
            c.JSON(403, gin.H{"error": "目标未授权"})
            c.Abort()
            return
        }

        // 3. 注入上下文
        c.Set("api_key", claims.ApiKey)
        c.Set("project", claims.Project)
        c.Next()
    }
}

消息通知系统

消息调度核心架构

graph LR
    A[API请求] --> B{消息等级判断}
    B -->|INFO| C[时段检查器<br/>08:00-23:00]
    B -->|WARNING| D[即时队列]
    B -->|CRITICAL| D
    C -->|时段内| E[批量聚合器<br/>5分钟窗口]
    C -->|时段外| F[延迟队列<br/>次日8:00]
    E --> G[优先级队列]
    D --> G
    F --> G
    G --> H[消息发送器选择]
    H --> I[速率限制器]
    I --> J[Telegram API]
    J -->|成功| K[消息日志]
    J -->|失败| L{重试判断}
    L -->|次数<3| M[指数退避重试]
    L -->|次数≥3| N[死信队列]
    M --> I

消息发送器包装层设计 [新增]

为支持不同类型的消息发送需求,系统实现了发送器包装层:

基础发送器接口:

type MessageSender interface {
    Send(chatID int64, content string, options ...SendOption) error
    ValidateTarget(chatID int64) error
}

type SendOption func(*SendConfig)

type SendConfig struct {
    ParseMode       string
    DisablePreview  bool
    ReplyToMessageID int
}

普通消息发送器实现:

type PlainMessageSender struct {
    bot         *tgbotapi.BotAPI
    rateLimiter *RateLimiter
    whitelist   *WhitelistChecker
    logger      *zap.Logger
}

func (s *PlainMessageSender) Send(chatID int64, text string, options ...SendOption) error {
    // 1. 白名单验证
    if err := s.whitelist.Check(chatID); err != nil {
        return fmt.Errorf("whitelist check failed: %w", err)
    }

    // 2. 应用配置选项
    config := &SendConfig{}
    for _, opt := range options {
        opt(config)
    }

    // 3. 等待速率限制器许可
    if !s.rateLimiter.AllowSend(chatID, false) {
        return ErrRateLimitExceeded
    }

    // 4. 发送消息
    msg := tgbotapi.NewMessage(chatID, text)
    if config.ParseMode != "" {
        msg.ParseMode = config.ParseMode
    }
    msg.DisableWebPagePreview = config.DisablePreview

    _, err := s.bot.Send(msg)
    return err
}

AI消息发送器实现:

type AIMessageSender struct {
    baseSender *PlainMessageSender
    logger     *zap.Logger
}

func (s *AIMessageSender) SendMarkdown(chatID int64, markdown string) error {
    // 转义Markdown特殊字符为MarkdownV2格式
    escaped := escapeMarkdownV2(markdown)

    // 使用基础发送器发送,指定ParseMode
    return s.baseSender.Send(chatID, escaped, func(cfg *SendConfig) {
        cfg.ParseMode = "MarkdownV2"
        cfg.DisablePreview = true
    })
}

func (s *AIMessageSender) SendStreamUpdate(chatID int64, messageID int, content string) error {
    // 检查速率限制(编辑消息)
    if !s.baseSender.rateLimiter.AllowSend(chatID, true) {
        return ErrRateLimitExceeded
    }

    edit := tgbotapi.NewEditMessageText(chatID, messageID, content)
    edit.ParseMode = "MarkdownV2"

    _, err := s.baseSender.bot.Send(edit)
    return err
}

// Markdown V2格式转义
func escapeMarkdownV2(text string) string {
    specialChars := []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
    escaped := text
    for _, char := range specialChars {
        escaped = strings.ReplaceAll(escaped, char, "\\"+char)
    }
    return escaped
}

发送器工厂:

type SenderFactory struct {
    bot         *tgbotapi.BotAPI
    rateLimiter *RateLimiter
    whitelist   *WhitelistChecker
}

func (f *SenderFactory) NewPlainSender() *PlainMessageSender {
    return &PlainMessageSender{
        bot:         f.bot,
        rateLimiter: f.rateLimiter,
        whitelist:   f.whitelist,
        logger:      zap.L(),
    }
}

func (f *SenderFactory) NewAISender() *AIMessageSender {
    return &AIMessageSender{
        baseSender: f.NewPlainSender(),
        logger:     zap.L(),
    }
}

优先级队列实现

数据结构设计:

type Message struct {
    ID          string
    ChatID      int64
    Level       string        // INFO/WARNING/CRITICAL
    Content     string
    Metadata    map[string]interface{}
    Priority    int           // CRITICAL:3, WARNING:2, INFO:1
    RetryCount  int
    ScheduledAt time.Time
    CreatedAt   time.Time
}

type PriorityQueue struct {
    heap        *priorityHeap  // 优先级小顶堆
    mutex       sync.RWMutex
    notEmpty    chan struct{}  // 通知消费者
    deadLetter  chan Message   // 死信队列通道
}

// 堆排序规则
func (pq *priorityHeap) Less(i, j int) bool {
    // 1. 优先级高的优先
    if pq[i].Priority != pq[j].Priority {
        return pq[i].Priority > pq[j].Priority
    }
    // 2. 相同优先级按时间戳排序
    return pq[i].ScheduledAt.Before(pq[j].ScheduledAt)
}

消息模板渲染引擎

模板定义规范:

type MessageTemplate struct {
    Project  string
    Level    string
    Template string
}

var templates = map[string]MessageTemplate{
    "octopus_critical": {
        Project: "octopus",
        Level:   "CRITICAL",
        Template: `🚨 【严重告警】{{.Title}}

📍 服务器: {{.Metadata.server}}
📊 详情: {{.Body}}
⏰ 时间: {{formatTime .Metadata.timestamp}}
🔗 查看详情: {{.Metadata.dashboard_url}}

#服务器监控 #CRITICAL`,
    },
    "tonystack_warning": {
        Project: "tonystack",
        Level:   "WARNING",
        Template: `⚠️ 【风险提示】{{.Title}}

💰 标的: {{.Metadata.symbol}}
📈 触发值: {{.Body}}
📉 当前价格: {{.Metadata.current_price}}
⏰ 时间: {{formatTime .Metadata.timestamp}}

#金融监控 #WARNING`,
    },
}

// 渲染函数
func RenderTemplate(msg Message) (string, error) {
    key := fmt.Sprintf("%s_%s", msg.Project, strings.ToLower(msg.Level))
    tmpl, exists := templates[key]
    if !exists {
        return "", errors.New("模板不存在")
    }

    t := template.New("message").Funcs(template.FuncMap{
        "formatTime": func(ts string) string {
            t, _ := time.Parse(time.RFC3339, ts)
            return t.Format("2006-01-02 15:04:05")
        },
    })

    t, _ = t.Parse(tmpl.Template)
    var buf bytes.Buffer
    t.Execute(&buf, msg)
    return buf.String(), nil
}

速率限制器设计

令牌桶算法实现:

type TokenBucket struct {
    capacity   int           // 桶容量
    tokens     int           // 当前令牌数
    refillRate time.Duration // 补充速率
    lastRefill time.Time
    mutex      sync.Mutex
}

func (tb *TokenBucket) Take() bool {
    tb.mutex.Lock()
    defer tb.mutex.Unlock()

    // 计算应补充的令牌数
    now := time.Now()
    elapsed := now.Sub(tb.lastRefill)
    refillCount := int(elapsed / tb.refillRate)

    if refillCount > 0 {
        tb.tokens = min(tb.capacity, tb.tokens + refillCount)
        tb.lastRefill = now
    }

    // 尝试获取令牌
    if tb.tokens > 0 {
        tb.tokens--
        return true
    }
    return false
}

type RateLimiter struct {
    globalBucket  *TokenBucket           // 全局30/s
    chatBuckets   sync.Map               // map[int64]*TokenBucket
    editBucket    *TokenBucket           // 编辑消息限流
}

func NewRateLimiter() *RateLimiter {
    return &RateLimiter{
        globalBucket: &TokenBucket{
            capacity:   30,
            tokens:     30,
            refillRate: time.Second / 30,
            lastRefill: time.Now(),
        },
        editBucket: &TokenBucket{
            capacity:   20,
            tokens:     20,
            refillRate: time.Minute / 20,
            lastRefill: time.Now(),
        },
    }
}

func (rl *RateLimiter) AllowSend(chatID int64, isEdit bool) bool {
    // 1. 检查全局限流
    if !rl.globalBucket.Take() {
        return false
    }

    // 2. 编辑消息检查专用桶
    if isEdit && !rl.editBucket.Take() {
        rl.globalBucket.tokens++ // 归还全局令牌
        return false
    }

    // 3. 检查单聊天限流
    bucket := rl.getChatBucket(chatID)
    if !bucket.Take() {
        rl.globalBucket.tokens++
        return false
    }

    return true
}

Telegram管理功能模块 [新增]

用户群组管理服务

服务架构:

graph TB
    A[Telegram Update] --> B[Update Handler]
    B --> C{消息类型}
    C -->|新用户消息| D[用户信息提取器]
    C -->|群组消息| E[群组信息提取器]
    D --> F[UserProfileService]
    E --> G[GroupProfileService]
    F --> H[SQLite存储]
    G --> H
    I[管理API] --> F
    I --> G

用户档案服务实现:

type UserProfileService struct {
    db     *sql.DB
    cache  *sync.Map  // 缓存用户信息
    logger *zap.Logger
}

type UserProfile struct {
    ID              int64
    UserID          int64
    Username        string
    FirstName       string
    LastName        string
    PhoneNumber     string
    LanguageCode    string
    IsBot           bool
    LastInteraction time.Time
}

func (s *UserProfileService) UpsertFromUpdate(update tgbotapi.Update) error {
    user := update.Message.From
    if user == nil {
        return nil
    }

    profile := UserProfile{
        UserID:          user.ID,
        Username:        user.UserName,
        FirstName:       user.FirstName,
        LastName:        user.LastName,
        LanguageCode:    user.LanguageCode,
        IsBot:           user.IsBot,
        LastInteraction: time.Now(),
    }

    // Upsert到数据库
    _, err := s.db.Exec(`
        INSERT INTO user_profiles (user_id, username, first_name, last_name, language_code, is_bot, last_interaction, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
        ON CONFLICT(user_id) DO UPDATE SET
            username = excluded.username,
            first_name = excluded.first_name,
            last_name = excluded.last_name,
            language_code = excluded.language_code,
            last_interaction = excluded.last_interaction,
            updated_at = CURRENT_TIMESTAMP
    `, profile.UserID, profile.Username, profile.FirstName, profile.LastName, profile.LanguageCode, profile.IsBot, profile.LastInteraction)

    if err == nil {
        s.cache.Store(profile.UserID, profile)
    }

    return err
}

func (s *UserProfileService) GetByUserID(userID int64) (*UserProfile, error) {
    // 先查缓存
    if cached, ok := s.cache.Load(userID); ok {
        profile := cached.(UserProfile)
        return &profile, nil
    }

    // 查数据库
    var profile UserProfile
    err := s.db.QueryRow(`
        SELECT id, user_id, username, first_name, last_name, phone_number, language_code, is_bot, last_interaction
        FROM user_profiles WHERE user_id = ?
    `, userID).Scan(&profile.ID, &profile.UserID, &profile.Username, &profile.FirstName,
        &profile.LastName, &profile.PhoneNumber, &profile.LanguageCode, &profile.IsBot, &profile.LastInteraction)

    if err == nil {
        s.cache.Store(userID, profile)
    }

    return &profile, err
}

func (s *UserProfileService) ListAllUsers() ([]UserProfile, error) {
    rows, err := s.db.Query(`
        SELECT id, user_id, username, first_name, last_name, phone_number, language_code, is_bot, last_interaction
        FROM user_profiles
        ORDER BY last_interaction DESC
    `)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var profiles []UserProfile
    for rows.Next() {
        var p UserProfile
        rows.Scan(&p.ID, &p.UserID, &p.Username, &p.FirstName, &p.LastName,
            &p.PhoneNumber, &p.LanguageCode, &p.IsBot, &p.LastInteraction)
        profiles = append(profiles, p)
    }

    return profiles, nil
}

群组档案服务实现:

type GroupProfileService struct {
    db     *sql.DB
    cache  *sync.Map
    logger *zap.Logger
}

type GroupProfile struct {
    ID              int64
    GroupID         int64
    Title           string
    Type            string  // group, supergroup, channel
    MemberCount     int
    LastInteraction time.Time
}

func (s *GroupProfileService) UpsertFromUpdate(update tgbotapi.Update) error {
    chat := update.Message.Chat
    if chat.Type == "private" {
        return nil  // 私聊不处理
    }

    profile := GroupProfile{
        GroupID:         chat.ID,
        Title:           chat.Title,
        Type:            chat.Type,
        MemberCount:     chat.MembersCount,
        LastInteraction: time.Now(),
    }

    _, err := s.db.Exec(`
        INSERT INTO group_profiles (group_id, title, type, member_count, last_interaction, updated_at)
        VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
        ON CONFLICT(group_id) DO UPDATE SET
            title = excluded.title,
            type = excluded.type,
            member_count = excluded.member_count,
            last_interaction = excluded.last_interaction,
            updated_at = CURRENT_TIMESTAMP
    `, profile.GroupID, profile.Title, profile.Type, profile.MemberCount, profile.LastInteraction)

    if err == nil {
        s.cache.Store(profile.GroupID, profile)
    }

    return err
}

机器人功能管理服务

动态指令注册系统:

type BotCommand struct {
    ID          int
    Command     string   // 指令名称(如 "notify")
    Description string   // 指令描述
    HandlerName string   // 处理器标识
    IsEnabled   bool     // 是否启用
    Scope       string   // user/group/all
}

type CommandService struct {
    db           *sql.DB
    bot          *tgbotapi.BotAPI
    handlers     map[string]CommandHandler  // 处理器注册表
    registeredCmd sync.Map                  // 已注册指令缓存
    logger       *zap.Logger
}

type CommandHandler interface {
    Handle(update tgbotapi.Update) error
    Description() string
}

// 初始化预定义指令
func (s *CommandService) RegisterBuiltinCommands() error {
    builtinCommands := []BotCommand{
        {Command: "start", Description: "开始使用机器人", HandlerName: "StartHandler", IsEnabled: true, Scope: "all"},
        {Command: "help", Description: "查看帮助信息", HandlerName: "HelpHandler", IsEnabled: true, Scope: "all"},
        {Command: "notify", Description: "创建定时提醒", HandlerName: "NotifyHandler", IsEnabled: true, Scope: "user"},
        {Command: "notify_list", Description: "查看提醒列表", HandlerName: "NotifyListHandler", IsEnabled: true, Scope: "user"},
    }

    for _, cmd := range builtinCommands {
        _, err := s.db.Exec(`
            INSERT INTO bot_commands (command, description, handler_name, is_enabled, scope)
            VALUES (?, ?, ?, ?, ?)
            ON CONFLICT(command) DO NOTHING
        `, cmd.Command, cmd.Description, cmd.HandlerName, cmd.IsEnabled, cmd.Scope)

        if err != nil {
            return err
        }
    }

    return s.SyncToTelegram()
}

// 同步指令到Telegram
func (s *CommandService) SyncToTelegram() error {
    rows, err := s.db.Query(`
        SELECT command, description
        FROM bot_commands
        WHERE is_enabled = 1
    `)
    if err != nil {
        return err
    }
    defer rows.Close()

    var commands []tgbotapi.BotCommand
    for rows.Next() {
        var cmd, desc string
        rows.Scan(&cmd, &desc)
        commands = append(commands, tgbotapi.BotCommand{
            Command:     cmd,
            Description: desc,
        })
    }

    // 设置Bot命令列表
    cfg := tgbotapi.NewSetMyCommands(commands...)
    _, err = s.bot.Request(cfg)
    return err
}

// 注册指令处理器
func (s *CommandService) RegisterHandler(handlerName string, handler CommandHandler) {
    s.handlers[handlerName] = handler
}

// 分发指令
func (s *CommandService) DispatchCommand(update tgbotapi.Update) error {
    if !update.Message.IsCommand() {
        return nil
    }

    command := update.Message.Command()

    // 查询指令配置
    var handlerName string
    var isEnabled bool
    err := s.db.QueryRow(`
        SELECT handler_name, is_enabled
        FROM bot_commands
        WHERE command = ?
    `, command).Scan(&handlerName, &isEnabled)

    if err != nil || !isEnabled {
        return fmt.Errorf("command not found or disabled: %s", command)
    }

    // 执行处理器
    handler, exists := s.handlers[handlerName]
    if !exists {
        return fmt.Errorf("handler not registered: %s", handlerName)
    }

    return handler.Handle(update)
}

// API: 添加新指令
func (s *CommandService) AddCommand(cmd BotCommand) error {
    _, err := s.db.Exec(`
        INSERT INTO bot_commands (command, description, handler_name, is_enabled, scope)
        VALUES (?, ?, ?, ?, ?)
    `, cmd.Command, cmd.Description, cmd.HandlerName, cmd.IsEnabled, cmd.Scope)

    if err == nil {
        s.SyncToTelegram()
    }
    return err
}

// API: 更新指令
func (s *CommandService) UpdateCommand(command string, updates map[string]interface{}) error {
    // 动态构建UPDATE语句
    setParts := []string{}
    args := []interface{}{}

    for key, value := range updates {
        setParts = append(setParts, fmt.Sprintf("%s = ?", key))
        args = append(args, value)
    }

    args = append(args, command)

    _, err := s.db.Exec(fmt.Sprintf(`
        UPDATE bot_commands
        SET %s, updated_at = CURRENT_TIMESTAMP
        WHERE command = ?
    `, strings.Join(setParts, ", ")), args...)

    if err == nil {
        s.SyncToTelegram()
    }
    return err
}

// API: 删除指令
func (s *CommandService) DeleteCommand(command string) error {
    _, err := s.db.Exec(`DELETE FROM bot_commands WHERE command = ?`, command)
    if err == nil {
        s.SyncToTelegram()
    }
    return err
}

// API: 列出所有指令
func (s *CommandService) ListCommands() ([]BotCommand, error) {
    rows, err := s.db.Query(`
        SELECT id, command, description, handler_name, is_enabled, scope
        FROM bot_commands
        ORDER BY command
    `)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var commands []BotCommand
    for rows.Next() {
        var cmd BotCommand
        rows.Scan(&cmd.ID, &cmd.Command, &cmd.Description, &cmd.HandlerName, &cmd.IsEnabled, &cmd.Scope)
        commands = append(commands, cmd)
    }

    return commands, nil
}

定时提醒系统

状态机设计

stateDiagram-v2
    [*] --> 等待输入: /notify命令
    等待输入 --> 选择日期: 点击日期按钮
    选择日期 --> 选择时间: 确认日期
    选择时间 --> 配置重复: 确认时间
    配置重复 --> 输入内容: 选择重复规则
    输入内容 --> 确认创建: 输入提醒文本
    确认创建 --> 已激活: 提交成功
    已激活 --> 已触发: 到达触发时间
    已触发 --> 已激活: 有重复规则
    已触发 --> 已完成: 无重复规则
    已激活 --> 已删除: 用户删除
    已完成 --> [*]
    已删除 --> [*]

日历选择器UI实现

InlineKeyboard布局:

func GenerateCalendar(year int, month time.Month) [][]tgbotapi.InlineKeyboardButton {
    firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
    lastDay := firstDay.AddDate(0, 1, -1)

    keyboard := [][]tgbotapi.InlineKeyboardButton{
        // 月份导航行
        {
            tgbotapi.NewInlineKeyboardButtonData("◀", fmt.Sprintf("cal_prev_%d_%d", year, month)),
            tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d年%d月", year, month), "cal_ignore"),
            tgbotapi.NewInlineKeyboardButtonData("▶", fmt.Sprintf("cal_next_%d_%d", year, month)),
        },
        // 星期标题行
        {
            tgbotapi.NewInlineKeyboardButtonData("日", "cal_ignore"),
            tgbotapi.NewInlineKeyboardButtonData("一", "cal_ignore"),
            tgbotapi.NewInlineKeyboardButtonData("二", "cal_ignore"),
            tgbotapi.NewInlineKeyboardButtonData("三", "cal_ignore"),
            tgbotapi.NewInlineKeyboardButtonData("四", "cal_ignore"),
            tgbotapi.NewInlineKeyboardButtonData("五", "cal_ignore"),
            tgbotapi.NewInlineKeyboardButtonData("六", "cal_ignore"),
        },
    }

    // 日期网格生成
    currentWeek := []tgbotapi.InlineKeyboardButton{}
    for weekday := 0; weekday < int(firstDay.Weekday()); weekday++ {
        currentWeek = append(currentWeek, tgbotapi.NewInlineKeyboardButtonData(" ", "cal_ignore"))
    }

    for day := 1; day <= lastDay.Day(); day++ {
        date := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
        callback := fmt.Sprintf("cal_select_%s", date.Format("2006-01-02"))

        // 过期日期禁用
        label := fmt.Sprintf("%d", day)
        if date.Before(time.Now().Truncate(24 * time.Hour)) {
            callback = "cal_ignore"
            label = "×"
        }

        currentWeek = append(currentWeek, tgbotapi.NewInlineKeyboardButtonData(label, callback))

        // 每行7天
        if len(currentWeek) == 7 {
            keyboard = append(keyboard, currentWeek)
            currentWeek = []tgbotapi.InlineKeyboardButton{}
        }
    }

    if len(currentWeek) > 0 {
        keyboard = append(keyboard, currentWeek)
    }

    return keyboard
}

提醒调度器实现

定时扫描机制:

type ReminderScheduler struct {
    db       *sql.DB
    bot      *tgbotapi.BotAPI
    ticker   *time.Ticker
    stopChan chan struct{}
}

func (rs *ReminderScheduler) Start() {
    rs.ticker = time.NewTicker(30 * time.Second)

    go func() {
        for {
            select {
            case <-rs.ticker.C:
                rs.processPendingReminders()
            case <-rs.stopChan:
                return
            }
        }
    }()
}

func (rs *ReminderScheduler) processPendingReminders() {
    // 查询需触发的提醒(未来5分钟内)
    rows, _ := rs.db.Query(`
        SELECT id, chat_id, user_id, content, repeat_rule, next_trigger
        FROM reminders
        WHERE status = 1
          AND next_trigger <= datetime('now', '+5 minutes')
        ORDER BY next_trigger
    `)
    defer rows.Close()

    for rows.Next() {
        var r Reminder
        rows.Scan(&r.ID, &r.ChatID, &r.UserID, &r.Content, &r.RepeatRule, &r.NextTrigger)

        // 等待到精确触发时间
        wait := r.NextTrigger.Sub(time.Now())
        if wait > 0 {
            time.Sleep(wait)
        }

        // 发送提醒消息
        msg := tgbotapi.NewMessage(r.ChatID, fmt.Sprintf("⏰ 提醒\\n\\n%s", r.Content))
        rs.bot.Send(msg)

        // 更新下次触发时间
        if r.RepeatRule != "" {
            nextTrigger := calculateNextTrigger(r.NextTrigger, r.RepeatRule)
            rs.db.Exec(`UPDATE reminders SET next_trigger = ? WHERE id = ?`, nextTrigger, r.ID)
        } else {
            rs.db.Exec(`UPDATE reminders SET status = 0 WHERE id = ?`, r.ID)
        }
    }
}

func calculateNextTrigger(current time.Time, rule string) time.Time {
    switch rule {
    case "daily":
        return current.AddDate(0, 0, 1)
    case "weekly":
        return current.AddDate(0, 0, 7)
    case "biweekly":
        return current.AddDate(0, 0, 14)
    case "monthly":
        return current.AddDate(0, 1, 0)
    default:
        // 解析RRULE格式
        return parseRRule(current, rule)
    }
}

AI智能体系统

触发机制 [更新]

双模式触发判断:

graph TB
    A[接收Telegram消息] --> B{聊天类型判断}
    B -->|私聊private| C[直接触发AI]
    B -->|群聊group/supergroup| D{检查@机器人}
    D -->|是| C
    D -->|否| E[忽略消息]
    C --> F[提取上下文]
    F --> G[调用AI API]
    G --> H[流式响应处理]
    H --> I[发送回复]

触发逻辑实现:

type AITriggerService struct {
    bot      *tgbotapi.BotAPI
    botUsername string
    aiService *AIService
}

func (s *AITriggerService) ShouldRespond(update tgbotapi.Update) bool {
    msg := update.Message
    if msg == nil {
        return false
    }

    // 私聊模式: 所有消息都响应
    if msg.Chat.IsPrivate() {
        return true
    }

    // 群聊模式: 检查是否@了机器人
    if msg.Chat.IsGroup() || msg.Chat.IsSuperGroup() {
        // 方式1: 检查Entities中的mention
        for _, entity := range msg.Entities {
            if entity.Type == "mention" {
                mention := msg.Text[entity.Offset:entity.Offset+entity.Length]
                if mention == "@"+s.botUsername {
                    return true
                }
            }
        }

        // 方式2: 检查命令是否针对本Bot
        if msg.IsCommand() {
            parts := strings.Split(msg.Command(), "@")
            if len(parts) == 2 && parts[1] == s.botUsername {
                return true
            }
            if len(parts) == 1 {
                return true  // 无@的命令也响应
            }
        }
    }

    return false
}

func (s *AITriggerService) HandleMessage(update tgbotapi.Update) error {
    if !s.ShouldRespond(update) {
        return nil
    }

    // 移除@机器人的部分
    cleanText := strings.ReplaceAll(update.Message.Text, "@"+s.botUsername, "")

    // 构建上下文并调用AI
    return s.aiService.ProcessMessage(update.Message.Chat.ID, update.Message.From.ID, cleanText)
}

AI集成实现 [更新]

OpenRouter优先方案:

graph LR
    A[AI请求] --> B[AIService]
    B --> C{提供商选择}
    C -->|首选| D[OpenRouterProvider]
    C -->|备选| E[其他Provider]
    D --> F[OpenAI Go SDK]
    F --> G[OpenRouter API]
    G --> H[Claude/Gemini/GPT]
    H --> I[流式响应]
    I --> J[AIMessageSender]
    J --> K[Telegram Bot API]

OpenRouter适配器实现:

import (
    "github.com/sashabaranov/go-openai"
)

type OpenRouterProvider struct {
    client *openai.Client
    model  string
    logger *zap.Logger
}

func NewOpenRouterProvider(apiKey string, model string) *OpenRouterProvider {
    config := openai.DefaultConfig(apiKey)
    config.BaseURL = "https://openrouter.ai/api/v1"

    // 可选: 添加自定义Headers
    config.HTTPClient = &http.Client{
        Transport: &customTransport{
            base: http.DefaultTransport,
            headers: map[string]string{
                "HTTP-Referer": "https://github.com/your-repo",  // 可选
                "X-Title": "NaughtyMan Bot",                      // 可选
            },
        },
    }

    return &OpenRouterProvider{
        client: openai.NewClientWithConfig(config),
        model:  model,
        logger: zap.L(),
    }
}

func (p *OpenRouterProvider) Chat(ctx context.Context, messages []ChatMessage, stream bool) (<-chan string, error) {
    // 转换消息格式
    openaiMessages := make([]openai.ChatCompletionMessage, len(messages))
    for i, msg := range messages {
        openaiMessages[i] = openai.ChatCompletionMessage{
            Role:    msg.Role,
            Content: msg.Content,
            Name:    msg.Name,
        }
    }

    if stream {
        return p.streamChat(ctx, openaiMessages)
    }
    return p.blockingChat(ctx, openaiMessages)
}

func (p *OpenRouterProvider) streamChat(ctx context.Context, messages []openai.ChatCompletionMessage) (<-chan string, error) {
    ch := make(chan string, 10)

    req := openai.ChatCompletionRequest{
        Model:    p.model,
        Messages: messages,
        Stream:   true,
    }

    stream, err := p.client.CreateChatCompletionStream(ctx, req)
    if err != nil {
        return nil, err
    }

    go func() {
        defer close(ch)
        defer stream.Close()

        for {
            response, err := stream.Recv()
            if errors.Is(err, io.EOF) {
                break
            }
            if err != nil {
                p.logger.Error("Stream error", zap.Error(err))
                break
            }

            if len(response.Choices) > 0 {
                content := response.Choices[^0].Delta.Content
                if content != "" {
                    ch <- content
                }
            }
        }
    }()

    return ch, nil
}

func (p *OpenRouterProvider) blockingChat(ctx context.Context, messages []openai.ChatCompletionMessage) (<-chan string, error) {
    ch := make(chan string, 1)

    req := openai.ChatCompletionRequest{
        Model:    p.model,
        Messages: messages,
        Stream:   false,
    }

    resp, err := p.client.CreateChatCompletion(ctx, req)
    if err != nil {
        return nil, err
    }

    go func() {
        defer close(ch)
        if len(resp.Choices) > 0 {
            ch <- resp.Choices[^0].Message.Content
        }
    }()

    return ch, nil
}

func (p *OpenRouterProvider) Name() string {
    return "OpenRouter"
}

func (p *OpenRouterProvider) SupportStream() bool {
    return true
}

func (p *OpenRouterProvider) SupportReasoning() bool {
    // 取决于使用的模型
    return strings.Contains(p.model, "claude-3.5") || strings.Contains(p.model, "gpt-4")
}

推荐模型配置:

var RecommendedModels = map[string]string{
    "reasoning": "anthropic/claude-3.5-sonnet",        // 思考链最佳
    "long_context": "google/gemini-pro-1.5",          // 长上下文
    "balanced": "openai/gpt-4-turbo",                 // 平衡性能
    "fast": "anthropic/claude-3-haiku",               // 快速响应
}

上下文管理

消息引用策略:

type ContextManager struct {
    db    *sql.DB
    cache *sync.Map  // chatID -> []Message
}

func (cm *ContextManager) BuildContext(chatID int64, currentMessage string, userID int64) ([]ChatMessage, error) {
    // 1. 查询最近3条历史消息
    rows, err := cm.db.Query(`
        SELECT user_id, user_message, ai_response, created_at
        FROM ai_conversations
        WHERE chat_id = ?
        ORDER BY created_at DESC
        LIMIT 3
    `, chatID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var history []struct {
        UserID     int64
        UserMsg    string
        AIResponse string
        CreatedAt  time.Time
    }

    for rows.Next() {
        var h struct {
            UserID     int64
            UserMsg    string
            AIResponse string
            CreatedAt  time.Time
        }
        rows.Scan(&h.UserID, &h.UserMsg, &h.AIResponse, &h.CreatedAt)
        history = append(history, h)
    }

    // 2. 构建消息列表(倒序排列以保持时间顺序)
    messages := []ChatMessage{
        {
            Role:    "system",
            Content: "你是一个友好的Telegram群助手,请用简洁专业的语言回答问题。",
        },
    }

    // 历史消息按时间正序添加
    for i := len(history) - 1; i >= 0; i-- {
        messages = append(messages,
            ChatMessage{
                Role:    "user",
                Content: history[i].UserMsg,
                Name:    fmt.Sprintf("user%d", history[i].UserID),
            },
            ChatMessage{
                Role:    "assistant",
                Content: history[i].AIResponse,
            },
        )
    }

    // 3. 添加当前消息
    messages = append(messages, ChatMessage{
        Role:    "user",
        Content: currentMessage,
        Name:    fmt.Sprintf("user%d", userID),
    })

    return messages, nil
}

流式响应处理

分段更新策略:

type StreamHandler struct {
    aiSender      *AIMessageSender
    updateBuffer  strings.Builder
    lastUpdate    time.Time
    messageID     int
    chatID        int64
    startTime     time.Time
}

func (sh *StreamHandler) ProcessStream(ctx context.Context, stream <-chan string) error {
    // 1. 发送初始消息
    initialMsg := tgbotapi.NewMessage(sh.chatID, "🤔 正在思考...")
    sent, err := sh.aiSender.baseSender.bot.Send(initialMsg)
    if err != nil {
        return err
    }

    sh.messageID = sent.MessageID
    sh.lastUpdate = time.Now()
    sh.startTime = time.Now()

    // 2. 处理流式分片
    for {
        select {
        case chunk, ok := <-stream:
            if !ok {
                // 流结束,发送最终消息
                duration := time.Since(sh.startTime)
                finalContent := sh.updateBuffer.String() + fmt.Sprintf("\n\n✅ 回答完成 | 用时%.1fs", duration.Seconds())
                sh.sendUpdate(finalContent)
                return nil
            }

            sh.updateBuffer.WriteString(chunk)

            // 触发更新条件: 500字符 OR 5秒间隔
            shouldUpdate := sh.updateBuffer.Len() >= 500 || time.Since(sh.lastUpdate) >= 5*time.Second

            if shouldUpdate {
                sh.sendUpdate(sh.updateBuffer.String())
            }

        case <-ctx.Done():
            return ctx.Err()
        }
    }
}

func (sh *StreamHandler) sendUpdate(content string) {
    // 使用AI消息发送器的流式更新方法
    err := sh.aiSender.SendStreamUpdate(sh.chatID, sh.messageID, content)
    if err != nil {
        sh.aiSender.logger.Warn("Failed to update message", zap.Error(err))
    } else {
        sh.lastUpdate = time.Now()
    }
}

AI交互日志记录 [新增]

日志记录服务:

type AIInteractionLogger struct {
    db            *sql.DB
    userService   *UserProfileService
    groupService  *GroupProfileService
    logger        *zap.Logger
}

type AIInteractionLog struct {
    Timestamp    time.Time
    UserID       int64
    Username     string
    PhoneNumber  string
    GroupID      int64
    GroupName    string
    MessageText  string
    AIResponse   string
    Duration     time.Duration
    Provider     string
    Model        string
    TokensUsed   int
}

func (l *AIInteractionLogger) LogInteraction(log AIInteractionLog) error {
    // 1. 获取用户详细信息
    userProfile, err := l.userService.GetByUserID(log.UserID)
    if err == nil {
        log.Username = userProfile.Username
        log.PhoneNumber = userProfile.PhoneNumber
    }

    // 2. 获取群组信息(如果是群聊)
    if log.GroupID != 0 {
        groupProfile, err := l.groupService.GetByGroupID(log.GroupID)
        if err == nil {
            log.GroupName = groupProfile.Title
        }
    }

    // 3. 写入数据库
    _, err = l.db.Exec(`
        INSERT INTO ai_interaction_log (
            user_id, username, phone_number, group_id, group_name,
            message_text, ai_response, duration_ms, provider, model, tokens_used, created_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, log.UserID, log.Username, log.PhoneNumber, log.GroupID, log.GroupName,
        log.MessageText, log.AIResponse, log.Duration.Milliseconds(),
        log.Provider, log.Model, log.TokensUsed, log.Timestamp)

    // 4. 输出结构化日志
    l.logger.Info("AI Interaction",
        zap.Int64("user_id", log.UserID),
        zap.String("username", log.Username),
        zap.String("phone", log.PhoneNumber),
        zap.Int64("group_id", log.GroupID),
        zap.String("group_name", log.GroupName),
        zap.String("message", log.MessageText),
        zap.Duration("duration", log.Duration),
        zap.String("provider", log.Provider),
        zap.String("model", log.Model),
    )

    return err
}

// 集成到AI服务中
func (s *AIService) ProcessMessage(chatID int64, userID int64, message string) error {
    startTime := time.Now()

    // 构建上下文
    context, _ := s.contextManager.BuildContext(chatID, message, userID)

    // 调用AI
    stream, err := s.provider.Chat(s.ctx, context, true)
    if err != nil {
        return err
    }

    // 处理流式响应
    handler := &StreamHandler{
        aiSender: s.aiSender,
        chatID:   chatID,
    }

    var responseBuilder strings.Builder
    for chunk := range stream {
        responseBuilder.WriteString(chunk)
        handler.updateBuffer.WriteString(chunk)
        // ... 更新逻辑
    }

    duration := time.Since(startTime)
    response := responseBuilder.String()

    // 记录交互日志
    s.interactionLogger.LogInteraction(AIInteractionLog{
        Timestamp:   startTime,
        UserID:      userID,
        GroupID:     chatID,
        MessageText: message,
        AIResponse:  response,
        Duration:    duration,
        Provider:    s.provider.Name(),
        Model:       s.config.Model,
    })

    return nil
}

数据库设计

ER图 [更新]

erDiagram
    API_CREDENTIALS ||--o{ TOKEN_SESSIONS : generates
    API_CREDENTIALS ||--o{ MESSAGE_LOG : sends
    WHITELIST ||--o{ API_CREDENTIALS : authorizes
    REMINDERS }o--|| USER_PROFILES : belongs_to
    USER_PROFILES ||--o{ AI_CONVERSATIONS : participates
    GROUP_PROFILES ||--o{ AI_CONVERSATIONS : contains
    BOT_COMMANDS }o--|| USER_PROFILES : registered_by

    API_CREDENTIALS {
        int id PK
        string api_key UK
        string api_secret
        string project
        json whitelist_ids
        int status
        datetime created_at
    }

    TOKEN_SESSIONS {
        int id PK
        string api_key FK
        string challenge
        datetime challenge_expire
        string access_token UK
        string refresh_token UK
        datetime token_expire
        string client_ip
    }

    WHITELIST {
        int id PK
        string type
        bigint entity_id UK
        string alias
        int status
        datetime added_at
    }

    MESSAGE_LOG {
        int id PK
        string message_id
        bigint chat_id
        string level
        string project FK
        text content
        string status
        int retry_count
        datetime sent_at
    }

    REMINDERS {
        int id PK
        bigint user_id FK
        bigint chat_id
        text content
        datetime trigger_time
        string repeat_rule
        datetime next_trigger
        int status
    }

    USER_PROFILES {
        int id PK
        bigint user_id UK
        string username
        string first_name
        string last_name
        string phone_number
        string language_code
        int is_bot
        datetime last_interaction
    }

    GROUP_PROFILES {
        int id PK
        bigint group_id UK
        string title
        string type
        int member_count
        datetime last_interaction
    }

    BOT_COMMANDS {
        int id PK
        string command UK
        string description
        string handler_name
        int is_enabled
        string scope
    }

    AI_CONVERSATIONS {
        int id PK
        bigint chat_id
        bigint user_id FK
        text user_message
        text ai_response
        string provider
        string model
        int tokens_used
        datetime created_at
    }

    AI_INTERACTION_LOG {
        int id PK
        bigint user_id FK
        string username
        string phone_number
        bigint group_id
        string group_name
        text message_text
        text ai_response
        int duration_ms
        string provider
        string model
        int tokens_used
        datetime created_at
    }

核心表DDL [更新]

-- 用户档案表
CREATE TABLE user_profiles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id BIGINT UNIQUE NOT NULL,
    username TEXT,
    first_name TEXT,
    last_name TEXT,
    phone_number TEXT,
    language_code TEXT,
    is_bot INTEGER DEFAULT 0,
    last_interaction DATETIME,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_last_interaction (last_interaction)
);

-- 群组档案表
CREATE TABLE group_profiles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    group_id BIGINT UNIQUE NOT NULL,
    title TEXT,
    type TEXT,  -- group, supergroup, channel
    member_count INTEGER,
    last_interaction DATETIME,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_group_id (group_id),
    INDEX idx_last_interaction (last_interaction)
);

-- 机器人指令表
CREATE TABLE bot_commands (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    command TEXT UNIQUE NOT NULL,
    description TEXT,
    handler_name TEXT NOT NULL,
    is_enabled INTEGER DEFAULT 1,
    scope TEXT DEFAULT 'all',  -- user/group/all
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_command (command),
    INDEX idx_enabled (is_enabled)
);

-- AI交互日志表
CREATE TABLE ai_interaction_log (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id BIGINT NOT NULL,
    username TEXT,
    phone_number TEXT,
    group_id BIGINT,
    group_name TEXT,
    message_text TEXT,
    ai_response TEXT,
    duration_ms INTEGER,
    provider TEXT,
    model TEXT,
    tokens_used INTEGER,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_group_id (group_id),
    INDEX idx_created_at (created_at)
);

-- 消息日志表优化索引
CREATE INDEX idx_message_log_composite ON message_log(status, created_at);
CREATE INDEX idx_message_log_chat ON message_log(chat_id, sent_at DESC);

-- 提醒表优化索引
CREATE INDEX idx_reminders_trigger ON reminders(next_trigger, status) WHERE status = 1;
CREATE INDEX idx_reminders_user ON reminders(user_id, status);

-- Token会话表优化索引
CREATE INDEX idx_token_expire ON token_sessions(token_expire) WHERE token_expire > datetime('now');

-- AI对话表优化索引
CREATE INDEX idx_ai_conversations_chat ON ai_conversations(chat_id, created_at DESC);

接口设计

RESTful API规范

管理API接口 [新增]

1. 用户管理接口

GET /api/v1/admin/users
Authorization: Bearer {admin_token}

响应:
{
  "users": [
    {
      "user_id": 123456789,
      "username": "john_doe",
      "first_name": "John",
      "last_name": "Doe",
      "phone_number": "+1234567890",
      "language_code": "zh-CN",
      "last_interaction": "2025-10-24T15:30:00Z"
    }
  ],
  "total": 42
}
GET /api/v1/admin/user/:id
Authorization: Bearer {admin_token}

响应:
{
  "user_id": 123456789,
  "username": "john_doe",
  "first_name": "John",
  "last_name": "Doe",
  "phone_number": "+1234567890",
  "language_code": "zh-CN",
  "is_bot": false,
  "last_interaction": "2025-10-24T15:30:00Z",
  "created_at": "2025-10-01T08:00:00Z",
  "interaction_count": 156
}

2. 群组管理接口

GET /api/v1/admin/groups
Authorization: Bearer {admin_token}

响应:
{
  "groups": [
    {
      "group_id": -987654321,
      "title": "开发团队讨论组",
      "type": "supergroup",
      "member_count": 25,
      "last_interaction": "2025-10-24T16:00:00Z"
    }
  ],
  "total": 8
}

3. 指令管理接口

GET /api/v1/admin/commands
Authorization: Bearer {admin_token}

响应:
{
  "commands": [
    {
      "id": 1,
      "command": "notify",
      "description": "创建定时提醒",
      "handler_name": "NotifyHandler",
      "is_enabled": true,
      "scope": "user"
    }
  ]
}
POST /api/v1/admin/commands
Authorization: Bearer {admin_token}
Content-Type: application/json

{
  "command": "custom_cmd",
  "description": "自定义指令",
  "handler_name": "CustomHandler",
  "is_enabled": true,
  "scope": "all"
}

响应:
{
  "id": 10,
  "message": "指令创建成功"
}
PUT /api/v1/admin/commands/:cmd
Authorization: Bearer {admin_token}
Content-Type: application/json

{
  "description": "更新后的描述",
  "is_enabled": false
}

响应:
{
  "message": "指令更新成功"
}
DELETE /api/v1/admin/commands/:cmd
Authorization: Bearer {admin_token}

响应:
{
  "message": "指令删除成功"
}
POST /api/v1/admin/commands/sync
Authorization: Bearer {admin_token}

响应:
{
  "message": "指令已同步到Telegram",
  "synced_count": 8
}

消息发送接口 [更新]

1. 模板消息发送

POST /api/v1/message/send
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "target_type": "user|group",
  "target_id": "123456789",
  "level": "info|warning|critical",
  "project": "octopus|tonystack",
  "content": {
    "title": "服务器CPU告警",
    "body": "服务器CPU使用率达85%",
    "metadata": {
      "server": "prod-web-01",
      "timestamp": "2025-10-21T10:23:00Z"
    }
  }
}

响应:
{
  "message_id": "msg_uuid_12345",
  "status": "queued",
  "scheduled_at": "2025-10-21T10:23:00Z"
}

2. 普通消息发送 [新增]

POST /api/v1/message/send-plain
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "target_type": "user|group",
  "target_id": "123456789",
  "text": "这是一条普通文本消息",
  "options": {
    "disable_preview": true
  }
}

响应:
{
  "message_id": "123",
  "status": "sent",
  "sent_at": "2025-10-24T15:45:00Z"
}

3. Markdown消息发送 [新增]

POST /api/v1/message/send-markdown
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "target_type": "user|group",
  "target_id": "123456789",
  "markdown": "**粗体** *斜体* `代码`\n\n- 列表项1\n- 列表项2"
}

响应:
{
  "message_id": "124",
  "status": "sent",
  "sent_at": "2025-10-24T15:46:00Z"
}

项目实施

实施计划 [更新]

阶段一:基础设施 (2周)

  • 速率限制器实现与测试
  • 数据库表结构设计(包含管理表)
  • 白名单+Token认证系统
  • 代理支持与连接测试

阶段二:核心功能 (3周)

  • 消息通知API开发
  • 分级模板渲染引擎
  • 消息发送器包装层实现
  • 消息队列与重试机制
  • 定时提醒CRUD功能

阶段三:管理功能 (2周) [新增]

  • 用户档案采集与存储
  • 群组信息管理服务
  • 动态指令注册系统
  • 管理API接口开发
  • 预定义功能自动注册

阶段四:AI功能 (3周)

  • OpenRouter API集成(使用OpenAI SDK)
  • 私聊/群聊双模式触发
  • 上下文管理器实现
  • 流式响应处理
  • AI交互日志记录
  • InlineKeyboard交互流程

阶段五:上线准备 (1周)

  • 集成测试与修复
  • 性能压测与优化
  • 文档编写
  • 监控告警配置
  • 灰度发布

团队组织

角色 职责 人数
技术负责人 架构设计审核、技术选型决策、风险评估 1
后端工程师 核心业务逻辑开发、API接口实现 2
AI集成工程师 OpenRouter适配、流式响应处理 1
测试工程师 单元测试、集成测试、压力测试 1
运维工程师 部署脚本、监控配置、应急响应 1

交付物清单

  • 源代码仓库(Git)
  • 数据库迁移脚本
  • API接口文档(OpenAPI 3.0)
  • 部署操作手册
  • 监控告警配置文件
  • 性能测试报告
  • 用户使用指南

附录

术语表

术语 英文 说明
令牌桶 Token Bucket 速率限制算法,以固定速率补充令牌实现流控
死信队列 Dead Letter Queue 存储多次失败消息的特殊队列
指数退避 Exponential Backoff 重试间隔呈指数增长的策略
WAL模式 Write-Ahead Logging SQLite日志模式,提升并发写入性能
RRULE Recurrence Rule iCalendar重复规则标准(RFC 5545)

参考资料


文档变更记录

v2.0 (2025-10-24更新)

主要变更:

  1. 新增消息发送器包装层(PlainMessageSender和AIMessageSender)
  2. 新增Telegram管理功能模块(用户群组管理、机器人功能管理)
  3. 更新AI触发机制为私聊全响应+群聊@模式
  4. 更新AI实现方案为OpenRouter优先(使用OpenAI Go SDK)
  5. 新增AI交互日志记录系统
  6. 更新数据库ER图和表结构
  7. 新增管理API接口文档
  8. 调整实施计划,新增管理功能阶段
  9. 术语规范: "机器人交互功能" -> "机器人预定义功能"

文档编制完成 | 总计约1.8万字 | 包含11个Mermaid图表 | 覆盖15个核心设计模块