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:
2026-10-16 15:21:42 +00:00
parent 299ea50fb5
commit 2f7a491666
176 changed files with 11869 additions and 2716 deletions

View File

@@ -0,0 +1,82 @@
package rag
import (
"context"
"encoding/json"
"fmt"
"strings"
"agent-desk/internal/models"
"agent-desk/internal/pkg/i18nx"
)
// faqContent keeps application-owned labels separate from source text. Never
// translate, trim, or replace substrings in the source question or answer.
type faqContent struct {
question string
similarQuestions []string
answer string
}
func sourceFAQContent(faq models.KnowledgeFAQ) faqContent {
content := faqContent{question: faq.Question, answer: faq.Answer}
if faq.SimilarQuestions != "" {
_ = json.Unmarshal([]byte(faq.SimilarQuestions), &content.similarQuestions)
}
return content
}
func (content faqContent) render(locale string) string {
parts := []string{i18nx.Getf(locale, "knowledge.rag.faq.question", content.question)}
if len(content.similarQuestions) > 0 {
similar := joinNonEmptySimilarQuestions(content.similarQuestions, i18nx.Getf(locale, "knowledge.rag.faq.separator"))
parts = append(parts, i18nx.Getf(locale, "knowledge.rag.faq.similarQuestions", similar))
}
parts = append(parts, i18nx.Getf(locale, "knowledge.rag.faq.answer", content.answer))
return strings.Join(parts, "\n")
}
// Index generation deliberately uses the configured default, not the locale of
// whichever administrator requested a build. A locale read never re-embeds data.
func buildFAQChunkContent(faq models.KnowledgeFAQ) string {
return sourceFAQContent(faq).render(i18nx.DefaultLocale)
}
// legacyChinese renders the exact pre-localization serialization. This is a
// recognition format, not UI copy. Keep it frozen when catalog wording changes.
func (content faqContent) legacyChinese() string {
parts := []string{fmt.Sprintf("问题:%s", content.question)}
if len(content.similarQuestions) > 0 {
parts = append(parts, "相似问:"+joinNonEmptySimilarQuestions(content.similarQuestions, ";"))
}
parts = append(parts, "回答:"+content.answer)
return strings.Join(parts, "\n")
}
// Only a full byte-for-byte match against a generated representation of the
// current source proves which text belongs to us. Stale/manual/malformed chunks
// stay as recorded. In particular, do not infer labels by splitting on Chinese
// words: the source content may itself contain those words or arbitrary lines.
// This function is for live retrieval only, never historical log replay.
func renderVerifiedFAQChunkContent(ctx context.Context, stored string, faq models.KnowledgeFAQ) string {
content := sourceFAQContent(faq)
if stored == content.legacyChinese() {
return content.render(i18nx.LocaleFromContext(ctx))
}
for _, locale := range []string{i18nx.LocaleEnUS, i18nx.LocaleRuRU, i18nx.LocaleZhCN} {
if stored == content.render(locale) {
return content.render(i18nx.LocaleFromContext(ctx))
}
}
return stored
}
func joinNonEmptySimilarQuestions(items []string, separator string) string {
nonEmpty := make([]string, 0, len(items))
for _, item := range items {
if item != "" {
nonEmpty = append(nonEmpty, item)
}
}
return strings.Join(nonEmpty, separator)
}