Merge branch 'v3' into feature/fzy/refine

This commit is contained in:
fuzhongyun 2025-12-15 16:43:54 +08:00
commit 4b83314849
49 changed files with 1653 additions and 312 deletions

View File

@ -5,6 +5,7 @@ package main
import ( import (
"ai_scheduler/internal/biz" "ai_scheduler/internal/biz"
"ai_scheduler/internal/biz/handle/dingtalk"
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/domain/workflow" "ai_scheduler/internal/domain/workflow"
@ -31,6 +32,7 @@ func InitializeApp(*config.Config, log.AllLogger) (*server.Servers, func(), erro
impl.ProviderImpl, impl.ProviderImpl,
utils.ProviderUtils, utils.ProviderUtils,
tools_bot.ProviderSetBotTools, tools_bot.ProviderSetBotTools,
dingtalk.ProviderSetDingTalk,
)) ))
} }

View File

@ -23,7 +23,7 @@ redis:
host: 47.97.27.195:6379 host: 47.97.27.195:6379
type: node type: node
pass: lansexiongdi@666 pass: lansexiongdi@666
key: report-api-test key: report-api
pollSize: 5 #连接池大小不配置或配置为0表示不启用连接池 pollSize: 5 #连接池大小不配置或配置为0表示不启用连接池
minIdleConns: 2 #最小空闲连接数 minIdleConns: 2 #最小空闲连接数
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭 maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭

View File

@ -71,6 +71,20 @@ tools:
zltxOrderAfterSaleResellerBatch: zltxOrderAfterSaleResellerBatch:
enabled: true enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai" base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai"
weather:
enabled: true
base_url: "https://restapi.amap.com/v3/weather/weatherInfo"
api_key: "12afbde5ab78cb7e575ff76bd0bdef2b"
cozeExpress:
enabled: true
base_url: "https://api.coze.cn"
api_key: "7582477438102552616"
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"
cozeCompany:
enabled: true
base_url: "https://api.coze.cn"
api_key: "7583905168607100978"
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"

4
go.mod
View File

@ -52,6 +52,7 @@ require (
github.com/clbanning/mxj/v2 v2.5.5 // indirect github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2 // indirect github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2 // indirect
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@ -61,8 +62,9 @@ require (
github.com/fasthttp/websocket v1.5.3 // indirect github.com/fasthttp/websocket v1.5.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/goph/emperror v0.17.2 // indirect github.com/goph/emperror v0.17.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect

6
go.sum
View File

@ -141,6 +141,8 @@ github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2/go.mod h1:S4OkvglPY9hsm9tXe
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20 h1:m6P88V9lLrxZsE7uj9otq7l7nqDuCSAJ86KhzRlWf0M=
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20/go.mod h1:wdT5CFt/sFsWz9hna2Z7DWzUra9spx0SoX1PUZyoSB0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -204,6 +206,8 @@ github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPArei
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w= github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU= github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -273,6 +277,8 @@ github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25d
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=

View File

@ -2,12 +2,13 @@ package biz
import ( import (
"ai_scheduler/internal/biz/do" "ai_scheduler/internal/biz/do"
"ai_scheduler/internal/biz/handle/dingtalk"
"ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/mapstructure"
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
@ -22,6 +23,7 @@ type DingTalkBotBiz struct {
botConfigImpl *impl.BotConfigImpl botConfigImpl *impl.BotConfigImpl
replier *chatbot.ChatbotReplier replier *chatbot.ChatbotReplier
log log.Logger log log.Logger
dingTalkUser *dingtalk.User
} }
// NewDingTalkBotBiz // NewDingTalkBotBiz
@ -29,12 +31,15 @@ func NewDingTalkBotBiz(
do *do.Do, do *do.Do,
handle *do.Handle, handle *do.Handle,
botConfigImpl *impl.BotConfigImpl, botConfigImpl *impl.BotConfigImpl,
dingTalkUser *dingtalk.User,
) *DingTalkBotBiz { ) *DingTalkBotBiz {
return &DingTalkBotBiz{ return &DingTalkBotBiz{
do: do, do: do,
handle: handle, handle: handle,
botConfigImpl: botConfigImpl, botConfigImpl: botConfigImpl,
replier: chatbot.NewChatbotReplier(), replier: chatbot.NewChatbotReplier(),
dingTalkUser: dingTalkUser,
} }
} }
@ -47,10 +52,11 @@ func (d *DingTalkBotBiz) GetDingTalkBotCfgList() (dingBotList []entitys.DingTalk
err = d.botConfigImpl.GetRangeToMapStruct(&cond, &botConfig) err = d.botConfigImpl.GetRangeToMapStruct(&cond, &botConfig)
for _, v := range botConfig { for _, v := range botConfig {
var config entitys.DingTalkBot var config entitys.DingTalkBot
err = mapstructure.Decode(v, &config) err = json.Unmarshal([]byte(v.BotConfig), &config)
if err != nil { if err != nil {
d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error()) d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error())
} }
config.BotIndex = v.BotIndex
dingBotList = append(dingBotList, config) dingBotList = append(dingBotList, config)
} }
return return
@ -64,8 +70,8 @@ func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallb
} }
entitys.ResLog(requireData.Ch, "recognize_start", "收到消息,正在处理中,请稍等") entitys.ResLog(requireData.Ch, "recognize_start", "收到消息,正在处理中,请稍等")
requireData.Sys, err = d.do.GetSysInfoForDingTalkBot(requireData) requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, data.SenderStaffId, dingtalk.WithId(1))
requireData.Tasks, err = d.do.GetTasks(requireData.Sys.SysID)
return return
} }

View File

