chore: 1. callbackService 结构优化 2. dingtalkClient 目录结构优化 3. tools_bot 结构优化
This commit is contained in:
parent
eaa2d4ca7e
commit
8e74c434ae
|
|
@ -1,4 +1,4 @@
|
||||||
package dingtalk_contact
|
package dingtalk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
|
|
@ -10,12 +10,12 @@ import (
|
||||||
"github.com/alibabacloud-go/tea/tea"
|
"github.com/alibabacloud-go/tea/tea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type ContactClient struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
cli *contact.Client
|
cli *contact.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContactClient(config *config.Config) (*Client, error) {
|
func NewContactClient(config *config.Config) (*ContactClient, error) {
|
||||||
cfg := &openapi.Config{
|
cfg := &openapi.Config{
|
||||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||||
|
|
@ -26,7 +26,7 @@ func NewContactClient(config *config.Config) (*Client, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Client{config: config, cli: c}, nil
|
return &ContactClient{config: config, cli: c}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchUserReq struct {
|
type SearchUserReq struct {
|
||||||
|
|
@ -40,7 +40,7 @@ type SearchUserResp struct {
|
||||||
Body interface{}
|
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 := &contact.SearchUserHeaders{}
|
||||||
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
||||||
resp, err := c.cli.SearchUserWithOptions(&contact.SearchUserRequest{
|
resp, err := c.cli.SearchUserWithOptions(&contact.SearchUserRequest{
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dingtalk_notable
|
package dingtalk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
|
|
@ -10,12 +10,12 @@ import (
|
||||||
"github.com/alibabacloud-go/tea/tea"
|
"github.com/alibabacloud-go/tea/tea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type NotableClient struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
cli *notable.Client
|
cli *notable.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNotableClient(config *config.Config) (*Client, error) {
|
func NewNotableClient(config *config.Config) (*NotableClient, error) {
|
||||||
cfg := &openapi.Config{
|
cfg := &openapi.Config{
|
||||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||||
|
|
@ -26,7 +26,7 @@ func NewNotableClient(config *config.Config) (*Client, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Client{config: config, cli: c}, nil
|
return &NotableClient{config: config, cli: c}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateRecordReq struct {
|
type UpdateRecordReq struct {
|
||||||
|
|
@ -41,7 +41,7 @@ type UpdateRecordsserResp struct {
|
||||||
Body interface{}
|
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 := ¬able.UpdateRecordsHeaders{}
|
||||||
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
||||||
resp, err := c.cli.UpdateRecordsWithOptions(
|
resp, err := c.cli.UpdateRecordsWithOptions(
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package dingtalk
|
package dingtalk
|
||||||
|
|
||||||
|
// 旧版sdk客户端 - 在用,勿删!
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -15,26 +17,26 @@ import (
|
||||||
"github.com/fastwego/dingding"
|
"github.com/fastwego/dingding"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type OldClient struct {
|
||||||
cfg *config.Config
|
config *config.Config
|
||||||
sdkClient *dingding.Client
|
cli *dingding.Client
|
||||||
atm *dingding.DefaultAccessTokenManager
|
atm *dingding.DefaultAccessTokenManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDingTalkClient(cfg *config.Config) *Client {
|
func NewOldClient(config *config.Config) *OldClient {
|
||||||
atm := &dingding.DefaultAccessTokenManager{
|
atm := &dingding.DefaultAccessTokenManager{
|
||||||
Id: cfg.Tools.DingTalkBot.APIKey,
|
Id: config.Tools.DingTalkBot.APIKey,
|
||||||
Name: "access_token",
|
Name: "access_token",
|
||||||
GetRefreshRequestFunc: func() *http.Request {
|
GetRefreshRequestFunc: func() *http.Request {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("appkey", cfg.Tools.DingTalkBot.APIKey)
|
params.Add("appkey", config.Tools.DingTalkBot.APIKey)
|
||||||
params.Add("appsecret", cfg.Tools.DingTalkBot.APISecret)
|
params.Add("appsecret", config.Tools.DingTalkBot.APISecret)
|
||||||
req, _ := http.NewRequest(http.MethodGet, dingding.ServerUrl+"/gettoken?"+params.Encode(), nil)
|
req, _ := http.NewRequest(http.MethodGet, dingding.ServerUrl+"/gettoken?"+params.Encode(), nil)
|
||||||
return req
|
return req
|
||||||
},
|
},
|
||||||
Cache: file.New(os.TempDir()),
|
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 {
|
type UserDetail struct {
|
||||||
|
|
@ -43,7 +45,7 @@ type UserDetail struct {
|
||||||
UnionID string `json:"unionid"`
|
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
|
var r io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
r = bytes.NewReader(body)
|
r = bytes.NewReader(body)
|
||||||
|
|
@ -55,10 +57,10 @@ func (c *Client) do(ctx context.Context, method, path string, body []byte) ([]by
|
||||||
if body != nil {
|
if body != nil {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
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 {
|
body := struct {
|
||||||
UserId string `json:"userid"`
|
UserId string `json:"userid"`
|
||||||
Language string `json:"language,omitempty"`
|
Language string `json:"language,omitempty"`
|
||||||
|
|
@ -82,7 +84,7 @@ func (c *Client) QueryUserDetails(ctx context.Context, userId string) (*UserDeta
|
||||||
return &resp.Result, nil
|
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 {
|
body := struct {
|
||||||
Mobile string `json:"mobile"`
|
Mobile string `json:"mobile"`
|
||||||
}{Mobile: mobile}
|
}{Mobile: mobile}
|
||||||
|
|
@ -104,8 +106,8 @@ func (c *Client) QueryUserDetailsByMobile(ctx context.Context, mobile string) (*
|
||||||
}
|
}
|
||||||
return &resp.Result, nil
|
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()
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,6 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/pkg/dingtalk"
|
"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_langchain"
|
||||||
"ai_scheduler/internal/pkg/utils_ollama"
|
"ai_scheduler/internal/pkg/utils_ollama"
|
||||||
|
|
||||||
|
|
@ -16,7 +14,7 @@ var ProviderSetClient = wire.NewSet(
|
||||||
utils_langchain.NewUtilLangChain,
|
utils_langchain.NewUtilLangChain,
|
||||||
utils_ollama.NewClient,
|
utils_ollama.NewClient,
|
||||||
NewSafeChannelPool,
|
NewSafeChannelPool,
|
||||||
dingtalk.NewDingTalkClient,
|
dingtalk.NewOldClient,
|
||||||
dingtalk_contact.NewContactClient,
|
dingtalk.NewContactClient,
|
||||||
dingtalk_notable.NewNotableClient,
|
dingtalk.NewNotableClient,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/data/constants"
|
||||||
errorcode "ai_scheduler/internal/data/error"
|
errorcode "ai_scheduler/internal/data/error"
|
||||||
|
"ai_scheduler/internal/entitys"
|
||||||
"ai_scheduler/internal/gateway"
|
"ai_scheduler/internal/gateway"
|
||||||
|
"ai_scheduler/internal/pkg"
|
||||||
"ai_scheduler/internal/pkg/dingtalk"
|
"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/pkg/util"
|
||||||
"ai_scheduler/internal/tools_bot"
|
"ai_scheduler/internal/tools_bot"
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -21,17 +22,17 @@ import (
|
||||||
type CallbackService struct {
|
type CallbackService struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
gateway *gateway.Gateway
|
gateway *gateway.Gateway
|
||||||
dingtalkClient *dingtalk.Client
|
dingtalkOldClient *dingtalk.OldClient
|
||||||
dingtalkContactClient *dingtalk_contact.Client
|
dingtalkContactClient *dingtalk.ContactClient
|
||||||
dingtalkNotableClient *dingtalk_notable.Client
|
dingtalkNotableClient *dingtalk.NotableClient
|
||||||
botTool *tools_bot.BotTool
|
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{
|
return &CallbackService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
gateway: gateway,
|
gateway: gateway,
|
||||||
dingtalkClient: dingtalkClient,
|
dingtalkOldClient: dingtalkOldClient,
|
||||||
dingtalkContactClient: dingtalkContactClient,
|
dingtalkContactClient: dingtalkContactClient,
|
||||||
dingtalkNotableClient: dingtalkNotableClient,
|
dingtalkNotableClient: dingtalkNotableClient,
|
||||||
botTool: botTool,
|
botTool: botTool,
|
||||||
|
|
@ -76,7 +77,7 @@ func (s *CallbackService) Callback(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// 时间窗口(如果提供了 ts 则校验,否则跳过),窗口 5 分钟
|
// 时间窗口(如果提供了 ts 则校验,否则跳过),窗口 5 分钟
|
||||||
if ts != "" && !validateTimestamp(ts, 5*time.Minute) {
|
if ts != "" && !validateTimestamp(ts, 5*time.Minute) {
|
||||||
// return errorcode.AuthNotFound
|
return errorcode.AuthNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 Envelope
|
// 解析 Envelope
|
||||||
|
|
@ -135,91 +136,37 @@ func parseInt64(s string) (int64, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CallbackService) handleDingTalkCallback(c *fiber.Ctx, env Envelope) error {
|
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 {
|
switch env.Action {
|
||||||
// bug/优化完成回调
|
// bug/优化完成回调
|
||||||
case ActionBugOptimizationSubmitDone:
|
case ActionBugOptimizationSubmitDone:
|
||||||
// 获取 session_id
|
// 业务处理
|
||||||
sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID)
|
msg, businessErr := s.handleBugOptimizationSubmitDone(ctx, env.Data)
|
||||||
if !ok {
|
if businessErr != nil {
|
||||||
return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID)
|
return businessErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var data BugOptimizationSubmitDoneData
|
// 发送日志
|
||||||
if err := json.Unmarshal(env.Data, &data); err != nil {
|
s.sendStreamLog(sessionID, msg)
|
||||||
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.botTool.DelTaskMapping(env.TaskID)
|
s.botTool.DelTaskMapping(env.TaskID)
|
||||||
|
|
||||||
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
|
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
|
||||||
case ActionBugOptimizationSubmitUpdate:
|
case ActionBugOptimizationSubmitUpdate:
|
||||||
// 获取 session_id
|
// 业务处理
|
||||||
// sessionID, ok := s.botTool.GetSessionByTaskID(env.TaskID)
|
msg, businessErr := s.handleBugOptimizationSubmitUpdate(ctx, env.Data)
|
||||||
// if !ok {
|
if businessErr != nil {
|
||||||
// return errorcode.ParamErr("missing session_id for task_id: %s", env.TaskID)
|
return businessErr
|
||||||
// }
|
|
||||||
|
|
||||||
var data BugOptimizationSubmitUpdateData
|
|
||||||
if err := json.Unmarshal(env.Data, &data); err != nil {
|
|
||||||
return errorcode.ParamErr("invalid data type: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Creator == "" {
|
s.sendStreamLog(sessionID, msg)
|
||||||
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("问题记录即将完成"))
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
|
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
|
||||||
default:
|
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 列表
|
// getDingtalkReceivers 解析接收者字符串为 DingTalk 用户 ID 列表
|
||||||
func (s *CallbackService) getDingtalkReceivers(ctx context.Context, receiverIds []string) string {
|
func (s *CallbackService) getDingtalkReceivers(ctx context.Context, receiverIds []string) string {
|
||||||
var receiverNames []string
|
var receiverNames []string
|
||||||
for _, receiverId := range receiverIds {
|
for _, receiverId := range receiverIds {
|
||||||
userDetails, err := s.dingtalkClient.QueryUserDetails(ctx, receiverId)
|
userDetails, err := s.dingtalkOldClient.QueryUserDetails(ctx, receiverId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -245,3 +221,60 @@ func (s *CallbackService) getDingtalkReceivers(ctx context.Context, receiverIds
|
||||||
|
|
||||||
return receivers
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -6,17 +6,10 @@ import (
|
||||||
errors "ai_scheduler/internal/data/error"
|
errors "ai_scheduler/internal/data/error"
|
||||||
"ai_scheduler/internal/data/impl"
|
"ai_scheduler/internal/data/impl"
|
||||||
"ai_scheduler/internal/entitys"
|
"ai_scheduler/internal/entitys"
|
||||||
"ai_scheduler/internal/pkg"
|
|
||||||
"ai_scheduler/internal/pkg/l_request"
|
|
||||||
"ai_scheduler/internal/pkg/utils_ollama"
|
"ai_scheduler/internal/pkg/utils_ollama"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2/log"
|
"github.com/gofiber/fiber/v2/log"
|
||||||
"github.com/google/uuid"
|
|
||||||
"xorm.io/builder"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BotTool struct {
|
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)}
|
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 执行直连天下订单详情查询
|
// Execute 执行直连天下订单详情查询
|
||||||
func (w *BotTool) Execute(ctx context.Context, toolName string, requireData *entitys.RequireData) (err error) {
|
func (w *BotTool) Execute(ctx context.Context, toolName string, requireData *entitys.RequireData) (err error) {
|
||||||
switch toolName {
|
switch toolName {
|
||||||
|
|
@ -51,97 +35,3 @@ func (w *BotTool) Execute(ctx context.Context, toolName string, requireData *ent
|
||||||
}
|
}
|
||||||
return
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue