feat(auto-reply): 优化自动回复逻辑和知识库功能

- 将默认回复详细程度从"detailed"调整为"medium",前后端保持一致
- 新增话题切换检测逻辑,当用户主动要求换话题时提供引导回复
- 优化上下文处理机制,仅在指代型追问时注入历史对话,避免模型复读旧内容
- 改进知识库检索逻辑,区分自包含问题和指代型问题的上下文需求
- 完善知识库完整性指令,确保回复详细程度与知识展开程度一致
- 重构知识库重建逻辑,支持递归扫描子目录中的文件,修复索引为空的问题
- 增强素材匹配算法,引入强信号检测机制,避免仅凭模糊匹配误发素材
- 新增素材开场白AI生成功能,支持图片、视频、文档等类型智能描述
- 改进知识库重建通知,显示具体的文件数、分片数及失败统计信息
This commit is contained in:
2026-06-26 14:25:35 +08:00
parent 1517be2a25
commit 849090a627
12 changed files with 809 additions and 40 deletions

View File

@@ -71,6 +71,53 @@ func TestRebuildKnowledgeIndexCountsOnlyRootKnowledgeFiles(t *testing.T) {
}
}
// TestRebuildKnowledgeIndexScansSubdirectories 锁住递归扫描行为:
// 知识库按分类分文件夹组织时(文件在子目录里),重建必须把子目录里的文件
// 一并索引。这是“重置索引后向量仍为空”那个问题的根因回归测试。
func TestRebuildKnowledgeIndexScansSubdirectories(t *testing.T) {
dir := t.TempDir()
// 根目录故意不放任何知识文件,全部放进多层子目录。
files := map[string]string{
filepath.Join("01_产品", "数控机床", "VMC850规格.md"): "VMC850 立式加工中心,主轴转速 8000rpm。",
filepath.Join("03_售后", "故障排查", "常见故障.md"): "报警 E01 表示伺服过载,请检查负载。",
filepath.Join("readme.txt"): "", // 空文件,应进 FailedFiles 不计入 FileCount
}
for rel, content := range files {
full := filepath.Join(dir, rel)
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
t.Fatalf("mkdir for %s: %v", rel, err)
}
if err := os.WriteFile(full, []byte(content), 0644); err != nil {
t.Fatalf("write %s: %v", rel, err)
}
}
cfg := config.NewDefaultAutoReplyConfig()
cfg.Knowledge.Directory = dir
cfg.Knowledge.IndexPath = filepath.Join(dir, "index.json")
cfg.Retrieval.EmbeddingIndexPath = filepath.Join(dir, "embedding_index.json")
engine := testAutoReplyEngine(cfg)
idx, err := engine.rebuildKnowledgeIndex()
if err != nil {
t.Fatalf("rebuildKnowledgeIndex failed: %v", err)
}
if idx.FileCount != 2 {
t.Fatalf("expected 2 indexed files from subdirectories, got %d (chunks=%d failed=%v)", idx.FileCount, len(idx.Chunks), idx.FailedFiles)
}
if len(idx.Chunks) == 0 {
t.Fatal("expected chunks from subdirectory files, got none")
}
// 确认子目录文件的相对路径作为 Source 被正确记录(用 / 分隔)。
sources := make(map[string]bool)
for _, chunk := range idx.Chunks {
sources[chunk.Source] = true
}
if !sources["01_产品/数控机床/VMC850规格.md"] {
t.Fatalf("expected nested source path recorded, got sources=%v", sources)
}
}
func TestParsePDFKnowledgeFileExtractsTextLayer(t *testing.T) {
path := filepath.Join(t.TempDir(), "text.pdf")
writeMinimalTextPDF(t, path, "AgentBox PDF content 123")