feat(report): improve daily intelligence UX and price tracking
This commit is contained in:
@@ -215,7 +215,7 @@ func fetchModels(cfg Config) ([]ModelInfo, error) {
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
lastErr = fmt.Errorf("非 200 响应: %d %s", resp.StatusCode, string(body))
|
||||
lastErr = retry.HTTPStatusError{StatusCode: resp.StatusCode, Body: string(body)}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
@@ -287,6 +287,38 @@ func parseModels(raw []byte) ([]ModelInfo, error) {
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func deriveModality(model ModelInfo) string {
|
||||
for _, capability := range model.Capabilities {
|
||||
normalized := strings.ToLower(capability)
|
||||
switch {
|
||||
case strings.Contains(normalized, "vision"), strings.Contains(normalized, "image"):
|
||||
return "multimodal"
|
||||
case strings.Contains(normalized, "audio"):
|
||||
return "audio"
|
||||
case strings.Contains(normalized, "video"):
|
||||
return "video"
|
||||
case strings.Contains(normalized, "code"):
|
||||
return "code"
|
||||
}
|
||||
}
|
||||
|
||||
hints := strings.ToLower(strings.Join([]string{model.ID, model.Name, model.Description}, " "))
|
||||
switch {
|
||||
case strings.Contains(hints, "video") && (strings.Contains(hints, "omni") || strings.Contains(hints, "vision") || strings.Contains(hints, "multimodal")):
|
||||
return "multimodal"
|
||||
case strings.Contains(hints, "vision") || strings.Contains(hints, "image") || strings.Contains(hints, "vl") || strings.Contains(hints, "omni") || strings.Contains(hints, "multimodal"):
|
||||
return "multimodal"
|
||||
case strings.Contains(hints, "audio") || strings.Contains(hints, "speech") || strings.Contains(hints, "voice"):
|
||||
return "audio"
|
||||
case strings.Contains(hints, "video"):
|
||||
return "video"
|
||||
case strings.Contains(hints, "code"):
|
||||
return "code"
|
||||
default:
|
||||
return "text"
|
||||
}
|
||||
}
|
||||
|
||||
func getString(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
@@ -443,7 +475,7 @@ func summarizeDB(connStr string, models []ModelInfo, batchSize int) error {
|
||||
`,
|
||||
"openrouter", m.ID, m.Name, m.Description, m.ContextLength,
|
||||
jsonCapabilities(m.Capabilities), m.Created, isFree, "active",
|
||||
rawPayload(m), providerID, "", "text",
|
||||
rawPayload(m), providerID, "", deriveModality(m),
|
||||
"official", now, batchID, collectorVersion,
|
||||
"https://openrouter.ai/api/v1/models", now).Scan(&modelID)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"llm-intelligence/internal/retry"
|
||||
)
|
||||
|
||||
// Test 1: parseModels 正确解析 name、context_length、capabilities、pricing input/prompt 和 output/completion
|
||||
@@ -48,6 +50,10 @@ func TestParseModels(t *testing.T) {
|
||||
if m.Pricing.Output != 10.0 {
|
||||
t.Errorf("Pricing.Output 错误: %f", m.Pricing.Output)
|
||||
}
|
||||
if modality := deriveModality(m); modality != "multimodal" {
|
||||
t.Errorf("deriveModality = %q, want %q", modality, "multimodal")
|
||||
|
||||
}
|
||||
|
||||
// 第二条:pricing 用 prompt/completion 别名回退
|
||||
m2 := models[1]
|
||||
@@ -65,6 +71,68 @@ func TestParseModels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveModality(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
capabilities []string
|
||||
want string
|
||||
}{
|
||||
{name: "vision first", capabilities: []string{"vision", "json_mode"}, want: "multimodal"},
|
||||
{name: "audio", capabilities: []string{"audio_generation"}, want: "audio"},
|
||||
{name: "code", capabilities: []string{"code_interpreter"}, want: "code"},
|
||||
{name: "text fallback", capabilities: []string{"function_calling"}, want: "text"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := deriveModality(ModelInfo{Capabilities: tt.capabilities}); got != tt.want {
|
||||
t.Fatalf("deriveModality() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveModalityInfersFromModelIdentityWithoutCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
model ModelInfo
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "omni id maps to multimodal",
|
||||
model: ModelInfo{
|
||||
ID: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
|
||||
Description: "accepts text, image, video, and audio inputs",
|
||||
},
|
||||
want: "multimodal",
|
||||
},
|
||||
{
|
||||
name: "audio id maps to audio",
|
||||
model: ModelInfo{
|
||||
ID: "openai/gpt-audio",
|
||||
Description: "audio model for natural sounding voices",
|
||||
},
|
||||
want: "audio",
|
||||
},
|
||||
{
|
||||
name: "vl id maps to multimodal",
|
||||
model: ModelInfo{
|
||||
ID: "qwen/qwen3-vl-32b-instruct",
|
||||
Description: "vision-language model for text, images, and video",
|
||||
},
|
||||
want: "multimodal",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := deriveModality(tt.model); got != tt.want {
|
||||
t.Fatalf("deriveModality(%+v) = %q, want %q", tt.model, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: run 无 API Key 时写入临时文件,JSON 含 total 和 models 字段
|
||||
func TestRunNoAPIKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
@@ -108,6 +176,60 @@ func TestFetchModelsFailsInStrictRealModeWithoutAPIKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchModelsDoesNotRetryPermanentHTTPErrors(t *testing.T) {
|
||||
attempts := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
attempts++
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := fetchModels(Config{
|
||||
APIKey: "test-key",
|
||||
APIURL: server.URL,
|
||||
MaxRetries: 3,
|
||||
TimeoutSec: 1,
|
||||
StrictReal: true,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected fetchModels to fail on 403")
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Fatalf("expected 1 attempt for permanent HTTP error, got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchModelsRetriesServerErrors(t *testing.T) {
|
||||
attempts := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
http.Error(w, "temporary", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":"openai/gpt-4o","name":"GPT-4o","context_length":128000,"pricing":{"input":2.5,"output":10.0}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
models, err := fetchModels(Config{
|
||||
APIKey: "test-key",
|
||||
APIURL: server.URL,
|
||||
MaxRetries: 3,
|
||||
TimeoutSec: 1,
|
||||
StrictReal: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected retry success, got %v", err)
|
||||
}
|
||||
if len(models) != 1 {
|
||||
t.Fatalf("expected 1 model, got %d", len(models))
|
||||
}
|
||||
if attempts != 3 {
|
||||
t.Fatalf("expected 3 attempts for temporary server error, got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFailsInStrictRealModeWhenDBWriteFails(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outPath := filepath.Join(tmpDir, "models.json")
|
||||
@@ -130,3 +252,12 @@ func TestRunFailsInStrictRealModeWhenDBWriteFails(t *testing.T) {
|
||||
t.Fatal("strict real mode should fail when database write fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryHTTPStatusErrorClassification(t *testing.T) {
|
||||
if retry.IsRetryable(retry.HTTPStatusError{StatusCode: http.StatusForbidden}) {
|
||||
t.Fatal("403 should not be retryable")
|
||||
}
|
||||
if !retry.IsRetryable(retry.HTTPStatusError{StatusCode: http.StatusBadGateway}) {
|
||||
t.Fatal("502 should be retryable")
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -161,6 +162,45 @@ func sampleReportForV1() *ReportV3 {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildModelSelectionsSeparatesTopListsFromAppendixLists(t *testing.T) {
|
||||
intlModels := []ModelInfo{
|
||||
{Name: "intl-1", InputPrice: 0.1, Currency: "USD"},
|
||||
{Name: "intl-2", InputPrice: 0.2, Currency: "USD"},
|
||||
{Name: "intl-3", InputPrice: 0.3, Currency: "USD"},
|
||||
{Name: "intl-4", InputPrice: 0.4, Currency: "USD"},
|
||||
{Name: "intl-5", InputPrice: 0.5, Currency: "USD"},
|
||||
{Name: "intl-6", InputPrice: 0.6, Currency: "USD"},
|
||||
}
|
||||
var domesticModels []ModelInfo
|
||||
for i := 0; i < 14; i++ {
|
||||
domesticModels = append(domesticModels, ModelInfo{
|
||||
Name: fmt.Sprintf("domestic-%02d", i+1),
|
||||
InputPrice: float64(i + 1),
|
||||
Currency: "CNY",
|
||||
ContextLength: 65536,
|
||||
})
|
||||
}
|
||||
selections := buildModelSelections(intlModels, domesticModels, nil)
|
||||
if len(selections.IntlTop5) != 5 {
|
||||
t.Fatalf("expected intl top5 length 5, got %d", len(selections.IntlTop5))
|
||||
}
|
||||
if len(selections.IntlAppendixList) != 6 {
|
||||
t.Fatalf("expected intl appendix length 6, got %d", len(selections.IntlAppendixList))
|
||||
}
|
||||
if len(selections.DomesticTop10) != 10 {
|
||||
t.Fatalf("expected domestic top10 length 10, got %d", len(selections.DomesticTop10))
|
||||
}
|
||||
if len(selections.DomesticAppendixList) != 14 {
|
||||
t.Fatalf("expected domestic appendix length 14, got %d", len(selections.DomesticAppendixList))
|
||||
}
|
||||
if selections.DomesticTop10[0].Name != "domestic-01" || selections.DomesticTop10[9].Name != "domestic-10" {
|
||||
t.Fatalf("domestic top10 ordering mismatch: first=%s tenth=%s", selections.DomesticTop10[0].Name, selections.DomesticTop10[9].Name)
|
||||
}
|
||||
if selections.DomesticAppendixList[13].Name != "domestic-14" {
|
||||
t.Fatalf("domestic appendix should preserve full list, got tail=%s", selections.DomesticAppendixList[13].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFreeSourceBreakdown(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
|
||||
@@ -357,12 +397,14 @@ func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
|
||||
EventType: "price_cut",
|
||||
ModelName: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
ProviderCountry: "CN",
|
||||
OperatorName: "DashScope",
|
||||
TrustLabel: "官方来源",
|
||||
Baseline: "较昨日 -18%",
|
||||
Summary: "价格下降已足以影响视觉模型默认选择。",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
SourceURL: "https://dashscope.aliyun.com/model/qwen-vl-max",
|
||||
UpdatedAt: "2026-05-13 10:00",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
|
||||
PriceChangePct: -18,
|
||||
@@ -375,9 +417,10 @@ func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
|
||||
if report.PageMode != "hot" {
|
||||
t.Fatalf("expected hot page mode, got %q", report.PageMode)
|
||||
}
|
||||
if !strings.Contains(report.HeroSummary, "GLM-5 已出现官方发布信号") {
|
||||
t.Fatalf("hero summary missing official release signal: %s", report.HeroSummary)
|
||||
if !strings.Contains(report.HeroSummary, "qwen-vl-max") || !strings.Contains(report.HeroSummary, "CN / Alibaba / DashScope") || !strings.Contains(report.HeroSummary, "价格下降") {
|
||||
t.Fatalf("hero summary should prioritize price change signal with org metadata, got %s", report.HeroSummary)
|
||||
}
|
||||
|
||||
if len(report.ActionItems) != 3 {
|
||||
t.Fatalf("expected 3 action items, got %d", len(report.ActionItems))
|
||||
}
|
||||
@@ -387,9 +430,14 @@ func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
|
||||
if report.ActionItems[0].Evidence == "" {
|
||||
t.Fatalf("expected action item evidence to be populated")
|
||||
}
|
||||
if !strings.Contains(report.HeadlineItems[0].Title, "GLM-5") {
|
||||
t.Fatalf("expected first headline to prioritize official release, got %+v", report.HeadlineItems[0])
|
||||
if report.HeadlineItems[0].ProviderCountry == "" {
|
||||
t.Fatalf("expected headline item country metadata, got %+v", report.HeadlineItems[0])
|
||||
}
|
||||
|
||||
if report.HeadlineItems[0].Label != "价格下调" || !strings.Contains(report.HeadlineItems[0].Title, "qwen-vl-max") {
|
||||
t.Fatalf("expected first headline to prioritize price cut, got %+v", report.HeadlineItems[0])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDecorateReportV1BuildsCalmDaySummary(t *testing.T) {
|
||||
@@ -403,6 +451,7 @@ func TestDecorateReportV1BuildsCalmDaySummary(t *testing.T) {
|
||||
}
|
||||
if !strings.Contains(report.HeroSummary, "稳定") {
|
||||
t.Fatalf("expected calm day summary to emphasize stability, got %s", report.HeroSummary)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,12 +495,14 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
EventType: "official_release",
|
||||
ModelName: "GLM-5",
|
||||
ProviderName: "Zhipu",
|
||||
ProviderCountry: "CN",
|
||||
OperatorName: "Zhipu",
|
||||
TrustLabel: "官方来源 / 一级证据",
|
||||
Baseline: "官方首次发布",
|
||||
Summary: "官方发布新模型,值得优先复查中文通用与推理场景默认选择。",
|
||||
SourceKindLabel: "一级官方发布",
|
||||
PrimarySource: "https://open.bigmodel.cn/dev/howuse/model",
|
||||
SourceURL: "https://open.bigmodel.cn/dev/howuse/model",
|
||||
UpdatedAt: "2026-05-13 08:30",
|
||||
EvidenceDetail: "models.release_date = 今日,且 source_url 指向官方文档",
|
||||
Priority: 120,
|
||||
@@ -474,6 +525,7 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
EventType: "new_model",
|
||||
ModelName: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
ProviderCountry: "CN",
|
||||
OperatorName: "OpenRouter",
|
||||
Audience: "适合想尽快验证新模型价值的选型读者",
|
||||
TrustLabel: "聚合来源",
|
||||
@@ -481,6 +533,7 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
Summary: "新模型进入情报池,值得重新评估低成本编码默认选择。",
|
||||
SourceKindLabel: "模型快照",
|
||||
PrimarySource: "OpenRouter / region_pricing",
|
||||
SourceURL: "https://openrouter.ai/models/deepseek/deepseek-v4-flash",
|
||||
UpdatedAt: "2026-05-13 09:30",
|
||||
EvidenceDetail: "models.created_at = 今日,且已存在最新价格快照",
|
||||
Priority: 95,
|
||||
@@ -500,6 +553,25 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
EvidenceDetail: "官方活动页记录 V3.2-Exp 在活动窗口内价格下调 50%+",
|
||||
Priority: 115,
|
||||
},
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
ProviderCountry: "CN",
|
||||
|
||||
OperatorName: "DashScope",
|
||||
Audience: "适合需要当天重排价格带的团队",
|
||||
TrustLabel: "官方来源",
|
||||
Baseline: "较昨日 -18%",
|
||||
Summary: "视觉模型价格下降已足以影响默认选型。",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
SourceURL: "https://dashscope.aliyun.com/model/qwen-vl-max",
|
||||
UpdatedAt: "2026-05-13 10:00",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
|
||||
PriceChangePct: -18,
|
||||
Priority: 100,
|
||||
},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
@@ -511,26 +583,31 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read markdown output: %v", err)
|
||||
}
|
||||
|
||||
content := string(body)
|
||||
if !strings.Contains(content, "### 国内模型 TOP 10") {
|
||||
t.Fatalf("markdown missing domestic top10 heading\n%s", content)
|
||||
}
|
||||
|
||||
content = string(body)
|
||||
for _, want := range []string{
|
||||
"## 今日结论",
|
||||
"## 今日行动建议",
|
||||
"## 今日变化",
|
||||
"## 今日价格新闻",
|
||||
"### ↓ Opportunity · 降价机会",
|
||||
"qwen-vl-max",
|
||||
"## 场景推荐",
|
||||
"## 完整数据附录",
|
||||
"- 影响对象:",
|
||||
"营销活动",
|
||||
"主来源: OpenRouter / region_pricing",
|
||||
"更新时间: 2026-05-13 09:30",
|
||||
"判定依据: models.created_at = 今日,且已存在最新价格快照",
|
||||
"## 💳 中转平台套餐订阅价",
|
||||
|
||||
"通用 Token Plan Lite",
|
||||
"Hy Token Plan Max",
|
||||
"¥39.00/月",
|
||||
"3500万 Tokens/月",
|
||||
"256K",
|
||||
} {
|
||||
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("markdown missing %q\n%s", want, content)
|
||||
}
|
||||
@@ -599,6 +676,22 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
EvidenceDetail: "官方活动页记录 V3.2-Exp 在活动窗口内价格下调 50%+",
|
||||
Priority: 115,
|
||||
},
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
OperatorName: "DashScope",
|
||||
Audience: "适合需要当天重排价格带的团队",
|
||||
TrustLabel: "官方来源",
|
||||
Baseline: "较昨日 -18%",
|
||||
Summary: "视觉模型价格下降已足以影响默认选型。",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 10:00",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
|
||||
PriceChangePct: -18,
|
||||
Priority: 100,
|
||||
},
|
||||
}
|
||||
report.TencentSubscriptionPlans = []SubscriptionPlanInfo{
|
||||
{
|
||||
@@ -628,17 +721,17 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
for _, want := range []string{
|
||||
"今日一句话结论",
|
||||
"三条行动建议",
|
||||
"今日价格新闻",
|
||||
"降价机会",
|
||||
"今日头条",
|
||||
"DeepSeek-V4-Flash",
|
||||
"一级官方发布",
|
||||
"二级权威佐证",
|
||||
"营销活动",
|
||||
"影响对象",
|
||||
"首次出现",
|
||||
"主来源",
|
||||
"更新时间",
|
||||
"判定依据",
|
||||
"模型快照",
|
||||
"场景推荐",
|
||||
"完整数据附录",
|
||||
"官方免费",
|
||||
@@ -652,6 +745,97 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownV3IncludesLinkedHeroAndHeadline(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report_markdown_links.md")
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
ProviderCountry: "CN",
|
||||
OperatorName: "DashScope",
|
||||
TrustLabel: "官方来源",
|
||||
Baseline: "较昨日 -18%",
|
||||
Summary: "视觉模型价格下降已足以影响默认选型。",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
SourceURL: "https://dashscope.aliyun.com/model/qwen-vl-max",
|
||||
UpdatedAt: "2026-05-13 10:00",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
|
||||
PriceChangePct: -18,
|
||||
Priority: 100,
|
||||
},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
if err := generateMarkdownV3(report, path); err != nil {
|
||||
t.Fatalf("generateMarkdownV3 returned error: %v", err)
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read markdown output: %v", err)
|
||||
}
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"> [今天最值得关注的是 qwen-vl-max(CN / Alibaba / DashScope)价格下降 18%,优先复查它是否改变默认选型与预算策略。](https://dashscope.aliyun.com/model/qwen-vl-max)",
|
||||
"## 今日头条",
|
||||
"[qwen-vl-max 成本下调 18%](https://dashscope.aliyun.com/model/qwen-vl-max)",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("markdown missing %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTMLV3IncludesLinksLowestPlanAndGPT56Leak(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report_links_leak.html")
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "promo_campaign",
|
||||
ModelName: "GPT-5.6",
|
||||
ProviderName: "OpenAI",
|
||||
ProviderCountry: "US",
|
||||
OperatorName: "OpenAI",
|
||||
Audience: "适合关注高端模型路线图、预算和替换窗口的团队",
|
||||
TrustLabel: "行业情报 / 待官方确认",
|
||||
SourceKindLabel: "泄露情报",
|
||||
PrimarySource: "https://openai.example.com/gpt-5-6-leak",
|
||||
SourceURL: "https://openai.example.com/gpt-5-6-leak",
|
||||
UpdatedAt: "2026-05-27 00:00",
|
||||
EvidenceDetail: "多个公开情报源出现 GPT-5.6 命名与规格片段,尚待官方正式发布确认。",
|
||||
Baseline: "提前泄露",
|
||||
Summary: "GPT-5.6 提前泄露信号出现,需立即复查其是否改变默认高端模型路线与预算预期。",
|
||||
Priority: 135,
|
||||
},
|
||||
}
|
||||
report.TencentSubscriptionPlans = []SubscriptionPlanInfo{
|
||||
{OperatorName: "MiniMax", PlanName: "Starter", PlanFamily: "token_plan", BillingCycle: "monthly", Currency: "USD", ListPrice: 10, PriceUnit: "USD/month", ModelCount: 1},
|
||||
{OperatorName: "MiniMax", PlanName: "Plus", PlanFamily: "token_plan", BillingCycle: "monthly", Currency: "USD", ListPrice: 20, PriceUnit: "USD/month", ModelCount: 1},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
if err := generateHTMLV3(report, path); err != nil {
|
||||
t.Fatalf("generateHTMLV3 returned error: %v", err)
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read html output: %v", err)
|
||||
}
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"GPT-5.6",
|
||||
"US / OpenAI",
|
||||
"https://openai.example.com/gpt-5-6-leak",
|
||||
"🏷 最低价",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("html missing %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTMLV3IncludesResellerSubscriptionComparison(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report.html")
|
||||
report := sampleReportForV1()
|
||||
@@ -720,6 +904,9 @@ func TestGenerateHTMLV3IncludesResellerSubscriptionComparison(t *testing.T) {
|
||||
t.Fatalf("html missing %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(content, "🏷 最低价") {
|
||||
t.Fatalf("expected lowest plan marker in subscription table\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownV3IncludesSignatureStabilitySection(t *testing.T) {
|
||||
@@ -781,6 +968,168 @@ func TestGenerateMarkdownV3IncludesSignatureStabilitySection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownV3IncludesThemedPriceNewsSections(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report_price_news.md")
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
OperatorName: "DashScope",
|
||||
Summary: "视觉模型价格下降已足以影响默认选型。",
|
||||
Audience: "适合需要当天重排价格带的团队",
|
||||
TrustLabel: "官方来源",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 10:00",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
|
||||
Baseline: "较昨日 -18%",
|
||||
PriceChangePct: -18,
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
EventType: "price_increase",
|
||||
ModelName: "claude-3.7-sonnet",
|
||||
ProviderName: "Anthropic",
|
||||
OperatorName: "Anthropic",
|
||||
Summary: "核心写作模型价格上调,需要准备预算回退。",
|
||||
Audience: "适合需要稳定预算的商用团队",
|
||||
TrustLabel: "官方来源",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 10:10",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日上涨 12%",
|
||||
Baseline: "较昨日 +12%",
|
||||
PriceChangePct: 12,
|
||||
Priority: 90,
|
||||
},
|
||||
{
|
||||
EventType: "promo_campaign",
|
||||
ModelName: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
OperatorName: "DeepSeek",
|
||||
Summary: "平台活动窗口出现后,值得重新评估低成本推理方案。",
|
||||
Audience: "适合计划趁活动窗口压低推理成本的团队",
|
||||
TrustLabel: "官方来源 / 一级证据",
|
||||
SourceKindLabel: "官方活动页",
|
||||
PrimarySource: "https://api-docs.deepseek.com/news/news250929",
|
||||
UpdatedAt: "2026-05-13 09:00",
|
||||
EvidenceDetail: "官方活动页记录活动窗口价格下调",
|
||||
Baseline: "活动窗口开启",
|
||||
Priority: 80,
|
||||
},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
if err := generateMarkdownV3(report, path); err != nil {
|
||||
t.Fatalf("generateMarkdownV3 returned error: %v", err)
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read markdown output: %v", err)
|
||||
}
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"## 今日价格新闻",
|
||||
"### ↓ Opportunity · 降价机会",
|
||||
"### ↑ Warning · 涨价预警",
|
||||
"### ✦ Campaign · 平台活动",
|
||||
"qwen-vl-max",
|
||||
"claude-3.7-sonnet",
|
||||
"DeepSeek-V4-Flash",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("markdown missing %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTMLV3IncludesThemedPriceNewsSections(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report_price_news.html")
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
OperatorName: "DashScope",
|
||||
Summary: "视觉模型价格下降已足以影响默认选型。",
|
||||
Audience: "适合需要当天重排价格带的团队",
|
||||
TrustLabel: "官方来源",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 10:00",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
|
||||
Baseline: "较昨日 -18%",
|
||||
PriceChangePct: -18,
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
EventType: "price_increase",
|
||||
ModelName: "claude-3.7-sonnet",
|
||||
ProviderName: "Anthropic",
|
||||
OperatorName: "Anthropic",
|
||||
Summary: "核心写作模型价格上调,需要准备预算回退。",
|
||||
Audience: "适合需要稳定预算的商用团队",
|
||||
TrustLabel: "官方来源",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 10:10",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日上涨 12%",
|
||||
Baseline: "较昨日 +12%",
|
||||
PriceChangePct: 12,
|
||||
Priority: 90,
|
||||
},
|
||||
{
|
||||
EventType: "promo_campaign",
|
||||
ModelName: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
OperatorName: "DeepSeek",
|
||||
Summary: "平台活动窗口出现后,值得重新评估低成本推理方案。",
|
||||
Audience: "适合计划趁活动窗口压低推理成本的团队",
|
||||
TrustLabel: "官方来源 / 一级证据",
|
||||
SourceKindLabel: "官方活动页",
|
||||
PrimarySource: "https://api-docs.deepseek.com/news/news250929",
|
||||
UpdatedAt: "2026-05-13 09:00",
|
||||
EvidenceDetail: "官方活动页记录活动窗口价格下调",
|
||||
Baseline: "活动窗口开启",
|
||||
Priority: 80,
|
||||
},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
if err := generateHTMLV3(report, path); err != nil {
|
||||
t.Fatalf("generateHTMLV3 returned error: %v", err)
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read html output: %v", err)
|
||||
}
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"今日价格新闻",
|
||||
"降价机会",
|
||||
"涨价预警",
|
||||
"平台活动",
|
||||
"Opportunity",
|
||||
"Warning",
|
||||
"Campaign",
|
||||
">↓<",
|
||||
">↑<",
|
||||
">✦<",
|
||||
"qwen-vl-max",
|
||||
"claude-3.7-sonnet",
|
||||
"DeepSeek-V4-Flash",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("html missing %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTMLV3IncludesSignatureStabilitySection(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report.html")
|
||||
report := sampleReportForV1()
|
||||
@@ -830,6 +1179,163 @@ func TestGenerateHTMLV3IncludesSignatureStabilitySection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTMLV3IncludesPriceNewsBadgeIcons(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report_price_news_badges.html")
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
OperatorName: "DashScope",
|
||||
Summary: "视觉模型价格下降已足以影响默认选型。",
|
||||
Audience: "适合需要当天重排价格带的团队",
|
||||
TrustLabel: "官方来源",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 10:00",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日下降 18%",
|
||||
Baseline: "较昨日 -18%",
|
||||
PriceChangePct: -18,
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
EventType: "price_increase",
|
||||
ModelName: "claude-3.7-sonnet",
|
||||
ProviderName: "Anthropic",
|
||||
OperatorName: "Anthropic",
|
||||
Summary: "核心写作模型价格上调,需要准备预算回退。",
|
||||
Audience: "适合需要稳定预算的商用团队",
|
||||
TrustLabel: "官方来源",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 10:10",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格较昨日上涨 12%",
|
||||
Baseline: "较昨日 +12%",
|
||||
PriceChangePct: 12,
|
||||
Priority: 90,
|
||||
},
|
||||
{
|
||||
EventType: "promo_campaign",
|
||||
ModelName: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
OperatorName: "DeepSeek",
|
||||
Summary: "平台活动窗口出现后,值得重新评估低成本推理方案。",
|
||||
Audience: "适合计划趁活动窗口压低推理成本的团队",
|
||||
TrustLabel: "官方来源 / 一级证据",
|
||||
SourceKindLabel: "官方活动页",
|
||||
PrimarySource: "https://api-docs.deepseek.com/news/news250929",
|
||||
UpdatedAt: "2026-05-13 09:00",
|
||||
EvidenceDetail: "官方活动页记录活动窗口价格下调",
|
||||
Baseline: "活动窗口开启",
|
||||
Priority: 80,
|
||||
},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
if err := generateHTMLV3(report, path); err != nil {
|
||||
t.Fatalf("generateHTMLV3 returned error: %v", err)
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read html output: %v", err)
|
||||
}
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"Opportunity",
|
||||
"Warning",
|
||||
"Campaign",
|
||||
">↓<",
|
||||
">↑<",
|
||||
">✦<",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("html missing %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTMLV3PaginatesAppendices(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report_appendix_pagination.html")
|
||||
report := sampleReportForV1()
|
||||
report.IntlTop5 = nil
|
||||
report.IntlAppendixList = nil
|
||||
report.DomesticTop10 = nil
|
||||
report.DomesticAppendixList = nil
|
||||
report.FreeTop20 = nil
|
||||
report.Operators = nil
|
||||
report.Resellers = nil
|
||||
|
||||
for i := 0; i < 65; i++ {
|
||||
report.DomesticAppendixList = append(report.DomesticAppendixList, ModelInfo{
|
||||
Name: fmt.Sprintf("domestic-model-%02d", i+1),
|
||||
ProviderName: "ProviderCN",
|
||||
InputPrice: 1,
|
||||
OutputPrice: 2,
|
||||
Currency: "CNY",
|
||||
ContextLength: 131072,
|
||||
})
|
||||
}
|
||||
|
||||
for i := 0; i < 22; i++ {
|
||||
report.FreeTop20 = append(report.FreeTop20, ModelInfo{
|
||||
Name: fmt.Sprintf("free-model-%02d", i+1),
|
||||
ProviderName: "ProviderFree",
|
||||
OperatorType: "official",
|
||||
ContextLength: 65536,
|
||||
})
|
||||
}
|
||||
for i := 0; i < 23; i++ {
|
||||
report.Operators = append(report.Operators, OperatorInfo{Name: fmt.Sprintf("Operator-%02d", i+1), ModelCount: i + 1, MinInputPrice: 0.1, AvgInputPrice: 0.2})
|
||||
}
|
||||
for i := 0; i < 22; i++ {
|
||||
report.Resellers = append(report.Resellers, OperatorInfo{Name: fmt.Sprintf("Reseller-%02d", i+1), ModelCount: i + 1, MinInputPrice: 0.3, AvgInputPrice: 0.4})
|
||||
}
|
||||
decorateReportV1(report)
|
||||
report.AppendixLinks = []AppendixLink{
|
||||
{Title: "国际低价", Description: "查看国际低价模型附录(日报仅保留前几页)", Anchor: "#appendix-pricing-intl"},
|
||||
{Title: "国内低价", Description: "查看国内低价模型附录(日报仅保留前几页)", Anchor: "#appendix-pricing-domestic"},
|
||||
{Title: "免费样本", Description: "查看免费模型代表样本附录", Anchor: "#appendix-free"},
|
||||
{Title: "平台覆盖", Description: "查看官方平台与聚合平台覆盖", Anchor: "#appendix-platforms"},
|
||||
{Title: "全量导出 JSON", Description: "其余完整数据请下载独立导出文件或转到查询页查看", Anchor: "/reports/daily/appendix/2026-05-13/full_appendix.json"},
|
||||
}
|
||||
|
||||
if err := generateHTMLV3(report, path); err != nil {
|
||||
t.Fatalf("generateHTMLV3 returned error: %v", err)
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read html output: %v", err)
|
||||
}
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"完整价格附录(国际低价)",
|
||||
"完整价格附录(国内低价)",
|
||||
"完整免费附录",
|
||||
"平台覆盖附录",
|
||||
"国际低价",
|
||||
"国内低价",
|
||||
"全量导出 JSON",
|
||||
"/reports/daily/appendix/2026-05-13/full_appendix.json",
|
||||
"data-appendix-page=\"1\"",
|
||||
"data-appendix-total-pages=\"1\"",
|
||||
"data-appendix-total-pages=\"2\"",
|
||||
"data-appendix-total-pages=\"3\"",
|
||||
"#appendix-pricing-intl",
|
||||
"#appendix-pricing-domestic",
|
||||
"#appendix-free",
|
||||
"#appendix-platforms",
|
||||
"上一页",
|
||||
"下一页",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("html missing %q\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHeadlineItemsUsesModelEvents(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
@@ -883,14 +1389,17 @@ 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") || items[0].Label != "一级官方发布" {
|
||||
t.Fatalf("expected official release event to rank first, got %+v", items[0])
|
||||
if items[0].Label != "价格下调" || !strings.Contains(items[0].Title, "glm-5") {
|
||||
t.Fatalf("expected price change event to rank first, got %+v", items[0])
|
||||
}
|
||||
if items[1].Baseline != "较昨日 -25%" {
|
||||
t.Fatalf("expected price_cut baseline to be preserved, got %+v", items[1])
|
||||
if items[1].Label != "一级官方发布" {
|
||||
t.Fatalf("expected official release to stay immediately after price change, got %+v", items[1])
|
||||
}
|
||||
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])
|
||||
if items[0].Baseline != "较昨日 -25%" {
|
||||
t.Fatalf("expected price_cut baseline to be preserved, got %+v", items[0])
|
||||
}
|
||||
if items[1].SourceKindLabel != "一级官方发布" || items[1].PrimarySource != "https://open.bigmodel.cn/dev/howuse/model" {
|
||||
t.Fatalf("expected official release evidence fields to be preserved, got %+v", items[1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,6 +1591,89 @@ func TestDecorateReportV1ElevatesSignatureDriftIntoHeroSummary(t *testing.T) {
|
||||
if !strings.Contains(report.HeroEvidence, "最近 5 次中出现 3 次结构变化") {
|
||||
t.Fatalf("expected hero evidence to mention drift count, got %q", report.HeroEvidence)
|
||||
}
|
||||
|
||||
}
|
||||
func TestDecorateReportV1PrefersPriceChangeInHeroSummary(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "official_release",
|
||||
ModelName: "GLM-5",
|
||||
Summary: "官方发布新模型。",
|
||||
PrimarySource: "official release",
|
||||
EvidenceDetail: "models.release_date = 今日",
|
||||
Priority: 120,
|
||||
},
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
OperatorName: "OpenRouter",
|
||||
Summary: "价格下降已足以影响默认选型,值得重新评估同类模型。",
|
||||
PrimarySource: "pricing_history",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格由 $0.60 调整为 $0.30,较昨日下降 50%",
|
||||
PriceChangePct: -50,
|
||||
OldInputPrice: 0.60,
|
||||
NewInputPrice: 0.30,
|
||||
OldOutputPrice: 2.40,
|
||||
NewOutputPrice: 1.20,
|
||||
Currency: "USD",
|
||||
Priority: 95,
|
||||
},
|
||||
}
|
||||
|
||||
decorateReportV1(report)
|
||||
|
||||
if !strings.Contains(report.HeroSummary, "DeepSeek-V4-Flash") || !strings.Contains(report.HeroSummary, "价格") {
|
||||
t.Fatalf("expected hero summary to prioritize price change, got %q", report.HeroSummary)
|
||||
}
|
||||
if !strings.Contains(report.HeroEvidence, "pricing_history") {
|
||||
t.Fatalf("expected hero evidence to mention pricing history, got %q", report.HeroEvidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHeadlineItemsPlacesPriceChangeBeforeOfficialRelease(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
report.ModelEvents = []ModelEvent{
|
||||
{
|
||||
EventType: "official_release",
|
||||
ModelName: "GLM-5",
|
||||
Summary: "官方发布新模型。",
|
||||
TrustLabel: "官方来源 / 一级证据",
|
||||
SourceKindLabel: "一级官方发布",
|
||||
PrimarySource: "official release",
|
||||
UpdatedAt: "2026-05-13 08:30",
|
||||
EvidenceDetail: "models.release_date = 今日",
|
||||
Priority: 120,
|
||||
},
|
||||
{
|
||||
EventType: "price_cut",
|
||||
ModelName: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
OperatorName: "OpenRouter",
|
||||
Summary: "价格下降已足以影响默认选型,值得重新评估同类模型。",
|
||||
TrustLabel: "聚合来源",
|
||||
SourceKindLabel: "价格快照",
|
||||
PrimarySource: "pricing_history",
|
||||
UpdatedAt: "2026-05-13 09:30",
|
||||
EvidenceDetail: "pricing_history 记录到输入价格由 $0.60 调整为 $0.30,较昨日下降 50%",
|
||||
PriceChangePct: -50,
|
||||
OldInputPrice: 0.60,
|
||||
NewInputPrice: 0.30,
|
||||
OldOutputPrice: 2.40,
|
||||
NewOutputPrice: 1.20,
|
||||
Currency: "USD",
|
||||
Priority: 95,
|
||||
},
|
||||
}
|
||||
|
||||
items := buildHeadlineItems(report)
|
||||
if len(items) == 0 {
|
||||
t.Fatalf("expected headline items")
|
||||
}
|
||||
if items[0].Label != "价格下调" {
|
||||
t.Fatalf("expected price change headline first, got %+v", items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureAuditSummaryToneRespectsConfiguredThreshold(t *testing.T) {
|
||||
|
||||
198
scripts/run_intraday_price_watch.sh
Normal file
198
scripts/run_intraday_price_watch.sh
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [[ -f ".env.local" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source ".env.local"
|
||||
fi
|
||||
if [[ -f ".env" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source ".env"
|
||||
fi
|
||||
|
||||
if [[ -z "${DATABASE_URL:-}" ]]; then
|
||||
echo "DATABASE_URL 未设置" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${OPENROUTER_API_KEY:-}" ]]; then
|
||||
echo "OPENROUTER_API_KEY 未设置,无法执行日内价格追踪" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPORT_DATE="${REPORT_DATE:-$(date +%F)}"
|
||||
FETCH_OUT="$ROOT_DIR/models.json"
|
||||
FETCH_TOTAL="0"
|
||||
PIPELINE_STAGE_SET="openrouter,multi_source,official_imports,daily_signal_snapshot"
|
||||
PIPELINE_SOURCE_SET="openrouter,moonshot,deepseek,openai,zhipu,baidu,bytedance,tencent_subscription,aliyun_subscription,baidu_subscription,ctyun_subscription,bytedance_subscription,huawei_package,zhipu_coding_plan,minimax_subscription,cucloud_catalog,cucloud_pricing,mobile_cloud_pricing,youdao_pricing,platform360_pricing,siliconflow_pricing,ppio_pricing,ucloud_pricing,coreshub_pricing,cloudflare_pricing,perplexity_pricing,vertex_pricing,bedrock_pricing,azure_openai_pricing,qwen_pricing,hunyuan_pricing,huawei_maas_pricing,baichuan_pricing,lingyiwanwu_pricing,sensenova_pricing,xfyun_pricing,bytedance_pricing,catalog_seed_verification"
|
||||
PIPELINE_FAILED_SOURCE_SET="none"
|
||||
MULTI_SOURCE_AUDIT="multi_source_audit=unavailable"
|
||||
PIPELINE_AUDIT_SUMMARY=""
|
||||
|
||||
normalize_summary_file() {
|
||||
local path="$1"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
return
|
||||
fi
|
||||
tr '\n' ' ' < "$path" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//'
|
||||
}
|
||||
|
||||
extract_failed_source_keys() {
|
||||
local summary="$1"
|
||||
printf '%s\n' "$summary" | sed -n 's/.*failed_source_keys=\([^ ]*\).*/\1/p'
|
||||
}
|
||||
|
||||
merge_failed_source_keys() {
|
||||
local keys="$1"
|
||||
if [[ -z "$keys" || "$keys" == "none" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ "$PIPELINE_FAILED_SOURCE_SET" == "none" ]]; then
|
||||
PIPELINE_FAILED_SOURCE_SET="$keys"
|
||||
return
|
||||
fi
|
||||
PIPELINE_FAILED_SOURCE_SET="${PIPELINE_FAILED_SOURCE_SET},${keys}"
|
||||
}
|
||||
|
||||
refresh_pipeline_audit() {
|
||||
PIPELINE_AUDIT_SUMMARY="runtime_audit stage_set=${PIPELINE_STAGE_SET} selected_source_keys=${PIPELINE_SOURCE_SET} failed_source_keys=${PIPELINE_FAILED_SOURCE_SET} openrouter_total=${FETCH_TOTAL:-0} ${MULTI_SOURCE_AUDIT}"
|
||||
}
|
||||
|
||||
run_or_fail() {
|
||||
local source_key="$1"
|
||||
local error_message="$2"
|
||||
shift 2
|
||||
if ! "$@"; then
|
||||
merge_failed_source_keys "$source_key"
|
||||
refresh_pipeline_audit
|
||||
echo "$error_message" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
refresh_pipeline_audit
|
||||
bash "$ROOT_DIR/scripts/apply_migration.sh"
|
||||
|
||||
run_or_fail "openrouter" "OpenRouter 日内价格采集失败" \
|
||||
go run "./scripts/fetch_openrouter.go" -api-key "$OPENROUTER_API_KEY" -db "$DATABASE_URL" -out "$FETCH_OUT" -strict-real
|
||||
|
||||
FETCH_TOTAL=$(python3 - <<'PY' "$FETCH_OUT"
|
||||
import json, sys
|
||||
path = sys.argv[1]
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
print(int(data.get("total", 0)))
|
||||
PY
|
||||
)
|
||||
if [[ "${FETCH_TOTAL:-0}" -lt 10 ]]; then
|
||||
merge_failed_source_keys "openrouter"
|
||||
refresh_pipeline_audit
|
||||
echo "本次日内采集结果异常: total=${FETCH_TOTAL:-0} < 10" >&2
|
||||
exit 1
|
||||
fi
|
||||
refresh_pipeline_audit
|
||||
|
||||
MULTI_SOURCE_OUTPUT="$(mktemp)"
|
||||
if ! go run "./scripts/fetch_multi_source.go" --sources moonshot,deepseek,openai > "$MULTI_SOURCE_OUTPUT"; then
|
||||
MULTI_SOURCE_SUMMARY="$(normalize_summary_file "$MULTI_SOURCE_OUTPUT")"
|
||||
if [[ -n "$MULTI_SOURCE_SUMMARY" ]]; then
|
||||
MULTI_SOURCE_AUDIT="multi_source_audit=${MULTI_SOURCE_SUMMARY}"
|
||||
merge_failed_source_keys "$(extract_failed_source_keys "$MULTI_SOURCE_SUMMARY")"
|
||||
else
|
||||
MULTI_SOURCE_AUDIT="multi_source_audit=stage_failed"
|
||||
merge_failed_source_keys "moonshot,deepseek,openai"
|
||||
fi
|
||||
cat "$MULTI_SOURCE_OUTPUT"
|
||||
rm -f "$MULTI_SOURCE_OUTPUT"
|
||||
refresh_pipeline_audit
|
||||
echo "日内多源价格补充同步失败" >&2
|
||||
exit 1
|
||||
fi
|
||||
MULTI_SOURCE_SUMMARY="$(normalize_summary_file "$MULTI_SOURCE_OUTPUT")"
|
||||
MULTI_SOURCE_AUDIT="multi_source_audit=${MULTI_SOURCE_SUMMARY:-none}"
|
||||
merge_failed_source_keys "$(extract_failed_source_keys "$MULTI_SOURCE_SUMMARY")"
|
||||
refresh_pipeline_audit
|
||||
cat "$MULTI_SOURCE_OUTPUT"
|
||||
rm -f "$MULTI_SOURCE_OUTPUT"
|
||||
|
||||
run_or_fail "zhipu" "智谱官方导入失败" go run -tags llm_script "./scripts/import_zhipu_data.go"
|
||||
run_or_fail "official_seed_export" "官方种子导出失败" go run -tags llm_script "./scripts/export_official_seed_json.go"
|
||||
run_or_fail "baidu" "百度官方导入失败" go run -tags llm_script "./scripts/import_phase2_data.go"
|
||||
run_or_fail "bytedance" "字节官方导入失败" go run -tags llm_script "./scripts/import_bytedance_data.go"
|
||||
run_or_fail "aliyun_subscription" "阿里云套餐导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/aliyun_subscription_lib.go ./scripts/import_aliyun_subscription.go
|
||||
run_or_fail "baidu_subscription" "百度套餐导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/baidu_subscription_lib.go ./scripts/import_baidu_subscription.go
|
||||
run_or_fail "ctyun_subscription" "天翼云套餐导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/ctyun_subscription_lib.go ./scripts/import_ctyun_subscription.go
|
||||
run_or_fail "bytedance_subscription" "火山方舟套餐导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/bytedance_subscription_lib.go ./scripts/import_bytedance_subscription.go
|
||||
run_or_fail "huawei_package" "华为云套餐包导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/huawei_package_lib.go ./scripts/import_huawei_package.go
|
||||
run_or_fail "zhipu_coding_plan" "智谱 Coding Plan 导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/zhipu_coding_plan_lib.go ./scripts/import_zhipu_coding_plan.go
|
||||
run_or_fail "minimax_subscription" "MiniMax Token Plan 导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/minimax_subscription_lib.go ./scripts/import_minimax_subscription.go
|
||||
run_or_fail "cucloud_catalog" "联通云目录校验失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/catalog_verification_common.go ./scripts/import_cucloud_catalog.go
|
||||
run_or_fail "cucloud_pricing" "联通云 Token Plan 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_cucloud_pricing.go
|
||||
run_or_fail "mobile_cloud_pricing" "移动云 MoMA 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_mobile_cloud_pricing.go
|
||||
run_or_fail "tencent_subscription" "腾讯云套餐导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/tencent_catalog_lib.go ./scripts/import_tencent_subscription.go
|
||||
run_or_fail "youdao_pricing" "网易有道价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/youdao_pricing_lib.go ./scripts/import_youdao_pricing.go
|
||||
run_or_fail "platform360_pricing" "360 智脑价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/platform360_pricing_lib.go ./scripts/import_360_pricing.go
|
||||
run_or_fail "siliconflow_pricing" "硅基流动价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/siliconflow_pricing_lib.go ./scripts/import_siliconflow_pricing.go
|
||||
run_or_fail "ppio_pricing" "PPIO 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/ppio_pricing_lib.go ./scripts/import_ppio_pricing.go
|
||||
run_or_fail "ucloud_pricing" "UCloud 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/ucloud_pricing_lib.go ./scripts/import_ucloud_pricing.go
|
||||
run_or_fail "coreshub_pricing" "CoresHub 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/coreshub_pricing_lib.go ./scripts/import_coreshub_pricing.go
|
||||
run_or_fail "cloudflare_pricing_signature" "Cloudflare Workers AI 价格页结构签名漂移" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/pricing_markdown_snapshot_lib.go ./scripts/cloudflare_pricing_snapshot_lib.go ./scripts/signature_guard_common.go ./scripts/official_import_signature_audit_lib.go ./scripts/cloudflare_pricing_signature_guard_lib.go ./scripts/cloudflare_pricing_import_runner.go ./scripts/cloudflare_pricing_lib.go ./scripts/cloudflare_pricing_signature_guard.go
|
||||
run_or_fail "cloudflare_pricing" "Cloudflare Workers AI 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/pricing_markdown_snapshot_lib.go ./scripts/cloudflare_pricing_snapshot_lib.go ./scripts/cloudflare_pricing_import_runner.go ./scripts/cloudflare_pricing_lib.go ./scripts/import_cloudflare_pricing.go
|
||||
run_or_fail "perplexity_pricing_signature" "Perplexity API 价格页结构签名漂移" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/pricing_markdown_snapshot_lib.go ./scripts/perplexity_pricing_snapshot_lib.go ./scripts/signature_guard_common.go ./scripts/official_import_signature_audit_lib.go ./scripts/perplexity_pricing_signature_guard_lib.go ./scripts/perplexity_pricing_import_runner.go ./scripts/perplexity_pricing_lib.go ./scripts/perplexity_pricing_signature_guard.go
|
||||
run_or_fail "perplexity_pricing" "Perplexity API 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/pricing_markdown_snapshot_lib.go ./scripts/perplexity_pricing_snapshot_lib.go ./scripts/perplexity_pricing_import_runner.go ./scripts/perplexity_pricing_lib.go ./scripts/import_perplexity_pricing.go
|
||||
run_or_fail "vertex_pricing_signature" "Vertex AI 价格页结构签名漂移" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/pricing_markdown_snapshot_lib.go ./scripts/signature_guard_common.go ./scripts/official_import_signature_audit_lib.go ./scripts/vertex_pricing_snapshot_lib.go ./scripts/vertex_pricing_signature_guard_lib.go ./scripts/vertex_pricing_import_runner.go ./scripts/vertex_pricing_lib.go ./scripts/vertex_pricing_signature_guard.go
|
||||
run_or_fail "vertex_pricing" "Vertex AI 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/vertex_pricing_snapshot_lib.go ./scripts/vertex_pricing_import_runner.go ./scripts/vertex_pricing_lib.go ./scripts/import_vertex_pricing.go
|
||||
run_or_fail "bedrock_pricing" "Amazon Bedrock 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/bedrock_pricing_lib.go ./scripts/import_bedrock_pricing.go
|
||||
run_or_fail "azure_openai_pricing" "Azure OpenAI 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/azure_openai_pricing_lib.go ./scripts/import_azure_openai_pricing.go
|
||||
run_or_fail "qwen_pricing" "通义千问价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_qwen_pricing.go
|
||||
run_or_fail "hunyuan_pricing" "腾讯混元价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_hunyuan_pricing.go
|
||||
run_or_fail "huawei_maas_pricing" "华为云 MaaS 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_huawei_maas_pricing.go
|
||||
run_or_fail "baichuan_pricing" "百川价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_baichuan_pricing.go
|
||||
run_or_fail "lingyiwanwu_pricing" "零一万物价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_lingyiwanwu_pricing.go
|
||||
run_or_fail "sensenova_pricing" "商汤 SenseNova 价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_sensenova_pricing.go
|
||||
run_or_fail "xfyun_pricing" "讯飞价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_xfyun_pricing.go
|
||||
run_or_fail "bytedance_pricing" "火山方舟价格导入失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/official_pricing_import_common.go ./scripts/import_bytedance_pricing.go
|
||||
refresh_pipeline_audit
|
||||
run_or_fail "catalog_seed_verification" "目录级官方入口核验失败" \
|
||||
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/import_catalog_seed_verification.go
|
||||
refresh_pipeline_audit
|
||||
run_or_fail "daily_signal_snapshot" "日内价格信号物化失败" \
|
||||
env SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" REPORT_TRIGGER_SOURCE="intraday" go run -tags llm_script ./scripts/materialize_daily_signals.go
|
||||
|
||||
echo "$PIPELINE_AUDIT_SUMMARY"
|
||||
64
scripts/secret_gate_lib.sh
Executable file
64
scripts/secret_gate_lib.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
secret_scan_paths() {
|
||||
local scan_root="${1:-}"
|
||||
shift || true
|
||||
|
||||
if [ -z "$scan_root" ]; then
|
||||
echo "secret_scan_paths requires scan root" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local patterns='(sk-[A-Za-z0-9_-]+|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}|-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----|authorization:[[:space:]]*bearer[[:space:]]+[A-Za-z0-9._-]{8,}|api[_-]?key[[:space:]]*[:=][[:space:]]*[A-Za-z0-9._-]{8,})'
|
||||
local excludes=(
|
||||
'--exclude=verify_phase6.sh'
|
||||
'--exclude=secret_gate_lib.sh'
|
||||
'--exclude=secret_gate_test.sh'
|
||||
'--exclude=.env.example'
|
||||
'--exclude=README.md'
|
||||
'--exclude=CONFIGURATION.md'
|
||||
'--exclude=DEPLOYMENT.md'
|
||||
'--exclude-dir=.git'
|
||||
'--exclude-dir=.serena'
|
||||
'--exclude-dir=node_modules'
|
||||
'--exclude-dir=dist'
|
||||
'--exclude-dir=logs'
|
||||
'--exclude-dir=reports'
|
||||
)
|
||||
|
||||
if grep -R -n -E -i "$patterns" "$scan_root" "$@" \
|
||||
--include='*.go' \
|
||||
--include='*.ts' \
|
||||
--include='*.tsx' \
|
||||
--include='*.js' \
|
||||
--include='*.jsx' \
|
||||
--include='*.sh' \
|
||||
--include='*.yml' \
|
||||
--include='*.yaml' \
|
||||
"${excludes[@]}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
secret_env_files() {
|
||||
local dockerignore_path="$1"
|
||||
|
||||
if [ ! -f "$dockerignore_path" ]; then
|
||||
echo "missing dockerignore: $dockerignore_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -Eq '^\.env(\..*)?$' "$dockerignore_path"; then
|
||||
echo "missing .env ignore rule in $dockerignore_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -Eq '^!\.env\.example$' "$dockerignore_path"; then
|
||||
echo "missing explicit .env.example allow rule in $dockerignore_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
60
scripts/secret_gate_test.sh
Executable file
60
scripts/secret_gate_test.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
. "$ROOT_DIR/scripts/secret_gate_lib.sh"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
SECRET_FILE="$TMP_DIR/secret.ts"
|
||||
CLEAN_FILE="$TMP_DIR/clean.ts"
|
||||
AWS_SECRET_FILE="$TMP_DIR/aws.ts"
|
||||
ENV_FILE="$TMP_DIR/.env"
|
||||
DOCKERIGNORE_FILE="$TMP_DIR/.dockerignore"
|
||||
MISSING_DOCKERIGNORE_FIXTURE="$ROOT_DIR/scripts/testdata/empty.dockerignore"
|
||||
|
||||
printf 'const key = "sk-test-secret";\n' > "$SECRET_FILE"
|
||||
printf 'const ok = true;\n' > "$CLEAN_FILE"
|
||||
printf 'const awsKey = "AKIA1234567890ABCDEF";\n' > "$AWS_SECRET_FILE"
|
||||
printf 'OPENROUTER_API_KEY=sk-test-secret\n' > "$ENV_FILE"
|
||||
printf '.env\n!.env.example\n' > "$DOCKERIGNORE_FILE"
|
||||
|
||||
|
||||
set +e
|
||||
secret_scan_paths "$SECRET_FILE" "$CLEAN_FILE" > /tmp/secret_gate_test_scan.out 2> /tmp/secret_gate_test_scan.err
|
||||
SCAN_RC=$?
|
||||
set -e
|
||||
if [ "$SCAN_RC" -eq 0 ]; then
|
||||
echo "expected secret_scan_paths to fail"
|
||||
exit 1
|
||||
fi
|
||||
grep -q "$SECRET_FILE" /tmp/secret_gate_test_scan.out
|
||||
|
||||
set +e
|
||||
secret_scan_paths "$AWS_SECRET_FILE" > /tmp/secret_gate_test_aws.out 2> /tmp/secret_gate_test_aws.err
|
||||
AWS_SCAN_RC=$?
|
||||
set -e
|
||||
if [ "$AWS_SCAN_RC" -eq 0 ]; then
|
||||
echo "expected secret_scan_paths to fail for aws-style key"
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'AKIA1234567890ABCDEF' /tmp/secret_gate_test_aws.out
|
||||
|
||||
secret_env_files "$DOCKERIGNORE_FILE" > /tmp/secret_gate_test_env.out 2> /tmp/secret_gate_test_env.err
|
||||
|
||||
set +e
|
||||
secret_env_files "$MISSING_DOCKERIGNORE_FIXTURE" > /tmp/secret_gate_test_env_fail.out 2> /tmp/secret_gate_test_env_fail.err
|
||||
ENV_RC=$?
|
||||
set -e
|
||||
if [ "$ENV_RC" -eq 0 ]; then
|
||||
echo "expected secret_env_files to fail without dockerignore entry"
|
||||
exit 1
|
||||
fi
|
||||
grep -q "missing .env ignore rule" /tmp/secret_gate_test_env_fail.err
|
||||
|
||||
echo "secret_gate_test: PASS"
|
||||
@@ -101,6 +101,17 @@ func fetchSubscriptionPage(url string, fixture string, client *http.Client) (str
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
body, err := fetchSubscriptionPageWithRetry(url, client)
|
||||
if err == nil {
|
||||
return body, nil
|
||||
}
|
||||
if markdownURL, ok := markdownFallbackURL(url, err); ok {
|
||||
return fetchSubscriptionPageWithRetry(markdownURL, client)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func fetchSubscriptionPageWithRetry(url string, client *http.Client) (string, error) {
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= subscriptionFetchMaxAttempts; attempt++ {
|
||||
body, retryable, err := fetchSubscriptionPageOnce(url, client)
|
||||
@@ -116,6 +127,7 @@ func fetchSubscriptionPage(url string, fixture string, client *http.Client) (str
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
|
||||
func fetchSubscriptionPageOnce(url string, client *http.Client) (string, bool, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -146,6 +158,20 @@ func fetchSubscriptionPageOnce(url string, client *http.Client) (string, bool, e
|
||||
return normalizeSubscriptionPage(string(body)), false, nil
|
||||
}
|
||||
|
||||
func markdownFallbackURL(url string, err error) (string, bool) {
|
||||
if strings.TrimSpace(url) == "" || err == nil {
|
||||
return "", false
|
||||
}
|
||||
lower := strings.ToLower(err.Error())
|
||||
if !strings.Contains(lower, "status 403") && !strings.Contains(lower, "forbidden") {
|
||||
return "", false
|
||||
}
|
||||
if strings.HasSuffix(url, ".md") {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimRight(url, "/") + ".md", true
|
||||
}
|
||||
|
||||
func isRetriableSubscriptionFetchError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -46,3 +48,34 @@ func TestIsRetriableSubscriptionFetchErrorRecognizesForbidden(t *testing.T) {
|
||||
t.Fatalf("403 应被视作可重试错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchSubscriptionPageFallsBackToMarkdownSuffixOnForbidden(t *testing.T) {
|
||||
attempts := map[string]int{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
attempts[r.URL.Path]++
|
||||
switch r.URL.Path {
|
||||
case "/cn/update/promotion":
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
case "/cn/update/promotion.md":
|
||||
_, _ = w.Write([]byte("# 上新活动\nGLM Coding Plan 低至20元/月"))
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unexpected path %s", r.URL.Path), http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
body, err := fetchSubscriptionPage(server.URL+"/cn/update/promotion", "", client)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchSubscriptionPage 返回错误: %v", err)
|
||||
}
|
||||
if !strings.Contains(body, "GLM Coding Plan 低至20元/月") {
|
||||
t.Fatalf("返回体缺少 markdown fallback 内容: %q", body)
|
||||
}
|
||||
if attempts["/cn/update/promotion"] != subscriptionFetchMaxAttempts {
|
||||
t.Fatalf("期望原始路径按重试上限请求 %d 次,实际 %d", subscriptionFetchMaxAttempts, attempts["/cn/update/promotion"])
|
||||
}
|
||||
if attempts["/cn/update/promotion.md"] != 1 {
|
||||
t.Fatalf("期望 .md 路径请求 1 次,实际 %d", attempts["/cn/update/promotion.md"])
|
||||
}
|
||||
}
|
||||
|
||||
1
scripts/testdata/empty.dockerignore
vendored
Normal file
1
scripts/testdata/empty.dockerignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# empty on purpose
|
||||
24
scripts/testdata/report_promo_campaigns.json
vendored
24
scripts/testdata/report_promo_campaigns.json
vendored
@@ -1,16 +1,16 @@
|
||||
[
|
||||
{
|
||||
"date": "2025-09-29",
|
||||
"model_name": "DeepSeek-V3.2-Exp",
|
||||
"provider_name": "DeepSeek",
|
||||
"operator_name": "DeepSeek",
|
||||
"summary": "官方活动窗口出现后,值得重新评估低成本推理和批量调用方案。",
|
||||
"audience": "适合计划趁活动窗口压低推理成本的团队",
|
||||
"baseline": "活动窗口开启",
|
||||
"trust_label": "官方来源 / 一级证据",
|
||||
"source_kind_label": "官方活动页",
|
||||
"primary_source": "https://api-docs.deepseek.com/news/news250929",
|
||||
"evidence_detail": "官方活动页记录 V3.2-Exp 在活动窗口内价格下调 50%+",
|
||||
"priority": 115
|
||||
"date": "2026-05-27",
|
||||
"model_name": "GPT-5.6",
|
||||
"provider_name": "OpenAI",
|
||||
"operator_name": "OpenAI",
|
||||
"summary": "GPT-5.6 提前泄露信号出现,需立即复查其是否改变默认高端模型路线与预算预期。",
|
||||
"audience": "适合关注高端模型路线图、预算和替换窗口的团队",
|
||||
"baseline": "提前泄露",
|
||||
"trust_label": "行业情报 / 待官方确认",
|
||||
"source_kind_label": "泄露情报",
|
||||
"primary_source": "https://openai.example.com/gpt-5-6-leak",
|
||||
"evidence_detail": "多个公开情报源出现 GPT-5.6 命名与规格片段,尚待官方正式发布确认。",
|
||||
"priority": 135
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,12 +4,15 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/verify_common.sh"
|
||||
. "$SCRIPT_DIR/secret_gate_lib.sh"
|
||||
|
||||
DB_URL="${DATABASE_URL:-host=/var/run/postgresql dbname=llm_intelligence user=long sslmode=disable}"
|
||||
SERVER_BIN="/tmp/llm_phase6_server"
|
||||
SERVER_LOG="/tmp/llm_phase6_server.log"
|
||||
SERVER_PORT="${PHASE6_PORT:-}"
|
||||
SERVER_PID=""
|
||||
API_AUTH_TOKEN="${API_AUTH_TOKEN:-phase6-local-token}"
|
||||
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" >/dev/null 2>&1; then
|
||||
@@ -40,8 +43,9 @@ reserve_server_port() {
|
||||
}
|
||||
|
||||
start_server() {
|
||||
DATABASE_URL="$DB_URL" PORT="$SERVER_PORT" "$SERVER_BIN" >"$SERVER_LOG" 2>&1 &
|
||||
DATABASE_URL="$DB_URL" PORT="$SERVER_PORT" API_AUTH_TOKEN="$API_AUTH_TOKEN" "$SERVER_BIN" >"$SERVER_LOG" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
if ! kill -0 "$SERVER_PID" >/dev/null 2>&1; then
|
||||
return 1
|
||||
@@ -165,7 +169,7 @@ else
|
||||
fi
|
||||
check_shell "API Server 可构建" "go build -o /dev/null ./cmd/server"
|
||||
check_shell "健康检查脚本通过" "DATABASE_URL='$DB_URL' bash healthcheck.sh"
|
||||
check_shell "密钥未硬编码进源码" "grep -R -n 'sk-' cmd internal frontend/src scripts .github/workflows --include='*.go' --include='*.ts' --include='*.tsx' --include='*.sh' --include='*.yml' --include='*.yaml' --exclude='verify_phase6.sh' >/tmp/llm_phase6_secret_scan.out 2>/dev/null; test ! -s /tmp/llm_phase6_secret_scan.out"
|
||||
check_shell "源码与环境文件未包含明显硬编码密钥" "source scripts/secret_gate_lib.sh && secret_scan_paths . cmd internal frontend/src scripts .github/workflows && secret_env_files .dockerignore"
|
||||
|
||||
run_window_gate
|
||||
|
||||
@@ -174,7 +178,7 @@ if go build -o "$SERVER_BIN" ./cmd/server >/tmp/llm_phase6_server_build.out 2>/t
|
||||
pass "API /health 可用"
|
||||
|
||||
set +e
|
||||
api_metrics="$(curl -sS -o /tmp/llm_phase6_models.json -w '%{http_code} %{time_total}' "http://127.0.0.1:${SERVER_PORT}/api/v1/models")"
|
||||
api_metrics="$(curl -sS -H "Authorization: Bearer ${API_AUTH_TOKEN}" -o /tmp/llm_phase6_models.json -w '%{http_code} %{time_total}' "http://127.0.0.1:${SERVER_PORT}/api/v1/models")"
|
||||
api_rc=$?
|
||||
set -e
|
||||
if [ "$api_rc" -eq 0 ]; then
|
||||
@@ -202,7 +206,7 @@ if go build -o "$SERVER_BIN" ./cmd/server >/tmp/llm_phase6_server_build.out 2>/t
|
||||
fi
|
||||
|
||||
set +e
|
||||
plan_metrics="$(curl -sS -o /tmp/llm_phase6_subscription_plans.json -w '%{http_code} %{time_total}' "http://127.0.0.1:${SERVER_PORT}/api/v1/subscription-plans")"
|
||||
plan_metrics="$(curl -sS -H "Authorization: Bearer ${API_AUTH_TOKEN}" -o /tmp/llm_phase6_subscription_plans.json -w '%{http_code} %{time_total}' "http://127.0.0.1:${SERVER_PORT}/api/v1/subscription-plans")"
|
||||
plan_rc=$?
|
||||
set -e
|
||||
if [ "$plan_rc" -eq 0 ]; then
|
||||
@@ -232,5 +236,6 @@ fi
|
||||
|
||||
check_shell "Phase 6 性能文档存在" "test -f docs/PERFORMANCE_TEST.md"
|
||||
check_shell "前端已具备测试入口" "cd frontend && npm run test -- --run >/tmp/llm_phase6_frontend_test.out 2>/tmp/llm_phase6_frontend_test.err"
|
||||
check_shell "secret gate 独立测试通过" "bash scripts/secret_gate_test.sh"
|
||||
|
||||
finish_phase
|
||||
|
||||
Reference in New Issue
Block a user