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

@@ -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 {
<div class="card-kicker">{{.Label}}</div>
<div class="card-title">{{.Title}}</div>
<div class="card-summary">{{.Summary}}</div>
{{if .SourceKindLabel}}<div class="source-line">事件来源:{{.SourceKindLabel}}</div>{{end}}
<div class="baseline-line">基线:{{.Baseline}}</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>
{{end}}
</div>

View File

@@ -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)
}
}