113 lines
2.8 KiB
Go
113 lines
2.8 KiB
Go
package feishu
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"ops-assistant/internal/channel"
|
|
"ops-assistant/internal/core/ops"
|
|
"ops-assistant/models"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// DefaultUserID 统一用户ID
|
|
const DefaultUserID int64 = 1
|
|
|
|
type Bot struct {
|
|
db *gorm.DB
|
|
opsSvc *ops.Service
|
|
appID string
|
|
appSecret string
|
|
verificationToken string
|
|
encryptKey string
|
|
}
|
|
|
|
func NewBot(db *gorm.DB, opsSvc *ops.Service, appID, appSecret, verificationToken, encryptKey string) *Bot {
|
|
return &Bot{
|
|
db: db,
|
|
opsSvc: opsSvc,
|
|
appID: appID,
|
|
appSecret: appSecret,
|
|
verificationToken: verificationToken,
|
|
encryptKey: encryptKey,
|
|
}
|
|
}
|
|
|
|
func (b *Bot) Start(ctx context.Context) {
|
|
log.Printf("🚀 Feishu Bot 已启用 app_id=%s", maskID(b.appID))
|
|
<-ctx.Done()
|
|
log.Printf("⏳ Feishu Bot 已停止")
|
|
}
|
|
|
|
func (b *Bot) RegisterRoutes(r *gin.Engine) {
|
|
r.POST("/webhook/feishu", b.handleWebhook)
|
|
}
|
|
|
|
func (b *Bot) handleWebhook(c *gin.Context) {
|
|
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20))
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
|
|
// 统一走 channel 包解析,便于后续扩展验签/解密
|
|
msg, verifyChallenge, err := channel.ParseFeishuInbound(body, b.verificationToken)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if verifyChallenge != "" {
|
|
c.JSON(200, gin.H{"challenge": verifyChallenge})
|
|
return
|
|
}
|
|
if msg == nil {
|
|
c.JSON(200, gin.H{"code": 0})
|
|
return
|
|
}
|
|
|
|
// 幂等去重
|
|
var existed models.MessageDedup
|
|
if err := b.db.Where("platform = ? AND event_id = ?", "feishu", msg.EventID).First(&existed).Error; err == nil {
|
|
c.JSON(200, gin.H{"code": 0})
|
|
return
|
|
}
|
|
_ = b.db.Create(&models.MessageDedup{Platform: "feishu", EventID: msg.EventID, ProcessedAt: time.Now()}).Error
|
|
|
|
reply := b.handleText(msg.Text)
|
|
if reply != "" && msg.UserID != "" {
|
|
tk, err := channel.GetFeishuTenantToken(c.Request.Context(), b.appID, b.appSecret)
|
|
if err == nil {
|
|
_ = channel.SendFeishuText(c.Request.Context(), tk, msg.UserID, reply)
|
|
}
|
|
}
|
|
|
|
c.JSON(200, gin.H{"code": 0})
|
|
}
|
|
|
|
func (b *Bot) handleText(text string) string {
|
|
trim := strings.TrimSpace(text)
|
|
if b.opsSvc != nil {
|
|
if handled, out := b.opsSvc.Handle(DefaultUserID, trim); handled {
|
|
return out
|
|
}
|
|
}
|
|
switch trim {
|
|
case "帮助", "help", "/help", "菜单", "功能", "/start":
|
|
return "🛠️ Ops-Assistant\n\n模块化运维命令助手,支持:\n• /cpa - CPA 管理\n• /cf - Cloudflare 管理\n• /cfdav - cfdav 管理\n• /sub - 订阅管理\n• /mail - 邮箱管理"
|
|
default:
|
|
return "❓ 无法识别,请使用 OPS 命令(发送「帮助」查看说明)。"
|
|
}
|
|
}
|
|
|
|
func maskID(s string) string {
|
|
if len(s) <= 6 {
|
|
return "***"
|
|
}
|
|
return s[:3] + "***" + s[len(s)-3:]
|
|
}
|