fix: 群聊使用知识库流程基本串通
This commit is contained in:
parent
855156374e
commit
634bca5c60
|
|
@ -6,7 +6,7 @@ server:
|
||||||
ollama:
|
ollama:
|
||||||
base_url: "http://192.168.6.115:11434"
|
base_url: "http://192.168.6.115:11434"
|
||||||
model: "qwen3:8b"
|
model: "qwen3:8b"
|
||||||
generate_model: "qwen3:8b"
|
generate_model: "deepseek-v3.2:cloud"
|
||||||
mapping_model: "qwen3:8b"
|
mapping_model: "qwen3:8b"
|
||||||
vl_model: "qwen2.5vl:7b"
|
vl_model: "qwen2.5vl:7b"
|
||||||
timeout: "120s"
|
timeout: "120s"
|
||||||
|
|
|
||||||
|
|
@ -480,7 +480,7 @@ func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, tota
|
||||||
func (g *GroupConfigBiz) handleKnowledgeV2(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
|
func (g *GroupConfigBiz) handleKnowledgeV2(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
|
||||||
// 请求知识库工具
|
// 请求知识库工具
|
||||||
knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig)
|
knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig)
|
||||||
knowledgeResp, err := knowledgeBase.Call(&knowledge_base.ChatRequest{
|
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
|
||||||
TenantID: constants.KnowledgeTenantIdDefault, // 后续动态接参
|
TenantID: constants.KnowledgeTenantIdDefault, // 后续动态接参
|
||||||
Query: rec.UserContent.Text,
|
Query: rec.UserContent.Text,
|
||||||
Mode: constants.KnowledgeModeMix,
|
Mode: constants.KnowledgeModeMix,
|
||||||
|
|
@ -607,15 +607,15 @@ func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec
|
||||||
CallbackType: tea.String("STREAM"),
|
CallbackType: tea.String("STREAM"),
|
||||||
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
CardData: &card_1_0.CreateAndDeliverRequestCardData{
|
||||||
CardParamMap: map[string]*string{
|
CardParamMap: map[string]*string{
|
||||||
"title": tea.String("创建群聊提醒"),
|
"title": tea.String("创建群聊提醒"),
|
||||||
"content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊:\n\n%s", issueOwnerStr)),
|
"content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊:\n\n%s", issueOwnerStr)),
|
||||||
"remark": tea.String("注:如若无需,忽略即可"),
|
"remark": tea.String("注:如若无需,忽略即可"),
|
||||||
"button_left": tea.String("创建群聊"),
|
"button_left": tea.String("创建群聊"),
|
||||||
"button_right": tea.String("忽略"),
|
"button_right": tea.String("忽略"),
|
||||||
"action_id": tea.String("create_group"),
|
"action_id": tea.String("create_group"),
|
||||||
"button_display": tea.String("true"),
|
"button_display": tea.String("true"),
|
||||||
"group_scope": tea.String(strings.TrimSpace(rec.UserContent.Text)),
|
"group_scope": tea.String(strings.TrimSpace(rec.UserContent.Text)),
|
||||||
// "_CARD_DEBUG_TOOL_ENTRY": tea.String("show"), // debug字段
|
"_CARD_DEBUG_TOOL_ENTRY": tea.String(constants.CardDebugToolEntryShow), // 调试字段
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||||
|
|
|
||||||
|
|
@ -262,8 +262,6 @@ type KnowledgeConfig struct {
|
||||||
TenantID string `mapstructure:"tenant_id"`
|
TenantID string `mapstructure:"tenant_id"`
|
||||||
// 模式
|
// 模式
|
||||||
Mode string `mapstructure:"mode"`
|
Mode string `mapstructure:"mode"`
|
||||||
// 是否流式
|
|
||||||
Stream bool `mapstructure:"stream"`
|
|
||||||
// 是否思考
|
// 是否思考
|
||||||
Think bool `mapstructure:"think"`
|
Think bool `mapstructure:"think"`
|
||||||
// 是否仅RAG
|
// 是否仅RAG
|
||||||
|
|
|
||||||
|
|
@ -131,3 +131,10 @@ const (
|
||||||
// 模板群机器人ID
|
// 模板群机器人ID
|
||||||
GroupTemplateRobotIdIssueHandling string = "VqgJYpB91j3RnB217690607273471011" // 问题处理群模板机器人ID
|
GroupTemplateRobotIdIssueHandling string = "VqgJYpB91j3RnB217690607273471011" // 问题处理群模板机器人ID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 群模板机器人 - 主应用机器人映射
|
||||||
|
var GroupTemplateRobotIdMap = map[string]string{
|
||||||
|
GroupTemplateRobotIdIssueHandling: "ding5wwvnf9hxeyjau7t",
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardDebugToolEntryShow string = "show" // 卡片调试工具 [show:展示 hide:隐藏]
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ func New(cfg config.KnowledgeConfig) *Client {
|
||||||
return &Client{cfg: cfg}
|
return &Client{cfg: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Call(req *ChatRequest) (io.ReadCloser, error) {
|
// 查询知识库
|
||||||
|
func (c *Client) Query(req *QueryRequest) (io.ReadCloser, error) {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return nil, fmt.Errorf("req is nil")
|
return nil, fmt.Errorf("req is nil")
|
||||||
}
|
}
|
||||||
|
|
@ -30,9 +31,6 @@ func (c *Client) Call(req *ChatRequest) (io.ReadCloser, error) {
|
||||||
if req.Mode == "" {
|
if req.Mode == "" {
|
||||||
req.Mode = c.cfg.Mode
|
req.Mode = c.cfg.Mode
|
||||||
}
|
}
|
||||||
if !req.Stream {
|
|
||||||
req.Stream = c.cfg.Stream // 仅支持流式输出
|
|
||||||
}
|
|
||||||
if !req.Think {
|
if !req.Think {
|
||||||
req.Think = c.cfg.Think
|
req.Think = c.cfg.Think
|
||||||
}
|
}
|
||||||
|
|
@ -76,3 +74,39 @@ func (c *Client) Call(req *ChatRequest) (io.ReadCloser, error) {
|
||||||
|
|
||||||
return rsp.Body, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCall(t *testing.T) {
|
func TestCall(t *testing.T) {
|
||||||
req := &ChatRequest{
|
req := &QueryRequest{
|
||||||
TenantID: "admin_test_qa",
|
TenantID: "admin_test_qa",
|
||||||
Query: "lightRAG 的优势?",
|
Query: "lightRAG 的优势?",
|
||||||
Mode: "naive",
|
Mode: "naive",
|
||||||
|
|
@ -18,7 +18,7 @@ func TestCall(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
client := New(config.KnowledgeConfig{BaseURL: "http://127.0.0.1:9600"})
|
client := New(config.KnowledgeConfig{BaseURL: "http://127.0.0.1:9600"})
|
||||||
resp, err := client.Call(req)
|
resp, err := client.Query(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Call failed: %v", err)
|
t.Errorf("Call failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import (
|
||||||
|
|
||||||
type openAIChunk struct {
|
type openAIChunk struct {
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Delta *Delta `json:"delta"`
|
Delta *Delta `json:"delta"`
|
||||||
FinishReason *string `json:"finish_reason"`
|
Message *Message `json:"message"`
|
||||||
|
FinishReason *string `json:"finish_reason"`
|
||||||
} `json:"choices"`
|
} `json:"choices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +20,12 @@ type Delta struct {
|
||||||
XRagStatus string `json:"x_rag_status"` // rag命中状态 hit|miss
|
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) {
|
func ParseOpenAIStreamData(dataLine string) (delta *Delta, done bool, err error) {
|
||||||
data := strings.TrimSpace(strings.TrimPrefix(dataLine, "data:"))
|
data := strings.TrimSpace(strings.TrimPrefix(dataLine, "data:"))
|
||||||
if data == "" {
|
if data == "" {
|
||||||
|
|
@ -46,3 +53,23 @@ func ParseOpenAIStreamData(dataLine string) (delta *Delta, done bool, err error)
|
||||||
|
|
||||||
return nil, false, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package knowledge_base
|
package knowledge_base
|
||||||
|
|
||||||
type ChatRequest struct {
|
type QueryRequest struct {
|
||||||
TenantID string // 租户 ID
|
TenantID string // 租户 ID
|
||||||
Query string // 查询内容
|
Query string // 查询内容
|
||||||
Mode string // 模式,默认 naive 可选:[bypass|naive|local|global|hybrid|mix]
|
Mode string // 模式,默认 naive 可选:[bypass|naive|local|global|hybrid|mix]
|
||||||
|
|
@ -8,3 +8,8 @@ type ChatRequest struct {
|
||||||
Think bool // 是否开启思考模式
|
Think bool // 是否开启思考模式
|
||||||
OnlyRAG bool // 是否仅开启 RAG 模式
|
OnlyRAG bool // 是否仅开启 RAG 模式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IngestTextRequest struct {
|
||||||
|
TenantID string // 租户 ID
|
||||||
|
Text string // 要注入的文本内容
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,9 @@ import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"ai_scheduler/internal/data/constants"
|
"ai_scheduler/internal/data/constants"
|
||||||
errorcode "ai_scheduler/internal/data/error"
|
errorcode "ai_scheduler/internal/data/error"
|
||||||
|
"ai_scheduler/internal/data/impl"
|
||||||
"ai_scheduler/internal/domain/component/callback"
|
"ai_scheduler/internal/domain/component/callback"
|
||||||
|
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||||
"ai_scheduler/internal/entitys"
|
"ai_scheduler/internal/entitys"
|
||||||
"ai_scheduler/internal/gateway"
|
"ai_scheduler/internal/gateway"
|
||||||
"ai_scheduler/internal/pkg"
|
"ai_scheduler/internal/pkg"
|
||||||
|
|
@ -14,9 +16,11 @@ import (
|
||||||
"ai_scheduler/internal/pkg/util"
|
"ai_scheduler/internal/pkg/util"
|
||||||
"ai_scheduler/internal/pkg/utils_ollama"
|
"ai_scheduler/internal/pkg/utils_ollama"
|
||||||
"ai_scheduler/internal/tool_callback"
|
"ai_scheduler/internal/tool_callback"
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -41,6 +45,7 @@ type CallbackService struct {
|
||||||
callbackManager callback.Manager
|
callbackManager callback.Manager
|
||||||
dingTalkBotBiz *biz.DingTalkBotBiz
|
dingTalkBotBiz *biz.DingTalkBotBiz
|
||||||
ollamaClient *utils_ollama.Client
|
ollamaClient *utils_ollama.Client
|
||||||
|
botConfigImpl *impl.BotConfigImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCallbackService(
|
func NewCallbackService(
|
||||||
|
|
@ -53,6 +58,7 @@ func NewCallbackService(
|
||||||
callbackManager callback.Manager,
|
callbackManager callback.Manager,
|
||||||
dingTalkBotBiz *biz.DingTalkBotBiz,
|
dingTalkBotBiz *biz.DingTalkBotBiz,
|
||||||
ollamaClient *utils_ollama.Client,
|
ollamaClient *utils_ollama.Client,
|
||||||
|
botConfigImpl *impl.BotConfigImpl,
|
||||||
) *CallbackService {
|
) *CallbackService {
|
||||||
return &CallbackService{
|
return &CallbackService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
|
@ -64,6 +70,7 @@ func NewCallbackService(
|
||||||
callbackManager: callbackManager,
|
callbackManager: callbackManager,
|
||||||
dingTalkBotBiz: dingTalkBotBiz,
|
dingTalkBotBiz: dingTalkBotBiz,
|
||||||
ollamaClient: ollamaClient,
|
ollamaClient: ollamaClient,
|
||||||
|
botConfigImpl: botConfigImpl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,11 +441,16 @@ func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) {
|
||||||
// issueHandling 问题处理群机器人回调
|
// issueHandling 问题处理群机器人回调
|
||||||
// 能力1: 通过[内容提取] 宏,分析用户QA问题,调出QA表单卡片
|
// 能力1: 通过[内容提取] 宏,分析用户QA问题,调出QA表单卡片
|
||||||
// 能力2: 通过[QA收集] 宏,收集用户反馈,写入知识库
|
// 能力2: 通过[QA收集] 宏,收集用户反馈,写入知识库
|
||||||
|
// 能力3: 通过[知识库查询] 宏,查询知识库,返回答案
|
||||||
func (s *CallbackService) issueHandling(c *fiber.Ctx, data chatbot.BotCallbackDataModel) error {
|
func (s *CallbackService) issueHandling(c *fiber.Ctx, data chatbot.BotCallbackDataModel) error {
|
||||||
// 宏解析
|
// 能力1、2:分析用户QA问题,写入知识库
|
||||||
if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") {
|
if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") {
|
||||||
s.issueHandlingExtractContent(data)
|
s.issueHandlingExtractContent(data)
|
||||||
}
|
}
|
||||||
|
// 能力3:查询知识库,返回答案
|
||||||
|
if strings.Contains(data.Text.Content, "[知识库查询]") {
|
||||||
|
s.issueHandlingQueryKnowledgeBase(data)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -551,7 +563,7 @@ func (s *CallbackService) issueHandlingExtractContent(data chatbot.BotCallbackDa
|
||||||
"textarea_display": tea.String("normal"),
|
"textarea_display": tea.String("normal"),
|
||||||
"action_id": tea.String("collect_qa"),
|
"action_id": tea.String("collect_qa"),
|
||||||
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
|
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
|
||||||
"_CARD_DEBUG_TOOL_ENTRY": tea.String("show"), // debug字段
|
"_CARD_DEBUG_TOOL_ENTRY": tea.String(constants.CardDebugToolEntryShow), // 调试字段
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
|
||||||
|
|
@ -569,6 +581,165 @@ func (s *CallbackService) issueHandlingExtractContent(data chatbot.BotCallbackDa
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 问题处理群机器人查询知识库
|
||||||
|
func (s *CallbackService) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDataModel) {
|
||||||
|
// 获取应用主机器人
|
||||||
|
mainRobotCode := data.RobotCode
|
||||||
|
if robotCode, ok := constants.GroupTemplateRobotIdMap[data.RobotCode]; ok {
|
||||||
|
mainRobotCode = robotCode
|
||||||
|
}
|
||||||
|
// 获取应用机器人配置
|
||||||
|
robotConfig, err := s.botConfigImpl.GetRobotConfig(mainRobotCode)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("应用机器人配置不存在: %s, err: %v", mainRobotCode, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 创建卡片
|
||||||
|
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, mainRobotCode)
|
||||||
|
_, err = s.dingtalkCardClient.CreateAndDeliver(
|
||||||
|
dingtalk.AppKey{
|
||||||
|
AppKey: robotConfig.ClientId,
|
||||||
|
AppSecret: robotConfig.ClientSecret,
|
||||||
|
},
|
||||||
|
&card_1_0.CreateAndDeliverRequest{
|
||||||
|
CardTemplateId: tea.String(constants.DingtalkCardTplBaseMsg),
|
||||||
|
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),
|
||||||
|
Recipients: []*string{
|
||||||
|
tea.String(data.SenderStaffId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 查询知识库
|
||||||
|
knowledgeBase := knowledge_base.New(s.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 = s.dingtalkCardClient.UpdateCard(
|
||||||
|
dingtalk.AppKey{
|
||||||
|
AppKey: robotConfig.ClientId,
|
||||||
|
AppSecret: robotConfig.ClientSecret,
|
||||||
|
},
|
||||||
|
&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
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取知识库 SSE 数据
|
||||||
|
func (s *CallbackService) readKnowledgeSSE(resp io.ReadCloser, channel chan string) (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 = "知识库未检测到匹配信息,即将为您创建群聊解决问题。"
|
||||||
|
channel <- missContent
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
// 推理内容
|
||||||
|
if delta.ReasoningContent != "" {
|
||||||
|
channel <- delta.ReasoningContent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 输出内容 - 段落
|
||||||
|
// 存入缓冲区
|
||||||
|
buffer.WriteString(delta.Content)
|
||||||
|
content := buffer.String()
|
||||||
|
|
||||||
|
// 检查是否有换行符,按段落输出
|
||||||
|
if idx := strings.LastIndex(content, "\n"); idx != -1 {
|
||||||
|
// 发送直到最后一个换行符的内容
|
||||||
|
toSend := content[:idx+1]
|
||||||
|
channel <- toSend
|
||||||
|
|
||||||
|
// 重置缓冲区,保留剩余部分
|
||||||
|
remaining := content[idx+1:]
|
||||||
|
buffer.Reset()
|
||||||
|
buffer.WriteString(remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return true, fmt.Errorf("读取SSE流中断: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送缓冲区剩余内容(仅在段落模式下需要)
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
channel <- buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CallbackDingtalkCard 处理钉钉卡片回调
|
// CallbackDingtalkCard 处理钉钉卡片回调
|
||||||
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-card
|
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-card
|
||||||
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
|
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
|
||||||
|
|
@ -613,21 +784,82 @@ func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// 问题处理群机器人 QA 收集
|
// 问题处理群机器人 QA 收集
|
||||||
func (s *CallbackService) issueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
|
func (s *CallbackService) issueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
|
||||||
if data.CardActionData.CardPrivateData.Params["submit"] != "submit" {
|
// 确认提交,文本写入知识库
|
||||||
// 取消提交,禁用输入框
|
if data.CardActionData.CardPrivateData.Params["submit"] == "submit" {
|
||||||
resp := &card.CardResponse{
|
content := data.CardActionData.CardPrivateData.Params["QA_details"].(string)
|
||||||
CardUpdateOptions: &card.CardUpdateOptions{
|
tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string)
|
||||||
UpdateCardDataByKey: true,
|
|
||||||
},
|
|
||||||
CardData: &card.CardDataDto{
|
|
||||||
CardParamMap: map[string]string{
|
|
||||||
"textarea_display": "disabled",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp
|
// 协程执行耗时操作,防止阻塞
|
||||||
|
util.SafeGo("inject_knowledge_base", func() {
|
||||||
|
knowledgeBase := knowledge_base.New(s.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)
|
||||||
|
|
||||||
|
// 获取主应用机器人(这里可能是群模板机器人)
|
||||||
|
mainRobotId := robotCode
|
||||||
|
if robotCode, ok := constants.GroupTemplateRobotIdMap[robotCode]; ok {
|
||||||
|
mainRobotId = robotCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 robot 配置
|
||||||
|
robotConfig, err := s.botConfigImpl.GetRobotConfig(mainRobotId)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("获取 robot 配置失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送卡片通知用户注入成功
|
||||||
|
outTrackId := constants.BuildCardOutTrackId(conversationId, robotCode)
|
||||||
|
s.dingtalkCardClient.CreateAndDeliver(
|
||||||
|
dingtalk.AppKey{
|
||||||
|
AppKey: robotConfig.ClientId,
|
||||||
|
AppSecret: robotConfig.ClientSecret,
|
||||||
|
},
|
||||||
|
&card_1_0.CreateAndDeliverRequest{
|
||||||
|
CardTemplateId: tea.String(constants.DingtalkCardTplBaseMsg),
|
||||||
|
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),
|
||||||
|
Recipients: []*string{
|
||||||
|
tea.String(data.UserId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// 取消提交,禁用输入框
|
||||||
|
resp := &card.CardResponse{
|
||||||
|
CardUpdateOptions: &card.CardUpdateOptions{
|
||||||
|
UpdateCardDataByKey: true,
|
||||||
|
},
|
||||||
|
CardData: &card.CardDataDto{
|
||||||
|
CardParamMap: map[string]string{
|
||||||
|
"textarea_display": "disabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue