496 lines
16 KiB
Go
496 lines
16 KiB
Go
package biz
|
||
|
||
import (
|
||
"ai_scheduler/internal/biz/do"
|
||
"ai_scheduler/internal/biz/handle/dingtalk"
|
||
"ai_scheduler/internal/biz/handle/qywx"
|
||
"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/tools"
|
||
"ai_scheduler/internal/tools/bbxt"
|
||
"ai_scheduler/tmpl/dataTemp"
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
"unicode"
|
||
|
||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||
|
||
"github.com/gofiber/fiber/v2/log"
|
||
"xorm.io/builder"
|
||
)
|
||
|
||
// AiRouterBiz 智能路由服务
|
||
type DingTalkBotBiz struct {
|
||
do *do.Do
|
||
handle *do.Handle
|
||
botConfigImpl *impl.BotConfigImpl
|
||
replier *chatbot.ChatbotReplier
|
||
log log.Logger
|
||
dingTalkUser *dingtalk.User
|
||
botGroupImpl *impl.BotGroupImpl
|
||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||
botGroupQywxImpl *impl.BotGroupQywxImpl
|
||
toolManager *tools.Manager
|
||
chatHis *impl.BotChatHisImpl
|
||
conf *config.Config
|
||
cardSend *dingtalk.SendCardClient
|
||
qywxGroupHandle *qywx.Group
|
||
groupConfigBiz *GroupConfigBiz
|
||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||
}
|
||
|
||
// NewDingTalkBotBiz
|
||
func NewDingTalkBotBiz(
|
||
do *do.Do,
|
||
handle *do.Handle,
|
||
botConfigImpl *impl.BotConfigImpl,
|
||
botGroupImpl *impl.BotGroupImpl,
|
||
dingTalkUser *dingtalk.User,
|
||
chatHis *impl.BotChatHisImpl,
|
||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||
toolManager *tools.Manager,
|
||
conf *config.Config,
|
||
cardSend *dingtalk.SendCardClient,
|
||
groupConfigBiz *GroupConfigBiz,
|
||
) *DingTalkBotBiz {
|
||
return &DingTalkBotBiz{
|
||
do: do,
|
||
handle: handle,
|
||
botConfigImpl: botConfigImpl,
|
||
replier: chatbot.NewChatbotReplier(),
|
||
dingTalkUser: dingTalkUser,
|
||
groupConfigBiz: groupConfigBiz,
|
||
botGroupImpl: botGroupImpl,
|
||
toolManager: toolManager,
|
||
chatHis: chatHis,
|
||
conf: conf,
|
||
cardSend: cardSend,
|
||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||
}
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) GetDingTalkBotCfgList() (dingBotList []entitys.DingTalkBot, err error) {
|
||
botConfig := make([]model.AiBotConfig, 0)
|
||
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"status": constants.Enable})
|
||
cond = cond.And(builder.Eq{"bot_type": constants.BotTypeDingTalk})
|
||
err = d.botConfigImpl.GetRangeToMapStruct(&cond, &botConfig)
|
||
for _, v := range botConfig {
|
||
var config entitys.DingTalkBot
|
||
err = json.Unmarshal([]byte(v.BotConfig), &config)
|
||
if err != nil {
|
||
d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error())
|
||
}
|
||
config.BotIndex = v.RobotCode
|
||
dingBotList = append(dingBotList, config)
|
||
}
|
||
return
|
||
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallbackDataModel) (requireData *entitys.RequireDataDingTalkBot, err error) {
|
||
requireData = &entitys.RequireDataDingTalkBot{
|
||
Req: data,
|
||
Ch: make(chan entitys.Response, 2),
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||
//entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等")
|
||
//defer close(requireData.Ch)
|
||
switch constants.ConversationType(requireData.Req.ConversationType) {
|
||
case constants.ConversationTypeSingle:
|
||
err = d.handleSingleChat(ctx, requireData)
|
||
case constants.ConversationTypeGroup:
|
||
err = d.handleGroupChat(ctx, requireData)
|
||
default:
|
||
err = errors.New("未知的聊天类型:" + requireData.Req.ConversationType)
|
||
}
|
||
if err != nil {
|
||
entitys.ResText(requireData.Ch, "", err.Error())
|
||
}
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||
entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新")
|
||
return
|
||
//requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1))
|
||
//if err != nil {
|
||
// return
|
||
//}
|
||
//requireData.ID=requireData.UserInfo.UserID
|
||
////如果不是管理或者不是老板,则进行权限判断
|
||
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
|
||
//
|
||
//}
|
||
//return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||
group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle, requireData.Req.RobotCode)
|
||
if err != nil {
|
||
return
|
||
}
|
||
groupConfig, err := d.groupConfigBiz.GetGroupConfig(ctx, group.ConfigID)
|
||
if err != nil {
|
||
return
|
||
}
|
||
//宏
|
||
err, isFinal := d.Macro(ctx, requireData, groupConfig)
|
||
if err != nil {
|
||
return
|
||
}
|
||
if isFinal {
|
||
return
|
||
}
|
||
requireData.ID = group.GroupID
|
||
groupTools, err := d.groupConfigBiz.getGroupTools(ctx, groupConfig)
|
||
if err != nil {
|
||
return
|
||
}
|
||
rec, err := d.recognize(ctx, requireData, groupTools)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig)
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) Macro(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, groupConfig *model.AiBotGroupConfig) (err error, isFinish bool) {
|
||
content := processString(requireData.Req.Text.Content)
|
||
|
||
if strings.Contains(content, "[利润同比报表]商品修改:") {
|
||
// 提取冒号后的内容
|
||
if parts := strings.SplitN(content, ":", 2); len(parts) == 2 {
|
||
itemInfo := strings.TrimSpace(parts[1])
|
||
log.Infof("商品修改信息: %s", itemInfo)
|
||
groupConfig.ProductName = itemInfo
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"config_id": groupConfig.ConfigID})
|
||
err = d.botGroupImpl.UpdateByCond(&cond, groupConfig)
|
||
if err != nil {
|
||
entitys.ResText(requireData.Ch, "", fmt.Sprintf("修改失败:%v", err))
|
||
}
|
||
entitys.ResText(requireData.Ch, "", "修改成功")
|
||
isFinish = true
|
||
return
|
||
}
|
||
}
|
||
|
||
if strings.Contains(content, "[利润同比报表]商品列表") {
|
||
// 提取冒号后的内容
|
||
if len(groupConfig.ProductName) == 0 {
|
||
entitys.ResText(requireData.Ch, "", "暂未设置")
|
||
} else {
|
||
entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
|
||
isFinish = true
|
||
}
|
||
return
|
||
}
|
||
|
||
if strings.Contains(content, "[负利润分析]获取") {
|
||
var (
|
||
data model.AiReportDailyCache
|
||
value map[int32]*bbxt.ResellerLossSumProductRelation
|
||
)
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"`index`": bbxt.IndexLossSumDetail})
|
||
cond = cond.And(builder.Eq{"`key`": time.Now().Format(time.DateOnly)})
|
||
err = d.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &data)
|
||
if err != nil {
|
||
entitys.ResText(requireData.Ch, "", "获取失败")
|
||
return
|
||
}
|
||
err = json.Unmarshal([]byte(data.Value), &value)
|
||
if err != nil {
|
||
entitys.ResText(requireData.Ch, "", "获取失败,格式解析错误")
|
||
return
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
if strings.Contains(content, "[负利润分析]更新") {
|
||
// 提取冒号后的内容
|
||
if len(groupConfig.ProductName) == 0 {
|
||
entitys.ResText(requireData.Ch, "", "暂未设置")
|
||
} else {
|
||
entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
|
||
isFinish = true
|
||
}
|
||
return
|
||
}
|
||
|
||
if strings.Contains(content, "[负利润分析]同步") {
|
||
// 提取冒号后的内容
|
||
if len(groupConfig.ProductName) == 0 {
|
||
entitys.ResText(requireData.Ch, "", "暂未设置")
|
||
} else {
|
||
entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
|
||
isFinish = true
|
||
}
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
func processString(s string) string {
|
||
// 1. 替换中文逗号为英文逗号
|
||
s = strings.ReplaceAll(s, ",", ",")
|
||
|
||
// 2. 过滤控制字符(如 \n, \t, \r 等)
|
||
var result []rune
|
||
for _, char := range s {
|
||
// 判断是否是控制字符(ASCII < 32 或 = 127)
|
||
if !unicode.IsControl(char) {
|
||
// 如果需要完全移除 \n 和 \t,可以改成:
|
||
// if !unicode.IsControl(char)
|
||
result = append(result, char)
|
||
}
|
||
}
|
||
|
||
return string(result)
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) {
|
||
group, err = d.botGroupImpl.GetByConversationIdAndRobotCode(conversationId, robotCode)
|
||
if err != nil {
|
||
if !errors.Is(err, sql.ErrNoRows) {
|
||
|
||
return
|
||
}
|
||
}
|
||
|
||
if group.GroupID == 0 {
|
||
group = &model.AiBotGroup{
|
||
ConversationID: conversationId,
|
||
Title: conversationTitle,
|
||
RobotCode: robotCode,
|
||
}
|
||
//如果不存在则创建
|
||
_, err = d.botGroupImpl.Add(group)
|
||
}
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, tools []model.AiBotTool) (rec *entitys.Recognize, err error) {
|
||
|
||
userContent, err := d.getUserContent(requireData.Req.Msgtype, requireData.Req.Text.Content)
|
||
if err != nil {
|
||
return
|
||
}
|
||
rec = &entitys.Recognize{
|
||
Ch: requireData.Ch,
|
||
SystemPrompt: d.defaultPrompt(),
|
||
UserContent: userContent,
|
||
}
|
||
//历史记录
|
||
rec.ChatHis, err = d.getHis(ctx, constants.ConversationType(requireData.Req.ConversationType), requireData.ID)
|
||
if err != nil {
|
||
return
|
||
}
|
||
//工具注册
|
||
if len(tools) > 0 {
|
||
rec.Tasks = make([]entitys.RegistrationTask, 0, len(tools))
|
||
for _, task := range tools {
|
||
taskConfig := entitys.TaskConfigDetail{}
|
||
if err = json.Unmarshal([]byte(task.Config), &taskConfig); err != nil {
|
||
log.Errorf("解析任务配置失败: %s, 任务ID: %s", err.Error(), task.Index)
|
||
continue // 解析失败时跳过该任务,而不是直接返回错误
|
||
}
|
||
|
||
rec.Tasks = append(rec.Tasks, entitys.RegistrationTask{
|
||
Name: task.Index,
|
||
Desc: task.Desc,
|
||
TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建
|
||
})
|
||
}
|
||
}
|
||
err = d.handle.Recognize(ctx, rec, &do.WithDingTalkBot{})
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.ConversationType, Id int32) (content entitys.ChatHis, err error) {
|
||
|
||
var (
|
||
his []model.AiBotChatHi
|
||
)
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"his_type": conversationType})
|
||
cond = cond.And(builder.Eq{"id": Id})
|
||
_, err = d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: d.conf.Sys.SessionLen}, &his, "his_id desc")
|
||
if err != nil {
|
||
return
|
||
}
|
||
messages := make([]entitys.HisMessage, 0, len(his))
|
||
for _, v := range his {
|
||
messages = append(messages, entitys.HisMessage{
|
||
Role: constants.Caller(v.Role), // 用户角色
|
||
Content: v.Content, // 用户输入内容
|
||
Timestamp: v.CreateAt.Format(time.DateTime),
|
||
})
|
||
}
|
||
return entitys.ChatHis{
|
||
SessionId: fmt.Sprintf("%s_%d", conversationType, Id),
|
||
Messages: messages,
|
||
Context: entitys.HisContext{
|
||
UserLanguage: constants.LangZhCN, // 默认中文
|
||
SystemMode: constants.SystemModeTechnicalSupport, // 默认技术支持模式
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{}) (content *entitys.RecognizeUserContent, err error) {
|
||
switch constants.BotMsgType(msgType) {
|
||
case constants.BotMsgTypeText:
|
||
content = &entitys.RecognizeUserContent{
|
||
Text: msgContent.(string),
|
||
}
|
||
default:
|
||
return nil, errors.New("未知的消息类型:" + msgType)
|
||
}
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotCallbackDataModel, content chan string) (err error) {
|
||
err = d.cardSend.NewCard(ctx, &dingtalk.CardSend{
|
||
RobotCode: data.RobotCode,
|
||
ConversationType: constants.ConversationType(data.ConversationType),
|
||
Template: constants.CardTempDefault,
|
||
ContentChannel: content, // 指定内容通道
|
||
ConversationId: data.ConversationId,
|
||
SenderStaffId: data.SenderStaffId,
|
||
Title: data.Text.Content,
|
||
})
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroup, report *bbxt.ReportRes) (err error) {
|
||
|
||
reportChan := make(chan string, 10)
|
||
defer close(reportChan)
|
||
reportChan <- report.Title
|
||
reportChan <- fmt.Sprintf("", report.Url)
|
||
err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
||
RobotCode: groupInfo.RobotCode,
|
||
ConversationType: constants.ConversationTypeGroup,
|
||
ConversationId: groupInfo.ConversationID,
|
||
Text: chatbot.BotCallbackDataTextModel{
|
||
Content: report.ReportName,
|
||
},
|
||
}, reportChan)
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) GetGroupInfo(ctx context.Context, groupId int) (group model.AiBotGroup, err error) {
|
||
|
||
cond := builder.NewCond()
|
||
cond = cond.And(builder.Eq{"group_id": groupId})
|
||
cond = cond.And(builder.Eq{"status": constants.Enable})
|
||
err = d.botGroupImpl.GetOneBySearchToStrut(&cond, &group)
|
||
|
||
return
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) ReplyText(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||
msg := content
|
||
if len(arg) > 0 {
|
||
msg = fmt.Sprintf(content, arg)
|
||
}
|
||
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) replyImg(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||
msg := content
|
||
if len(arg) > 0 {
|
||
msg = fmt.Sprintf(content, arg)
|
||
}
|
||
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) replyFile(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||
msg := content
|
||
if len(arg) > 0 {
|
||
msg = fmt.Sprintf(content, arg)
|
||
}
|
||
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) replyMarkdown(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||
msg := content
|
||
if len(arg) > 0 {
|
||
msg = fmt.Sprintf(content, arg)
|
||
}
|
||
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) replyActionCard(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||
msg := content
|
||
if len(arg) > 0 {
|
||
msg = fmt.Sprintf(content, arg)
|
||
}
|
||
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) SaveHis(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, chat []string) (err error) {
|
||
if len(chat) == 0 {
|
||
return
|
||
}
|
||
his := []*model.AiBotChatHi{
|
||
{
|
||
HisType: requireData.Req.ConversationType,
|
||
ID: requireData.ID,
|
||
Role: "user",
|
||
Content: requireData.Req.Text.Content,
|
||
},
|
||
{
|
||
HisType: requireData.Req.ConversationType,
|
||
ID: requireData.ID,
|
||
Role: "system",
|
||
Content: strings.Join(chat, "\n"),
|
||
},
|
||
}
|
||
_, err = d.chatHis.Add(his)
|
||
return err
|
||
}
|
||
|
||
func (d *DingTalkBotBiz) defaultPrompt() string {
|
||
now := time.Now().Format(time.DateTime)
|
||
return `[system] 你是一个智能路由系统,核心职责是 **精准解析用户意图并路由至对应任务模块**。请严格遵循以下规则:
|
||
[rule]
|
||
1. **返回格式**:
|
||
仅输出以下 **严格格式化的 JSON 字符串**(禁用 Markdown):
|
||
{ "index": "工具索引index", "confidence": 0.0-1.0,"reasoning": "判断理由","parameters":"jsonstring |提取参数","is_match":true||false,"chat": "追问内容"}
|
||
关键规则(按优先级排序):
|
||
|
||
2. **工具匹配**:
|
||
|
||
- 若匹配到工具,使用工具的 parameters 作为模板做参数匹配
|
||
- 注意区分 parameters 中的 必须参数(required) 和 可选参数(optional),按下述参数提取规则处理。
|
||
- 若**完全无法匹配**,立即设置 is_match: false,并在 chat 中已第一人称的角度提醒用户需要适用何种工具(例:"请问您是要查询订单还是商品呢")。
|
||
|
||
1. **参数提取**:
|
||
|
||
- 根据 parameters 字段列出的参数名,从用户输入中提取对应值。
|
||
- **仅提取**明确提及的参数,忽略未列出的内容。
|
||
- 必须参数仅使用用户直接提及的参数,不允许从上下文推断。
|
||
- 若必须参数缺失,立即设置 is_match: false,并在 chat 中已第一人称的角度提醒用户提供缺少的参数追问(例:"需要您补充XX信息")。
|
||
|
||
4. 格式强制要求:
|
||
-所有字段值必须是**字符串**(包括 confidence)。
|
||
-parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。
|
||
当前时间:` + now + `,所有的时间识别精确到秒`
|
||
}
|