feat(report): expose headline evidence details
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user