@ -131,7 +131,8 @@ func (d *Do) loadSystemInfo(ctx context.Context, client *gateway.Client, require
// 获取任务列表的辅助函数 // 获取任务列表的辅助函数
func (d *Do) loadTaskList(ctx context.Context, client *gateway.Client, requireData *entitys.RequireData) error { func (d *Do) loadTaskList(ctx context.Context, client *gateway.Client, requireData *entitys.RequireData) error {
if taskInfo := client.GetTasks(); len(taskInfo) == 0 { if taskInfo := client.GetTasks(); len(taskInfo) == 0 {
tasks, err := d.GetTasks(requireData.Sys.SysID) // 从数据库获取任务列表, 0 表示获取公共的任务
tasks, err := d.GetTasks(requireData.Sys.SysID, 0)
if err != nil { if err != nil {
return err return err
} }
@ -140,6 +141,7 @@ func (d *Do) loadTaskList(ctx context.Context, client *gateway.Client, requireDa
} else { } else {
requireData.Tasks = taskInfo requireData.Tasks = taskInfo
} }
return nil return nil
} }
@ -225,7 +227,7 @@ func (d *Do) GetSysInfo(requireData *entitys.RequireData) (sysInfo model.AiSy, e
func (d *Do) GetSysInfoForDingTalkBot(requireData *entitys.RequireDataDingTalkBot) (sysInfo model.AiSy, err error) { func (d *Do) GetSysInfoForDingTalkBot(requireData *entitys.RequireDataDingTalkBot) (sysInfo model.AiSy, err error) {
cond := builder.NewCond() cond := builder.NewCond()
cond = cond.And(builder.Eq{"app_key": requireData.Key}) cond = cond.And(builder.Eq{"app_key": requireData.Auth})
cond = cond.And(builder.IsNull{"delete_at"}) cond = cond.And(builder.IsNull{"delete_at"})
cond = cond.And(builder.Eq{"status": 1}) cond = cond.And(builder.Eq{"status": 1})
err = d.sysImpl.GetOneBySearchToStrut(&cond, &sysInfo) err = d.sysImpl.GetOneBySearchToStrut(&cond, &sysInfo)
@ -242,12 +244,12 @@ func (d *Do) getSessionChatHis(requireData *entitys.RequireData) (his []model.Ai
return return
} }
func (d *Do) GetTasks(sysId int32) (tasks []model.AiTask, err error) { func (d *Do) GetTasks(sysId ...int32) (tasks []model.AiTask, err error) {
cond := builder.NewCond() cond := builder.NewCond()
cond = cond.And(builder.Eq{"sys_id": sysId}) //cond = cond.And(builder.Eq{"sys_id": sysId})
cond = cond.And(builder.IsNull{"delete_at"}) cond = cond.And(builder.IsNull{"delete_at"})
cond = cond.And(builder.Eq{"status": 1}) cond = cond.And(builder.Eq{"status": 1}.And(builder.In("sys_id", sysId)))
_, err = d.taskImpl.GetListToStruct(&cond, nil, &tasks, "") _, err = d.taskImpl.GetListToStruct(&cond, nil, &tasks, "")
return return

View File

@ -14,6 +14,7 @@ import (
"ai_scheduler/internal/pkg/mapstructure" "ai_scheduler/internal/pkg/mapstructure"
"ai_scheduler/internal/pkg/util" "ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/public"
"ai_scheduler/internal/tools_bot" "ai_scheduler/internal/tools_bot"
"context" "context"
"encoding/json" "encoding/json"
@ -52,6 +53,7 @@ func NewHandle(
func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptProcessor PromptOption) (match entitys.Match, err error) { func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptProcessor PromptOption) (match entitys.Match, err error) {
entitys.ResLog(rec.Ch, "recognize_start", "准备意图识别") entitys.ResLog(rec.Ch, "recognize_start", "准备意图识别")
prompt, err := promptProcessor.CreatePrompt(ctx, rec) prompt, err := promptProcessor.CreatePrompt(ctx, rec)
//意图识别 //意图识别
recognizeMsg, err := r.Ollama.IntentRecognize(ctx, &entitys.ToolSelect{ recognizeMsg, err := r.Ollama.IntentRecognize(ctx, &entitys.ToolSelect{
@ -189,7 +191,7 @@ func (r *Handle) handleKnowle(ctx context.Context, requireData *entitys.RequireD
return fmt.Errorf("tool not found: %s", configData.Tool) return fmt.Errorf("tool not found: %s", configData.Tool)
} }
if knowledgeTool, ok := tool.(*tools.KnowledgeBaseTool); !ok { if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok {
return fmt.Errorf("未找到知识库Tool: %s", configData.Tool) return fmt.Errorf("未找到知识库Tool: %s", configData.Tool)
} else { } else {
host = knowledgeTool.GetConfig().BaseURL host = knowledgeTool.GetConfig().BaseURL
@ -200,7 +202,7 @@ func (r *Handle) handleKnowle(ctx context.Context, requireData *entitys.RequireD
// 知识库的session为空请求知识库获取, 并绑定 // 知识库的session为空请求知识库获取, 并绑定
if requireData.SessionInfo.KnowlegeSessionID == "" { if requireData.SessionInfo.KnowlegeSessionID == "" {
// 请求知识库 // 请求知识库
if sessionIdKnowledge, err = tools.GetKnowledgeBaseSession(host, requireData.Sys.KnowlegeBaseID, requireData.Sys.KnowlegeTenantKey); err != nil { if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, requireData.Sys.KnowlegeBaseID, requireData.Sys.KnowlegeTenantKey); err != nil {
return return
} }

View File

@ -0,0 +1,118 @@
package dingtalk
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/utils"
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/gofiber/fiber/v2/log"
"github.com/redis/go-redis/v9"
"xorm.io/builder"
)
type Auth struct {
redis *redis.Client
cfg *config.Config
botConfigImpl *impl.BotConfigImpl
}
func NewAuth(cfg *config.Config, redis *utils.Rdb, botConfigImpl *impl.BotConfigImpl) *Auth {
return &Auth{
redis: redis.Rdb,
cfg: cfg,
botConfigImpl: botConfigImpl,
}
}
func (a *Auth) GetAccessToken(ctx context.Context, clientId string, clientSecret string) (authInfo *AuthInfo, err error) {
if clientId == "" {
return nil, errors.New("clientId is empty")
}
accessToken := a.redis.Get(ctx, a.getKey(clientId)).Val()
if accessToken == "" {
dingTalkAuthRes, _err := a.getNewAccessToken(ctx, clientId, clientSecret)
if _err != nil {
return nil, _err
}
err = a.redis.SetEx(ctx, a.getKey(clientId), dingTalkAuthRes.AccessToken, time.Duration(dingTalkAuthRes.ExpireIn-3600)*time.Second).Err()
if err != nil {
return
}
accessToken = dingTalkAuthRes.AccessToken
}
return &AuthInfo{
ClientId: clientId,
ClientSecret: clientSecret,
AccessToken: accessToken,
}, nil
}
func (a *Auth) getKey(clientId string) string {
return a.cfg.Redis.Key + ":" + constants.DingTalkAuthBaseKeyPrefix + ":" + clientId
}
func (a *Auth) getNewAccessToken(ctx context.Context, clientId string, clientSecret string) (auth DingTalkAuthIRes, err error) {
if clientId == "" || clientSecret == "" {
err = errors.New("clientId or clientSecret is empty")
return
}
req := l_request.Request{
Method: http.MethodPost,
Url: "https://api.dingtalk.com/v1.0/oauth2/accessToken",
Json: map[string]interface{}{
"appKey": clientId,
"appSecret": clientSecret,
},
}
res, err := req.Send()
if err != nil {
return
}
err = json.Unmarshal(res.Content, &auth)
return
}
func (a *Auth) GetTokenFromBotOption(ctx context.Context, botOption ...BotOption) (token *AuthInfo, err error) {
botInfo := &Bot{}
for _, option := range botOption {
option(botInfo)
}
if botInfo.id == 0 && botInfo.botConfig == nil {
err = errors.New("botInfo is nil")
return
}
if botInfo.botConfig == nil {
var botConfigDo model.AiBotConfig
cond := builder.NewCond()
cond = cond.And(builder.Eq{"bot_id": botInfo.id})
err = a.botConfigImpl.GetOneBySearchToStrut(&cond, &botConfigDo)
if err != nil {
return
}
if botConfigDo.BotID == 0 {
err = errors.New("未找到机器人服务配置")
return
}
botInfo.botConfig = &botConfigDo
}
var botConfig entitys.DingTalkBot
err = json.Unmarshal([]byte(botInfo.botConfig.BotConfig), &botConfig)
if err != nil {
log.Infof("初始化“%s”失败:%s", botInfo.botConfig.BotName, err.Error())
return
}
return a.GetAccessToken(ctx, botConfig.ClientId, botConfig.ClientSecret)
}

View File

@ -0,0 +1,107 @@
package dingtalk
import (
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request"
"context"
"encoding/json"
"fmt"
"net/http"
"xorm.io/builder"
)
type Dept struct {
dingDeptImpl *impl.BotDeptImpl
auth *Auth
}
func NewDept(dingDeptImpl *impl.BotDeptImpl, auth *Auth) *Dept {
return &Dept{
dingDeptImpl: dingDeptImpl,
auth: auth,
}
}
func (d *Dept) GetDeptInfoByDeptIds(ctx context.Context, deptIds []int, authInfo *AuthInfo) (depts []*entitys.Dept, err error) {
if len(deptIds) == 0 || authInfo == nil {
return
}
var deptsInfo []model.AiBotDept
cond := builder.NewCond()
cond = cond.And(builder.Eq{"dingtalk_dept_id": deptIds})
err = d.dingDeptImpl.GetRangeToMapStruct(&cond, &deptsInfo)
if err != nil {
return
}
var existDept = make([]int, len(deptsInfo), 0)
for _, dept := range deptsInfo {
depts = append(depts, &entitys.Dept{
DeptId: int(dept.DeptID),
Name: dept.Name,
})
existDept = append(existDept, int(dept.DeptID))
}
diff := pkg.Difference(deptIds, existDept)
if len(diff) > 0 {
deptDo := make([]model.AiBotDept, 0)
for _, deptId := range diff {
deptInfo, _err := d.GetDeptInfoFromDingTalk(ctx, deptId, authInfo.AccessToken)
if _err != nil {
return nil, _err
}
depts = append(depts, &entitys.Dept{
DeptId: deptInfo.DeptId,
Name: deptInfo.Name,
})
deptDo = append(deptDo, model.AiBotDept{
DingtalkDeptID: int32(deptInfo.DeptId),
Name: deptInfo.Name,
})
}
if len(deptDo) > 0 {
_, err = d.dingDeptImpl.Add(deptDo)
if err != nil {
return nil, err
}
}
}
return
}
func (d *Dept) GetDeptInfoFromDingTalk(ctx context.Context, deptId int, token string) (depts DeptResResult, err error) {
if deptId == 0 || len(token) == 0 {
return
}
req := l_request.Request{
Method: http.MethodPost,
Url: constants.GetDingTalkRequestUrl(constants.RequestUrlGetDeptGet, map[string]string{
"access_token": token,
}),
Json: map[string]interface{}{
"dept_id": deptId,
},
}
res, _err := req.Send()
if _err != nil {
err = _err
return
}
var deptInfo DeptRes
err = json.Unmarshal(res.Content, &deptInfo)
if err != nil {
return
}
if deptInfo.Errcode != 0 {
fmt.Errorf("钉钉请求报错:%s", deptInfo.Errmsg)
}
return deptInfo.DeptResResult, err
}

View File

@ -0,0 +1,21 @@
package dingtalk
import "ai_scheduler/internal/data/model"
type Bot struct {
id int
botConfig *model.AiBotConfig
}
type BotOption func(*Bot)
func WithId(id int) BotOption {
return func(b *Bot) {
b.id = id
}
}
func WithBootConfig(BotConfig *model.AiBotConfig) BotOption {
return func(bot *Bot) {
bot.botConfig = BotConfig
}
}

View File

@ -0,0 +1,11 @@
package dingtalk
import (
"github.com/google/wire"
)
var ProviderSetDingTalk = wire.NewSet(
NewUser,
NewAuth,
NewDept,
)

View File

@ -0,0 +1,84 @@
package dingtalk
import "time"
type DingTalkAuthIRes struct {
AccessToken string `json:"accessToken"`
ExpireIn int64 `json:"expireIn"`
}
type UserInfoRes struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Result UserInfoResResult `json:"result"`
RequestId string `json:"request_id"`
}
type UserInfoResResult struct {
Active bool `json:"active"`
Admin bool `json:"admin"`
Avatar string `json:"avatar"`
Boss bool `json:"boss"`
CreateTime time.Time `json:"create_time"`
DeptIdList []int `json:"dept_id_list"`
DeptOrderList []struct {
DeptId int `json:"dept_id"`
Order int64 `json:"order"`
} `json:"dept_order_list"`
ExclusiveAccount bool `json:"exclusive_account"`
HideMobile bool `json:"hide_mobile"`
HiredDate int64 `json:"hired_date"`
JobNumber string `json:"job_number"`
LeaderInDept []struct {
DeptId int `json:"dept_id"`
Leader bool `json:"leader"`
} `json:"leader_in_dept"`
ManagerUserid string `json:"manager_userid"`
Name string `json:"name"`
RealAuthed bool `json:"real_authed"`
RoleList []struct {
GroupName string `json:"group_name"`
Id int `json:"id"`
Name string `json:"name"`
} `json:"role_list"`
Senior bool `json:"senior"`
Title string `json:"title"`
Unionid string `json:"unionid"`
Userid string `json:"userid"`
}
type DeptRes struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
DeptResResult DeptResResult `json:"result"`
RequestId string `json:"request_id"`
}
type DeptResResult struct {
DeptPermits []int `json:"dept_permits"`
OuterPermitUsers []string `json:"outer_permit_users"`
DeptManagerUseridList []string `json:"dept_manager_userid_list"`
OrgDeptOwner string `json:"org_dept_owner"`
OuterDept bool `json:"outer_dept"`
DeptGroupChatId string `json:"dept_group_chat_id"`
GroupContainSubDept bool `json:"group_contain_sub_dept"`
AutoAddUser bool `json:"auto_add_user"`
HideDept bool `json:"hide_dept"`
Name string `json:"name"`
OuterPermitDepts []int `json:"outer_permit_depts"`
UserPermits []interface{} `json:"user_permits"`
DeptId int `json:"dept_id"`
CreateDeptGroup bool `json:"create_dept_group"`
Order int `json:"order"`
Code string `json:"code"`
UnionDeptExt struct {
CorpId string `json:"corp_id"`
DeptId int `json:"dept_id"`
} `json:"union_dept_ext"`
}
type AuthInfo struct {
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
AccessToken string `json:"accessToken"`
}

View File

@ -0,0 +1,126 @@
package dingtalk
import (
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
)
type User struct {
dingUserImpl *impl.BotUserImpl
botConfigImpl *impl.BotConfigImpl
auth *Auth
dept *Dept
}
func NewUser(
dingUserImpl *impl.BotUserImpl,
auth *Auth,
dept *Dept,
) *User {
return &User{
dingUserImpl: dingUserImpl,
auth: auth,
dept: dept,
}
}
func (u *User) GetUserInfoFromBot(ctx context.Context, staffId string, botOption ...BotOption) (userInfo *entitys.DingTalkUserInfo, err error) {
if len(staffId) == 0 {
return
}
user, err := u.dingUserImpl.GetByStaffId(staffId)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return
}
}
authInfo, err := u.auth.GetTokenFromBotOption(ctx, botOption...)
if err != nil || authInfo == nil {
return
}
//如果没有找到,则新增
if user == nil {
DingUserInfo, _err := u.getUserInfoFromDingTalk(ctx, authInfo.AccessToken, staffId)
if _err != nil {
return nil, _err
}
user = &model.AiBotUser{
StaffID: DingUserInfo.Userid,
Name: DingUserInfo.Name,
Title: DingUserInfo.Title,
//Extension: DingUserInfo.Extension,
DeptIDList: strings.Join(pkg.SliceIntToString(DingUserInfo.DeptIdList), ","),
IsBoss: int32(pkg.Ter(DingUserInfo.Boss, constants.IsBossTrue, constants.IsBossFalse)),
IsSenior: int32(pkg.Ter(DingUserInfo.Senior, constants.IsSeniorTrue, constants.IsSeniorFalse)),
HiredDate: time.UnixMilli(DingUserInfo.HiredDate),
}
_, err = u.dingUserImpl.Add(user)
if err != nil {
return
}
}
userInfo = &entitys.DingTalkUserInfo{
UserId: int(user.UserID),
StaffId: user.StaffID,
Name: user.Name,
IsBoss: constants.IsBoss(user.IsBoss),
IsSenior: constants.IsSenior(user.IsSenior),
HiredDate: user.HiredDate,
Extension: user.Extension,
}
if len(user.DeptIDList) > 0 {
deptIdList := pkg.SliceStringToInt(strings.Split(user.DeptIDList, ","))
depts, _err := u.dept.GetDeptInfoByDeptIds(ctx, deptIdList, authInfo)
if _err != nil {
return nil, err
}
for _, dept := range depts {
userInfo.Dept = append(userInfo.Dept, dept)
}
}
return userInfo, nil
}
func (u *User) getUserInfoFromDingTalk(ctx context.Context, token string, staffId string) (user UserInfoResResult, err error) {
if token == "" && staffId == "" {
err = errors.New("获取钉钉用户信息的必要参数不足")
return
}
req := l_request.Request{
Method: http.MethodPost,
Url: constants.GetDingTalkRequestUrl(constants.RequestUrlGetUserGet, map[string]string{
"access_token": token,
}),
Data: map[string]string{
"userid": staffId,
},
}
res, err := req.Send()
if err != nil {
return
}
var userInfoRes UserInfoRes
err = json.Unmarshal(res.Content, &userInfoRes)
if err != nil {
return
}
if userInfoRes.Errcode != 0 {
fmt.Errorf("钉钉请求报错:%s", userInfoRes.Errmsg)
}
return userInfoRes.Result, err
}

View File

@ -7,6 +7,7 @@ import (
"ai_scheduler/internal/gateway" "ai_scheduler/internal/gateway"
"context" "context"
"encoding/json" "encoding/json"
"strings"
"time" "time"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
@ -66,6 +67,7 @@ func (r *AiRouterBiz) RouteWithSocket(client *gateway.Client, req *entitys.ChatS
return return
} }
//意图识别 //意图识别
requireData.Match, err = r.handle.Recognize(ctx, &rec, sys) requireData.Match, err = r.handle.Recognize(ctx, &rec, sys)
if err != nil { if err != nil {
log.Errorf("意图识别失败: %s", err.Error()) log.Errorf("意图识别失败: %s", err.Error())
@ -136,9 +138,13 @@ func (r *AiRouterBiz) buildUserContent(requireData *entitys.RequireData) (*entit
// 处理文件和图片 // 处理文件和图片
fileUrls := []string{requireData.Req.File, requireData.Req.Img} fileUrls := []string{requireData.Req.File, requireData.Req.Img}
for _, url := range fileUrls { for _, item := range fileUrls {
if url != "" { // 处理逗号分隔的多个URL
files = append(files, &entitys.RecognizeFile{FileUrl: url}) urlList := strings.Split(item, ",")
for _, url := range urlList {
if url != "" {
files = append(files, &entitys.RecognizeFile{FileUrl: url})
}
} }
} }

View File

@ -129,6 +129,10 @@ type ToolsConfig struct {
ZltxOrderAfterSaleReseller ToolConfig `mapstructure:"zltxOrderAfterSaleReseller"` ZltxOrderAfterSaleReseller ToolConfig `mapstructure:"zltxOrderAfterSaleReseller"`
// 下游批充订单售后 // 下游批充订单售后
ZltxOrderAfterSaleResellerBatch ToolConfig `mapstructure:"zltxOrderAfterSaleResellerBatch"` ZltxOrderAfterSaleResellerBatch ToolConfig `mapstructure:"zltxOrderAfterSaleResellerBatch"`
// Coze 快递查询工具
CozeExpress ToolConfig `mapstructure:"cozeExpress"`
// Coze 公司查询工具
CozeCompany ToolConfig `mapstructure:"cozeCompany"`
} }
// ToolConfig 单个工具配置 // ToolConfig 单个工具配置

View File

@ -31,3 +31,5 @@ type BotType int
const ( const (
BotTypeDingTalk BotType = 1 // 系统的bug/优化建议 BotTypeDingTalk BotType = 1 // 系统的bug/优化建议
) )
const DingTalkAuthBaseKeyPrefix = "dingTalk_auth"

View File

@ -31,3 +31,5 @@ var UseFulMap = map[UseFul]string{
UseFulNotUnclear: "回答不明确", UseFulNotUnclear: "回答不明确",
UseFulNotError: "理解错误", UseFulNotError: "理解错误",
} }
type BaseBool int32

View File

@ -0,0 +1,38 @@
package constants
import "net/url"
const DingTalkBseUrl = "https://oapi.dingtalk.com"
type RequestUrl string
const (
RequestUrlGetUserGet RequestUrl = "/topapi/v2/user/get"
RequestUrlGetDeptGet RequestUrl = "/topapi/v2/department/get"
)
func GetDingTalkRequestUrl(path RequestUrl, query map[string]string) string {
u, _ := url.Parse(DingTalkBseUrl + string(path))
q := u.Query()
for key, val := range query {
q.Add(key, val)
}
u.RawQuery = q.Encode()
return u.String()
}
// IsBoss 是否是老板
type IsBoss int
const (
IsBossTrue IsBoss = 1
IsBossFalse IsBoss = 0
)
// IsSenior 是否是老板
type IsSenior int
const (
IsSeniorTrue IsSenior = 1
IsSeniorFalse IsSenior = 0
)

View File

@ -0,0 +1,91 @@
package constants
import (
"ai_scheduler/internal/biz"
"ai_scheduler/internal/biz/do"
"ai_scheduler/internal/biz/llm_service"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/gateway"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/utils_ollama"
"ai_scheduler/internal/pkg/utils_vllm"
"ai_scheduler/internal/server"
"ai_scheduler/internal/services"
"ai_scheduler/internal/tools"
"ai_scheduler/internal/tools_bot"
"ai_scheduler/utils"
"os"
"testing"
)
const
func TestMain(m *testing.M) {
bootstrap := initialize.LoadConfigWithTest()
businessLogger := log2.NewBusinessLogger(bootstrap.Logs, id, Name, Version)
helper := pkg.NewLogHelper(businessLogger, bootstrap)
db, cleanup := utils.NewGormDb(configConfig)
sysImpl := impl.NewSysImpl(db)
taskImpl := impl.NewTaskImpl(db)
chatHisImpl := impl.NewChatHisImpl(db)
doDo := do.NewDo(sysImpl, taskImpl, chatHisImpl, configConfig)
client, cleanup2, err := utils_ollama.NewClient(configConfig)
if err != nil {
cleanup()
return nil, nil, err
}
utils_vllmClient, cleanup3, err := utils_vllm.NewClient(configConfig)
if err != nil {
cleanup2()
cleanup()
return nil, nil, err
}
ollamaService := llm_service.NewOllamaGenerate(client, utils_vllmClient, configConfig, chatHisImpl)
manager := tools.NewManager(configConfig, client)
sessionImpl := impl.NewSessionImpl(db)
botTool := tools_bot.NewBotTool(configConfig, client, sessionImpl)
handle := do.NewHandle(ollamaService, manager, configConfig, sessionImpl, botTool)
aiRouterBiz := biz.NewAiRouterBiz(doDo, handle)
chatHistoryBiz := biz.NewChatHistoryBiz(chatHisImpl, taskImpl)
gatewayGateway := gateway.NewGateway()
chatService := services.NewChatService(aiRouterBiz, chatHistoryBiz, gatewayGateway, configConfig)
sessionBiz := biz.NewSessionBiz(configConfig, sessionImpl, sysImpl, chatHisImpl)
sessionService := services.NewSessionService(sessionBiz, chatHistoryBiz)
taskBiz := biz.NewTaskBiz(configConfig, taskImpl)
taskService := services.NewTaskService(sessionBiz, taskBiz)
oldClient := dingtalk.NewOldClient(configConfig)
contactClient, err := dingtalk.NewContactClient(configConfig)
if err != nil {
cleanup3()
cleanup2()
cleanup()
return nil, nil, err
}
notableClient, err := dingtalk.NewNotableClient(configConfig)
if err != nil {
cleanup3()
cleanup2()
cleanup()
return nil, nil, err
}
callbackService := services.NewCallbackService(configConfig, gatewayGateway, oldClient, contactClient, notableClient, botTool)
historyService := services.NewHistoryService(chatHistoryBiz)
app := server.NewHTTPServer(chatService, sessionService, taskService, gatewayGateway, callbackService, historyService)
botConfigImpl := impl.NewBotConfigImpl(db)
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl)
dingBotService := services.NewDingBotService(configConfig, dingTalkBotBiz)
v := server.ProvideAllDingBotServices(dingBotService)
dingTalkBotServer := server.NewDingTalkBotServer(v)
servers := server.NewServers(configConfig, app, dingTalkBotServer)
code := m.Run()
os.Exit(code)
}
func Test_GetUserInfo(t *testing.T) {
var c entitys.TaskConfig
config := `{"param": {"type": "object", "required": ["number"], "properties": {"number": {"type": "string", "description": "订单编号/流水号"}}}, "request": {"url": "http://www.baidu.com/${number}", "headers": {"Authorization": "${authorization}"}, "method": "GET"}}`
err := json.Unmarshal([]byte(config), &c)
t.Log(err)
}

View File

@ -0,0 +1,17 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
)
type BotDeptImpl struct {
dataTemp.DataTemp
}
func NewBotDeptImpl(db *utils.Db) *BotDeptImpl {
return &BotDeptImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotDept)),
}
}

View File

@ -0,0 +1,27 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
"database/sql"
)
type BotUserImpl struct {
dataTemp.DataTemp
}
func NewBotUserImpl(db *utils.Db) *BotUserImpl {
return &BotUserImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotUser)),
}
}
func (k BotUserImpl) GetByStaffId(staffId string) (data *model.AiBotUser, err error) {
err = k.Db.Model(k.Model).Where("staff_id = ?", staffId).Find(data).Error
if data == nil {
err = sql.ErrNoRows
}
return
}

View File

@ -10,4 +10,7 @@ var ProviderImpl = wire.NewSet(
NewTaskImpl, NewTaskImpl,
NewChatHisImpl, NewChatHisImpl,
NewBotConfigImpl, NewBotConfigImpl,
NewBotDeptImpl,
NewBotUserImpl,
NewBotChatHisImpl,
) )

View File

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

View File

@ -0,0 +1,26 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiBotDept = "ai_bot_dept"
// AiBotDept mapped from table <ai_bot_dept>
type AiBotDept struct {
DeptID int32 `gorm:"column:dept_id;primaryKey" json:"dept_id"`
DingtalkDeptID int32 `gorm:"column:dingtalk_dept_id;not null;comment:标记部门的唯一id钉钉钉钉侧提供的dept_id" json:"dingtalk_dept_id"` // 标记部门的唯一id钉钉钉钉侧提供的dept_id
Name string `gorm:"column:name;not null;comment:用户名称" json:"name"` // 用户名称
Status int32 `gorm:"column:status;not null" json:"status"`
DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"`
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
}
// TableName AiBotDept's table name
func (*AiBotDept) TableName() string {
return TableNameAiBotDept
}

View File

@ -0,0 +1,33 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiBotUser = "ai_bot_user"
// AiBotUser mapped from table <ai_bot_user>
type AiBotUser struct {
UserID int32 `gorm:"column:user_id;primaryKey" json:"user_id"`
StaffID string `gorm:"column:staff_id;not null;comment:标记用户用的唯一id钉钉钉钉侧提供的user_id" json:"staff_id"` // 标记用户用的唯一id钉钉钉钉侧提供的user_id
Name string `gorm:"column:name;not null;comment:用户名称" json:"name"` // 用户名称
Title string `gorm:"column:title;not null;comment:职位" json:"title"` // 职位
Extension string `gorm:"column:extension;not null;default:1;comment:信息面板" json:"extension"` // 信息面板
RoleList string `gorm:"column:role_list;not null;comment:角色列表。" json:"role_list"` // 角色列表。
DeptIDList string `gorm:"column:dept_id_list;not null;comment:所在部门id列表" json:"dept_id_list"` // 所在部门id列表
IsBoss int32 `gorm:"column:is_boss;not null;comment:是否是老板" json:"is_boss"` // 是否是老板
IsSenior int32 `gorm:"column:is_senior;not null;comment:是否是高管" json:"is_senior"` // 是否是高管
HiredDate time.Time `gorm:"column:hired_date;not null;default:CURRENT_TIMESTAMP;comment:入职时间" json:"hired_date"` // 入职时间
Status int32 `gorm:"column:status;not null" json:"status"`
DeleteAt *time.Time `gorm:"column:delete_at" json:"delete_at"`
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
}
// TableName AiBotUser's table name
func (*AiBotUser) TableName() string {
return TableNameAiBotUser
}

View File

@ -8,11 +8,8 @@ import (
) )
type RequireDataDingTalkBot struct { type RequireDataDingTalkBot struct {
Session string
Key string
Sys model.AiSy
Histories []model.AiChatHi Histories []model.AiChatHi
SessionInfo model.AiSession UserInfo *DingTalkUserInfo
Tasks []model.AiTask Tasks []model.AiTask
Match *Match Match *Match
Req *chatbot.BotCallbackDataModel Req *chatbot.BotCallbackDataModel
@ -24,7 +21,7 @@ type RequireDataDingTalkBot struct {
} }
type DingTalkBot struct { type DingTalkBot struct {
BotIndex string BotIndex string `json:"bot_index"`
ClientId string ClientId string `json:"client_id"`
ClientSecret string ClientSecret string `json:"client_secret"`
} }

View File

@ -0,0 +1,22 @@
package entitys
import (
"ai_scheduler/internal/data/constants"
"time"
)
type DingTalkUserInfo struct {
UserId int `json:"user_id"`
StaffId string `json:"staff_id"`
Name string `json:"name"`
Dept []*Dept `json:"dept"`
IsBoss constants.IsBoss `json:"is_boss"`
IsSenior constants.IsSenior `json:"is_senior"`
HiredDate time.Time `json:"hired_date"`
Extension string `json:"extension"`
}
type Dept struct {
Name string `json:"name"`
DeptId int `json:"dept_id"`
}

View File

@ -1,8 +1,6 @@
package entitys package entitys
import ( import "ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/constants"
)
type Recognize struct { type Recognize struct {
SystemPrompt string // 系统提示内容 SystemPrompt string // 系统提示内容
@ -28,7 +26,7 @@ type RecognizeUserContent struct {
type FileData []byte type FileData []byte
type RecognizeFile struct { type RecognizeFile struct {
File []FileData // 文件数据(二进制格式) FileData FileData // 文件数据(二进制格式)
FileUrl string // 文件下载链接
FileType constants.Caller // 文件类型(文件类型,能填最好填,可以跳过一层判断) FileType constants.Caller // 文件类型(文件类型,能填最好填,可以跳过一层判断)
FileUrl string // 文件下载链接
} }

View File

@ -30,7 +30,7 @@ type FirstSockRequest struct {
type ChatSockRequest struct { type ChatSockRequest struct {
Text string `json:"text" binding:"required"` Text string `json:"text" binding:"required"`
Img string `json:"img" binding:"required"` Img string `json:"img" binding:"required"` // 多图片使用 英文, 分割
File string `json:"file" binding:"required"` File string `json:"file" binding:"required"`
Tags string `json:"tags" binding:"required"` Tags string `json:"tags" binding:"required"`
MarkHis int64 `json:"mark_his" ` MarkHis int64 `json:"mark_his" `

View File

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
) )
@ -65,3 +66,70 @@ func HexEncode(src, dst []byte) int {
} }
return len(src) * 2 return len(src) * 2
} }
// Ter 三目运算 Ter(true, 1, 2)
func Ter[T any](cond bool, a, b T) T {
if cond {
return a
}
return b
}
// StringToSlice [num,num]转slice
func StringToSlice(s string) ([]int, error) {
// 1. 去掉两端的方括号
trimmed := strings.Trim(s, "[]")
// 2. 按逗号分割
parts := strings.Split(trimmed, ",")
// 3. 转换为 []int
result := make([]int, 0, len(parts))
for _, part := range parts {
num, err := strconv.Atoi(strings.TrimSpace(part))
if err != nil {
return nil, err
}
result = append(result, num)
}
return result, nil
}
// Difference 差集
func Difference[T comparable](a, b []T) []T {
// 创建 b 的映射T 必须是可比较的类型)
bMap := make(map[T]struct{}, len(b))
for _, item := range b {
bMap[item] = struct{}{}
}
var diff []T // 修正为 []T 而非 []int
for _, item := range a {
if _, found := bMap[item]; !found {
diff = append(diff, item)
}
}
return diff
}
// SliceStringToInt []string=>[]int
func SliceStringToInt(strSlice []string) []int {
numSlice := make([]int, len(strSlice))
for i, str := range strSlice {
num, err := strconv.Atoi(str)
if err != nil {
return nil
}
numSlice[i] = num
}
return numSlice
}
// SliceIntToString []int=>[]string
func SliceIntToString(slice []int) []string {
strSlice := make([]string, len(slice)) // len=cap=len(slice)
for i, num := range slice {
strSlice[i] = strconv.Itoa(num) // 直接赋值,无 append
}
return strSlice
}

View File

@ -62,10 +62,10 @@ func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
} }
err := cli.Start(ctx) err := cli.Start(ctx)
if err != nil { if err != nil {
log.Info("%s启动失败", name) log.Infof("%s启动失败", name)
continue continue
} }
log.Info("%s启动成功", name) log.Infof("%s启动成功", name)
} }
} }
func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) { func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) {

View File

@ -31,7 +31,7 @@ func (d *DingBotService) OnChatBotMessageReceived(ctx context.Context, data *cha
return return
} }
go func() { go func() {
defer close(requireData.Ch) //defer close(requireData.Ch)
//if match, _err := d.dingTalkBotBiz.Recognize(ctx, data); _err != nil { //if match, _err := d.dingTalkBotBiz.Recognize(ctx, data); _err != nil {
// requireData.Ch <- entitys.Response{ // requireData.Ch <- entitys.Response{
// Type: entitys.ResponseEnd, // Type: entitys.ResponseEnd,

View File

@ -1,121 +0,0 @@
package tools
import (
"ai_scheduler/internal/entitys"
"context"
"encoding/json"
"fmt"
"math"
)
// CalculatorTool 计算器工具
type CalculatorTool struct{}
// NewCalculatorTool 创建计算器工具
func NewCalculatorTool() *CalculatorTool {
return &CalculatorTool{}
}
// Name 返回工具名称
func (c *CalculatorTool) Name() string {
return "calculate"
}
// Description 返回工具描述
func (c *CalculatorTool) Description() string {
return "执行基本的数学运算,支持加减乘除和幂运算"
}
// Definition 返回工具定义
func (c *CalculatorTool) Definition() entitys.ToolDefinition {
return entitys.ToolDefinition{
Type: "function",
Function: entitys.FunctionDef{
Name: c.Name(),
Description: c.Description(),
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"operation": map[string]interface{}{
"type": "string",
"description": "运算类型",
"enum": []string{"add", "subtract", "multiply", "divide", "power"},
},
"a": map[string]interface{}{
"type": "number",
"description": "第一个数字",
},
"b": map[string]interface{}{
"type": "number",
"description": "第二个数字",
},
},
"required": []string{"operation", "a", "b"},
},
},
}
}
// CalculateRequest 计算请求参数
type CalculateRequest struct {
Operation string `json:"operation"`
A float64 `json:"a"`
B float64 `json:"b"`
}
// CalculateResponse 计算响应
type CalculateResponse struct {
Operation string `json:"operation"`
A float64 `json:"a"`
B float64 `json:"b"`
Result float64 `json:"result"`
Expression string `json:"expression"`
}
// Execute 执行计算
func (c *CalculatorTool) Execute(ctx context.Context, args json.RawMessage) (interface{}, error) {
var req CalculateRequest
if err := json.Unmarshal(args, &req); err != nil {
return nil, fmt.Errorf("invalid calculate request: %w", err)
}
var result float64
var expression string
switch req.Operation {
case "add":
result = req.A + req.B
expression = fmt.Sprintf("%.2f + %.2f = %.2f", req.A, req.B, result)
case "subtract":
result = req.A - req.B
expression = fmt.Sprintf("%.2f - %.2f = %.2f", req.A, req.B, result)
case "multiply":
result = req.A * req.B
expression = fmt.Sprintf("%.2f × %.2f = %.2f", req.A, req.B, result)
case "divide":
if req.B == 0 {
return nil, fmt.Errorf("division by zero is not allowed")
}
result = req.A / req.B
expression = fmt.Sprintf("%.2f ÷ %.2f = %.2f", req.A, req.B, result)
case "power":
result = math.Pow(req.A, req.B)
expression = fmt.Sprintf("%.2f ^ %.2f = %.2f", req.A, req.B, result)
default:
return nil, fmt.Errorf("unsupported operation: %s", req.Operation)
}
// 检查结果是否有效
if math.IsNaN(result) || math.IsInf(result, 0) {
return nil, fmt.Errorf("calculation resulted in invalid number")
}
return &CalculateResponse{
Operation: req.Operation,
A: req.A,
B: req.B,
Result: result,
Expression: expression,
}, nil
}

