package llmstxt import ( "context" "fmt" "os" "path/filepath" "sync" "videoweb/pub/common/log" "videoweb/pub/common/utils" "videoweb/pub/pubdef" "videoweb/web/service/srv_cfg" ) const ( maxRecentToolsInBrief = 50 // llms.txt 中最新工具条数上限 maxRecentArticlesBrief = 50 // llms.txt 中最新文章条数上限 maxRecentInFull = 500 // llms-full.txt 中最新工具/文章条数上限 ) var generateMu sync.Mutex // GenerateLLMSTxt 生成 llms.txt(摘要)和 llms-full.txt(详细) func GenerateLLMSTxt() { generateMu.Lock() defer generateMu.Unlock() ctx := context.Background() config, serr := srv_cfg.GetSystemConfig(ctx) if serr != nil || config == nil { log.Error("GenerateLLMSTxt GetSystemConfig", log.Any("err", serr)) return } //baseURL := strings.TrimSuffix(config.WebAddr, "/") //categories, subCategories, cateNameMap, subCateNameMap := loadCategories(ctx) //recentTools := loadRecentTools(ctx, maxRecentToolsInBrief) //recentArticles := loadRecentArticles(ctx, maxRecentArticlesBrief) // //var brief strings.Builder //writeBrief(&brief, config.WebTitle, config.WebDescription, baseURL, config.WebKeyworld, recentTools, recentArticles) // //if err := writeTextFile("llms.txt", brief.String()); err != nil { // log.Error("GenerateLLMSTxt write llms.txt", log.Any("err", err)) // return //} //if err := commitFile("llms.txt"); err != nil { // log.Error("GenerateLLMSTxt commit llms.txt", log.Any("err", err)) // return //} // //allTools := loadRecentTools(ctx, maxRecentInFull) //allArticles := loadRecentArticles(ctx, maxRecentInFull) // //var full strings.Builder //writeFull(&full, config.WebTitle, config.WebDescription, baseURL, config.WebKeyworld, categories, subCategories, cateNameMap, subCateNameMap, allTools, allArticles) // //if err := writeTextFile("llms-full.txt", full.String()); err != nil { // log.Error("GenerateLLMSTxt write llms-full.txt", log.Any("err", err)) // return //} //if err := commitFile("llms-full.txt"); err != nil { // log.Error("GenerateLLMSTxt commit llms-full.txt", log.Any("err", err)) // return //} //log.Info("GenerateLLMSTxt done", // log.Any("categories", len(categories)), // log.Any("subCategories", len(subCategories)), // log.Any("tools", len(allTools)), // log.Any("articles", len(allArticles)), //) } // ── 数据加载 ────────────────────────────────────────────────── // //func loadCategories(ctx context.Context) ( // categories toolcategorymod.ToolCategoryModelList, // subCategories toolsubcategorymod.ToolSubCategoryModelList, // cateNameMap map[int64]string, // subCateNameMap map[int64]string, //) { // cateFilter := bson.M{ // toolcategorymod.FN.ToolCategory.Status: 1, // toolcategorymod.FN.ToolCategory.Deleted: bson.M{"$ne": 1}, // } // cateOpt := options.Find().SetSort(bson.D{ // {Key: toolcategorymod.FN.ToolCategory.Sort, Value: 1}, // {Key: toolcategorymod.FN.ToolCategory.Id, Value: 1}, // }) // categories, _ = toolcategorymod.FindMany(ctx, cateFilter, cateOpt) // // subFilter := bson.M{ // toolsubcategorymod.FN.ToolSubCategory.Status: 1, // toolsubcategorymod.FN.ToolSubCategory.Deleted: bson.M{"$ne": 1}, // } // subOpt := options.Find().SetSort(bson.D{ // {Key: toolsubcategorymod.FN.ToolSubCategory.Sort, Value: 1}, // {Key: toolsubcategorymod.FN.ToolSubCategory.Id, Value: 1}, // }) // subCategories, _ = toolsubcategorymod.FindMany(ctx, subFilter, subOpt) // // cateNameMap = make(map[int64]string, len(categories)) // for _, c := range categories { // cateNameMap[c.Id] = c.Name // } // subCateNameMap = make(map[int64]string, len(subCategories)) // for _, s := range subCategories { // subCateNameMap[s.Id] = s.Name // } // return categories, subCategories, cateNameMap, subCateNameMap //} // //func loadRecentTools(ctx context.Context, limit int64) toolmod.ToolModelList { // opt := options.Find(). // SetSort(bson.D{ // {Key: toolmod.FN.Tool.Sort, Value: 1}, // {Key: toolmod.FN.Tool.Hot, Value: -1}, // {Key: toolmod.FN.Tool.CreatedAt, Value: -1}, // }). // SetLimit(limit) // filter := bson.M{ // toolmod.FN.Tool.ReviewStatus: 1, // toolmod.FN.Tool.Slug: bson.M{"$nin": []interface{}{"", nil}}, // } // list, _ := toolmod.FindMany(ctx, filter, opt) // return list //} // //func loadRecentArticles(ctx context.Context, limit int64) articlemod.ArticleModelList { // opt := options.Find(). // SetSort(bson.D{{Key: articlemod.FN.Article.CreatedAt, Value: -1}}). // SetLimit(limit) // filter := bson.M{ // articlemod.FN.Article.ReviewStatus: 1, // articlemod.FN.Article.Slug: bson.M{"$nin": []interface{}{"", nil}}, // } // list, _ := articlemod.FindMany(ctx, filter, opt) // return list //} // //// ── 内容构建 ────────────────────────────────────────────────── // //func writeBrief( // b *strings.Builder, // title, desc, baseURL string, // keywords []string, // recentTools toolmod.ToolModelList, // recentArticles articlemod.ArticleModelList, //) { // writeHeader(b, title, desc, keywords) // writeImportantPages(b, baseURL) // // if len(recentTools) > 0 { // fmt.Fprintf(b, "## 最新 AI 工具\n\n") // for _, t := range recentTools { // if t == nil || t.Slug == "" { // continue // } // summary := t.Intro // if summary == "" { // summary = t.Desc // } // if summary == "" { // summary = t.Title // } // fmt.Fprintf(b, "- [%s](%s): %s\n", t.Title, toolDetailURL(baseURL, t.Slug), truncate(summary, 100)) // } // b.WriteString("\n") // } // // if len(recentArticles) > 0 { // fmt.Fprintf(b, "## 最新 AI 资讯\n\n") // for _, a := range recentArticles { // if a == nil || a.Slug == "" { // continue // } // summary := a.Intro // if summary == "" { // summary = a.Title // } // fmt.Fprintf(b, "- [%s](%s): %s\n", a.Title, articleDetailURL(baseURL, a.Slug), truncate(summary, 100)) // } // b.WriteString("\n") // } //} // //func GetCategorySlug(id int64, categories toolcategorymod.ToolCategoryModelList) string { // if id <= 0 { // return "" // } // if len(categories) == 0 { // return "" // } // for _, category := range categories { // if category.Id == id { // return category.Slug // } // } // return "" //} //func GetSubCategorySlug(id int64, categories toolsubcategorymod.ToolSubCategoryModelList) string { // if id <= 0 { // return "" // } // if len(categories) == 0 { // return "" // } // for _, category := range categories { // if category.Id == id { // return category.Slug // } // } // return "" //} //func writeFull( // b *strings.Builder, // title, desc, baseURL string, // keywords []string, // categories toolcategorymod.ToolCategoryModelList, // subCategories toolsubcategorymod.ToolSubCategoryModelList, // cateNameMap, subCateNameMap map[int64]string, // tools toolmod.ToolModelList, // articles articlemod.ArticleModelList, //) { // writeHeader(b, title, desc, keywords) // writeImportantPages(b, baseURL) // writeCategorySections(b, baseURL, categories, subCategories, true) // // if len(tools) > 0 { // fmt.Fprintf(b, "## AI 工具详情(最新 %d 条)\n\n", len(tools)) // for _, t := range tools { // if t == nil || t.Slug == "" { // continue // } // fmt.Fprintf(b, "### [%s](%s)\n\n", t.Title, toolDetailURL(baseURL, t.Slug)) // // summary := t.Desc // if summary == "" { // summary = t.Intro // } // if summary != "" { // fmt.Fprintf(b, "%s\n\n", truncate(summary, 500)) // } // if subName := subCateNameMap[t.SubCateId]; subName != "" { // fmt.Fprintf(b, "- 子分类:[%s](%s)\n", subName, subCategoryListURL(baseURL, GetCategorySlug(t.CategoryId, categories), GetSubCategorySlug(t.SubCateId, subCategories))) // } // if t.OfficialUrl != "" { // fmt.Fprintf(b, "- 官网:%s\n", t.OfficialUrl) // } // if len(t.SeoInfo.Keyworld) > 0 { // fmt.Fprintf(b, "- 关键词:%s\n", strings.Join(t.SeoInfo.Keyworld, "、")) // } // if !t.CreatedAt.IsZero() { // fmt.Fprintf(b, "- 发布时间:%s\n", t.CreatedAt.Format("2006-01-02")) // } // b.WriteString("\n") // } // } // // if len(articles) > 0 { // fmt.Fprintf(b, "## AI 资讯详情(最新 %d 条)\n\n", len(articles)) // for _, a := range articles { // if a == nil || a.Slug == "" { // continue // } // fmt.Fprintf(b, "### [%s](%s)\n\n", a.Title, articleDetailURL(baseURL, a.Slug)) // // if a.Intro != "" { // fmt.Fprintf(b, "%s\n\n", truncate(a.Intro, 500)) // } // if a.Author != "" { // fmt.Fprintf(b, "- 作者:%s\n", a.Author) // } // if len(a.SeoInfo.Keyworld) > 0 { // fmt.Fprintf(b, "- 关键词:%s\n", strings.Join(a.SeoInfo.Keyworld, "、")) // } // if !a.CreatedAt.IsZero() { // fmt.Fprintf(b, "- 发布时间:%s\n", a.CreatedAt.Format("2006-01-02")) // } // b.WriteString("\n") // } // } //} // //func writeHeader(b *strings.Builder, title, desc string, keywords []string) { // now := time.Now().Format("2006-01-02") // fmt.Fprintf(b, "# %s\n\n", title) // if desc != "" { // fmt.Fprintf(b, "> %s\n\n", desc) // } // if len(keywords) > 0 { // fmt.Fprintf(b, "> 关键词:%s\n\n", strings.Join(keywords, "、")) // } // fmt.Fprintf(b, "> 更新时间:%s\n\n", now) //} // //func writeImportantPages(b *strings.Builder, baseURL string) { // fmt.Fprintf(b, "## 重要页面\n\n") // fmt.Fprintf(b, "- [首页](%s/): AI 工具导航主页\n", baseURL) // fmt.Fprintf(b, "- [工具列表](%s/tool/list): 全部 AI 工具\n", baseURL) // fmt.Fprintf(b, "- [热门推荐](%s/tool/hot): 热门 AI 工具\n", baseURL) // fmt.Fprintf(b, "- [AI 资讯](%s/article/list): AI 行业资讯文章\n", baseURL) // fmt.Fprintf(b, "- [搜索](%s/search): 搜索工具与资讯\n", baseURL) // fmt.Fprintf(b, "- [关于我们](%s/about/): 站点介绍\n", baseURL) // fmt.Fprintf(b, "- [Sitemap](%s/sitemap.xml): 网站地图\n\n", baseURL) //} // //func writeCategorySections( // b *strings.Builder, // baseURL string, // categories toolcategorymod.ToolCategoryModelList, // subCategories toolsubcategorymod.ToolSubCategoryModelList, // withSeo bool, //) { // // if len(subCategories) > 0 { // fmt.Fprintf(b, "## 工具子分类\n\n") // for _, s := range subCategories { // if s == nil { // continue // } // subDesc := subCategoryDesc(s, withSeo) // fmt.Fprintf(b, "- [%s](%s): %s\n", s.Name, subCategoryListURL(baseURL, GetCategorySlug(s.CategoryId, categories), GetSubCategorySlug(s.Id, subCategories)), subDesc) // } // b.WriteString("\n") // } //} // //func subCategoryDesc(s *toolsubcategorymod.ToolSubCategoryModel, withSeo bool) string { // if withSeo && s.SeoInfo.Description != "" { // return s.SeoInfo.Description // } // return s.Name + "相关 AI 工具" //} // ── URL 构建 ────────────────────────────────────────────────── func toolDetailURL(baseURL, slug string) string { return utils.JoinUrlPath(baseURL, fmt.Sprintf("/tool/details/%s", slug)) } func articleDetailURL(baseURL, slug string) string { return utils.JoinUrlPath(baseURL, fmt.Sprintf("/article/details/%s", slug)) } func subCategoryListURL(baseURL string, categorySlug, subCateSlug string) string { return utils.JoinUrlPath(baseURL, fmt.Sprintf("/classification/%s/%s", categorySlug, subCateSlug)) } // ── 工具函数 ────────────────────────────────────────────────── func truncate(s string, maxLen int) string { runes := []rune(s) if len(runes) <= maxLen { return s } return string(runes[:maxLen]) + "…" } func writeTextFile(filename, content string) error { if err := os.MkdirAll(pubdef.OutputDir, 0755); err != nil { return fmt.Errorf("mkdir %s: %w", pubdef.OutputDir, err) } tmpPath := filepath.Join(pubdef.OutputDir, filename+".tmp") f, err := os.Create(tmpPath) if err != nil { return fmt.Errorf("create %s: %w", tmpPath, err) } defer f.Close() if _, err := f.WriteString(content); err != nil { return fmt.Errorf("write %s: %w", filename, err) } return f.Sync() } func commitFile(filename string) error { src := filepath.Join(pubdef.OutputDir, filename+".tmp") dst := filepath.Join(pubdef.OutputDir, filename) if err := os.Rename(src, dst); err != nil { os.Remove(src) return fmt.Errorf("rename %s: %w", filename, err) } return nil }