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 } } }