package classify import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/aisim/kb-cli/internal/llm" ) // Document 文档信息 type Document struct { Path string `json:"path"` Title string `json:"title"` Content string `json:"content"` } // Classification 分类结果 type Classification struct { Path string `json:"path"` Platform string `json:"platform"` Device string `json:"device"` Confidence float64 `json:"confidence"` } // ClassificationResult 分类结果集合 type ClassificationResult struct { Total int `json:"total"` Classified int `json:"classified"` NeedsReview int `json:"needs_review"` Items []Classification `json:"items"` } // ScanDocuments 扫描知识库文档 func ScanDocuments(vaultPath string) ([]Document, error) { var docs []Document // 转换为绝对路径 absPath, err := filepath.Abs(vaultPath) if err != nil { return nil, fmt.Errorf("转换路径失败: %w", err) } // 扫描整个知识库的所有 .md 文件 err = filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 跳过隐藏目录和特殊目录 if info.IsDir() { name := info.Name() if strings.HasPrefix(name, ".") || name == "node_modules" { return filepath.SkipDir } // 跳过待审阅目录 if name == "待审阅" { return filepath.SkipDir } return nil } // 只处理 .md 文件 if !strings.HasSuffix(path, ".md") { return nil } content, err := os.ReadFile(path) if err != nil { return err } relPath, err := filepath.Rel(absPath, path) if err != nil { relPath = path } title := extractTitle(string(content)) summary := extractSummary(string(content), 500) docs = append(docs, Document{ Path: relPath, Title: title, Content: summary, }) return nil }) if err != nil { return nil, fmt.Errorf("扫描知识库失败: %w", err) } return docs, nil } // extractTitle 从 frontmatter 或第一行 # 提取标题 func extractTitle(content string) string { lines := strings.Split(content, "\n") inFrontmatter := false for _, line := range lines { line = strings.TrimSpace(line) if line == "---" { inFrontmatter = !inFrontmatter continue } if inFrontmatter { if strings.HasPrefix(line, "title:") { title := strings.TrimPrefix(line, "title:") title = strings.TrimSpace(title) title = strings.Trim(title, "\"'") return title } } else if strings.HasPrefix(line, "# ") { return strings.TrimPrefix(line, "# ") } } return "未知标题" } // extractSummary 提取摘要(前 maxLen 字) func extractSummary(content string, maxLen int) string { // 跳过 frontmatter lines := strings.Split(content, "\n") startIdx := 0 inFrontmatter := false for i, line := range lines { if strings.TrimSpace(line) == "---" { if !inFrontmatter { inFrontmatter = true } else { startIdx = i + 1 break } } } // 提取正文 summary := strings.Join(lines[startIdx:], "\n") summary = strings.TrimSpace(summary) if len(summary) > maxLen { summary = summary[:maxLen] + "..." } return summary } // ClassifyBatch 批量分类文档 func ClassifyBatch(docs []Document, llmClient *llm.Client) ([]Classification, error) { // 构建 prompt prompt := buildClassifyPrompt(docs) // 调用 LLM response, err := llmClient.ClassifyDocuments(prompt) if err != nil { return nil, err } // 解析响应 var results []Classification if err := json.Unmarshal([]byte(response), &results); err != nil { return nil, fmt.Errorf("解析 LLM 响应失败: %w", err) } return results, nil } // buildClassifyPrompt 构建分类 prompt func buildClassifyPrompt(docs []Document) string { prompt := `你是知识库分类专家。分析以下文档,判断其所属平台、设备和内容类型。 ## 实体定义(必须严格遵守) ### 硬件设备 | 设备 | 归属 | 说明 | |------|------|------| | 智能枪 | 运营管理平台配套 | 智能控制箱+智能枪头,独立4G网络,可独立工作或安装在电子秤中 | | 电子秤 | 电子秤平台 | 含扫码枪(分防爆/非防爆),扫码枪是电子秤配件,**不是智能枪** | | 智能阀 | 运营管理平台基础 | NFC识别芯片,是运营平台气瓶管理的基础 | | 艾信盒子 | 两平台共用 | 4G通信设备,同步存储充装数据,控制上传第三方平台 | ### 软件平台 | 平台 | 域名 | 用户 | 核心功能 | |------|------|------|----------| | 运营管理平台 | rb.zhiheiot.com | 老板/管理层/客服/配送调度 | 经营管理、售后、配送、工单、会员 | | 电子秤平台 | elc.zhiheiot.com | 气站站长/充装员/开票员 | 充装作业(扫码、充装、称重、开票/补单)+ 终端配送(简化版) | ### App终端 | App | 归属平台 | 功能 | |-----|----------|------| | 易配送App | 运营管理平台 | NFC芯片识别配送 | | 安全用气App | 电子秤平台 | 二维码配送 | | 艾信助手App | 融合两平台 | 建档 + 充前/充后检查 | ### 小程序 | 小程序 | 归属 | 用户 | 功能 | |--------|------|------|------| | 艾信LPG物联网小程序 | 电子秤平台 | 气站管理员/配送员 | 报表/迎检/配送 | | 艾信发货小程序 | 内部工具 | 艾信内部员工 | 设备总览、客户资料、气站资料 | ## 关键区分点(必须遵守) | 容易混淆 | 正确归属 | |----------|----------| | 智能枪 vs 扫码枪 | 智能枪是独立设备(4G网络);扫码枪是电子秤配件 | | 开票/补单/充装 | 电子秤平台功能 | | 主板版本过低 | 智能枪问题 | | 艾信发货小程序 | 内部工具,可控制所有智能枪、电子秤 | | 艾信LPG物联网小程序 | 电子秤平台气站管理员用 | | 配送功能 | 两平台都有,运营平台用易配送App,电子秤平台用安全用气App | ## 分类规则 1. **platform**(属于哪个平台,必须使用中文): - 电子秤平台:充装作业、开票、电子秤设备、安全用气App、艾信LPG小程序 - 运营管理平台:配送、档案、会员、工单、易配送App、智能枪、智能阀 - 共有:两个平台都涉及(艾信盒子、艾信助手App、充装记录同步) - 第三方平台:祥康、监管平台等 - 通用:不涉及具体平台/内部工具 2. **device**(涉及什么设备/App,必须使用中文): - 电子秤:含扫码枪、电磁阀、称重传感器、显示屏、键盘 - 智能枪:智能控制箱+智能枪头,独立4G网络 - 智能阀:NFC识别智能阀的芯片 - 艾信盒子:4G通信设备 - 安全用气App:电子秤平台配送端 - 易配送App:运营管理平台配送端 - 艾信助手App:融合两平台建档 - 艾信LPG小程序:电子秤平台管理+配送 - 艾信发货小程序:内部工具 - 无:不涉及具体设备 3. **content_type**(内容类型): - FAQ / PRD / 知识 / 案例 / 实体 / 其他 ## 输出格式 输出 JSON 数组,每项包含: - path: 文件路径(字符串) - platform: 平台名称(中文,必须是上述枚举值之一) - device: 设备名称(中文,必须是上述枚举值之一) - content_type: 内容类型(字符串,必须是上述枚举值之一) - confidence: 置信度(浮点数,范围 0.0-1.0,例如 0.95) 示例输出: [ { "path": "FAQ/充装类/001-智能枪通气杆卡住漏气.md", "platform": "运营管理平台", "device": "智能枪", "content_type": "FAQ", "confidence": 0.95 } ] 文档列表: ` for _, doc := range docs { prompt += fmt.Sprintf("\n文件: %s\n标题: %s\n摘要: %s\n", doc.Path, doc.Title, doc.Content) } prompt += "\n请输出 JSON 数组:" return prompt } // SaveClassification 保存分类结果 func SaveClassification(vaultPath string, results []Classification) error { // 统计 total := len(results) classified := 0 needsReview := 0 for _, r := range results { if r.Confidence >= 0.8 { classified++ } else { needsReview++ } } result := ClassificationResult{ Total: total, Classified: classified, NeedsReview: needsReview, Items: results, } path := filepath.Join(vaultPath, "classification.json") data, err := json.MarshalIndent(result, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0644) }