mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-30 21:20:12 +02:00
Add files via upload
This commit is contained in:
4 files changed
+89
-33
No files matched your search
@@ -189,6 +189,9 @@ curl -X POST "http://localhost:8080/api/robot/test" \
|
|||||||
|
|
||||||
按顺序检查:
|
按顺序检查:
|
||||||
|
|
||||||
|
0. **笔记本合盖睡眠 / 断网后**
|
||||||
|
钉钉、飞书均使用长连接收消息,睡眠或断网后连接会断开。程序会**自动重连**(约 5 秒~60 秒内重试)。唤醒或恢复网络后稍等一会儿再发消息;若仍无反应,可重启 CyberStrikeAI 进程。
|
||||||
|
|
||||||
1. **Client ID / Client Secret 是否与开放平台完全一致**
|
1. **Client ID / Client Secret 是否与开放平台完全一致**
|
||||||
从「凭证与基础信息」里**复制粘贴**,不要手打。注意数字 **0** 与字母 **o**、数字 **1** 与字母 **l**(例如 `ding9gf9tiozuc504aer` 中间是 **504** 不是 5o4)。
|
从「凭证与基础信息」里**复制粘贴**,不要手打。注意数字 **0** 与字母 **o**、数字 **1** 与字母 **l**(例如 `ding9gf9tiozuc504aer` 中间是 **504** 不是 5o4)。
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,9 @@ API: `POST /api/robot/test` (requires login). Body: `{"platform":"optional","use
|
|||||||
|
|
||||||
Check in this order:
|
Check in this order:
|
||||||
|
|
||||||
|
0. **After laptop sleep or network drop**
|
||||||
|
DingTalk and Lark both use long-lived connections; they break when the machine sleeps or the network drops. The app **auto-reconnects** (retries within about 5–60 seconds). After wake or network recovery, wait a moment before sending; if there is still no response, restart the CyberStrikeAI process.
|
||||||
|
|
||||||
1. **Client ID / Client Secret match the open platform exactly**
|
1. **Client ID / Client Secret match the open platform exactly**
|
||||||
Copy from “Credentials and basic info”; avoid typing. Watch **0** vs **o** and **1** vs **l** (e.g. `ding9gf9tiozuc504aer` has **504**, not 5o4).
|
Copy from “Credentials and basic info”; avoid typing. Watch **0** vs **o** and **1** vs **l** (e.g. `ding9gf9tiozuc504aer` has **504**, not 5o4).
|
||||||
|
|
||||||
|
|||||||
+41
-16
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"cyberstrike-ai/internal/config"
|
"cyberstrike-ai/internal/config"
|
||||||
|
|
||||||
@@ -15,30 +16,54 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dingReconnectInitial = 5 * time.Second // 首次重连间隔
|
||||||
|
dingReconnectMax = 60 * time.Second // 最大重连间隔
|
||||||
|
)
|
||||||
|
|
||||||
// StartDing 启动钉钉 Stream 长连接(无需公网),收到消息后调用 handler 并通过 SessionWebhook 回复。
|
// StartDing 启动钉钉 Stream 长连接(无需公网),收到消息后调用 handler 并通过 SessionWebhook 回复。
|
||||||
// ctx 被取消时长连接会退出,便于配置变更时重启。
|
// 断线(如笔记本睡眠、网络中断)后会自动重连;ctx 被取消时退出,便于配置变更时重启。
|
||||||
func StartDing(ctx context.Context, cfg config.RobotDingtalkConfig, h MessageHandler, logger *zap.Logger) {
|
func StartDing(ctx context.Context, cfg config.RobotDingtalkConfig, h MessageHandler, logger *zap.Logger) {
|
||||||
if !cfg.Enabled || cfg.ClientID == "" || cfg.ClientSecret == "" {
|
if !cfg.Enabled || cfg.ClientID == "" || cfg.ClientSecret == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
streamClient := client.NewStreamClient(
|
go runDingLoop(ctx, cfg, h, logger)
|
||||||
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) {
|
// runDingLoop 循环维持钉钉长连接:断开且 ctx 未取消时按退避间隔重连。
|
||||||
go handleDingMessage(ctx, msg, h, logger)
|
func runDingLoop(ctx context.Context, cfg config.RobotDingtalkConfig, h MessageHandler, logger *zap.Logger) {
|
||||||
return nil, nil
|
backoff := dingReconnectInitial
|
||||||
}).OnEventReceived),
|
for {
|
||||||
)
|
streamClient := client.NewStreamClient(
|
||||||
logger.Info("钉钉 Stream 正在连接…", zap.String("client_id", cfg.ClientID))
|
client.WithAppCredential(client.NewAppCredentialConfig(cfg.ClientID, cfg.ClientSecret)),
|
||||||
go func() {
|
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)
|
err := streamClient.Start(ctx)
|
||||||
if err != nil && ctx.Err() == nil {
|
if ctx.Err() != nil {
|
||||||
logger.Error("钉钉 Stream 长连接退出", zap.Error(err))
|
|
||||||
} else if ctx.Err() != nil {
|
|
||||||
logger.Info("钉钉 Stream 已按配置重启关闭")
|
logger.Info("钉钉 Stream 已按配置重启关闭")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}()
|
if err != nil {
|
||||||
logger.Info("钉钉 Stream 已启动(无需公网),等待收消息", zap.String("client_id", cfg.ClientID))
|
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) {
|
func handleDingMessage(ctx context.Context, msg *chatbot.BotCallbackDataModel, h MessageHandler, logger *zap.Logger) {
|
||||||
|
|||||||
+42
-17
@@ -4,45 +4,70 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"cyberstrike-ai/internal/config"
|
"cyberstrike-ai/internal/config"
|
||||||
|
|
||||||
|
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||||
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
|
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
|
||||||
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
||||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
|
||||||
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
|
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
larkReconnectInitial = 5 * time.Second // 首次重连间隔
|
||||||
|
larkReconnectMax = 60 * time.Second // 最大重连间隔
|
||||||
|
)
|
||||||
|
|
||||||
type larkTextContent struct {
|
type larkTextContent struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartLark 启动飞书长连接(无需公网),收到消息后调用 handler 并回复。
|
// StartLark 启动飞书长连接(无需公网),收到消息后调用 handler 并回复。
|
||||||
// ctx 被取消时长连接会退出,便于配置变更时重启。
|
// 断线(如笔记本睡眠、网络中断)后会自动重连;ctx 被取消时退出,便于配置变更时重启。
|
||||||
func StartLark(ctx context.Context, cfg config.RobotLarkConfig, h MessageHandler, logger *zap.Logger) {
|
func StartLark(ctx context.Context, cfg config.RobotLarkConfig, h MessageHandler, logger *zap.Logger) {
|
||||||
if !cfg.Enabled || cfg.AppID == "" || cfg.AppSecret == "" {
|
if !cfg.Enabled || cfg.AppID == "" || cfg.AppSecret == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
larkClient := lark.NewClient(cfg.AppID, cfg.AppSecret)
|
go runLarkLoop(ctx, cfg, h, logger)
|
||||||
eventHandler := dispatcher.NewEventDispatcher("", "").OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
|
}
|
||||||
go handleLarkMessage(ctx, event, h, larkClient, logger)
|
|
||||||
return nil
|
// runLarkLoop 循环维持飞书长连接:断开且 ctx 未取消时按退避间隔重连。
|
||||||
})
|
func runLarkLoop(ctx context.Context, cfg config.RobotLarkConfig, h MessageHandler, logger *zap.Logger) {
|
||||||
wsClient := larkws.NewClient(cfg.AppID, cfg.AppSecret,
|
backoff := larkReconnectInitial
|
||||||
larkws.WithEventHandler(eventHandler),
|
for {
|
||||||
larkws.WithLogLevel(larkcore.LogLevelInfo),
|
larkClient := lark.NewClient(cfg.AppID, cfg.AppSecret)
|
||||||
)
|
eventHandler := dispatcher.NewEventDispatcher("", "").OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
|
||||||
go func() {
|
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)
|
err := wsClient.Start(ctx)
|
||||||
if err != nil && ctx.Err() == nil {
|
if ctx.Err() != nil {
|
||||||
logger.Error("飞书长连接退出", zap.Error(err))
|
|
||||||
} else if ctx.Err() != nil {
|
|
||||||
logger.Info("飞书长连接已按配置重启关闭")
|
logger.Info("飞书长连接已按配置重启关闭")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}()
|
if err != nil {
|
||||||
logger.Info("飞书长连接已启动(无需公网)", zap.String("app_id", cfg.AppID))
|
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) {
|
func handleLarkMessage(ctx context.Context, event *larkim.P2MessageReceiveV1, h MessageHandler, client *lark.Client, logger *zap.Logger) {
|
||||||
|
|||||||
Reference in new issue
Block a user