feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Modified in the independent hardening fork; see docs/hardening.
|
||||
|
||||
package builders
|
||||
|
||||
import (
|
||||
@@ -45,9 +47,7 @@ func BuildConversationWithLocale(item *models.Conversation, locale string) respo
|
||||
ClosedBy: item.ClosedBy,
|
||||
CloseReason: item.CloseReason,
|
||||
}
|
||||
if identity := services.ConversationService.GetConversationExternalIdentity(item); identity != nil {
|
||||
ret.CustomerOnline = services.WsService.IsGuestOnline(identity.ExternalID)
|
||||
}
|
||||
ret.CustomerOnline = services.WsService.IsCustomerOnline(item.CustomerID, item.ChannelID)
|
||||
if item.CurrentAssigneeID > 0 {
|
||||
if user := services.UserService.Get(item.CurrentAssigneeID); user != nil {
|
||||
ret.CurrentAssigneeName = user.Nickname
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
// Modified in the independent AgentDesk fork: localized knowledge workflows and safe display errors.
|
||||
package builders
|
||||
|
||||
import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func BuildKnowledgeBase(item *models.KnowledgeBase) response.KnowledgeBaseResponse {
|
||||
func BuildKnowledgeBase(item *models.KnowledgeBase, locales ...string) response.KnowledgeBaseResponse {
|
||||
return response.KnowledgeBaseResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
KnowledgeType: item.KnowledgeType,
|
||||
KnowledgeTypeName: enums.GetKnowledgeBaseTypeLabel(enums.KnowledgeBaseType(item.KnowledgeType)),
|
||||
KnowledgeTypeName: knowledgeLabel(locales, "type", item.KnowledgeType),
|
||||
Status: item.Status,
|
||||
StatusName: enums.GetStatusLabel(item.Status),
|
||||
StatusName: knowledgeLabel(locales, "status", item.Status),
|
||||
DefaultTopK: item.DefaultTopK,
|
||||
DefaultScoreThreshold: item.DefaultScoreThreshold,
|
||||
DefaultRerankLimit: item.DefaultRerankLimit,
|
||||
@@ -24,7 +26,7 @@ func BuildKnowledgeBase(item *models.KnowledgeBase) response.KnowledgeBaseRespon
|
||||
ChunkMaxTokens: item.ChunkMaxTokens,
|
||||
ChunkOverlapTokens: item.ChunkOverlapTokens,
|
||||
AnswerMode: item.AnswerMode,
|
||||
AnswerModeName: enums.GetKnowledgeAnswerModeLabel(enums.KnowledgeAnswerMode(item.AnswerMode)),
|
||||
AnswerModeName: knowledgeLabel(locales, "mode", item.AnswerMode),
|
||||
Remark: item.Remark,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
@@ -33,18 +35,18 @@ func BuildKnowledgeBase(item *models.KnowledgeBase) response.KnowledgeBaseRespon
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKnowledgeDocument(item *models.KnowledgeDocument) response.KnowledgeDocumentResponse {
|
||||
func BuildKnowledgeDocument(item *models.KnowledgeDocument, locales ...string) response.KnowledgeDocumentResponse {
|
||||
return response.KnowledgeDocumentResponse{
|
||||
ID: item.ID,
|
||||
KnowledgeBaseID: item.KnowledgeBaseID,
|
||||
DirectoryID: item.DirectoryID,
|
||||
Title: item.Title,
|
||||
Status: item.Status,
|
||||
StatusName: enums.GetStatusLabel(item.Status),
|
||||
StatusName: knowledgeLabel(locales, "status", item.Status),
|
||||
IndexStatus: item.IndexStatus,
|
||||
IndexStatusName: enums.GetKnowledgeDocumentIndexStatusLabel(item.IndexStatus),
|
||||
IndexStatusName: knowledgeLabel(locales, "index", item.IndexStatus),
|
||||
IndexedAt: item.IndexedAt,
|
||||
IndexError: item.IndexError,
|
||||
IndexError: knowledgeIndexError(locales, item.IndexError),
|
||||
ContentHash: item.ContentHash,
|
||||
ContentType: item.ContentType,
|
||||
Content: item.Content,
|
||||
@@ -55,18 +57,18 @@ func BuildKnowledgeDocument(item *models.KnowledgeDocument) response.KnowledgeDo
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKnowledgeDocumentList(item *models.KnowledgeDocument) response.KnowledgeDocumentListResponse {
|
||||
func BuildKnowledgeDocumentList(item *models.KnowledgeDocument, locales ...string) response.KnowledgeDocumentListResponse {
|
||||
return response.KnowledgeDocumentListResponse{
|
||||
ID: item.ID,
|
||||
KnowledgeBaseID: item.KnowledgeBaseID,
|
||||
DirectoryID: item.DirectoryID,
|
||||
Title: item.Title,
|
||||
Status: item.Status,
|
||||
StatusName: enums.GetStatusLabel(item.Status),
|
||||
StatusName: knowledgeLabel(locales, "status", item.Status),
|
||||
IndexStatus: item.IndexStatus,
|
||||
IndexStatusName: enums.GetKnowledgeDocumentIndexStatusLabel(item.IndexStatus),
|
||||
IndexStatusName: knowledgeLabel(locales, "index", item.IndexStatus),
|
||||
IndexedAt: item.IndexedAt,
|
||||
IndexError: item.IndexError,
|
||||
IndexError: knowledgeIndexError(locales, item.IndexError),
|
||||
ContentHash: item.ContentHash,
|
||||
ContentType: item.ContentType,
|
||||
CreatedAt: item.CreatedAt,
|
||||
@@ -76,7 +78,7 @@ func BuildKnowledgeDocumentList(item *models.KnowledgeDocument) response.Knowled
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKnowledgeFAQ(item *models.KnowledgeFAQ) response.KnowledgeFAQResponse {
|
||||
func BuildKnowledgeFAQ(item *models.KnowledgeFAQ, locales ...string) response.KnowledgeFAQResponse {
|
||||
return response.KnowledgeFAQResponse{
|
||||
ID: item.ID,
|
||||
KnowledgeBaseID: item.KnowledgeBaseID,
|
||||
@@ -85,11 +87,11 @@ func BuildKnowledgeFAQ(item *models.KnowledgeFAQ) response.KnowledgeFAQResponse
|
||||
Answer: item.Answer,
|
||||
SimilarQuestions: parseSimilarQuestions(item.SimilarQuestions),
|
||||
Status: item.Status,
|
||||
StatusName: enums.GetStatusLabel(item.Status),
|
||||
StatusName: knowledgeLabel(locales, "status", item.Status),
|
||||
IndexStatus: item.IndexStatus,
|
||||
IndexStatusName: enums.GetKnowledgeDocumentIndexStatusLabel(item.IndexStatus),
|
||||
IndexStatusName: knowledgeLabel(locales, "index", item.IndexStatus),
|
||||
IndexedAt: item.IndexedAt,
|
||||
IndexError: item.IndexError,
|
||||
IndexError: knowledgeIndexError(locales, item.IndexError),
|
||||
Remark: item.Remark,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
@@ -98,7 +100,7 @@ func BuildKnowledgeFAQ(item *models.KnowledgeFAQ) response.KnowledgeFAQResponse
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKnowledgeDirectory(item *models.KnowledgeDirectory) response.KnowledgeDirectoryResponse {
|
||||
func BuildKnowledgeDirectory(item *models.KnowledgeDirectory, locales ...string) response.KnowledgeDirectoryResponse {
|
||||
return response.KnowledgeDirectoryResponse{
|
||||
ID: item.ID,
|
||||
KnowledgeBaseID: item.KnowledgeBaseID,
|
||||
@@ -106,7 +108,7 @@ func BuildKnowledgeDirectory(item *models.KnowledgeDirectory) response.Knowledge
|
||||
Name: item.Name,
|
||||
SortNo: item.SortNo,
|
||||
Status: item.Status,
|
||||
StatusName: enums.GetStatusLabel(item.Status),
|
||||
StatusName: knowledgeLabel(locales, "status", item.Status),
|
||||
Remark: item.Remark,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
@@ -116,14 +118,14 @@ func BuildKnowledgeDirectory(item *models.KnowledgeDirectory) response.Knowledge
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKnowledgeRetrieveLog(item *models.KnowledgeRetrieveLog) response.KnowledgeRetrieveLogResponse {
|
||||
func BuildKnowledgeRetrieveLog(item *models.KnowledgeRetrieveLog, locales ...string) response.KnowledgeRetrieveLogResponse {
|
||||
return response.KnowledgeRetrieveLogResponse{
|
||||
ID: item.ID,
|
||||
KnowledgeBaseID: item.KnowledgeBaseID,
|
||||
Channel: item.Channel,
|
||||
ChannelName: enums.GetKnowledgeRetrieveChannelLabel(enums.KnowledgeRetrieveChannel(item.Channel)),
|
||||
ChannelName: knowledgeLabel(locales, "channel", item.Channel),
|
||||
Scene: item.Scene,
|
||||
SceneName: enums.GetKnowledgeRetrieveSceneLabel(enums.KnowledgeRetrieveScene(item.Scene)),
|
||||
SceneName: knowledgeLabel(locales, "scene", item.Scene),
|
||||
SessionID: item.SessionID,
|
||||
ConversationID: item.ConversationID,
|
||||
RequestID: item.RequestID,
|
||||
@@ -131,7 +133,7 @@ func BuildKnowledgeRetrieveLog(item *models.KnowledgeRetrieveLog) response.Knowl
|
||||
RewriteQuestion: item.RewriteQuestion,
|
||||
Answer: item.Answer,
|
||||
AnswerStatus: item.AnswerStatus,
|
||||
AnswerStatusName: enums.GetKnowledgeAnswerStatusLabel(enums.KnowledgeAnswerStatus(item.AnswerStatus)),
|
||||
AnswerStatusName: knowledgeLabel(locales, "answer", item.AnswerStatus),
|
||||
HitCount: item.HitCount,
|
||||
TopScore: item.TopScore,
|
||||
ChunkProvider: item.ChunkProvider,
|
||||
@@ -153,7 +155,7 @@ func BuildKnowledgeRetrieveLog(item *models.KnowledgeRetrieveLog) response.Knowl
|
||||
}
|
||||
}
|
||||
|
||||
func BuildKnowledgeRetrieveHitResponse(item *models.KnowledgeRetrieveHit) response.KnowledgeRetrieveHitResponse {
|
||||
func BuildKnowledgeRetrieveHitResponse(item *models.KnowledgeRetrieveHit, locales ...string) response.KnowledgeRetrieveHitResponse {
|
||||
return response.KnowledgeRetrieveHitResponse{
|
||||
ID: item.ID,
|
||||
RetrieveLogID: item.RetrieveLogID,
|
||||
@@ -167,7 +169,7 @@ func BuildKnowledgeRetrieveHitResponse(item *models.KnowledgeRetrieveHit) respon
|
||||
Title: item.Title,
|
||||
SectionPath: item.SectionPath,
|
||||
ChunkType: item.ChunkType,
|
||||
ChunkTypeName: enums.GetKnowledgeChunkTypeLabel(enums.KnowledgeChunkType(item.ChunkType)),
|
||||
ChunkTypeName: knowledgeLabel(locales, "chunk", item.ChunkType),
|
||||
Provider: item.Provider,
|
||||
RankNo: item.RankNo,
|
||||
Score: item.Score,
|
||||
@@ -189,3 +191,41 @@ func parseSimilarQuestions(raw string) []string {
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
var knowledgeLabelCodes = map[string]map[string]bool{
|
||||
"type": {"document": true, "faq": true},
|
||||
"status": {"0": true, "1": true, "2": true},
|
||||
"index": {"pending": true, "indexed": true, "failed": true},
|
||||
"mode": {"1": true, "2": true},
|
||||
"channel": {"im": true, "agent_assist": true, "api": true, "debug": true},
|
||||
"scene": {"first_response": true, "assist": true, "qa": true},
|
||||
"answer": {"1": true, "2": true, "3": true, "4": true},
|
||||
"chunk": {"text": true, "faq": true, "table": true, "code": true},
|
||||
}
|
||||
|
||||
// Labels are application-owned; titles, content, snippets and historical answers are not.
|
||||
func knowledgeLabel(locales []string, group string, value any) string {
|
||||
code := fmt.Sprint(value)
|
||||
|
||||
key := "knowledge.labels.unknown"
|
||||
if knowledgeLabelCodes[group][code] {
|
||||
key = "knowledge.labels." + group + "." + code
|
||||
}
|
||||
return i18nx.Getf(knowledgeLocale(locales), key)
|
||||
}
|
||||
|
||||
func knowledgeLocale(locales []string) string {
|
||||
if len(locales) > 0 {
|
||||
return i18nx.NormalizeLocale(locales[0])
|
||||
}
|
||||
return i18nx.DefaultLocale
|
||||
}
|
||||
|
||||
// Legacy index_error values can contain SQL, credentials or provider response bodies.
|
||||
// Keep those values out of the public DTO; this does not rewrite persisted diagnostics.
|
||||
func knowledgeIndexError(locales []string, stored string) string {
|
||||
if stored == "" {
|
||||
return ""
|
||||
}
|
||||
return i18nx.Getf(knowledgeLocale(locales), "error.knowledge.indexFailed")
|
||||
}
|
||||
|
||||
47
internal/builders/knowledge_builder_i18n_test.go
Normal file
47
internal/builders/knowledge_builder_i18n_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package builders
|
||||
|
||||
import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKnowledgeBuildersLocalizeLabelsWithoutChangingContent(t *testing.T) {
|
||||
for _, locale := range []string{"en-US", "ru-RU", "zh-CN"} {
|
||||
t.Run(locale, func(t *testing.T) {
|
||||
kb := BuildKnowledgeBase(&models.KnowledgeBase{Name: "用户 название", KnowledgeType: "faq", Status: enums.StatusOk, AnswerMode: 1}, locale)
|
||||
if kb.Name != "用户 название" || kb.KnowledgeTypeName != i18nx.Getf(locale, "knowledge.labels.type.faq") || kb.StatusName != i18nx.Getf(locale, "knowledge.labels.status.0") {
|
||||
t.Fatalf("unexpected labels/content: %+v", kb)
|
||||
}
|
||||
raw := " 原文 answer\n回答: keep this "
|
||||
faq := BuildKnowledgeFAQ(&models.KnowledgeFAQ{Question: raw, Answer: raw, Status: enums.StatusDisabled, IndexStatus: enums.KnowledgeDocumentIndexStatusFailed, IndexError: "SQL failed password=fixture-only"}, locale)
|
||||
if faq.Question != raw || faq.Answer != raw {
|
||||
t.Fatal("authored content changed")
|
||||
}
|
||||
if faq.IndexStatusName != i18nx.Getf(locale, "knowledge.labels.index.failed") || faq.StatusName != i18nx.Getf(locale, "knowledge.labels.status.1") {
|
||||
t.Fatalf("unexpected labels: %+v", faq)
|
||||
}
|
||||
if faq.IndexError != i18nx.Getf(locale, "error.knowledge.indexFailed") || strings.Contains(faq.IndexError, "fixture-only") {
|
||||
t.Fatalf("unsafe index error %q", faq.IndexError)
|
||||
}
|
||||
doc := BuildKnowledgeDocument(&models.KnowledgeDocument{Content: raw, IndexError: "provider raw error"}, locale)
|
||||
list := BuildKnowledgeDocumentList(&models.KnowledgeDocument{IndexError: "provider raw error"}, locale)
|
||||
if doc.Content != raw || doc.IndexError != faq.IndexError || list.IndexError != faq.IndexError {
|
||||
t.Fatal("document content or safe error mismatch")
|
||||
}
|
||||
log := BuildKnowledgeRetrieveLog(&models.KnowledgeRetrieveLog{Answer: raw, Question: raw, AnswerStatus: 2, TraceData: raw}, locale)
|
||||
hit := BuildKnowledgeRetrieveHitResponse(&models.KnowledgeRetrieveHit{Snippet: raw, ChunkType: "faq"}, locale)
|
||||
if log.Answer != raw || log.Question != raw || log.TraceData != raw || hit.Snippet != raw {
|
||||
t.Fatal("historical data was rewritten")
|
||||
}
|
||||
if log.AnswerStatusName != i18nx.Getf(locale, "knowledge.labels.answer.2") {
|
||||
t.Fatalf("answer label = %q", log.AnswerStatusName)
|
||||
}
|
||||
})
|
||||
}
|
||||
if BuildKnowledgeFAQ(&models.KnowledgeFAQ{}, "en-US").IndexError != "" {
|
||||
t.Fatal("empty error should remain empty")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user