feat: enhance dingtalk card send and auth handling

This commit is contained in:
renzhiyuan 2025-12-18 18:15:44 +08:00
parent ae34efb989
commit c2906ad926
19 changed files with 1240 additions and 158 deletions

5
go.mod
View File

@ -3,6 +3,7 @@ module ai_scheduler
go 1.24.7 go 1.24.7
require ( require (
gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go v0.9.3
gitea.cdlsxd.cn/self-tools/l_request v1.0.8 gitea.cdlsxd.cn/self-tools/l_request v1.0.8
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.12 github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.12
github.com/alibabacloud-go/dingtalk v1.6.96 github.com/alibabacloud-go/dingtalk v1.6.96
@ -11,6 +12,7 @@ require (
github.com/cloudwego/eino v0.7.7 github.com/cloudwego/eino v0.7.7
github.com/cloudwego/eino-ext/components/model/ollama v0.1.6 github.com/cloudwego/eino-ext/components/model/ollama v0.1.6
github.com/cloudwego/eino-ext/components/model/openai v0.1.5 github.com/cloudwego/eino-ext/components/model/openai v0.1.5
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20
github.com/emirpasic/gods v1.18.1 github.com/emirpasic/gods v1.18.1
github.com/faabiosr/cachego v0.26.0 github.com/faabiosr/cachego v0.26.0
github.com/fastwego/dingding v1.0.0-beta.4 github.com/fastwego/dingding v1.0.0-beta.4
@ -24,10 +26,10 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0 github.com/google/wire v0.7.0
github.com/ollama/ollama v0.12.7 github.com/ollama/ollama v0.12.7
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
github.com/redis/go-redis/v9 v9.16.0 github.com/redis/go-redis/v9 v9.16.0
github.com/spf13/viper v1.17.0 github.com/spf13/viper v1.17.0
github.com/tmc/langchaingo v0.1.13 github.com/tmc/langchaingo v0.1.13
golang.org/x/sync v0.15.0
google.golang.org/grpc v1.64.0 google.golang.org/grpc v1.64.0
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.0
@ -52,7 +54,6 @@ require (
github.com/clbanning/mxj/v2 v2.5.5 // indirect github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2 // indirect github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2 // indirect
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect

6
go.sum
View File

@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go v0.9.3 h1:qaSPxVz5kHCs2AWvShnOG8mUgrUP9Gc3uUB4ZX1BF5A=
gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go v0.9.3/go.mod h1:5mCPTjBxOk69LRJPHWJRNTkfxcffqlQSOBMD4M5JVnE=
gitea.cdlsxd.cn/self-tools/l_request v1.0.8 h1:FaKRql9mCVcSoaGqPeBOAruZ52slzRngQ6VRTYKNSsA= gitea.cdlsxd.cn/self-tools/l_request v1.0.8 h1:FaKRql9mCVcSoaGqPeBOAruZ52slzRngQ6VRTYKNSsA=
gitea.cdlsxd.cn/self-tools/l_request v1.0.8/go.mod h1:Qf4hVXm2Eu5vOvwXk8D7U0q/aekMCkZ4Fg9wnRKlasQ= gitea.cdlsxd.cn/self-tools/l_request v1.0.8/go.mod h1:Qf4hVXm2Eu5vOvwXk8D7U0q/aekMCkZ4Fg9wnRKlasQ=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
@ -275,8 +277,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -354,8 +354,6 @@ github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1ls
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv81PdkYOiWbI8CNBi1boC8=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@ -8,9 +8,15 @@ import (
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
"ai_scheduler/tmpl/dataTemp"
"io"
"net/http"
"strconv" "strconv"
"time"
"ai_scheduler/internal/config"
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
@ -18,9 +24,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/coze-dev/coze-go"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
"xorm.io/builder" "xorm.io/builder"
) )
@ -36,6 +42,8 @@ type DingTalkBotBiz struct {
botGroupImpl *impl.BotGroupImpl botGroupImpl *impl.BotGroupImpl
toolManager *tools.Manager toolManager *tools.Manager
chatHis *impl.BotChatHisImpl chatHis *impl.BotChatHisImpl
conf *config.Config
cardSend *dingtalk.SendCardClient
} }
// NewDingTalkBotBiz // NewDingTalkBotBiz
@ -48,6 +56,8 @@ func NewDingTalkBotBiz(
tools *tools_regis.ToolRegis, tools *tools_regis.ToolRegis,
chatHis *impl.BotChatHisImpl, chatHis *impl.BotChatHisImpl,
toolManager *tools.Manager, toolManager *tools.Manager,
conf *config.Config,
cardSend *dingtalk.SendCardClient,
) *DingTalkBotBiz { ) *DingTalkBotBiz {
return &DingTalkBotBiz{ return &DingTalkBotBiz{
do: do, do: do,
@ -59,6 +69,8 @@ func NewDingTalkBotBiz(
botGroupImpl: botGroupImpl, botGroupImpl: botGroupImpl,
toolManager: toolManager, toolManager: toolManager,
chatHis: chatHis, chatHis: chatHis,
conf: conf,
cardSend: cardSend,
} }
} }
@ -75,7 +87,7 @@ func (d *DingTalkBotBiz) GetDingTalkBotCfgList() (dingBotList []entitys.DingTalk
if err != nil { if err != nil {
d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error()) d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error())
} }
config.BotIndex = v.BotIndex config.BotIndex = v.RobotCode
dingBotList = append(dingBotList, config) dingBotList = append(dingBotList, config)
} }
return return
@ -92,8 +104,8 @@ func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallb
} }
func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等") //entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等")
defer close(requireData.Ch) //defer close(requireData.Ch)
switch constants.ConversationType(requireData.Req.ConversationType) { switch constants.ConversationType(requireData.Req.ConversationType) {
case constants.ConversationTypeSingle: case constants.ConversationTypeSingle:
err = d.handleSingleChat(ctx, requireData) err = d.handleSingleChat(ctx, requireData)
@ -124,7 +136,7 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti
} }
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle) group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle, requireData.Req.RobotCode)
if err != nil { if err != nil {
return return
@ -142,8 +154,8 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit
return d.handleMatch(ctx, rec) return d.handleMatch(ctx, rec)
} }
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string) (group *model.AiBotGroup, err error) { func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) {
group, err = d.botGroupImpl.GetByConversationId(conversationId) group, err = d.botGroupImpl.GetByConversationIdAndRobotCode(conversationId, robotCode)
if err != nil { if err != nil {
if !errors.Is(err, sql.ErrNoRows) { if !errors.Is(err, sql.ErrNoRows) {
@ -155,10 +167,11 @@ func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, c
group = &model.AiBotGroup{ group = &model.AiBotGroup{
ConversationID: conversationId, ConversationID: conversationId,
Title: conversationTitle, Title: conversationTitle,
RobotCode: robotCode,
ToolList: "", ToolList: "",
} }
//如果不存在则创建 //如果不存在则创建
d.botGroupImpl.Add(group) _, err = d.botGroupImpl.Add(group)
} }
return return
} }
@ -199,13 +212,19 @@ func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.Req
userContent, err := d.getUserContent(requireData.Req.Msgtype, requireData.Req.Text.Content) userContent, err := d.getUserContent(requireData.Req.Msgtype, requireData.Req.Text.Content)
if err != nil { if err != nil {
return nil, err return
} }
rec = &entitys.Recognize{ rec = &entitys.Recognize{
Ch: requireData.Ch, Ch: requireData.Ch,
SystemPrompt: d.defaultPrompt(), SystemPrompt: d.defaultPrompt(),
UserContent: userContent, UserContent: userContent,
} }
//历史记录
rec.ChatHis, err = d.getHis(ctx, constants.ConversationType(requireData.Req.ConversationType), requireData.ID)
if err != nil {
return
}
//工具注册
if len(tools) > 0 { if len(tools) > 0 {
rec.Tasks = make([]entitys.RegistrationTask, 0, len(tools)) rec.Tasks = make([]entitys.RegistrationTask, 0, len(tools))
for _, task := range tools { for _, task := range tools {
@ -226,6 +245,36 @@ func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.Req
return return
} }
func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.ConversationType, Id int32) (content entitys.ChatHis, err error) {
var (
his []model.AiBotChatHi
)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"his_type": conversationType})
cond = cond.And(builder.Eq{"id": Id})
_, err = d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: d.conf.Sys.SessionLen}, &his, "his_id desc")
if err != nil {
return
}
messages := make([]entitys.HisMessage, 0, len(his))
for _, v := range his {
messages = append(messages, entitys.HisMessage{
Role: constants.Caller(v.Role), // 用户角色
Content: v.Content, // 用户输入内容
Timestamp: v.CreateAt.Format(time.DateTime),
})
}
return entitys.ChatHis{
SessionId: fmt.Sprintf("%s_%d", conversationType, Id),
Messages: messages,
Context: entitys.HisContext{
UserLanguage: constants.LangZhCN, // 默认中文
SystemMode: constants.SystemModeTechnicalSupport, // 默认技术支持模式
},
}, nil
}
func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{}) (content *entitys.RecognizeUserContent, err error) { func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{}) (content *entitys.RecognizeUserContent, err error) {
switch constants.BotMsgType(msgType) { switch constants.BotMsgType(msgType) {
case constants.BotMsgTypeText: case constants.BotMsgTypeText:
@ -261,16 +310,116 @@ func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize
return d.otherTask(ctx, rec) return d.otherTask(ctx, rec)
} }
switch constants.TaskType(pointTask.Type) { switch constants.TaskType(pointTask.Type) {
//case constants.TaskTypeApi:
//return d.handleApiTask(ctx, requireData, pointTask)
case constants.TaskTypeFunc: case constants.TaskTypeFunc:
return d.handleTask(ctx, rec, pointTask) return d.handleTask(ctx, rec, pointTask)
case constants.TaskTypeCozeWorkflow:
return d.handleCozeWorkflow(ctx, rec, pointTask)
default: default:
return d.otherTask(ctx, rec) return d.otherTask(ctx, rec)
} }
return return
} }
func (d *DingTalkBotBiz) handleCozeWorkflow(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) {
entitys.ResLoading(rec.Ch, task.Index, "正在执行工作流(coze)\n")
customClient := &http.Client{
Timeout: time.Minute * 30,
}
authCli := coze.NewTokenAuth(d.conf.Coze.ApiSecret)
cozeCli := coze.NewCozeAPI(
authCli,
coze.WithBaseURL(d.conf.Coze.BaseURL),
coze.WithHttpClient(customClient),
)
// 从参数中获取workflowID
type requestParams struct {
Request l_request.Request `json:"request"`
}
var config requestParams
err = json.Unmarshal([]byte(task.Config), &config)
if err != nil {
return err
}
workflowId, ok := config.Request.Json["workflow_id"].(string)
if !ok {
return fmt.Errorf("workflow_id不能为空")
}
// 提取参数
var data map[string]interface{}
err = json.Unmarshal([]byte(rec.Match.Parameters), &data)
req := &coze.RunWorkflowsReq{
WorkflowID: workflowId,
Parameters: data,
// IsAsync: true,
}
stream := config.Request.Json["stream"].(bool)
entitys.ResLog(rec.Ch, task.Index, "工作流执行中...")
if stream {
streamResp, err := cozeCli.Workflows.Runs.Stream(ctx, req)
if err != nil {
return err
}
handleCozeWorkflowEvents(ctx, streamResp, cozeCli, workflowId, rec.Ch, task.Index)
} else {
resp, err := cozeCli.Workflows.Runs.Create(ctx, req)
if err != nil {
return err
}
entitys.ResJson(rec.Ch, task.Index, resp.Data)
}
return
}
// handleCozeWorkflowEvents 处理 coze 工作流事件
func handleCozeWorkflowEvents(ctx context.Context, resp coze.Stream[coze.WorkflowEvent], cozeCli coze.CozeAPI, workflowID string, ch chan entitys.Response, index string) {
defer resp.Close()
for {
event, err := resp.Recv()
if errors.Is(err, io.EOF) {
fmt.Println("Stream finished")
break
}
if err != nil {
fmt.Println("Error receiving event:", err)
break
}
switch event.Event {
case coze.WorkflowEventTypeMessage:
entitys.ResStream(ch, index, event.Message.Content)
case coze.WorkflowEventTypeError:
entitys.ResError(ch, index, fmt.Sprintf("工作流执行错误: %s", event.Error))
case coze.WorkflowEventTypeDone:
entitys.ResEnd(ch, index, "工作流执行完成")
case coze.WorkflowEventTypeInterrupt:
resumeReq := &coze.ResumeRunWorkflowsReq{
WorkflowID: workflowID,
EventID: event.Interrupt.InterruptData.EventID,
ResumeData: "your data",
InterruptType: event.Interrupt.InterruptData.Type,
}
newResp, err := cozeCli.Workflows.Runs.Resume(ctx, resumeReq)
if err != nil {
entitys.ResError(ch, index, fmt.Sprintf("工作流恢复执行错误: %s", err.Error()))
return
}
entitys.ResLog(ch, index, "工作流恢复执行中...")
handleCozeWorkflowEvents(ctx, newResp, cozeCli, workflowID, ch, index)
}
}
fmt.Printf("done, log:%s\n", resp.Response().LogID())
}
func (d *DingTalkBotBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) { func (d *DingTalkBotBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) {
var configData entitys.ConfigDataTool var configData entitys.ConfigDataTool
err = json.Unmarshal([]byte(task.Config), &configData) err = json.Unmarshal([]byte(task.Config), &configData)
@ -290,56 +439,42 @@ func (d *DingTalkBotBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
entitys.ResText(rec.Ch, "", rec.Match.Reasoning) entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
return return
} }
func (d *DingTalkBotBiz) HandleRes(ctx context.Context, data *chatbot.BotCallbackDataModel, resp entitys.Response) error {
switch resp.Type { //func (d *DingTalkBotBiz) HandleRes(ctx context.Context, data *chatbot.BotCallbackDataModel, resp entitys.Response, ch chan string) error {
case entitys.ResponseText: // switch resp.Type {
return d.replyText(ctx, data.SessionWebhook, resp.Content) // case entitys.ResponseText:
case entitys.ResponseStream: // return d.replyText(ctx, data.SessionWebhook, resp.Content)
return d.replySteam(ctx, data.SessionWebhook, resp.Content) // case entitys.ResponseStream:
case entitys.ResponseImg: //
return d.replyImg(ctx, data.SessionWebhook, resp.Content) // return d.replySteam(ctx, data, ch)
case entitys.ResponseFile: // case entitys.ResponseImg:
return d.replyFile(ctx, data.SessionWebhook, resp.Content) // return d.replyImg(ctx, data.SessionWebhook, resp.Content)
case entitys.ResponseMarkdown: // case entitys.ResponseFile:
return d.replyMarkdown(ctx, data.SessionWebhook, resp.Content) // return d.replyFile(ctx, data.SessionWebhook, resp.Content)
case entitys.ResponseActionCard: // case entitys.ResponseMarkdown:
return d.replyActionCard(ctx, data.SessionWebhook, resp.Content) // return d.replyMarkdown(ctx, data.SessionWebhook, resp.Content)
default: // case entitys.ResponseActionCard:
return nil // return d.replyActionCard(ctx, data.SessionWebhook, resp.Content)
} // default:
// return nil
// }
//}
func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotCallbackDataModel, content chan string) (err error) {
err = d.cardSend.NewCard(ctx, &dingtalk.CardSend{
RobotCode: data.RobotCode,
ConversationType: constants.ConversationType(data.ConversationType),
Template: constants.CardTempDefault,
ContentChannel: content, // 指定内容通道
ConversationId: data.ConversationId,
SenderStaffId: data.SenderStaffId,
Title: data.Text.Content,
})
return
} }
func (d *DingTalkBotBiz) SaveHis(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, chat []string) (err error) { func (d *DingTalkBotBiz) ReplyText(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
if len(chat) == 0 {
return
}
his := []*model.AiBotChatHi{
{
HisType: requireData.Req.ConversationType,
ID: requireData.ID,
Role: "user",
Content: requireData.Req.Text.Content,
},
{
HisType: requireData.Req.ConversationType,
ID: requireData.ID,
Role: "system",
Content: strings.Join(chat, "\n"),
},
}
_, err = d.chatHis.Add(his)
return err
}
func (d *DingTalkBotBiz) replySteam(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content
if len(arg) > 0 {
msg = fmt.Sprintf(content, arg)
}
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
}
func (d *DingTalkBotBiz) replyText(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
msg := content msg := content
if len(arg) > 0 { if len(arg) > 0 {
msg = fmt.Sprintf(content, arg) msg = fmt.Sprintf(content, arg)
@ -379,6 +514,28 @@ func (d *DingTalkBotBiz) replyActionCard(ctx context.Context, SessionWebhook str
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg)) return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
} }
func (d *DingTalkBotBiz) SaveHis(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, chat []string) (err error) {
if len(chat) == 0 {
return
}
his := []*model.AiBotChatHi{
{
HisType: requireData.Req.ConversationType,
ID: requireData.ID,
Role: "user",
Content: requireData.Req.Text.Content,
},
{
HisType: requireData.Req.ConversationType,
ID: requireData.ID,
Role: "system",
Content: strings.Join(chat, "\n"),
},
}
_, err = d.chatHis.Add(his)
return err
}
func (d *DingTalkBotBiz) defaultPrompt() string { func (d *DingTalkBotBiz) defaultPrompt() string {
return `[system] 你是一个智能路由系统核心职责是 **精准解析用户意图并路由至对应任务模块**请严格遵循以下规则 return `[system] 你是一个智能路由系统核心职责是 **精准解析用户意图并路由至对应任务模块**请严格遵循以下规则

View File

@ -6,6 +6,7 @@ import (
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/l_request"
"ai_scheduler/utils" "ai_scheduler/utils"
"context" "context"
@ -38,21 +39,26 @@ func (a *Auth) GetAccessToken(ctx context.Context, clientId string, clientSecret
return nil, errors.New("clientId is empty") return nil, errors.New("clientId is empty")
} }
accessToken := a.redis.Get(ctx, a.getKey(clientId)).Val() accessToken := a.redis.Get(ctx, a.getKey(clientId)).Val()
var expire time.Duration
if accessToken == "" { if accessToken == "" {
dingTalkAuthRes, _err := a.getNewAccessToken(ctx, clientId, clientSecret) dingTalkAuthRes, _err := a.getNewAccessToken(ctx, clientId, clientSecret)
if _err != nil { if _err != nil {
return nil, _err return nil, _err
} }
err = a.redis.SetEx(ctx, a.getKey(clientId), dingTalkAuthRes.AccessToken, time.Duration(dingTalkAuthRes.ExpireIn-3600)*time.Second).Err() expire = time.Duration(dingTalkAuthRes.ExpireIn-3600) * time.Second
err = a.redis.SetEx(ctx, a.getKey(clientId), dingTalkAuthRes.AccessToken, expire).Err()
if err != nil { if err != nil {
return return
} }
accessToken = dingTalkAuthRes.AccessToken accessToken = dingTalkAuthRes.AccessToken
} else {
expire, _ = a.redis.TTL(ctx, a.getKey(clientId)).Result()
} }
return &AuthInfo{ return &AuthInfo{
ClientId: clientId, ClientId: clientId,
ClientSecret: clientSecret, ClientSecret: clientSecret,
AccessToken: accessToken, AccessToken: accessToken,
Expire: expire,
}, nil }, nil
} }
@ -60,6 +66,10 @@ func (a *Auth) getKey(clientId string) string {
return a.cfg.Redis.Key + ":" + constants.DingTalkAuthBaseKeyPrefix + ":" + clientId return a.cfg.Redis.Key + ":" + constants.DingTalkAuthBaseKeyPrefix + ":" + clientId
} }
func (a *Auth) getKeyBot(botCode string) string {
return a.cfg.Redis.Key + ":" + constants.DingTalkAuthBaseKeyBotPrefix + ":" + botCode
}
func (a *Auth) getNewAccessToken(ctx context.Context, clientId string, clientSecret string) (auth DingTalkAuthIRes, err error) { func (a *Auth) getNewAccessToken(ctx context.Context, clientId string, clientSecret string) (auth DingTalkAuthIRes, err error) {
if clientId == "" || clientSecret == "" { if clientId == "" || clientSecret == "" {
err = errors.New("clientId or clientSecret is empty") err = errors.New("clientId or clientSecret is empty")
@ -89,30 +99,61 @@ func (a *Auth) GetTokenFromBotOption(ctx context.Context, botOption ...BotOption
option(botInfo) option(botInfo)
} }
if botInfo.id == 0 && botInfo.botConfig == nil { if botInfo.Id == 0 && botInfo.BotConfig == nil && botInfo.BotCode == "" {
err = errors.New("botInfo is nil") err = errors.New("botInfo is nil")
return return
} }
if botInfo.botConfig == nil {
var botConfigDo model.AiBotConfig if botInfo.BotConfig == nil {
cond := builder.NewCond() err = a.GetBotConfigFromModel(botInfo)
cond = cond.And(builder.Eq{"bot_id": botInfo.id})
err = a.botConfigImpl.GetOneBySearchToStrut(&cond, &botConfigDo)
if err != nil { if err != nil {
return return
} }
if botConfigDo.BotID == 0 { }
err = errors.New("未找到机器人服务配置")
authInfo := a.redis.Get(ctx, a.getKeyBot(botInfo.BotConfig.RobotCode)).Val()
if authInfo == "" {
var botConfig entitys.DingTalkBot
err = json.Unmarshal([]byte(botInfo.BotConfig.BotConfig), &botConfig)
if err != nil {
log.Infof("初始化“%s”失败:%s", botInfo.BotConfig.BotName, err.Error())
return return
} }
botInfo.botConfig = &botConfigDo token, err = a.GetAccessToken(ctx, botConfig.ClientId, botConfig.ClientSecret)
if err != nil {
return
}
err = a.redis.SetEx(ctx, a.getKeyBot(botInfo.BotConfig.RobotCode), pkg.JsonStringIgonErr(token), token.Expire).Err()
if err != nil {
return
}
} else {
var tokenData AuthInfo
err = json.Unmarshal([]byte(authInfo), &tokenData)
token = &tokenData
} }
var botConfig entitys.DingTalkBot return
err = json.Unmarshal([]byte(botInfo.botConfig.BotConfig), &botConfig) }
func (a *Auth) GetBotConfigFromModel(botInfo *Bot) (err error) {
var (
botConfigDo model.AiBotConfig
)
cond := builder.NewCond()
if botInfo.Id > 0 {
cond = cond.And(builder.Eq{"bot_id": botInfo.Id})
}
if botInfo.BotCode != "" {
cond = cond.And(builder.Eq{"robot_code": botInfo.BotCode})
}
err = a.botConfigImpl.GetOneBySearchToStrut(&cond, &botConfigDo)
if err != nil { if err != nil {
log.Infof("初始化“%s”失败:%s", botInfo.botConfig.BotName, err.Error())
return return
} }
return a.GetAccessToken(ctx, botConfig.ClientId, botConfig.ClientSecret) if botConfigDo.BotID == 0 {
err = errors.New("未找到机器人服务配置")
return
}
botInfo.BotConfig = &botConfigDo
return nil
} }

View File

@ -3,19 +3,34 @@ package dingtalk
import "ai_scheduler/internal/data/model" import "ai_scheduler/internal/data/model"
type Bot struct { type Bot struct {
id int Id int
botConfig *model.AiBotConfig BotCode string
BotConfig *model.AiBotConfig
} }
type BotOption func(*Bot) type BotOption func(*Bot)
func WithId(id int) BotOption { func WithId(id int) BotOption {
return func(b *Bot) { return func(b *Bot) {
b.id = id b.Id = id
} }
} }
func WithBootConfig(BotConfig *model.AiBotConfig) BotOption { func WithBotConfig(BotConfig *model.AiBotConfig) BotOption {
return func(bot *Bot) { return func(bot *Bot) {
bot.botConfig = BotConfig bot.BotConfig = BotConfig
}
}
func WithBotCode(BotCode string) BotOption {
return func(bot *Bot) {
bot.BotCode = BotCode
}
}
func WithBot(botSelf *Bot) BotOption {
return func(bot *Bot) {
bot.BotCode = botSelf.BotCode
bot.Id = botSelf.Id
bot.BotConfig = botSelf.BotConfig
} }
} }

View File

@ -8,4 +8,5 @@ var ProviderSetDingTalk = wire.NewSet(
NewUser, NewUser,
NewAuth, NewAuth,
NewDept, NewDept,
NewSendCardClient,
) )

