From b4e28d5be46f763d87100d0c396d6b56e63ba8aa Mon Sep 17 00:00:00 2001 From: phamnazage-jpg Date: Wed, 13 May 2026 21:16:08 +0800 Subject: [PATCH] feat(report): expose headline evidence details --- scripts/generate_daily_report.go | 241 +++++++++++++++++++------- scripts/generate_daily_report_test.go | 215 ++++++++++++++++------- 2 files changed, 326 insertions(+), 130 deletions(-) diff --git a/scripts/generate_daily_report.go b/scripts/generate_daily_report.go index 6c13313..2243d82 100644 --- a/scripts/generate_daily_report.go +++ b/scripts/generate_daily_report.go @@ -205,29 +205,37 @@ type ActionItem struct { } type HeadlineItem struct { - Label string - Title string - Summary string - Baseline string - TrustLabel string - Tone string + 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 - Baseline string - Summary string - Currency string - OldInputPrice float64 - NewInputPrice float64 - OldOutputPrice float64 - NewOutputPrice float64 - PriceChangePct float64 - Priority int + 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 { @@ -805,7 +813,8 @@ func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { 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 + 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 @@ -832,6 +841,7 @@ func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { isFree bool contextLength int providerCountry string + createdAt time.Time ) if err := rows.Scan( &modelName, @@ -844,6 +854,7 @@ func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { &isFree, &contextLength, &providerCountry, + &createdAt, ); err != nil { return nil, err } @@ -869,17 +880,21 @@ func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) { } events = append(events, ModelEvent{ - EventType: "new_model", - ModelName: modelName, - ProviderName: providerName, - OperatorName: operatorName, - TrustLabel: buildTrustLabel(model), - Baseline: "首次出现", - Summary: summary, - Currency: currency, - NewInputPrice: inputPrice, + 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), + Priority: 85 + minInt(contextLength/(1024*128), 10), }) } return events, rows.Err() @@ -909,7 +924,8 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) { 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 + 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 @@ -936,6 +952,7 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) { oldOutputPrice float64 newOutputPrice float64 providerCountry string + changedAt time.Time ) if err := rows.Scan( &modelName, @@ -948,6 +965,7 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) { &oldOutputPrice, &newOutputPrice, &providerCountry, + &changedAt, ); err != nil { return nil, err } @@ -967,32 +985,32 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) { } eventType := "price_increase" - label := "价格上调" summary := "价格上调已足以影响默认成本,需要确认备用模型。" if changePct < 0 { eventType = "price_cut" - label = "价格下调" summary = "价格下降已足以影响默认选型,值得重新评估同类模型。" } events = append(events, ModelEvent{ - EventType: eventType, - ModelName: modelName, - ProviderName: providerName, - OperatorName: operatorName, - TrustLabel: buildTrustLabel(model), - 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), + 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), }) - - _ = label } return events, rows.Err() } @@ -1089,15 +1107,19 @@ func enrichModelEvents(r *ReportV3) []ModelEvent { } existing[key] = struct{}{} events = append(events, ModelEvent{ - EventType: "free_highlight", - ModelName: model.Name, - ProviderName: model.ProviderName, - OperatorName: model.OperatorName, - TrustLabel: buildTrustLabel(model), - Baseline: "今日快照", - Summary: buildModelEvidence(model), - Currency: model.Currency, - Priority: priority, + 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, }) } @@ -1311,11 +1333,15 @@ func buildHeadlineItemsFromEvents(events []ModelEvent) []HeadlineItem { func headlineItemFromModelEvent(event ModelEvent) HeadlineItem { item := HeadlineItem{ - Title: event.ModelName, - Summary: event.Summary, - Baseline: event.Baseline, - TrustLabel: event.TrustLabel, - Tone: "neutral", + 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 { @@ -1343,6 +1369,58 @@ func headlineItemFromModelEvent(event ModelEvent) HeadlineItem { 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 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 @@ -1670,7 +1748,19 @@ 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.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) } @@ -1984,11 +2074,26 @@ body { font-weight: 700; } .trust-line, -.baseline-line { +.baseline-line, +.source-line { margin-top: 8px; font-size: 0.9rem; color: var(--ink-soft); } +.evidence-block { + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed var(--line); + display: grid; + gap: 6px; +} +.evidence-item { + font-size: 0.92rem; + color: var(--ink-soft); +} +.evidence-item strong { + color: var(--ink); +} .scene-header { display: flex; justify-content: space-between; @@ -2167,8 +2272,14 @@ th {
{{.Label}}
{{.Title}}
{{.Summary}}
+ {{if .SourceKindLabel}}
事件来源:{{.SourceKindLabel}}
{{end}}
基线:{{.Baseline}}
可信度:{{.TrustLabel}}
+
+ {{if .PrimarySource}}
主来源:{{.PrimarySource}}
{{end}} + {{if .UpdatedAt}}
更新时间:{{.UpdatedAt}}
{{end}} + {{if .EvidenceDetail}}
判定依据:{{.EvidenceDetail}}
{{end}} +
{{end}} diff --git a/scripts/generate_daily_report_test.go b/scripts/generate_daily_report_test.go index 392a394..698777d 100644 --- a/scripts/generate_daily_report_test.go +++ b/scripts/generate_daily_report_test.go @@ -184,25 +184,33 @@ func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) { report := sampleReportForV1() report.ModelEvents = []ModelEvent{ { - EventType: "new_model", - ModelName: "DeepSeek-V4-Flash", - ProviderName: "DeepSeek", - OperatorName: "OpenRouter", - TrustLabel: "聚合来源", - Baseline: "首次出现", - Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", - Priority: 95, + EventType: "new_model", + ModelName: "DeepSeek-V4-Flash", + ProviderName: "DeepSeek", + OperatorName: "OpenRouter", + TrustLabel: "聚合来源", + Baseline: "首次出现", + Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", + SourceKindLabel: "模型快照", + PrimarySource: "OpenRouter / region_pricing", + UpdatedAt: "2026-05-13 09:30", + EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照", + Priority: 95, }, { - EventType: "price_cut", - ModelName: "qwen-vl-max", - ProviderName: "Alibaba", - OperatorName: "DashScope", - TrustLabel: "官方来源", - Baseline: "较昨日 -18%", - Summary: "价格下降已足以影响视觉模型默认选择。", - PriceChangePct: -18, - Priority: 90, + EventType: "price_cut", + ModelName: "qwen-vl-max", + ProviderName: "Alibaba", + OperatorName: "DashScope", + TrustLabel: "官方来源", + Baseline: "较昨日 -18%", + Summary: "价格下降已足以影响视觉模型默认选择。", + SourceKindLabel: "价格快照", + PrimarySource: "pricing_history", + UpdatedAt: "2026-05-13 10:00", + EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%", + PriceChangePct: -18, + Priority: 90, }, } @@ -277,6 +285,22 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) { ModelPreview: "hy3-preview", }, } + report.ModelEvents = []ModelEvent{ + { + EventType: "new_model", + ModelName: "DeepSeek-V4-Flash", + ProviderName: "DeepSeek", + OperatorName: "OpenRouter", + TrustLabel: "聚合来源", + Baseline: "首次出现", + Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", + SourceKindLabel: "模型快照", + PrimarySource: "OpenRouter / region_pricing", + UpdatedAt: "2026-05-13 09:30", + EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照", + Priority: 95, + }, + } decorateReportV1(report) if err := generateMarkdownV3(report, path); err != nil { @@ -295,6 +319,9 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) { "## 今日变化", "## 场景推荐", "## 完整数据附录", + "主来源: OpenRouter / region_pricing", + "更新时间: 2026-05-13 09:30", + "判定依据: models.created_at = 今日,且已存在最新价格快照", "## 💳 腾讯云套餐订阅价", "通用 Token Plan Lite", "Hy Token Plan Max", @@ -313,14 +340,18 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) { report := sampleReportForV1() report.ModelEvents = []ModelEvent{ { - EventType: "new_model", - ModelName: "DeepSeek-V4-Flash", - ProviderName: "DeepSeek", - OperatorName: "OpenRouter", - TrustLabel: "聚合来源", - Baseline: "首次出现", - Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", - Priority: 95, + EventType: "new_model", + ModelName: "DeepSeek-V4-Flash", + ProviderName: "DeepSeek", + OperatorName: "OpenRouter", + TrustLabel: "聚合来源", + Baseline: "首次出现", + Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", + SourceKindLabel: "模型快照", + PrimarySource: "OpenRouter / region_pricing", + UpdatedAt: "2026-05-13 09:30", + EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照", + Priority: 95, }, } report.TencentSubscriptionPlans = []SubscriptionPlanInfo{ @@ -354,6 +385,10 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) { "今日头条", "DeepSeek-V4-Flash", "首次出现", + "主来源", + "更新时间", + "判定依据", + "模型快照", "场景推荐", "完整数据附录", "官方免费", @@ -371,25 +406,33 @@ func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) { report := sampleReportForV1() report.ModelEvents = []ModelEvent{ { - EventType: "price_cut", - ModelName: "glm-5", - ProviderName: "Zhipu", - OperatorName: "Zhipu", - TrustLabel: "官方来源", - Baseline: "较昨日 -25%", - Summary: "价格下降已足以影响中文通用场景默认选型。", - PriceChangePct: -25, - Priority: 100, + EventType: "price_cut", + ModelName: "glm-5", + ProviderName: "Zhipu", + OperatorName: "Zhipu", + TrustLabel: "官方来源", + Baseline: "较昨日 -25%", + Summary: "价格下降已足以影响中文通用场景默认选型。", + SourceKindLabel: "价格快照", + PrimarySource: "pricing_history", + UpdatedAt: "2026-05-13 10:00", + EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 25%", + PriceChangePct: -25, + Priority: 100, }, { - EventType: "new_model", - ModelName: "DeepSeek-V4-Flash", - ProviderName: "DeepSeek", - OperatorName: "OpenRouter", - TrustLabel: "聚合来源", - Baseline: "首次出现", - Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", - Priority: 90, + EventType: "new_model", + ModelName: "DeepSeek-V4-Flash", + ProviderName: "DeepSeek", + OperatorName: "OpenRouter", + TrustLabel: "聚合来源", + Baseline: "首次出现", + Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", + SourceKindLabel: "模型快照", + PrimarySource: "OpenRouter / region_pricing", + UpdatedAt: "2026-05-13 09:30", + EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照", + Priority: 90, }, } @@ -404,39 +447,54 @@ func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) { if items[0].Baseline != "较昨日 -25%" { t.Fatalf("expected event baseline to be preserved, got %+v", items[0]) } + if items[0].SourceKindLabel != "价格快照" || items[0].PrimarySource != "pricing_history" { + t.Fatalf("expected event evidence fields to be preserved, got %+v", items[0]) + } } func TestBuildHeadlineItemsDeduplicatesSameModel(t *testing.T) { report := sampleReportForV1() report.ModelEvents = []ModelEvent{ { - EventType: "price_cut", - ModelName: "OpenAI: GPT-4o", - ProviderName: "OpenAI", - TrustLabel: "官方来源", - Baseline: "较昨日 -20%", - Summary: "价格下降影响默认成本。", - PriceChangePct: -20, - Priority: 95, + EventType: "price_cut", + ModelName: "OpenAI: GPT-4o", + ProviderName: "OpenAI", + TrustLabel: "官方来源", + Baseline: "较昨日 -20%", + Summary: "价格下降影响默认成本。", + SourceKindLabel: "价格快照", + PrimarySource: "pricing_history", + UpdatedAt: "2026-05-13 10:00", + EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 20%", + PriceChangePct: -20, + Priority: 95, }, { - EventType: "price_increase", - ModelName: "OpenAI: GPT-4o", - ProviderName: "OpenAI", - TrustLabel: "官方来源", - Baseline: "较昨日 +5%", - Summary: "同日另有上调记录。", - PriceChangePct: 5, - Priority: 80, + EventType: "price_increase", + ModelName: "OpenAI: GPT-4o", + ProviderName: "OpenAI", + TrustLabel: "官方来源", + Baseline: "较昨日 +5%", + Summary: "同日另有上调记录。", + SourceKindLabel: "价格快照", + PrimarySource: "pricing_history", + UpdatedAt: "2026-05-13 11:00", + EvidenceDetail: "pricing_history 记录到输入价格较昨日上涨 5%", + PriceChangePct: 5, + Priority: 80, }, { - EventType: "new_model", - ModelName: "Claude Opus 4.7", - ProviderName: "Anthropic", - TrustLabel: "聚合来源", - Baseline: "首次出现", - Summary: "新模型上线。", - Priority: 70, + EventType: "new_model", + ModelName: "Claude Opus 4.7", + ProviderName: "Anthropic", + TrustLabel: "聚合来源", + Baseline: "首次出现", + Summary: "新模型上线。", + SourceKindLabel: "模型快照", + PrimarySource: "OpenRouter / region_pricing", + UpdatedAt: "2026-05-13 09:00", + EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照", + Priority: 70, }, } @@ -449,3 +507,30 @@ func TestBuildHeadlineItemsDeduplicatesSameModel(t *testing.T) { t.Fatalf("expected duplicate model event to be removed, got %+v", items) } } + +func TestHeadlineItemFromModelEventIncludesEvidenceFields(t *testing.T) { + item := headlineItemFromModelEvent(ModelEvent{ + EventType: "new_model", + ModelName: "DeepSeek-V4-Flash", + TrustLabel: "聚合来源", + Baseline: "首次出现", + Summary: "新模型进入情报池。", + SourceKindLabel: "模型快照", + PrimarySource: "OpenRouter / region_pricing", + UpdatedAt: "2026-05-13 09:30", + EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照", + }) + + if item.SourceKindLabel != "模型快照" { + t.Fatalf("expected source kind label to be propagated, got %+v", item) + } + if item.PrimarySource != "OpenRouter / region_pricing" { + t.Fatalf("expected primary source to be propagated, got %+v", item) + } + if item.UpdatedAt != "2026-05-13 09:30" { + t.Fatalf("expected updated at to be propagated, got %+v", item) + } + if item.EvidenceDetail == "" { + t.Fatalf("expected evidence detail to be populated, got %+v", item) + } +}