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
@@ -7,26 +7,26 @@ import (
"strings"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/services"
)
type AnalyzeConversationInput struct {
Goal string `json:"goal"`
ObservedIssue string `json:"observedIssue"`
NeedTicket bool `json:"needTicket"`
NeedHumanHandoff bool `json:"needHumanHandoff"`
NeedQualityCheck bool `json:"needQualityCheck"`
AdditionalContext string `json:"additionalContext"`
ObservedIssue string `json:"observed_issue"`
NeedHumanHandoff bool `json:"need_human_handoff"`
NeedQualityCheck bool `json:"need_quality_check"`
AdditionalContext string `json:"additional_context"`
}
type AnalyzeConversationResult struct {
Summary string `json:"summary"`
UserIntent string `json:"userIntent"`
RiskLevel string `json:"riskLevel"`
RiskSignals []string `json:"riskSignals,omitempty"`
RecommendedNextAction string `json:"recommendedNextAction"`
RecommendedQuestions []string `json:"recommendedQuestions,omitempty"`
ConversationFacts []string `json:"conversationFacts,omitempty"`
UserIntent string `json:"user_intent"`
RiskLevel string `json:"risk_level"`
RiskSignals []string `json:"risk_signals,omitempty"`
RecommendedNextAction string `json:"recommended_next_action"`
RecommendedQuestions []string `json:"recommended_questions,omitempty"`
ConversationFacts []string `json:"conversation_facts,omitempty"`
}
type AnalyzeConversationGraph struct {
@@ -130,9 +130,6 @@ func collectRiskSignals(joined string, input AnalyzeConversationInput) []string
if containsAny(joined, "人工", "转人工", "真人", "客服") || input.NeedHumanHandoff {
add("handoff_requested")
}
if containsAny(joined, "工单", "报障", "售后", "登记", "记录问题") || input.NeedTicket {
add("ticket_expected")
}
if input.NeedQualityCheck {
add("quality_review_requested")
}
@@ -143,8 +140,6 @@ func detectUserIntent(joined string, input AnalyzeConversationInput) string {
switch {
case input.NeedHumanHandoff || containsAny(joined, "人工", "转人工", "真人"):
return "handoff_request"
case input.NeedTicket || containsAny(joined, "工单", "报障", "售后", "登记问题"):
return "ticket_request"
case containsAny(joined, "投诉", "举报", "差评", "赔偿"):
return "complaint"
default:
@@ -171,8 +166,6 @@ func recommendNextAction(intent string, signals []string, input AnalyzeConversat
return "quality_review"
case containsSignal(signals, "handoff_requested") || intent == "handoff_request":
return "handoff_to_human"
case containsSignal(signals, "ticket_expected") || intent == "ticket_request":
return "prepare_ticket"
case containsSignal(signals, "complaint_escalation"):
return "handoff_to_human"
default:
@@ -182,9 +175,6 @@ func recommendNextAction(intent string, signals []string, input AnalyzeConversat
func recommendQuestions(intent string, signals []string, input AnalyzeConversationInput) []string {
questions := make([]string, 0, 3)
if containsSignal(signals, "ticket_expected") && strings.TrimSpace(input.ObservedIssue) == "" {
questions = append(questions, "Please confirm the specific issue, error message, and expected outcome.")
}
if containsSignal(signals, "handoff_requested") {
questions = append(questions, "Please confirm whether the user explicitly requested human support and why the issue needs human handling.")
}
@@ -222,3 +212,36 @@ func containsSignal(signals []string, target string) bool {
}
return false
}
func buildRecentMessageDigest(messages []models.Message) string {
parts := make([]string, 0, len(messages))
for i := range messages {
content := strings.TrimSpace(messages[i].Content)
if content == "" {
continue
}
parts = append(parts, messageSenderLabel(messages[i].SenderType)+""+limitAnalysisText(content, 60))
}
return strings.Join(parts, " | ")
}
func messageSenderLabel(senderType enums.IMSenderType) string {
switch senderType {
case enums.IMSenderTypeCustomer:
return "Customer"
case enums.IMSenderTypeAgent:
return "Agent"
case enums.IMSenderTypeAI:
return "AI"
default:
return "Message"
}
}
func limitAnalysisText(value string, max int) string {
runes := []rune(strings.TrimSpace(value))
if max <= 0 || len(runes) <= max {
return string(runes)
}
return strings.TrimSpace(string(runes[:max])) + "..."
}