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