diff --git a/config/config.yaml b/config/config.yaml index 6586e20..c41dac1 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -5,6 +5,9 @@ server: ollama: base_url: "http://172.17.0.1:11434" + # model: "qwen3:8b" + # generate_model: "qwen3:8b" + # mapping_model: "qwen3:8b" model: "qwen3-coder:480b-cloud" generate_model: "qwen3-coder:480b-cloud" mapping_model: "deepseek-v3.2:cloud" @@ -36,7 +39,7 @@ sys: channel_pool_len: 100 channel_pool_size: 32 llm_pool_len: 5 - heartbeat_interval: 30 + heartbeat_interval: 300 key: report-api pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池 minIdleConns: 2 #最小空闲连接数 @@ -47,7 +50,7 @@ redis: host: 47.97.27.195:6379 type: node pass: lansexiongdi@666 - key: report-api-test + key: ai_scheduler_prov pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池 minIdleConns: 2 #最小空闲连接数 maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭 @@ -109,7 +112,7 @@ tools: api_key: "7583905168607100978" api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ" zltxResellerAuthProductToManagerAndDefaultLossReason: - base_url: "https://revcl.1688sup.com/api/admin/resellerAuthProductToManagerAndDefaultLossReason" + base_url: "https://revcl.1688sup.com/api/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason" # eino tool 配置 eino_tools: diff --git a/config/config_test.yaml b/config/config_test.yaml index 4a61e1c..ad03eff 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -4,10 +4,10 @@ server: host: "0.0.0.0" ollama: - base_url: "http://127.0.0.1:11434" - model: "qwen3-coder:480b-cloud" - generate_model: "qwen3-coder:480b-cloud" - mapping_model: "deepseek-v3.2:cloud" + base_url: "http://192.168.6.115:11434" + model: "qwen3:8b" + generate_model: "qwen3:8b" + mapping_model: "qwen3:8b" vl_model: "gemini-3-pro-preview" timeout: "120s" level: "info" @@ -26,11 +26,11 @@ coze: lsxd: # 统一登录 - login_url: "https://api.user.1688sup.com/v1/login/phone" - phone: "ORlviZN7N06W2+WKLe76xg==" - password: "V5Uh8C4bamEM6UQZh4TCeQ==" - code: "456789" - check_token_url: "https://api.user.1688sup.com/v1/user/welcome" + login_url: "http://api.test.user.1688sup.com/v1/login/phone" + phone: "OFJ8UpqOlI7+w3Qklf36ZA==" + password: "tEbFegH/DRRh6LutFb7o3g==" + code: "123456" + check_token_url: "http://api.test.user.1688sup.com/v1/user/welcome" sys: @@ -43,7 +43,7 @@ redis: host: 47.97.27.195:6379 type: node pass: lansexiongdi@666 - key: ai_scheduler-test + key: ai_scheduler_test pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池 minIdleConns: 2 #最小空闲连接数 maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭 diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go index f40cc3c..5fbd711 100644 --- a/internal/biz/ding_talk_bot.go +++ b/internal/biz/ding_talk_bot.go @@ -9,6 +9,7 @@ import ( "ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/model" "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg" "ai_scheduler/internal/tools" "ai_scheduler/internal/tools/bbxt" "ai_scheduler/tmpl/dataTemp" @@ -471,11 +472,16 @@ func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.Req rec.Tasks = append(rec.Tasks, entitys.RegistrationTask{ Name: task.Index, - Desc: task.Desc, + Desc: task.TempPrompt, TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建 }) } } + + rec.Ext = pkg.JsonByteIgonErr(&entitys.TaskExt{ + UserName: requireData.Req.SenderNick, + }) + err = d.handle.Recognize(ctx, rec, &do.WithDingTalkBot{}) return } @@ -644,7 +650,7 @@ func (d *DingTalkBotBiz) defaultPrompt() string { - 注意区分 parameters 中的 必须参数(required) 和 可选参数(optional),按下述参数提取规则处理。 - 若**完全无法匹配**,立即设置 is_match: false,并在 chat 中已第一人称的角度提醒用户需要适用何种工具(例:"请问您是要查询订单还是商品呢")。 -1. **参数提取**: +3. **参数提取**: - 根据 parameters 字段列出的参数名,从用户输入中提取对应值。 - **仅提取**明确提及的参数,忽略未列出的内容。 diff --git a/internal/biz/do/handle.go b/internal/biz/do/handle.go index fa6bd57..9b705a8 100644 --- a/internal/biz/do/handle.go +++ b/internal/biz/do/handle.go @@ -207,7 +207,9 @@ func (r *Handle) HandleMatch(ctx context.Context, client *gateway.Client, rec *e case constants.TaskTypeFunc: return r.handleTask(ctx, rec, pointTask) case constants.TaskTypeBot: - return r.handleBot(ctx, rec, pointTask) + return r.HandleBot(ctx, rec, &entitys.Task{ + Index: pointTask.Index, + }) case constants.TaskTypeEinoWorkflow: return r.handleEinoWorkflow(ctx, rec, pointTask) case constants.TaskTypeCozeWorkflow: @@ -416,23 +418,34 @@ func (r *Handle) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Respo } // bot 临时实现,后续转到 eino 工作流 -func (r *Handle) handleBot(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) { +func (r *Handle) HandleBot(ctx context.Context, rec *entitys.Recognize, task *entitys.Task) (err error) { if task.Index == "bug_optimization_submit" { - entitys.ResLoading(rec.Ch, task.Index, "需求记录中...") - - // Ext 中获取 sessionId - sessionID := rec.GetSession() + var unionId string + entitys.ResLoading(rec.Ch, task.Index, "需求记录中...\n") // 获取dingtalk accessToken accessToken, _ := r.dingtalkOldClient.GetAccessToken() - // 获取创建者 dingtalk unionId - unionId := r.getUserDingtalkUnionId(ctx, accessToken, sessionID) + // Ext 中获取 sessionId + taskExt := rec.GetTaskExt() + if taskExt == nil { + return errorcode.ParamErr("taskExt参数错误") + } + if len(taskExt.Session) > 0 { + // 获取创建者 dingtalk unionId + unionId = r.getUserDingtalkUnionId(ctx, accessToken, taskExt.Session) + } else if len(taskExt.UserName) > 0 { + unionId = r.getUserDingtalkUnionIdWithUserName(ctx, accessToken, taskExt.UserName) + } else { + return errorcode.ParamErr("taskExt参数错误,重要参数缺失") + } + // 附件url var attachmentUrl string for _, file := range rec.UserContent.File { attachmentUrl = file.FileUrl break } - recordId, err := r.dingtalkNotableClient.InsertRecord(accessToken, &dingtalk.InsertRecordReq{ + + req := &dingtalk.InsertRecordReq{ BaseId: r.conf.Dingtalk.TableDemand.BaseId, SheetIdOrName: r.conf.Dingtalk.TableDemand.SheetIdOrName, // OperatorId: tool_callback.BotBugOptimizationSubmitAdminUnionId, @@ -440,7 +453,9 @@ func (r *Handle) handleBot(ctx context.Context, rec *entitys.Recognize, task *mo CreatorUnionId: unionId, Content: rec.UserContent.Text, AttachmentUrl: attachmentUrl, - }) + } + + recordId, err := r.dingtalkNotableClient.InsertRecord(accessToken, req) if err != nil { errCode := r.dingtalkNotableClient.GetHTTPStatus(err) // 权限不足 @@ -453,12 +468,16 @@ func (r *Handle) handleBot(ctx context.Context, rec *entitys.Recognize, task *mo if recordId == "" { return errors.NewBusinessErr(422, "创建记录失败,请联系管理员") } - + var detailPage string entitys.ResLog(rec.Ch, task.Index, "需求记录完成") - - // 构建跳转链接 - detailPage := util.BuildJumpLink(r.conf.Dingtalk.TableDemand.Url, "去查看") - + switch rec.OutPutScene { + case entitys.OutPutSceneDingTalk: + // 构建跳转链接 + detailPage = "[去查看](" + r.conf.Dingtalk.TableDemand.Url + ")" + default: + // 构建跳转链接 + detailPage = util.BuildJumpLink(r.conf.Dingtalk.TableDemand.Url, "去查看") + } entitys.ResText(rec.Ch, task.Index, fmt.Sprintf("需求已记录,正在分配相关人员处理,请您耐心等待处理结果。点击查看工单进度:%s", detailPage)) return nil @@ -469,16 +488,21 @@ func (r *Handle) handleBot(ctx context.Context, rec *entitys.Recognize, task *mo // getUserDingtalkUnionId 获取用户的 dingtalk unionId func (r *Handle) getUserDingtalkUnionId(ctx context.Context, accessToken, sessionID string) (unionId string) { - // 查询用户名 + if len(sessionID) == 0 { + // 查询用户名 + return "" + } session, has, err := r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(sessionID)) if err != nil || !has { log.Warnf("session not found: %s", sessionID) return } - creatorName := session.UserName + return r.getUserDingtalkUnionIdWithUserName(ctx, accessToken, session.UserName) +} +func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) { // 获取创建者uid 用户名 -> dingtalk uid - creatorId, err := r.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, creatorName) + creatorId, err := r.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, userName) if err != nil { log.Warnf("search dingtalk user one failed: %v", err) return diff --git a/internal/biz/do/macro.go b/internal/biz/do/macro.go index ae6b9fe..fe1ffc5 100644 --- a/internal/biz/do/macro.go +++ b/internal/biz/do/macro.go @@ -1,11 +1,15 @@ package do import ( + "ai_scheduler/internal/config" "ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/model" "ai_scheduler/internal/pkg" - + "ai_scheduler/internal/pkg/lsxd" "ai_scheduler/internal/tools/bbxt" + "ai_scheduler/utils" + "database/sql" + "errors" "context" "encoding/json" @@ -23,19 +27,25 @@ import ( type Macro struct { botGroupImpl *impl.BotGroupImpl reportDailyCacheImpl *impl.ReportDailyCacheImpl + config *config.Config + rdb *utils.Rdb } func NewMacro( botGroupImpl *impl.BotGroupImpl, reportDailyCacheImpl *impl.ReportDailyCacheImpl, + config *config.Config, + rdb *utils.Rdb, ) *Macro { return &Macro{ botGroupImpl: botGroupImpl, reportDailyCacheImpl: reportDailyCacheImpl, + config: config, + rdb: rdb, } } -type MacroFunc func(ctx context.Context, content string, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) +type MacroFunc func(ctx context.Context, content string, groupConfig *model.AiBotGroupConfig, config *config.Config) (successMsg string, err error, isFinish bool) func (m *Macro) Router(ctx context.Context, reqContent string, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) { reqContent = strings.TrimSpace(reqContent) @@ -47,60 +57,115 @@ func (m *Macro) Router(ctx context.Context, reqContent string, groupConfig *mode case strings.HasPrefix(reqContent, "[负利润分析]导出"): return m.NegativeProfitGet(ctx) case strings.HasPrefix(reqContent, "[负利润分析]更新"): - return m.NegativeProfitUpdate(ctx, reqContent, groupConfig) + return m.NegativeProfitUpdate(ctx, reqContent) + case strings.HasPrefix(reqContent, "[负利润分析]清理"): + return m.NegativeProfitClear() default: } return } -func (m *Macro) NegativeProfitUpdate(ctx context.Context, content string, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) { - //newContent := strings.ReplaceAll(strings.TrimSpace(content), "[负利润分析]更新:", "") - jsonData, err := ParseLossData(content) - if err != nil { - err = fmt.Errorf("解析失败:%v", err) - return - } +func (m *Macro) NegativeProfitClear() (successMsg string, err error, isFinish bool) { dayDate := time.Now().Format(time.DateOnly) cond := builder.NewCond() cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}) cond = cond.And(builder.Eq{"cache_key": dayDate}) + cond = cond.And(builder.Eq{"status": 1}) err = m.reportDailyCacheImpl.UpdateByCond(&cond, &model.AiReportDailyCache{ - Value: pkg.JsonStringIgonErr(jsonData), + Status: 2, }) if err != nil { err = fmt.Errorf("解析失败:%v", err) return } isFinish = true + successMsg = "清理成功" + return +} + +func (m *Macro) NegativeProfitUpdate(ctx context.Context, content string) (successMsg string, err error, isFinish bool) { + //newContent := strings.ReplaceAll(strings.TrimSpace(content), "[负利润分析]更新:", "") + jsonData, err := ParseLossData(content) + b, err := bbxt.NewBbxtTools(m.config, lsxd.NewLogin(m.config, m.rdb)) + if err != nil { + return + } + now := time.Now() + dayData := now.Format(time.DateOnly) + value, err := b.GetMapResellerLossSumProductRelation(ctx, now, m.GetReportCache) + if err != nil { + return + } + var setData = make(map[int32]*bbxt.ResellerLossSumProductRelation) + for k, v := range jsonData { + if _, ok := value[k]; !ok { + continue + } + for productId, product := range v.Products { + if _, ok := value[k].Products[productId]; !ok { + continue + } + if value[k].Products[productId].LossReason == product.LossReason { + continue + } + if _, ex := setData[k]; !ex { + setData[k] = &bbxt.ResellerLossSumProductRelation{ + ResellerName: value[k].ResellerName, + AfterSaleName: value[k].AfterSaleName, + Products: make(map[int32]*bbxt.LossReason), + } + } + setData[k].Products[productId] = &bbxt.LossReason{ + ProductName: product.ProductName, + LossReason: product.LossReason, + } + } + } + + cond := builder.NewCond() + cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}) + cond = cond.And(builder.Eq{"cache_key": dayData}) + cond = cond.And(builder.Eq{"status": 1}) + var cache model.AiReportDailyCache + err = m.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache) + if err != nil { + return + } + if cache.ID == 0 { + + cache = model.AiReportDailyCache{ + CacheKey: dayData, + CacheIndex: bbxt.IndexLossSumDetail, + Value: pkg.JsonStringIgonErr(setData), + } + _, err = m.reportDailyCacheImpl.Add(&cache) + if err != nil { + return + } + } else { + err = m.reportDailyCacheImpl.UpdateByCond(&cond, &model.AiReportDailyCache{ + Value: pkg.JsonStringIgonErr(setData), + }) + if err != nil { + return + } + } + isFinish = true successMsg = "更新成功" return } func (m *Macro) NegativeProfitGet(ctx context.Context) (successMsg string, err error, isFinish bool) { - var ( - data model.AiReportDailyCache - value map[int32]*bbxt.ResellerLossSumProductRelation - ) - isFinish = true - cond := builder.NewCond() - cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}) - cond = cond.And(builder.Eq{"cache_key": time.Now().Format(time.DateOnly)}) - err = m.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &data) + b, err := bbxt.NewBbxtTools(m.config, lsxd.NewLogin(m.config, m.rdb)) if err != nil { - err = fmt.Errorf("获取失败:%v", err) return } - if data.ID == 0 { - successMsg = "暂未获取今日负利润分析数据,请先呼出报表数据" - return - } - err = json.Unmarshal([]byte(data.Value), &value) + now := time.Now() + value, err := b.GetMapResellerLossSumProductRelation(ctx, now, m.GetReportCache) if err != nil { - err = fmt.Errorf("获取失败,格式解析错误:%v", err) return } - //将value转为string //**[供应商id]供应商名称->商务名称**\n //└──商品名称:亏损原因\n @@ -116,6 +181,7 @@ func (m *Macro) NegativeProfitGet(ctx context.Context) (successMsg string, err e } } successMsg = valueString.String() + isFinish = true return } @@ -286,3 +352,109 @@ func ParseLossData(input string) (map[int32]*bbxt.ResellerLossSumProductRelation return result, nil } + +func (m *Macro) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error { + dayDate := day.Format(time.DateOnly) + + // 1. 从 API 获取数据并填充 + apiRelations, err := bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail) + if err != nil { + return fmt.Errorf("get API data failed: %w", err) + } + + // 使用 API 数据填充损失原因 + fillLossReasonFromData(totalDetail, apiRelations, "未填写") + + // 2. 从缓存获取数据并覆盖 + cachedRelations, err := m.getCachedRelations(dayDate) + if err != nil { + return fmt.Errorf("get cache data failed: %w", err) + } + + // 使用缓存数据覆盖损失原因 + if cachedRelations != nil { + fillLossReasonFromData(totalDetail, cachedRelations, "") + } + + return nil +} + +// 从缓存获取关系数据 +func (m *Macro) getCachedRelations(dayDate string) (map[int32]*bbxt.ResellerLossSumProductRelation, error) { + cond := builder.NewCond(). + And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}). + And(builder.Eq{"cache_key": dayDate}). + And(builder.Eq{"status": 1}) + + var cache model.AiReportDailyCache + err := m.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // 缓存不存在是正常情况 + return nil, nil + } + return nil, fmt.Errorf("query cache failed: %w", err) + } + + if cache.ID == 0 { + return nil, nil + } + + var relations map[int32]*bbxt.ResellerLossSumProductRelation + if err := json.Unmarshal([]byte(cache.Value), &relations); err != nil { + return nil, fmt.Errorf("unmarshal cache failed: %w", err) + } + + return relations, nil +} + +// 使用指定数据源填充损失原因 +func fillLossReasonFromData( + totalDetail []*bbxt.ResellerLoss, + relations map[int32]*bbxt.ResellerLossSumProductRelation, + defaultReason string, // 当数据不存在时使用的默认值 +) { + for _, detail := range totalDetail { + resellerRelation, exists := relations[detail.ResellerId] + if !exists { + continue + } + + // 设置售后经理(只有在有值时才设置) + if resellerRelation.AfterSaleName != "" { + detail.Manager = resellerRelation.AfterSaleName + } + + // 为每个产品设置损失原因 + for _, product := range detail.ProductLoss { + setProductLossReason(product, resellerRelation, defaultReason) + } + } +} + +// 设置单个产品的损失原因 +func setProductLossReason( + product *bbxt.ProductLoss, + resellerRelation *bbxt.ResellerLossSumProductRelation, + defaultReason string, +) { + // 如果该经销商没有产品数据,跳过 + if resellerRelation.Products == nil { + return + } + + productRelation, exists := resellerRelation.Products[product.ProductId] + if !exists { + if defaultReason != "" { + product.LossReason = defaultReason + } + return + } + + // 如果有损失原因则设置,否则使用默认值 + if productRelation.LossReason != "" { + product.LossReason = productRelation.LossReason + } else if defaultReason != "" { + product.LossReason = defaultReason + } +} diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go index 49f9059..102ea84 100644 --- a/internal/biz/group_config.go +++ b/internal/biz/group_config.go @@ -1,6 +1,7 @@ package biz import ( + "ai_scheduler/internal/biz/do" "ai_scheduler/internal/biz/tools_regis" "ai_scheduler/internal/config" "ai_scheduler/internal/data/constants" @@ -50,6 +51,8 @@ type GroupConfigBiz struct { conf *config.Config rdb *utils.Rdb dingtalkCardClient *dingtalk.CardClient + macro *do.Macro + handle *do.Handle } // NewDingTalkBotBiz @@ -64,6 +67,8 @@ func NewGroupConfigBiz( rdb *utils.Rdb, toolManager *tools.Manager, dingtalkCardClient *dingtalk.CardClient, + macro *do.Macro, + handle *do.Handle, ) *GroupConfigBiz { return &GroupConfigBiz{ botTools: tools.BootTools, @@ -76,6 +81,8 @@ func NewGroupConfigBiz( rdb: rdb, toolManager: toolManager, dingtalkCardClient: dingtalkCardClient, + macro: macro, + handle: handle, } } @@ -101,7 +108,7 @@ func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model. return } - reports, err = reportList.DailyReport(ctx, time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient, g.GetReportCache) + reports, err = reportList.DailyReport(ctx, time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient, g.macro.GetReportCache) if err != nil { return } @@ -178,7 +185,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz var reports []*bbxt.ReportRes switch rec.Match.Index { case "report_loss_analysis": - repo, _err := rep.StatisOursProductLossSum(ctx, t, g.GetReportCache) + repo, _err := rep.StatisOursProductLossSum(ctx, t, g.macro.GetReportCache) if _err != nil { return _err } @@ -199,7 +206,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz reports = append(reports, repo) case "report_daily": product := strings.Split(groupConfig.ProductName, ",") - repo, _err := rep.DailyReport(ctx, t, bbxt.DownWardValue, product, bbxt.SumFilter, nil, g.GetReportCache) + repo, _err := rep.DailyReport(ctx, t, bbxt.DownWardValue, product, bbxt.SumFilter, nil, g.macro.GetReportCache) if _err != nil { return _err } @@ -260,12 +267,18 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize break } } + rec.OutPutScene = entitys.OutPutSceneDingTalk if pointTask == nil || pointTask.Index == "other" { return g.otherTask(ctx, rec) } + switch constants.TaskType(pointTask.Type) { case constants.TaskTypeFunc: return g.handleTask(ctx, rec, pointTask) + case constants.TaskTypeBot: + return g.handle.HandleBot(ctx, rec, &entitys.Task{ + Index: pointTask.Index, + }) case constants.TaskTypeReport: return g.handleReport(ctx, rec, pointTask, groupConfig) case constants.TaskTypeCozeWorkflow: @@ -318,7 +331,6 @@ func (q *GroupConfigBiz) handleTask(ctx context.Context, rec *entitys.Recognize, if err != nil { return } - err = q.toolManager.ExecuteTool(ctx, configData.Tool, rec) if err != nil { return diff --git a/internal/data/model/ai_report_daily_cache.gen.go b/internal/data/model/ai_report_daily_cache.gen.go index bf7057c..3f7015b 100644 --- a/internal/data/model/ai_report_daily_cache.gen.go +++ b/internal/data/model/ai_report_daily_cache.gen.go @@ -12,6 +12,7 @@ type AiReportDailyCache struct { CacheKey string `gorm:"column:cache_key;not null;default:1;comment:索引方式,可以是任意类型" json:"cache_key"` // 索引方式,可以是任意类型 Value string `gorm:"column:value;comment:类型下所需路由以及参数" json:"value"` // 类型下所需路由以及参数 CacheIndex string `gorm:"column:cache_index;not null;comment:类型" json:"cache_index"` // 类型 + Status int32 `gorm:"column:status;not null;default:1" json:"status"` } // TableName AiReportDailyCache's table name diff --git a/internal/domain/workflow/recharge/statistics_ours_product.go b/internal/domain/workflow/recharge/statistics_ours_product.go index 278d32b..3ef2e5a 100644 --- a/internal/domain/workflow/recharge/statistics_ours_product.go +++ b/internal/domain/workflow/recharge/statistics_ours_product.go @@ -125,7 +125,7 @@ func (w *statisticsOursProduct) formatContext(ctx context.Context, input *Statis Time: time.Now(), StartTime: startTime, EndTime: endTime, - Title: fmt.Sprintf("截止 %s 亏损100以上我们的商品统计", endTimeStr), + Title: fmt.Sprintf("截止 %s 电商系统亏损100以上我们的商品统计", endTimeStr), }, nil } diff --git a/internal/entitys/bot.go b/internal/entitys/bot.go index 55d5743..7f84e29 100644 --- a/internal/entitys/bot.go +++ b/internal/entitys/bot.go @@ -29,3 +29,7 @@ func (d *DingTalkBot) GetAppKey() dingtalk.AppKey { AppSecret: d.ClientSecret, } } + +type Task struct { + Index string `json:"bot_index"` +} diff --git a/internal/entitys/recognize.go b/internal/entitys/recognize.go index fcd2fe5..57a92e0 100644 --- a/internal/entitys/recognize.go +++ b/internal/entitys/recognize.go @@ -14,8 +14,14 @@ type Recognize struct { Ch chan Response Match *Match Ext []byte + OutPutScene OutPutScene } +type OutPutScene string + +const OutPutSceneDingTalk OutPutScene = "markdown" +const OutPutFormatHtml OutPutScene = "html" + type TaskExt struct { Auth string `json:"auth"` Session string `json:"session"` @@ -23,6 +29,7 @@ type TaskExt struct { SessionInfo model.AiSession Sys model.AiSy KnowledgeConf KnowledgeBaseRequest + UserName string } type RegistrationTask struct { diff --git a/internal/entitys/types.go b/internal/entitys/types.go index 815eeba..ab9e245 100644 --- a/internal/entitys/types.go +++ b/internal/entitys/types.go @@ -64,7 +64,9 @@ type FunctionCall struct { type ToolDefinition struct { Type string `json:"type"` Function FunctionDef `json:"function"` + AuthFunc AuthFunc `json:"function"` } +type AuthFunc func(rec *Recognize) (token []byte, err error) // FunctionDef 函数定义 type FunctionDef struct { @@ -77,7 +79,7 @@ type FunctionDef struct { type Tool interface { Name() string Description() string - Definition() ToolDefinition + Definition(ctx context.Context) ToolDefinition Execute(ctx context.Context, requireData *Recognize) error } diff --git a/internal/pkg/lsxd/login.go b/internal/pkg/lsxd/login.go index 13b7f1c..8d33341 100644 --- a/internal/pkg/lsxd/login.go +++ b/internal/pkg/lsxd/login.go @@ -165,7 +165,8 @@ func (l *Login) cacheToken(ctx context.Context, token string) error { if token == "" { return errors.New("token is empty") } - return l.redisCli.Set(ctx, l.getCacheKey(), token, constants.EXPIRE_LSXD_TOKEN).Err() + key := l.getCacheKey() + return l.redisCli.Set(ctx, key, token, constants.EXPIRE_LSXD_TOKEN).Err() } func (l *Login) doRequest(ctx context.Context, method string, url string, authorization string, body []byte) (int, error) { @@ -190,7 +191,7 @@ func (l *Login) doRequestWithBody(ctx context.Context, method string, url string req.Header.Set("Content-Type", contentType) } if authorization != "" { - req.Header.Set("Authorization", authorization) + req.Header.Set("Authorization", "Bearer "+authorization) } resp, err := (&http.Client{}).Do(req) diff --git a/internal/pkg/utils_ollama/client.go b/internal/pkg/utils_ollama/client.go index febd8cd..16928f6 100644 --- a/internal/pkg/utils_ollama/client.go +++ b/internal/pkg/utils_ollama/client.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "sync" + "time" "github.com/ollama/ollama/api" ) @@ -43,11 +44,12 @@ func (c *Client) ToolSelect(ctx context.Context, messages []api.Message, tools [ // 构建聊天请求 req := &api.ChatRequest{ - Model: c.config.Model, - Messages: messages, - Stream: new(bool), // 设置为false,不使用流式响应 - Think: &api.ThinkValue{Value: false}, - Tools: tools, + Model: c.config.Model, + Messages: messages, + Stream: new(bool), // 设置为false,不使用流式响应 + Think: &api.ThinkValue{Value: false}, + Tools: tools, + KeepAlive: &api.Duration{Duration: 24 * time.Hour}, } err = c.client.Chat(ctx, req, func(resp api.ChatResponse) error { res = resp diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go index 2ef4640..4554c4d 100644 --- a/internal/tools/bbxt/api.go +++ b/internal/tools/bbxt/api.go @@ -4,13 +4,14 @@ import ( "ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/util" - "encoding/json" "fmt" "net/http" "net/url" "strings" + + "github.com/go-kratos/kratos/v2/log" ) type StatisOursProductLossSumReq struct { @@ -252,10 +253,13 @@ func GetManagerAndDefaultLossReasonApi(param *GetManagerAndDefaultLossReasonRequ "Authorization": fmt.Sprintf("Bearer %s", token), }, } + log.Debug(pkg.JsonStringIgonErr(req.Headers)) res, err := req.Send() if err != nil { + log.Debug(err.Error()) return nil, err } + log.Debug(string(res.Content)) if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason) } diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go index de1e638..7a03cce 100644 --- a/internal/tools/bbxt/bbxt.go +++ b/internal/tools/bbxt/bbxt.go @@ -31,7 +31,7 @@ var ( SumFilter int32 = -150 ) -var resellerBlackListProduct = []string{ +var ResellerBlackListProduct = []string{ "悦跑", "电商-独立", "蓝星严选连续包月", @@ -103,9 +103,7 @@ func (b *BbxtTools) DailyReport( return } - -// StatisOursProductLossSum 负利润分析 -func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, initFunc LossSumInitFunc) (report []*ReportRes, err error) { +func (b *BbxtTools) ResellerLossSort(ctx context.Context, now time.Time) ([]*ResellerLoss, error) { ct := []string{ time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"), adjustedTime(now), //adjustedTime(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())), @@ -115,13 +113,10 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, Ct: ct, }) if err != nil { - return + return nil, err } var ( resellerMap = make(map[int32]*ResellerLoss) - total [][]string - gt []*ResellerLoss - totalDetail []*ResellerLoss ) for _, info := range data.List { @@ -166,14 +161,66 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, sort.Slice(resellers, func(i, j int) bool { return resellers[i].Total < resellers[j].Total }) - var ( - totalSum float64 + return resellers, nil +} +func (b *BbxtTools) GetMapResellerLossSumProductRelation(ctx context.Context, now time.Time, initFunc LossSumInitFunc) (map[int32]*ResellerLossSumProductRelation, error) { + resellers, err := b.ResellerLossSort(ctx, now) + if err != nil { + return nil, err + } + var totalDetail []*ResellerLoss + for _, v := range resellers { + if v.Total <= -100 && !slices.Contains(ResellerBlackListProduct, v.ResellerName) { + + totalDetail = append(totalDetail, v) + } + + } + if len(totalDetail) > 0 { + err = initFunc(ctx, now, totalDetail, b) + if err != nil { + return nil, err + } + } + value := b.ResellerLossToMapResellerLossSumProductRelation(totalDetail) + return value, nil +} + +func (b *BbxtTools) ResellerLossToMapResellerLossSumProductRelation(totalDetail []*ResellerLoss) map[int32]*ResellerLossSumProductRelation { + relations := make(map[int32]*ResellerLossSumProductRelation) + for _, detail := range totalDetail { + if _, ok := relations[detail.ResellerId]; !ok { + relations[detail.ResellerId] = &ResellerLossSumProductRelation{ + ResellerName: detail.ResellerName, + AfterSaleName: detail.Manager, + Products: make(map[int32]*LossReason), + } + } + for _, product := range detail.ProductLoss { + relations[detail.ResellerId].Products[product.ProductId] = &LossReason{ + ProductName: product.ProductName, + LossReason: product.LossReason, + } + } + + } + return relations +} + +// StatisOursProductLossSum 负利润分析 +func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, initFunc LossSumInitFunc) (report []*ReportRes, err error) { + resellers, err := b.ResellerLossSort(ctx, now) + var ( + total [][]string + gt []*ResellerLoss + totalDetail []*ResellerLoss + totalSum float64 totalSum500 float64 ) // 构建分组 for _, v := range resellers { - if v.Total <= -100 && !slices.Contains(resellerBlackListProduct, v.ResellerName) { + if v.Total <= -100 && !slices.Contains(ResellerBlackListProduct, v.ResellerName) { total = append(total, []string{ fmt.Sprintf("%s", v.ResellerName), fmt.Sprintf("%.2f", v.Total), @@ -181,7 +228,7 @@ func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, totalSum += v.Total totalDetail = append(totalDetail, v) } - if v.Total <= -500 && !slices.Contains(resellerBlackListProduct, v.ResellerName) { + if v.Total <= -500 && !slices.Contains(ResellerBlackListProduct, v.ResellerName) { gt = append(gt, v) totalSum500 += v.Total } @@ -263,7 +310,7 @@ func (b *BbxtTools) GetResellerLossMannagerAndLossReasonFromApi(ctx context.Cont for _, product := range v.ProductLoss { relationMap[v.ResellerId].Products[product.ProductId] = &LossReason{ ProductName: product.ProductName, - LossReason: "未填写", // 初始化为未填写 + LossReason: "", // 初始化为未填写 } } } @@ -356,10 +403,6 @@ func (b *BbxtTools) GetProfitRankingSum(now time.Time) (report *ReportRes, err e // GetStatisOfficialProductSum 销量同比分析 func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []string) (report *ReportRes, err error) { - var productMap = make(map[string]int) - for k, v := range productName { - productMap[v] = k - } ct := []string{ time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"), adjustedTime(now), @@ -381,7 +424,11 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str if err != nil { return } - var total = make([][]string, len(ids)) + + // 创建临时map存储产品数据 + productDataMap := make(map[string][]string) + var productNamesInResult []string + for _, v := range data.OfficialProductSum { var ( yeterDatyDiff string @@ -397,7 +444,8 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str } else { lastWeekDiff = fmt.Sprintf("%s↓%d", GreenStyle, v.HistoryTwoDiff) } - total[productMap[v.OfficialProductName]] = []string{ + + rowData := []string{ fmt.Sprintf("%s", v.OfficialProductName), fmt.Sprintf("%d", v.CurrentNum), fmt.Sprintf("%d", v.HistoryOneNum), @@ -406,7 +454,20 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str lastWeekDiff, } + // 存储到map中,key为产品名 + productDataMap[v.OfficialProductName] = rowData + productNamesInResult = append(productNamesInResult, v.OfficialProductName) } + + // 按照productName的顺序构建total + var total [][]string + + for _, name := range productName { + if rowData, exists := productDataMap[name]; exists { + total = append(total, rowData) + } + } + timeCh := now.Format("1月2日15点") title := "截至" + timeCh + "销售同比分析" //总量生成excel diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go index 0bf9b61..3d0f623 100644 --- a/internal/tools/bbxt/bbxt_test.go +++ b/internal/tools/bbxt/bbxt_test.go @@ -72,22 +72,29 @@ func Test_GetStatisOfficialProductSumDecline(t *testing.T) { } s := "官方--腾讯-周卡,官方--腾讯-月卡,官方--腾讯-季卡,官方--腾讯-年卡,官方--优酷周卡,官方--优酷月卡,官方--优酷季卡,官方--优酷年卡,官方--爱奇艺-周卡,官方--爱奇艺-月卡,官方--爱奇艺-季卡,官方--爱奇艺-年卡,官方--芒果-PC周卡,官方--芒果-PC月卡,官方--芒果-PC季卡,官方--美团外卖红包5元,官方--美团外卖红包10元,官方--QQ音乐-绿钻月卡,官方--饿了么超级会员月卡,官方--网易云黑胶vip月卡,官方--喜马拉雅巅峰会员月卡" //s := "官方--QQ音乐-绿钻月卡" - now := time.Now() - report, err := o.GetStatisOfficialProductSumDecline(time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, now.Location()), 1000, strings.Split(s, ","), -150) + report, err := o.GetStatisOfficialProductSumDecline(time.Now(), 1000, strings.Split(s, ","), -150) t.Log(report, err) } func Test_GetStatisOfficialProductSum(t *testing.T) { - + run() configs := configConfig o, err := NewBbxtTools(nil, lsxd.NewLogin(configs, utils.NewRdb(configConfig))) if err != nil { panic(err) } - s := "官方--美团外卖红包5元,官方--美团外卖红包10元,官方--饿了么超级会员月卡,官方--网易云黑胶vip月卡,官方--喜马拉雅巅峰会员月卡,官方--芒果-PC季卡,官方--芒果-PC月卡,官方--芒果-PC周卡,官方--腾讯-周卡,官方--优酷周卡,官方--QQ音乐-绿钻月卡,官方--爱奇艺-周卡,官方--腾讯-月卡,官方--腾讯-季卡,官方--腾讯-年卡,官方--优酷月卡,官方--优酷季卡,官方--优酷年卡,官方--爱奇艺-月卡,官方--爱奇艺-季卡,官方--爱奇艺-年卡" - report, err := o.GetStatisOfficialProductSum(time.Now(), strings.Split(s, ",")) + s := "官方--腾讯-周卡,官方--腾讯-月卡,官方--腾讯-季卡,官方--腾讯-年卡,官方--优酷周卡,官方--优酷月卡,官方--优酷季卡,官方--优酷年卡,官方--爱奇艺-周卡,官方--爱奇艺-月卡,官方--爱奇艺-季卡,官方--爱奇艺-年卡,官方--芒果-PC周卡,官方--芒果-PC月卡,官方--芒果-PC季卡,官方--美团外卖红包5元,官方--美团外卖红包10元,官方--QQ音乐-绿钻月卡,官方--饿了么超级会员月卡,官方--网易云黑胶vip月卡,官方--喜马拉雅巅峰会员月卡" + now := time.Now() + noon := time.Date( + now.Year(), + now.Month(), + now.Day(), + 12, 0, 0, 0, + now.Location(), + ) + report, err := o.GetStatisOfficialProductSum(noon, strings.Split(s, ",")) t.Log(report, err) diff --git a/internal/tools/manager.go b/internal/tools/manager.go index eedb4ee..d46d067 100644 --- a/internal/tools/manager.go +++ b/internal/tools/manager.go @@ -6,6 +6,7 @@ import ( "ai_scheduler/internal/pkg/utils_ollama" "ai_scheduler/internal/tools/public" zltxtool "ai_scheduler/internal/tools/zltx" + "ai_scheduler/utils" "context" "fmt" @@ -15,10 +16,11 @@ import ( type Manager struct { tools map[string]entitys.Tool llm *utils_ollama.Client + rdb *utils.Rdb } // NewManager 创建工具管理器 -func NewManager(config *config.Config, llm *utils_ollama.Client) *Manager { +func NewManager(config *config.Config, llm *utils_ollama.Client, rdb *utils.Rdb) *Manager { m := &Manager{ tools: make(map[string]entitys.Tool), llm: llm, @@ -26,7 +28,7 @@ func NewManager(config *config.Config, llm *utils_ollama.Client) *Manager { // 注册直连天下订单详情工具 if config.Tools.ZltxOrderDetail.Enabled { - zltxOrderDetailTool := zltxtool.NewZltxOrderDetailTool(config.Tools.ZltxOrderDetail, m.llm) + zltxOrderDetailTool := zltxtool.NewZltxOrderDetailTool(config, rdb, m.llm) m.tools[zltxOrderDetailTool.Name()] = zltxOrderDetailTool } @@ -43,7 +45,7 @@ func NewManager(config *config.Config, llm *utils_ollama.Client) *Manager { } //注册直连天下订单统计工具 if config.Tools.ZltxOrderStatistics.Enabled { - zltxOrderStatisticsTool := zltxtool.NewZltxOrderStatisticsTool(config.Tools.ZltxOrderStatistics) + zltxOrderStatisticsTool := zltxtool.NewZltxOrderStatisticsTool(config, rdb) m.tools[zltxOrderStatisticsTool.Name()] = zltxOrderStatisticsTool } @@ -104,5 +106,10 @@ func (m *Manager) ExecuteTool(ctx context.Context, name string, rec *entitys.Rec return fmt.Errorf("tool not found: %s", name) } + return m.ExecuteToolWithToolObj(ctx, tool, rec) +} + +// ExecuteTool 执行工具 +func (m *Manager) ExecuteToolWithToolObj(ctx context.Context, tool entitys.Tool, rec *entitys.Recognize) error { return tool.Execute(ctx, rec) } diff --git a/internal/tools/public/coze_company.go b/internal/tools/public/coze_company.go index e061965..722423d 100644 --- a/internal/tools/public/coze_company.go +++ b/internal/tools/public/coze_company.go @@ -50,7 +50,7 @@ func (c *CozeCompany) Description() string { } // Definition 返回工具定义 -func (c *CozeCompany) Definition() entitys.ToolDefinition { +func (c *CozeCompany) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ diff --git a/internal/tools/public/coze_express.go b/internal/tools/public/coze_express.go index 58e6172..f8608fc 100644 --- a/internal/tools/public/coze_express.go +++ b/internal/tools/public/coze_express.go @@ -47,7 +47,7 @@ func (c *CozeExpress) Description() string { } // Definition 返回工具定义 -func (c *CozeExpress) Definition() entitys.ToolDefinition { +func (c *CozeExpress) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ diff --git a/internal/tools/public/konwledge_base.go b/internal/tools/public/konwledge_base.go index 48ddc1e..133b63c 100644 --- a/internal/tools/public/konwledge_base.go +++ b/internal/tools/public/konwledge_base.go @@ -38,7 +38,7 @@ func (k *KnowledgeBaseTool) Description() string { } // Definition 返回工具定义 -func (k *KnowledgeBaseTool) Definition() entitys.ToolDefinition { +func (k *KnowledgeBaseTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ diff --git a/internal/tools/public/normal_chat.go b/internal/tools/public/normal_chat.go index cde667c..9206187 100644 --- a/internal/tools/public/normal_chat.go +++ b/internal/tools/public/normal_chat.go @@ -38,7 +38,7 @@ type NormalChat struct { } // Definition 返回工具定义 -func (w *NormalChatTool) Definition() entitys.ToolDefinition { +func (w *NormalChatTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{} } diff --git a/internal/tools/public/weather.go b/internal/tools/public/weather.go index ca36907..84b7af1 100644 --- a/internal/tools/public/weather.go +++ b/internal/tools/public/weather.go @@ -35,7 +35,7 @@ func (w *WeatherTool) Description() string { } // Definition 返回工具定义 -func (w *WeatherTool) Definition() entitys.ToolDefinition { +func (w *WeatherTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ diff --git a/internal/tools/zltx/order_after_reseller.go b/internal/tools/zltx/order_after_reseller.go index fcb71e0..effa369 100644 --- a/internal/tools/zltx/order_after_reseller.go +++ b/internal/tools/zltx/order_after_reseller.go @@ -33,7 +33,7 @@ func (t *OrderAfterSaleResellerTool) Description() string { } // 未使用-仅实现接口 -func (t *OrderAfterSaleResellerTool) Definition() entitys.ToolDefinition { +func (t *OrderAfterSaleResellerTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{} } diff --git a/internal/tools/zltx/order_after_reseller_batch.go b/internal/tools/zltx/order_after_reseller_batch.go index 664d3d5..e775f0b 100644 --- a/internal/tools/zltx/order_after_reseller_batch.go +++ b/internal/tools/zltx/order_after_reseller_batch.go @@ -31,7 +31,7 @@ func (t *OrderAfterSaleResellerBatchTool) Description() string { } // 未使用-仅实现接口 -func (t *OrderAfterSaleResellerBatchTool) Definition() entitys.ToolDefinition { +func (t *OrderAfterSaleResellerBatchTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{} } diff --git a/internal/tools/zltx/order_after_supplier.go b/internal/tools/zltx/order_after_supplier.go index 196fb64..2a81809 100644 --- a/internal/tools/zltx/order_after_supplier.go +++ b/internal/tools/zltx/order_after_supplier.go @@ -33,7 +33,7 @@ func (t *OrderAfterSaleSupplierTool) Description() string { } // 未使用-仅实现接口 -func (t *OrderAfterSaleSupplierTool) Definition() entitys.ToolDefinition { +func (t *OrderAfterSaleSupplierTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{} } diff --git a/internal/tools/zltx/zltx_order_detail.go b/internal/tools/zltx/zltx_order_detail.go index 6e33e63..7546149 100644 --- a/internal/tools/zltx/zltx_order_detail.go +++ b/internal/tools/zltx/zltx_order_detail.go @@ -4,26 +4,30 @@ import ( "ai_scheduler/internal/config" "ai_scheduler/internal/entitys" "ai_scheduler/internal/pkg" + "ai_scheduler/internal/pkg/lsxd" "ai_scheduler/internal/pkg/rec_extra" "ai_scheduler/internal/pkg/utils_ollama" + "ai_scheduler/utils" "context" "encoding/json" "fmt" + "strings" + "time" "gitea.cdlsxd.cn/self-tools/l_request" - "github.com/gofiber/fiber/v2/log" "github.com/ollama/ollama/api" ) // ZltxOrderDetailTool 直连天下订单详情工具 type ZltxOrderDetailTool struct { - config config.ToolConfig + config *config.Config llm *utils_ollama.Client + rdb *utils.Rdb } // NewZltxOrderDetailTool 创建直连天下订单详情工具 -func NewZltxOrderDetailTool(config config.ToolConfig, llm *utils_ollama.Client) *ZltxOrderDetailTool { - return &ZltxOrderDetailTool{config: config, llm: llm} +func NewZltxOrderDetailTool(config *config.Config, rdb *utils.Rdb, llm *utils_ollama.Client) *ZltxOrderDetailTool { + return &ZltxOrderDetailTool{config: config, rdb: rdb, llm: llm} } // Name 返回工具名称 @@ -37,7 +41,7 @@ func (w *ZltxOrderDetailTool) Description() string { } // Definition 返回工具定义 -func (w *ZltxOrderDetailTool) Definition() entitys.ToolDefinition { +func (w *ZltxOrderDetailTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ @@ -54,6 +58,19 @@ func (w *ZltxOrderDetailTool) Definition() entitys.ToolDefinition { "required": []string{"number"}, }, }, + AuthFunc: func(rec *entitys.Recognize) ([]byte, error) { + ext, err := rec_extra.GetTaskRecExt(rec) + if err != nil { + return nil, err + } + if len(ext.Auth) > 0 { + return []byte(ext.Auth), nil + } else { + login := lsxd.NewLogin(w.config, w.rdb) + token := login.GetToken(ctx) + return []byte(token), nil + } + }, } } @@ -62,14 +79,74 @@ type ZltxOrderDetailRequest struct { OrderNumber interface{} `json:"order_number"` } +//type ZltxOrderDetailResponse struct { +// Code int `json:"code"` +// Error string `json:"error"` +// Data ZltxOrderDetailData `json:"data"` +// Mes string `json:"mes"` +//} + // ZltxOrderDetailResponse 直连天下订单详情响应 type ZltxOrderDetailResponse struct { - Code int `json:"code"` - Error string `json:"error"` - Data ZltxOrderDetailData `json:"data"` - Mes string `json:"mes"` + Code int `json:"code"` + Data Data `json:"data"` + Error string `json:"error"` } +type Data struct { + Direct *Direct `json:"direct"` + Order *Order `json:"order"` +} + +type Direct struct { + SerialNumber string `json:"serialNumber"` + OrderOrderNumber string `json:"orderOrderNumber"` + TerminalAccount string `json:"terminalAccount"` + OursProductId int `json:"oursProductId"` + OursProductName string `json:"oursProductName"` + Status int `json:"status"` + TradePrice float64 `json:"tradePrice"` + PlatformProductId int `json:"platformProductId"` + PlatformProductName string `json:"platformProductName"` + PlatformId int `json:"platformId"` + PlatformName string `json:"platformName"` + PlatformPrice float64 `json:"platformPrice"` + CreateTime int `json:"createTime"` + ExecuteTime int `json:"executeTime"` + Type int `json:"type"` + Reason string `json:"reason"` + ResellerId int `json:"resellerId"` + ResellerName string `json:"resellerName"` + Aftermarket int `json:"aftermarket"` + PurchasePrice float64 `json:"purchasePrice"` + NeedAi bool `json:"needAi"` + AiReason string `json:"aiReason"` +} + +type Order struct { + Id string `json:"id"` + ResellerId int `json:"resellerId"` + ResellerName string `json:"resellerName"` + Status int `json:"status"` + PayStatus int `json:"payStatus"` + CreateTime int `json:"createTime"` + PayTime int `json:"payTime"` + Type int `json:"type"` + Account string `json:"account"` + Quantity int `json:"quantity"` + Amount float64 `json:"amount"` + PayAmount float64 `json:"payAmount"` + ResellerOrderNumber string `json:"resellerOrderNumber"` + Price int `json:"price"` + NotifyTime int `json:"notifyTime"` + FinishTime int `json:"finishTime"` + Aftermarket int `json:"aftermarket"` + Remark string `json:"remark"` + OursProductId int `json:"oursProductId"` + OursProductName string `json:"oursProductName"` + OursProductPrice int `json:"oursProductPrice"` + TradePrice float64 `json:"tradePrice"` +} type ZltxOrderLogResponse struct { Code int `json:"code"` Error string `json:"error"` @@ -93,7 +170,7 @@ func (w *ZltxOrderDetailTool) Execute(ctx context.Context, rec *entitys.Recogniz } // 这里可以集成真实的直连天下订单详情API - return w.getZltxOrderDetail(rec, req.OrderNumber) + return w.getZltxOrderDetail(ctx, rec, req.OrderNumber) } func (r *ZltxOrderDetailRequest) UnmarshalJSON(data []byte) error { @@ -113,8 +190,8 @@ func (r *ZltxOrderDetailRequest) UnmarshalJSON(data []byte) error { } // getMockZltxOrderDetail 获取模拟直连天下订单详情数据 -func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number interface{}) (err error) { - log.Infof("订单编号:%v,类型:%v") +func (w *ZltxOrderDetailTool) getZltxOrderDetail(ctx context.Context, rec *entitys.Recognize, number interface{}) (err error) { + var orderNum string switch number.(type) { case int, int32, int64: @@ -127,15 +204,15 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number orderNum = fmt.Sprintf("%v", number) } - ext, err := rec_extra.GetTaskRecExt(rec) + token, err := w.Definition(ctx).AuthFunc(rec) if err != nil { - return + return err } //查询订单详情 req := l_request.Request{ - Url: fmt.Sprintf(w.config.BaseURL, orderNum), + Url: fmt.Sprintf(w.config.Tools.ZltxOrderDetail.BaseURL, orderNum), Headers: map[string]string{ - "Authorization": fmt.Sprintf("Bearer %s", ext.Auth), + "Authorization": fmt.Sprintf("Bearer %s", token), }, Method: "GET", } @@ -156,15 +233,17 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number if err = json.Unmarshal(res.Content, &resData); err != nil { return } - entitys.ResJson(rec.Ch, w.Name(), res.Text) - - if resData.Data.Direct != nil { + if rec.OutPutScene == entitys.OutPutSceneDingTalk { + entitys.ResJson(rec.Ch, w.Name(), resData.ToForm()) + } else { + entitys.ResJson(rec.Ch, w.Name(), res.Text) entitys.ResLoading(rec.Ch, w.Name(), "正在分析订单日志") - + } + if resData.Data.Direct != nil { req = l_request.Request{ - Url: fmt.Sprintf(w.config.AddURL, resData.Data.Direct["orderOrderNumber"].(string), resData.Data.Direct["serialNumber"].(string)), + Url: fmt.Sprintf(w.config.Tools.ZltxOrderDetail.AddURL, resData.Data.Direct.OrderOrderNumber, resData.Data.Direct.SerialNumber), Headers: map[string]string{ - "Authorization": fmt.Sprintf("Bearer %s", ext.Auth), + "Authorization": fmt.Sprintf("Bearer %s", token), }, Method: "GET", } @@ -184,7 +263,7 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number return fmt.Errorf("订单日志解析失败:%s", err) } - err = w.llm.ChatStream(context.TODO(), rec.Ch, []api.Message{ + err = w.llm.ChatStream(ctx, rec.Ch, []api.Message{ { Role: "system", Content: "你是一个订单日志助手。用户可能会提供订单日志,你需要按以下规则处理:\n" + @@ -216,3 +295,95 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number } return } + +func (z *ZltxOrderDetailResponse) ToForm() string { + var res strings.Builder + res.WriteString("**普通订单** \n") + res.WriteString("\n## 订单号:" + z.Data.Order.Id) + res.WriteString("\n## 分销商订单号:" + z.Data.Order.ResellerOrderNumber) + res.WriteString("\n## 分销商:" + z.Data.Order.ResellerName) + res.WriteString("\n## 订单商品:" + z.Data.Order.OursProductName) + res.WriteString("\n## 充值账号:" + z.Data.Order.Account) + res.WriteString("\n## 折扣价/原价:" + fmt.Sprintf("%.2f", z.Data.Order.TradePrice)) + res.WriteString("\n## 数量:" + fmt.Sprintf("%d", z.Data.Order.Quantity)) + res.WriteString("\n## 订单总额:" + fmt.Sprintf("%.2f", z.Data.Order.Amount)) + res.WriteString("\n## 订单状态:" + orderStatusMap(z.Data.Order.Status)) + res.WriteString("\n## 支付状态:" + orderPayStatusMap(z.Data.Order.Status)) + res.WriteString("\n## 创建时间:" + unixToDateFormat(z.Data.Order.CreateTime)) + res.WriteString("\n## 完成时间:" + unixToDateFormat(z.Data.Order.FinishTime)) + res.WriteString("\n---\n") + + res.WriteString("\n**充值流水** \n") + res.WriteString("\n## 订单号:" + z.Data.Direct.OrderOrderNumber) + res.WriteString("\n## 流水号:" + z.Data.Direct.SerialNumber) + res.WriteString("\n## 商品:" + z.Data.Direct.OursProductName) + res.WriteString("\n## 使用接口:" + z.Data.Direct.PlatformName) + res.WriteString("\n## 接口商品名称:" + z.Data.Direct.PlatformProductName) + res.WriteString("\n## 上游价:" + fmt.Sprintf("%.2f", z.Data.Direct.PlatformPrice)) + res.WriteString("\n## 下游价:" + fmt.Sprintf("%.2f", z.Data.Direct.TradePrice)) + res.WriteString("\n## 上游采购价:" + fmt.Sprintf("%.2f", z.Data.Direct.PurchasePrice)) + res.WriteString("\n## 充值账号:" + z.Data.Direct.TerminalAccount) + res.WriteString("\n## 创建时间:" + unixToDateFormat(z.Data.Direct.CreateTime)) + res.WriteString("\n## 处理时间:" + unixToDateFormat(z.Data.Direct.ExecuteTime)) + res.WriteString("\n## 状态:" + directStatusMap(z.Data.Direct.Status)) + res.WriteString("\n") + res.WriteString("\n") + res.WriteString("\n") + return res.String() +} + +func orderStatusMap(status int) string { + var OrderStatus = map[int]string{ + 0: "下单中", + 1: "订单完成", + 2: "部分成功", + 3: "充值处理中", + 4: "已暂停", + -1: "关闭订单", + -2: "全部失败", + } + if _, ok := OrderStatus[status]; !ok { + return "未知" + } + return OrderStatus[status] +} + +func unixToDateFormat(unix int) string { + return time.Unix(int64(unix), 0).Format("2006-01-02 15:04:05") +} +func orderPayStatusMap(status int) string { + var OrderPayStatus = map[int]string{ + 0: "未支付", + 1: "支付中", + 2: "支付成功", + 3: "退款中", + 4: "全部退款", + 5: "部分退款", + -1: "支付失败", + -2: "退款失败", + } + if _, ok := OrderPayStatus[status]; !ok { + return "未知" + } + return OrderPayStatus[status] +} + +func directStatusMap(status int) string { + var DirectStatus = map[int]string{ + 0: "待充值", + 1: "充值成功", + 2: "充值中", + -1: "充值失败", + -2: "失败重试", + -98: "下单异常", + -99: "查询异常", + -6: "手动失败", + -5: "手动重试", + -4: "叠加卡单", + -3: "卡单", + } + if _, ok := DirectStatus[status]; !ok { + return "未知" + } + return DirectStatus[status] +} diff --git a/internal/tools/zltx/zltx_order_direct_log.go b/internal/tools/zltx/zltx_order_direct_log.go index 188a954..3ec9156 100644 --- a/internal/tools/zltx/zltx_order_direct_log.go +++ b/internal/tools/zltx/zltx_order_direct_log.go @@ -23,7 +23,7 @@ func (t *ZltxOrderLogTool) Description() string { return "查询订单日志" } -func (t *ZltxOrderLogTool) Definition() entitys.ToolDefinition { +func (t *ZltxOrderLogTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ diff --git a/internal/tools/zltx/zltx_product.go b/internal/tools/zltx/zltx_product.go index 0c63d84..2ed7e78 100644 --- a/internal/tools/zltx/zltx_product.go +++ b/internal/tools/zltx/zltx_product.go @@ -25,7 +25,7 @@ func (z ZltxProductTool) Description() string { return "获取直连天下商品信息" } -func (z ZltxProductTool) Definition() entitys.ToolDefinition { +func (z ZltxProductTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ diff --git a/internal/tools/zltx/zltx_statistics.go b/internal/tools/zltx/zltx_statistics.go index 29e143a..6c12235 100644 --- a/internal/tools/zltx/zltx_statistics.go +++ b/internal/tools/zltx/zltx_statistics.go @@ -3,7 +3,9 @@ package zltx import ( "ai_scheduler/internal/config" "ai_scheduler/internal/entitys" + "ai_scheduler/internal/pkg/lsxd" "ai_scheduler/internal/pkg/rec_extra" + "ai_scheduler/utils" "context" "encoding/json" "fmt" @@ -13,7 +15,8 @@ import ( ) type ZltxOrderStatisticsTool struct { - config config.ToolConfig + config *config.Config + rdb *utils.Rdb } func (z ZltxOrderStatisticsTool) Name() string { @@ -24,7 +27,7 @@ func (z ZltxOrderStatisticsTool) Description() string { return "通过账号获取订单统计信息" } -func (z ZltxOrderStatisticsTool) Definition() entitys.ToolDefinition { +func (z ZltxOrderStatisticsTool) Definition(ctx context.Context) entitys.ToolDefinition { return entitys.ToolDefinition{ Type: "function", Function: entitys.FunctionDef{ @@ -41,6 +44,19 @@ func (z ZltxOrderStatisticsTool) Definition() entitys.ToolDefinition { "required": []string{"number"}, }, }, + AuthFunc: func(rec *entitys.Recognize) ([]byte, error) { + ext, err := rec_extra.GetTaskRecExt(rec) + if err != nil { + return nil, err + } + if len(ext.Auth) > 0 { + return []byte(ext.Auth), nil + } else { + login := lsxd.NewLogin(z.config, z.rdb) + token := login.GetToken(ctx) + return []byte(token), nil + } + }, } } @@ -56,7 +72,7 @@ func (z ZltxOrderStatisticsTool) Execute(ctx context.Context, rec *entitys.Recog if req.Number == nil { return fmt.Errorf("number is required") } - return z.getZltxOrderStatistics(req.Number, rec) + return z.getZltxOrderStatistics(ctx, req.Number, rec) } type ZltxOrderStatisticsResponse struct { @@ -76,17 +92,17 @@ type ZltxOrderStatisticsData struct { Total int `json:"total"` } -func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number interface{}, rec *entitys.Recognize) error { - ext, err := rec_extra.GetTaskRecExt(rec) +func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(ctx context.Context, number interface{}, rec *entitys.Recognize) error { + token, err := z.Definition(ctx).AuthFunc(rec) if err != nil { return err } //查询订单详情 - url := fmt.Sprintf("%s%s", z.config.BaseURL, fmt.Sprintf("%v", number)) + url := fmt.Sprintf("%s%s", z.config.Tools.ZltxOrderStatistics.BaseURL, fmt.Sprintf("%v", number)) req := l_request.Request{ Url: url, Headers: map[string]string{ - "Authorization": fmt.Sprintf("Bearer %s", ext.Auth), + "Authorization": fmt.Sprintf("Bearer %s", string(token)), }, Method: "GET", } @@ -116,8 +132,9 @@ func (z ZltxOrderStatisticsTool) getZltxOrderStatistics(number interface{}, rec return nil } -func NewZltxOrderStatisticsTool(config config.ToolConfig) *ZltxOrderStatisticsTool { +func NewZltxOrderStatisticsTool(config *config.Config, rdb *utils.Rdb) *ZltxOrderStatisticsTool { return &ZltxOrderStatisticsTool{ config: config, + rdb: rdb, } } diff --git a/tmpl/excel_temp/kshj_gt.xlsx b/tmpl/excel_temp/kshj_gt.xlsx index ef1033d..6496930 100644 Binary files a/tmpl/excel_temp/kshj_gt.xlsx and b/tmpl/excel_temp/kshj_gt.xlsx differ diff --git a/tmpl/excel_temp/kshj_total_ana.xlsx b/tmpl/excel_temp/kshj_total_ana.xlsx index c4824cd..a2b8178 100644 Binary files a/tmpl/excel_temp/kshj_total_ana.xlsx and b/tmpl/excel_temp/kshj_total_ana.xlsx differ