// generate_daily_report.go - 日报生成器 // 读取 fetch_openrouter.go 产出的 JSON,输出 Markdown 报告到 reports/daily/ package main import ( "encoding/json" "flag" "fmt" "os" "path/filepath" "sort" "strings" "time" ) // ReportInput fetch_openrouter.go JSON 输出结构 type ReportInput struct { GeneratedAt string `json:"generated_at"` Total int `json:"total"` Free int `json:"free"` Paid int `json:"paid"` Models []ModelRow `json:"models"` } type ModelRow struct { ID string `json:"id"` Name string `json:"name,omitempty"` ContextLength int `json:"context_length,omitempty"` Capabilities []string `json:"capabilities,omitempty"` Pricing ModelPricing `json:"pricing,omitempty"` } type ModelPricing struct { Input float64 `json:"input"` Output float64 `json:"output"` } func main() { jsonPath := flag.String("json", "models.json", "采集器 JSON 输出路径") outDir := flag.String("out", "reports/daily", "报告输出目录") topN := flag.Int("top", 10, "免费/低价 TOP N 模型数量") flag.Parse() if err := run(*jsonPath, *outDir, *topN); err != nil { fmt.Fprintf(os.Stderr, "日报生成失败: %v\n", err) os.Exit(1) } } func run(jsonPath, outDir string, topN int) error { data, err := os.ReadFile(jsonPath) if err != nil { return fmt.Errorf("读取 JSON 文件失败: %w", err) } var input ReportInput if err := json.Unmarshal(data, &input); err != nil { return fmt.Errorf("解析 JSON 失败: %w", err) } // 创建输出目录 if err := os.MkdirAll(outDir, 0755); err != nil { return fmt.Errorf("创建输出目录失败: %w", err) } // 按价格升序排列,取最便宜的 topN var paidModels []ModelRow for _, m := range input.Models { if m.Pricing.Input > 0 { paidModels = append(paidModels, m) } } sort.Slice(paidModels, func(i, j int) bool { return paidModels[i].Pricing.Input < paidModels[j].Pricing.Input }) if len(paidModels) > topN { paidModels = paidModels[:topN] } // 按上下文长度降序排列,取最大的 topN var freeModels []ModelRow for _, m := range input.Models { if m.Pricing.Input == 0 && m.Pricing.Output == 0 { freeModels = append(freeModels, m) } } sort.Slice(freeModels, func(i, j int) bool { return freeModels[i].ContextLength > freeModels[j].ContextLength }) if len(freeModels) > topN { freeModels = freeModels[:topN] } // 从 generated_at 推导报告日期,格式如 2026-05-05T08:00:00Z → 2026-05-05 var date string if input.GeneratedAt != "" { t, err := time.Parse(time.RFC3339, input.GeneratedAt) if err == nil { date = t.Format("2006-01-02") } else { date = time.Now().Format("2006-01-02") } } else { date = time.Now().Format("2006-01-02") } filename := fmt.Sprintf("daily_report_%s.md", date) outPath := filepath.Join(outDir, filename) f, err := os.Create(outPath) if err != nil { return fmt.Errorf("创建报告文件失败: %w", err) } defer f.Close() // 写入 Markdown fmt.Fprintln(f, "# LLM Intelligence Hub - 每日报告") fmt.Fprintf(f, "**报告日期**: %s \n", date) fmt.Fprintf(f, "**原始采集时间**: %s \n", input.GeneratedAt) fmt.Fprintln(f) fmt.Fprintln(f, "## 概览") fmt.Fprintln(f) fmt.Fprintf(f, "| 指标 | 数值 |\n|------|------|\n") fmt.Fprintf(f, "| 模型总数 | %d |\n", input.Total) fmt.Fprintf(f, "| 免费模型 | %d |\n", input.Free) fmt.Fprintf(f, "| 付费模型 | %d |\n", input.Paid) fmt.Fprintln(f) fmt.Fprintln(f, "## 免费模型 TOP "+fmt.Sprint(topN)+"(按上下文长度排序)") fmt.Fprintln(f) if len(freeModels) > 0 { fmt.Fprintln(f, "| 模型 | 上下文长度 | 特性 |") fmt.Fprintln(f, "|------|------------|------|") for _, m := range freeModels { caps := "无" if len(m.Capabilities) > 0 { caps = strings.Join(m.Capabilities, ", ") } fmt.Fprintf(f, "| %s | %d | %s |\n", m.ID, m.ContextLength, caps) } } else { fmt.Fprintln(f, "_暂无免费模型数据_") } fmt.Fprintln(f) fmt.Fprintln(f, "## 低价模型 TOP "+fmt.Sprint(topN)+"(按输入价格升序,$/M Token)") fmt.Fprintln(f) if len(paidModels) > 0 { fmt.Fprintln(f, "| 模型 | 输入价格 | 输出价格 | 上下文长度 |") fmt.Fprintln(f, "|------|---------|---------|------------|") for _, m := range paidModels { fmt.Fprintf(f, "| %s | %.4f | %.4f | %d |\n", m.ID, m.Pricing.Input, m.Pricing.Output, m.ContextLength) } } else { fmt.Fprintln(f, "_暂无付费模型数据_") } fmt.Fprintln(f) fmt.Fprintf(f, "\n---\n_由 LLM Intelligence Hub 自动生成 %s_\n", date) // T-3.5.1: 同步写入 latest_models.json(供 Explorer 优先读取) // 路径基于 outDir 稳定推导:outDir/../../frontend/src/data/latest_models.json latestPath := filepath.Join(outDir, "..", "..", "frontend", "src", "data", "latest_models.json") if err := os.MkdirAll(filepath.Dir(latestPath), 0755); err != nil { fmt.Fprintf(os.Stderr, "警告: 创建 latest_models.json 目录失败: %v\n", err) } else { // T-3.5.1 补丁: 规范化免费模型 pricing 字段,空对象 {} 显式写出 input/output=0 for i := range input.Models { p := &input.Models[i].Pricing if p.Input == 0 && p.Output == 0 { *p = ModelPricing{Input: 0, Output: 0} } } lf, err := os.Create(latestPath) if err != nil { fmt.Fprintf(os.Stderr, "警告: 写入 latest_models.json 失败: %v\n", err) } else { enc := json.NewEncoder(lf) enc.SetIndent("", " ") if err := enc.Encode(input); err != nil { fmt.Fprintf(os.Stderr, "警告: JSON Encode latest_models.json 失败: %v\n", err) } lf.Close() fmt.Printf("latest_models.json 已同步写入: %s\n", latestPath) } } return nil }