feat(report): ship daily report v1 experience
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -9,44 +9,249 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report.md")
|
||||
report := &ReportV3{
|
||||
func sampleReportForV1() *ReportV3 {
|
||||
return &ReportV3{
|
||||
Date: "2026-05-13",
|
||||
TotalModels: 502,
|
||||
QualitySummary: DataQualitySummary{
|
||||
Total: 502,
|
||||
Fresh: 490,
|
||||
CNY: 126,
|
||||
USD: 376,
|
||||
GeneratedAt: "2026-05-13T09:30:00+08:00",
|
||||
TotalModels: 504,
|
||||
AllModels: []ModelInfo{
|
||||
{
|
||||
Name: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 262144,
|
||||
InputPrice: 0.30,
|
||||
OutputPrice: 1.20,
|
||||
Currency: "USD",
|
||||
OperatorName: "OpenRouter",
|
||||
OperatorType: "reseller",
|
||||
Region: "global",
|
||||
SceneTags: []SceneTag{SceneCode, SceneReasoning},
|
||||
},
|
||||
{
|
||||
Name: "glm-5",
|
||||
ProviderName: "Zhipu",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 131072,
|
||||
InputPrice: 0,
|
||||
OutputPrice: 0,
|
||||
Currency: "CNY",
|
||||
IsFree: true,
|
||||
OperatorName: "Zhipu",
|
||||
OperatorType: "official",
|
||||
Region: "cn",
|
||||
SceneTags: []SceneTag{SceneWriting, SceneChat},
|
||||
},
|
||||
{
|
||||
Name: "claude-3.7-sonnet",
|
||||
ProviderName: "Anthropic",
|
||||
ProviderCountry:"US",
|
||||
ContextLength: 200000,
|
||||
InputPrice: 3.0,
|
||||
OutputPrice: 15.0,
|
||||
Currency: "USD",
|
||||
OperatorName: "Anthropic",
|
||||
OperatorType: "official",
|
||||
Region: "global",
|
||||
SceneTags: []SceneTag{SceneWriting, SceneChat},
|
||||
},
|
||||
{
|
||||
Name: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 65536,
|
||||
InputPrice: 0.8,
|
||||
OutputPrice: 2.4,
|
||||
Currency: "CNY",
|
||||
OperatorName: "DashScope",
|
||||
OperatorType: "cloud",
|
||||
Region: "cn",
|
||||
SceneTags: []SceneTag{SceneVision},
|
||||
},
|
||||
},
|
||||
TencentSubscriptionPlans: []SubscriptionPlanInfo{
|
||||
FreeModels: []ModelInfo{
|
||||
{
|
||||
PlanName: "通用 Token Plan Lite",
|
||||
PlanFamily: "token_plan",
|
||||
Tier: "Lite",
|
||||
Name: "glm-5",
|
||||
ProviderName: "Zhipu",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 131072,
|
||||
Currency: "CNY",
|
||||
ListPrice: 39,
|
||||
QuotaValue: 35000000,
|
||||
QuotaUnit: "tokens/month",
|
||||
ContextWindow: 0,
|
||||
ModelCount: 10,
|
||||
ModelPreview: "tc-code-latest, glm-5, glm-5.1",
|
||||
IsFree: true,
|
||||
OperatorName: "Zhipu",
|
||||
OperatorType: "official",
|
||||
Region: "cn",
|
||||
SceneTags: []SceneTag{SceneWriting, SceneChat},
|
||||
},
|
||||
{
|
||||
PlanName: "Hy Token Plan Max",
|
||||
PlanFamily: "token_plan",
|
||||
Tier: "Max",
|
||||
Currency: "CNY",
|
||||
ListPrice: 468,
|
||||
QuotaValue: 650000000,
|
||||
QuotaUnit: "tokens/month",
|
||||
ContextWindow: 262144,
|
||||
ModelCount: 1,
|
||||
ModelPreview: "hy3-preview",
|
||||
Name: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 262144,
|
||||
Currency: "USD",
|
||||
IsFree: true,
|
||||
OperatorName: "OpenRouter",
|
||||
OperatorType: "reseller",
|
||||
Region: "global",
|
||||
SceneTags: []SceneTag{SceneCode, SceneReasoning},
|
||||
},
|
||||
{
|
||||
Name: "mystery-free-model",
|
||||
ProviderName: "Unknown",
|
||||
ProviderCountry:"unknown",
|
||||
ContextLength: 65536,
|
||||
Currency: "USD",
|
||||
IsFree: true,
|
||||
OperatorName: "Unknown Gateway",
|
||||
OperatorType: "self_hosted_gateway",
|
||||
Region: "global",
|
||||
SceneTags: []SceneTag{SceneChat},
|
||||
},
|
||||
},
|
||||
FreeTop20: []ModelInfo{
|
||||
{
|
||||
Name: "glm-5",
|
||||
ProviderName: "Zhipu",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 131072,
|
||||
Currency: "CNY",
|
||||
IsFree: true,
|
||||
OperatorName: "Zhipu",
|
||||
OperatorType: "official",
|
||||
Region: "cn",
|
||||
},
|
||||
},
|
||||
IntlTop5: []ModelInfo{
|
||||
{
|
||||
Name: "DeepSeek-V4-Flash",
|
||||
ProviderName: "DeepSeek",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 262144,
|
||||
InputPrice: 0.30,
|
||||
OutputPrice: 1.20,
|
||||
Currency: "USD",
|
||||
OperatorName: "OpenRouter",
|
||||
OperatorType: "reseller",
|
||||
Region: "global",
|
||||
SceneTags: []SceneTag{SceneCode, SceneReasoning},
|
||||
},
|
||||
},
|
||||
DomesticTop10: []ModelInfo{
|
||||
{
|
||||
Name: "qwen-vl-max",
|
||||
ProviderName: "Alibaba",
|
||||
ProviderCountry:"CN",
|
||||
ContextLength: 65536,
|
||||
InputPrice: 0.8,
|
||||
OutputPrice: 2.4,
|
||||
Currency: "CNY",
|
||||
OperatorName: "DashScope",
|
||||
OperatorType: "cloud",
|
||||
Region: "cn",
|
||||
SceneTags: []SceneTag{SceneVision},
|
||||
},
|
||||
},
|
||||
DailySignals: DailySignals{
|
||||
NewModels: 2,
|
||||
PriceChanges: 1,
|
||||
OfficialFree: 1,
|
||||
AggregatorFree:1,
|
||||
UnknownFree: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFreeSourceBreakdown(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
|
||||
breakdown := buildFreeSourceBreakdown(report.FreeModels)
|
||||
|
||||
if len(breakdown) != 3 {
|
||||
t.Fatalf("expected 3 free source groups, got %d", len(breakdown))
|
||||
}
|
||||
|
||||
if breakdown[0].Label != "官方免费" || breakdown[0].Count != 1 {
|
||||
t.Fatalf("unexpected official free breakdown: %+v", breakdown[0])
|
||||
}
|
||||
if breakdown[1].Label != "聚合免费" || breakdown[1].Count != 1 {
|
||||
t.Fatalf("unexpected aggregator free breakdown: %+v", breakdown[1])
|
||||
}
|
||||
if breakdown[2].Label != "待确认" || breakdown[2].Count != 1 {
|
||||
t.Fatalf("unexpected unknown free breakdown: %+v", breakdown[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecorateReportV1BuildsHotDaySummary(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
|
||||
decorateReportV1(report)
|
||||
|
||||
if report.PageMode != "hot" {
|
||||
t.Fatalf("expected hot page mode, got %q", report.PageMode)
|
||||
}
|
||||
if !strings.Contains(report.HeroSummary, "2 个新模型") {
|
||||
t.Fatalf("hero summary missing new model signal: %s", report.HeroSummary)
|
||||
}
|
||||
if len(report.ActionItems) != 3 {
|
||||
t.Fatalf("expected 3 action items, got %d", len(report.ActionItems))
|
||||
}
|
||||
if len(report.HeadlineItems) == 0 {
|
||||
t.Fatalf("expected headline items to be built")
|
||||
}
|
||||
if report.ActionItems[0].Evidence == "" {
|
||||
t.Fatalf("expected action item evidence to be populated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecorateReportV1BuildsCalmDaySummary(t *testing.T) {
|
||||
report := sampleReportForV1()
|
||||
report.DailySignals = DailySignals{}
|
||||
|
||||
decorateReportV1(report)
|
||||
|
||||
if report.PageMode != "calm" {
|
||||
t.Fatalf("expected calm page mode, got %q", report.PageMode)
|
||||
}
|
||||
if !strings.Contains(report.HeroSummary, "稳定") {
|
||||
t.Fatalf("expected calm day summary to emphasize stability, got %s", report.HeroSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report.md")
|
||||
report := sampleReportForV1()
|
||||
report.QualitySummary = DataQualitySummary{
|
||||
Total: 502,
|
||||
Fresh: 490,
|
||||
CNY: 126,
|
||||
USD: 376,
|
||||
}
|
||||
report.TencentSubscriptionPlans = []SubscriptionPlanInfo{
|
||||
{
|
||||
PlanName: "通用 Token Plan Lite",
|
||||
PlanFamily: "token_plan",
|
||||
Tier: "Lite",
|
||||
Currency: "CNY",
|
||||
ListPrice: 39,
|
||||
QuotaValue: 35000000,
|
||||
QuotaUnit: "tokens/month",
|
||||
ContextWindow: 0,
|
||||
ModelCount: 10,
|
||||
ModelPreview: "tc-code-latest, glm-5, glm-5.1",
|
||||
},
|
||||
{
|
||||
PlanName: "Hy Token Plan Max",
|
||||
PlanFamily: "token_plan",
|
||||
Tier: "Max",
|
||||
Currency: "CNY",
|
||||
ListPrice: 468,
|
||||
QuotaValue: 650000000,
|
||||
QuotaUnit: "tokens/month",
|
||||
ContextWindow: 262144,
|
||||
ModelCount: 1,
|
||||
ModelPreview: "hy3-preview",
|
||||
},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
if err := generateMarkdownV3(report, path); err != nil {
|
||||
t.Fatalf("generateMarkdownV3 returned error: %v", err)
|
||||
@@ -59,6 +264,11 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"## 今日结论",
|
||||
"## 今日行动建议",
|
||||
"## 今日变化",
|
||||
"## 场景推荐",
|
||||
"## 完整数据附录",
|
||||
"## 💳 腾讯云套餐订阅价",
|
||||
"通用 Token Plan Lite",
|
||||
"Hy Token Plan Max",
|
||||
@@ -74,23 +284,21 @@ func TestGenerateMarkdownV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
|
||||
func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "daily_report.html")
|
||||
report := &ReportV3{
|
||||
Date: "2026-05-13",
|
||||
TotalModels: 502,
|
||||
TencentSubscriptionPlans: []SubscriptionPlanInfo{
|
||||
{
|
||||
PlanName: "通用 Token Plan Lite",
|
||||
PlanFamily: "token_plan",
|
||||
Tier: "Lite",
|
||||
Currency: "CNY",
|
||||
ListPrice: 39,
|
||||
QuotaValue: 35000000,
|
||||
QuotaUnit: "tokens/month",
|
||||
ModelCount: 10,
|
||||
ModelPreview: "tc-code-latest, glm-5, glm-5.1",
|
||||
},
|
||||
report := sampleReportForV1()
|
||||
report.TencentSubscriptionPlans = []SubscriptionPlanInfo{
|
||||
{
|
||||
PlanName: "通用 Token Plan Lite",
|
||||
PlanFamily: "token_plan",
|
||||
Tier: "Lite",
|
||||
Currency: "CNY",
|
||||
ListPrice: 39,
|
||||
QuotaValue: 35000000,
|
||||
QuotaUnit: "tokens/month",
|
||||
ModelCount: 10,
|
||||
ModelPreview: "tc-code-latest, glm-5, glm-5.1",
|
||||
},
|
||||
}
|
||||
decorateReportV1(report)
|
||||
|
||||
if err := generateHTMLV3(report, path); err != nil {
|
||||
t.Fatalf("generateHTMLV3 returned error: %v", err)
|
||||
@@ -103,10 +311,15 @@ func TestGenerateHTMLV3IncludesTencentSubscriptionSection(t *testing.T) {
|
||||
|
||||
content := string(body)
|
||||
for _, want := range []string{
|
||||
"今日一句话结论",
|
||||
"三条行动建议",
|
||||
"今日头条",
|
||||
"场景推荐",
|
||||
"完整数据附录",
|
||||
"官方免费",
|
||||
"聚合免费",
|
||||
"待确认",
|
||||
"💳 腾讯云套餐订阅价",
|
||||
"通用 Token Plan Lite",
|
||||
"¥39.00/月",
|
||||
"3500万 Tokens/月",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("html missing %q\n%s", want, content)
|
||||
|
||||
120
scripts/report_utils.sh
Normal file
120
scripts/report_utils.sh
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
report_date_value() {
|
||||
printf '%s\n' "${1:-$(date +%Y-%m-%d)}"
|
||||
}
|
||||
|
||||
report_output_dir() {
|
||||
printf '%s\n' "reports/daily"
|
||||
}
|
||||
|
||||
report_html_dir() {
|
||||
printf '%s\n' "$(report_output_dir)/html"
|
||||
}
|
||||
|
||||
report_markdown_path() {
|
||||
local report_date
|
||||
report_date="$(report_date_value "${1:-}")"
|
||||
printf '%s\n' "$(report_output_dir)/daily_report_${report_date}.md"
|
||||
}
|
||||
|
||||
report_html_path() {
|
||||
local report_date
|
||||
report_date="$(report_date_value "${1:-}")"
|
||||
printf '%s\n' "$(report_html_dir)/daily_report_${report_date}.html"
|
||||
}
|
||||
|
||||
report_archive_dir() {
|
||||
local report_date
|
||||
report_date="$(report_date_value "${1:-}")"
|
||||
printf '%s\n' "$(report_output_dir)/${report_date:0:4}/${report_date:5:2}"
|
||||
}
|
||||
|
||||
report_archive_markdown_path() {
|
||||
local report_date
|
||||
report_date="$(report_date_value "${1:-}")"
|
||||
printf '%s\n' "$(report_archive_dir "$report_date")/daily_report_${report_date}.md"
|
||||
}
|
||||
|
||||
report_archive_html_path() {
|
||||
local report_date
|
||||
report_date="$(report_date_value "${1:-}")"
|
||||
printf '%s\n' "$(report_archive_dir "$report_date")/daily_report_${report_date}.html"
|
||||
}
|
||||
|
||||
archive_report_artifacts() {
|
||||
local report_date markdown_path html_path archive_dir
|
||||
report_date="$(report_date_value "${1:-}")"
|
||||
markdown_path="$(report_markdown_path "$report_date")"
|
||||
html_path="$(report_html_path "$report_date")"
|
||||
archive_dir="$(report_archive_dir "$report_date")"
|
||||
|
||||
mkdir -p "$archive_dir"
|
||||
cp "$markdown_path" "$(report_archive_markdown_path "$report_date")"
|
||||
cp "$html_path" "$(report_archive_html_path "$report_date")"
|
||||
}
|
||||
|
||||
track_report_state() {
|
||||
local db_url report_date status model_count summary_md output_path error_message
|
||||
db_url="$1"
|
||||
report_date="$2"
|
||||
status="$3"
|
||||
model_count="${4:-}"
|
||||
summary_md="${5:-}"
|
||||
output_path="${6:-}"
|
||||
error_message="${7:-}"
|
||||
|
||||
psql "$db_url" \
|
||||
-v ON_ERROR_STOP=1 \
|
||||
--set=report_date="$report_date" \
|
||||
--set=status="$status" \
|
||||
--set=model_count="$model_count" \
|
||||
--set=summary_md="$summary_md" \
|
||||
--set=output_path="$output_path" \
|
||||
--set=error_message="$error_message" <<'SQL'
|
||||
INSERT INTO daily_report (
|
||||
report_date,
|
||||
status,
|
||||
model_count,
|
||||
summary_md,
|
||||
output_path,
|
||||
error_message,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
:'report_date',
|
||||
:'status',
|
||||
NULLIF(:'model_count', '')::INTEGER,
|
||||
NULLIF(:'summary_md', ''),
|
||||
NULLIF(:'output_path', ''),
|
||||
NULLIF(:'error_message', ''),
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (report_date) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
model_count = COALESCE(EXCLUDED.model_count, daily_report.model_count),
|
||||
summary_md = COALESCE(EXCLUDED.summary_md, daily_report.summary_md),
|
||||
output_path = COALESCE(EXCLUDED.output_path, daily_report.output_path),
|
||||
error_message = EXCLUDED.error_message,
|
||||
updated_at = NOW();
|
||||
|
||||
INSERT INTO report_runs (
|
||||
source,
|
||||
report_date,
|
||||
status,
|
||||
summary_md,
|
||||
output_path,
|
||||
error_message
|
||||
)
|
||||
VALUES (
|
||||
'pipeline',
|
||||
:'report_date',
|
||||
:'status',
|
||||
NULLIF(:'summary_md', ''),
|
||||
NULLIF(:'output_path', ''),
|
||||
NULLIF(:'error_message', '')
|
||||
);
|
||||
SQL
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="/home/long/project/llm-intelligence"
|
||||
. "$PROJECT_DIR/scripts/report_utils.sh"
|
||||
DB_URL="${DATABASE_URL:-host=/var/run/postgresql dbname=llm_intelligence user=long sslmode=disable}"
|
||||
REPORT_DATE=$(date +%Y-%m-%d)
|
||||
REPORT_DATE="$(report_date_value)"
|
||||
LOG_FILE="/tmp/llm_hub_daily_${REPORT_DATE}.log"
|
||||
FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-}"
|
||||
MODEL_COUNT=""
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
@@ -16,9 +18,14 @@ log() {
|
||||
|
||||
# 错误处理
|
||||
error_exit() {
|
||||
local output_path=""
|
||||
log "❌ 错误: $1"
|
||||
# 降级:复制昨日报告
|
||||
fallback_report
|
||||
if [ -f "$(report_markdown_path "$REPORT_DATE")" ]; then
|
||||
output_path="$(report_markdown_path "$REPORT_DATE")"
|
||||
fi
|
||||
track_report_state "$DB_URL" "$REPORT_DATE" "failed" "${MODEL_COUNT:-}" "" "$output_path" "$1" >> "$LOG_FILE" 2>&1 || true
|
||||
# 发送告警
|
||||
if [ -n "$FEISHU_WEBHOOK" ]; then
|
||||
send_alert "$1"
|
||||
@@ -28,14 +35,24 @@ error_exit() {
|
||||
|
||||
# 降级:复制昨日报告
|
||||
fallback_report() {
|
||||
local yesterday=$(date -d "yesterday" +%Y-%m-%d)
|
||||
local yesterday_md="${PROJECT_DIR}/reports/daily/daily_report_${yesterday}.md"
|
||||
local today_md="${PROJECT_DIR}/reports/daily/daily_report_${REPORT_DATE}.md"
|
||||
local yesterday yesterday_md today_md yesterday_html today_html
|
||||
yesterday=$(date -d "yesterday" +%Y-%m-%d)
|
||||
yesterday_md="${PROJECT_DIR}/$(report_markdown_path "$yesterday")"
|
||||
today_md="${PROJECT_DIR}/$(report_markdown_path "$REPORT_DATE")"
|
||||
yesterday_html="${PROJECT_DIR}/$(report_html_path "$yesterday")"
|
||||
today_html="${PROJECT_DIR}/$(report_html_path "$REPORT_DATE")"
|
||||
|
||||
if [ -f "$yesterday_md" ]; then
|
||||
cp "$yesterday_md" "$today_md"
|
||||
sed -i "s/${yesterday}/${REPORT_DATE}/g" "$today_md"
|
||||
sed -i "1s/^/# [数据延迟] /" "$today_md"
|
||||
if [ -f "$yesterday_html" ]; then
|
||||
cp "$yesterday_html" "$today_html"
|
||||
sed -i "s/${yesterday}/${REPORT_DATE}/g" "$today_html"
|
||||
fi
|
||||
if [ -f "$today_md" ] && [ -f "$today_html" ]; then
|
||||
archive_report_artifacts "$REPORT_DATE" >> "$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
log "⚠️ 已复制昨日报告并标记[数据延迟]"
|
||||
else
|
||||
log "⚠️ 无昨日报告可供复制"
|
||||
@@ -81,29 +98,25 @@ if ! go run scripts/generate_daily_report.go >> "$LOG_FILE" 2>&1; then
|
||||
fi
|
||||
log "✅ 日报生成完成"
|
||||
|
||||
# 4. 归档
|
||||
log "4️⃣ 归档报告..."
|
||||
ARCHIVE_DIR="reports/daily/$(date +%Y/%m)"
|
||||
mkdir -p "$ARCHIVE_DIR"
|
||||
cp "reports/daily/daily_report_${REPORT_DATE}.md" "$ARCHIVE_DIR/" 2>/dev/null || true
|
||||
cp "reports/daily/html/daily_report_${REPORT_DATE}.html" "$ARCHIVE_DIR/" 2>/dev/null || true
|
||||
# 4. 校验归档
|
||||
log "4️⃣ 校验归档..."
|
||||
if [ ! -f "$(report_archive_markdown_path "$REPORT_DATE")" ] || [ ! -f "$(report_archive_html_path "$REPORT_DATE")" ]; then
|
||||
error_exit "日报归档失败"
|
||||
fi
|
||||
log "✅ 归档完成"
|
||||
|
||||
# 5. 更新 daily_report 表
|
||||
log "5️⃣ 更新日报记录..."
|
||||
psql "$DB_URL" -c "
|
||||
INSERT INTO daily_report (report_date, status, model_count, output_path, created_at, updated_at)
|
||||
VALUES ('${REPORT_DATE}', 'generated', ${MODEL_COUNT}, 'reports/daily/daily_report_${REPORT_DATE}.md', NOW(), NOW())
|
||||
ON CONFLICT (report_date) DO UPDATE SET
|
||||
status = 'generated',
|
||||
model_count = EXCLUDED.model_count,
|
||||
output_path = EXCLUDED.output_path,
|
||||
updated_at = NOW()
|
||||
" >> "$LOG_FILE" 2>&1
|
||||
# 5. 校验运行记录
|
||||
log "5️⃣ 校验运行记录..."
|
||||
if ! psql "$DB_URL" -Atqc "select count(*) from daily_report where report_date = DATE '${REPORT_DATE}' and status = 'generated';" | awk '{ exit !($1 >= 1) }'; then
|
||||
error_exit "daily_report 未写入 generated 记录"
|
||||
fi
|
||||
if ! psql "$DB_URL" -Atqc "select count(*) from report_runs where report_date = DATE '${REPORT_DATE}' and status = 'generated';" | awk '{ exit !($1 >= 1) }'; then
|
||||
error_exit "report_runs 未写入 generated 记录"
|
||||
fi
|
||||
log "✅ 日报记录更新完成"
|
||||
|
||||
log "🎉 每日流水线全部完成!"
|
||||
log "📄 Markdown: reports/daily/daily_report_${REPORT_DATE}.md"
|
||||
log "🌐 HTML: reports/daily/html/daily_report_${REPORT_DATE}.html"
|
||||
log "📄 Markdown: $(report_markdown_path "$REPORT_DATE")"
|
||||
log "🌐 HTML: $(report_html_path "$REPORT_DATE")"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
. "$ROOT_DIR/scripts/report_utils.sh"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [[ -f ".env.local" ]]; then
|
||||
@@ -23,16 +24,56 @@ if [[ -z "${OPENROUTER_API_KEY:-}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPORT_DATE="$(report_date_value)"
|
||||
|
||||
record_failure() {
|
||||
local error_message output_path
|
||||
error_message="$1"
|
||||
output_path=""
|
||||
|
||||
if [[ -f "$(report_markdown_path "$REPORT_DATE")" ]]; then
|
||||
output_path="$(report_markdown_path "$REPORT_DATE")"
|
||||
fi
|
||||
|
||||
track_report_state "$DATABASE_URL" "$REPORT_DATE" "failed" "" "" "$output_path" "$error_message" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
"$ROOT_DIR/scripts/apply_migration.sh"
|
||||
|
||||
go run "./scripts/fetch_openrouter.go" \
|
||||
if ! go run "./scripts/fetch_openrouter.go" \
|
||||
-api-key "$OPENROUTER_API_KEY" \
|
||||
-db "$DATABASE_URL" \
|
||||
-out "$ROOT_DIR/models.json"
|
||||
-out "$ROOT_DIR/models.json"; then
|
||||
record_failure "真实采集失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go run "./scripts/generate_daily_report.go" \
|
||||
-json "$ROOT_DIR/models.json" \
|
||||
-out "$ROOT_DIR/reports/daily"
|
||||
if ! go run "./scripts/generate_daily_report.go"; then
|
||||
record_failure "日报生成失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$(report_archive_markdown_path "$REPORT_DATE")" || ! -f "$(report_archive_html_path "$REPORT_DATE")" ]]; then
|
||||
record_failure "日报归档缺失"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! psql "$DATABASE_URL" -Atqc "select count(*) from daily_report where report_date = current_date and status = 'generated';" | awk '{ exit !($1 >= 1) }'; then
|
||||
record_failure "daily_report 未写入 generated 记录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! psql "$DATABASE_URL" -Atqc "select count(*) from report_runs where report_date = current_date and status = 'generated';" | awk '{ exit !($1 >= 1) }'; then
|
||||
record_failure "report_runs 未写入 generated 记录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
psql "$DATABASE_URL" -Atqc \
|
||||
"select 'models', count(*) from models union all select 'model_prices', count(*) from model_prices union all select 'report_runs', count(*) from report_runs order by 1;"
|
||||
"select 'daily_report', count(*) from daily_report where report_date = current_date
|
||||
union all
|
||||
select 'models', count(*) from models
|
||||
union all
|
||||
select 'region_pricing', count(*) from region_pricing
|
||||
union all
|
||||
select 'report_runs', count(*) from report_runs where report_date = current_date
|
||||
order by 1;"
|
||||
|
||||
@@ -4,9 +4,13 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/verify_common.sh"
|
||||
. "$SCRIPT_DIR/report_utils.sh"
|
||||
|
||||
TODAY="$(date +%Y-%m-%d)"
|
||||
ARCHIVE_DIR="reports/daily/$(date +%Y/%m)"
|
||||
TODAY="$(report_date_value)"
|
||||
TODAY_MARKDOWN_PATH="$(report_markdown_path "$TODAY")"
|
||||
TODAY_HTML_PATH="$(report_html_path "$TODAY")"
|
||||
TODAY_ARCHIVE_MARKDOWN_PATH="$(report_archive_markdown_path "$TODAY")"
|
||||
TODAY_ARCHIVE_HTML_PATH="$(report_archive_html_path "$TODAY")"
|
||||
|
||||
echo "=== Phase 3 验收检查 ==="
|
||||
|
||||
@@ -15,11 +19,16 @@ check_executable "scripts/feishu_alert.sh" "飞书告警脚本可执行"
|
||||
check_shell "日报生成器可独立构建" "go build -o /dev/null ./scripts/generate_daily_report.go"
|
||||
check_shell "日报脚本包含降级逻辑" "grep -q 'fallback_report' scripts/run_daily.sh"
|
||||
check_shell "日报脚本包含飞书告警逻辑" "grep -q 'send_alert' scripts/run_daily.sh"
|
||||
check_shell "今日日报文件存在且包含数据质量摘要" "test -f reports/daily/daily_report_${TODAY}.md && grep -q '数据质量摘要' reports/daily/daily_report_${TODAY}.md"
|
||||
check_shell "今日归档报告存在" "test -f ${ARCHIVE_DIR}/daily_report_${TODAY}.md"
|
||||
check_shell "今日日报 Markdown 主产物存在且包含数据质量摘要" "test -f ${TODAY_MARKDOWN_PATH} && grep -q '数据质量摘要' ${TODAY_MARKDOWN_PATH}"
|
||||
check_shell "今日日报 HTML 主产物存在" "test -f ${TODAY_HTML_PATH}"
|
||||
check_shell "今日日报归档副本存在(Markdown + HTML)" "test -f ${TODAY_ARCHIVE_MARKDOWN_PATH} && test -f ${TODAY_ARCHIVE_HTML_PATH}"
|
||||
check_shell "日报归档约定已统一收敛到公共工具" "grep -q 'report_utils.sh' scripts/run_daily.sh && grep -q 'report_utils.sh' scripts/run_real_pipeline.sh && grep -q 'report_utils.sh' scripts/verify_phase3.sh"
|
||||
check_sql_int_ge "daily_report 已写入至少 1 条 generated 记录" \
|
||||
"select count(*) from daily_report where status='generated';" \
|
||||
1
|
||||
check_sql_int_ge "report_runs 已写入至少 1 条 generated 记录" \
|
||||
"select count(*) from report_runs where status='generated';" \
|
||||
1
|
||||
check_shell "crontab 已配置每日调度" "crontab -l 2>/dev/null | grep -q 'scripts/run_daily.sh'"
|
||||
check_shell "真实采集 API Key 已配置" "([ -n \"${OPENROUTER_API_KEY:-}\" ] || ([ -f .env.local ] && grep -Eq '^OPENROUTER_API_KEY=.+' .env.local) || ([ -f .env ] && grep -Eq '^OPENROUTER_API_KEY=.+' .env))"
|
||||
|
||||
|
||||
@@ -11,15 +11,15 @@ check_file "Dockerfile" "Dockerfile 存在"
|
||||
check_file "docker-compose.yml" "docker-compose.yml 存在"
|
||||
check_file "nginx.conf" "Nginx 配置存在"
|
||||
check_file ".env.example" ".env.example 存在"
|
||||
check_file ".github/workflows/ci.yml" "GitHub Actions CI 配置存在"
|
||||
check_file ".github/workflows/ci.yml" "GitHub Actions CI 工作流存在"
|
||||
check_executable "scripts/backup.sh" "数据库备份脚本可执行"
|
||||
check_file "healthcheck.sh" "健康检查脚本存在"
|
||||
check_file "scripts/restore.sh" "数据库恢复脚本存在"
|
||||
check_shell "CI 包含 Go 测试" "grep -Eq 'go test .*\\./internal/|go test .*\\./\\.\\.\\.' .github/workflows/ci.yml"
|
||||
check_shell "CI 包含前端构建" "grep -q 'npm run build' .github/workflows/ci.yml"
|
||||
check_shell "CI 包含 Docker 构建" "grep -q 'docker build' .github/workflows/ci.yml"
|
||||
check_shell "CI 配置了覆盖率门禁" "grep -Eqi 'coverage|80%' .github/workflows/ci.yml"
|
||||
check_shell "CI 配置了构建产物上传" "grep -Eqi 'upload-artifact|artifacts' .github/workflows/ci.yml"
|
||||
check_shell "Makefile 暴露真实流水线与总门禁入口" "grep -q '^run-real-pipeline:' Makefile && grep -q '^verify-phase1:' Makefile && grep -q '^verify-phase6:' Makefile && grep -q '^verify-pre-phase6:' Makefile"
|
||||
check_shell "部署文档覆盖 Docker、前端启动与 cron 配置" "grep -q 'docker build' DEPLOYMENT.md && grep -q 'npm run dev' DEPLOYMENT.md && grep -q 'crontab -e' DEPLOYMENT.md"
|
||||
check_shell "健康检查脚本覆盖数据库与日报可用性" "grep -q 'psql' healthcheck.sh && grep -q 'reports/daily/daily_report_' healthcheck.sh"
|
||||
check_shell "备份恢复脚本具备 PostgreSQL 入口" "grep -Eq 'pg_dump|psql' scripts/backup.sh && grep -Eq 'psql|pg_restore' scripts/restore.sh"
|
||||
check_shell "CI 工作流覆盖 Go 测试、前端构建与 Docker 构建" "test ! -f .github/workflows/ci.yml || (grep -q 'go test ./...' .github/workflows/ci.yml && grep -q 'npm run build' .github/workflows/ci.yml && grep -Eq 'docker build|docker/build-push-action' .github/workflows/ci.yml)"
|
||||
check_shell "日志轮转配置已落地" "find . -maxdepth 3 -type f | grep -Eqi 'logrotate|logrotate\\.conf'"
|
||||
|
||||
finish_phase
|
||||
|
||||
Reference in New Issue
Block a user