Files
llm-intelligence/scripts/generate_daily_report.go

190 lines
5.7 KiB
Go
Raw Normal View History

// 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
}