feat(report): add model-level event headlines
This commit is contained in:
@@ -179,6 +179,7 @@ type ReportV3 struct {
|
|||||||
HeadlineItems []HeadlineItem
|
HeadlineItems []HeadlineItem
|
||||||
SceneSections []SceneSection
|
SceneSections []SceneSection
|
||||||
AppendixLinks []AppendixLink
|
AppendixLinks []AppendixLink
|
||||||
|
ModelEvents []ModelEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailySignals struct {
|
type DailySignals struct {
|
||||||
@@ -212,6 +213,23 @@ type HeadlineItem struct {
|
|||||||
Tone 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
|
||||||
|
}
|
||||||
|
|
||||||
type Recommendation struct {
|
type Recommendation struct {
|
||||||
Name string
|
Name string
|
||||||
Provider string
|
Provider string
|
||||||
@@ -494,6 +512,11 @@ func generateReportDataV3(db *sql.DB, date string) (*ReportV3, error) {
|
|||||||
} else {
|
} else {
|
||||||
report.DailySignals = signals
|
report.DailySignals = signals
|
||||||
}
|
}
|
||||||
|
if events, err := loadModelEvents(db, date); err != nil {
|
||||||
|
logger.Warn("加载模型级事件失败", "error", err)
|
||||||
|
} else {
|
||||||
|
report.ModelEvents = events
|
||||||
|
}
|
||||||
decorateReportV1(report)
|
decorateReportV1(report)
|
||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
@@ -729,6 +752,298 @@ func loadDailySignals(db *sql.DB, date string) (DailySignals, error) {
|
|||||||
return signals, nil
|
return signals, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
|
||||||
|
var events []ModelEvent
|
||||||
|
|
||||||
|
newModelEvents, err := loadNewModelEvents(db, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events = append(events, newModelEvents...)
|
||||||
|
|
||||||
|
priceEvents, err := loadPriceChangeEvents(db, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events = append(events, priceEvents...)
|
||||||
|
|
||||||
|
sort.Slice(events, func(i, j int) bool {
|
||||||
|
if events[i].Priority != events[j].Priority {
|
||||||
|
return events[i].Priority > events[j].Priority
|
||||||
|
}
|
||||||
|
return events[i].ModelName < events[j].ModelName
|
||||||
|
})
|
||||||
|
|
||||||
|
return dedupeModelEvents(events), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
WITH latest_prices AS (
|
||||||
|
SELECT
|
||||||
|
rp.model_id,
|
||||||
|
COALESCE(o.name, 'Unknown') AS operator_name,
|
||||||
|
COALESCE(o.type, 'reseller') AS operator_type,
|
||||||
|
rp.currency,
|
||||||
|
rp.input_price_per_mtok,
|
||||||
|
rp.output_price_per_mtok,
|
||||||
|
rp.is_free,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY rp.model_id
|
||||||
|
ORDER BY rp.effective_date DESC NULLS LAST, rp.id DESC
|
||||||
|
) AS rn
|
||||||
|
FROM region_pricing rp
|
||||||
|
LEFT JOIN operator o ON rp.operator_id = o.id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(NULLIF(m.name, ''), m.external_id) AS model_name,
|
||||||
|
COALESCE(mp.name, split_part(m.external_id, '/', 1)) AS provider_name,
|
||||||
|
COALESCE(lp.operator_name, 'OpenRouter') AS operator_name,
|
||||||
|
COALESCE(lp.operator_type, 'reseller') AS operator_type,
|
||||||
|
COALESCE(lp.currency, 'USD') AS currency,
|
||||||
|
COALESCE(lp.input_price_per_mtok, 0) AS input_price,
|
||||||
|
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
|
||||||
|
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
|
||||||
|
WHERE m.deleted_at IS NULL
|
||||||
|
AND DATE(m.created_at) = $1::date
|
||||||
|
ORDER BY m.created_at DESC, m.id DESC
|
||||||
|
LIMIT 8
|
||||||
|
`, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var events []ModelEvent
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
modelName string
|
||||||
|
providerName string
|
||||||
|
operatorName string
|
||||||
|
operatorType string
|
||||||
|
currency string
|
||||||
|
inputPrice float64
|
||||||
|
outputPrice float64
|
||||||
|
isFree bool
|
||||||
|
contextLength int
|
||||||
|
providerCountry string
|
||||||
|
)
|
||||||
|
if err := rows.Scan(
|
||||||
|
&modelName,
|
||||||
|
&providerName,
|
||||||
|
&operatorName,
|
||||||
|
&operatorType,
|
||||||
|
¤cy,
|
||||||
|
&inputPrice,
|
||||||
|
&outputPrice,
|
||||||
|
&isFree,
|
||||||
|
&contextLength,
|
||||||
|
&providerCountry,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
model := ModelInfo{
|
||||||
|
Name: modelName,
|
||||||
|
ProviderName: providerName,
|
||||||
|
ProviderCountry: providerCountry,
|
||||||
|
ContextLength: contextLength,
|
||||||
|
InputPrice: inputPrice,
|
||||||
|
OutputPrice: outputPrice,
|
||||||
|
Currency: currency,
|
||||||
|
IsFree: isFree,
|
||||||
|
OperatorName: operatorName,
|
||||||
|
OperatorType: operatorType,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := "新模型进入情报池,值得重新评估当前默认选择。"
|
||||||
|
if isFree {
|
||||||
|
summary = fmt.Sprintf("新模型首日可免费试用,需注意其免费来源属于%s。", classifyFreeSource(model))
|
||||||
|
} else if contextLength >= 1024*256 {
|
||||||
|
summary = fmt.Sprintf("新模型带来 %s 长上下文,值得复查 Agent 和代码场景。", formatContextWindowCompact(contextLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
events = append(events, ModelEvent{
|
||||||
|
EventType: "new_model",
|
||||||
|
ModelName: modelName,
|
||||||
|
ProviderName: providerName,
|
||||||
|
OperatorName: operatorName,
|
||||||
|
TrustLabel: buildTrustLabel(model),
|
||||||
|
Baseline: "首次出现",
|
||||||
|
Summary: summary,
|
||||||
|
Currency: currency,
|
||||||
|
NewInputPrice: inputPrice,
|
||||||
|
NewOutputPrice: outputPrice,
|
||||||
|
Priority: 85 + minInt(contextLength/(1024*128), 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return events, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPriceChangeEvents(db *sql.DB, date string) ([]ModelEvent, error) {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
WITH latest_prices AS (
|
||||||
|
SELECT
|
||||||
|
rp.model_id,
|
||||||
|
COALESCE(o.name, 'Unknown') AS operator_name,
|
||||||
|
COALESCE(o.type, 'reseller') AS operator_type,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY rp.model_id
|
||||||
|
ORDER BY rp.effective_date DESC NULLS LAST, rp.id DESC
|
||||||
|
) AS rn
|
||||||
|
FROM region_pricing rp
|
||||||
|
LEFT JOIN operator o ON rp.operator_id = o.id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(NULLIF(m.name, ''), m.external_id) AS model_name,
|
||||||
|
COALESCE(mp.name, split_part(m.external_id, '/', 1)) AS provider_name,
|
||||||
|
COALESCE(lp.operator_name, 'OpenRouter') AS operator_name,
|
||||||
|
COALESCE(lp.operator_type, 'reseller') AS operator_type,
|
||||||
|
ph.currency,
|
||||||
|
COALESCE(ph.old_input_price, 0),
|
||||||
|
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
|
||||||
|
FROM pricing_history ph
|
||||||
|
JOIN models m ON ph.model_id = m.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
|
||||||
|
WHERE DATE(ph.changed_at) = $1::date
|
||||||
|
ORDER BY ph.changed_at DESC, ph.id DESC
|
||||||
|
LIMIT 16
|
||||||
|
`, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var events []ModelEvent
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
modelName string
|
||||||
|
providerName string
|
||||||
|
operatorName string
|
||||||
|
operatorType string
|
||||||
|
currency string
|
||||||
|
oldInputPrice float64
|
||||||
|
newInputPrice float64
|
||||||
|
oldOutputPrice float64
|
||||||
|
newOutputPrice float64
|
||||||
|
providerCountry string
|
||||||
|
)
|
||||||
|
if err := rows.Scan(
|
||||||
|
&modelName,
|
||||||
|
&providerName,
|
||||||
|
&operatorName,
|
||||||
|
&operatorType,
|
||||||
|
¤cy,
|
||||||
|
&oldInputPrice,
|
||||||
|
&newInputPrice,
|
||||||
|
&oldOutputPrice,
|
||||||
|
&newOutputPrice,
|
||||||
|
&providerCountry,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
changePct := signedPriceChangePct(oldInputPrice, newInputPrice, oldOutputPrice, newOutputPrice)
|
||||||
|
if changePct == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
model := ModelInfo{
|
||||||
|
Name: modelName,
|
||||||
|
ProviderName: providerName,
|
||||||
|
ProviderCountry: providerCountry,
|
||||||
|
Currency: currency,
|
||||||
|
OperatorName: operatorName,
|
||||||
|
OperatorType: operatorType,
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
|
||||||
|
_ = label
|
||||||
|
}
|
||||||
|
return events, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeModelEvents(events []ModelEvent) []ModelEvent {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
result := make([]ModelEvent, 0, len(events))
|
||||||
|
for _, event := range events {
|
||||||
|
key := event.EventType + "|" + event.ModelName
|
||||||
|
if _, exists := seen[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
result = append(result, event)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func signedPriceChangePct(oldInput, newInput, oldOutput, newOutput float64) float64 {
|
||||||
|
inputPct := signedChange(oldInput, newInput)
|
||||||
|
outputPct := signedChange(oldOutput, newOutput)
|
||||||
|
if abs(inputPct) >= abs(outputPct) {
|
||||||
|
return inputPct
|
||||||
|
}
|
||||||
|
return outputPct
|
||||||
|
}
|
||||||
|
|
||||||
|
func signedChange(oldValue, newValue float64) float64 {
|
||||||
|
if oldValue == 0 {
|
||||||
|
if newValue == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return ((newValue - oldValue) / oldValue) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func minInt(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(v float64) float64 {
|
||||||
|
if v < 0 {
|
||||||
|
return -v
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func decorateReportV1(r *ReportV3) {
|
func decorateReportV1(r *ReportV3) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
@@ -750,6 +1065,7 @@ func decorateReportV1(r *ReportV3) {
|
|||||||
r.MarketLabels = buildMarketLabels(r)
|
r.MarketLabels = buildMarketLabels(r)
|
||||||
r.HeroSummary, r.HeroEvidence = buildHeroSummary(r)
|
r.HeroSummary, r.HeroEvidence = buildHeroSummary(r)
|
||||||
r.SceneSections = buildSceneSections(r)
|
r.SceneSections = buildSceneSections(r)
|
||||||
|
r.ModelEvents = enrichModelEvents(r)
|
||||||
r.ActionItems = buildActionItems(r)
|
r.ActionItems = buildActionItems(r)
|
||||||
r.HeadlineItems = buildHeadlineItems(r)
|
r.HeadlineItems = buildHeadlineItems(r)
|
||||||
r.AppendixLinks = []AppendixLink{
|
r.AppendixLinks = []AppendixLink{
|
||||||
@@ -759,6 +1075,54 @@ func decorateReportV1(r *ReportV3) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enrichModelEvents(r *ReportV3) []ModelEvent {
|
||||||
|
events := append([]ModelEvent{}, r.ModelEvents...)
|
||||||
|
existing := make(map[string]struct{}, len(events))
|
||||||
|
for _, event := range events {
|
||||||
|
existing[event.EventType+"|"+event.ModelName] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFreeHighlight := func(model ModelInfo, priority int) {
|
||||||
|
key := "free_highlight|" + model.Name
|
||||||
|
if _, exists := existing[key]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range r.FreeModels {
|
||||||
|
if classifyFreeSource(model) == "官方免费" {
|
||||||
|
addFreeHighlight(model, 72)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, model := range r.FreeModels {
|
||||||
|
if classifyFreeSource(model) == "聚合免费" {
|
||||||
|
addFreeHighlight(model, 68)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(events, func(i, j int) bool {
|
||||||
|
if events[i].Priority != events[j].Priority {
|
||||||
|
return events[i].Priority > events[j].Priority
|
||||||
|
}
|
||||||
|
return events[i].ModelName < events[j].ModelName
|
||||||
|
})
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
func buildFreeSourceBreakdown(models []ModelInfo) []FreeSourceStat {
|
func buildFreeSourceBreakdown(models []ModelInfo) []FreeSourceStat {
|
||||||
counts := map[string]int{
|
counts := map[string]int{
|
||||||
"官方免费": 0,
|
"官方免费": 0,
|
||||||
@@ -863,6 +1227,10 @@ func buildHeroSummary(r *ReportV3) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildHeadlineItems(r *ReportV3) []HeadlineItem {
|
func buildHeadlineItems(r *ReportV3) []HeadlineItem {
|
||||||
|
if items := buildHeadlineItemsFromEvents(r.ModelEvents); len(items) > 0 {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
var items []HeadlineItem
|
var items []HeadlineItem
|
||||||
|
|
||||||
if r.DailySignals.NewModels > 0 {
|
if r.DailySignals.NewModels > 0 {
|
||||||
@@ -914,6 +1282,67 @@ func buildHeadlineItems(r *ReportV3) []HeadlineItem {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildHeadlineItemsFromEvents(events []ModelEvent) []HeadlineItem {
|
||||||
|
if len(events) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(events, func(i, j int) bool {
|
||||||
|
if events[i].Priority != events[j].Priority {
|
||||||
|
return events[i].Priority > events[j].Priority
|
||||||
|
}
|
||||||
|
return events[i].ModelName < events[j].ModelName
|
||||||
|
})
|
||||||
|
|
||||||
|
var items []HeadlineItem
|
||||||
|
usedModels := make(map[string]struct{})
|
||||||
|
for _, event := range dedupeModelEvents(events) {
|
||||||
|
if _, exists := usedModels[event.ModelName]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usedModels[event.ModelName] = struct{}{}
|
||||||
|
items = append(items, headlineItemFromModelEvent(event))
|
||||||
|
if len(items) >= 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func headlineItemFromModelEvent(event ModelEvent) HeadlineItem {
|
||||||
|
item := HeadlineItem{
|
||||||
|
Title: event.ModelName,
|
||||||
|
Summary: event.Summary,
|
||||||
|
Baseline: event.Baseline,
|
||||||
|
TrustLabel: event.TrustLabel,
|
||||||
|
Tone: "neutral",
|
||||||
|
}
|
||||||
|
|
||||||
|
switch event.EventType {
|
||||||
|
case "new_model":
|
||||||
|
item.Label = "新模型"
|
||||||
|
item.Title = fmt.Sprintf("%s 进入今日情报池", event.ModelName)
|
||||||
|
item.Tone = "info"
|
||||||
|
case "price_cut":
|
||||||
|
item.Label = "价格下调"
|
||||||
|
item.Title = fmt.Sprintf("%s 成本下调 %.0f%%", event.ModelName, abs(event.PriceChangePct))
|
||||||
|
item.Tone = "success"
|
||||||
|
case "price_increase":
|
||||||
|
item.Label = "价格上调"
|
||||||
|
item.Title = fmt.Sprintf("%s 成本上调 %.0f%%", event.ModelName, abs(event.PriceChangePct))
|
||||||
|
item.Tone = "caution"
|
||||||
|
case "free_highlight":
|
||||||
|
item.Label = "免费机会"
|
||||||
|
item.Title = fmt.Sprintf("%s 当前可免费试用", event.ModelName)
|
||||||
|
item.Tone = "warning"
|
||||||
|
default:
|
||||||
|
item.Label = "观察重点"
|
||||||
|
item.Title = fmt.Sprintf("%s 值得关注", event.ModelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
func buildActionItems(r *ReportV3) []ActionItem {
|
func buildActionItems(r *ReportV3) []ActionItem {
|
||||||
var actions []ActionItem
|
var actions []ActionItem
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,29 @@ func TestBuildFreeSourceBreakdown(t *testing.T) {
|
|||||||
|
|
||||||
func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
|
func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
|
||||||
report := sampleReportForV1()
|
report := sampleReportForV1()
|
||||||
|
report.ModelEvents = []ModelEvent{
|
||||||
|
{
|
||||||
|
EventType: "new_model",
|
||||||
|
ModelName: "DeepSeek-V4-Flash",
|
||||||
|
ProviderName: "DeepSeek",
|
||||||
|
OperatorName: "OpenRouter",
|
||||||
|
TrustLabel: "聚合来源",
|
||||||
|
Baseline: "首次出现",
|
||||||
|
Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。",
|
||||||
|
Priority: 95,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EventType: "price_cut",
|
||||||
|
ModelName: "qwen-vl-max",
|
||||||
|
ProviderName: "Alibaba",
|
||||||
|
OperatorName: "DashScope",
|
||||||
|
TrustLabel: "官方来源",
|
||||||
|
Baseline: "较昨日 -18%",
|
||||||
|
Summary: "价格下降已足以影响视觉模型默认选择。",
|
||||||
|
PriceChangePct: -18,
|
||||||
|
Priority: 90,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
decorateReportV1(report)
|
decorateReportV1(report)
|
||||||
|
|
||||||
@@ -200,6 +223,9 @@ func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
|
|||||||
if report.ActionItems[0].Evidence == "" {
|
if report.ActionItems[0].Evidence == "" {
|
||||||
t.Fatalf("expected action item evidence to be populated")
|
t.Fatalf("expected action item evidence to be populated")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(report.HeadlineItems[0].Title, "DeepSeek-V4-Flash") {
|
||||||
|
t.Fatalf("expected first headline to come from model events, got %+v", report.HeadlineItems[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecorateReportV1BuildsCalmDaySummary(t *testing.T) {
|
func TestDecorateReportV1BuildsCalmDaySummary(t *testing.T) {
|
||||||
@@ -285,6 +311,18 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
|||||||
func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||||
path := filepath.Join(t.TempDir(), "daily_report.html")
|
path := filepath.Join(t.TempDir(), "daily_report.html")
|
||||||
report := sampleReportForV1()
|
report := sampleReportForV1()
|
||||||
|
report.ModelEvents = []ModelEvent{
|
||||||
|
{
|
||||||
|
EventType: "new_model",
|
||||||
|
ModelName: "DeepSeek-V4-Flash",
|
||||||
|
ProviderName: "DeepSeek",
|
||||||
|
OperatorName: "OpenRouter",
|
||||||
|
TrustLabel: "聚合来源",
|
||||||
|
Baseline: "首次出现",
|
||||||
|
Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。",
|
||||||
|
Priority: 95,
|
||||||
|
},
|
||||||
|
}
|
||||||
report.TencentSubscriptionPlans = []SubscriptionPlanInfo{
|
report.TencentSubscriptionPlans = []SubscriptionPlanInfo{
|
||||||
{
|
{
|
||||||
PlanName: "通用 Token Plan Lite",
|
PlanName: "通用 Token Plan Lite",
|
||||||
@@ -314,6 +352,8 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
|||||||
"今日一句话结论",
|
"今日一句话结论",
|
||||||
"三条行动建议",
|
"三条行动建议",
|
||||||
"今日头条",
|
"今日头条",
|
||||||
|
"DeepSeek-V4-Flash",
|
||||||
|
"首次出现",
|
||||||
"场景推荐",
|
"场景推荐",
|
||||||
"完整数据附录",
|
"完整数据附录",
|
||||||
"官方免费",
|
"官方免费",
|
||||||
@@ -326,3 +366,86 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: "new_model",
|
||||||
|
ModelName: "DeepSeek-V4-Flash",
|
||||||
|
ProviderName: "DeepSeek",
|
||||||
|
OperatorName: "OpenRouter",
|
||||||
|
TrustLabel: "聚合来源",
|
||||||
|
Baseline: "首次出现",
|
||||||
|
Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。",
|
||||||
|
Priority: 90,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
items := buildHeadlineItems(report)
|
||||||
|
|
||||||
|
if len(items) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 headline items, got %d", len(items))
|
||||||
|
}
|
||||||
|
if !strings.Contains(items[0].Title, "glm-5") {
|
||||||
|
t.Fatalf("expected price_cut event to rank first, got %+v", items[0])
|
||||||
|
}
|
||||||
|
if items[0].Baseline != "较昨日 -25%" {
|
||||||
|
t.Fatalf("expected event baseline 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_increase",
|
||||||
|
ModelName: "OpenAI: GPT-4o",
|
||||||
|
ProviderName: "OpenAI",
|
||||||
|
TrustLabel: "官方来源",
|
||||||
|
Baseline: "较昨日 +5%",
|
||||||
|
Summary: "同日另有上调记录。",
|
||||||
|
PriceChangePct: 5,
|
||||||
|
Priority: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EventType: "new_model",
|
||||||
|
ModelName: "Claude Opus 4.7",
|
||||||
|
ProviderName: "Anthropic",
|
||||||
|
TrustLabel: "聚合来源",
|
||||||
|
Baseline: "首次出现",
|
||||||
|
Summary: "新模型上线。",
|
||||||
|
Priority: 70,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
items := buildHeadlineItems(report)
|
||||||
|
|
||||||
|
if len(items) != 2 {
|
||||||
|
t.Fatalf("expected 2 deduplicated headline items, got %d", len(items))
|
||||||
|
}
|
||||||
|
if strings.Contains(items[1].Title, "GPT-4o") {
|
||||||
|
t.Fatalf("expected duplicate model event to be removed, got %+v", items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user