Files
llm-intelligence/scripts/materialize_daily_signals.go

979 lines
28 KiB
Go
Raw Normal View History

//go:build llm_script
package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"log/slog"
"os"
"sort"
"strings"
"time"
_ "github.com/lib/pq"
)
type signalModelInfo struct {
Name string
ProviderName string
ProviderCountry string
ContextLength int
InputPrice float64
OutputPrice float64
Currency string
IsFree bool
OperatorName string
OperatorType string
}
type signalDailySignals struct {
NewModels int `json:"new_models"`
PriceChanges int `json:"price_changes"`
OfficialFree int `json:"official_free"`
AggregatorFree int `json:"aggregator_free"`
UnknownFree int `json:"unknown_free"`
}
type signalModelEvent struct {
EventType string `json:"event_type"`
ModelName string `json:"model_name"`
ProviderName string `json:"provider_name"`
OperatorName string `json:"operator_name"`
Audience string `json:"audience"`
TrustLabel string `json:"trust_label"`
SourceKindLabel string `json:"source_kind_label"`
PrimarySource string `json:"primary_source"`
UpdatedAt string `json:"updated_at"`
EvidenceDetail string `json:"evidence_detail"`
Baseline string `json:"baseline"`
Summary string `json:"summary"`
Currency string `json:"currency"`
OldInputPrice float64 `json:"old_input_price"`
NewInputPrice float64 `json:"new_input_price"`
OldOutputPrice float64 `json:"old_output_price"`
NewOutputPrice float64 `json:"new_output_price"`
PriceChangePct float64 `json:"price_change_pct"`
Priority int `json:"priority"`
}
type signalPromoCampaignDefinition struct {
Date string `json:"date"`
ModelName string `json:"model_name"`
ProviderName string `json:"provider_name"`
OperatorName string `json:"operator_name"`
Summary string `json:"summary"`
Audience string `json:"audience"`
Baseline string `json:"baseline"`
TrustLabel string `json:"trust_label"`
SourceKindLabel string `json:"source_kind_label"`
PrimarySource string `json:"primary_source"`
EvidenceDetail string `json:"evidence_detail"`
Priority int `json:"priority"`
}
type dailySignalSnapshot struct {
SignalDate string
Status string
Signals signalDailySignals
EventCount int
PageMode string
EventTypeCounts map[string]int
TopEvents []signalModelEvent
SourceAudit string
}
type materializeDailySignalsConfig struct {
Date string
SourceAudit string
DryRun bool
}
var signalLogger *slog.Logger
const signalUSDToCNY = 7.25
func init() {
signalLogger = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
}
func main() {
loadSignalEnv()
var cfg materializeDailySignalsConfig
flag.StringVar(&cfg.Date, "date", signalDateValue(), "信号日期,格式 YYYY-MM-DD")
flag.StringVar(&cfg.SourceAudit, "source-audit", os.Getenv("SIGNAL_SOURCE_AUDIT"), "运行审计摘要")
flag.BoolVar(&cfg.DryRun, "dry-run", false, "仅计算并打印摘要,不写入数据库")
flag.Parse()
db, err := sql.Open("postgres", defaultSignalDSN())
if err != nil {
fmt.Fprintf(os.Stderr, "open db: %v\n", err)
os.Exit(1)
}
defer db.Close()
if err := runMaterializeDailySignals(db, cfg); err != nil {
fmt.Fprintf(os.Stderr, "materialize_daily_signals: %v\n", err)
os.Exit(1)
}
}
func loadSignalEnv() {
for _, path := range []string{".env.local", ".env"} {
data, err := os.ReadFile(path)
if err != nil {
continue
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.Trim(strings.TrimSpace(value), `"'`)
if key == "" {
continue
}
if _, exists := os.LookupEnv(key); exists {
continue
}
_ = os.Setenv(key, value)
}
}
}
func defaultSignalDSN() string {
if dsn := os.Getenv("DATABASE_URL"); dsn != "" {
return dsn
}
return "postgres://long@/llm_intelligence?host=/var/run/postgresql"
}
func signalDateValue() string {
if value := strings.TrimSpace(os.Getenv("REPORT_DATE")); value != "" {
return value
}
return time.Now().Format("2006-01-02")
}
func runMaterializeDailySignals(db *sql.DB, cfg materializeDailySignalsConfig) error {
signals, err := loadSignalDailySignals(db, cfg.Date)
if err != nil {
return err
}
freeSignals, err := loadSignalFreeBreakdown(db)
if err != nil {
return err
}
signals.OfficialFree = freeSignals.OfficialFree
signals.AggregatorFree = freeSignals.AggregatorFree
signals.UnknownFree = freeSignals.UnknownFree
events, err := loadSignalModelEvents(db, cfg.Date)
if err != nil {
return err
}
snapshot := dailySignalSnapshot{
SignalDate: cfg.Date,
Status: "generated",
Signals: signals,
EventCount: len(events),
PageMode: buildSignalPageMode(signals, events),
EventTypeCounts: summarizeSignalEventTypes(events),
TopEvents: events,
SourceAudit: strings.TrimSpace(cfg.SourceAudit),
}
if cfg.DryRun {
fmt.Printf("source=daily-signal-materializer date=%s new_models=%d price_changes=%d event_count=%d page_mode=%s dry_run=true\n",
snapshot.SignalDate, snapshot.Signals.NewModels, snapshot.Signals.PriceChanges, snapshot.EventCount, snapshot.PageMode)
return nil
}
if err := upsertDailySignalSnapshot(db, snapshot); err != nil {
return err
}
fmt.Printf("source=daily-signal-materializer date=%s new_models=%d price_changes=%d event_count=%d page_mode=%s dry_run=false\n",
snapshot.SignalDate, snapshot.Signals.NewModels, snapshot.Signals.PriceChanges, snapshot.EventCount, snapshot.PageMode)
return nil
}
func upsertDailySignalSnapshot(db *sql.DB, snapshot dailySignalSnapshot) error {
eventTypeCounts, err := json.Marshal(snapshot.EventTypeCounts)
if err != nil {
return fmt.Errorf("marshal event_type_counts: %w", err)
}
topEvents, err := json.Marshal(snapshot.TopEvents)
if err != nil {
return fmt.Errorf("marshal top_events: %w", err)
}
_, err = db.Exec(
`INSERT INTO daily_signal_snapshot (
signal_date, status, new_models, price_changes,
official_free, aggregator_free, unknown_free,
event_count, page_mode, event_type_counts, top_events, source_audit
) VALUES (
$1::date, $2, $3, $4,
$5, $6, $7,
$8, $9, $10::jsonb, $11::jsonb, $12
)
ON CONFLICT (signal_date)
DO UPDATE SET
status = EXCLUDED.status,
new_models = EXCLUDED.new_models,
price_changes = EXCLUDED.price_changes,
official_free = EXCLUDED.official_free,
aggregator_free = EXCLUDED.aggregator_free,
unknown_free = EXCLUDED.unknown_free,
event_count = EXCLUDED.event_count,
page_mode = EXCLUDED.page_mode,
event_type_counts = EXCLUDED.event_type_counts,
top_events = EXCLUDED.top_events,
source_audit = EXCLUDED.source_audit,
generated_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP`,
snapshot.SignalDate, snapshot.Status, snapshot.Signals.NewModels, snapshot.Signals.PriceChanges,
snapshot.Signals.OfficialFree, snapshot.Signals.AggregatorFree, snapshot.Signals.UnknownFree,
snapshot.EventCount, snapshot.PageMode, string(eventTypeCounts), string(topEvents), signalNullIfBlank(snapshot.SourceAudit),
)
if err != nil {
return fmt.Errorf("upsert daily_signal_snapshot: %w", err)
}
return nil
}
func loadSignalDailySignals(db *sql.DB, date string) (signalDailySignals, error) {
signals := signalDailySignals{}
if err := db.QueryRow(`
SELECT COUNT(*)
FROM models
WHERE deleted_at IS NULL
AND DATE(created_at) = $1::date
`, date).Scan(&signals.NewModels); err != nil {
return signals, err
}
if err := db.QueryRow(`
SELECT COUNT(*)
FROM pricing_history
WHERE DATE(changed_at) = $1::date
`, date).Scan(&signals.PriceChanges); err != nil {
return signals, err
}
return signals, nil
}
func loadSignalFreeBreakdown(db *sql.DB) (signalDailySignals, 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(mp.country, 'unknown') AS provider_country,
COALESCE(m.context_length, 0) AS context_length,
COALESCE(lp.input_price_per_mtok, 0) AS input_price,
COALESCE(lp.output_price_per_mtok, 0) AS output_price,
COALESCE(lp.currency, 'USD') AS currency,
COALESCE(lp.operator_name, 'OpenRouter') AS operator_name,
COALESCE(lp.operator_type, 'reseller') AS operator_type
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 COALESCE(lp.is_free, false) = true
`)
if err != nil {
return signalDailySignals{}, err
}
defer rows.Close()
signals := signalDailySignals{}
for rows.Next() {
var model signalModelInfo
if err := rows.Scan(
&model.Name,
&model.ProviderName,
&model.ProviderCountry,
&model.ContextLength,
&model.InputPrice,
&model.OutputPrice,
&model.Currency,
&model.OperatorName,
&model.OperatorType,
); err != nil {
return signalDailySignals{}, err
}
switch classifySignalFreeSource(model) {
case "官方免费":
signals.OfficialFree++
case "聚合免费":
signals.AggregatorFree++
default:
signals.UnknownFree++
}
}
return signals, rows.Err()
}
func loadSignalModelEvents(db *sql.DB, date string) ([]signalModelEvent, error) {
var events []signalModelEvent
newModelEvents, err := loadSignalNewModelEvents(db, date)
if err != nil {
return nil, err
}
events = append(events, newModelEvents...)
releaseEvents, err := loadSignalOfficialReleaseEvents(db, date)
if err != nil {
return nil, err
}
events = append(events, releaseEvents...)
promoEvents, err := loadSignalPromoCampaignEvents(date)
if err != nil {
return nil, err
}
events = append(events, promoEvents...)
priceEvents, err := loadSignalPriceChangeEvents(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 dedupeSignalEvents(events), nil
}
func loadSignalPromoCampaignEvents(date string) ([]signalModelEvent, error) {
path, err := resolveSignalPromoCampaignDataPath()
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
body, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var definitions []signalPromoCampaignDefinition
if err := json.Unmarshal(body, &definitions); err != nil {
return nil, err
}
events := make([]signalModelEvent, 0)
for _, definition := range definitions {
if definition.Date != date {
continue
}
events = append(events, signalModelEvent{
EventType: "promo_campaign",
ModelName: definition.ModelName,
ProviderName: definition.ProviderName,
OperatorName: definition.OperatorName,
Audience: signalFirstNonEmpty(definition.Audience, "适合计划利用活动窗口压低成本的团队"),
TrustLabel: signalFirstNonEmpty(definition.TrustLabel, "官方来源 / 一级证据"),
SourceKindLabel: signalFirstNonEmpty(definition.SourceKindLabel, "官方活动页"),
PrimarySource: definition.PrimarySource,
UpdatedAt: signalFormatEventUpdatedAt("", definition.Date),
EvidenceDetail: definition.EvidenceDetail,
Baseline: signalFirstNonEmpty(definition.Baseline, "活动窗口开启"),
Summary: definition.Summary,
Priority: signalMaxInt(definition.Priority, 115),
})
}
return events, nil
}
func resolveSignalPromoCampaignDataPath() (string, error) {
candidates := []string{
filepathJoin("scripts", "testdata", "report_promo_campaigns.json"),
filepathJoin("testdata", "report_promo_campaigns.json"),
}
for _, candidate := range candidates {
if _, err := os.Stat(candidate); err == nil {
return candidate, nil
}
}
return "", os.ErrNotExist
}
func loadSignalOfficialReleaseEvents(db *sql.DB, date string) ([]signalModelEvent, 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(m.date_confidence, 'unknown') AS date_confidence,
COALESCE(m.date_source_kind, 'unknown') AS date_source_kind,
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 []signalModelEvent
for rows.Next() {
var (
modelName string
providerName string
operatorName string
operatorType string
sourceURL string
dateConfidence string
dateSourceKind string
providerCountry string
releaseDate time.Time
currency string
)
if err := rows.Scan(
&modelName,
&providerName,
&operatorName,
&operatorType,
&sourceURL,
&dateConfidence,
&dateSourceKind,
&providerCountry,
&releaseDate,
&currency,
); err != nil {
return nil, err
}
model := signalModelInfo{
Name: modelName,
ProviderName: providerName,
ProviderCountry: providerCountry,
Currency: currency,
OperatorName: operatorName,
OperatorType: operatorType,
}
events = append(events, signalModelEvent{
EventType: "official_release",
ModelName: modelName,
ProviderName: providerName,
OperatorName: operatorName,
Audience: "适合需要复查默认选型与路线图判断的团队",
TrustLabel: buildSignalReleaseTrustLabel(model, dateConfidence),
SourceKindLabel: buildSignalReleaseSourceKindLabel(dateSourceKind, dateConfidence),
PrimarySource: sourceURL,
UpdatedAt: releaseDate.Format("2006-01-02 15:04"),
EvidenceDetail: buildSignalReleaseEvidenceDetail(dateSourceKind, dateConfidence),
Baseline: "官方首次发布",
Summary: fmt.Sprintf("%s 官方发布新模型,值得优先复查默认选型。", providerName),
Currency: currency,
Priority: 120,
})
}
return events, rows.Err()
}
func loadSignalNewModelEvents(db *sql.DB, date string) ([]signalModelEvent, 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,
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
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 []signalModelEvent
for rows.Next() {
var model signalModelInfo
var createdAt time.Time
if err := rows.Scan(
&model.Name,
&model.ProviderName,
&model.OperatorName,
&model.OperatorType,
&model.Currency,
&model.InputPrice,
&model.OutputPrice,
&model.IsFree,
&model.ContextLength,
&model.ProviderCountry,
&createdAt,
); err != nil {
return nil, err
}
summary := "新模型进入情报池,值得重新评估当前默认选择。"
if model.IsFree {
summary = fmt.Sprintf("新模型首日可免费试用,需注意其免费来源属于%s。", classifySignalFreeSource(model))
} else if model.ContextLength >= 1024*256 {
summary = fmt.Sprintf("新模型带来 %s 长上下文,值得复查 Agent 和代码场景。", signalFormatContextWindowCompact(model.ContextLength))
}
events = append(events, signalModelEvent{
EventType: "new_model",
ModelName: model.Name,
ProviderName: model.ProviderName,
OperatorName: model.OperatorName,
Audience: "适合想尽快验证新模型价值的选型读者",
TrustLabel: buildSignalTrustLabel(model),
SourceKindLabel: "模型快照",
PrimarySource: buildSignalPrimarySource("region_pricing", model.OperatorName),
UpdatedAt: createdAt.Format("2006-01-02 15:04"),
EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照",
Baseline: "首次出现",
Summary: summary,
Currency: model.Currency,
NewInputPrice: model.InputPrice,
NewOutputPrice: model.OutputPrice,
Priority: 85 + signalMinInt(model.ContextLength/(1024*128), 10),
})
}
return events, rows.Err()
}
func loadSignalPriceChangeEvents(db *sql.DB, date string) ([]signalModelEvent, 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,
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
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 []signalModelEvent
for rows.Next() {
var (
model signalModelInfo
oldInputPrice float64
newInputPrice float64
oldOutputPrice float64
newOutputPrice float64
changedAt time.Time
)
if err := rows.Scan(
&model.Name,
&model.ProviderName,
&model.OperatorName,
&model.OperatorType,
&model.Currency,
&oldInputPrice,
&newInputPrice,
&oldOutputPrice,
&newOutputPrice,
&model.ProviderCountry,
&changedAt,
); err != nil {
return nil, err
}
changePct := signalSignedPriceChangePct(oldInputPrice, newInputPrice, oldOutputPrice, newOutputPrice)
if changePct == 0 {
continue
}
eventType := "price_increase"
summary := "价格上调已足以影响默认成本,需要确认备用模型。"
if changePct < 0 {
eventType = "price_cut"
summary = "价格下降已足以影响默认选型,值得重新评估同类模型。"
}
events = append(events, signalModelEvent{
EventType: eventType,
ModelName: model.Name,
ProviderName: model.ProviderName,
OperatorName: model.OperatorName,
Audience: buildSignalPriceEventAudience(changePct),
TrustLabel: buildSignalTrustLabel(model),
SourceKindLabel: "价格快照",
PrimarySource: "pricing_history",
UpdatedAt: changedAt.Format("2006-01-02 15:04"),
EvidenceDetail: buildSignalPriceEvidenceDetail(changePct, oldInputPrice, newInputPrice, model.Currency),
Baseline: fmt.Sprintf("较昨日 %+.0f%%", changePct),
Summary: summary,
Currency: model.Currency,
OldInputPrice: oldInputPrice,
NewInputPrice: newInputPrice,
OldOutputPrice: oldOutputPrice,
NewOutputPrice: newOutputPrice,
PriceChangePct: changePct,
Priority: 70 + signalMinInt(int(signalAbs(changePct)), 25),
})
}
return events, rows.Err()
}
func summarizeSignalEventTypes(events []signalModelEvent) map[string]int {
counts := make(map[string]int)
for _, event := range events {
counts[event.EventType]++
}
return counts
}
func dedupeSignalEvents(events []signalModelEvent) []signalModelEvent {
seen := make(map[string]struct{})
result := make([]signalModelEvent, 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 classifySignalFreeSource(model signalModelInfo) string {
switch model.OperatorType {
case "official", "cloud":
return "官方免费"
case "reseller":
if isSignalVerifiedAggregator(model.OperatorName) {
return "聚合免费"
}
}
return "待确认"
}
func isSignalVerifiedAggregator(name string) bool {
switch strings.ToLower(strings.TrimSpace(name)) {
case "openrouter", "siliconflow", "fireworks", "groq":
return true
default:
return false
}
}
func buildSignalPageMode(signals signalDailySignals, events []signalModelEvent) string {
if hasSignalEventType(events, "official_release") || hasSignalEventType(events, "promo_campaign") {
return "hot"
}
if signals.NewModels == 0 && signals.PriceChanges == 0 {
return "calm"
}
if signals.NewModels+signals.PriceChanges >= 3 {
return "hot"
}
return "standard"
}
func hasSignalEventType(events []signalModelEvent, eventType string) bool {
for _, event := range events {
if event.EventType == eventType {
return true
}
}
return false
}
func buildSignalTrustLabel(model signalModelInfo) string {
switch model.OperatorType {
case "official", "cloud":
return "官方来源"
case "reseller":
if isSignalVerifiedAggregator(model.OperatorName) {
return "聚合来源"
}
}
return "待验证来源"
}
func buildSignalPrimarySource(sourceKind, operatorName string) string {
switch sourceKind {
case "region_pricing":
if operatorName == "" {
return "region_pricing"
}
return operatorName + " / region_pricing"
default:
return sourceKind
}
}
func buildSignalPriceEvidenceDetail(changePct, oldPrice, newPrice float64, currency string) string {
direction := "上涨"
if changePct < 0 {
direction = "下降"
}
return fmt.Sprintf(
"pricing_history 记录到输入价格由 %s 调整为 %s较昨日%s %.0f%%",
signalFormatPrice(oldPrice, currency),
signalFormatPrice(newPrice, currency),
direction,
signalAbs(changePct),
)
}
func buildSignalReleaseSourceKindLabel(dateSourceKind, dateConfidence string) string {
switch {
case dateSourceKind == "secondary_authoritative_report" || dateConfidence == "secondary_authoritative":
return "二级权威佐证发布"
case dateSourceKind == "official_announcement" && dateConfidence == "official_primary":
return "一级官方发布"
case dateSourceKind == "official_product_page":
return "官方产品页"
case dateSourceKind == "catalog_backfill":
return "目录回填"
default:
return "一级官方发布"
}
}
func buildSignalReleaseEvidenceDetail(dateSourceKind, dateConfidence string) string {
switch {
case dateSourceKind == "secondary_authoritative_report" || dateConfidence == "secondary_authoritative":
return "models.release_date = 今日,发布日期采用次级权威报道佐证,模型来源页保留官方文档"
case dateSourceKind == "official_announcement" && dateConfidence == "official_primary":
return "models.release_date = 今日,且 source_url 指向官方发布页"
case dateSourceKind == "official_product_page":
return "models.release_date = 今日,来源页为官方产品页,发布日期置信度待确认"
case dateSourceKind == "catalog_backfill":
return "models.release_date = 今日,发布日期来自目录级元数据回填"
default:
return "models.release_date = 今日,且已记录发布日期证据元数据"
}
}
func buildSignalReleaseTrustLabel(model signalModelInfo, dateConfidence string) string {
base := buildSignalTrustLabel(model)
switch dateConfidence {
case "official_primary":
return base + " / 一级证据"
case "secondary_authoritative":
return base + " / 二级佐证"
default:
return base
}
}
func buildSignalPriceEventAudience(changePct float64) string {
if changePct < 0 {
return "适合以成本为先、准备趁降价重排默认选型的团队"
}
return "适合需要提前准备替代模型和预算回退方案的团队"
}
func signalFormatEventUpdatedAt(value, fallbackDate string) string {
if strings.TrimSpace(value) != "" {
return value
}
if fallbackDate != "" {
return fallbackDate + " 00:00"
}
return "-"
}
func signalFormatPrice(price float64, currency string) string {
if price <= 0 {
return "免费"
}
if currency == "CNY" {
if price < 1 {
return fmt.Sprintf("¥%.2f", price)
}
return fmt.Sprintf("¥%.1f", price)
}
cny := price * signalUSDToCNY
if cny < 1 {
return fmt.Sprintf("¥%.2f", cny)
}
return fmt.Sprintf("¥%.1f", cny)
}
func signalFormatContextWindowCompact(value int) string {
if value <= 0 {
return "-"
}
if value%(1024*1024) == 0 {
return fmt.Sprintf("%dM", value/(1024*1024))
}
if value%1024 == 0 {
return fmt.Sprintf("%dK", value/1024)
}
return fmt.Sprintf("%d", value)
}
func signalSignedPriceChangePct(oldInput, newInput, oldOutput, newOutput float64) float64 {
inputPct := signalSignedChange(oldInput, newInput)
outputPct := signalSignedChange(oldOutput, newOutput)
if signalAbs(inputPct) >= signalAbs(outputPct) {
return inputPct
}
return outputPct
}
func signalSignedChange(oldValue, newValue float64) float64 {
if oldValue == 0 {
if newValue == 0 {
return 0
}
return 100
}
return ((newValue - oldValue) / oldValue) * 100
}
func signalFirstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func signalAbs(v float64) float64 {
if v < 0 {
return -v
}
return v
}
func signalMinInt(a, b int) int {
if a < b {
return a
}
return b
}
func signalMaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func filepathJoin(parts ...string) string {
return strings.Join(parts, string(os.PathSeparator))
}
func signalNullIfBlank(value string) any {
if strings.TrimSpace(value) == "" {
return nil
}
return value
}