View File

@ -0,0 +1,287 @@
package dingtalk
import (
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/pkg"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
"github.com/gofiber/fiber/v2/log"
"github.com/google/uuid"
)
const DefaultInterval = 100 * time.Millisecond
const HeardBeatX = 50
type SendCardClient struct {
Auth *Auth
CardClient *sync.Map
mu sync.RWMutex // 保护 CardClient 的并发访问
logger log.AllLogger // 日志记录
botOption *Bot
}
func NewSendCardClient(auth *Auth, logger log.AllLogger) *SendCardClient {
return &SendCardClient{
Auth: auth,
CardClient: &sync.Map{},
logger: logger,
botOption: &Bot{},
}
}
// initClient 初始化或复用 DingTalk 客户端
func (s *SendCardClient) initClient(robotCode string) (*dingtalkim_1_0.Client, error) {
if client, ok := s.CardClient.Load(robotCode); ok {
return client.(*dingtalkim_1_0.Client), nil
}
s.botOption.BotCode = robotCode
config := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
client, err := dingtalkim_1_0.NewClient(config)
if err != nil {
s.logger.Error("failed to init DingTalk client")
return nil, fmt.Errorf("init client failed: %w", err)
}
s.CardClient.Store(robotCode, client)
return client, nil
}
func (s *SendCardClient) NewCard(ctx context.Context, cardSend *CardSend) error {
// 参数校验
if (len(cardSend.ContentSlice) == 0 || cardSend.ContentSlice == nil) && cardSend.ContentChannel == nil {
return errors.New("卡片内容不能为空")
}
if cardSend.UpdateInterval == 0 {
cardSend.UpdateInterval = DefaultInterval // 默认更新间隔
}
if cardSend.Title == "" {
cardSend.Title = "钉钉卡片"
}
//替换标题
replace, err := pkg.SafeReplace(string(cardSend.Template), "${title}", cardSend.Title)
if err != nil {
return err
}
cardSend.Template = constants.CardTemp(replace)
// 初始化客户端
client, err := s.initClient(cardSend.RobotCode)
if err != nil {
return fmt.Errorf("初始化client失败: %w", err)
}
// 生成卡片实例ID
cardInstanceId, err := uuid.NewUUID()
if err != nil {
return fmt.Errorf("创建uuid失败: %w", err)
}
// 构建初始请求
request, err := s.buildBaseRequest(cardSend, cardInstanceId.String())
if err != nil {
return fmt.Errorf("请求失败: %w", err)
}
// 发送初始卡片
if _, err := s.SendInteractiveCard(ctx, request, cardSend.RobotCode, client); err != nil {
return fmt.Errorf("发送初始卡片失败: %w", err)
}
// 处理切片内容(同步)
if len(cardSend.ContentSlice) > 0 {
if err := s.processContentSlice(ctx, cardSend, cardInstanceId.String(), client); err != nil {
return fmt.Errorf("内容同步失败: %w", err)
}
}
// 处理通道内容(异步)
if cardSend.ContentChannel != nil {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
s.processContentChannel(ctx, cardSend, cardInstanceId.String(), client)
}()
wg.Wait()
}
return nil
}
// buildBaseRequest 构建基础请求
func (s *SendCardClient) buildBaseRequest(cardSend *CardSend, cardInstanceId string) (*dingtalkim_1_0.SendRobotInteractiveCardRequest, error) {
cardData := fmt.Sprintf(string(cardSend.Template), "") // 初始空内容
request := &dingtalkim_1_0.SendRobotInteractiveCardRequest{
CardTemplateId: tea.String("StandardCard"),
CardBizId: tea.String(cardInstanceId),
CardData: tea.String(cardData),
RobotCode: tea.String(cardSend.RobotCode),
SendOptions: &dingtalkim_1_0.SendRobotInteractiveCardRequestSendOptions{},
PullStrategy: tea.Bool(false),
}
switch cardSend.ConversationType {
case constants.ConversationTypeGroup:
request.SetOpenConversationId(cardSend.ConversationId)
case constants.ConversationTypeSingle:
receiver, err := json.Marshal(map[string]string{"userId": cardSend.SenderStaffId})
if err != nil {
return nil, fmt.Errorf("数据整理失败: %w", err)
}
request.SetSingleChatReceiver(string(receiver))
default:
return nil, errors.New("未知的聊天场景")
}
return request, nil
}
// processContentChannel 处理通道内容(异步更新)
func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) {
defer func() {
if r := recover(); r != nil {
s.logger.Error("panic in processContentChannel")
}
}()
ticker := time.NewTicker(cardSend.UpdateInterval)
defer ticker.Stop()
heartbeatTicker := time.NewTicker(time.Duration(HeardBeatX) * DefaultInterval)
defer heartbeatTicker.Stop()
var (
contentBuilder strings.Builder
lastUpdate time.Time
)
for {
select {
case content, ok := <-cardSend.ContentChannel:
if !ok {
// 通道关闭,发送最终内容
if contentBuilder.Len() > 0 {
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
s.logger.Errorf("更新卡片失败1:%s", err.Error())
}
}
return
}
contentBuilder.WriteString(content)
if contentBuilder.Len() > 0 {
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
s.logger.Errorf("更新卡片失败2%s", err.Error())
}
}
lastUpdate = time.Now()
case <-heartbeatTicker.C:
if time.Now().Unix()-lastUpdate.Unix() >= HeardBeatX {
return
}
case <-ctx.Done():
s.logger.Info("context canceled, stop channel processing")
return
}
}
}
// processContentSlice 处理切片内容(同步更新)
func (s *SendCardClient) processContentSlice(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) error {
var contentBuilder strings.Builder
for _, content := range cardSend.ContentSlice {
contentBuilder.WriteString(content)
err := s.updateCardRequest(ctx, &UpdateCardRequest{
Template: string(cardSend.Template),
Content: contentBuilder.String(),
Client: client,
RobotCode: cardSend.RobotCode,
CardInstanceId: cardInstanceId,
})
if err != nil {
return fmt.Errorf("更新卡片失败: %w", err)
}
time.Sleep(cardSend.UpdateInterval) // 控制更新频率
}
return nil
}
// updateCardContent 封装卡片更新逻辑
func (s *SendCardClient) updateCardContent(ctx context.Context, cardSend *CardSend, cardInstanceId, content string, client *dingtalkim_1_0.Client) error {
err := s.updateCardRequest(ctx, &UpdateCardRequest{
Template: string(cardSend.Template),
Content: content,
Client: client,
RobotCode: cardSend.RobotCode,
CardInstanceId: cardInstanceId,
})
return err
}
func (s *SendCardClient) updateCardRequest(ctx context.Context, updateCardRequest *UpdateCardRequest) error {
content, err := pkg.SafeReplace(updateCardRequest.Template, "%s", updateCardRequest.Content)
if err != nil {
return err
}
updateRequest := &dingtalkim_1_0.UpdateRobotInteractiveCardRequest{
CardBizId: tea.String(updateCardRequest.CardInstanceId),
CardData: tea.String(content),
}
_, err = s.UpdateInteractiveCard(ctx, updateRequest, updateCardRequest.RobotCode, updateCardRequest.Client)
return err
}
// UpdateInteractiveCard 更新交互卡片(封装错误处理)
func (s *SendCardClient) UpdateInteractiveCard(ctx context.Context, request *dingtalkim_1_0.UpdateRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (*dingtalkim_1_0.UpdateRobotInteractiveCardResponse, error) {
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
if err != nil {
return nil, fmt.Errorf("get token failed: %w", err)
}
headers := &dingtalkim_1_0.UpdateRobotInteractiveCardHeaders{
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
}
response, err := client.UpdateRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
if err != nil {
return nil, fmt.Errorf("API call failed: %w,request:%v", err, request.String())
}
return response, nil
}
// SendInteractiveCard 发送交互卡片(封装错误处理)
func (s *SendCardClient) SendInteractiveCard(ctx context.Context, request *dingtalkim_1_0.SendRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (res *dingtalkim_1_0.SendRobotInteractiveCardResponse, err error) {
err = s.Auth.GetBotConfigFromModel(s.botOption)
if err != nil {
return nil, fmt.Errorf("初始化bot失败: %w", err)
}
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
if err != nil {
return nil, fmt.Errorf("get token failed: %w", err)
}
headers := &dingtalkim_1_0.SendRobotInteractiveCardHeaders{
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
}
response, err := client.SendRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
if err != nil {
return nil, fmt.Errorf("API call failed: %w", err)
}
return response, nil
}

View File

@ -0,0 +1,280 @@
package dingtalk
import (
"ai_scheduler/internal/data/constants"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dingtalkcard_1_0 "github.com/alibabacloud-go/dingtalk/card_1_0"
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
"github.com/gofiber/fiber/v2/log"
"github.com/google/uuid"
)
const DefaultInterval = 100 * time.Millisecond
const HeardBeatX = 50
type SendCardClient struct {
Auth *Auth
CardClient *sync.Map
mu sync.RWMutex // 保护 CardClient 的并发访问
logger log.AllLogger // 日志记录
botOption *Bot
}
func NewSendCardClient(auth *Auth, logger log.AllLogger) *SendCardClient {
return &SendCardClient{
Auth: auth,
CardClient: &sync.Map{},
logger: logger,
botOption: &Bot{},
}
}
// initClient 初始化或复用 DingTalk 客户端
func (s *SendCardClient) initClient(robotCode string) (*dingtalkcard_1_0.Client, error) {
if client, ok := s.CardClient.Load(robotCode); ok {
return client.(*dingtalkcard_1_0.Client), nil
}
s.botOption.BotCode = robotCode
config := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
client, err := dingtalkcard_1_0.NewClient(config)
if err != nil {
s.logger.Error("failed to init DingTalk client")
return nil, fmt.Errorf("init client failed: %w", err)
}
s.CardClient.Store(robotCode, client)
return client, nil
}
func (s *SendCardClient) NewCard(ctx context.Context, cardSend *CardSend) error {
// 参数校验
if (len(cardSend.ContentSlice) == 0 || cardSend.ContentSlice == nil) && cardSend.ContentChannel == nil {
return errors.New("卡片内容不能为空")
}
if cardSend.UpdateInterval == 0 {
cardSend.UpdateInterval = DefaultInterval // 默认更新间隔
}
if cardSend.Title == "" {
cardSend.Title = "钉钉卡片"
}
//替换标题
cardSend.Template = constants.CardTemp(strings.Replace(string(cardSend.Template), "${title}", cardSend.Title, 1))
// 初始化客户端
client, err := s.initClient(cardSend.RobotCode)
if err != nil {
return fmt.Errorf("初始化client失败: %w", err)
}
// 生成卡片实例ID
cardInstanceId, err := uuid.NewUUID()
if err != nil {
return fmt.Errorf("创建uuid失败: %w", err)
}
// 构建初始请求
request, err := s.buildBaseRequest(cardSend, cardInstanceId.String())
if err != nil {
return fmt.Errorf("请求失败: %w", err)
}
// 发送初始卡片
if _, err := s.SendInteractiveCard(ctx, request, cardSend.RobotCode, client); err != nil {
return fmt.Errorf("发送初始卡片失败: %w", err)
}
// 处理切片内容(同步)
if len(cardSend.ContentSlice) > 0 {
if err := s.processContentSlice(ctx, cardSend, cardInstanceId.String(), client); err != nil {
return fmt.Errorf("内容同步失败: %w", err)
}
}
// 处理通道内容(异步)
if cardSend.ContentChannel != nil {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
s.processContentChannel(ctx, cardSend, cardInstanceId.String(), client)
}()
wg.Wait()
}
return nil
}
// buildBaseRequest 构建基础请求
func (s *SendCardClient) buildBaseRequest(cardSend *CardSend, cardInstanceId string) (*dingtalkcard_1_0.StreamingUpdateRequest, error) {
cardData := fmt.Sprintf(string(cardSend.Template), "") // 初始空内容
request := &dingtalkcard_1_0.StreamingUpdateRequest{
OutTrackId: tea.String("your-out-track-id"),
Guid: tea.String("0F714542-0AFC-2B0E-CF14-E2D39F5BFFE8"),
Key: tea.String("your-ai-param"),
Content: tea.String("test"),
IsFull: tea.Bool(false),
IsFinalize: tea.Bool(false),
IsError: tea.Bool(false),
}
switch cardSend.ConversationType {
case constants.ConversationTypeGroup:
request.SetOpenConversationId(cardSend.ConversationId)
case constants.ConversationTypeSingle:
receiver, err := json.Marshal(map[string]string{"userId": cardSend.SenderStaffId})
if err != nil {
return nil, fmt.Errorf("数据整理失败: %w", err)
}
request.SetSingleChatReceiver(string(receiver))
default:
return nil, errors.New("未知的聊天场景")
}
return request, nil
}
// processContentChannel 处理通道内容(异步更新)
func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) {
defer func() {
if r := recover(); r != nil {
s.logger.Error("panic in processContentChannel")
}
}()
ticker := time.NewTicker(cardSend.UpdateInterval)
defer ticker.Stop()
heartbeatTicker := time.NewTicker(time.Duration(HeardBeatX) * DefaultInterval)
defer heartbeatTicker.Stop()
var (
contentBuilder strings.Builder
lastUpdate time.Time
)
for {
select {
case content, ok := <-cardSend.ContentChannel:
if !ok {
// 通道关闭,发送最终内容
if contentBuilder.Len() > 0 {
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
s.logger.Errorf("更新卡片失败1:%s", err.Error())
}
}
return
}
contentBuilder.WriteString(content)
if contentBuilder.Len() > 0 {
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
s.logger.Errorf("更新卡片失败2%s", err.Error())
}
}
lastUpdate = time.Now()
case <-heartbeatTicker.C:
if time.Now().Unix()-lastUpdate.Unix() >= HeardBeatX {
return
}
case <-ctx.Done():
s.logger.Info("context canceled, stop channel processing")
return
}
}
}
// processContentSlice 处理切片内容(同步更新)
func (s *SendCardClient) processContentSlice(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) error {
var contentBuilder strings.Builder
for _, content := range cardSend.ContentSlice {
contentBuilder.WriteString(content)
err := s.updateCardRequest(ctx, &UpdateCardRequest{
Template: string(cardSend.Template),
Content: contentBuilder.String(),
Client: client,
RobotCode: cardSend.RobotCode,
CardInstanceId: cardInstanceId,
})
if err != nil {
return fmt.Errorf("更新卡片失败: %w", err)
}
time.Sleep(cardSend.UpdateInterval) // 控制更新频率
}
return nil
}
// updateCardContent 封装卡片更新逻辑
func (s *SendCardClient) updateCardContent(ctx context.Context, cardSend *CardSend, cardInstanceId, content string, client *dingtalkim_1_0.Client) error {
err := s.updateCardRequest(ctx, &UpdateCardRequest{
Template: string(cardSend.Template),
Content: content,
Client: client,
RobotCode: cardSend.RobotCode,
CardInstanceId: cardInstanceId,
})
return err
}
func (s *SendCardClient) updateCardRequest(ctx context.Context, updateCardRequest *UpdateCardRequest) error {
updateRequest := &dingtalkim_1_0.UpdateRobotInteractiveCardRequest{
CardBizId: tea.String(updateCardRequest.CardInstanceId),
CardData: tea.String(fmt.Sprintf(updateCardRequest.Template, updateCardRequest.Content)),
}
_, err := s.UpdateInteractiveCard(ctx, updateRequest, updateCardRequest.RobotCode, updateCardRequest.Client)
return err
}
// UpdateInteractiveCard 更新交互卡片(封装错误处理)
func (s *SendCardClient) UpdateInteractiveCard(ctx context.Context, request *dingtalkim_1_0.UpdateRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (*dingtalkim_1_0.UpdateRobotInteractiveCardResponse, error) {
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
if err != nil {
return nil, fmt.Errorf("get token failed: %w", err)
}
headers := &dingtalkim_1_0.UpdateRobotInteractiveCardHeaders{
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
}
response, err := client.UpdateRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
if err != nil {
return nil, fmt.Errorf("API call failed: %w,request:%v", err, request.String())
}
return response, nil
}
// SendInteractiveCard 发送交互卡片(封装错误处理)
func (s *SendCardClient) SendInteractiveCard(ctx context.Context, request *dingtalkim_1_0.SendRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (res *dingtalkim_1_0.SendRobotInteractiveCardResponse, err error) {
err = s.Auth.GetBotConfigFromModel(s.botOption)
if err != nil {
return nil, fmt.Errorf("初始化bot失败: %w", err)
}
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
if err != nil {
return nil, fmt.Errorf("get token failed: %w", err)
}
headers := &dingtalkim_1_0.SendRobotInteractiveCardHeaders{
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
}
response, err := client.SendRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
if err != nil {
return nil, fmt.Errorf("API call failed: %w", err)
}
return response, nil
}

View File

@ -1,6 +1,11 @@
package dingtalk package dingtalk
import "time" import (
"ai_scheduler/internal/data/constants"
"time"
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
)
type DingTalkAuthIRes struct { type DingTalkAuthIRes struct {
AccessToken string `json:"accessToken"` AccessToken string `json:"accessToken"`
@ -78,7 +83,28 @@ type DeptResResult struct {
} }
type AuthInfo struct { type AuthInfo struct {
ClientId string `json:"clientId"` ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"` ClientSecret string `json:"clientSecret"`
AccessToken string `json:"accessToken"` AccessToken string `json:"accessToken"`
Expire time.Duration `json:"expireIn"`
}
type CardSend struct {
RobotCode string
ConversationType constants.ConversationType
ConversationId string
Template constants.CardTemp
SenderStaffId string
Title string
ContentSlice []string
ContentChannel chan string
UpdateInterval time.Duration // 控制通道更新的频率
}
type UpdateCardRequest struct {
Template string
Content string
Client *dingtalkim_1_0.Client
RobotCode string
CardInstanceId string
} }

View File

@ -34,6 +34,8 @@ const (
const DingTalkAuthBaseKeyPrefix = "dingTalk_auth" const DingTalkAuthBaseKeyPrefix = "dingTalk_auth"
const DingTalkAuthBaseKeyBotPrefix = "dingTalk_auth_bot"
// PermissionType 工具使用权限 // PermissionType 工具使用权限
type PermissionType int32 type PermissionType int32

View File

@ -49,3 +49,32 @@ type BotMsgType string
const ( const (
BotMsgTypeText BotMsgType = "text" BotMsgTypeText BotMsgType = "text"
) )
type CardTemp string
const (
CardTempDefault CardTemp = `{
"config": {
"autoLayout": true,
"enableForward": true
},
"header": {
"title": {
"type": "text",
"text": "${title}",
},
"logo": "@lALPDfJ6V_FPDmvNAfTNAfQ"
},
"contents": [
{
"type": "divider",
"id": "divider_1765952728523"
},
{
"type": "markdown",
"text": "%s",
"id": "markdown_1765970168635"
}
]
}`
)

View File

@ -17,9 +17,9 @@ func NewBotGroupImpl(db *utils.Db) *BotGroupImpl {
} }
} }
func (k BotGroupImpl) GetByConversationId(staffId string) (*model.AiBotGroup, error) { func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) {
var data model.AiBotGroup var data model.AiBotGroup
err := k.Db.Model(k.Model).Where("conversation_id = ?", staffId).Find(&data).Error err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ?", staffId, robotCode).Find(&data).Error
if data.GroupID == 0 { if data.GroupID == 0 {
err = sql.ErrNoRows err = sql.ErrNoRows
} }

View File

@ -13,11 +13,11 @@ const TableNameAiBotConfig = "ai_bot_config"
// AiBotConfig mapped from table <ai_bot_config> // AiBotConfig mapped from table <ai_bot_config>
type AiBotConfig struct { type AiBotConfig struct {
BotID int32 `gorm:"column:bot_id;primaryKey;autoIncrement:true" json:"bot_id"` BotID int32 `gorm:"column:bot_id;primaryKey;autoIncrement:true" json:"bot_id"`
SysID int32 `gorm:"column:sys_id;not null" json:"sys_id"`
BotType int32 `gorm:"column:bot_type;not null;default:1;comment:类型1为钉钉机器人" json:"bot_type"` // 类型1为钉钉机器人 BotType int32 `gorm:"column:bot_type;not null;default:1;comment:类型1为钉钉机器人" json:"bot_type"` // 类型1为钉钉机器人
BotName string `gorm:"column:bot_name;not null;comment:名字" json:"bot_name"` // 名字 SysPrompt string `gorm:"column:sys_prompt" json:"sys_prompt"`
BotConfig string `gorm:"column:bot_config;not null;comment:配置" json:"bot_config"` // 配置 BotName string `gorm:"column:bot_name;not null;comment:名字" json:"bot_name"` // 名字
BotIndex string `gorm:"column:bot_index;not null;comment:索引" json:"bot_index"` // 索引 BotConfig string `gorm:"column:bot_config;not null;comment:配置" json:"bot_config"` // 配置
RobotCode string `gorm:"column:robot_code;not null;comment:索引" json:"robot_code"` // 索引
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"` CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"`
Status int32 `gorm:"column:status;not null" json:"status"` Status int32 `gorm:"column:status;not null" json:"status"`

View File

@ -14,6 +14,7 @@ const TableNameAiBotGroup = "ai_bot_group"
type AiBotGroup struct { type AiBotGroup struct {
GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"` GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"`
ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID
RobotCode string `gorm:"column:robot_code;not null;comment:绑定机器人code" json:"robot_code"` // 绑定机器人code
Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称 Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称
ToolList string `gorm:"column:tool_list;not null;comment:开通工具列表" json:"tool_list"` // 开通工具列表 ToolList string `gorm:"column:tool_list;not null;comment:开通工具列表" json:"tool_list"` // 开通工具列表
Status int32 `gorm:"column:status;not null;default:1" json:"status"` Status int32 `gorm:"column:status;not null;default:1" json:"status"`

View File

@ -3,7 +3,7 @@ package entitys
import ( import (
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
) )
type RequireDataDingTalkBot struct { type RequireDataDingTalkBot struct {

View File

@ -133,3 +133,35 @@ func SliceIntToString(slice []int) []string {
} }
return strSlice return strSlice
} }
// SafeReplace 替换字符串中的 %s并自动转义特殊字符如 "
/**
* SafeReplace 函数用于安全地替换模板字符串中的占位符
* @param template 原始模板字符串
* @param replaceTag 要被替换的占位符 "%s"
* @param replacements 可变参数用于替换占位符的字符串
* @return 返回替换后的字符串和可能的错误
*/
func SafeReplace(template string, replaceTag string, replacements ...string) (string, error) {
// 如果没有提供替换参数,直接返回原始模板
if len(replacements) == 0 {
return template, nil
}
// 检查模板中 %s 的数量是否匹配替换参数
expectedReplacements := strings.Count(template, replaceTag)
if expectedReplacements != len(replacements) {
return "", fmt.Errorf("模板需要 %d 个替换参数,但提供了 %d 个", expectedReplacements, len(replacements))
}
// 逐个替换 %s并转义特殊字符
for _, rep := range replacements {
// 转义特殊字符(如 ", \, \n 等)
escaped := strconv.Quote(rep)
// 去掉 strconv.Quote 添加的额外引号
escaped = escaped[1 : len(escaped)-1]
template = strings.Replace(template, replaceTag, escaped, 1)
}
return template, nil
}

View File

@ -4,10 +4,12 @@ import (
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/services" "ai_scheduler/internal/services"
"context" "context"
"fmt"
"sync"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/client"
"github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/log"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
) )
type DingBotServiceInterface interface { type DingBotServiceInterface interface {
@ -54,18 +56,48 @@ func ProvideAllDingBotServices(
} }
func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) { func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
for name, cli := range d.Clients { if botIndex == "" {
if botIndex != "All" { log.Info("未指定机器人索引,跳过启动")
if name != botIndex { return
continue }
var targets []string
switch {
case botIndex == "All":
targets = make([]string, 0, len(d.Clients))
for name := range d.Clients {
targets = append(targets, name)
}
default:
if _, exists := d.Clients[botIndex]; exists {
targets = []string{botIndex}
} else {
log.Infof("未找到索引为 %s 的机器人", botIndex)
return
}
}
var wg sync.WaitGroup
errors := make([]error, 0, len(targets))
for _, name := range targets {
wg.Add(1)
go func(name string) {
defer wg.Done()
err := d.Clients[name].Start(ctx)
if err != nil {
log.Errorf("%s 启动失败: %v", name, err)
errors = append(errors, fmt.Errorf("%s: %w", name, err))
} else {
log.Infof("%s 启动成功", name)
} }
} }(name)
err := cli.Start(ctx) }
if err != nil {
log.Infof("%s启动失败", name) wg.Wait()
continue if len(errors) > 0 {
} log.Errorf("部分机器人启动失败,总数: %d, 成功: %d, 失败: %d",
log.Infof("%s启动成功", name) len(targets), len(targets)-len(errors), len(errors))
} }
} }
func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) { func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) {

View File

@ -2,15 +2,15 @@ package services
import ( import (
"ai_scheduler/internal/biz" "ai_scheduler/internal/biz"
"log"
"time"
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"context" "context"
"log"
"sync"
"time"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"golang.org/x/sync/errgroup"
) )
type DingBotService struct { type DingBotService struct {
@ -18,65 +18,115 @@ type DingBotService struct {
dingTalkBotBiz *biz.DingTalkBotBiz dingTalkBotBiz *biz.DingTalkBotBiz
} }
func NewDingBotService(config *config.Config, DingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService { func NewDingBotService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService {
return &DingBotService{config: config, dingTalkBotBiz: DingTalkBotBiz} return &DingBotService{
config: config,
dingTalkBotBiz: dingTalkBotBiz,
}
} }
func (d *DingBotService) GetServiceCfg() ([]entitys.DingTalkBot, error) { func (d *DingBotService) GetServiceCfg() ([]entitys.DingTalkBot, error) {
return d.dingTalkBotBiz.GetDingTalkBotCfgList() return d.dingTalkBotBiz.GetDingTalkBotCfgList()
} }
func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error) { func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) {
var (
lastErr error
chat []string
)
requireData, err := d.dingTalkBotBiz.InitRequire(ctx, data) requireData, err := d.dingTalkBotBiz.InitRequire(ctx, data)
if err != nil { if err != nil {
return return nil, err
} }
// 使用 ctx.Done() 通知 Do 方法提前终止
subCtx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
_ = d.dingTalkBotBiz.SaveHis(ctx, requireData, chat)
}() // 启动后台任务(独立生命周期,带超时控制)
// 异步执行 Do 方法
done := make(chan error, 1)
go func() { go func() {
done <- d.dingTalkBotBiz.Do(subCtx, requireData) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
if err := d.runBackgroundTasks(ctx, data, requireData); err != nil {
log.Printf("后台任务执行失败: %v", err)
}
}() }()
for { return []byte("success"), nil
select { }
case <-ctx.Done():
lastErr = ctx.Err() func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.BotCallbackDataModel, requireData *entitys.RequireDataDingTalkBot) error {
goto cleanup g, ctx := errgroup.WithContext(ctx)
case resp, ok := <-requireData.Ch: var (
if !ok { chat []string
return []byte("success"), nil chatMu sync.Mutex
} resChan = make(chan string, 10)
if resp.Type == entitys.ResponseLog { )
continue
} // 1. 流式处理协程
if resp.Type == entitys.ResponseText || resp.Type == entitys.ResponseStream || resp.Type == entitys.ResponseJson { g.Go(func() error {
chat = append(chat, resp.Content) defer func() {
} // 确保通道最终关闭
if err := d.dingTalkBotBiz.HandleRes(ctx, data, resp); err != nil { close(resChan)
log.Printf("HandleRes 失败: %v", err) }()
return d.dingTalkBotBiz.HandleStreamRes(ctx, data, resChan)
})
// 2. 业务处理协程负责关闭requireData.Ch
g.Go(func() error {
// 在完成时关闭通道
defer close(requireData.Ch)
return d.dingTalkBotBiz.Do(ctx, requireData)
})
// 3. 结果收集协程(修改后的版本)
resultDone := make(chan struct{})
g.Go(func() error {
// 使用defer确保通道关闭
defer close(resultDone)
// 处理通道中的数据
for {
select {
case resp, ok := <-requireData.Ch:
if !ok {
return nil // 通道已关闭,正常退出
}
if resp.Type != entitys.ResponseLog {
chatMu.Lock()
chat = append(chat, resp.Content)
chatMu.Unlock()
select {
case resChan <- resp.Content:
case <-ctx.Done():
return ctx.Err()
}
}
case <-ctx.Done():
return ctx.Err() // 上下文取消,提前退出
} }
} }
} })
cleanup:
select { // 4. 统一关闭通道的协程只关闭resChan
case _err := <-done: g.Go(func() error {
if _err != nil { <-resultDone
panic(_err) // resChan已在流式处理协程关闭
return nil
})
// 5. 历史记录保存协程
g.Go(func() error {
<-resultDone
chatMu.Lock()
savedChat := make([]string, len(chat))
copy(savedChat, chat)
chatMu.Unlock()
if err := d.dingTalkBotBiz.SaveHis(ctx, requireData, savedChat); err != nil {
log.Printf("保存历史记录失败: %v", err)
return err
} }
case <-time.After(1 * time.Second): return nil
log.Println("警告:等待 Do 方法超时,可能发生 goroutine 泄漏") })
// 阻塞直到所有协程完成或出错
if err := g.Wait(); err != nil {
return err
} }
return nil, lastErr return nil
} }

