Compare commits
30 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c1971e71c1 | |
|
|
ece04df2cb | |
|
|
7e71ad52a4 | |
|
|
847eb8b5db | |
|
|
3b6471a196 | |
|
|
b3b09f184b | |
|
|
2cdeb4a9ae | |
|
|
22a9de2841 | |
|
|
6f33665e16 | |
|
|
71a5118180 | |
|
|
5b11cb728f | |
|
|
2fd3d2ae60 | |
|
|
39d2fc1e62 | |
|
|
36db8e7a86 | |
|
|
fa08cad74a | |
|
|
32cd8691b7 | |
|
|
5560e879d0 | |
|
|
c74fe839d8 | |
|
|
634bca5c60 | |
|
|
855156374e | |
|
|
498d165915 | |
|
|
451f68056c | |
|
|
5d58cbc0f6 | |
|
|
7f5947c443 | |
|
|
9468037d66 | |
|
|
534da15898 | |
|
|
a0b76f1581 | |
|
|
17d7b01fdf | |
|
|
44864cc7f0 | |
|
|
e8061799b8 |
|
|
@ -13,6 +13,7 @@ func main() {
|
|||
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
|
||||
onBot := flag.String("bot", "", "bot start")
|
||||
cron := flag.String("cron", "", "close")
|
||||
runJob := flag.String("runJob", "", "run single job and exit")
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
bc, err := config.LoadConfig(*configPath)
|
||||
|
|
@ -33,6 +34,11 @@ func main() {
|
|||
if *cron == "start" {
|
||||
app.Cron.Run(ctx)
|
||||
}
|
||||
// 运行指定任务并退出
|
||||
if *runJob != "" {
|
||||
app.Cron.RunOnce(ctx, *runJob)
|
||||
return
|
||||
}
|
||||
|
||||
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ server:
|
|||
host: "0.0.0.0"
|
||||
|
||||
ollama:
|
||||
base_url: "http://192.168.6.109:11434"
|
||||
model: "qwen3-coder:480b-cloud"
|
||||
generate_model: "qwen3-coder:480b-cloud"
|
||||
mapping_model: "deepseek-v3.2:cloud"
|
||||
base_url: "http://192.168.6.115:11434"
|
||||
model: "qwen3:8b"
|
||||
generate_model: "qwen3:8b"
|
||||
mapping_model: "qwen3:8b"
|
||||
# model: "qwen3-coder:480b-cloud"
|
||||
# generate_model: "qwen3-coder:480b-cloud"
|
||||
# mapping_model: "deepseek-v3.2:cloud"
|
||||
vl_model: "qwen2.5vl:7b"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
|
@ -88,7 +91,22 @@ tools:
|
|||
zltxOrderAfterSaleResellerBatch:
|
||||
enabled: true
|
||||
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_tools:
|
||||
# == 货易通 hyt ==
|
||||
|
|
@ -147,6 +165,26 @@ dingtalk:
|
|||
# 机器人群组
|
||||
bot_group_id:
|
||||
bbxt: 23
|
||||
# 互动卡片
|
||||
card:
|
||||
# 卡片回调路由key - https://gateway.dev.cdlsxd.cn/zltx_api/aitest/api/v1//callback/dingtalk-card
|
||||
callback_route_key: "gateway.dev.cdlsxd.cn-dingtalk-card"
|
||||
# 卡片调试工具 [show:展示 hide:隐藏]
|
||||
debug_tool_entry_show: "hide"
|
||||
# 卡片模板
|
||||
template:
|
||||
# 基础消息卡片(title + content)
|
||||
base_msg: "291468f8-a048-4132-a37e-a14365e855e9.schema"
|
||||
# 内容收集卡片(title + textarea + button)
|
||||
content_collect: "3a447814-6a3e-4a02-b48a-92c57b349d77.schema"
|
||||
# 创建群聊申请(title + content + button)
|
||||
create_group_approve: "faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"
|
||||
# 场景群
|
||||
scene_group:
|
||||
# 问题处理群模板ID
|
||||
group_template_id_issue_handling: "aa3aa4fe-e709-4491-b24b-c3d5b27e86d0"
|
||||
# 问题处理群模板机器人ID
|
||||
group_template_robot_id_issue_handling: "VqgJYpB91j3RnB217690607273471011"
|
||||
|
||||
qywx:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
|
|
@ -169,6 +207,16 @@ default_prompt:
|
|||
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
|
||||
'
|
||||
user_prompt: '识别图片内容'
|
||||
|
||||
# 权限配置
|
||||
permissionConfig:
|
||||
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
|
||||
|
||||
# 知识库配置
|
||||
knowledge_config:
|
||||
base_url: "http://192.168.6.115:9600"
|
||||
tenant_id: "default"
|
||||
mode: "naive"
|
||||
stream: true
|
||||
think: false
|
||||
only_rag: true
|
||||
|
|
|
|||
|
|
@ -154,6 +154,26 @@ dingtalk:
|
|||
# 机器人群组
|
||||
bot_group_id:
|
||||
bbxt: 23
|
||||
# 互动卡片
|
||||
card:
|
||||
# 卡片回调路由key
|
||||
callback_route_key: "gateway.dev.cdlsxd.cn-dingtalk-card"
|
||||
# 卡片调试工具 [show:展示 hide:隐藏]
|
||||
debug_tool_entry_show: "hide"
|
||||
# 卡片模板
|
||||
template:
|
||||
# 基础消息卡片(title + content)
|
||||
base_msg: "291468f8-a048-4132-a37e-a14365e855e9.schema"
|
||||
# 内容收集卡片(title + textarea + button)
|
||||
content_collect: "3a447814-6a3e-4a02-b48a-92c57b349d77.schema"
|
||||
# 创建群聊申请(title + content + button)
|
||||
create_group_approve: "faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"
|
||||
# 场景群
|
||||
scene_group:
|
||||
# 问题处理群模板ID
|
||||
group_template_id_issue_handling: "aa3aa4fe-e709-4491-b24b-c3d5b27e86d0"
|
||||
# 问题处理群模板机器人ID
|
||||
group_template_robot_id_issue_handling: "VqgJYpB91j3RnB217690607273471011"
|
||||
|
||||
qywx:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
|
|
@ -179,6 +199,15 @@ default_prompt:
|
|||
permissionConfig:
|
||||
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
|
||||
|
||||
# 知识库配置
|
||||
knowledge_config:
|
||||
base_url: "http://192.168.6.115:9600"
|
||||
tenant_id: "default"
|
||||
mode: "naive"
|
||||
stream: true
|
||||
think: false
|
||||
only_rag: true
|
||||
|
||||
# llm 服务配置
|
||||
llm:
|
||||
providers:
|
||||
|
|
|
|||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/eino-contrib/jsonschema v1.0.3 // 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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
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/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,290 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
type CallbackBiz struct {
|
||||
cfg *config.Config
|
||||
ollamaClient *utils_ollama.Client
|
||||
dingtalkCardClient *dingtalk.CardClient
|
||||
botConfigImpl *impl.BotConfigImpl
|
||||
}
|
||||
|
||||
func NewCallbackBiz(
|
||||
cfg *config.Config,
|
||||
ollamaClient *utils_ollama.Client,
|
||||
dingtalkCardClient *dingtalk.CardClient,
|
||||
botConfigImpl *impl.BotConfigImpl,
|
||||
) *CallbackBiz {
|
||||
return &CallbackBiz{
|
||||
cfg: cfg,
|
||||
ollamaClient: ollamaClient,
|
||||
dingtalkCardClient: dingtalkCardClient,
|
||||
botConfigImpl: botConfigImpl,
|
||||
}
|
||||
}
|
||||
|
||||
// IssueHandlingGroup 问题处理群机器人回调
|
||||
// 能力1: 通过[内容提取] 宏,分析用户QA问题,调出QA表单卡片
|
||||
// 能力2: 通过[QA收集] 宏,收集用户反馈,写入知识库
|
||||
// 能力3: 通过[知识库查询] 宏,查询知识库,返回答案
|
||||
func (c *CallbackBiz) IssueHandlingGroup(data chatbot.BotCallbackDataModel) error {
|
||||
// 能力1、2:分析用户QA问题,写入知识库
|
||||
if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") {
|
||||
c.issueHandlingExtractContent(data)
|
||||
}
|
||||
// 能力3:查询知识库,返回答案
|
||||
if strings.Contains(data.Text.Content, "[知识库查询]") {
|
||||
c.issueHandlingQueryKnowledgeBase(data)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 问题处理群机器人内容提取
|
||||
func (c *CallbackBiz) issueHandlingExtractContent(data chatbot.BotCallbackDataModel) {
|
||||
// 1.提取用户输入
|
||||
prompt := fmt.Sprintf(constants.IssueHandlingExtractContentPrompt, data.Text.Content)
|
||||
log.Infof("问题提取提示词: %s", prompt)
|
||||
// LLM 提取
|
||||
generateResp, err := c.ollamaClient.Generation(context.Background(), &api.GenerateRequest{
|
||||
Model: c.cfg.Ollama.GenerateModel,
|
||||
Prompt: prompt,
|
||||
Stream: util.AnyToPoint(false),
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("问题提取失败: %v", err)
|
||||
return
|
||||
}
|
||||
// 解析 JSON 响应
|
||||
var resp struct {
|
||||
Items []struct {
|
||||
Question string `json:"question"`
|
||||
Answer string `json:"answer"`
|
||||
Confidence string `json:"confidence"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil {
|
||||
log.Errorf("解析 JSON 响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2.构建文本域内容
|
||||
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)
|
||||
if err != nil {
|
||||
log.Errorf("获取应用配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 4.创建并投放卡片
|
||||
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId
|
||||
_, err = c.dingtalkCardClient.CreateAndDeliver(
|
||||
appKey,
|
||||
&card_1_0.CreateAndDeliverRequest{
|
||||
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.ContentCollect),
|
||||
OutTrackId: tea.String(outTrackId),
|
||||
CallbackType: tea.String("HTTP"),
|
||||
CallbackRouteKey: tea.String(c.cfg.Dingtalk.Card.CallbackRouteKey),
|
||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||
CardParamMap: map[string]*string{
|
||||
"title": tea.String("QA知识收集"),
|
||||
"button_display": tea.String("true"),
|
||||
"QA_details_now": tea.String(cardContent),
|
||||
"textarea_display": tea.String("normal"),
|
||||
"action_id": tea.String("collect_qa"),
|
||||
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
|
||||
"_CARD_DEBUG_TOOL_ENTRY": tea.String(c.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段
|
||||
},
|
||||
},
|
||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||
SupportForward: tea.Bool(false),
|
||||
},
|
||||
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
|
||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||
RobotCode: tea.String(c.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// 问题处理群机器人查询知识库
|
||||
func (c *CallbackBiz) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDataModel) {
|
||||
// 获取应用配置
|
||||
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
|
||||
if err != nil {
|
||||
log.Errorf("应用机器人配置不存在: %s, err: %v", data.RobotCode, err)
|
||||
return
|
||||
}
|
||||
// 创建卡片
|
||||
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode)
|
||||
_, err = c.dingtalkCardClient.CreateAndDeliver(
|
||||
appKey,
|
||||
&card_1_0.CreateAndDeliverRequest{
|
||||
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
|
||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||
CardParamMap: map[string]*string{
|
||||
"title": tea.String(data.Text.Content),
|
||||
"markdown": tea.String("知识库检索中..."),
|
||||
},
|
||||
},
|
||||
OutTrackId: tea.String(outTrackId),
|
||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||
SupportForward: tea.Bool(false),
|
||||
},
|
||||
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
|
||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||
RobotCode: tea.String(data.RobotCode),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 查询知识库
|
||||
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
|
||||
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
||||
TenantID: constants.KnowledgeTenantIdDefault,
|
||||
Query: data.Text.Content,
|
||||
Mode: constants.KnowledgeModeMix,
|
||||
Stream: false,
|
||||
Think: false,
|
||||
OnlyRAG: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("查询知识库失败: %v", err)
|
||||
return
|
||||
}
|
||||
knowledgeRespBytes, err := io.ReadAll(knowledgeResp)
|
||||
if err != nil {
|
||||
log.Errorf("读取知识库响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 卡片更新
|
||||
message, isRetrieved, err := knowledge_base.ParseOpenAIHTTPData(string(knowledgeRespBytes))
|
||||
if err != nil {
|
||||
log.Errorf("读取知识库 SSE 数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
content := message.Content
|
||||
if !isRetrieved {
|
||||
content = "知识库未检测到匹配信息,请核查知识库数据是否正确。"
|
||||
}
|
||||
|
||||
// 卡片更新
|
||||
_, err = c.dingtalkCardClient.UpdateCard(
|
||||
appKey,
|
||||
&card_1_0.UpdateCardRequest{
|
||||
OutTrackId: tea.String(outTrackId),
|
||||
CardData: &card_1_0.UpdateCardRequestCardData{
|
||||
CardParamMap: map[string]*string{
|
||||
"markdown": tea.String(content),
|
||||
},
|
||||
},
|
||||
CardUpdateOptions: &card_1_0.UpdateCardRequestCardUpdateOptions{
|
||||
UpdateCardDataByKey: tea.Bool(true),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("更新卡片失败: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// IssueHandlingCollectQA 问题处理群机器人 QA 收集回调
|
||||
func (c *CallbackBiz) IssueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
|
||||
// 确认提交,文本写入知识库
|
||||
if data.CardActionData.CardPrivateData.Params["submit"] == "submit" {
|
||||
content := data.CardActionData.CardPrivateData.Params["QA_details"].(string)
|
||||
tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string)
|
||||
|
||||
// 协程执行耗时操作,防止阻塞
|
||||
util.SafeGo("inject_knowledge_base", func() {
|
||||
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
|
||||
err := knowledgeBase.IngestText(&knowledge_base.IngestTextRequest{
|
||||
TenantID: tenantID,
|
||||
Text: content,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("注入知识库失败: %v", err)
|
||||
} else {
|
||||
log.Infof("注入知识库成功: tenantID=%s", tenantID)
|
||||
}
|
||||
|
||||
// 解析当前卡片的 ConversationId 和 robotCode
|
||||
conversationId, robotCode := constants.ParseCardOutTrackId(data.OutTrackId)
|
||||
|
||||
// 获取应用配置
|
||||
appKey, err := c.botConfigImpl.GetRobotAppKey(robotCode)
|
||||
if err != nil {
|
||||
log.Errorf("获取应用机器人配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 发送卡片通知用户注入成功
|
||||
outTrackId := constants.BuildCardOutTrackId(conversationId, robotCode)
|
||||
c.dingtalkCardClient.CreateAndDeliver(
|
||||
appKey,
|
||||
&card_1_0.CreateAndDeliverRequest{
|
||||
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
|
||||
OutTrackId: tea.String(outTrackId),
|
||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||
CardParamMap: map[string]*string{
|
||||
"title": tea.String("QA知识收集结果"),
|
||||
"markdown": tea.String("[Get] **成功**"),
|
||||
},
|
||||
},
|
||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||
SupportForward: tea.Bool(false),
|
||||
},
|
||||
OpenSpaceId: tea.String("dtv1.card//im_group." + conversationId),
|
||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||
RobotCode: tea.String(robotCode),
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// 取消提交,禁用输入框
|
||||
resp := &card.CardResponse{
|
||||
CardUpdateOptions: &card.CardUpdateOptions{
|
||||
UpdateCardDataByKey: true,
|
||||
},
|
||||
CardData: &card.CardDataDto{
|
||||
CardParamMap: map[string]string{
|
||||
"textarea_display": "disabled",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
|
@ -21,9 +22,17 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
|
||||
dingtalkPkg "ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
|
||||
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
|
|
@ -40,12 +49,19 @@ type DingTalkBotBiz struct {
|
|||
botGroupQywxImpl *impl.BotGroupQywxImpl
|
||||
toolManager *tools.Manager
|
||||
chatHis *impl.BotChatHisImpl
|
||||
botUserImpl *impl.BotUserImpl
|
||||
conf *config.Config
|
||||
cardSend *dingtalk.SendCardClient
|
||||
qywxGroupHandle *qywx.Group
|
||||
groupConfigBiz *GroupConfigBiz
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
macro *do.Macro
|
||||
dingtalkOauth2Client *dingtalkPkg.Oauth2Client
|
||||
dingTalkOld *dingtalkPkg.OldClient
|
||||
dingtalkCardClient *dingtalkPkg.CardClient
|
||||
redisCli *redis.Client
|
||||
issueImpl *impl.IssueImpl
|
||||
sysImpl *impl.SysImpl
|
||||
}
|
||||
|
||||
// NewDingTalkBotBiz
|
||||
|
|
@ -54,14 +70,22 @@ func NewDingTalkBotBiz(
|
|||
handle *do.Handle,
|
||||
botConfigImpl *impl.BotConfigImpl,
|
||||
botGroupImpl *impl.BotGroupImpl,
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
dingTalkUser *dingtalk.User,
|
||||
chatHis *impl.BotChatHisImpl,
|
||||
botUserImpl *impl.BotUserImpl,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
toolManager *tools.Manager,
|
||||
conf *config.Config,
|
||||
cardSend *dingtalk.SendCardClient,
|
||||
groupConfigBiz *GroupConfigBiz,
|
||||
macro *do.Macro,
|
||||
dingtalkOauth2Client *dingtalkPkg.Oauth2Client,
|
||||
dingTalkOld *dingtalkPkg.OldClient,
|
||||
dingtalkCardClient *dingtalkPkg.CardClient,
|
||||
rdb *utils.Rdb,
|
||||
issueImpl *impl.IssueImpl,
|
||||
sysImpl *impl.SysImpl,
|
||||
) *DingTalkBotBiz {
|
||||
return &DingTalkBotBiz{
|
||||
do: do,
|
||||
|
|
@ -71,12 +95,20 @@ func NewDingTalkBotBiz(
|
|||
dingTalkUser: dingTalkUser,
|
||||
groupConfigBiz: groupConfigBiz,
|
||||
botGroupImpl: botGroupImpl,
|
||||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
toolManager: toolManager,
|
||||
chatHis: chatHis,
|
||||
botUserImpl: botUserImpl,
|
||||
conf: conf,
|
||||
cardSend: cardSend,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
macro: macro,
|
||||
dingtalkOauth2Client: dingtalkOauth2Client,
|
||||
dingTalkOld: dingTalkOld,
|
||||
dingtalkCardClient: dingtalkCardClient,
|
||||
redisCli: rdb.Rdb,
|
||||
issueImpl: issueImpl,
|
||||
sysImpl: sysImpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,19 +158,264 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat
|
|||
return
|
||||
}
|
||||
|
||||
// handleSingleChat 单聊处理
|
||||
// 先不接意图识别-仅提供问题处理
|
||||
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||||
entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新")
|
||||
// 1. 获取用户信息
|
||||
requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1))
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
requireData.ID = int32(requireData.UserInfo.UserId)
|
||||
|
||||
// 2. 检查会话状态 (Redis)
|
||||
statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId)
|
||||
status, _ := d.redisCli.Get(ctx, statusKey).Result()
|
||||
|
||||
if status == "WAITING_FOR_SYS_CONFIRM" {
|
||||
// 用户回复了系统名称
|
||||
sysName := requireData.Req.Text.Content
|
||||
d.redisCli.Del(ctx, statusKey)
|
||||
return d.handleWithSpecificSys(ctx, requireData, sysName)
|
||||
}
|
||||
|
||||
// 3. 获取历史记录 (最近6轮用户输入)
|
||||
userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, 6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 改写 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
|
||||
}
|
||||
|
||||
// 构造识别对象
|
||||
rec := &entitys.Recognize{
|
||||
Ch: requireData.Ch,
|
||||
SystemPrompt: d.defaultPrompt(),
|
||||
UserContent: &entitys.RecognizeUserContent{
|
||||
Text: queryText,
|
||||
},
|
||||
}
|
||||
|
||||
// 5. 调用知识库
|
||||
isRetrieved, err := d.groupConfigBiz.handleKnowledge(ctx, rec, nil, requireData.Req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isRetrieved {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. Fallback: 分类 -> 规则 -> 拉群
|
||||
return d.fallbackToGroupCreation(ctx, requireData)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// fallbackToGroupCreation 分类并拉群
|
||||
func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error {
|
||||
// 1. 获取所有系统和问题类型用于分类
|
||||
allSys, err := d.sysImpl.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string {
|
||||
return sys.SysName
|
||||
})
|
||||
allIssueTypes, err := d.issueImpl.IssueType.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string {
|
||||
return it.Name
|
||||
})
|
||||
|
||||
// 2. LLM 分类
|
||||
classification, err := d.handle.ClassifyIssue(ctx, sysNames, issueTypeNames, requireData.Req.Text.Content)
|
||||
if err != nil {
|
||||
// 分类失败,使用兜底
|
||||
return d.createDefaultGroup(ctx, requireData, "系统无法识别")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
if sys.SysID == 0 {
|
||||
|
||||
// 判断全局是否存在该规则
|
||||
_, found, _ := d.issueImpl.IssueAssignRule.FindOne(
|
||||
d.issueImpl.WithSysID(0),
|
||||
d.issueImpl.WithIssueTypeID(issueType.ID),
|
||||
d.issueImpl.WithStatus(1),
|
||||
)
|
||||
if !found {
|
||||
// 无法明确系统,且全局无该能力,询问用户
|
||||
statusKey := fmt.Sprintf("ai_bot:session:status:%s", requireData.Req.SenderStaffId)
|
||||
d.redisCli.Set(ctx, statusKey, "WAITING_FOR_SYS_CONFIRM", time.Hour)
|
||||
entitys.ResText(requireData.Ch, "", "抱歉,我无法确定您咨询的是哪个系统。请告诉我具体系统名称(如:直连天下系统、货易通系统),以便我为您安排对应的技术支持。")
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys)
|
||||
}
|
||||
|
||||
// fallbackToGroupCreationWithSys 在已知系统的情况下进行分类并拉群
|
||||
func (d *DingTalkBotBiz) fallbackToGroupCreationWithSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sys *model.AiSy) error {
|
||||
// 1. 获取所有问题类型
|
||||
allIssueTypes, err := d.issueImpl.IssueType.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string {
|
||||
return it.Name
|
||||
})
|
||||
|
||||
// 2. LLM 再次分类(确定问题类型和简述)
|
||||
classification, err := d.handle.ClassifyIssue(ctx, []string{sys.SysName}, issueTypeNames, requireData.Req.Text.Content)
|
||||
if err != nil {
|
||||
return d.createDefaultGroup(ctx, requireData, "问题类型识别失败")
|
||||
}
|
||||
issueType, found, err := d.issueImpl.IssueType.FindOne(d.issueImpl.WithName(classification.IssueTypeName))
|
||||
if !found {
|
||||
log.Errorf("issue type %s not found; err: %v", classification.IssueTypeName, err)
|
||||
return fmt.Errorf("问题类型 %s 不存在", classification.IssueTypeName)
|
||||
}
|
||||
|
||||
// 3. 查找分配规则
|
||||
rule, found, err := d.issueImpl.IssueAssignRule.FindOne(
|
||||
d.issueImpl.WithSysID(sys.SysID),
|
||||
d.issueImpl.WithIssueTypeID(issueType.ID),
|
||||
d.issueImpl.WithStatus(1),
|
||||
)
|
||||
if !found {
|
||||
// 创建默认分配规则 - 暂不考虑并发,有唯一索引
|
||||
rule = model.AiIssueAssignRule{
|
||||
SysID: sys.SysID,
|
||||
IssueTypeID: issueType.ID,
|
||||
Status: 1,
|
||||
}
|
||||
if err := d.issueImpl.IssueAssignRule.Create(&rule); err != nil {
|
||||
log.Errorf("create assign rule for sys %s and issue type %s failed; err: %v", sys.SysName, issueType.Name, err)
|
||||
return fmt.Errorf("创建分配规则 %s-%s 失败", sys.SysName, issueType.Name)
|
||||
} else {
|
||||
log.Infof("create assign rule for sys %s and issue type %s success; rule id: %d", sys.SysName, issueType.Name, rule.ID)
|
||||
}
|
||||
}
|
||||
|
||||
var staffIds []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 != "" {
|
||||
staffIds = append(staffIds, botUser.StaffID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底处理人
|
||||
if len(staffIds) == 0 {
|
||||
staffIds = []string{"17415698414368678"}
|
||||
}
|
||||
|
||||
// 合并提问者
|
||||
staffIds = append([]string{requireData.Req.SenderStaffId}, staffIds...)
|
||||
|
||||
// 4. 发送确认卡片
|
||||
groupName := fmt.Sprintf("[%s]-%s", classification.IssueTypeName, classification.Summary)
|
||||
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
|
||||
RobotCode: requireData.Req.RobotCode,
|
||||
ConversationId: requireData.Req.ConversationId,
|
||||
SenderStaffId: requireData.Req.SenderStaffId,
|
||||
UserIds: staffIds,
|
||||
GroupName: groupName,
|
||||
Summary: 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) {
|
||||
|
|
@ -172,7 +449,7 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit
|
|||
return
|
||||
}
|
||||
|
||||
return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig)
|
||||
return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig, requireData.Req)
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) {
|
||||
|
|
@ -252,6 +529,9 @@ func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.
|
|||
}
|
||||
messages := make([]entitys.HisMessage, 0, len(his))
|
||||
for _, v := range his {
|
||||
if v.Role != "user" {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, entitys.HisMessage{
|
||||
Role: constants.Caller(v.Role), // 用户角色
|
||||
Content: v.Content, // 用户输入内容
|
||||
|
|
@ -411,3 +691,310 @@ func (d *DingTalkBotBiz) defaultPrompt() string {
|
|||
-parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。
|
||||
当前时间:` + now + `,所有的时间识别精确到秒`
|
||||
}
|
||||
|
||||
// CreateIssueHandlingGroupAndInit 创建问题处理群聊并初始化
|
||||
func (d *DingTalkBotBiz) CreateIssueHandlingGroupAndInit(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) {
|
||||
|
||||
// 解析 OutTrackId 以获取 SpaceId 和 BotId
|
||||
spaceId, botId := constants.ParseCardOutTrackId(data.OutTrackId)
|
||||
|
||||
// 获取操作状态
|
||||
status := data.CardActionData.CardPrivateData.Params["status"]
|
||||
if status == "confirm" {
|
||||
// 获取新群聊人员 (从卡片参数中统一解析)
|
||||
targetUserIdsStr := data.CardActionData.CardPrivateData.Params["target_user_ids"].(string)
|
||||
var userIds []string
|
||||
if targetUserIdsStr != "" {
|
||||
userIds = strings.Split(targetUserIdsStr, ",")
|
||||
}
|
||||
|
||||
if len(userIds) == 0 {
|
||||
return nil, errors.New("target_user_ids 参数不能为空")
|
||||
}
|
||||
|
||||
// 创建群聊及群初始化(异步响应)
|
||||
util.SafeGo("CreateIssueHandlingGroupAndInit", func() {
|
||||
err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds)
|
||||
if err != nil {
|
||||
log.Errorf("创建群聊及群初始化失败: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 构建关闭创建群组卡片按钮的响应
|
||||
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.ConversationId, 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, ",")),
|
||||
},
|
||||
},
|
||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||
SupportForward: tea.Bool(false),
|
||||
},
|
||||
OpenSpaceId: tea.String("dtv1.card//im_group." + params.ConversationId),
|
||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||
RobotCode: tea.String(params.RobotCode),
|
||||
Recipients: recipients,
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// buildNewGroupUserIds 构建新群聊人员列表
|
||||
func (d *DingTalkBotBiz) buildNewGroupUserIds(spaceId, botId, groupOwner string) ([]string, error) {
|
||||
// 群id+机器人id确认一个群配置
|
||||
botGroup, err := d.botGroupImpl.GetByConversationIdAndRobotCode(spaceId, botId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取群配置
|
||||
var groupConfig model.AiBotGroupConfig
|
||||
cond := builder.NewCond().And(builder.Eq{"config_id": botGroup.ConfigID})
|
||||
err = d.botGroupConfigImpl.GetOneBySearchToStrut(&cond, &groupConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取处理人列表
|
||||
issueOwnerJson := groupConfig.IssueOwner
|
||||
type issueOwnerType struct {
|
||||
UserId string `json:"userid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var issueOwner []issueOwnerType
|
||||
if err = json.Unmarshal([]byte(issueOwnerJson), &issueOwner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 合并所有userid
|
||||
userIds := []string{groupOwner} // 当前用户为群主
|
||||
for _, owner := range issueOwner {
|
||||
userIds = append(userIds, owner.UserId)
|
||||
}
|
||||
|
||||
return userIds, nil
|
||||
}
|
||||
|
||||
// createIssueHandlingGroupAndInit 创建问题处理群聊及群初始化
|
||||
func (d *DingTalkBotBiz) createIssueHandlingGroupAndInit(ctx context.Context, callbackParams map[string]any, spaceId, botId string, userIds []string) error {
|
||||
// 获取应用配置
|
||||
appKey, err := d.botConfigImpl.GetRobotAppKey(botId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取 access_token
|
||||
accessToken, err := d.dingtalkOauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appKey.AccessToken = accessToken
|
||||
|
||||
// 创建群聊
|
||||
_, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, "问题处理群", userIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 添加当前机器人到新群 - SDK 有问题,后续再考虑使用
|
||||
// _, err = d.dingtalkImClient.AddRobotToConversation(
|
||||
// appKey,
|
||||
// &im_1_0.AddRobotToConversationRequest{
|
||||
// OpenConversationId: tea.String(openConversationId),
|
||||
// RobotCode: tea.String(botId),
|
||||
// })
|
||||
// if err != nil {
|
||||
// fmt.Printf("添加机器人到会话失败: %v", err)
|
||||
// }
|
||||
|
||||
// 返回新群分享链接,直接进群 - SDK 有问题,后续再考虑使用
|
||||
// newGroupShareLink, err = d.dingTalkOld.GetJoinGroupQrcode(ctx, chatId, data.UserId)
|
||||
// if err != nil {
|
||||
// fmt.Printf("获取入群二维码失败: %v", err)
|
||||
// }
|
||||
|
||||
// 初始化群聊
|
||||
groupScope := callbackParams["group_scope"].(string) // 群主题
|
||||
d.initIssueHandlingGroup(appKey, openConversationId, groupScope)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createIssueHandlingGroup 创建问题处理群聊会话
|
||||
func (d *DingTalkBotBiz) createIssueHandlingGroup(ctx context.Context, accessToken string, groupName string, userIds []string) (chatId, openConversationId string, err error) {
|
||||
// 是否使用模板群开关
|
||||
var useTemplateGroup bool = true
|
||||
|
||||
if groupName == "" {
|
||||
groupName = "问题处理群"
|
||||
}
|
||||
|
||||
// 创建内部群会话
|
||||
if !useTemplateGroup {
|
||||
return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, groupName, userIds)
|
||||
}
|
||||
|
||||
// 根据群模板ID创建群
|
||||
if useTemplateGroup {
|
||||
return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, groupName, userIds, d.conf.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// initIssueHandlingGroup 初始化问题处理群聊
|
||||
func (d *DingTalkBotBiz) initIssueHandlingGroup(appKey dingtalkPkg.AppKey, openConversationId, groupScope string) error {
|
||||
// 1.开场白
|
||||
outTrackId := constants.BuildCardOutTrackId(openConversationId, d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling)
|
||||
_, err := d.dingtalkCardClient.CreateAndDeliver(
|
||||
appKey,
|
||||
&card_1_0.CreateAndDeliverRequest{
|
||||
CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.BaseMsg),
|
||||
OutTrackId: tea.String(outTrackId),
|
||||
CallbackType: tea.String("HTTP"),
|
||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||
CardParamMap: map[string]*string{
|
||||
"title": tea.String("当前会话主题"),
|
||||
"markdown": tea.String("问题:" + groupScope),
|
||||
},
|
||||
},
|
||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||
SupportForward: tea.Bool(false),
|
||||
},
|
||||
OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId),
|
||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||
RobotCode: tea.String(d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
|
||||
AtUserIds: map[string]*string{
|
||||
"@ALL": tea.String("@ALL"),
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 机器人能力
|
||||
// 构建卡片 OutTrackId
|
||||
outTrackId = constants.BuildCardOutTrackId(openConversationId, d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling)
|
||||
_, err = d.dingtalkCardClient.CreateAndDeliver(
|
||||
appKey,
|
||||
&card_1_0.CreateAndDeliverRequest{
|
||||
CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.BaseMsg),
|
||||
OutTrackId: tea.String(outTrackId),
|
||||
CallbackType: tea.String("HTTP"),
|
||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||
CardParamMap: map[string]*string{
|
||||
"title": tea.String("当前机器人能力"),
|
||||
"markdown": tea.String("- 聊天内容提取(@机器人 [内容提取]{聊天记录/问答描述}) \n - QA知识收集(卡片信息收集) \n - QA知识问答(@机器人 [知识库查询]{问题描述})"),
|
||||
},
|
||||
},
|
||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||
SupportForward: tea.Bool(false),
|
||||
},
|
||||
OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId),
|
||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||
RobotCode: tea.String(d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
|
||||
AtUserIds: map[string]*string{
|
||||
"@ALL": tea.String("@ALL"),
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildCreateGroupCardResp 构建关闭创建群组卡片按钮
|
||||
func (d *DingTalkBotBiz) buildCreateGroupCardResp() *card.CardResponse {
|
||||
return &card.CardResponse{
|
||||
CardData: &card.CardDataDto{
|
||||
CardParamMap: map[string]string{
|
||||
"button_display": "false",
|
||||
},
|
||||
},
|
||||
CardUpdateOptions: &card.CardUpdateOptions{
|
||||
UpdateCardDataByKey: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@ import (
|
|||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||
"ai_scheduler/internal/domain/workflow/runtime"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/gateway"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/mapstructure"
|
||||
"ai_scheduler/internal/pkg/rec_extra"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/public"
|
||||
"bufio"
|
||||
errorsSpecial "errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -31,6 +31,7 @@ import (
|
|||
|
||||
"github.com/coze-dev/coze-go"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/ollama/ollama/api"
|
||||
"gorm.io/gorm/utils"
|
||||
)
|
||||
|
||||
|
|
@ -91,6 +92,79 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr
|
|||
return
|
||||
}
|
||||
|
||||
// RewriteQuery 改写查询词,支持多轮对话
|
||||
func (r *Handle) RewriteQuery(ctx context.Context, history []model.AiBotChatHi, currentQuery string) (string, error) {
|
||||
if len(history) == 0 {
|
||||
return currentQuery, nil
|
||||
}
|
||||
|
||||
var histStr strings.Builder
|
||||
for _, h := range history {
|
||||
role := "用户"
|
||||
if h.Role != "user" {
|
||||
role = "助手"
|
||||
}
|
||||
histStr.WriteString(fmt.Sprintf("%s: %s\n", role, h.Content))
|
||||
}
|
||||
|
||||
systemPrompt := `你是一个搜索查询改写专家。请结合用户的历史对话上下文,将用户当前的输入改写为一个独立的、语义完整的、适合知识库检索的中文查询词。
|
||||
要求:
|
||||
1. 保持原意,补全指代(如“它”、“刚才那个问题”)。
|
||||
2. 只返回改写后的查询词,不要有任何解释。
|
||||
3. 如果当前输入已经很完整,直接返回原句。`
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// ClassifyIssue 问题分类分析
|
||||
func (r *Handle) ClassifyIssue(ctx context.Context, systems []string, issueTypes []string, userInput string) (*IssueClassification, error) {
|
||||
systemPrompt := fmt.Sprintf(`你是一个技术支持路由专家。请分析用户的输入,并将其归类到最合适的系统和问题类型中。
|
||||
可用系统列表: [%s]
|
||||
可用问题类型: [%s]
|
||||
|
||||
请仅以 JSON 格式回复,包含以下字段:
|
||||
- sys_name: 系统名称,若未提及系统关键词,则为"全局"
|
||||
- issue_type_name: 问题类型名称
|
||||
- summary: 15字以内的问题简述(用于群命名)
|
||||
- reason: 分类判断理由;系统名称判断理由`, strings.Join(systems, ", "), strings.Join(issueTypes, ", "))
|
||||
|
||||
messages := []api.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{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
|
||||
}
|
||||
|
||||
func (r *Handle) handleOtherTask(ctx context.Context, requireData *entitys.RequireData) (err error) {
|
||||
entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning)
|
||||
return
|
||||
|
|
@ -129,7 +203,7 @@ func (r *Handle) HandleMatch(ctx context.Context, client *gateway.Client, rec *e
|
|||
case constants.TaskTypeApi:
|
||||
return r.handleApiTask(ctx, rec, pointTask)
|
||||
case constants.TaskTypeKnowle:
|
||||
return r.handleKnowle(ctx, rec, pointTask)
|
||||
return r.handleKnowleV2(ctx, rec, pointTask)
|
||||
case constants.TaskTypeFunc:
|
||||
return r.handleTask(ctx, rec, pointTask)
|
||||
case constants.TaskTypeBot:
|
||||
|
|
@ -166,81 +240,115 @@ 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 (
|
||||
// configData entitys.ConfigDataTool
|
||||
// sessionIdKnowledge string
|
||||
// query string
|
||||
// host string
|
||||
// )
|
||||
// err = json.Unmarshal([]byte(task.Config), &configData)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// ext, err := rec_extra.GetTaskRecExt(rec)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// // 通过session 找到知识库session
|
||||
// var has bool
|
||||
// if len(ext.Session) == 0 {
|
||||
// return errors.SessionNotFound
|
||||
// }
|
||||
// ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session))
|
||||
// if err != nil {
|
||||
// return
|
||||
// } else if !has {
|
||||
// return errors.SessionNotFound
|
||||
// }
|
||||
|
||||
// // 找到知识库的host
|
||||
// {
|
||||
// tool, exists := r.toolManager.GetTool(configData.Tool)
|
||||
// if !exists {
|
||||
// return fmt.Errorf("tool not found: %s", configData.Tool)
|
||||
// }
|
||||
|
||||
// if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok {
|
||||
// return fmt.Errorf("未找到知识库Tool: %s", configData.Tool)
|
||||
// } else {
|
||||
// host = knowledgeTool.GetConfig().BaseURL
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// // 知识库的session为空,请求知识库获取, 并绑定
|
||||
// if ext.SessionInfo.KnowlegeSessionID == "" {
|
||||
// // 请求知识库
|
||||
// if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 绑定知识库session,下次可以使用
|
||||
// ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
|
||||
// if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 用户输入解析
|
||||
// var ok bool
|
||||
// input := make(map[string]string)
|
||||
// if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
|
||||
// return
|
||||
// }
|
||||
// if query, ok = input["query"]; !ok {
|
||||
// return fmt.Errorf("query不能为空")
|
||||
// }
|
||||
|
||||
// ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
|
||||
// Session: ext.SessionInfo.KnowlegeSessionID,
|
||||
// ApiKey: ext.Sys.KnowlegeTenantKey,
|
||||
// Query: query,
|
||||
// }
|
||||
// rec.Ext = pkg.JsonByteIgonErr(ext)
|
||||
// // 执行工具
|
||||
// err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
// 知识库V2 - lightRAG自建
|
||||
func (r *Handle) handleKnowleV2(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||
// 获取用户session信息
|
||||
|
||||
var (
|
||||
configData entitys.ConfigDataTool
|
||||
sessionIdKnowledge string
|
||||
query string
|
||||
host string
|
||||
)
|
||||
err = json.Unmarshal([]byte(task.Config), &configData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ext, err := rec_extra.GetTaskRecExt(rec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 通过session 找到知识库session
|
||||
var has bool
|
||||
if len(ext.Session) == 0 {
|
||||
return errors.SessionNotFound
|
||||
}
|
||||
ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session))
|
||||
// 获取租户ID 形式为 {biz-user} 比如 "zltx-platform"
|
||||
tenantID := ext.Sys.KnowlegeTenantKey
|
||||
|
||||
// 请求知识库工具
|
||||
knowledgeBase := knowledge_base.New(r.conf.KnowledgeConfig)
|
||||
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
||||
TenantID: tenantID, // 后续动态接参
|
||||
Query: rec.UserContent.Text,
|
||||
Mode: constants.KnowledgeModeMix,
|
||||
Stream: true,
|
||||
Think: false,
|
||||
OnlyRAG: true,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
} else if !has {
|
||||
return errors.SessionNotFound
|
||||
return fmt.Errorf("请求知识库工具失败,err: %v", err)
|
||||
}
|
||||
|
||||
// 找到知识库的host
|
||||
{
|
||||
tool, exists := r.toolManager.GetTool(configData.Tool)
|
||||
if !exists {
|
||||
return fmt.Errorf("tool not found: %s", configData.Tool)
|
||||
}
|
||||
|
||||
if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok {
|
||||
return fmt.Errorf("未找到知识库Tool: %s", configData.Tool)
|
||||
} else {
|
||||
host = knowledgeTool.GetConfig().BaseURL
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 知识库的session为空,请求知识库获取, 并绑定
|
||||
if ext.SessionInfo.KnowlegeSessionID == "" {
|
||||
// 请求知识库
|
||||
if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 绑定知识库session,下次可以使用
|
||||
ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
|
||||
if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 用户输入解析
|
||||
var ok bool
|
||||
input := make(map[string]string)
|
||||
if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
|
||||
return
|
||||
}
|
||||
if query, ok = input["query"]; !ok {
|
||||
return fmt.Errorf("query不能为空")
|
||||
}
|
||||
|
||||
ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
|
||||
Session: ext.SessionInfo.KnowlegeSessionID,
|
||||
ApiKey: ext.Sys.KnowlegeTenantKey,
|
||||
Query: query,
|
||||
}
|
||||
rec.Ext = pkg.JsonByteIgonErr(ext)
|
||||
// 执行工具
|
||||
err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
|
||||
// 读取知识库SSE数据
|
||||
err = r.readKnowledgeSSE(knowledgeResp, rec.Ch, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -248,6 +356,67 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
|
|||
return
|
||||
}
|
||||
|
||||
// 读取知识库 SSE 数据
|
||||
func (r *Handle) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (err error) {
|
||||
scanner := bufio.NewScanner(resp)
|
||||
var buffer strings.Builder
|
||||
|
||||
var taskIndex string = "knowledgeBase"
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析SSE数据失败: %w", err)
|
||||
}
|
||||
if done {
|
||||
break
|
||||
}
|
||||
if delta == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 推理内容
|
||||
if delta.ReasoningContent != "" {
|
||||
entitys.ResStream(channel, taskIndex, delta.ReasoningContent)
|
||||
continue
|
||||
}
|
||||
// 输出内容 - 段落
|
||||
if delta.Content != "" && useParagraphMode {
|
||||
// 存入缓冲区
|
||||
buffer.WriteString(delta.Content)
|
||||
content := buffer.String()
|
||||
|
||||
// 检查是否有换行符,按段落输出
|
||||
if idx := strings.LastIndex(content, "\n"); idx != -1 {
|
||||
// 发送直到最后一个换行符的内容
|
||||
toSend := content[:idx+1]
|
||||
entitys.ResStream(channel, taskIndex, toSend)
|
||||
|
||||
// 重置缓冲区,保留剩余部分
|
||||
remaining := content[idx+1:]
|
||||
buffer.Reset()
|
||||
buffer.WriteString(remaining)
|
||||
}
|
||||
}
|
||||
// 输出内容 - 逐字
|
||||
if delta.Content != "" && !useParagraphMode {
|
||||
entitys.ResStream(channel, taskIndex, delta.Content)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("读取SSE流中断: %w", err)
|
||||
}
|
||||
|
||||
// 发送缓冲区剩余内容(仅在段落模式下需要)
|
||||
if useParagraphMode && buffer.Len() > 0 {
|
||||
entitys.ResStream(channel, taskIndex, buffer.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// bot 临时实现,后续转到 eino 工作流
|
||||
func (r *Handle) HandleBot(ctx context.Context, rec *entitys.Recognize, task *entitys.Task) (err error) {
|
||||
if task.Index == "bug_optimization_submit" {
|
||||
|
|
@ -333,7 +502,7 @@ func (r *Handle) getUserDingtalkUnionId(ctx context.Context, accessToken, sessio
|
|||
|
||||
func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) {
|
||||
// 获取创建者uid 用户名 -> dingtalk uid
|
||||
creatorId, err := r.dingtalkContactClient.SearchUserOne(accessToken, userName)
|
||||
creatorId, err := r.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, userName)
|
||||
if err != nil {
|
||||
log.Warnf("search dingtalk user one failed: %v", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -138,20 +138,24 @@ func (f *WithDingTalkBot) CreatePrompt(ctx context.Context, rec *entitys.Recogni
|
|||
mes = append(prompt, api.Message{
|
||||
Role: "system", // 系统角色
|
||||
Content: rec.SystemPrompt, // 系统提示内容
|
||||
// }, api.Message{ // 助手回复无需
|
||||
// Role: "assistant", // 助手角色
|
||||
// Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容
|
||||
}, api.Message{
|
||||
Role: "assistant", // 助手角色
|
||||
Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容
|
||||
Content: "用户历史输入:" + pkg.JsonStringIgonErr(rec.ChatHis), // 用户历史输入
|
||||
}, api.Message{
|
||||
Role: "user", // 用户角色
|
||||
Content: content.String(), // 用户输入内容
|
||||
})
|
||||
fmt.Printf("[意图识别]最终prompt:%v", mes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recognize) (content strings.Builder, err error) {
|
||||
var hasFile bool
|
||||
if rec.UserContent.File != nil && len(rec.UserContent.File) > 0 {
|
||||
if len(rec.UserContent.File) > 0 {
|
||||
hasFile = true
|
||||
}
|
||||
content.WriteString(rec.UserContent.Text)
|
||||
|
|
@ -165,11 +169,10 @@ func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recog
|
|||
content.WriteString(rec.UserContent.Tag)
|
||||
}
|
||||
|
||||
if len(rec.ChatHis.Messages) > 0 {
|
||||
content.WriteString("\n")
|
||||
content.WriteString("### 引用历史聊天记录:\n")
|
||||
content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
|
||||
}
|
||||
// if len(rec.ChatHis.Messages) > 0 {
|
||||
// content.WriteString("### 引用历史聊天记录:\n")
|
||||
// content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
|
||||
// }
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,19 @@ import (
|
|||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||
"ai_scheduler/internal/domain/workflow/recharge"
|
||||
"ai_scheduler/internal/domain/workflow/runtime"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"ai_scheduler/utils"
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -26,7 +30,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/coze-dev/coze-go"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
|
@ -35,12 +43,14 @@ import (
|
|||
type GroupConfigBiz struct {
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
botConfigImpl *impl.BotConfigImpl
|
||||
ossClient *utils_oss.Client
|
||||
workflowManager *runtime.Registry
|
||||
botTools []model.AiBotTool
|
||||
toolManager *tools.Manager
|
||||
conf *config.Config
|
||||
rdb *utils.Rdb
|
||||
dingtalkCardClient *dingtalk.CardClient
|
||||
macro *do.Macro
|
||||
handle *do.Handle
|
||||
}
|
||||
|
|
@ -50,24 +60,28 @@ func NewGroupConfigBiz(
|
|||
tools *tools_regis.ToolRegis,
|
||||
ossClient *utils_oss.Client,
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
botConfigImpl *impl.BotConfigImpl,
|
||||
workflowManager *runtime.Registry,
|
||||
conf *config.Config,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
rdb *utils.Rdb,
|
||||
macro *do.Macro,
|
||||
toolManager *tools.Manager,
|
||||
dingtalkCardClient *dingtalk.CardClient,
|
||||
macro *do.Macro,
|
||||
handle *do.Handle,
|
||||
) *GroupConfigBiz {
|
||||
return &GroupConfigBiz{
|
||||
botTools: tools.BootTools,
|
||||
ossClient: ossClient,
|
||||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
botConfigImpl: botConfigImpl,
|
||||
workflowManager: workflowManager,
|
||||
conf: conf,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
rdb: rdb,
|
||||
macro: macro,
|
||||
toolManager: toolManager,
|
||||
dingtalkCardClient: dingtalkCardClient,
|
||||
macro: macro,
|
||||
handle: handle,
|
||||
}
|
||||
}
|
||||
|
|
@ -235,7 +249,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
|||
return nil
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig) (err error) {
|
||||
func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
|
||||
|
||||
if !rec.Match.IsMatch {
|
||||
if len(rec.Match.Chat) != 0 {
|
||||
|
|
@ -269,6 +283,9 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize
|
|||
return g.handleReport(ctx, rec, pointTask, groupConfig)
|
||||
case constants.TaskTypeCozeWorkflow:
|
||||
return g.handleCozeWorkflow(ctx, rec, pointTask)
|
||||
case constants.TaskTypeKnowle: // 知识库lightRAG版本
|
||||
_, err = g.handleKnowledge(ctx, rec, groupConfig, callback)
|
||||
return err
|
||||
default:
|
||||
return g.otherTask(ctx, rec)
|
||||
}
|
||||
|
|
@ -426,3 +443,213 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
|
|||
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
|
||||
return
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error {
|
||||
var ResellerProductRelation map[int32]*bbxt.ResellerLossSumProductRelation
|
||||
|
||||
dayDate := day.Format(time.DateOnly)
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail})
|
||||
cond = cond.And(builder.Eq{"cache_key": dayDate})
|
||||
var cache model.AiReportDailyCache
|
||||
err := g.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cache.ID == 0 {
|
||||
ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache = model.AiReportDailyCache{
|
||||
CacheKey: dayDate,
|
||||
CacheIndex: bbxt.IndexLossSumDetail,
|
||||
Value: pkg.JsonStringIgonErr(ResellerProductRelation),
|
||||
}
|
||||
_, err = g.reportDailyCacheImpl.Add(&cache)
|
||||
} else {
|
||||
err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range totalDetail {
|
||||
if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
|
||||
continue
|
||||
}
|
||||
v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
|
||||
for _, vv := range v.ProductLoss {
|
||||
if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
|
||||
continue
|
||||
}
|
||||
vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleKnowledge 处理知识库V2版本
|
||||
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)
|
||||
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
||||
TenantID: constants.KnowledgeTenantIdDefault, // 后续动态接参
|
||||
Query: rec.UserContent.Text,
|
||||
Mode: constants.KnowledgeModeMix,
|
||||
Stream: true,
|
||||
Think: false,
|
||||
OnlyRAG: true,
|
||||
})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("请求知识库工具失败,err: %v", err)
|
||||
}
|
||||
|
||||
// 读取知识库SSE数据
|
||||
isRetrieved, err = g.readKnowledgeSSE(knowledgeResp, rec.Ch, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 未检索到匹配信息,群聊时询问是否拉群
|
||||
if !isRetrieved && callback.ConversationType == constants.ConversationTypeGroup {
|
||||
g.shouldCreateIssueHandlingGroup(ctx, rec, groupConfig, callback)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 读取知识库 SSE 数据
|
||||
func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (isRetrieved bool, err error) {
|
||||
scanner := bufio.NewScanner(resp)
|
||||
var buffer strings.Builder
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("解析SSE数据失败: %w", err)
|
||||
}
|
||||
if done {
|
||||
break
|
||||
}
|
||||
if delta == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 知识库未命中 输出提示后中断
|
||||
if delta.XRagStatus == constants.KnowledgeRagStatusMiss {
|
||||
var missContent string = "知识库未检测到匹配信息,即将为您创建群聊解决问题。"
|
||||
entitys.ResStream(channel, "", missContent)
|
||||
return false, nil
|
||||
}
|
||||
// 推理内容
|
||||
if delta.ReasoningContent != "" {
|
||||
entitys.ResStream(channel, "", delta.ReasoningContent)
|
||||
continue
|
||||
}
|
||||
// 输出内容 - 段落
|
||||
if delta.Content != "" && useParagraphMode {
|
||||
// 存入缓冲区
|
||||
buffer.WriteString(delta.Content)
|
||||
content := buffer.String()
|
||||
|
||||
// 检查是否有换行符,按段落输出
|
||||
if idx := strings.LastIndex(content, "\n"); idx != -1 {
|
||||
// 发送直到最后一个换行符的内容
|
||||
toSend := content[:idx+1]
|
||||
entitys.ResStream(channel, "", toSend)
|
||||
|
||||
// 重置缓冲区,保留剩余部分
|
||||
remaining := content[idx+1:]
|
||||
buffer.Reset()
|
||||
buffer.WriteString(remaining)
|
||||
}
|
||||
}
|
||||
// 输出内容 - 逐字
|
||||
if delta.Content != "" && !useParagraphMode {
|
||||
entitys.ResStream(channel, "", delta.Content)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return true, fmt.Errorf("读取SSE流中断: %w", err)
|
||||
}
|
||||
|
||||
// 发送缓冲区剩余内容(仅在段落模式下需要)
|
||||
if useParagraphMode && buffer.Len() > 0 {
|
||||
entitys.ResStream(channel, "", buffer.String())
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 询问是否创建群聊处理问题
|
||||
func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) error {
|
||||
// 获取群问题处理人
|
||||
type issueOwnerType struct {
|
||||
UserId string `json:"userid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var issueOwner []issueOwnerType
|
||||
if err := json.Unmarshal([]byte(groupConfig.IssueOwner), &issueOwner); err != nil {
|
||||
return fmt.Errorf("解析群问题处理人失败,err: %v", err)
|
||||
}
|
||||
// 合并所有name、Id
|
||||
userNames := make([]string, 0, len(issueOwner))
|
||||
userIds := make([]string, 0, len(issueOwner))
|
||||
for _, owner := range issueOwner {
|
||||
userNames = append(userNames, "@"+owner.Name)
|
||||
userIds = append(userIds, owner.UserId)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取机器人配置失败,err: %v", err)
|
||||
}
|
||||
|
||||
// 构建卡片 OutTrackId
|
||||
outTrackId := constants.BuildCardOutTrackId(callback.ConversationId, callback.RobotCode)
|
||||
// 发送钉钉卡片
|
||||
_, err = g.dingtalkCardClient.CreateAndDeliver(
|
||||
appKey,
|
||||
&card_1_0.CreateAndDeliverRequest{
|
||||
CardTemplateId: tea.String(g.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(strings.TrimSpace(rec.UserContent.Text)),
|
||||
"target_user_ids": tea.String(strings.Join(targetUserIds, ",")),
|
||||
},
|
||||
},
|
||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||
SupportForward: tea.Bool(false),
|
||||
},
|
||||
OpenSpaceId: tea.String("dtv1.card//im_group." + callback.ConversationId),
|
||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
||||
RobotCode: tea.String(callback.RobotCode),
|
||||
Recipients: recipientsUsers,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送钉钉卡片失败,err: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,14 @@ func (r *OllamaService) IntentRecognize(ctx context.Context, req *entitys.ToolSe
|
|||
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) RecognizeWithImg(ctx context.Context, imgByte []api.ImageData, ch chan entitys.Response) (desc api.GenerateResponse, err error) {
|
||||
// if imgByte == nil {
|
||||
// return
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@ var ProviderSetBiz = wire.NewSet(
|
|||
NewQywxAppBiz,
|
||||
NewGroupConfigBiz,
|
||||
do.NewMacro,
|
||||
NewCallbackBiz,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ import (
|
|||
"ai_scheduler/internal/domain/repo"
|
||||
"ai_scheduler/internal/domain/workflow"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"testing"
|
||||
|
|
@ -53,6 +55,11 @@ func run() {
|
|||
|
||||
registry := workflow.NewRegistry(configConfig, client, repos, components)
|
||||
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
|
||||
botConfigImpl := impl.NewBotConfigImpl(db)
|
||||
qywxAppBiz = NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
|
||||
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig)
|
||||
reportDailyCacheImpl := impl.NewReportDailyCacheImpl(db)
|
||||
toolManager := tools.NewManager(configConfig, client)
|
||||
oauth2Client, _ := dingtalk.NewOauth2Client(rdb)
|
||||
dingtalkCardClient, _ := dingtalk.NewCardClient(oauth2Client)
|
||||
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, reportDailyCacheImpl, rdb, toolManager, dingtalkCardClient)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type Config struct {
|
|||
Oss Oss `mapstructure:"oss"`
|
||||
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
||||
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
||||
KnowledgeConfig KnowledgeConfig `mapstructure:"knowledge_config"`
|
||||
LLM LLM `mapstructure:"llm"`
|
||||
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
||||
Qywx QywxConfig `mapstructure:"qywx"`
|
||||
|
|
@ -75,6 +76,8 @@ type DingtalkConfig struct {
|
|||
ApiSecret string `mapstructure:"api_secret"`
|
||||
TableDemand AITableConfig `mapstructure:"table_demand"`
|
||||
BotGroupID map[string]int `mapstructure:"bot_group_id"` // 机器人群组
|
||||
Card CardConfig `mapstructure:"card"` // 互动卡片
|
||||
SceneGroup SceneGroupConfig `mapstructure:"scene_group"` // 场景群
|
||||
}
|
||||
|
||||
// QywxConfig 企业微信配置
|
||||
|
|
@ -96,6 +99,34 @@ type AITableConfig struct {
|
|||
SheetIdOrName string `mapstructure:"sheet_id_or_name"`
|
||||
}
|
||||
|
||||
// CardConfig 互动卡片配置
|
||||
type CardConfig struct {
|
||||
// 卡片回调路由key
|
||||
CallbackRouteKey string `mapstructure:"callback_route_key"`
|
||||
// 卡片调试工具 [show:展示 hide:隐藏]
|
||||
DebugToolEntryShow string `mapstructure:"debug_tool_entry_show"`
|
||||
// 卡片模板
|
||||
Template CardTemplateConfig `mapstructure:"template"`
|
||||
}
|
||||
|
||||
// CardTemplateConfig 卡片模板配置
|
||||
type CardTemplateConfig struct {
|
||||
// 基础消息卡片(title + content)
|
||||
BaseMsg string `mapstructure:"base_msg"`
|
||||
// 内容收集卡片(title + textarea + button)
|
||||
ContentCollect string `mapstructure:"content_collect"`
|
||||
// 创建群聊申请(title + content + button)
|
||||
CreateGroupApprove string `mapstructure:"create_group_approve"`
|
||||
}
|
||||
|
||||
// SceneGroupConfig 场景群配置
|
||||
type SceneGroupConfig struct {
|
||||
// 问题处理群模板ID
|
||||
GroupTemplateIDIssueHandling string `mapstructure:"group_template_id_issue_handling"`
|
||||
// 问题处理群模板机器人ID
|
||||
GroupTemplateRobotIDIssueHandling string `mapstructure:"group_template_robot_id_issue_handling"`
|
||||
}
|
||||
|
||||
// SysConfig 系统配置
|
||||
type SysConfig struct {
|
||||
SessionLen int `mapstructure:"session_len"`
|
||||
|
|
@ -253,6 +284,20 @@ type PermissionConfig struct {
|
|||
PermissionURL string `mapstructure:"permission_url"`
|
||||
}
|
||||
|
||||
// KnowledgeConfig 知识库配置
|
||||
type KnowledgeConfig struct {
|
||||
// 知识库地址
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
// 默认租户ID
|
||||
TenantID string `mapstructure:"tenant_id"`
|
||||
// 模式
|
||||
Mode string `mapstructure:"mode"`
|
||||
// 是否思考
|
||||
Think bool `mapstructure:"think"`
|
||||
// 是否仅RAG
|
||||
OnlyRAG bool `mapstructure:"only_rag"`
|
||||
}
|
||||
|
||||
// LoadConfig 加载配置
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package constants
|
||||
|
||||
import "net/url"
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const DingTalkBseUrl = "https://oapi.dingtalk.com"
|
||||
|
||||
|
|
@ -78,3 +83,83 @@ const (
|
|||
]
|
||||
}`
|
||||
)
|
||||
|
||||
// 交互卡片回调
|
||||
const (
|
||||
// 回调类型
|
||||
CardActionCallbackTypeAction string = "actionCallback" // 交互卡片回调事件类型
|
||||
|
||||
// 回调事件类型
|
||||
CardActionTypeCreateGroup string = "create_group" // 创建群聊
|
||||
)
|
||||
|
||||
// dingtalk 卡片 OutTrackId 模板
|
||||
const CardOutTrackIdTemplate string = "{space_id}:{bot_id}:{uuid}"
|
||||
|
||||
func BuildCardOutTrackId(spaceId string, botId string) (outTrackId string) {
|
||||
uuid := uuid.New().String()
|
||||
|
||||
outTrackId = strings.ReplaceAll(CardOutTrackIdTemplate, "{space_id}", spaceId)
|
||||
outTrackId = strings.ReplaceAll(outTrackId, "{bot_id}", botId)
|
||||
outTrackId = strings.ReplaceAll(outTrackId, "{uuid}", uuid)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ParseCardOutTrackId(outTrackId string) (spaceId string, botId string) {
|
||||
parts := strings.Split(outTrackId, ":")
|
||||
if len(parts) != 3 {
|
||||
return
|
||||
}
|
||||
spaceId, botId, _ = parts[0], parts[1], parts[2]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 问题处理群机器人 - LLM 提示词
|
||||
const IssueHandlingExtractContentPrompt string = `你是一个【问题与答案生成助手】。
|
||||
|
||||
你的职责是:
|
||||
- 分析用户输入的内容
|
||||
- 识别其中隐含或明确的问题
|
||||
- 基于输入内容本身,生成对应的问题与答案
|
||||
|
||||
当用户输入为【多条群聊聊天记录】时:
|
||||
- 结合问题主题,判断聊天记录中正在讨论或试图解决的问题
|
||||
- 一个群聊中可能包含多个相互独立的问题,但它们都围绕着一个主题,一般为用户提出的第一个问题,尽可能总结为一个问题
|
||||
- 若确实问题很独立,需要分别识别,对每个问题,整理出清晰、可复用的“问题描述”和“对应答案”
|
||||
|
||||
生成答案时的原则:
|
||||
- 答案必须来源于聊天内容中已经给出的信息或共识
|
||||
- 不要引入外部知识,不要使用聊天记录中真实人名或敏感信息,适当总结
|
||||
- 若聊天中未形成明确答案,应明确标记为“暂无明确结论”
|
||||
- 若存在多种不同观点,应分别列出,不要擅自合并或裁决
|
||||
|
||||
【JSON 输出原则】:
|
||||
- 你的最终输出必须是**合法的 JSON**
|
||||
- 不得输出任何额外解释性文字
|
||||
- JSON 结构必须严格符合以下约定
|
||||
|
||||
JSON 结构约定:
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"question": "清晰、独立、可复用的问题描述",
|
||||
"answer": "基于聊天内容整理出的答案;如无结论则为“暂无明确结论”",
|
||||
"confidence": "high | medium | low"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
字段说明:
|
||||
- items:问题与答案列表;若未识别到有效问题,则返回空数组 []
|
||||
- question:抽象后的标准问题表述,不包含具体聊天语句
|
||||
- answer:整理后的答案,不得引入聊天之外的信息
|
||||
- confidence:根据聊天中信息的一致性和明确程度给出判断
|
||||
|
||||
如果无法从输入中识别出任何有效问题,返回:
|
||||
{ "items": [] }
|
||||
|
||||
用户输入:
|
||||
%s
|
||||
`
|
||||
|
|
|
|||
|
|
@ -20,3 +20,24 @@ func GetKnowledgeId(caller Caller) KnowledgeId {
|
|||
}
|
||||
return CallerKnowledgeIdMap[caller]
|
||||
}
|
||||
|
||||
// 知识库
|
||||
const (
|
||||
KnowledgeTenantIdDefault = "default"
|
||||
)
|
||||
|
||||
// 知识库模式
|
||||
const (
|
||||
KnowledgeModeBypass = "bypass" // 绕过知识库,直接返回用户输入
|
||||
KnowledgeModeNaive = "naive" // 简单模式,直接返回知识库答案
|
||||
KnowledgeModeLocal = "local" // 本地模式,仅使用本地知识库
|
||||
KnowledgeModeGlobal = "global" // 全局模式,使用全局知识库
|
||||
KnowledgeModeHybrid = "hybrid" // 混合模式,结合本地和全局知识库
|
||||
KnowledgeModeMix = "mix" // 混合模式,结合本地、全局和知识库
|
||||
)
|
||||
|
||||
// 知识库命中状态
|
||||
const (
|
||||
KnowledgeRagStatusHit = "hit" // 知识库命中
|
||||
KnowledgeRagStatusMiss = "miss" // 知识库未命中
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,避免使用出现问题
|
||||
type PO interface {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@ package impl
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"encoding/json"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type BotConfigImpl struct {
|
||||
|
|
@ -15,3 +20,33 @@ func NewBotConfigImpl(db *utils.Db) *BotConfigImpl {
|
|||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotConfig)),
|
||||
}
|
||||
}
|
||||
|
||||
// GetRobotConfig 获取机器人配置
|
||||
func (b *BotConfigImpl) GetRobotConfig(robotCode string) (*entitys.DingTalkBot, error) {
|
||||
// 获取机器人配置
|
||||
var botConfig model.AiBotConfig
|
||||
cond := builder.NewCond().And(builder.Eq{"robot_code": robotCode}).And(builder.Eq{"status": 1})
|
||||
err := b.GetOneBySearchToStrut(&cond, &botConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 解出 config
|
||||
var config entitys.DingTalkBot
|
||||
err = json.Unmarshal([]byte(botConfig.BotConfig), &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// GetRobotAppKey 获取机器人应用ID
|
||||
func (b *BotConfigImpl) GetRobotAppKey(robotCode string) (dingtalk.AppKey, error) {
|
||||
// 获取机器人配置
|
||||
dingTalkBotConfig, err := b.GetRobotConfig(robotCode)
|
||||
if err != nil {
|
||||
return dingtalk.AppKey{}, err
|
||||
}
|
||||
|
||||
return dingTalkBotConfig.GetAppKey(), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func NewBotGroupImpl(db *utils.Db) *BotGroupImpl {
|
|||
|
||||
func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) {
|
||||
var data model.AiBotGroup
|
||||
err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ?", staffId, robotCode).Find(&data).Error
|
||||
err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ? and status = 1", staffId, robotCode).Find(&data).Error
|
||||
if data.GroupID == 0 {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"database/sql"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type BotUserImpl struct {
|
||||
|
|
@ -25,3 +27,12 @@ func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) {
|
|||
}
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func (k BotUserImpl) GetByUserIds(userIds []int32) ([]model.AiBotUser, error) {
|
||||
var data []model.AiBotUser
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.In("user_id", userIds))
|
||||
_, err := k.GetListToStruct2(&cond, nil, &data)
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ var ProviderImpl = wire.NewSet(
|
|||
NewBotGroupConfigImpl,
|
||||
NewBotGroupQywxImpl,
|
||||
NewReportDailyCacheImpl,
|
||||
NewIssueImpl,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ type AiBotGroupConfig struct {
|
|||
ConfigID int32 `gorm:"column:config_id;primaryKey;autoIncrement:true" json:"config_id"`
|
||||
ToolList string `gorm:"column:tool_list;not null" json:"tool_list"`
|
||||
ProductName string `gorm:"column:product_name;not null" json:"product_name"`
|
||||
IssueOwner string `gorm:"column:issue_owner;comment:群组问题处理人" json:"issue_owner"` // 群组问题处理人
|
||||
}
|
||||
|
||||
// TableName AiBotGroupConfig's table name
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package knowledge_base
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.KnowledgeConfig
|
||||
}
|
||||
|
||||
func New(cfg config.KnowledgeConfig) *Client {
|
||||
return &Client{cfg: cfg}
|
||||
}
|
||||
|
||||
// 查询知识库
|
||||
func (c *Client) Query(req *QueryRequest) (io.ReadCloser, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("req is nil")
|
||||
}
|
||||
if req.TenantID == "" {
|
||||
return nil, fmt.Errorf("tenantID is empty")
|
||||
}
|
||||
if req.Query == "" {
|
||||
return nil, fmt.Errorf("query is empty")
|
||||
}
|
||||
if req.Mode == "" {
|
||||
req.Mode = c.cfg.Mode
|
||||
}
|
||||
if !req.Think {
|
||||
req.Think = c.cfg.Think
|
||||
}
|
||||
if !req.OnlyRAG {
|
||||
req.OnlyRAG = c.cfg.OnlyRAG
|
||||
}
|
||||
|
||||
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
|
||||
|
||||
rsp, err := (&l_request.Request{
|
||||
Method: "POST",
|
||||
Url: baseURL + "/query",
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Tenant-ID": req.TenantID,
|
||||
"Accept": "text/event-stream",
|
||||
},
|
||||
Json: map[string]interface{}{
|
||||
"query": req.Query,
|
||||
"mode": req.Mode,
|
||||
"stream": req.Stream,
|
||||
"think": req.Think,
|
||||
"only_rag": req.OnlyRAG,
|
||||
},
|
||||
}).SendNoParseResponse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rsp == nil || rsp.Body == nil {
|
||||
return nil, fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
defer rsp.Body.Close()
|
||||
bodyPreview, _ := io.ReadAll(io.LimitReader(rsp.Body, 4096))
|
||||
if len(bodyPreview) > 0 {
|
||||
return nil, fmt.Errorf("knowledge base returned status %d: %s", rsp.StatusCode, string(bodyPreview))
|
||||
}
|
||||
return nil, fmt.Errorf("knowledge base returned status %d", rsp.StatusCode)
|
||||
}
|
||||
|
||||
return rsp.Body, nil
|
||||
}
|
||||
|
||||
// IngestText 向知识库中注入文本
|
||||
func (c *Client) IngestText(req *IngestTextRequest) error {
|
||||
if req == nil {
|
||||
return fmt.Errorf("req is nil")
|
||||
}
|
||||
if req.TenantID == "" {
|
||||
return fmt.Errorf("tenantID is empty")
|
||||
}
|
||||
if req.Text == "" {
|
||||
return fmt.Errorf("text is empty")
|
||||
}
|
||||
|
||||
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
|
||||
|
||||
rsp, err := (&l_request.Request{
|
||||
Method: "POST",
|
||||
Url: baseURL + "/ingest/text",
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Tenant-ID": req.TenantID,
|
||||
},
|
||||
Json: map[string]interface{}{
|
||||
"text": req.Text,
|
||||
},
|
||||
}).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
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package knowledge_base
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCall(t *testing.T) {
|
||||
req := &QueryRequest{
|
||||
TenantID: "admin_test_qa",
|
||||
Query: "lightRAG 的优势?",
|
||||
Mode: "naive",
|
||||
Stream: true,
|
||||
Think: false,
|
||||
OnlyRAG: true,
|
||||
}
|
||||
|
||||
client := New(config.KnowledgeConfig{BaseURL: "http://127.0.0.1:9600"})
|
||||
resp, err := client.Query(req)
|
||||
if err != nil {
|
||||
t.Errorf("Call failed: %v", err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Error("Response is nil")
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
scanner := bufio.NewScanner(resp)
|
||||
var outThinking strings.Builder
|
||||
var outContent strings.Builder
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
delta, done, err := ParseOpenAIStreamData(line)
|
||||
if err != nil {
|
||||
t.Fatalf("parse openai stream failed: %v", err)
|
||||
}
|
||||
if delta == nil {
|
||||
continue
|
||||
}
|
||||
if done {
|
||||
break
|
||||
}
|
||||
|
||||
if delta.XRagStatus != "" {
|
||||
t.Logf("XRagStatus: %s", delta.XRagStatus)
|
||||
}
|
||||
if delta.Content != "" {
|
||||
outContent.WriteString(delta.Content)
|
||||
}
|
||||
if delta.ReasoningContent != "" {
|
||||
outThinking.WriteString(delta.ReasoningContent)
|
||||
}
|
||||
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
t.Fatalf("scan failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Thinking: %s", outThinking.String())
|
||||
t.Logf("Content: %s", outContent.String())
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package knowledge_base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type openAIChunk struct {
|
||||
Choices []struct {
|
||||
Delta *Delta `json:"delta"`
|
||||
Message *Message `json:"message"`
|
||||
FinishReason *string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
type Delta struct {
|
||||
ReasoningContent string `json:"reasoning_content"` // 推理内容
|
||||
Content string `json:"content"` // 内容
|
||||
XRagStatus string `json:"x_rag_status"` // rag命中状态 hit|miss
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"` // 角色
|
||||
Content string `json:"content"` // 内容
|
||||
XRagStatus string `json:"x_rag_status"` // rag命中状态 hit|miss
|
||||
}
|
||||
|
||||
func ParseOpenAIStreamData(dataLine string) (delta *Delta, done bool, err error) {
|
||||
data := strings.TrimSpace(strings.TrimPrefix(dataLine, "data:"))
|
||||
if data == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
data = strings.TrimSpace(data)
|
||||
if data == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
if data == "[DONE]" {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
var chunk openAIChunk
|
||||
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||||
return nil, false, fmt.Errorf("unmarshal openai stream chunk failed: %w", err)
|
||||
}
|
||||
|
||||
for _, c := range chunk.Choices {
|
||||
if c.Delta != nil {
|
||||
return c.Delta, false, nil // 只输出第一个delta
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func ParseOpenAIHTTPData(body string) (message *Message, done bool, err error) {
|
||||
data := strings.TrimSpace(body)
|
||||
if data == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
var resp openAIChunk
|
||||
if err := json.Unmarshal([]byte(data), &resp); err != nil {
|
||||
return nil, false, fmt.Errorf("unmarshal openai stream chunk failed: %w", err)
|
||||
}
|
||||
|
||||
for _, c := range resp.Choices {
|
||||
if c.Message != nil {
|
||||
return c.Message, true, nil // 只输出第一个message
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package knowledge_base
|
||||
|
||||
type QueryRequest struct {
|
||||
TenantID string // 租户 ID
|
||||
Query string // 查询内容
|
||||
Mode string // 模式,默认 naive 可选:[bypass|naive|local|global|hybrid|mix]
|
||||
Stream bool // 仅支持流式输出
|
||||
Think bool // 是否开启思考模式
|
||||
OnlyRAG bool // 是否仅开启 RAG 模式
|
||||
}
|
||||
|
||||
type IngestTextRequest struct {
|
||||
TenantID string // 租户 ID
|
||||
Text string // 要注入的文本内容
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/domain/tools/common/excel_generator"
|
||||
"ai_scheduler/internal/domain/tools/common/image_converter"
|
||||
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||
"ai_scheduler/internal/domain/tools/hyt/goods_add"
|
||||
"ai_scheduler/internal/domain/tools/hyt/goods_brand_search"
|
||||
"ai_scheduler/internal/domain/tools/hyt/goods_category_add"
|
||||
|
|
@ -25,6 +26,7 @@ type Manager struct {
|
|||
type CommonTools struct {
|
||||
ExcelGenerator *excel_generator.Client
|
||||
ImageConverter *image_converter.Client
|
||||
KnowledgeBase *knowledge_base.Client
|
||||
}
|
||||
|
||||
type HytTools struct {
|
||||
|
|
@ -60,6 +62,7 @@ func NewManager(cfg *config.Config) *Manager {
|
|||
Common: &CommonTools{
|
||||
ExcelGenerator: excel_generator.New(),
|
||||
ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic),
|
||||
KnowledgeBase: knowledge_base.New(cfg.KnowledgeConfig),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package entitys
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
)
|
||||
|
|
@ -22,6 +23,13 @@ type DingTalkBot struct {
|
|||
ClientSecret string `json:"client_secret"`
|
||||
}
|
||||
|
||||
func (d *DingTalkBot) GetAppKey() dingtalk.AppKey {
|
||||
return dingtalk.AppKey{
|
||||
AppKey: d.ClientId,
|
||||
AppSecret: d.ClientSecret,
|
||||
}
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Index string `json:"bot_index"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
card "github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type CardClient struct {
|
||||
cli *card.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewCardClient(oauth2Client *Oauth2Client) (*CardClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
c, err := card.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CardClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||
}
|
||||
|
||||
// 创建并投放卡片
|
||||
func (c *CardClient) CreateAndDeliver(appKey AppKey, cardData *card.CreateAndDeliverRequest) (bool, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 调用API
|
||||
resp, err := c.cli.CreateAndDeliverWithOptions(
|
||||
cardData,
|
||||
&card.CreateAndDeliverHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.Body == nil {
|
||||
return false, errorcode.ParamErrf("empty response body")
|
||||
}
|
||||
if !*resp.Body.Success {
|
||||
return false, errorcode.ParamErrf("create and deliver failed")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 更新卡片
|
||||
func (c *CardClient) UpdateCard(appKey AppKey, cardData *card.UpdateCardRequest) (bool, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 调用API
|
||||
resp, err := c.cli.UpdateCardWithOptions(
|
||||
cardData,
|
||||
&card.UpdateCardHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.Body == nil {
|
||||
return false, errorcode.ParamErrf("empty response body")
|
||||
}
|
||||
|
||||
return *resp.Body.Success, nil
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
|
|
@ -11,14 +10,12 @@ import (
|
|||
)
|
||||
|
||||
type ContactClient struct {
|
||||
config *config.Config
|
||||
cli *contact.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewContactClient(config *config.Config) (*ContactClient, error) {
|
||||
func NewContactClient(oauth2Client *Oauth2Client) (*ContactClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
|
|
@ -26,7 +23,7 @@ func NewContactClient(config *config.Config) (*ContactClient, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ContactClient{config: config, cli: c}, nil
|
||||
return &ContactClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||
}
|
||||
|
||||
type SearchUserReq struct {
|
||||
|
|
@ -40,15 +37,23 @@ type SearchUserResp struct {
|
|||
Body interface{}
|
||||
}
|
||||
|
||||
func (c *ContactClient) SearchUserOne(accessToken string, name string) (string, error) {
|
||||
headers := &contact.SearchUserHeaders{}
|
||||
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
||||
resp, err := c.cli.SearchUserWithOptions(&contact.SearchUserRequest{
|
||||
func (c *ContactClient) SearchUserOne(appKey AppKey, name string) (string, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := c.cli.SearchUserWithOptions(
|
||||
&contact.SearchUserRequest{
|
||||
FullMatchField: tea.Int32(1),
|
||||
QueryWord: tea.String(name),
|
||||
Offset: tea.Int32(0),
|
||||
Size: tea.Int32(1),
|
||||
}, headers, &util.RuntimeOptions{})
|
||||
},
|
||||
&contact.SearchUserHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
im "github.com/alibabacloud-go/dingtalk/im_1_0"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type ImClient struct {
|
||||
cli *im.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewImClient(oauth2Client *Oauth2Client) (*ImClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
c, err := im.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ImClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||
}
|
||||
|
||||
// 创建并投放卡片
|
||||
func (c *ImClient) AddRobotToConversation(appKey AppKey, imData *im.AddRobotToConversationRequest) (string, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 调用API
|
||||
resp, err := c.cli.AddRobotToConversationWithOptions(
|
||||
imData,
|
||||
&im.AddRobotToConversationHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Body == nil {
|
||||
return "", errorcode.ParamErrf("empty response body")
|
||||
}
|
||||
|
||||
return *resp.Body.ChatBotUserId, nil
|
||||
}
|
||||
|
||||
// 创建场景群 不返回chatid,如果没有获取群聊分享链接的诉求,可以使用该接口
|
||||
func (c *ImClient) CreateSceneGroup(appKey AppKey, req *im.CreateSceneGroupConversationRequest) (openConversationId string, err error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 调用API
|
||||
resp, err := c.cli.CreateSceneGroupConversationWithOptions(
|
||||
req,
|
||||
&im.CreateSceneGroupConversationHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Body == nil {
|
||||
return "", errorcode.ParamErrf("empty response body")
|
||||
}
|
||||
|
||||
return *resp.Body.OpenConversationId, nil
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
|
@ -13,14 +12,12 @@ import (
|
|||
)
|
||||
|
||||
type NotableClient struct {
|
||||
config *config.Config
|
||||
cli *notable.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewNotableClient(config *config.Config) (*NotableClient, error) {
|
||||
func NewNotableClient(oauth2Client *Oauth2Client) (*NotableClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
|
|
@ -28,7 +25,7 @@ func NewNotableClient(config *config.Config) (*NotableClient, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &NotableClient{config: config, cli: c}, nil
|
||||
return &NotableClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||
}
|
||||
|
||||
type UpdateRecordReq struct {
|
||||
|
|
@ -43,9 +40,13 @@ type UpdateRecordsserResp struct {
|
|||
Body interface{}
|
||||
}
|
||||
|
||||
func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (bool, error) {
|
||||
headers := ¬able.UpdateRecordsHeaders{}
|
||||
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
||||
func (c *NotableClient) UpdateRecord(appKey AppKey, req *UpdateRecordReq) (bool, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := c.cli.UpdateRecordsWithOptions(
|
||||
tea.String(req.BaseId),
|
||||
tea.String(req.SheetId),
|
||||
|
|
@ -63,7 +64,10 @@ func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (
|
|||
Id: tea.String(req.RecordId),
|
||||
},
|
||||
},
|
||||
}, headers, &util.RuntimeOptions{})
|
||||
},
|
||||
¬able.UpdateRecordsHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
oauth2 "github.com/alibabacloud-go/dingtalk/oauth2_1_0"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Oauth2Client struct {
|
||||
cli *oauth2.Client
|
||||
redisCli *redis.Client
|
||||
}
|
||||
|
||||
func NewOauth2Client(rds *utils.Rdb) (*Oauth2Client, error) {
|
||||
cfg := &openapi.Config{
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
c, err := oauth2.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Oauth2Client{cli: c, redisCli: rds.Rdb}, nil
|
||||
}
|
||||
|
||||
type AppKey struct {
|
||||
AppKey string `json:"appKey"`
|
||||
AppSecret string `json:"appSecret"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
}
|
||||
|
||||
// GetAccessToken 获取access token
|
||||
func (c *Oauth2Client) GetAccessToken(req AppKey) (string, error) {
|
||||
// 兼容直接传入 access token 场景
|
||||
if req.AccessToken != "" {
|
||||
return req.AccessToken, nil
|
||||
}
|
||||
|
||||
// 取cache
|
||||
ctx := context.Background()
|
||||
accessToken, err := c.redisCli.Get(ctx, fmt.Sprintf("dingtalk:oauth2:%s:access_token", req.AppKey)).Result()
|
||||
if err == nil {
|
||||
fmt.Println("get access token from cache:", accessToken)
|
||||
return accessToken, nil
|
||||
}
|
||||
if err != redis.Nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 调用API
|
||||
resp, err := c.cli.GetAccessToken(&oauth2.GetAccessTokenRequest{
|
||||
AppKey: tea.String(req.AppKey),
|
||||
AppSecret: tea.String(req.AppSecret),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Body == nil {
|
||||
return "", errorcode.ParamErrf("empty response body")
|
||||
}
|
||||
|
||||
// 缓存token
|
||||
c.redisCli.Set(ctx, fmt.Sprintf("dingtalk:oauth2:%s:access_token", req.AppKey), *resp.Body.AccessToken, time.Duration(*resp.Body.ExpireIn)*time.Second)
|
||||
|
||||
return *resp.Body.AccessToken, nil
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ package dingtalk
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
|
@ -12,6 +13,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/faabiosr/cachego/file"
|
||||
"github.com/fastwego/dingding"
|
||||
|
|
@ -111,3 +113,141 @@ func (c *OldClient) QueryUserDetailsByMobile(ctx context.Context, mobile string)
|
|||
func (c *OldClient) GetAccessToken() (string, error) {
|
||||
return c.atm.GetAccessToken()
|
||||
}
|
||||
|
||||
// CreateInternalGroupConversation 创建企业内部群聊
|
||||
func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, accessToken, groupName string, userIds []string) (chatId, openConversationId string, err error) {
|
||||
body := struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
UserIds []string `json:"useridlist"`
|
||||
ShowHistoryType int `json:"showHistoryType"`
|
||||
Searchable int `json:"searchable"`
|
||||
ValidationType int `json:"validationType"`
|
||||
MentionAllAuthority int `json:"mentionAllAuthority"`
|
||||
ManagementType int `json:"managementType"`
|
||||
ChatBannedType int `json:"chatBannedType"`
|
||||
}{
|
||||
Name: groupName,
|
||||
Owner: userIds[0],
|
||||
UserIds: userIds,
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
req := l_request.Request{
|
||||
Method: "POST",
|
||||
JsonByte: b,
|
||||
Url: "https://oapi.dingtalk.com/chat/create?access_token=" + accessToken,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
res, err := req.Send()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Code int `json:"errcode"`
|
||||
Msg string `json:"errmsg"`
|
||||
ChatId string `json:"chatid"`
|
||||
OpenConversationId string `json:"openConversationId"`
|
||||
ConversationTag int `json:"conversationTag"`
|
||||
}
|
||||
if err = json.Unmarshal(res.Content, &resp); err != nil {
|
||||
return
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return "", "", errors.New(resp.Msg)
|
||||
}
|
||||
|
||||
return resp.ChatId, resp.OpenConversationId, nil
|
||||
}
|
||||
|
||||
// CreateSceneGroupConversation 创建场景群-基于群模板
|
||||
func (c *OldClient) CreateSceneGroupConversation(ctx context.Context, accessToken, groupName string, userIds []string, templateId string) (chatId, openConversationId string, err error) {
|
||||
body := struct {
|
||||
Title string `json:"title"` // 群名称
|
||||
TemplateId string `json:"template_id"` // 群模板ID
|
||||
OwnerUserID string `json:"owner_user_id"` // 群主的userid。
|
||||
UserIds string `json:"user_ids"` // 群成员userid列表。
|
||||
SubAdminIds string `json:"subadmin_ids"` // 群管理员userid列表。
|
||||
UUID string `json:"uuid"` // 建群去重的业务ID,由接口调用方指定。
|
||||
Icon string `json:"icon"` // 群头像,格式为mediaId。需要调用上传媒体文件接口上传群头像,获取mediaId。
|
||||
MentionAllAuthority int `json:"mention_all_authority"` // @all 权限:0(默认):所有人都可以@all 1:仅群主可@all
|
||||
ShowHistoryType int `json:"show_history_type"` // 新成员是否可查看聊天历史消息:0(默认):不可以查看历史记录 1:可以查看历史记录
|
||||
ValidationType int `json:"validation_type"` // 入群是否需要验证:0(默认):不验证入群 1:入群验证
|
||||
Searchable int `json:"searchable"` // 群是否可搜索:0(默认):不可搜索 1:可搜索
|
||||
ChatBannedType int `json:"chat_banned_type"` // 是否开启群禁言:0(默认):不禁言 1:全员禁言
|
||||
ManagementType int `json:"management_type"` // 管理类型:0(默认):所有人可管理 1:仅群主可管理
|
||||
OnlyAdminCanDing int `json:"only_admin_can_ding"` // 群内发DING权限:0(默认):所有人可发DING 1:仅群主和管理员可发DING
|
||||
AllMembersCanCreateMcsConf int `json:"all_members_can_create_mcs_conf"` // 群会议权限:0:仅群主和管理员可发起视频和语音会议 1(默认):所有人可发起视频和语音会议
|
||||
AllMembersCanCreateCalendar int `json:"all_members_can_create_calendar"` // 群日历权限:0:仅群主和管理员可创建群日历 1(默认):所有人可创建群日历
|
||||
GroupEmailDisabled int `json:"group_email_disabled"` // 群邮件权限:0(默认):群内成员可以对本群发送群邮件 1:群内成员不可对本群发送群邮件
|
||||
OnlyAdminCanSetMsgTop int `json:"only_admin_can_set_msg_top"` // 置顶群消息权限:0(默认):所有人可置顶群消息 1:仅群主和管理员可置顶群消息
|
||||
AddFriendForbidden int `json:"add_friend_forbidden"` // 群成员私聊权限:0(默认):所有人可私聊 1:普通群成员之间不能够加好友、单聊,且部分功能使用受限(管理员与非管理员之间不受影响)
|
||||
GroupLiveSwitch int `json:"group_live_switch"` // 群直播权限:0:仅群主与管理员可发起直播 1(默认):群内任意成员可发起群直播
|
||||
MembersToAdminChat int `json:"members_to_admin_chat"` // 是否禁止非管理员向管理员发起单聊:0(默认):非管理员可以向管理员发起单聊 1:禁止非管理员向管理员发起单聊
|
||||
}{
|
||||
Title: groupName,
|
||||
TemplateId: templateId,
|
||||
OwnerUserID: userIds[0],
|
||||
UserIds: strings.Join(userIds, ","),
|
||||
SubAdminIds: strings.Join(userIds, ","),
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
req := l_request.Request{
|
||||
Method: "POST",
|
||||
JsonByte: b,
|
||||
Url: "https://oapi.dingtalk.com/topapi/im/chat/scenegroup/create?access_token=" + accessToken,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
res, err := req.Send()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Code int `json:"errcode"`
|
||||
Msg string `json:"errmsg"`
|
||||
Result struct {
|
||||
ChatId string `json:"chat_id"`
|
||||
OpenConversationId string `json:"open_conversation_id"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err = json.Unmarshal(res.Content, &resp); err != nil {
|
||||
return
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return "", "", errors.New(resp.Msg)
|
||||
}
|
||||
return resp.Result.ChatId, resp.Result.OpenConversationId, nil
|
||||
}
|
||||
|
||||
// 获取入群二维码链接
|
||||
func (c *OldClient) GetJoinGroupQrcode(ctx context.Context, chatId, userId string) (string, error) {
|
||||
body := struct {
|
||||
ChatId string `json:"chatid"`
|
||||
UserId string `json:"userid"`
|
||||
}{ChatId: chatId, UserId: userId}
|
||||
b, _ := json.Marshal(body)
|
||||
res, err := c.do(ctx, http.MethodPost, "/topapi/chat/qrcode/get", b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var resp struct {
|
||||
Code int `json:"errcode"`
|
||||
Msg string `json:"errmsg"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(res, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return "", errors.New(resp.Msg)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"encoding/json"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
robot "github.com/alibabacloud-go/dingtalk/robot_1_0"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
type RobotClient struct {
|
||||
cli *robot.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewRobotClient(oauth2Client *Oauth2Client) (*RobotClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
c, err := robot.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RobotClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||
}
|
||||
|
||||
type SendGroupMessagesReq struct {
|
||||
MsgKey string
|
||||
MsgParam map[string]any
|
||||
OpenConversationId string
|
||||
RobotCode string
|
||||
}
|
||||
|
||||
func (c *RobotClient) SendGroupMessages(appKey AppKey, req *SendGroupMessagesReq) (string, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
msgParamBytes, _ := json.Marshal(req.MsgParam)
|
||||
msgParamJson := string(msgParamBytes)
|
||||
resp, err := c.cli.OrgGroupSendWithOptions(
|
||||
&robot.OrgGroupSendRequest{
|
||||
MsgKey: tea.String(req.MsgKey),
|
||||
MsgParam: tea.String(msgParamJson),
|
||||
OpenConversationId: tea.String(req.OpenConversationId),
|
||||
RobotCode: tea.String(req.RobotCode),
|
||||
},
|
||||
&robot.OrgGroupSendHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Body == nil {
|
||||
return "", errorcode.ParamErrf("empty response body")
|
||||
}
|
||||
|
||||
return *resp.Body.ProcessQueryKey, nil
|
||||
}
|
||||
|
|
@ -21,6 +21,10 @@ var ProviderSetClient = wire.NewSet(
|
|||
dingtalk.NewOldClient,
|
||||
dingtalk.NewContactClient,
|
||||
dingtalk.NewNotableClient,
|
||||
dingtalk.NewRobotClient,
|
||||
dingtalk.NewOauth2Client,
|
||||
dingtalk.NewCardClient,
|
||||
dingtalk.NewImClient,
|
||||
|
||||
utils_oss.NewClient,
|
||||
lsxd.NewLogin,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type FlexibleType string
|
||||
|
||||
func (ft *FlexibleType) UnmarshalJSON(data []byte) error {
|
||||
var v interface{}
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
*ft = FlexibleType(val)
|
||||
case float64:
|
||||
*ft = FlexibleType(strconv.FormatFloat(val, 'f', -1, 64))
|
||||
case bool:
|
||||
*ft = FlexibleType(strconv.FormatBool(val))
|
||||
default:
|
||||
*ft = FlexibleType(fmt.Sprintf("%v", val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ft FlexibleType) Int() int {
|
||||
if ft == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
i, _ := strconv.Atoi(string(ft))
|
||||
return i
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
)
|
||||
|
||||
var (
|
||||
logger *log.Helper
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// getLogger 懒加载获取日志器
|
||||
func getLogger() *log.Helper {
|
||||
once.Do(func() {
|
||||
// 如果没有手动初始化,使用默认的标准输出日志器
|
||||
if logger == nil {
|
||||
stdLogger := log.With(log.NewStdLogger(os.Stdout),
|
||||
"ts", log.DefaultTimestamp,
|
||||
"caller", log.DefaultCaller,
|
||||
"component", "safe_pool",
|
||||
)
|
||||
logger = log.NewHelper(stdLogger)
|
||||
}
|
||||
})
|
||||
return logger
|
||||
}
|
||||
|
||||
// InitSafePool 初始化安全协程池(可选,如果不调用会使用默认日志器)
|
||||
func InitSafePool(l log.Logger) {
|
||||
logger = log.NewHelper(l)
|
||||
}
|
||||
|
||||
// SafeGo 安全执行协程
|
||||
// taskName: 协程任务名称,用于日志记录
|
||||
// fn: 要执行的函数
|
||||
func SafeGo(taskName string, fn func()) {
|
||||
gopool.Go(func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
getLogger().Errorf("协程 [%s] 发生panic: %v\n堆栈信息:\n%s", taskName, r, string(stack))
|
||||
}
|
||||
}()
|
||||
|
||||
// 记录协程开始执行
|
||||
getLogger().Infof("协程 [%s] 开始执行", taskName)
|
||||
start := time.Now()
|
||||
|
||||
// 执行用户函数
|
||||
fn()
|
||||
|
||||
// 记录协程执行完成
|
||||
duration := time.Since(start)
|
||||
getLogger().Infof("协程 [%s] 执行完成,耗时: %v", taskName, duration)
|
||||
})
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package server
|
|||
import (
|
||||
"ai_scheduler/internal/services"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
|
@ -20,6 +21,7 @@ type cronJob struct {
|
|||
EntryId int32
|
||||
Func func(context.Context) error
|
||||
Name string
|
||||
Key string
|
||||
Schedule string
|
||||
}
|
||||
|
||||
|
|
@ -42,11 +44,13 @@ func (c *CronServer) InitJobs(ctx context.Context) {
|
|||
{
|
||||
Func: c.cronService.CronReportSendDingTalk,
|
||||
Name: "直连天下报表推送(钉钉)",
|
||||
Key: "ding_report_dingtalk",
|
||||
Schedule: "20 12,18,23 * * *",
|
||||
},
|
||||
{
|
||||
Func: c.cronService.CronReportSendQywx,
|
||||
Name: "直连天下报表推送(微信)",
|
||||
Key: "ding_report_qywx",
|
||||
Schedule: "20 12,18,23 * * *",
|
||||
},
|
||||
}
|
||||
|
|
@ -96,3 +100,39 @@ func (c *CronServer) Stop() {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"sync"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/client"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
type DingBotServiceInterface interface {
|
||||
GetServiceCfg() ([]entitys.DingTalkBot, error)
|
||||
OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error)
|
||||
OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error)
|
||||
}
|
||||
|
||||
type DingTalkBotServer struct {
|
||||
|
|
@ -38,7 +40,7 @@ func NewDingTalkBotServer(
|
|||
}
|
||||
cli := DingBotServerInit(serviceConf.ClientId, serviceConf.ClientSecret, service)
|
||||
if cli == nil {
|
||||
log.Info("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error())
|
||||
log.Infof("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error())
|
||||
continue
|
||||
}
|
||||
clients[serviceConf.BotIndex] = cli
|
||||
|
|
@ -52,7 +54,9 @@ func NewDingTalkBotServer(
|
|||
func ProvideAllDingBotServices(
|
||||
dingBotSvc *services.DingBotService,
|
||||
) []DingBotServiceInterface {
|
||||
return []DingBotServiceInterface{dingBotSvc}
|
||||
return []DingBotServiceInterface{
|
||||
dingBotSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
|
||||
|
|
@ -103,5 +107,6 @@ func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
|
|||
func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) {
|
||||
cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
|
||||
cli.RegisterChatBotCallbackRouter(service.OnChatBotMessageReceived)
|
||||
cli.RegisterCardCallbackRouter(service.OnCardMessageReceived)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
|
|||
r.Post("/chat/useful", ChatService.Useful)
|
||||
// 回调
|
||||
r.Post("/callback", callbackService.Callback)
|
||||
// 钉钉机器人回调
|
||||
r.Post("/callback/dingtalk-robot", callbackService.CallbackDingtalkRobot)
|
||||
// 钉钉卡片回调
|
||||
r.Post("/callback/dingtalk-card", callbackService.CallbackDingtalkCard)
|
||||
// 回调
|
||||
r.Get("/qywx/callback", callbackService.QywxCallback)
|
||||
//广播
|
||||
|
|
@ -154,6 +158,12 @@ func registerCommon(c *fiber.Ctx, err error) error {
|
|||
}
|
||||
var data interface{}
|
||||
json.Unmarshal(c.Response().Body(), &data)
|
||||
|
||||
// 检查是否需要跳过响应包装
|
||||
if c.Locals("skip_response_wrap") == true {
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"data": data,
|
||||
"message": errors.Success.Error(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
|
|
@ -19,6 +20,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
|
@ -30,17 +33,39 @@ type CallbackService struct {
|
|||
dingtalkOldClient *dingtalk.OldClient
|
||||
dingtalkContactClient *dingtalk.ContactClient
|
||||
dingtalkNotableClient *dingtalk.NotableClient
|
||||
dingtalkCardClient *dingtalk.CardClient
|
||||
callbackManager callback.Manager
|
||||
dingTalkBotBiz *biz.DingTalkBotBiz
|
||||
callbackBiz *biz.CallbackBiz
|
||||
// ollamaClient *utils_ollama.Client
|
||||
// botConfigImpl *impl.BotConfigImpl
|
||||
}
|
||||
|
||||
func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkOldClient *dingtalk.OldClient, dingtalkContactClient *dingtalk.ContactClient, dingtalkNotableClient *dingtalk.NotableClient, callbackManager callback.Manager) *CallbackService {
|
||||
func NewCallbackService(
|
||||
cfg *config.Config,
|
||||
gateway *gateway.Gateway,
|
||||
dingtalkOldClient *dingtalk.OldClient,
|
||||
dingtalkContactClient *dingtalk.ContactClient,
|
||||
dingtalkNotableClient *dingtalk.NotableClient,
|
||||
dingtalkCardClient *dingtalk.CardClient,
|
||||
callbackManager callback.Manager,
|
||||
dingTalkBotBiz *biz.DingTalkBotBiz,
|
||||
callbackBiz *biz.CallbackBiz,
|
||||
// ollamaClient *utils_ollama.Client,
|
||||
// botConfigImpl *impl.BotConfigImpl,
|
||||
) *CallbackService {
|
||||
return &CallbackService{
|
||||
cfg: cfg,
|
||||
gateway: gateway,
|
||||
dingtalkOldClient: dingtalkOldClient,
|
||||
dingtalkContactClient: dingtalkContactClient,
|
||||
dingtalkNotableClient: dingtalkNotableClient,
|
||||
dingtalkCardClient: dingtalkCardClient,
|
||||
callbackManager: callbackManager,
|
||||
dingTalkBotBiz: dingTalkBotBiz,
|
||||
callbackBiz: callbackBiz,
|
||||
// ollamaClient: ollamaClient,
|
||||
// botConfigImpl: botConfigImpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +295,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
|
|||
|
||||
// 获取创建者uid
|
||||
accessToken, _ := s.dingtalkOldClient.GetAccessToken()
|
||||
creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator)
|
||||
creatorId, err := s.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, data.Creator)
|
||||
if err != nil {
|
||||
return "", errorcode.ParamErrf("invalid data type: %v", err)
|
||||
}
|
||||
|
|
@ -286,7 +311,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
|
|||
unionId := userDetails.UnionID
|
||||
|
||||
// 更新记录
|
||||
ok, err := s.dingtalkNotableClient.UpdateRecord(accessToken, &dingtalk.UpdateRecordReq{
|
||||
ok, err := s.dingtalkNotableClient.UpdateRecord(dingtalk.AppKey{AccessToken: accessToken}, &dingtalk.UpdateRecordReq{
|
||||
BaseId: data.BaseId,
|
||||
SheetId: data.SheetId,
|
||||
RecordId: data.RecordId,
|
||||
|
|
@ -364,3 +389,75 @@ func getString(str, endstr string, start int, msg *string) int {
|
|||
*msg = str[start:end]
|
||||
return end + len(endstr)
|
||||
}
|
||||
|
||||
// CallbackDingtalkRobot 钉钉机器人回调
|
||||
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-robot
|
||||
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
|
||||
func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) {
|
||||
// 获取body中的参数
|
||||
body := c.Request().Body()
|
||||
var data chatbot.BotCallbackDataModel
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return fmt.Errorf("invalid body: %v", err)
|
||||
}
|
||||
|
||||
// token 校验 ? token 好像没带?
|
||||
|
||||
// 通过机器人ID路由到不同能力
|
||||
switch data.RobotCode {
|
||||
case s.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling:
|
||||
// 问题处理群机器人
|
||||
// err := s.issueHandling(data)
|
||||
err := s.callbackBiz.IssueHandlingGroup(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IssueHandlingGroup failed: %v", err)
|
||||
}
|
||||
default:
|
||||
// 其他机器人
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallbackDingtalkCard 处理钉钉卡片回调
|
||||
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-card
|
||||
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
|
||||
func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) error {
|
||||
// 获取body中的参数
|
||||
body := c.Request().Body()
|
||||
|
||||
// HTTP 回调结构与SDK结构体不符,包装结构体
|
||||
tmp := struct {
|
||||
card.CardRequest // 嵌入原结构体
|
||||
UserIdType util.FlexibleType `json:"userIdType"` // 重写type字段
|
||||
}{}
|
||||
if err := json.Unmarshal(body, &tmp); err != nil {
|
||||
return fmt.Errorf("invalid body: %v", err)
|
||||
}
|
||||
// 异常字段覆盖
|
||||
data := tmp.CardRequest
|
||||
data.UserIdType = tmp.UserIdType.Int()
|
||||
if err := json.Unmarshal([]byte(data.Content), &data.CardActionData); err != nil {
|
||||
return fmt.Errorf("invalid content: %v", err)
|
||||
}
|
||||
|
||||
// 非回调类型不处理
|
||||
if data.Type != constants.CardActionCallbackTypeAction {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理卡片回调
|
||||
var resp *card.CardResponse
|
||||
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
|
||||
switch actionId {
|
||||
case "collect_qa":
|
||||
// 问题处理群机器人 QA 收集
|
||||
resp = s.callbackBiz.IssueHandlingCollectQA(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 跳过响应包装
|
||||
c.Locals("skip_response_wrap", true)
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ package services
|
|||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
|
@ -18,7 +20,10 @@ type DingBotService struct {
|
|||
dingTalkBotBiz *biz.DingTalkBotBiz
|
||||
}
|
||||
|
||||
func NewDingBotService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService {
|
||||
func NewDingBotService(
|
||||
config *config.Config,
|
||||
dingTalkBotBiz *biz.DingTalkBotBiz,
|
||||
) *DingBotService {
|
||||
return &DingBotService{
|
||||
config: config,
|
||||
dingTalkBotBiz: dingTalkBotBiz,
|
||||
|
|
@ -140,3 +145,26 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnCardMessageReceived 处理卡片回调
|
||||
func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) {
|
||||
// 非回调类型暂不接受
|
||||
if data.Type != constants.CardActionCallbackTypeAction {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// action 处理 - 这里先只处理第一个匹配的actionId
|
||||
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
|
||||
switch actionId {
|
||||
case constants.CardActionTypeCreateGroup:
|
||||
resp, err = d.dingTalkBotBiz.CreateIssueHandlingGroupAndInit(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return &card.CardResponse{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,10 +93,14 @@ func run() {
|
|||
ollamaService := llm_service.NewOllamaGenerate(client, utils_vllmClient, configConfig, chatHisImpl)
|
||||
// 初始化工具管理器
|
||||
manager := tools.NewManager(configConfig, client)
|
||||
// 初始化钉钉认证客户端
|
||||
oauth2Client, _ := dingtalk.NewOauth2Client(rdb)
|
||||
// 初始化钉钉联系人客户端
|
||||
contactClient, _ := dingtalk.NewContactClient(configConfig)
|
||||
contactClient, _ := dingtalk.NewContactClient(oauth2Client)
|
||||
// 初始化钉钉记事本客户端
|
||||
notableClient, _ := dingtalk.NewNotableClient(configConfig)
|
||||
notableClient, _ := dingtalk.NewNotableClient(oauth2Client)
|
||||
// 初始化钉钉卡片客户端
|
||||
cardClient, _ := dingtalk.NewCardClient(oauth2Client)
|
||||
// 初始化工具注册
|
||||
toolRegis := tools_regis.NewToolsRegis(botToolsImpl)
|
||||
// 初始化机器人聊天历史实现层
|
||||
|
|
@ -120,8 +124,10 @@ func run() {
|
|||
group := qywx.NewGroup(botGroupQywxImpl, qywxAuth)
|
||||
other := qywx.NewOther(qywxAuth)
|
||||
qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
|
||||
groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb)
|
||||
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, botChatHisImpl, impl.NewReportDailyCacheImpl(db), manager, configConfig, sendCardClient, groupConfigBiz)
|
||||
groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, impl.NewReportDailyCacheImpl(db), rdb, manager, cardClient)
|
||||
macro := do.NewMacro(botGroupImpl, impl.NewReportDailyCacheImpl(db))
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue