forked from niuniu/llm-intelligence
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
This commit is contained in:
189
scripts/generate_daily_report.go
Normal file
189
scripts/generate_daily_report.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user