View File

@ -4,6 +4,7 @@ import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/utils_ollama" "ai_scheduler/internal/pkg/utils_ollama"
"ai_scheduler/internal/tools/public"
zltxtool "ai_scheduler/internal/tools/zltx" zltxtool "ai_scheduler/internal/tools/zltx"
"context" "context"
@ -43,30 +44,30 @@ func NewManager(config *config.Config, llm *utils_ollama.Client) *Manager {
// 注册直连天下订单详情工具 // 注册直连天下订单详情工具
if config.Tools.ZltxOrderDetail.Enabled { if config.Tools.ZltxOrderDetail.Enabled {
zltxOrderDetailTool := NewZltxOrderDetailTool(config.Tools.ZltxOrderDetail, m.llm) zltxOrderDetailTool := zltxtool.NewZltxOrderDetailTool(config.Tools.ZltxOrderDetail, m.llm)
m.tools[zltxOrderDetailTool.Name()] = zltxOrderDetailTool m.tools[zltxOrderDetailTool.Name()] = zltxOrderDetailTool
} }
//注册直连天下订单日志工具 //注册直连天下订单日志工具
if config.Tools.ZltxOrderDirectLog.Enabled { if config.Tools.ZltxOrderDirectLog.Enabled {
zltxOrderLogTool := NewZltxOrderLogTool(config.Tools.ZltxOrderDirectLog) zltxOrderLogTool := zltxtool.NewZltxOrderLogTool(config.Tools.ZltxOrderDirectLog)
m.tools[zltxOrderLogTool.Name()] = zltxOrderLogTool m.tools[zltxOrderLogTool.Name()] = zltxOrderLogTool
} }
//注册直连天下商品工具 //注册直连天下商品工具
if config.Tools.ZltxProduct.Enabled { if config.Tools.ZltxProduct.Enabled {
zltxProductTool := NewZltxProductTool(config.Tools.ZltxProduct) zltxProductTool := zltxtool.NewZltxProductTool(config.Tools.ZltxProduct)
m.tools[zltxProductTool.Name()] = zltxProductTool m.tools[zltxProductTool.Name()] = zltxProductTool
} }
//注册直连天下订单统计工具 //注册直连天下订单统计工具
if config.Tools.ZltxOrderStatistics.Enabled { if config.Tools.ZltxOrderStatistics.Enabled {
zltxOrderStatisticsTool := NewZltxOrderStatisticsTool(config.Tools.ZltxOrderStatistics) zltxOrderStatisticsTool := zltxtool.NewZltxOrderStatisticsTool(config.Tools.ZltxOrderStatistics)
m.tools[zltxOrderStatisticsTool.Name()] = zltxOrderStatisticsTool m.tools[zltxOrderStatisticsTool.Name()] = zltxOrderStatisticsTool
} }
// 注册知识库工具 // 注册知识库工具
if config.Tools.Knowledge.Enabled { if config.Tools.Knowledge.Enabled {
knowledgeTool := NewKnowledgeBaseTool(config.Tools.Knowledge) knowledgeTool := public.NewKnowledgeBaseTool(config.Tools.Knowledge)
m.tools[knowledgeTool.Name()] = knowledgeTool m.tools[knowledgeTool.Name()] = knowledgeTool
} }
@ -85,9 +86,24 @@ func NewManager(config *config.Config, llm *utils_ollama.Client) *Manager {
zltxOrderAfterSaleResellerBatchTool := zltxtool.NewOrderAfterSaleResellerBatchTool(config.Tools.ZltxOrderAfterSaleResellerBatch) zltxOrderAfterSaleResellerBatchTool := zltxtool.NewOrderAfterSaleResellerBatchTool(config.Tools.ZltxOrderAfterSaleResellerBatch)
m.tools[zltxOrderAfterSaleResellerBatchTool.Name()] = zltxOrderAfterSaleResellerBatchTool m.tools[zltxOrderAfterSaleResellerBatchTool.Name()] = zltxOrderAfterSaleResellerBatchTool
} }
// 注册天气工具
if config.Tools.Weather.Enabled {
weatherTool := public.NewWeatherTool(config.Tools.Weather)
m.tools[weatherTool.Name()] = weatherTool
}
// 注册 Coze 快递查询工具
if config.Tools.CozeExpress.Enabled {
cozeTool := public.NewCozeExpress(config.Tools.CozeExpress, m.llm)
m.tools[cozeTool.Name()] = cozeTool
}
// 注册 Coze 公司查询工具
if config.Tools.CozeCompany.Enabled {
cozeTool := public.NewCozeCompany(config.Tools.CozeCompany, m.llm)
m.tools[cozeTool.Name()] = cozeTool
}
// 普通对话 // 普通对话
chat := NewNormalChatTool(m.llm, config) chat := public.NewNormalChatTool(m.llm, config)
m.tools[chat.Name()] = chat m.tools[chat.Name()] = chat
return m return m

