package draft import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "sort" "strings" "time" "github.com/aisim/kb-cli/internal/index" "github.com/aisim/kb-cli/internal/llm" "github.com/aisim/kb-cli/internal/search" ) // Intake 草稿录入器 type Intake struct { vaultPath string llmClient *llm.Client store *index.Store } // DraftInfo 草稿信息(用于 list 输出) type DraftInfo struct { Filepath string `json:"filepath"` Filename string `json:"filename"` Title string `json:"title,omitempty"` Status string `json:"status,omitempty"` Type string `json:"type,omitempty"` Created string `json:"created,omitempty"` HasMergeHint bool `json:"has_merge_hint"` } // DraftMeta 草稿元数据 type DraftMeta struct { Title string `yaml:"title"` Type string `yaml:"type"` Status string `yaml:"status"` Source string `yaml:"source,omitempty"` Tags []string `yaml:"tags"` Created string `yaml:"created"` } // Counter 计数器 type Counter struct { Week string `json:"week"` Next int `json:"next"` ResetDay string `json:"reset_day"` LastUpdated string `json:"last_updated"` } // UnmarshalJSON 自定义反序列化,兼容 week 字段为数字或字符串 func (c *Counter) UnmarshalJSON(data []byte) error { type Alias Counter aux := &struct { Week interface{} `json:"week"` *Alias }{ Alias: (*Alias)(c), } if err := json.Unmarshal(data, aux); err != nil { return err } switch v := aux.Week.(type) { case float64: c.Week = fmt.Sprintf("%d", int(v)) case string: c.Week = v } return nil } // NewIntake 创建草稿录入器 func NewIntake(vaultPath string, store *index.Store) *Intake { // 展开 ~ 为实际的用户目录 if strings.HasPrefix(vaultPath, "~/") { if home, err := os.UserHomeDir(); err == nil { vaultPath = filepath.Join(home, vaultPath[2:]) } } return &Intake{ vaultPath: vaultPath, llmClient: llm.NewClient(), store: store, } } // CreateDraft 创建草稿 func (i *Intake) CreateDraft(draftType, title, content, source string, force bool) error { // 1. 调用 LLM 提取 tags fmt.Println("正在提取 tags...") extractResult, err := i.llmClient.ExtractTags(content) if err != nil { return fmt.Errorf("提取 tags 失败: %w", err) } fmt.Printf("提取到 %d 个 tags: %v\n", len(extractResult.Tags), extractResult.Tags) // 2. 使用 tags 搜索知识库 fmt.Println("正在搜索相关文档...") candidates, err := i.searchByTags(extractResult.Tags) if err != nil { return fmt.Errorf("搜索知识库失败: %w", err) } fmt.Printf("找到 %d 个相关文档\n", len(candidates)) // 3. 调用 LLM 生成合并指示 var mergeHint *llm.MergeHint if len(candidates) > 0 { fmt.Println("正在生成合并指示...") dirStructure := i.scanDirectoryStructure() mergeHint, err = i.llmClient.GenerateMergeHint(content, candidates, dirStructure) if err != nil { fmt.Printf("警告: 生成合并指示失败: %v\n", err) mergeHint = nil } } // 4. 创建草稿目录和文件 draftDir, err := i.createDraftDir(draftType, title, content, source, extractResult.Tags, force) if err != nil { return fmt.Errorf("创建草稿目录失败: %w", err) } // 5. 写入 merge.md(无论是否有候选文档都生成) if mergeHint != nil { if err := i.writeMergeHint(draftDir, mergeHint, candidates); err != nil { fmt.Printf("警告: 写入合并指示失败: %v\n", err) } } else { // 没有候选文档时,生成默认的 merge.md if err := i.writeDefaultMergeHint(draftDir); err != nil { fmt.Printf("警告: 写入默认合并指示失败: %v\n", err) } } fmt.Printf("草稿创建成功: %s\n", draftDir) return nil } // searchByTags 使用 tags 搜索知识库 func (i *Intake) searchByTags(tags []string) ([]llm.SearchCandidate, error) { if len(tags) == 0 { return nil, nil } // 使用所有 tags 作为关键词搜索,使用 OR 逻辑(任意一个匹配即可) // 这样即使某些 tag 匹配不到,其他 tag 也能找到结果 opts := search.SearchOptions{ TopN: 3, WithContent: true, // 需要内容给 LLM 评估 } results, err := search.Search(i.store, tags, opts) if err != nil { // 如果搜索失败,尝试逐个 tag 搜索,返回第一个有结果的 for _, tag := range tags { singleResults, singleErr := search.Search(i.store, []string{tag}, opts) if singleErr == nil && len(singleResults) > 0 { results = singleResults break } } if len(results) == 0 { return nil, nil // 所有 tag 都搜索失败,返回空而不是错误 } } candidates := make([]llm.SearchCandidate, 0, len(results)) for _, r := range results { // 截取前5000字给 LLM 评估 content := r.Content if len(content) > 5000 { content = content[:5000] + "\n...(内容已截断)" } candidates = append(candidates, llm.SearchCandidate{ Title: r.Title, Path: r.Path, Score: float64(r.Score), Content: content, }) } return candidates, nil } // createDraftDir 创建草稿目录和文件 func (i *Intake) createDraftDir(draftType, title, content, source string, tags []string, force bool) (string, error) { // 确定子目录 subDir := i.getSubDir(draftType) reviewDir := filepath.Join(i.vaultPath, "待审阅", subDir) // 检查是否已存在相同标题的草稿 if !force { if err := i.checkDuplicate(reviewDir, title); err != nil { return "", err } } // 获取下一个编号 seq, err := i.getNextSeq(reviewDir) if err != nil { return "", fmt.Errorf("获取编号失败: %w", err) } // 生成目录名 now := time.Now() dateStr := now.Format("20060102") safeTitle := i.sanitizeTitle(title) dirName := fmt.Sprintf("%03d-%s-%s", seq, dateStr, safeTitle) draftDir := filepath.Join(reviewDir, dirName) // 创建目录 if err := os.MkdirAll(draftDir, 0755); err != nil { return "", fmt.Errorf("创建目录失败: %w", err) } // 写入 draft.md draftPath := filepath.Join(draftDir, "draft.md") if err := i.writeDraftFile(draftPath, title, draftType, source, tags, content); err != nil { return "", fmt.Errorf("写入 draft.md 失败: %w", err) } return draftDir, nil } // getSubDir 获取子目录名 func (i *Intake) getSubDir(draftType string) string { typeMap := map[string]string{ "售后": "售后提取", "产品": "产品提取", "运营": "运营提取", "行业": "行业提取", "TAPD": "TAPD提取", } if subDir, ok := typeMap[draftType]; ok { return subDir } return "沟通提取" } // checkDuplicate 检查重复 func (i *Intake) checkDuplicate(reviewDir, title string) error { entries, err := os.ReadDir(reviewDir) if err != nil { if os.IsNotExist(err) { return nil } return err } safeTitle := i.sanitizeTitle(title) for _, entry := range entries { if !entry.IsDir() { continue } // 检查目录名是否包含相同标题 if strings.Contains(entry.Name(), safeTitle) { return fmt.Errorf("待审阅区已存在相同标题的草稿: %s,使用 --force 强制创建", entry.Name()) } } return nil } // getNextSeq 获取下一个编号 func (i *Intake) getNextSeq(reviewDir string) (int, error) { // 读取 counter.json counterPath := filepath.Join(i.vaultPath, "待审阅", "counter.json") counter, err := i.readCounter(counterPath) if err != nil { // 如果不存在,从目录中推断 return i.inferNextSeq(reviewDir) } // 检查是否需要重置(每周重置) now := time.Now() currentWeek := i.getWeekNumber(now) if counter.Week != currentWeek { counter.Week = currentWeek counter.Next = 1 counter.ResetDay = now.Format("2006-01-02") } nextSeq := counter.Next counter.Next++ counter.LastUpdated = now.Format(time.RFC3339) // 写回 counter.json if err := i.writeCounter(counterPath, counter); err != nil { return 0, fmt.Errorf("更新计数器失败: %w", err) } return nextSeq, nil } // readCounter 读取计数器 func (i *Intake) readCounter(path string) (*Counter, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var counter Counter if err := json.Unmarshal(data, &counter); err != nil { return nil, err } return &counter, nil } // writeCounter 写入计数器 func (i *Intake) writeCounter(path string, counter *Counter) error { data, err := json.MarshalIndent(counter, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0644) } // inferNextSeq 从目录推断下一个编号 func (i *Intake) inferNextSeq(reviewDir string) (int, error) { entries, err := os.ReadDir(reviewDir) if err != nil { if os.IsNotExist(err) { return 1, nil } return 0, err } maxSeq := 0 re := regexp.MustCompile(`^(\d{3})-`) for _, entry := range entries { if !entry.IsDir() { continue } matches := re.FindStringSubmatch(entry.Name()) if len(matches) > 1 { var seq int fmt.Sscanf(matches[1], "%d", &seq) if seq > maxSeq { maxSeq = seq } } } return maxSeq + 1, nil } // getWeekNumber 获取周数(返回纯数字周数,与 counter.json 格式一致) func (i *Intake) getWeekNumber(t time.Time) string { _, week := t.ISOWeek() return fmt.Sprintf("%d", week) } // sanitizeTitle 清理标题 func (i *Intake) sanitizeTitle(title string) string { // 替换不安全字符 re := regexp.MustCompile(`[\\/:*?"<>|\s]`) safe := re.ReplaceAllString(title, "_") // 限制长度 if len(safe) > 50 { safe = safe[:50] } return safe } // writeDraftFile 写入 draft.md func (i *Intake) writeDraftFile(path, title, draftType, source string, tags []string, content string) error { now := time.Now().Format("2006-01-02 15:04:05") // 构建 frontmatter meta := DraftMeta{ Title: title, Type: draftType, Status: "draft", Source: source, Tags: tags, Created: now, } var sb strings.Builder sb.WriteString("---\n") sb.WriteString(fmt.Sprintf("title: %q\n", meta.Title)) sb.WriteString(fmt.Sprintf("type: %q\n", meta.Type)) sb.WriteString(fmt.Sprintf("status: %q\n", meta.Status)) if meta.Source != "" { sb.WriteString(fmt.Sprintf("source: %q\n", meta.Source)) } sb.WriteString("tags: [") for i, tag := range meta.Tags { if i > 0 { sb.WriteString(", ") } sb.WriteString(fmt.Sprintf("%q", tag)) } sb.WriteString("]\n") sb.WriteString(fmt.Sprintf("created: %q\n", meta.Created)) sb.WriteString("---\n\n") sb.WriteString(content) return os.WriteFile(path, []byte(sb.String()), 0644) } // writeMergeHint 写入 merge.md func (i *Intake) writeMergeHint(draftDir string, hint *llm.MergeHint, candidates []llm.SearchCandidate) error { mergePath := filepath.Join(draftDir, "merge.md") now := time.Now().Format("2006-01-02 15:04:05") var sb strings.Builder sb.WriteString("---\n") sb.WriteString(fmt.Sprintf("generated_at: %q\n", now)) sb.WriteString(fmt.Sprintf("confidence: %q\n", hint.Recommendation.Confidence)) sb.WriteString("---\n\n") sb.WriteString("## 合并指示\n\n") sb.WriteString(fmt.Sprintf("**操作类型**: %s\n", hint.Recommendation.Action)) sb.WriteString(fmt.Sprintf("**目标**: %s\n", hint.Recommendation.Target)) sb.WriteString(fmt.Sprintf("**理由**: %s\n\n", hint.Recommendation.Reason)) sb.WriteString("## 依据\n\n") sb.WriteString("### 搜索到的相关文档(Top 3)\n\n") for i, cand := range candidates { sb.WriteString(fmt.Sprintf("%d. **%s** (相似度: %.2f)\n", i+1, cand.Title, cand.Score)) sb.WriteString(fmt.Sprintf(" - 路径: `%s`\n\n", cand.Path)) } sb.WriteString("## LLM 判断\n\n") for _, a := range hint.Analysis { sb.WriteString(fmt.Sprintf("- **%s**: %s - %s\n", a.Path, a.Relevance, a.Reason)) } return os.WriteFile(mergePath, []byte(sb.String()), 0644) } // writeDefaultMergeHint 写入默认的 merge.md(当没有候选文档时) func (i *Intake) writeDefaultMergeHint(draftDir string) error { mergePath := filepath.Join(draftDir, "merge.md") now := time.Now().Format("2006-01-02 15:04:05") var sb strings.Builder sb.WriteString("---\n") sb.WriteString(fmt.Sprintf("generated_at: %q\n", now)) sb.WriteString("confidence: \"low\"\n") sb.WriteString("---\n\n") sb.WriteString("## 合并指示\n\n") sb.WriteString("**操作类型**: new\n") sb.WriteString("**目标**: 新建独立文档\n") sb.WriteString("**理由**: 未找到相关文档,建议新建\n\n") sb.WriteString("## 依据\n\n") sb.WriteString("### 搜索到的相关文档\n\n") sb.WriteString("未找到相关文档。\n\n") sb.WriteString("## 建议\n\n") sb.WriteString("审阅时请根据草稿内容确定合适的分类和存放位置。\n") return os.WriteFile(mergePath, []byte(sb.String()), 0644) } // scanDirectoryStructure 扫描知识库目录结构,返回格式化的目录树字符串 func (i *Intake) scanDirectoryStructure() string { // 知识库根目录下的主要分类目录 topDirs := []string{ "电子秤平台", "运营管理平台", "第三方平台", "共有硬件", "通用", } var sb strings.Builder for _, topDir := range topDirs { dirPath := filepath.Join(i.vaultPath, topDir) entries, err := os.ReadDir(dirPath) if err != nil { continue } for _, entry := range entries { if entry.IsDir() { sb.WriteString(fmt.Sprintf("%s/%s/\n", topDir, entry.Name())) } } } return sb.String() } // RebuildTags 批量重建 tags func (i *Intake) RebuildTags(limit int, dryRun bool) error { // 扫描知识库所有 .md 文件 files, err := i.scanMarkdownFiles() if err != nil { return fmt.Errorf("扫描文件失败: %w", err) } if limit > 0 && len(files) > limit { files = files[:limit] } fmt.Printf("找到 %d 个文件\n", len(files)) updated := 0 for _, file := range files { fmt.Printf("处理: %s\n", file) // 读取文件内容 content, err := os.ReadFile(file) if err != nil { fmt.Printf(" 警告: 读取失败: %v\n", err) continue } // 提取正文(去掉 frontmatter) body := i.extractBody(string(content)) // 调用 LLM 提取 tags extractResult, err := i.llmClient.ExtractTags(body) if err != nil { fmt.Printf(" 警告: 提取 tags 失败: %v\n", err) continue } if dryRun { fmt.Printf(" 将更新 tags: %v\n", extractResult.Tags) } else { // 更新文件 frontmatter if err := i.updateTagsInFile(file, string(content), extractResult.Tags); err != nil { fmt.Printf(" 警告: 更新 tags 失败: %v\n", err) continue } fmt.Printf(" 已更新 tags: %v\n", extractResult.Tags) } updated++ } fmt.Printf("处理完成: %d/%d 文件\n", updated, len(files)) return nil } // scanMarkdownFiles 扫描 markdown 文件 func (i *Intake) scanMarkdownFiles() ([]string, error) { var files []string // 扫描 FAQ、知识、文档目录 dirs := []string{"FAQ", "知识", "文档"} for _, dir := range dirs { dirPath := filepath.Join(i.vaultPath, dir) if _, err := os.Stat(dirPath); os.IsNotExist(err) { continue } err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(path, ".md") { files = append(files, path) } return nil }) if err != nil { return nil, err } } // 排序 sort.Strings(files) return files, nil } // extractBody 提取正文(去掉 frontmatter) func (i *Intake) extractBody(content string) string { // 查找 frontmatter 结束位置 re := regexp.MustCompile(`(?s)^---\n.*?\n---\n`) loc := re.FindStringIndex(content) if loc != nil { return content[loc[1]:] } return content } // updateTagsInFile 更新文件中的 tags func (i *Intake) updateTagsInFile(path, content string, tags []string) error { // 解析现有 frontmatter re := regexp.MustCompile(`(?s)^---\n(.*?)\n---`) matches := re.FindStringSubmatchIndex(content) if matches == nil { return fmt.Errorf("未找到 frontmatter") } frontmatter := content[matches[2]:matches[3]] // 替换 tags 行 tagsRe := regexp.MustCompile(`(?m)^tags:.*$`) tagsLine := "tags: [" for i, tag := range tags { if i > 0 { tagsLine += ", " } tagsLine += fmt.Sprintf("%q", tag) } tagsLine += "]" newFrontmatter := tagsRe.ReplaceAllString(frontmatter, tagsLine) // 重建文件内容 newContent := content[:matches[2]] + newFrontmatter + content[matches[3]:] return os.WriteFile(path, []byte(newContent), 0644) } // ListDrafts 列出所有活跃草稿 func (i *Intake) ListDrafts() ([]DraftInfo, error) { reviewBase := filepath.Join(i.vaultPath, "待审阅") if _, err := os.Stat(reviewBase); os.IsNotExist(err) { return []DraftInfo{}, nil } var drafts []DraftInfo err := filepath.Walk(reviewBase, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 跳过 archive 目录 if info.IsDir() && info.Name() == "archive" { return filepath.SkipDir } // 检查是否是目录结构(包含 draft.md) if !info.IsDir() && info.Name() == "draft.md" { draftDir := filepath.Dir(path) mergeFile := filepath.Join(draftDir, "merge.md") content, err := os.ReadFile(path) if err != nil { return nil } relPath, _ := filepath.Rel(i.vaultPath, path) hasMerge := fileExists(mergeFile) draft := DraftInfo{ Filepath: relPath, Filename: filepath.Base(draftDir), HasMergeHint: hasMerge, } // 解析 frontmatter fm := parseFrontmatter(string(content)) if title, ok := fm["title"]; ok { draft.Title = title } if status, ok := fm["status"]; ok { draft.Status = status } if draftType, ok := fm["type"]; ok { draft.Type = draftType } if created, ok := fm["created"]; ok { draft.Created = created } drafts = append(drafts, draft) } // 兼容旧结构(单文件 .md) if !info.IsDir() && filepath.Ext(info.Name()) == ".md" && info.Name() != "draft.md" && info.Name() != "merge.md" && info.Name() != "索引.json" { // 检查是否是数字开头 name := info.Name() if len(name) >= 3 && isDigit(name[0]) && isDigit(name[1]) && isDigit(name[2]) { content, err := os.ReadFile(path) if err != nil { return nil } relPath, _ := filepath.Rel(i.vaultPath, path) draft := DraftInfo{ Filepath: relPath, Filename: name, HasMergeHint: false, } // 解析 frontmatter fm := parseFrontmatter(string(content)) if title, ok := fm["title"]; ok { draft.Title = title } if status, ok := fm["status"]; ok { draft.Status = status } if draftType, ok := fm["type"]; ok { draft.Type = draftType } if created, ok := fm["created"]; ok { draft.Created = created } drafts = append(drafts, draft) } } return nil }) if err != nil { return nil, err } return drafts, nil } // ConvertOldDrafts 转换老格式草稿到新格式 func (i *Intake) ConvertOldDrafts(target string) (int, error) { reviewDir := filepath.Join(i.vaultPath, "待审阅") var dirs []string if target != "" { dirs = append(dirs, filepath.Join(reviewDir, target)) } else { entries, err := os.ReadDir(reviewDir) if err != nil { return 0, err } for _, entry := range entries { if entry.IsDir() && entry.Name() != "archive" { dirs = append(dirs, filepath.Join(reviewDir, entry.Name())) } } } total := 0 for _, dir := range dirs { if _, err := os.Stat(dir); os.IsNotExist(err) { continue } entries, err := os.ReadDir(dir) if err != nil { continue } var oldFiles []string for _, entry := range entries { if !entry.IsDir() && filepath.Ext(entry.Name()) == ".md" { // 检查是否是数字开头的单文件 name := entry.Name() if len(name) >= 3 && isDigit(name[0]) && isDigit(name[1]) && isDigit(name[2]) { oldFiles = append(oldFiles, filepath.Join(dir, name)) } } } if len(oldFiles) == 0 { continue } fmt.Printf("\n🔍 %s: 找到 %d 个老格式文件\n\n", filepath.Base(dir), len(oldFiles)) for _, oldFile := range oldFiles { if convertOneDraft(oldFile) { total++ } } } return total, nil } // convertOneDraft 转换单个老格式草稿 func convertOneDraft(oldFile string) bool { fmt.Printf("📄 转换: %s\n", filepath.Base(oldFile)) content, err := os.ReadFile(oldFile) if err != nil { fmt.Printf(" ⚠️ 读取失败: %v\n", err) return false } // 解析文件名 name := filepath.Base(oldFile) ext := filepath.Ext(name) nameWithoutExt := strings.TrimSuffix(name, ext) // 提取编号、日期、标题 parts := strings.SplitN(nameWithoutExt, "-", 3) if len(parts) < 3 { fmt.Printf(" ⚠️ 文件名格式不匹配: %s\n", name) return false } num := parts[0] dateStr := parts[1] title := parts[2] // 创建新目录 newDirName := fmt.Sprintf("%s-%s-%s", num, dateStr, title) newDir := filepath.Join(filepath.Dir(oldFile), newDirName) if err := os.MkdirAll(newDir, 0755); err != nil { fmt.Printf(" ⚠️ 创建目录失败: %v\n", err) return false } // 解析 frontmatter fm := parseFrontmatter(string(content)) // 构建标准化 frontmatter var sb strings.Builder sb.WriteString("---\n") if title, ok := fm["title"]; ok { sb.WriteString(fmt.Sprintf("title: %q\n", title)) } else { sb.WriteString(fmt.Sprintf("title: %q\n", strings.ReplaceAll(title, "_", "/"))) } sb.WriteString("status: \"draft\"\n") if draftType, ok := fm["type"]; ok { sb.WriteString(fmt.Sprintf("type: %q\n", draftType)) } else { sb.WriteString("type: \"TAPD\"\n") } if source, ok := fm["source"]; ok { sb.WriteString(fmt.Sprintf("source: %q\n", source)) } else { sb.WriteString(fmt.Sprintf("source: \"TAPD-%s\"\n", dateStr)) } if tags, ok := fm["tags"]; ok { // 如果 tags 已经是数组格式,直接使用 if strings.HasPrefix(tags, "[") { sb.WriteString(fmt.Sprintf("tags: %s\n", tags)) } else { sb.WriteString(fmt.Sprintf("tags: [%s]\n", tags)) } } else { sb.WriteString("tags: []\n") } if created, ok := fm["created"]; ok { sb.WriteString(fmt.Sprintf("created: %q\n", created)) } else if created, ok := fm["date"]; ok { sb.WriteString(fmt.Sprintf("created: %q\n", created)) } else { // 从日期字符串构造 if len(dateStr) == 8 { created := fmt.Sprintf("%s-%s-%s", dateStr[:4], dateStr[4:6], dateStr[6:8]) sb.WriteString(fmt.Sprintf("created: %q\n", created)) } } sb.WriteString("---\n\n") // 提取正文(去掉原 frontmatter) body := extractBody(string(content)) sb.WriteString(body) // 写入 draft.md draftFile := filepath.Join(newDir, "draft.md") if err := os.WriteFile(draftFile, []byte(sb.String()), 0644); err != nil { fmt.Printf(" ⚠️ 写入 draft.md 失败: %v\n", err) return false } fmt.Printf(" ✅ 创建: draft.md\n") // 删除原文件 if err := os.Remove(oldFile); err != nil { fmt.Printf(" ⚠️ 删除原文件失败: %v\n", err) } else { fmt.Printf(" 🗑️ 删除: %s\n", name) } return true } // extractBody 提取正文(去掉 frontmatter) func extractBody(content string) string { if !strings.HasPrefix(content, "---") { return content } endIdx := strings.Index(content[3:], "\n---") if endIdx == -1 { return content } return strings.TrimSpace(content[endIdx+7:]) } // parseFrontmatter 解析 frontmatter func parseFrontmatter(content string) map[string]string { result := make(map[string]string) if !strings.HasPrefix(content, "---") { return result } endIdx := strings.Index(content[3:], "\n---") if endIdx == -1 { return result } fmText := content[3 : endIdx+3] lines := strings.Split(fmText, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || !strings.Contains(line, ":") { continue } parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) // 去除引号 value = strings.Trim(value, "\"'") result[key] = value } return result } // isDigit 检查字符是否为数字 func isDigit(c byte) bool { return c >= '0' && c <= '9' } // fileExists 检查文件是否存在 func fileExists(path string) bool { _, err := os.Stat(path) return err == nil }