//go:build llm_script // generate_daily_report.go v3.0 - 日报生成器(现代化UI版) // 支持:国家分类、运营商分类、信息图风格HTML package main import ( "database/sql" "encoding/json" "fmt" "html/template" "io" "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 := os.Getenv("REPORT_OUTPUT_DIR") if outDir == "" { 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. 归档主产物,确保运行脚本和门禁使用统一路径约定 if err := archiveReportArtifacts(date, mdPath, htmlPath); err != nil { return fmt.Errorf("归档日报失败: %w", err) } // 6. 同步写入日报状态与运行轨迹 if err := saveReportTrackingV3(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 GeneratedAt string TotalModels int AllModels []ModelInfo 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 DailySignals DailySignals PageMode string MarketLabels []string HeroSummary string HeroEvidence string FreeBreakdown []FreeSourceStat ActionItems []ActionItem HeadlineItems []HeadlineItem SceneSections []SceneSection AppendixLinks []AppendixLink } type DailySignals struct { NewModels int PriceChanges int OfficialFree int AggregatorFree int UnknownFree int } type FreeSourceStat struct { Label string Description string Tone string Count int } type ActionItem struct { Title string Audience string Evidence string Tags []string } type HeadlineItem struct { Label string Title string Summary string Baseline string TrustLabel string Tone string } type Recommendation struct { Name string Provider string Operator string Usage string PriceSummary string Evidence string TrustLabel string Tags []string } type SceneSection struct { Title string Description string Lead Recommendation Others []Recommendation } type AppendixLink struct { Title string Description string Anchor string } 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 } report := &ReportV3{ Date: date, GeneratedAt: time.Now().Format(time.RFC3339), TotalModels: len(allModels), AllModels: 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, }, } if signals, err := loadDailySignals(db, date); err != nil { logger.Warn("加载日报变化信号失败", "error", err) } else { report.DailySignals = signals } decorateReportV1(report) return report, 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 } func loadDailySignals(db *sql.DB, date string) (DailySignals, error) { signals := DailySignals{} if err := db.QueryRow(` SELECT COUNT(*) FROM models WHERE deleted_at IS NULL AND DATE(created_at) = $1::date `, date).Scan(&signals.NewModels); err != nil { return signals, err } if err := db.QueryRow(` SELECT COUNT(*) FROM pricing_history WHERE DATE(changed_at) = $1::date `, date).Scan(&signals.PriceChanges); err != nil { return signals, err } return signals, nil } func decorateReportV1(r *ReportV3) { if r == nil { return } r.FreeBreakdown = buildFreeSourceBreakdown(r.FreeModels) for _, item := range r.FreeBreakdown { switch item.Label { case "官方免费": r.DailySignals.OfficialFree = item.Count case "聚合免费": r.DailySignals.AggregatorFree = item.Count case "待确认": r.DailySignals.UnknownFree = item.Count } } r.PageMode = buildPageMode(r.DailySignals) r.MarketLabels = buildMarketLabels(r) r.HeroSummary, r.HeroEvidence = buildHeroSummary(r) r.SceneSections = buildSceneSections(r) r.ActionItems = buildActionItems(r) r.HeadlineItems = buildHeadlineItems(r) r.AppendixLinks = []AppendixLink{ {Title: "完整价格", Description: "查看完整模型价格表", Anchor: "#appendix-pricing"}, {Title: "完整免费", Description: "查看全部免费模型与来源", Anchor: "#appendix-free"}, {Title: "平台覆盖", Description: "查看官方平台与聚合平台覆盖", Anchor: "#appendix-platforms"}, } } func buildFreeSourceBreakdown(models []ModelInfo) []FreeSourceStat { counts := map[string]int{ "官方免费": 0, "聚合免费": 0, "待确认": 0, } for _, model := range models { counts[classifyFreeSource(model)]++ } order := []FreeSourceStat{ {Label: "官方免费", Description: "官方或云厂商直接提供免费能力", Tone: "official", Count: counts["官方免费"]}, {Label: "聚合免费", Description: "主流聚合平台提供免费路由或免费变体", Tone: "aggregator", Count: counts["聚合免费"]}, {Label: "待确认", Description: "免费机制或来源仍需进一步核验", Tone: "caution", Count: counts["待确认"]}, } var result []FreeSourceStat for _, item := range order { if item.Count > 0 { result = append(result, item) } } return result } func classifyFreeSource(model ModelInfo) string { switch model.OperatorType { case "official", "cloud": return "官方免费" case "reseller": if isVerifiedAggregator(model.OperatorName) { return "聚合免费" } } return "待确认" } func isVerifiedAggregator(name string) bool { normalized := strings.ToLower(strings.TrimSpace(name)) switch normalized { case "openrouter", "siliconflow", "fireworks", "groq": return true default: return false } } func buildPageMode(signals DailySignals) string { if signals.NewModels == 0 && signals.PriceChanges == 0 { return "calm" } if signals.NewModels+signals.PriceChanges >= 3 { return "hot" } return "standard" } func buildMarketLabels(r *ReportV3) []string { labels := []string{} switch r.PageMode { case "hot": labels = append(labels, "热点日") case "calm": labels = append(labels, "平静日") default: labels = append(labels, "常规日") } if r.DailySignals.NewModels > 0 { labels = append(labels, "新模型日") } if r.DailySignals.PriceChanges > 0 { labels = append(labels, "价格波动") } if r.DailySignals.AggregatorFree > r.DailySignals.OfficialFree { labels = append(labels, "聚合免费偏多") } else if r.DailySignals.OfficialFree > 0 { labels = append(labels, "官方免费可看") } if len(labels) > 3 { return labels[:3] } return labels } func buildHeroSummary(r *ReportV3) (string, string) { switch r.PageMode { case "hot": return fmt.Sprintf( "今天最值得关注的是 %d 个新模型与 %d 次价格变化同时出现,免费机会仍以聚合平台为主。", r.DailySignals.NewModels, r.DailySignals.PriceChanges, ), fmt.Sprintf("官方免费 %d 个,聚合免费 %d 个", r.DailySignals.OfficialFree, r.DailySignals.AggregatorFree) case "calm": return "今天没有大规模上新或明显价格波动,优先关注稳定商用与低成本选择。", "观察重点转向稳定推荐与来源可信度" default: return fmt.Sprintf( "今天有 %d 个新模型和 %d 次价格变化,值得优先复查低成本与来源清晰的可用选择。", r.DailySignals.NewModels, r.DailySignals.PriceChanges, ), fmt.Sprintf("免费来源分层:官方 %d / 聚合 %d / 待确认 %d", r.DailySignals.OfficialFree, r.DailySignals.AggregatorFree, r.DailySignals.UnknownFree) } } func buildHeadlineItems(r *ReportV3) []HeadlineItem { var items []HeadlineItem if r.DailySignals.NewModels > 0 { items = append(items, HeadlineItem{ Label: "新模型", Title: fmt.Sprintf("今日新增 %d 个模型进入情报池", r.DailySignals.NewModels), Summary: "建议优先复查今天的新上架模型是否改变当前低成本或中文场景的最优解。", Baseline: "首次出现", TrustLabel: "数据库追踪", Tone: "info", }) } if r.DailySignals.PriceChanges > 0 { items = append(items, HeadlineItem{ Label: "价格变化", Title: fmt.Sprintf("今日检测到 %d 次价格变化", r.DailySignals.PriceChanges), Summary: "价格变化已足以影响低成本选型,建议重新确认默认模型与备用模型。", Baseline: "较昨日", TrustLabel: "价格快照", Tone: "success", }) } if r.DailySignals.AggregatorFree > 0 || r.DailySignals.OfficialFree > 0 || r.DailySignals.UnknownFree > 0 { items = append(items, HeadlineItem{ Label: "免费策略", Title: "免费机会主要来自聚合平台,不等于官方长期免费", Summary: fmt.Sprintf("官方免费 %d 个,聚合免费 %d 个,待确认 %d 个。", r.DailySignals.OfficialFree, r.DailySignals.AggregatorFree, r.DailySignals.UnknownFree), Baseline: "今日快照", TrustLabel: "来源已分层", Tone: "warning", }) } if len(items) == 0 { items = append(items, HeadlineItem{ Label: "观察重点", Title: "今日无重大上新或显著调价", Summary: "平静日优先关注稳定商用与长期成本,避免被噪音列表打断判断。", Baseline: "较昨日", TrustLabel: "日报编辑规则", Tone: "neutral", }) } if len(items) > 3 { return items[:3] } return items } func buildActionItems(r *ReportV3) []ActionItem { var actions []ActionItem if section := findSceneSection(r.SceneSections, "低成本编码"); section != nil { actions = append(actions, ActionItem{ Title: fmt.Sprintf("今天先看 %s", section.Lead.Name), Audience: "适合控制编码与推理成本的团队", Evidence: section.Lead.Evidence, Tags: []string{"低成本编码", section.Lead.TrustLabel}, }) } if section := findSceneSection(r.SceneSections, "中文通用"); section != nil { actions = append(actions, ActionItem{ Title: fmt.Sprintf("正式上线优先 %s", section.Lead.Name), Audience: "适合中文业务和稳定商用场景", Evidence: section.Lead.Evidence, Tags: []string{"中文通用", section.Lead.TrustLabel}, }) } actions = append(actions, ActionItem{ Title: "免费尝鲜先区分来源", Audience: "适合想快速试用免费模型的读者", Evidence: fmt.Sprintf("官方免费 %d 个,聚合免费 %d 个,待确认 %d 个", r.DailySignals.OfficialFree, r.DailySignals.AggregatorFree, r.DailySignals.UnknownFree), Tags: []string{"免费策略", "来源分层"}, }) if len(actions) > 3 { return actions[:3] } return actions } func findSceneSection(sections []SceneSection, title string) *SceneSection { for i := range sections { if sections[i].Title == title { return §ions[i] } } return nil } func buildSceneSections(r *ReportV3) []SceneSection { allModels := r.AllModels if len(allModels) == 0 { allModels = append(allModels, r.IntlTop5...) allModels = append(allModels, r.DomesticTop10...) allModels = append(allModels, r.FreeModels...) } builders := []struct { title string description string filter func(ModelInfo) bool sorter func(a, b ModelInfo) bool }{ { title: "低成本编码", description: "优先看能明显降低编码与工具调用成本的模型。", filter: func(m ModelInfo) bool { return hasSceneTag(m, SceneCode) }, sorter: func(a, b ModelInfo) bool { return compareByCostAndTrust(a, b) }, }, { title: "中文通用", description: "优先看中文业务、写作和稳定对话能力。", filter: func(m ModelInfo) bool { return strings.EqualFold(m.ProviderCountry, "CN") || hasSceneTag(m, SceneWriting) || hasSceneTag(m, SceneChat) }, sorter: func(a, b ModelInfo) bool { return compareByTrustThenPrice(a, b) }, }, { title: "Agent / 工具调用", description: "优先看推理、代码和长上下文能力。", filter: func(m ModelInfo) bool { return hasSceneTag(m, SceneReasoning) || hasSceneTag(m, SceneCode) }, sorter: func(a, b ModelInfo) bool { if a.ContextLength != b.ContextLength { return a.ContextLength > b.ContextLength } return compareByTrustThenPrice(a, b) }, }, { title: "视觉 / 多模态", description: "优先看视觉、多模态和图像理解相关模型。", filter: func(m ModelInfo) bool { return hasSceneTag(m, SceneVision) }, sorter: func(a, b ModelInfo) bool { return compareByTrustThenPrice(a, b) }, }, } var sections []SceneSection for _, builder := range builders { var matches []ModelInfo for _, model := range allModels { if builder.filter(model) { matches = append(matches, model) } } if len(matches) == 0 { continue } sort.Slice(matches, func(i, j int) bool { return builder.sorter(matches[i], matches[j]) }) recommendations := buildRecommendations(matches, 3) if len(recommendations) == 0 { continue } section := SceneSection{ Title: builder.title, Description: builder.description, Lead: recommendations[0], } if len(recommendations) > 1 { section.Others = recommendations[1:] } sections = append(sections, section) } return sections } func buildRecommendations(models []ModelInfo, limit int) []Recommendation { seen := make(map[string]struct{}) var result []Recommendation for _, model := range models { key := model.Name + "|" + model.OperatorName if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} result = append(result, Recommendation{ Name: model.Name, Provider: model.ProviderName, Operator: model.OperatorName, Usage: buildUsage(model), PriceSummary: buildPriceSummary(model), Evidence: buildModelEvidence(model), TrustLabel: buildTrustLabel(model), Tags: buildModelTags(model), }) if len(result) >= limit { break } } return result } func hasSceneTag(model ModelInfo, target SceneTag) bool { for _, tag := range model.SceneTags { if tag == target { return true } } return false } func compareByCostAndTrust(a, b ModelInfo) bool { if a.IsFree != b.IsFree { return a.IsFree } if trustRank(a) != trustRank(b) { return trustRank(a) < trustRank(b) } if normalizePrice(a.InputPrice, a.Currency) != normalizePrice(b.InputPrice, b.Currency) { return normalizePrice(a.InputPrice, a.Currency) < normalizePrice(b.InputPrice, b.Currency) } return a.ContextLength > b.ContextLength } func compareByTrustThenPrice(a, b ModelInfo) bool { if trustRank(a) != trustRank(b) { return trustRank(a) < trustRank(b) } if a.IsFree != b.IsFree { return a.IsFree } if normalizePrice(a.InputPrice, a.Currency) != normalizePrice(b.InputPrice, b.Currency) { return normalizePrice(a.InputPrice, a.Currency) < normalizePrice(b.InputPrice, b.Currency) } return a.ContextLength > b.ContextLength } func normalizePrice(price float64, currency string) float64 { if price <= 0 { return 0 } if currency == "USD" { return price * USD_TO_CNY } return price } func trustRank(model ModelInfo) int { switch model.OperatorType { case "official", "cloud": return 0 case "reseller": if isVerifiedAggregator(model.OperatorName) { return 1 } } return 2 } func buildUsage(model ModelInfo) string { switch { case hasSceneTag(model, SceneVision): return "适合视觉与多模态" case hasSceneTag(model, SceneCode): return "适合编码与工具调用" case hasSceneTag(model, SceneReasoning): return "适合推理与 Agent" case hasSceneTag(model, SceneWriting): return "适合中文写作与通用对话" default: return "适合通用场景" } } func buildPriceSummary(model ModelInfo) string { if model.IsFree || model.InputPrice <= 0 { return "免费" } return fmt.Sprintf("输入 %s/M", formatPrice(model.InputPrice, model.Currency)) } func buildModelEvidence(model ModelInfo) string { if model.IsFree { switch classifyFreeSource(model) { case "官方免费": return "官方免费额度已确认" case "聚合免费": return "聚合免费,适合尝鲜" default: return "免费机制仍待确认" } } if model.ContextLength >= 1024*256 { return fmt.Sprintf("长上下文 %s", formatContextWindowCompact(model.ContextLength)) } return fmt.Sprintf("输入 %s/M", formatPrice(model.InputPrice, model.Currency)) } func buildTrustLabel(model ModelInfo) string { switch model.OperatorType { case "official", "cloud": return "官方来源" case "reseller": if isVerifiedAggregator(model.OperatorName) { return "聚合来源" } } return "待验证来源" } func buildModelTags(model ModelInfo) []string { tags := []string{buildTrustLabel(model)} if model.IsFree { tags = append(tags, classifyFreeSource(model)) } if len(model.SceneTags) > 0 { tags = append(tags, string(model.SceneTags[0])) } if len(tags) > 3 { return tags[:3] } return tags } // ============ Markdown生成 ============ func generateMarkdownV3(r *ReportV3, path string) error { decorateReportV1(r) 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**页面状态**: %s \n\n", r.Date, r.GeneratedAt, r.PageMode) fmt.Fprintf(f, "## 今日结论\n\n") fmt.Fprintf(f, "> %s\n\n", r.HeroSummary) if r.HeroEvidence != "" { fmt.Fprintf(f, "- 证据: %s\n", r.HeroEvidence) } if len(r.MarketLabels) > 0 { fmt.Fprintf(f, "- 市场标签: %s\n", strings.Join(r.MarketLabels, " / ")) } fmt.Fprintf(f, "\n") fmt.Fprintf(f, "## 今日行动建议\n\n") for i, item := range r.ActionItems { fmt.Fprintf(f, "%d. **%s** \n", i+1, item.Title) fmt.Fprintf(f, " %s \n", item.Audience) fmt.Fprintf(f, " 证据: %s \n", item.Evidence) if len(item.Tags) > 0 { fmt.Fprintf(f, " 标签: %s\n", strings.Join(item.Tags, " / ")) } } fmt.Fprintf(f, "\n") fmt.Fprintf(f, "## 今日变化\n\n") fmt.Fprintf(f, "| 指标 | 数值 |\n|------|------|\n") fmt.Fprintf(f, "| 今日新增模型 | %d |\n", r.DailySignals.NewModels) fmt.Fprintf(f, "| 今日价格变化 | %d |\n", r.DailySignals.PriceChanges) fmt.Fprintf(f, "| 官方免费 | %d |\n", r.DailySignals.OfficialFree) fmt.Fprintf(f, "| 聚合免费 | %d |\n", r.DailySignals.AggregatorFree) fmt.Fprintf(f, "| 待确认免费 | %d |\n\n", r.DailySignals.UnknownFree) for _, item := range r.HeadlineItems { fmt.Fprintf(f, "### %s · %s\n\n", item.Label, item.Title) fmt.Fprintf(f, "- 影响: %s\n", item.Summary) fmt.Fprintf(f, "- 基线: %s\n", item.Baseline) fmt.Fprintf(f, "- 可信度: %s\n\n", item.TrustLabel) } if len(r.FreeBreakdown) > 0 { fmt.Fprintf(f, "### 免费来源分层\n\n") fmt.Fprintf(f, "| 类型 | 数量 | 说明 |\n|------|------|------|\n") for _, item := range r.FreeBreakdown { fmt.Fprintf(f, "| %s | %d | %s |\n", item.Label, item.Count, item.Description) } fmt.Fprintf(f, "\n") } fmt.Fprintf(f, "## 场景推荐\n\n") for _, section := range r.SceneSections { fmt.Fprintf(f, "### %s\n\n", section.Title) fmt.Fprintf(f, "- 主推荐: **%s** (%s) · %s · %s\n", section.Lead.Name, section.Lead.Provider, section.Lead.PriceSummary, section.Lead.Evidence) for _, other := range section.Others { fmt.Fprintf(f, "- 备选: %s (%s) · %s\n", other.Name, other.Provider, other.PriceSummary) } fmt.Fprintf(f, "\n") } fmt.Fprintf(f, "## 完整数据附录\n\n") for _, link := range r.AppendixLinks { fmt.Fprintf(f, "- **%s**: %s\n", link.Title, link.Description) } fmt.Fprintf(f, "\n") 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\n", r.QualitySummary.USD) 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 { fmt.Fprintf(f, "| %d | %s | %s | %s | %s | %s |\n", i+1, m.Name, m.ProviderName, formatPriceWithCurrency(m.InputPrice, m.Currency), formatPriceWithCurrency(m.OutputPrice, m.Currency), formatContextWindowCompact(m.ContextLength)) } fmt.Fprintf(f, "\n") } 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 { fmt.Fprintf(f, "| %d | %s | %s | %s | %s | %s |\n", i+1, m.Name, m.ProviderName, formatDomesticPrice(m.InputPrice, m.Currency), formatDomesticPrice(m.OutputPrice, m.Currency), formatContextWindowCompact(m.ContextLength)) } fmt.Fprintf(f, "\n") } if len(r.FreeTop20) > 0 { fmt.Fprintf(f, "### 免费模型代表样本\n\n") fmt.Fprintf(f, "| 模型 | 厂商 | 来源类型 | 上下文 |\n") fmt.Fprintf(f, "|------|------|----------|--------|\n") for _, m := range r.FreeTop20 { fmt.Fprintf(f, "| %s | %s | %s | %s |\n", m.Name, m.ProviderName, classifyFreeSource(m), formatContextWindowCompact(m.ContextLength)) } fmt.Fprintf(f, "\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") for _, op := range r.Operators { fmt.Fprintf(f, "- **%s**: %d 个模型,最低 %s/M\n", op.Name, op.ModelCount, formatPrice(op.MinInputPrice, "USD")) } for _, op := range r.Resellers { fmt.Fprintf(f, "- **%s**: %d 个模型,最低 %s/M\n", op.Name, op.ModelCount, formatPrice(op.MinInputPrice, "USD")) } fmt.Fprintf(f, "\n---\n\n📌 **说明**: 本报告由 LLM Intelligence Hub 自动生成。\n") fmt.Fprintf(f, "- 免费不等于官方永久免费,需结合来源标签判断。\n") fmt.Fprintf(f, "- 国际模型价格按 1 USD = 7.25 CNY 换算显示。\n") fmt.Fprintf(f, "\n_生成时间: %s_\n", r.GeneratedAt) 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 { decorateReportV1(r) tmpl := `
先给行动,再给证据。每张卡只回答“今天该先看什么”。
只保留真正影响当天判断的变化事件。
免费可用不等于官方长期免费,必须先区分来源。
按场景给出有限候选,优先帮助读者当天做出选择。
长表格后置,适合深度比价时再展开。
| 国际候选 | 厂商 | 输入 | 输出 | 上下文 |
|---|---|---|---|---|
| {{.Name}} | {{.ProviderName}} | {{formatPriceWithCurrency .InputPrice .Currency}} | {{formatPriceWithCurrency .OutputPrice .Currency}} | {{formatContextWindowCompact .ContextLength}} |
| 国内候选 | 厂商 | 输入(CNY) | 输出(CNY) | 上下文 |
|---|---|---|---|---|
| {{.Name}} | {{.ProviderName}} | {{formatDomesticPrice .InputPrice .Currency}} | {{formatDomesticPrice .OutputPrice .Currency}} | {{formatContextWindowCompact .ContextLength}} |
| 模型 | 厂商 | 来源类型 | 上下文 |
|---|---|---|---|
| {{.Name}} | {{.ProviderName}} | {{classifyFreeSource .}} | {{formatContextWindowCompact .ContextLength}} |
| 官方/云平台 | 模型数 | 最低输入价 | 平均输入价 |
|---|---|---|---|
| {{.Name}} | {{.ModelCount}} | {{formatPrice .MinInputPrice "USD"}} | {{formatPrice .AvgInputPrice "USD"}} |
| 聚合平台 | 模型数 | 最低输入价 | 平均输入价 |
|---|---|---|---|
| {{.Name}} | {{.ModelCount}} | {{formatPrice .MinInputPrice "USD"}} | {{formatPrice .AvgInputPrice "USD"}} |
以下为套餐订阅价,不参与按模型输入/输出单价排行。
| 套餐 | 月费 | 月额度 | 上下文上限 | 覆盖模型 |
|---|---|---|---|---|
| {{.PlanName}} | {{formatSubscriptionPrice .ListPrice .Currency}} | {{formatSubscriptionQuota .QuotaValue .QuotaUnit}} | {{formatContextWindowCompact .ContextWindow}} | {{.ModelCount}} 个{{if .ModelPreview}}({{.ModelPreview}}){{end}} |