View File

@ -0,0 +1,264 @@
package public
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
"encoding/json"
"fmt"
"github.com/ollama/ollama/api"
"net/http"
"time"
"github.com/coze-dev/coze-go"
)
type CozeCompany struct {
cozeApi coze.CozeAPI
config config.ToolConfig
llm *utils_ollama.Client
}
// NewCoze 创建 Coze 实例
func NewCozeCompany(config config.ToolConfig, llm *utils_ollama.Client) *CozeCompany {
return &CozeCompany{
cozeApi: newCozeApi(config),
config: config,
llm: llm,
}
}
// newCozeClient 创建 Coze 客户端
func newCozeApi(config config.ToolConfig) coze.CozeAPI {
authCli := coze.NewTokenAuth(config.APISecret)
cozeApi := coze.NewCozeAPI(authCli, coze.WithBaseURL(config.BaseURL), coze.WithHttpClient(&http.Client{
Timeout: time.Second * 120,
}))
return cozeApi
}
// Name 返回工具名称
func (c *CozeCompany) Name() string {
return "coze_company"
}
// Description 返回工具描述
func (c *CozeCompany) Description() string {
return "查询企业信息"
}
// Definition 返回工具定义
func (c *CozeCompany) Definition() entitys.ToolDefinition {
return entitys.ToolDefinition{
Type: "function",
Function: entitys.FunctionDef{
Name: c.Name(),
Description: c.Description(),
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"company_name": map[string]interface{}{
"type": "string",
"description": "企业名称",
},
},
"required": []string{"company_name"},
},
},
}
}
// Execute 执行查询
func (c *CozeCompany) Execute(ctx context.Context, requireData *entitys.RequireData) error {
var req map[string]interface{}
if err := json.Unmarshal([]byte(requireData.Match.Parameters), &req); err != nil {
return fmt.Errorf("invalid express request: %w", err)
}
if req["company_name"] == "" {
return fmt.Errorf("company_name is required")
}
// 调用 Coze 工作流
rsp, err := c.callWorkflow(ctx, req)
if err != nil {
return fmt.Errorf("failed to get real weather: %w", err)
}
companyInfo := CompanyInfo{}
err = json.Unmarshal([]byte(rsp.Data), &companyInfo)
if err != nil {
return fmt.Errorf("failed to unmarshal company info: %w", err)
}
// 调用 LLM 模型
err = c.llm.ChatStream(ctx, requireData.Ch, []api.Message{
{
Role: "system",
Content: `# Role: 企业信息分析与经营诊断专家
请基于以下12项指定数据字段无需补充未提供的信息完成目标企业的全维度分析总结要求每部分结论必须100%锚定对应数据拒绝主观推测突出风险可见性关键信息关联性
输入数据清单需逐一对应分析
行政处罚公司是否有行政处罚含处罚事由处罚机关处罚日期处罚文号
清算信息公司的清算信息含清算原因清算组构成清算进展状态
变更记录公司的变更记录含变更事项注册资本/股东/法定代表人/经营范围变更时间变更前后内容
主要成员公司名搜索公司的主要成员含姓名职位任职时间核心履历关键词
企业详情公司名称搜索公司详细信息含成立时间注册资本经营范围行业分类注册地址
经营异常公司是否有经营异常含列入原因列入日期移出状态
破产重组公司破产重组的信息含申请法院受理时间重组方案核心内容
严重违法是否有严重违法信息含违法事由认定机关公示期限
司法信息司法信息含案件类型原被告身份判决结果/进展
被执行信息公司的被执行信息含执行法院执行标的未履行金额失信状态
企业手机号公司名称查询企业手机号需标注是否为公开备案号
股东信息公司对应的股东含股东名称出资额出资比例股东类型自然人/企业/机构
分析总结框架严格对应数据
1. 企业基础画像锚定公司名称搜索公司详细信息
核心属性成立时间注册资本实缴/认缴行业分类批发和零售业软件和信息技术服务业主营业务从经营范围中提炼1-2个核心赛道专注于智能仓储设备研发与销售
注册地址是否与主要经营地一致若公司名称搜索公司详细信息有披露
2. 股权结构与股东特征锚定公司对应的股东
股权集中度前三大股东出资占比之和第一大股东持股60%为绝对控股股东
股东类型分布自然人股东企业股东机构股东的占比70%为企业股东含1家行业头部企业
关键股东亮点若有知名企业/机构股东需明确标注股东含XX产业投资基金具备产业链资源协同潜力
3. 经营稳定性与合规风险锚定公司是否有行政处罚/公司是否有经营异常/是否有严重违法信息/公司的清算信息/公司破产重组的信息
行政处罚风险是否存在行政处罚
若有列清处罚事由虚假宣传税务逾期申报处罚机关处罚日期并判断是否属于高频违规如1年内2次同类处罚高风险
若无标注无公开行政处罚记录
经营异常风险是否存在经营异常
若有说明列入原因通过登记的住所无法联系未公示年度报告是否已移出并评估对业务的影响地址失联可能导致客户信任下降
若无标注无经营异常记录
严重违法红线是否存在严重违法
若有明确违法事由拒不履行生效法律文书欺诈消费者认定机关公示期限并标注触及监管红线需重点核查整改情况
若无标注无严重违法记录
极端状态预警是否存在清算或破产重组
若有清算说明清算原因股东会决议解散营业期限届满清算进展清算组已完成资产清查
若有破产重组说明申请法院受理时间重组方案核心内容拟引入战略投资者注资5000万
若无标注无清算或破产重组记录
4. 司法与执行风险锚定司法信息/公司的被执行信息
涉诉情况司法案件的核心特征80%为买卖合同纠纷20%为劳动争议仲裁作为被告的案件占比75%判决胜诉率约60%
被执行压力是否存在被执行信息
若有列清执行法院执行标的金额未履行金额是否纳入失信被执行人名单并计算未履行金额占注册资本的比例未履行800万占注册资本的20%
若无标注无公开被执行记录
5. 管理团队与联系信息锚定公司名搜索公司的主要成员/公司名称查询企业手机号
核心团队稳定性主要成员的任职时间分布CEO任职4年CFO任职2年核心团队平均任职3年是否有频繁变动1年内2位高管离职需提示管理稳定性风险
关键岗位资质核心成员如法定代表人CEOCFO的过往履历亮点CEO曾任职XX上市公司主导过亿元级项目落地
联系信息可信度企业手机号是否为公开备案与工商登记预留电话一致可信度高为非备案号需提示联系信息真实性存疑
6. 综合结论与行动建议整合所有数据
整体风险评级基于数据密度给出低风险/中风险/高风险定性示例无行政处罚无被执行仅1条普通合同纠纷低风险有失信被执行+严重违法高风险
Top3核心风险严重违法被执行破产重组行政处罚经营异常司法纠纷排序列出最需关注的3个问题1. 未履行金额占注册资本20%存在债务违约风险2. 1年内2次地址异常经营稳定性弱3. 自然人股东占比过高决策易受个人因素影响
actionable 建议针对每个核心风险给出可执行的核查/应对动作核查未履行金额对应的案件进展评估企业偿债能力要求企业提供近1年的地址证明确认经营场所稳定性穿透核查自然人股东的资产状况降低决策风险
输出规则强制遵守
数据溯源每句结论必须标注对应数据字段根据公司是否有行政处罚企业2023年因税务逾期申报被区税务局处罚
量化优先拒绝模糊表述如不说很多案件要说近1年涉及5起买卖合同纠纷
风险分级标注风险等级越多越严重严重违法被执行经营异常
语言风格专业简洁避免冗余适合风控/投资/合作前的快速决策阅读
示例输出片段
3. 经营稳定性与合规风险
行政处罚根据公司是否有行政处罚企业2022年11月因发布虚假广告被区市场监管局处以3万元罚款文号X市监罚字2022456无后续同类处罚风险等级
经营异常根据公司是否有经营异常企业2023年6月因通过登记的住所无法联系被列入异常名录2023年12月已移出风险等级
严重违法根据是否有严重违法信息无公开严重违法记录风险等级无
极端状态根据公司的清算信息/公司破产重组的信息无清算或破产重组记录风险等级无
6. 综合结论与行动建议
整体评级低风险仅1次轻微行政处罚无重大合规瑕疵
Top3核心风险1. 根据司法信息近1年作为被告的合同纠纷占比75%需警惕应收账款回收风险2. 根据公司名搜索公司的主要成员1年内2位销售总监离职管理稳定性弱3. 根据公司名称查询企业手机号企业手机号为非备案号联系信息真实性存疑
建议核查合同纠纷案件的原告身份及回款情况评估坏账概率要求企业提供离职人员的交接说明确认业务连续性索要企业备案的联系方式验证沟通有效性
`,
},
{
Role: "assistant",
Content: fmt.Sprintf(`请分析企业:%s
公司是否有行政处罚%v
公司的清算信息%v
公司的变更记录%v
公司名搜索公司的主要成员%v
公司名称搜索公司详细信息%v
公司是否有经营异常%v
公司破产重组的信息%v
是否有严重违法信息%v
司法信息%v
公司的被执行信息%v
公司名称查询企业手机号%v
公司对应的股东%v`, req["company_name"], companyInfo.Xzcf, companyInfo.Clears, companyInfo.Changes, companyInfo.Employees, companyInfo.Searchdata, companyInfo.Operations, companyInfo.BankruptcyPublicList, companyInfo.Illegals, companyInfo.JudicialList, companyInfo.Executes, companyInfo.Phone, companyInfo.Partners),
},
{
Role: "user",
Content: requireData.Req.Text,
},
},
c.Name(), "")
if err != nil {
return fmt.Errorf("failed to get express info: %w", err)
}
//entitys.ResText(requireData.Ch, "", rsp.Data)
return nil
}
// CallWorkflow 调用 Coze 工作流
// 参数:
// - ctx: 上下文,用于控制超时和取消
// - workflowId: 工作流 ID
// - params: 工作流参数
// 返回:
// - interface{}: 工作流执行结果
// - error: 错误信息
func (c *CozeCompany) callWorkflow(ctx context.Context, params map[string]interface{}) (*coze.RunWorkflowsResp, error) {
// 准备工作流请求参数
workflowReq := &coze.RunWorkflowsReq{
WorkflowID: c.config.APIKey,
Parameters: params,
}
// 调用工作流
resp, err := c.cozeApi.Workflows.Runs.Create(ctx, workflowReq)
if err != nil {
return nil, fmt.Errorf("工作流调用失败: %w", err)
}
// 处理工作流响应
if resp == nil {
return nil, fmt.Errorf("工作流响应为空")
}
// 返回工作流执行结果
return resp, nil
}
type CompanyInfo struct {
BankruptcyPublicList interface{} `json:"bankruptcy_public_list"` // 破产公示列表
Changes interface{} `json:"changes"` // 变更记录
Clears interface{} `json:"clears"` // 清算记录
Employees interface{} `json:"employees"` // 员工列表
Executes interface{} `json:"executes"` // 执行记录
Illegals interface{} `json:"illegals"` // 违法记录
JudicialList interface{} `json:"judicial_list"` // 司法记录
Operations interface{} `json:"operations"` // 经营记录
Partners interface{} `json:"partners"` // 合伙人列表
Phone string `json:"phone"` // 联系电话
// 搜索数据
Searchdata struct {
Authority interface{} `json:"authority"`
BusinessScope interface{} `json:"business_scope"`
Capital interface{} `json:"capital"`
CompanyAddress interface{} `json:"company_address"`
CompanyName string `json:"company_name"`
CompanyStatus interface{} `json:"company_status"`
CompanyType interface{} `json:"company_type"`
CreditNo interface{} `json:"credit_no"`
EstablishDate interface{} `json:"establish_date"`
Industry interface{} `json:"industry"`
LegalPerson interface{} `json:"legal_person"`
Province interface{} `json:"province"`
} `json:"searchdata"`
Xzcf interface{} `json:"xzcf"` // 行政处罚
}

