Files
CyberStrikeAI/internal/robot/slack.go
T
2026-07-06 13:58:12 +08:00

136 lines
3.9 KiB
Go

package robot
import (
"context"
"strings"
"cyberstrike-ai/internal/config"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
)
const (
slackPlatform = "slack"
slackMaxMessageRunes = 3900
)
// StartSlack 启动 Slack Socket Mode(出站 WebSocket,无需公网回调)。
func StartSlack(ctx context.Context, robotsCfg config.RobotsConfig, h MessageHandler, logger *zap.Logger) {
cfg := robotsCfg.Slack
if !cfg.Enabled || strings.TrimSpace(cfg.BotToken) == "" || strings.TrimSpace(cfg.AppToken) == "" {
return
}
go runSlackLoop(ctx, cfg, h, logger)
}
func runSlackLoop(ctx context.Context, cfg config.RobotSlackConfig, h MessageHandler, logger *zap.Logger) {
backoff := reconnectInitial
for {
err := runSlackSocket(ctx, cfg, h, logger)
if ctx.Err() != nil {
logger.Info("Slack Socket Mode 已按配置关闭")
return
}
if err != nil {
logger.Warn("Slack Socket Mode 异常,将自动重连", zap.Error(err), zap.Duration("retry_after", backoff))
}
if !waitReconnect(ctx, &backoff) {
return
}
}
}
func runSlackSocket(ctx context.Context, cfg config.RobotSlackConfig, h MessageHandler, logger *zap.Logger) error {
api := slack.New(
strings.TrimSpace(cfg.BotToken),
slack.OptionAppLevelToken(strings.TrimSpace(cfg.AppToken)),
)
client := socketmode.New(api)
logger.Info("Slack Socket Mode 正在连接…")
go func() {
for evt := range client.Events {
switch evt.Type {
case socketmode.EventTypeEventsAPI:
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok {
continue
}
client.Ack(*evt.Request)
if eventsAPIEvent.Type != slackevents.CallbackEvent {
continue
}
switch ev := eventsAPIEvent.InnerEvent.Data.(type) {
case *slackevents.MessageEvent:
handleSlackMessage(ctx, api, eventsAPIEvent.TeamID, ev, h, logger)
case *slackevents.AppMentionEvent:
handleSlackAppMention(ctx, api, eventsAPIEvent.TeamID, ev, h, logger)
}
case socketmode.EventTypeConnecting:
logger.Info("Slack Socket Mode 正在连接…")
case socketmode.EventTypeConnected:
logger.Info("Slack Socket Mode 已连接,等待收消息")
}
}
}()
return client.RunContext(ctx)
}
func handleSlackMessage(ctx context.Context, api *slack.Client, teamID string, ev *slackevents.MessageEvent, h MessageHandler, logger *zap.Logger) {
if ev == nil || ev.BotID != "" || ev.SubType != "" {
return
}
if ev.ChannelType != "im" {
return
}
text := strings.TrimSpace(ev.Text)
if text == "" {
return
}
userID := slackSessionKey(teamID, ev.User)
logger.Info("Slack 收到消息", zap.String("from", userID), zap.String("content", text))
reply := h.HandleMessage(slackPlatform, userID, text)
slackPostReply(ctx, api, ev.Channel, reply, logger)
}
func handleSlackAppMention(ctx context.Context, api *slack.Client, teamID string, ev *slackevents.AppMentionEvent, h MessageHandler, logger *zap.Logger) {
if ev == nil || ev.BotID != "" {
return
}
text := strings.TrimSpace(ev.Text)
if text == "" {
return
}
userID := slackSessionKey(teamID, ev.User)
logger.Info("Slack 收到 @ 消息", zap.String("from", userID), zap.String("content", text))
reply := h.HandleMessage(slackPlatform, userID, text)
slackPostReply(ctx, api, ev.Channel, reply, logger)
}
func slackSessionKey(teamID, userID string) string {
teamID = strings.TrimSpace(teamID)
userID = strings.TrimSpace(userID)
if teamID == "" {
teamID = "default"
}
return "t:" + teamID + "|u:" + userID
}
func slackPostReply(ctx context.Context, api *slack.Client, channel, reply string, logger *zap.Logger) {
reply = trimReply(reply)
if reply == "" {
return
}
for _, chunk := range splitTextChunks(reply, slackMaxMessageRunes) {
_, _, err := api.PostMessageContext(ctx, channel, slack.MsgOptionText(chunk, false))
if err != nil {
logger.Warn("Slack 发送回复失败", zap.String("channel", channel), zap.Error(err))
return
}
}
}