diff --git a/go.mod b/go.mod index a9f67ac..d5857a7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/cloudwego/eino v0.8.7 github.com/cloudwego/eino-ext/components/model/openai v0.1.11 + github.com/coreos/go-oidc/v3 v3.18.0 github.com/eino-contrib/jsonschema v1.0.3 github.com/gin-gonic/gin v1.12.0 github.com/glebarez/sqlite v1.11.0 @@ -19,10 +20,8 @@ require ( github.com/mlogclub/codegen v1.0.3 github.com/mlogclub/simple v1.2.40 github.com/modelcontextprotocol/go-sdk v1.4.1 - github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/openai/openai-go/v3 v3.28.0 github.com/panjf2000/ants/v2 v2.12.0 - github.com/pelletier/go-toml/v2 v2.2.4 github.com/qdrant/go-client v1.17.1 github.com/robfig/cron/v3 v3.0.1 github.com/silenceper/wechat/v2 v2.1.12 @@ -32,6 +31,7 @@ require ( github.com/yuin/goldmark v1.4.13 golang.org/x/crypto v0.49.0 golang.org/x/net v0.51.0 + golang.org/x/oauth2 v0.36.0 golang.org/x/text v0.35.0 golang.org/x/tools v0.42.0 gopkg.in/yaml.v3 v3.0.1 @@ -51,7 +51,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/eino-ext/libs/acl/openai v0.1.15 // indirect - github.com/coreos/go-oidc/v3 v3.18.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/evanphx/json-patch v0.5.2 // indirect @@ -83,6 +82,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nikolalohinski/gonja v1.5.3 // indirect github.com/nxadm/tail v1.4.11 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect @@ -109,7 +109,6 @@ require ( golang.org/x/arch v0.22.0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/time v0.14.0 // indirect diff --git a/go.sum b/go.sum index 532b993..056b82e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= @@ -197,8 +195,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= -github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -334,8 +330,6 @@ go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4Len go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/internal/ai/embedding.go b/internal/ai/embedding.go index d94dba0..e88685e 100644 --- a/internal/ai/embedding.go +++ b/internal/ai/embedding.go @@ -32,7 +32,7 @@ func (s *embedding) GetModel(ctx context.Context) (*models.AIConfig, error) { func (s *embedding) GenerateEmbedding(ctx context.Context, text string) (*EmbeddingResult, error) { if text == "" { - return nil, errorsx.InvalidParam("文本内容不能为空") + return nil, errorsx.InvalidParamI18n("error.e0215") } result, err := s.callEmbeddingAPI(ctx, text) @@ -45,7 +45,7 @@ func (s *embedding) GenerateEmbedding(ctx context.Context, text string) (*Embedd func (s *embedding) GenerateBatchEmbeddings(ctx context.Context, texts []string) ([]EmbeddingResult, error) { if len(texts) == 0 { - return nil, errorsx.InvalidParam("文本列表不能为空") + return nil, errorsx.InvalidParamI18n("error.e0216") } results := make([]EmbeddingResult, 0, len(texts)) diff --git a/internal/ai/mcps/client.go b/internal/ai/mcps/client.go index 16192a5..2c3b5d6 100644 --- a/internal/ai/mcps/client.go +++ b/internal/ai/mcps/client.go @@ -2,12 +2,12 @@ package mcps import ( "context" - "fmt" "net/http" "strings" "time" "agent-desk/internal/pkg/errorsx" + "agent-desk/internal/pkg/i18nx" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -52,7 +52,7 @@ func (c *Client) ListTools(ctx context.Context, cfg ServerConfig) ([]ToolInfo, e result, err := session.ListTools(ctx, nil) if err != nil { - return nil, fmt.Errorf("列出 MCP 工具失败: %w", err) + return nil, i18nx.Errorf("error.mcp.listToolsFailed", err) } ret := make([]ToolInfo, 0, len(result.Tools)) for _, tool := range result.Tools { @@ -70,7 +70,7 @@ func (c *Client) ListTools(ctx context.Context, cfg ServerConfig) ([]ToolInfo, e func (c *Client) CallTool(ctx context.Context, cfg ServerConfig, toolName string, arguments map[string]any) (*ToolCallResult, error) { toolName = strings.TrimSpace(toolName) if toolName == "" { - return nil, errorsx.InvalidParam("toolName不能为空") + return nil, errorsx.InvalidParamI18n("error.e0076") } session, closeFn, err := c.connect(ctx, cfg) @@ -84,7 +84,7 @@ func (c *Client) CallTool(ctx context.Context, cfg ServerConfig, toolName string Arguments: arguments, }) if err != nil { - return nil, fmt.Errorf("调用 MCP 工具失败: %w", err) + return nil, i18nx.Errorf("error.mcp.callToolFailed", err) } return &ToolCallResult{ ServerCode: cfg.Code, @@ -97,10 +97,10 @@ func (c *Client) CallTool(ctx context.Context, cfg ServerConfig, toolName string func (c *Client) connect(ctx context.Context, cfg ServerConfig) (*mcp.ClientSession, func(), error) { if strings.TrimSpace(cfg.Code) == "" { - return nil, nil, errorsx.InvalidParam("serverCode不能为空") + return nil, nil, errorsx.InvalidParamI18n("error.e0070") } if strings.TrimSpace(cfg.Endpoint) == "" { - return nil, nil, errorsx.InvalidParam("MCP endpoint不能为空") + return nil, nil, errorsx.InvalidParamI18n("error.e0032") } timeout := time.Duration(cfg.TimeoutMS) * time.Millisecond @@ -128,7 +128,7 @@ func (c *Client) connect(ctx context.Context, cfg ServerConfig) (*mcp.ClientSess session, err := client.Connect(connCtx, transport, nil) if err != nil { cancel() - return nil, nil, fmt.Errorf("连接 MCP Server 失败: %w", err) + return nil, nil, i18nx.Errorf("error.mcp.connectServerFailed", err) } return session, func() { _ = session.Close() diff --git a/internal/ai/mcps/runtime.go b/internal/ai/mcps/runtime.go index 78dd8b2..bffcc14 100644 --- a/internal/ai/mcps/runtime.go +++ b/internal/ai/mcps/runtime.go @@ -39,18 +39,18 @@ func (s *RuntimeService) ListTools(ctx context.Context, serverCode string) ([]To func (s *RuntimeService) resolveServer(serverCode string) (ServerConfig, error) { cfg := config.Current() if !cfg.MCP.Enabled { - return ServerConfig{}, errorsx.InvalidParam("MCP未启用") + return ServerConfig{}, errorsx.InvalidParamI18n("error.e0035") } serverCode = strings.TrimSpace(serverCode) if serverCode == "" { - return ServerConfig{}, errorsx.InvalidParam("serverCode不能为空") + return ServerConfig{}, errorsx.InvalidParamI18n("error.e0070") } server, ok := cfg.MCP.Servers[serverCode] if !ok { - return ServerConfig{}, errorsx.InvalidParam("MCP服务配置不存在") + return ServerConfig{}, errorsx.InvalidParamI18n("error.e0034") } if !server.Enabled { - return ServerConfig{}, errorsx.InvalidParam("MCP服务未启用") + return ServerConfig{}, errorsx.InvalidParamI18n("error.e0033") } return ServerConfig{ Code: serverCode, diff --git a/internal/ai/rag/answer.go b/internal/ai/rag/answer.go index a343807..7ef1b10 100644 --- a/internal/ai/rag/answer.go +++ b/internal/ai/rag/answer.go @@ -25,7 +25,7 @@ var Answer = &answer{} func (s *answer) DebugSearch(ctx context.Context, req request.KnowledgeSearchRequest) (*response.KnowledgeSearchResponse, error) { if strings.TrimSpace(req.Question) == "" { - return nil, errorsx.InvalidParam("问题不能为空") + return nil, errorsx.InvalidParamI18n("error.e0340") } startedAt := time.Now() results, err := s.retrieve(req, ctx) @@ -60,7 +60,7 @@ func (s *answer) DebugSearch(ctx context.Context, req request.KnowledgeSearchReq func (s *answer) DebugAnswer(ctx context.Context, req request.KnowledgeAnswerRequest, operator *dto.AuthPrincipal) (*response.KnowledgeAnswerResponse, error) { if strings.TrimSpace(req.Question) == "" { - return nil, errorsx.InvalidParam("问题不能为空") + return nil, errorsx.InvalidParamI18n("error.e0340") } startedAt := time.Now() @@ -276,7 +276,7 @@ func (s *answer) BuildDocumentIndex(ctx context.Context, documentID int64) error func (s *answer) retrieve(req request.KnowledgeSearchRequest, ctx context.Context) ([]RetrieveResult, error) { if len(normalizeKnowledgeBaseIDs(req.KnowledgeBaseIDs)) == 0 { - return nil, errorsx.InvalidParam("知识库不能为空") + return nil, errorsx.InvalidParamI18n("error.e0284") } knowledgeBases := s.loadKnowledgeBases(req.KnowledgeBaseIDs) diff --git a/internal/ai/runtime/debug_run.go b/internal/ai/runtime/debug_run.go index 8fa4595..1515074 100644 --- a/internal/ai/runtime/debug_run.go +++ b/internal/ai/runtime/debug_run.go @@ -22,16 +22,16 @@ func init() { func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*response.SkillDebugRunResponse, error) { aiAgent := svc.AIAgentService.Get(req.AIAgentID) if aiAgent == nil || aiAgent.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("AI Agent不存在或未启用") + return nil, errorsx.InvalidParamI18n("error.e0007") } aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) if aiConfig == nil { - return nil, errorsx.InvalidParam("AI Agent关联的AI配置不存在") + return nil, errorsx.InvalidParamI18n("error.e0008") } var conversation *models.Conversation if req.ConversationID > 0 { if conversation = svc.ConversationService.Get(req.ConversationID); conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } } else { conversation = &models.Conversation{ID: req.ConversationID, AIAgentID: req.AIAgentID} @@ -57,32 +57,32 @@ func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*resp func DebugResumeSkill(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) { aiAgent := svc.AIAgentService.Get(req.AIAgentID) if aiAgent == nil || aiAgent.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("AI Agent不存在或未启用") + return nil, errorsx.InvalidParamI18n("error.e0007") } aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) if aiConfig == nil { - return nil, errorsx.InvalidParam("AI Agent关联的AI配置不存在") + return nil, errorsx.InvalidParamI18n("error.e0008") } pendingInterrupt := svc.ConversationInterruptService.GetByCheckPointID(strings.TrimSpace(req.CheckPointID)) if pendingInterrupt == nil { - return nil, errorsx.InvalidParam("CheckPoint 不存在") + return nil, errorsx.InvalidParamI18n("error.e0014") } if pendingInterrupt.AIAgentID > 0 && pendingInterrupt.AIAgentID != req.AIAgentID { - return nil, errorsx.InvalidParam("CheckPoint 与 AI Agent 不匹配") + return nil, errorsx.InvalidParamI18n("error.e0015") } conversationID := req.ConversationID if conversationID <= 0 { conversationID = pendingInterrupt.ConversationID } if conversationID <= 0 { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } conversation := svc.ConversationService.Get(conversationID) if conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } if conversation.AIAgentID > 0 && conversation.AIAgentID != req.AIAgentID { - return nil, errorsx.InvalidParam("会话与 AI Agent 不匹配") + return nil, errorsx.InvalidParamI18n("error.e0117") } resumeText := strings.TrimSpace(req.UserMessage) summary, err := Service.Resume(ctx, applicationruntime.ResumeRequest{ diff --git a/internal/ai/runtime/tools/tool_search_tool.go b/internal/ai/runtime/tools/tool_search_tool.go index 2ef2a94..12c6d46 100644 --- a/internal/ai/runtime/tools/tool_search_tool.go +++ b/internal/ai/runtime/tools/tool_search_tool.go @@ -10,6 +10,7 @@ import ( "agent-desk/internal/ai/mcps" impladapter "agent-desk/internal/ai/runtime/internal/impl/adapter" "agent-desk/internal/ai/runtime/registry" + "agent-desk/internal/pkg/i18nx" "agent-desk/internal/pkg/toolx" einotool "github.com/cloudwego/eino/components/tool" @@ -161,10 +162,10 @@ func (t *ToolSearchTool) invokeTargetTool(ctx context.Context, toolCode string, toolCode = strings.TrimSpace(toolCode) serverCode, toolName := toolx.SplitMCPToolCode(toolCode) if serverCode == "" || toolName == "" { - return "", fmt.Errorf("tool_search 只支持调用 MCP toolCode") + return "", i18nx.Errorf("error.e0077") } if !containsToolCode(t.allowedToolCodes, toolCode) { - return "", fmt.Errorf("目标工具未被当前会话授权") + return "", i18nx.Errorf("error.e0279") } result, err := mcps.Runtime.CallTool(ctx, serverCode, toolName, cloneArguments(arguments)) if err != nil { diff --git a/internal/ai/skills/matcher.go b/internal/ai/skills/matcher.go index 73da422..9124151 100644 --- a/internal/ai/skills/matcher.go +++ b/internal/ai/skills/matcher.go @@ -21,7 +21,7 @@ func MatchSkill(execCtx context.Context, ctx RuntimeContext) (*models.SkillDefin if strs.IsNotBlank(ctx.ManualSkillCode) { skill := loader.findManualSkillDefinition(ctx.ManualSkillCode) if skill == nil || skill.Status != enums.StatusOk { - return nil, "", nil, errorsx.InvalidParam("Skill 不存在或未启用") + return nil, "", nil, errorsx.InvalidParamI18n("error.e0054") } return skill, "manual_skill_code", &RouteTrace{ Status: "manual_selected", diff --git a/internal/bootstrap/server.go b/internal/bootstrap/server.go index 3071585..8dd5863 100644 --- a/internal/bootstrap/server.go +++ b/internal/bootstrap/server.go @@ -55,7 +55,7 @@ func NewServer() (*gin.Engine, error) { }, NotFoundPrefixes: notFoundPrefixes, NotFoundHandler: func(ctx *gin.Context) { - httpx.WriteHttpStatusJSON(ctx, http.StatusNotFound, web.JsonErrorCode(http.StatusNotFound, i18nx.T(ctx, "error.notFound", nil))) + httpx.WriteHttpStatusJSON(ctx, http.StatusNotFound, web.JsonErrorCode(http.StatusNotFound, i18nx.T(ctx, "error.notFound"))) }, }) diff --git a/internal/handlers/api/channel_handler.go b/internal/handlers/api/channel_handler.go index 287aee3..28faf49 100644 --- a/internal/handlers/api/channel_handler.go +++ b/internal/handlers/api/channel_handler.go @@ -8,13 +8,12 @@ import ( "agent-desk/internal/services" "github.com/gin-gonic/gin" - "github.com/mlogclub/simple/web" ) func ChannelAnyConfig(ctx *gin.Context) { channel := services.ChannelService.GetEnabledChannel(ctx) if channel == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } cfg, err := resolveWidgetConfig(channel.ChannelType, channel.ConfigJSON) @@ -70,6 +69,6 @@ func resolveWidgetConfig(channelType, rawConfig string) (*webLikeWidgetConfig, e Width: "100%", }, nil default: - return nil, errorsx.InvalidParam("该渠道不支持开放客服配置") + return nil, errorsx.InvalidParamI18n("error.e0313") } } diff --git a/internal/handlers/api/conversation_handler.go b/internal/handlers/api/conversation_handler.go index db039bb..93472e1 100644 --- a/internal/handlers/api/conversation_handler.go +++ b/internal/handlers/api/conversation_handler.go @@ -11,7 +11,6 @@ import ( "agent-desk/internal/pkg/httpx/params" "github.com/gin-gonic/gin" - "github.com/mlogclub/simple/web" ) func ConversationGetBy(ctx *gin.Context) { @@ -20,22 +19,22 @@ func ConversationGetBy(ctx *gin.Context) { return } if services.ChannelService.GetEnabledChannel(ctx) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } item := services.ConversationService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("会话不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116")) return } if !services.ConversationService.IsCustomerConversationOwner(item, *external) { - httpx.WriteJSON(ctx, web.JsonErrorMsg("无权访问该会话")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0222")) return } @@ -49,12 +48,12 @@ func ConversationGetBy(ctx *gin.Context) { func ConversationPostCreate_or_match(ctx *gin.Context) { channel := services.ChannelService.GetEnabledChannel(ctx) if channel == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } @@ -68,12 +67,12 @@ func ConversationPostCreate_or_match(ctx *gin.Context) { func ConversationPostClose(ctx *gin.Context) { if services.ChannelService.GetEnabledChannel(ctx) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } diff --git a/internal/handlers/api/customer_handler.go b/internal/handlers/api/customer_handler.go index cc401c5..18a2b06 100644 --- a/internal/handlers/api/customer_handler.go +++ b/internal/handlers/api/customer_handler.go @@ -6,13 +6,12 @@ import ( "agent-desk/internal/services" "github.com/gin-gonic/gin" - "github.com/mlogclub/simple/web" ) func CustomerPostSession_exchange(ctx *gin.Context) { channel := services.ChannelService.GetEnabledChannel(ctx) if channel == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道不存在或已停用")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0209")) return } externalUser, err := openidentity.GetExternalUser(ctx, services.ChannelService.GetUserTokenSecret(channel)) diff --git a/internal/handlers/api/message_handler.go b/internal/handlers/api/message_handler.go index c3970a3..05cf819 100644 --- a/internal/handlers/api/message_handler.go +++ b/internal/handlers/api/message_handler.go @@ -13,33 +13,32 @@ import ( "agent-desk/internal/pkg/httpx/params" "github.com/gin-gonic/gin" - "github.com/mlogclub/simple/web" "github.com/spf13/cast" ) func MessageAnyList(ctx *gin.Context) { if services.ChannelService.GetEnabledChannel(ctx) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } conversationID, _ := params.GetInt64(ctx, "conversationId") if conversationID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } conversation := services.ConversationService.Get(conversationID) if conversation == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("会话不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116")) return } if !services.ConversationService.IsCustomerConversationOwner(conversation, *external) { - httpx.WriteJSON(ctx, web.JsonErrorMsg("无权访问该会话")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0222")) return } @@ -58,12 +57,12 @@ func MessageAnyList(ctx *gin.Context) { func MessagePostSend(ctx *gin.Context) { if services.ChannelService.GetEnabledChannel(ctx) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } @@ -83,12 +82,12 @@ func MessagePostSend(ctx *gin.Context) { func MessagePostRead(ctx *gin.Context) { if services.ChannelService.GetEnabledChannel(ctx) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } @@ -106,32 +105,32 @@ func MessagePostRead(ctx *gin.Context) { func MessagePostUpload_image(ctx *gin.Context) { if services.ChannelService.GetEnabledChannel(ctx) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId")) if rawConv == "" { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } conversationID, err := strconv.ParseInt(rawConv, 10, 64) if err != nil || conversationID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } conversation := services.ConversationService.Get(conversationID) if conversation == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("会话不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116")) return } if !services.ConversationService.IsCustomerConversationOwner(conversation, *external) { - httpx.WriteJSON(ctx, web.JsonErrorMsg("无权访问该会话")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0222")) return } if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeCustomer, nil, external); err != nil { @@ -141,11 +140,11 @@ func MessagePostUpload_image(ctx *gin.Context) { header, err := ctx.FormFile("file") if err != nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("请选择上传图片")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0322")) return } if !strings.HasPrefix(strings.ToLower(header.Header.Get("Content-Type")), "image/") { - httpx.WriteJSON(ctx, web.JsonErrorMsg("仅支持上传图片文件")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0090")) return } @@ -159,23 +158,23 @@ func MessagePostUpload_image(ctx *gin.Context) { func MessagePostUpload_attachment(ctx *gin.Context) { if services.ChannelService.GetEnabledChannel(ctx) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("接入渠道未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211")) return } external := httpx.GetExternalUser(ctx) if external == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("外部身份未初始化")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150")) return } rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId")) if rawConv == "" { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } conversationID, err := strconv.ParseInt(rawConv, 10, 64) if err != nil || conversationID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeCustomer, nil, external); err != nil { @@ -185,7 +184,7 @@ func MessagePostUpload_attachment(ctx *gin.Context) { header, err := ctx.FormFile("file") if err != nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("请选择上传附件")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0324")) return } item, err := services.AssetService.UploadFile(header, "attachments", nil) diff --git a/internal/handlers/dashboard/agent_handler.go b/internal/handlers/dashboard/agent_handler.go index 19384f2..6dcd17f 100644 --- a/internal/handlers/dashboard/agent_handler.go +++ b/internal/handlers/dashboard/agent_handler.go @@ -55,7 +55,7 @@ func AgentGetBy(ctx *gin.Context) { } item := services.AgentProfileService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("客服档案不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0164")) return } httpx.WriteJSON(ctx, builders.BuildAgentProfileResponse(item)) diff --git a/internal/handlers/dashboard/agent_run_log_handler.go b/internal/handlers/dashboard/agent_run_log_handler.go index 351a2c4..32084e3 100644 --- a/internal/handlers/dashboard/agent_run_log_handler.go +++ b/internal/handlers/dashboard/agent_run_log_handler.go @@ -59,7 +59,7 @@ func AgentRunLogGetBy(ctx *gin.Context) { item := services.AgentRunLogService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Agent 运行日志不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0013")) return } httpx.WriteJSON(ctx, builders.BuildAgentRunLog(item)) diff --git a/internal/handlers/dashboard/agent_team_handler.go b/internal/handlers/dashboard/agent_team_handler.go index 00acae7..b9c8bdc 100644 --- a/internal/handlers/dashboard/agent_team_handler.go +++ b/internal/handlers/dashboard/agent_team_handler.go @@ -13,7 +13,6 @@ import ( "github.com/gin-gonic/gin" "github.com/mlogclub/simple/sqls" - "github.com/mlogclub/simple/web" ) func AgentTeamAnyList(ctx *gin.Context) { @@ -61,7 +60,7 @@ func AgentTeamGetBy(ctx *gin.Context) { } item := services.AgentTeamService.Get(id) if item == nil || item.Status == enums.StatusDeleted { - httpx.WriteJSON(ctx, web.JsonErrorMsg("客服组不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0169")) return } httpx.WriteJSON(ctx, buildAgentTeamResponse(item)) diff --git a/internal/handlers/dashboard/agent_team_schedule_handler.go b/internal/handlers/dashboard/agent_team_schedule_handler.go index ae8b075..568b13c 100644 --- a/internal/handlers/dashboard/agent_team_schedule_handler.go +++ b/internal/handlers/dashboard/agent_team_schedule_handler.go @@ -7,6 +7,7 @@ import ( "agent-desk/internal/pkg/dto/request" "agent-desk/internal/pkg/dto/response" "agent-desk/internal/pkg/httpx" + "agent-desk/internal/pkg/i18nx" "agent-desk/internal/services" "agent-desk/internal/pkg/httpx/params" @@ -66,6 +67,7 @@ func AgentTeamSchedulePostBatch_preview(ctx *gin.Context) { httpx.WriteJSON(ctx, err) return } + req.Locale = i18nx.Locale(ctx) ret, err := services.AgentTeamScheduleService.BatchPreview(req, operator) if err != nil { httpx.WriteJSON(ctx, err) @@ -85,6 +87,7 @@ func AgentTeamSchedulePostBatch_generate(ctx *gin.Context) { httpx.WriteJSON(ctx, err) return } + req.Locale = i18nx.Locale(ctx) ret, err := services.AgentTeamScheduleService.BatchGenerate(req, operator) if err != nil { httpx.WriteJSON(ctx, err) @@ -104,7 +107,7 @@ func AgentTeamScheduleGetBy(ctx *gin.Context) { } item := services.AgentTeamScheduleService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("客服组排班不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0172")) return } httpx.WriteJSON(ctx, buildAgentTeamScheduleResponse(item)) diff --git a/internal/handlers/dashboard/ai_agent_handler.go b/internal/handlers/dashboard/ai_agent_handler.go index b625687..2184c4b 100644 --- a/internal/handlers/dashboard/ai_agent_handler.go +++ b/internal/handlers/dashboard/ai_agent_handler.go @@ -64,7 +64,7 @@ func AIAgentGetBy(ctx *gin.Context) { } item := services.AIAgentService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("AI Agent 不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002")) return } httpx.WriteJSON(ctx, buildAIAgentResponseWithLocale(item, i18nx.Locale(ctx))) diff --git a/internal/handlers/dashboard/ai_config_handler.go b/internal/handlers/dashboard/ai_config_handler.go index d8d3ff9..10a8fe0 100644 --- a/internal/handlers/dashboard/ai_config_handler.go +++ b/internal/handlers/dashboard/ai_config_handler.go @@ -62,7 +62,7 @@ func AIConfigGetBy(ctx *gin.Context) { item := services.AIConfigService.Get(id) if item == nil || item.Status == enums.StatusDeleted { - httpx.WriteJSON(ctx, web.JsonErrorMsg("AI配置不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0012")) return } httpx.WriteJSON(ctx, response.BuildAIConfigResponse(item)) diff --git a/internal/handlers/dashboard/asset_handler.go b/internal/handlers/dashboard/asset_handler.go index 475a72b..78ac2a3 100644 --- a/internal/handlers/dashboard/asset_handler.go +++ b/internal/handlers/dashboard/asset_handler.go @@ -51,7 +51,7 @@ func AssetGetBy(ctx *gin.Context) { } item := services.AssetService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("文件不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0214")) return } httpx.WriteJSON(ctx, builders.BuildAsset(item)) @@ -71,7 +71,7 @@ func AssetPostCreate(ctx *gin.Context) { } header, err := ctx.FormFile("file") if err != nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("请选择上传文件")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0323")) return } item, err := services.AssetService.UploadFile(header, req.Prefix, operator) diff --git a/internal/handlers/dashboard/channel_handler.go b/internal/handlers/dashboard/channel_handler.go index 81e3267..ac2b95e 100644 --- a/internal/handlers/dashboard/channel_handler.go +++ b/internal/handlers/dashboard/channel_handler.go @@ -44,7 +44,7 @@ func ChannelGetBy(ctx *gin.Context) { } item := services.ChannelService.Get(id) if item == nil || item.Status == enums.StatusDeleted { - httpx.WriteJSON(ctx, web.JsonErrorMsg("channel not found")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0062")) return } httpx.WriteJSON(ctx, buildChannelResponse(item)) diff --git a/internal/handlers/dashboard/conversation_handler.go b/internal/handlers/dashboard/conversation_handler.go index d4e85bb..50c4c82 100644 --- a/internal/handlers/dashboard/conversation_handler.go +++ b/internal/handlers/dashboard/conversation_handler.go @@ -112,7 +112,7 @@ func ConversationGetBy(ctx *gin.Context) { item := services.ConversationService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("会话不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116")) return } @@ -137,7 +137,7 @@ func ConversationAnyMessage_list(ctx *gin.Context) { limit, _ = params.GetInt(ctx, "limit") ) if conversation := services.ConversationService.Get(conversationID); conversation == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("会话不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116")) return } @@ -311,12 +311,12 @@ func ConversationPostUpload_image(ctx *gin.Context) { rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId")) if rawConv == "" { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } conversationID, err := strconv.ParseInt(rawConv, 10, 64) if err != nil || conversationID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeAgent, operator, nil); err != nil { @@ -326,11 +326,11 @@ func ConversationPostUpload_image(ctx *gin.Context) { header, err := ctx.FormFile("file") if err != nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("请选择上传图片")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0322")) return } if !strings.HasPrefix(strings.ToLower(header.Header.Get("Content-Type")), "image/") { - httpx.WriteJSON(ctx, web.JsonErrorMsg("仅支持上传图片文件")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0090")) return } @@ -351,12 +351,12 @@ func ConversationPostUpload_attachment(ctx *gin.Context) { rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId")) if rawConv == "" { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } conversationID, err := strconv.ParseInt(rawConv, 10, 64) if err != nil || conversationID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("conversationId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064")) return } if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeAgent, operator, nil); err != nil { @@ -366,7 +366,7 @@ func ConversationPostUpload_attachment(ctx *gin.Context) { header, err := ctx.FormFile("file") if err != nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("请选择上传附件")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0324")) return } item, err := services.AssetService.UploadFile(header, "attachments", operator) diff --git a/internal/handlers/dashboard/customer_contact_handler.go b/internal/handlers/dashboard/customer_contact_handler.go index 72a4dc3..90e27b2 100644 --- a/internal/handlers/dashboard/customer_contact_handler.go +++ b/internal/handlers/dashboard/customer_contact_handler.go @@ -10,7 +10,6 @@ import ( "agent-desk/internal/pkg/httpx/params" "github.com/gin-gonic/gin" - "github.com/mlogclub/simple/web" ) // AnyList GET/POST /customer-contact/list?customerId= @@ -21,7 +20,7 @@ func CustomerContactAnyList(ctx *gin.Context) { } customerID, _ := params.GetInt64(ctx, "customerId") if customerID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("customerId 必填")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0065")) return } list := services.CustomerContactService.FindActiveByCustomerID(customerID) diff --git a/internal/handlers/dashboard/knowledge_base_handler.go b/internal/handlers/dashboard/knowledge_base_handler.go index 11434d8..c6b70c3 100644 --- a/internal/handlers/dashboard/knowledge_base_handler.go +++ b/internal/handlers/dashboard/knowledge_base_handler.go @@ -72,7 +72,7 @@ func KnowledgeBaseGetBy(ctx *gin.Context) { item := services.KnowledgeBaseService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("知识库不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0283")) return } resp := builders.BuildKnowledgeBase(item) @@ -169,7 +169,7 @@ func KnowledgeBasePostRebuild_index(ctx *gin.Context) { knowledgeBase := services.KnowledgeBaseService.Get(req.ID) if knowledgeBase == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("知识库不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0283")) return } diff --git a/internal/handlers/dashboard/knowledge_directory_handler.go b/internal/handlers/dashboard/knowledge_directory_handler.go index a08b74e..842629b 100644 --- a/internal/handlers/dashboard/knowledge_directory_handler.go +++ b/internal/handlers/dashboard/knowledge_directory_handler.go @@ -21,7 +21,7 @@ func KnowledgeDirectoryGetList_all(ctx *gin.Context) { } knowledgeBaseID, ok := params.GetInt64(ctx, "knowledgeBaseId") if !ok || knowledgeBaseID <= 0 { - httpx.WriteJSON(ctx, errorsx.InvalidParam("知识库不存在")) + httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0283")) return } list := services.KnowledgeDirectoryService.FindAllByKnowledgeBaseID(knowledgeBaseID) diff --git a/internal/handlers/dashboard/knowledge_document_handler.go b/internal/handlers/dashboard/knowledge_document_handler.go index 6c704e0..cc36a53 100644 --- a/internal/handlers/dashboard/knowledge_document_handler.go +++ b/internal/handlers/dashboard/knowledge_document_handler.go @@ -37,7 +37,7 @@ func KnowledgeDocumentAnyList(ctx *gin.Context) { } if indexStatus, ok := params.Get(ctx, "indexStatus"); ok { if !enums.IsValidKnowledgeDocumentIndexStatus(indexStatus) { - httpx.WriteJSON(ctx, web.JsonErrorMsg("indexStatus参数不合法")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0067")) return } cnd.Where("index_status = ?", indexStatus) @@ -66,7 +66,7 @@ func KnowledgeDocumentGetBy(ctx *gin.Context) { item := services.KnowledgeDocumentService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("文档不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0218")) return } resp := builders.BuildKnowledgeDocument(item) diff --git a/internal/handlers/dashboard/knowledge_faq_handler.go b/internal/handlers/dashboard/knowledge_faq_handler.go index 17aacab..50b149f 100644 --- a/internal/handlers/dashboard/knowledge_faq_handler.go +++ b/internal/handlers/dashboard/knowledge_faq_handler.go @@ -38,7 +38,7 @@ func KnowledgeFAQGetExport(ctx *gin.Context) { } knowledgeBaseID, ok := params.GetInt64(ctx, "knowledgeBaseId") if !ok || knowledgeBaseID <= 0 { - httpx.WriteJSON(ctx, errorsx.InvalidParam("知识库不存在")) + httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0283")) return } file, err := services.KnowledgeFAQService.ExportKnowledgeFAQs(knowledgeBaseID) @@ -64,17 +64,17 @@ func KnowledgeFAQPostImport(ctx *gin.Context) { } knowledgeBaseID, ok := params.GetInt64(ctx, "knowledgeBaseId") if !ok || knowledgeBaseID <= 0 { - httpx.WriteJSON(ctx, errorsx.InvalidParam("知识库不存在")) + httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0283")) return } header, err := ctx.FormFile("file") if err != nil { - httpx.WriteJSON(ctx, errorsx.InvalidParam("请选择导入文件")) + httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0327")) return } file, err := header.Open() if err != nil { - httpx.WriteJSON(ctx, errorsx.InvalidParam("导入文件读取失败")) + httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0176")) return } defer file.Close() @@ -83,12 +83,12 @@ func KnowledgeFAQPostImport(ctx *gin.Context) { Mode: mode, Filename: header.Filename, Reader: file, + Locale: i18nx.Locale(ctx), }, operator) if err != nil { httpx.WriteJSON(ctx, err) return } - translateKnowledgeFAQImportResult(i18nx.Locale(ctx), result) httpx.WriteJSON(ctx, result) } @@ -130,7 +130,7 @@ func KnowledgeFAQGetBy(ctx *gin.Context) { item := services.KnowledgeFAQService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("FAQ不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0025")) return } resp := builders.BuildKnowledgeFAQ(item) @@ -235,15 +235,6 @@ func writeKnowledgeFAQExcelFile(ctx *gin.Context, file *response.KnowledgeFAQExp ctx.Data(http.StatusOK, file.ContentType, file.Data) } -func translateKnowledgeFAQImportResult(locale string, result *response.KnowledgeFAQImportResult) { - if result == nil || len(result.Errors) == 0 { - return - } - for i := range result.Errors { - result.Errors[i].Message = i18nx.TranslateKnownMessage(locale, result.Errors[i].Message) - } -} - func fillKnowledgeFAQDirectory(resp *response.KnowledgeFAQResponse, directoryPaths map[int64]string) { resp.DirectoryPath = directoryPaths[resp.DirectoryID] resp.DirectoryName = resp.DirectoryPath diff --git a/internal/handlers/dashboard/knowledge_retrieve_handler.go b/internal/handlers/dashboard/knowledge_retrieve_handler.go index 8c84e72..bf46330 100644 --- a/internal/handlers/dashboard/knowledge_retrieve_handler.go +++ b/internal/handlers/dashboard/knowledge_retrieve_handler.go @@ -12,7 +12,6 @@ import ( "agent-desk/internal/pkg/httpx/params" "github.com/gin-gonic/gin" - "github.com/mlogclub/simple/web" ) func KnowledgeRetrievePostDebugSearch(ctx *gin.Context) { @@ -92,5 +91,5 @@ func KnowledgeRetrievePostBuild(ctx *gin.Context) { return } - httpx.WriteJSON(ctx, web.JsonErrorMsg("documentId或faqId不能为空")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0066")) } diff --git a/internal/handlers/dashboard/knowledge_retrieve_log_handler.go b/internal/handlers/dashboard/knowledge_retrieve_log_handler.go index a7b7c29..84a152f 100644 --- a/internal/handlers/dashboard/knowledge_retrieve_log_handler.go +++ b/internal/handlers/dashboard/knowledge_retrieve_log_handler.go @@ -60,7 +60,7 @@ func KnowledgeRetrieveLogGetBy(ctx *gin.Context) { logItem := services.KnowledgeRetrieveLogService.Get(id) if logItem == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("检索日志不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0241")) return } diff --git a/internal/handlers/dashboard/permission_handler.go b/internal/handlers/dashboard/permission_handler.go index e688a39..a769bb9 100644 --- a/internal/handlers/dashboard/permission_handler.go +++ b/internal/handlers/dashboard/permission_handler.go @@ -59,7 +59,7 @@ func PermissionGetBy(ctx *gin.Context) { item := services.PermissionService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("权限不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0236")) return } httpx.WriteJSON(ctx, &response.PermissionResponse{ diff --git a/internal/handlers/dashboard/quick_reply_handler.go b/internal/handlers/dashboard/quick_reply_handler.go index 611c485..261bbe0 100644 --- a/internal/handlers/dashboard/quick_reply_handler.go +++ b/internal/handlers/dashboard/quick_reply_handler.go @@ -44,7 +44,7 @@ func QuickReplyGetBy(ctx *gin.Context) { item := services.QuickReplyService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("快捷回复不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0203")) return } httpx.WriteJSON(ctx, builders.BuildQuickReplyResponse(item)) diff --git a/internal/handlers/dashboard/role_handler.go b/internal/handlers/dashboard/role_handler.go index c30efad..1690eaa 100644 --- a/internal/handlers/dashboard/role_handler.go +++ b/internal/handlers/dashboard/role_handler.go @@ -71,7 +71,7 @@ func RoleGetBy(ctx *gin.Context) { item := services.RoleService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("角色不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0305")) return } diff --git a/internal/handlers/dashboard/skill_definition_handler.go b/internal/handlers/dashboard/skill_definition_handler.go index e11a234..c97b3f0 100644 --- a/internal/handlers/dashboard/skill_definition_handler.go +++ b/internal/handlers/dashboard/skill_definition_handler.go @@ -73,7 +73,7 @@ func SkillDefinitionGetBy(ctx *gin.Context) { item := services.SkillDefinitionService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Skill 不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053")) return } httpx.WriteJSON(ctx, builders.BuildSkillDefinitionResponse(item)) @@ -131,24 +131,24 @@ func SkillDefinitionPostUpdate_status(ctx *gin.Context) { return } if req.ID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Skill ID 不合法")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0052")) return } if !enums.IsValidStatus(req.Status) { - httpx.WriteJSON(ctx, web.JsonErrorMsg("状态值不合法")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0254")) return } item := services.SkillDefinitionService.Get(req.ID) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Skill 不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053")) return } if item.Status == enums.StatusDeleted { - httpx.WriteJSON(ctx, web.JsonErrorMsg("已删除的 Skill 不能直接修改状态,请先恢复")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0184")) return } if req.Status == int(enums.StatusDeleted) { - httpx.WriteJSON(ctx, web.JsonErrorMsg("请使用删除接口处理删除状态")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0319")) return } @@ -177,11 +177,11 @@ func SkillDefinitionPostDelete(ctx *gin.Context) { return } if req.ID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Skill ID 不合法")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0052")) return } if services.SkillDefinitionService.Get(req.ID) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Skill 不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053")) return } if err := services.SkillDefinitionService.Updates(req.ID, map[string]any{ @@ -209,17 +209,17 @@ func SkillDefinitionPostRestore(ctx *gin.Context) { return } if req.ID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Skill ID 不合法")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0052")) return } item := services.SkillDefinitionService.Get(req.ID) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("Skill 不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053")) return } if item.Status != enums.StatusDeleted { - httpx.WriteJSON(ctx, web.JsonErrorMsg("仅已删除的 Skill 支持恢复")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0088")) return } diff --git a/internal/handlers/dashboard/tag_handler.go b/internal/handlers/dashboard/tag_handler.go index 3490a31..8bd779e 100644 --- a/internal/handlers/dashboard/tag_handler.go +++ b/internal/handlers/dashboard/tag_handler.go @@ -49,7 +49,7 @@ func TagGetBy(ctx *gin.Context) { item := services.TagService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("标签不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0238")) return } result := builders.BuildTagResponse(item) diff --git a/internal/handlers/dashboard/ticket_handler.go b/internal/handlers/dashboard/ticket_handler.go index 6a56ed0..3ab5bae 100644 --- a/internal/handlers/dashboard/ticket_handler.go +++ b/internal/handlers/dashboard/ticket_handler.go @@ -252,11 +252,11 @@ func TicketAnyProgressList(ctx *gin.Context) { } ticketID, _ := params.GetInt64(ctx, "ticketId") if ticketID <= 0 { - httpx.WriteJSON(ctx, web.JsonErrorMsg("工单不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0178")) return } if services.TicketService.Get(ticketID) == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("工单不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0178")) return } progresses := services.TicketProgressService.Find(sqls.NewCnd().Eq("ticket_id", ticketID).Asc("id")) diff --git a/internal/handlers/dashboard/user_handler.go b/internal/handlers/dashboard/user_handler.go index 27f4414..d0127d4 100644 --- a/internal/handlers/dashboard/user_handler.go +++ b/internal/handlers/dashboard/user_handler.go @@ -68,7 +68,7 @@ func UserGetBy(ctx *gin.Context) { item := services.UserService.Get(id) if item == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("用户不存在")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0255")) return } httpx.WriteJSON(ctx, builders.BuildUserResponse(item, builders.UserBuildOptions{ @@ -191,7 +191,7 @@ func UserPostChange_password(ctx *gin.Context) { principal = services.AuthService.GetAuthPrincipal(ctx) } if principal == nil { - httpx.WriteJSON(ctx, web.JsonErrorMsg("未登录或登录已过期")) + httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.auth.expired")) return } diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go index 5a2a99e..27f71bc 100644 --- a/internal/middleware/auth_middleware.go +++ b/internal/middleware/auth_middleware.go @@ -18,7 +18,7 @@ func AuthMiddleware(ctx *gin.Context) { func authenticateRequest(ctx *gin.Context) bool { if _, err := services.AuthService.Authenticate(ctx); err != nil { result := web.JsonError(err) - result.Message = i18nx.T(ctx, "error.auth.expired", nil) + result.Message = i18nx.T(ctx, "error.auth.expired") ctx.JSON(200, result) ctx.Abort() return false diff --git a/internal/middleware/chat_middleware.go b/internal/middleware/chat_middleware.go index 9d2476b..c608800 100644 --- a/internal/middleware/chat_middleware.go +++ b/internal/middleware/chat_middleware.go @@ -11,7 +11,7 @@ import ( func ExternalUserMiddleware(ctx *gin.Context) { channel := services.ChannelService.GetEnabledChannel(ctx) if channel == nil { - ctx.JSON(200, web.JsonErrorMsg("接入渠道异常")) + ctx.JSON(200, httpx.JsonErrorMsg(ctx, "error.e0210")) ctx.Abort() return } diff --git a/internal/oidcclient/oidcclient.go b/internal/oidcclient/oidcclient.go index 1b250d4..248b1a4 100644 --- a/internal/oidcclient/oidcclient.go +++ b/internal/oidcclient/oidcclient.go @@ -4,6 +4,7 @@ import ( "agent-desk/internal/pkg/config" "agent-desk/internal/pkg/dto/response" "agent-desk/internal/pkg/errorsx" + "agent-desk/internal/pkg/i18nx" "context" "crypto/hmac" "crypto/rand" @@ -11,7 +12,6 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" - "fmt" "strings" "sync" "time" @@ -66,16 +66,16 @@ func Init(ctx context.Context) error { } oidcCfg = cfg if strings.TrimSpace(cfg.Issuer) == "" { - return fmt.Errorf("OIDC issuer 未配置") + return i18nx.Errorf("error.e0039") } if strings.TrimSpace(cfg.ClientID) == "" { - return fmt.Errorf("OIDC clientId 未配置") + return i18nx.Errorf("error.e0036") } if strings.TrimSpace(cfg.ClientSecret) == "" { - return fmt.Errorf("OIDC clientSecret 未配置") + return i18nx.Errorf("error.e0037") } if strings.TrimSpace(cfg.RedirectURL) == "" { - return fmt.Errorf("OIDC redirectUrl 未配置") + return i18nx.Errorf("error.e0040") } p, err := gooidc.NewProvider(ctx, strings.TrimSpace(cfg.Issuer)) @@ -119,7 +119,7 @@ func ExchangeCode(ctx context.Context, code string) (*Profile, error) { } code = strings.TrimSpace(code) if code == "" { - return nil, errorsx.InvalidParam("OIDC 授权 code 不能为空") + return nil, errorsx.InvalidParamI18n("error.e0041") } token, err := oauthConfig.Exchange(ctx, code) if err != nil { @@ -127,7 +127,7 @@ func ExchangeCode(ctx context.Context, code string) (*Profile, error) { } rawIDToken, ok := token.Extra("id_token").(string) if !ok || strings.TrimSpace(rawIDToken) == "" { - return nil, errorsx.Unauthorized("OIDC id_token 不存在") + return nil, errorsx.UnauthorizedI18n("error.e0038") } idToken, err := idTokenVerifier.Verify(ctx, rawIDToken) if err != nil { @@ -171,32 +171,32 @@ func CreateState(next string) (string, error) { func ParseState(state string) (string, error) { secret := stateSecret() if secret == "" { - return "", errorsx.Unauthorized("OIDC 登录状态无效或已过期") + return "", errorsx.UnauthorizedI18n("error.e0046") } parts := strings.Split(strings.TrimSpace(state), ".") if len(parts) != 2 { - return "", errorsx.Unauthorized("OIDC 登录状态无效或已过期") + return "", errorsx.UnauthorizedI18n("error.e0046") } if !hmac.Equal([]byte(parts[1]), []byte(signState(parts[0], secret))) { - return "", errorsx.Unauthorized("OIDC 登录状态无效或已过期") + return "", errorsx.UnauthorizedI18n("error.e0046") } body, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { - return "", errorsx.Unauthorized("OIDC 登录状态无效或已过期") + return "", errorsx.UnauthorizedI18n("error.e0046") } payload := statePayload{} if err = json.Unmarshal(body, &payload); err != nil { - return "", errorsx.Unauthorized("OIDC 登录状态无效或已过期") + return "", errorsx.UnauthorizedI18n("error.e0046") } if payload.ExpiredAt <= time.Now().Unix() { - return "", errorsx.Unauthorized("OIDC 登录状态无效或已过期") + return "", errorsx.UnauthorizedI18n("error.e0046") } return sanitizeNextPath(payload.Next), nil } func IssueLoginTicket(loginResp *response.LoginResponse) (string, error) { if loginResp == nil { - return "", fmt.Errorf("登录结果不能为空") + return "", i18nx.Errorf("error.e0272") } ticket, err := randomToken("olt_") if err != nil { @@ -213,15 +213,15 @@ func IssueLoginTicket(loginResp *response.LoginResponse) (string, error) { func ConsumeLoginTicket(ticket string) (*response.LoginResponse, error) { ticket = strings.TrimSpace(ticket) if ticket == "" { - return nil, errorsx.InvalidParam("ticket 不能为空") + return nil, errorsx.InvalidParamI18n("error.e0072") } value, ok := loginTicketStore.LoadAndDelete(ticket) if !ok { - return nil, errorsx.Unauthorized("登录票据无效或已过期") + return nil, errorsx.UnauthorizedI18n("error.e0271") } record, ok := value.(loginTicket) if !ok || record.Response == nil || time.Now().After(record.ExpiredAt) { - return nil, errorsx.Unauthorized("登录票据无效或已过期") + return nil, errorsx.UnauthorizedI18n("error.e0271") } return record.Response, nil } @@ -241,7 +241,7 @@ func profileFromIDToken(idToken *gooidc.IDToken) (*Profile, error) { RawProfile: string(raw), } if strings.TrimSpace(profile.Subject) == "" { - return nil, errorsx.Unauthorized("OIDC 用户标识不存在") + return nil, errorsx.UnauthorizedI18n("error.e0043") } return profile, nil } @@ -272,7 +272,7 @@ func profileFromUserInfo(userInfo *gooidc.UserInfo, fallback *Profile) (*Profile } } if strings.TrimSpace(profile.Subject) == "" { - return nil, errorsx.Unauthorized("OIDC 用户标识不存在") + return nil, errorsx.UnauthorizedI18n("error.e0043") } return profile, nil } diff --git a/internal/pkg/dto/request/agent_request.go b/internal/pkg/dto/request/agent_request.go index 1c872c3..93c1ce0 100644 --- a/internal/pkg/dto/request/agent_request.go +++ b/internal/pkg/dto/request/agent_request.go @@ -76,4 +76,5 @@ type AgentTeamScheduleBatchRequest struct { StartTime string `json:"startTime"` EndTime string `json:"endTime"` Remark string `json:"remark"` + Locale string `json:"-"` } diff --git a/internal/pkg/dto/request/knowledge_request.go b/internal/pkg/dto/request/knowledge_request.go index 6e68ee6..f0d20b0 100644 --- a/internal/pkg/dto/request/knowledge_request.go +++ b/internal/pkg/dto/request/knowledge_request.go @@ -101,6 +101,7 @@ type ImportKnowledgeFAQRequest struct { Mode KnowledgeFAQImportMode Filename string Reader io.Reader + Locale string } type KnowledgeSearchRequest struct { diff --git a/internal/pkg/errorsx/errors.go b/internal/pkg/errorsx/errors.go index 12bb2de..ac561a4 100644 --- a/internal/pkg/errorsx/errors.go +++ b/internal/pkg/errorsx/errors.go @@ -1,6 +1,10 @@ package errorsx -import "github.com/mlogclub/simple/web" +import ( + "agent-desk/internal/pkg/i18nx" + + "github.com/mlogclub/simple/web" +) const ( CodeInvalidParam = 1000 @@ -16,26 +20,92 @@ func InvalidParam(message string) error { return web.NewError(CodeInvalidParam, message) } +func InvalidParamI18n(key string, args ...any) error { + return NewI18nError(CodeInvalidParam, key, args...) +} + func BusinessError(code int, message string) error { return web.NewError(CodeBusinessError+code, message) } +func BusinessErrorI18n(code int, key string, args ...any) error { + return NewI18nError(CodeBusinessError+code, key, args...) +} + func Unauthorized(message string) error { return web.NewError(CodeAuthUnauthorized, message) } +func UnauthorizedI18n(key string, args ...any) error { + return NewI18nError(CodeAuthUnauthorized, key, args...) +} + func Forbidden(message string) error { return web.NewError(CodeAuthForbidden, message) } +func ForbiddenI18n(key string, args ...any) error { + return NewI18nError(CodeAuthForbidden, key, args...) +} + func InvalidToken(message string) error { return web.NewError(CodeAuthInvalidToken, message) } +func InvalidTokenI18n(key string, args ...any) error { + return NewI18nError(CodeAuthInvalidToken, key, args...) +} + func InvalidAccount(message string) error { return web.NewError(CodeAuthInvalidAccount, message) } +func InvalidAccountI18n(key string, args ...any) error { + return NewI18nError(CodeAuthInvalidAccount, key, args...) +} + func CredentialLocked(message string) error { return web.NewError(CodeAuthCredentialLocked, message) } + +func CredentialLockedI18n(key string, args ...any) error { + return NewI18nError(CodeAuthCredentialLocked, key, args...) +} + +type I18nError struct { + Code int + Key string + Args []any +} + +func NewI18nError(code int, key string, args ...any) *I18nError { + return &I18nError{Code: code, Key: key, Args: args} +} + +func (e *I18nError) Error() string { + if e == nil { + return "" + } + return e.Message(i18nx.LocaleZhCN) +} + +func (e *I18nError) Unwrap() error { + if e == nil { + return nil + } + return web.NewError(e.Code, e.Error()) +} + +func (e *I18nError) Message(locale string) string { + if e == nil { + return "" + } + return i18nx.Getf(locale, e.Key, e.Args...) +} + +func (e *I18nError) JsonResult(locale string) *web.JsonResult { + if e == nil { + return web.JsonSuccess() + } + return web.JsonErrorCode(e.Code, e.Message(locale)) +} diff --git a/internal/pkg/httpx/error.go b/internal/pkg/httpx/error.go new file mode 100644 index 0000000..2a6b2fa --- /dev/null +++ b/internal/pkg/httpx/error.go @@ -0,0 +1,12 @@ +package httpx + +import ( + "agent-desk/internal/pkg/i18nx" + + "github.com/gin-gonic/gin" + "github.com/mlogclub/simple/web" +) + +func JsonErrorMsg(ctx *gin.Context, key string, args ...any) *web.JsonResult { + return web.JsonErrorMsg(i18nx.T(ctx, key, args...)) +} diff --git a/internal/pkg/httpx/path.go b/internal/pkg/httpx/path.go index ad810ff..8553002 100644 --- a/internal/pkg/httpx/path.go +++ b/internal/pkg/httpx/path.go @@ -12,7 +12,7 @@ import ( func GetPathInt64(ctx *gin.Context, name string) (int64, bool) { value, err := strconv.ParseInt(ctx.Param(name), 10, 64) if err != nil { - WriteHttpStatusJSON(ctx, http.StatusBadRequest, web.JsonErrorMsg(i18nx.T(ctx, "error.path.invalid", nil))) + WriteHttpStatusJSON(ctx, http.StatusBadRequest, web.JsonErrorMsg(i18nx.T(ctx, "error.path.invalid"))) return 0, false } return value, true diff --git a/internal/pkg/httpx/response.go b/internal/pkg/httpx/response.go index 6f3f446..d6347b9 100644 --- a/internal/pkg/httpx/response.go +++ b/internal/pkg/httpx/response.go @@ -1,6 +1,7 @@ package httpx import ( + "agent-desk/internal/pkg/errorsx" "agent-desk/internal/pkg/i18nx" "net/http" @@ -20,6 +21,10 @@ type pageData struct { paging *sqls.Paging } +type localizedError interface { + Message(locale string) string +} + func CursorData(results any, cursor string, hasMore bool) any { return cursorData{results: results, cursor: cursor, hasMore: hasMore} } @@ -29,22 +34,14 @@ func PageData(results any, paging *sqls.Paging) any { } func WriteJSON(ctx *gin.Context, result any) { - ctx.JSON(http.StatusOK, localizeJSONResult(ctx, buildJSONResult(result))) + ctx.JSON(http.StatusOK, buildJSONResult(ctx, result)) } func WriteHttpStatusJSON(ctx *gin.Context, statusCode int, result any) { - ctx.JSON(statusCode, localizeJSONResult(ctx, buildJSONResult(result))) + ctx.JSON(statusCode, buildJSONResult(ctx, result)) } -func localizeJSONResult(ctx *gin.Context, result *web.JsonResult) *web.JsonResult { - if result == nil || result.Success || result.Message == "" { - return result - } - result.Message = i18nx.TranslateKnownMessage(i18nx.Locale(ctx), result.Message) - return result -} - -func buildJSONResult(result any) *web.JsonResult { +func buildJSONResult(ctx *gin.Context, result any) *web.JsonResult { switch value := result.(type) { case nil: return web.JsonSuccess() @@ -56,6 +53,12 @@ func buildJSONResult(result any) *web.JsonResult { return web.JsonError(value) case web.CodeError: return web.JsonError(&value) + case *errorsx.I18nError: + return value.JsonResult(i18nx.Locale(ctx)) + case errorsx.I18nError: + return value.JsonResult(i18nx.Locale(ctx)) + case localizedError: + return web.JsonErrorMsg(value.Message(i18nx.Locale(ctx))) case error: return web.JsonError(value) case cursorData: diff --git a/internal/pkg/httpx/response_test.go b/internal/pkg/httpx/response_test.go index bdef304..a02e50b 100644 --- a/internal/pkg/httpx/response_test.go +++ b/internal/pkg/httpx/response_test.go @@ -1,6 +1,7 @@ package httpx import ( + "agent-desk/internal/pkg/errorsx" "agent-desk/internal/pkg/i18nx" "encoding/json" "errors" @@ -68,12 +69,12 @@ func TestWriteJSONWrapsCommonResultTypes(t *testing.T) { } } -func TestWriteJSONLocalizesKnownErrorMessages(t *testing.T) { +func TestWriteJSONLocalizesI18nErrors(t *testing.T) { gin.SetMode(gin.TestMode) ctx, recorder := testContext() i18nx.SetLocale(ctx, i18nx.LocaleEnUS) - WriteJSON(ctx, web.JsonErrorMsg("会话不存在")) + WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0116")) var got web.JsonResult if err := json.Unmarshal(recorder.Body.Bytes(), &got); err != nil { diff --git a/internal/pkg/i18nx/bundle.go b/internal/pkg/i18nx/bundle.go deleted file mode 100644 index b11d0af..0000000 --- a/internal/pkg/i18nx/bundle.go +++ /dev/null @@ -1,36 +0,0 @@ -package i18nx - -import ( - "embed" - "log/slog" - "sync" - - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/pelletier/go-toml/v2" - "golang.org/x/text/language" -) - -//go:embed locales/*.toml -var messageFiles embed.FS - -var ( - bundleOnce sync.Once - bundle *i18n.Bundle -) - -func Bundle() *i18n.Bundle { - bundleOnce.Do(func() { - b := i18n.NewBundle(language.AmericanEnglish) - b.RegisterUnmarshalFunc("toml", toml.Unmarshal) - for _, name := range []string{ - "locales/active.zh-CN.toml", - "locales/active.en-US.toml", - } { - if _, err := b.LoadMessageFileFS(messageFiles, name); err != nil { - slog.Error("load i18n message file failed", "file", name, "err", err) - } - } - bundle = b - }) - return bundle -} diff --git a/internal/pkg/i18nx/error.go b/internal/pkg/i18nx/error.go new file mode 100644 index 0000000..53730e3 --- /dev/null +++ b/internal/pkg/i18nx/error.go @@ -0,0 +1,38 @@ +package i18nx + +type Error struct { + Key string + Args []any + Cause error +} + +func Errorf(key string, args ...any) *Error { + err := &Error{Key: key, Args: args} + for _, arg := range args { + if cause, ok := arg.(error); ok { + err.Cause = cause + } + } + return err +} + +func (e *Error) Error() string { + if e == nil { + return "" + } + return e.Message(LocaleZhCN) +} + +func (e *Error) Message(locale string) string { + if e == nil { + return "" + } + return Getf(locale, e.Key, e.Args...) +} + +func (e *Error) Unwrap() error { + if e == nil { + return nil + } + return e.Cause +} diff --git a/internal/pkg/i18nx/i18nx_test.go b/internal/pkg/i18nx/i18nx_test.go index bc1be4f..a02a6d5 100644 --- a/internal/pkg/i18nx/i18nx_test.go +++ b/internal/pkg/i18nx/i18nx_test.go @@ -63,111 +63,63 @@ func TestResolveLocalePrefersXLocale(t *testing.T) { func TestTranslateFallsBackToEnglish(t *testing.T) { t.Parallel() - if got := TLocale(LocaleEnUS, "error.auth.expired", nil); got != "Your session has expired. Please sign in again." { + if got := TLocale(LocaleEnUS, "error.auth.expired"); got != "Your session has expired. Please sign in again." { t.Fatalf("english translation = %q", got) } - if got := TLocale("fr-FR", "error.auth.expired", nil); got != "Your session has expired. Please sign in again." { + if got := TLocale("fr-FR", "error.auth.expired"); got != "Your session has expired. Please sign in again." { t.Fatalf("fallback translation = %q", got) } } -func TestTranslateKnownMessageSupportsFormattedMessages(t *testing.T) { +func TestGetfSupportsLocaleKeys(t *testing.T) { t.Parallel() tests := []struct { - name string - message string - want string + name string + locale string + key string + args []any + want string }{ { - name: "batch limit", - message: "单次最多生成 31 条排班", - want: "You can generate at most 31 schedule entries at a time.", + name: "batch limit", + locale: LocaleEnUS, + key: "error.agentTeamSchedule.batchLimit", + args: []any{31}, + want: "You can generate at most 31 schedule entries at a time.", }, { - name: "start date", - message: "开始时间格式错误,请使用 yyyy-MM-dd", - want: "Invalid start time format. Use yyyy-MM-dd.", + name: "directory not found", + locale: LocaleEnUS, + key: "error.e0273", + want: "Directory not found.", }, { - name: "end date time", - message: "结束时间格式错误,请使用 yyyy-MM-dd HH:mm:ss 或 RFC3339", - want: "Invalid end time format. Use yyyy-MM-dd HH:mm:ss or RFC3339.", + name: "single knowledge base reference", + locale: LocaleEnUS, + key: "error.knowledgeBase.referencedByAgent", + args: []any{"SalesBot"}, + want: "This knowledge base is referenced by AI Agent \"SalesBot\". Remove the binding first.", }, { - name: "oidc issuer config", - message: "OIDC issuer 未配置", - want: "OIDC issuer is not configured.", + name: "multiple knowledge base references", + locale: LocaleEnUS, + key: "error.knowledgeBase.referencedByAgents", + args: []any{3}, + want: "This knowledge base is referenced by 3 AI Agents. Remove the bindings first.", }, { - name: "oidc login result", - message: "登录结果不能为空", - want: "Sign-in result is required.", + name: "faq import duplicated question", + locale: LocaleEnUS, + key: "error.knowledgeFAQImport.duplicateQuestionInFile", + args: []any{12}, + want: "The standard question is duplicated in the same file. It first appeared on row 12.", }, { - name: "directory not found", - message: "目录不存在", - want: "Directory not found.", - }, - { - name: "directory has docs", - message: "该目录下存在文档,无法删除", - want: "This directory contains documents and cannot be deleted.", - }, - { - name: "move documents", - message: "请选择要移动的文档", - want: "Select documents to move.", - }, - { - name: "move faqs across knowledge bases", - message: "只能移动当前知识库下的FAQ", - want: "Only FAQs in the current knowledge base can be moved.", - }, - { - name: "single knowledge base reference", - message: "知识库已被 AI Agent「SalesBot」引用,请先解除绑定", - want: "This knowledge base is referenced by AI Agent \"SalesBot\". Remove the binding first.", - }, - { - name: "multiple knowledge base references", - message: "知识库已被 3 个 AI Agent 引用,请先解除绑定", - want: "This knowledge base is referenced by 3 AI Agents. Remove the bindings first.", - }, - { - name: "schedule conflict range", - message: "该客服组在 2026-06-02 09:00:00 至 2026-06-02 10:00:00 已存在排班", - want: "This agent team already has a schedule from 2026-06-02 09:00:00 to 2026-06-02 10:00:00.", - }, - { - name: "faq import duplicated question", - message: "同一文件中标准问题重复,首次出现于第12行", - want: "The standard question is duplicated in the same file. It first appeared on row 12.", - }, - { - name: "faq import existing question", - message: "标准问题已存在,已跳过", - want: "The standard question already exists and was skipped.", - }, - { - name: "required param", - message: "参数:name不能为空", - want: "Parameter \"name\" is required.", - }, - { - name: "mcp call failed", - message: "调用 MCP 工具失败: timeout", - want: "Failed to call MCP tool: timeout", - }, - { - name: "wxwork unsupported outbound", - message: "当前暂不支持企业微信下行消息类型: voice", - want: "The current WeCom outbound message type is not supported yet: voice", - }, - { - name: "wxwork callback missing", - message: "企业微信登录回调地址未配置", - want: "WeCom sign-in callback URL is not configured.", + name: "zh keeps chinese value", + locale: LocaleZhCN, + key: "error.e0273", + want: "目录不存在", }, } @@ -175,19 +127,18 @@ func TestTranslateKnownMessageSupportsFormattedMessages(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - if got := TranslateKnownMessage(LocaleEnUS, tt.message); got != tt.want { - t.Fatalf("TranslateKnownMessage() = %q, want %q", got, tt.want) + if got := Getf(tt.locale, tt.key, tt.args...); got != tt.want { + t.Fatalf("Getf() = %q, want %q", got, tt.want) } }) } } -func TestTranslateKnownMessageKeepsChineseLocale(t *testing.T) { +func TestGetfFallsBackToKey(t *testing.T) { t.Parallel() - message := "目录不存在" - if got := TranslateKnownMessage(LocaleZhCN, message); got != message { - t.Fatalf("TranslateKnownMessage() = %q, want %q", got, message) + if got := Getf(LocaleEnUS, "missing.key"); got != "missing.key" { + t.Fatalf("missing translation = %q, want %q", got, "missing.key") } } diff --git a/internal/pkg/i18nx/known_messages.go b/internal/pkg/i18nx/known_messages.go deleted file mode 100644 index 51641d6..0000000 --- a/internal/pkg/i18nx/known_messages.go +++ /dev/null @@ -1,440 +0,0 @@ -package i18nx - -import ( - "fmt" - "regexp" - "strings" -) - -var ( - maxScheduleBatchMessagePattern = regexp.MustCompile(`^单次最多生成 ([0-9]+) 条排班$`) - knowledgeBaseReferencedByAgentPattern = regexp.MustCompile(`^知识库已被 AI Agent「(.+)」引用,请先解除绑定$`) - knowledgeBaseReferencedByAgentsPattern = regexp.MustCompile(`^知识库已被 ([0-9]+) 个 AI Agent 引用,请先解除绑定$`) - agentTeamScheduleConflictPattern = regexp.MustCompile(`^该客服组在 (.+) 至 (.+) 已存在排班$`) - knowledgeFAQImportDuplicatePattern = regexp.MustCompile(`^同一文件中标准问题重复,首次出现于第([0-9]+)行$`) - requiredParamMessagePattern = regexp.MustCompile(`^参数:(.+)不能为空$`) - mcpListToolsMessagePattern = regexp.MustCompile(`^列出 MCP 工具失败: (.+)$`) - mcpCallToolMessagePattern = regexp.MustCompile(`^调用 MCP 工具失败: (.+)$`) - mcpConnectServerMessagePattern = regexp.MustCompile(`^连接 MCP Server 失败: (.+)$`) - wxworkUnsupportedOutboundPattern = regexp.MustCompile(`^不支持的企业微信下行消息类型: (.+)$`) - wxworkUnsupportedCurrentOutboundPattern = regexp.MustCompile(`^当前暂不支持企业微信下行消息类型: (.+)$`) -) - -var knownMessages = map[string]map[string]string{ - LocaleEnUS: { - "未登录或登录已过期": "Your session has expired. Please sign in again.", - "无权限执行该操作": "You do not have permission to perform this action.", - "无权限操作": "You do not have permission to perform this action.", - "参数不合法": "Invalid request parameters.", - "路径参数错误": "Invalid path parameter.", - "状态值不合法": "Invalid status value.", - "用户名和密码不能为空": "Enter both username and password.", - "用户名或密码错误": "The username or password is incorrect.", - "登录失败次数过多,请稍后再试": "Too many failed sign-in attempts. Please try again later.", - "用户不存在": "User not found.", - "用户不存在或已被禁用": "The user does not exist or has been disabled.", - "用户名不能为空": "Enter a username.", - "用户名已存在": "This username is already in use.", - "手机号已存在": "This phone number is already in use.", - "邮箱已存在": "This email address is already in use.", - "新密码不能为空": "Enter a new password.", - "禁用角色不允许分配": "Disabled roles cannot be assigned.", - "角色不存在": "Role not found.", - "角色名称和编码不能为空": "Enter both role name and role code.", - "角色编码已存在": "This role code is already in use.", - "系统内置角色不允许删除": "Built-in system roles cannot be deleted.", - "角色已被用户使用,无法删除": "This role is assigned to users and cannot be deleted.", - "权限不存在": "Permission not found.", - "会话不存在": "Conversation not found.", - "会话筛选项不合法": "Invalid conversation filter.", - "只有待接入会话允许分配": "Only queued conversations can be assigned.", - "只有待接入会话允许自动分配": "Only queued conversations can be auto-assigned.", - "当前会话已分配客服": "This conversation has already been assigned.", - "当前暂不在人工客服服务时间内": "Human support is currently outside service hours.", - "目标客服不能为空": "Select a target agent.", - "目标客服不存在": "Target agent not found.", - "无权转接该会话": "You do not have permission to transfer this conversation.", - "只有处理中会话允许转接": "Only active conversations can be transferred.", - "当前会话未分配客服": "This conversation is not assigned to an agent.", - "目标客服不能与当前指派人相同": "The target agent must be different from the current assignee.", - "无权访问该会话": "You do not have permission to access this conversation.", - "当前状态不允许关闭会话": "This conversation cannot be closed in its current status.", - "关闭原因不能为空": "Enter a close reason.", - "无权关闭该会话": "You do not have permission to close this conversation.", - "已关闭的会话无法关联客户": "Closed conversations cannot be linked to customers.", - "无权限关联该会话": "You do not have permission to link this conversation.", - "外部用户标识不能为空": "External user ID is required.", - "接入渠道未初始化": "The channel is not initialized.", - "接入渠道不存在或已停用": "The channel does not exist or has been disabled.", - "接入渠道异常": "Channel error.", - "channel not found": "Channel not found.", - "该渠道不支持开放客服配置": "This channel does not support public support configuration.", - "外部身份未初始化": "External identity is not initialized.", - "conversationId不能为空": "Conversation ID is required.", - "请选择上传图片": "Choose an image to upload.", - "仅支持上传图片文件": "Only image files are supported.", - "请选择上传附件": "Choose an attachment to upload.", - "请选择上传文件": "Choose a file to upload.", - "文件不存在": "File not found.", - "文件不可访问": "This file cannot be accessed.", - "上传文件超过大小限制": "The uploaded file exceeds the size limit.", - "图片资源不存在": "Image asset not found.", - "当前暂不支持该存储类型的文件读取": "Reading files from this storage provider is not supported yet.", - "客户不存在": "Customer not found.", - "客户名称不能为空": "Enter a customer name.", - "所属公司不存在": "Company not found.", - "公司不存在": "Company not found.", - "公司名称不能为空": "Enter a company name.", - "公司名称已存在": "This company name is already in use.", - "工单不存在": "Ticket not found.", - "工单标题不能为空": "Enter a ticket title.", - "工单描述不能为空": "Enter a ticket description.", - "工单来源不合法": "Invalid ticket source.", - "工单状态不合法": "Invalid ticket status.", - "处理进展不能为空": "Enter a progress update.", - "负责人不存在": "Assignee not found.", - "会话与客户不匹配": "The conversation does not belong to this customer.", - "视图名称不能为空": "Enter a view name.", - "视图筛选条件格式不正确": "The view filter format is invalid.", - "视图不存在": "View not found.", - "标签不存在": "Tag not found.", - "标签名称不能为空": "Enter a tag name.", - "父标签不存在": "Parent tag not found.", - "同级下已存在相同名称的标签": "A tag with this name already exists at the same level.", - "不能将标签设为自己的子标签": "A tag cannot be moved under itself.", - "该标签下存在子标签,无法删除": "This tag has child tags and cannot be deleted.", - "该标签已关联会话,无法删除": "This tag is linked to conversations and cannot be deleted.", - "该标签已关联工单,无法删除": "This tag is linked to tickets and cannot be deleted.", - "快捷回复不存在": "Quick reply not found.", - "客服档案不存在": "Agent profile not found.", - "请选择关联用户": "Select a user to link.", - "关联用户不存在": "Linked user not found.", - "请选择所属客服组": "Select an agent team.", - "所属客服组不存在": "Agent team not found.", - "客服工号和展示名不能为空": "Enter both agent ID and display name.", - "该用户已存在客服档案": "This user already has an agent profile.", - "客服工号已存在": "This agent ID is already in use.", - "客服状态不合法": "Invalid agent status.", - "最大并发接待数不能小于 0": "Maximum concurrent conversations cannot be less than 0.", - "客服组不存在": "Agent team not found.", - "客服组排班不存在": "Agent team schedule not found.", - "知识库不存在": "Knowledge base not found.", - "FAQ不存在": "FAQ not found.", - "FAQ知识库不支持文档": "FAQ knowledge bases do not support documents.", - "文档不存在": "Document not found.", - "内容类型不支持": "Unsupported content type.", - "问题不能为空": "Enter a question.", - "答案不能为空": "Enter an answer.", - "相似问格式不合法": "Invalid similar-question format.", - "当前知识库不是FAQ知识库": "This knowledge base is not an FAQ knowledge base.", - "documentId或faqId不能为空": "Document ID or FAQ ID is required.", - "检索日志不存在": "Retrieval log not found.", - "indexStatus参数不合法": "Invalid index status.", - "AI配置不存在": "AI configuration not found.", - "启用中的AI配置不允许删除": "Enabled AI configurations cannot be deleted.", - "配置名称不能为空": "Enter a configuration name.", - "供应商不能为空": "Select a provider.", - "基础地址不能为空": "Enter a base URL.", - "模型类型不能为空": "Select a model type.", - "模型名称不能为空": "Enter a model name.", - "AI Agent 不存在": "AI Agent not found.", - "AI Agent不存在或未启用": "AI Agent not found or not enabled.", - "AI Agent 不存在或已停用": "AI Agent not found or disabled.", - "AI Agent关联的AI配置不存在": "The AI configuration linked to this AI Agent does not exist.", - "CheckPoint 不存在": "Checkpoint not found.", - "CheckPoint 与 AI Agent 不匹配": "The checkpoint does not belong to this AI Agent.", - "会话与 AI Agent 不匹配": "The conversation does not belong to this AI Agent.", - "Skill 不存在或未启用": "Skill not found or not enabled.", - "Skill 不存在": "Skill not found.", - "Skill ID 不合法": "Invalid Skill ID.", - "Skill 编码已存在": "This Skill code is already in use.", - "Skill 编码不能为空": "Enter a Skill code.", - "Skill 名称不能为空": "Enter a Skill name.", - "技能说明不能为空": "Enter a Skill description.", - "JSON 数组格式不合法": "Invalid JSON array format.", - "已删除的 Skill 不能直接修改状态,请先恢复": "Restore this deleted Skill before changing its status.", - "请使用删除接口处理删除状态": "Use the delete endpoint to mark a Skill as deleted.", - "仅已删除的 Skill 支持恢复": "Only deleted Skills can be restored.", - "MCP未启用": "MCP is not enabled.", - "serverCode不能为空": "Server code is required.", - "MCP endpoint不能为空": "MCP endpoint is required.", - "MCP服务配置不存在": "MCP server configuration not found.", - "MCP服务未启用": "MCP server is not enabled.", - "toolName不能为空": "Tool name is required.", - "toolCode不能为空": "Tool code is required.", - "toolCode格式不合法": "Invalid tool code format.", - "toolCode 绑定的 MCP 服务不存在或未启用": "The MCP server bound to this tool code does not exist or is not enabled.", - "文本内容不能为空": "Text content is required.", - "文本列表不能为空": "Text list is required.", - "密码长度不合法": "Invalid password length.", - "AI Agent 不存在或未启用": "AI Agent not found or not enabled.", - "AI Agent 名称不能为空": "Enter an AI Agent name.", - "AI Agent 名称已存在": "This AI Agent name is already in use.", - "AI 配置不存在": "AI configuration not found.", - "AI 配置不能为空": "Select an AI configuration.", - "AI 配置未启用": "The AI configuration is not enabled.", - "Agent 运行日志不存在": "Agent run log not found.", - "Direct Tool 的 toolCode 与 serverCode 不一致": "Direct Tool toolCode does not match serverCode.", - "Direct Tool 的 toolCode 与 toolName 不一致": "Direct Tool toolCode does not match toolName.", - "Direct Tool 的 toolCode 格式不合法": "Invalid Direct Tool toolCode format.", - "Direct Tool 的 toolCode、serverCode 和 toolName 不能为空": "Direct Tool toolCode, serverCode, and toolName are required.", - "Direct Tools 仅允许配置 MCP 工具": "Direct Tools can only contain MCP tools.", - "Direct Tools 配置格式不合法": "Invalid Direct Tools configuration format.", - "Graph Tools 仅允许配置 Graph Tool": "Graph Tools can only contain Graph tools.", - "Graph Tools 配置格式不合法": "Invalid Graph Tools configuration format.", - "HTML消息中的图片必须使用已上传文件": "Images in HTML messages must use uploaded files.", - "OIDC id_token 不存在": "OIDC id_token is missing.", - "OIDC clientId 未配置": "OIDC clientId is not configured.", - "OIDC clientSecret 未配置": "OIDC clientSecret is not configured.", - "OIDC issuer 未配置": "OIDC issuer is not configured.", - "OIDC redirectUrl 未配置": "OIDC redirectUrl is not configured.", - "OIDC 授权 code 不能为空": "OIDC authorization code is required.", - "OIDC 用户信息不存在": "OIDC user information not found.", - "OIDC 用户标识不存在": "OIDC user identifier not found.", - "OIDC 登录密钥未配置": "OIDC sign-in secret is not configured.", - "OIDC 登录未启用": "OIDC sign-in is not enabled.", - "OIDC 登录状态无效或已过期": "OIDC sign-in state is invalid or has expired.", - "OIDC 账号绑定的系统用户不存在": "The system user linked to this OIDC account does not exist.", - "OSS accessKeyId 未配置": "OSS accessKeyId is not configured.", - "OSS accessKeySecret 未配置": "OSS accessKeySecret is not configured.", - "OSS bucket 未配置": "OSS bucket is not configured.", - "OSS endpoint 未配置": "OSS endpoint is not configured.", - "Skill 未启用": "Skill is not enabled.", - "Web渠道配置 position 只能为 left 或 right": "Web channel position must be left or right.", - "Web渠道配置不合法": "Invalid web channel configuration.", - "aiAgentId不能为空": "AI Agent ID is required.", - "checkPointId不能为空": "Checkpoint ID is required.", - "customerId 必填": "Customer ID is required.", - "openKfID不能为空": "openKfID is required.", - "openKfId 已被其他渠道使用": "This openKfId is already used by another channel.", - "skillCode不能为空": "Skill code is required.", - "ticket 不能为空": "Ticket is required.", - "userMessage不能为空": "User message is required.", - "登录结果不能为空": "Sign-in result is required.", - "不支持的发送人类型": "Unsupported sender type.", - "不支持的已读操作类型": "Unsupported read operation type.", - "不支持的文件存储类型": "Unsupported file storage type.", - "不能添加或修改历史日期的排班": "Schedules cannot be added or changed for past dates.", - "事务上下文不能为空": "Transaction context is required.", - "仅允许撤回自己发送的消息": "You can only recall messages you sent.", - "仅支持撤回客服消息": "Only agent messages can be recalled.", - "仅能指定一条主联系方式": "Only one primary contact method can be set.", - "企业微信 openKfID 不能为空": "WeCom openKfID is required.", - "企业微信媒体ID不能为空": "WeCom media ID is required.", - "企业微信客户ID不能为空": "WeCom customer ID is required.", - "企业微信手机号已被系统用户占用": "This WeCom phone number is already used by a system user.", - "企业微信接入渠道未绑定AI Agent": "The WeCom channel is not linked to an AI Agent.", - "企业微信接入渠道绑定的AI Agent不存在或已禁用": "The AI Agent linked to this WeCom channel does not exist or has been disabled.", - "企业微信未启用或配置不完整": "WeCom is not enabled or its configuration is incomplete.", - "企业微信消息ID不能为空": "WeCom message ID is required.", - "企业微信渠道配置不合法": "Invalid WeCom channel configuration.", - "企业微信渠道配置缺少 openKfId": "WeCom channel configuration is missing openKfId.", - "企业微信登录状态无效": "WeCom sign-in state is invalid.", - "企业微信登录回调地址未配置": "WeCom sign-in callback URL is not configured.", - "企业微信 AgentID 未配置": "WeCom AgentID is not configured.", - "微信授权 code 不能为空": "WeChat authorization code is required.", - "当前登录身份不是企业内部成员": "The current sign-in identity is not an internal enterprise member.", - "企业微信返回的消息ID为空": "WeCom returned an empty message ID.", - "企业微信返回的图片 media_id 为空": "WeCom returned an empty image media_id.", - "图片消息缺少 assetId": "Image message is missing assetId.", - "平台消息不存在": "Platform message not found.", - "文本消息内容为空": "Text message content is empty.", - "HTML 消息内容为空": "HTML message content is empty.", - "企业微信用户ID已被系统用户名占用": "This WeCom user ID is already used as a system username.", - "企业微信用户ID获取失败": "Failed to get the WeCom user ID.", - "企业微信用户信息不存在": "WeCom user information not found.", - "企业微信登录密钥未配置": "WeCom sign-in secret is not configured.", - "企业微信登录未启用": "WeCom sign-in is not enabled.", - "企业微信登录状态无效或已过期": "WeCom sign-in state is invalid or has expired.", - "企业微信账号绑定的系统用户不存在": "The system user linked to this WeCom account does not exist.", - "企业微信邮箱已被系统用户占用": "This WeCom email address is already used by a system user.", - "会话已关闭": "This conversation is closed.", - "会话未分配客服,暂不允许发送消息": "This conversation has not been assigned to an agent, so messages cannot be sent yet.", - "兜底策略不合法": "Invalid fallback policy.", - "分块策略不支持": "Unsupported chunking strategy.", - "创建会话失败": "Failed to create the conversation.", - "单条排班记录不能跨天": "A single schedule entry cannot span multiple days.", - "只有待接入未分配会话允许自动分配": "Only queued unassigned conversations can be auto-assigned.", - "回复超时秒数不能小于 0": "Reply timeout seconds cannot be less than 0.", - "存在冲突排班,请先处理冲突": "There are conflicting schedules. Resolve the conflicts first.", - "存在无效工单标签": "Some ticket tags are invalid.", - "存在未启用的工单标签": "Some ticket tags are disabled.", - "客服会话不能为空": "Support session is required.", - "客服会话参数不完整": "Support session parameters are incomplete.", - "客服会话密钥未配置": "Support session secret is not configured.", - "客服会话已过期": "The support session has expired.", - "客服会话校验失败": "Support session verification failed.", - "客服组下仍有关联 AI Agent,无法删除": "This agent team is linked to AI Agents and cannot be deleted.", - "客服组下仍有关联客服档案,无法删除": "This agent team has linked agent profiles and cannot be deleted.", - "客服组下仍有关联组排班,无法删除": "This agent team has schedules and cannot be deleted.", - "客服组名称不能为空": "Enter an agent team name.", - "客服组名称已存在": "This agent team name is already in use.", - "客服组未启用": "Agent team is not enabled.", - "客服组状态不合法": "Invalid agent team status.", - "已有接入渠道绑定该 AI Agent,无法删除": "This AI Agent is linked to channels and cannot be deleted.", - "当前 OIDC 绑定已停用": "The current OIDC binding has been disabled.", - "当前企业微信绑定已停用": "The current WeCom binding has been disabled.", - "当前会话不处于 AI 接待状态": "This conversation is not currently handled by AI.", - "当前会话已分配给其他客服": "This conversation has been assigned to another agent.", - "当前会话已由人工客服接管": "This conversation has already been taken over by a human agent.", - "当前渠道不支持用户 JWT Secret": "This channel does not support a user JWT secret.", - "当前系统账号已被禁用": "The current system account has been disabled.", - "微信公众号渠道配置不合法": "Invalid WeChat Official Account channel configuration.", - "接入渠道不存在": "Channel not found.", - "接收人不能为空": "Recipient is required.", - "文档知识库不能使用FAQ分块策略": "Document knowledge bases cannot use the FAQ chunking strategy.", - "目标工具未被当前会话授权": "The target tool is not authorized for the current conversation.", - "tool_search 只支持调用 MCP toolCode": "tool_search only supports calling MCP toolCode.", - "时间格式错误": "Invalid time format.", - "星期必须在 1 到 7 之间": "Weekday must be between 1 and 7.", - "服务模式不合法": "Invalid service mode.", - "未找到匹配的企业微信接入渠道": "No matching WeCom channel was found.", - "未生成任何排班": "No schedules were generated.", - "未配置可用的 AI 配置": "No available AI configuration is configured.", - "未配置可用的 Embedding 模型": "No available embedding model is configured.", - "标题和内容不能为空": "Enter both title and content.", - "消息不存在": "Message not found.", - "消息内容不能为空": "Message content is required.", - "消息已撤回": "This message has been recalled.", - "渠道名称不能为空": "Enter a channel name.", - "渠道标识已存在": "This channel code is already in use.", - "渠道状态不合法": "Invalid channel status.", - "渠道类型不合法": "Invalid channel type.", - "用户名称不能为空": "User name is required.", - "用户标识不能为空": "User ID is required.", - "用户身份不能为空": "User identity is required.", - "用户身份已过期": "User identity has expired.", - "用户身份校验失败": "User identity verification failed.", - "用户身份校验未配置": "User identity verification is not configured.", - "登录票据无效或已过期": "The sign-in ticket is invalid or has expired.", - "知识库下存在FAQ,无法删除": "This knowledge base contains FAQs and cannot be deleted.", - "知识库下存在文档,无法删除": "This knowledge base contains documents and cannot be deleted.", - "知识库不能为空": "Knowledge base is required.", - "知识库未启用": "Knowledge base is not enabled.", - "知识库类型不支持": "Unsupported knowledge base type.", - "目录名称不能为空": "Directory name is required.", - "目录不存在": "Directory not found.", - "目录不能移动到其他知识库": "Directories cannot be moved to another knowledge base.", - "存在子目录的目录不能移动到二级目录": "A directory with subdirectories cannot be moved to the second level.", - "同级下已存在相同名称的目录": "A directory with this name already exists at the same level.", - "该目录下存在子目录,无法删除": "This directory contains subdirectories and cannot be deleted.", - "该目录下存在文档,无法删除": "This directory contains documents and cannot be deleted.", - "该目录下存在FAQ,无法删除": "This directory contains FAQs and cannot be deleted.", - "只能调整同知识库同级目录排序": "Only directories in the same knowledge base and same level can be reordered.", - "知识库目录不存在": "Knowledge base directory not found.", - "知识库目录不属于当前知识库": "This directory does not belong to the current knowledge base.", - "知识库目录不可用": "This knowledge base directory is unavailable.", - "不能将目录设为自己的子目录": "A directory cannot be moved under itself.", - "父目录不存在": "Parent directory not found.", - "父目录不属于当前知识库": "The parent directory does not belong to the current knowledge base.", - "知识库目录最多支持二级": "Knowledge base directories support at most two levels.", - "请选择要移动的文档": "Select documents to move.", - "只能移动当前知识库下的文档": "Only documents in the current knowledge base can be moved.", - "请选择要删除的文档": "Select documents to delete.", - "请选择要移动的FAQ": "Select FAQs to move.", - "只能移动当前知识库下的FAQ": "Only FAQs in the current knowledge base can be moved.", - "请选择要删除的FAQ": "Select FAQs to delete.", - "导入模式不合法": "Invalid import mode.", - "请选择导入文件": "Choose a file to import.", - "仅支持.xlsx文件": "Only .xlsx files are supported.", - "导入文件读取失败": "Failed to read the import file.", - "Excel文件解析失败": "Failed to parse the Excel file.", - "Excel文件为空": "The Excel file is empty.", - "Excel文件读取失败": "Failed to read the Excel file.", - "缺少标准问题列": "The standard question column is missing.", - "缺少答案列": "The answer column is missing.", - "标准问题已存在,已跳过": "The standard question already exists and was skipped.", - "问题不能超过500字": "The question cannot exceed 500 characters.", - "组长用户不存在": "Team lead user not found.", - "结束日期必须晚于或等于开始日期": "End date must be later than or equal to start date.", - "结束时间必须晚于开始时间": "End time must be later than start time.", - "联系方式不存在": "Contact method not found.", - "联系方式不能为空": "Contact method is required.", - "联系方式类型不合法": "Invalid contact method type.", - "该客服组在所选时间段已存在排班": "This agent team already has a schedule in the selected time period.", - "该联系方式已存在": "This contact method already exists.", - "请至少选择一个知识库": "Select at least one knowledge base.", - "请选择 AI Agent": "Select an AI Agent.", - "请选择客服组": "Select an agent team.", - "请选择星期": "Select a weekday.", - "转人工模式不合法": "Invalid human handoff mode.", - "通知不存在": "Notification not found.", - "附件不存在": "Attachment not found.", - "附件尚未上传完成": "The attachment has not finished uploading.", - "附件消息 payload 格式错误": "Invalid attachment message payload format.", - "附件消息缺少 assetId": "Attachment message is missing assetId.", - "附件消息缺少 payload": "Attachment message is missing payload.", - "默认客服组待接入池模式必须至少选择一个客服组": "Default team queue mode requires at least one agent team.", - }, -} - -func TranslateKnownMessage(locale string, message string) string { - normalized := NormalizeLocale(locale) - if normalized == LocaleZhCN { - return message - } - if translations, ok := knownMessages[normalized]; ok { - if translated, exists := translations[message]; exists { - return translated - } - } - if translated, ok := translateKnownPattern(normalized, message); ok { - return translated - } - return message -} - -func translateKnownPattern(locale string, message string) (string, bool) { - if locale != LocaleEnUS { - return "", false - } - if matches := maxScheduleBatchMessagePattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("You can generate at most %s schedule entries at a time.", matches[1]), true - } - if matches := knowledgeBaseReferencedByAgentPattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("This knowledge base is referenced by AI Agent %q. Remove the binding first.", matches[1]), true - } - if matches := knowledgeBaseReferencedByAgentsPattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("This knowledge base is referenced by %s AI Agents. Remove the bindings first.", matches[1]), true - } - if matches := agentTeamScheduleConflictPattern.FindStringSubmatch(message); len(matches) == 3 { - return fmt.Sprintf("This agent team already has a schedule from %s to %s.", matches[1], matches[2]), true - } - if matches := knowledgeFAQImportDuplicatePattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("The standard question is duplicated in the same file. It first appeared on row %s.", matches[1]), true - } - if matches := requiredParamMessagePattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("Parameter %q is required.", matches[1]), true - } - if matches := mcpListToolsMessagePattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("Failed to list MCP tools: %s", matches[1]), true - } - if matches := mcpCallToolMessagePattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("Failed to call MCP tool: %s", matches[1]), true - } - if matches := mcpConnectServerMessagePattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("Failed to connect to MCP Server: %s", matches[1]), true - } - if matches := wxworkUnsupportedOutboundPattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("Unsupported WeCom outbound message type: %s", matches[1]), true - } - if matches := wxworkUnsupportedCurrentOutboundPattern.FindStringSubmatch(message); len(matches) == 2 { - return fmt.Sprintf("The current WeCom outbound message type is not supported yet: %s", matches[1]), true - } - if strings.HasPrefix(message, "开始时间格式错误,请使用 ") { - return translateTimeFormatMessage(message, "开始时间格式错误,请使用 ", "start time") - } - if strings.HasPrefix(message, "结束时间格式错误,请使用 ") { - return translateTimeFormatMessage(message, "结束时间格式错误,请使用 ", "end time") - } - return "", false -} - -func translateTimeFormatMessage(message string, prefix string, field string) (string, bool) { - format := strings.TrimPrefix(message, prefix) - if format == message || format == "" { - return "", false - } - format = strings.ReplaceAll(format, " 或 ", " or ") - return fmt.Sprintf("Invalid %s format. Use %s.", field, format), true -} diff --git a/internal/pkg/i18nx/locales.go b/internal/pkg/i18nx/locales.go new file mode 100644 index 0000000..46d9751 --- /dev/null +++ b/internal/pkg/i18nx/locales.go @@ -0,0 +1,97 @@ +package i18nx + +import ( + "embed" + "fmt" + "log/slog" + "path/filepath" + "strings" + "sync" + + "github.com/gin-gonic/gin" + "gopkg.in/yaml.v3" +) + +//go:embed locales/*.yml +var localeFiles embed.FS + +var ( + messagesOnce sync.Once + messages map[string]map[string]string +) + +func T(ctx *gin.Context, key string, args ...any) string { + if ctx == nil { + return Getf(DefaultLocale, key, args...) + } + return Getf(Locale(ctx), key, args...) +} + +func TLocale(locale string, key string, args ...any) string { + return Getf(locale, key, args...) +} + +func Get(key string) string { + return Getf(DefaultLocale, key) +} + +func Getf(locale string, key string, args ...any) string { + format := lookup(NormalizeLocale(locale), key) + if len(args) == 0 { + return format + } + return fmt.Sprintf(format, args...) +} + +func lookup(locale string, key string) string { + loadMessages() + if value := lookupLocale(locale, key); value != "" { + return value + } + if locale != DefaultLocale { + if value := lookupLocale(DefaultLocale, key); value != "" { + return value + } + } + slog.Warn("translation key not found", "key", key, "locale", locale) + return key +} + +func lookupLocale(locale string, key string) string { + if values, ok := messages[locale]; ok { + return values[key] + } + slog.Warn("locale not found", "locale", locale) + return "" +} + +func loadMessages() { + messagesOnce.Do(func() { + loaded := make(map[string]map[string]string) + entries, err := localeFiles.ReadDir("locales") + if err != nil { + slog.Error("read locale files failed", "err", err) + messages = loaded + return + } + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yml") { + continue + } + path := filepath.Join("locales", entry.Name()) + data, err := localeFiles.ReadFile(path) + if err != nil { + slog.Error("read locale file failed", "file", path, "err", err) + continue + } + values := make(map[string]string) + if err := yaml.Unmarshal(data, &values); err != nil { + slog.Error("parse locale file failed", "file", path, "err", err) + continue + } + locale := strings.TrimSuffix(entry.Name(), ".yml") + loaded[NormalizeLocale(locale)] = values + } + messages = loaded + }) +} diff --git a/internal/pkg/i18nx/locales/active.en-US.toml b/internal/pkg/i18nx/locales/active.en-US.toml deleted file mode 100644 index 36d66a0..0000000 --- a/internal/pkg/i18nx/locales/active.en-US.toml +++ /dev/null @@ -1,8 +0,0 @@ -["error.auth.expired"] -other = "Your session has expired. Please sign in again." - -["error.notFound"] -other = "Not found" - -["error.path.invalid"] -other = "Invalid path parameter." diff --git a/internal/pkg/i18nx/locales/active.zh-CN.toml b/internal/pkg/i18nx/locales/active.zh-CN.toml deleted file mode 100644 index 6d7883d..0000000 --- a/internal/pkg/i18nx/locales/active.zh-CN.toml +++ /dev/null @@ -1,8 +0,0 @@ -["error.auth.expired"] -other = "未登录或登录已过期" - -["error.notFound"] -other = "Not found" - -["error.path.invalid"] -other = "路径参数错误" diff --git a/internal/pkg/i18nx/locales/en-US.yml b/internal/pkg/i18nx/locales/en-US.yml new file mode 100644 index 0000000..6d92981 --- /dev/null +++ b/internal/pkg/i18nx/locales/en-US.yml @@ -0,0 +1,358 @@ +error.e0001: "AI Agent not found" +error.e0002: "AI Agent not found." +error.e0003: "AI Agent not found or disabled." +error.e0004: "AI Agent not found or not enabled." +error.e0005: "Enter an AI Agent name." +error.e0006: "This AI Agent name is already in use." +error.e0007: "AI Agent not found or not enabled." +error.e0008: "The AI configuration linked to this AI Agent does not exist." +error.e0009: "AI configuration not found." +error.e0010: "Select an AI configuration." +error.e0011: "The AI configuration is not enabled." +error.e0012: "AI configuration not found." +error.e0013: "Agent run log not found." +error.e0014: "Checkpoint not found." +error.e0015: "The checkpoint does not belong to this AI Agent." +error.e0016: "Direct Tool toolCode does not match serverCode." +error.e0017: "Direct Tool toolCode does not match toolName." +error.e0018: "Invalid Direct Tool toolCode format." +error.e0019: "Direct Tool toolCode, serverCode, and toolName are required." +error.e0020: "Direct Tools can only contain MCP tools." +error.e0021: "Invalid Direct Tools configuration format." +error.e0022: "The Excel file is empty." +error.e0023: "Failed to parse the Excel file." +error.e0024: "Failed to read the Excel file." +error.e0025: "FAQ not found." +error.e0026: "FAQ knowledge bases do not support documents." +error.e0027: "Graph Tools can only contain Graph tools." +error.e0028: "Invalid Graph Tools configuration format." +error.e0029: "HTML message content is empty." +error.e0030: "Images in HTML messages must use uploaded files." +error.e0031: "Invalid JSON array format." +error.e0032: "MCP endpoint is required." +error.e0033: "MCP server is not enabled." +error.e0034: "MCP server configuration not found." +error.e0035: "MCP is not enabled." +error.e0036: "OIDC clientId is not configured." +error.e0037: "OIDC clientSecret is not configured." +error.e0038: "OIDC id_token is missing." +error.e0039: "OIDC issuer is not configured." +error.e0040: "OIDC redirectUrl is not configured." +error.e0041: "OIDC authorization code is required." +error.e0042: "OIDC user information not found." +error.e0043: "OIDC user identifier not found." +error.e0044: "OIDC sign-in secret is not configured." +error.e0045: "OIDC sign-in is not enabled." +error.e0046: "OIDC sign-in state is invalid or has expired." +error.e0047: "The system user linked to this OIDC account does not exist." +error.e0048: "OSS accessKeyId is not configured." +error.e0049: "OSS accessKeySecret is not configured." +error.e0050: "OSS bucket is not configured." +error.e0051: "OSS endpoint is not configured." +error.e0052: "Invalid Skill ID." +error.e0053: "Skill not found." +error.e0054: "Skill not found or not enabled." +error.e0055: "Enter a Skill name." +error.e0056: "Skill is not enabled." +error.e0057: "Enter a Skill code." +error.e0058: "This Skill code is already in use." +error.e0059: "Web channel position must be left or right." +error.e0060: "Invalid web channel configuration." +error.e0061: "AI Agent ID is required." +error.e0062: "Channel not found." +error.e0063: "Checkpoint ID is required." +error.e0064: "Conversation ID is required." +error.e0065: "Customer ID is required." +error.e0066: "Document ID or FAQ ID is required." +error.e0067: "Invalid index status." +error.e0068: "openKfID is required." +error.e0069: "This openKfId is already used by another channel." +error.e0070: "Server code is required." +error.e0071: "Skill code is required." +error.e0072: "Ticket is required." +error.e0073: "The MCP server bound to this tool code does not exist or is not enabled." +error.e0074: "Tool code is required." +error.e0075: "Invalid tool code format." +error.e0076: "Tool name is required." +error.e0077: "tool_search only supports calling MCP toolCode." +error.e0078: "User message is required." +error.e0079: "The uploaded file exceeds the size limit." +error.e0080: "Unsupported sender type." +error.e0081: "Unsupported read operation type." +error.e0082: "Unsupported file storage type." +error.e0083: "A tag cannot be moved under itself." +error.e0084: "A directory cannot be moved under itself." +error.e0085: "Schedules cannot be added or changed for past dates." +error.e0086: "Transaction context is required." +error.e0087: "You can only recall messages you sent." +error.e0088: "Only deleted Skills can be restored." +error.e0089: "Only .xlsx files are supported." +error.e0090: "Only image files are supported." +error.e0091: "Only agent messages can be recalled." +error.e0092: "Only one primary contact method can be set." +error.e0093: "WeCom AgentID is not configured." +error.e0094: "WeCom openKfID is required." +error.e0095: "WeCom media ID is required." +error.e0096: "WeCom customer ID is required." +error.e0097: "This WeCom phone number is already used by a system user." +error.e0098: "The WeCom channel is not linked to an AI Agent." +error.e0099: "The AI Agent linked to this WeCom channel does not exist or has been disabled." +error.e0100: "WeCom is not enabled or its configuration is incomplete." +error.e0101: "WeCom message ID is required." +error.e0102: "Invalid WeCom channel configuration." +error.e0103: "WeCom channel configuration is missing openKfId." +error.e0104: "This WeCom user ID is already used as a system username." +error.e0105: "Failed to get the WeCom user ID." +error.e0106: "WeCom user information not found." +error.e0107: "WeCom sign-in callback URL is not configured." +error.e0108: "WeCom sign-in secret is not configured." +error.e0109: "WeCom sign-in is not enabled." +error.e0110: "WeCom sign-in state is invalid." +error.e0111: "WeCom sign-in state is invalid or has expired." +error.e0112: "The system user linked to this WeCom account does not exist." +error.e0113: "WeCom returned an empty image media_id." +error.e0114: "WeCom returned an empty message ID." +error.e0115: "This WeCom email address is already used by a system user." +error.e0116: "Conversation not found." +error.e0117: "The conversation does not belong to this AI Agent." +error.e0118: "The conversation does not belong to this customer." +error.e0119: "This conversation is closed." +error.e0120: "This conversation has not been assigned to an agent, so messages cannot be sent yet." +error.e0121: "Invalid conversation filter." +error.e0122: "Select a provider." +error.e0123: "Invalid fallback policy." +error.e0124: "Company not found." +error.e0125: "Enter a company name." +error.e0126: "This company name is already in use." +error.e0127: "Linked user not found." +error.e0128: "Enter a close reason." +error.e0129: "Unsupported content type." +error.e0130: "Unsupported chunking strategy." +error.e0131: "Failed to create the conversation." +error.e0132: "A single schedule entry cannot span multiple days." +error.e0133: "Invalid request parameters." +error.e0134: "Only active conversations can be transferred." +error.e0135: "Only queued conversations can be assigned." +error.e0136: "Only queued conversations can be auto-assigned." +error.e0137: "Only queued unassigned conversations can be auto-assigned." +error.e0138: "Only FAQs in the current knowledge base can be moved." +error.e0139: "Only documents in the current knowledge base can be moved." +error.e0140: "Only directories in the same knowledge base and same level can be reordered." +error.e0141: "A tag with this name already exists at the same level." +error.e0142: "A directory with this name already exists at the same level." +error.e0143: "Enabled AI configurations cannot be deleted." +error.e0144: "Reply timeout seconds cannot be less than 0." +error.e0145: "Image message is missing assetId." +error.e0146: "Image asset not found." +error.e0147: "Enter a base URL." +error.e0148: "Enter a progress update." +error.e0149: "External user ID is required." +error.e0150: "External identity is not initialized." +error.e0151: "There are conflicting schedules. Resolve the conflicts first." +error.e0152: "A directory with subdirectories cannot be moved to the second level." +error.e0153: "Some ticket tags are invalid." +error.e0154: "Some ticket tags are disabled." +error.e0155: "Customer not found." +error.e0156: "Enter a customer name." +error.e0157: "Support session is required." +error.e0158: "Support session parameters are incomplete." +error.e0159: "Support session secret is not configured." +error.e0160: "The support session has expired." +error.e0161: "Support session verification failed." +error.e0162: "Enter both agent ID and display name." +error.e0163: "This agent ID is already in use." +error.e0164: "Agent profile not found." +error.e0165: "Invalid agent status." +error.e0166: "This agent team is linked to AI Agents and cannot be deleted." +error.e0167: "This agent team has linked agent profiles and cannot be deleted." +error.e0168: "This agent team has schedules and cannot be deleted." +error.e0169: "Agent team not found." +error.e0170: "Enter an agent team name." +error.e0171: "This agent team name is already in use." +error.e0172: "Agent team schedule not found." +error.e0173: "Agent team is not enabled." +error.e0174: "Invalid agent team status." +error.e0175: "Invalid password length." +error.e0176: "Failed to read the import file." +error.e0177: "Invalid import mode." +error.e0178: "Ticket not found." +error.e0179: "Enter a ticket description." +error.e0180: "Invalid ticket source." +error.e0181: "Enter a ticket title." +error.e0182: "Invalid ticket status." +error.e0183: "Closed conversations cannot be linked to customers." +error.e0184: "Restore this deleted Skill before changing its status." +error.e0185: "This AI Agent is linked to channels and cannot be deleted." +error.e0186: "Platform message not found." +error.e0187: "The current OIDC binding has been disabled." +error.e0188: "The current WeCom binding has been disabled." +error.e0189: "This conversation is not currently handled by AI." +error.e0190: "This conversation has already been assigned." +error.e0191: "This conversation has been assigned to another agent." +error.e0192: "This conversation has already been taken over by a human agent." +error.e0193: "This conversation is not assigned to an agent." +error.e0194: "Human support is currently outside service hours." +error.e0195: "Reading files from this storage provider is not supported yet." +error.e0196: "This channel does not support a user JWT secret." +error.e0197: "This conversation cannot be closed in its current status." +error.e0198: "The current sign-in identity is not an internal enterprise member." +error.e0199: "This knowledge base is not an FAQ knowledge base." +error.e0200: "The current system account has been disabled." +error.e0201: "Invalid WeChat Official Account channel configuration." +error.e0202: "WeChat authorization code is required." +error.e0203: "Quick reply not found." +error.e0204: "Company not found." +error.e0205: "Agent team not found." +error.e0206: "This phone number is already in use." +error.e0207: "Enter a Skill description." +error.e0208: "Channel not found." +error.e0209: "The channel does not exist or has been disabled." +error.e0210: "Channel error." +error.e0211: "The channel is not initialized." +error.e0212: "Recipient is required." +error.e0213: "This file cannot be accessed." +error.e0214: "File not found." +error.e0215: "Text content is required." +error.e0216: "Text list is required." +error.e0217: "Text message content is empty." +error.e0218: "Document not found." +error.e0219: "Document knowledge bases cannot use the FAQ chunking strategy." +error.e0220: "Enter a new password." +error.e0221: "You do not have permission to close this conversation." +error.e0222: "You do not have permission to access this conversation." +error.e0223: "You do not have permission to transfer this conversation." +error.e0224: "You do not have permission to link this conversation." +error.e0225: "You do not have permission to perform this action." +error.e0226: "You do not have permission to perform this action." +error.e0227: "Invalid time format." +error.e0228: "Weekday must be between 1 and 7." +error.e0229: "Maximum concurrent conversations cannot be less than 0." +error.e0230: "Invalid service mode." +error.e0231: "No matching WeCom channel was found." +error.e0232: "No schedules were generated." +error.auth.expired: "Your session has expired. Please sign in again." +error.e0234: "No available AI configuration is configured." +error.e0235: "No available embedding model is configured." +error.e0236: "Permission not found." +error.e0237: "The standard question already exists and was skipped." +error.e0238: "Tag not found." +error.e0239: "Enter a tag name." +error.e0240: "Enter both title and content." +error.e0241: "Retrieval log not found." +error.e0242: "Enter a model name." +error.e0243: "Select a model type." +error.e0244: "Message not found." +error.e0245: "Message content is required." +error.e0246: "This message has been recalled." +error.e0247: "Enter a channel name." +error.e0248: "This channel code is already in use." +error.e0249: "Invalid channel status." +error.e0250: "Invalid channel type." +error.e0251: "Parent tag not found." +error.e0252: "Parent directory not found." +error.e0253: "The parent directory does not belong to the current knowledge base." +error.e0254: "Invalid status value." +error.e0255: "User not found." +error.e0256: "The user does not exist or has been disabled." +error.e0257: "Enter a username." +error.e0258: "Enter both username and password." +error.e0259: "This username is already in use." +error.e0260: "The username or password is incorrect." +error.e0261: "User name is required." +error.e0262: "User ID is required." +error.e0263: "User identity is required." +error.e0264: "User identity has expired." +error.e0265: "User identity verification failed." +error.e0266: "User identity verification is not configured." +error.e0267: "Login credential has been invalidated." +error.e0268: "Login credential has expired." +error.e0269: "Invalid login credential." +error.e0270: "Too many failed sign-in attempts. Please try again later." +error.e0271: "The sign-in ticket is invalid or has expired." +error.e0272: "Sign-in result is required." +error.e0273: "Directory not found." +error.e0274: "Directories cannot be moved to another knowledge base." +error.e0275: "Directory name is required." +error.e0276: "Target agent not found." +error.e0277: "The target agent must be different from the current assignee." +error.e0278: "Select a target agent." +error.e0279: "The target tool is not authorized for the current conversation." +error.e0280: "Invalid similar-question format." +error.e0281: "This knowledge base contains FAQs and cannot be deleted." +error.e0282: "This knowledge base contains documents and cannot be deleted." +error.e0283: "Knowledge base not found." +error.e0284: "Knowledge base is required." +error.e0285: "Knowledge base is not enabled." +error.e0286: "This knowledge base directory is unavailable." +error.e0287: "Knowledge base directory not found." +error.e0288: "This directory does not belong to the current knowledge base." +error.e0289: "Knowledge base directories support at most two levels." +error.e0290: "Unsupported knowledge base type." +error.e0291: "Disabled roles cannot be assigned." +error.e0292: "Enter an answer." +error.e0293: "Built-in system roles cannot be deleted." +error.e0294: "Team lead user not found." +error.e0295: "End date must be later than or equal to start date." +error.e0296: "End time must be later than start time." +error.e0297: "The standard question column is missing." +error.e0298: "The answer column is missing." +error.e0299: "Contact method not found." +error.e0300: "Contact method is required." +error.e0301: "Invalid contact method type." +error.e0302: "View not found." +error.e0303: "Enter a view name." +error.e0304: "The view filter format is invalid." +error.e0305: "Role not found." +error.e0306: "Enter both role name and role code." +error.e0307: "This role is assigned to users and cannot be deleted." +error.e0308: "This role code is already in use." +error.e0309: "This agent team already has a schedule in the selected time period." +error.e0310: "This tag has child tags and cannot be deleted." +error.e0311: "This tag is linked to conversations and cannot be deleted." +error.e0312: "This tag is linked to tickets and cannot be deleted." +error.e0313: "This channel does not support public support configuration." +error.e0314: "This user already has an agent profile." +error.e0315: "This directory contains FAQs and cannot be deleted." +error.e0316: "This directory contains subdirectories and cannot be deleted." +error.e0317: "This directory contains documents and cannot be deleted." +error.e0318: "This contact method already exists." +error.e0319: "Use the delete endpoint to mark a Skill as deleted." +error.e0320: "Select at least one knowledge base." +error.e0321: "Select an AI Agent." +error.e0322: "Choose an image to upload." +error.e0323: "Choose a file to upload." +error.e0324: "Choose an attachment to upload." +error.e0325: "Select a user to link." +error.e0326: "Select an agent team." +error.e0327: "Choose a file to import." +error.e0328: "Select an agent team." +error.e0329: "Select a weekday." +error.e0330: "Select FAQs to delete." +error.e0331: "Select documents to delete." +error.e0332: "Select FAQs to move." +error.e0333: "Select documents to move." +error.e0334: "Assignee not found." +error.path.invalid: "Invalid path parameter." +error.e0336: "Invalid human handoff mode." +error.e0337: "Notification not found." +error.e0338: "This email address is already in use." +error.e0339: "Enter a configuration name." +error.e0340: "Enter a question." +error.e0341: "The question cannot exceed 500 characters." +error.e0342: "Attachment not found." +error.e0343: "The attachment has not finished uploading." +error.e0344: "Invalid attachment message payload format." +error.e0345: "Attachment message is missing assetId." +error.e0346: "Attachment message is missing payload." +error.e0347: "Default team queue mode requires at least one agent team." +error.notFound: "Not found" +error.knowledgeBase.referencedByAgent: "This knowledge base is referenced by AI Agent \"%s\". Remove the binding first." +error.knowledgeBase.referencedByAgents: "This knowledge base is referenced by %d AI Agents. Remove the bindings first." +error.agentTeamSchedule.batchLimit: "You can generate at most %d schedule entries at a time." +error.agentTeamSchedule.conflictRange: "This agent team already has a schedule from %s to %s." +error.knowledgeFAQImport.duplicateQuestionInFile: "The standard question is duplicated in the same file. It first appeared on row %d." +error.mcp.listToolsFailed: "Failed to list MCP tools: %v" +error.mcp.callToolFailed: "Failed to call MCP tool: %v" +error.mcp.connectServerFailed: "Failed to connect to MCP Server: %v" +error.wxwork.unsupportedOutboundMessageType: "Unsupported WeCom outbound message type: %s" +error.wxwork.currentUnsupportedOutboundMessageType: "The current WeCom outbound message type is not supported yet: %s" diff --git a/internal/pkg/i18nx/locales/zh-CN.yml b/internal/pkg/i18nx/locales/zh-CN.yml new file mode 100644 index 0000000..2a4ffcc --- /dev/null +++ b/internal/pkg/i18nx/locales/zh-CN.yml @@ -0,0 +1,358 @@ +error.e0001: "AI Agent not found" +error.e0002: "AI Agent 不存在" +error.e0003: "AI Agent 不存在或已停用" +error.e0004: "AI Agent 不存在或未启用" +error.e0005: "AI Agent 名称不能为空" +error.e0006: "AI Agent 名称已存在" +error.e0007: "AI Agent不存在或未启用" +error.e0008: "AI Agent关联的AI配置不存在" +error.e0009: "AI 配置不存在" +error.e0010: "AI 配置不能为空" +error.e0011: "AI 配置未启用" +error.e0012: "AI配置不存在" +error.e0013: "Agent 运行日志不存在" +error.e0014: "CheckPoint 不存在" +error.e0015: "CheckPoint 与 AI Agent 不匹配" +error.e0016: "Direct Tool 的 toolCode 与 serverCode 不一致" +error.e0017: "Direct Tool 的 toolCode 与 toolName 不一致" +error.e0018: "Direct Tool 的 toolCode 格式不合法" +error.e0019: "Direct Tool 的 toolCode、serverCode 和 toolName 不能为空" +error.e0020: "Direct Tools 仅允许配置 MCP 工具" +error.e0021: "Direct Tools 配置格式不合法" +error.e0022: "Excel文件为空" +error.e0023: "Excel文件解析失败" +error.e0024: "Excel文件读取失败" +error.e0025: "FAQ不存在" +error.e0026: "FAQ知识库不支持文档" +error.e0027: "Graph Tools 仅允许配置 Graph Tool" +error.e0028: "Graph Tools 配置格式不合法" +error.e0029: "HTML 消息内容为空" +error.e0030: "HTML消息中的图片必须使用已上传文件" +error.e0031: "JSON 数组格式不合法" +error.e0032: "MCP endpoint不能为空" +error.e0033: "MCP服务未启用" +error.e0034: "MCP服务配置不存在" +error.e0035: "MCP未启用" +error.e0036: "OIDC clientId 未配置" +error.e0037: "OIDC clientSecret 未配置" +error.e0038: "OIDC id_token 不存在" +error.e0039: "OIDC issuer 未配置" +error.e0040: "OIDC redirectUrl 未配置" +error.e0041: "OIDC 授权 code 不能为空" +error.e0042: "OIDC 用户信息不存在" +error.e0043: "OIDC 用户标识不存在" +error.e0044: "OIDC 登录密钥未配置" +error.e0045: "OIDC 登录未启用" +error.e0046: "OIDC 登录状态无效或已过期" +error.e0047: "OIDC 账号绑定的系统用户不存在" +error.e0048: "OSS accessKeyId 未配置" +error.e0049: "OSS accessKeySecret 未配置" +error.e0050: "OSS bucket 未配置" +error.e0051: "OSS endpoint 未配置" +error.e0052: "Skill ID 不合法" +error.e0053: "Skill 不存在" +error.e0054: "Skill 不存在或未启用" +error.e0055: "Skill 名称不能为空" +error.e0056: "Skill 未启用" +error.e0057: "Skill 编码不能为空" +error.e0058: "Skill 编码已存在" +error.e0059: "Web渠道配置 position 只能为 left 或 right" +error.e0060: "Web渠道配置不合法" +error.e0061: "aiAgentId不能为空" +error.e0062: "channel not found" +error.e0063: "checkPointId不能为空" +error.e0064: "conversationId不能为空" +error.e0065: "customerId 必填" +error.e0066: "documentId或faqId不能为空" +error.e0067: "indexStatus参数不合法" +error.e0068: "openKfID不能为空" +error.e0069: "openKfId 已被其他渠道使用" +error.e0070: "serverCode不能为空" +error.e0071: "skillCode不能为空" +error.e0072: "ticket 不能为空" +error.e0073: "toolCode 绑定的 MCP 服务不存在或未启用" +error.e0074: "toolCode不能为空" +error.e0075: "toolCode格式不合法" +error.e0076: "toolName不能为空" +error.e0077: "tool_search 只支持调用 MCP toolCode" +error.e0078: "userMessage不能为空" +error.e0079: "上传文件超过大小限制" +error.e0080: "不支持的发送人类型" +error.e0081: "不支持的已读操作类型" +error.e0082: "不支持的文件存储类型" +error.e0083: "不能将标签设为自己的子标签" +error.e0084: "不能将目录设为自己的子目录" +error.e0085: "不能添加或修改历史日期的排班" +error.e0086: "事务上下文不能为空" +error.e0087: "仅允许撤回自己发送的消息" +error.e0088: "仅已删除的 Skill 支持恢复" +error.e0089: "仅支持.xlsx文件" +error.e0090: "仅支持上传图片文件" +error.e0091: "仅支持撤回客服消息" +error.e0092: "仅能指定一条主联系方式" +error.e0093: "企业微信 AgentID 未配置" +error.e0094: "企业微信 openKfID 不能为空" +error.e0095: "企业微信媒体ID不能为空" +error.e0096: "企业微信客户ID不能为空" +error.e0097: "企业微信手机号已被系统用户占用" +error.e0098: "企业微信接入渠道未绑定AI Agent" +error.e0099: "企业微信接入渠道绑定的AI Agent不存在或已禁用" +error.e0100: "企业微信未启用或配置不完整" +error.e0101: "企业微信消息ID不能为空" +error.e0102: "企业微信渠道配置不合法" +error.e0103: "企业微信渠道配置缺少 openKfId" +error.e0104: "企业微信用户ID已被系统用户名占用" +error.e0105: "企业微信用户ID获取失败" +error.e0106: "企业微信用户信息不存在" +error.e0107: "企业微信登录回调地址未配置" +error.e0108: "企业微信登录密钥未配置" +error.e0109: "企业微信登录未启用" +error.e0110: "企业微信登录状态无效" +error.e0111: "企业微信登录状态无效或已过期" +error.e0112: "企业微信账号绑定的系统用户不存在" +error.e0113: "企业微信返回的图片 media_id 为空" +error.e0114: "企业微信返回的消息ID为空" +error.e0115: "企业微信邮箱已被系统用户占用" +error.e0116: "会话不存在" +error.e0117: "会话与 AI Agent 不匹配" +error.e0118: "会话与客户不匹配" +error.e0119: "会话已关闭" +error.e0120: "会话未分配客服,暂不允许发送消息" +error.e0121: "会话筛选项不合法" +error.e0122: "供应商不能为空" +error.e0123: "兜底策略不合法" +error.e0124: "公司不存在" +error.e0125: "公司名称不能为空" +error.e0126: "公司名称已存在" +error.e0127: "关联用户不存在" +error.e0128: "关闭原因不能为空" +error.e0129: "内容类型不支持" +error.e0130: "分块策略不支持" +error.e0131: "创建会话失败" +error.e0132: "单条排班记录不能跨天" +error.e0133: "参数不合法" +error.e0134: "只有处理中会话允许转接" +error.e0135: "只有待接入会话允许分配" +error.e0136: "只有待接入会话允许自动分配" +error.e0137: "只有待接入未分配会话允许自动分配" +error.e0138: "只能移动当前知识库下的FAQ" +error.e0139: "只能移动当前知识库下的文档" +error.e0140: "只能调整同知识库同级目录排序" +error.e0141: "同级下已存在相同名称的标签" +error.e0142: "同级下已存在相同名称的目录" +error.e0143: "启用中的AI配置不允许删除" +error.e0144: "回复超时秒数不能小于 0" +error.e0145: "图片消息缺少 assetId" +error.e0146: "图片资源不存在" +error.e0147: "基础地址不能为空" +error.e0148: "处理进展不能为空" +error.e0149: "外部用户标识不能为空" +error.e0150: "外部身份未初始化" +error.e0151: "存在冲突排班,请先处理冲突" +error.e0152: "存在子目录的目录不能移动到二级目录" +error.e0153: "存在无效工单标签" +error.e0154: "存在未启用的工单标签" +error.e0155: "客户不存在" +error.e0156: "客户名称不能为空" +error.e0157: "客服会话不能为空" +error.e0158: "客服会话参数不完整" +error.e0159: "客服会话密钥未配置" +error.e0160: "客服会话已过期" +error.e0161: "客服会话校验失败" +error.e0162: "客服工号和展示名不能为空" +error.e0163: "客服工号已存在" +error.e0164: "客服档案不存在" +error.e0165: "客服状态不合法" +error.e0166: "客服组下仍有关联 AI Agent,无法删除" +error.e0167: "客服组下仍有关联客服档案,无法删除" +error.e0168: "客服组下仍有关联组排班,无法删除" +error.e0169: "客服组不存在" +error.e0170: "客服组名称不能为空" +error.e0171: "客服组名称已存在" +error.e0172: "客服组排班不存在" +error.e0173: "客服组未启用" +error.e0174: "客服组状态不合法" +error.e0175: "密码长度不合法" +error.e0176: "导入文件读取失败" +error.e0177: "导入模式不合法" +error.e0178: "工单不存在" +error.e0179: "工单描述不能为空" +error.e0180: "工单来源不合法" +error.e0181: "工单标题不能为空" +error.e0182: "工单状态不合法" +error.e0183: "已关闭的会话无法关联客户" +error.e0184: "已删除的 Skill 不能直接修改状态,请先恢复" +error.e0185: "已有接入渠道绑定该 AI Agent,无法删除" +error.e0186: "平台消息不存在" +error.e0187: "当前 OIDC 绑定已停用" +error.e0188: "当前企业微信绑定已停用" +error.e0189: "当前会话不处于 AI 接待状态" +error.e0190: "当前会话已分配客服" +error.e0191: "当前会话已分配给其他客服" +error.e0192: "当前会话已由人工客服接管" +error.e0193: "当前会话未分配客服" +error.e0194: "当前暂不在人工客服服务时间内" +error.e0195: "当前暂不支持该存储类型的文件读取" +error.e0196: "当前渠道不支持用户 JWT Secret" +error.e0197: "当前状态不允许关闭会话" +error.e0198: "当前登录身份不是企业内部成员" +error.e0199: "当前知识库不是FAQ知识库" +error.e0200: "当前系统账号已被禁用" +error.e0201: "微信公众号渠道配置不合法" +error.e0202: "微信授权 code 不能为空" +error.e0203: "快捷回复不存在" +error.e0204: "所属公司不存在" +error.e0205: "所属客服组不存在" +error.e0206: "手机号已存在" +error.e0207: "技能说明不能为空" +error.e0208: "接入渠道不存在" +error.e0209: "接入渠道不存在或已停用" +error.e0210: "接入渠道异常" +error.e0211: "接入渠道未初始化" +error.e0212: "接收人不能为空" +error.e0213: "文件不可访问" +error.e0214: "文件不存在" +error.e0215: "文本内容不能为空" +error.e0216: "文本列表不能为空" +error.e0217: "文本消息内容为空" +error.e0218: "文档不存在" +error.e0219: "文档知识库不能使用FAQ分块策略" +error.e0220: "新密码不能为空" +error.e0221: "无权关闭该会话" +error.e0222: "无权访问该会话" +error.e0223: "无权转接该会话" +error.e0224: "无权限关联该会话" +error.e0225: "无权限执行该操作" +error.e0226: "无权限操作" +error.e0227: "时间格式错误" +error.e0228: "星期必须在 1 到 7 之间" +error.e0229: "最大并发接待数不能小于 0" +error.e0230: "服务模式不合法" +error.e0231: "未找到匹配的企业微信接入渠道" +error.e0232: "未生成任何排班" +error.auth.expired: "未登录或登录已过期" +error.e0234: "未配置可用的 AI 配置" +error.e0235: "未配置可用的 Embedding 模型" +error.e0236: "权限不存在" +error.e0237: "标准问题已存在,已跳过" +error.e0238: "标签不存在" +error.e0239: "标签名称不能为空" +error.e0240: "标题和内容不能为空" +error.e0241: "检索日志不存在" +error.e0242: "模型名称不能为空" +error.e0243: "模型类型不能为空" +error.e0244: "消息不存在" +error.e0245: "消息内容不能为空" +error.e0246: "消息已撤回" +error.e0247: "渠道名称不能为空" +error.e0248: "渠道标识已存在" +error.e0249: "渠道状态不合法" +error.e0250: "渠道类型不合法" +error.e0251: "父标签不存在" +error.e0252: "父目录不存在" +error.e0253: "父目录不属于当前知识库" +error.e0254: "状态值不合法" +error.e0255: "用户不存在" +error.e0256: "用户不存在或已被禁用" +error.e0257: "用户名不能为空" +error.e0258: "用户名和密码不能为空" +error.e0259: "用户名已存在" +error.e0260: "用户名或密码错误" +error.e0261: "用户名称不能为空" +error.e0262: "用户标识不能为空" +error.e0263: "用户身份不能为空" +error.e0264: "用户身份已过期" +error.e0265: "用户身份校验失败" +error.e0266: "用户身份校验未配置" +error.e0267: "登录凭证已失效" +error.e0268: "登录凭证已过期" +error.e0269: "登录凭证无效" +error.e0270: "登录失败次数过多,请稍后再试" +error.e0271: "登录票据无效或已过期" +error.e0272: "登录结果不能为空" +error.e0273: "目录不存在" +error.e0274: "目录不能移动到其他知识库" +error.e0275: "目录名称不能为空" +error.e0276: "目标客服不存在" +error.e0277: "目标客服不能与当前指派人相同" +error.e0278: "目标客服不能为空" +error.e0279: "目标工具未被当前会话授权" +error.e0280: "相似问格式不合法" +error.e0281: "知识库下存在FAQ,无法删除" +error.e0282: "知识库下存在文档,无法删除" +error.e0283: "知识库不存在" +error.e0284: "知识库不能为空" +error.e0285: "知识库未启用" +error.e0286: "知识库目录不可用" +error.e0287: "知识库目录不存在" +error.e0288: "知识库目录不属于当前知识库" +error.e0289: "知识库目录最多支持二级" +error.e0290: "知识库类型不支持" +error.e0291: "禁用角色不允许分配" +error.e0292: "答案不能为空" +error.e0293: "系统内置角色不允许删除" +error.e0294: "组长用户不存在" +error.e0295: "结束日期必须晚于或等于开始日期" +error.e0296: "结束时间必须晚于开始时间" +error.e0297: "缺少标准问题列" +error.e0298: "缺少答案列" +error.e0299: "联系方式不存在" +error.e0300: "联系方式不能为空" +error.e0301: "联系方式类型不合法" +error.e0302: "视图不存在" +error.e0303: "视图名称不能为空" +error.e0304: "视图筛选条件格式不正确" +error.e0305: "角色不存在" +error.e0306: "角色名称和编码不能为空" +error.e0307: "角色已被用户使用,无法删除" +error.e0308: "角色编码已存在" +error.e0309: "该客服组在所选时间段已存在排班" +error.e0310: "该标签下存在子标签,无法删除" +error.e0311: "该标签已关联会话,无法删除" +error.e0312: "该标签已关联工单,无法删除" +error.e0313: "该渠道不支持开放客服配置" +error.e0314: "该用户已存在客服档案" +error.e0315: "该目录下存在FAQ,无法删除" +error.e0316: "该目录下存在子目录,无法删除" +error.e0317: "该目录下存在文档,无法删除" +error.e0318: "该联系方式已存在" +error.e0319: "请使用删除接口处理删除状态" +error.e0320: "请至少选择一个知识库" +error.e0321: "请选择 AI Agent" +error.e0322: "请选择上传图片" +error.e0323: "请选择上传文件" +error.e0324: "请选择上传附件" +error.e0325: "请选择关联用户" +error.e0326: "请选择客服组" +error.e0327: "请选择导入文件" +error.e0328: "请选择所属客服组" +error.e0329: "请选择星期" +error.e0330: "请选择要删除的FAQ" +error.e0331: "请选择要删除的文档" +error.e0332: "请选择要移动的FAQ" +error.e0333: "请选择要移动的文档" +error.e0334: "负责人不存在" +error.path.invalid: "路径参数错误" +error.e0336: "转人工模式不合法" +error.e0337: "通知不存在" +error.e0338: "邮箱已存在" +error.e0339: "配置名称不能为空" +error.e0340: "问题不能为空" +error.e0341: "问题不能超过500字" +error.e0342: "附件不存在" +error.e0343: "附件尚未上传完成" +error.e0344: "附件消息 payload 格式错误" +error.e0345: "附件消息缺少 assetId" +error.e0346: "附件消息缺少 payload" +error.e0347: "默认客服组待接入池模式必须至少选择一个客服组" +error.notFound: "数据不存在" +error.knowledgeBase.referencedByAgent: "知识库已被 AI Agent「%s」引用,请先解除绑定" +error.knowledgeBase.referencedByAgents: "知识库已被 %d 个 AI Agent 引用,请先解除绑定" +error.agentTeamSchedule.batchLimit: "单次最多生成 %d 条排班" +error.agentTeamSchedule.conflictRange: "该客服组在 %s 至 %s 已存在排班" +error.knowledgeFAQImport.duplicateQuestionInFile: "同一文件中标准问题重复,首次出现于第%d行" +error.mcp.listToolsFailed: "列出 MCP 工具失败: %v" +error.mcp.callToolFailed: "调用 MCP 工具失败: %v" +error.mcp.connectServerFailed: "连接 MCP Server 失败: %v" +error.wxwork.unsupportedOutboundMessageType: "不支持的企业微信下行消息类型: %s" +error.wxwork.currentUnsupportedOutboundMessageType: "当前暂不支持企业微信下行消息类型: %s" diff --git a/internal/pkg/i18nx/message.go b/internal/pkg/i18nx/message.go deleted file mode 100644 index 2062ed1..0000000 --- a/internal/pkg/i18nx/message.go +++ /dev/null @@ -1,44 +0,0 @@ -package i18nx - -import ( - "github.com/gin-gonic/gin" - "github.com/nicksnyder/go-i18n/v2/i18n" -) - -func T(ctx *gin.Context, messageID string, data map[string]any) string { - if ctx != nil { - if value, exists := ctx.Get(contextLocaleKey); exists { - if locale, ok := value.(string); ok { - return TLocale(locale, messageID, data) - } - } - } - return TLocale(DefaultLocale, messageID, data) -} - -func TLocale(locale string, messageID string, data map[string]any) string { - normalized := NormalizeLocale(locale) - message := localize(normalized, messageID, data) - if message != "" { - return message - } - if normalized != DefaultLocale { - message = localize(DefaultLocale, messageID, data) - if message != "" { - return message - } - } - return messageID -} - -func localize(locale string, messageID string, data map[string]any) string { - localizer := i18n.NewLocalizer(Bundle(), locale, DefaultLocale) - message, err := localizer.Localize(&i18n.LocalizeConfig{ - MessageID: messageID, - TemplateData: data, - }) - if err != nil { - return "" - } - return message -} diff --git a/internal/pkg/openidentity/openidentity.go b/internal/pkg/openidentity/openidentity.go index eb317c9..cb6b365 100644 --- a/internal/pkg/openidentity/openidentity.go +++ b/internal/pkg/openidentity/openidentity.go @@ -44,10 +44,10 @@ func GetExternalUser(ctx *gin.Context, secret string) (*ExternalUser, error) { func verifyUserToken(userToken, secret string) (*UserTokenClaims, error) { if strs.IsBlank(userToken) { - return nil, errorsx.Unauthorized("用户身份不能为空") + return nil, errorsx.UnauthorizedI18n("error.e0263") } if strs.IsBlank(secret) { - return nil, errorsx.Unauthorized("用户身份校验未配置") + return nil, errorsx.UnauthorizedI18n("error.e0266") } claims := &UserTokenClaims{} @@ -63,22 +63,22 @@ func verifyUserToken(userToken, secret string) (*UserTokenClaims, error) { })) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { - return nil, errorsx.Unauthorized("用户身份已过期") + return nil, errorsx.UnauthorizedI18n("error.e0264") } - return nil, errorsx.Unauthorized("用户身份校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0265") } if token == nil || !token.Valid { - return nil, errorsx.Unauthorized("用户身份校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0265") } if strs.IsBlank(claims.UserID) { - return nil, errorsx.Unauthorized("用户标识不能为空") + return nil, errorsx.UnauthorizedI18n("error.e0262") } if strs.IsBlank(claims.Name) { - return nil, errorsx.Unauthorized("用户名称不能为空") + return nil, errorsx.UnauthorizedI18n("error.e0261") } if claims.ExpiresAt == nil { - return nil, errorsx.Unauthorized("用户身份已过期") + return nil, errorsx.UnauthorizedI18n("error.e0264") } return claims, nil @@ -98,7 +98,7 @@ func getUserToken(ctx *gin.Context) string { func getGuestUser(ctx *gin.Context) (*ExternalUser, error) { externalID := getExternalID(ctx) if strs.IsBlank(externalID) { - return nil, errorsx.Unauthorized("用户标识不能为空") + return nil, errorsx.UnauthorizedI18n("error.e0262") } return &ExternalUser{ ExternalSource: enums.ExternalSourceGuest, diff --git a/internal/pkg/toolx/mcp_tool.go b/internal/pkg/toolx/mcp_tool.go index 7f49189..df6311a 100644 --- a/internal/pkg/toolx/mcp_tool.go +++ b/internal/pkg/toolx/mcp_tool.go @@ -38,10 +38,10 @@ func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgen parsedServerCode, parsedToolName := SplitMCPToolCode(toolCode) if parsedServerCode != "" && parsedToolName != "" { if serverCode != "" && !strings.EqualFold(serverCode, parsedServerCode) { - return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 与 serverCode 不一致") + return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0016") } if toolName != "" && !strings.EqualFold(toolName, parsedToolName) { - return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode 与 toolName 不一致") + return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0017") } serverCode = parsedServerCode toolName = parsedToolName @@ -53,14 +53,14 @@ func NormalizeMCPToolRequest(item request.AIAgentMCPToolRequest) (request.AIAgen toolCode = BuildMCPToolCode(serverCode, toolName) } if toolCode == "" { - return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("Direct Tool 的 toolCode、serverCode 和 toolName 不能为空") + 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.InvalidParam("Direct Tool 的 toolCode 格式不合法") + return request.AIAgentMCPToolRequest{}, errorsx.InvalidParamI18n("error.e0018") } ret := request.AIAgentMCPToolRequest{ ToolCode: toolCode, diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index d17b5af..d258567 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -54,7 +54,7 @@ func FormatTime(t time.Time) string { func GenerateRandomPassword(length int) (string, error) { if length <= 0 { - return "", errorsx.InvalidParam("密码长度不合法") + return "", errorsx.InvalidParamI18n("error.e0175") } const charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789" diff --git a/internal/services/agent_profile_service.go b/internal/services/agent_profile_service.go index b703fe1..fedca57 100644 --- a/internal/services/agent_profile_service.go +++ b/internal/services/agent_profile_service.go @@ -88,7 +88,7 @@ func (s *agentProfileService) GetDispatchAgents(teamIds []int64) []models.AgentP func (s *agentProfileService) CreateAgentProfile(req request.CreateAgentProfileRequest, operator *dto.AuthPrincipal) (*models.AgentProfile, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } item, err := s.buildProfileModel(0, req) if err != nil { @@ -104,11 +104,11 @@ func (s *agentProfileService) CreateAgentProfile(req request.CreateAgentProfileR func (s *agentProfileService) UpdateAgentProfile(req request.UpdateAgentProfileRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(req.ID) if current == nil { - return errorsx.InvalidParam("客服档案不存在") + return errorsx.InvalidParamI18n("error.e0164") } item, err := s.buildProfileModel(req.ID, req.CreateAgentProfileRequest) if err != nil { @@ -139,7 +139,7 @@ func (s *agentProfileService) UpdateAgentProfile(req request.UpdateAgentProfileR func (s *agentProfileService) DeleteAgentProfile(id int64) error { current := s.Get(id) if current == nil { - return errorsx.InvalidParam("客服档案不存在") + return errorsx.InvalidParamI18n("error.e0164") } repositories.AgentProfileRepository.Delete(sqls.DB(), id) return nil @@ -147,33 +147,33 @@ func (s *agentProfileService) DeleteAgentProfile(id int64) error { func (s *agentProfileService) buildProfileModel(id int64, req request.CreateAgentProfileRequest) (*models.AgentProfile, error) { if req.UserID <= 0 { - return nil, errorsx.InvalidParam("请选择关联用户") + return nil, errorsx.InvalidParamI18n("error.e0325") } if UserService.Get(req.UserID) == nil { - return nil, errorsx.InvalidParam("关联用户不存在") + return nil, errorsx.InvalidParamI18n("error.e0127") } if req.TeamID <= 0 { - return nil, errorsx.InvalidParam("请选择所属客服组") + return nil, errorsx.InvalidParamI18n("error.e0328") } if AgentTeamService.Get(req.TeamID) == nil { - return nil, errorsx.InvalidParam("所属客服组不存在") + return nil, errorsx.InvalidParamI18n("error.e0205") } req.AgentCode = strings.TrimSpace(req.AgentCode) req.DisplayName = strings.TrimSpace(req.DisplayName) if req.AgentCode == "" || req.DisplayName == "" { - return nil, errorsx.InvalidParam("客服工号和展示名不能为空") + return nil, errorsx.InvalidParamI18n("error.e0162") } if exists := s.Take("user_id = ? AND id <> ?", req.UserID, id); exists != nil { - return nil, errorsx.InvalidParam("该用户已存在客服档案") + return nil, errorsx.InvalidParamI18n("error.e0314") } if exists := s.Take("agent_code = ? AND id <> ?", req.AgentCode, id); exists != nil { - return nil, errorsx.InvalidParam("客服工号已存在") + return nil, errorsx.InvalidParamI18n("error.e0163") } if !enums.IsValidServiceStatus(req.ServiceStatus) { - return nil, errorsx.InvalidParam("客服状态不合法") + return nil, errorsx.InvalidParamI18n("error.e0165") } if req.MaxConcurrentCount < 0 { - return nil, errorsx.InvalidParam("最大并发接待数不能小于 0") + return nil, errorsx.InvalidParamI18n("error.e0229") } return &models.AgentProfile{ UserID: req.UserID, diff --git a/internal/services/agent_team_schedule_service.go b/internal/services/agent_team_schedule_service.go index 8a70cd6..acf7194 100644 --- a/internal/services/agent_team_schedule_service.go +++ b/internal/services/agent_team_schedule_service.go @@ -6,9 +6,9 @@ import ( "agent-desk/internal/pkg/dto/request" "agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/errorsx" + "agent-desk/internal/pkg/i18nx" "agent-desk/internal/pkg/utils" "agent-desk/internal/repositories" - "fmt" "slices" "strings" "sync" @@ -101,7 +101,7 @@ func (s *agentTeamScheduleService) FindCalendarSchedules(req request.AgentTeamSc return nil, err } if !endAtValue.After(startAtValue) { - return nil, errorsx.InvalidParam("结束时间必须晚于开始时间") + return nil, errorsx.InvalidParamI18n("error.e0296") } return repositories.AgentTeamScheduleRepository.FindByTimeRange(sqls.DB(), startAtValue, endAtValue, req.TeamID), nil } @@ -128,7 +128,7 @@ func (s *agentTeamScheduleService) Delete(id int64) { func (s *agentTeamScheduleService) CreateAgentTeamSchedule(req request.CreateAgentTeamScheduleRequest, operator *dto.AuthPrincipal) (*models.AgentTeamSchedule, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } s.writeMu.Lock() item, err := s.buildScheduleModel(0, req.TeamID, req.StartAt, req.EndAt, req.Remark) @@ -148,12 +148,12 @@ func (s *agentTeamScheduleService) CreateAgentTeamSchedule(req request.CreateAge func (s *agentTeamScheduleService) UpdateAgentTeamSchedule(req request.UpdateAgentTeamScheduleRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } s.writeMu.Lock() if s.Get(req.ID) == nil { s.writeMu.Unlock() - return errorsx.InvalidParam("客服组排班不存在") + return errorsx.InvalidParamI18n("error.e0172") } item, err := s.buildScheduleModel(req.ID, req.TeamID, req.StartAt, req.EndAt, req.Remark) if err != nil { @@ -179,7 +179,7 @@ func (s *agentTeamScheduleService) UpdateAgentTeamSchedule(req request.UpdateAge func (s *agentTeamScheduleService) DeleteAgentTeamSchedule(id int64) error { if s.Get(id) == nil { - return errorsx.InvalidParam("客服组排班不存在") + return errorsx.InvalidParamI18n("error.e0172") } repositories.AgentTeamScheduleRepository.Delete(sqls.DB(), id) return nil @@ -187,19 +187,19 @@ func (s *agentTeamScheduleService) DeleteAgentTeamSchedule(id int64) error { func (s *agentTeamScheduleService) BatchPreview(req request.AgentTeamScheduleBatchRequest, operator *dto.AuthPrincipal) (*AgentTeamScheduleBatchPreviewResult, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } candidates, err := s.buildBatchScheduleCandidates(req) if err != nil { return nil, err } - conflicts := s.findBatchConflict(candidates) + conflicts := s.findBatchConflict(candidates, req.Locale) return buildBatchPreviewResult(candidates, conflicts), nil } func (s *agentTeamScheduleService) BatchGenerate(req request.AgentTeamScheduleBatchRequest, operator *dto.AuthPrincipal) (*AgentTeamScheduleBatchGenerateResult, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } s.writeMu.Lock() candidates, err := s.buildBatchScheduleCandidates(req) @@ -207,11 +207,11 @@ func (s *agentTeamScheduleService) BatchGenerate(req request.AgentTeamScheduleBa s.writeMu.Unlock() return nil, err } - conflicts := s.findBatchConflict(candidates) + conflicts := s.findBatchConflict(candidates, req.Locale) for _, conflict := range conflicts { if conflict != "" { s.writeMu.Unlock() - return nil, errorsx.InvalidParam("存在冲突排班,请先处理冲突") + return nil, errorsx.InvalidParamI18n("error.e0151") } } @@ -227,10 +227,10 @@ func (s *agentTeamScheduleService) BatchGenerate(req request.AgentTeamScheduleBa }) } if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { - conflicts := s.findBatchConflictByDB(ctx.Tx, candidates) + conflicts := s.findBatchConflictByDB(ctx.Tx, candidates, req.Locale) for _, conflict := range conflicts { if conflict != "" { - return errorsx.InvalidParam("存在冲突排班,请先处理冲突") + return errorsx.InvalidParamI18n("error.e0151") } } return repositories.AgentTeamScheduleRepository.CreateBatch(ctx.Tx, schedules) @@ -247,14 +247,14 @@ func (s *agentTeamScheduleService) BatchGenerate(req request.AgentTeamScheduleBa func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt, endAt, remark string) (*models.AgentTeamSchedule, error) { if teamID <= 0 { - return nil, errorsx.InvalidParam("请选择客服组") + return nil, errorsx.InvalidParamI18n("error.e0326") } team := AgentTeamService.Get(teamID) if team == nil { - return nil, errorsx.InvalidParam("客服组不存在") + return nil, errorsx.InvalidParamI18n("error.e0169") } if !slices.Contains(enums.StatusValues, team.Status) { - return nil, errorsx.InvalidParam("客服组状态不合法") + return nil, errorsx.InvalidParamI18n("error.e0174") } startAtValue, err := parseRequiredDateTime(startAt, "开始时间格式错误") if err != nil { @@ -265,18 +265,18 @@ func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt, return nil, err } if !endAtValue.After(startAtValue) { - return nil, errorsx.InvalidParam("结束时间必须晚于开始时间") + return nil, errorsx.InvalidParamI18n("error.e0296") } if !sameLocalDay(startAtValue, endAtValue) { - return nil, errorsx.InvalidParam("单条排班记录不能跨天") + return nil, errorsx.InvalidParamI18n("error.e0132") } if startAtValue.Before(startOfLocalDay(time.Now())) { - return nil, errorsx.InvalidParam("不能添加或修改历史日期的排班") + return nil, errorsx.InvalidParamI18n("error.e0085") } overlapping := repositories.AgentTeamScheduleRepository.FindOverlappingByTeamIDsAndTimeRange(sqls.DB(), []int64{teamID}, startAtValue, endAtValue) for _, item := range overlapping { if item.ID != id { - return nil, errorsx.InvalidParam("该客服组在所选时间段已存在排班") + return nil, errorsx.InvalidParamI18n("error.e0309") } } return &models.AgentTeamSchedule{ @@ -290,7 +290,7 @@ func (s *agentTeamScheduleService) buildScheduleModel(id, teamID int64, startAt, func (s *agentTeamScheduleService) buildBatchScheduleCandidates(req request.AgentTeamScheduleBatchRequest) ([]batchScheduleCandidate, error) { teamIDs := uniquePositiveInt64s(req.TeamIDs) if len(teamIDs) == 0 { - return nil, errorsx.InvalidParam("请选择客服组") + return nil, errorsx.InvalidParamI18n("error.e0326") } weekdays, err := normalizeBatchWeekdays(req.Weekdays) if err != nil { @@ -305,10 +305,10 @@ func (s *agentTeamScheduleService) buildBatchScheduleCandidates(req request.Agen return nil, err } if endDate.Before(startDate) { - return nil, errorsx.InvalidParam("结束日期必须晚于或等于开始日期") + return nil, errorsx.InvalidParamI18n("error.e0295") } if startDate.Before(startOfLocalDay(time.Now())) { - return nil, errorsx.InvalidParam("不能添加或修改历史日期的排班") + return nil, errorsx.InvalidParamI18n("error.e0085") } startClock, err := parseRequiredClock(req.StartTime, "开始时间格式错误") if err != nil { @@ -321,7 +321,7 @@ func (s *agentTeamScheduleService) buildBatchScheduleCandidates(req request.Agen firstStartAt := combineDateAndClock(startDate, startClock) firstEndAt := combineDateAndClock(startDate, endClock) if !firstEndAt.After(firstStartAt) { - return nil, errorsx.InvalidParam("结束时间必须晚于开始时间") + return nil, errorsx.InvalidParamI18n("error.e0296") } teams := AgentTeamService.FindByIds(teamIDs) @@ -332,10 +332,10 @@ func (s *agentTeamScheduleService) buildBatchScheduleCandidates(req request.Agen for _, teamID := range teamIDs { team, ok := teamsByID[teamID] if !ok || team.Status == enums.StatusDeleted { - return nil, errorsx.InvalidParam("客服组不存在") + return nil, errorsx.InvalidParamI18n("error.e0169") } if !slices.Contains(enums.StatusValues, team.Status) { - return nil, errorsx.InvalidParam("客服组状态不合法") + return nil, errorsx.InvalidParamI18n("error.e0174") } } @@ -352,7 +352,7 @@ func (s *agentTeamScheduleService) buildBatchScheduleCandidates(req request.Agen continue } if len(candidates) >= maxAgentTeamScheduleBatchItems { - return nil, errorsx.InvalidParam(fmt.Sprintf("单次最多生成 %d 条排班", maxAgentTeamScheduleBatchItems)) + return nil, errorsx.InvalidParamI18n("error.agentTeamSchedule.batchLimit", maxAgentTeamScheduleBatchItems) } candidates = append(candidates, batchScheduleCandidate{ TeamID: teamID, @@ -365,7 +365,7 @@ func (s *agentTeamScheduleService) buildBatchScheduleCandidates(req request.Agen } } if len(candidates) == 0 { - return nil, errorsx.InvalidParam("未生成任何排班") + return nil, errorsx.InvalidParamI18n("error.e0232") } return candidates, nil } @@ -430,11 +430,11 @@ func buildBatchPreviewResult(candidates []batchScheduleCandidate, conflicts map[ } } -func (s *agentTeamScheduleService) findBatchConflict(candidates []batchScheduleCandidate) map[int]string { - return s.findBatchConflictByDB(sqls.DB(), candidates) +func (s *agentTeamScheduleService) findBatchConflict(candidates []batchScheduleCandidate, locale string) map[int]string { + return s.findBatchConflictByDB(sqls.DB(), candidates, locale) } -func (s *agentTeamScheduleService) findBatchConflictByDB(db *gorm.DB, candidates []batchScheduleCandidate) map[int]string { +func (s *agentTeamScheduleService) findBatchConflictByDB(db *gorm.DB, candidates []batchScheduleCandidate, locale string) map[int]string { conflicts := make(map[int]string) if len(candidates) == 0 { return conflicts @@ -458,7 +458,7 @@ func (s *agentTeamScheduleService) findBatchConflictByDB(db *gorm.DB, candidates continue } if item.StartAt.Before(candidate.EndAt) && item.EndAt.After(candidate.StartAt) { - conflicts[i] = fmt.Sprintf("该客服组在 %s 至 %s 已存在排班", item.StartAt.Format(time.DateTime), item.EndAt.Format(time.DateTime)) + conflicts[i] = i18nx.Getf(locale, "error.agentTeamSchedule.conflictRange", item.StartAt.Format(time.DateTime), item.EndAt.Format(time.DateTime)) break } } @@ -471,7 +471,7 @@ func normalizeBatchWeekdays(values []int) ([]int, error) { ret := make([]int, 0, len(values)) for _, value := range values { if value < 1 || value > 7 { - return nil, errorsx.InvalidParam("星期必须在 1 到 7 之间") + return nil, errorsx.InvalidParamI18n("error.e0228") } if _, ok := seen[value]; ok { continue @@ -480,7 +480,7 @@ func normalizeBatchWeekdays(values []int) ([]int, error) { ret = append(ret, value) } if len(ret) == 0 { - return nil, errorsx.InvalidParam("请选择星期") + return nil, errorsx.InvalidParamI18n("error.e0329") } return ret, nil } @@ -532,7 +532,7 @@ func parseDateTimeValue(value string) (time.Time, error) { return ret, nil } } - return time.Time{}, errorsx.InvalidParam("时间格式错误") + return time.Time{}, errorsx.InvalidParamI18n("error.e0227") } func startOfLocalDay(value time.Time) time.Time { diff --git a/internal/services/agent_team_service.go b/internal/services/agent_team_service.go index 9a0342e..643214e 100644 --- a/internal/services/agent_team_service.go +++ b/internal/services/agent_team_service.go @@ -79,7 +79,7 @@ func (s *agentTeamService) Delete(id int64) { func (s *agentTeamService) CreateAgentTeam(req request.CreateAgentTeamRequest, operator *dto.AuthPrincipal) (*models.AgentTeam, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } item, err := s.buildTeamModel(0, req.Name, req.LeaderUserID, req.Status, req.Description, req.Remark) if err != nil { @@ -94,11 +94,11 @@ func (s *agentTeamService) CreateAgentTeam(req request.CreateAgentTeamRequest, o func (s *agentTeamService) UpdateAgentTeam(req request.UpdateAgentTeamRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(req.ID) if current == nil || current.Status == enums.StatusDeleted { - return errorsx.InvalidParam("客服组不存在") + return errorsx.InvalidParamI18n("error.e0169") } item, err := s.buildTeamModel(req.ID, req.Name, req.LeaderUserID, req.Status, req.Description, req.Remark) if err != nil { @@ -119,17 +119,17 @@ func (s *agentTeamService) UpdateAgentTeam(req request.UpdateAgentTeamRequest, o func (s *agentTeamService) DeleteAgentTeam(id int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(id) if current == nil || current.Status == enums.StatusDeleted { - return errorsx.InvalidParam("客服组不存在") + return errorsx.InvalidParamI18n("error.e0169") } if AgentProfileService.Take("team_id = ?", id) != nil { - return errorsx.Forbidden("客服组下仍有关联客服档案,无法删除") + return errorsx.ForbiddenI18n("error.e0167") } if AgentTeamScheduleService.Take("team_id = ?", id) != nil { - return errorsx.Forbidden("客服组下仍有关联组排班,无法删除") + return errorsx.ForbiddenI18n("error.e0168") } if AIAgentService.Take( "(team_ids = ? OR team_ids LIKE ? OR team_ids LIKE ? OR team_ids LIKE ?) AND status <> ?", @@ -139,7 +139,7 @@ func (s *agentTeamService) DeleteAgentTeam(id int64, operator *dto.AuthPrincipal "%,"+utils.JoinInt64s([]int64{id})+",%", enums.StatusDeleted, ) != nil { - return errorsx.Forbidden("客服组下仍有关联 AI Agent,无法删除") + return errorsx.ForbiddenI18n("error.e0166") } return repositories.AgentTeamRepository.Updates(sqls.DB(), id, map[string]any{ "status": enums.StatusDeleted, @@ -152,16 +152,16 @@ func (s *agentTeamService) DeleteAgentTeam(id int64, operator *dto.AuthPrincipal func (s *agentTeamService) buildTeamModel(id int64, name string, leaderUserID int64, status int, description, remark string) (*models.AgentTeam, error) { name = strings.TrimSpace(name) if name == "" { - return nil, errorsx.InvalidParam("客服组名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0170") } if exists := s.Take("name = ? AND status <> ? AND id <> ?", name, enums.StatusDeleted, id); exists != nil { - return nil, errorsx.InvalidParam("客服组名称已存在") + return nil, errorsx.InvalidParamI18n("error.e0171") } if leaderUserID > 0 && UserService.Get(leaderUserID) == nil { - return nil, errorsx.InvalidParam("组长用户不存在") + return nil, errorsx.InvalidParamI18n("error.e0294") } if status != 0 && status != 1 { - return nil, errorsx.InvalidParam("客服组状态不合法") + return nil, errorsx.InvalidParamI18n("error.e0174") } return &models.AgentTeam{ Name: name, diff --git a/internal/services/ai_agent_service.go b/internal/services/ai_agent_service.go index 9d682ce..724f602 100644 --- a/internal/services/ai_agent_service.go +++ b/internal/services/ai_agent_service.go @@ -66,7 +66,7 @@ func (s *aIAgentService) FindByIds(ids []int64) []models.AIAgent { func (s *aIAgentService) CreateAIAgent(req request.CreateAIAgentRequest, operator *dto.AuthPrincipal) (*models.AIAgent, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } item, err := s.buildAIAgentModel(0, req) if err != nil { @@ -83,10 +83,10 @@ func (s *aIAgentService) CreateAIAgent(req request.CreateAIAgentRequest, operato func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } if s.Get(req.ID) == nil { - return errorsx.InvalidParam("AI Agent 不存在") + return errorsx.InvalidParamI18n("error.e0002") } item, err := s.buildAIAgentModel(req.ID, req.CreateAIAgentRequest) if err != nil { @@ -117,10 +117,10 @@ func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operato func (s *aIAgentService) DeleteAIAgent(id int64, operator *dto.AuthPrincipal) error { current := s.Get(id) if current == nil { - return errorsx.InvalidParam("AI Agent 不存在") + return errorsx.InvalidParamI18n("error.e0002") } if ChannelService.Take("ai_agent_id = ?", id) != nil { - return errorsx.Forbidden("已有接入渠道绑定该 AI Agent,无法删除") + return errorsx.ForbiddenI18n("error.e0185") } return repositories.AIAgentRepository.Updates(sqls.DB(), id, map[string]any{ "status": enums.StatusDeleted, @@ -133,23 +133,23 @@ func (s *aIAgentService) DeleteAIAgent(id int64, operator *dto.AuthPrincipal) er func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRequest) (*models.AIAgent, error) { name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("AI Agent 名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0005") } if exists := s.Take("name = ? AND id <> ?", name, id); exists != nil { - return nil, errorsx.InvalidParam("AI Agent 名称已存在") + return nil, errorsx.InvalidParamI18n("error.e0006") } if req.AIConfigID <= 0 { - return nil, errorsx.InvalidParam("AI 配置不能为空") + return nil, errorsx.InvalidParamI18n("error.e0010") } aiConfig := AIConfigService.Get(req.AIConfigID) if aiConfig == nil { - return nil, errorsx.InvalidParam("AI 配置不存在") + return nil, errorsx.InvalidParamI18n("error.e0009") } if aiConfig.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("AI 配置未启用") + return nil, errorsx.InvalidParamI18n("error.e0011") } if !slices.Contains(enums.IMConversationServiceModeValues, req.ServiceMode) { - return nil, errorsx.InvalidParam("服务模式不合法") + return nil, errorsx.InvalidParamI18n("error.e0230") } teamIDs, err := s.normalizeTeamIDs(req.TeamIDs) if err != nil { @@ -157,19 +157,19 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe } if !slices.Contains(enums.AIAgentHandoffModeValues, enums.AIAgentHandoffMode(req.HandoffMode)) { - return nil, errorsx.InvalidParam("转人工模式不合法") + return nil, errorsx.InvalidParamI18n("error.e0336") } if req.FallbackMode == 0 { req.FallbackMode = enums.AIAgentFallbackModeNoAnswer } if !slices.Contains(enums.AIAgentFallbackModeValues, enums.AIAgentFallbackMode(req.FallbackMode)) { - return nil, errorsx.InvalidParam("兜底策略不合法") + return nil, errorsx.InvalidParamI18n("error.e0123") } if enums.AIAgentHandoffMode(req.HandoffMode) == enums.AIAgentHandoffModeDefaultTeamPool && len(teamIDs) == 0 { - return nil, errorsx.InvalidParam("默认客服组待接入池模式必须至少选择一个客服组") + return nil, errorsx.InvalidParamI18n("error.e0347") } if req.ReplyTimeoutSeconds < 0 { - return nil, errorsx.InvalidParam("回复超时秒数不能小于 0") + return nil, errorsx.InvalidParamI18n("error.e0144") } knowledgeIDs, err := s.normalizeKnowledgeIDs(req.KnowledgeIDs) @@ -177,7 +177,7 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe return nil, err } if len(knowledgeIDs) == 0 { - return nil, errorsx.InvalidParam("请至少选择一个知识库") + return nil, errorsx.InvalidParamI18n("error.e0320") } skillIDs, err := s.normalizeSkillIDs(req.SkillIDs) if err != nil { @@ -195,7 +195,7 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe if len(directTools) > 0 { buf, marshalErr := json.Marshal(directTools) if marshalErr != nil { - return nil, errorsx.InvalidParam("Direct Tools 配置格式不合法") + return nil, errorsx.InvalidParamI18n("error.e0021") } directToolsJSON = string(buf) } @@ -203,7 +203,7 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe if len(graphTools) > 0 { buf, marshalErr := json.Marshal(graphTools) if marshalErr != nil { - return nil, errorsx.InvalidParam("Graph Tools 配置格式不合法") + return nil, errorsx.InvalidParamI18n("error.e0028") } graphToolsJSON = string(buf) } @@ -241,7 +241,7 @@ func (s *aIAgentService) normalizeTeamIDs(input []int64) ([]int64, error) { continue } // if team.Status != enums.StatusOk { - // return nil, errorsx.InvalidParam("客服组未启用") + // return nil, errorsx.InvalidParamI18n("error.e0173") // } seen[id] = struct{}{} ret = append(ret, id) @@ -265,7 +265,7 @@ func (s *aIAgentService) normalizeKnowledgeIDs(input []int64) ([]int64, error) { continue } // if kb.Status != enums.StatusOk { - // return nil, errorsx.InvalidParam("知识库未启用") + // return nil, errorsx.InvalidParamI18n("error.e0285") // } seen[id] = struct{}{} ret = append(ret, id) @@ -288,7 +288,7 @@ func (s *aIAgentService) normalizeSkillIDs(input []int64) ([]int64, error) { continue } // if skill.Status != enums.StatusOk { - // return nil, errorsx.InvalidParam("Skill 未启用") + // return nil, errorsx.InvalidParamI18n("error.e0056") // } seen[id] = struct{}{} ret = append(ret, id) @@ -311,7 +311,7 @@ func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequ continue } if toolx.ResolveToolSourceType(normalized.ToolCode) != enums.ToolSourceTypeMCP { - return nil, errorsx.InvalidParam("Direct Tools 仅允许配置 MCP 工具") + return nil, errorsx.InvalidParamI18n("error.e0020") } if err := ToolCatalogService.ValidateToolCode(normalized.ToolCode); err != nil { return nil, err @@ -338,7 +338,7 @@ func (s *aIAgentService) normalizeGraphTools(input []string) ([]string, error) { continue } if !toolx.IsAgentDirectGraphToolCode(toolCode) { - return nil, errorsx.InvalidParam("Graph Tools 仅允许配置 Graph Tool") + return nil, errorsx.InvalidParamI18n("error.e0027") } if _, exists := seen[toolCode]; exists { continue @@ -362,14 +362,14 @@ func (s *aIAgentService) UpdateSort(ids []int64) error { func (s *aIAgentService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(id) if current == nil { - return errorsx.InvalidParam("AI Agent 不存在") + return errorsx.InvalidParamI18n("error.e0002") } if status != int(enums.StatusOk) && status != int(enums.StatusDisabled) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } return repositories.AIAgentRepository.Updates(sqls.DB(), id, map[string]any{ diff --git a/internal/services/ai_config_service.go b/internal/services/ai_config_service.go index 488367f..224ccf0 100644 --- a/internal/services/ai_config_service.go +++ b/internal/services/ai_config_service.go @@ -77,7 +77,7 @@ func (s *aIConfigService) Delete(id int64) { func (s *aIConfigService) CreateAIConfig(req request.CreateAIConfigRequest, operator *dto.AuthPrincipal) (*models.AIConfig, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } item, err := s.buildAIConfigModel(req) if err != nil { @@ -100,11 +100,11 @@ func (s *aIConfigService) CreateAIConfig(req request.CreateAIConfigRequest, oper func (s *aIConfigService) UpdateAIConfig(req request.UpdateAIConfigRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(req.ID) if current == nil { - return errorsx.InvalidParam("AI配置不存在") + return errorsx.InvalidParamI18n("error.e0012") } item, err := s.buildAIConfigModel(req.CreateAIConfigRequest) if err != nil { @@ -141,7 +141,7 @@ func (s *aIConfigService) DeleteAIConfig(id int64, operator *dto.AuthPrincipal) return nil } if current.Status == enums.StatusOk { - return errorsx.Forbidden("启用中的AI配置不允许删除") + return errorsx.ForbiddenI18n("error.e0143") } return repositories.AIConfigRepository.Updates(sqls.DB(), id, map[string]any{ "status": enums.StatusDeleted, @@ -153,14 +153,14 @@ func (s *aIConfigService) DeleteAIConfig(id int64, operator *dto.AuthPrincipal) func (s *aIConfigService) UpdateStatus(id int64, status enums.Status, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(id) if current == nil { - return errorsx.InvalidParam("AI配置不存在") + return errorsx.InvalidParamI18n("error.e0012") } if status != enums.StatusOk && status != enums.StatusDisabled { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } return sqls.WithTransaction(func(ctx *sqls.TxContext) error { @@ -206,19 +206,19 @@ func (s *aIConfigService) buildAIConfigModel(req request.CreateAIConfigRequest) modelName := strings.TrimSpace(req.ModelName) if name == "" { - return nil, errorsx.InvalidParam("配置名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0339") } if strs.IsBlank(string(req.Provider)) { - return nil, errorsx.InvalidParam("供应商不能为空") + return nil, errorsx.InvalidParamI18n("error.e0122") } if baseURL == "" { - return nil, errorsx.InvalidParam("基础地址不能为空") + return nil, errorsx.InvalidParamI18n("error.e0147") } if strs.IsBlank(string(req.ModelType)) { - return nil, errorsx.InvalidParam("模型类型不能为空") + return nil, errorsx.InvalidParamI18n("error.e0243") } if modelName == "" { - return nil, errorsx.InvalidParam("模型名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0242") } if req.Dimension < 0 { req.Dimension = 0 diff --git a/internal/services/asset_service.go b/internal/services/asset_service.go index 77eda39..b7596be 100644 --- a/internal/services/asset_service.go +++ b/internal/services/asset_service.go @@ -48,7 +48,7 @@ func (s *assetService) FindPageByCnd(cnd *sqls.Cnd) (list []models.Asset, paging func (s *assetService) OpenReader(asset *models.Asset) (io.ReadCloser, error) { cfg := config.Current() if asset == nil { - return nil, errorsx.InvalidParam("图片资源不存在") + return nil, errorsx.InvalidParamI18n("error.e0146") } switch asset.Provider { case "", enums.AssetProviderLocal: @@ -56,7 +56,7 @@ func (s *assetService) OpenReader(asset *models.Asset) (io.ReadCloser, error) { case enums.AssetProviderOSS: return storage.NewOSSStorage(cfg.Storage.OSS).Read(asset.StorageKey) default: - return nil, errorsx.InvalidParam("当前暂不支持该存储类型的文件读取") + return nil, errorsx.InvalidParamI18n("error.e0195") } } @@ -73,12 +73,12 @@ func (s *assetService) UploadBytes(data []byte, prefix, filename string, princip func (s *assetService) UploadFile(file *multipart.FileHeader, prefix string, principal *dto.AuthPrincipal) (*models.Asset, error) { if file == nil { - return nil, errorsx.InvalidParam("请选择上传文件") + return nil, errorsx.InvalidParamI18n("error.e0323") } cfg := config.Current() if file.Size > cfg.Storage.MaxUploadSizeBytes() { - return nil, errorsx.InvalidParam("上传文件超过大小限制") + return nil, errorsx.InvalidParamI18n("error.e0079") } src, err := file.Open() @@ -137,10 +137,10 @@ func (s *assetService) Upload(reader io.Reader, info storage.UploadInfo) (*model func (s *assetService) GetSignedURL(id int64) (string, error) { item := s.Get(id) if item == nil { - return "", errorsx.InvalidParam("文件不存在") + return "", errorsx.InvalidParamI18n("error.e0214") } if item.Status != enums.AssetStatusSuccess { - return "", errorsx.InvalidParam("文件不可访问") + return "", errorsx.InvalidParamI18n("error.e0213") } provider, err := storage.NewProvider(item.Provider) @@ -153,11 +153,11 @@ func (s *assetService) GetSignedURL(id int64) (string, error) { func (s *assetService) DeleteAsset(id int64, principal *dto.AuthPrincipal) error { if principal == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(id) if item == nil { - return errorsx.InvalidParam("文件不存在") + return errorsx.InvalidParamI18n("error.e0214") } return repositories.AssetRepository.Updates(sqls.DB(), id, map[string]any{ "status": enums.AssetStatusDeleted, diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 1fc6c87..829202c 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -70,11 +70,11 @@ func (s *authService) RequirePermission(ctx *gin.Context, permission constants.P } if principal == nil { - return nil, errorsx.Forbidden("无权限执行该操作") + return nil, errorsx.ForbiddenI18n("error.e0225") } if !s.HasPermission(ctx, permission.Code) { - return principal, errorsx.Forbidden("无权限执行该操作") + return principal, errorsx.ForbiddenI18n("error.e0225") } return principal, nil } @@ -84,22 +84,22 @@ func (s *authService) Login(req request.LoginRequest, authCfg config.AuthConfig, principal := normalizeLoginPrincipal(username) password := req.Password if username == "" || strings.TrimSpace(password) == "" { - return nil, errorsx.InvalidParam("用户名和密码不能为空") + return nil, errorsx.InvalidParamI18n("error.e0258") } if s.isCredentialLocked(principal, authCfg) { _ = s.createLoginCredentialLog(principal, 0, false, clientIP, userAgent, "credential locked") - return nil, errorsx.CredentialLocked("登录失败次数过多,请稍后再试") + return nil, errorsx.CredentialLockedI18n("error.e0270") } user := UserService.GetByUsername(username) if user == nil || user.Status != enums.StatusOk { _ = s.createLoginCredentialLog(principal, 0, false, clientIP, userAgent, "user not found") - return nil, errorsx.InvalidAccount("用户名或密码错误") + return nil, errorsx.InvalidAccountI18n("error.e0260") } if strs.IsBlank(user.Password) || bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil { _ = s.createLoginCredentialLog(principal, user.ID, false, clientIP, userAgent, "password mismatch") - return nil, errorsx.InvalidAccount("用户名或密码错误") + return nil, errorsx.InvalidAccountI18n("error.e0260") } var ret *response.LoginResponse @@ -153,7 +153,7 @@ func (s *authService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error) token = strings.TrimSpace(ctx.Query("accessToken")) } if token == "" { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } session, err := s.validateSessionToken(token) @@ -163,7 +163,7 @@ func (s *authService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error) user := UserService.Get(session.UserID) if user == nil || user.Status != enums.StatusOk { - return nil, errorsx.Unauthorized("用户不存在或已被禁用") + return nil, errorsx.UnauthorizedI18n("error.e0256") } roles, permissions, err := s.loadUserAuthScope(sqls.DB(), user.ID) @@ -276,17 +276,17 @@ func (s *authService) resolveTokenTTL(authCfg config.AuthConfig) time.Duration { func (s *authService) validateSessionToken(token string) (*models.LoginSession, error) { if strings.TrimSpace(token) == "" { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } session := LoginSessionService.FindOne(sqls.NewCnd().Eq("token", token)) if session == nil { - return nil, errorsx.InvalidToken("登录凭证无效") + return nil, errorsx.InvalidTokenI18n("error.e0269") } if session.RevokedAt != nil { - return nil, errorsx.InvalidToken("登录凭证已失效") + return nil, errorsx.InvalidTokenI18n("error.e0267") } if time.Now().After(session.ExpiredAt) { - return nil, errorsx.InvalidToken("登录凭证已过期") + return nil, errorsx.InvalidTokenI18n("error.e0268") } return session, nil } diff --git a/internal/services/channel_service.go b/internal/services/channel_service.go index b3ea75a..c278a9c 100644 --- a/internal/services/channel_service.go +++ b/internal/services/channel_service.go @@ -80,7 +80,7 @@ func (s *channelService) UpdateColumn(id int64, name string, value interface{}) func (s *channelService) CreateChannel(req request.CreateChannelRequest, operator *dto.AuthPrincipal) (*models.Channel, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } item, err := s.buildChannelModel(0, req) if err != nil { @@ -95,11 +95,11 @@ func (s *channelService) CreateChannel(req request.CreateChannelRequest, operato func (s *channelService) UpdateChannel(req request.UpdateChannelRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(req.ID) if current == nil || current.Status == enums.StatusDeleted { - return errorsx.InvalidParam("接入渠道不存在") + return errorsx.InvalidParamI18n("error.e0208") } item, err := s.buildChannelModel(req.ID, req.CreateChannelRequest) if err != nil { @@ -121,14 +121,14 @@ func (s *channelService) UpdateChannel(req request.UpdateChannelRequest, operato func (s *channelService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(id) if item == nil || item.Status == enums.StatusDeleted { - return errorsx.InvalidParam("接入渠道不存在") + return errorsx.InvalidParamI18n("error.e0208") } if status != int(enums.StatusOk) && status != int(enums.StatusDisabled) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } return s.Updates(id, map[string]any{ "status": status, @@ -140,11 +140,11 @@ func (s *channelService) UpdateStatus(id int64, status int, operator *dto.AuthPr func (s *channelService) DeleteChannel(id int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(id) if item == nil || item.Status == enums.StatusDeleted { - return errorsx.InvalidParam("接入渠道不存在") + return errorsx.InvalidParamI18n("error.e0208") } return s.Updates(id, map[string]any{ "status": enums.StatusDeleted, @@ -233,7 +233,7 @@ func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfi cfg.Position = "right" } if cfg.Position != "left" && cfg.Position != "right" { - return nil, errorsx.InvalidParam("Web渠道配置 position 只能为 left 或 right") + return nil, errorsx.InvalidParamI18n("error.e0059") } cfg.Width = strings.TrimSpace(cfg.Width) if cfg.Width == "" { @@ -292,14 +292,14 @@ func (s *channelService) GetUserTokenSecret(channel *models.Channel) string { func (s *channelService) ResetUserTokenSecret(channelID int64, operator *dto.AuthPrincipal) (string, error) { if operator == nil { - return "", errorsx.Unauthorized("未登录或登录已过期") + return "", errorsx.UnauthorizedI18n("error.auth.expired") } channel := s.Get(channelID) if channel == nil || channel.Status == enums.StatusDeleted { - return "", errorsx.InvalidParam("接入渠道不存在") + return "", errorsx.InvalidParamI18n("error.e0208") } if channel.ChannelType != enums.ChannelTypeWeb && channel.ChannelType != enums.ChannelTypeWechatMP { - return "", errorsx.InvalidParam("当前渠道不支持用户 JWT Secret") + return "", errorsx.InvalidParamI18n("error.e0196") } secret, err := generateUserTokenSecret() if err != nil { @@ -385,32 +385,32 @@ func (s *channelService) GetEnabledChannel(ctx *gin.Context) *models.Channel { func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRequest) (*models.Channel, error) { channelType := strings.TrimSpace(req.ChannelType) if channelType != enums.ChannelTypeWeb && channelType != enums.ChannelTypeWechatMP && channelType != enums.ChannelTypeWxWorkKF { - return nil, errorsx.InvalidParam("渠道类型不合法") + return nil, errorsx.InvalidParamI18n("error.e0250") } name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("渠道名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0247") } if req.AIAgentID <= 0 { - return nil, errorsx.InvalidParam("请选择 AI Agent") + return nil, errorsx.InvalidParamI18n("error.e0321") } aiAgent := AIAgentService.Get(req.AIAgentID) if aiAgent == nil || aiAgent.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("AI Agent 不存在或未启用") + return nil, errorsx.InvalidParamI18n("error.e0004") } status := enums.Status(req.Status) if req.Status == 0 { status = enums.StatusOk } if status != enums.StatusOk && status != enums.StatusDisabled { - return nil, errorsx.InvalidParam("渠道状态不合法") + return nil, errorsx.InvalidParamI18n("error.e0249") } channelID := "" if id > 0 { current := s.Get(id) if current == nil || current.Status == enums.StatusDeleted { - return nil, errorsx.InvalidParam("接入渠道不存在") + return nil, errorsx.InvalidParamI18n("error.e0208") } channelID = strings.TrimSpace(current.ChannelID) } @@ -421,11 +421,11 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe channelID = strs.UUID() } if exists := s.Take("channel_id = ? AND status <> ? AND id <> ?", channelID, enums.StatusDeleted, id); exists != nil { - return nil, errorsx.InvalidParam("渠道标识已存在") + return nil, errorsx.InvalidParamI18n("error.e0248") } cfg, err := s.ParseWebChannelConfig(configJSON) if err != nil { - return nil, errorsx.InvalidParam("Web渠道配置不合法") + return nil, errorsx.InvalidParamI18n("error.e0060") } if strings.TrimSpace(cfg.UserTokenSecret) == "" { secret, err := generateUserTokenSecret() @@ -444,11 +444,11 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe channelID = strs.UUID() } if exists := s.Take("channel_id = ? AND status <> ? AND id <> ?", channelID, enums.StatusDeleted, id); exists != nil { - return nil, errorsx.InvalidParam("渠道标识已存在") + return nil, errorsx.InvalidParamI18n("error.e0248") } cfg, err := s.ParseWechatMPChannelConfig(configJSON) if err != nil { - return nil, errorsx.InvalidParam("微信公众号渠道配置不合法") + return nil, errorsx.InvalidParamI18n("error.e0201") } if strings.TrimSpace(cfg.UserTokenSecret) == "" { secret, err := generateUserTokenSecret() @@ -467,17 +467,17 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe channelID = strs.UUID() } if exists := s.Take("channel_id = ? AND status <> ? AND id <> ?", channelID, enums.StatusDeleted, id); exists != nil { - return nil, errorsx.InvalidParam("渠道标识已存在") + return nil, errorsx.InvalidParamI18n("error.e0248") } cfg, err := s.ParseWxWorkKFChannelConfig(configJSON) if err != nil { - return nil, errorsx.InvalidParam("企业微信渠道配置不合法") + return nil, errorsx.InvalidParamI18n("error.e0102") } if cfg == nil || cfg.OpenKfID == "" { - return nil, errorsx.InvalidParam("企业微信渠道配置缺少 openKfId") + return nil, errorsx.InvalidParamI18n("error.e0103") } if channel := s.GetEnabledWxWorkKFChannelByOpenKfID(cfg.OpenKfID); channel != nil && channel.ID != id { - return nil, errorsx.InvalidParam("openKfId 已被其他渠道使用") + return nil, errorsx.InvalidParamI18n("error.e0069") } } diff --git a/internal/services/company_service.go b/internal/services/company_service.go index 340c1e5..6783982 100644 --- a/internal/services/company_service.go +++ b/internal/services/company_service.go @@ -58,16 +58,16 @@ func (s *companyService) Count(cnd *sqls.Cnd) int64 { func (s *companyService) CreateCompany(req request.CreateCompanyRequest, operator *dto.AuthPrincipal) (*models.Company, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("公司名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0125") } existing := repositories.CompanyRepository.GetByName(sqls.DB(), name) if existing != nil && existing.Status != enums.StatusDeleted { - return nil, errorsx.InvalidParam("公司名称已存在") + return nil, errorsx.InvalidParamI18n("error.e0126") } item := &models.Company{ @@ -85,20 +85,20 @@ func (s *companyService) CreateCompany(req request.CreateCompanyRequest, operato func (s *companyService) UpdateCompany(req request.UpdateCompanyRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(req.ID) if item == nil { - return errorsx.InvalidParam("公司不存在") + return errorsx.InvalidParamI18n("error.e0124") } name := strings.TrimSpace(req.Name) if name == "" { - return errorsx.InvalidParam("公司名称不能为空") + return errorsx.InvalidParamI18n("error.e0125") } existing := repositories.CompanyRepository.GetByName(sqls.DB(), name) if existing != nil && existing.ID != req.ID { - return errorsx.InvalidParam("公司名称已存在") + return errorsx.InvalidParamI18n("error.e0126") } now := time.Now() @@ -118,7 +118,7 @@ func (s *companyService) UpdateCompany(req request.UpdateCompanyRequest, operato func (s *companyService) DeleteCompany(id int64, operator dto.AuthPrincipal) error { item := s.Get(id) if item == nil { - return errorsx.InvalidParam("公司不存在") + return errorsx.InvalidParamI18n("error.e0124") } return repositories.CompanyRepository.Updates(sqls.DB(), id, map[string]any{ @@ -131,14 +131,14 @@ func (s *companyService) DeleteCompany(id int64, operator dto.AuthPrincipal) err func (s *companyService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(id) if item == nil { - return errorsx.InvalidParam("公司不存在") + return errorsx.InvalidParamI18n("error.e0124") } if status != int(enums.StatusOk) && status != int(enums.StatusDisabled) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } now := time.Now() return repositories.CompanyRepository.Updates(sqls.DB(), id, map[string]any{ diff --git a/internal/services/conversation_human_dispatch_service.go b/internal/services/conversation_human_dispatch_service.go index 426851a..69430ba 100644 --- a/internal/services/conversation_human_dispatch_service.go +++ b/internal/services/conversation_human_dispatch_service.go @@ -52,7 +52,7 @@ func (s *conversationHumanDispatchService) TryOffHoursHandoffByAI(conversationID func (s *conversationHumanDispatchService) TryOffHoursHandoffByAIWithRequestID(conversationID int64, aiAgent models.AIAgent, reason string, requestID string) (bool, error) { conversation := ConversationService.Get(conversationID) if conversation == nil { - return false, errorsx.InvalidParam("会话不存在") + return false, errorsx.InvalidParamI18n("error.e0116") } teamIDs := orderedPositiveIDs(aiAgent.TeamIDs) activeTeamIDs := ConversationDispatchService.findActiveScheduleTeamIDs(teamIDs, time.Now()) @@ -75,7 +75,7 @@ func (s *conversationHumanDispatchService) HandoffByAI(conversationID int64, aiA func (s *conversationHumanDispatchService) HandoffByAIWithRequestID(conversationID int64, aiAgent models.AIAgent, reason string, requestID string) (*HandoffDecisionResult, error) { conversation := ConversationService.Get(conversationID) if conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } teamIDs := orderedPositiveIDs(aiAgent.TeamIDs) activeTeamIDs := ConversationDispatchService.findActiveScheduleTeamIDs(teamIDs, time.Now()) @@ -110,10 +110,10 @@ func (s *conversationHumanDispatchService) ApplyHumanOnlyCreate(conversationID i func (s *conversationHumanDispatchService) DispatchPendingConversation(conversationID int64, aiAgent models.AIAgent) (*HandoffDecisionResult, error) { conversation := ConversationService.Get(conversationID) if conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } if conversation.Status != enums.IMConversationStatusPending || conversation.CurrentAssigneeID > 0 { - return nil, errorsx.InvalidParam("只有待接入未分配会话允许自动分配") + return nil, errorsx.InvalidParamI18n("error.e0137") } activeTeamIDs := ConversationDispatchService.findActiveScheduleTeamIDs(orderedPositiveIDs(aiAgent.TeamIDs), time.Now()) if len(activeTeamIDs) == 0 { @@ -227,7 +227,7 @@ func (s *conversationHumanDispatchService) moveToTeamPoolWithRequestID(conversat err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { current := repositories.ConversationRepository.Get(ctx.Tx, conversationID) if current == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if err := ConversationAssignmentService.FinishActiveAssignments(ctx, conversationID, now); err != nil { return err @@ -273,7 +273,7 @@ func (s *conversationHumanDispatchService) moveToGlobalPool(conversationID int64 return sqls.WithTransaction(func(ctx *sqls.TxContext) error { conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if err := repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{ "status": enums.IMConversationStatusPending, diff --git a/internal/services/conversation_read_state_service.go b/internal/services/conversation_read_state_service.go index 495249c..2c68229 100644 --- a/internal/services/conversation_read_state_service.go +++ b/internal/services/conversation_read_state_service.go @@ -37,7 +37,7 @@ type readerCursor struct { func agentReaderCursor(operator *dto.AuthPrincipal) (readerCursor, error) { if operator == nil { - return readerCursor{}, errorsx.Unauthorized("未登录或登录已过期") + return readerCursor{}, errorsx.UnauthorizedI18n("error.auth.expired") } return readerCursor{ readerType: enums.IMSenderTypeAgent, @@ -50,7 +50,7 @@ func agentReaderCursor(operator *dto.AuthPrincipal) (readerCursor, error) { func customerReaderCursor(external *openidentity.ExternalUser) (readerCursor, error) { if external == nil || strings.TrimSpace(external.ExternalID) == "" { - return readerCursor{}, errorsx.Unauthorized("外部用户标识不能为空") + return readerCursor{}, errorsx.UnauthorizedI18n("error.e0149") } extID := strings.TrimSpace(external.ExternalID) name := strings.TrimSpace(external.ExternalName) @@ -195,7 +195,7 @@ func (s *conversationReadStateService) markReadTxWithCursor(ctx *sqls.TxContext, return nil, nil } if c.readerType != enums.IMSenderTypeAgent && c.readerType != enums.IMSenderTypeCustomer { - return nil, errorsx.InvalidParam("不支持的已读操作类型") + return nil, errorsx.InvalidParamI18n("error.e0081") } now := time.Now() diff --git a/internal/services/conversation_service.go b/internal/services/conversation_service.go index ae7da15..b8daf1c 100644 --- a/internal/services/conversation_service.go +++ b/internal/services/conversation_service.go @@ -74,7 +74,7 @@ func (s *conversationService) ListConversations(userID int64, filter request.Age case request.AgentConversationFilterClosed: cnd.Eq("current_assignee_id", userID).Eq("status", enums.IMConversationStatusClosed).Desc("last_active_at").Desc("id") default: - return nil, nil, errorsx.InvalidParam("会话筛选项不合法") + return nil, nil, errorsx.InvalidParamI18n("error.e0121") } list, paging := repositories.ConversationRepository.FindPageByCnd(sqls.DB(), cnd) @@ -103,7 +103,7 @@ func (s *conversationService) getLatestNotFinishedByCustomerID(db *gorm.DB, cust func (s *conversationService) Create(externalUser openidentity.ExternalUser, channelID, aiAgentID int64) (*models.Conversation, error) { aiAgent := AIAgentService.Get(aiAgentID) if aiAgent == nil || aiAgent.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("AI Agent 不存在") + return nil, errorsx.InvalidParamI18n("error.e0002") } var conversation *models.Conversation @@ -184,20 +184,20 @@ func (s *conversationService) Create(externalUser openidentity.ExternalUser, cha func (s *conversationService) AssignConversation(req request.AssignConversationRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } targetProfile := AgentProfileService.GetByUserID(req.AssigneeID) if targetProfile == nil || targetProfile.Status != enums.StatusOk { - return errorsx.InvalidParam("目标客服不存在") + return errorsx.InvalidParamI18n("error.e0276") } var assignedEvent events.ConversationAssignedEvent if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { conversation := repositories.ConversationRepository.Get(ctx.Tx, req.ConversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if conversation.Status != enums.IMConversationStatusPending { - return errorsx.InvalidParam("只有待接入会话允许分配") + return errorsx.InvalidParamI18n("error.e0135") } now := time.Now() if err := ConversationAssignmentService.FinishActiveAssignments(ctx, req.ConversationID, now); err != nil { @@ -245,62 +245,62 @@ func (s *conversationService) AssignConversation(req request.AssignConversationR func (s *conversationService) AutoAssignConversation(conversationID int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } conversation := s.Get(conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if conversation.Status != enums.IMConversationStatusPending { - return errorsx.InvalidParam("只有待接入会话允许自动分配") + return errorsx.InvalidParamI18n("error.e0136") } if conversation.CurrentAssigneeID > 0 { - return errorsx.InvalidParam("当前会话已分配客服") + return errorsx.InvalidParamI18n("error.e0190") } aiAgent := AIAgentService.Get(conversation.AIAgentID) if aiAgent == nil || aiAgent.Status != enums.StatusOk { - return errorsx.InvalidParam("AI Agent 不存在或已停用") + return errorsx.InvalidParamI18n("error.e0003") } result, err := ConversationHumanDispatchService.DispatchPendingConversation(conversationID, *aiAgent) if err != nil { return err } if result == nil || result.Decision == HandoffDecisionOffHours { - return errorsx.InvalidParam("当前暂不在人工客服服务时间内") + return errorsx.InvalidParamI18n("error.e0194") } return nil } func (s *conversationService) TransferConversation(conversationID, toUserID int64, reason string, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } if toUserID <= 0 { - return errorsx.InvalidParam("目标客服不能为空") + return errorsx.InvalidParamI18n("error.e0278") } targetProfile := AgentProfileService.GetByUserID(toUserID) if targetProfile == nil || targetProfile.Status != enums.StatusOk { - return errorsx.InvalidParam("目标客服不存在") + return errorsx.InvalidParamI18n("error.e0276") } var assignedEvent events.ConversationAssignedEvent if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if !s.canTransferConversation(conversation, operator) { - return errorsx.Forbidden("无权转接该会话") + return errorsx.ForbiddenI18n("error.e0223") } if conversation.Status != enums.IMConversationStatusActive { - return errorsx.InvalidParam("只有处理中会话允许转接") + return errorsx.InvalidParamI18n("error.e0134") } if conversation.CurrentAssigneeID <= 0 { - return errorsx.InvalidParam("当前会话未分配客服") + return errorsx.InvalidParamI18n("error.e0193") } if conversation.CurrentAssigneeID == toUserID { - return errorsx.InvalidParam("目标客服不能与当前指派人相同") + return errorsx.InvalidParamI18n("error.e0277") } now := time.Now() if err := ConversationAssignmentService.FinishActiveAssignments(ctx, conversationID, now); err != nil { @@ -352,7 +352,7 @@ func (s *conversationService) HandoffByAI(conversationID int64, aiAgent models.A func (s *conversationService) HandoffByAIWithRequestID(conversationID int64, aiAgent models.AIAgent, reason string, requestID string) error { if conversationID <= 0 { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } _, err := ConversationHumanDispatchService.HandoffByAIWithRequestID(conversationID, aiAgent, reason, requestID) if err != nil { @@ -371,7 +371,7 @@ func (s *conversationService) TryOffHoursHandoffByAI(conversationID int64, aiAge func (s *conversationService) TryOffHoursHandoffByAIWithRequestID(conversationID int64, aiAgent models.AIAgent, reason string, requestID string) (bool, error) { if conversationID <= 0 { - return false, errorsx.InvalidParam("会话不存在") + return false, errorsx.InvalidParamI18n("error.e0116") } handled, err := ConversationHumanDispatchService.TryOffHoursHandoffByAIWithRequestID(conversationID, aiAgent, reason, requestID) if err != nil { @@ -386,7 +386,7 @@ func (s *conversationService) TryOffHoursHandoffByAIWithRequestID(conversationID func (s *conversationService) CloseConversation(conversationID int64, closeReason string, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } return s.closeConversation(conversationID, enums.IMSenderTypeAgent, closeReason, operator) } @@ -394,10 +394,10 @@ func (s *conversationService) CloseConversation(conversationID int64, closeReaso func (s *conversationService) CloseCustomerConversation(conversationID int64, externalUser openidentity.ExternalUser) error { conversation := s.Get(conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if !s.IsCustomerConversationOwner(conversation, externalUser) { - return errorsx.Forbidden("无权访问该会话") + return errorsx.ForbiddenI18n("error.e0222") } return s.closeConversation(conversationID, enums.IMSenderTypeCustomer, "", nil) } @@ -406,7 +406,7 @@ func (s *conversationService) closeConversation(conversationID int64, senderType if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if conversation.Status == enums.IMConversationStatusClosed { return nil @@ -414,7 +414,7 @@ func (s *conversationService) closeConversation(conversationID int64, senderType if conversation.Status != enums.IMConversationStatusAIServing && conversation.Status != enums.IMConversationStatusPending && conversation.Status != enums.IMConversationStatusActive { - return errorsx.InvalidParam("当前状态不允许关闭会话") + return errorsx.InvalidParamI18n("error.e0197") } var ( now = time.Now() @@ -427,13 +427,13 @@ func (s *conversationService) closeConversation(conversationID int64, senderType eventDesc = "客户关闭会话" } else { if operator == nil { - return errorsx.InvalidParam("无权限操作") + return errorsx.InvalidParamI18n("error.e0226") } if closeReason == "" { - return errorsx.InvalidParam("关闭原因不能为空") + return errorsx.InvalidParamI18n("error.e0128") } if !s.canCloseConversation(conversation, operator) { - return errorsx.Forbidden("无权关闭该会话") + return errorsx.ForbiddenI18n("error.e0221") } operatorID = operator.UserID operatorName = operator.Nickname @@ -471,11 +471,11 @@ func (s *conversationService) closeConversation(conversationID int64, senderType // MarkAgentConversationReadToMessage 控制台客服将会话已读推进到指定消息。 func (s *conversationService) MarkAgentConversationReadToMessage(conversationID, messageID int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } conversation := s.Get(conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } changed, err := s.markConversationReadWithActor(conversation, messageID, agentConversationReadActor{operator: operator}) if err != nil { @@ -492,14 +492,14 @@ func (s *conversationService) MarkAgentConversationReadToMessage(conversationID, // MarkCustomerConversationReadToMessage IM 客户将会话已读推进到指定消息(需为会话归属外部身份)。 func (s *conversationService) MarkCustomerConversationReadToMessage(conversationID, messageID int64, external *openidentity.ExternalUser) error { if external == nil || strings.TrimSpace(external.ExternalID) == "" { - return errorsx.Unauthorized("外部用户标识不能为空") + return errorsx.UnauthorizedI18n("error.e0149") } conversation := s.Get(conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if !s.IsCustomerConversationOwner(conversation, *external) { - return errorsx.Forbidden("无权访问该会话") + return errorsx.ForbiddenI18n("error.e0222") } changed, err := s.markConversationReadWithActor(conversation, messageID, customerConversationReadActor{external: external}) if err != nil { @@ -574,7 +574,7 @@ func (a customerConversationReadActor) conversationUpdateAudit() (int64, string) func (s *conversationService) markConversationReadWithActor(conversation *models.Conversation, messageID int64, actor conversationReadActor) (bool, error) { if conversation == nil { - return false, errorsx.InvalidParam("会话不存在") + return false, errorsx.InvalidParamI18n("error.e0116") } targetMessage, err := MessageService.GetConversationReadTarget(conversation.ID, messageID) if err != nil { @@ -615,7 +615,7 @@ func (s *conversationService) markConversationReadWithActor(conversation *models err = sqls.WithTransaction(func(ctx *sqls.TxContext) error { currentConversation := repositories.ConversationRepository.Get(ctx.Tx, conversation.ID) if currentConversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if err := actor.markRead(ctx, currentConversation, targetMessage); err != nil { return err @@ -741,30 +741,30 @@ func (s *conversationService) buildEventPayload(payload map[string]any) string { // LinkConversationCustomer 将会话绑定到指定客户。 func (s *conversationService) LinkConversationCustomer(conversationID, customerID int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } if conversationID <= 0 || customerID <= 0 { - return errorsx.InvalidParam("参数不合法") + return errorsx.InvalidParamI18n("error.e0133") } cust := CustomerService.Get(customerID) if cust == nil || cust.Status == enums.StatusDeleted { - return errorsx.InvalidParam("客户不存在") + return errorsx.InvalidParamI18n("error.e0155") } conv := s.Get(conversationID) if conv == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if conv.Status == enums.IMConversationStatusClosed { - return errorsx.InvalidParam("已关闭的会话无法关联客户") + return errorsx.InvalidParamI18n("error.e0183") } if !s.canLinkConversationCustomer(conv, operator) { - return errorsx.Forbidden("无权限关联该会话") + return errorsx.ForbiddenI18n("error.e0224") } err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { current := repositories.ConversationRepository.Get(ctx.Tx, conversationID) if current == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } now := time.Now() return repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{ diff --git a/internal/services/customer_contact_service.go b/internal/services/customer_contact_service.go index 682c8f5..eb075b7 100644 --- a/internal/services/customer_contact_service.go +++ b/internal/services/customer_contact_service.go @@ -167,7 +167,7 @@ func (s *customerContactService) ReplaceAllForCustomerInTx( operator *dto.AuthPrincipal, ) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } type line struct { id *int64 @@ -184,7 +184,7 @@ func (s *customerContactService) ReplaceAllForCustomerInTx( continue } if !enums.IsValidContactType(ct) { - return errorsx.InvalidParam("联系方式类型不合法") + return errorsx.InvalidParamI18n("error.e0301") } items = append(items, line{ id: r.ID, @@ -204,7 +204,7 @@ func (s *customerContactService) ReplaceAllForCustomerInTx( if primaryCount == 0 { items[0].primary = true } else if primaryCount > 1 { - return errorsx.InvalidParam("仅能指定一条主联系方式") + return errorsx.InvalidParamI18n("error.e0092") } } @@ -241,10 +241,10 @@ func (s *customerContactService) ReplaceAllForCustomerInTx( if it.id != nil && *it.id > 0 { row := repositories.CustomerContactRepository.Get(ctx.Tx, *it.id) if row == nil || row.CustomerID != customerID || row.Status == enums.StatusDeleted { - return errorsx.InvalidParam("联系方式不存在") + return errorsx.InvalidParamI18n("error.e0299") } if s.hasDuplicateContact(ctx.Tx, customerID, it.ct, it.val, *it.id) { - return errorsx.InvalidParam("该联系方式已存在") + return errorsx.InvalidParamI18n("error.e0318") } if it.primary { if err := s.clearPrimaryExcept(ctx.Tx, customerID, *it.id); err != nil { @@ -265,7 +265,7 @@ func (s *customerContactService) ReplaceAllForCustomerInTx( continue } if s.hasDuplicateContact(ctx.Tx, customerID, it.ct, it.val, 0) { - return errorsx.InvalidParam("该联系方式已存在") + return errorsx.InvalidParamI18n("error.e0318") } if deleted := s.findSoftDeletedContactByNaturalKey(ctx.Tx, customerID, it.ct, it.val); deleted != nil { if it.primary { @@ -331,10 +331,10 @@ func (s *customerContactService) clearPrimaryExcept(db *gorm.DB, customerID int6 func (s *customerContactService) validateContactStatus(status int) error { if !enums.IsValidStatus(status) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } if status == int(enums.StatusDeleted) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } return nil } @@ -342,21 +342,21 @@ func (s *customerContactService) validateContactStatus(status int) error { // CreateCustomerContact 创建联系方式;主联系方式在同一客户下唯一。 func (s *customerContactService) CreateCustomerContact(req request.CreateCustomerContactRequest, operator *dto.AuthPrincipal) (*models.CustomerContact, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } if req.CustomerID <= 0 { - return nil, errorsx.InvalidParam("客户不存在") + return nil, errorsx.InvalidParamI18n("error.e0155") } if CustomerService.Get(req.CustomerID) == nil { - return nil, errorsx.InvalidParam("客户不存在") + return nil, errorsx.InvalidParamI18n("error.e0155") } ct := strings.TrimSpace(req.ContactType) if !enums.IsValidContactType(ct) { - return nil, errorsx.InvalidParam("联系方式类型不合法") + return nil, errorsx.InvalidParamI18n("error.e0301") } val := strings.TrimSpace(req.ContactValue) if val == "" { - return nil, errorsx.InvalidParam("联系方式不能为空") + return nil, errorsx.InvalidParamI18n("error.e0300") } if err := s.validateContactStatus(req.Status); err != nil { return nil, err @@ -369,7 +369,7 @@ func (s *customerContactService) CreateCustomerContact(req request.CreateCustome var created *models.CustomerContact err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { if s.hasDuplicateContact(ctx.Tx, req.CustomerID, enums.ContactType(ct), val, 0) { - return errorsx.InvalidParam("该联系方式已存在") + return errorsx.InvalidParamI18n("error.e0318") } now := time.Now() if deleted := s.findSoftDeletedContactByNaturalKey(ctx.Tx, req.CustomerID, enums.ContactType(ct), val); deleted != nil { @@ -439,22 +439,22 @@ func (s *customerContactService) CreateCustomerContact(req request.CreateCustome // UpdateCustomerContact 更新联系方式。 func (s *customerContactService) UpdateCustomerContact(req request.UpdateCustomerContactRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } if req.ID <= 0 { - return errorsx.InvalidParam("联系方式不存在") + return errorsx.InvalidParamI18n("error.e0299") } current := s.Get(req.ID) if current == nil { - return errorsx.InvalidParam("联系方式不存在") + return errorsx.InvalidParamI18n("error.e0299") } ct := strings.TrimSpace(req.ContactType) if !enums.IsValidContactType(ct) { - return errorsx.InvalidParam("联系方式类型不合法") + return errorsx.InvalidParamI18n("error.e0301") } val := strings.TrimSpace(req.ContactValue) if val == "" { - return errorsx.InvalidParam("联系方式不能为空") + return errorsx.InvalidParamI18n("error.e0300") } if err := s.validateContactStatus(req.Status); err != nil { return err @@ -462,7 +462,7 @@ func (s *customerContactService) UpdateCustomerContact(req request.UpdateCustome return sqls.WithTransaction(func(ctx *sqls.TxContext) error { if s.hasDuplicateContact(ctx.Tx, current.CustomerID, enums.ContactType(ct), val, req.ID) { - return errorsx.InvalidParam("该联系方式已存在") + return errorsx.InvalidParamI18n("error.e0318") } if req.IsPrimary { if err := s.clearPrimaryExcept(ctx.Tx, current.CustomerID, req.ID); err != nil { @@ -500,14 +500,14 @@ func (s *customerContactService) UpdateCustomerContact(req request.UpdateCustome // DeleteCustomerContact 软删除联系方式并同步客户主联系方式冗余字段。 func (s *customerContactService) DeleteCustomerContact(id int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } if id <= 0 { - return errorsx.InvalidParam("联系方式不存在") + return errorsx.InvalidParamI18n("error.e0299") } current := s.Get(id) if current == nil { - return errorsx.InvalidParam("联系方式不存在") + return errorsx.InvalidParamI18n("error.e0299") } return sqls.WithTransaction(func(ctx *sqls.TxContext) error { now := time.Now() diff --git a/internal/services/customer_service.go b/internal/services/customer_service.go index 8ef3bd2..14d8fa4 100644 --- a/internal/services/customer_service.go +++ b/internal/services/customer_service.go @@ -116,12 +116,12 @@ func (s *customerService) CountByCompanyIDs(companyIDs []int64) map[int64]int64 func (s *customerService) EnsureExternalCustomer(ctx *sqls.TxContext, externalUser openidentity.ExternalUser) (int64, error) { if ctx == nil || ctx.Tx == nil { - return 0, errorsx.InvalidParam("事务上下文不能为空") + return 0, errorsx.InvalidParamI18n("error.e0086") } externalSource := externalUser.ExternalSource externalID := strings.TrimSpace(externalUser.ExternalID) if strings.TrimSpace(string(externalSource)) == "" || externalID == "" { - return 0, errorsx.Unauthorized("外部用户标识不能为空") + return 0, errorsx.UnauthorizedI18n("error.e0149") } now := time.Now() if identity := repositories.CustomerIdentityRepository.GetBy(ctx.Tx, externalSource, externalID); identity != nil { @@ -189,17 +189,17 @@ func hashUUID(uuid string) string { func (s *customerService) CreateCustomer(req request.CreateCustomerRequest, operator *dto.AuthPrincipal) (*models.Customer, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("客户名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0156") } if req.CompanyID > 0 { company := CompanyService.Get(req.CompanyID) if company == nil { - return nil, errorsx.InvalidParam("所属公司不存在") + return nil, errorsx.InvalidParamI18n("error.e0204") } } @@ -222,21 +222,21 @@ func (s *customerService) CreateCustomer(req request.CreateCustomerRequest, oper func (s *customerService) UpdateCustomer(req request.UpdateCustomerRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(req.ID) if item == nil { - return errorsx.InvalidParam("客户不存在") + return errorsx.InvalidParamI18n("error.e0155") } name := strings.TrimSpace(req.Name) if name == "" { - return errorsx.InvalidParam("客户名称不能为空") + return errorsx.InvalidParamI18n("error.e0156") } if req.CompanyID > 0 { company := CompanyService.Get(req.CompanyID) if company == nil { - return errorsx.InvalidParam("所属公司不存在") + return errorsx.InvalidParamI18n("error.e0204") } } @@ -262,7 +262,7 @@ func (s *customerService) UpdateCustomer(req request.UpdateCustomerRequest, oper func (s *customerService) DeleteCustomer(id int64, operator dto.AuthPrincipal) error { item := s.Get(id) if item == nil { - return errorsx.InvalidParam("客户不存在") + return errorsx.InvalidParamI18n("error.e0155") } return repositories.CustomerRepository.Updates(sqls.DB(), id, map[string]any{ "status": enums.StatusDeleted, @@ -289,14 +289,14 @@ func (s *customerService) syncConversationCustomerName(db *gorm.DB, customerID i func (s *customerService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(id) if item == nil { - return errorsx.InvalidParam("客户不存在") + return errorsx.InvalidParamI18n("error.e0155") } if status != int(enums.StatusOk) && status != int(enums.StatusDisabled) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } return repositories.CustomerRepository.Updates(sqls.DB(), id, map[string]any{ "status": status, @@ -309,15 +309,15 @@ func (s *customerService) UpdateStatus(id int64, status int, operator *dto.AuthP // SaveCustomerProfile 单事务保存客户主信息与联系方式全量(新建或更新)。 func (s *customerService) SaveCustomerProfile(req request.SaveCustomerProfileRequest, operator *dto.AuthPrincipal) (*models.Customer, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("客户名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0156") } if req.CompanyID > 0 { if CompanyService.Get(req.CompanyID) == nil { - return nil, errorsx.InvalidParam("所属公司不存在") + return nil, errorsx.InvalidParamI18n("error.e0204") } } createMode := req.ID == nil || *req.ID <= 0 @@ -345,7 +345,7 @@ func (s *customerService) SaveCustomerProfile(req request.SaveCustomerProfileReq customerID = *req.ID cur := repositories.CustomerRepository.Get(ctx.Tx, customerID) if cur == nil { - return errorsx.InvalidParam("客户不存在") + return errorsx.InvalidParamI18n("error.e0155") } now := time.Now() if err := repositories.CustomerRepository.Updates(ctx.Tx, customerID, map[string]any{ diff --git a/internal/services/customer_session_service.go b/internal/services/customer_session_service.go index 2474a47..9e03de8 100644 --- a/internal/services/customer_session_service.go +++ b/internal/services/customer_session_service.go @@ -54,7 +54,7 @@ type CustomerSessionVerifyResult struct { func (s *customerSessionService) Exchange(channel *models.Channel, externalUser openidentity.ExternalUser) (*response.CustomerSessionExchangeResponse, error) { if channel == nil || channel.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("接入渠道不存在或已停用") + return nil, errorsx.InvalidParamI18n("error.e0209") } var customerID int64 if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { @@ -69,7 +69,7 @@ func (s *customerSessionService) Exchange(channel *models.Channel, externalUser } customer := CustomerService.Get(customerID) if customer == nil || customer.Status == enums.StatusDeleted { - return nil, errorsx.InvalidParam("客户不存在") + return nil, errorsx.InvalidParamI18n("error.e0155") } token, expiresAt, err := s.Sign(channel, customer, externalUser) if err != nil { @@ -93,7 +93,7 @@ func (s *customerSessionService) Sign(channel *models.Channel, customer *models. return "", time.Time{}, errorsx.BusinessError(1, "客服会话密钥未配置") } if channel == nil || customer == nil { - return "", time.Time{}, errorsx.InvalidParam("客服会话参数不完整") + return "", time.Time{}, errorsx.InvalidParamI18n("error.e0158") } now := time.Now() expiresAt := now.Add(time.Duration(cfg.TTL()) * time.Minute) @@ -119,21 +119,21 @@ func (s *customerSessionService) Sign(channel *models.Channel, customer *models. func (s *customerSessionService) VerifyRequest(ctx *gin.Context, channel *models.Channel) (*CustomerSessionVerifyResult, error) { token := s.getCustomerSessionToken(ctx) if token == "" { - return nil, errorsx.Unauthorized("客服会话不能为空") + return nil, errorsx.UnauthorizedI18n("error.e0157") } claims, err := s.verifyToken(token) if err != nil { return nil, err } if channel == nil || channel.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("接入渠道不存在或已停用") + return nil, errorsx.InvalidParamI18n("error.e0209") } if claims.ChannelID != channel.ID || strings.TrimSpace(claims.ChannelCode) != strings.TrimSpace(channel.ChannelID) { - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } customer := CustomerService.Get(claims.CustomerID) if customer == nil || customer.Status == enums.StatusDeleted { - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } external, err := s.externalUserFromClaims(claims, customer) if err != nil { @@ -183,15 +183,15 @@ func (s *customerSessionService) verifyToken(rawToken string) (*customerSessionC })) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { - return nil, errorsx.Unauthorized("客服会话已过期") + return nil, errorsx.UnauthorizedI18n("error.e0160") } - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } if token == nil || !token.Valid || claims.TokenType != customerSessionTokenType || claims.ExpiresAt == nil { - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } if claims.ChannelID <= 0 || strings.TrimSpace(claims.ChannelCode) == "" || claims.CustomerID <= 0 || strings.TrimSpace(claims.IdentityKey) == "" { - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } return claims, nil } @@ -200,7 +200,7 @@ func (s *customerSessionService) externalUserFromClaims(claims *customerSessionC identityKey := strings.TrimSpace(claims.IdentityKey) parts := strings.SplitN(identityKey, ":", 2) if len(parts) != 2 || strings.TrimSpace(parts[1]) == "" { - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } var source enums.ExternalSource switch parts[0] { @@ -209,11 +209,11 @@ func (s *customerSessionService) externalUserFromClaims(claims *customerSessionC case "guest": source = enums.ExternalSourceGuest default: - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), source, parts[1]) if identity == nil || identity.CustomerID != claims.CustomerID { - return nil, errorsx.Unauthorized("客服会话校验失败") + return nil, errorsx.UnauthorizedI18n("error.e0161") } name := strings.TrimSpace(claims.CustomerName) if customer != nil && strings.TrimSpace(customer.Name) != "" { diff --git a/internal/services/im_message_asset.go b/internal/services/im_message_asset.go index d148a33..13155bf 100644 --- a/internal/services/im_message_asset.go +++ b/internal/services/im_message_asset.go @@ -22,24 +22,24 @@ type imMessageAssetPayload struct { func parseIMMessageAssetPayload(payload string) (*imMessageAssetPayload, error) { payload = strings.TrimSpace(payload) if payload == "" { - return nil, errorsx.InvalidParam("附件消息缺少 payload") + return nil, errorsx.InvalidParamI18n("error.e0346") } ret := &imMessageAssetPayload{} if err := json.Unmarshal([]byte(payload), ret); err != nil { - return nil, errorsx.InvalidParam("附件消息 payload 格式错误") + return nil, errorsx.InvalidParamI18n("error.e0344") } ret.AssetID = strings.TrimSpace(ret.AssetID) ret.Provider = enums.AssetProvider(strings.TrimSpace(string(ret.Provider))) ret.StorageKey = strings.TrimSpace(ret.StorageKey) if ret.AssetID == "" { - return nil, errorsx.InvalidParam("附件消息缺少 assetId") + return nil, errorsx.InvalidParamI18n("error.e0345") } return ret, nil } func buildIMMessageAssetPayload(asset *models.Asset) (string, error) { if asset == nil { - return "", errorsx.InvalidParam("附件不存在") + return "", errorsx.InvalidParamI18n("error.e0342") } payload, err := json.Marshal(imMessageAssetPayload{ AssetID: asset.AssetID, @@ -107,10 +107,10 @@ func hydrateIMMessageAssetPayload(payload *imMessageAssetPayload) *imMessageAsse func validateConversationAsset(asset *models.Asset, conversationID int64, messageType enums.IMMessageType) error { if asset == nil { - return errorsx.InvalidParam("附件不存在") + return errorsx.InvalidParamI18n("error.e0342") } if asset.Status != enums.AssetStatusSuccess { - return errorsx.InvalidParam("附件尚未上传完成") + return errorsx.InvalidParamI18n("error.e0343") } return nil } diff --git a/internal/services/knowledge_base_service.go b/internal/services/knowledge_base_service.go index 72b61ea..d269fc9 100644 --- a/internal/services/knowledge_base_service.go +++ b/internal/services/knowledge_base_service.go @@ -2,7 +2,6 @@ package services import ( "context" - "fmt" "time" "agent-desk/internal/ai/rag" @@ -78,7 +77,7 @@ func (s *knowledgeBaseService) Delete(id int64) { func (s *knowledgeBaseService) CreateKnowledgeBase(req request.CreateKnowledgeBaseRequest, operator *dto.AuthPrincipal) (*models.KnowledgeBase, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } item, err := s.buildKnowledgeBaseModel(req) if err != nil { @@ -94,11 +93,11 @@ func (s *knowledgeBaseService) CreateKnowledgeBase(req request.CreateKnowledgeBa func (s *knowledgeBaseService) UpdateKnowledgeBase(req request.UpdateKnowledgeBaseRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(req.ID) if current == nil { - return errorsx.InvalidParam("知识库不存在") + return errorsx.InvalidParamI18n("error.e0283") } item, err := s.buildKnowledgeBaseModel(req.CreateKnowledgeBaseRequest) if err != nil { @@ -126,15 +125,15 @@ func (s *knowledgeBaseService) UpdateKnowledgeBase(req request.UpdateKnowledgeBa func (s *knowledgeBaseService) DeleteKnowledgeBase(id int64) error { current := s.Get(id) if current == nil { - return errorsx.InvalidParam("知识库不存在") + return errorsx.InvalidParamI18n("error.e0283") } referencingAgents := repositories.AIAgentRepository.FindByKnowledgeBaseID(sqls.DB(), id) if len(referencingAgents) > 0 { if len(referencingAgents) == 1 { - return errorsx.Forbidden(fmt.Sprintf("知识库已被 AI Agent「%s」引用,请先解除绑定", referencingAgents[0].Name)) + return errorsx.ForbiddenI18n("error.knowledgeBase.referencedByAgent", referencingAgents[0].Name) } - return errorsx.Forbidden(fmt.Sprintf("知识库已被 %d 个 AI Agent 引用,请先解除绑定", len(referencingAgents))) + return errorsx.ForbiddenI18n("error.knowledgeBase.referencedByAgents", len(referencingAgents)) } if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { @@ -185,7 +184,7 @@ func (s *knowledgeBaseService) buildKnowledgeBaseModel(req request.CreateKnowled item.KnowledgeType = string(enums.KnowledgeBaseTypeDocument) } if !isValidKnowledgeType(item.KnowledgeType) { - return nil, errorsx.InvalidParam("知识库类型不支持") + return nil, errorsx.InvalidParamI18n("error.e0290") } if item.DefaultScoreThreshold == 0 { item.DefaultScoreThreshold = 0.2 @@ -202,10 +201,10 @@ func (s *knowledgeBaseService) buildKnowledgeBaseModel(req request.CreateKnowled item.ChunkMaxTokens = 0 item.ChunkOverlapTokens = 0 } else if item.ChunkProvider == string(enums.KnowledgeChunkProviderFAQ) { - return nil, errorsx.InvalidParam("文档知识库不能使用FAQ分块策略") + return nil, errorsx.InvalidParamI18n("error.e0219") } if !isValidChunkProvider(item.ChunkProvider) { - return nil, errorsx.InvalidParam("分块策略不支持") + return nil, errorsx.InvalidParamI18n("error.e0130") } if item.KnowledgeType != string(enums.KnowledgeBaseTypeFAQ) && item.ChunkTargetTokens == 0 { item.ChunkTargetTokens = 300 diff --git a/internal/services/knowledge_directory_service.go b/internal/services/knowledge_directory_service.go index 8e0b999..29cb089 100644 --- a/internal/services/knowledge_directory_service.go +++ b/internal/services/knowledge_directory_service.go @@ -42,20 +42,20 @@ func (s *knowledgeDirectoryService) Count(cnd *sqls.Cnd) int64 { func (s *knowledgeDirectoryService) CreateDirectory(req request.CreateKnowledgeDirectoryRequest, operator *dto.AuthPrincipal) (*models.KnowledgeDirectory, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("目录名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0275") } if req.KnowledgeBaseID <= 0 || KnowledgeBaseService.Get(req.KnowledgeBaseID) == nil { - return nil, errorsx.InvalidParam("知识库不存在") + return nil, errorsx.InvalidParamI18n("error.e0283") } if err := s.validateParent(req.KnowledgeBaseID, req.ParentID, 0); err != nil { return nil, err } if existing := s.findByName(req.KnowledgeBaseID, req.ParentID, name); existing != nil { - return nil, errorsx.InvalidParam("同级下已存在相同名称的目录") + return nil, errorsx.InvalidParamI18n("error.e0142") } item := &models.KnowledgeDirectory{ KnowledgeBaseID: req.KnowledgeBaseID, @@ -74,30 +74,30 @@ func (s *knowledgeDirectoryService) CreateDirectory(req request.CreateKnowledgeD func (s *knowledgeDirectoryService) UpdateDirectory(req request.UpdateKnowledgeDirectoryRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(req.ID) if item == nil { - return errorsx.InvalidParam("目录不存在") + return errorsx.InvalidParamI18n("error.e0273") } name := strings.TrimSpace(req.Name) if name == "" { - return errorsx.InvalidParam("目录名称不能为空") + return errorsx.InvalidParamI18n("error.e0275") } if req.KnowledgeBaseID <= 0 { req.KnowledgeBaseID = item.KnowledgeBaseID } if req.KnowledgeBaseID != item.KnowledgeBaseID { - return errorsx.InvalidParam("目录不能移动到其他知识库") + return errorsx.InvalidParamI18n("error.e0274") } if err := s.validateParent(item.KnowledgeBaseID, req.ParentID, req.ID); err != nil { return err } if req.ParentID > 0 && s.Count(sqls.NewCnd().Eq("parent_id", req.ID)) > 0 { - return errorsx.InvalidParam("存在子目录的目录不能移动到二级目录") + return errorsx.InvalidParamI18n("error.e0152") } if existing := s.findByName(item.KnowledgeBaseID, req.ParentID, name); existing != nil && existing.ID != req.ID { - return errorsx.InvalidParam("同级下已存在相同名称的目录") + return errorsx.InvalidParamI18n("error.e0142") } return repositories.KnowledgeDirectoryRepository.Updates(sqls.DB(), req.ID, map[string]any{ "parent_id": req.ParentID, @@ -112,16 +112,16 @@ func (s *knowledgeDirectoryService) UpdateDirectory(req request.UpdateKnowledgeD func (s *knowledgeDirectoryService) DeleteDirectory(id int64) error { item := s.Get(id) if item == nil { - return errorsx.InvalidParam("目录不存在") + return errorsx.InvalidParamI18n("error.e0273") } if s.Count(sqls.NewCnd().Eq("parent_id", id)) > 0 { - return errorsx.InvalidParam("该目录下存在子目录,无法删除") + return errorsx.InvalidParamI18n("error.e0316") } if KnowledgeDocumentService.Count(sqls.NewCnd().Eq("directory_id", id)) > 0 { - return errorsx.InvalidParam("该目录下存在文档,无法删除") + return errorsx.InvalidParamI18n("error.e0317") } if KnowledgeFAQService.Count(sqls.NewCnd().Eq("directory_id", id)) > 0 { - return errorsx.InvalidParam("该目录下存在FAQ,无法删除") + return errorsx.InvalidParamI18n("error.e0315") } return repositories.KnowledgeDirectoryRepository.Delete(sqls.DB(), id) } @@ -131,10 +131,10 @@ func (s *knowledgeDirectoryService) UpdateSort(knowledgeBaseID int64, parentID i for i, id := range ids { item := repositories.KnowledgeDirectoryRepository.Get(ctx.Tx, id) if item == nil { - return errorsx.InvalidParam("目录不存在") + return errorsx.InvalidParamI18n("error.e0273") } if item.KnowledgeBaseID != knowledgeBaseID || item.ParentID != parentID { - return errorsx.InvalidParam("只能调整同知识库同级目录排序") + return errorsx.InvalidParamI18n("error.e0140") } if err := repositories.KnowledgeDirectoryRepository.UpdateColumn(ctx.Tx, id, "sort_no", i+1); err != nil { return err @@ -150,13 +150,13 @@ func (s *knowledgeDirectoryService) RequireUsableDirectory(knowledgeBaseID int64 } item := s.Get(directoryID) if item == nil { - return nil, errorsx.InvalidParam("知识库目录不存在") + return nil, errorsx.InvalidParamI18n("error.e0287") } if item.KnowledgeBaseID != knowledgeBaseID { - return nil, errorsx.InvalidParam("知识库目录不属于当前知识库") + return nil, errorsx.InvalidParamI18n("error.e0288") } if item.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("知识库目录不可用") + return nil, errorsx.InvalidParamI18n("error.e0286") } return item, nil } @@ -192,17 +192,17 @@ func (s *knowledgeDirectoryService) validateParent(knowledgeBaseID int64, parent return nil } if parentID == selfID { - return errorsx.InvalidParam("不能将目录设为自己的子目录") + return errorsx.InvalidParamI18n("error.e0084") } parent := s.Get(parentID) if parent == nil { - return errorsx.InvalidParam("父目录不存在") + return errorsx.InvalidParamI18n("error.e0252") } if parent.KnowledgeBaseID != knowledgeBaseID { - return errorsx.InvalidParam("父目录不属于当前知识库") + return errorsx.InvalidParamI18n("error.e0253") } if parent.ParentID > 0 { - return errorsx.InvalidParam("知识库目录最多支持二级") + return errorsx.InvalidParamI18n("error.e0289") } return nil } diff --git a/internal/services/knowledge_document_service.go b/internal/services/knowledge_document_service.go index d3f4040..0e70047 100644 --- a/internal/services/knowledge_document_service.go +++ b/internal/services/knowledge_document_service.go @@ -85,14 +85,14 @@ func (s *knowledgeDocumentService) Delete(id int64) { func (s *knowledgeDocumentService) CreateKnowledgeDocument(req request.CreateKnowledgeDocumentRequest, operator *dto.AuthPrincipal) (*models.KnowledgeDocument, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } kb := KnowledgeBaseService.Get(req.KnowledgeBaseID) if kb == nil { - return nil, errorsx.InvalidParam("知识库不存在") + return nil, errorsx.InvalidParamI18n("error.e0283") } if kb.KnowledgeType == string(enums.KnowledgeBaseTypeFAQ) { - return nil, errorsx.InvalidParam("FAQ知识库不支持文档") + return nil, errorsx.InvalidParamI18n("error.e0026") } if _, err := KnowledgeDirectoryService.RequireUsableDirectory(req.KnowledgeBaseID, req.DirectoryID); err != nil { return nil, err @@ -123,18 +123,18 @@ func (s *knowledgeDocumentService) CreateKnowledgeDocument(req request.CreateKno func (s *knowledgeDocumentService) UpdateKnowledgeDocument(req request.UpdateKnowledgeDocumentRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(req.ID) if current == nil { - return errorsx.InvalidParam("文档不存在") + return errorsx.InvalidParamI18n("error.e0218") } kb := KnowledgeBaseService.Get(req.KnowledgeBaseID) if kb == nil { - return errorsx.InvalidParam("知识库不存在") + return errorsx.InvalidParamI18n("error.e0283") } if kb.KnowledgeType == string(enums.KnowledgeBaseTypeFAQ) { - return errorsx.InvalidParam("FAQ知识库不支持文档") + return errorsx.InvalidParamI18n("error.e0026") } if _, err := KnowledgeDirectoryService.RequireUsableDirectory(req.KnowledgeBaseID, req.DirectoryID); err != nil { return err @@ -185,18 +185,18 @@ func (s *knowledgeDocumentService) DeleteKnowledgeDocument(id int64) error { func (s *knowledgeDocumentService) BatchMoveKnowledgeDocuments(req request.BatchMoveKnowledgeDocumentRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } ids := uniquePositiveIDs(req.IDs) if len(ids) == 0 { - return errorsx.InvalidParam("请选择要移动的文档") + return errorsx.InvalidParamI18n("error.e0333") } kb := KnowledgeBaseService.Get(req.KnowledgeBaseID) if kb == nil { - return errorsx.InvalidParam("知识库不存在") + return errorsx.InvalidParamI18n("error.e0283") } if kb.KnowledgeType == string(enums.KnowledgeBaseTypeFAQ) { - return errorsx.InvalidParam("FAQ知识库不支持文档") + return errorsx.InvalidParamI18n("error.e0026") } if _, err := KnowledgeDirectoryService.RequireUsableDirectory(req.KnowledgeBaseID, req.DirectoryID); err != nil { return err @@ -204,10 +204,10 @@ func (s *knowledgeDocumentService) BatchMoveKnowledgeDocuments(req request.Batch for _, id := range ids { current := s.Get(id) if current == nil || current.Status == enums.StatusDeleted { - return errorsx.InvalidParam("文档不存在") + return errorsx.InvalidParamI18n("error.e0218") } if current.KnowledgeBaseID != req.KnowledgeBaseID { - return errorsx.InvalidParam("只能移动当前知识库下的文档") + return errorsx.InvalidParamI18n("error.e0139") } } now := time.Now() @@ -240,7 +240,7 @@ func (s *knowledgeDocumentService) BatchMoveKnowledgeDocuments(req request.Batch func (s *knowledgeDocumentService) BatchDeleteKnowledgeDocuments(req request.BatchDeleteKnowledgeDocumentRequest) error { ids := uniquePositiveIDs(req.IDs) if len(ids) == 0 { - return errorsx.InvalidParam("请选择要删除的文档") + return errorsx.InvalidParamI18n("error.e0331") } for _, id := range ids { if err := s.DeleteKnowledgeDocument(id); err != nil { @@ -255,7 +255,7 @@ func (s *knowledgeDocumentService) buildKnowledgeDocumentModel(req request.Creat req.ContentType = enums.KnowledgeDocumentContentTypeHTML } if req.ContentType != enums.KnowledgeDocumentContentTypeHTML && req.ContentType != enums.KnowledgeDocumentContentTypeMarkdown { - return nil, errorsx.InvalidParam("内容类型不支持") + return nil, errorsx.InvalidParamI18n("error.e0129") } plainText := rag.ExtractPlainText(req.Content, req.ContentType) diff --git a/internal/services/knowledge_faq_excel.go b/internal/services/knowledge_faq_excel.go index d1ff464..cf376fe 100644 --- a/internal/services/knowledge_faq_excel.go +++ b/internal/services/knowledge_faq_excel.go @@ -18,6 +18,7 @@ import ( "agent-desk/internal/pkg/dto/response" "agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/errorsx" + "agent-desk/internal/pkg/i18nx" "agent-desk/internal/pkg/utils" "agent-desk/internal/repositories" @@ -78,22 +79,22 @@ func (s *knowledgeFAQService) ExportKnowledgeFAQs(knowledgeBaseID int64) (*respo func (s *knowledgeFAQService) ImportKnowledgeFAQs(req request.ImportKnowledgeFAQRequest, operator *dto.AuthPrincipal) (*response.KnowledgeFAQImportResult, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } if req.Mode != request.KnowledgeFAQImportModeAppend && req.Mode != request.KnowledgeFAQImportModeOverwrite { - return nil, errorsx.InvalidParam("导入模式不合法") + return nil, errorsx.InvalidParamI18n("error.e0177") } if req.Reader == nil { - return nil, errorsx.InvalidParam("请选择导入文件") + return nil, errorsx.InvalidParamI18n("error.e0327") } if strings.ToLower(filepath.Ext(req.Filename)) != ".xlsx" { - return nil, errorsx.InvalidParam("仅支持.xlsx文件") + return nil, errorsx.InvalidParamI18n("error.e0089") } kb, err := s.requireFAQKnowledgeBase(req.KnowledgeBaseID) if err != nil { return nil, err } - rows, result, err := parseKnowledgeFAQImportRows(req.Reader) + rows, result, err := parseKnowledgeFAQImportRows(req.Reader, req.Locale) if err != nil { return nil, err } @@ -119,14 +120,14 @@ func (s *knowledgeFAQService) ImportKnowledgeFAQs(req request.ImportKnowledgeFAQ existing, exists := existingMap[row.Question] if exists && req.Mode == request.KnowledgeFAQImportModeAppend { result.Skipped++ - result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: row.RowNo, Message: "标准问题已存在,已跳过"}) + result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: row.RowNo, Message: i18nx.Getf(req.Locale, "error.e0237")}) continue } similarQuestions, marshalErr := json.Marshal(row.SimilarQuestions) if marshalErr != nil { result.Failed++ - result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: row.RowNo, Message: "相似问格式不合法"}) + result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: row.RowNo, Message: i18nx.Getf(req.Locale, "error.e0280")}) continue } @@ -227,33 +228,33 @@ func buildKnowledgeFAQExcelFile(filename string, workbook *excelize.File) (*resp }, nil } -func parseKnowledgeFAQImportRows(reader io.Reader) ([]knowledgeFAQImportRow, *response.KnowledgeFAQImportResult, error) { +func parseKnowledgeFAQImportRows(reader io.Reader, locale string) ([]knowledgeFAQImportRow, *response.KnowledgeFAQImportResult, error) { workbook, err := excelize.OpenReader(reader) if err != nil { - return nil, nil, errorsx.InvalidParam("Excel文件解析失败") + return nil, nil, errorsx.InvalidParamI18n("error.e0023") } defer workbook.Close() sheet := knowledgeFAQExcelSheetName if index, _ := workbook.GetSheetIndex(sheet); index < 0 { sheets := workbook.GetSheetList() if len(sheets) == 0 { - return nil, nil, errorsx.InvalidParam("Excel文件为空") + return nil, nil, errorsx.InvalidParamI18n("error.e0022") } sheet = sheets[0] } table, err := workbook.GetRows(sheet) if err != nil { - return nil, nil, errorsx.InvalidParam("Excel文件读取失败") + return nil, nil, errorsx.InvalidParamI18n("error.e0024") } if len(table) == 0 { - return nil, nil, errorsx.InvalidParam("Excel文件为空") + return nil, nil, errorsx.InvalidParamI18n("error.e0022") } headerMap := buildKnowledgeFAQHeaderMap(table[0]) if _, ok := headerMap["question"]; !ok { - return nil, nil, errorsx.InvalidParam("缺少标准问题列") + return nil, nil, errorsx.InvalidParamI18n("error.e0297") } if _, ok := headerMap["answer"]; !ok { - return nil, nil, errorsx.InvalidParam("缺少答案列") + return nil, nil, errorsx.InvalidParamI18n("error.e0298") } result := &response.KnowledgeFAQImportResult{Errors: make([]response.KnowledgeFAQImportError, 0)} @@ -275,22 +276,22 @@ func parseKnowledgeFAQImportRows(reader io.Reader) ([]knowledgeFAQImportRow, *re } if row.Question == "" { result.Failed++ - result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: "问题不能为空"}) + result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: i18nx.Getf(locale, "error.e0340")}) continue } if len([]rune(row.Question)) > 500 { result.Failed++ - result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: "问题不能超过500字"}) + result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: i18nx.Getf(locale, "error.e0341")}) continue } if row.Answer == "" { result.Failed++ - result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: "答案不能为空"}) + result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: i18nx.Getf(locale, "error.e0292")}) continue } if firstRow, exists := seen[row.Question]; exists { result.Failed++ - result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: fmt.Sprintf("同一文件中标准问题重复,首次出现于第%d行", firstRow)}) + result.Errors = append(result.Errors, response.KnowledgeFAQImportError{Row: rowNo, Message: i18nx.Getf(locale, "error.knowledgeFAQImport.duplicateQuestionInFile", firstRow)}) continue } seen[row.Question] = rowNo diff --git a/internal/services/knowledge_faq_service.go b/internal/services/knowledge_faq_service.go index 407bc05..76d8edb 100644 --- a/internal/services/knowledge_faq_service.go +++ b/internal/services/knowledge_faq_service.go @@ -46,7 +46,7 @@ func (s *knowledgeFAQService) Count(cnd *sqls.Cnd) int64 { func (s *knowledgeFAQService) CreateKnowledgeFAQ(req request.CreateKnowledgeFAQRequest, operator *dto.AuthPrincipal) (*models.KnowledgeFAQ, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } kb, err := s.requireFAQKnowledgeBase(req.KnowledgeBaseID) if err != nil { @@ -76,11 +76,11 @@ func (s *knowledgeFAQService) CreateKnowledgeFAQ(req request.CreateKnowledgeFAQR func (s *knowledgeFAQService) UpdateKnowledgeFAQ(req request.UpdateKnowledgeFAQRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } current := s.Get(req.ID) if current == nil { - return errorsx.InvalidParam("FAQ不存在") + return errorsx.InvalidParamI18n("error.e0025") } if _, err := s.requireFAQKnowledgeBase(req.KnowledgeBaseID); err != nil { return err @@ -114,7 +114,7 @@ func (s *knowledgeFAQService) UpdateKnowledgeFAQ(req request.UpdateKnowledgeFAQR func (s *knowledgeFAQService) DeleteKnowledgeFAQ(id int64) error { current := s.Get(id) if current == nil { - return errorsx.InvalidParam("FAQ不存在") + return errorsx.InvalidParamI18n("error.e0025") } if err := repositories.KnowledgeFAQRepository.Delete(sqls.DB(), id); err != nil { return err @@ -124,11 +124,11 @@ func (s *knowledgeFAQService) DeleteKnowledgeFAQ(id int64) error { func (s *knowledgeFAQService) BatchMoveKnowledgeFAQs(req request.BatchMoveKnowledgeFAQRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } ids := uniquePositiveIDs(req.IDs) if len(ids) == 0 { - return errorsx.InvalidParam("请选择要移动的FAQ") + return errorsx.InvalidParamI18n("error.e0332") } if _, err := s.requireFAQKnowledgeBase(req.KnowledgeBaseID); err != nil { return err @@ -139,10 +139,10 @@ func (s *knowledgeFAQService) BatchMoveKnowledgeFAQs(req request.BatchMoveKnowle for _, id := range ids { current := s.Get(id) if current == nil { - return errorsx.InvalidParam("FAQ不存在") + return errorsx.InvalidParamI18n("error.e0025") } if current.KnowledgeBaseID != req.KnowledgeBaseID { - return errorsx.InvalidParam("只能移动当前知识库下的FAQ") + return errorsx.InvalidParamI18n("error.e0138") } } now := time.Now() @@ -175,7 +175,7 @@ func (s *knowledgeFAQService) BatchMoveKnowledgeFAQs(req request.BatchMoveKnowle func (s *knowledgeFAQService) BatchDeleteKnowledgeFAQs(req request.BatchDeleteKnowledgeFAQRequest) error { ids := uniquePositiveIDs(req.IDs) if len(ids) == 0 { - return errorsx.InvalidParam("请选择要删除的FAQ") + return errorsx.InvalidParamI18n("error.e0330") } for _, id := range ids { if err := s.DeleteKnowledgeFAQ(id); err != nil { @@ -187,17 +187,17 @@ func (s *knowledgeFAQService) BatchDeleteKnowledgeFAQs(req request.BatchDeleteKn func (s *knowledgeFAQService) buildKnowledgeFAQModel(req request.CreateKnowledgeFAQRequest) (*models.KnowledgeFAQ, error) { if req.KnowledgeBaseID <= 0 { - return nil, errorsx.InvalidParam("知识库不存在") + return nil, errorsx.InvalidParamI18n("error.e0283") } if req.Question == "" { - return nil, errorsx.InvalidParam("问题不能为空") + return nil, errorsx.InvalidParamI18n("error.e0340") } if req.Answer == "" { - return nil, errorsx.InvalidParam("答案不能为空") + return nil, errorsx.InvalidParamI18n("error.e0292") } similarQuestions, err := json.Marshal(normalizeSimilarQuestions(req.SimilarQuestions)) if err != nil { - return nil, errorsx.InvalidParam("相似问格式不合法") + return nil, errorsx.InvalidParamI18n("error.e0280") } return &models.KnowledgeFAQ{ KnowledgeBaseID: req.KnowledgeBaseID, @@ -212,10 +212,10 @@ func (s *knowledgeFAQService) buildKnowledgeFAQModel(req request.CreateKnowledge func (s *knowledgeFAQService) requireFAQKnowledgeBase(knowledgeBaseID int64) (*models.KnowledgeBase, error) { kb := KnowledgeBaseService.Get(knowledgeBaseID) if kb == nil { - return nil, errorsx.InvalidParam("知识库不存在") + return nil, errorsx.InvalidParamI18n("error.e0283") } if kb.KnowledgeType != "faq" { - return nil, errorsx.InvalidParam("当前知识库不是FAQ知识库") + return nil, errorsx.InvalidParamI18n("error.e0199") } return kb, nil } diff --git a/internal/services/login_session_service.go b/internal/services/login_session_service.go index 686c8a9..498ac15 100644 --- a/internal/services/login_session_service.go +++ b/internal/services/login_session_service.go @@ -71,7 +71,7 @@ func (s *loginSessionService) Delete(id int64) { func (s *loginSessionService) Revoke(id int64, operatorID int64, operatorName string) error { session := s.Get(id) if session == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } now := time.Now() return s.Updates(id, map[string]any{ diff --git a/internal/services/mcp_debug_service.go b/internal/services/mcp_debug_service.go index 30233b6..9b3deab 100644 --- a/internal/services/mcp_debug_service.go +++ b/internal/services/mcp_debug_service.go @@ -95,18 +95,18 @@ func (s *mCPDebugService) CallTool(ctx context.Context, serverCode string, toolN func (s *mCPDebugService) resolveServer(serverCode string) (mcps.ServerConfig, error) { cfg := config.Current() if !cfg.MCP.Enabled { - return mcps.ServerConfig{}, errorsx.InvalidParam("MCP未启用") + return mcps.ServerConfig{}, errorsx.InvalidParamI18n("error.e0035") } serverCode = strings.TrimSpace(serverCode) if serverCode == "" { - return mcps.ServerConfig{}, errorsx.InvalidParam("serverCode不能为空") + return mcps.ServerConfig{}, errorsx.InvalidParamI18n("error.e0070") } server, ok := cfg.MCP.Servers[serverCode] if !ok { - return mcps.ServerConfig{}, errorsx.InvalidParam("MCP服务配置不存在") + return mcps.ServerConfig{}, errorsx.InvalidParamI18n("error.e0034") } if !server.Enabled { - return mcps.ServerConfig{}, errorsx.InvalidParam("MCP服务未启用") + return mcps.ServerConfig{}, errorsx.InvalidParamI18n("error.e0033") } return mcps.ServerConfig{ Code: serverCode, diff --git a/internal/services/message_service.go b/internal/services/message_service.go index 70ab2a3..48fe4a1 100644 --- a/internal/services/message_service.go +++ b/internal/services/message_service.go @@ -122,7 +122,7 @@ func (s *messageService) GetConversationReadTarget(conversationID, messageID int if messageID > 0 { message := s.Get(messageID) if message == nil || message.ConversationID != conversationID { - return nil, errorsx.InvalidParam("消息不存在") + return nil, errorsx.InvalidParamI18n("error.e0244") } return message, nil } @@ -138,7 +138,7 @@ func (s *messageService) SendMessage(conversationID int64, senderType enums.IMSe case enums.IMSenderTypeCustomer: return s.sendMessage(conversationID, enums.IMSenderTypeCustomer, 0, clientMsgID, messageType, content, payload, nil, external, "") default: - return nil, errorsx.InvalidParam("不支持的发送人类型") + return nil, errorsx.InvalidParamI18n("error.e0080") } } @@ -152,24 +152,24 @@ func (s *messageService) SendAgentMessageWithRequestID(conversationID int64, req func (s *messageService) RecallAgentMessage(messageID int64, operator *dto.AuthPrincipal) (*models.Message, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } if messageID <= 0 { - return nil, errorsx.InvalidParam("消息不存在") + return nil, errorsx.InvalidParamI18n("error.e0244") } message := s.Get(messageID) if message == nil { - return nil, errorsx.InvalidParam("消息不存在") + return nil, errorsx.InvalidParamI18n("error.e0244") } if message.SenderType != enums.IMSenderTypeAgent { - return nil, errorsx.InvalidParam("仅支持撤回客服消息") + return nil, errorsx.InvalidParamI18n("error.e0091") } if message.SenderID != operator.UserID { - return nil, errorsx.Forbidden("仅允许撤回自己发送的消息") + return nil, errorsx.ForbiddenI18n("error.e0087") } if message.RecalledAt != nil || message.SendStatus == enums.IMMessageStatusRecalled { - return nil, errorsx.InvalidParam("消息已撤回") + return nil, errorsx.InvalidParamI18n("error.e0246") } conversation, err := s.ValidateConversationSender(message.ConversationID, enums.IMSenderTypeAgent, operator, nil) @@ -260,10 +260,10 @@ func (s *messageService) SendAIServiceNotice(conversationID int64, aiAgentID int func (s *messageService) SendAIServiceNoticeWithRequestID(conversationID int64, aiAgentID int64, content string, requestID string) (*models.Message, error) { conversation := ConversationService.Get(conversationID) if conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } if conversation.Status == enums.IMConversationStatusClosed { - return nil, errorsx.InvalidParam("会话已关闭") + return nil, errorsx.InvalidParamI18n("error.e0119") } return s.sendValidatedMessage(conversation, enums.IMSenderTypeAI, aiAgentID, strs.UUID(), enums.IMMessageTypeText, content, "", &dto.AuthPrincipal{ UserID: 0, @@ -378,10 +378,10 @@ func (s *messageService) sendMessage(conversationID int64, senderType enums.IMSe if senderType == enums.IMSenderTypeCustomer { if external == nil || strings.TrimSpace(external.ExternalID) == "" { - return nil, errorsx.Unauthorized("外部用户标识不能为空") + return nil, errorsx.UnauthorizedI18n("error.e0149") } } else if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } if strs.IsBlank(string(messageType)) { @@ -404,7 +404,7 @@ func (s *messageService) sendValidatedMessage(conversation *models.Conversation, return nil, err } if strs.IsBlank(content) && strs.IsBlank(payload) { - return nil, errorsx.InvalidParam("消息内容不能为空") + return nil, errorsx.InvalidParamI18n("error.e0245") } // 防抖,消息存在就不再发送了 @@ -610,11 +610,11 @@ func (s *messageService) normalizeMessageContent(conversationID int64, messageTy sanitized := utils.SanitizeMessageHTML(content) normalized, err := utils.NormalizeMessageHTMLAssets(sanitized) if err != nil { - return "", "", "", errorsx.InvalidParam("HTML消息中的图片必须使用已上传文件") + return "", "", "", errorsx.InvalidParamI18n("error.e0030") } summary := utils.BuildHTMLSummary(normalized) if summary == "" { - return "", "", "", errorsx.InvalidParam("消息内容不能为空") + return "", "", "", errorsx.InvalidParamI18n("error.e0245") } return normalized, "", summary, nil case enums.IMMessageTypeImage, enums.IMMessageTypeAttachment: @@ -639,7 +639,7 @@ func (s *messageService) normalizeMessageContent(conversationID int64, messageTy default: content = strings.TrimSpace(content) if content == "" && strings.TrimSpace(payload) == "" { - return "", "", "", errorsx.InvalidParam("消息内容不能为空") + return "", "", "", errorsx.InvalidParamI18n("error.e0245") } return content, strings.TrimSpace(payload), buildMessageSummary(messageType, content), nil } @@ -648,38 +648,38 @@ func (s *messageService) normalizeMessageContent(conversationID int64, messageTy func (s *messageService) ValidateConversationSender(conversationID int64, senderType enums.IMSenderType, operator *dto.AuthPrincipal, external *openidentity.ExternalUser) (*models.Conversation, error) { conversation := ConversationService.Get(conversationID) if conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } if conversation.Status == enums.IMConversationStatusClosed { - return nil, errorsx.InvalidParam("会话已关闭") + return nil, errorsx.InvalidParamI18n("error.e0119") } switch senderType { case enums.IMSenderTypeAgent: if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } if conversation.Status != enums.IMConversationStatusActive || conversation.CurrentAssigneeID == 0 { - return nil, errorsx.InvalidParam("会话未分配客服,暂不允许发送消息") + return nil, errorsx.InvalidParamI18n("error.e0120") } if conversation.CurrentAssigneeID != operator.UserID { - return nil, errorsx.Forbidden("当前会话已分配给其他客服") + return nil, errorsx.ForbiddenI18n("error.e0191") } case enums.IMSenderTypeAI: if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } if conversation.Status != enums.IMConversationStatusAIServing && !s.allowAIMessageOnPendingHandoff(conversation) { - return nil, errorsx.Forbidden("当前会话不处于 AI 接待状态") + return nil, errorsx.ForbiddenI18n("error.e0189") } if conversation.CurrentAssigneeID != 0 { - return nil, errorsx.Forbidden("当前会话已由人工客服接管") + return nil, errorsx.ForbiddenI18n("error.e0192") } case enums.IMSenderTypeCustomer: if external == nil || !ConversationService.IsCustomerConversationOwner(conversation, *external) { - return nil, errorsx.Forbidden("无权访问该会话") + return nil, errorsx.ForbiddenI18n("error.e0222") } default: - return nil, errorsx.InvalidParam("不支持的发送人类型") + return nil, errorsx.InvalidParamI18n("error.e0080") } return conversation, nil } diff --git a/internal/services/notification_service.go b/internal/services/notification_service.go index 9044452..804c534 100644 --- a/internal/services/notification_service.go +++ b/internal/services/notification_service.go @@ -26,7 +26,7 @@ type notificationService struct { func (s *notificationService) Create(req request.CreateNotificationRequest) (*models.Notification, error) { if req.RecipientUserID <= 0 { - return nil, errorsx.InvalidParam("接收人不能为空") + return nil, errorsx.InvalidParamI18n("error.e0212") } now := time.Now() item := &models.Notification{ @@ -82,11 +82,11 @@ func (s *notificationService) CountUnread(userID int64) int64 { func (s *notificationService) MarkRead(id int64, userID int64) error { if id <= 0 { - return errorsx.InvalidParam("通知不存在") + return errorsx.InvalidParamI18n("error.e0337") } item := repositories.NotificationRepository.Get(sqls.DB(), id) if item == nil || item.RecipientUserID != userID { - return errorsx.InvalidParam("通知不存在") + return errorsx.InvalidParamI18n("error.e0337") } if item.ReadAt != nil { return nil @@ -99,7 +99,7 @@ func (s *notificationService) MarkRead(id int64, userID int64) error { func (s *notificationService) MarkAllRead(userID int64) error { if userID <= 0 { - return errorsx.InvalidParam("接收人不能为空") + return errorsx.InvalidParamI18n("error.e0212") } return repositories.NotificationRepository.MarkAllRead(sqls.DB(), userID, time.Now()) } diff --git a/internal/services/oidc_login_service.go b/internal/services/oidc_login_service.go index 4f2766a..7cb54b4 100644 --- a/internal/services/oidc_login_service.go +++ b/internal/services/oidc_login_service.go @@ -87,7 +87,7 @@ func (s *oidcLoginService) loginWithOIDCProfile(profile *oidcLoginProfile, authC } if user.Status != enums.StatusOk { - return errorsx.Unauthorized("当前系统账号已被禁用") + return errorsx.UnauthorizedI18n("error.e0200") } if err = repositories.UserRepository.Updates(ctx.Tx, user.ID, map[string]any{ diff --git a/internal/services/quick_reply_service.go b/internal/services/quick_reply_service.go index eb90e32..460056d 100644 --- a/internal/services/quick_reply_service.go +++ b/internal/services/quick_reply_service.go @@ -74,12 +74,12 @@ func (s *quickReplyService) Delete(id int64) { func (s *quickReplyService) CreateQuickReply(req request.CreateQuickReplyRequest, operator *dto.AuthPrincipal) (*models.QuickReply, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } title := strings.TrimSpace(req.Title) content := strings.TrimSpace(req.Content) if title == "" || content == "" { - return nil, errorsx.InvalidParam("标题和内容不能为空") + return nil, errorsx.InvalidParamI18n("error.e0240") } item := &models.QuickReply{ GroupName: strings.TrimSpace(req.GroupName), @@ -97,11 +97,11 @@ func (s *quickReplyService) CreateQuickReply(req request.CreateQuickReplyRequest func (s *quickReplyService) UpdateQuickReply(req request.UpdateQuickReplyRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(req.ID) if item == nil { - return errorsx.InvalidParam("快捷回复不存在") + return errorsx.InvalidParamI18n("error.e0203") } return s.Updates(req.ID, map[string]any{ "group_name": strings.TrimSpace(req.GroupName), @@ -118,7 +118,7 @@ func (s *quickReplyService) UpdateQuickReply(req request.UpdateQuickReplyRequest func (s *quickReplyService) DeleteQuickReply(id int64) error { item := s.Get(id) if item == nil { - return errorsx.InvalidParam("快捷回复不存在") + return errorsx.InvalidParamI18n("error.e0203") } s.Delete(id) return nil diff --git a/internal/services/role_service.go b/internal/services/role_service.go index b7abb23..cf1046b 100644 --- a/internal/services/role_service.go +++ b/internal/services/role_service.go @@ -78,10 +78,10 @@ func (s *roleService) CreateRole(req request.CreateRoleRequest, operator *dto.Au name := strings.TrimSpace(req.Name) code := strings.TrimSpace(req.Code) if name == "" || code == "" { - return nil, errorsx.InvalidParam("角色名称和编码不能为空") + return nil, errorsx.InvalidParamI18n("error.e0306") } if s.Take("code = ?", code) != nil { - return nil, errorsx.InvalidParam("角色编码已存在") + return nil, errorsx.InvalidParamI18n("error.e0308") } role := &models.Role{ @@ -102,7 +102,7 @@ func (s *roleService) CreateRole(req request.CreateRoleRequest, operator *dto.Au func (s *roleService) UpdateRole(req request.UpdateRoleRequest, operator *dto.AuthPrincipal) error { role := s.Get(req.ID) if role == nil { - return errorsx.InvalidParam("角色不存在") + return errorsx.InvalidParamI18n("error.e0305") } now := time.Now() return s.Updates(req.ID, map[string]any{ @@ -136,13 +136,13 @@ func (s *roleService) UpdateSort(ids []int64) error { func (s *roleService) DeleteRole(id int64) error { role := s.Get(id) if role == nil { - return errorsx.InvalidParam("角色不存在") + return errorsx.InvalidParamI18n("error.e0305") } if role.IsSystem { - return errorsx.Forbidden("系统内置角色不允许删除") + return errorsx.ForbiddenI18n("error.e0293") } if UserRoleService.Take("role_id = ?", id) != nil { - return errorsx.Forbidden("角色已被用户使用,无法删除") + return errorsx.ForbiddenI18n("error.e0307") } s.Delete(id) return nil @@ -151,10 +151,10 @@ func (s *roleService) DeleteRole(id int64) error { func (s *roleService) UpdateStatus(id int64, status enums.Status, operator *dto.AuthPrincipal) error { role := s.Get(id) if role == nil { - return errorsx.InvalidParam("角色不存在") + return errorsx.InvalidParamI18n("error.e0305") } if !slices.Contains(enums.StatusValues, status) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } if err := s.Updates(id, map[string]any{ "status": status, @@ -170,7 +170,7 @@ func (s *roleService) UpdateStatus(id int64, status enums.Status, operator *dto. func (s *roleService) AssignPermissions(roleID int64, permissionIDs []int64, operator *dto.AuthPrincipal) error { role := s.Get(roleID) if role == nil { - return errorsx.InvalidParam("角色不存在") + return errorsx.InvalidParamI18n("error.e0305") } return s.replaceRolePermissions(roleID, permissionIDs, operator) @@ -184,7 +184,7 @@ func (s *roleService) replaceRolePermissions(roleID int64, permissionIDs []int64 for _, permissionID := range permissionIDs { permission := PermissionService.Get(permissionID) if permission == nil { - return errorsx.InvalidParam("权限不存在") + return errorsx.InvalidParamI18n("error.e0236") } relation := &models.RolePermission{ RoleID: roleID, diff --git a/internal/services/skill_definition_service.go b/internal/services/skill_definition_service.go index dba0bc2..ddf83b3 100644 --- a/internal/services/skill_definition_service.go +++ b/internal/services/skill_definition_service.go @@ -86,14 +86,14 @@ func (s *skillDefinitionService) GetByIDs(ids []int64) map[int64]models.SkillDef func (s *skillDefinitionService) CreateSkillDefinition(req request.CreateSkillDefinitionRequest, operator *dto.AuthPrincipal) (*models.SkillDefinition, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } normalized, err := s.normalizeSkillDefinitionRequest(req) if err != nil { return nil, err } if s.Take("code = ?", normalized.Code) != nil { - return nil, errorsx.InvalidParam("Skill 编码已存在") + return nil, errorsx.InvalidParamI18n("error.e0058") } item := &models.SkillDefinition{ Code: normalized.Code, @@ -114,21 +114,21 @@ func (s *skillDefinitionService) CreateSkillDefinition(req request.CreateSkillDe func (s *skillDefinitionService) UpdateSkillDefinition(req request.UpdateSkillDefinitionRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } if req.ID <= 0 { - return errorsx.InvalidParam("Skill ID 不合法") + return errorsx.InvalidParamI18n("error.e0052") } current := s.Get(req.ID) if current == nil { - return errorsx.InvalidParam("Skill 不存在") + return errorsx.InvalidParamI18n("error.e0053") } normalized, err := s.normalizeSkillDefinitionRequest(req.CreateSkillDefinitionRequest) if err != nil { return err } if exists := s.Take("code = ? AND id <> ?", normalized.Code, req.ID); exists != nil { - return errorsx.InvalidParam("Skill 编码已存在") + return errorsx.InvalidParamI18n("error.e0058") } return repositories.SkillDefinitionRepository.Updates(sqls.DB(), req.ID, map[string]any{ "code": normalized.Code, @@ -153,13 +153,13 @@ func (s *skillDefinitionService) normalizeSkillDefinitionRequest(req request.Cre Remark: strings.TrimSpace(req.Remark), } if normalized.Code == "" { - return nil, errorsx.InvalidParam("Skill 编码不能为空") + return nil, errorsx.InvalidParamI18n("error.e0057") } if normalized.Name == "" { - return nil, errorsx.InvalidParam("Skill 名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0055") } if normalized.Instruction == "" { - return nil, errorsx.InvalidParam("技能说明不能为空") + return nil, errorsx.InvalidParamI18n("error.e0207") } examples, err := normalizeSkillStringArray(req.Examples) if err != nil { @@ -182,11 +182,11 @@ func (s *skillDefinitionService) normalizeSkillDefinitionRequest(req request.Cre func normalizeSkillStringArray(input []string) ([]string, error) { buf, err := json.Marshal(input) if err != nil { - return nil, errorsx.InvalidParam("JSON 数组格式不合法") + return nil, errorsx.InvalidParamI18n("error.e0031") } var ret []string if err := json.Unmarshal(buf, &ret); err != nil { - return nil, errorsx.InvalidParam("JSON 数组格式不合法") + return nil, errorsx.InvalidParamI18n("error.e0031") } normalized := make([]string, 0, len(ret)) seen := make(map[string]struct{}, len(ret)) diff --git a/internal/services/skill_runtime_service.go b/internal/services/skill_runtime_service.go index fe7fda7..075cb89 100644 --- a/internal/services/skill_runtime_service.go +++ b/internal/services/skill_runtime_service.go @@ -22,13 +22,13 @@ type skillRuntimeService struct{} func (s *skillRuntimeService) DebugRun(ctx context.Context, req request.SkillDebugRunRequest) (*response.SkillDebugRunResponse, error) { if req.AIAgentID <= 0 { - return nil, errorsx.InvalidParam("aiAgentId不能为空") + return nil, errorsx.InvalidParamI18n("error.e0061") } if strings.TrimSpace(req.SkillCode) == "" { - return nil, errorsx.InvalidParam("skillCode不能为空") + return nil, errorsx.InvalidParamI18n("error.e0071") } if strings.TrimSpace(req.UserMessage) == "" { - return nil, errorsx.InvalidParam("userMessage不能为空") + return nil, errorsx.InvalidParamI18n("error.e0078") } if SkillDebugRunHook == nil { return nil, fmt.Errorf("skill debug runner is not initialized") @@ -38,13 +38,13 @@ func (s *skillRuntimeService) DebugRun(ctx context.Context, req request.SkillDeb func (s *skillRuntimeService) DebugResume(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) { if req.AIAgentID <= 0 { - return nil, errorsx.InvalidParam("aiAgentId不能为空") + return nil, errorsx.InvalidParamI18n("error.e0061") } if strings.TrimSpace(req.CheckPointID) == "" { - return nil, errorsx.InvalidParam("checkPointId不能为空") + return nil, errorsx.InvalidParamI18n("error.e0063") } if strings.TrimSpace(req.UserMessage) == "" { - return nil, errorsx.InvalidParam("userMessage不能为空") + return nil, errorsx.InvalidParamI18n("error.e0078") } if SkillDebugResumeHook == nil { return nil, fmt.Errorf("skill debug resume runner is not initialized") diff --git a/internal/services/storage/oss.go b/internal/services/storage/oss.go index d412be3..e8daff9 100644 --- a/internal/services/storage/oss.go +++ b/internal/services/storage/oss.go @@ -132,16 +132,16 @@ func (s *OSSStorage) getBucket() (*oss.Bucket, error) { func (s *OSSStorage) validate() error { if strings.TrimSpace(s.cfg.Endpoint) == "" { - return errorsx.InvalidParam("OSS endpoint 未配置") + return errorsx.InvalidParamI18n("error.e0051") } if strings.TrimSpace(s.cfg.Bucket) == "" { - return errorsx.InvalidParam("OSS bucket 未配置") + return errorsx.InvalidParamI18n("error.e0050") } if strings.TrimSpace(s.cfg.AccessKeyID) == "" { - return errorsx.InvalidParam("OSS accessKeyId 未配置") + return errorsx.InvalidParamI18n("error.e0048") } if strings.TrimSpace(s.cfg.AccessKeySecret) == "" { - return errorsx.InvalidParam("OSS accessKeySecret 未配置") + return errorsx.InvalidParamI18n("error.e0049") } return nil } diff --git a/internal/services/storage/provider.go b/internal/services/storage/provider.go index 2a25527..b52907c 100644 --- a/internal/services/storage/provider.go +++ b/internal/services/storage/provider.go @@ -45,6 +45,6 @@ func NewProvider(provider enums.AssetProvider) (FileStorageProvider, error) { case enums.AssetProviderOSS: return NewOSSStorage(cfg.OSS), nil default: - return nil, errorsx.InvalidParam("不支持的文件存储类型") + return nil, errorsx.InvalidParamI18n("error.e0082") } } diff --git a/internal/services/tag_service.go b/internal/services/tag_service.go index 4c447c0..f0beda7 100644 --- a/internal/services/tag_service.go +++ b/internal/services/tag_service.go @@ -87,24 +87,24 @@ func (s *tagService) FindByNameAndParentID(name string, parentID int64) *models. func (s *tagService) CreateTag(req request.CreateTagRequest, operator *dto.AuthPrincipal) (*models.Tag, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("标签名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0239") } if req.ParentID > 0 { parent := s.Get(req.ParentID) if parent == nil { - return nil, errorsx.InvalidParam("父标签不存在") + return nil, errorsx.InvalidParamI18n("error.e0251") } } existing := s.FindByNameAndParentID(name, req.ParentID) if existing != nil { - return nil, errorsx.InvalidParam("同级下已存在相同名称的标签") + return nil, errorsx.InvalidParamI18n("error.e0141") } item := &models.Tag{ @@ -132,32 +132,32 @@ func (s *tagService) NextSortNo(parentID int64) int { func (s *tagService) UpdateTag(req request.UpdateTagRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(req.ID) if item == nil { - return errorsx.InvalidParam("标签不存在") + return errorsx.InvalidParamI18n("error.e0238") } name := strings.TrimSpace(req.Name) if name == "" { - return errorsx.InvalidParam("标签名称不能为空") + return errorsx.InvalidParamI18n("error.e0239") } if req.ParentID > 0 { if req.ParentID == req.ID { - return errorsx.InvalidParam("不能将标签设为自己的子标签") + return errorsx.InvalidParamI18n("error.e0083") } parent := s.Get(req.ParentID) if parent == nil { - return errorsx.InvalidParam("父标签不存在") + return errorsx.InvalidParamI18n("error.e0251") } } existing := s.FindByNameAndParentID(name, req.ParentID) if existing != nil && existing.ID != req.ID { - return errorsx.InvalidParam("同级下已存在相同名称的标签") + return errorsx.InvalidParamI18n("error.e0141") } return s.Updates(req.ID, map[string]any{ @@ -184,17 +184,17 @@ func (s *tagService) UpdateSort(ids []int64) error { func (s *tagService) DeleteTag(id int64) error { item := s.Get(id) if item == nil { - return errorsx.InvalidParam("标签不存在") + return errorsx.InvalidParamI18n("error.e0238") } if s.HasChildren(id) { - return errorsx.InvalidParam("该标签下存在子标签,无法删除") + return errorsx.InvalidParamI18n("error.e0310") } if ConversationTagService.Take("tag_id = ?", id) != nil { - return errorsx.InvalidParam("该标签已关联会话,无法删除") + return errorsx.InvalidParamI18n("error.e0311") } if TicketTagService.Take("tag_id = ?", id) != nil { - return errorsx.InvalidParam("该标签已关联工单,无法删除") + return errorsx.InvalidParamI18n("error.e0312") } s.Delete(id) @@ -247,16 +247,16 @@ func (s *tagService) GetSelfAndDescendantIDs(tagID int64) []int64 { func (s *tagService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(id) if item == nil { - return errorsx.InvalidParam("标签不存在") + return errorsx.InvalidParamI18n("error.e0238") } if status != int(enums.StatusOk) && status != int(enums.StatusDisabled) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } now := time.Now() diff --git a/internal/services/ticket_service.go b/internal/services/ticket_service.go index 669d725..6ea5969 100644 --- a/internal/services/ticket_service.go +++ b/internal/services/ticket_service.go @@ -185,22 +185,22 @@ func (s *ticketService) GetTags(ticketID int64) []models.Tag { func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } title := strings.TrimSpace(req.Title) description := strings.TrimSpace(req.Description) if title == "" { - return nil, errorsx.InvalidParam("工单标题不能为空") + return nil, errorsx.InvalidParamI18n("error.e0181") } if description == "" { - return nil, errorsx.InvalidParam("工单描述不能为空") + return nil, errorsx.InvalidParamI18n("error.e0179") } source := enums.TicketSource(strings.TrimSpace(req.Source)) if source == "" { source = enums.TicketSourceManual } if !enums.IsValidTicketSource(string(source)) { - return nil, errorsx.InvalidParam("工单来源不合法") + return nil, errorsx.InvalidParamI18n("error.e0180") } if err := s.validateTicketRefs(req.CustomerID, req.ConversationID, req.CurrentAssigneeID); err != nil { return nil, err @@ -254,11 +254,11 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator * func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConversationRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } conversation := ConversationService.Get(req.ConversationID) if conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") + return nil, errorsx.InvalidParamI18n("error.e0116") } title := strings.TrimSpace(req.Title) if title == "" { @@ -288,19 +288,19 @@ func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConve func (s *ticketService) UpdateTicket(req request.UpdateTicketRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } title := strings.TrimSpace(req.Title) description := strings.TrimSpace(req.Description) if title == "" { - return errorsx.InvalidParam("工单标题不能为空") + return errorsx.InvalidParamI18n("error.e0181") } if description == "" { - return errorsx.InvalidParam("工单描述不能为空") + return errorsx.InvalidParamI18n("error.e0179") } ticket := s.Get(req.TicketID) if ticket == nil { - return errorsx.InvalidParam("工单不存在") + return errorsx.InvalidParamI18n("error.e0178") } if err := s.validateAssignee(req.CurrentAssigneeID); err != nil { return err @@ -327,22 +327,22 @@ func (s *ticketService) UpdateTicket(req request.UpdateTicketRequest, operator * func (s *ticketService) LinkCustomer(ticketID int64, customerID int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } ticket := s.Get(ticketID) if ticket == nil { - return errorsx.InvalidParam("工单不存在") + return errorsx.InvalidParamI18n("error.e0178") } if customerID <= 0 || CustomerService.Get(customerID) == nil { - return errorsx.InvalidParam("客户不存在") + return errorsx.InvalidParamI18n("error.e0155") } if ticket.ConversationID > 0 { conversation := ConversationService.Get(ticket.ConversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if conversation.CustomerID > 0 && conversation.CustomerID != customerID { - return errorsx.InvalidParam("会话与客户不匹配") + return errorsx.InvalidParamI18n("error.e0118") } } now := time.Now() @@ -356,7 +356,7 @@ func (s *ticketService) LinkCustomer(ticketID int64, customerID int64, operator func (s *ticketService) AssignTicket(req request.AssignTicketRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } var assignedEvent *events.TicketAssignedEvent if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { @@ -377,15 +377,15 @@ func (s *ticketService) AssignTicket(req request.AssignTicketRequest, operator * func (s *ticketService) ChangeStatus(req request.ChangeTicketStatusRequest, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } status := strings.TrimSpace(req.Status) if !enums.IsValidTicketStatus(status) { - return errorsx.InvalidParam("工单状态不合法") + return errorsx.InvalidParamI18n("error.e0182") } ticket := s.Get(req.TicketID) if ticket == nil { - return errorsx.InvalidParam("工单不存在") + return errorsx.InvalidParamI18n("error.e0178") } now := time.Now() var handledAt *time.Time @@ -403,15 +403,15 @@ func (s *ticketService) ChangeStatus(req request.ChangeTicketStatusRequest, oper func (s *ticketService) AddProgress(req request.CreateTicketProgressRequest, operator *dto.AuthPrincipal) (*models.TicketProgress, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } content := strings.TrimSpace(req.Content) if content == "" { - return nil, errorsx.InvalidParam("处理进展不能为空") + return nil, errorsx.InvalidParamI18n("error.e0148") } ticket := s.Get(req.TicketID) if ticket == nil { - return nil, errorsx.InvalidParam("工单不存在") + return nil, errorsx.InvalidParamI18n("error.e0178") } now := time.Now() progress := &models.TicketProgress{ @@ -438,7 +438,7 @@ func (s *ticketService) AddProgress(req request.CreateTicketProgressRequest, ope func (s *ticketService) GetDetail(id int64) (*TicketDetailAggregate, error) { ticket := s.Get(id) if ticket == nil { - return nil, errorsx.InvalidParam("工单不存在") + return nil, errorsx.InvalidParamI18n("error.e0178") } aggregate := &TicketDetailAggregate{ Ticket: ticket, @@ -497,14 +497,14 @@ func (s *ticketService) GetSummary(operator *dto.AuthPrincipal, staleHours ...in func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequest, operator *dto.AuthPrincipal) (*events.TicketAssignedEvent, error) { ticket := repositories.TicketRepository.Get(tx, req.TicketID) if ticket == nil { - return nil, errorsx.InvalidParam("工单不存在") + return nil, errorsx.InvalidParamI18n("error.e0178") } if err := s.validateRequiredAssignee(req.ToUserID); err != nil { return nil, err } toUser := repositories.UserRepository.Get(tx, req.ToUserID) if toUser == nil || toUser.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("负责人不存在") + return nil, errorsx.InvalidParamI18n("error.e0334") } var fromUser *models.User if ticket.CurrentAssigneeID > 0 { @@ -627,15 +627,15 @@ func (s *ticketService) enrichTicketTags(db *gorm.DB, aggregate *TicketListAggre func (s *ticketService) validateTicketRefs(customerID, conversationID, assigneeID int64) error { if customerID > 0 && CustomerService.Get(customerID) == nil { - return errorsx.InvalidParam("客户不存在") + return errorsx.InvalidParamI18n("error.e0155") } if conversationID > 0 { conversation := ConversationService.Get(conversationID) if conversation == nil { - return errorsx.InvalidParam("会话不存在") + return errorsx.InvalidParamI18n("error.e0116") } if customerID > 0 && conversation.CustomerID != customerID { - return errorsx.InvalidParam("会话与客户不匹配") + return errorsx.InvalidParamI18n("error.e0118") } } return s.validateAssignee(assigneeID) @@ -650,11 +650,11 @@ func (s *ticketService) validateAssignee(userID int64) error { func (s *ticketService) validateRequiredAssignee(userID int64) error { if userID <= 0 { - return errorsx.InvalidParam("负责人不存在") + return errorsx.InvalidParamI18n("error.e0334") } user := UserService.Get(userID) if user == nil || user.Status != enums.StatusOk { - return errorsx.InvalidParam("负责人不存在") + return errorsx.InvalidParamI18n("error.e0334") } return nil } diff --git a/internal/services/ticket_tag_service.go b/internal/services/ticket_tag_service.go index 63751fc..d597601 100644 --- a/internal/services/ticket_tag_service.go +++ b/internal/services/ticket_tag_service.go @@ -66,11 +66,11 @@ func (s *ticketTagService) ValidateTagIDs(tagIDs []int64) ([]int64, error) { } tags := repositories.TagRepository.Find(sqls.DB(), sqls.NewCnd().In("id", normalized)) if len(tags) != len(normalized) { - return nil, errorsx.InvalidParam("存在无效工单标签") + return nil, errorsx.InvalidParamI18n("error.e0153") } for i := range tags { if tags[i].Status != enums.StatusOk { - return nil, errorsx.InvalidParam("存在未启用的工单标签") + return nil, errorsx.InvalidParamI18n("error.e0154") } } return normalized, nil diff --git a/internal/services/ticket_view_service.go b/internal/services/ticket_view_service.go index 63636aa..6e616ea 100644 --- a/internal/services/ticket_view_service.go +++ b/internal/services/ticket_view_service.go @@ -41,21 +41,21 @@ func (s *ticketViewService) ListByUser(userID int64) []models.TicketView { func (s *ticketViewService) Save(req request.SaveTicketViewRequest, operator *dto.AuthPrincipal) (*models.TicketView, error) { if operator == nil { - return nil, errorsx.Unauthorized("未登录或登录已过期") + return nil, errorsx.UnauthorizedI18n("error.auth.expired") } name := strings.TrimSpace(req.Name) if name == "" { - return nil, errorsx.InvalidParam("视图名称不能为空") + return nil, errorsx.InvalidParamI18n("error.e0303") } filtersJSON, err := json.Marshal(req.Filters) if err != nil { - return nil, errorsx.InvalidParam("视图筛选条件格式不正确") + return nil, errorsx.InvalidParamI18n("error.e0304") } now := time.Now() if req.ID > 0 { item := repositories.TicketViewRepository.Get(sqls.DB(), req.ID) if item == nil || item.UserID != operator.UserID { - return nil, errorsx.InvalidParam("视图不存在") + return nil, errorsx.InvalidParamI18n("error.e0302") } if err := repositories.TicketViewRepository.Updates(sqls.DB(), req.ID, map[string]any{ "name": name, @@ -82,11 +82,11 @@ func (s *ticketViewService) Save(req request.SaveTicketViewRequest, operator *dt func (s *ticketViewService) Delete(id int64, operator *dto.AuthPrincipal) error { if operator == nil { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } item := repositories.TicketViewRepository.Get(sqls.DB(), id) if item == nil || item.UserID != operator.UserID { - return errorsx.InvalidParam("视图不存在") + return errorsx.InvalidParamI18n("error.e0302") } return repositories.TicketViewRepository.Delete(sqls.DB(), id) } diff --git a/internal/services/tool_catalog_service.go b/internal/services/tool_catalog_service.go index 98087b3..28fbe61 100644 --- a/internal/services/tool_catalog_service.go +++ b/internal/services/tool_catalog_service.go @@ -95,21 +95,21 @@ func (s *toolCatalogService) ValidateToolCode(toolCode string) error { cfg := config.Current() toolCode = strings.TrimSpace(toolCode) if toolCode == "" { - return errorsx.InvalidParam("toolCode不能为空") + return errorsx.InvalidParamI18n("error.e0074") } if toolx.IsAgentDirectToolCode(toolCode) { return nil } serverCode, toolName := toolx.SplitMCPToolCode(toolCode) if serverCode == "" || toolName == "" { - return errorsx.InvalidParam("toolCode格式不合法") + return errorsx.InvalidParamI18n("error.e0075") } if !cfg.MCP.Enabled { - return errorsx.InvalidParam("MCP未启用") + return errorsx.InvalidParamI18n("error.e0035") } server, ok := cfg.MCP.Servers[serverCode] if !ok || !server.Enabled { - return errorsx.InvalidParam("toolCode 绑定的 MCP 服务不存在或未启用") + return errorsx.InvalidParamI18n("error.e0073") } return nil } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 87fb5e2..e62285b 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -91,19 +91,19 @@ func (s *userService) GetByEmail(email string) *models.User { func (s *userService) CreateUser(req request.CreateUserRequest, operator *dto.AuthPrincipal) (*models.User, string, error) { username := strings.TrimSpace(req.Username) if username == "" { - return nil, "", errorsx.InvalidParam("用户名不能为空") + return nil, "", errorsx.InvalidParamI18n("error.e0257") } if s.GetByUsername(username) != nil { - return nil, "", errorsx.InvalidParam("用户名已存在") + return nil, "", errorsx.InvalidParamI18n("error.e0259") } mobile := utils.NormalizeNullableString(req.Mobile) email := utils.NormalizeNullableString(req.Email) if mobile != nil && s.GetByMobile(*mobile) != nil { - return nil, "", errorsx.InvalidParam("手机号已存在") + return nil, "", errorsx.InvalidParamI18n("error.e0206") } if email != nil && s.GetByEmail(*email) != nil { - return nil, "", errorsx.InvalidParam("邮箱已存在") + return nil, "", errorsx.InvalidParamI18n("error.e0338") } plain, err := utils.GenerateRandomPassword(12) @@ -146,19 +146,19 @@ func (s *userService) CreateUser(req request.CreateUserRequest, operator *dto.Au func (s *userService) UpdateUser(req request.UpdateUserRequest, operator *dto.AuthPrincipal) error { user := s.Get(req.ID) if user == nil || user.DeletedAt != nil { - return errorsx.InvalidParam("用户不存在") + return errorsx.InvalidParamI18n("error.e0255") } mobile := utils.NormalizeNullableString(req.Mobile) email := utils.NormalizeNullableString(req.Email) if mobile != nil { if existed := s.GetByMobile(*mobile); existed != nil && existed.ID != req.ID { - return errorsx.InvalidParam("手机号已存在") + return errorsx.InvalidParamI18n("error.e0206") } } if email != nil { if existed := s.GetByEmail(*email); existed != nil && existed.ID != req.ID { - return errorsx.InvalidParam("邮箱已存在") + return errorsx.InvalidParamI18n("error.e0338") } } @@ -177,7 +177,7 @@ func (s *userService) UpdateUser(req request.UpdateUserRequest, operator *dto.Au func (s *userService) DeleteUser(id int64, operator *dto.AuthPrincipal) error { user := s.Get(id) if user == nil { - return errorsx.InvalidParam("用户不存在") + return errorsx.InvalidParamI18n("error.e0255") } if err := s.Updates(id, map[string]any{ @@ -195,10 +195,10 @@ func (s *userService) DeleteUser(id int64, operator *dto.AuthPrincipal) error { func (s *userService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { user := s.Get(id) if user == nil { - return errorsx.InvalidParam("用户不存在") + return errorsx.InvalidParamI18n("error.e0255") } if !slices.Contains(enums.StatusValues, enums.Status(status)) { - return errorsx.InvalidParam("状态值不合法") + return errorsx.InvalidParamI18n("error.e0254") } if err := s.Updates(id, map[string]any{ "status": status, @@ -227,7 +227,7 @@ func (s *userService) ResetPassword(userID int64, operator *dto.AuthPrincipal) ( func (s *userService) ChangeOwnPassword(password string, operator *dto.AuthPrincipal) error { if operator == nil || operator.UserID <= 0 { - return errorsx.Unauthorized("未登录或登录已过期") + return errorsx.UnauthorizedI18n("error.auth.expired") } return s.changePassword(operator.UserID, password, operator) } @@ -235,7 +235,7 @@ func (s *userService) ChangeOwnPassword(password string, operator *dto.AuthPrinc func (s *userService) AssignRoles(userID int64, roleIDs []int64, operator *dto.AuthPrincipal) error { user := s.Get(userID) if user == nil || user.DeletedAt != nil { - return errorsx.InvalidParam("用户不存在") + return errorsx.InvalidParamI18n("error.e0255") } if err := s.replaceUserRoles(userID, roleIDs, operator); err != nil { return err @@ -256,10 +256,10 @@ func (s *userService) replaceUserRolesDB(db *gorm.DB, userID int64, roleIDs []in for _, roleID := range roleIDs { role := RoleService.Get(roleID) if role == nil { - return errorsx.InvalidParam("角色不存在") + return errorsx.InvalidParamI18n("error.e0305") } if role.Status != enums.StatusOk { - return errorsx.InvalidParam("禁用角色不允许分配") + return errorsx.InvalidParamI18n("error.e0291") } relation := &models.UserRole{ UserID: userID, @@ -276,10 +276,10 @@ func (s *userService) replaceUserRolesDB(db *gorm.DB, userID int64, roleIDs []in func (s *userService) changePassword(userID int64, password string, operator *dto.AuthPrincipal) error { user := s.Get(userID) if user == nil || user.DeletedAt != nil { - return errorsx.InvalidParam("用户不存在") + return errorsx.InvalidParamI18n("error.e0255") } if strings.TrimSpace(password) == "" { - return errorsx.InvalidParam("新密码不能为空") + return errorsx.InvalidParamI18n("error.e0220") } passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) diff --git a/internal/services/ws_service.go b/internal/services/ws_service.go index 64411d7..f286f0d 100644 --- a/internal/services/ws_service.go +++ b/internal/services/ws_service.go @@ -6,6 +6,7 @@ import ( "agent-desk/internal/pkg/dto/response" "agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/errorsx" + "agent-desk/internal/pkg/i18nx" "agent-desk/internal/pkg/openidentity" "agent-desk/internal/pkg/utils" "encoding/json" @@ -44,7 +45,7 @@ func newWsService() *wsService { func (s *wsService) HandleDashboardWS(ctx *gin.Context) { principal := AuthService.GetAuthPrincipal(ctx) if principal == nil { - ctx.AbortWithStatusJSON(http.StatusUnauthorized, web.JsonError(errorsx.Unauthorized("未登录或登录已过期"))) + ctx.AbortWithStatusJSON(http.StatusUnauthorized, web.JsonErrorCode(errorsx.CodeAuthUnauthorized, i18nx.T(ctx, "error.auth.expired"))) return } if err := s.upgradeConnection(ctx, principal, nil, realtimeRoleAdmin); err != nil { @@ -57,7 +58,7 @@ func (s *wsService) HandleDashboardWS(ctx *gin.Context) { func (s *wsService) HandleDashboardNotificationWS(ctx *gin.Context) { principal := AuthService.GetAuthPrincipal(ctx) if principal == nil { - ctx.AbortWithStatusJSON(http.StatusUnauthorized, web.JsonError(errorsx.Unauthorized("未登录或登录已过期"))) + ctx.AbortWithStatusJSON(http.StatusUnauthorized, web.JsonErrorCode(errorsx.CodeAuthUnauthorized, i18nx.T(ctx, "error.auth.expired"))) return } if err := s.upgradeConnection(ctx, principal, nil, realtimeRoleNotification); err != nil { @@ -70,7 +71,7 @@ func (s *wsService) HandleDashboardNotificationWS(ctx *gin.Context) { func (s *wsService) HandleOpenWS(ctx *gin.Context) { channel := ChannelService.GetEnabledChannel(ctx) if channel == nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, web.JsonErrorMsg("接入渠道不存在或已停用")) + ctx.AbortWithStatusJSON(http.StatusBadRequest, web.JsonErrorCode(errorsx.CodeInvalidParam, i18nx.T(ctx, "error.e0209"))) return } diff --git a/internal/services/wxwork_kf_inbound_service.go b/internal/services/wxwork_kf_inbound_service.go index d8f43c6..053c258 100644 --- a/internal/services/wxwork_kf_inbound_service.go +++ b/internal/services/wxwork_kf_inbound_service.go @@ -80,7 +80,7 @@ func (s *wxWorkKFInboundService) SyncCallbackMessages(message kf.CallbackMessage func (s *wxWorkKFInboundService) consumeSyncMessage(item syncmsg.Message) error { msgID := strings.TrimSpace(item.MsgID) if msgID == "" { - return errorsx.InvalidParam("企业微信消息ID不能为空") + return errorsx.InvalidParamI18n("error.e0101") } if WxWorkKFMessageRefService.Take("wx_msg_id = ?", msgID) != nil { return nil @@ -369,7 +369,7 @@ func (s *wxWorkKFInboundService) recordOrphanEvent(item syncmsg.Message, content func (s *wxWorkKFInboundService) ensureConversation(base syncmsg.BaseMessage, profile map[string]any) (*models.Conversation, error) { externalID := strings.TrimSpace(base.ExternalUserID) if externalID == "" { - return nil, errorsx.InvalidParam("企业微信客户ID不能为空") + return nil, errorsx.InvalidParamI18n("error.e0096") } channel, err := s.getChannelByOpenKfID(base.OpenKFID) if err != nil { @@ -474,7 +474,7 @@ func (s *wxWorkKFInboundService) createMessageRef(conversationID, messageID int6 func (s *wxWorkKFInboundService) saveNextCursor(openKfID, nextCursor string) error { openKfID = strings.TrimSpace(openKfID) if openKfID == "" { - return errorsx.InvalidParam("openKfID不能为空") + return errorsx.InvalidParamI18n("error.e0068") } now := time.Now() state := WxWorkKFSyncStateService.Take("open_kf_id = ?", openKfID) @@ -517,7 +517,7 @@ func (s *wxWorkKFInboundService) appendConversationEvent(conversationID int64, c func (s *wxWorkKFInboundService) buildInboundAssetPayload(conversationID int64, mediaID string) (string, string, error) { mediaID = strings.TrimSpace(mediaID) if mediaID == "" { - return "", "", errorsx.InvalidParam("企业微信媒体ID不能为空") + return "", "", errorsx.InvalidParamI18n("error.e0095") } materialCli := wxwork.GetWorkCli().GetMaterial() data, err := materialCli.GetTempFile(mediaID) @@ -542,18 +542,18 @@ func (s *wxWorkKFInboundService) buildInboundAssetPayload(conversationID int64, func (s *wxWorkKFInboundService) getChannelByOpenKfID(openKfID string) (*models.Channel, error) { openKfID = strings.TrimSpace(openKfID) if openKfID == "" { - return nil, errorsx.InvalidParam("企业微信 openKfID 不能为空") + return nil, errorsx.InvalidParamI18n("error.e0094") } channel := ChannelService.GetEnabledWxWorkKFChannelByOpenKfID(openKfID) if channel == nil { - return nil, errorsx.InvalidParam("未找到匹配的企业微信接入渠道") + return nil, errorsx.InvalidParamI18n("error.e0231") } if channel.AIAgentID <= 0 { - return nil, errorsx.InvalidParam("企业微信接入渠道未绑定AI Agent") + return nil, errorsx.InvalidParamI18n("error.e0098") } agent := AIAgentService.Get(channel.AIAgentID) if agent == nil || agent.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("企业微信接入渠道绑定的AI Agent不存在或已禁用") + return nil, errorsx.InvalidParamI18n("error.e0099") } return channel, nil } diff --git a/internal/services/wxwork_kf_outbound_service.go b/internal/services/wxwork_kf_outbound_service.go index 79c5530..5f2ae83 100644 --- a/internal/services/wxwork_kf_outbound_service.go +++ b/internal/services/wxwork_kf_outbound_service.go @@ -9,6 +9,7 @@ import ( "agent-desk/internal/models" "agent-desk/internal/pkg/enums" + "agent-desk/internal/pkg/i18nx" "agent-desk/internal/pkg/utils" "agent-desk/internal/repositories" "agent-desk/internal/wxwork" @@ -233,7 +234,7 @@ func (s *wxWorkKFOutboundService) sendOutboundChunk(mapping *models.WxWorkKFConv case enums.IMMessageTypeImage: return s.sendImageMessage(mapping, message, chunk, chunkIndex) default: - return "", fmt.Errorf("不支持的企业微信下行消息类型: %s", chunk.MessageType) + return "", i18nx.Errorf("error.wxwork.unsupportedOutboundMessageType", chunk.MessageType) } } @@ -265,7 +266,7 @@ func (s *wxWorkKFOutboundService) sendTextMessage(mapping *models.WxWorkKFConver return "", err } if strings.TrimSpace(resp.MsgID) == "" { - return "", fmt.Errorf("企业微信返回的消息ID为空") + return "", i18nx.Errorf("error.e0114") } slog.Info("wxwork text message accepted", "conversation_id", message.ConversationID, @@ -281,12 +282,12 @@ func (s *wxWorkKFOutboundService) sendTextMessage(mapping *models.WxWorkKFConver func (s *wxWorkKFOutboundService) sendImageMessage(mapping *models.WxWorkKFConversation, message *models.Message, chunk wxWorkKFOutboundChunk, chunkIndex int) (string, error) { if strings.TrimSpace(chunk.AssetID) == "" { - return "", fmt.Errorf("图片消息缺少 assetId") + return "", i18nx.Errorf("error.e0145") } asset := AssetService.GetByAssetID(chunk.AssetID) if asset == nil { - return "", fmt.Errorf("图片资源不存在") + return "", i18nx.Errorf("error.e0146") } fileReader, err := AssetService.OpenReader(asset) if err != nil { @@ -315,7 +316,7 @@ func (s *wxWorkKFOutboundService) sendImageMessage(mapping *models.WxWorkKFConve return "", err } if strings.TrimSpace(uploadResp.MediaID) == "" { - return "", fmt.Errorf("企业微信返回的图片 media_id 为空") + return "", i18nx.Errorf("error.e0113") } kfCli, err := wxwork.GetWorkCli().GetKF() @@ -337,7 +338,7 @@ func (s *wxWorkKFOutboundService) sendImageMessage(mapping *models.WxWorkKFConve return "", err } if strings.TrimSpace(resp.MsgID) == "" { - return "", fmt.Errorf("企业微信返回的消息ID为空") + return "", i18nx.Errorf("error.e0114") } slog.Info("wxwork image message accepted", "conversation_id", message.ConversationID, @@ -421,19 +422,19 @@ func (s *wxWorkKFOutboundService) parseOutboxPayload(raw string) (*wxWorkKFOutbo func (s *wxWorkKFOutboundService) buildOutboundChunks(message *models.Message) ([]wxWorkKFOutboundChunk, error) { if message == nil { - return nil, fmt.Errorf("平台消息不存在") + return nil, i18nx.Errorf("error.e0186") } switch message.MessageType { case enums.IMMessageTypeText: content := strings.TrimSpace(message.Content) if content == "" { - return nil, fmt.Errorf("文本消息内容为空") + return nil, i18nx.Errorf("error.e0217") } return []wxWorkKFOutboundChunk{{MessageType: enums.IMMessageTypeText, Content: content}}, nil case enums.IMMessageTypeHTML: return s.buildHTMLChunks(message.Content) default: - return nil, fmt.Errorf("当前暂不支持企业微信下行消息类型: %s", message.MessageType) + return nil, i18nx.Errorf("error.wxwork.currentUnsupportedOutboundMessageType", message.MessageType) } } @@ -467,7 +468,7 @@ func (s *wxWorkKFOutboundService) buildHTMLChunks(content string) ([]wxWorkKFOut } } if len(chunks) == 0 { - return nil, fmt.Errorf("HTML 消息内容为空") + return nil, i18nx.Errorf("error.e0029") } return chunks, nil } diff --git a/internal/services/wxwork_login_service.go b/internal/services/wxwork_login_service.go index 9d7f019..7519533 100644 --- a/internal/services/wxwork_login_service.go +++ b/internal/services/wxwork_login_service.go @@ -47,7 +47,7 @@ func (s *wxWorkLoginService) BuildWxWorkQRCodeLoginURL(next string) (string, err func (s *wxWorkLoginService) LoginByWxWork(code, state string, authCfg config.AuthConfig, clientIP, userAgent string) (string, string, error) { next, err := wxwork.ParseState(state) if err != nil { - return "", "", errorsx.Unauthorized("企业微信登录状态无效或已过期") + return "", "", errorsx.UnauthorizedI18n("error.e0111") } profile, err := wxwork.GetUserDetail(code) if err != nil { @@ -96,7 +96,7 @@ func (s *wxWorkLoginService) loginWithWxWorkProfile(profile *wxwork.LoginUser, a } if user.Status != enums.StatusOk { - return errorsx.Unauthorized("当前系统账号已被禁用") + return errorsx.UnauthorizedI18n("error.e0200") } if err = repositories.UserRepository.Updates(ctx.Tx, user.ID, map[string]any{ diff --git a/internal/wxwork/login.go b/internal/wxwork/login.go index 9a78c16..b6c8350 100644 --- a/internal/wxwork/login.go +++ b/internal/wxwork/login.go @@ -3,6 +3,7 @@ package wxwork import ( "agent-desk/internal/pkg/dto/response" "agent-desk/internal/pkg/errorsx" + "agent-desk/internal/pkg/i18nx" "crypto/hmac" "crypto/rand" "crypto/sha256" @@ -25,7 +26,7 @@ const ( ) var ( - errStateInvalid = fmt.Errorf("企业微信登录状态无效") + errStateInvalid = i18nx.Errorf("error.e0110") loginTicketStore sync.Map ) @@ -42,13 +43,13 @@ type loginTicket struct { func BuildLoginURL(state string) (string, error) { if !Enabled() { - return "", fmt.Errorf("企业微信登录未启用") + return "", i18nx.Errorf("error.e0109") } if strings.TrimSpace(wxCfg.OAuthRedirect) == "" { - return "", fmt.Errorf("企业微信登录回调地址未配置") + return "", i18nx.Errorf("error.e0107") } if strings.TrimSpace(wxCfg.AgentID) == "" { - return "", fmt.Errorf("企业微信 AgentID 未配置") + return "", i18nx.Errorf("error.e0093") } return fmt.Sprintf( "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&agentid=%s&state=%s#wechat_redirect", @@ -61,13 +62,13 @@ func BuildLoginURL(state string) (string, error) { func BuildQRCodeLoginURL(state string) (string, error) { if !Enabled() { - return "", fmt.Errorf("企业微信登录未启用") + return "", i18nx.Errorf("error.e0109") } if strings.TrimSpace(wxCfg.OAuthRedirect) == "" { - return "", fmt.Errorf("企业微信登录回调地址未配置") + return "", i18nx.Errorf("error.e0107") } if strings.TrimSpace(wxCfg.AgentID) == "" { - return "", fmt.Errorf("企业微信 AgentID 未配置") + return "", i18nx.Errorf("error.e0093") } return fmt.Sprintf( "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s", @@ -129,7 +130,7 @@ func ParseState(state string) (string, error) { func IssueLoginTicket(loginResp *response.LoginResponse) (string, error) { if loginResp == nil { - return "", fmt.Errorf("登录结果不能为空") + return "", i18nx.Errorf("error.e0272") } ticket, err := randomToken("wlt_") if err != nil { @@ -146,15 +147,15 @@ func IssueLoginTicket(loginResp *response.LoginResponse) (string, error) { func ConsumeLoginTicket(ticket string) (*response.LoginResponse, error) { ticket = strings.TrimSpace(ticket) if ticket == "" { - return nil, errorsx.InvalidParam("ticket 不能为空") + return nil, errorsx.InvalidParamI18n("error.e0072") } value, ok := loginTicketStore.LoadAndDelete(ticket) if !ok { - return nil, errorsx.Unauthorized("登录票据无效或已过期") + return nil, errorsx.UnauthorizedI18n("error.e0271") } record, ok := value.(loginTicket) if !ok || record.Response == nil || time.Now().After(record.ExpiredAt) { - return nil, errorsx.Unauthorized("登录票据无效或已过期") + return nil, errorsx.UnauthorizedI18n("error.e0271") } return record.Response, nil } @@ -194,11 +195,11 @@ func randomToken(prefix string) (string, error) { func GetUserDetail(code string) (*LoginUser, error) { if !Enabled() { - return nil, fmt.Errorf("企业微信登录未启用") + return nil, i18nx.Errorf("error.e0109") } code = strings.TrimSpace(code) if code == "" { - return nil, fmt.Errorf("微信授权 code 不能为空") + return nil, i18nx.Errorf("error.e0202") } oauthClient := w.GetOauth() @@ -207,7 +208,7 @@ func GetUserDetail(code string) (*LoginUser, error) { return nil, err } if strings.TrimSpace(userInfo.UserID) == "" { - return nil, fmt.Errorf("当前登录身份不是企业内部成员") + return nil, i18nx.Errorf("error.e0198") } ret := &LoginUser{