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)
|
}
|