View File

@ -0,0 +1,140 @@
package public
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
"encoding/json"
"fmt"
"github.com/ollama/ollama/api"
"github.com/coze-dev/coze-go"
)
type CozeExpress struct {
cozeApi coze.CozeAPI
config config.ToolConfig
llm *utils_ollama.Client
}
// NewCozeExpress 创建 CozeExpress 实例
func NewCozeExpress(config config.ToolConfig, llm *utils_ollama.Client) *CozeExpress {
return &CozeExpress{
cozeApi: newCozeApi(config),
config: config,
llm: llm,
}
}
// newCozeExpressClient 创建 CozeExpress 客户端
func newCozeExpressApi(config config.ToolConfig) coze.CozeAPI {
authCli := coze.NewTokenAuth(config.APISecret)
cozeApi := coze.NewCozeAPI(authCli, coze.WithBaseURL(config.BaseURL))
return cozeApi
}
// Name 返回工具名称
func (c *CozeExpress) Name() string {
return "coze_express"
}
// Description 返回工具描述
func (c *CozeExpress) Description() string {
return "查询快递物流信息"
}
// Definition 返回工具定义
func (c *CozeExpress) Definition() entitys.ToolDefinition {
return entitys.ToolDefinition{
Type: "function",
Function: entitys.FunctionDef{
Name: c.Name(),
Description: c.Description(),
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"express_id": map[string]interface{}{
"type": "string",
"description": "快递单号",
},
},
"required": []string{"express_id"},
},
},
}
}
// Execute 执行查询
func (c *CozeExpress) Execute(ctx context.Context, requireData *entitys.RequireData) error {
var req map[string]interface{}
if err := json.Unmarshal([]byte(requireData.Match.Parameters), &req); err != nil {
return fmt.Errorf("invalid express request: %w", err)
}
if req["express_id"] == "" {
return fmt.Errorf("express_id is required")
}
// 调用 Coze 工作流查询快递物流信息
rsp, err := c.callWorkflow(ctx, req)
if err != nil {
return fmt.Errorf("failed to get real weather: %w", err)
}
err = c.llm.ChatStream(ctx, requireData.Ch, []api.Message{
{
Role: "system",
Content: "你是一个快递查询助手。用户可能会提供快递单号,你需要分析快递单号,根据快递单号查询物流信息并反馈给我",
},
{
Role: "assistant",
Content: fmt.Sprintf("聊天记录:%s", pkg.JsonStringIgonErr(requireData.Histories)),
},
{
Role: "assistant",
Content: fmt.Sprintf("需要分析的快递单号:%s", rsp.Data),
},
{
Role: "user",
Content: requireData.Req.Text,
},
}, c.Name(), "")
if err != nil {
return fmt.Errorf("failed to get express info: %w", err)
}
//entitys.ResText(requireData.Ch, "", rsp.Data)
return nil
}
// CallWorkflow 调用 Coze 工作流
// 参数:
// - ctx: 上下文,用于控制超时和取消
// - workflowId: 工作流 ID
// - params: 工作流参数
// 返回:
// - interface{}: 工作流执行结果
// - error: 错误信息
func (c *CozeExpress) callWorkflow(ctx context.Context, params map[string]interface{}) (*coze.RunWorkflowsResp, error) {
// 准备工作流请求参数
workflowReq := &coze.RunWorkflowsReq{
WorkflowID: c.config.APIKey,
Parameters: params,
}
// 调用工作流
resp, err := c.cozeApi.Workflows.Runs.Create(ctx, workflowReq)
if err != nil {
return nil, fmt.Errorf("工作流调用失败: %w", err)
}
// 处理工作流响应
if resp == nil {
return nil, fmt.Errorf("工作流响应为空")
}
// 返回工作流执行结果
return resp, nil
}

