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
-36
View File
@@ -103,34 +103,6 @@ var (
Appendix: i18nx.Get("tool.graph.analyzeConversation.appendix"),
AppendixKey: "tool.graph.analyzeConversation.appendix",
}
GraphPrepareTicketDraft = ToolSpec{
Code: "graph/prepare_ticket_draft",
ServerCode: "graph",
Name: "prepare_ticket_draft",
Title: i18nx.Get("tool.graph.prepareTicketDraft.title"),
TitleKey: "tool.graph.prepareTicketDraft.title",
Description: i18nx.Get("tool.graph.prepareTicketDraft.description"),
DescriptionKey: "tool.graph.prepareTicketDraft.description",
SourceType: enums.ToolSourceTypeGraph,
DirectAccess: true,
RuntimeStatic: true,
Appendix: i18nx.Get("tool.graph.prepareTicketDraft.appendix"),
AppendixKey: "tool.graph.prepareTicketDraft.appendix",
}
GraphCreateTicketConfirm = ToolSpec{
Code: "graph/create_ticket_with_confirmation",
ServerCode: "graph",
Name: "create_ticket_with_confirmation",
Title: i18nx.Get("tool.graph.createTicketConfirm.title"),
TitleKey: "tool.graph.createTicketConfirm.title",
Description: i18nx.Get("tool.graph.createTicketConfirm.description"),
DescriptionKey: "tool.graph.createTicketConfirm.description",
SourceType: enums.ToolSourceTypeGraph,
RuntimeStatic: true,
Aliases: []string{"builtin/create_ticket_with_confirmation"},
Appendix: i18nx.Get("tool.graph.createTicketConfirm.appendix"),
AppendixKey: "tool.graph.createTicketConfirm.appendix",
}
GraphHandoffConversation = ToolSpec{
Code: "graph/handoff_to_human",
ServerCode: "graph",
@@ -151,8 +123,6 @@ var (
BuiltinKnowledgeRetrieve,
GraphTriageServiceRequest,
GraphAnalyzeConversation,
GraphPrepareTicketDraft,
GraphCreateTicketConfirm,
GraphHandoffConversation,
}
)
@@ -401,15 +371,9 @@ func IsImpliedAllowedToolCode(toolCode string, allowedToolCodes map[string]struc
}
switch toolCode {
case GraphTriageServiceRequest.Code, GraphAnalyzeConversation.Code:
if _, ok := allowedToolCodes[GraphCreateTicketConfirm.Code]; ok {
return true
}
if _, ok := allowedToolCodes[GraphHandoffConversation.Code]; ok {
return true
}
case GraphPrepareTicketDraft.Code:
_, ok := allowedToolCodes[GraphCreateTicketConfirm.Code]
return ok
}
return false
}
+6 -35
View File
@@ -1,23 +1,19 @@
package toolx
import (
"testing"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
)
import "testing"
func TestResolveToolMetadata(t *testing.T) {
item := ResolveToolMetadata("builtin/create_ticket_with_confirmation", "")
if item.ToolCode != GraphCreateTicketConfirm.Code {
item := ResolveToolMetadata(GraphHandoffConversation.Code, "")
if item.ToolCode != GraphHandoffConversation.Code {
t.Fatalf("unexpected tool code: %s", item.ToolCode)
}
if item.ServerCode != GraphCreateTicketConfirm.ServerCode {
if item.ServerCode != GraphHandoffConversation.ServerCode {
t.Fatalf("unexpected server code: %s", item.ServerCode)
}
if item.ToolName != GraphCreateTicketConfirm.Name {
if item.ToolName != GraphHandoffConversation.Name {
t.Fatalf("unexpected tool name: %s", item.ToolName)
}
if item.SourceType != GraphCreateTicketConfirm.SourceType {
if item.SourceType != GraphHandoffConversation.SourceType {
t.Fatalf("unexpected source type: %s", item.SourceType)
}
}
@@ -37,28 +33,3 @@ func TestResolveToolMetadataFallsBackToName(t *testing.T) {
t.Fatalf("unexpected source type: %s", item.SourceType)
}
}
func TestRegisteredToolTextUsesEnglishLocale(t *testing.T) {
title := GetRegisteredToolTitleLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleEnUS)
if title != "Create Ticket With Confirmation" {
t.Fatalf("unexpected english title: %q", title)
}
description := GetRegisteredToolDescriptionLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleEnUS)
want := "Guides ticket creation with parameter preparation, customer confirmation, actual ticket creation, and final result delivery."
if description != want {
t.Fatalf("unexpected english description: %q", description)
}
}
func TestRegisteredToolTextKeepsChineseLocale(t *testing.T) {
title := GetRegisteredToolTitleLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
if title != "创建工单确认流程" {
t.Fatalf("unexpected chinese title: %q", title)
}
description := GetRegisteredToolDescriptionLocale(GraphCreateTicketConfirm.Code, i18nx.LocaleZhCN)
if description != "Graph Tool。处理工单参数准备、用户确认、实际创建工单和结果返回。" {
t.Fatalf("unexpected chinese description: %q", description)
}
}
-51
View File
@@ -1,51 +0,0 @@
package toolx
import (
"strings"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
)
const (
MCPRiskLevelRead = "read"
MCPRiskLevelWrite = "write"
)
type TrustedMCPToolPolicy struct {
ToolCode string
Title string
RiskLevel string
RequireConfirmation bool
}
var trustedMCPToolPolicies = map[string]TrustedMCPToolPolicy{
"system/server_time": {
ToolCode: "system/server_time",
Title: "获取当前时间",
RiskLevel: MCPRiskLevelRead,
RequireConfirmation: false,
},
"system/service_info": {
ToolCode: "system/service_info",
Title: "查看服务信息",
RiskLevel: MCPRiskLevelRead,
RequireConfirmation: false,
},
}
func GetTrustedMCPToolPolicy(toolCode string) (TrustedMCPToolPolicy, bool) {
policy, ok := trustedMCPToolPolicies[NormalizeToolCodeAlias(strings.TrimSpace(toolCode))]
return policy, ok
}
func ApplyTrustedMCPToolPolicy(item request.AIAgentMCPToolRequest) request.AIAgentMCPToolRequest {
policy, ok := GetTrustedMCPToolPolicy(item.ToolCode)
if !ok {
return item
}
item.ToolCode = policy.ToolCode
item.Title = policy.Title
item.RiskLevel = policy.RiskLevel
item.RequireConfirmation = policy.RequireConfirmation
return item
}
-98
View File
@@ -1,98 +0,0 @@
package toolx
import (
"encoding/json"
"strings"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
)
func BuildMCPToolCode(serverCode, toolName string) string {
serverCode = strings.TrimSpace(serverCode)
toolName = strings.TrimSpace(toolName)
if serverCode == "" || toolName == "" {
return ""
}
return serverCode + "/" + toolName
}
func SplitMCPToolCode(toolCode string) (string, string) {
toolCode = strings.TrimSpace(toolCode)
if toolCode == "" {
return "", ""
}
idx := strings.Index(toolCode, "/")
if idx <= 0 || idx >= len(toolCode)-1 {
return "", ""
}
return strings.TrimSpace(toolCode[:idx]), strings.TrimSpace(toolCode[idx+1:])
}
func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgentMCPToolRequest, error) {
toolCode := strings.TrimSpace(item.ToolCode)
toolCode = NormalizeToolCodeAlias(toolCode)
serverCode := strings.TrimSpace(item.ServerCode)
toolName := strings.TrimSpace(item.ToolName)
if toolCode != "" {
parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode)
if parsedServerCode != "" && parsedToolName != "" {
if serverCode != "" && !strings.EqualFold(serverCode, parsedServerCode) {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0016")
}
if toolName != "" && !strings.EqualFold(toolName, parsedToolName) {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0017")
}
serverCode = parsedServerCode
toolName = parsedToolName
} else {
serverCode = ""
toolName = ""
}
} else {
toolCode = BuildMCPToolCode(serverCode, toolName)
}
if toolCode == "" {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0019")
}
if parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
serverCode = parsedServerCode
toolName = parsedToolName
}
if serverCode == "" && toolName == "" && strings.Contains(toolCode, "/") && !strings.HasPrefix(toolCode, "builtin/") {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0018")
}
ret := request.AIAgentMCPToolRequest{
ToolCode: toolCode,
ServerCode: serverCode,
ToolName: toolName,
Title: strings.TrimSpace(item.Title),
Description: strings.TrimSpace(item.Description),
RiskLevel: strings.ToLower(strings.TrimSpace(item.RiskLevel)),
RequireConfirmation: item.RequireConfirmation,
}
if len(item.Arguments) > 0 {
ret.Arguments = make(map[string]string, len(item.Arguments))
for key, value := range item.Arguments {
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
continue
}
ret.Arguments[key] = value
}
}
return ret, nil
}
func ParseAgentMCPToolsJSON(raw string) ([]request.AIAgentMCPToolRequest, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
var ret []request.AIAgentMCPToolRequest
if err := json.Unmarshal([]byte(raw), &ret); err != nil {
return nil, err
}
return ret, nil
}