Files
llm-intelligence/scripts/generate_daily_report.go
Your Name ba054f04cf feat(phase1): OpenRouter采集器接入PostgreSQL,数据链路闭环
- 将 fetch_openrouter.go 的 summarize() 实现为 PostgreSQL upsert
- 新增 -db 参数和 DATABASE_URL 环境变量支持
- 打通 models + model_prices 表的最小可运行链路
- 创建 llm_intelligence 数据库并运行 migration
- 前端 Explorer 验证 T-3.2~T-3.5 全部通过
- 日报生成器正常产出 Markdown 和 latest_models.json
2026-05-08 13:49:12 +08:00

190 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}