View File

@ -0,0 +1,130 @@
package services
import (
"ai_scheduler/internal/biz"
"log"
"sync"
"time"
"ai_scheduler/internal/config"
"ai_scheduler/internal/entitys"
"context"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
)
type DingBotService struct {
config *config.Config
dingTalkBotBiz *biz.DingTalkBotBiz
}
func NewDingBotService(config *config.Config, DingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService {
return &DingBotService{config: config, dingTalkBotBiz: DingTalkBotBiz}
}
func (d *DingBotService) GetServiceCfg() ([]entitys.DingTalkBot, error) {
return d.dingTalkBotBiz.GetDingTalkBotCfgList()
}
func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) ([]byte, error) {
var (
lastErr error
chat []string
streamWG sync.WaitGroup
resChan = make(chan string, 100) // 缓冲通道防止阻塞
)
// 初始化请求
requireData, err := d.dingTalkBotBiz.InitRequire(ctx, data)
if err != nil {
return nil, err
}
// 创建子上下文用于控制goroutine生命周期
subCtx, cancel := context.WithCancel(ctx)
defer cancel()
// 启动流式处理goroutine
streamWG.Add(1)
go func() {
defer streamWG.Done()
err = d.dingTalkBotBiz.HandleStreamRes(subCtx, data, resChan)
if err != nil {
return
}
}()
// 启动业务处理goroutine
done := make(chan error, 1)
go func() {
done <- d.dingTalkBotBiz.Do(subCtx, requireData)
}()
// 主处理循环
for {
select {
case <-ctx.Done():
lastErr = ctx.Err()
goto cleanup
case resp, ok := <-requireData.Ch:
if !ok {
goto cleanup
}
// 处理不同类型响应
switch resp.Type {
case entitys.ResponseLog:
// 忽略日志类型
continue
//case entitys.ResponseText, entitys.ResponseJson:
// chat = append(chat, resp.Content)
// if err := d.dingTalkBotBiz.ReplyText(ctx, data.SessionWebhook, resp.Content); err != nil {
// log.Printf("处理非流响应失败: %v", err)
// lastErr = err
// }
default:
chat = append(chat, resp.Content)
select {
case resChan <- resp.Content:
case <-ctx.Done():
lastErr = ctx.Err()
goto cleanup
}
}
}
}
cleanup:
streamWG.Wait()
// 关闭流式通道
close(resChan)
// 保存历史记录
if saveErr := d.dingTalkBotBiz.SaveHis(ctx, requireData, chat); saveErr != nil {
log.Printf("保存历史记录失败: %v", saveErr)
if lastErr == nil {
lastErr = saveErr
}
}
// 等待业务处理完成(带超时)
select {
case err := <-done:
if err != nil {
log.Printf("业务处理失败: %v", err)
if lastErr == nil {
lastErr = err
}
}
case <-time.After(3 * time.Second): // 增加超时时间
log.Println("警告等待业务处理超时可能发生goroutine泄漏")
}
if lastErr != nil {
return nil, lastErr
}
return []byte("success"), nil
}