//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, err := resolveReportDate(time.Now(), os.Args[1:], os.Getenv("REPORT_DATE")) if err != nil { return err } // 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 } 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 } // ============ 数据模型 ============ 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 } 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 SourceKindLabel string PrimarySource string UpdatedAt string EvidenceDetail string Tone string } type ModelEvent struct { EventType string ModelName string ProviderName string OperatorName 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 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 } if events, err := loadModelEvents(db, date); err != nil { logger.Warn("加载模型级事件失败", "error", err) } else { report.ModelEvents = events } 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 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...) 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 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, 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, 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, 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 abs(v float64) float64 { if v < 0 { return -v } return v } 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, 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") { 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 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) } 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 { if items := buildHeadlineItemsFromEvents(r.ModelEvents); len(items) > 0 { return items } 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 buildHeadlineItemsFromEvents(events []ModelEvent) []HeadlineItem { if len(events) == 0 { return nil } 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) >= 3 { break } } return items } func headlineItemFromModelEvent(event ModelEvent) HeadlineItem { item := HeadlineItem{ Title: event.ModelName, Summary: event.Summary, 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 "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 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) 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.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 := ` LLM Intelligence Hub - {{.Date}}
AI 模型与价格情报晨报
{{.Date}} · {{.GeneratedAt}}
{{range .MarketLabels}}{{.}}{{end}}
今日一句话结论
{{.HeroSummary}}
{{if .HeroEvidence}}
{{.HeroEvidence}}
{{end}}
模型总数
{{.TotalModels}}
今日新增模型
{{.DailySignals.NewModels}}
今日价格变化
{{.DailySignals.PriceChanges}}
免费样本
{{len .FreeModels}}

三条行动建议

先给行动,再给证据。每张卡只回答“今天该先看什么”。

{{range $i, $item := .ActionItems}}
行动建议
{{$item.Title}}
{{$item.Audience}}
{{range $item.Tags}}{{.}}{{end}}
{{$item.Evidence}}
{{end}}

今日头条

只保留真正影响当天判断的变化事件。

{{range .HeadlineItems}}
{{.Label}}
{{.Title}}
{{.Summary}}
{{if .SourceKindLabel}}
事件来源:{{.SourceKindLabel}}
{{end}}
基线:{{.Baseline}}
可信度:{{.TrustLabel}}
{{if .PrimarySource}}
主来源:{{.PrimarySource}}
{{end}} {{if .UpdatedAt}}
更新时间:{{.UpdatedAt}}
{{end}} {{if .EvidenceDetail}}
判定依据:{{.EvidenceDetail}}
{{end}}
{{end}}

免费来源分层

免费可用不等于官方长期免费,必须先区分来源。

{{range .FreeBreakdown}}
{{.Label}}
{{.Count}}
{{.Description}}
{{end}}

场景推荐

按场景给出有限候选,优先帮助读者当天做出选择。

{{range .SceneSections}}
{{.Title}}
{{.Description}}
{{.Lead.Name}}
{{.Lead.Provider}} · {{.Lead.Operator}} · {{.Lead.Usage}}
{{range .Lead.Tags}}{{.}}{{end}}
{{.Lead.Evidence}}
{{.Lead.PriceSummary}} · {{.Lead.TrustLabel}}
{{if .Others}}
{{range .Others}}
{{.Name}} · {{.Provider}}
{{.PriceSummary}} · {{.Evidence}}
{{end}}
{{end}}
{{end}}

完整数据附录

长表格后置,适合深度比价时再展开。

{{range .AppendixLinks}} {{end}}

完整价格附录

{{if .IntlTop5}} {{range .IntlTop5}} {{end}}
国际候选厂商输入输出上下文
{{.Name}} {{.ProviderName}} {{formatPriceWithCurrency .InputPrice .Currency}} {{formatPriceWithCurrency .OutputPrice .Currency}} {{formatContextWindowCompact .ContextLength}}
{{end}} {{if .DomesticTop10}} {{range .DomesticTop10}} {{end}}
国内候选厂商输入(CNY)输出(CNY)上下文
{{.Name}} {{.ProviderName}} {{formatDomesticPrice .InputPrice .Currency}} {{formatDomesticPrice .OutputPrice .Currency}} {{formatContextWindowCompact .ContextLength}}
{{end}}

完整免费附录

{{range .FreeTop20}} {{end}}
模型厂商来源类型上下文
{{.Name}} {{.ProviderName}} {{classifyFreeSource .}} {{formatContextWindowCompact .ContextLength}}

