refactor: 将客服后端重构为宿主可嵌入模块

- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
t
2026-08-28 22:23:13 +08:00
parent 6845c728f8
commit 18c9354095
377 changed files with 13199 additions and 22881 deletions
+67 -64
View File
@@ -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,
}