feat(report): expose headline evidence details

This commit is contained in:
phamnazage-jpg
2026-05-13 21:16:08 +08:00
parent 79d991a7e9
commit b4e28d5be4
2 changed files with 326 additions and 130 deletions

View File

@@ -210,6 +210,10 @@ type HeadlineItem struct {
Summary string Summary string
Baseline string Baseline string
TrustLabel string TrustLabel string
SourceKindLabel string
PrimarySource string
UpdatedAt string
EvidenceDetail string
Tone string Tone string
} }
@@ -219,6 +223,10 @@ type ModelEvent struct {
ProviderName string ProviderName string
OperatorName string OperatorName string
TrustLabel string TrustLabel string
SourceKindLabel string
PrimarySource string
UpdatedAt string
EvidenceDetail string
Baseline string Baseline string
Summary string Summary string
Currency string Currency string
@@ -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.output_price_per_mtok, 0) AS output_price,
COALESCE(lp.is_free, false) AS is_free, COALESCE(lp.is_free, false) AS is_free,
COALESCE(m.context_length, 0) AS context_length, 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 FROM models m
LEFT JOIN model_provider mp ON m.provider_id = mp.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 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 isFree bool
contextLength int contextLength int
providerCountry string providerCountry string
createdAt time.Time
) )
if err := rows.Scan( if err := rows.Scan(
&modelName, &modelName,
@@ -844,6 +854,7 @@ func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
&isFree, &isFree,
&contextLength, &contextLength,
&providerCountry, &providerCountry,
&createdAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -874,6 +885,10 @@ func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
ProviderName: providerName, ProviderName: providerName,
OperatorName: operatorName, OperatorName: operatorName,
TrustLabel: buildTrustLabel(model), TrustLabel: buildTrustLabel(model),
SourceKindLabel: "模型快照",
PrimarySource: buildPrimarySource("region_pricing", operatorName),
UpdatedAt: createdAt.Format("2006-01-02 15:04"),
EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照",
Baseline: "首次出现", Baseline: "首次出现",
Summary: summary, Summary: summary,
Currency: currency, Currency: currency,
@@ -909,7 +924,8 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) {
COALESCE(ph.new_input_price, 0), COALESCE(ph.new_input_price, 0),
COALESCE(ph.old_output_price, 0), COALESCE(ph.old_output_price, 0),
COALESCE(ph.new_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 FROM pricing_history ph
JOIN models m ON ph.model_id = m.id JOIN models m ON ph.model_id = m.id
LEFT JOIN model_provider mp ON m.provider_id = mp.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 oldOutputPrice float64
newOutputPrice float64 newOutputPrice float64
providerCountry string providerCountry string
changedAt time.Time
) )
if err := rows.Scan( if err := rows.Scan(
&modelName, &modelName,
@@ -948,6 +965,7 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) {
&oldOutputPrice, &oldOutputPrice,
&newOutputPrice, &newOutputPrice,
&providerCountry, &providerCountry,
&changedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -967,11 +985,9 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) {
} }
eventType := "price_increase" eventType := "price_increase"
label := "价格上调"
summary := "价格上调已足以影响默认成本,需要确认备用模型。" summary := "价格上调已足以影响默认成本,需要确认备用模型。"
if changePct < 0 { if changePct < 0 {
eventType = "price_cut" eventType = "price_cut"
label = "价格下调"
summary = "价格下降已足以影响默认选型,值得重新评估同类模型。" summary = "价格下降已足以影响默认选型,值得重新评估同类模型。"
} }
@@ -981,6 +997,10 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) {
ProviderName: providerName, ProviderName: providerName,
OperatorName: operatorName, OperatorName: operatorName,
TrustLabel: buildTrustLabel(model), 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), Baseline: fmt.Sprintf("较昨日 %+.0f%%", changePct),
Summary: summary, Summary: summary,
Currency: currency, Currency: currency,
@@ -991,8 +1011,6 @@ func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) {
PriceChangePct: changePct, PriceChangePct: changePct,
Priority: 70 + minInt(int(abs(changePct)), 25), Priority: 70 + minInt(int(abs(changePct)), 25),
}) })
_ = label
} }
return events, rows.Err() return events, rows.Err()
} }
@@ -1094,6 +1112,10 @@ func enrichModelEvents(r *ReportV3) []ModelEvent {
ProviderName: model.ProviderName, ProviderName: model.ProviderName,
OperatorName: model.OperatorName, OperatorName: model.OperatorName,
TrustLabel: buildTrustLabel(model), TrustLabel: buildTrustLabel(model),
SourceKindLabel: "免费策略快照",
PrimarySource: buildPrimarySource("free_snapshot", model.OperatorName),
UpdatedAt: formatEventUpdatedAt(r.GeneratedAt, r.Date),
EvidenceDetail: buildFreeEvidenceDetail(model),
Baseline: "今日快照", Baseline: "今日快照",
Summary: buildModelEvidence(model), Summary: buildModelEvidence(model),
Currency: model.Currency, Currency: model.Currency,
@@ -1315,6 +1337,10 @@ func headlineItemFromModelEvent(event ModelEvent) HeadlineItem {
Summary: event.Summary, Summary: event.Summary,
Baseline: event.Baseline, Baseline: event.Baseline,
TrustLabel: event.TrustLabel, TrustLabel: event.TrustLabel,
SourceKindLabel: event.SourceKindLabel,
PrimarySource: event.PrimarySource,
UpdatedAt: event.UpdatedAt,
EvidenceDetail: event.EvidenceDetail,
Tone: "neutral", Tone: "neutral",
} }
@@ -1343,6 +1369,58 @@ func headlineItemFromModelEvent(event ModelEvent) HeadlineItem {
return item 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 { func buildActionItems(r *ReportV3) []ActionItem {
var actions []ActionItem var actions []ActionItem
@@ -1670,7 +1748,19 @@ func generateMarkdownV3(r *ReportV3, path string) error {
for _, item := range r.HeadlineItems { for _, item := range r.HeadlineItems {
fmt.Fprintf(f, "### %s · %s\n\n", item.Label, item.Title) fmt.Fprintf(f, "### %s · %s\n\n", item.Label, item.Title)
fmt.Fprintf(f, "- 影响: %s\n", item.Summary) 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) 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) fmt.Fprintf(f, "- 可信度: %s\n\n", item.TrustLabel)
} }
@@ -1984,11 +2074,26 @@ body {
font-weight: 700; font-weight: 700;
} }
.trust-line, .trust-line,
.baseline-line { .baseline-line,
.source-line {
margin-top: 8px; margin-top: 8px;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--ink-soft); 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 { .scene-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -2167,8 +2272,14 @@ th {
<div class="card-kicker">{{.Label}}</div> <div class="card-kicker">{{.Label}}</div>
<div class="card-title">{{.Title}}</div> <div class="card-title">{{.Title}}</div>
<div class="card-summary">{{.Summary}}</div> <div class="card-summary">{{.Summary}}</div>
{{if .SourceKindLabel}}<div class="source-line">事件来源:{{.SourceKindLabel}}</div>{{end}}
<div class="baseline-line">基线:{{.Baseline}}</div> <div class="baseline-line">基线:{{.Baseline}}</div>
<div class="trust-line">可信度:{{.TrustLabel}}</div> <div class="trust-line">可信度:{{.TrustLabel}}</div>
<div class="evidence-block">
{{if .PrimarySource}}<div class="evidence-item"><strong>主来源</strong>{{.PrimarySource}}</div>{{end}}
{{if .UpdatedAt}}<div class="evidence-item"><strong>更新时间</strong>{{.UpdatedAt}}</div>{{end}}
{{if .EvidenceDetail}}<div class="evidence-item"><strong>判定依据</strong>{{.EvidenceDetail}}</div>{{end}}
</div>
</article> </article>
{{end}} {{end}}
</div> </div>

View File

@@ -191,6 +191,10 @@ func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
TrustLabel: "聚合来源", TrustLabel: "聚合来源",
Baseline: "首次出现", Baseline: "首次出现",
Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。",
SourceKindLabel: "模型快照",
PrimarySource: "OpenRouter / region_pricing",
UpdatedAt: "2026-05-13 09:30",
EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照",
Priority: 95, Priority: 95,
}, },
{ {
@@ -201,6 +205,10 @@ func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
TrustLabel: "官方来源", TrustLabel: "官方来源",
Baseline: "较昨日 -18%", Baseline: "较昨日 -18%",
Summary: "价格下降已足以影响视觉模型默认选择。", Summary: "价格下降已足以影响视觉模型默认选择。",
SourceKindLabel: "价格快照",
PrimarySource: "pricing_history",
UpdatedAt: "2026-05-13 10:00",
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
PriceChangePct: -18, PriceChangePct: -18,
Priority: 90, Priority: 90,
}, },
@@ -277,6 +285,22 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
ModelPreview: "hy3-preview", 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) decorateReportV1(report)
if err := generateMarkdownV3(report, path); err != nil { 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", "通用 Token Plan Lite",
"Hy Token Plan Max", "Hy Token Plan Max",
@@ -320,6 +347,10 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
TrustLabel: "聚合来源", TrustLabel: "聚合来源",
Baseline: "首次出现", Baseline: "首次出现",
Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。",
SourceKindLabel: "模型快照",
PrimarySource: "OpenRouter / region_pricing",
UpdatedAt: "2026-05-13 09:30",
EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照",
Priority: 95, Priority: 95,
}, },
} }
@@ -354,6 +385,10 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
"今日头条", "今日头条",
"DeepSeek-V4-Flash", "DeepSeek-V4-Flash",
"首次出现", "首次出现",
"主来源",
"更新时间",
"判定依据",
"模型快照",
"场景推荐", "场景推荐",
"完整数据附录", "完整数据附录",
"官方免费", "官方免费",
@@ -378,6 +413,10 @@ func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) {
TrustLabel: "官方来源", TrustLabel: "官方来源",
Baseline: "较昨日 -25%", Baseline: "较昨日 -25%",
Summary: "价格下降已足以影响中文通用场景默认选型。", Summary: "价格下降已足以影响中文通用场景默认选型。",
SourceKindLabel: "价格快照",
PrimarySource: "pricing_history",
UpdatedAt: "2026-05-13 10:00",
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 25%",
PriceChangePct: -25, PriceChangePct: -25,
Priority: 100, Priority: 100,
}, },
@@ -389,6 +428,10 @@ func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) {
TrustLabel: "聚合来源", TrustLabel: "聚合来源",
Baseline: "首次出现", Baseline: "首次出现",
Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。", Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。",
SourceKindLabel: "模型快照",
PrimarySource: "OpenRouter / region_pricing",
UpdatedAt: "2026-05-13 09:30",
EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照",
Priority: 90, Priority: 90,
}, },
} }
@@ -404,6 +447,9 @@ func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) {
if items[0].Baseline != "较昨日 -25%" { if items[0].Baseline != "较昨日 -25%" {
t.Fatalf("expected event baseline to be preserved, got %+v", items[0]) 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) { func TestBuildHeadlineItemsDeduplicatesSameModel(t *testing.T) {
@@ -416,6 +462,10 @@ func TestBuildHeadlineItemsDeduplicatesSameModel(t *testing.T) {
TrustLabel: "官方来源", TrustLabel: "官方来源",
Baseline: "较昨日 -20%", Baseline: "较昨日 -20%",
Summary: "价格下降影响默认成本。", Summary: "价格下降影响默认成本。",
SourceKindLabel: "价格快照",
PrimarySource: "pricing_history",
UpdatedAt: "2026-05-13 10:00",
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 20%",
PriceChangePct: -20, PriceChangePct: -20,
Priority: 95, Priority: 95,
}, },
@@ -426,6 +476,10 @@ func TestBuildHeadlineItemsDeduplicatesSameModel(t *testing.T) {
TrustLabel: "官方来源", TrustLabel: "官方来源",
Baseline: "较昨日 +5%", Baseline: "较昨日 +5%",
Summary: "同日另有上调记录。", Summary: "同日另有上调记录。",
SourceKindLabel: "价格快照",
PrimarySource: "pricing_history",
UpdatedAt: "2026-05-13 11:00",
EvidenceDetail: "pricing_history 记录到输入价格较昨日上涨 5%",
PriceChangePct: 5, PriceChangePct: 5,
Priority: 80, Priority: 80,
}, },
@@ -436,6 +490,10 @@ func TestBuildHeadlineItemsDeduplicatesSameModel(t *testing.T) {
TrustLabel: "聚合来源", TrustLabel: "聚合来源",
Baseline: "首次出现", Baseline: "首次出现",
Summary: "新模型上线。", Summary: "新模型上线。",
SourceKindLabel: "模型快照",
PrimarySource: "OpenRouter / region_pricing",
UpdatedAt: "2026-05-13 09:00",
EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照",
Priority: 70, Priority: 70,
}, },
} }
@@ -449,3 +507,30 @@ func TestBuildHeadlineItemsDeduplicatesSameModel(t *testing.T) {
t.Fatalf("expected duplicate model event to be removed, got %+v", items) 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)
}
}