//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" "strconv" "strings" "time" _ "github.com/lib/pq" ) var logger *slog.Logger type ReportRunContext struct { RunKind string TriggerSource string IsOfficialDaily bool RuntimeAudit string } 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, err := resolveReportDate(time.Now(), os.Args[1:], os.Getenv("REPORT_DATE")) if err != nil { return err } runContext := resolveReportRunContext( date, time.Now(), os.Getenv("REPORT_RUN_KIND"), os.Getenv("REPORT_TRIGGER_SOURCE"), os.Getenv("REPORT_IS_OFFICIAL_DAILY"), os.Getenv("REPORT_RUNTIME_AUDIT"), ) // 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, runContext); 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 } func resolveReportDate(now time.Time, args []string, envDate string) (string, error) { date := strings.TrimSpace(envDate) for i := 0; i < len(args); i++ { switch { case args[i] == "-date" || args[i] == "--date": if i+1 >= len(args) { return "", fmt.Errorf("缺少 -date 参数值,期望格式 YYYY-MM-DD") } date = strings.TrimSpace(args[i+1]) i++ case strings.HasPrefix(args[i], "-date="): date = strings.TrimSpace(strings.TrimPrefix(args[i], "-date=")) case strings.HasPrefix(args[i], "--date="): date = strings.TrimSpace(strings.TrimPrefix(args[i], "--date=")) } } if date == "" { return now.Format("2006-01-02"), nil } parsed, err := time.Parse("2006-01-02", date) if err != nil { return "", fmt.Errorf("无效报告日期 %q,期望格式 YYYY-MM-DD", date) } return parsed.Format("2006-01-02"), nil } func resolveReportRunContext(reportDate string, now time.Time, envRunKind, envTriggerSource, envOfficialDaily, envRuntimeAudit string) ReportRunContext { runKind := strings.TrimSpace(envRunKind) if runKind == "" { runKind = "manual" } triggerSource := strings.TrimSpace(envTriggerSource) if triggerSource == "" { triggerSource = "cli" } isOfficialDaily := strings.EqualFold(strings.TrimSpace(envOfficialDaily), "true") if strings.TrimSpace(envOfficialDaily) == "" && reportDate == now.Format("2006-01-02") && runKind == "scheduled" { isOfficialDaily = true } return ReportRunContext{ RunKind: runKind, TriggerSource: triggerSource, IsOfficialDaily: isOfficialDaily, RuntimeAudit: strings.TrimSpace(envRuntimeAudit), } } func resolveSignatureAuditReportConfig() SignatureAuditReportConfig { return SignatureAuditReportConfig{ Window: positiveEnvIntOrDefault("REPORT_SIGNATURE_AUDIT_WINDOW", 5), ChangedRunsThreshold: positiveEnvIntOrDefault("REPORT_SIGNATURE_AUDIT_CHANGED_THRESHOLD", 1), } } func positiveEnvIntOrDefault(key string, fallback int) int { raw := strings.TrimSpace(os.Getenv(key)) if raw == "" { return fallback } value, err := strconv.Atoi(raw) if err != nil || value <= 0 { return fallback } return value } func composeTrackedSummary(summary string, runContext ReportRunContext) string { runtimeAudit := strings.TrimSpace(runContext.RuntimeAudit) summary = strings.TrimSpace(summary) if runtimeAudit == "" { return summary } if summary == "" { return runtimeAudit } return runtimeAudit + "\n" + summary } // ============ 数据模型 ============ 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 ModelEvents []ModelEvent SignatureAuditSummaries []SignatureAuditSourceSummary SignatureAuditRows []SignatureAuditReportRow SignatureAuditConfig SignatureAuditReportConfig } type DailySignals struct { NewModels int PriceChanges int OfficialFree int AggregatorFree int UnknownFree int } type SignatureAuditSourceSummary struct { SourceKey string SourceLabel string RunsInWindow int ChangedRuns int LatestCheckedAt string LatestStatus string LatestStructureState string } type SignatureAuditReportRow struct { SourceKey string SourceLabel string RecentRank int CheckedAt string StructureState string StructureChanged bool Status string DriftDetected bool BaselineInitialized bool StructureSHA256 string PreviousStructureSHA256 string SnapshotPath string SignaturePath string ErrorMessage string } type SignatureAuditReportConfig struct { Window int ChangedRunsThreshold 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 Audience string Baseline string TrustLabel string SourceKindLabel string PrimarySource string UpdatedAt string EvidenceDetail string Tone string } type ModelEvent struct { EventType string ModelName string ProviderName string OperatorName string Audience string TrustLabel string SourceKindLabel string PrimarySource string UpdatedAt string EvidenceDetail string Baseline string Summary string Currency string OldInputPrice float64 NewInputPrice float64 OldOutputPrice float64 NewOutputPrice float64 PriceChangePct float64 Priority int } type PromoCampaignDefinition struct { Date string `json:"date"` ModelName string `json:"model_name"` ProviderName string `json:"provider_name"` OperatorName string `json:"operator_name"` Summary string `json:"summary"` Audience string `json:"audience"` Baseline string `json:"baseline"` TrustLabel string `json:"trust_label"` SourceKindLabel string `json:"source_kind_label"` PrimarySource string `json:"primary_source"` EvidenceDetail string `json:"evidence_detail"` Priority int `json:"priority"` } 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 { ProviderName string ProviderCN string OperatorName string OperatorCN string PlanName string PlanFamily string Tier string BillingCycle string Currency string ListPrice float64 PriceUnit string QuotaValue int64 QuotaUnit string ContextWindow int ModelCount int ModelPreview string SourceURL string EffectiveDate string Notes string } // ============ 数据查询(新Schema) ============ func generateReportDataV3(db *sql.DB, date string) (*ReportV3, error) { signatureAuditCfg := resolveSignatureAuditReportConfig() // 查询模型+厂商+定价+运营商信息 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, }, SignatureAuditConfig: signatureAuditCfg, } if signals, events, ok, err := loadMaterializedDailySignalSnapshot(db, date); err != nil { logger.Warn("加载物化关键信号失败", "error", err) } else if ok { report.DailySignals = signals report.ModelEvents = events } if report.DailySignals == (DailySignals{}) { if signals, err := loadDailySignals(db, date); err != nil { logger.Warn("加载日报变化信号失败", "error", err) } else { report.DailySignals = signals } } if len(report.ModelEvents) == 0 { if events, err := loadModelEvents(db, date); err != nil { logger.Warn("加载模型级事件失败", "error", err) } else { report.ModelEvents = events } } if summaries, rows, ok, err := loadSignatureAuditSection(db, signatureAuditCfg.Window); err != nil { logger.Warn("加载结构签名稳定性摘要失败", "error", err) } else if ok { report.SignatureAuditSummaries = summaries report.SignatureAuditRows = rows } decorateReportV1(report) return report, nil } func loadMaterializedDailySignalSnapshot(db *sql.DB, date string) (DailySignals, []ModelEvent, bool, error) { var ( signals DailySignals rawTopEvents string ) err := db.QueryRow(` SELECT new_models, price_changes, official_free, aggregator_free, unknown_free, COALESCE(top_events::text, '[]') FROM daily_signal_snapshot WHERE signal_date = $1::date AND status = 'generated' `, date).Scan( &signals.NewModels, &signals.PriceChanges, &signals.OfficialFree, &signals.AggregatorFree, &signals.UnknownFree, &rawTopEvents, ) if err == sql.ErrNoRows { return DailySignals{}, nil, false, nil } if err != nil { if strings.Contains(err.Error(), `relation "daily_signal_snapshot" does not exist`) { return DailySignals{}, nil, false, nil } return DailySignals{}, nil, false, err } var events []ModelEvent if err := json.Unmarshal([]byte(rawTopEvents), &events); err != nil { return DailySignals{}, nil, false, fmt.Errorf("unmarshal materialized top_events: %w", err) } return signals, events, true, nil } func loadSignatureAuditSection(db *sql.DB, limitPerSource int) ([]SignatureAuditSourceSummary, []SignatureAuditReportRow, bool, error) { summaries, rows, err := queryOfficialImportSignatureAuditWindow(db, limitPerSource, "", false) if err != nil { if strings.Contains(err.Error(), `relation "official_import_signature_audit_recent_view" does not exist`) || strings.Contains(err.Error(), `relation "official_import_signature_audit" does not exist`) { return nil, nil, false, nil } return nil, nil, false, err } if len(summaries) == 0 { return nil, nil, false, nil } reportSummaries := make([]SignatureAuditSourceSummary, 0, len(summaries)) for _, summary := range summaries { reportSummaries = append(reportSummaries, SignatureAuditSourceSummary{ SourceKey: summary.SourceKey, SourceLabel: signatureAuditSourceLabel(summary.SourceKey), RunsInWindow: summary.RunsInWindow, ChangedRuns: summary.ChangedRuns, LatestCheckedAt: summary.LatestCheckedAt.Format("2006-01-02 15:04:05"), LatestStatus: summary.LatestStatus, LatestStructureState: summary.LatestStructureState, }) } reportRows := make([]SignatureAuditReportRow, 0, len(rows)) for _, row := range rows { reportRows = append(reportRows, SignatureAuditReportRow{ SourceKey: row.SourceKey, SourceLabel: signatureAuditSourceLabel(row.SourceKey), RecentRank: row.RecentRank, CheckedAt: row.CheckedAt.Format("2006-01-02 15:04:05"), StructureState: row.StructureState, StructureChanged: row.StructureChanged, Status: row.Status, DriftDetected: row.DriftDetected, BaselineInitialized: row.BaselineInitialized, StructureSHA256: row.StructureSHA256, PreviousStructureSHA256: nullStringOrNone(row.PreviousObservedSHA256), SnapshotPath: nullStringOrNone(row.SnapshotPath), SignaturePath: nullStringOrNone(row.SignaturePath), ErrorMessage: nullStringOrNone(row.ErrorMessage), }) } return reportSummaries, reportRows, true, nil } func loadTencentSubscriptionPlans(db *sql.DB) ([]SubscriptionPlanInfo, error) { rows, err := db.Query(` SELECT COALESCE(mp.name, 'unknown') AS provider_name, COALESCE(mp.name_cn, mp.name, 'unknown') AS provider_name_cn, COALESCE(o.name, 'unknown') AS operator_name, COALESCE(o.name_cn, o.name, 'unknown') AS operator_name_cn, sp.plan_name, sp.plan_family, sp.tier, COALESCE(sp.billing_cycle, ''), sp.currency, sp.list_price, COALESCE(sp.price_unit, ''), COALESCE(sp.quota_value, 0), COALESCE(sp.quota_unit, ''), COALESCE(sp.context_window, 0), COALESCE(sp.model_scope, '[]'), COALESCE(sp.source_url, ''), COALESCE(TO_CHAR(sp.effective_date, 'YYYY-MM-DD'), ''), COALESCE(sp.notes, '') FROM subscription_plan sp JOIN model_provider mp ON mp.id = sp.provider_id LEFT JOIN operator o ON o.id = sp.operator_id ORDER BY COALESCE(o.name_cn, o.name, 'unknown') ASC, sp.plan_family ASC, 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.ProviderName, &plan.ProviderCN, &plan.OperatorName, &plan.OperatorCN, &plan.PlanName, &plan.PlanFamily, &plan.Tier, &plan.BillingCycle, &plan.Currency, &plan.ListPrice, &plan.PriceUnit, &plan.QuotaValue, &plan.QuotaUnit, &plan.ContextWindow, &modelScopeRaw, &plan.SourceURL, &plan.EffectiveDate, &plan.Notes, ); 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, priceUnit string) string { unit := strings.ToLower(strings.TrimSpace(priceUnit)) switch { case currency == "CNY" && unit == "cny/pack": return fmt.Sprintf("¥%.2f/包", price) case currency == "CNY": return fmt.Sprintf("¥%.2f/月", price) case currency == "USD" && unit == "usd/pack": return fmt.Sprintf("$%.2f/pack", price) case currency == "USD": return fmt.Sprintf("$%.2f/month", price) default: if strings.TrimSpace(priceUnit) != "" { return fmt.Sprintf("%.2f %s", price, priceUnit) } return fmt.Sprintf("%.2f %s", price, currency) } } func formatPlanFamily(planFamily string) string { switch strings.ToLower(strings.TrimSpace(planFamily)) { case "token_plan": return "Token Plan" case "coding_plan": return "Coding Plan" case "package_plan": return "套餐包" default: if strings.TrimSpace(planFamily) == "" { return "-" } return planFamily } } func formatBillingCycle(cycle string) string { switch strings.ToLower(strings.TrimSpace(cycle)) { case "monthly": return "包月" case "quarterly": return "3个月" case "": return "-" default: return cycle } } func formatPlanOperator(plan SubscriptionPlanInfo) string { if strings.TrimSpace(plan.OperatorCN) != "" && strings.TrimSpace(plan.OperatorCN) != "unknown" { return plan.OperatorCN } if strings.TrimSpace(plan.OperatorName) != "" && strings.TrimSpace(plan.OperatorName) != "unknown" { return plan.OperatorName } if strings.TrimSpace(plan.ProviderCN) != "" && strings.TrimSpace(plan.ProviderCN) != "unknown" { return plan.ProviderCN } if strings.TrimSpace(plan.ProviderName) != "" { return plan.ProviderName } return "-" } func formatPlanNotes(notes string) string { notes = strings.TrimSpace(notes) if notes == "" { return "-" } return notes } 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 loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { var events []ModelEvent newModelEvents, err := loadNewModelEvents(db, date) if err != nil { return nil, err } events = append(events, newModelEvents...) releaseEvents, err := loadOfficialReleaseEvents(db, date) if err != nil { return nil, err } events = append(events, releaseEvents...) promoEvents, err := loadPromoCampaignEvents(date) if err != nil { return nil, err } events = append(events, promoEvents...) priceEvents, err := loadPriceChangeEvents(db, date) if err != nil { return nil, err } events = append(events, priceEvents...) sort.Slice(events, func(i, j int) bool { if events[i].Priority != events[j].Priority { return events[i].Priority > events[j].Priority } return events[i].ModelName < events[j].ModelName }) return dedupeModelEvents(events), nil } func loadPromoCampaignEvents(date string) ([]ModelEvent, error) { definitions, err := loadPromoCampaignDefinitions() if err != nil { return nil, err } var events []ModelEvent for _, definition := range definitions { if definition.Date != date { continue } events = append(events, ModelEvent{ EventType: "promo_campaign", ModelName: definition.ModelName, ProviderName: definition.ProviderName, OperatorName: definition.OperatorName, Audience: firstNonEmpty(definition.Audience, "适合计划利用活动窗口压低成本的团队"), TrustLabel: firstNonEmpty(definition.TrustLabel, "官方来源 / 一级证据"), SourceKindLabel: firstNonEmpty(definition.SourceKindLabel, "官方活动页"), PrimarySource: definition.PrimarySource, UpdatedAt: formatEventUpdatedAt("", definition.Date), EvidenceDetail: definition.EvidenceDetail, Baseline: firstNonEmpty(definition.Baseline, "活动窗口开启"), Summary: definition.Summary, Priority: maxInt(definition.Priority, 115), }) } return events, nil } func loadPromoCampaignDefinitions() ([]PromoCampaignDefinition, error) { path, err := resolvePromoCampaignDataPath() if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } body, err := os.ReadFile(path) if err != nil { return nil, err } var definitions []PromoCampaignDefinition if err := json.Unmarshal(body, &definitions); err != nil { return nil, err } return definitions, nil } func resolvePromoCampaignDataPath() (string, error) { candidates := []string{ filepath.Join("scripts", "testdata", "report_promo_campaigns.json"), filepath.Join("testdata", "report_promo_campaigns.json"), } for _, candidate := range candidates { if _, err := os.Stat(candidate); err == nil { return candidate, nil } } return "", os.ErrNotExist } func loadOfficialReleaseEvents(db *sql.DB, date string) ([]ModelEvent, error) { rows, err := db.Query(` WITH latest_prices AS ( SELECT rp.model_id, COALESCE(o.name, 'Unknown') AS operator_name, COALESCE(o.type, 'reseller') AS operator_type, rp.currency, 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 COALESCE(NULLIF(m.name, ''), m.external_id) AS model_name, COALESCE(mp.name, split_part(m.external_id, '/', 1)) AS provider_name, COALESCE(lp.operator_name, 'Unknown') AS operator_name, COALESCE(lp.operator_type, 'reseller') AS operator_type, COALESCE(m.source_url, '') AS source_url, COALESCE(m.date_confidence, 'unknown') AS date_confidence, COALESCE(m.date_source_kind, 'unknown') AS date_source_kind, COALESCE(mp.country, 'unknown') AS provider_country, COALESCE(m.release_date, m.created_at::date) AS release_date, COALESCE(lp.currency, 'USD') AS currency 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 AND m.release_date = $1::date AND COALESCE(m.source_url, '') <> '' AND COALESCE(lp.operator_type, 'reseller') IN ('official', 'cloud') ORDER BY m.release_date DESC, m.id DESC LIMIT 8 `, date) if err != nil { return nil, err } defer rows.Close() var events []ModelEvent for rows.Next() { var ( modelName string providerName string operatorName string operatorType string sourceURL string dateConfidence string dateSourceKind string providerCountry string releaseDate time.Time currency string ) if err := rows.Scan( &modelName, &providerName, &operatorName, &operatorType, &sourceURL, &dateConfidence, &dateSourceKind, &providerCountry, &releaseDate, ¤cy, ); err != nil { return nil, err } model := ModelInfo{ Name: modelName, ProviderName: providerName, ProviderCountry: providerCountry, Currency: currency, OperatorName: operatorName, OperatorType: operatorType, } events = append(events, ModelEvent{ EventType: "official_release", ModelName: modelName, ProviderName: providerName, OperatorName: operatorName, Audience: "适合需要复查默认选型与路线图判断的团队", TrustLabel: buildReleaseTrustLabel(model, dateConfidence), SourceKindLabel: buildReleaseSourceKindLabel(dateSourceKind, dateConfidence), PrimarySource: sourceURL, UpdatedAt: releaseDate.Format("2006-01-02 15:04"), EvidenceDetail: buildReleaseEvidenceDetail(dateSourceKind, dateConfidence), Baseline: "官方首次发布", Summary: fmt.Sprintf("%s 官方发布新模型,值得优先复查默认选型。", providerName), Currency: currency, Priority: 120, }) } return events, rows.Err() } func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { rows, err := db.Query(` WITH latest_prices AS ( SELECT rp.model_id, COALESCE(o.name, 'Unknown') AS operator_name, COALESCE(o.type, 'reseller') AS operator_type, rp.currency, rp.input_price_per_mtok, rp.output_price_per_mtok, rp.is_free, 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 COALESCE(NULLIF(m.name, ''), m.external_id) AS model_name, COALESCE(mp.name, split_part(m.external_id, '/', 1)) AS provider_name, COALESCE(lp.operator_name, 'OpenRouter') AS operator_name, COALESCE(lp.operator_type, 'reseller') AS operator_type, COALESCE(lp.currency, 'USD') AS currency, COALESCE(lp.input_price_per_mtok, 0) AS input_price, COALESCE(lp.output_price_per_mtok, 0) AS output_price, COALESCE(lp.is_free, false) AS is_free, COALESCE(m.context_length, 0) AS context_length, COALESCE(mp.country, 'unknown') AS provider_country, m.created_at 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 AND DATE(m.created_at) = $1::date ORDER BY m.created_at DESC, m.id DESC LIMIT 8 `, date) if err != nil { return nil, err } defer rows.Close() var events []ModelEvent for rows.Next() { var ( modelName string providerName string operatorName string operatorType string currency string inputPrice float64 outputPrice float64 isFree bool contextLength int providerCountry string createdAt time.Time ) if err := rows.Scan( &modelName, &providerName, &operatorName, &operatorType, ¤cy, &inputPrice, &outputPrice, &isFree, &contextLength, &providerCountry, &createdAt, ); err != nil { return nil, err } model := ModelInfo{ Name: modelName, ProviderName: providerName, ProviderCountry: providerCountry, ContextLength: contextLength, InputPrice: inputPrice, OutputPrice: outputPrice, Currency: currency, IsFree: isFree, OperatorName: operatorName, OperatorType: operatorType, } summary := "新模型进入情报池,值得重新评估当前默认选择。" if isFree { summary = fmt.Sprintf("新模型首日可免费试用,需注意其免费来源属于%s。", classifyFreeSource(model)) } else if contextLength >= 1024*256 { summary = fmt.Sprintf("新模型带来 %s 长上下文,值得复查 Agent 和代码场景。", formatContextWindowCompact(contextLength)) } events = append(events, ModelEvent{ EventType: "new_model", ModelName: modelName, ProviderName: providerName, OperatorName: operatorName, Audience: "适合想尽快验证新模型价值的选型读者", TrustLabel: buildTrustLabel(model), SourceKindLabel: "模型快照", PrimarySource: buildPrimarySource("region_pricing", operatorName), UpdatedAt: createdAt.Format("2006-01-02 15:04"), EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照", Baseline: "首次出现", Summary: summary, Currency: currency, NewInputPrice: inputPrice, NewOutputPrice: outputPrice, Priority: 85 + minInt(contextLength/(1024*128), 10), }) } return events, rows.Err() } func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) { rows, err := db.Query(` WITH latest_prices AS ( SELECT rp.model_id, COALESCE(o.name, 'Unknown') AS operator_name, 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 COALESCE(NULLIF(m.name, ''), m.external_id) AS model_name, COALESCE(mp.name, split_part(m.external_id, '/', 1)) AS provider_name, COALESCE(lp.operator_name, 'OpenRouter') AS operator_name, COALESCE(lp.operator_type, 'reseller') AS operator_type, ph.currency, COALESCE(ph.old_input_price, 0), COALESCE(ph.new_input_price, 0), COALESCE(ph.old_output_price, 0), COALESCE(ph.new_output_price, 0), COALESCE(mp.country, 'unknown') AS provider_country, ph.changed_at FROM pricing_history ph JOIN models m ON ph.model_id = m.id 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 DATE(ph.changed_at) = $1::date ORDER BY ph.changed_at DESC, ph.id DESC LIMIT 16 `, date) if err != nil { return nil, err } defer rows.Close() var events []ModelEvent for rows.Next() { var ( modelName string providerName string operatorName string operatorType string currency string oldInputPrice float64 newInputPrice float64 oldOutputPrice float64 newOutputPrice float64 providerCountry string changedAt time.Time ) if err := rows.Scan( &modelName, &providerName, &operatorName, &operatorType, ¤cy, &oldInputPrice, &newInputPrice, &oldOutputPrice, &newOutputPrice, &providerCountry, &changedAt, ); err != nil { return nil, err } changePct := signedPriceChangePct(oldInputPrice, newInputPrice, oldOutputPrice, newOutputPrice) if changePct == 0 { continue } model := ModelInfo{ Name: modelName, ProviderName: providerName, ProviderCountry: providerCountry, Currency: currency, OperatorName: operatorName, OperatorType: operatorType, } eventType := "price_increase" summary := "价格上调已足以影响默认成本,需要确认备用模型。" if changePct < 0 { eventType = "price_cut" summary = "价格下降已足以影响默认选型,值得重新评估同类模型。" } events = append(events, ModelEvent{ EventType: eventType, ModelName: modelName, ProviderName: providerName, OperatorName: operatorName, Audience: buildPriceEventAudience(changePct), TrustLabel: buildTrustLabel(model), SourceKindLabel: "价格快照", PrimarySource: "pricing_history", UpdatedAt: changedAt.Format("2006-01-02 15:04"), EvidenceDetail: buildPriceEvidenceDetail(changePct, oldInputPrice, newInputPrice, currency), Baseline: fmt.Sprintf("较昨日 %+.0f%%", changePct), Summary: summary, Currency: currency, OldInputPrice: oldInputPrice, NewInputPrice: newInputPrice, OldOutputPrice: oldOutputPrice, NewOutputPrice: newOutputPrice, PriceChangePct: changePct, Priority: 70 + minInt(int(abs(changePct)), 25), }) } return events, rows.Err() } func dedupeModelEvents(events []ModelEvent) []ModelEvent { seen := make(map[string]struct{}) result := make([]ModelEvent, 0, len(events)) for _, event := range events { key := event.EventType + "|" + event.ModelName if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} result = append(result, event) } return result } func signedPriceChangePct(oldInput, newInput, oldOutput, newOutput float64) float64 { inputPct := signedChange(oldInput, newInput) outputPct := signedChange(oldOutput, newOutput) if abs(inputPct) >= abs(outputPct) { return inputPct } return outputPct } func signedChange(oldValue, newValue float64) float64 { if oldValue == 0 { if newValue == 0 { return 0 } return 100 } return ((newValue - oldValue) / oldValue) * 100 } func minInt(a, b int) int { if a < b { return a } return b } func maxInt(a, b int) int { if a > b { return a } return b } func abs(v float64) float64 { if v < 0 { return -v } return v } func buildPriceEventAudience(changePct float64) string { if changePct < 0 { return "适合以成本为先、准备趁降价重排默认选型的团队" } return "适合需要提前准备替代模型和预算回退方案的团队" } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" } 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.ModelEvents = enrichModelEvents(r) r.PageMode = buildPageModeWithEvents(r.DailySignals, r.ModelEvents) 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 enrichModelEvents(r *ReportV3) []ModelEvent { events := append([]ModelEvent{}, r.ModelEvents...) existing := make(map[string]struct{}, len(events)) for _, event := range events { existing[event.EventType+"|"+event.ModelName] = struct{}{} } addFreeHighlight := func(model ModelInfo, priority int) { key := "free_highlight|" + model.Name if _, exists := existing[key]; exists { return } existing[key] = struct{}{} events = append(events, ModelEvent{ EventType: "free_highlight", ModelName: model.Name, ProviderName: model.ProviderName, OperatorName: model.OperatorName, Audience: "适合先试后买、但需要先判断免费来源的读者", TrustLabel: buildTrustLabel(model), SourceKindLabel: "免费策略快照", PrimarySource: buildPrimarySource("free_snapshot", model.OperatorName), UpdatedAt: formatEventUpdatedAt(r.GeneratedAt, r.Date), EvidenceDetail: buildFreeEvidenceDetail(model), Baseline: "今日快照", Summary: buildModelEvidence(model), Currency: model.Currency, Priority: priority, }) } for _, model := range r.FreeModels { if classifyFreeSource(model) == "官方免费" { addFreeHighlight(model, 72) break } } for _, model := range r.FreeModels { if classifyFreeSource(model) == "聚合免费" { addFreeHighlight(model, 68) break } } sort.Slice(events, func(i, j int) bool { if events[i].Priority != events[j].Priority { return events[i].Priority > events[j].Priority } return events[i].ModelName < events[j].ModelName }) return events } 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 { return buildPageModeWithEvents(signals, nil) } func buildPageModeWithEvents(signals DailySignals, events []ModelEvent) string { if hasEventType(events, "official_release") || hasEventType(events, "promo_campaign") { return "hot" } 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 buildPageModeWithEvents(r.DailySignals, r.ModelEvents) { case "hot": labels = append(labels, "热点日") case "calm": labels = append(labels, "平静日") default: labels = append(labels, "常规日") } if hasEventType(r.ModelEvents, "official_release") { labels = append(labels, "官方发布") } if hasEventType(r.ModelEvents, "promo_campaign") { 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 hasEventType(events []ModelEvent, eventType string) bool { for _, event := range events { if event.EventType == eventType { return true } } return false } func buildHeroSummary(r *ReportV3) (string, string) { if official := firstEventByType(r.ModelEvents, "official_release"); official != nil { return fmt.Sprintf("今天最值得关注的是 %s 已出现官方发布信号,优先复查它对默认选型的影响。", official.ModelName), fmt.Sprintf("主来源:%s", official.PrimarySource) } if promo := firstEventByType(r.ModelEvents, "promo_campaign"); promo != nil { return fmt.Sprintf("今天最值得关注的是 %s 已进入活动窗口,优先判断这次活动是否值得改变默认成本策略。", promo.ModelName), fmt.Sprintf("主来源:%s", promo.PrimarySource) } if summary, changedCount := topChangedSignatureAuditSummary(r.SignatureAuditSummaries, effectiveSignatureAuditReportConfig(r).ChangedRunsThreshold); summary != nil { return fmt.Sprintf("今天最值得关注的是 %s 的价格页结构开始抖动,优先复查抓取和解析结果是否仍然可信。", summary.SourceLabel), fmt.Sprintf("最近 %d 次中出现 %d 次结构变化;当前有 %d 个平台处于变化窗口。", summary.RunsInWindow, summary.ChangedRuns, changedCount) } 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 firstEventByType(events []ModelEvent, eventType string) *ModelEvent { for i := range events { if events[i].EventType == eventType { return &events[i] } } return nil } func buildHeadlineItems(r *ReportV3) []HeadlineItem { var items []HeadlineItem if auditItem, ok := buildSignatureAuditHeadlineItem(r.SignatureAuditSummaries, effectiveSignatureAuditReportConfig(r).ChangedRunsThreshold); ok { items = append(items, auditItem) } if eventItems := buildHeadlineItemsFromEvents(r.ModelEvents); len(eventItems) > 0 { items = append(items, eventItems...) if len(items) > 4 { return items[:4] } return items } if r.DailySignals.NewModels > 0 { items = append(items, HeadlineItem{ Label: "新模型", Title: fmt.Sprintf("今日新增 %d 个模型进入情报池", r.DailySignals.NewModels), Summary: "建议优先复查今天的新上架模型是否改变当前低成本或中文场景的最优解。", Audience: "适合想快速筛出新增机会的读者", Baseline: "首次出现", TrustLabel: "数据库追踪", Tone: "info", }) } if r.DailySignals.PriceChanges > 0 { items = append(items, HeadlineItem{ Label: "价格变化", Title: fmt.Sprintf("今日检测到 %d 次价格变化", r.DailySignals.PriceChanges), Summary: "价格变化已足以影响低成本选型,建议重新确认默认模型与备用模型。", Audience: "适合以成本为先、需要重排默认选型的团队", 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), Audience: "适合想先试用、但不想误把聚合免费当官方免费的读者", Baseline: "今日快照", TrustLabel: "来源已分层", Tone: "warning", }) } if len(items) == 0 { items = append(items, HeadlineItem{ Label: "观察重点", Title: "今日无重大上新或显著调价", Summary: "平静日优先关注稳定商用与长期成本,避免被噪音列表打断判断。", Audience: "适合更重视稳定性和长期成本的团队", Baseline: "较昨日", TrustLabel: "日报编辑规则", Tone: "neutral", }) } if len(items) > 3 { return items[:3] } return items } func buildHeadlineItemsFromEvents(events []ModelEvent) []HeadlineItem { if len(events) == 0 { return nil } limit := 3 if hasEventType(events, "official_release") || hasEventType(events, "promo_campaign") { limit = 4 } sort.Slice(events, func(i, j int) bool { if events[i].Priority != events[j].Priority { return events[i].Priority > events[j].Priority } return events[i].ModelName < events[j].ModelName }) var items []HeadlineItem usedModels := make(map[string]struct{}) for _, event := range dedupeModelEvents(events) { if _, exists := usedModels[event.ModelName]; exists { continue } usedModels[event.ModelName] = struct{}{} items = append(items, headlineItemFromModelEvent(event)) if len(items) >= limit { break } } return items } func headlineItemFromModelEvent(event ModelEvent) HeadlineItem { item := HeadlineItem{ Title: event.ModelName, Summary: event.Summary, Audience: event.Audience, Baseline: event.Baseline, TrustLabel: event.TrustLabel, SourceKindLabel: event.SourceKindLabel, PrimarySource: event.PrimarySource, UpdatedAt: event.UpdatedAt, EvidenceDetail: event.EvidenceDetail, Tone: "neutral", } switch event.EventType { case "official_release": item.Label = "一级官方发布" item.Title = fmt.Sprintf("%s 官方发布", event.ModelName) item.Tone = "official-primary" if event.SourceKindLabel == "二级权威佐证发布" { item.Label = "二级权威佐证" item.Title = fmt.Sprintf("%s 进入权威佐证发布时间线", event.ModelName) item.Tone = "secondary-evidence" } case "new_model": item.Label = "新模型" item.Title = fmt.Sprintf("%s 进入今日情报池", event.ModelName) item.Tone = "info" case "price_cut": item.Label = "价格下调" item.Title = fmt.Sprintf("%s 成本下调 %.0f%%", event.ModelName, abs(event.PriceChangePct)) item.Tone = "success" case "price_increase": item.Label = "价格上调" item.Title = fmt.Sprintf("%s 成本上调 %.0f%%", event.ModelName, abs(event.PriceChangePct)) item.Tone = "caution" case "promo_campaign": item.Label = "营销活动" item.Title = fmt.Sprintf("%s 进入活动窗口", event.ModelName) item.Tone = "promo" case "free_highlight": item.Label = "免费机会" item.Title = fmt.Sprintf("%s 当前可免费试用", event.ModelName) item.Tone = "warning" default: item.Label = "观察重点" item.Title = fmt.Sprintf("%s 值得关注", event.ModelName) } return item } func buildPrimarySource(sourceKind, operatorName string) string { switch sourceKind { case "region_pricing": if operatorName == "" { return "region_pricing" } return operatorName + " / region_pricing" case "free_snapshot": if operatorName == "" { return "free snapshot" } return operatorName + " / free snapshot" default: return sourceKind } } func buildPriceEvidenceDetail(changePct, oldPrice, newPrice float64, currency string) string { direction := "上涨" if changePct < 0 { direction = "下降" } return fmt.Sprintf( "pricing_history 记录到输入价格由 %s 调整为 %s,较昨日%s %.0f%%", formatPrice(oldPrice, currency), formatPrice(newPrice, currency), direction, abs(changePct), ) } func buildReleaseSourceKindLabel(dateSourceKind, dateConfidence string) string { switch { case dateSourceKind == "secondary_authoritative_report" || dateConfidence == "secondary_authoritative": return "二级权威佐证发布" case dateSourceKind == "official_announcement" && dateConfidence == "official_primary": return "一级官方发布" case dateSourceKind == "official_product_page": return "官方产品页" case dateSourceKind == "catalog_backfill": return "目录回填" default: return "一级官方发布" } } func buildReleaseEvidenceDetail(dateSourceKind, dateConfidence string) string { switch { case dateSourceKind == "secondary_authoritative_report" || dateConfidence == "secondary_authoritative": return "models.release_date = 今日,发布日期采用次级权威报道佐证,模型来源页保留官方文档" case dateSourceKind == "official_announcement" && dateConfidence == "official_primary": return "models.release_date = 今日,且 source_url 指向官方发布页" case dateSourceKind == "official_product_page": return "models.release_date = 今日,来源页为官方产品页,发布日期置信度待确认" case dateSourceKind == "catalog_backfill": return "models.release_date = 今日,发布日期来自目录级元数据回填" default: return "models.release_date = 今日,且已记录发布日期证据元数据" } } func buildReleaseTrustLabel(model ModelInfo, dateConfidence string) string { base := buildTrustLabel(model) switch dateConfidence { case "official_primary": return base + " / 一级证据" case "secondary_authoritative": return base + " / 二级佐证" default: return base } } func buildFreeEvidenceDetail(model ModelInfo) string { switch classifyFreeSource(model) { case "官方免费": return fmt.Sprintf("%s 当前快照显示为官方免费入口", model.OperatorName) case "聚合免费": return fmt.Sprintf("%s 当前快照显示为聚合免费入口", model.OperatorName) default: return fmt.Sprintf("%s 当前快照显示免费,但来源仍待确认", model.OperatorName) } } func formatEventUpdatedAt(value, fallbackDate string) string { if strings.TrimSpace(value) != "" { return value } if fallbackDate != "" { return fallbackDate + " 00:00" } return "-" } func buildActionItems(r *ReportV3) []ActionItem { var actions []ActionItem if action, ok := buildSignatureAuditActionItem(r.SignatureAuditSummaries, effectiveSignatureAuditReportConfig(r).ChangedRunsThreshold); ok { actions = append(actions, action) } 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 topChangedSignatureAuditSummary(summaries []SignatureAuditSourceSummary, changedRunsThreshold int) (*SignatureAuditSourceSummary, int) { var selected *SignatureAuditSourceSummary changedCount := 0 for i := range summaries { summary := &summaries[i] if summary.ChangedRuns < changedRunsThreshold { continue } changedCount++ if selected == nil { selected = summary continue } if summary.ChangedRuns > selected.ChangedRuns { selected = summary continue } if summary.ChangedRuns == selected.ChangedRuns && summary.SourceLabel < selected.SourceLabel { selected = summary } } return selected, changedCount } func buildSignatureAuditHeadlineItem(summaries []SignatureAuditSourceSummary, changedRunsThreshold int) (HeadlineItem, bool) { summary, changedCount := topChangedSignatureAuditSummary(summaries, changedRunsThreshold) if summary == nil { return HeadlineItem{}, false } item := HeadlineItem{ Label: "结构波动", Title: fmt.Sprintf("%s 结构签名开始抖动", summary.SourceLabel), Summary: fmt.Sprintf("最近 %d 次中出现 %d 次结构变化,当前共有 %d 个平台进入变化窗口。", summary.RunsInWindow, summary.ChangedRuns, changedCount), Audience: "适合维护官方价格 importer、需要优先确认抓取与解析可信度的团队", Baseline: "近期结构签名窗口", TrustLabel: "结构签名巡检", SourceKindLabel: "官方价格页结构签名", PrimarySource: "official_import_signature_audit_recent_view", UpdatedAt: summary.LatestCheckedAt, EvidenceDetail: fmt.Sprintf("最新状态=%s,最新结构状态=%s", summary.LatestStatus, summary.LatestStructureState), Tone: "caution", } return item, true } func buildSignatureAuditActionItem(summaries []SignatureAuditSourceSummary, changedRunsThreshold int) (ActionItem, bool) { summary, changedCount := topChangedSignatureAuditSummary(summaries, changedRunsThreshold) if summary == nil { return ActionItem{}, false } return ActionItem{ Title: fmt.Sprintf("优先复查 %s 价格 importer", summary.SourceLabel), Audience: "适合负责官方价格采集、需要先确认页面结构是否漂移的维护者", Evidence: fmt.Sprintf("最近 %d 次中出现 %d 次结构变化;当前共有 %d 个平台进入变化窗口。", summary.RunsInWindow, summary.ChangedRuns, changedCount), Tags: []string{"结构稳定性", "官方价格页", summary.SourceLabel}, }, true } func effectiveSignatureAuditReportConfig(r *ReportV3) SignatureAuditReportConfig { cfg := SignatureAuditReportConfig{ Window: 5, ChangedRunsThreshold: 1, } if r == nil { return cfg } if r.SignatureAuditConfig.Window > 0 { cfg.Window = r.SignatureAuditConfig.Window } if r.SignatureAuditConfig.ChangedRunsThreshold > 0 { cfg.ChangedRunsThreshold = r.SignatureAuditConfig.ChangedRunsThreshold } return cfg } 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 signatureAuditSourceLabel(sourceKey string) string { switch strings.TrimSpace(sourceKey) { case "vertex_pricing_signature": return "Google Cloud Vertex AI" case "cloudflare_pricing_signature": return "Cloudflare Workers AI" case "perplexity_pricing_signature": return "Perplexity API" default: if strings.TrimSpace(sourceKey) == "" { return "未知平台" } return sourceKey } } func buildSignatureAuditSectionLead(r *ReportV3) string { if len(r.SignatureAuditSummaries) == 0 { return "" } cfg := effectiveSignatureAuditReportConfig(r) changedSources := make([]string, 0) for _, summary := range r.SignatureAuditSummaries { if summary.ChangedRuns >= cfg.ChangedRunsThreshold { changedSources = append(changedSources, summary.SourceLabel) } } if len(changedSources) == 0 { return fmt.Sprintf("最近窗口内未出现达到阈值的结构变化,当前阈值为 %d 次,官方价格页结构整体稳定。", cfg.ChangedRunsThreshold) } return fmt.Sprintf("最近窗口内有 %d 个平台达到结构变化阈值(%d 次),优先复查 %s。", len(changedSources), cfg.ChangedRunsThreshold, strings.Join(changedSources, " / ")) } func signatureAuditSummaryTone(r *ReportV3, summary SignatureAuditSourceSummary) string { if summary.ChangedRuns >= effectiveSignatureAuditReportConfig(r).ChangedRunsThreshold { return "warning" } return "official" } 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) if item.Audience != "" { fmt.Fprintf(f, "- 影响对象: %s\n", item.Audience) } if item.SourceKindLabel != "" { fmt.Fprintf(f, "- 事件来源: %s\n", item.SourceKindLabel) } if item.PrimarySource != "" { fmt.Fprintf(f, "- 主来源: %s\n", item.PrimarySource) } if item.UpdatedAt != "" { fmt.Fprintf(f, "- 更新时间: %s\n", item.UpdatedAt) } fmt.Fprintf(f, "- 基线: %s\n", item.Baseline) if item.EvidenceDetail != "" { fmt.Fprintf(f, "- 判定依据: %s\n", item.EvidenceDetail) } fmt.Fprintf(f, "- 可信度: %s\n\n", item.TrustLabel) } if len(r.SignatureAuditSummaries) > 0 { fmt.Fprintf(f, "## 结构稳定性\n\n") if lead := buildSignatureAuditSectionLead(r); lead != "" { fmt.Fprintf(f, "> %s\n\n", lead) } fmt.Fprintf(f, "| 平台 | 近期窗口 | 最新状态 | 最新结构状态 | 最近检查 |\n|------|----------|----------|--------------|----------|\n") for _, item := range r.SignatureAuditSummaries { fmt.Fprintf(f, "| %s | 最近 %d 次中出现 %d 次结构变化 | %s | %s | %s |\n", item.SourceLabel, item.RunsInWindow, item.ChangedRuns, item.LatestStatus, item.LatestStructureState, item.LatestCheckedAt) } fmt.Fprintf(f, "\n") if len(r.SignatureAuditRows) > 0 { fmt.Fprintf(f, "### 近期结构记录\n\n") fmt.Fprintf(f, "| 平台 | recent_rank | 检查时间 | 结构状态 | 状态 | 结构签名 |\n|------|-------------|----------|----------|------|----------|\n") for _, item := range r.SignatureAuditRows { fmt.Fprintf(f, "| %s | %d | %s | %s | %s | %s |\n", item.SourceLabel, item.RecentRank, item.CheckedAt, item.StructureState, item.Status, item.StructureSHA256) } fmt.Fprintf(f, "\n") } } 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 | %s | %s | %s | %d 个(%s) |\n", formatPlanOperator(plan), formatPlanFamily(plan.PlanFamily), plan.PlanName, formatBillingCycle(plan.BillingCycle), formatSubscriptionPrice(plan.ListPrice, plan.Currency, plan.PriceUnit), formatSubscriptionQuota(plan.QuotaValue, plan.QuotaUnit), formatPlanNotes(plan.Notes), 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 := `
先给行动,再给证据。每张卡只回答“今天该先看什么”。
只保留真正影响当天判断的变化事件。
{{signatureAuditSectionLead .}}
| 平台 | recent_rank | 检查时间 | 结构状态 | 状态 | 结构签名 |
|---|---|---|---|---|---|
| {{.SourceLabel}} | {{.RecentRank}} | {{.CheckedAt}} | {{.StructureState}} | {{.Status}} | {{.StructureSHA256}} |
免费可用不等于官方长期免费,必须先区分来源。
按场景给出有限候选,优先帮助读者当天做出选择。
长表格后置,适合深度比价时再展开。
| 国际候选 | 厂商 | 输入 | 输出 | 上下文 |
|---|---|---|---|---|
| {{.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"}} |
以下为云平台 / 中转平台套餐订阅价,包含标准月套餐与首购活动套餐,不参与按模型输入/输出单价排行。
| 平台 | 套餐类型 | 套餐 | 周期 | 价格 | 套餐额度 | 活动说明 | 覆盖模型 |
|---|---|---|---|---|---|---|---|
| {{formatPlanOperator .}} | {{formatPlanFamily .PlanFamily}} | {{.PlanName}} | {{formatBillingCycle .BillingCycle}} | {{formatSubscriptionPrice .ListPrice .Currency .PriceUnit}} | {{formatSubscriptionQuota .QuotaValue .QuotaUnit}} | {{formatPlanNotes .Notes}} | {{.ModelCount}} 个{{if .ModelPreview}}({{.ModelPreview}}){{end}}{{if gt .ContextWindow 0}} · {{formatContextWindowCompact .ContextWindow}}{{end}} |