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