平台覆盖附录

{{if .Operators}} {{range .Operators}} {{end}}
官方/云平台模型数最低输入价平均输入价
{{.Name}}{{.ModelCount}}{{formatPrice .MinInputPrice "USD"}}{{formatPrice .AvgInputPrice "USD"}}
{{end}} {{if .Resellers}} {{range .Resellers}} {{end}}
聚合平台模型数最低输入价平均输入价
{{.Name}}{{.ModelCount}}{{formatPrice .MinInputPrice "USD"}}{{formatPrice .AvgInputPrice "USD"}}
{{end}}
{{if .TencentSubscriptionPlans}}

💳 腾讯云套餐订阅价

以下为套餐订阅价,不参与按模型输入/输出单价排行。

{{range .TencentSubscriptionPlans}} {{end}}
套餐月费月额度上下文上限覆盖模型
{{.PlanName}} {{formatSubscriptionPrice .ListPrice .Currency}} {{formatSubscriptionQuota .QuotaValue .QuotaUnit}} {{formatContextWindowCompact .ContextWindow}} {{.ModelCount}} 个{{if .ModelPreview}}({{.ModelPreview}}){{end}}
{{end}}
` funcMap := template.FuncMap{ "add": func(a, b int) int { return a + b }, "classifyFreeSource": classifyFreeSource, "formatPrice": formatPrice, "formatPriceWithCurrency": formatPriceWithCurrency, "formatDomesticPrice": formatDomesticPrice, "formatSubscriptionPrice": formatSubscriptionPrice, "formatSubscriptionQuota": formatSubscriptionQuota, "formatContextWindowCompact": formatContextWindowCompact, } t := template.Must(template.New("report").Funcs(funcMap).Parse(tmpl)) f, err := os.Create(path) if err != nil { return err } defer f.Close() return t.Execute(f, r) } func saveReportTrackingV3(db *sql.DB, r *ReportV3, mdPath string) error { summary := r.HeroSummary if summary == "" { summary = fmt.Sprintf("models=%d free=%d intl=%d domestic=%d", r.TotalModels, len(r.FreeModels), len(r.IntlTop5), len(r.DomesticTop10)) } tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(` INSERT INTO daily_report (report_date, status, model_count, new_models, free_models, summary_md, output_path, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) ON CONFLICT (report_date) DO UPDATE SET status = EXCLUDED.status, model_count = EXCLUDED.model_count, free_models = EXCLUDED.free_models, summary_md = EXCLUDED.summary_md, output_path = EXCLUDED.output_path, error_message = NULL, updated_at = NOW() `, r.Date, "generated", r.TotalModels, 0, len(r.FreeModels), summary, mdPath); err != nil { return err } if _, err := tx.Exec(` INSERT INTO report_runs (source, report_date, status, summary_md, output_path, error_message) VALUES ($1, $2, $3, $4, $5, NULL) `, "generate_daily_report", r.Date, "generated", summary, mdPath); err != nil { return err } return tx.Commit() } func archiveReportArtifacts(date, mdPath, htmlPath string) error { reportDir := filepath.Dir(mdPath) archiveDir := filepath.Join(reportDir, date[:4], date[5:7]) archiveMDPath := filepath.Join(archiveDir, filepath.Base(mdPath)) archiveHTMLPath := filepath.Join(archiveDir, filepath.Base(htmlPath)) if err := os.MkdirAll(archiveDir, 0755); err != nil { return err } if err := copyFile(mdPath, archiveMDPath); err != nil { return err } if err := copyFile(htmlPath, archiveHTMLPath); err != nil { return err } return nil } func copyFile(src, dst string) error { sourceFile, err := os.Open(src) if err != nil { return err } defer sourceFile.Close() targetFile, err := os.Create(dst) if err != nil { return err } defer targetFile.Close() if _, err := io.Copy(targetFile, sourceFile); err != nil { return err } return nil } // deriveProviderName 从 modelID 中提取厂商名 func deriveProviderName(modelID string) string { parts := strings.SplitN(modelID, "/", 2) if len(parts) == 0 || parts[0] == "" { return "Unknown" } raw := parts[0] raw = strings.ReplaceAll(raw, "-", " ") raw = strings.ReplaceAll(raw, "_", " ") words := strings.Fields(raw) for i, word := range words { if len(word) == 0 { continue } words[i] = strings.ToUpper(word[:1]) + strings.ToLower(word[1:]) } if len(words) == 0 { return "Unknown" } return strings.Join(words, " ") }