Remove obsolete HTML file for "水浒传" and update logging in main.go to exclude document knowledge base details
This commit is contained in:
Vendored
+6
-302
@@ -5,34 +5,18 @@ import (
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"golang.org/x/net/html"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Chapter struct {
|
||||
Title string
|
||||
Link string
|
||||
Content string
|
||||
}
|
||||
|
||||
type InitResult struct {
|
||||
DocumentKnowledgeBaseID int64
|
||||
FAQKnowledgeBaseID int64
|
||||
TotalChapters int
|
||||
CreatedDocuments int
|
||||
UpdatedDocuments int
|
||||
TotalFAQs int
|
||||
CreatedFAQs int
|
||||
UpdatedFAQs int
|
||||
FAQKnowledgeBaseID int64
|
||||
TotalFAQs int
|
||||
CreatedFAQs int
|
||||
UpdatedFAQs int
|
||||
}
|
||||
|
||||
type faqSeed struct {
|
||||
@@ -43,39 +27,11 @@ type faqSeed struct {
|
||||
}
|
||||
|
||||
func Init() (*InitResult, error) {
|
||||
chapters, err := read("水浒传")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read books failed: %w", err)
|
||||
}
|
||||
|
||||
if len(chapters) == 0 {
|
||||
return nil, fmt.Errorf("no chapters found")
|
||||
}
|
||||
|
||||
faqSeeds := knowledgeFAQSeeds()
|
||||
result := &InitResult{
|
||||
TotalChapters: len(chapters),
|
||||
TotalFAQs: len(faqSeeds),
|
||||
TotalFAQs: len(faqSeeds),
|
||||
}
|
||||
err = sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
kbModel, ensureErr := ensureKnowledgeBase(ctx.Tx)
|
||||
if ensureErr != nil {
|
||||
return ensureErr
|
||||
}
|
||||
result.DocumentKnowledgeBaseID = kbModel.ID
|
||||
|
||||
for _, chapter := range chapters {
|
||||
created, upsertErr := upsertKnowledgeDocument(ctx.Tx, kbModel.ID, chapter)
|
||||
if upsertErr != nil {
|
||||
return upsertErr
|
||||
}
|
||||
if created {
|
||||
result.CreatedDocuments++
|
||||
} else {
|
||||
result.UpdatedDocuments++
|
||||
}
|
||||
}
|
||||
|
||||
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
faqKnowledgeBase, ensureFAQErr := ensureFAQKnowledgeBase(ctx.Tx)
|
||||
if ensureFAQErr != nil {
|
||||
return ensureFAQErr
|
||||
@@ -102,54 +58,6 @@ func Init() (*InitResult, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ensureKnowledgeBase(db *gorm.DB) (*models.KnowledgeBase, error) {
|
||||
now := time.Now()
|
||||
item := repositories.KnowledgeBaseRepository.FindOne(db, sqls.NewCnd().Eq("name", "水浒传"))
|
||||
if item == nil {
|
||||
item = &models.KnowledgeBase{
|
||||
Name: "水浒传",
|
||||
Description: "四大名著测试数据",
|
||||
Status: enums.StatusOk,
|
||||
DefaultTopK: 10,
|
||||
DefaultScoreThreshold: 0.2,
|
||||
DefaultRerankLimit: 5,
|
||||
ChunkProvider: string(enums.KnowledgeChunkProviderStructured),
|
||||
ChunkTargetTokens: 300,
|
||||
ChunkMaxTokens: 400,
|
||||
ChunkOverlapTokens: 40,
|
||||
AnswerMode: int(enums.KnowledgeAnswerModeStrict),
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: constants.SystemAuditUserID,
|
||||
CreateUserName: constants.SystemAuditUserName,
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: constants.SystemAuditUserID,
|
||||
UpdateUserName: constants.SystemAuditUserName,
|
||||
},
|
||||
}
|
||||
if err := repositories.KnowledgeBaseRepository.Create(db, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
err := repositories.KnowledgeBaseRepository.Updates(db, item.ID, map[string]any{
|
||||
"description": "四大名著测试数据",
|
||||
"status": enums.StatusOk,
|
||||
"chunk_provider": string(enums.KnowledgeChunkProviderStructured),
|
||||
"chunk_target_tokens": 300,
|
||||
"chunk_max_tokens": 400,
|
||||
"chunk_overlap_tokens": 40,
|
||||
"update_user_id": constants.SystemAuditUserID,
|
||||
"update_user_name": constants.SystemAuditUserName,
|
||||
"updated_at": now,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repositories.KnowledgeBaseRepository.Get(db, item.ID), nil
|
||||
}
|
||||
|
||||
func ensureFAQKnowledgeBase(db *gorm.DB) (*models.KnowledgeBase, error) {
|
||||
now := time.Now()
|
||||
item := repositories.KnowledgeBaseRepository.FindOne(db, sqls.NewCnd().Eq("name", "贝壳客服平台 FAQ"))
|
||||
@@ -206,45 +114,6 @@ func ensureFAQKnowledgeBase(db *gorm.DB) (*models.KnowledgeBase, error) {
|
||||
return repositories.KnowledgeBaseRepository.Get(db, item.ID), nil
|
||||
}
|
||||
|
||||
func upsertKnowledgeDocument(db *gorm.DB, knowledgeBaseID int64, chapter Chapter) (bool, error) {
|
||||
now := time.Now()
|
||||
item := repositories.KnowledgeDocumentRepository.FindOne(db, sqls.NewCnd().Eq("knowledge_base_id", knowledgeBaseID).Eq("title", chapter.Title))
|
||||
if item == nil {
|
||||
item = &models.KnowledgeDocument{
|
||||
KnowledgeBaseID: knowledgeBaseID,
|
||||
Title: chapter.Title,
|
||||
ContentType: enums.KnowledgeDocumentContentTypeMarkdown,
|
||||
Content: chapter.Content,
|
||||
Status: enums.StatusOk,
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
CreateUserID: constants.SystemAuditUserID,
|
||||
CreateUserName: constants.SystemAuditUserName,
|
||||
UpdatedAt: now,
|
||||
UpdateUserID: constants.SystemAuditUserID,
|
||||
UpdateUserName: constants.SystemAuditUserName,
|
||||
},
|
||||
}
|
||||
if err := repositories.KnowledgeDocumentRepository.Create(db, item); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := repositories.KnowledgeDocumentRepository.Updates(db, item.ID, map[string]any{
|
||||
"content_type": enums.KnowledgeDocumentContentTypeMarkdown,
|
||||
"content": chapter.Content,
|
||||
"status": enums.StatusOk,
|
||||
"update_user_id": constants.SystemAuditUserID,
|
||||
"update_user_name": constants.SystemAuditUserName,
|
||||
"updated_at": now,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func upsertKnowledgeFAQ(db *gorm.DB, knowledgeBaseID int64, seed faqSeed) (bool, error) {
|
||||
now := time.Now()
|
||||
similarQuestions, err := json.Marshal(seed.SimilarQuestions)
|
||||
@@ -421,168 +290,3 @@ func knowledgeFAQSeeds() []faqSeed {
|
||||
{Question: "低频但高风险的问题也要录入 FAQ 吗?", Answer: "建议录入,但要在答案中明确风险提示和升级路径,比如退款争议、隐私投诉、法务取证等。即便命中率不高,这类问题也能显著降低误答风险。", SimilarQuestions: []string{"低频问题有必要建 FAQ 吗", "高风险问答怎么处理", "投诉类知识要不要建"}, Remark: "知识运营"},
|
||||
}
|
||||
}
|
||||
|
||||
func read(name string) (chapters []Chapter, err error) {
|
||||
path := filepath.Join("cmd", "testdata", "kb", name+".html")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
doc, err := html.Parse(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseDir := filepath.Dir(path)
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
var walk func(*html.Node) error
|
||||
walk = func(n *html.Node) error {
|
||||
if n.Type == html.ElementNode && n.Data == "a" {
|
||||
parent := n.Parent
|
||||
if parent != nil && parent.Type == html.ElementNode && parent.Data == "span" && parent.Parent != nil && hasClass(parent.Parent, "chapter") {
|
||||
href := strings.TrimSpace(getAttr(n, "href"))
|
||||
if href != "" {
|
||||
resolved := filepath.Clean(filepath.Join(baseDir, href))
|
||||
if _, ok := seen[resolved]; !ok {
|
||||
seen[resolved] = struct{}{}
|
||||
|
||||
title := cleanText(nodeText(parent))
|
||||
title = strings.ReplaceAll(title, "(原文)", "")
|
||||
title = strings.TrimSpace(title)
|
||||
|
||||
content, readErr := readChapterContent(resolved)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
|
||||
chapters = append(chapters, Chapter{
|
||||
Title: title,
|
||||
Link: resolved,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if err := walk(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = walk(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chapters, nil
|
||||
}
|
||||
|
||||
func readChapterContent(link string) (content string, err error) {
|
||||
data, err := os.ReadFile(link)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
doc, err := html.Parse(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := make([]string, 0, 128)
|
||||
var walk func(*html.Node)
|
||||
walk = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode {
|
||||
if n.Data == "h1" {
|
||||
title := cleanText(nodeText(n))
|
||||
title = strings.ReplaceAll(title, " 原文", "")
|
||||
title = strings.TrimSpace(title)
|
||||
if title != "" {
|
||||
lines = append(lines, title)
|
||||
}
|
||||
}
|
||||
|
||||
if n.Data == "p" {
|
||||
if hasClass(n, "next") || getAttr(n, "id") == "home" || getAttr(n, "id") == "list" {
|
||||
return
|
||||
}
|
||||
if isInsideClass(n, "pn") {
|
||||
return
|
||||
}
|
||||
line := cleanText(nodeText(n))
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
|
||||
walk(doc)
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func getAttr(n *html.Node, key string) string {
|
||||
for _, a := range n.Attr {
|
||||
if a.Key == key {
|
||||
return a.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasClass(n *html.Node, className string) bool {
|
||||
classes := strings.Fields(getAttr(n, "class"))
|
||||
for _, c := range classes {
|
||||
if c == className {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isInsideClass(n *html.Node, className string) bool {
|
||||
for p := n.Parent; p != nil; p = p.Parent {
|
||||
if hasClass(p, className) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nodeText(n *html.Node) string {
|
||||
if n == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
var walk func(*html.Node)
|
||||
walk = func(cur *html.Node) {
|
||||
if cur.Type == html.TextNode {
|
||||
b.WriteString(cur.Data)
|
||||
}
|
||||
for c := cur.FirstChild; c != nil; c = c.NextSibling {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
|
||||
walk(n)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func cleanText(s string) string {
|
||||
s = strings.ReplaceAll(s, "\u00a0", " ")
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user