Refactor localization strings to English across various services and UI components

- Updated titles and subtitles in channel_service.go for web channel and WeChat MP channel configurations.
- Changed handoff messages in conversation_human_dispatch_service.go to English.
- Modified notification messages in event handlers for ticket and conversation assignments to English.
- Adjusted ticket creation messages in ticket_service.go to reflect English localization.
- Updated default locale settings in i18n configuration files to English (en-US).
- Changed HTML language attribute in layout.tsx to English.
- Refactored widget locale detection logic in agent-desk-sdk.ts to prioritize English.
This commit is contained in:
mlogclub
2026-05-31 20:06:44 +08:00
parent bcf2ee38c0
commit b128447a7e
36 changed files with 315 additions and 164 deletions
@@ -183,13 +183,13 @@ func recommendNextAction(intent string, signals []string, input AnalyzeConversat
func recommendQuestions(intent string, signals []string, input AnalyzeConversationInput) []string {
questions := make([]string, 0, 3)
if containsSignal(signals, "ticket_expected") && strings.TrimSpace(input.ObservedIssue) == "" {
questions = append(questions, "请进一步确认用户遇到的具体问题现象、报错信息和期望处理结果。")
questions = append(questions, "Please confirm the specific issue, error message, and expected outcome.")
}
if containsSignal(signals, "handoff_requested") {
questions = append(questions, "请确认用户是否明确要求人工客服,以及当前问题为何需要人工继续处理。")
questions = append(questions, "Please confirm whether the user explicitly requested human support and why the issue needs human handling.")
}
if intent == "complaint" {
questions = append(questions, "请确认投诉点、影响范围和用户当前最希望解决的事项。")
questions = append(questions, "Please confirm the complaint, impact, and the user's most important desired outcome.")
}
return questions
}
@@ -89,7 +89,7 @@ func (g *CreateTicketGraph) Run(ctx context.Context, argumentsInJSON string) (st
Handled: true,
Terminal: true,
Action: "ticket_created",
ReplyText: fmt.Sprintf("工单已创建,工单号:%s,标题:%s", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)),
ReplyText: fmt.Sprintf("Ticket created. Ticket no: %s. Title: %s.", strings.TrimSpace(item.TicketNo), strings.TrimSpace(item.Title)),
ShouldRetry: false,
}), nil
case ConfirmationDecisionCancel:
@@ -134,7 +134,7 @@ func (g *CreateTicketGraph) buildCreateRequest(argumentsInJSON string) (request.
}
func (g *CreateTicketGraph) buildConfirmationPrompt(req request.CreateTicketFromConversationRequest) string {
return fmt.Sprintf("我准备为你创建工单。\n标题:%s\n描述:%s\n请直接回复“确认”或“取消”。",
return fmt.Sprintf("I am ready to create a ticket for you.\nTitle: %s\nDescription: %s\nPlease reply with \"Confirm\" or \"Cancel\".",
strings.TrimSpace(req.Title), strings.TrimSpace(req.Description))
}
+2 -2
View File
@@ -122,7 +122,7 @@ func (g *HandoffGraph) Run(ctx context.Context, argumentsInJSON string) (string,
}
func (g *HandoffGraph) buildReason(argumentsInJSON string) (string, error) {
reason := "用户需要转人工支持"
reason := "The user needs human support."
var args handoffGraphArgs
if strings.TrimSpace(argumentsInJSON) != "" {
if err := json.Unmarshal([]byte(argumentsInJSON), &args); err != nil {
@@ -136,7 +136,7 @@ func (g *HandoffGraph) buildReason(argumentsInJSON string) (string, error) {
}
func (g *HandoffGraph) buildConfirmationPrompt(reason string) string {
return fmt.Sprintf("我准备为你转接人工客服。\n原因:%s\n请直接回复“确认”或“取消”。", strings.TrimSpace(reason))
return fmt.Sprintf("I am ready to connect you to a human support agent.\nReason: %s\nPlease reply with \"Confirm\" or \"Cancel\".", strings.TrimSpace(reason))
}
func parseHandoffDecision(value string) ConfirmationDecision {
+11 -8
View File
@@ -5,11 +5,11 @@ import "strings"
const (
InterruptTypeTicketCreationConfirmation = "ticket_creation_confirmation"
InterruptTypeHandoffConfirmation = "handoff_confirmation"
ConfirmOrCancelPrompt = "请回复“确认”或“取消”。"
NeedExplicitConfirmationPrompt = "我需要你的明确确认,请直接回复“确认”或“取消”。"
ConfirmationExpiredReply = "本次确认已失效,请重新发起。"
CancelCreateTicketReply = "已取消本次工单创建。"
CancelHandoffReply = "已取消本次转人工。"
ConfirmOrCancelPrompt = `Please reply with "Confirm" or "Cancel".`
NeedExplicitConfirmationPrompt = `I need your explicit confirmation. Please reply with "Confirm" or "Cancel".`
ConfirmationExpiredReply = "This confirmation has expired. Please start again."
CancelCreateTicketReply = "Ticket creation has been cancelled."
CancelHandoffReply = "Human handoff has been cancelled."
)
type ConfirmationDecision string
@@ -24,13 +24,13 @@ func ParseConfirmationDecision(value string) ConfirmationDecision {
if value == "" {
return ""
}
confirmWords := []string{"确认", "是", "好的", "可以", "ok", "yes", "继续", "同意"}
confirmWords := []string{"确认", "是", "好的", "可以", "ok", "yes", "continue", "confirm", "继续", "同意"}
for _, item := range confirmWords {
if strings.Contains(value, item) {
return ConfirmationDecisionConfirm
}
}
cancelWords := []string{"取消", "不用", "不需要", "算了", "no"}
cancelWords := []string{"取消", "不用", "不需要", "算了", "no", "cancel"}
for _, item := range cancelWords {
if strings.Contains(value, item) {
return ConfirmationDecisionCancel
@@ -41,5 +41,8 @@ func ParseConfirmationDecision(value string) ConfirmationDecision {
func IsCancellationReply(replyText string) bool {
replyText = strings.TrimSpace(replyText)
return strings.Contains(replyText, CancelCreateTicketReply) || strings.Contains(replyText, CancelHandoffReply)
return strings.Contains(replyText, CancelCreateTicketReply) ||
strings.Contains(replyText, CancelHandoffReply) ||
strings.Contains(replyText, "已取消本次工单创建。") ||
strings.Contains(replyText, "已取消本次转人工。")
}
@@ -78,11 +78,11 @@ func buildPrepareTicketDraftResult(conversation models.Conversation, messages []
result.Description = buildDraftDescription(conversation, messages, input)
if strings.TrimSpace(result.Title) == "" {
result.MissingFields = append(result.MissingFields, "title")
result.FollowUpQuestions = append(result.FollowUpQuestions, "请补充一个简洁的工单标题,明确概括用户遇到的问题。")
result.FollowUpQuestions = append(result.FollowUpQuestions, "Please provide a concise ticket title that clearly summarizes the issue.")
}
if !hasSufficientIssueContext(input, result.Description) {
result.MissingFields = append(result.MissingFields, "issue")
result.FollowUpQuestions = append(result.FollowUpQuestions, "请补充具体问题现象、报错信息或用户诉求,以便整理成工单。")
result.FollowUpQuestions = append(result.FollowUpQuestions, "Please provide the specific issue, error message, or request so I can prepare the ticket.")
}
result.Ready = result.Title != "" && result.Description != "" && len(result.MissingFields) == 0
return result
@@ -107,22 +107,22 @@ func buildDraftDescription(conversation models.Conversation, messages []models.M
}
parts := make([]string, 0, 6)
if input.Issue != "" {
parts = append(parts, "问题现象:"+input.Issue)
parts = append(parts, "Issue: "+input.Issue)
}
if input.Impact != "" {
parts = append(parts, "影响范围:"+input.Impact)
parts = append(parts, "Impact: "+input.Impact)
}
if input.ExpectedOutcome != "" {
parts = append(parts, "用户诉求:"+input.ExpectedOutcome)
parts = append(parts, "Requested outcome: "+input.ExpectedOutcome)
}
if input.CurrentAttempt != "" {
parts = append(parts, "已尝试处理:"+input.CurrentAttempt)
parts = append(parts, "Attempts so far: "+input.CurrentAttempt)
}
if strings.TrimSpace(conversation.LastMessageSummary) != "" {
parts = append(parts, "会话摘要:"+strings.TrimSpace(conversation.LastMessageSummary))
parts = append(parts, "Conversation summary: "+strings.TrimSpace(conversation.LastMessageSummary))
}
if recent := buildRecentMessageDigest(messages); recent != "" {
parts = append(parts, "最近消息:"+recent)
parts = append(parts, "Recent messages: "+recent)
}
return strings.TrimSpace(strings.Join(parts, "\n"))
}
@@ -137,10 +137,10 @@ func hasSufficientIssueContext(input PrepareTicketDraftInput, description string
func buildConversationFacts(conversation models.Conversation, messages []models.Message) []string {
facts := make([]string, 0, 4)
if strings.TrimSpace(conversation.LastMessageSummary) != "" {
facts = append(facts, "最近摘要:"+strings.TrimSpace(conversation.LastMessageSummary))
facts = append(facts, "Recent summary: "+strings.TrimSpace(conversation.LastMessageSummary))
}
if digest := buildRecentMessageDigest(messages); digest != "" {
facts = append(facts, "最近消息:"+digest)
facts = append(facts, "Recent messages: "+digest)
}
return facts
}
@@ -163,13 +163,13 @@ func buildRecentMessageDigest(messages []models.Message) string {
func messageSenderLabel(senderType enums.IMSenderType) string {
switch senderType {
case enums.IMSenderTypeCustomer:
return "用户"
return "Customer"
case enums.IMSenderTypeAgent:
return "客服"
return "Agent"
case enums.IMSenderTypeAI:
return "AI"
default:
return "消息"
return "Message"
}
}
@@ -39,7 +39,7 @@ func buildConversationInterrupt(conversation models.Conversation, message models
func resolveInterruptPrompt(summary *applicationruntime.Summary) string {
if summary == nil || len(summary.Interrupts) == 0 {
return "请继续补充信息后再试。"
return "Please provide more information and try again."
}
if prompt := extractInterruptMessage(summary.Interrupts[0].InfoPreview); prompt != "" {
return prompt
@@ -47,7 +47,7 @@ func resolveInterruptPrompt(summary *applicationruntime.Summary) string {
if prompt := strings.TrimSpace(summary.Interrupts[0].InfoPreview); prompt != "" {
return prompt
}
return "请继续补充信息后再试。"
return "Please provide more information and try again."
}
func extractInterruptMessage(infoPreview string) string {
@@ -48,7 +48,7 @@ func (t *AnalyzeConversationTool) Build(ctx registry.Context) (einotool.BaseTool
func (t *AnalyzeConversationTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: toolx.GraphAnalyzeConversation.Name,
Desc: "Graph Tool。用于整理当前对话摘要、识别投诉/资金/情绪等风险信号,并给出继续解答、建单或转人工的建议。",
Desc: "Graph Tool. Summarizes the current conversation, identifies complaint/payment/sentiment risk signals, and recommends whether to continue answering, create a ticket, or hand off to a human.",
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
Version: einojsonschema.Version,
Type: "object",
@@ -57,42 +57,42 @@ func (t *AnalyzeConversationTool) Info(ctx context.Context) (*schema.ToolInfo, e
Key: "goal",
Value: &einojsonschema.Schema{
Type: "string",
Description: "当前分析目标,例如判断是否需要转人工、是否需要建单、是否需要做风险质检。",
Description: "Analysis goal, such as whether to hand off to a human, create a ticket, or perform risk review.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "observedIssue",
Value: &einojsonschema.Schema{
Type: "string",
Description: "当前观察到的主要问题或诉求。",
Description: "Main issue or request observed in the conversation.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "needTicket",
Value: &einojsonschema.Schema{
Type: "boolean",
Description: "是否重点评估建单必要性。",
Description: "Whether to focus on evaluating ticket creation.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "needHumanHandoff",
Value: &einojsonschema.Schema{
Type: "boolean",
Description: "是否重点评估转人工必要性。",
Description: "Whether to focus on evaluating human handoff.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "needQualityCheck",
Value: &einojsonschema.Schema{
Type: "boolean",
Description: "是否重点做风险/质检分析。",
Description: "Whether to focus on risk or quality review.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "additionalContext",
Value: &einojsonschema.Schema{
Type: "string",
Description: "补充上下文,例如你已发现的争议点、投诉点或业务限制。",
Description: "Additional context, such as disputes, complaint points, or business constraints already identified.",
},
},
)),
@@ -52,7 +52,7 @@ func (t *CreateTicketGraphTool) Build(ctx registry.Context) (einotool.BaseTool,
func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: toolx.GraphCreateTicketConfirm.Name,
Desc: "Graph Tool。用于封装建单参数整理、用户确认、真正创建工单和结果返回的确定性流程。仅在用户明确要求建单且标题、描述已整理清楚后调用。",
Desc: "Graph Tool. Handles ticket parameter preparation, user confirmation, actual ticket creation, and result return. Use only when the user explicitly asks to create a ticket and the title and description are clear.",
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
Version: einojsonschema.Version,
Type: "object",
@@ -65,14 +65,14 @@ func (t *CreateTicketGraphTool) Info(ctx context.Context) (*schema.ToolInfo, err
Key: "title",
Value: &einojsonschema.Schema{
Type: "string",
Description: "工单标题,简洁概括问题。",
Description: "Ticket title. Concisely summarizes the issue.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "description",
Value: &einojsonschema.Schema{
Type: "string",
Description: "工单描述,清晰整理用户问题、现象和诉求。",
Description: "Ticket description. Clearly captures the user's issue, symptoms, and request.",
},
},
)),
@@ -52,7 +52,7 @@ func (t *HandoffGraphTool) Build(ctx registry.Context) (einotool.BaseTool, error
func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: toolx.GraphHandoffConversation.Name,
Desc: "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。仅在用户明确要求人工客服,或你已确认必须转人工处理时调用;若结果标记 terminal=true shouldRetry=false,不要重复调用。",
Desc: "Graph Tool. Handles handoff reason preparation, user confirmation, actual human handoff, and result return. Use only when the user explicitly asks for a human agent or you have confirmed that human handling is required. Do not repeat the call when the result has terminal=true and shouldRetry=false.",
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
Version: einojsonschema.Version,
Type: "object",
@@ -61,7 +61,7 @@ func (t *HandoffGraphTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
Key: "reason",
Value: &einojsonschema.Schema{
Type: "string",
Description: "转人工原因,简洁说明为何需要人工介入,例如用户明确要求人工、问题需要人工核验、需要人工售后处理等。",
Description: "Handoff reason. Briefly explain why a human is needed, such as explicit user request, manual verification, or after-sales handling.",
},
},
)),
@@ -48,7 +48,7 @@ func (t *PrepareTicketDraftTool) Build(ctx registry.Context) (einotool.BaseTool,
func (t *PrepareTicketDraftTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: toolx.GraphPrepareTicketDraft.Name,
Desc: "Graph Tool。用于根据当前会话和已收集信息整理工单草稿,输出建议标题、建议描述、缺失字段和追问建议。适合在真正调用 create_ticket_with_confirmation 前先整理工单内容。",
Desc: "Graph Tool. Prepares a ticket draft from the current conversation and collected information. Use it before create_ticket_with_confirmation when the ticket content needs to be organized.",
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
Version: einojsonschema.Version,
Type: "object",
@@ -57,35 +57,35 @@ func (t *PrepareTicketDraftTool) Info(ctx context.Context) (*schema.ToolInfo, er
Key: "title",
Value: &einojsonschema.Schema{
Type: "string",
Description: "已整理出的工单标题,可选。",
Description: "Prepared ticket title. Optional.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "description",
Value: &einojsonschema.Schema{
Type: "string",
Description: "已整理出的工单描述,可选。",
Description: "Prepared ticket description. Optional.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "issue",
Value: &einojsonschema.Schema{
Type: "string",
Description: "用户当前遇到的问题现象或报错信息。",
Description: "The issue or error message the user is experiencing.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "impact",
Value: &einojsonschema.Schema{
Type: "string",
Description: "问题影响范围,例如无法登录、无法下单、业务中断等。",
Description: "Impact scope, such as unable to sign in, unable to place an order, or business interruption.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "expectedOutcome",
Value: &einojsonschema.Schema{
Type: "string",
Description: "用户期望的处理结果或诉求。",
Description: "The user's expected outcome or request.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
@@ -48,7 +48,7 @@ func (t *TriageServiceRequestTool) Build(ctx registry.Context) (einotool.BaseToo
func (t *TriageServiceRequestTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: toolx.GraphTriageServiceRequest.Name,
Desc: "Graph Tool。用于综合分析当前对话,判断应该继续解答、整理工单草稿还是转人工;当判断为建单时,会一并返回结构化工单草稿建议。",
Desc: "Graph Tool. Analyzes the current conversation to decide whether to continue answering, prepare a ticket draft, or hand off to a human. When ticket creation is recommended, it returns a structured ticket draft suggestion.",
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&einojsonschema.Schema{
Version: einojsonschema.Version,
Type: "object",
@@ -57,35 +57,35 @@ func (t *TriageServiceRequestTool) Info(ctx context.Context) (*schema.ToolInfo,
Key: "goal",
Value: &einojsonschema.Schema{
Type: "string",
Description: "当前分析目标,例如判断是否需要升级、是否要建单或转人工。",
Description: "Analysis goal, such as whether to escalate, create a ticket, or hand off to a human.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "observedIssue",
Value: &einojsonschema.Schema{
Type: "string",
Description: "当前观察到的主要问题或争议点。",
Description: "Main issue or dispute observed in the conversation.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "needTicket",
Value: &einojsonschema.Schema{
Type: "boolean",
Description: "是否重点评估建单必要性。",
Description: "Whether to focus on evaluating ticket creation.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "needHumanHandoff",
Value: &einojsonschema.Schema{
Type: "boolean",
Description: "是否重点评估转人工必要性。",
Description: "Whether to focus on evaluating human handoff.",
},
},
orderedmap.Pair[string, *einojsonschema.Schema]{
Key: "additionalContext",
Value: &einojsonschema.Schema{
Type: "string",
Description: "补充上下文,例如你已发现的风险点或限制条件。",
Description: "Additional context, such as risk signals or constraints already identified.",
},
},
)),
+4 -4
View File
@@ -14,7 +14,7 @@ import (
)
func BuildConversation(item *models.Conversation) response.ConversationResponse {
return BuildConversationWithLocale(item, i18nx.LocaleZhCN)
return BuildConversationWithLocale(item, i18nx.DefaultLocale)
}
func BuildConversationWithLocale(item *models.Conversation, locale string) response.ConversationResponse {
@@ -114,7 +114,7 @@ func BuildParticipantResponses(conversationID int64) []response.ConversationPart
}
func BuildMessages(list []models.Message) []response.MessageResponse {
return BuildMessagesWithLocale(list, i18nx.LocaleZhCN)
return BuildMessagesWithLocale(list, i18nx.DefaultLocale)
}
func BuildMessagesWithLocale(list []models.Message, locale string) []response.MessageResponse {
@@ -132,7 +132,7 @@ func BuildMessagesWithLocale(list []models.Message, locale string) []response.Me
}
func BuildMessage(item *models.Message) response.MessageResponse {
return BuildMessageWithLocale(item, i18nx.LocaleZhCN)
return BuildMessageWithLocale(item, i18nx.DefaultLocale)
}
func BuildMessageWithLocale(item *models.Message, locale string) response.MessageResponse {
@@ -141,7 +141,7 @@ func BuildMessageWithLocale(item *models.Message, locale string) response.Messag
}
func BuildMessageWithReadStates(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenderNames, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile) response.MessageResponse {
return BuildMessageWithReadStatesAndLocale(item, agentReadState, customerReadState, aiSenderNames, userSenderNames, agentProfiles, i18nx.LocaleZhCN)
return BuildMessageWithReadStatesAndLocale(item, agentReadState, customerReadState, aiSenderNames, userSenderNames, agentProfiles, i18nx.DefaultLocale)
}
func BuildMessageWithReadStatesAndLocale(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenderNames, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile, locale string) response.MessageResponse {
+2 -2
View File
@@ -16,7 +16,7 @@ var (
)
func BuildNotification(item *models.Notification) *response.NotificationResponse {
return BuildNotificationWithLocale(item, i18nx.LocaleZhCN)
return BuildNotificationWithLocale(item, i18nx.DefaultLocale)
}
func BuildNotificationWithLocale(item *models.Notification, locale string) *response.NotificationResponse {
@@ -39,7 +39,7 @@ func BuildNotificationWithLocale(item *models.Notification, locale string) *resp
}
func BuildNotificationList(list []models.Notification) []response.NotificationResponse {
return BuildNotificationListWithLocale(list, i18nx.LocaleZhCN)
return BuildNotificationListWithLocale(list, i18nx.DefaultLocale)
}
func BuildNotificationListWithLocale(list []models.Notification, locale string) []response.NotificationResponse {
@@ -161,7 +161,7 @@ func AIAgentPostUpdate_status(ctx *gin.Context) {
}
func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
return buildAIAgentResponseWithLocale(item, i18nx.LocaleZhCN)
return buildAIAgentResponseWithLocale(item, i18nx.DefaultLocale)
}
func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) response.AIAgentResponse {
+105 -5
View File
@@ -18,7 +18,7 @@ const (
const (
BootstrapAdminUsername = "admin"
BootstrapAdminPassword = "ChangeMe123!"
BootstrapAdminNickname = "超级管理员"
BootstrapAdminNickname = "Super Admin"
)
// Permission 权限结构体
@@ -278,11 +278,111 @@ var PermissionMap = make(map[string]Permission)
// init 初始化 PermissionMap
func init() {
normalizeBuiltinPermissionNames()
for _, permission := range Permissions {
PermissionMap[permission.Code] = permission
}
}
func normalizeBuiltinPermissionNames() {
for i := range Permissions {
Permissions[i].Name = builtinPermissionName(Permissions[i].Code, Permissions[i].Name)
}
}
func builtinPermissionName(code string, fallback string) string {
if name, ok := builtinPermissionNameOverrides[code]; ok {
return name
}
resourceKey, actionKey, ok := splitPermissionCode(code)
if !ok {
return fallback
}
action, ok := builtinPermissionActionLabels[actionKey]
if !ok {
return fallback
}
resource, ok := builtinPermissionResourceLabels[resourceKey]
if !ok {
return fallback
}
return action + " " + resource
}
func splitPermissionCode(code string) (string, string, bool) {
for i := 0; i < len(code); i++ {
if code[i] == '.' {
return code[:i], code[i+1:], i > 0 && i < len(code)-1
}
}
return "", "", false
}
var builtinPermissionActionLabels = map[string]string{
"view": "View",
"create": "Create",
"update": "Update",
"delete": "Delete",
"assignRole": "Assign roles to",
"assignPermission": "Assign permissions to",
"sync": "Sync",
"revoke": "Revoke",
"assign": "Assign",
"transfer": "Transfer",
"close": "Close",
"send": "Send",
"tag": "Manage tags for",
"handover": "Handle handoffs for",
"recycle": "Recycle",
"linkCustomer": "Link customers to",
"changeStatus": "Change status for",
"progress": "Update progress for",
"updateStatus": "Update status for",
"config": "Configure service rules for",
"batchGenerate": "Batch generate",
"call": "Call",
}
var builtinPermissionResourceLabels = map[string]string{
"user": "users",
"role": "roles",
"permission": "permissions",
"session": "sessions",
"conversation": "conversations",
"ticket": "tickets",
"notification": "notifications",
"quickReply": "quick replies",
"tag": "tags",
"company": "companies",
"channel": "channels",
"customer": "customers",
"agent": "agents",
"agentTeam": "agent teams",
"agentTeamSchedule": "agent team schedules",
"asset": "file assets",
"aiAgent": "AI Agents",
"aiConfig": "AI configurations",
"knowledgeBase": "knowledge bases",
"knowledgeDocument": "knowledge documents",
"knowledgeFAQ": "knowledge FAQs",
"skillDefinition": "Skill definitions",
"mcp": "MCP tools",
}
var builtinPermissionNameOverrides = map[string]string{
"user.assignRole": "Assign user roles",
"role.assignPermission": "Assign role permissions",
"session.revoke": "Revoke sessions",
"conversation.send": "Send conversation messages",
"conversation.linkCustomer": "Link conversation customer",
"ticket.changeStatus": "Change ticket status",
"ticket.progress": "Update ticket progress",
"agent.config": "Configure agent service rules",
"agentTeamSchedule.batchGenerate": "Batch generate agent team schedules",
"mcp.view": "View MCP debug information",
"mcp.call": "Call MCP tools",
}
type RoleSpec struct {
Name string
Code string
@@ -290,10 +390,10 @@ type RoleSpec struct {
}
var Roles = []RoleSpec{
{Name: "超级管理员", Code: RoleCodeSuperAdmin, SortNo: 1},
{Name: "管理员", Code: RoleCodeAdmin, SortNo: 2},
{Name: "客服组长", Code: RoleCodeCsTeamLeader, SortNo: 3},
{Name: "客服", Code: RoleCodeCsUser, SortNo: 4},
{Name: "Super Admin", Code: RoleCodeSuperAdmin, SortNo: 1},
{Name: "Admin", Code: RoleCodeAdmin, SortNo: 2},
{Name: "Support Team Lead", Code: RoleCodeCsTeamLeader, SortNo: 3},
{Name: "Support Agent", Code: RoleCodeCsUser, SortNo: 4},
}
var RolePermissions = map[string][]Permission{
+46
View File
@@ -0,0 +1,46 @@
package constants
import "testing"
func TestBuiltinAuthSeedNamesDefaultToEnglish(t *testing.T) {
t.Parallel()
if BootstrapAdminNickname != "Super Admin" {
t.Fatalf("BootstrapAdminNickname = %q, want %q", BootstrapAdminNickname, "Super Admin")
}
roles := map[string]string{}
for _, role := range Roles {
roles[role.Code] = role.Name
}
tests := map[string]string{
RoleCodeSuperAdmin: "Super Admin",
RoleCodeAdmin: "Admin",
RoleCodeCsTeamLeader: "Support Team Lead",
RoleCodeCsUser: "Support Agent",
}
for code, want := range tests {
if got := roles[code]; got != want {
t.Fatalf("role %s name = %q, want %q", code, got, want)
}
}
permissions := map[string]string{}
for _, permission := range Permissions {
permissions[permission.Code] = permission.Name
}
permissionTests := map[string]string{
"user.view": "View users",
"ticket.create": "Create tickets",
"conversation.send": "Send conversation messages",
"channel.view": "View channels",
"agent.view": "View agents",
}
for code, want := range permissionTests {
if got := permissions[code]; got != want {
t.Fatalf("permission %s name = %q, want %q", code, got, want)
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ var (
func Bundle() *i18n.Bundle {
bundleOnce.Do(func() {
b := i18n.NewBundle(language.SimplifiedChinese)
b := i18n.NewBundle(language.AmericanEnglish)
b.RegisterUnmarshalFunc("toml", toml.Unmarshal)
for _, name := range []string{
"locales/active.zh-CN.toml",
+3 -3
View File
@@ -6,15 +6,15 @@ const contextLocaleKey = "i18nx.locale"
func Locale(ctx *gin.Context) string {
if ctx == nil {
return LocaleZhCN
return DefaultLocale
}
value, ok := ctx.Get(contextLocaleKey)
if !ok {
return LocaleZhCN
return DefaultLocale
}
locale, ok := value.(string)
if !ok {
return LocaleZhCN
return DefaultLocale
}
return NormalizeLocale(locale)
}
+4 -4
View File
@@ -16,14 +16,14 @@ func TestNormalizeLocale(t *testing.T) {
in string
want string
}{
{name: "default for blank", in: "", want: LocaleZhCN},
{name: "default for blank", in: "", want: LocaleEnUS},
{name: "exact chinese", in: "zh-CN", want: LocaleZhCN},
{name: "underscore chinese", in: "zh_CN", want: LocaleZhCN},
{name: "short chinese", in: "zh", want: LocaleZhCN},
{name: "exact english", in: "en-US", want: LocaleEnUS},
{name: "underscore english", in: "en_US", want: LocaleEnUS},
{name: "short english", in: "en", want: LocaleEnUS},
{name: "unsupported falls back", in: "fr-FR", want: LocaleZhCN},
{name: "unsupported falls back", in: "fr-FR", want: LocaleEnUS},
}
for _, tt := range tests {
@@ -60,13 +60,13 @@ func TestResolveLocalePrefersXLocale(t *testing.T) {
}
}
func TestTranslateFallsBackToChinese(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." {
t.Fatalf("english translation = %q", got)
}
if got := TLocale("fr-FR", "error.auth.expired", nil); got != "未登录或登录已过期" {
if got := TLocale("fr-FR", "error.auth.expired", nil); got != "Your session has expired. Please sign in again." {
t.Fatalf("fallback translation = %q", got)
}
}
+4 -4
View File
@@ -13,7 +13,7 @@ func T(ctx *gin.Context, messageID string, data map[string]any) string {
}
}
}
return TLocale(LocaleZhCN, messageID, data)
return TLocale(DefaultLocale, messageID, data)
}
func TLocale(locale string, messageID string, data map[string]any) string {
@@ -22,8 +22,8 @@ func TLocale(locale string, messageID string, data map[string]any) string {
if message != "" {
return message
}
if normalized != LocaleZhCN {
message = localize(LocaleZhCN, messageID, data)
if normalized != DefaultLocale {
message = localize(DefaultLocale, messageID, data)
if message != "" {
return message
}
@@ -32,7 +32,7 @@ func TLocale(locale string, messageID string, data map[string]any) string {
}
func localize(locale string, messageID string, data map[string]any) string {
localizer := i18n.NewLocalizer(Bundle(), locale, LocaleZhCN)
localizer := i18n.NewLocalizer(Bundle(), locale, DefaultLocale)
message, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: messageID,
TemplateData: data,
+7 -6
View File
@@ -9,8 +9,9 @@ import (
)
const (
LocaleZhCN = "zh-CN"
LocaleEnUS = "en-US"
LocaleZhCN = "zh-CN"
LocaleEnUS = "en-US"
DefaultLocale = LocaleEnUS
)
var supportedLocales = map[string]string{
@@ -26,17 +27,17 @@ var supportedLocales = map[string]string{
func NormalizeLocale(value string) string {
key := strings.ToLower(strings.TrimSpace(value))
if key == "" {
return LocaleZhCN
return DefaultLocale
}
if locale, ok := supportedLocales[key]; ok {
return locale
}
return LocaleZhCN
return DefaultLocale
}
func ResolveRequestLocale(req *http.Request) string {
if req == nil {
return LocaleZhCN
return DefaultLocale
}
if locale := normalizeSupportedLocale(req.Header.Get("X-Locale")); locale != "" {
return locale
@@ -47,7 +48,7 @@ func ResolveRequestLocale(req *http.Request) string {
if locale := normalizeSupportedLocale(req.URL.Query().Get("locale")); locale != "" {
return locale
}
return LocaleZhCN
return DefaultLocale
}
func Middleware() gin.HandlerFunc {
+39 -39
View File
@@ -58,87 +58,87 @@ var (
Code: "graph/triage_service_request",
ServerCode: "graph",
Name: "triage_service_request",
Title: "升级分流判断",
Description: "Graph Tool。用于综合分析当前对话,判断应继续解答、整理工单草稿还是转人工,并在需要建单时一并整理工单草稿。",
Title: "Triage service request",
Description: "Graph Tool. Analyzes the current conversation to decide whether to continue answering, prepare a ticket draft, or hand off to a human. It also prepares a ticket draft when ticket creation is recommended.",
SourceType: enums.ToolSourceTypeGraph,
RuntimeStatic: true,
Appendix: strings.TrimSpace(`
当你需要判断“继续解答 / 建单 / 转人工”这类复杂升级路径时,优先先调用 triage_service_request 这个 Graph Tool,并遵守以下规则:
1. 该工具会综合当前对话输出 recommendedAction,并在需要建单时附带 ticketDraft。
2. 如果 recommendedAction=continue_answering,则优先继续澄清或解答,不要直接升级。
3. 如果 recommendedAction=prepare_ticket,则优先使用 ticketDraft 或继续补充缺失字段,再调用 create_ticket_with_confirmation
4. 如果 recommendedAction=handoff_to_human,则确认理由充分后再调用 handoff_to_human
5. 当升级路径不明确时,优先使用该工具,而不是直接凭主 prompt 做复杂分流判断。
When you need to decide between continuing the answer, creating a ticket, or handing off to a human, call triage_service_request first and follow these rules:
1. The tool returns recommendedAction and includes ticketDraft when ticket creation is needed.
2. If recommendedAction=continue_answering, continue clarifying or answering instead of escalating directly.
3. If recommendedAction=prepare_ticket, use ticketDraft or collect missing fields before calling create_ticket_with_confirmation.
4. If recommendedAction=handoff_to_human, confirm the reason is sufficient before calling handoff_to_human.
5. When the escalation path is unclear, use this tool instead of making a complex routing decision from the main prompt alone.
`),
}
GraphAnalyzeConversation = ToolSpec{
Code: "graph/analyze_conversation",
ServerCode: "graph",
Name: "analyze_conversation",
Title: "分析对话风险与摘要",
Description: "Graph Tool。用于整理当前对话摘要、识别风险信号,并给出继续解答、建单或转人工的建议。",
Title: "Analyze conversation risk and summary",
Description: "Graph Tool. Summarizes the current conversation, identifies risk signals, and recommends whether to continue answering, create a ticket, or hand off to a human.",
SourceType: enums.ToolSourceTypeGraph,
RuntimeStatic: true,
Appendix: strings.TrimSpace(`
当对话可能涉及投诉升级、退款赔偿、明显负面情绪、是否要建单、是否要转人工等复杂判断时,优先调用 analyze_conversation 这个 Graph Tool,并遵守以下规则:
1. 该工具用于输出结构化摘要、风险信号和下一步建议,不代表实际已经建单或转人工。
2. 如果工具建议为 handoff_to_human,应先确认是否满足转人工条件,再考虑调用 handoff_to_human
3. 如果工具建议为 prepare_ticket,应优先调用 prepare_ticket_draft 或继续补充信息,而不是直接建单。
4. 如果工具建议为 continue_answering,优先继续澄清和解答,不要过早升级动作。
When the conversation may involve escalation, refunds, compensation, clear negative sentiment, ticket creation, or human handoff, call analyze_conversation first and follow these rules:
1. This tool returns a structured summary, risk signals, and next-step recommendation. It does not create tickets or hand off to a human.
2. If the tool recommends handoff_to_human, confirm the handoff conditions before calling handoff_to_human.
3. If the tool recommends prepare_ticket, call prepare_ticket_draft or collect more information before creating a ticket.
4. If the tool recommends continue_answering, continue clarifying and answering instead of escalating too early.
`),
}
GraphPrepareTicketDraft = ToolSpec{
Code: "graph/prepare_ticket_draft",
ServerCode: "graph",
Name: "prepare_ticket_draft",
Title: "整理工单草稿",
Description: "Graph Tool。用于根据当前会话和已收集信息整理工单草稿,输出建议标题、描述、缺失字段和追问建议。",
Title: "Prepare ticket draft",
Description: "Graph Tool. Prepares a ticket draft from the current conversation and collected information, including a suggested title, description, missing fields, and follow-up questions.",
SourceType: enums.ToolSourceTypeGraph,
RuntimeStatic: true,
Appendix: strings.TrimSpace(`
当用户已经表达了建单、投诉、报障、售后处理等诉求,但工单标题、描述或问题整理还比较散乱时,优先调用 prepare_ticket_draft 这个 Graph Tool,并遵守以下规则:
1. 该工具用于整理工单草稿,会返回建议标题、建议描述、缺失字段和追问建议。
2. 如果工具返回 ready=false,优先根据 missingFields followUpQuestions 继续追问,不要直接创建工单。
3. 如果工具返回 ready=true,再结合结果考虑调用 create_ticket_with_confirmation
4. 该工具用于“整理草稿”,不代表已经创建工单。
When the user has asked to create a ticket, file a complaint, report an issue, or request after-sales handling, but the title or description is still unclear, call prepare_ticket_draft first and follow these rules:
1. This tool prepares a ticket draft and returns a suggested title, suggested description, missing fields, and follow-up questions.
2. If ready=false, ask follow-up questions based on missingFields and followUpQuestions instead of creating a ticket directly.
3. If ready=true, use the result to consider calling create_ticket_with_confirmation.
4. This tool only prepares a draft. It does not create a ticket.
`),
}
GraphCreateTicketConfirm = ToolSpec{
Code: "graph/create_ticket_with_confirmation",
ServerCode: "graph",
Name: "create_ticket_with_confirmation",
Title: "创建工单确认流程",
Description: "Graph Tool。用于封装建单参数整理、用户确认、真正建单和结果返回的确定性流程。",
Title: "Create ticket confirmation flow",
Description: "Graph Tool. Handles ticket parameter preparation, user confirmation, actual ticket creation, and result return.",
SourceType: enums.ToolSourceTypeGraph,
DirectAccess: true,
RuntimeStatic: true,
Aliases: []string{"builtin/create_ticket_with_confirmation"},
Appendix: strings.TrimSpace(`
你可以在确认信息充分后调用 create_ticket_with_confirmation 这个 Graph Tool 来创建工单,但必须遵守以下规则:
1. 只有在用户明确表达希望提交工单、投诉、报障、售后处理等诉求时,才考虑调用该工具。
2. 调用前你必须已经整理出清晰的工单标题和问题描述;如果信息还比较散乱,优先先调用 prepare_ticket_draft 或继续追问,不要过早调用。
3. 一旦准备创建工单,必须调用 create_ticket_with_confirmation 工具,禁止直接口头宣称“已经创建工单”。
4. Graph Tool 会先向用户发起确认。用户确认后才会真正创建工单;用户取消则结束本次建单流程。
5. 如果用户只是咨询、抱怨或泛泛表达不满,但没有明确要求建单,优先继续澄清,不要主动创建工单。
You can call create_ticket_with_confirmation after enough information has been collected, but follow these rules:
1. Only consider this tool when the user explicitly wants to submit a ticket, complaint, issue report, or after-sales request.
2. Before calling it, prepare a clear ticket title and issue description. If the information is still scattered, call prepare_ticket_draft or ask follow-up questions first.
3. Once you are ready to create a ticket, you must call create_ticket_with_confirmation. Do not simply claim in text that the ticket has been created.
4. This Graph Tool asks the user for confirmation first. The ticket is created only after the user confirms; if the user cancels, the flow ends.
5. If the user is only asking questions, complaining generally, or expressing dissatisfaction without explicitly requesting a ticket, continue clarifying instead of proactively creating one.
`),
}
GraphHandoffConversation = ToolSpec{
Code: "graph/handoff_to_human",
ServerCode: "graph",
Name: "handoff_to_human",
Title: "转人工确认流程",
Description: "Graph Tool。用于封装转人工原因整理、用户确认、真正转人工和结果返回的确定性流程。",
Title: "Human handoff confirmation flow",
Description: "Graph Tool. Handles handoff reason preparation, user confirmation, actual human handoff, and result return.",
SourceType: enums.ToolSourceTypeGraph,
DirectAccess: true,
RuntimeStatic: true,
Appendix: strings.TrimSpace(`
你可以在确认需要人工介入后调用 handoff_to_human 这个 Graph Tool 来转人工,但必须遵守以下规则:
1. 只有在用户明确要求人工客服,或你已经判断该问题必须由人工继续处理时,才调用该工具。
2. 调用前先尽量整理清楚转人工原因;如果理由含糊,先追问或澄清,不要直接转人工。
3. 一旦决定转人工,必须调用 handoff_to_human 工具,禁止只在回复里口头说“我帮你转人工了”。
4. Graph Tool 会先向用户发起确认。用户确认后才会真正转人工;用户取消则结束本次转人工流程。
5. 如果问题仍可由当前对话继续解决,优先继续解答,不要过早转人工。
6. 如果工具返回 terminal=true shouldRetry=false,说明转人工流程已经结束,禁止重复调用该工具。
You can call handoff_to_human after confirming that human help is needed, but follow these rules:
1. Only call this tool when the user explicitly asks for a human agent or you have determined that the issue must be handled by a human.
2. Before calling it, summarize the handoff reason clearly. If the reason is vague, ask a follow-up question first.
3. Once you decide to hand off, you must call handoff_to_human. Do not simply say in text that you have connected the user to a human.
4. This Graph Tool asks the user for confirmation first. The handoff happens only after the user confirms; if the user cancels, the flow ends.
5. If the issue can still be solved in the current conversation, continue helping instead of escalating too early.
6. If the tool returns terminal=true and shouldRetry=false, the handoff flow has ended. Do not call it repeatedly.
`),
}
RegisteredToolSpecs = []ToolSpec{
+6 -6
View File
@@ -208,8 +208,8 @@ func (s *channelService) ListWxWorkKFAccounts() ([]response.WxWorkKFAccountRespo
func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfig, error) {
raw = strings.TrimSpace(raw)
cfg := &dto.WebChannelConfig{
Title: "在线客服",
Subtitle: "欢迎咨询",
Title: "Support",
Subtitle: "How can we help?",
ThemeColor: "#2563eb",
Position: "right",
Width: "380px",
@@ -221,7 +221,7 @@ func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfi
}
cfg.Title = strings.TrimSpace(cfg.Title)
if cfg.Title == "" {
cfg.Title = "在线客服"
cfg.Title = "Support"
}
cfg.Subtitle = strings.TrimSpace(cfg.Subtitle)
cfg.ThemeColor = strings.TrimSpace(cfg.ThemeColor)
@@ -246,8 +246,8 @@ func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfi
func (s *channelService) ParseWechatMPChannelConfig(raw string) (*dto.WechatMPChannelConfig, error) {
raw = strings.TrimSpace(raw)
cfg := &dto.WechatMPChannelConfig{
Title: "公众号客服",
Subtitle: "欢迎咨询",
Title: "Official Account Support",
Subtitle: "How can we help?",
ThemeColor: "#2563eb",
}
if raw != "" {
@@ -257,7 +257,7 @@ func (s *channelService) ParseWechatMPChannelConfig(raw string) (*dto.WechatMPCh
}
cfg.Title = strings.TrimSpace(cfg.Title)
if cfg.Title == "" {
cfg.Title = "公众号客服"
cfg.Title = "Official Account Support"
}
cfg.Subtitle = strings.TrimSpace(cfg.Subtitle)
cfg.ThemeColor = strings.TrimSpace(cfg.ThemeColor)
@@ -19,8 +19,8 @@ import (
var ConversationHumanDispatchService = newConversationHumanDispatchService()
const (
HandoffWaitingMessage = "已为你转接人工客服,请稍候。"
HandoffOffHoursMessage = "当前暂不在人工客服服务时间内,你可以先继续描述问题,我会尽力协助;服务时间开始后也可以再次转人工。"
HandoffWaitingMessage = "We are connecting you to a human support agent. Please wait."
HandoffOffHoursMessage = "Human support is currently outside service hours. You can keep describing the issue and I will do my best to help. You can also request a human agent again when service hours resume."
)
type HandoffDecisionType string
@@ -42,7 +42,7 @@ func TestConversationHumanDispatchAIHandoffOffHoursKeepsAIServingAndSendsNotice(
if message == nil {
t.Fatalf("expected off-hours notice message")
}
if message.SenderType != enums.IMSenderTypeAI || !strings.Contains(message.Content, "当前暂不在人工客服服务时间内") {
if message.SenderType != enums.IMSenderTypeAI || !strings.Contains(message.Content, "Human support is currently outside service hours") {
t.Fatalf("unexpected off-hours message: %+v", message)
}
}
@@ -37,11 +37,11 @@ func handleConversationAssignedNotify(ctx context.Context, event events.Conversa
func conversationAssignedNotifyTitle(assignType string) string {
switch strings.TrimSpace(assignType) {
case events.ConversationAssignTypeTransfer:
return "会话转接提醒"
return "Conversation transferred"
case events.ConversationAssignTypeAutoAssign:
return "会话自动分配提醒"
return "Conversation auto-assigned"
default:
return "会话分配提醒"
return "Conversation assigned"
}
}
@@ -49,21 +49,21 @@ func buildConversationAssignedNotifyBody(conversation *models.Conversation, assi
if conversation == nil {
return ""
}
reasonLabel := "分配原因"
reasonLabel := "Assignment reason"
if strings.TrimSpace(assignType) == events.ConversationAssignTypeTransfer {
reasonLabel = "转接原因"
reasonLabel = "Transfer reason"
}
lines := []string{
fmt.Sprintf("会话ID: #%d", conversation.ID),
fmt.Sprintf("会话摘要: %s", strs.DefaultIfBlank(services.ConversationService.BuildConversationSummary(conversation), "-")),
fmt.Sprintf("接入渠道: %s", resolveConversationChannelLabel(conversation)),
fmt.Sprintf("当前状态: %s", enums.GetIMConversationStatusLabel(conversation.Status)),
fmt.Sprintf("处理人: %s", resolveNotifyUserLabel(assigneeID)),
fmt.Sprintf("Conversation ID: #%d", conversation.ID),
fmt.Sprintf("Summary: %s", strs.DefaultIfBlank(services.ConversationService.BuildConversationSummary(conversation), "-")),
fmt.Sprintf("Channel: %s", resolveConversationChannelLabel(conversation)),
fmt.Sprintf("Status: %s", enums.GetIMConversationStatusLabel(conversation.Status)),
fmt.Sprintf("Assignee: %s", resolveNotifyUserLabel(assigneeID)),
}
if strings.TrimSpace(reason) != "" {
lines = append(lines, fmt.Sprintf("%s: %s", reasonLabel, strings.TrimSpace(reason)))
}
lines = append(lines, fmt.Sprintf("时间: %s", time.Now().Format("2006-01-02 15:04:05")))
lines = append(lines, fmt.Sprintf("Time: %s", time.Now().Format("2006-01-02 15:04:05")))
return strings.Join(lines, "\n")
}
@@ -31,16 +31,16 @@ func handleTicketAssignedInAppNotification(ctx context.Context, event events.Tic
if ticket == nil {
return nil
}
content := fmt.Sprintf("工单 %s 已指派给你", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID)))
content := fmt.Sprintf("Ticket %s has been assigned to you.", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID)))
if title := strings.TrimSpace(ticket.Title); title != "" {
content = content + "\n" + title
}
if reason := strings.TrimSpace(event.Reason); reason != "" {
content = content + "\n指派原因: " + reason
content = content + "\nAssignment reason: " + reason
}
_, err := services.NotificationService.CreateAndPush(request.CreateNotificationRequest{
RecipientUserID: event.ToUserID,
Title: "工单指派提醒",
Title: "Ticket assigned",
Content: content,
NotificationType: "ticket_assigned",
BizType: "ticket",
@@ -61,12 +61,12 @@ func handleConversationAssignedInAppNotification(ctx context.Context, event even
if conversation == nil {
return nil
}
content := fmt.Sprintf("会话 #%d 已分配给你", conversation.ID)
content := fmt.Sprintf("Conversation #%d has been assigned to you.", conversation.ID)
if summary := strings.TrimSpace(services.ConversationService.BuildConversationSummary(conversation)); summary != "" {
content = content + "\n" + summary
}
if reason := strings.TrimSpace(event.Reason); reason != "" {
content = content + "\n分配原因: " + reason
content = content + "\nAssignment reason: " + reason
}
_, err := services.NotificationService.CreateAndPush(request.CreateNotificationRequest{
RecipientUserID: event.ToUserID,
@@ -29,7 +29,7 @@ func handleTicketAssignedNotify(ctx context.Context, event events.TicketAssigned
return nil
}
content := buildTicketAssignedNotifyBody(ticket, event.ToUserID, event.Reason)
return services.WxWorkNotifyService.SendTextToAssigneeOrDefault(event.ToUserID, "工单指派提醒", content)
return services.WxWorkNotifyService.SendTextToAssigneeOrDefault(event.ToUserID, "Ticket assigned", content)
}
func buildTicketAssignedNotifyBody(ticket *models.Ticket, assigneeID int64, reason string) string {
@@ -37,14 +37,14 @@ func buildTicketAssignedNotifyBody(ticket *models.Ticket, assigneeID int64, reas
return ""
}
lines := []string{
fmt.Sprintf("工单号: %s", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID))),
fmt.Sprintf("工单标题: %s", strs.DefaultIfBlank(ticket.Title, "-")),
fmt.Sprintf("当前状态: %s", enums.GetTicketStatusLabel(ticket.Status)),
fmt.Sprintf("处理人: %s", resolveNotifyUserLabel(assigneeID)),
fmt.Sprintf("Ticket no: %s", strs.DefaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID))),
fmt.Sprintf("Title: %s", strs.DefaultIfBlank(ticket.Title, "-")),
fmt.Sprintf("Status: %s", enums.GetTicketStatusLabel(ticket.Status)),
fmt.Sprintf("Assignee: %s", resolveNotifyUserLabel(assigneeID)),
}
if strings.TrimSpace(reason) != "" {
lines = append(lines, fmt.Sprintf("指派原因: %s", strings.TrimSpace(reason)))
lines = append(lines, fmt.Sprintf("Assignment reason: %s", strings.TrimSpace(reason)))
}
lines = append(lines, fmt.Sprintf("时间: %s", time.Now().Format("2006-01-02 15:04:05")))
lines = append(lines, fmt.Sprintf("Time: %s", time.Now().Format("2006-01-02 15:04:05")))
return strings.Join(lines, "\n")
}
+2 -2
View File
@@ -237,7 +237,7 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *
}
return repositories.TicketProgressRepository.Create(ctx.Tx, &models.TicketProgress{
TicketID: ticket.ID,
Content: "创建工单",
Content: "Created ticket",
AuthorID: operator.UserID,
CreatedAt: time.Now(),
})
@@ -265,7 +265,7 @@ func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConve
title = strings.TrimSpace(ConversationService.BuildConversationSummary(conversation))
}
if title == "" {
title = "会话工单"
title = "Conversation ticket"
}
description := strings.TrimSpace(req.Description)
if description == "" {
+1 -1
View File
@@ -81,7 +81,7 @@ func TestTicketServiceCreateTicketSetsPendingStatusAndTicketNo(t *testing.T) {
if len(progresses) != 1 {
t.Fatalf("expected initial progress, got %d", len(progresses))
}
if progresses[0].Content != "创建工单" || progresses[0].AuthorID != operator.UserID {
if progresses[0].Content != "Created ticket" || progresses[0].AuthorID != operator.UserID {
t.Fatalf("unexpected initial progress: %+v", progresses[0])
}
+1 -1
View File
@@ -34,7 +34,7 @@ type MCPToolCatalogItem struct {
}
func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalogItem, error) {
return s.ListMCPToolsWithLocale(ctx, i18nx.LocaleZhCN)
return s.ListMCPToolsWithLocale(ctx, i18nx.DefaultLocale)
}
func (s *toolCatalogService) ListMCPToolsWithLocale(ctx context.Context, locale string) ([]MCPToolCatalogItem, error) {
+1 -1
View File
@@ -43,7 +43,7 @@ export default function RootLayout({
children: React.ReactNode
}>) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<html lang="en-US" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
+1
View File
@@ -25,6 +25,7 @@ async function loadConfig() {
test("normalizes supported locale aliases", async () => {
const { DEFAULT_LOCALE, normalizeLocale } = await loadConfig()
assert.equal(DEFAULT_LOCALE, "en-US")
assert.equal(normalizeLocale("zh-CN"), "zh-CN")
assert.equal(normalizeLocale("zh_CN"), "zh-CN")
assert.equal(normalizeLocale("zh"), "zh-CN")
+1 -1
View File
@@ -1,7 +1,7 @@
export const SUPPORTED_LOCALES = ["zh-CN", "en-US"] as const
export type AppLocale = (typeof SUPPORTED_LOCALES)[number]
export const DEFAULT_LOCALE: AppLocale = "zh-CN"
export const DEFAULT_LOCALE: AppLocale = "en-US"
export const LOCALE_STORAGE_KEY = "cs_ai_agent_locale"
const LOCALE_ALIASES: Record<string, AppLocale> = {
+2 -2
View File
@@ -42,9 +42,9 @@ function getWidgetLocale() {
try {
const stored = window.localStorage?.getItem("cs_ai_agent_locale")
const language = stored || document.documentElement.lang || window.navigator?.language || ""
return language.toLowerCase().startsWith("en") ? "en-US" : "zh-CN"
return language.toLowerCase().startsWith("zh") ? "zh-CN" : "en-US"
} catch {
return "zh-CN"
return "en-US"
}
}
File diff suppressed because one or more lines are too long