fix: 1.增加知识库配置,增加知识库工具方法,增加知识库输出格式化方法 2.优化机器人对话时知识库调用链路,增加配置、常量 3.增加新&旧SDK创建场景群方法 4.增加问题处理群机器人对话唤起QA数据收集组件
This commit is contained in:
parent
7f5947c443
commit
5d58cbc0f6
|
|
@ -169,6 +169,16 @@ default_prompt:
|
||||||
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
|
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
|
||||||
'
|
'
|
||||||
user_prompt: '识别图片内容'
|
user_prompt: '识别图片内容'
|
||||||
|
|
||||||
# 权限配置
|
# 权限配置
|
||||||
permissionConfig:
|
permissionConfig:
|
||||||
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
|
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
|
||||||
|
|
||||||
|
# 知识库配置
|
||||||
|
knowledge_config:
|
||||||
|
base_url: "http://127.0.0.1:9600"
|
||||||
|
tenant_id: "default"
|
||||||
|
mode: "naive"
|
||||||
|
stream: true
|
||||||
|
think: false
|
||||||
|
only_rag: true
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit
|
||||||
return
|
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) {
|
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"ai_scheduler/internal/data/constants"
|
"ai_scheduler/internal/data/constants"
|
||||||
"ai_scheduler/internal/data/impl"
|
"ai_scheduler/internal/data/impl"
|
||||||
"ai_scheduler/internal/data/model"
|
"ai_scheduler/internal/data/model"
|
||||||
|
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||||
"ai_scheduler/internal/domain/workflow/recharge"
|
"ai_scheduler/internal/domain/workflow/recharge"
|
||||||
"ai_scheduler/internal/domain/workflow/runtime"
|
"ai_scheduler/internal/domain/workflow/runtime"
|
||||||
"ai_scheduler/internal/entitys"
|
"ai_scheduler/internal/entitys"
|
||||||
|
|
@ -28,6 +29,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||||
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||||
"github.com/alibabacloud-go/tea/tea"
|
"github.com/alibabacloud-go/tea/tea"
|
||||||
"github.com/coze-dev/coze-go"
|
"github.com/coze-dev/coze-go"
|
||||||
|
|
@ -39,6 +41,7 @@ import (
|
||||||
type GroupConfigBiz struct {
|
type GroupConfigBiz struct {
|
||||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||||
|
botConfigImpl *impl.BotConfigImpl
|
||||||
ossClient *utils_oss.Client
|
ossClient *utils_oss.Client
|
||||||
workflowManager *runtime.Registry
|
workflowManager *runtime.Registry
|
||||||
botTools []model.AiBotTool
|
botTools []model.AiBotTool
|
||||||
|
|
@ -53,6 +56,7 @@ func NewGroupConfigBiz(
|
||||||
tools *tools_regis.ToolRegis,
|
tools *tools_regis.ToolRegis,
|
||||||
ossClient *utils_oss.Client,
|
ossClient *utils_oss.Client,
|
||||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||||
|
botConfigImpl *impl.BotConfigImpl,
|
||||||
workflowManager *runtime.Registry,
|
workflowManager *runtime.Registry,
|
||||||
conf *config.Config,
|
conf *config.Config,
|
||||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||||
|
|
@ -64,6 +68,7 @@ func NewGroupConfigBiz(
|
||||||
botTools: tools.BootTools,
|
botTools: tools.BootTools,
|
||||||
ossClient: ossClient,
|
ossClient: ossClient,
|
||||||
botGroupConfigImpl: botGroupConfigImpl,
|
botGroupConfigImpl: botGroupConfigImpl,
|
||||||
|
botConfigImpl: botConfigImpl,
|
||||||
workflowManager: workflowManager,
|
workflowManager: workflowManager,
|
||||||
conf: conf,
|
conf: conf,
|
||||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||||
|
|
@ -236,7 +241,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
||||||
return nil
|
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 !rec.Match.IsMatch {
|
||||||
if len(rec.Match.Chat) != 0 {
|
if len(rec.Match.Chat) != 0 {
|
||||||
|
|
@ -265,8 +270,7 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize
|
||||||
case constants.TaskTypeCozeWorkflow:
|
case constants.TaskTypeCozeWorkflow:
|
||||||
return g.handleCozeWorkflow(ctx, rec, pointTask)
|
return g.handleCozeWorkflow(ctx, rec, pointTask)
|
||||||
case constants.TaskTypeKnowle: // 知识库V2版本
|
case constants.TaskTypeKnowle: // 知识库V2版本
|
||||||
return g.handleKnowledgeV2(ctx, rec, groupConfig)
|
return g.handleKnowledgeV2(ctx, rec, groupConfig, callback)
|
||||||
// return g.handleKnowledgeV3(ctx, rec, pointTask)
|
|
||||||
default:
|
default:
|
||||||
return g.otherTask(ctx, rec)
|
return g.otherTask(ctx, rec)
|
||||||
}
|
}
|
||||||
|
|
@ -473,152 +477,69 @@ func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, tota
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleKnowledgeV2 处理知识库V2版本
|
// handleKnowledgeV2 处理知识库V2版本
|
||||||
func (g *GroupConfigBiz) handleKnowledgeV2(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig) (err error) {
|
func (g *GroupConfigBiz) handleKnowledgeV2(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
|
||||||
req := l_request.Request{
|
// 请求知识库工具
|
||||||
Method: "POST",
|
knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig)
|
||||||
Url: "http://127.0.0.1:9600/query",
|
knowledgeResp, err := knowledgeBase.Call(&knowledge_base.ChatRequest{
|
||||||
Headers: map[string]string{
|
TenantID: constants.KnowledgeTenantIdDefault, // 后续动态接参
|
||||||
"Content-Type": "application/json",
|
Query: rec.UserContent.Text,
|
||||||
"X-Tenant-ID": "default",
|
Mode: constants.KnowledgeModeMix,
|
||||||
},
|
Stream: true,
|
||||||
Json: map[string]interface{}{
|
Think: false,
|
||||||
"query": rec.UserContent.Text,
|
})
|
||||||
"mode": "naive",
|
|
||||||
"stream": true,
|
|
||||||
"think": false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
resp, err := req.SendNoParseResponse()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("请求失败,err: %v", err)
|
return fmt.Errorf("请求知识库工具失败,err: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
isRetrieved, err := g.connectAndReadSSE(resp, rec.Ch, true)
|
// 读取知识库SSE数据
|
||||||
|
isRetrieved, err := g.readKnowledgeSSE(knowledgeResp, rec.Ch, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未检索到匹配信息,询问是否拉群
|
// 未检索到匹配信息,询问是否拉群
|
||||||
if !isRetrieved {
|
if !isRetrieved {
|
||||||
// 获取群问题处理人
|
g.shouldCreateIssueHandlingGroup(ctx, rec, groupConfig, callback)
|
||||||
type issueOwnerType struct {
|
return nil
|
||||||
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
|
|
||||||
var userNames []string
|
|
||||||
for _, owner := range issueOwner {
|
|
||||||
userNames = append(userNames, "@"+owner.Name)
|
|
||||||
}
|
|
||||||
issueOwnerStr := strings.Join(userNames, "、")
|
|
||||||
|
|
||||||
// 构建卡片 OutTrackId
|
|
||||||
outTrackId := constants.BuildCardOutTrackId("cidwP24PLZhLVOS2dVIkEawLw==", "ding5wwvnf9hxeyjau7t")
|
|
||||||
|
|
||||||
// 发送钉钉卡片
|
|
||||||
_, err = g.dingtalkCardClient.CreateAndDeliver(dingtalk.AppKey{
|
|
||||||
AppKey: "ding5wwvnf9hxeyjau7t",
|
|
||||||
AppSecret: "FxXVlTzxrKXvJ8h-9uK0s5TjaBfOJSXumpmrHal-NmQAtku9wOPxcss0Af6WHoAK",
|
|
||||||
}, &card_1_0.CreateAndDeliverRequest{
|
|
||||||
CardTemplateId: tea.String("faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"),
|
|
||||||
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_left_link": tea.String(""),
|
|
||||||
"button_right": tea.String("忽略"),
|
|
||||||
"button_right_link": tea.String(""),
|
|
||||||
"action_id": tea.String("create_group"),
|
|
||||||
"button_display": tea.String("true"),
|
|
||||||
"_CARD_DEBUG_TOOL_ENTRY": tea.String("show"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
|
||||||
SupportForward: tea.Bool(false),
|
|
||||||
},
|
|
||||||
OpenSpaceId: tea.String("dtv1.card//im_group.cidwP24PLZhLVOS2dVIkEawLw=="),
|
|
||||||
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
|
|
||||||
RobotCode: tea.String("ding5wwvnf9hxeyjau7t"),
|
|
||||||
Recipients: []*string{
|
|
||||||
tea.String("17415698414368678"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("发送钉钉卡片失败,err: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
// 读取知识库 SSE 数据
|
||||||
}
|
func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (isRetrieved bool, err error) {
|
||||||
|
scanner := bufio.NewScanner(resp)
|
||||||
// 连接 SSE 并读取数据
|
|
||||||
// event: thinking
|
|
||||||
// data: {"text": "1. 上下文检索中...\n"}
|
|
||||||
// event: answer
|
|
||||||
// data: {"text": "根据"}
|
|
||||||
func (g *GroupConfigBiz) connectAndReadSSE(resp *http.Response, channel chan entitys.Response, useParagraphMode bool) (isRetrieved bool, err error) {
|
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
|
||||||
var buffer strings.Builder
|
var buffer strings.Builder
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
// 解析event行
|
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
|
||||||
if strings.HasPrefix(line, "event:") {
|
if err != nil {
|
||||||
eventStr := strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
return false, fmt.Errorf("解析SSE数据失败: %w", err)
|
||||||
if eventStr == "" {
|
}
|
||||||
|
if done {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if delta == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// thinking不输出
|
// 知识库未命中 输出提示后中断
|
||||||
if eventStr == "thinking" {
|
if delta.XRagStatus == constants.KnowledgeRagStatusMiss {
|
||||||
continue
|
var missContent string = "知识库未检测到匹配信息,即将为您创建群聊解决问题。"
|
||||||
}
|
entitys.ResStream(channel, "", missContent)
|
||||||
// system 事件输出
|
|
||||||
if eventStr == "system" {
|
|
||||||
// 未检索到,直接返回
|
|
||||||
dataStr := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
|
||||||
if dataStr != "retrieved" {
|
|
||||||
entitys.ResStream(channel, "", fmt.Sprintf("知识库未检测到匹配信息,即将为您创建群聊解决问题?"))
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
// 推理内容
|
||||||
|
if delta.ReasoningContent != "" {
|
||||||
|
entitys.ResStream(channel, "", delta.ReasoningContent)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 输出内容 - 段落
|
||||||
continue
|
if delta.Content != "" && useParagraphMode {
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 data 行
|
|
||||||
if strings.HasPrefix(line, "data:") {
|
|
||||||
dataStr := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
|
||||||
if dataStr == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var data struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(dataStr), &data); err != nil {
|
|
||||||
log.Errorf("SSE数据解析失败: %v body: %s", err, dataStr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Text != "" {
|
|
||||||
if useParagraphMode {
|
|
||||||
// 存入缓冲区
|
// 存入缓冲区
|
||||||
buffer.WriteString(data.Text)
|
buffer.WriteString(delta.Content)
|
||||||
content := buffer.String()
|
content := buffer.String()
|
||||||
|
|
||||||
// 检查是否有换行符,按段落输出
|
// 检查是否有换行符,按段落输出
|
||||||
|
|
@ -632,14 +553,12 @@ func (g *GroupConfigBiz) connectAndReadSSE(resp *http.Response, channel chan ent
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
buffer.WriteString(remaining)
|
buffer.WriteString(remaining)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// 逐字输出模式:直接发送
|
// 输出内容 - 逐字
|
||||||
entitys.ResStream(channel, "", data.Text)
|
if delta.Content != "" && !useParagraphMode {
|
||||||
|
entitys.ResStream(channel, "", delta.Content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return true, fmt.Errorf("读取SSE流中断: %w", err)
|
return true, fmt.Errorf("读取SSE流中断: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -652,33 +571,67 @@ func (g *GroupConfigBiz) connectAndReadSSE(resp *http.Response, channel chan ent
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleKnowledgeV3 处理知识库V3同步版本
|
// 询问是否创建群聊处理问题
|
||||||
func (g *GroupConfigBiz) handleKnowledgeV3(ctx context.Context, rec *entitys.Recognize, pointTask *model.AiBotTool) (err error) {
|
func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) error {
|
||||||
req := l_request.Request{
|
// 获取群问题处理人
|
||||||
Method: "POST",
|
type issueOwnerType struct {
|
||||||
Url: "http://127.0.0.1:9600/query",
|
UserId string `json:"userid"`
|
||||||
Headers: map[string]string{
|
Name string `json:"name"`
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Tenant-ID": "default",
|
|
||||||
},
|
|
||||||
Json: map[string]interface{}{
|
|
||||||
"query": rec.UserContent.Text,
|
|
||||||
"mode": "naive",
|
|
||||||
"stream": false,
|
|
||||||
"think": false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
resp, err := req.Send()
|
var issueOwner []issueOwnerType
|
||||||
|
if err := json.Unmarshal([]byte(groupConfig.IssueOwner), &issueOwner); err != nil {
|
||||||
|
return fmt.Errorf("解析群问题处理人失败,err: %v", err)
|
||||||
|
}
|
||||||
|
// 合并所有name
|
||||||
|
var userNames []string
|
||||||
|
for _, owner := range issueOwner {
|
||||||
|
userNames = append(userNames, "@"+owner.Name)
|
||||||
|
}
|
||||||
|
issueOwnerStr := strings.Join(userNames, "、")
|
||||||
|
|
||||||
|
botConfig, err := g.botConfigImpl.GetRobotConfig(callback.RobotCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("请求失败,err: %v", err)
|
return fmt.Errorf("获取机器人配置失败,err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := make(map[string]string)
|
// 构建卡片 OutTrackId
|
||||||
if err := json.Unmarshal([]byte(resp.Text), &obj); err != nil {
|
outTrackId := constants.BuildCardOutTrackId(callback.ConversationId, botConfig.ClientId)
|
||||||
return fmt.Errorf("解析响应失败,err: %v", err)
|
|
||||||
|
// 发送钉钉卡片
|
||||||
|
_, err = g.dingtalkCardClient.CreateAndDeliver(dingtalk.AppKey{
|
||||||
|
AppKey: botConfig.ClientId,
|
||||||
|
AppSecret: botConfig.ClientSecret,
|
||||||
|
}, &card_1_0.CreateAndDeliverRequest{
|
||||||
|
CardTemplateId: tea.String(constants.DingtalkCardTplCreateGroupApprove),
|
||||||
|
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)),
|
||||||
|
// "_CARD_DEBUG_TOOL_ENTRY": tea.String("show"), // debug字段
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: []*string{
|
||||||
|
tea.String(callback.SenderStaffId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("发送钉钉卡片失败,err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entitys.ResText(rec.Ch, "", obj["response"])
|
return nil
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ type Config struct {
|
||||||
Oss Oss `mapstructure:"oss"`
|
Oss Oss `mapstructure:"oss"`
|
||||||
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
||||||
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
||||||
|
KnowledgeConfig KnowledgeConfig `mapstructure:"knowledge_config"`
|
||||||
LLM LLM `mapstructure:"llm"`
|
LLM LLM `mapstructure:"llm"`
|
||||||
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
||||||
Qywx QywxConfig `mapstructure:"qywx"`
|
Qywx QywxConfig `mapstructure:"qywx"`
|
||||||
|
|
@ -253,6 +254,22 @@ type PermissionConfig struct {
|
||||||
PermissionURL string `mapstructure:"permission_url"`
|
PermissionURL string `mapstructure:"permission_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KnowledgeConfig 知识库配置
|
||||||
|
type KnowledgeConfig struct {
|
||||||
|
// 知识库地址
|
||||||
|
BaseURL string `mapstructure:"base_url"`
|
||||||
|
// 默认租户ID
|
||||||
|
TenantID string `mapstructure:"tenant_id"`
|
||||||
|
// 模式
|
||||||
|
Mode string `mapstructure:"mode"`
|
||||||
|
// 是否流式
|
||||||
|
Stream bool `mapstructure:"stream"`
|
||||||
|
// 是否思考
|
||||||
|
Think bool `mapstructure:"think"`
|
||||||
|
// 是否仅RAG
|
||||||
|
OnlyRAG bool `mapstructure:"only_rag"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig 加载配置
|
// LoadConfig 加载配置
|
||||||
func LoadConfig(configPath string) (*Config, error) {
|
func LoadConfig(configPath string) (*Config, error) {
|
||||||
viper.SetConfigFile(configPath)
|
viper.SetConfigFile(configPath)
|
||||||
|
|
|
||||||
|
|
@ -116,3 +116,18 @@ func ParseCardOutTrackId(outTrackId string) (spaceId string, botId string) {
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dingtalk 卡片模板
|
||||||
|
const (
|
||||||
|
DingtalkCardTplBaseMsg string = "291468f8-a048-4132-a37e-a14365e855e9.schema" // 基础消息卡片(title + content)
|
||||||
|
DingtalkCardTplCreateGroupApprove string = "faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema" // 创建群聊申请
|
||||||
|
)
|
||||||
|
|
||||||
|
// dingtalk 模板群相关
|
||||||
|
const (
|
||||||
|
// 群模板id
|
||||||
|
GroupTemplateIdIssueHandling string = "420089e3-b0fb-40f5-89d2-ec47223bff3b" // 问题处理群模板id
|
||||||
|
|
||||||
|
// 模板群机器人ID
|
||||||
|
GroupTemplateRobotIdIssueHandling string = "VqgJYpB91j3RnB217690607273471011" // 问题处理群模板机器人ID
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,24 @@ func GetKnowledgeId(caller Caller) KnowledgeId {
|
||||||
}
|
}
|
||||||
return CallerKnowledgeIdMap[caller]
|
return CallerKnowledgeIdMap[caller]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 知识库
|
||||||
|
const (
|
||||||
|
KnowledgeTenantIdDefault = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 知识库模式
|
||||||
|
const (
|
||||||
|
KnowledgeModeBypass = "bypass" // 绕过知识库,直接返回用户输入
|
||||||
|
KnowledgeModeNaive = "naive" // 简单模式,直接返回知识库答案
|
||||||
|
KnowledgeModeLocal = "local" // 本地模式,仅使用本地知识库
|
||||||
|
KnowledgeModeGlobal = "global" // 全局模式,使用全局知识库
|
||||||
|
KnowledgeModeHybrid = "hybrid" // 混合模式,结合本地和全局知识库
|
||||||
|
KnowledgeModeMix = "mix" // 混合模式,结合本地、全局和知识库
|
||||||
|
)
|
||||||
|
|
||||||
|
// 知识库命中状态
|
||||||
|
const (
|
||||||
|
KnowledgeRagStatusHit = "hit" // 知识库命中
|
||||||
|
KnowledgeRagStatusMiss = "miss" // 知识库未命中
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,12 @@ package impl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/data/model"
|
"ai_scheduler/internal/data/model"
|
||||||
|
"ai_scheduler/internal/entitys"
|
||||||
"ai_scheduler/tmpl/dataTemp"
|
"ai_scheduler/tmpl/dataTemp"
|
||||||
"ai_scheduler/utils"
|
"ai_scheduler/utils"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BotConfigImpl struct {
|
type BotConfigImpl struct {
|
||||||
|
|
@ -15,3 +19,22 @@ func NewBotConfigImpl(db *utils.Db) *BotConfigImpl {
|
||||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotConfig)),
|
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})
|
||||||
|
err := b.GetOneBySearchToStrut(&cond, &botConfig)
|
||||||
|
if err != nil {
|
||||||
|
return entitys.DingTalkBot{}, err
|
||||||
|
}
|
||||||
|
// 解出 config
|
||||||
|
var config entitys.DingTalkBot
|
||||||
|
err = json.Unmarshal([]byte(botConfig.BotConfig), &config)
|
||||||
|
if err != nil {
|
||||||
|
return entitys.DingTalkBot{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
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) Call(req *ChatRequest) (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.Stream {
|
||||||
|
req.Stream = c.cfg.Stream // 仅支持流式输出
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package knowledge_base
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"bufio"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCall(t *testing.T) {
|
||||||
|
req := &ChatRequest{
|
||||||
|
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.Call(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,48 @@
|
||||||
|
package knowledge_base
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type openAIChunk struct {
|
||||||
|
Choices []struct {
|
||||||
|
Delta *Delta `json:"delta"`
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package knowledge_base
|
||||||
|
|
||||||
|
type ChatRequest struct {
|
||||||
|
TenantID string // 租户 ID
|
||||||
|
Query string // 查询内容
|
||||||
|
Mode string // 模式,默认 naive 可选:[bypass|naive|local|global|hybrid|mix]
|
||||||
|
Stream bool // 仅支持流式输出
|
||||||
|
Think bool // 是否开启思考模式
|
||||||
|
OnlyRAG bool // 是否仅开启 RAG 模式
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"ai_scheduler/internal/domain/tools/common/excel_generator"
|
"ai_scheduler/internal/domain/tools/common/excel_generator"
|
||||||
"ai_scheduler/internal/domain/tools/common/image_converter"
|
"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_add"
|
||||||
"ai_scheduler/internal/domain/tools/hyt/goods_brand_search"
|
"ai_scheduler/internal/domain/tools/hyt/goods_brand_search"
|
||||||
"ai_scheduler/internal/domain/tools/hyt/goods_category_add"
|
"ai_scheduler/internal/domain/tools/hyt/goods_category_add"
|
||||||
|
|
@ -25,6 +26,7 @@ type Manager struct {
|
||||||
type CommonTools struct {
|
type CommonTools struct {
|
||||||
ExcelGenerator *excel_generator.Client
|
ExcelGenerator *excel_generator.Client
|
||||||
ImageConverter *image_converter.Client
|
ImageConverter *image_converter.Client
|
||||||
|
KnowledgeBase *knowledge_base.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type HytTools struct {
|
type HytTools struct {
|
||||||
|
|
@ -60,6 +62,7 @@ func NewManager(cfg *config.Config) *Manager {
|
||||||
Common: &CommonTools{
|
Common: &CommonTools{
|
||||||
ExcelGenerator: excel_generator.New(),
|
ExcelGenerator: excel_generator.New(),
|
||||||
ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic),
|
ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic),
|
||||||
|
KnowledgeBase: knowledge_base.New(cfg.KnowledgeConfig),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,28 @@ func (c *ImClient) AddRobotToConversation(appKey AppKey, imData *im.AddRobotToCo
|
||||||
|
|
||||||
return *resp.Body.ChatBotUserId, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package dingtalk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -12,6 +13,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/faabiosr/cachego/file"
|
"github.com/faabiosr/cachego/file"
|
||||||
"github.com/fastwego/dingding"
|
"github.com/fastwego/dingding"
|
||||||
|
|
@ -113,7 +115,7 @@ func (c *OldClient) GetAccessToken() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateInternalGroupConversation 创建企业内部群聊
|
// CreateInternalGroupConversation 创建企业内部群聊
|
||||||
func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, groupName string, userIds []string) (chatId, openConversationId string, err error) {
|
func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, accessToken, groupName string, userIds []string) (chatId, openConversationId string, err error) {
|
||||||
body := struct {
|
body := struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
|
@ -130,11 +132,20 @@ func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, groupNa
|
||||||
UserIds: userIds,
|
UserIds: userIds,
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(body)
|
b, _ := json.Marshal(body)
|
||||||
var res []byte
|
|
||||||
res, err = c.do(ctx, http.MethodPost, "/chat/create", b)
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp struct {
|
var resp struct {
|
||||||
Code int `json:"errcode"`
|
Code int `json:"errcode"`
|
||||||
Msg string `json:"errmsg"`
|
Msg string `json:"errmsg"`
|
||||||
|
|
@ -142,7 +153,7 @@ func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, groupNa
|
||||||
OpenConversationId string `json:"openConversationId"`
|
OpenConversationId string `json:"openConversationId"`
|
||||||
ConversationTag int `json:"conversationTag"`
|
ConversationTag int `json:"conversationTag"`
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(res, &resp); err != nil {
|
if err = json.Unmarshal(res.Content, &resp); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.Code != 0 {
|
if resp.Code != 0 {
|
||||||
|
|
@ -152,6 +163,70 @@ func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, groupNa
|
||||||
return resp.ChatId, resp.OpenConversationId, nil
|
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) {
|
func (c *OldClient) GetJoinGroupQrcode(ctx context.Context, chatId, userId string) (string, error) {
|
||||||
body := struct {
|
body := struct {
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ import (
|
||||||
|
|
||||||
type RobotClient struct {
|
type RobotClient struct {
|
||||||
cli *robot.Client
|
cli *robot.Client
|
||||||
|
oauth2Client *Oauth2Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRobotClient() (*RobotClient, error) {
|
func NewRobotClient(oauth2Client *Oauth2Client) (*RobotClient, error) {
|
||||||
cfg := &openapi.Config{
|
cfg := &openapi.Config{
|
||||||
Protocol: tea.String("https"),
|
Protocol: tea.String("https"),
|
||||||
RegionId: tea.String("central"),
|
RegionId: tea.String("central"),
|
||||||
|
|
@ -23,7 +24,7 @@ func NewRobotClient() (*RobotClient, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &RobotClient{cli: c}, nil
|
return &RobotClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendGroupMessagesReq struct {
|
type SendGroupMessagesReq struct {
|
||||||
|
|
@ -34,6 +35,12 @@ type SendGroupMessagesReq struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RobotClient) SendGroupMessages(appKey AppKey, req *SendGroupMessagesReq) (string, error) {
|
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)
|
msgParamBytes, _ := json.Marshal(req.MsgParam)
|
||||||
msgParamJson := string(msgParamBytes)
|
msgParamJson := string(msgParamBytes)
|
||||||
resp, err := c.cli.OrgGroupSendWithOptions(
|
resp, err := c.cli.OrgGroupSendWithOptions(
|
||||||
|
|
@ -43,7 +50,7 @@ func (c *RobotClient) SendGroupMessages(appKey AppKey, req *SendGroupMessagesReq
|
||||||
OpenConversationId: tea.String(req.OpenConversationId),
|
OpenConversationId: tea.String(req.OpenConversationId),
|
||||||
RobotCode: tea.String(req.RobotCode),
|
RobotCode: tea.String(req.RobotCode),
|
||||||
},
|
},
|
||||||
&robot.OrgGroupSendHeaders{XAcsDingtalkAccessToken: tea.String(appKey.AccessToken)},
|
&robot.OrgGroupSendHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||||
&util.RuntimeOptions{},
|
&util.RuntimeOptions{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ var ProviderSetClient = wire.NewSet(
|
||||||
dingtalk.NewOldClient,
|
dingtalk.NewOldClient,
|
||||||
dingtalk.NewContactClient,
|
dingtalk.NewContactClient,
|
||||||
dingtalk.NewNotableClient,
|
dingtalk.NewNotableClient,
|
||||||
// dingtalk.NewRobotClient,
|
dingtalk.NewRobotClient,
|
||||||
dingtalk.NewOauth2Client,
|
dingtalk.NewOauth2Client,
|
||||||
dingtalk.NewCardClient,
|
dingtalk.NewCardClient,
|
||||||
dingtalk.NewImClient,
|
dingtalk.NewImClient,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"ai_scheduler/internal/biz"
|
||||||
"ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
|
"ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"ai_scheduler/internal/data/constants"
|
"ai_scheduler/internal/data/constants"
|
||||||
|
|
@ -11,6 +12,7 @@ import (
|
||||||
"ai_scheduler/internal/pkg"
|
"ai_scheduler/internal/pkg"
|
||||||
"ai_scheduler/internal/pkg/dingtalk"
|
"ai_scheduler/internal/pkg/dingtalk"
|
||||||
"ai_scheduler/internal/pkg/util"
|
"ai_scheduler/internal/pkg/util"
|
||||||
|
"ai_scheduler/internal/pkg/utils_ollama"
|
||||||
"ai_scheduler/internal/tool_callback"
|
"ai_scheduler/internal/tool_callback"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -19,8 +21,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/log"
|
"github.com/gofiber/fiber/v2/log"
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CallbackService 统一回调入口
|
// CallbackService 统一回调入口
|
||||||
|
|
@ -30,17 +36,33 @@ type CallbackService struct {
|
||||||
dingtalkOldClient *dingtalk.OldClient
|
dingtalkOldClient *dingtalk.OldClient
|
||||||
dingtalkContactClient *dingtalk.ContactClient
|
dingtalkContactClient *dingtalk.ContactClient
|
||||||
dingtalkNotableClient *dingtalk.NotableClient
|
dingtalkNotableClient *dingtalk.NotableClient
|
||||||
|
dingtalkCardClient *dingtalk.CardClient
|
||||||
callbackManager callback.Manager
|
callbackManager callback.Manager
|
||||||
|
dingTalkBotBiz *biz.DingTalkBotBiz
|
||||||
|
ollamaClient *utils_ollama.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
ollamaClient *utils_ollama.Client,
|
||||||
|
) *CallbackService {
|
||||||
return &CallbackService{
|
return &CallbackService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
gateway: gateway,
|
gateway: gateway,
|
||||||
dingtalkOldClient: dingtalkOldClient,
|
dingtalkOldClient: dingtalkOldClient,
|
||||||
dingtalkContactClient: dingtalkContactClient,
|
dingtalkContactClient: dingtalkContactClient,
|
||||||
dingtalkNotableClient: dingtalkNotableClient,
|
dingtalkNotableClient: dingtalkNotableClient,
|
||||||
|
dingtalkCardClient: dingtalkCardClient,
|
||||||
callbackManager: callbackManager,
|
callbackManager: callbackManager,
|
||||||
|
dingTalkBotBiz: dingTalkBotBiz,
|
||||||
|
ollamaClient: ollamaClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,22 +389,189 @@ func getString(str, endstr string, start int, msg *string) int {
|
||||||
|
|
||||||
// CallbackDingtalkRobot 钉钉机器人回调
|
// CallbackDingtalkRobot 钉钉机器人回调
|
||||||
func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) {
|
func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) {
|
||||||
// 获取query中的参数
|
// 获取body中的参数
|
||||||
query := c.Request().URI().QueryString()
|
body := c.Request().Body()
|
||||||
str, _ := url.QueryUnescape(string(query))
|
var data chatbot.BotCallbackDataModel
|
||||||
// 转map
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
params := make(map[string]string)
|
return fmt.Errorf("invalid body: %v", err)
|
||||||
for _, param := range strings.Split(str, "&") {
|
|
||||||
kv := strings.Split(param, "=")
|
|
||||||
if len(kv) == 2 {
|
|
||||||
params[kv[0]] = kv[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(body))
|
||||||
|
|
||||||
|
// token 校验 ? token 好像没带?
|
||||||
|
|
||||||
|
// 通过机器人ID路由到不同能力
|
||||||
|
switch data.RobotCode {
|
||||||
|
case constants.GroupTemplateRobotIdIssueHandling:
|
||||||
|
// 问题处理群机器人
|
||||||
|
err := s.issueHandling(c, data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("issueHandling failed: %v", err)
|
||||||
}
|
}
|
||||||
// token 校验
|
default:
|
||||||
token := params["token"]
|
// 其他机器人
|
||||||
if token != "aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T" {
|
return nil
|
||||||
return fmt.Errorf("token not match")
|
}
|
||||||
|
|
||||||
|
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
// defer cancel()
|
||||||
|
|
||||||
|
// 统一初始化请求参数
|
||||||
|
// requireData, err := s.dingTalkBotBiz.InitRequire(ctx, &data)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("初始化请求参数失败: %v", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 这里需要再实现一套HTTP形式的回调,用于处理钉钉群模板机器人的回调
|
||||||
|
// 主程等待处理结果
|
||||||
|
// resChan := make(chan string, 10)
|
||||||
|
// defer close(resChan)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// issueHandling 问题处理群机器人回调
|
||||||
|
// 能力1: 通过[内容提取] 宏,分析用户QA问题,调出QA表单卡片
|
||||||
|
// 能力2: 通过[QA收集] 宏,收集用户反馈,写入知识库
|
||||||
|
func (s *CallbackService) issueHandling(c *fiber.Ctx, data chatbot.BotCallbackDataModel) error {
|
||||||
|
// 宏解析
|
||||||
|
if strings.Contains(data.Text.Content, "[内容提取]") {
|
||||||
|
s.issueHandlingExtractContent(data)
|
||||||
|
}
|
||||||
|
if strings.Contains(data.Text.Content, "[QA收集]") {
|
||||||
|
s.issueHandlingCollectQA()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 问题处理群机器人内容提取
|
||||||
|
func (s *CallbackService) issueHandlingExtractContent(data chatbot.BotCallbackDataModel) {
|
||||||
|
systemPrompt := `你是一个【问题与答案生成助手】。
|
||||||
|
|
||||||
|
你的职责是:
|
||||||
|
- 分析用户输入的内容
|
||||||
|
- 识别其中隐含或明确的问题
|
||||||
|
- 基于输入内容本身,生成对应的问题与答案
|
||||||
|
|
||||||
|
当用户输入为【多条群聊聊天记录】时:
|
||||||
|
- 结合问题主题,判断聊天记录中正在讨论或试图解决的问题
|
||||||
|
- 一个群聊中可能包含多个相互独立的问题,但它们都围绕着一个主题,尽可能总结为一个问题
|
||||||
|
- 若确实问题很独立,需要分别识别,对每个问题,整理出清晰、可复用的“问题描述”和“对应答案”
|
||||||
|
|
||||||
|
生成答案时的原则:
|
||||||
|
- 答案必须来源于聊天内容中已经给出的信息或共识
|
||||||
|
- 不要引入外部知识,不要使用聊天记录中真实人名或敏感信息,适当总结
|
||||||
|
- 若聊天中未形成明确答案,应明确标记为“暂无明确结论”
|
||||||
|
- 若存在多种不同观点,应分别列出,不要擅自合并或裁决
|
||||||
|
|
||||||
|
【JSON 输出原则】:
|
||||||
|
- 你的最终输出必须是**合法的 JSON**
|
||||||
|
- 不得输出任何额外解释性文字
|
||||||
|
- JSON 结构必须严格符合以下约定
|
||||||
|
|
||||||
|
JSON 结构约定:
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"question": "清晰、独立、可复用的问题描述",
|
||||||
|
"answer": "基于聊天内容整理出的答案;如无结论则为“暂无明确结论”",
|
||||||
|
"confidence": "high | medium | low"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
- items:问题与答案列表;若未识别到有效问题,则返回空数组 []
|
||||||
|
- question:抽象后的标准问题表述,不包含具体聊天语句
|
||||||
|
- answer:整理后的答案,不得引入聊天之外的信息
|
||||||
|
- confidence:根据聊天中信息的一致性和明确程度给出判断
|
||||||
|
|
||||||
|
如果无法从输入中识别出任何有效问题,返回:
|
||||||
|
{ "items": [] }
|
||||||
|
|
||||||
|
问题主题:
|
||||||
|
%s
|
||||||
|
|
||||||
|
用户输入:
|
||||||
|
%s
|
||||||
|
`
|
||||||
|
|
||||||
|
prompt := fmt.Sprintf(systemPrompt, "紧急加款,提示当前账户为离线账户,请输入银行流水号", data.Text.Content)
|
||||||
|
|
||||||
|
fmt.Println("prompt:", prompt)
|
||||||
|
|
||||||
|
generateResp, err := s.ollamaClient.Generation(context.Background(), &api.GenerateRequest{
|
||||||
|
Model: s.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
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// 调用卡片
|
||||||
|
// 构建卡片 OutTrackId
|
||||||
|
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode)
|
||||||
|
|
||||||
|
_, err = s.dingtalkCardClient.CreateAndDeliver(
|
||||||
|
dingtalk.AppKey{
|
||||||
|
AppKey: "ding5wwvnf9hxeyjau7t",
|
||||||
|
AppSecret: "FxXVlTzxrKXvJ8h-9uK0s5TjaBfOJSXumpmrHal-NmQAtku9wOPxcss0Af6WHoAK",
|
||||||
|
},
|
||||||
|
&card_1_0.CreateAndDeliverRequest{
|
||||||
|
CardTemplateId: tea.String("3a447814-6a3e-4a02-b48a-92c57b349d77.schema"),
|
||||||
|
OutTrackId: tea.String(outTrackId),
|
||||||
|
CallbackType: tea.String("HTTP"),
|
||||||
|
CallbackRouteKey: tea.String("gateway.dev.cdlsxd.cn-dingtalk-robot"),
|
||||||
|
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("show"), // debug字段
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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(constants.GroupTemplateRobotIdIssueHandling),
|
||||||
|
Recipients: []*string{
|
||||||
|
tea.String(data.SenderStaffId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 问题处理群机器人 QA 收集
|
||||||
|
func (s *CallbackService) issueHandlingCollectQA() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"ai_scheduler/internal/pkg/dingtalk"
|
"ai_scheduler/internal/pkg/dingtalk"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -18,7 +17,6 @@ import (
|
||||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
|
"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/chatbot"
|
||||||
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
"github.com/alibabacloud-go/dingtalk/card_1_0"
|
||||||
"github.com/alibabacloud-go/dingtalk/im_1_0"
|
|
||||||
"github.com/alibabacloud-go/tea/tea"
|
"github.com/alibabacloud-go/tea/tea"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
|
@ -30,6 +28,7 @@ type DingBotService struct {
|
||||||
dingTalkOld *dingtalk.OldClient
|
dingTalkOld *dingtalk.OldClient
|
||||||
dingtalkCardClient *dingtalk.CardClient
|
dingtalkCardClient *dingtalk.CardClient
|
||||||
dingtalkImClient *dingtalk.ImClient
|
dingtalkImClient *dingtalk.ImClient
|
||||||
|
dingtalkOauth2Client *dingtalk.Oauth2Client
|
||||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||||
botGroupImpl *impl.BotGroupImpl
|
botGroupImpl *impl.BotGroupImpl
|
||||||
botConfigImpl *impl.BotConfigImpl
|
botConfigImpl *impl.BotConfigImpl
|
||||||
|
|
@ -41,6 +40,7 @@ func NewDingBotService(
|
||||||
dingTalkOld *dingtalk.OldClient,
|
dingTalkOld *dingtalk.OldClient,
|
||||||
dingtalkCardClient *dingtalk.CardClient,
|
dingtalkCardClient *dingtalk.CardClient,
|
||||||
dingtalkImClient *dingtalk.ImClient,
|
dingtalkImClient *dingtalk.ImClient,
|
||||||
|
dingtalkOauth2Client *dingtalk.Oauth2Client,
|
||||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||||
botGroupImpl *impl.BotGroupImpl,
|
botGroupImpl *impl.BotGroupImpl,
|
||||||
botConfigImpl *impl.BotConfigImpl,
|
botConfigImpl *impl.BotConfigImpl,
|
||||||
|
|
@ -51,6 +51,7 @@ func NewDingBotService(
|
||||||
dingTalkOld: dingTalkOld,
|
dingTalkOld: dingTalkOld,
|
||||||
dingtalkCardClient: dingtalkCardClient,
|
dingtalkCardClient: dingtalkCardClient,
|
||||||
dingtalkImClient: dingtalkImClient,
|
dingtalkImClient: dingtalkImClient,
|
||||||
|
dingtalkOauth2Client: dingtalkOauth2Client,
|
||||||
botGroupConfigImpl: botGroupConfigImpl,
|
botGroupConfigImpl: botGroupConfigImpl,
|
||||||
botGroupImpl: botGroupImpl,
|
botGroupImpl: botGroupImpl,
|
||||||
botConfigImpl: botConfigImpl,
|
botConfigImpl: botConfigImpl,
|
||||||
|
|
@ -180,9 +181,6 @@ func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.C
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 卡片同步回调超时时间为2s,2s内同步返回,2s后异步更新卡片
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
// action 处理 - 这里先只处理第一个匹配的actionId
|
// action 处理 - 这里先只处理第一个匹配的actionId
|
||||||
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
|
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
|
||||||
switch actionId {
|
switch actionId {
|
||||||
|
|
@ -197,78 +195,18 @@ func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.C
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新群分享链接
|
// 创建群聊及群初始化(ws中,直接协程)
|
||||||
newGroupShareLink := ""
|
|
||||||
timeOutLimit := 1500 * time.Millisecond
|
|
||||||
|
|
||||||
// 钉钉appKey
|
|
||||||
appKey := dingtalk.AppKey{}
|
|
||||||
|
|
||||||
if data.CardActionData.CardPrivateData.Params["status"] == "confirm" {
|
if data.CardActionData.CardPrivateData.Params["status"] == "confirm" {
|
||||||
// 创建群聊 - 这里用的是“统一登录平台”这个应用的接口
|
go func() {
|
||||||
// 不是很关心成功失败,ws中,后续考虑协程去创建
|
err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds)
|
||||||
chatId, openConversationId, err := d.dingTalkOld.CreateInternalGroupConversation(ctx, "问题处理群", userIds)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("创建群聊失败: %v", err)
|
log.Printf("创建群聊及群初始化失败: %v", err)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
_ = openConversationId
|
|
||||||
// 获取机器人配置
|
|
||||||
var botConfig model.AiBotConfig
|
|
||||||
cond := builder.NewCond().And(builder.Eq{"robot_code": botId})
|
|
||||||
err = d.botConfigImpl.GetOneBySearchToStrut(&cond, &botConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 解出 config
|
|
||||||
var config entitys.DingTalkBot
|
|
||||||
err = json.Unmarshal([]byte(botConfig.BotConfig), &config)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("配置解析失败 %v", err.Error())
|
|
||||||
}
|
|
||||||
appKey = dingtalk.AppKey{
|
|
||||||
AppKey: config.ClientId,
|
|
||||||
AppSecret: config.ClientSecret,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加当前机器人到新群
|
|
||||||
_, err = d.dingtalkImClient.AddRobotToConversation(
|
|
||||||
appKey,
|
|
||||||
&im_1_0.AddRobotToConversationRequest{
|
|
||||||
OpenConversationId: tea.String(openConversationId),
|
|
||||||
RobotCode: tea.String(botId),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("添加机器人到会话失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回新群分享链接,直接进群
|
|
||||||
newGroupShareLink, err = d.dingTalkOld.GetJoinGroupQrcode(ctx, chatId, data.UserId)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("获取入群二维码失败: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endTime := time.Now()
|
|
||||||
if endTime.Sub(startTime) > timeOutLimit {
|
|
||||||
// 异步更新卡片
|
|
||||||
d.dingtalkCardClient.UpdateCard(appKey, &card_1_0.UpdateCardRequest{
|
|
||||||
OutTrackId: tea.String(data.OutTrackId),
|
|
||||||
CardData: &card_1_0.UpdateCardRequestCardData{
|
|
||||||
CardParamMap: map[string]*string{
|
|
||||||
"button_display": tea.String("false"),
|
|
||||||
"new_group_share_link": tea.String(newGroupShareLink),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CardUpdateOptions: &card_1_0.UpdateCardRequestCardUpdateOptions{
|
|
||||||
UpdateCardDataByKey: tea.Bool(true),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建关闭创建群组卡片按钮的响应
|
// 构建关闭创建群组卡片按钮的响应
|
||||||
resp = d.buildCreateGroupCardResp(newGroupShareLink)
|
resp = d.buildCreateGroupCardResp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -312,13 +250,139 @@ func (d *DingBotService) buildNewGroupUserIds(ctx context.Context, spaceId, botI
|
||||||
return userIds, nil
|
return userIds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createIssueHandlingGroupAndInit 创建问题处理群聊及群初始化
|
||||||
|
func (d *DingBotService) createIssueHandlingGroupAndInit(ctx context.Context, callbackParams map[string]any, spaceId, botId string, userIds []string) error {
|
||||||
|
// 获取机器人配置
|
||||||
|
botConfig, err := d.botConfigImpl.GetRobotConfig(botId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
appKey := dingtalk.AppKey{
|
||||||
|
AppKey: botConfig.ClientId,
|
||||||
|
AppSecret: botConfig.ClientSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 access_token
|
||||||
|
accessToken, err := d.dingtalkOauth2Client.GetAccessToken(appKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appKey.AccessToken = accessToken
|
||||||
|
|
||||||
|
// 创建群聊
|
||||||
|
_, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, spaceId, botId, 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)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 初始化群聊
|
||||||
|
// 1.开场白
|
||||||
|
|
||||||
|
// 构建卡片 OutTrackId
|
||||||
|
outTrackId := constants.BuildCardOutTrackId(openConversationId, constants.GroupTemplateRobotIdIssueHandling)
|
||||||
|
|
||||||
|
// 群主题
|
||||||
|
groupScope := callbackParams["group_scope"].(string)
|
||||||
|
|
||||||
|
_, err = d.dingtalkCardClient.CreateAndDeliver(
|
||||||
|
appKey,
|
||||||
|
&card_1_0.CreateAndDeliverRequest{
|
||||||
|
CardTemplateId: tea.String(constants.DingtalkCardTplBaseMsg),
|
||||||
|
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(constants.GroupTemplateRobotIdIssueHandling),
|
||||||
|
AtUserIds: map[string]*string{
|
||||||
|
"@ALL": tea.String("@ALL"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. 机器人能力
|
||||||
|
// 构建卡片 OutTrackId
|
||||||
|
outTrackId = constants.BuildCardOutTrackId(openConversationId, constants.GroupTemplateRobotIdIssueHandling)
|
||||||
|
_, err = d.dingtalkCardClient.CreateAndDeliver(
|
||||||
|
appKey,
|
||||||
|
&card_1_0.CreateAndDeliverRequest{
|
||||||
|
CardTemplateId: tea.String(constants.DingtalkCardTplBaseMsg),
|
||||||
|
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知识收集 (@机器人 [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(constants.GroupTemplateRobotIdIssueHandling),
|
||||||
|
AtUserIds: map[string]*string{
|
||||||
|
"@ALL": tea.String("@ALL"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createGroupV1 创建普通内部群会话
|
||||||
|
// 这里用的是“统一登录平台”这个应用的接口加入群聊 - 这里用的是“统一登录平台”这个应用的接口
|
||||||
|
func (d *DingBotService) createIssueHandlingGroup(ctx context.Context, accessToken, spaceId, botId string, userIds []string) (chatId, openConversationId string, err error) {
|
||||||
|
// 是否使用模板群开关
|
||||||
|
var useTemplateGroup bool = true
|
||||||
|
|
||||||
|
// 创建内部群会话
|
||||||
|
if !useTemplateGroup {
|
||||||
|
return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, "问题处理群", userIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据群模板ID创建群
|
||||||
|
if useTemplateGroup {
|
||||||
|
return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, "问题处理群", userIds, constants.GroupTemplateIdIssueHandling)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// buildCreateGroupCardResp 构建关闭创建群组卡片按钮
|
// buildCreateGroupCardResp 构建关闭创建群组卡片按钮
|
||||||
func (d *DingBotService) buildCreateGroupCardResp(newGroupShareLink string) *card.CardResponse {
|
func (d *DingBotService) buildCreateGroupCardResp() *card.CardResponse {
|
||||||
return &card.CardResponse{
|
return &card.CardResponse{
|
||||||
CardData: &card.CardDataDto{
|
CardData: &card.CardDataDto{
|
||||||
CardParamMap: map[string]string{
|
CardParamMap: map[string]string{
|
||||||
"button_display": "false",
|
"button_display": "false",
|
||||||
"new_group_share_link": newGroupShareLink,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CardUpdateOptions: &card.CardUpdateOptions{
|
CardUpdateOptions: &card.CardUpdateOptions{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue