190 lines
5.7 KiB
Go
190 lines
5.7 KiB
Go
|
|
// 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
|
|||
|
|
}
|