From ac966af3d126d59081d119e05ba02f946257f669 Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Sun, 26 Jul 2026 23:22:36 +0800
Subject: [PATCH] fix: 移除所有help中的等号格式
---
internal/classify/llm.go | 301 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 301 insertions(+), 0 deletions(-)
diff --git a/internal/classify/llm.go b/internal/classify/llm.go
new file mode 100644
index 0000000..dff37e2
--- /dev/null
+++ b/internal/classify/llm.go
@@ -0,0 +1,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)
+}
--
Gitblit v1.9.1