diff --git a/scripts/generate_daily_report.go b/scripts/generate_daily_report.go index 2243d82..86df158 100644 --- a/scripts/generate_daily_report.go +++ b/scripts/generate_daily_report.go @@ -769,6 +769,12 @@ func loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { } 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 @@ -785,6 +791,98 @@ func loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { 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(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 + providerCountry string + releaseDate time.Time + currency string + ) + if err := rows.Scan( + &modelName, + &providerName, + &operatorName, + &operatorType, + &sourceURL, + &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: buildTrustLabel(model), + SourceKindLabel: "官方发布", + PrimarySource: sourceURL, + UpdatedAt: releaseDate.Format("2006-01-02 15:04"), + EvidenceDetail: "models.release_date = 今日,且 source_url 指向官方发布页", + 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 ( @@ -1079,11 +1177,11 @@ func decorateReportV1(r *ReportV3) { } } - r.PageMode = buildPageMode(r.DailySignals) + 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.ModelEvents = enrichModelEvents(r) r.ActionItems = buildActionItems(r) r.HeadlineItems = buildHeadlineItems(r) r.AppendixLinks = []AppendixLink{ @@ -1193,6 +1291,13 @@ func isVerifiedAggregator(name string) bool { } 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" } @@ -1204,7 +1309,7 @@ func buildPageMode(signals DailySignals) string { func buildMarketLabels(r *ReportV3) []string { labels := []string{} - switch r.PageMode { + switch buildPageModeWithEvents(r.DailySignals, r.ModelEvents) { case "hot": labels = append(labels, "热点日") case "calm": @@ -1212,6 +1317,9 @@ func buildMarketLabels(r *ReportV3) []string { default: labels = append(labels, "常规日") } + if hasEventType(r.ModelEvents, "official_release") { + labels = append(labels, "官方发布") + } if r.DailySignals.NewModels > 0 { labels = append(labels, "新模型日") } @@ -1229,7 +1337,20 @@ func buildMarketLabels(r *ReportV3) []string { 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( @@ -1248,6 +1369,15 @@ func buildHeroSummary(r *ReportV3) (string, string) { } } +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 @@ -1345,6 +1475,10 @@ func headlineItemFromModelEvent(event ModelEvent) HeadlineItem { } switch event.EventType { + case "official_release": + item.Label = "官方发布" + item.Title = fmt.Sprintf("%s 官方发布", event.ModelName) + item.Tone = "info" case "new_model": item.Label = "新模型" item.Title = fmt.Sprintf("%s 进入今日情报池", event.ModelName) diff --git a/scripts/generate_daily_report_test.go b/scripts/generate_daily_report_test.go index 698777d..29473b9 100644 --- a/scripts/generate_daily_report_test.go +++ b/scripts/generate_daily_report_test.go @@ -405,6 +405,20 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) { func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) { report := sampleReportForV1() report.ModelEvents = []ModelEvent{ + { + EventType: "official_release", + ModelName: "GLM-5", + ProviderName: "Zhipu", + OperatorName: "Zhipu", + TrustLabel: "官方来源", + Baseline: "官方首次发布", + Summary: "官方发布新模型,值得优先复查中文通用与推理场景默认选择。", + SourceKindLabel: "官方发布", + PrimarySource: "https://open.bigmodel.cn/dev/howuse/model", + UpdatedAt: "2026-05-13 08:30", + EvidenceDetail: "models.release_date = 今日,且 source_url 指向官方文档", + Priority: 120, + }, { EventType: "price_cut", ModelName: "glm-5", @@ -441,14 +455,14 @@ func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) { if len(items) < 2 { t.Fatalf("expected at least 2 headline items, got %d", len(items)) } - if !strings.Contains(items[0].Title, "glm-5") { - t.Fatalf("expected price_cut event to rank first, got %+v", items[0]) + if !strings.Contains(items[0].Title, "GLM-5") || items[0].Label != "官方发布" { + t.Fatalf("expected official release event to rank first, got %+v", items[0]) } - if items[0].Baseline != "较昨日 -25%" { - t.Fatalf("expected event baseline to be preserved, got %+v", items[0]) + if items[1].Baseline != "较昨日 -25%" { + t.Fatalf("expected price_cut baseline to be preserved, got %+v", items[1]) } - if items[0].SourceKindLabel != "价格快照" || items[0].PrimarySource != "pricing_history" { - t.Fatalf("expected event evidence fields to be preserved, got %+v", items[0]) + if items[0].SourceKindLabel != "官方发布" || items[0].PrimarySource != "https://open.bigmodel.cn/dev/howuse/model" { + t.Fatalf("expected official release evidence fields to be preserved, got %+v", items[0]) } } @@ -534,3 +548,30 @@ func TestHeadlineItemFromModelEventIncludesEvidenceFields(t *testing.T) { t.Fatalf("expected evidence detail to be populated, got %+v", item) } } + +func TestHeadlineItemFromOfficialReleaseEvent(t *testing.T) { + item := headlineItemFromModelEvent(ModelEvent{ + EventType: "official_release", + ModelName: "Claude Sonnet 4.5", + TrustLabel: "官方来源", + Baseline: "官方首次发布", + Summary: "官方发布新模型。", + SourceKindLabel: "官方发布", + PrimarySource: "https://docs.anthropic.com/en/release-notes/api", + UpdatedAt: "2026-05-13 07:00", + EvidenceDetail: "models.release_date = 今日,且 source_url 指向官方发布页", + }) + + if item.Label != "官方发布" { + t.Fatalf("expected label to be 官方发布, got %+v", item) + } + if !strings.Contains(item.Title, "官方发布") { + t.Fatalf("expected title to mention 官方发布, got %+v", item) + } + if item.SourceKindLabel != "官方发布" { + t.Fatalf("expected source kind label to be 官方发布, got %+v", item) + } + if item.PrimarySource != "https://docs.anthropic.com/en/release-notes/api" { + t.Fatalf("expected primary source to be preserved, got %+v", item) + } +}