refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -28,29 +28,34 @@ func newAgentRunService() *agentRunService {
|
||||
|
||||
type agentRunService struct{}
|
||||
|
||||
type BusinessToolMemory struct {
|
||||
ToolCode string
|
||||
Result string
|
||||
}
|
||||
|
||||
type AgentRunMetrics struct {
|
||||
TotalRuns int `json:"totalRuns"`
|
||||
CompletedRuns int `json:"completedRuns"`
|
||||
FailedRuns int `json:"failedRuns"`
|
||||
InterruptedRuns int `json:"interruptedRuns"`
|
||||
CompletionRate float64 `json:"completionRate"`
|
||||
ToolCalls int `json:"toolCalls"`
|
||||
ToolSuccessRate float64 `json:"toolSuccessRate"`
|
||||
AverageSteps float64 `json:"averageSteps"`
|
||||
AverageDurationMS int64 `json:"averageDurationMs"`
|
||||
P95DurationMS int64 `json:"p95DurationMs"`
|
||||
PromptTokens int64 `json:"promptTokens"`
|
||||
CompletionTokens int64 `json:"completionTokens"`
|
||||
HandoffRate float64 `json:"handoffRate"`
|
||||
KnowledgeFallbackRate float64 `json:"knowledgeFallbackRate"`
|
||||
ResumedInterrupts int `json:"resumedInterrupts"`
|
||||
ResolvedInterrupts int `json:"resolvedInterrupts"`
|
||||
InterruptRecoveryRate float64 `json:"interruptRecoveryRate"`
|
||||
ReviewedRuns int `json:"reviewedRuns"`
|
||||
ResolvedRuns int `json:"resolvedRuns"`
|
||||
ResolutionRate float64 `json:"resolutionRate"`
|
||||
UnsupportedEvidenceRuns int `json:"unsupportedEvidenceRuns"`
|
||||
UnsupportedEvidenceRate float64 `json:"unsupportedEvidenceRate"`
|
||||
TotalRuns int `json:"total_runs"`
|
||||
CompletedRuns int `json:"completed_runs"`
|
||||
FailedRuns int `json:"failed_runs"`
|
||||
InterruptedRuns int `json:"interrupted_runs"`
|
||||
CompletionRate float64 `json:"completion_rate"`
|
||||
ToolCalls int `json:"tool_calls"`
|
||||
ToolSuccessRate float64 `json:"tool_success_rate"`
|
||||
AverageSteps float64 `json:"average_steps"`
|
||||
AverageDurationMS int64 `json:"average_duration_ms"`
|
||||
P95DurationMS int64 `json:"p95_duration_ms"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
HandoffRate float64 `json:"handoff_rate"`
|
||||
KnowledgeFallbackRate float64 `json:"knowledge_fallback_rate"`
|
||||
ResumedInterrupts int `json:"resumed_interrupts"`
|
||||
ResolvedInterrupts int `json:"resolved_interrupts"`
|
||||
InterruptRecoveryRate float64 `json:"interrupt_recovery_rate"`
|
||||
ReviewedRuns int `json:"reviewed_runs"`
|
||||
ResolvedRuns int `json:"resolved_runs"`
|
||||
ResolutionRate float64 `json:"resolution_rate"`
|
||||
UnsupportedEvidenceRuns int `json:"unsupported_evidence_runs"`
|
||||
UnsupportedEvidenceRate float64 `json:"unsupported_evidence_rate"`
|
||||
}
|
||||
|
||||
const maxAgentAuditPreviewChars = 4000
|
||||
@@ -86,6 +91,44 @@ func (s *agentRunService) GetLatestStepID(agentRunID int64) int64 {
|
||||
return step.ID
|
||||
}
|
||||
|
||||
func (s *agentRunService) FindRecentBusinessToolMemory(conversationID int64, limit int) []BusinessToolMemory {
|
||||
if limit <= 0 || limit > 10 {
|
||||
limit = 4
|
||||
}
|
||||
runs := repositories.AgentRunRepository.FindRecentByConversationID(sqls.DB(), conversationID, limit)
|
||||
runIDs := make([]int64, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
runIDs = append(runIDs, run.ID)
|
||||
}
|
||||
items := repositories.AgentToolCallRepository.FindByAgentRunIDs(sqls.DB(), runIDs)
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].ID > items[j].ID })
|
||||
result := make([]BusinessToolMemory, 0, limit)
|
||||
seen := make(map[string]struct{}, limit)
|
||||
cutoff := time.Now().Add(-15 * time.Minute)
|
||||
for i := range items {
|
||||
toolCode := strings.TrimSpace(items[i].ToolCode)
|
||||
value := strings.TrimSpace(items[i].ResultPreview)
|
||||
if items[i].Status != "completed" || !strings.HasPrefix(toolCode, "business/") || items[i].CreatedAt.Before(cutoff) {
|
||||
continue
|
||||
}
|
||||
if toolCode == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[toolCode]; exists {
|
||||
continue
|
||||
}
|
||||
seen[toolCode] = struct{}{}
|
||||
result = append(result, BusinessToolMemory{ToolCode: toolCode, Result: value})
|
||||
if len(result) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
for left, right := 0, len(result)-1; left < right; left, right = left+1, right-1 {
|
||||
result[left], result[right] = result[right], result[left]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *agentRunService) GetQualityFeedback(agentRunID int64) *models.AgentRunQualityFeedback {
|
||||
return repositories.AgentRunQualityFeedbackRepository.GetByAgentRunID(sqls.DB(), agentRunID)
|
||||
}
|
||||
@@ -232,7 +275,6 @@ type AgentLoopRunInput struct {
|
||||
AIAgentID int64
|
||||
AgentRevisionID int64
|
||||
SourceMessageID int64
|
||||
WorkflowRunID int64
|
||||
Status string
|
||||
PromptTokens int
|
||||
CompletionTokens int
|
||||
@@ -251,7 +293,6 @@ type AgentLoopRunInput struct {
|
||||
type AgentLoopStepInput struct {
|
||||
StepType string
|
||||
StepCode string
|
||||
WorkflowRunID int64
|
||||
Status string
|
||||
InputPreview string
|
||||
OutputPreview string
|
||||
@@ -269,44 +310,6 @@ type AgentLoopToolCallInput struct {
|
||||
DurationMS int
|
||||
}
|
||||
|
||||
// RecordResume closes or re-interrupts the original Agent Loop parent run,
|
||||
// appends a normalized resume step, and records an optional resumed tool call.
|
||||
func (s *agentRunService) RecordResume(db *gorm.DB, agentRunID, workflowRunID int64, status, replyText string, toolCall *AgentLoopToolCallInput) error {
|
||||
if agentRunID <= 0 {
|
||||
return nil
|
||||
}
|
||||
run := repositories.AgentRunRepository.Get(db, agentRunID)
|
||||
if run == nil {
|
||||
return nil
|
||||
}
|
||||
status = firstNonEmptyString(status, "completed")
|
||||
now := time.Now()
|
||||
if err := repositories.AgentRunRepository.Updates(db, run.ID, map[string]any{
|
||||
"status": status, "ended_at": &now, "error_message": "", "updated_at": now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
step := &models.AgentStep{
|
||||
AgentRunID: run.ID, WorkflowRunID: workflowRunID,
|
||||
StepType: "resume", StepCode: "confirmation_resume", Status: status,
|
||||
InputPreview: "customer confirmation", OutputPreview: sanitizeAgentAuditPreview(replyText),
|
||||
StartedAt: now, EndedAt: &now, CreatedAt: now,
|
||||
}
|
||||
if err := repositories.AgentStepRepository.Create(db, step); err != nil {
|
||||
return err
|
||||
}
|
||||
if toolCall == nil {
|
||||
return nil
|
||||
}
|
||||
return repositories.AgentToolCallRepository.Create(db, &models.AgentToolCall{
|
||||
AgentRunID: run.ID, AgentStepID: step.ID, ToolCode: strings.TrimSpace(toolCall.ToolCode),
|
||||
RiskLevel: strings.TrimSpace(toolCall.RiskLevel), RequireConfirm: toolCall.RequireConfirm,
|
||||
Status: firstNonEmptyString(toolCall.Status, status), ArgumentsPreview: sanitizeAgentAuditPreview(toolCall.ArgumentsPreview),
|
||||
ResultPreview: sanitizeAgentAuditPreview(toolCall.ResultPreview), ErrorMessage: sanitizeAgentAuditPreview(toolCall.ErrorMessage),
|
||||
DurationMS: toolCall.DurationMS, CreatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
// RecordAgentLoopRun writes the Agent Loop parent audit run and its normalized
|
||||
// root step in one transaction owned by the caller.
|
||||
func (s *agentRunService) RecordAgentLoopRun(db *gorm.DB, input AgentLoopRunInput) (int64, error) {
|
||||
@@ -321,7 +324,7 @@ func (s *agentRunService) RecordAgentLoopRun(db *gorm.DB, input AgentLoopRunInpu
|
||||
}
|
||||
run := &models.AgentRun{
|
||||
ConversationID: input.ConversationID, AIAgentID: input.AIAgentID, AgentRevisionID: input.AgentRevisionID,
|
||||
SourceMessageID: input.SourceMessageID, WorkflowRunID: input.WorkflowRunID, Status: status,
|
||||
SourceMessageID: input.SourceMessageID, Status: status,
|
||||
PromptTokens: input.PromptTokens, CompletionTokens: input.CompletionTokens, StartedAt: startedAt, EndedAt: input.EndedAt,
|
||||
ErrorMessage: sanitizeAgentAuditPreview(input.ErrorMessage), TraceData: sanitizeAgentAuditPreview(input.TraceData), CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
@@ -345,7 +348,7 @@ func (s *agentRunService) RecordAgentLoopRun(db *gorm.DB, input AgentLoopRunInpu
|
||||
}
|
||||
for _, extra := range input.AdditionalSteps {
|
||||
extraStep := &models.AgentStep{
|
||||
AgentRunID: run.ID, WorkflowRunID: extra.WorkflowRunID, StepType: strings.TrimSpace(extra.StepType), StepCode: strings.TrimSpace(extra.StepCode),
|
||||
AgentRunID: run.ID, StepType: strings.TrimSpace(extra.StepType), StepCode: strings.TrimSpace(extra.StepCode),
|
||||
Status: firstNonEmptyString(extra.Status, status), InputPreview: sanitizeAgentAuditPreview(extra.InputPreview), OutputPreview: sanitizeAgentAuditPreview(extra.OutputPreview),
|
||||
ErrorMessage: sanitizeAgentAuditPreview(extra.ErrorMessage), StartedAt: startedAt, EndedAt: input.EndedAt, DurationMS: durationMS, CreatedAt: now,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user