ai_scheduler/internal/services/callback.go

281 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/util"
"ai_scheduler/internal/tools_bot"
"context"
"encoding/json"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
// CallbackService 统一回调入口
type CallbackService struct {
cfg *config.Config
gateway *gateway.Gateway
dingtalkOldClient *dingtalk.OldClient
dingtalkContactClient *dingtalk.ContactClient
dingtalkNotableClient *dingtalk.NotableClient
botTool *tools_bot.BotTool
}
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,
dingtalkOldClient: dingtalkOldClient,
dingtalkContactClient: dingtalkContactClient,
dingtalkNotableClient: dingtalkNotableClient,
botTool: botTool,
}
}
// Envelope 回调统一请求体
type Envelope struct {
Action string `json:"action"`
TaskID string `json:"task_id"`
Data json.RawMessage `json:"data"`
}
// bug_optimization_submit 工单回调
const (
ActionBugOptimizationSubmitDone = "bug_optimization_submit_done" // 工单完成回调
ActionBugOptimizationSubmitUpdate = "bug_optimization_submit_update" // 工单更新回调
)
// BugOptimizationSubmitDoneData 工单完成回调数据
type BugOptimizationSubmitDoneData struct {
Receivers []string `json:"receivers"`
DetailPage string `json:"detail_page"`
Msg string `json:"msg"`
}
// BugOptimizationSubmitUpdateData 工单更新回调数据
type BugOptimizationSubmitUpdateData struct {
BaseId string `json:"base_id"` // 表格ID
SheetId string `json:"sheet_id"` // 表单ID
RecordId string `json:"record_id"` // 记录ID
UnionId string `json:"union_id"` // 钉钉用户 UnionID
Creator string `json:"creator"` // 钉钉用户名称
}
// Callback 统一回调处理
// 头部X-Source-Key / X-Timestamp
func (s *CallbackService) Callback(c *fiber.Ctx) error {
// 读取头
sourceKey := strings.TrimSpace(c.Get("X-Source-Key"))
ts := strings.TrimSpace(c.Get("X-Timestamp"))
// 时间窗口(如果提供了 ts 则校验,否则跳过),窗口 5 分钟
if ts != "" && !validateTimestamp(ts, 5*time.Minute) {
return errorcode.AuthNotFound
}
// 解析 Envelope
var env Envelope
if err := json.Unmarshal(c.Body(), &env); err != nil {
return errorcode.ParamErr("invalid json: %v", err)
}
if env.Action == "" || env.TaskID == "" {
return errorcode.ParamErr("missing action/task_id")
}
if env.Data == nil {
return errorcode.ParamErr("missing data")
}
switch sourceKey {
case "dingtalk":
return s.handleDingTalkCallback(c, env)
default:
return errorcode.AuthNotFound
}
}
func validateTimestamp(ts string, window time.Duration) bool {
// 期望毫秒时间戳或秒级,简单容错
// 尝试解析为整数
var n int64
for _, base := range []int64{1, 1000} { // 秒或毫秒
if v, ok := parseInt64(ts); ok {
n = v
// 归一为毫秒
if base == 1 && len(ts) <= 10 {
n = n * 1000
}
now := time.Now().UnixMilli()
diff := now - n
if diff < 0 {
diff = -diff
}
if diff <= window.Milliseconds() {
return true
}
}
}
return false
}
func parseInt64(s string) (int64, bool) {
var n int64
for _, ch := range s {
if ch < '0' || ch > '9' {
return 0, false
}
n = n*10 + int64(ch-'0')
}
return n, true
}
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:
// 业务处理
msg, businessErr := s.handleBugOptimizationSubmitDone(ctx, env.Data)
if businessErr != nil {
return businessErr
}
// 发送日志
s.sendStreamLog(sessionID, msg)
// 删除映射
s.botTool.DelTaskMapping(env.TaskID)
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
case ActionBugOptimizationSubmitUpdate:
// 业务处理
msg, businessErr := s.handleBugOptimizationSubmitUpdate(ctx, env.Data)
if businessErr != nil {
return businessErr
}
s.sendStreamLog(sessionID, msg)
return c.JSON(fiber.Map{"code": 0, "message": "ok"})
default:
return errorcode.ParamErr("unknown action: %s", env.Action)
}
}
// 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.dingtalkOldClient.QueryUserDetails(ctx, receiverId)
if err != nil {
return ""
}
if userDetails == nil {
return ""
}
receiverNames = append(receiverNames, "@"+userDetails.Name)
}
receivers := strings.Join(receiverNames, " ")
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
}