//go:build llm_script // generate_daily_report.go v3.0 - 日报生成器(现代化UI版) // 支持:国家分类、运营商分类、信息图风格HTML package main import ( "database/sql" "encoding/json" "fmt" "html/template" "log/slog" "os" "path/filepath" "sort" "strings" "time" _ "github.com/lib/pq" ) var logger *slog.Logger func init() { logger = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) } func main() { loadProjectEnv() if err := run(); err != nil { logger.Error("日报生成失败", "error", err) os.Exit(1) } logger.Info("日报生成完成") } func loadProjectEnv() { for _, path := range []string{".env.local", ".env"} { loadEnvFile(path) } } func loadEnvFile(path string) { f, err := os.Open(path) if err != nil { return } defer f.Close() buf := make([]byte, 4096) n, _ := f.Read(buf) content := string(buf[:n]) for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } key, value, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) value = strings.TrimSpace(value) value = strings.Trim(value, `"'`) if key == "" { continue } if _, exists := os.LookupEnv(key); exists { continue } _ = os.Setenv(key, value) } } func run() error { dbConn := os.Getenv("DATABASE_URL") if dbConn == "" { return fmt.Errorf("DATABASE_URL 未设置") } db, err := sql.Open("postgres", dbConn) if err != nil { return fmt.Errorf("连接数据库失败: %w", err) } defer db.Close() date := time.Now().Format("2006-01-02") // 1. 获取报告数据(使用新schema) report, err := generateReportDataV3(db, date) if err != nil { return fmt.Errorf("生成报告数据失败: %w", err) } // 2. 创建目录 outDir := "reports/daily" os.MkdirAll(outDir, 0755) os.MkdirAll(outDir+"/html", 0755) // 3. 生成 Markdown mdPath := filepath.Join(outDir, fmt.Sprintf("daily_report_%s.md", date)) if err := generateMarkdownV3(report, mdPath); err != nil { return err } // 4. 生成 HTML(现代化UI) htmlPath := filepath.Join(outDir, "html", fmt.Sprintf("daily_report_%s.html", date)) if err := generateHTMLV3(report, htmlPath); err != nil { return err } // 5. 保存到 daily_report 表 if err := saveDailyReportV3(db, report, mdPath); err != nil { logger.Warn("保存日报记录失败", "error", err) } logger.Info("日报生成完成", "models", report.TotalModels, "free", len(report.FreeModels), "intl", len(report.IntlTop5), "domestic", len(report.DomesticTop10), "md", mdPath, "html", htmlPath) return nil } // ============ 数据模型 ============ const ( USD_TO_CNY = 7.25 // USD 转 CNY 汇率 ) type ModelInfo struct { ID, Name, ProviderName string ProviderCountry string ContextLength int InputPrice, OutputPrice float64 Currency string IsFree bool OperatorName string OperatorType string // cloud / reseller / official Region string Modality string SceneTags []SceneTag } type ReportV3 struct { Date string TotalModels int FreeModels []ModelInfo FreeTop20 []ModelInfo // 免费模型前20个(展示用) IntlTop5 []ModelInfo // 国际前5(付费低价) DomesticTop10 []ModelInfo // 国内前10(付费低价) TopContext []ModelInfo // 大上下文TOP10 TencentSubscriptionPlans []SubscriptionPlanInfo Operators []OperatorInfo Resellers []OperatorInfo QualitySummary DataQualitySummary HasCNYData bool HasDomesticData bool } type OperatorInfo struct { Name, Type, Country string ModelCount int AvgInputPrice float64 MinInputPrice float64 } type DataQualitySummary struct { Total, Fresh, Stale, CNY, USD int } type SubscriptionPlanInfo struct { PlanName string PlanFamily string Tier string Currency string ListPrice float64 QuotaValue int64 QuotaUnit string ContextWindow int ModelCount int ModelPreview string SourceURL string } // ============ 数据查询(新Schema) ============ func generateReportDataV3(db *sql.DB, date string) (*ReportV3, error) { // 查询模型+厂商+定价+运营商信息 rows, err := db.Query(` WITH latest_prices AS ( SELECT rp.model_id, rp.input_price_per_mtok, rp.output_price_per_mtok, rp.currency, rp.region, rp.is_free, o.name as operator_name, COALESCE(o.name_cn, o.name) as operator_name_cn, COALESCE(o.type, 'reseller') as operator_type, ROW_NUMBER() OVER ( PARTITION BY rp.model_id ORDER BY rp.effective_date DESC NULLS LAST, rp.id DESC ) AS rn FROM region_pricing rp LEFT JOIN operator o ON rp.operator_id = o.id ) SELECT m.external_id, COALESCE(NULLIF(m.name, ''), m.external_id) as name, COALESCE(mp.name, split_part(m.external_id, '/', 1)) as provider_name, COALESCE(mp.country, 'unknown') as provider_country, COALESCE(m.context_length, 0), m.modality, COALESCE(lp.input_price_per_mtok, 0), COALESCE(lp.output_price_per_mtok, 0), COALESCE(lp.currency, 'USD'), COALESCE(lp.is_free, false), COALESCE(lp.operator_name, 'OpenRouter'), COALESCE(lp.operator_type, 'reseller'), COALESCE(lp.region, 'global') FROM models m LEFT JOIN model_provider mp ON m.provider_id = mp.id LEFT JOIN latest_prices lp ON lp.model_id = m.id AND lp.rn = 1 WHERE m.deleted_at IS NULL ORDER BY m.id `) if err != nil { return nil, err } defer rows.Close() var allModels []ModelInfo var freeModels []ModelInfo var intlModels []ModelInfo // 国际模型(US/EU/unknown) var domesticModels []ModelInfo // 国内模型(CN) providerSet := make(map[string]struct{}) operatorSet := make(map[string]OperatorInfo) for rows.Next() { var m ModelInfo if err := rows.Scan( &m.ID, &m.Name, &m.ProviderName, &m.ProviderCountry, &m.ContextLength, &m.Modality, &m.InputPrice, &m.OutputPrice, &m.Currency, &m.IsFree, &m.OperatorName, &m.OperatorType, &m.Region, ); err != nil { logger.Warn("扫描模型数据失败", "error", err) continue } m.SceneTags = deriveSceneTags(m.Name, m.Modality, nil) allModels = append(allModels, m) if m.IsFree { freeModels = append(freeModels, m) } // 国家分类 - 国内官方平台 vs OpenRouter上的国内模型 isDomesticOfficial := (m.ProviderCountry == "CN" || m.ProviderCountry == "cn") && (m.OperatorType == "official" || m.OperatorType == "cloud") isDomesticReseller := (m.ProviderCountry == "CN" || m.ProviderCountry == "cn") && m.OperatorType == "reseller" if isDomesticOfficial { domesticModels = append(domesticModels, m) } else if isDomesticReseller { // OpenRouter上的国内模型,归入国际分类但标记 intlModels = append(intlModels, m) } else { intlModels = append(intlModels, m) } providerSet[m.ProviderName] = struct{}{} // 统计运营商 op := operatorSet[m.OperatorName] op.Name = m.OperatorName op.Type = m.OperatorType op.Country = m.ProviderCountry op.ModelCount++ if op.MinInputPrice == 0 || m.InputPrice < op.MinInputPrice { op.MinInputPrice = m.InputPrice } op.AvgInputPrice = (op.AvgInputPrice*float64(op.ModelCount-1) + m.InputPrice) / float64(op.ModelCount) operatorSet[m.OperatorName] = op } // 排序 sort.Slice(intlModels, func(i, j int) bool { if intlModels[i].IsFree != intlModels[j].IsFree { return intlModels[i].IsFree } return intlModels[i].InputPrice < intlModels[j].InputPrice }) sort.Slice(domesticModels, func(i, j int) bool { if domesticModels[i].IsFree != domesticModels[j].IsFree { return domesticModels[i].IsFree } return domesticModels[i].InputPrice < domesticModels[j].InputPrice }) sort.Slice(freeModels, func(i, j int) bool { return freeModels[i].ContextLength > freeModels[j].ContextLength }) // 提取TOP - 国际排除免费,国内包含免费(展示真实低价+免费精选) var intlTop5 []ModelInfo intlPaid := filterPaid(intlModels) if len(intlPaid) > 5 { intlTop5 = intlPaid[:5] } else { intlTop5 = intlPaid } var domesticTop10 []ModelInfo // 国内模型:优先展示付费低价,然后补充免费模型 domesticPaid := filterPaid(domesticModels) domesticTop10 = append(domesticTop10, domesticPaid...) // 补充免费国内模型(按上下文排序) var domesticFree []ModelInfo for _, m := range domesticModels { if m.IsFree { domesticFree = append(domesticFree, m) } } sort.Slice(domesticFree, func(i, j int) bool { return domesticFree[i].ContextLength > domesticFree[j].ContextLength }) for _, m := range domesticFree { if len(domesticTop10) >= 10 { break } domesticTop10 = append(domesticTop10, m) } // 免费模型只展示前20个 + 分类统计 var freeTop20 []ModelInfo if len(freeModels) > 20 { freeTop20 = freeModels[:20] } else { freeTop20 = freeModels } // 如果付费不足,用免费模型补充"推荐" if len(intlTop5) == 0 && len(intlModels) > 0 { if len(intlModels) > 5 { intlTop5 = intlModels[:5] } else { intlTop5 = intlModels } } if len(domesticTop10) == 0 && len(domesticModels) > 0 { if len(domesticModels) > 10 { domesticTop10 = domesticModels[:10] } else { domesticTop10 = domesticModels } } // 运营商分类 var operators, resellers []OperatorInfo for _, op := range operatorSet { if op.Type == "cloud" || op.Type == "official" { operators = append(operators, op) } else { resellers = append(resellers, op) } } // 数据质量统计 var fresh, stale, cny, usd int for _, m := range allModels { if m.InputPrice > 0 || m.IsFree { fresh++ } else { stale++ } if m.Currency == "CNY" { cny++ } else if m.Currency == "USD" { usd++ } } tencentPlans, err := loadTencentSubscriptionPlans(db) if err != nil { return nil, err } return &ReportV3{ Date: date, TotalModels: len(allModels), FreeModels: freeModels, FreeTop20: freeTop20, IntlTop5: intlTop5, DomesticTop10: domesticTop10, TencentSubscriptionPlans: tencentPlans, Operators: operators, Resellers: resellers, HasCNYData: cny > 0, HasDomesticData: len(domesticModels) > 0, QualitySummary: DataQualitySummary{ Total: len(allModels), Fresh: fresh, Stale: stale, CNY: cny, USD: usd, }, }, nil } func loadTencentSubscriptionPlans(db *sql.DB) ([]SubscriptionPlanInfo, error) { rows, err := db.Query(` SELECT sp.plan_name, sp.plan_family, sp.tier, sp.currency, sp.list_price, COALESCE(sp.quota_value, 0), COALESCE(sp.quota_unit, ''), COALESCE(sp.context_window, 0), COALESCE(sp.model_scope, '[]'), COALESCE(sp.source_url, '') FROM subscription_plan sp JOIN model_provider mp ON mp.id = sp.provider_id WHERE mp.name = 'Tencent' ORDER BY sp.list_price ASC, sp.plan_name ASC `) if err != nil { if strings.Contains(err.Error(), `relation "subscription_plan" does not exist`) { return nil, nil } return nil, err } defer rows.Close() var plans []SubscriptionPlanInfo for rows.Next() { var plan SubscriptionPlanInfo var modelScopeRaw string if err := rows.Scan( &plan.PlanName, &plan.PlanFamily, &plan.Tier, &plan.Currency, &plan.ListPrice, &plan.QuotaValue, &plan.QuotaUnit, &plan.ContextWindow, &modelScopeRaw, &plan.SourceURL, ); err != nil { return nil, err } var modelIDs []string if err := json.Unmarshal([]byte(modelScopeRaw), &modelIDs); err == nil { plan.ModelCount = len(modelIDs) if len(modelIDs) > 3 { plan.ModelPreview = strings.Join(modelIDs[:3], ", ") } else { plan.ModelPreview = strings.Join(modelIDs, ", ") } } plans = append(plans, plan) } return plans, rows.Err() } func filterPaid(models []ModelInfo) []ModelInfo { var paid []ModelInfo for _, m := range models { if !m.IsFree && m.InputPrice > 0 { paid = append(paid, m) } } sort.Slice(paid, func(i, j int) bool { return paid[i].InputPrice < paid[j].InputPrice }) return paid } func formatPrice(price float64, currency string) string { if price <= 0 { return "免费" } if currency == "CNY" { if price < 1 { return fmt.Sprintf("¥%.2f", price) } return fmt.Sprintf("¥%.1f", price) } // USD - convert to CNY for display cny := price * USD_TO_CNY if cny < 1 { return fmt.Sprintf("¥%.2f", cny) } return fmt.Sprintf("¥%.1f", cny) } func formatPriceWithCurrency(price float64, currency string) string { if price <= 0 { return "免费" } if currency == "CNY" { return fmt.Sprintf("¥%.2f", price) } return fmt.Sprintf("$%.2f", price) } // formatDomesticPrice 显示国内模型价格(统一转换为CNY) func formatDomesticPrice(price float64, currency string) string { if price <= 0 { return "免费" } if currency == "USD" { price = price * USD_TO_CNY } return fmt.Sprintf("¥%.2f", price) } // Deprecated: use formatPrice func formatCNY(price float64) string { return formatPrice(price, "USD") } func formatPriceUSD(price float64) string { if price <= 0 { return "免费" } return fmt.Sprintf("$%.2f", price) } func formatSubscriptionPrice(price float64, currency string) string { switch currency { case "CNY": return fmt.Sprintf("¥%.2f/月", price) case "USD": return fmt.Sprintf("$%.2f/month", price) default: return fmt.Sprintf("%.2f %s", price, currency) } } func formatSubscriptionQuota(value int64, unit string) string { if value <= 0 { return "-" } if unit == "tokens/month" { switch { case value%10000 == 0 && value < 100000000: return fmt.Sprintf("%d万 Tokens/月", value/10000) case value%100000000 == 0: return fmt.Sprintf("%d亿 Tokens/月", value/100000000) case value >= 10000000: return fmt.Sprintf("%.1f亿 Tokens/月", float64(value)/100000000) } } return fmt.Sprintf("%d %s", value, unit) } func formatContextWindowCompact(value int) string { if value <= 0 { return "-" } if value%(1024*1024) == 0 { return fmt.Sprintf("%dM", value/(1024*1024)) } if value%1024 == 0 { return fmt.Sprintf("%dK", value/1024) } return fmt.Sprintf("%d", value) } // 场景标签 type SceneTag string const ( SceneCode SceneTag = "代码" SceneReasoning SceneTag = "推理" SceneWriting SceneTag = "写作" SceneVision SceneTag = "视觉" SceneChat SceneTag = "对话" ) func deriveSceneTags(name, modality string, capabilities []string) []SceneTag { var tags []SceneTag lowerName := strings.ToLower(name) // 代码模型 if strings.Contains(lowerName, "codex") || strings.Contains(lowerName, "coder") || strings.Contains(lowerName, "code") || strings.Contains(modality, "code") { tags = append(tags, SceneCode) } // 推理模型 if strings.Contains(lowerName, "o1") || strings.Contains(lowerName, "o3") || strings.Contains(lowerName, "o4") || strings.Contains(lowerName, "reasoning") || strings.Contains(lowerName, "r1") || strings.Contains(lowerName, "thinking") { tags = append(tags, SceneReasoning) } // 视觉模型 if strings.Contains(modality, "vision") || strings.Contains(modality, "multimodal") || strings.Contains(lowerName, "vl") || strings.Contains(lowerName, "vision") { tags = append(tags, SceneVision) } // 写作/对话(兜底) if len(tags) == 0 { if strings.Contains(modality, "text") || strings.Contains(modality, "chat") { tags = append(tags, SceneChat) } } return tags } // ============ Markdown生成 ============ func generateMarkdownV3(r *ReportV3, path string) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() fmt.Fprintf(f, "# 🤖 LLM Intelligence Hub - 每日情报报告\n\n") fmt.Fprintf(f, "**报告日期**: %s \n**生成时间**: %s \n\n", r.Date, time.Now().Format(time.RFC3339)) // 数据质量摘要 fmt.Fprintf(f, "## 📊 数据质量摘要\n\n") fmt.Fprintf(f, "| 指标 | 数值 |\n|------|------|\n") fmt.Fprintf(f, "| 模型总数 | %d |\n", r.QualitySummary.Total) fmt.Fprintf(f, "| 数据新鲜 | %d |\n", r.QualitySummary.Fresh) fmt.Fprintf(f, "| CNY定价 | %d |\n", r.QualitySummary.CNY) fmt.Fprintf(f, "| USD定价 | %d |\n", r.QualitySummary.USD) fmt.Fprintf(f, "| 厂商总数 | %d |\n\n", len(r.IntlTop5)+len(r.DomesticTop10)) // 免费模型(只展示前20个 + 分类统计) if len(r.FreeModels) > 0 { fmt.Fprintf(f, "## 🆓 免费模型(共 %d 个)\n\n", len(r.FreeModels)) // 分类统计 freeByCountry := make(map[string]int) freeByProvider := make(map[string]int) for _, m := range r.FreeModels { country := m.ProviderCountry if country == "unknown" { country = "国际" } freeByCountry[country]++ freeByProvider[m.ProviderName]++ } fmt.Fprintf(f, "**按国家分布**: ") first := true for country, count := range freeByCountry { if !first { fmt.Fprintf(f, ", ") } fmt.Fprintf(f, "%s %d个", country, count) first = false } fmt.Fprintf(f, "\n\n") fmt.Fprintf(f, "**代表性模型(前20个)**:\n\n") fmt.Fprintf(f, "| 模型 | 厂商 | 国家 | 上下文 |\n") fmt.Fprintf(f, "|------|------|------|--------|\n") for _, m := range r.FreeTop20 { country := m.ProviderCountry if country == "unknown" { country = "国际" } fmt.Fprintf(f, "| %s | %s | %s | %d |\n", m.Name, m.ProviderName, country, m.ContextLength) } if len(r.FreeModels) > 20 { fmt.Fprintf(f, "| ... | ... | ... | ... |\n") fmt.Fprintf(f, "\n> 共 %d 个免费模型,以上为前20个代表性模型\n", len(r.FreeModels)) } fmt.Fprintf(f, "\n") } // 国际前5 if len(r.IntlTop5) > 0 { fmt.Fprintf(f, "## 🌍 国际推荐模型 TOP 5\n\n") fmt.Fprintf(f, "| 排名 | 模型 | 厂商 | 场景 | 输入(原价) | 输出(原价) | 上下文 |\n") fmt.Fprintf(f, "|------|------|------|------|-----------|-----------|--------|\n") for i, m := range r.IntlTop5 { scene := "对话" if len(m.SceneTags) > 0 { scene = string(m.SceneTags[0]) } fmt.Fprintf(f, "| %d | %s | %s | %s | %s | %s | %d |\n", i+1, m.Name, m.ProviderName, scene, formatPriceWithCurrency(m.InputPrice, m.Currency), formatPriceWithCurrency(m.OutputPrice, m.Currency), m.ContextLength) } fmt.Fprintf(f, "\n") } // 国内前10 if len(r.DomesticTop10) > 0 { fmt.Fprintf(f, "## 🇨🇳 国内模型 TOP 10\n\n") fmt.Fprintf(f, "| 排名 | 模型 | 厂商 | 场景 | 输入(CNY) | 输出(CNY) | 上下文 |\n") fmt.Fprintf(f, "|------|------|------|------|-----------|-----------|--------|\n") for i, m := range r.DomesticTop10 { scene := "对话" if len(m.SceneTags) > 0 { scene = string(m.SceneTags[0]) } fmt.Fprintf(f, "| %d | %s | %s | %s | %s | %s | %d |\n", i+1, m.Name, m.ProviderName, scene, formatDomesticPrice(m.InputPrice, m.Currency), formatDomesticPrice(m.OutputPrice, m.Currency), m.ContextLength) } fmt.Fprintf(f, "\n") } else { fmt.Fprintf(f, "## 🇨🇳 国内模型 TOP 10\n\n") fmt.Fprintf(f, "> ⚠️ 暂无国内厂商数据。当前仅采集了 OpenRouter(国际平台),国内厂商数据将在 Phase 2 接入。\n\n") } if len(r.TencentSubscriptionPlans) > 0 { fmt.Fprintf(f, "## 💳 腾讯云套餐订阅价\n\n") fmt.Fprintf(f, "> 以下为套餐订阅价,不参与按模型输入/输出单价排行。\n\n") fmt.Fprintf(f, "| 套餐 | 月费 | 月额度 | 上下文上限 | 覆盖模型 |\n") fmt.Fprintf(f, "|------|------|--------|------------|----------|\n") for _, plan := range r.TencentSubscriptionPlans { fmt.Fprintf( f, "| %s | %s | %s | %s | %d 个(%s) |\n", plan.PlanName, formatSubscriptionPrice(plan.ListPrice, plan.Currency), formatSubscriptionQuota(plan.QuotaValue, plan.QuotaUnit), formatContextWindowCompact(plan.ContextWindow), plan.ModelCount, plan.ModelPreview, ) } fmt.Fprintf(f, "\n") } // 分类模型展示 fmt.Fprintf(f, "## 📊 模型分类概览\n\n") // 国内模型分类 - 只展示官方平台 if len(r.DomesticTop10) > 0 { fmt.Fprintf(f, "### 🇨🇳 国内官方平台模型\n\n") // 按厂商分组 domesticByOperator := make(map[string][]ModelInfo) for _, m := range r.DomesticTop10 { if m.OperatorType == "official" || m.OperatorType == "cloud" { domesticByOperator[m.OperatorName] = append(domesticByOperator[m.OperatorName], m) } } for opName, models := range domesticByOperator { fmt.Fprintf(f, "**%s** (%d个)\n\n", opName, len(models)) fmt.Fprintf(f, "| 模型 | 场景 | 输入(CNY) | 输出(CNY) | 上下文 |\n") fmt.Fprintf(f, "|------|------|-----------|-----------|--------|\n") for _, m := range models { scene := "对话" if len(m.SceneTags) > 0 { scene = string(m.SceneTags[0]) } fmt.Fprintf(f, "| %s | %s | %s | %s | %d |\n", m.Name, scene, formatDomesticPrice(m.InputPrice, m.Currency), formatDomesticPrice(m.OutputPrice, m.Currency), m.ContextLength) } fmt.Fprintf(f, "\n") } } // 代码模型 codeModels := filterByScene(r.FreeModels, SceneCode) if len(codeModels) > 0 { fmt.Fprintf(f, "### 💻 代码模型(%d个)\n\n", len(codeModels)) fmt.Fprintf(f, "| 模型 | 厂商 | 输入(原价) | 输出(原价) |\n") fmt.Fprintf(f, "|------|------|-----------|-----------|\n") for _, m := range codeModels { if len(m.Name) > 30 { m.Name = m.Name[:27] + "..." } fmt.Fprintf(f, "| %s | %s | %s | %s |\n", m.Name, m.ProviderName, formatPriceWithCurrency(m.InputPrice, m.Currency), formatPriceWithCurrency(m.OutputPrice, m.Currency)) } fmt.Fprintf(f, "\n") } // 推理模型 reasoningModels := filterByScene(r.FreeModels, SceneReasoning) if len(reasoningModels) > 0 { fmt.Fprintf(f, "### 🧠 推理模型(%d个)\n\n", len(reasoningModels)) fmt.Fprintf(f, "| 模型 | 厂商 | 输入(原价) | 输出(原价) |\n") fmt.Fprintf(f, "|------|------|-----------|-----------|\n") for _, m := range reasoningModels { if len(m.Name) > 30 { m.Name = m.Name[:27] + "..." } fmt.Fprintf(f, "| %s | %s | %s | %s |\n", m.Name, m.ProviderName, formatPriceWithCurrency(m.InputPrice, m.Currency), formatPriceWithCurrency(m.OutputPrice, m.Currency)) } fmt.Fprintf(f, "\n") } // 视觉/多模态模型 visionModels := filterByScene(r.FreeModels, SceneVision) if len(visionModels) > 0 { fmt.Fprintf(f, "### 👁️ 视觉/多模态模型(%d个)\n\n", len(visionModels)) fmt.Fprintf(f, "| 模型 | 厂商 | 输入(原价) | 输出(原价) |\n") fmt.Fprintf(f, "|------|------|-----------|-----------|\n") for _, m := range visionModels { if len(m.Name) > 30 { m.Name = m.Name[:27] + "..." } fmt.Fprintf(f, "| %s | %s | %s | %s |\n", m.Name, m.ProviderName, formatPriceWithCurrency(m.InputPrice, m.Currency), formatPriceWithCurrency(m.OutputPrice, m.Currency)) } fmt.Fprintf(f, "\n") } // 运营商 - 区分国内和国际 var domesticOps, intlOps []OperatorInfo for _, op := range r.Operators { if op.Country == "CN" { domesticOps = append(domesticOps, op) } else { intlOps = append(intlOps, op) } } if len(domesticOps) > 0 { fmt.Fprintf(f, "## 🇨🇳 国内官方平台(%d 家)\n\n", len(domesticOps)) for _, op := range domesticOps { fmt.Fprintf(f, "- **%s**: %d 个模型,最低 ¥%.2f/MTok\n", op.Name, op.ModelCount, op.MinInputPrice) } fmt.Fprintf(f, "\n") } if len(intlOps) > 0 { fmt.Fprintf(f, "## ☁️ 国际官方平台(%d 家)\n\n", len(intlOps)) for _, op := range intlOps { fmt.Fprintf(f, "- **%s**: %d 个模型,最低 $%.2f/MTok\n", op.Name, op.ModelCount, op.MinInputPrice) } fmt.Fprintf(f, "\n") } // 中转商 if len(r.Resellers) > 0 { fmt.Fprintf(f, "## 🔀 中转/聚合平台(%d 家)\n\n", len(r.Resellers)) for _, op := range r.Resellers { fmt.Fprintf(f, "- **%s**: %d 个模型,最低 $%.2f/MTok\n", op.Name, op.ModelCount, op.MinInputPrice) } fmt.Fprintf(f, "\n") } fmt.Fprintf(f, "---\n\n📌 **说明**: 本报告由 LLM Intelligence Hub 自动生成。\n") fmt.Fprintf(f, "- 国际模型价格按 1 USD = 7.25 CNY 换算显示,括号内为原生货币价格\n") fmt.Fprintf(f, "- 国内模型价格为厂商原生 CNY 定价\n") fmt.Fprintf(f, "- 数据来源: OpenRouter API + 智谱AI + 百度千帆 + Moonshot + DeepSeek + OpenAI\n") fmt.Fprintf(f, "\n_生成时间: %s_\n", time.Now().Format(time.RFC3339)) return nil } func filterByScene(models []ModelInfo, tag SceneTag) []ModelInfo { var result []ModelInfo for _, m := range models { for _, t := range m.SceneTags { if t == tag { result = append(result, m) break } } } return result } // ============ HTML生成(现代化UI) ============ func generateHTMLV3(r *ReportV3, path string) error { tmpl := `
每日情报报告 · {{.Date}} · {{.TotalModels}} 模型覆盖
⚠️ 当前仅接入 OpenRouter 数据源,国内厂商 CNY 定价将在 Phase 2 接入。
代表性模型(前20个):
... 共 {{len .FreeModels}} 个免费模型,以上为前20个
{{end}}| 排名 | 模型 | 厂商 | 输入价格 | 输出价格 | 上下文 |
|---|---|---|---|---|---|
| {{add $i 1}} | {{$m.Name}} | {{$m.ProviderName}} | ${{printf "%.2f" $m.InputPrice}} | ${{printf "%.2f" $m.OutputPrice}} | {{$m.ContextLength}} |
| 排名 | 模型 | 厂商 | 输入价格 | 输出价格 | 上下文 |
|---|---|---|---|---|---|
| {{add $i 1}} | {{$m.Name}} | {{$m.ProviderName}} | ${{printf "%.2f" $m.InputPrice}} | ${{printf "%.2f" $m.OutputPrice}} | {{$m.ContextLength}} |
以下为套餐订阅价,不参与按模型输入/输出单价排行。
| 套餐 | 月费 | 月额度 | 上下文上限 | 覆盖模型 |
|---|---|---|---|---|
| {{.PlanName}} | {{formatSubscriptionPrice .ListPrice .Currency}} | {{formatSubscriptionQuota .QuotaValue .QuotaUnit}} | {{formatContextWindowCompact .ContextWindow}} | {{.ModelCount}} 个{{if .ModelPreview}}({{.ModelPreview}}){{end}} |
| 平台 | 模型数 | 最低价格 | 平均价格 |
|---|---|---|---|
| {{.Name}} | {{.ModelCount}} | ${{printf "%.2f" .MinInputPrice}} | ${{printf "%.2f" .AvgInputPrice}} |
| 平台 | 模型数 | 最低价格 | 平均价格 |
|---|---|---|---|
| {{.Name}} | {{.ModelCount}} | ${{printf "%.2f" .MinInputPrice}} | ${{printf "%.2f" .AvgInputPrice}} |