feat(report): close v2 headline and coverage gaps

This commit is contained in:
phamnazage-jpg
2026-05-14 10:23:13 +08:00
parent d7fbd25dde
commit 618dff33da
7 changed files with 706 additions and 2 deletions

View File

@@ -240,6 +240,7 @@ type HeadlineItem struct {
Label string
Title string
Summary string
Audience string
Baseline string
TrustLabel string
SourceKindLabel string
@@ -254,6 +255,7 @@ type ModelEvent struct {
ModelName string
ProviderName string
OperatorName string
Audience string
TrustLabel string
SourceKindLabel string
PrimarySource string
@@ -270,6 +272,21 @@ type ModelEvent struct {
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
@@ -807,6 +824,12 @@ func loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
}
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
@@ -823,6 +846,71 @@ func loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
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 (
@@ -907,6 +995,7 @@ func loadOfficialReleaseEvents(db *sql.DB, date string) ([]ModelEvent, error) {
ModelName: modelName,
ProviderName: providerName,
OperatorName: operatorName,
Audience: "适合需要复查默认选型与路线图判断的团队",
TrustLabel: buildReleaseTrustLabel(model, dateConfidence),
SourceKindLabel: buildReleaseSourceKindLabel(dateSourceKind, dateConfidence),
PrimarySource: sourceURL,
@@ -1020,6 +1109,7 @@ func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
ModelName: modelName,
ProviderName: providerName,
OperatorName: operatorName,
Audience: "适合想尽快验证新模型价值的选型读者",
TrustLabel: buildTrustLabel(model),
SourceKindLabel: "模型快照",
PrimarySource: buildPrimarySource("region_pricing", operatorName),
@@ -1132,6 +1222,7 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) {
ModelName: modelName,
ProviderName: providerName,
OperatorName: operatorName,
Audience: buildPriceEventAudience(changePct),
TrustLabel: buildTrustLabel(model),
SourceKindLabel: "价格快照",
PrimarySource: "pricing_history",
@@ -1191,6 +1282,13 @@ func minInt(a, b int) int {
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
@@ -1198,6 +1296,22 @@ func abs(v float64) float64 {
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
@@ -1247,6 +1361,7 @@ func enrichModelEvents(r *ReportV3) []ModelEvent {
ModelName: model.Name,
ProviderName: model.ProviderName,
OperatorName: model.OperatorName,
Audience: "适合先试后买、但需要先判断免费来源的读者",
TrustLabel: buildTrustLabel(model),
SourceKindLabel: "免费策略快照",
PrimarySource: buildPrimarySource("free_snapshot", model.OperatorName),
@@ -1333,7 +1448,7 @@ func buildPageMode(signals DailySignals) string {
}
func buildPageModeWithEvents(signals DailySignals, events []ModelEvent) string {
if hasEventType(events, "official_release") {
if hasEventType(events, "official_release") || hasEventType(events, "promo_campaign") {
return "hot"
}
if signals.NewModels == 0 && signals.PriceChanges == 0 {
@@ -1358,6 +1473,9 @@ func buildMarketLabels(r *ReportV3) []string {
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, "新模型日")
}
@@ -1389,6 +1507,10 @@ func buildHeroSummary(r *ReportV3) (string, string) {
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)
}
switch r.PageMode {
case "hot":
return fmt.Sprintf(
@@ -1428,6 +1550,7 @@ func buildHeadlineItems(r *ReportV3) []HeadlineItem {
Label: "新模型",
Title: fmt.Sprintf("今日新增 %d 个模型进入情报池", r.DailySignals.NewModels),
Summary: "建议优先复查今天的新上架模型是否改变当前低成本或中文场景的最优解。",
Audience: "适合想快速筛出新增机会的读者",
Baseline: "首次出现",
TrustLabel: "数据库追踪",
Tone: "info",
@@ -1438,6 +1561,7 @@ func buildHeadlineItems(r *ReportV3) []HeadlineItem {
Label: "价格变化",
Title: fmt.Sprintf("今日检测到 %d 次价格变化", r.DailySignals.PriceChanges),
Summary: "价格变化已足以影响低成本选型,建议重新确认默认模型与备用模型。",
Audience: "适合以成本为先、需要重排默认选型的团队",
Baseline: "较昨日",
TrustLabel: "价格快照",
Tone: "success",
@@ -1449,6 +1573,7 @@ func buildHeadlineItems(r *ReportV3) []HeadlineItem {
Label: "免费策略",
Title: "免费机会主要来自聚合平台,不等于官方长期免费",
Summary: fmt.Sprintf("官方免费 %d 个,聚合免费 %d 个,待确认 %d 个。", r.DailySignals.OfficialFree, r.DailySignals.AggregatorFree, r.DailySignals.UnknownFree),
Audience: "适合想先试用、但不想误把聚合免费当官方免费的读者",
Baseline: "今日快照",
TrustLabel: "来源已分层",
Tone: "warning",
@@ -1460,6 +1585,7 @@ func buildHeadlineItems(r *ReportV3) []HeadlineItem {
Label: "观察重点",
Title: "今日无重大上新或显著调价",
Summary: "平静日优先关注稳定商用与长期成本,避免被噪音列表打断判断。",
Audience: "适合更重视稳定性和长期成本的团队",
Baseline: "较昨日",
TrustLabel: "日报编辑规则",
Tone: "neutral",
@@ -1477,6 +1603,11 @@ func buildHeadlineItemsFromEvents(events []ModelEvent) []HeadlineItem {
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
@@ -1492,7 +1623,7 @@ func buildHeadlineItemsFromEvents(events []ModelEvent) []HeadlineItem {
}
usedModels[event.ModelName] = struct{}{}
items = append(items, headlineItemFromModelEvent(event))
if len(items) >= 3 {
if len(items) >= limit {
break
}
}
@@ -1503,6 +1634,7 @@ 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,
@@ -1534,6 +1666,10 @@ func headlineItemFromModelEvent(event ModelEvent) HeadlineItem {
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)
@@ -1967,6 +2103,9 @@ func generateMarkdownV3(r *ReportV3, path string) error {
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)
}
@@ -2535,6 +2674,7 @@ th {
<div class="card-kicker headline-badge badge-{{.Tone}}">{{.Label}}</div>
<div class="card-title">{{.Title}}</div>
<div class="card-summary">{{.Summary}}</div>
{{if .Audience}}<div class="source-line">影响对象:{{.Audience}}</div>{{end}}
{{if .SourceKindLabel}}<div class="source-line">事件来源:{{.SourceKindLabel}}</div>{{end}}
<div class="baseline-line">基线:{{.Baseline}}</div>
<div class="trust-line">可信度:{{.TrustLabel}}</div>