ai_xiaopei
6 days ago 1196f409d86bc61e7596eb274840244a62ce84ba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
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)
}