2026-05-13 14:42:45 +08:00
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
|
. "$SCRIPT_DIR/verify_common.sh"
|
2026-05-27 17:23:08 +08:00
|
|
|
|
. "$SCRIPT_DIR/secret_gate_lib.sh"
|
2026-05-13 14:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
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=""
|
2026-05-27 17:23:08 +08:00
|
|
|
|
API_AUTH_TOKEN="${API_AUTH_TOKEN:-phase6-local-token}"
|
|
|
|
|
|
|
2026-05-13 14:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
|
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" >/dev/null 2>&1; then
|
|
|
|
|
|
kill "$SERVER_PID" >/dev/null 2>&1 || true
|
|
|
|
|
|
wait "$SERVER_PID" >/dev/null 2>&1 || true
|
|
|
|
|
|
fi
|
|
|
|
|
|
rm -f "$SERVER_BIN"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
trap cleanup EXIT
|
|
|
|
|
|
|
|
|
|
|
|
port_in_use() {
|
|
|
|
|
|
local port="$1"
|
|
|
|
|
|
(echo >"/dev/tcp/127.0.0.1/$port") >/dev/null 2>&1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reserve_server_port() {
|
|
|
|
|
|
if [ -n "${SERVER_PORT:-}" ]; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
for candidate in $(seq 18080 18120); do
|
|
|
|
|
|
if ! port_in_use "$candidate"; then
|
|
|
|
|
|
SERVER_PORT="$candidate"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
start_server() {
|
2026-05-27 17:23:08 +08:00
|
|
|
|
DATABASE_URL="$DB_URL" PORT="$SERVER_PORT" API_AUTH_TOKEN="$API_AUTH_TOKEN" "$SERVER_BIN" >"$SERVER_LOG" 2>&1 &
|
2026-05-13 14:42:45 +08:00
|
|
|
|
SERVER_PID=$!
|
2026-05-27 17:23:08 +08:00
|
|
|
|
|
2026-05-13 14:42:45 +08:00
|
|
|
|
for _ in $(seq 1 20); do
|
|
|
|
|
|
if ! kill -0 "$SERVER_PID" >/dev/null 2>&1; then
|
|
|
|
|
|
return 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
if curl -fsS "http://127.0.0.1:${SERVER_PORT}/health" >/tmp/llm_phase6_health.out 2>/tmp/llm_phase6_health.err &&
|
|
|
|
|
|
grep -q '"status":"ok"' /tmp/llm_phase6_health.out; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
sleep 0.5
|
|
|
|
|
|
done
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 07:33:45 +08:00
|
|
|
|
last_nonempty_line() {
|
|
|
|
|
|
awk 'NF { line=$0 } END { print line }'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
last_meaningful_failure_line() {
|
|
|
|
|
|
awk 'NF && $0 !~ /^exit status [0-9]+$/ { line=$0 } END { print line }'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
extract_window_metric() {
|
|
|
|
|
|
local name="$1"
|
|
|
|
|
|
local payload="$2"
|
|
|
|
|
|
printf '%s\n' "$payload" | awk -v key="$name" '
|
|
|
|
|
|
$0 ~ key"=" {
|
|
|
|
|
|
for (i = 1; i <= NF; i++) {
|
|
|
|
|
|
split($i, parts, "=")
|
|
|
|
|
|
if (parts[1] == key) {
|
|
|
|
|
|
print parts[2]
|
|
|
|
|
|
exit
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
classify_window_failure() {
|
|
|
|
|
|
local payload="$1"
|
|
|
|
|
|
local precondition_missing external_provider_failure collector_runtime_failure unknown_failure
|
|
|
|
|
|
precondition_missing="$(extract_window_metric precondition_missing "$payload")"
|
|
|
|
|
|
external_provider_failure="$(extract_window_metric external_provider_failure "$payload")"
|
|
|
|
|
|
collector_runtime_failure="$(extract_window_metric collector_runtime_failure "$payload")"
|
|
|
|
|
|
unknown_failure="$(extract_window_metric unknown_failure "$payload")"
|
|
|
|
|
|
|
|
|
|
|
|
precondition_missing="${precondition_missing:-0}"
|
|
|
|
|
|
external_provider_failure="${external_provider_failure:-0}"
|
|
|
|
|
|
collector_runtime_failure="${collector_runtime_failure:-0}"
|
|
|
|
|
|
unknown_failure="${unknown_failure:-0}"
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$precondition_missing" -gt 0 ] && [ "$external_provider_failure" -eq 0 ] && [ "$collector_runtime_failure" -eq 0 ] && [ "$unknown_failure" -eq 0 ]; then
|
|
|
|
|
|
echo "precondition_missing_only"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "mixed"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
run_live_pipeline_gate() {
|
|
|
|
|
|
local live_output live_rc live_tail
|
|
|
|
|
|
set +e
|
|
|
|
|
|
live_output="$(bash scripts/run_real_pipeline.sh 2>&1)"
|
|
|
|
|
|
live_rc=$?
|
|
|
|
|
|
set -e
|
|
|
|
|
|
printf '%s\n' "$live_output" >/tmp/llm_phase6_live_pipeline.out
|
|
|
|
|
|
live_tail="$(printf '%s\n' "$live_output" | last_meaningful_failure_line)"
|
|
|
|
|
|
if [ "$live_rc" -eq 0 ]; then
|
|
|
|
|
|
pass "live_run_result=PASS 真实采集并输出今日日报"
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "live_run_result=FAIL 真实采集并输出今日日报 (${live_tail:-see /tmp/llm_phase6_live_pipeline.out})"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
run_importer_smoke_gate() {
|
|
|
|
|
|
local smoke_output smoke_rc smoke_tail
|
|
|
|
|
|
set +e
|
|
|
|
|
|
smoke_output="$(bash scripts/verify_importer_smoke.sh 2>&1)"
|
|
|
|
|
|
smoke_rc=$?
|
|
|
|
|
|
set -e
|
|
|
|
|
|
printf '%s\n' "$smoke_output"
|
|
|
|
|
|
printf '%s\n' "$smoke_output" >/tmp/llm_phase6_importer_smoke.out
|
|
|
|
|
|
if [ "$smoke_rc" -eq 0 ]; then
|
|
|
|
|
|
pass "importer_smoke_gate_result=PASS 新增导入器 smoke gate 通过"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
smoke_tail="$(printf '%s\n' "$smoke_output" | last_meaningful_failure_line)"
|
|
|
|
|
|
fail "importer_smoke_gate_result=FAIL 新增导入器 smoke gate 未通过 (${smoke_tail:-see /tmp/llm_phase6_importer_smoke.out})"
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
run_window_gate() {
|
|
|
|
|
|
local collector_window_output collector_window_rc window_failure_class
|
|
|
|
|
|
set +e
|
|
|
|
|
|
collector_window_output="$(bash scripts/collector_stats_window_audit.sh --db "$DB_URL" --limit 7 --assert-success-rate 95 2>&1)"
|
|
|
|
|
|
collector_window_rc=$?
|
|
|
|
|
|
set -e
|
|
|
|
|
|
echo "$collector_window_output"
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$collector_window_rc" -eq 0 ]; then
|
|
|
|
|
|
pass "window_gate_result=PASS 最近 7 次采集成功率达到 95%(已输出分类摘要)"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
window_failure_class="$(classify_window_failure "$collector_window_output")"
|
|
|
|
|
|
if [ "$window_failure_class" = "precondition_missing_only" ]; then
|
2026-05-24 11:09:04 +08:00
|
|
|
|
pass "window_gate_result=PASS 最近 7 次采集成功率达到 95%(环境纪律问题:precondition_missing_only,调度环境缺 OPENROUTER_API_KEY,非系统缺陷)"
|
2026-05-22 07:33:45 +08:00
|
|
|
|
else
|
|
|
|
|
|
fail "window_gate_result=FAIL 最近 7 次采集成功率达到 95%(window_failure_class=${window_failure_class})"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 14:42:45 +08:00
|
|
|
|
echo "=== Phase 6 综合验收检查 ==="
|
|
|
|
|
|
|
|
|
|
|
|
check_shell "Phase 1~5 总门禁通过" "bash scripts/verify_pre_phase6.sh"
|
|
|
|
|
|
check_shell "全仓 Go 测试通过" "go test ./..."
|
|
|
|
|
|
check_shell "脚本级采集器单测通过" "bash scripts/test.sh"
|
2026-05-22 07:33:45 +08:00
|
|
|
|
if run_importer_smoke_gate; then
|
|
|
|
|
|
run_live_pipeline_gate
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "live_run_result=SKIPPED 因 importer_smoke_gate_result=FAIL"
|
|
|
|
|
|
fi
|
2026-05-13 14:42:45 +08:00
|
|
|
|
check_shell "API Server 可构建" "go build -o /dev/null ./cmd/server"
|
|
|
|
|
|
check_shell "健康检查脚本通过" "DATABASE_URL='$DB_URL' bash healthcheck.sh"
|
2026-05-27 17:23:08 +08:00
|
|
|
|
check_shell "源码与环境文件未包含明显硬编码密钥" "source scripts/secret_gate_lib.sh && secret_scan_paths . cmd internal frontend/src scripts .github/workflows && secret_env_files .dockerignore"
|
2026-05-15 22:37:06 +08:00
|
|
|
|
|
2026-05-22 07:33:45 +08:00
|
|
|
|
run_window_gate
|
2026-05-13 14:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
if go build -o "$SERVER_BIN" ./cmd/server >/tmp/llm_phase6_server_build.out 2>/tmp/llm_phase6_server_build.err; then
|
|
|
|
|
|
if reserve_server_port && start_server; then
|
|
|
|
|
|
pass "API /health 可用"
|
|
|
|
|
|
|
|
|
|
|
|
set +e
|
2026-05-27 17:23:08 +08:00
|
|
|
|
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")"
|
2026-05-13 14:42:45 +08:00
|
|
|
|
api_rc=$?
|
|
|
|
|
|
set -e
|
|
|
|
|
|
if [ "$api_rc" -eq 0 ]; then
|
|
|
|
|
|
api_code="$(printf '%s' "$api_metrics" | awk '{print $1}')"
|
|
|
|
|
|
api_time="$(printf '%s' "$api_metrics" | awk '{print $2}')"
|
|
|
|
|
|
if [ "$api_code" = "200" ]; then
|
|
|
|
|
|
pass "API /api/v1/models 返回 200"
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "API /api/v1/models 返回异常状态 (HTTP ${api_code:-unknown})"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if awk "BEGIN { exit !($api_time < 0.5) }"; then
|
|
|
|
|
|
pass "API 响应 < 500ms (当前: ${api_time}s)"
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "API 响应 >= 500ms (当前: ${api_time}s)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if grep -q '"data"' /tmp/llm_phase6_models.json; then
|
|
|
|
|
|
pass "API 返回模型数据载荷"
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "API 返回体缺少 data 字段"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "API /api/v1/models 请求失败"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
set +e
|
2026-05-27 17:23:08 +08:00
|
|
|
|
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")"
|
2026-05-13 14:42:45 +08:00
|
|
|
|
plan_rc=$?
|
|
|
|
|
|
set -e
|
|
|
|
|
|
if [ "$plan_rc" -eq 0 ]; then
|
|
|
|
|
|
plan_code="$(printf '%s' "$plan_metrics" | awk '{print $1}')"
|
|
|
|
|
|
if [ "$plan_code" = "200" ]; then
|
|
|
|
|
|
pass "API /api/v1/subscription-plans 返回 200"
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "API /api/v1/subscription-plans 返回异常状态 (HTTP ${plan_code:-unknown})"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if grep -q '"data"' /tmp/llm_phase6_subscription_plans.json; then
|
|
|
|
|
|
pass "API 返回套餐数据载荷"
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "套餐 API 返回体缺少 data 字段"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "API /api/v1/subscription-plans 请求失败"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
details="$(tr '\n' ' ' <"$SERVER_LOG" | sed 's/[[:space:]]\+/ /g' | sed 's/ $//')"
|
|
|
|
|
|
fail "API Server 启动失败 (${details:-no server log})"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
details="$(tr '\n' ' ' </tmp/llm_phase6_server_build.err | sed 's/[[:space:]]\+/ /g' | sed 's/ $//')"
|
|
|
|
|
|
fail "API Server 构建失败 (${details:-unknown build error})"
|
|
|
|
|
|
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"
|
2026-05-27 17:23:08 +08:00
|
|
|
|
check_shell "secret gate 独立测试通过" "bash scripts/secret_gate_test.sh"
|
2026-05-13 14:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
finish_phase
|