View File

@ -1,4 +1,4 @@
package tools package public
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -1,4 +1,4 @@
package tools package public
import ( import (
"testing" "testing"

View File

@ -1,4 +1,4 @@
package tools package public
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -0,0 +1,345 @@
package public
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/l_request"
"log"
"strconv"
"context"
"encoding/json"
"fmt"
"time"
)
// WeatherTool 天气查询工具
type WeatherTool struct {
mockData bool
config config.ToolConfig
}
// NewWeatherTool 创建天气工具
func NewWeatherTool(config config.ToolConfig) *WeatherTool {
return &WeatherTool{config: config}
}
// Name 返回工具名称
func (w *WeatherTool) Name() string {
return "get_weather"
}
// Description 返回工具描述
func (w *WeatherTool) Description() string {
return "获取指定城市的天气信息"
}
// Definition 返回工具定义
func (w *WeatherTool) Definition() entitys.ToolDefinition {
return entitys.ToolDefinition{
Type: "function",
Function: entitys.FunctionDef{
Name: w.Name(),
Description: w.Description(),
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"description": "城市名称,如:北京、上海、广州",
},
"unit": map[string]interface{}{
"type": "string",
"description": "温度单位celsius(摄氏度)或fahrenheit(华氏度)",
"enum": []string{"celsius", "fahrenheit"},
"default": "celsius",
},
"extensions": map[string]interface{}{
"type": "string",
"description": "扩展参数base/all base:返回实况天气 all:返回预报天气",
"enum": []string{"base", "all"},
"default": "base",
},
},
"required": []string{"city"},
},
},
}
}
// WeatherRequest 天气请求参数
type WeatherRequest struct {
City string `json:"city"`
Extensions string `json:"extensions"` // 扩展参数base/all base:返回实况天气 all:返回预报天气
Unit string `json:"unit,omitempty"`
}
// WeatherResponse 天气响应
type WeatherResponse struct {
City string `json:"city"`
Unit string `json:"unit"`
Timestamp string `json:"timestamp"`
LiveWeather *LiveWeather `json:"live_weather,omitempty"` // 实时天气
Forecasts []ForecastWeather `json:"forecasts,omitempty"` // 预报天气
}
// ForecastWeather 预报天气
type ForecastWeather struct {
Date string `json:"date"`
Week string `json:"week"`
DayWeather string `json:"day_weather"`
NightWeather string `json:"night_weather"`
DayTemp float64 `json:"day_temp"`
NightTemp float64 `json:"night_temp"`
DayWind string `json:"day_wind"`
NightWind string `json:"night_wind"`
DayWindPower string `json:"day_wind_power"`
NightWindPower string `json:"night_wind_power"`
}
// LiveWeather 实时天气
type LiveWeather struct {
Temperature float64 `json:"temperature"`
Condition string `json:"condition"`
Humidity int `json:"humidity"`
WindSpeed float64 `json:"wind_speed"`
WindDirection string `json:"wind_direction"`
}
// Execute 执行天气查询
func (w *WeatherTool) Execute(ctx context.Context, requireData *entitys.RequireData) error {
var req WeatherRequest
if err := json.Unmarshal([]byte(requireData.Match.Parameters), &req); err != nil {
return fmt.Errorf("invalid weather request: %w", err)
}
if req.City == "" {
return fmt.Errorf("city is required")
}
if req.Unit == "" {
req.Unit = "celsius"
}
// 设置默认获取实时天气信息
if req.Extensions == "" {
req.Extensions = "base"
}
// 这里可以集成真实的天气API
responseMsg, err := w.getRealWeather(req)
if err != nil {
return fmt.Errorf("failed to get real weather: %w", err)
}
// 根据 extensions 参数返回不同的天气信息
if req.Extensions == "base" {
entitys.ResText(requireData.Ch, "", fmt.Sprintf("%s实时天气%s温度%.1f℃,湿度:%d%%,风速:%.1fkm/h风向%s",
req.City,
responseMsg.LiveWeather.Condition,
responseMsg.LiveWeather.Temperature,
responseMsg.LiveWeather.Humidity,
responseMsg.LiveWeather.WindSpeed,
responseMsg.LiveWeather.WindDirection))
} else {
rspStr := fmt.Sprintf("%s天气预报\n", req.City)
for _, forecast := range responseMsg.Forecasts {
rspStr += fmt.Sprintf("%s 温度:%.1f℃/%.1f℃ 风力:%s %s\n",
forecast.Date, forecast.DayTemp, forecast.NightTemp, forecast.DayWind, forecast.NightWind)
}
entitys.ResText(requireData.Ch, "", rspStr)
}
return nil
}
//// getMockWeather 获取模拟天气数据
//func (w *WeatherTool) getMockWeather(city, unit string) *WeatherResponse {
// rand.Seed(time.Now().UnixNano())
//
// // 模拟不同城市的基础温度
// baseTemp := map[string]float64{
// "北京": 15.0,
// "上海": 18.0,
// "广州": 25.0,
// "深圳": 26.0,
// "杭州": 17.0,
// "成都": 16.0,
// }
//
// temp := baseTemp[city]
// if temp == 0 {
// temp = 20.0 // 默认温度
// }
//
// // 添加随机变化
// temp += (rand.Float64() - 0.5) * 10
//
// // 转换温度单位
// if unit == "fahrenheit" {
// temp = temp*9/5 + 32
// }
//
// conditions := []string{"晴朗", "多云", "阴天", "小雨", "中雨"}
// condition := conditions[rand.Intn(len(conditions))]
//
// return &WeatherResponse{
// City: city,
// Temperature: float64(int(temp*10)) / 10, // 保留一位小数
// Unit: unit,
// Condition: condition,
// Humidity: rand.Intn(40) + 40, // 40-80%
// WindSpeed: float64(rand.Intn(20)) + 1.0,
// Timestamp: time.Now().Format("2006-01-02 15:04:05"),
// }
//}
// getRealWeather 调用高德天气API
func (w *WeatherTool) getRealWeather(request WeatherRequest) (*WeatherResponse, error) {
// 构建请求URL
req := l_request.Request{
Url: w.config.BaseURL,
Headers: map[string]string{},
Params: map[string]string{
"city": request.City, // 城市名称
"key": w.config.APIKey, // API密钥
// extensions: 基础天气数据 可选值base/all base:返回实况天气 all:返回预报天气
"extensions": request.Extensions, // 基础天气数据
"output": "JSON", // JSON格式返回
},
Method: "GET",
}
res, err := req.Send()
if err != nil {
return nil, err
}
// 解析API响应
var apiResp struct {
Status string `json:"status"`
Count string `json:"count"`
Info string `json:"info"`
Infocode string `json:"infocode"`
// 预报天气信息数据
Forecasts []struct {
City string `json:"city"`
Adcode string `json:"adcode"`
Province string `json:"province"`
Reporttime string `json:"reporttime"`
Casts []struct {
Date string `json:"date"`
Week string `json:"week"`
Dayweather string `json:"dayweather"`
Nightweather string `json:"nightweather"`
Daytemp string `json:"daytemp"`
Nighttemp string `json:"nighttemp"`
Daywind string `json:"daywind"`
Nightwind string `json:"nightwind"`
Daypower string `json:"daypower"`
Nightpower string `json:"nightpower"`
DaytempFloat string `json:"daytemp_float"`
NighttempFloat string `json:"nighttemp_float"`
} `json:"casts"`
} `json:"forecasts"`
// 实况天气信息数据
Lives []struct {
Province string `json:"province"`
City string `json:"city"`
Adcode string `json:"adcode"`
Weather string `json:"weather"`
Temperature string `json:"temperature"`
Winddirection string `json:"winddirection"`
Windpower string `json:"windpower"`
Humidity string `json:"humidity"`
Reporttime string `json:"reporttime"`
TemperatureFloat string `json:"temperature_float"`
HumidityFloat string `json:"humidity_float"`
} `json:"lives"`
}
log.Printf("weather API response: %s", string(res.Content))
if err = json.Unmarshal(res.Content, &apiResp); err != nil {
return nil, fmt.Errorf("parse weather API response failed: %w", err)
}
// 检查API返回状态
if apiResp.Status != "1" {
return nil, fmt.Errorf("weather API returned error: %s, info: %s", apiResp.Status, apiResp.Info)
}
// 获取城市名称
cityName := ""
if len(apiResp.Lives) > 0 {
cityName = apiResp.Lives[0].City
} else if len(apiResp.Forecasts) > 0 {
cityName = apiResp.Forecasts[0].City
} else {
return nil, fmt.Errorf("no weather data found")
}
// 构建响应
response := &WeatherResponse{
City: cityName,
Unit: request.Unit,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
// 处理实时天气
if len(apiResp.Lives) > 0 {
liveData := apiResp.Lives[0]
// 转换温度
temp, _ := strconv.ParseFloat(liveData.Temperature, 64)
if request.Unit == "fahrenheit" {
temp = temp*9/5 + 32
}
// 转换湿度和风速
humidity, _ := strconv.Atoi(liveData.Humidity)
windSpeed, _ := strconv.ParseFloat(liveData.Windpower, 64)
response.LiveWeather = &LiveWeather{
Temperature: temp,
Condition: liveData.Weather,
Humidity: humidity,
WindSpeed: windSpeed,
WindDirection: liveData.Winddirection,
}
}
// 处理预报天气
if len(apiResp.Forecasts) > 0 && len(apiResp.Forecasts[0].Casts) > 0 {
response.Forecasts = make([]ForecastWeather, 0, len(apiResp.Forecasts[0].Casts))
for _, cast := range apiResp.Forecasts[0].Casts {
// 转换温度
dayTemp, _ := strconv.ParseFloat(cast.Daytemp, 64)
nightTemp, _ := strconv.ParseFloat(cast.Nighttemp, 64)
if request.Unit == "fahrenheit" {
dayTemp = dayTemp*9/5 + 32
nightTemp = nightTemp*9/5 + 32
}
forecast := ForecastWeather{
Date: cast.Date,
Week: cast.Week,
DayWeather: cast.Dayweather,
NightWeather: cast.Nightweather,
DayTemp: dayTemp,
NightTemp: nightTemp,
DayWind: cast.Daywind,
NightWind: cast.Nightwind,
DayWindPower: cast.Daypower,
NightWindPower: cast.Nightpower,
}
response.Forecasts = append(response.Forecasts, forecast)
}
}
return response, nil
}

View File

@ -1,139 +0,0 @@
package tools
import (
"ai_scheduler/internal/entitys"
"context"
"encoding/json"
"fmt"
"math/rand"
"time"
)
// WeatherTool 天气查询工具
type WeatherTool struct {
mockData bool
}
// NewWeatherTool 创建天气工具
func NewWeatherTool() *WeatherTool {
return &WeatherTool{}
}
// Name 返回工具名称
func (w *WeatherTool) Name() string {
return "get_weather"
}
// Description 返回工具描述
func (w *WeatherTool) Description() string {
return "获取指定城市的天气信息"
}
// Definition 返回工具定义
func (w *WeatherTool) Definition() entitys.ToolDefinition {
return entitys.ToolDefinition{
Type: "function",
Function: entitys.FunctionDef{
Name: w.Name(),
Description: w.Description(),
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"description": "城市名称,如:北京、上海、广州",
},
"unit": map[string]interface{}{
"type": "string",
"description": "温度单位celsius(摄氏度)或fahrenheit(华氏度)",
"enum": []string{"celsius", "fahrenheit"},
"default": "celsius",
},
},
"required": []string{"city"},
},
},
}
}
// WeatherRequest 天气请求参数
type WeatherRequest struct {
City string `json:"city"`
Unit string `json:"unit,omitempty"`
}
// WeatherResponse 天气响应
type WeatherResponse struct {
City string `json:"city"`
Temperature float64 `json:"temperature"`
Unit string `json:"unit"`
Condition string `json:"condition"`
Humidity int `json:"humidity"`
WindSpeed float64 `json:"wind_speed"`
Timestamp string `json:"timestamp"`
}
// Execute 执行天气查询
func (w *WeatherTool) Execute(ctx context.Context, args json.RawMessage) (interface{}, error) {
var req WeatherRequest
if err := json.Unmarshal(args, &req); err != nil {
return nil, fmt.Errorf("invalid weather request: %w", err)
}
if req.City == "" {
return nil, fmt.Errorf("city is required")
}
if req.Unit == "" {
req.Unit = "celsius"
}
if w.mockData {
return w.getMockWeather(req.City, req.Unit), nil
}
// 这里可以集成真实的天气API
return w.getMockWeather(req.City, req.Unit), nil
}
// getMockWeather 获取模拟天气数据
func (w *WeatherTool) getMockWeather(city, unit string) *WeatherResponse {
rand.Seed(time.Now().UnixNano())
// 模拟不同城市的基础温度
baseTemp := map[string]float64{
"北京": 15.0,
"上海": 18.0,
"广州": 25.0,
"深圳": 26.0,
"杭州": 17.0,
"成都": 16.0,
}
temp := baseTemp[city]
if temp == 0 {
temp = 20.0 // 默认温度
}
// 添加随机变化
temp += (rand.Float64() - 0.5) * 10
// 转换温度单位
if unit == "fahrenheit" {
temp = temp*9/5 + 32
}
conditions := []string{"晴朗", "多云", "阴天", "小雨", "中雨"}
condition := conditions[rand.Intn(len(conditions))]
return &WeatherResponse{
City: city,
Temperature: float64(int(temp*10)) / 10, // 保留一位小数
Unit: unit,
Condition: condition,
Humidity: rand.Intn(40) + 40, // 40-80%
WindSpeed: float64(rand.Intn(20)) + 1.0,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
}

View File

@ -1,4 +1,4 @@
package tools package zltx
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -1,4 +1,4 @@
package tools package zltx
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -1,4 +1,4 @@
package tools package zltx
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -1,4 +1,4 @@
package tools package zltx
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -1,4 +1,4 @@
package tools package zltx
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -1,4 +1,4 @@
package tools package zltx
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"

View File

@ -13,10 +13,10 @@ type Rdb struct {
var rdb *Rdb var rdb *Rdb
func NewRdb(c *config.Redis) *Rdb { func NewRdb(c *config.Config) *Rdb {
if rdb == nil { if rdb == nil {
//构建 redis //构建 redis
rdbBuild := buildRdb(c) rdbBuild := buildRdb(&c.Redis)
//退出时清理资源 //退出时清理资源
rdb = &Rdb{Rdb: rdbBuild} rdb = &Rdb{Rdb: rdbBuild}
} }