Compare commits
18 Commits
2f5b0af3a4
...
02e88ed610
| Author | SHA1 | Date |
|---|---|---|
|
|
02e88ed610 | |
|
|
b104572e1b | |
|
|
99865c2bc4 | |
|
|
cfeaa6e201 | |
|
|
c9c9bca9ce | |
|
|
ec41a3d787 | |
|
|
719fd805e6 | |
|
|
c174ab683a | |
|
|
c1971e71c1 | |
|
|
ece04df2cb | |
|
|
7e71ad52a4 | |
|
|
847eb8b5db | |
|
|
3b6471a196 | |
|
|
b3b09f184b | |
|
|
21585e731f | |
|
|
71ed8146f5 | |
|
|
22b7a3d096 | |
|
|
9418d95a65 |
|
|
@ -13,6 +13,7 @@ func main() {
|
||||||
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
|
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
|
||||||
onBot := flag.String("bot", "", "bot start")
|
onBot := flag.String("bot", "", "bot start")
|
||||||
cron := flag.String("cron", "", "close")
|
cron := flag.String("cron", "", "close")
|
||||||
|
runJob := flag.String("runJob", "", "run single job and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
bc, err := config.LoadConfig(*configPath)
|
bc, err := config.LoadConfig(*configPath)
|
||||||
|
|
@ -33,6 +34,11 @@ func main() {
|
||||||
if *cron == "start" {
|
if *cron == "start" {
|
||||||
app.Cron.Run(ctx)
|
app.Cron.Run(ctx)
|
||||||
}
|
}
|
||||||
|
// 运行指定任务并退出
|
||||||
|
if *runJob != "" {
|
||||||
|
app.Cron.RunOnce(ctx, *runJob)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ sys:
|
||||||
channel_pool_len: 100
|
channel_pool_len: 100
|
||||||
channel_pool_size: 32
|
channel_pool_size: 32
|
||||||
llm_pool_len: 5
|
llm_pool_len: 5
|
||||||
heartbeat_interval: 30
|
heartbeat_interval: 300
|
||||||
key: report-api
|
key: report-api
|
||||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||||
minIdleConns: 2 #最小空闲连接数
|
minIdleConns: 2 #最小空闲连接数
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ server:
|
||||||
|
|
||||||
ollama:
|
ollama:
|
||||||
base_url: "http://192.168.6.115:11434"
|
base_url: "http://192.168.6.115:11434"
|
||||||
model: "qwen3-coder:480b-cloud"
|
model: "qwen3:8b"
|
||||||
generate_model: "qwen3-coder:480b-cloud"
|
generate_model: "qwen3:8b"
|
||||||
mapping_model: "deepseek-v3.2:cloud"
|
mapping_model: "qwen3:8b"
|
||||||
|
# model: "qwen3-coder:480b-cloud"
|
||||||
|
# generate_model: "qwen3-coder:480b-cloud"
|
||||||
|
# mapping_model: "deepseek-v3.2:cloud"
|
||||||
vl_model: "qwen2.5vl:7b"
|
vl_model: "qwen2.5vl:7b"
|
||||||
timeout: "120s"
|
timeout: "120s"
|
||||||
level: "info"
|
level: "info"
|
||||||
|
|
@ -88,7 +91,22 @@ 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"
|
||||||
|
zltxResellerAuthProductToManagerAndDefaultLossReason:
|
||||||
|
base_url: "https://revcl.1688sup.com/api/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason"
|
||||||
# eino tool 配置
|
# eino tool 配置
|
||||||
eino_tools:
|
eino_tools:
|
||||||
# == 货易通 hyt ==
|
# == 货易通 hyt ==
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -62,6 +62,7 @@ require (
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/duke-git/lancet/v2 v2.3.8 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/eino-contrib/jsonschema v1.0.3 // indirect
|
github.com/eino-contrib/jsonschema v1.0.3 // indirect
|
||||||
github.com/eino-contrib/ollama v0.1.0 // indirect
|
github.com/eino-contrib/ollama v0.1.0 // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -155,6 +155,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/duke-git/lancet/v2 v2.3.8 h1:dlkqn6Nj2LRWFuObNxttkMHxrFeaV6T26JR8jbEVbPg=
|
||||||
|
github.com/duke-git/lancet/v2 v2.3.8/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
||||||
|
|
|
||||||
|
|
@ -77,33 +77,23 @@ func (c *CallbackBiz) issueHandlingExtractContent(data chatbot.BotCallbackDataMo
|
||||||
}
|
}
|
||||||
// 解析 JSON 响应
|
// 解析 JSON 响应
|
||||||
var resp struct {
|
var resp struct {
|
||||||
Items []struct {
|
Question string `json:"question"`
|
||||||
Question string `json:"question"`
|
Answer string `json:"answer"`
|
||||||
Answer string `json:"answer"`
|
Confidence string `json:"confidence"`
|
||||||
Confidence string `json:"confidence"`
|
|
||||||
} `json:"items"`
|
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil {
|
if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil {
|
||||||
log.Errorf("解析 JSON 响应失败: %v", err)
|
log.Errorf("解析 JSON 响应失败: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.构建文本域内容
|
// 2.获取应用AppKey
|
||||||
cardContentTpl := "问题:%s \n答案:%s"
|
|
||||||
var cardContentList []string
|
|
||||||
for _, item := range resp.Items {
|
|
||||||
cardContentList = append(cardContentList, fmt.Sprintf(cardContentTpl, item.Question, item.Answer))
|
|
||||||
}
|
|
||||||
cardContent := strings.Join(cardContentList, "\n\n")
|
|
||||||
|
|
||||||
// 3.获取应用AppKey
|
|
||||||
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
|
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("获取应用配置失败: %v", err)
|
log.Errorf("获取应用配置失败: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4.创建并投放卡片
|
// 3.创建并投放卡片
|
||||||
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId
|
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId
|
||||||
_, err = c.dingtalkCardClient.CreateAndDeliver(
|
_, err = c.dingtalkCardClient.CreateAndDeliver(
|
||||||
appKey,
|
appKey,
|
||||||
|
|
@ -114,13 +104,14 @@ func (c *CallbackBiz) issueHandlingExtractContent(data chatbot.BotCallbackDataMo
|
||||||
CallbackRouteKey: tea.String(c.cfg.Dingtalk.Card.CallbackRouteKey),
|
CallbackRouteKey: tea.String(c.cfg.Dingtalk.Card.CallbackRouteKey),
|
||||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||||
CardParamMap: map[string]*string{
|
CardParamMap: map[string]*string{
|
||||||
|
"_CARD_DEBUG_TOOL_ENTRY": tea.String(c.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段
|
||||||
"title": tea.String("QA知识收集"),
|
"title": tea.String("QA知识收集"),
|
||||||
"button_display": tea.String("true"),
|
"button_display": tea.String("true"),
|
||||||
"QA_details_now": tea.String(cardContent),
|
|
||||||
"textarea_display": tea.String("normal"),
|
"textarea_display": tea.String("normal"),
|
||||||
"action_id": tea.String("collect_qa"),
|
"action_id": tea.String("collect_qa"),
|
||||||
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
|
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
|
||||||
"_CARD_DEBUG_TOOL_ENTRY": tea.String(c.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段
|
"question": tea.String(resp.Question),
|
||||||
|
"answer": tea.String(resp.Answer),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||||
|
|
@ -223,15 +214,21 @@ func (c *CallbackBiz) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDa
|
||||||
func (c *CallbackBiz) IssueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
|
func (c *CallbackBiz) IssueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
|
||||||
// 确认提交,文本写入知识库
|
// 确认提交,文本写入知识库
|
||||||
if data.CardActionData.CardPrivateData.Params["submit"] == "submit" {
|
if data.CardActionData.CardPrivateData.Params["submit"] == "submit" {
|
||||||
content := data.CardActionData.CardPrivateData.Params["QA_details"].(string)
|
question := data.CardActionData.CardPrivateData.Params["question_local"].(string)
|
||||||
|
answer := data.CardActionData.CardPrivateData.Params["answer_local"].(string)
|
||||||
tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string)
|
tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string)
|
||||||
|
|
||||||
// 协程执行耗时操作,防止阻塞
|
// 协程执行耗时操作,防止阻塞
|
||||||
util.SafeGo("inject_knowledge_base", func() {
|
util.SafeGo("inject_knowledge_base", func() {
|
||||||
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
|
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
|
||||||
err := knowledgeBase.IngestText(&knowledge_base.IngestTextRequest{
|
err := knowledgeBase.IngestBatchQA(&knowledge_base.IngestBacthQARequest{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
Text: content,
|
QAList: []*knowledge_base.QA{
|
||||||
|
{
|
||||||
|
Question: question,
|
||||||
|
Answer: answer,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("注入知识库失败: %v", err)
|
log.Errorf("注入知识库失败: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ import (
|
||||||
"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/domain/tools/common/knowledge_base"
|
||||||
"ai_scheduler/internal/entitys"
|
"ai_scheduler/internal/entitys"
|
||||||
"ai_scheduler/internal/pkg"
|
"ai_scheduler/internal/pkg"
|
||||||
"ai_scheduler/internal/tools"
|
"ai_scheduler/internal/tools"
|
||||||
"ai_scheduler/internal/tools/bbxt"
|
"ai_scheduler/internal/tools/bbxt"
|
||||||
"ai_scheduler/tmpl/dataTemp"
|
"ai_scheduler/tmpl/dataTemp"
|
||||||
|
"ai_scheduler/utils"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -25,10 +27,13 @@ import (
|
||||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||||
|
|
||||||
dingtalkPkg "ai_scheduler/internal/pkg/dingtalk"
|
dingtalkPkg "ai_scheduler/internal/pkg/dingtalk"
|
||||||
|
"ai_scheduler/internal/pkg/util"
|
||||||
|
|
||||||
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||||
"github.com/alibabacloud-go/tea/tea"
|
"github.com/alibabacloud-go/tea/tea"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/gofiber/fiber/v2/log"
|
"github.com/gofiber/fiber/v2/log"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -45,6 +50,7 @@ type DingTalkBotBiz struct {
|
||||||
botGroupQywxImpl *impl.BotGroupQywxImpl
|
botGroupQywxImpl *impl.BotGroupQywxImpl
|
||||||
toolManager *tools.Manager
|
toolManager *tools.Manager
|
||||||
chatHis *impl.BotChatHisImpl
|
chatHis *impl.BotChatHisImpl
|
||||||
|
botUserImpl *impl.BotUserImpl
|
||||||
conf *config.Config
|
conf *config.Config
|
||||||
cardSend *dingtalk.SendCardClient
|
cardSend *dingtalk.SendCardClient
|
||||||
qywxGroupHandle *qywx.Group
|
qywxGroupHandle *qywx.Group
|
||||||
|
|
@ -54,6 +60,9 @@ type DingTalkBotBiz struct {
|
||||||
dingtalkOauth2Client *dingtalkPkg.Oauth2Client
|
dingtalkOauth2Client *dingtalkPkg.Oauth2Client
|
||||||
dingTalkOld *dingtalkPkg.OldClient
|
dingTalkOld *dingtalkPkg.OldClient
|
||||||
dingtalkCardClient *dingtalkPkg.CardClient
|
dingtalkCardClient *dingtalkPkg.CardClient
|
||||||
|
redisCli *redis.Client
|
||||||
|
issueImpl *impl.IssueImpl
|
||||||
|
sysImpl *impl.SysImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDingTalkBotBiz
|
// NewDingTalkBotBiz
|
||||||
|
|
@ -65,6 +74,7 @@ func NewDingTalkBotBiz(
|
||||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||||
dingTalkUser *dingtalk.User,
|
dingTalkUser *dingtalk.User,
|
||||||
chatHis *impl.BotChatHisImpl,
|
chatHis *impl.BotChatHisImpl,
|
||||||
|
botUserImpl *impl.BotUserImpl,
|
||||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||||
toolManager *tools.Manager,
|
toolManager *tools.Manager,
|
||||||
conf *config.Config,
|
conf *config.Config,
|
||||||
|
|
@ -74,6 +84,9 @@ func NewDingTalkBotBiz(
|
||||||
dingtalkOauth2Client *dingtalkPkg.Oauth2Client,
|
dingtalkOauth2Client *dingtalkPkg.Oauth2Client,
|
||||||
dingTalkOld *dingtalkPkg.OldClient,
|
dingTalkOld *dingtalkPkg.OldClient,
|
||||||
dingtalkCardClient *dingtalkPkg.CardClient,
|
dingtalkCardClient *dingtalkPkg.CardClient,
|
||||||
|
rdb *utils.Rdb,
|
||||||
|
issueImpl *impl.IssueImpl,
|
||||||
|
sysImpl *impl.SysImpl,
|
||||||
) *DingTalkBotBiz {
|
) *DingTalkBotBiz {
|
||||||
return &DingTalkBotBiz{
|
return &DingTalkBotBiz{
|
||||||
do: do,
|
do: do,
|
||||||
|
|
@ -86,6 +99,7 @@ func NewDingTalkBotBiz(
|
||||||
botGroupConfigImpl: botGroupConfigImpl,
|
botGroupConfigImpl: botGroupConfigImpl,
|
||||||
toolManager: toolManager,
|
toolManager: toolManager,
|
||||||
chatHis: chatHis,
|
chatHis: chatHis,
|
||||||
|
botUserImpl: botUserImpl,
|
||||||
conf: conf,
|
conf: conf,
|
||||||
cardSend: cardSend,
|
cardSend: cardSend,
|
||||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||||
|
|
@ -93,6 +107,9 @@ func NewDingTalkBotBiz(
|
||||||
dingtalkOauth2Client: dingtalkOauth2Client,
|
dingtalkOauth2Client: dingtalkOauth2Client,
|
||||||
dingTalkOld: dingTalkOld,
|
dingTalkOld: dingTalkOld,
|
||||||
dingtalkCardClient: dingtalkCardClient,
|
dingtalkCardClient: dingtalkCardClient,
|
||||||
|
redisCli: rdb.Rdb,
|
||||||
|
issueImpl: issueImpl,
|
||||||
|
sysImpl: sysImpl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,19 +159,294 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
// handleSingleChat 单聊处理
|
||||||
entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新")
|
// 先不接意图识别-仅提供问题处理
|
||||||
return
|
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error {
|
||||||
//requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1))
|
// 1. 获取用户信息
|
||||||
//if err != nil {
|
user, err := d.botUserImpl.GetByStaffId(requireData.Req.SenderStaffId)
|
||||||
// return
|
if err != nil {
|
||||||
//}
|
return err
|
||||||
//requireData.ID=requireData.UserInfo.UserID
|
}
|
||||||
////如果不是管理或者不是老板,则进行权限判断
|
requireData.ID = int32(user.UserID)
|
||||||
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
|
requireData.UserInfo = &entitys.DingTalkUserInfo{
|
||||||
//
|
UserId: int(user.UserID),
|
||||||
//}
|
StaffId: user.StaffID,
|
||||||
//return
|
Name: user.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取历史记录 (最近6轮用户输入)
|
||||||
|
userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, d.conf.Sys.SessionLen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 系统&问题分类(意图识别阶段)
|
||||||
|
resolveResult, err := d.resolveSystemAndIssueType(ctx, requireData, userHist)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("系统&分类结果: %s - %s,原因:%s", resolveResult.Sys.SysName, resolveResult.IssueType.Name, resolveResult.Classification.Reason)
|
||||||
|
|
||||||
|
// 4. 分类处理(后续考虑接各自的工作流/agent)
|
||||||
|
switch resolveResult.IssueType.Code {
|
||||||
|
case constants.IssueTypeKnowledgeQA:
|
||||||
|
// 知识库问答
|
||||||
|
return d.handleKnowledgeQA(ctx, requireData, userHist, resolveResult)
|
||||||
|
default: // 其他问题类型
|
||||||
|
// 系统为空,再次询问
|
||||||
|
if resolveResult.Sys.SysID == 0 {
|
||||||
|
entitys.ResText(requireData.Ch, "", "\n抱歉,我无法确定您咨询的是哪个系统。请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您准确解答或安排对应的技术支持。")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 知识库问答
|
||||||
|
func (d *DingTalkBotBiz) handleKnowledgeQA(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, userHist []model.AiBotChatHi, resolveResult *resolveSystemAndIssueTypeResult) error {
|
||||||
|
// 获取租户ID
|
||||||
|
tenantId := constants.KnowledgeTenantIdDefault
|
||||||
|
if resolveResult.Sys.KnowlegeTenantKey != "" {
|
||||||
|
tenantId = resolveResult.Sys.KnowlegeTenantKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改写 Query (Query Rewriting)
|
||||||
|
rewrittenQuery, err := d.handle.RewriteQuery(ctx, userHist, requireData.Req.Text.Content)
|
||||||
|
var queryText = requireData.Req.Text.Content
|
||||||
|
if err == nil && rewrittenQuery != "" {
|
||||||
|
queryText = rewrittenQuery
|
||||||
|
}
|
||||||
|
log.Debugf("改写前后的Query: %s -> %s", requireData.Req.Text.Content, queryText)
|
||||||
|
|
||||||
|
// 获取知识库结果
|
||||||
|
isRetrieved, responseContent, err := d.getKnowledgeAnswer(ctx, requireData.Ch, tenantId, queryText)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isRetrieved {
|
||||||
|
// 过一遍 LLM 判断是否真的命中知识库
|
||||||
|
isRetrieved, err = d.handle.IsAnswerRelevant(ctx, queryText, responseContent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未匹配&全局 -> 明确具体系统
|
||||||
|
if !isRetrieved && resolveResult.Sys.SysID == 0 {
|
||||||
|
entitys.ResText(requireData.Ch, "", "\n抱歉,知识库未命中,无法回答您的问题。\n若您的问题是某一具体系统的,请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您准确解答。")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 未匹配&指定系统 -> 拉群卡片
|
||||||
|
if !isRetrieved && resolveResult.Sys.SysID != 0 {
|
||||||
|
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉,%s知识库未命中,无法回答您的问题。即将为您创建群聊解答。", resolveResult.Sys.SysName))
|
||||||
|
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取知识库问答结果
|
||||||
|
func (d *DingTalkBotBiz) getKnowledgeAnswer(ctx context.Context, ch chan entitys.Response, tenantId string, queryText string) (bool, string, error) {
|
||||||
|
// 请求知识库工具
|
||||||
|
knowledgeBase := knowledge_base.New(d.conf.KnowledgeConfig)
|
||||||
|
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
||||||
|
TenantID: tenantId, // 后续动态接参
|
||||||
|
Query: queryText,
|
||||||
|
Mode: constants.KnowledgeModeMix,
|
||||||
|
Stream: true,
|
||||||
|
Think: false,
|
||||||
|
OnlyRAG: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, "", fmt.Errorf("请求知识库工具失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取知识库SSE数据
|
||||||
|
return d.groupConfigBiz.readKnowledgeSSE(knowledgeResp, ch, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolveSystemAndIssueTypeResult struct {
|
||||||
|
Sys model.AiSy
|
||||||
|
IssueType model.AiIssueType
|
||||||
|
Classification *do.IssueClassification
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析系统和问题类型
|
||||||
|
func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, userHist []model.AiBotChatHi) (*resolveSystemAndIssueTypeResult, error) {
|
||||||
|
// 1. 获取所有系统和问题类型用于分类
|
||||||
|
allSys, err := d.sysImpl.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string {
|
||||||
|
return sys.SysName
|
||||||
|
})
|
||||||
|
allIssueTypes, err := d.issueImpl.IssueType.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string {
|
||||||
|
return it.Name
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. LLM 分类
|
||||||
|
// 系统名称
|
||||||
|
classificationSys, err := d.handle.ClassifyIssueSystem(ctx, sysNames, requireData.Req.Text.Content, userHist)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 问题类型
|
||||||
|
classificationIssueType, err := d.handle.ClassifyIssueType(ctx, issueTypeNames, sysNames, requireData.Req.Text.Content, userHist)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 合并
|
||||||
|
classification := &do.IssueClassification{
|
||||||
|
SysName: classificationSys.SysName,
|
||||||
|
IssueTypeName: classificationIssueType.IssueTypeName,
|
||||||
|
Summary: classificationIssueType.Summary,
|
||||||
|
Reason: fmt.Sprintf("系统名称推断理由:%s\n问题类型推断理由:%s", classificationSys.Reason, classificationIssueType.Reason),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 匹配系统
|
||||||
|
var sys model.AiSy
|
||||||
|
for _, s := range allSys {
|
||||||
|
if s.SysName == classification.SysName {
|
||||||
|
sys = s
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 匹配问题类型
|
||||||
|
var issueType model.AiIssueType
|
||||||
|
for _, it := range allIssueTypes {
|
||||||
|
if it.Name == classification.IssueTypeName {
|
||||||
|
issueType = it
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resolveSystemAndIssueTypeResult{
|
||||||
|
Sys: sys,
|
||||||
|
IssueType: issueType,
|
||||||
|
Classification: classification,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWithSpecificSys 处理用户明确指定的系统
|
||||||
|
// func (d *DingTalkBotBiz) handleWithSpecificSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sysName string) error {
|
||||||
|
// // 1. 查找系统
|
||||||
|
// var sys model.AiSy
|
||||||
|
// cond := builder.NewCond().And(builder.Eq{"sys_name": sysName})
|
||||||
|
// err := d.sysImpl.GetOneBySearchToStrut(&cond, &sys)
|
||||||
|
// if err != nil {
|
||||||
|
// if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
// entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。")
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 2. 既然已经明确了系统,直接尝试拉群(这里假设问题类型为“其他”或由LLM再次分析)
|
||||||
|
// // 为简化,这里再次调用分类逻辑,但带上已确定的系统
|
||||||
|
// return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getRecentUserHistory 获取最近的用户输入历史
|
||||||
|
func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) {
|
||||||
|
var his []model.AiBotChatHi
|
||||||
|
cond := builder.NewCond().
|
||||||
|
And(builder.Eq{"his_type": conversationType}).
|
||||||
|
And(builder.Eq{"id": id}).
|
||||||
|
And(builder.Eq{"role": "user"})
|
||||||
|
|
||||||
|
_, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return his, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在已知系统&问题类型的情况下进行分类并拉群
|
||||||
|
func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, resolveResult *resolveSystemAndIssueTypeResult) error {
|
||||||
|
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n检测到您想咨询 %s-%s 问题。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
|
||||||
|
|
||||||
|
// 查找分配规则
|
||||||
|
rule, found, _ := d.issueImpl.IssueAssignRule.FindOne(
|
||||||
|
d.issueImpl.WithSysID(resolveResult.Sys.SysID),
|
||||||
|
d.issueImpl.WithIssueTypeID(resolveResult.IssueType.ID),
|
||||||
|
d.issueImpl.WithStatus(1),
|
||||||
|
)
|
||||||
|
if !found {
|
||||||
|
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉,当前系统未配置路由规则 %s-%s,请联系管理员配置。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupMember, groupMemberName []string
|
||||||
|
if rule.ID != 0 {
|
||||||
|
// 获取配置的用户
|
||||||
|
assignUsers, err := d.issueImpl.IssueAssignUser.FindAll(d.issueImpl.WithRuleID(rule.ID))
|
||||||
|
if len(assignUsers) == 0 {
|
||||||
|
log.Errorf("assign user not found for rule %d; err: %v", rule.ID, err)
|
||||||
|
return fmt.Errorf("分配用户 %d 不存在", rule.ID)
|
||||||
|
}
|
||||||
|
userIds := slice.Map(assignUsers, func(_ int, au model.AiIssueAssignUser) int32 {
|
||||||
|
return au.UserID
|
||||||
|
})
|
||||||
|
// 获取有效用户
|
||||||
|
botUsers, err := d.botUserImpl.GetByUserIds(userIds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅获取有效用户的 staff_id
|
||||||
|
for _, au := range assignUsers {
|
||||||
|
botUser, found := slice.Find(botUsers, func(_ int, bu model.AiBotUser) bool {
|
||||||
|
return bu.UserID == au.UserID
|
||||||
|
})
|
||||||
|
if found && botUser.StaffID != "" {
|
||||||
|
groupMember = append(groupMember, botUser.StaffID)
|
||||||
|
groupMemberName = append(groupMemberName, "@"+botUser.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底处理人
|
||||||
|
if len(groupMember) == 0 {
|
||||||
|
groupMember = []string{"17415698414368678"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并提问者
|
||||||
|
groupMember = append([]string{requireData.Req.SenderStaffId}, groupMember...)
|
||||||
|
groupMember = slice.Unique(groupMember)
|
||||||
|
|
||||||
|
// 先回复用户
|
||||||
|
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n已检索到处理人\n%s\n是否创建群聊?", strings.Join(groupMemberName, "、")))
|
||||||
|
|
||||||
|
// 发送确认卡片
|
||||||
|
groupName := fmt.Sprintf("[%s]-%s", resolveResult.IssueType.Name, resolveResult.Classification.Summary)
|
||||||
|
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
|
||||||
|
RobotCode: requireData.Req.RobotCode,
|
||||||
|
ConversationId: requireData.Req.ConversationId,
|
||||||
|
SenderStaffId: requireData.Req.SenderStaffId,
|
||||||
|
UserIds: groupMember,
|
||||||
|
GroupName: groupName,
|
||||||
|
Summary: resolveResult.Classification.Summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDefaultGroup 兜底发送确认卡片
|
||||||
|
func (d *DingTalkBotBiz) createDefaultGroup(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, reason string) error {
|
||||||
|
userIds := []string{requireData.Req.SenderStaffId, "17415698414368678"}
|
||||||
|
groupName := fmt.Sprintf("[未知]-%s", reason)
|
||||||
|
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
|
||||||
|
RobotCode: requireData.Req.RobotCode,
|
||||||
|
ConversationId: requireData.Req.ConversationId,
|
||||||
|
SenderStaffId: requireData.Req.SenderStaffId,
|
||||||
|
UserIds: userIds,
|
||||||
|
GroupName: groupName,
|
||||||
|
Summary: reason,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||||||
|
|
@ -437,27 +729,125 @@ func (d *DingTalkBotBiz) CreateIssueHandlingGroupAndInit(ctx context.Context, da
|
||||||
// 解析 OutTrackId 以获取 SpaceId 和 BotId
|
// 解析 OutTrackId 以获取 SpaceId 和 BotId
|
||||||
spaceId, botId := constants.ParseCardOutTrackId(data.OutTrackId)
|
spaceId, botId := constants.ParseCardOutTrackId(data.OutTrackId)
|
||||||
|
|
||||||
// 获取新群聊人员
|
// 获取操作状态
|
||||||
var userIds []string
|
status := data.CardActionData.CardPrivateData.Params["status"]
|
||||||
userIds, err = d.buildNewGroupUserIds(spaceId, botId, data.UserId)
|
if status == "confirm" {
|
||||||
if err != nil {
|
// 获取新群聊人员 (从卡片参数中统一解析)
|
||||||
return nil, err
|
targetUserIdsStr := data.CardActionData.CardPrivateData.Params["target_user_ids"].(string)
|
||||||
}
|
var userIds []string
|
||||||
|
if targetUserIdsStr != "" {
|
||||||
|
userIds = strings.Split(targetUserIdsStr, ",")
|
||||||
|
}
|
||||||
|
|
||||||
// 创建群聊及群初始化(异步响应)
|
if len(userIds) == 0 {
|
||||||
if data.CardActionData.CardPrivateData.Params["status"] == "confirm" {
|
return nil, errors.New("target_user_ids 参数不能为空")
|
||||||
go func() {
|
}
|
||||||
|
|
||||||
|
// 创建群聊及群初始化(异步响应)
|
||||||
|
util.SafeGo("CreateIssueHandlingGroupAndInit", func() {
|
||||||
err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds)
|
err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("创建群聊及群初始化失败: %v", err)
|
log.Errorf("创建群聊及群初始化失败: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建关闭创建群组卡片按钮的响应
|
// 构建关闭创建群组卡片按钮的响应
|
||||||
return d.buildCreateGroupCardResp(), nil
|
return d.buildCreateGroupCardResp(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendGroupCreationConfirmCardParams struct {
|
||||||
|
RobotCode string
|
||||||
|
ConversationId string
|
||||||
|
SenderStaffId string
|
||||||
|
UserIds []string
|
||||||
|
GroupName string
|
||||||
|
Summary string
|
||||||
|
IsGroupChat bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendGroupCreationConfirmCard 发送创建群聊确认卡片
|
||||||
|
func (d *DingTalkBotBiz) SendGroupCreationConfirmCard(ctx context.Context, params *SendGroupCreationConfirmCardParams) error {
|
||||||
|
// 获取人员姓名用于展示
|
||||||
|
var userNames []string
|
||||||
|
for _, uid := range params.UserIds {
|
||||||
|
if uid == params.SenderStaffId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
user, err := d.botUserImpl.GetByStaffId(uid)
|
||||||
|
if err == nil && user != nil {
|
||||||
|
userNames = append(userNames, "@"+user.Name)
|
||||||
|
} else {
|
||||||
|
userNames = append(userNames, "@"+uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
issueOwnerStr := strings.Join(userNames, "、")
|
||||||
|
|
||||||
|
// 获取应用配置
|
||||||
|
appKey, err := d.botConfigImpl.GetRobotAppKey(params.RobotCode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建卡片 OutTrackId
|
||||||
|
outTrackId := constants.BuildCardOutTrackId(params.SenderStaffId, params.RobotCode)
|
||||||
|
|
||||||
|
// 准备可见人员列表
|
||||||
|
var recipients []*string
|
||||||
|
if params.IsGroupChat {
|
||||||
|
// 群聊:提问者 + 负责人可见
|
||||||
|
for _, uid := range params.UserIds {
|
||||||
|
recipients = append(recipients, tea.String(uid))
|
||||||
|
}
|
||||||
|
// 确保提问者也在可见列表中
|
||||||
|
foundSender := false
|
||||||
|
for _, uid := range params.UserIds {
|
||||||
|
if uid == params.SenderStaffId {
|
||||||
|
foundSender = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundSender {
|
||||||
|
recipients = append(recipients, tea.String(params.SenderStaffId))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 单聊:仅提问者可见
|
||||||
|
recipients = append(recipients, tea.String(params.SenderStaffId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送钉钉卡片
|
||||||
|
_, err = d.dingtalkCardClient.CreateAndDeliver(
|
||||||
|
appKey,
|
||||||
|
&card_1_0.CreateAndDeliverRequest{
|
||||||
|
CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.CreateGroupApprove),
|
||||||
|
OutTrackId: tea.String(outTrackId),
|
||||||
|
CallbackType: tea.String("STREAM"),
|
||||||
|
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||||
|
CardParamMap: map[string]*string{
|
||||||
|
"title": tea.String("创建群聊提醒"),
|
||||||
|
"content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊:\n\n%s", issueOwnerStr)),
|
||||||
|
"remark": tea.String("注:如若无需,忽略即可"),
|
||||||
|
"button_left": tea.String("创建群聊"),
|
||||||
|
"button_right": tea.String("忽略"),
|
||||||
|
"action_id": tea.String("create_group"),
|
||||||
|
"button_display": tea.String("true"),
|
||||||
|
"group_scope": tea.String(params.Summary),
|
||||||
|
"target_user_ids": tea.String(strings.Join(params.UserIds, ",")),
|
||||||
|
"group_name": tea.String(params.GroupName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OpenSpaceId: tea.String("dtv1.card//IM_ROBOT." + params.SenderStaffId),
|
||||||
|
ImRobotOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImRobotOpenDeliverModel{
|
||||||
|
SpaceType: tea.String("IM_ROBOT"),
|
||||||
|
RobotCode: tea.String(params.RobotCode),
|
||||||
|
},
|
||||||
|
ImRobotOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImRobotOpenSpaceModel{
|
||||||
|
SupportForward: tea.Bool(false),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// buildNewGroupUserIds 构建新群聊人员列表
|
// buildNewGroupUserIds 构建新群聊人员列表
|
||||||
func (d *DingTalkBotBiz) buildNewGroupUserIds(spaceId, botId, groupOwner string) ([]string, error) {
|
func (d *DingTalkBotBiz) buildNewGroupUserIds(spaceId, botId, groupOwner string) ([]string, error) {
|
||||||
// 群id+机器人id确认一个群配置
|
// 群id+机器人id确认一个群配置
|
||||||
|
|
@ -510,7 +900,11 @@ func (d *DingTalkBotBiz) createIssueHandlingGroupAndInit(ctx context.Context, ca
|
||||||
appKey.AccessToken = accessToken
|
appKey.AccessToken = accessToken
|
||||||
|
|
||||||
// 创建群聊
|
// 创建群聊
|
||||||
_, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, userIds)
|
var groupName string
|
||||||
|
if s, ok := callbackParams["group_name"].(string); ok {
|
||||||
|
groupName = s
|
||||||
|
}
|
||||||
|
_, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, groupName, userIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -540,18 +934,22 @@ func (d *DingTalkBotBiz) createIssueHandlingGroupAndInit(ctx context.Context, ca
|
||||||
}
|
}
|
||||||
|
|
||||||
// createIssueHandlingGroup 创建问题处理群聊会话
|
// createIssueHandlingGroup 创建问题处理群聊会话
|
||||||
func (d *DingTalkBotBiz) createIssueHandlingGroup(ctx context.Context, accessToken string, userIds []string) (chatId, openConversationId string, err error) {
|
func (d *DingTalkBotBiz) createIssueHandlingGroup(ctx context.Context, accessToken string, groupName string, userIds []string) (chatId, openConversationId string, err error) {
|
||||||
// 是否使用模板群开关
|
// 是否使用模板群开关
|
||||||
var useTemplateGroup bool = true
|
var useTemplateGroup bool = true
|
||||||
|
|
||||||
|
if groupName == "" {
|
||||||
|
groupName = "问题处理群"
|
||||||
|
}
|
||||||
|
|
||||||
// 创建内部群会话
|
// 创建内部群会话
|
||||||
if !useTemplateGroup {
|
if !useTemplateGroup {
|
||||||
return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, "问题处理群", userIds)
|
return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, groupName, userIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据群模板ID创建群
|
// 根据群模板ID创建群
|
||||||
if useTemplateGroup {
|
if useTemplateGroup {
|
||||||
return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, "问题处理群", userIds, d.conf.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling)
|
return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, groupName, userIds, d.conf.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,12 @@ import (
|
||||||
"ai_scheduler/internal/domain/workflow/runtime"
|
"ai_scheduler/internal/domain/workflow/runtime"
|
||||||
"ai_scheduler/internal/entitys"
|
"ai_scheduler/internal/entitys"
|
||||||
"ai_scheduler/internal/gateway"
|
"ai_scheduler/internal/gateway"
|
||||||
"ai_scheduler/internal/pkg"
|
|
||||||
"ai_scheduler/internal/pkg/dingtalk"
|
"ai_scheduler/internal/pkg/dingtalk"
|
||||||
"ai_scheduler/internal/pkg/l_request"
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
"ai_scheduler/internal/pkg/mapstructure"
|
"ai_scheduler/internal/pkg/mapstructure"
|
||||||
"ai_scheduler/internal/pkg/rec_extra"
|
"ai_scheduler/internal/pkg/rec_extra"
|
||||||
"ai_scheduler/internal/pkg/util"
|
"ai_scheduler/internal/pkg/util"
|
||||||
"ai_scheduler/internal/tools"
|
"ai_scheduler/internal/tools"
|
||||||
"ai_scheduler/internal/tools/public"
|
|
||||||
"bufio"
|
"bufio"
|
||||||
errorsSpecial "errors"
|
errorsSpecial "errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -33,6 +31,7 @@ import (
|
||||||
|
|
||||||
"github.com/coze-dev/coze-go"
|
"github.com/coze-dev/coze-go"
|
||||||
"github.com/gofiber/fiber/v2/log"
|
"github.com/gofiber/fiber/v2/log"
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
"gorm.io/gorm/utils"
|
"gorm.io/gorm/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -96,6 +95,220 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewriteQuery 改写查询词,支持多轮对话
|
||||||
|
func (r *Handle) RewriteQuery(ctx context.Context, history []model.AiBotChatHi, currentQuery string) (string, error) {
|
||||||
|
if len(history) == 0 {
|
||||||
|
return currentQuery, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
histStr := strings.Builder{}
|
||||||
|
for _, h := range history {
|
||||||
|
if h.Role == "user" {
|
||||||
|
histStr.WriteString(fmt.Sprintf("%s:%s\n", h.CreateAt, h.Content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt := `你是一个搜索查询改写专家。请结合用户的历史对话上下文,将用户当前的输入改写为一个独立的、语义完整的、适合知识库检索的中文查询词。
|
||||||
|
要求:
|
||||||
|
1. 当前输入最能反映用户的意图,权重按照时间逆序依次减弱,改写后的查询词应与当前输入的语义相关。
|
||||||
|
2. 保持原意,补全指代(如“它”、“刚才那个问题”)。
|
||||||
|
3. 只返回改写后的查询词,不要有任何解释。
|
||||||
|
4. 如果当前输入已经很完整,直接返回原句。`
|
||||||
|
|
||||||
|
userPrompt := fmt.Sprintf("### 历史对话:\n%s\n### 当前输入:\n%s\n### 改写后的查询词:", histStr.String(), currentQuery)
|
||||||
|
|
||||||
|
messages := []api.Message{
|
||||||
|
{Role: "system", Content: systemPrompt},
|
||||||
|
{Role: "user", Content: userPrompt},
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Ollama.Chat(ctx, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IssueClassification struct {
|
||||||
|
SysName string `json:"sys_name"`
|
||||||
|
IssueTypeName string `json:"issue_type_name"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClassifyIssueSys 问题系统分析
|
||||||
|
func (r *Handle) ClassifyIssueSystem(ctx context.Context, systems []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) {
|
||||||
|
systemPrompt := fmt.Sprintf(`## 角色
|
||||||
|
你是一个系统类型判定专家。你的唯一任务是基于多轮对话识别用户当前讨论的系统(sys_name)。不需要输出问题类型。输出必须严格遵守 JSON 格式。
|
||||||
|
|
||||||
|
## 推理规则
|
||||||
|
|
||||||
|
1. 系统判定逻辑:
|
||||||
|
- 当前输入明确提到系统 → 直接覆盖历史系统
|
||||||
|
- 当前输入未提系统,但历史对话有 → 继承最近历史系统
|
||||||
|
- 当前输入和历史均未出现 → "全局"
|
||||||
|
- 询问公司、企业、制度层面的问题 → "全局"
|
||||||
|
|
||||||
|
2. 特殊规则:
|
||||||
|
- 如果当前输入仅包含系统名称(如“CRM”),视为系统上下文补充,仅更新 sys_name,不做其他推断
|
||||||
|
|
||||||
|
## 背景数据
|
||||||
|
可用系统列表:[%s]
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
{
|
||||||
|
"sys_name": "系统名称",
|
||||||
|
"reason": "说明系统来源:当前输入 / 历史继承 / 默认"
|
||||||
|
}
|
||||||
|
`, strings.Join(systems, ", "))
|
||||||
|
|
||||||
|
historyStr := strings.Builder{}
|
||||||
|
historyStr.WriteString("### 历史对话:\n")
|
||||||
|
for _, h := range userHist {
|
||||||
|
if h.Role == "user" {
|
||||||
|
historyStr.WriteString(fmt.Sprintf("%s:%s\n", h.CreateAt, h.Content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []api.Message{
|
||||||
|
{Role: "system", Content: systemPrompt},
|
||||||
|
{Role: "assistant", Content: historyStr.String()},
|
||||||
|
{Role: "user", Content: userInput},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r.Ollama.Chat(ctx, messages)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试清理 JSON 内容(有时模型会返回 markdown 块)
|
||||||
|
resp = strings.TrimPrefix(resp, "```json")
|
||||||
|
resp = strings.TrimSuffix(resp, "```")
|
||||||
|
resp = strings.TrimSpace(resp)
|
||||||
|
|
||||||
|
var result IssueClassification
|
||||||
|
if err := json.Unmarshal([]byte(resp), &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClassifyIssueType 问题分类分析
|
||||||
|
func (r *Handle) ClassifyIssueType(ctx context.Context, issueTypes []string, systems []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) {
|
||||||
|
systemPrompt := fmt.Sprintf(`## 角色
|
||||||
|
你是一个业务问题类型分析专家。你的任务是基于多轮对话识别用户讨论的**问题类型(issue_type_name)**,问题类型必须严格来自“背景数据-可用问题类型列表”。
|
||||||
|
|
||||||
|
你不负责系统名称判断。输出必须严格遵守 JSON 格式。
|
||||||
|
|
||||||
|
## 推理规则
|
||||||
|
|
||||||
|
1. 构建完整问题意图
|
||||||
|
- 将当前输入与历史对话合并理解为完整问题演进
|
||||||
|
- 当前输入可能是补充条件、追问、修正或只给模块名/报错片段
|
||||||
|
- 不要只看当前一句
|
||||||
|
- 忽略历史中的系统名称相关
|
||||||
|
|
||||||
|
2. 问题类型判定逻辑
|
||||||
|
- 当前输入明确匹配列表中某个类型 → 使用该类型
|
||||||
|
- 当前输入未明确,但历史已有 → 继承历史类型
|
||||||
|
- 当前输入未匹配,历史也没有 → 选择最接近的列表类型(尽量匹配意图)
|
||||||
|
- 除非是闲聊(如“你好”“在吗”),禁止返回空值
|
||||||
|
- 除非明确是需求,否则禁止返回“开发需求”类型,疑问句式一定不能返回“开发需求”类型
|
||||||
|
|
||||||
|
3. 特殊规则
|
||||||
|
- 当前输入只包含系统名/模块名/参数名 → 视为问题补充,继承历史 issue_type_name
|
||||||
|
- 输出必须严格匹配列表中的类型,不允许生成列表外的自造类型
|
||||||
|
|
||||||
|
## 背景数据
|
||||||
|
可用问题类型列表:[%s]
|
||||||
|
系统名称列表参考:[%s]
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
{
|
||||||
|
"issue_type_name": "问题类型名称",
|
||||||
|
"summary": "15字内问题标题",
|
||||||
|
"reason": "说明问题类型是基于哪句话判断,或说明继承自历史,继承自哪条历史"
|
||||||
|
}`, strings.Join(issueTypes, ", "), strings.Join(systems, ", "))
|
||||||
|
|
||||||
|
historyStr := strings.Builder{}
|
||||||
|
historyStr.WriteString("### 历史对话:\n")
|
||||||
|
for _, h := range userHist {
|
||||||
|
if h.Role == "user" {
|
||||||
|
historyStr.WriteString(fmt.Sprintf("%s:%s\n", h.CreateAt, h.Content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []api.Message{
|
||||||
|
{Role: "system", Content: systemPrompt},
|
||||||
|
{Role: "assistant", Content: historyStr.String()},
|
||||||
|
{Role: "user", Content: userInput},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r.Ollama.Chat(ctx, messages)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试清理 JSON 内容(有时模型会返回 markdown 块)
|
||||||
|
resp = strings.TrimPrefix(resp, "```json")
|
||||||
|
resp = strings.TrimSuffix(resp, "```")
|
||||||
|
resp = strings.TrimSpace(resp)
|
||||||
|
|
||||||
|
var result IssueClassification
|
||||||
|
if err := json.Unmarshal([]byte(resp), &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsAnswerRelevant struct {
|
||||||
|
Relevance string `json:"relevance"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断答案是否回答了问题
|
||||||
|
func (r *Handle) IsAnswerRelevant(ctx context.Context, question string, answer string) (bool, error) {
|
||||||
|
prompt := `## 角色
|
||||||
|
你是一个答案评估专家,你的任务是判断给定的答案是否真正回答了用户的问题。你必须严格分析语义、意图和信息覆盖情况,避免只看关键词。
|
||||||
|
|
||||||
|
## 输入
|
||||||
|
- question: %s
|
||||||
|
- answer: %s
|
||||||
|
|
||||||
|
## 判断逻辑
|
||||||
|
1. **直接回答**:答案明确提供了解决方案、步骤、结论或可执行信息 → 输出 True
|
||||||
|
2. **未回答**:答案仅泛泛提示、缺少关键步骤或信息,或者只是提供背景、登录信息等无关内容 → 输出 False
|
||||||
|
3. **部分回答**:答案提供了一部分可用信息,但未完全解决问题 → 输出 “Partial”
|
||||||
|
|
||||||
|
## 输出要求
|
||||||
|
输出严格 JSON 格式,只包含以下字段:
|
||||||
|
|
||||||
|
{
|
||||||
|
"relevance": "True / False / Partial",
|
||||||
|
"reason": "简要说明为什么答案被认为回答或未回答问题"
|
||||||
|
}`
|
||||||
|
resp, err := r.Ollama.Generation(ctx, fmt.Sprintf(prompt, question, answer))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试清理 JSON 内容(有时模型会返回 markdown 块)
|
||||||
|
resp = strings.TrimPrefix(resp, "```json")
|
||||||
|
resp = strings.TrimSuffix(resp, "```")
|
||||||
|
resp = strings.TrimSpace(resp)
|
||||||
|
|
||||||
|
var result IsAnswerRelevant
|
||||||
|
if err := json.Unmarshal([]byte(resp), &result); err != nil {
|
||||||
|
return false, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("分析结果:%s,原因:%s", result.Relevance, result.Reason)
|
||||||
|
|
||||||
|
if result.Relevance == "True" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Handle) handleOtherTask(ctx context.Context, requireData *entitys.RequireData) (err error) {
|
func (r *Handle) handleOtherTask(ctx context.Context, requireData *entitys.RequireData) (err error) {
|
||||||
entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning)
|
entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning)
|
||||||
return
|
return
|
||||||
|
|
@ -171,87 +384,87 @@ func (r *Handle) handleTask(ctx context.Context, rec *entitys.Recognize, task *m
|
||||||
}
|
}
|
||||||
|
|
||||||
// 知识库
|
// 知识库
|
||||||
func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
// func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||||
|
|
||||||
var (
|
// var (
|
||||||
configData entitys.ConfigDataTool
|
// configData entitys.ConfigDataTool
|
||||||
sessionIdKnowledge string
|
// sessionIdKnowledge string
|
||||||
query string
|
// query string
|
||||||
host string
|
// host string
|
||||||
)
|
// )
|
||||||
err = json.Unmarshal([]byte(task.Config), &configData)
|
// err = json.Unmarshal([]byte(task.Config), &configData)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
ext, err := rec_extra.GetTaskRecExt(rec)
|
// ext, err := rec_extra.GetTaskRecExt(rec)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
// 通过session 找到知识库session
|
// // 通过session 找到知识库session
|
||||||
var has bool
|
// var has bool
|
||||||
if len(ext.Session) == 0 {
|
// if len(ext.Session) == 0 {
|
||||||
return errors.SessionNotFound
|
// return errors.SessionNotFound
|
||||||
}
|
// }
|
||||||
ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session))
|
// ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session))
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return
|
// return
|
||||||
} else if !has {
|
// } else if !has {
|
||||||
return errors.SessionNotFound
|
// return errors.SessionNotFound
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 找到知识库的host
|
// // 找到知识库的host
|
||||||
{
|
// {
|
||||||
tool, exists := r.toolManager.GetTool(configData.Tool)
|
// tool, exists := r.toolManager.GetTool(configData.Tool)
|
||||||
if !exists {
|
// if !exists {
|
||||||
return fmt.Errorf("tool not found: %s", configData.Tool)
|
// return fmt.Errorf("tool not found: %s", configData.Tool)
|
||||||
}
|
// }
|
||||||
|
|
||||||
if knowledgeTool, ok := tool.(*public.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
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 知识库的session为空,请求知识库获取, 并绑定
|
// // 知识库的session为空,请求知识库获取, 并绑定
|
||||||
if ext.SessionInfo.KnowlegeSessionID == "" {
|
// if ext.SessionInfo.KnowlegeSessionID == "" {
|
||||||
// 请求知识库
|
// // 请求知识库
|
||||||
if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
|
// if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 绑定知识库session,下次可以使用
|
// // 绑定知识库session,下次可以使用
|
||||||
ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
|
// ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
|
||||||
if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
|
// if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 用户输入解析
|
// // 用户输入解析
|
||||||
var ok bool
|
// var ok bool
|
||||||
input := make(map[string]string)
|
// input := make(map[string]string)
|
||||||
if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
|
// if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
if query, ok = input["query"]; !ok {
|
// if query, ok = input["query"]; !ok {
|
||||||
return fmt.Errorf("query不能为空")
|
// return fmt.Errorf("query不能为空")
|
||||||
}
|
// }
|
||||||
|
|
||||||
ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
|
// ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
|
||||||
Session: ext.SessionInfo.KnowlegeSessionID,
|
// Session: ext.SessionInfo.KnowlegeSessionID,
|
||||||
ApiKey: ext.Sys.KnowlegeTenantKey,
|
// ApiKey: ext.Sys.KnowlegeTenantKey,
|
||||||
Query: query,
|
// Query: query,
|
||||||
}
|
// }
|
||||||
rec.Ext = pkg.JsonByteIgonErr(ext)
|
// rec.Ext = pkg.JsonByteIgonErr(ext)
|
||||||
// 执行工具
|
// // 执行工具
|
||||||
err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
|
// err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 知识库V2 - lightRAG自建
|
// 知识库V2 - lightRAG自建
|
||||||
func (r *Handle) handleKnowleV2(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
func (r *Handle) handleKnowleV2(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||||
"github.com/alibabacloud-go/tea/tea"
|
"github.com/alibabacloud-go/tea/tea"
|
||||||
"github.com/coze-dev/coze-go"
|
"github.com/coze-dev/coze-go"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/gofiber/fiber/v2/log"
|
"github.com/gofiber/fiber/v2/log"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
@ -49,9 +50,9 @@ type GroupConfigBiz struct {
|
||||||
toolManager *tools.Manager
|
toolManager *tools.Manager
|
||||||
conf *config.Config
|
conf *config.Config
|
||||||
rdb *utils.Rdb
|
rdb *utils.Rdb
|
||||||
|
dingtalkCardClient *dingtalk.CardClient
|
||||||
macro *do.Macro
|
macro *do.Macro
|
||||||
handle *do.Handle
|
handle *do.Handle
|
||||||
dingtalkCardClient *dingtalk.CardClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDingTalkBotBiz
|
// NewDingTalkBotBiz
|
||||||
|
|
@ -64,10 +65,10 @@ func NewGroupConfigBiz(
|
||||||
conf *config.Config,
|
conf *config.Config,
|
||||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||||
rdb *utils.Rdb,
|
rdb *utils.Rdb,
|
||||||
macro *do.Macro,
|
|
||||||
toolManager *tools.Manager,
|
toolManager *tools.Manager,
|
||||||
handle *do.Handle,
|
|
||||||
dingtalkCardClient *dingtalk.CardClient,
|
dingtalkCardClient *dingtalk.CardClient,
|
||||||
|
macro *do.Macro,
|
||||||
|
handle *do.Handle,
|
||||||
) *GroupConfigBiz {
|
) *GroupConfigBiz {
|
||||||
return &GroupConfigBiz{
|
return &GroupConfigBiz{
|
||||||
botTools: tools.BootTools,
|
botTools: tools.BootTools,
|
||||||
|
|
@ -78,10 +79,10 @@ func NewGroupConfigBiz(
|
||||||
conf: conf,
|
conf: conf,
|
||||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||||
rdb: rdb,
|
rdb: rdb,
|
||||||
macro: macro,
|
|
||||||
toolManager: toolManager,
|
toolManager: toolManager,
|
||||||
handle: handle,
|
|
||||||
dingtalkCardClient: dingtalkCardClient,
|
dingtalkCardClient: dingtalkCardClient,
|
||||||
|
macro: macro,
|
||||||
|
handle: handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,7 +284,8 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize
|
||||||
case constants.TaskTypeCozeWorkflow:
|
case constants.TaskTypeCozeWorkflow:
|
||||||
return g.handleCozeWorkflow(ctx, rec, pointTask)
|
return g.handleCozeWorkflow(ctx, rec, pointTask)
|
||||||
case constants.TaskTypeKnowle: // 知识库lightRAG版本
|
case constants.TaskTypeKnowle: // 知识库lightRAG版本
|
||||||
return g.handleKnowledge(ctx, rec, groupConfig, callback)
|
_, err = g.handleKnowledge(ctx, rec, groupConfig, callback)
|
||||||
|
return err
|
||||||
default:
|
default:
|
||||||
return g.otherTask(ctx, rec)
|
return g.otherTask(ctx, rec)
|
||||||
}
|
}
|
||||||
|
|
@ -489,7 +491,7 @@ func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, tota
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleKnowledge 处理知识库V2版本
|
// handleKnowledge 处理知识库V2版本
|
||||||
func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
|
func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (isRetrieved bool, err error) {
|
||||||
// 请求知识库工具
|
// 请求知识库工具
|
||||||
knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig)
|
knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig)
|
||||||
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
||||||
|
|
@ -501,35 +503,35 @@ func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recog
|
||||||
OnlyRAG: true,
|
OnlyRAG: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("请求知识库工具失败,err: %v", err)
|
return false, fmt.Errorf("请求知识库工具失败,err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取知识库SSE数据
|
// 读取知识库SSE数据
|
||||||
isRetrieved, err := g.readKnowledgeSSE(knowledgeResp, rec.Ch, true)
|
isRetrieved, _, err = g.readKnowledgeSSE(knowledgeResp, rec.Ch, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未检索到匹配信息,询问是否拉群
|
// 未检索到匹配信息,群聊时询问是否拉群
|
||||||
if !isRetrieved {
|
if !isRetrieved && callback.ConversationType == constants.ConversationTypeGroup {
|
||||||
g.shouldCreateIssueHandlingGroup(ctx, rec, groupConfig, callback)
|
g.shouldCreateIssueHandlingGroup(ctx, rec, groupConfig, callback)
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取知识库 SSE 数据
|
// 读取知识库 SSE 数据
|
||||||
func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (isRetrieved bool, err error) {
|
func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (isRetrieved bool, allContent string, err error) {
|
||||||
scanner := bufio.NewScanner(resp)
|
scanner := bufio.NewScanner(resp)
|
||||||
var buffer strings.Builder
|
var buffer strings.Builder
|
||||||
|
var allContentBuilder strings.Builder
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
|
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("解析SSE数据失败: %w", err)
|
return false, "", fmt.Errorf("解析SSE数据失败: %w", err)
|
||||||
}
|
}
|
||||||
if done {
|
if done {
|
||||||
break
|
break
|
||||||
|
|
@ -540,9 +542,9 @@ func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entit
|
||||||
|
|
||||||
// 知识库未命中 输出提示后中断
|
// 知识库未命中 输出提示后中断
|
||||||
if delta.XRagStatus == constants.KnowledgeRagStatusMiss {
|
if delta.XRagStatus == constants.KnowledgeRagStatusMiss {
|
||||||
var missContent string = "知识库未检测到匹配信息,即将为您创建群聊解决问题。"
|
var missContent string = "知识库未检测到匹配信息。"
|
||||||
entitys.ResStream(channel, "", missContent)
|
entitys.ResStream(channel, "", missContent)
|
||||||
return false, nil
|
return false, missContent, nil
|
||||||
}
|
}
|
||||||
// 推理内容
|
// 推理内容
|
||||||
if delta.ReasoningContent != "" {
|
if delta.ReasoningContent != "" {
|
||||||
|
|
@ -553,6 +555,7 @@ func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entit
|
||||||
if delta.Content != "" && useParagraphMode {
|
if delta.Content != "" && useParagraphMode {
|
||||||
// 存入缓冲区
|
// 存入缓冲区
|
||||||
buffer.WriteString(delta.Content)
|
buffer.WriteString(delta.Content)
|
||||||
|
allContentBuilder.WriteString(delta.Content)
|
||||||
content := buffer.String()
|
content := buffer.String()
|
||||||
|
|
||||||
// 检查是否有换行符,按段落输出
|
// 检查是否有换行符,按段落输出
|
||||||
|
|
@ -570,10 +573,11 @@ func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entit
|
||||||
// 输出内容 - 逐字
|
// 输出内容 - 逐字
|
||||||
if delta.Content != "" && !useParagraphMode {
|
if delta.Content != "" && !useParagraphMode {
|
||||||
entitys.ResStream(channel, "", delta.Content)
|
entitys.ResStream(channel, "", delta.Content)
|
||||||
|
allContentBuilder.WriteString(delta.Content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return true, fmt.Errorf("读取SSE流中断: %w", err)
|
return true, "", fmt.Errorf("读取SSE流中断: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送缓冲区剩余内容(仅在段落模式下需要)
|
// 发送缓冲区剩余内容(仅在段落模式下需要)
|
||||||
|
|
@ -581,7 +585,7 @@ func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entit
|
||||||
entitys.ResStream(channel, "", buffer.String())
|
entitys.ResStream(channel, "", buffer.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, allContentBuilder.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 询问是否创建群聊处理问题
|
// 询问是否创建群聊处理问题
|
||||||
|
|
@ -597,12 +601,16 @@ func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec
|
||||||
}
|
}
|
||||||
// 合并所有name、Id
|
// 合并所有name、Id
|
||||||
userNames := make([]string, 0, len(issueOwner))
|
userNames := make([]string, 0, len(issueOwner))
|
||||||
userIds := make([]*string, 0, len(issueOwner))
|
userIds := make([]string, 0, len(issueOwner))
|
||||||
for _, owner := range issueOwner {
|
for _, owner := range issueOwner {
|
||||||
userNames = append(userNames, "@"+owner.Name)
|
userNames = append(userNames, "@"+owner.Name)
|
||||||
userIds = append(userIds, tea.String(owner.UserId))
|
userIds = append(userIds, owner.UserId)
|
||||||
}
|
}
|
||||||
issueOwnerStr := strings.Join(userNames, "、")
|
issueOwnerStr := strings.Join(userNames, "、")
|
||||||
|
targetUserIds := append(userIds, callback.SenderStaffId)
|
||||||
|
recipientsUsers := slice.Map(targetUserIds, func(_ int, item string) *string {
|
||||||
|
return tea.String(item)
|
||||||
|
})
|
||||||
|
|
||||||
// 获取应用配置
|
// 获取应用配置
|
||||||
appKey, err := g.botConfigImpl.GetRobotAppKey(callback.RobotCode)
|
appKey, err := g.botConfigImpl.GetRobotAppKey(callback.RobotCode)
|
||||||
|
|
@ -612,7 +620,6 @@ func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec
|
||||||
|
|
||||||
// 构建卡片 OutTrackId
|
// 构建卡片 OutTrackId
|
||||||
outTrackId := constants.BuildCardOutTrackId(callback.ConversationId, callback.RobotCode)
|
outTrackId := constants.BuildCardOutTrackId(callback.ConversationId, callback.RobotCode)
|
||||||
|
|
||||||
// 发送钉钉卡片
|
// 发送钉钉卡片
|
||||||
_, err = g.dingtalkCardClient.CreateAndDeliver(
|
_, err = g.dingtalkCardClient.CreateAndDeliver(
|
||||||
appKey,
|
appKey,
|
||||||
|
|
@ -622,15 +629,15 @@ func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec
|
||||||
CallbackType: tea.String("STREAM"),
|
CallbackType: tea.String("STREAM"),
|
||||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||||
CardParamMap: map[string]*string{
|
CardParamMap: map[string]*string{
|
||||||
"title": tea.String("创建群聊提醒"),
|
"title": tea.String("创建群聊提醒"),
|
||||||
"content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊:\n\n%s", issueOwnerStr)),
|
"content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊:\n\n%s", issueOwnerStr)),
|
||||||
"remark": tea.String("注:如若无需,忽略即可"),
|
"remark": tea.String("注:如若无需,忽略即可"),
|
||||||
"button_left": tea.String("创建群聊"),
|
"button_left": tea.String("创建群聊"),
|
||||||
"button_right": tea.String("忽略"),
|
"button_right": tea.String("忽略"),
|
||||||
"action_id": tea.String("create_group"),
|
"action_id": tea.String("create_group"),
|
||||||
"button_display": tea.String("true"),
|
"button_display": tea.String("true"),
|
||||||
"group_scope": tea.String(strings.TrimSpace(rec.UserContent.Text)),
|
"group_scope": tea.String(strings.TrimSpace(rec.UserContent.Text)),
|
||||||
// "_CARD_DEBUG_TOOL_ENTRY": tea.String(g.conf.Dingtalk.Card.DebugToolEntryShow), // 调试字段
|
"target_user_ids": tea.String(strings.Join(targetUserIds, ",")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||||
|
|
@ -639,7 +646,7 @@ func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec
|
||||||
OpenSpaceId: tea.String("dtv1.card//im_group." + callback.ConversationId),
|
OpenSpaceId: tea.String("dtv1.card//im_group." + callback.ConversationId),
|
||||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||||
RobotCode: tea.String(callback.RobotCode),
|
RobotCode: tea.String(callback.RobotCode),
|
||||||
Recipients: append(userIds, tea.String(callback.SenderStaffId)),
|
Recipients: recipientsUsers,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,27 @@ func (r *OllamaService) IntentRecognize(ctx context.Context, req *entitys.ToolSe
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *OllamaService) Chat(ctx context.Context, messages []api.Message) (string, error) {
|
||||||
|
res, err := r.client.Chat(ctx, r.config.Ollama.Model, messages)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return res.Message.Content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OllamaService) Generation(ctx context.Context, prompt string) (string, error) {
|
||||||
|
res, err := r.client.Generation(ctx, &api.GenerateRequest{
|
||||||
|
Model: r.config.Ollama.GenerateModel,
|
||||||
|
Stream: new(bool),
|
||||||
|
Prompt: prompt,
|
||||||
|
Think: &api.ThinkValue{Value: false},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return res.Response, nil
|
||||||
|
}
|
||||||
|
|
||||||
//func (r *OllamaService) RecognizeWithImg(ctx context.Context, imgByte []api.ImageData, ch chan entitys.Response) (desc api.GenerateResponse, err error) {
|
//func (r *OllamaService) RecognizeWithImg(ctx context.Context, imgByte []api.ImageData, ch chan entitys.Response) (desc api.GenerateResponse, err error) {
|
||||||
// if imgByte == nil {
|
// if imgByte == nil {
|
||||||
// return
|
// return
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,11 @@ const (
|
||||||
PermissionTypeNone = 1
|
PermissionTypeNone = 1
|
||||||
PermissionTypeDept = 2
|
PermissionTypeDept = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IssueType 问题类型
|
||||||
|
const (
|
||||||
|
IssueTypeKnowledgeQA = "knowledge_qa" // 知识问答
|
||||||
|
IssueTypeUI = "ui" // UI需求
|
||||||
|
IssueTypeBug = "bug" // Bug
|
||||||
|
IssueTypeDemand = "demand" // 开发需求
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,7 @@ const IssueHandlingExtractContentPrompt string = `你是一个【问题与答案
|
||||||
|
|
||||||
当用户输入为【多条群聊聊天记录】时:
|
当用户输入为【多条群聊聊天记录】时:
|
||||||
- 结合问题主题,判断聊天记录中正在讨论或试图解决的问题
|
- 结合问题主题,判断聊天记录中正在讨论或试图解决的问题
|
||||||
- 一个群聊中可能包含多个相互独立的问题,但它们都围绕着一个主题,一般为用户提出的第一个问题,尽可能总结为一个问题
|
- 一个群聊中可能包含多个相互独立的问题,但它们都围绕着一个主题,一般为用户提出的第一个问题。尽可能总结为一个问题、一个答案
|
||||||
- 若确实问题很独立,需要分别识别,对每个问题,整理出清晰、可复用的“问题描述”和“对应答案”
|
|
||||||
|
|
||||||
生成答案时的原则:
|
生成答案时的原则:
|
||||||
- 答案必须来源于聊天内容中已经给出的信息或共识
|
- 答案必须来源于聊天内容中已经给出的信息或共识
|
||||||
|
|
@ -142,23 +141,18 @@ const IssueHandlingExtractContentPrompt string = `你是一个【问题与答案
|
||||||
|
|
||||||
JSON 结构约定:
|
JSON 结构约定:
|
||||||
{
|
{
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"question": "清晰、独立、可复用的问题描述",
|
"question": "清晰、独立、可复用的问题描述",
|
||||||
"answer": "基于聊天内容整理出的答案;如无结论则为“暂无明确结论”",
|
"answer": "基于聊天内容整理出的答案;如无结论则为“暂无明确结论”",
|
||||||
"confidence": "high | medium | low"
|
"confidence": "high | medium | low"
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
字段说明:
|
字段说明:
|
||||||
- items:问题与答案列表;若未识别到有效问题,则返回空数组 []
|
|
||||||
- question:抽象后的标准问题表述,不包含具体聊天语句
|
- question:抽象后的标准问题表述,不包含具体聊天语句
|
||||||
- answer:整理后的答案,不得引入聊天之外的信息
|
- answer:整理后的答案,不得引入聊天之外的信息
|
||||||
- confidence:根据聊天中信息的一致性和明确程度给出判断
|
- confidence:根据聊天中信息的一致性和明确程度给出判断
|
||||||
|
|
||||||
如果无法从输入中识别出任何有效问题,返回:
|
如果无法从输入中识别出任何有效问题,返回:
|
||||||
{ "items": [] }
|
{ "confidence": "low" }
|
||||||
|
|
||||||
用户输入:
|
用户输入:
|
||||||
%s
|
%s
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package impl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/data/model"
|
||||||
|
"ai_scheduler/utils"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueImpl struct {
|
||||||
|
IssueType BaseRepository[model.AiIssueType]
|
||||||
|
IssueAssignRule BaseRepository[model.AiIssueAssignRule]
|
||||||
|
IssueAssignUser BaseRepository[model.AiIssueAssignUser]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssueImpl(db *utils.Db) *IssueImpl {
|
||||||
|
return &IssueImpl{
|
||||||
|
IssueType: NewBaseModel[model.AiIssueType](db.Client),
|
||||||
|
IssueAssignRule: NewBaseModel[model.AiIssueAssignRule](db.Client),
|
||||||
|
IssueAssignUser: NewBaseModel[model.AiIssueAssignUser](db.Client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithName 名称查询
|
||||||
|
func (a *IssueImpl) WithName(name string) CondFunc {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("name = ?", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCode 编码查询
|
||||||
|
func (a *IssueImpl) WithCode(code string) CondFunc {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("code = ?", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSysID 系统ID查询
|
||||||
|
func (a *IssueImpl) WithSysID(sysID any) CondFunc {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("sys_id = ?", sysID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIssueTypeID 问题类型ID查询
|
||||||
|
func (a *IssueImpl) WithIssueTypeID(issueTypeID any) CondFunc {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("issue_type_id = ?", issueTypeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRuleID 规则ID查询
|
||||||
|
func (a *IssueImpl) WithRuleID(ruleID any) CondFunc {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("rule_id = ?", ruleID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStatus 状态查询
|
||||||
|
func (a *IssueImpl) WithStatus(status any) CondFunc {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,8 @@ BaseModel 是一个泛型结构体,用于封装GORM数据库通用操作。
|
||||||
// 定义受支持的PO类型集合(可根据需要扩展), 只有包含表结构才能使用BaseModel,避免使用出现问题
|
// 定义受支持的PO类型集合(可根据需要扩展), 只有包含表结构才能使用BaseModel,避免使用出现问题
|
||||||
type PO interface {
|
type PO interface {
|
||||||
model.AiChatHi |
|
model.AiChatHi |
|
||||||
model.AiSy | model.AiSession | model.AiTask | model.AiBotConfig
|
model.AiSy | model.AiSession | model.AiTask | model.AiBotConfig |
|
||||||
|
model.AiIssueType | model.AiIssueAssignRule | model.AiIssueAssignUser
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseModel[P PO] struct {
|
type BaseModel[P PO] struct {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"ai_scheduler/tmpl/dataTemp"
|
"ai_scheduler/tmpl/dataTemp"
|
||||||
"ai_scheduler/utils"
|
"ai_scheduler/utils"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BotUserImpl struct {
|
type BotUserImpl struct {
|
||||||
|
|
@ -25,3 +27,14 @@ func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) {
|
||||||
}
|
}
|
||||||
return &data, err
|
return &data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (k BotUserImpl) GetByUserIds(userIds []int32) ([]model.AiBotUser, error) {
|
||||||
|
var data []model.AiBotUser
|
||||||
|
cond := builder.NewCond()
|
||||||
|
for _, userId := range userIds {
|
||||||
|
cond = cond.Or(builder.Eq{"user_id": userId})
|
||||||
|
}
|
||||||
|
_, err := k.GetListToStruct(&cond, nil, &data, "user_id")
|
||||||
|
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ var ProviderImpl = wire.NewSet(
|
||||||
NewBotGroupConfigImpl,
|
NewBotGroupConfigImpl,
|
||||||
NewBotGroupQywxImpl,
|
NewBotGroupQywxImpl,
|
||||||
NewReportDailyCacheImpl,
|
NewReportDailyCacheImpl,
|
||||||
|
NewIssueImpl,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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 TableNameAiIssueAssignRule = "ai_issue_assign_rule"
|
||||||
|
|
||||||
|
// AiIssueAssignRule AI问题分配规则表,指定系统+问题类型对应分配规则
|
||||||
|
type AiIssueAssignRule struct {
|
||||||
|
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||||
|
SysID int32 `gorm:"column:sys_id;not null;comment:系统ID,关联 ai_sys.id" json:"sys_id"` // 系统ID,关联 ai_sys.id
|
||||||
|
IssueTypeID int32 `gorm:"column:issue_type_id;not null;comment:问题类型ID,关联 ai_issue_type.id" json:"issue_type_id"` // 问题类型ID,关联 ai_issue_type.id
|
||||||
|
Status int32 `gorm:"column:status;not null;default:1;comment:规则状态:1=启用,0=停用" json:"status"` // 规则状态:1=启用,0=停用
|
||||||
|
Description string `gorm:"column:description;comment:规则描述,用于说明规则用途" json:"description"` // 规则描述,用于说明规则用途
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName AiIssueAssignRule's table name
|
||||||
|
func (*AiIssueAssignRule) TableName() string {
|
||||||
|
return TableNameAiIssueAssignRule
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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 TableNameAiIssueAssignUser = "ai_issue_assign_user"
|
||||||
|
|
||||||
|
// AiIssueAssignUser 规则对应的用户表,命中规则时需要通知的钉钉用户
|
||||||
|
type AiIssueAssignUser struct {
|
||||||
|
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||||
|
RuleID int32 `gorm:"column:rule_id;not null;comment:规则ID,关联 ai_issue_assign_rule.id" json:"rule_id"` // 规则ID,关联 ai_issue_assign_rule.id
|
||||||
|
UserID int32 `gorm:"column:user_id;not null;comment:钉钉用户ID,关联 ai_bot_user.id" json:"user_id"` // 钉钉用户ID,关联 ai_bot_user.id
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName AiIssueAssignUser's table name
|
||||||
|
func (*AiIssueAssignUser) TableName() string {
|
||||||
|
return TableNameAiIssueAssignUser
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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 TableNameAiIssueType = "ai_issue_type"
|
||||||
|
|
||||||
|
// AiIssueType AI问题类型表
|
||||||
|
type AiIssueType struct {
|
||||||
|
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
||||||
|
Code string `gorm:"column:code;not null;comment:问题类型编码,例如: ui, bug, demand" json:"code"` // 问题类型编码,例如: ui, bug, demand
|
||||||
|
Name string `gorm:"column:name;not null;comment:问题类型名称,例如: UI问题, Bug, 需求" json:"name"` // 问题类型名称,例如: UI问题, Bug, 需求
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName AiIssueType's table name
|
||||||
|
func (*AiIssueType) TableName() string {
|
||||||
|
return TableNameAiIssueType
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package knowledge_base
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"ai_scheduler/internal/pkg/l_request"
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -110,3 +111,53 @@ func (c *Client) IngestText(req *IngestTextRequest) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IngestBatchQA 向知识库中注入问答对
|
||||||
|
func (c *Client) IngestBatchQA(req *IngestBacthQARequest) error {
|
||||||
|
if req == nil {
|
||||||
|
return fmt.Errorf("req is nil")
|
||||||
|
}
|
||||||
|
if req.TenantID == "" {
|
||||||
|
return fmt.Errorf("tenantID is empty")
|
||||||
|
}
|
||||||
|
for _, item := range req.QAList {
|
||||||
|
if item.Question == "" {
|
||||||
|
return fmt.Errorf("question is empty")
|
||||||
|
}
|
||||||
|
if item.Answer == "" {
|
||||||
|
return fmt.Errorf("answer is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data := []map[string]string{}
|
||||||
|
for _, item := range req.QAList {
|
||||||
|
data = append(data, map[string]string{
|
||||||
|
"question": item.Question,
|
||||||
|
"answer": item.Answer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
jsonByte, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
|
||||||
|
|
||||||
|
rsp, err := (&l_request.Request{
|
||||||
|
Method: "POST",
|
||||||
|
Url: baseURL + "/ingest/batch_qa",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Tenant-ID": req.TenantID,
|
||||||
|
},
|
||||||
|
JsonByte: jsonByte,
|
||||||
|
}).Send()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("knowledge base returned status %d: %s", rsp.StatusCode, rsp.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,13 @@ type IngestTextRequest struct {
|
||||||
TenantID string // 租户 ID
|
TenantID string // 租户 ID
|
||||||
Text string // 要注入的文本内容
|
Text string // 要注入的文本内容
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IngestBacthQARequest struct {
|
||||||
|
TenantID string // 租户 ID
|
||||||
|
QAList []*QA // 问答对列表
|
||||||
|
}
|
||||||
|
|
||||||
|
type QA struct {
|
||||||
|
Question string // 问题
|
||||||
|
Answer string // 答案
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ type DingTalkBot struct {
|
||||||
ClientSecret string `json:"client_secret"`
|
ClientSecret string `json:"client_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Task struct {
|
|
||||||
Index string `json:"bot_index"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DingTalkBot) GetAppKey() dingtalk.AppKey {
|
func (d *DingTalkBot) GetAppKey() dingtalk.AppKey {
|
||||||
return dingtalk.AppKey{
|
return dingtalk.AppKey{
|
||||||
AppKey: d.ClientId,
|
AppKey: d.ClientId,
|
||||||
AppSecret: d.ClientSecret,
|
AppSecret: d.ClientSecret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
Index string `json:"bot_index"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/services"
|
"ai_scheduler/internal/services"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2/log"
|
"github.com/gofiber/fiber/v2/log"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
|
|
@ -20,6 +21,7 @@ type cronJob struct {
|
||||||
EntryId int32
|
EntryId int32
|
||||||
Func func(context.Context) error
|
Func func(context.Context) error
|
||||||
Name string
|
Name string
|
||||||
|
Key string
|
||||||
Schedule string
|
Schedule string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,11 +44,13 @@ func (c *CronServer) InitJobs(ctx context.Context) {
|
||||||
{
|
{
|
||||||
Func: c.cronService.CronReportSendDingTalk,
|
Func: c.cronService.CronReportSendDingTalk,
|
||||||
Name: "直连天下报表推送(钉钉)",
|
Name: "直连天下报表推送(钉钉)",
|
||||||
|
Key: "ding_report_dingtalk",
|
||||||
Schedule: "20 12,18,23 * * *",
|
Schedule: "20 12,18,23 * * *",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Func: c.cronService.CronReportSendQywx,
|
Func: c.cronService.CronReportSendQywx,
|
||||||
Name: "直连天下报表推送(微信)",
|
Name: "直连天下报表推送(微信)",
|
||||||
|
Key: "ding_report_qywx",
|
||||||
Schedule: "20 12,18,23 * * *",
|
Schedule: "20 12,18,23 * * *",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -96,3 +100,39 @@ func (c *CronServer) Stop() {
|
||||||
c.log.Info("Cron调度器已停止")
|
c.log.Info("Cron调度器已停止")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CronServer) RunOnce(ctx context.Context, key string) error {
|
||||||
|
|
||||||
|
if c.jobs == nil {
|
||||||
|
c.InitJobs(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取key对应的任务
|
||||||
|
var job *cronJob
|
||||||
|
for _, j := range c.jobs {
|
||||||
|
if j.Key == key {
|
||||||
|
job = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if job == nil {
|
||||||
|
return fmt.Errorf("unknown job key: %s\n", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Printf("任务[once]:%s执行时发生panic: %v\n", job.Name, r)
|
||||||
|
}
|
||||||
|
fmt.Printf("任务[once]:%s执行结束\n", job.Name)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Printf("任务[once]:%s开始执行\n", job.Name)
|
||||||
|
|
||||||
|
err := job.Func(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("任务[once]:%s执行失败: %s\n", job.Name, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("任务[once]:%s执行成功\n", job.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,8 @@ func run() {
|
||||||
qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
|
qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
|
||||||
groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb, manager, cardClient)
|
groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb, manager, cardClient)
|
||||||
macro := do.NewMacro(botGroupImpl, impl.NewReportDailyCacheImpl(db))
|
macro := do.NewMacro(botGroupImpl, impl.NewReportDailyCacheImpl(db))
|
||||||
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, botGroupConfigImpl, user, botChatHisImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz, macro, oauth2Client, oldClient, cardClient)
|
issueImpl := impl.NewIssueImpl(db)
|
||||||
|
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, botGroupConfigImpl, user, botChatHisImpl, botUserImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz, macro, oauth2Client, oldClient, cardClient, rdb, issueImpl, sysImpl)
|
||||||
// 初始化钉钉机器人服务
|
// 初始化钉钉机器人服务
|
||||||
cronService = NewCronService(configConfig, dingTalkBotBiz, qywxAppBiz, groupConfigBiz)
|
cronService = NewCronService(configConfig, dingTalkBotBiz, qywxAppBiz, groupConfigBiz)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue