feat(report): add official release event source
This commit is contained in:
@@ -769,6 +769,12 @@ func loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
|
||||
}
|
||||
events = append(events, newModelEvents...)
|
||||
|
||||
releaseEvents, err := loadOfficialReleaseEvents(db, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, releaseEvents...)
|
||||
|
||||
priceEvents, err := loadPriceChangeEvents(db, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -785,6 +791,98 @@ func loadModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
|
||||
return dedupeModelEvents(events), nil
|
||||
}
|
||||
|
||||
func loadOfficialReleaseEvents(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,
|
||||
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, 'Unknown') AS operator_name,
|
||||
COALESCE(lp.operator_type, 'reseller') AS operator_type,
|
||||
COALESCE(m.source_url, '') AS source_url,
|
||||
COALESCE(mp.country, 'unknown') AS provider_country,
|
||||
COALESCE(m.release_date, m.created_at::date) AS release_date,
|
||||
COALESCE(lp.currency, 'USD') AS currency
|
||||
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 m.release_date = $1::date
|
||||
AND COALESCE(m.source_url, '') <> ''
|
||||
AND COALESCE(lp.operator_type, 'reseller') IN ('official', 'cloud')
|
||||
ORDER BY m.release_date 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
|
||||
sourceURL string
|
||||
providerCountry string
|
||||
releaseDate time.Time
|
||||
currency string
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&modelName,
|
||||
&providerName,
|
||||
&operatorName,
|
||||
&operatorType,
|
||||
&sourceURL,
|
||||
&providerCountry,
|
||||
&releaseDate,
|
||||
¤cy,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
model := ModelInfo{
|
||||
Name: modelName,
|
||||
ProviderName: providerName,
|
||||
ProviderCountry: providerCountry,
|
||||
Currency: currency,
|
||||
OperatorName: operatorName,
|
||||
OperatorType: operatorType,
|
||||
}
|
||||
|
||||
events = append(events, ModelEvent{
|
||||
EventType: "official_release",
|
||||
ModelName: modelName,
|
||||
ProviderName: providerName,
|
||||
OperatorName: operatorName,
|
||||
TrustLabel: buildTrustLabel(model),
|
||||
SourceKindLabel: "官方发布",
|
||||
PrimarySource: sourceURL,
|
||||
UpdatedAt: releaseDate.Format("2006-01-02 15:04"),
|
||||
EvidenceDetail: "models.release_date = 今日,且 source_url 指向官方发布页",
|
||||
Baseline: "官方首次发布",
|
||||
Summary: fmt.Sprintf("%s 官方发布新模型,值得优先复查默认选型。", providerName),
|
||||
Currency: currency,
|
||||
Priority: 120,
|
||||
})
|
||||
}
|
||||
return events, rows.Err()
|
||||
}
|
||||
|
||||
func loadNewModelEvents(db *sql.DB, date string) ([]ModelEvent, error) {
|
||||
rows, err := db.Query(`
|
||||
WITH latest_prices AS (
|
||||
@@ -1079,11 +1177,11 @@ func decorateReportV1(r *ReportV3) {
|
||||
}
|
||||
}
|
||||
|
||||
r.PageMode = buildPageMode(r.DailySignals)
|
||||
r.ModelEvents = enrichModelEvents(r)
|
||||
r.PageMode = buildPageModeWithEvents(r.DailySignals, r.ModelEvents)
|
||||
r.MarketLabels = buildMarketLabels(r)
|
||||
r.HeroSummary, r.HeroEvidence = buildHeroSummary(r)
|
||||
r.SceneSections = buildSceneSections(r)
|
||||
r.ModelEvents = enrichModelEvents(r)
|
||||
r.ActionItems = buildActionItems(r)
|
||||
r.HeadlineItems = buildHeadlineItems(r)
|
||||
r.AppendixLinks = []AppendixLink{
|
||||
@@ -1193,6 +1291,13 @@ func isVerifiedAggregator(name string) bool {
|
||||
}
|
||||
|
||||
func buildPageMode(signals DailySignals) string {
|
||||
return buildPageModeWithEvents(signals, nil)
|
||||
}
|
||||
|
||||
func buildPageModeWithEvents(signals DailySignals, events []ModelEvent) string {
|
||||
if hasEventType(events, "official_release") {
|
||||
return "hot"
|
||||
}
|
||||
if signals.NewModels == 0 && signals.PriceChanges == 0 {
|
||||
return "calm"
|
||||
}
|
||||
@@ -1204,7 +1309,7 @@ func buildPageMode(signals DailySignals) string {
|
||||
|
||||
func buildMarketLabels(r *ReportV3) []string {
|
||||
labels := []string{}
|
||||
switch r.PageMode {
|
||||
switch buildPageModeWithEvents(r.DailySignals, r.ModelEvents) {
|
||||
case "hot":
|
||||
labels = append(labels, "热点日")
|
||||
case "calm":
|
||||
@@ -1212,6 +1317,9 @@ func buildMarketLabels(r *ReportV3) []string {
|
||||
default:
|
||||
labels = append(labels, "常规日")
|
||||
}
|
||||
if hasEventType(r.ModelEvents, "official_release") {
|
||||
labels = append(labels, "官方发布")
|
||||
}
|
||||
if r.DailySignals.NewModels > 0 {
|
||||
labels = append(labels, "新模型日")
|
||||
}
|
||||
@@ -1229,7 +1337,20 @@ func buildMarketLabels(r *ReportV3) []string {
|
||||
return labels
|
||||
}
|
||||
|
||||
func hasEventType(events []ModelEvent, eventType string) bool {
|
||||
for _, event := range events {
|
||||
if event.EventType == eventType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildHeroSummary(r *ReportV3) (string, string) {
|
||||
if official := firstEventByType(r.ModelEvents, "official_release"); official != nil {
|
||||
return fmt.Sprintf("今天最值得关注的是 %s 已出现官方发布信号,优先复查它对默认选型的影响。", official.ModelName),
|
||||
fmt.Sprintf("主来源:%s", official.PrimarySource)
|
||||
}
|
||||
switch r.PageMode {
|
||||
case "hot":
|
||||
return fmt.Sprintf(
|
||||
@@ -1248,6 +1369,15 @@ func buildHeroSummary(r *ReportV3) (string, string) {
|
||||
}
|
||||
}
|
||||
|
||||
func firstEventByType(events []ModelEvent, eventType string) *ModelEvent {
|
||||
for i := range events {
|
||||
if events[i].EventType == eventType {
|
||||
return &events[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildHeadlineItems(r *ReportV3) []HeadlineItem {
|
||||
if items := buildHeadlineItemsFromEvents(r.ModelEvents); len(items) > 0 {
|
||||
return items
|
||||
@@ -1345,6 +1475,10 @@ func headlineItemFromModelEvent(event ModelEvent) HeadlineItem {
|
||||
}
|
||||
|
||||
switch event.EventType {
|
||||
case "official_release":
|
||||
item.Label = "官方发布"
|
||||
item.Title = fmt.Sprintf("%s 官方发布", event.ModelName)
|
||||
item.Tone = "info"
|
||||
case "new_model":
|
||||
item.Label = "新模型"
|
||||
item.Title = fmt.Sprintf("%s 进入今日情报池", event.ModelName)
|
||||
|
||||
@@ -405,6 +405,20 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "official_release",
|
||||
ModelName: "GLM-5",
|
||||
ProviderName: "Zhipu",
|
||||
OperatorName: "Zhipu",
|
||||
TrustLabel: "官方来源",
|
||||
Baseline: "官方首次发布",
|
||||
Summary: "官方发布新模型,值得优先复查中文通用与推理场景默认选择。",
|
||||
SourceKindLabel: "官方发布",
|
||||
PrimarySource: "https://open.bigmodel.cn/dev/howuse/model",
|
||||
UpdatedAt: "2026-05-13 08:30",
|
||||
EvidenceDetail: "models.release_date = 今日,且 source_url 指向官方文档",
|
||||
Priority: 120,
|
||||
},
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "glm-5",
|
||||
@@ -441,14 +455,14 @@ func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) {
|
||||
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 !strings.Contains(items[0].Title, "GLM-5") || items[0].Label != "官方发布" {
|
||||
t.Fatalf("expected official release 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])
|
||||
if items[1].Baseline != "较昨日 -25%" {
|
||||
t.Fatalf("expected price_cut baseline to be preserved, got %+v", items[1])
|
||||
}
|
||||
if items[0].SourceKindLabel != "价格快照" || items[0].PrimarySource != "pricing_history" {
|
||||
t.Fatalf("expected event evidence fields to be preserved, got %+v", items[0])
|
||||
if items[0].SourceKindLabel != "官方发布" || items[0].PrimarySource != "https://open.bigmodel.cn/dev/howuse/model" {
|
||||
t.Fatalf("expected official release evidence fields to be preserved, got %+v", items[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,3 +548,30 @@ func TestHeadlineItemFromModelEventIncludesEvidenceFields(t *testing.T) {
|
||||
t.Fatalf("expected evidence detail to be populated, got %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadlineItemFromOfficialReleaseEvent(t *testing.T) {
|
||||
item := headlineItemFromModelEvent(ModelEvent{
|
||||
EventType: "official_release",
|
||||
ModelName: "Claude Sonnet 4.5",
|
||||
TrustLabel: "官方来源",
|
||||
Baseline: "官方首次发布",
|
||||
Summary: "官方发布新模型。",
|
||||
SourceKindLabel: "官方发布",
|
||||
PrimarySource: "https://docs.anthropic.com/en/release-notes/api",
|
||||
UpdatedAt: "2026-05-13 07:00",
|
||||
EvidenceDetail: "models.release_date = 今日,且 source_url 指向官方发布页",
|
||||
})
|
||||
|
||||
if item.Label != "官方发布" {
|
||||
t.Fatalf("expected label to be 官方发布, got %+v", item)
|
||||
}
|
||||
if !strings.Contains(item.Title, "官方发布") {
|
||||
t.Fatalf("expected title to mention 官方发布, got %+v", item)
|
||||
}
|
||||
if item.SourceKindLabel != "官方发布" {
|
||||
t.Fatalf("expected source kind label to be 官方发布, got %+v", item)
|
||||
}
|
||||
if item.PrimarySource != "https://docs.anthropic.com/en/release-notes/api" {
|
||||
t.Fatalf("expected primary source to be preserved, got %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user