From 8e74c434aee4cb0a8d8618607fb3f5cefb71f8b8 Mon Sep 17 00:00:00 2001 From: fuzhongyun <15339891972@163.com> Date: Mon, 17 Nov 2025 10:16:35 +0800 Subject: [PATCH] =?UTF-8?q?chore:=201.=20callbackService=20=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=BC=98=E5=8C=96=202.=20dingtalkClient=20=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E7=BB=93=E6=9E=84=E4=BC=98=E5=8C=96=203.=20tools=5Fbo?= =?UTF-8?q?t=20=E7=BB=93=E6=9E=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client.go => dingtalk/contact_client.go} | 10 +- .../client.go => dingtalk/notable_client.go} | 10 +- .../pkg/dingtalk/{client.go => old_client.go} | 40 ++-- internal/pkg/provider_set.go | 8 +- internal/services/callback.go | 195 ++++++++++-------- internal/tools_bot/bug_optimization_submit.go | 115 +++++++++++ internal/tools_bot/dtalk_bot.go | 110 ---------- 7 files changed, 263 insertions(+), 225 deletions(-) rename internal/pkg/{dingtalk_contact/client.go => dingtalk/contact_client.go} (84%) rename internal/pkg/{dingtalk_notable/client.go => dingtalk/notable_client.go} (85%) rename internal/pkg/dingtalk/{client.go => old_client.go} (69%) create mode 100644 internal/tools_bot/bug_optimization_submit.go diff --git a/internal/pkg/dingtalk_contact/client.go b/internal/pkg/dingtalk/contact_client.go similarity index 84% rename from internal/pkg/dingtalk_contact/client.go rename to internal/pkg/dingtalk/contact_client.go index 4382846..4461995 100644 --- a/internal/pkg/dingtalk_contact/client.go +++ b/internal/pkg/dingtalk/contact_client.go @@ -1,4 +1,4 @@ -package dingtalk_contact +package dingtalk import ( "ai_scheduler/internal/config" @@ -10,12 +10,12 @@ import ( "github.com/alibabacloud-go/tea/tea" ) -type Client struct { +type ContactClient struct { config *config.Config cli *contact.Client } -func NewContactClient(config *config.Config) (*Client, error) { +func NewContactClient(config *config.Config) (*ContactClient, error) { cfg := &openapi.Config{ AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey), AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret), @@ -26,7 +26,7 @@ func NewContactClient(config *config.Config) (*Client, error) { if err != nil { return nil, err } - return &Client{config: config, cli: c}, nil + return &ContactClient{config: config, cli: c}, nil } type SearchUserReq struct { @@ -40,7 +40,7 @@ type SearchUserResp struct { Body interface{} } -func (c *Client) SearchUserOne(accessToken string, name string) (string, error) { +func (c *ContactClient) SearchUserOne(accessToken string, name string) (string, error) { headers := &contact.SearchUserHeaders{} headers.XAcsDingtalkAccessToken = tea.String(accessToken) resp, err := c.cli.SearchUserWithOptions(&contact.SearchUserRequest{ diff --git a/internal/pkg/dingtalk_notable/client.go b/internal/pkg/dingtalk/notable_client.go similarity index 85% rename from internal/pkg/dingtalk_notable/client.go rename to internal/pkg/dingtalk/notable_client.go index a1d6719..7cfe04f 100644 --- a/internal/pkg/dingtalk_notable/client.go +++ b/internal/pkg/dingtalk/notable_client.go @@ -1,4 +1,4 @@ -package dingtalk_notable +package dingtalk import ( "ai_scheduler/internal/config" @@ -10,12 +10,12 @@ import ( "github.com/alibabacloud-go/tea/tea" ) -type Client struct { +type NotableClient struct { config *config.Config cli *notable.Client } -func NewNotableClient(config *config.Config) (*Client, error) { +func NewNotableClient(config *config.Config) (*NotableClient, error) { cfg := &openapi.Config{ AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey), AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret), @@ -26,7 +26,7 @@ func NewNotableClient(config *config.Config) (*Client, error) { if err != nil { return nil, err } - return &Client{config: config, cli: c}, nil + return &NotableClient{config: config, cli: c}, nil } type UpdateRecordReq struct { @@ -41,7 +41,7 @@ type UpdateRecordsserResp struct { Body interface{} } -func (c *Client) UpdateRecord(accessToken string, req *UpdateRecordReq) (bool, error) { +func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (bool, error) { headers := ¬able.UpdateRecordsHeaders{} headers.XAcsDingtalkAccessToken = tea.String(accessToken) resp, err := c.cli.UpdateRecordsWithOptions( diff --git a/internal/pkg/dingtalk/client.go b/internal/pkg/dingtalk/old_client.go similarity index 69% rename from internal/pkg/dingtalk/client.go rename to internal/pkg/dingtalk/old_client.go index 8a8ece2..7742d3a 100644 --- a/internal/pkg/dingtalk/client.go +++ b/internal/pkg/dingtalk/old_client.go @@ -1,5 +1,7 @@ package dingtalk +// 旧版sdk客户端 - 在用,勿删! + import ( "ai_scheduler/internal/config" "bytes" @@ -15,26 +17,26 @@ import ( "github.com/fastwego/dingding" ) -type Client struct { - cfg *config.Config - sdkClient *dingding.Client - atm *dingding.DefaultAccessTokenManager +type OldClient struct { + config *config.Config + cli *dingding.Client + atm *dingding.DefaultAccessTokenManager } -func NewDingTalkClient(cfg *config.Config) *Client { - atm := &dingding.DefaultAccessTokenManager{ - Id: cfg.Tools.DingTalkBot.APIKey, +func NewOldClient(config *config.Config) *OldClient { + atm := &dingding.DefaultAccessTokenManager{ + Id: config.Tools.DingTalkBot.APIKey, Name: "access_token", GetRefreshRequestFunc: func() *http.Request { params := url.Values{} - params.Add("appkey", cfg.Tools.DingTalkBot.APIKey) - params.Add("appsecret", cfg.Tools.DingTalkBot.APISecret) + params.Add("appkey", config.Tools.DingTalkBot.APIKey) + params.Add("appsecret", config.Tools.DingTalkBot.APISecret) req, _ := http.NewRequest(http.MethodGet, dingding.ServerUrl+"/gettoken?"+params.Encode(), nil) return req }, Cache: file.New(os.TempDir()), - } - return &Client{cfg: cfg, sdkClient: dingding.NewClient(atm), atm: atm} + } + return &OldClient{config: config, cli: dingding.NewClient(atm), atm: atm} } type UserDetail struct { @@ -43,7 +45,7 @@ type UserDetail struct { UnionID string `json:"unionid"` } -func (c *Client) do(ctx context.Context, method, path string, body []byte) ([]byte, error) { +func (c *OldClient) do(ctx context.Context, method, path string, body []byte) ([]byte, error) { var r io.Reader if body != nil { r = bytes.NewReader(body) @@ -55,10 +57,10 @@ func (c *Client) do(ctx context.Context, method, path string, body []byte) ([]by if body != nil { req.Header.Set("Content-Type", "application/json") } - return c.sdkClient.Do(req) + return c.cli.Do(req) } -func (c *Client) QueryUserDetails(ctx context.Context, userId string) (*UserDetail, error) { +func (c *OldClient) QueryUserDetails(ctx context.Context, userId string) (*UserDetail, error) { body := struct { UserId string `json:"userid"` Language string `json:"language,omitempty"` @@ -82,7 +84,7 @@ func (c *Client) QueryUserDetails(ctx context.Context, userId string) (*UserDeta return &resp.Result, nil } -func (c *Client) QueryUserDetailsByMobile(ctx context.Context, mobile string) (*UserDetail, error) { +func (c *OldClient) QueryUserDetailsByMobile(ctx context.Context, mobile string) (*UserDetail, error) { body := struct { Mobile string `json:"mobile"` }{Mobile: mobile} @@ -104,8 +106,8 @@ func (c *Client) QueryUserDetailsByMobile(ctx context.Context, mobile string) (* } return &resp.Result, nil } -// GetAccessToken 通过 fastwego 的 AccessTokenManager 获取当前可用 access_token -func (c *Client) GetAccessToken() (string, error) { - return c.atm.GetAccessToken() -} +// GetAccessToken 通过 fastwego 的 AccessTokenManager 获取当前可用 access_token +func (c *OldClient) GetAccessToken() (string, error) { + return c.atm.GetAccessToken() +} diff --git a/internal/pkg/provider_set.go b/internal/pkg/provider_set.go index a4d6b64..93d9180 100644 --- a/internal/pkg/provider_set.go +++ b/internal/pkg/provider_set.go @@ -2,8 +2,6 @@ package pkg import ( "ai_scheduler/internal/pkg/dingtalk" - "ai_scheduler/internal/pkg/dingtalk_contact" - "ai_scheduler/internal/pkg/dingtalk_notable" "ai_scheduler/internal/pkg/utils_langchain" "ai_scheduler/internal/pkg/utils_ollama" @@ -16,7 +14,7 @@ var ProviderSetClient = wire.NewSet( utils_langchain.NewUtilLangChain, utils_ollama.NewClient, NewSafeChannelPool, - dingtalk.NewDingTalkClient, - dingtalk_contact.NewContactClient, - dingtalk_notable.NewNotableClient, + dingtalk.NewOldClient, + dingtalk.NewContactClient, + dingtalk.NewNotableClient, ) diff --git a/internal/services/callback.go b/internal/services/callback.go index bda89d4..aff1ae5 100644 --- a/internal/services/callback.go +++ b/internal/services/callback.go @@ -2,11 +2,12 @@ package services import ( "ai_scheduler/internal/config" + "ai_scheduler/internal/data/constants" errorcode "ai_scheduler/internal/data/error" + "ai_scheduler/internal/entitys" "ai_scheduler/internal/gateway" + "ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg/dingtalk" - "ai_scheduler/internal/pkg/dingtalk_contact" - "ai_scheduler/internal/pkg/dingtalk_notable" "ai_scheduler/internal/pkg/util" "ai_scheduler/internal/tools_bot" "context" @@ -21,17 +22,17 @@ import ( type CallbackService struct { cfg *config.Config gateway *gateway.Gateway - dingtalkClient *dingtalk.Client - dingtalkContactClient *dingtalk_contact.Client - dingtalkNotableClient *dingtalk_notable.Client + dingtalkOldClient *dingtalk.OldClient + dingtalkContactClient *dingtalk.ContactClient + dingtalkNotableClient *dingtalk.NotableClient botTool *tools_bot.BotTool } -func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkClient *dingtalk.Client, dingtalkContactClient *dingtalk_contact.Client, dingtalkNotableClient *dingtalk_notable.Client, botTool *tools_bot.BotTool) *CallbackService { +func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkOldClient *dingtalk.OldClient, dingtalkContactClient *dingtalk.ContactClient, dingtalkNotableClient *dingtalk.NotableClient, botTool *tools_bot.BotTool) *CallbackService { return &CallbackService{ cfg: cfg, gateway: gateway, - dingtalkClient: dingtalkClient, + dingtalkOldClient: dingtalkOldClient, dingtalkContactClient: dingtalkContactClient, dingtalkNotableClient: dingtalkNotableClient, botTool: botTool, @@ -76,7 +77,7 @@ func (s *CallbackService) Callback(c *fiber.Ctx) error { // 时间窗口(如果提供了 ts 则校验,否则跳过),窗口 5 分钟 if ts != "" && !validateTimestamp(ts, 5*time.Minute) { - // return errorcode.AuthNotFound + return errorcode.AuthNotFound } // 解析 Envelope @@ -135,91 +136,37 @@ func parseInt64(s string) (int64, bool) { } func (s *CallbackService) handleDingTalkCallback(c *fiber.Ctx, env Envelope) error { + // 校验taskId + sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID) + if !ok { + return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID) + } + ctx := c.Context() + switch env.Action { // bug/优化完成回调 case ActionBugOptimizationSubmitDone: - // 获取 session_id - sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID) - if !ok { - return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID) + // 业务处理 + msg, businessErr := s.handleBugOptimizationSubmitDone(ctx, env.Data) + if businessErr != nil { + return businessErr } - var data BugOptimizationSubmitDoneData - if err := json.Unmarshal(env.Data, &data); err != nil { - return errorcode.ParamErr("invalid data type: %v", err) - } - - if len(data.Receivers) == 0 { - return errorcode.ParamErr("empty receivers") - } - // 构建接收者 - receivers := s.getDingtalkReceivers(c.Context(), data.Receivers) - - // 构建跳转链接 - var detailPage string - if data.DetailPage != "" { - detailPage = util.BuildJumpLink(data.DetailPage, "去查看") - } - - msg := data.Msg - msg = util.ReplacePlaceholder(msg, "receivers", receivers) - msg = util.ReplacePlaceholder(msg, "detail_page", util.EscapeJSONString(detailPage)) - - s.gateway.SendToUid(sessionID, []byte(msg)) + // 发送日志 + s.sendStreamLog(sessionID, msg) // 删除映射 s.botTool.DelTaskMapping(env.TaskID) return c.JSON(fiber.Map{"code": 0, "message": "ok"}) case ActionBugOptimizationSubmitUpdate: - // 获取 session_id - // sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID) - // if !ok { - // return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID) - // } - - var data BugOptimizationSubmitUpdateData - if err := json.Unmarshal(env.Data, &data); err != nil { - return errorcode.ParamErr("invalid data type: %v", err) + // 业务处理 + msg, businessErr := s.handleBugOptimizationSubmitUpdate(ctx, env.Data) + if businessErr != nil { + return businessErr } - if data.Creator == "" { - return errorcode.ParamErr("empty creator") - } - - // 获取创建者uid - accessToken, _ := s.dingtalkClient.GetAccessToken() - creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator) - if err != nil { - return errorcode.ParamErr("invalid data type: %v", err) - } - - // 获取用户详情 - userDetails, err := s.dingtalkClient.QueryUserDetails(c.Context(), creatorId) - if err != nil { - return errorcode.ParamErr("invalid data type: %v", err) - } - if userDetails == nil { - return errorcode.ParamErr("user details not found") - } - unionId := userDetails.UnionID - - // 更新记录 - ok, err := s.dingtalkNotableClient.UpdateRecord(accessToken, &dingtalk_notable.UpdateRecordReq{ - BaseId: data.BaseId, - SheetId: data.SheetId, - RecordId: data.RecordId, - OperatorId: tools_bot.BotBugOptimizationSubmitAdminUnionId, - CreatorUnionId: unionId, - }) - if err != nil { - return errorcode.ParamErr("invalid data type: %v", err) - } - if !ok { - return errorcode.ParamErr("update record failed") - } - - // s.gateway.SendToUid(sessionID, []byte("问题记录即将完成")) + s.sendStreamLog(sessionID, msg) return c.JSON(fiber.Map{"code": 0, "message": "ok"}) default: @@ -227,11 +174,40 @@ func (s *CallbackService) handleDingTalkCallback(c *fiber.Ctx, env Envelope) err } } +// handleBugOptimizationSubmitDone 处理 bug 优化提交完成回调 +func (s *CallbackService) handleBugOptimizationSubmitDone(ctx context.Context, taskData json.RawMessage) (string, *errorcode.BusinessErr) { + var data BugOptimizationSubmitDoneData + if err := json.Unmarshal(taskData, &data); err != nil { + return "", errorcode.ParamErr("invalid data type: %v", err) + } + + if len(data.Receivers) == 0 { + return "", errorcode.ParamErr("empty receivers") + } + // 构建接收者 + receivers := s.getDingtalkReceivers(ctx, data.Receivers) + if receivers == "" { + return "", errorcode.ParamErr("invalid receivers") + } + + // 构建跳转链接 + var detailPage string + if data.DetailPage != "" { + detailPage = util.BuildJumpLink(data.DetailPage, "去查看") + } + + msg := data.Msg + msg = util.ReplacePlaceholder(msg, "receivers", receivers) + msg = util.ReplacePlaceholder(msg, "detail_page", util.EscapeJSONString(detailPage)) + + return msg, nil +} + // getDingtalkReceivers 解析接收者字符串为 DingTalk 用户 ID 列表 func (s *CallbackService) getDingtalkReceivers(ctx context.Context, receiverIds []string) string { var receiverNames []string for _, receiverId := range receiverIds { - userDetails, err := s.dingtalkClient.QueryUserDetails(ctx, receiverId) + userDetails, err := s.dingtalkOldClient.QueryUserDetails(ctx, receiverId) if err != nil { return "" } @@ -245,3 +221,60 @@ func (s *CallbackService) getDingtalkReceivers(ctx context.Context, receiverIds return receivers } + +// sendStreamLog 发送流式日志 +func (s *CallbackService) sendStreamLog(sessionID string, content string) { + streamLog := entitys.Response{ + Index: constants.BotToolsBugOptimizationSubmit, + Content: content, + Type: entitys.ResponseLog, + } + streamLogBytes := pkg.JsonByteIgonErr(streamLog) + s.gateway.SendToUid(sessionID, streamLogBytes) +} + +// handleBugOptimizationSubmitUpdate 处理 bug 优化提交更新回调 +func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context, taskData json.RawMessage) (string, *errorcode.BusinessErr) { + var data BugOptimizationSubmitUpdateData + if err := json.Unmarshal(taskData, &data); err != nil { + return "", errorcode.ParamErr("invalid data type: %v", err) + } + + if data.Creator == "" { + return "", errorcode.ParamErr("empty creator") + } + + // 获取创建者uid + accessToken, _ := s.dingtalkOldClient.GetAccessToken() + creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator) + if err != nil { + return "", errorcode.ParamErr("invalid data type: %v", err) + } + + // 获取用户详情 + userDetails, err := s.dingtalkOldClient.QueryUserDetails(ctx, creatorId) + if err != nil { + return "", errorcode.ParamErr("invalid data type: %v", err) + } + if userDetails == nil { + return "", errorcode.ParamErr("user details not found") + } + unionId := userDetails.UnionID + + // 更新记录 + ok, err := s.dingtalkNotableClient.UpdateRecord(accessToken, &dingtalk.UpdateRecordReq{ + BaseId: data.BaseId, + SheetId: data.SheetId, + RecordId: data.RecordId, + OperatorId: tools_bot.BotBugOptimizationSubmitAdminUnionId, + CreatorUnionId: unionId, + }) + if err != nil { + return "", errorcode.ParamErr("invalid data type: %v", err) + } + if !ok { + return "", errorcode.ParamErr("update record failed") + } + + return "问题记录即将完成", nil +} diff --git a/internal/tools_bot/bug_optimization_submit.go b/internal/tools_bot/bug_optimization_submit.go new file mode 100644 index 0000000..5aee1c2 --- /dev/null +++ b/internal/tools_bot/bug_optimization_submit.go @@ -0,0 +1,115 @@ +package tools_bot + +import ( + errors "ai_scheduler/internal/data/error" + "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" + "ai_scheduler/internal/pkg/l_request" + "context" + "encoding/json" + "fmt" + "time" + + "github.com/gofiber/fiber/v2/log" + "github.com/google/uuid" + "xorm.io/builder" +) + +// BugOptimizationSubmitForm 工单提交表单参数 +type BugOptimizationSubmitForm struct { + Mark string `json:"mark"` // 工单标识 + Text string `json:"text"` // 工单描述 + Img string `json:"img"` // 工单截图 + Creator string `json:"creator"` // 工单创建人 + TaskId string `json:"task_id"` // 当初任务ID +} + +const ( + // 工单QA + BotBugOptimizationSubmitQA = "温子新" + BotBugOptimizationSubmitPM = "贺泽琨" + + // 管理员unionId - fzy + BotBugOptimizationSubmitAdminUnionId = "uoCiPKNdFmuiSFmAiiXmmiSKpQiEiE" +) + +// BugOptimizationSubmit 工单提交 +func (w *BotTool) BugOptimizationSubmit(ctx context.Context, requireData *entitys.RequireData) (err error) { + // 获取用户信息 + cond := builder.NewCond() + cond = cond.And(builder.Eq{"session_id": requireData.Session}) + sessionInfo, err := w.sessionImpl.GetOneBySearch(&cond) + if err != nil { + err = errors.SysErr("获取会话信息失败:%v", err.Error()) + return + } + userName := sessionInfo["user_name"].(string) + + // 构建工单表单参数 + body := BugOptimizationSubmitForm{ + Mark: requireData.Match.Index, + Text: requireData.Req.Text, + Img: requireData.Req.Img, + Creator: userName, + TaskId: uuid.New().String(), + } + + request := l_request.Request{ + Url: "https://connector.dingtalk.com/webhook/flow/10352c521dd02104cee9000c", + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + JsonByte: pkg.JsonByteIgonErr(body), + } + res, err := request.Send() + if err != nil { + log.Errorf("发送请求失败: %s", err.Error()) + return + } + + data := make(map[string]bool) + if err = json.Unmarshal(res.Content, &data); err != nil { + return fmt.Errorf("解析工单响应失败:%w", err) + } + + if data["success"] { + // 记录 task_id 到 session_id 的映射 + w.SetTaskMapping(body.TaskId, requireData.Session) + + // 等待异步回调完成再结束 + for { + sessionID, ok := w.GetSessionByTaskID(body.TaskId) + if !ok || sessionID != requireData.Session { + break + } + entitys.ResLoading(requireData.Ch, requireData.Match.Index, "问题记录中...") + time.Sleep(time.Second) + } + + return + } + + entitys.ResJson(requireData.Ch, requireData.Match.Index, fmt.Sprintf("bug问题请咨询 @%s ,优化建议请咨询 @%s 。", BotBugOptimizationSubmitQA, BotBugOptimizationSubmitPM)) + return +} + +// SetTaskMapping 设置 task_id 到 session_id 的映射(内存版)。 +// 后续考虑使用 Redis,确保幂等与过期清理。 +func (w *BotTool) SetTaskMapping(taskID, sessionID string) { + if taskID == "" || sessionID == "" { + return + } + w.taskMap[taskID] = sessionID +} + +// GetSessionByTaskID 读取映射 +func (w *BotTool) GetSessionByTaskID(taskID string) (string, bool) { + v, ok := w.taskMap[taskID] + return v, ok +} + +// DelTaskMapping 删除 task_id 到 session_id 的映射(内存版)。 +func (w *BotTool) DelTaskMapping(taskID string) { + delete(w.taskMap, taskID) +} diff --git a/internal/tools_bot/dtalk_bot.go b/internal/tools_bot/dtalk_bot.go index 50bb391..18ac27f 100644 --- a/internal/tools_bot/dtalk_bot.go +++ b/internal/tools_bot/dtalk_bot.go @@ -6,17 +6,10 @@ import ( errors "ai_scheduler/internal/data/error" "ai_scheduler/internal/data/impl" "ai_scheduler/internal/entitys" - "ai_scheduler/internal/pkg" - "ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/utils_ollama" "context" - "encoding/json" - "fmt" - "time" "github.com/gofiber/fiber/v2/log" - "github.com/google/uuid" - "xorm.io/builder" ) type BotTool struct { @@ -31,15 +24,6 @@ func NewBotTool(config *config.Config, llm *utils_ollama.Client, sessionImpl *im return &BotTool{config: config, llm: llm, sessionImpl: sessionImpl, taskMap: make(map[string]string)} } -// BugOptimizationSubmitForm 工单提交表单参数 -type BugOptimizationSubmitForm struct { - Mark string `json:"mark"` // 工单标识 - Text string `json:"text"` // 工单描述 - Img string `json:"img"` // 工单截图 - Creator string `json:"creator"` // 工单创建人 - TaskId string `json:"task_id"` // 当初任务ID -} - // Execute 执行直连天下订单详情查询 func (w *BotTool) Execute(ctx context.Context, toolName string, requireData *entitys.RequireData) (err error) { switch toolName { @@ -51,97 +35,3 @@ func (w *BotTool) Execute(ctx context.Context, toolName string, requireData *ent } return } - -const ( - // 工单QA - BotBugOptimizationSubmitQA = "温子新" - BotBugOptimizationSubmitPM = "贺泽琨" - - // 管理员unionId - fzy - BotBugOptimizationSubmitAdminUnionId = "uoCiPKNdFmuiSFmAiiXmmiSKpQiEiE" -) - -// 现存问题: -// 1. 回调时 session 直接传入不安全 todo -// 2. 创建人无法指定[钉钉用户],影响后续状态变化时通知 -// 3. 回调接口,[接收人]、[文档地址不能]动态配置 -// 4. 测试环境与线上环境,使用的不是同一个钉钉主体 -func (w *BotTool) BugOptimizationSubmit(ctx context.Context, requireData *entitys.RequireData) (err error) { - // 获取用户信息 - cond := builder.NewCond() - cond = cond.And(builder.Eq{"session_id": requireData.Session}) - sessionInfo, err := w.sessionImpl.GetOneBySearch(&cond) - if err != nil { - err = errors.SysErr("获取会话信息失败:%v", err.Error()) - return - } - userName := sessionInfo["user_name"].(string) - - // 构建工单表单参数 - body := BugOptimizationSubmitForm{ - Mark: requireData.Match.Index, - Text: requireData.Req.Text, - Img: requireData.Req.Img, - Creator: userName, - TaskId: uuid.New().String(), - } - - request := l_request.Request{ - Url: "https://connector.dingtalk.com/webhook/flow/10352c521dd02104cee9000c", - Method: "POST", - Headers: map[string]string{ - "Content-Type": "application/json", - }, - JsonByte: pkg.JsonByteIgonErr(body), - } - res, err := request.Send() - if err != nil { - log.Errorf("发送请求失败: %s", err.Error()) - return - } - - data := make(map[string]bool) - if err = json.Unmarshal(res.Content, &data); err != nil { - return fmt.Errorf("解析工单响应失败:%w", err) - } - - if data["success"] { - // 记录 task_id 到 session_id 的映射 - w.SetTaskMapping(body.TaskId, requireData.Session) - - // 等待异步回调完成再结束 - for { - sessionID, ok := w.GetSessionByTaskID(body.TaskId) - if !ok || sessionID != requireData.Session { - break - } - entitys.ResLoading(requireData.Ch, requireData.Match.Index, "问题内容记录中...") - time.Sleep(time.Second) - } - - return - } - - entitys.ResJson(requireData.Ch, requireData.Match.Index, fmt.Sprintf("bug问题请咨询 @%s ,优化建议请咨询 @%s 。", BotBugOptimizationSubmitQA, BotBugOptimizationSubmitPM)) - return -} - -// SetTaskMapping 设置 task_id 到 session_id 的映射(内存版)。 -// 后续考虑使用 Redis,确保幂等与过期清理。 -func (w *BotTool) SetTaskMapping(taskID, sessionID string) { - if taskID == "" || sessionID == "" { - return - } - w.taskMap[taskID] = sessionID -} - -// GetSessionByTaskID 读取映射 -func (w *BotTool) GetSessionByTaskID(taskID string) (string, bool) { - v, ok := w.taskMap[taskID] - return v, ok -} - -// DelTaskMapping 删除 task_id 到 session_id 的映射(内存版)。 -func (w *BotTool) DelTaskMapping(taskID string) { - delete(w.taskMap, taskID) -}