refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user