mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-25 21:31:08 +02:00
Delete robot directory
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
package robot
|
|
||||||
|
|
||||||
// MessageHandler 供飞书/钉钉长连接调用的消息处理接口(由 handler.RobotHandler 实现)
|
|
||||||
type MessageHandler interface {
|
|
||||||
HandleMessage(platform, userID, text string) string
|
|
||||||
}
|
|
||||||
-137
@@ -1,137 +0,0 @@
|
|||||||
package robot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"cyberstrike-ai/internal/config"
|
|
||||||
|
|
||||||
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
|
|
||||||
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
|
|
||||||
dingutils "github.com/open-dingtalk/dingtalk-stream-sdk-go/utils"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dingReconnectInitial = 5 * time.Second // 首次重连间隔
|
|
||||||
dingReconnectMax = 60 * time.Second // 最大重连间隔
|
|
||||||
)
|
|
||||||
|
|
||||||
// StartDing 启动钉钉 Stream 长连接(无需公网),收到消息后调用 handler 并通过 SessionWebhook 回复。
|
|
||||||
// 断线(如笔记本睡眠、网络中断)后会自动重连;ctx 被取消时退出,便于配置变更时重启。
|
|
||||||
func StartDing(ctx context.Context, cfg config.RobotDingtalkConfig, h MessageHandler, logger *zap.Logger) {
|
|
||||||
if !cfg.Enabled || cfg.ClientID == "" || cfg.ClientSecret == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go runDingLoop(ctx, cfg, h, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runDingLoop 循环维持钉钉长连接:断开且 ctx 未取消时按退避间隔重连。
|
|
||||||
func runDingLoop(ctx context.Context, cfg config.RobotDingtalkConfig, h MessageHandler, logger *zap.Logger) {
|
|
||||||
backoff := dingReconnectInitial
|
|
||||||
for {
|
|
||||||
streamClient := client.NewStreamClient(
|
|
||||||
client.WithAppCredential(client.NewAppCredentialConfig(cfg.ClientID, cfg.ClientSecret)),
|
|
||||||
client.WithSubscription(dingutils.SubscriptionTypeKCallback, "/v1.0/im/bot/messages/get",
|
|
||||||
chatbot.NewDefaultChatBotFrameHandler(func(ctx context.Context, msg *chatbot.BotCallbackDataModel) ([]byte, error) {
|
|
||||||
go handleDingMessage(ctx, msg, h, logger)
|
|
||||||
return nil, nil
|
|
||||||
}).OnEventReceived),
|
|
||||||
)
|
|
||||||
logger.Info("钉钉 Stream 正在连接…", zap.String("client_id", cfg.ClientID))
|
|
||||||
err := streamClient.Start(ctx)
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
logger.Info("钉钉 Stream 已按配置重启关闭")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("钉钉 Stream 长连接断开(如睡眠/断网),将自动重连", zap.Error(err), zap.Duration("retry_after", backoff))
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-time.After(backoff):
|
|
||||||
// 下次重连间隔递增,上限 60 秒,避免频繁重试
|
|
||||||
if backoff < dingReconnectMax {
|
|
||||||
backoff *= 2
|
|
||||||
if backoff > dingReconnectMax {
|
|
||||||
backoff = dingReconnectMax
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDingMessage(ctx context.Context, msg *chatbot.BotCallbackDataModel, h MessageHandler, logger *zap.Logger) {
|
|
||||||
if msg == nil || msg.SessionWebhook == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
content := ""
|
|
||||||
if msg.Text.Content != "" {
|
|
||||||
content = strings.TrimSpace(msg.Text.Content)
|
|
||||||
}
|
|
||||||
if content == "" && msg.Msgtype == "richText" {
|
|
||||||
if cMap, ok := msg.Content.(map[string]interface{}); ok {
|
|
||||||
if rich, ok := cMap["richText"].([]interface{}); ok {
|
|
||||||
for _, c := range rich {
|
|
||||||
if m, ok := c.(map[string]interface{}); ok {
|
|
||||||
if txt, ok := m["text"].(string); ok {
|
|
||||||
content = strings.TrimSpace(txt)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if content == "" {
|
|
||||||
logger.Debug("钉钉消息内容为空,已忽略", zap.String("msgtype", msg.Msgtype))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("钉钉收到消息", zap.String("sender", msg.SenderId), zap.String("content", content))
|
|
||||||
userID := msg.SenderId
|
|
||||||
if userID == "" {
|
|
||||||
userID = msg.ConversationId
|
|
||||||
}
|
|
||||||
reply := h.HandleMessage("dingtalk", userID, content)
|
|
||||||
// 使用 markdown 类型以便正确展示标题、列表、代码块等格式
|
|
||||||
title := reply
|
|
||||||
if idx := strings.IndexAny(reply, "\n"); idx > 0 {
|
|
||||||
title = strings.TrimSpace(reply[:idx])
|
|
||||||
}
|
|
||||||
if len(title) > 50 {
|
|
||||||
title = title[:50] + "…"
|
|
||||||
}
|
|
||||||
if title == "" {
|
|
||||||
title = "回复"
|
|
||||||
}
|
|
||||||
body := map[string]interface{}{
|
|
||||||
"msgtype": "markdown",
|
|
||||||
"markdown": map[string]string{
|
|
||||||
"title": title,
|
|
||||||
"text": reply,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(body)
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, msg.SessionWebhook, bytes.NewReader(bodyBytes))
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("钉钉构造回复请求失败", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("钉钉回复请求失败", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
logger.Warn("钉钉回复非 200", zap.Int("status", resp.StatusCode))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debug("钉钉回复成功", zap.String("content_preview", reply))
|
|
||||||
}
|
|
||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
package robot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"cyberstrike-ai/internal/config"
|
|
||||||
|
|
||||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
|
||||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
|
||||||
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
|
|
||||||
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
|
||||||
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
larkReconnectInitial = 5 * time.Second // 首次重连间隔
|
|
||||||
larkReconnectMax = 60 * time.Second // 最大重连间隔
|
|
||||||
)
|
|
||||||
|
|
||||||
type larkTextContent struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartLark 启动飞书长连接(无需公网),收到消息后调用 handler 并回复。
|
|
||||||
// 断线(如笔记本睡眠、网络中断)后会自动重连;ctx 被取消时退出,便于配置变更时重启。
|
|
||||||
func StartLark(ctx context.Context, cfg config.RobotLarkConfig, h MessageHandler, logger *zap.Logger) {
|
|
||||||
if !cfg.Enabled || cfg.AppID == "" || cfg.AppSecret == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go runLarkLoop(ctx, cfg, h, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runLarkLoop 循环维持飞书长连接:断开且 ctx 未取消时按退避间隔重连。
|
|
||||||
func runLarkLoop(ctx context.Context, cfg config.RobotLarkConfig, h MessageHandler, logger *zap.Logger) {
|
|
||||||
backoff := larkReconnectInitial
|
|
||||||
for {
|
|
||||||
larkClient := lark.NewClient(cfg.AppID, cfg.AppSecret)
|
|
||||||
eventHandler := dispatcher.NewEventDispatcher("", "").OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
|
|
||||||
go handleLarkMessage(ctx, event, h, larkClient, logger)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
wsClient := larkws.NewClient(cfg.AppID, cfg.AppSecret,
|
|
||||||
larkws.WithEventHandler(eventHandler),
|
|
||||||
larkws.WithLogLevel(larkcore.LogLevelInfo),
|
|
||||||
)
|
|
||||||
logger.Info("飞书长连接正在连接…", zap.String("app_id", cfg.AppID))
|
|
||||||
err := wsClient.Start(ctx)
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
logger.Info("飞书长连接已按配置重启关闭")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("飞书长连接断开(如睡眠/断网),将自动重连", zap.Error(err), zap.Duration("retry_after", backoff))
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-time.After(backoff):
|
|
||||||
if backoff < larkReconnectMax {
|
|
||||||
backoff *= 2
|
|
||||||
if backoff > larkReconnectMax {
|
|
||||||
backoff = larkReconnectMax
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLarkMessage(ctx context.Context, event *larkim.P2MessageReceiveV1, h MessageHandler, client *lark.Client, logger *zap.Logger) {
|
|
||||||
if event == nil || event.Event == nil || event.Event.Message == nil || event.Event.Sender == nil || event.Event.Sender.SenderId == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := event.Event.Message
|
|
||||||
msgType := larkcore.StringValue(msg.MessageType)
|
|
||||||
if msgType != larkim.MsgTypeText {
|
|
||||||
logger.Debug("飞书暂仅处理文本消息", zap.String("msg_type", msgType))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var textBody larkTextContent
|
|
||||||
if err := json.Unmarshal([]byte(larkcore.StringValue(msg.Content)), &textBody); err != nil {
|
|
||||||
logger.Warn("飞书消息 Content 解析失败", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
text := strings.TrimSpace(textBody.Text)
|
|
||||||
if text == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userID := ""
|
|
||||||
if event.Event.Sender.SenderId.UserId != nil {
|
|
||||||
userID = *event.Event.Sender.SenderId.UserId
|
|
||||||
}
|
|
||||||
messageID := larkcore.StringValue(msg.MessageId)
|
|
||||||
reply := h.HandleMessage("lark", userID, text)
|
|
||||||
contentBytes, _ := json.Marshal(larkTextContent{Text: reply})
|
|
||||||
_, err := client.Im.Message.Reply(ctx, larkim.NewReplyMessageReqBuilder().
|
|
||||||
MessageId(messageID).
|
|
||||||
Body(larkim.NewReplyMessageReqBodyBuilder().
|
|
||||||
MsgType(larkim.MsgTypeText).
|
|
||||||
Content(string(contentBytes)).
|
|
||||||
Build()).
|
|
||||||
Build())
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("飞书回复失败", zap.String("message_id", messageID), zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debug("飞书已回复", zap.String("message_id", messageID))
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user