mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-16 21:23:29 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0198a64c | |||
| 1e50272229 | |||
| 39b47a86fb | |||
| 74738ee555 |
+1
-1
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.3.11"
|
||||
version: "v1.3.12"
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
|
||||
@@ -189,6 +189,9 @@ curl -X POST "http://localhost:8080/api/robot/test" \
|
||||
|
||||
按顺序检查:
|
||||
|
||||
0. **笔记本合盖睡眠 / 断网后**
|
||||
钉钉、飞书均使用长连接收消息,睡眠或断网后连接会断开。程序会**自动重连**(约 5 秒~60 秒内重试)。唤醒或恢复网络后稍等一会儿再发消息;若仍无反应,可重启 CyberStrikeAI 进程。
|
||||
|
||||
1. **Client ID / Client Secret 是否与开放平台完全一致**
|
||||
从「凭证与基础信息」里**复制粘贴**,不要手打。注意数字 **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:
|
||||
|
||||
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**
|
||||
Copy from “Credentials and basic info”; avoid typing. Watch **0** vs **o** and **1** vs **l** (e.g. `ding9gf9tiozuc504aer` has **504**, not 5o4).
|
||||
|
||||
|
||||
+42
-28
@@ -138,43 +138,56 @@ func (h *RobotHandler) clearConversation(platform, userID string) (newConvID str
|
||||
func (h *RobotHandler) HandleMessage(platform, userID, text string) (reply string) {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return "请输入内容或发送「帮助」查看命令。"
|
||||
return "请输入内容或发送「帮助」/ help 查看命令。"
|
||||
}
|
||||
|
||||
// 命令分发
|
||||
// 命令分发(支持中英文)
|
||||
switch {
|
||||
case text == robotCmdHelp || text == "help" || text == "?" || text == "?":
|
||||
return h.cmdHelp()
|
||||
case text == robotCmdList || text == robotCmdListAlt:
|
||||
case text == robotCmdList || text == robotCmdListAlt || text == "list":
|
||||
return h.cmdList()
|
||||
case strings.HasPrefix(text, robotCmdSwitch+" ") || strings.HasPrefix(text, robotCmdContinue+" "):
|
||||
case strings.HasPrefix(text, robotCmdSwitch+" ") || strings.HasPrefix(text, robotCmdContinue+" ") || strings.HasPrefix(text, "switch ") || strings.HasPrefix(text, "continue "):
|
||||
var id string
|
||||
if strings.HasPrefix(text, robotCmdSwitch+" ") {
|
||||
switch {
|
||||
case strings.HasPrefix(text, robotCmdSwitch+" "):
|
||||
id = strings.TrimSpace(text[len(robotCmdSwitch)+1:])
|
||||
} else {
|
||||
case strings.HasPrefix(text, robotCmdContinue+" "):
|
||||
id = strings.TrimSpace(text[len(robotCmdContinue)+1:])
|
||||
case strings.HasPrefix(text, "switch "):
|
||||
id = strings.TrimSpace(text[7:])
|
||||
default:
|
||||
id = strings.TrimSpace(text[9:])
|
||||
}
|
||||
return h.cmdSwitch(platform, userID, id)
|
||||
case text == robotCmdNew:
|
||||
case text == robotCmdNew || text == "new":
|
||||
return h.cmdNew(platform, userID)
|
||||
case text == robotCmdClear:
|
||||
case text == robotCmdClear || text == "clear":
|
||||
return h.cmdClear(platform, userID)
|
||||
case text == robotCmdCurrent:
|
||||
case text == robotCmdCurrent || text == "current":
|
||||
return h.cmdCurrent(platform, userID)
|
||||
case text == robotCmdStop || text == "stop":
|
||||
return h.cmdStop(platform, userID)
|
||||
case text == robotCmdRoles || text == robotCmdRolesList:
|
||||
case text == robotCmdRoles || text == robotCmdRolesList || text == "roles":
|
||||
return h.cmdRoles()
|
||||
case strings.HasPrefix(text, robotCmdRoles+" ") || strings.HasPrefix(text, robotCmdSwitchRole+" "):
|
||||
case strings.HasPrefix(text, robotCmdRoles+" ") || strings.HasPrefix(text, robotCmdSwitchRole+" ") || strings.HasPrefix(text, "role "):
|
||||
var roleName string
|
||||
if strings.HasPrefix(text, robotCmdRoles+" ") {
|
||||
switch {
|
||||
case strings.HasPrefix(text, robotCmdRoles+" "):
|
||||
roleName = strings.TrimSpace(text[len(robotCmdRoles)+1:])
|
||||
} else {
|
||||
case strings.HasPrefix(text, robotCmdSwitchRole+" "):
|
||||
roleName = strings.TrimSpace(text[len(robotCmdSwitchRole)+1:])
|
||||
default:
|
||||
roleName = strings.TrimSpace(text[5:])
|
||||
}
|
||||
return h.cmdSwitchRole(platform, userID, roleName)
|
||||
case strings.HasPrefix(text, robotCmdDelete+" "):
|
||||
convID := strings.TrimSpace(text[len(robotCmdDelete)+1:])
|
||||
case strings.HasPrefix(text, robotCmdDelete+" ") || strings.HasPrefix(text, "delete "):
|
||||
var convID string
|
||||
if strings.HasPrefix(text, robotCmdDelete+" ") {
|
||||
convID = strings.TrimSpace(text[len(robotCmdDelete)+1:])
|
||||
} else {
|
||||
convID = strings.TrimSpace(text[7:])
|
||||
}
|
||||
return h.cmdDelete(platform, userID, convID)
|
||||
case text == robotCmdVersion || text == "version":
|
||||
return h.cmdVersion()
|
||||
@@ -219,19 +232,20 @@ func (h *RobotHandler) HandleMessage(platform, userID, text string) (reply strin
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdHelp() string {
|
||||
return `【CyberStrikeAI 机器人命令】
|
||||
· 帮助 — 显示本帮助
|
||||
· 列表 / 对话列表 — 列出所有对话标题与 ID
|
||||
· 切换 <对话ID> / 继续 <对话ID> — 指定对话继续
|
||||
· 新对话 — 开启新对话
|
||||
· 清空 — 清空当前上下文(等同于新对话)
|
||||
· 当前 — 显示当前对话 ID 与标题
|
||||
· 停止 — 中断当前正在执行的任务
|
||||
· 角色 / 角色列表 — 列出所有可用角色
|
||||
· 角色 <角色名> / 切换角色 <角色名> — 切换当前角色
|
||||
· 删除 <对话ID> — 删除指定对话
|
||||
· 版本 — 显示当前版本号
|
||||
除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。`
|
||||
return `【CyberStrikeAI 机器人命令 / Bot Commands】
|
||||
· 帮助 / help — 显示本帮助 / Show this help
|
||||
· 列表 / 对话列表 / list — 列出所有对话标题与 ID / List conversations
|
||||
· 切换 <ID> / 继续 <ID> / switch <ID> — 指定对话继续 / Switch to conversation
|
||||
· 新对话 / new — 开启新对话 / Start new conversation
|
||||
· 清空 / clear — 清空当前上下文 / Clear context (same as new)
|
||||
· 当前 / current — 显示当前对话 ID 与标题 / Show current conversation
|
||||
· 停止 / stop — 中断当前任务 / Stop running task
|
||||
· 角色 / 角色列表 / roles — 列出所有可用角色 / List roles
|
||||
· 角色 <名> / 切换角色 <名> / role <name> — 切换当前角色 / Switch role
|
||||
· 删除 <ID> / delete <ID> — 删除指定对话 / Delete conversation
|
||||
· 版本 / version — 显示当前版本号 / Show version
|
||||
除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。
|
||||
Otherwise, send any text for AI penetration testing / security analysis.`
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdList() string {
|
||||
|
||||
+57
-18
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
|
||||
@@ -15,30 +16,54 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
dingReconnectInitial = 5 * time.Second // 首次重连间隔
|
||||
dingReconnectMax = 60 * time.Second // 最大重连间隔
|
||||
)
|
||||
|
||||
// StartDing 启动钉钉 Stream 长连接(无需公网),收到消息后调用 handler 并通过 SessionWebhook 回复。
|
||||
// ctx 被取消时长连接会退出,便于配置变更时重启。
|
||||
// 断线(如笔记本睡眠、网络中断)后会自动重连;ctx 被取消时退出,便于配置变更时重启。
|
||||
func StartDing(ctx context.Context, cfg config.RobotDingtalkConfig, h MessageHandler, logger *zap.Logger) {
|
||||
if !cfg.Enabled || cfg.ClientID == "" || cfg.ClientSecret == "" {
|
||||
return
|
||||
}
|
||||
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))
|
||||
go func() {
|
||||
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 err != nil && ctx.Err() == nil {
|
||||
logger.Error("钉钉 Stream 长连接退出", zap.Error(err))
|
||||
} else if ctx.Err() != nil {
|
||||
if ctx.Err() != nil {
|
||||
logger.Info("钉钉 Stream 已按配置重启关闭")
|
||||
return
|
||||
}
|
||||
}()
|
||||
logger.Info("钉钉 Stream 已启动(无需公网),等待收消息", zap.String("client_id", cfg.ClientID))
|
||||
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) {
|
||||
@@ -73,9 +98,23 @@ func handleDingMessage(ctx context.Context, msg *chatbot.BotCallbackDataModel, h
|
||||
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": "text",
|
||||
"text": map[string]string{"content": reply},
|
||||
"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))
|
||||
|
||||
+42
-17
@@ -4,45 +4,70 @@ 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"
|
||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||
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 被取消时长连接会退出,便于配置变更时重启。
|
||||
// 断线(如笔记本睡眠、网络中断)后会自动重连;ctx 被取消时退出,便于配置变更时重启。
|
||||
func StartLark(ctx context.Context, cfg config.RobotLarkConfig, h MessageHandler, logger *zap.Logger) {
|
||||
if !cfg.Enabled || cfg.AppID == "" || cfg.AppSecret == "" {
|
||||
return
|
||||
}
|
||||
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),
|
||||
)
|
||||
go func() {
|
||||
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 err != nil && ctx.Err() == nil {
|
||||
logger.Error("飞书长连接退出", zap.Error(err))
|
||||
} else if ctx.Err() != nil {
|
||||
if ctx.Err() != nil {
|
||||
logger.Info("飞书长连接已按配置重启关闭")
|
||||
return
|
||||
}
|
||||
}()
|
||||
logger.Info("飞书长连接已启动(无需公网)", zap.String("app_id", cfg.AppID))
|
||||
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) {
|
||||
|
||||
+12
-12
@@ -1289,19 +1289,19 @@
|
||||
|
||||
<div class="settings-subsection">
|
||||
<h4>机器人命令说明</h4>
|
||||
<p class="settings-description">在对话中可发送以下命令:</p>
|
||||
<p class="settings-description">在对话中可发送以下命令(支持中英文):</p>
|
||||
<ul style="color: var(--text-muted); font-size: 13px; line-height: 1.8; margin: 8px 0 0 16px;">
|
||||
<li><strong>帮助</strong> — 显示命令帮助</li>
|
||||
<li><strong>列表</strong> 或 <strong>对话列表</strong> — 列出所有对话标题与 ID</li>
|
||||
<li><strong>切换 <对话ID></strong> 或 <strong>继续 <对话ID></strong> — 指定对话 ID 继续对话</li>
|
||||
<li><strong>新对话</strong> — 开启新对话</li>
|
||||
<li><strong>清空</strong> — 清空当前对话上下文(不删除历史)</li>
|
||||
<li><strong>当前</strong> — 显示当前对话 ID 与标题</li>
|
||||
<li><strong>停止</strong> — 中断当前正在执行的任务</li>
|
||||
<li><strong>角色</strong> 或 <strong>角色列表</strong> — 列出所有可用角色</li>
|
||||
<li><strong>角色 <角色名></strong> 或 <strong>切换角色 <角色名></strong> — 切换当前角色</li>
|
||||
<li><strong>删除 <对话ID></strong> — 删除指定对话</li>
|
||||
<li><strong>版本</strong> — 显示当前版本号</li>
|
||||
<li><strong>帮助</strong> / help — 显示命令帮助</li>
|
||||
<li><strong>列表</strong> / <strong>对话列表</strong> / list — 列出所有对话标题与 ID</li>
|
||||
<li><strong>切换 <ID></strong> / <strong>继续 <ID></strong> / switch <ID> — 指定对话继续</li>
|
||||
<li><strong>新对话</strong> / new — 开启新对话</li>
|
||||
<li><strong>清空</strong> / clear — 清空当前上下文(等同于新对话)</li>
|
||||
<li><strong>当前</strong> / current — 显示当前对话 ID 与标题</li>
|
||||
<li><strong>停止</strong> / stop — 中断当前正在执行的任务</li>
|
||||
<li><strong>角色</strong> / <strong>角色列表</strong> / roles — 列出所有可用角色</li>
|
||||
<li><strong>角色 <名></strong> / <strong>切换角色 <名></strong> / role <name> — 切换当前角色</li>
|
||||
<li><strong>删除 <ID></strong> / delete <ID> — 删除指定对话</li>
|
||||
<li><strong>版本</strong> / version — 显示当前版本号</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user