#!/usr/bin/env bash # End-to-end proof that the scaffold works against the REAL DeepSeek API. # # compose up → migrate → start API → dev token → POST /v1/agent/chat # → print the ob_ai_call row and the bl_ledger rows # # This is the only script that spends real money. Everything in `make test` is # hermetic. set -Eeuo pipefail cd "$(dirname "$0")/.." COMPOSE="docker compose -f deploy/docker-compose.dev.yml" API_PORT="${API_PORT:-8010}" BASE="http://127.0.0.1:${API_PORT}" LOG_DIR=".smoke" API_LOG="${LOG_DIR}/api.log" API_PID="" bold() { printf '\n\033[1m== %s\033[0m\n' "$1"; } fail() { printf '\033[31mFAIL: %s\033[0m\n' "$1" >&2; exit 1; } cleanup() { if [[ -n "${API_PID}" ]] && kill -0 "${API_PID}" 2>/dev/null; then kill "${API_PID}" 2>/dev/null || true wait "${API_PID}" 2>/dev/null || true fi } trap cleanup EXIT mkdir -p "${LOG_DIR}" [[ -f .env ]] || fail ".env not found. Copy .env.example to .env and set DEEPSEEK_API_KEY." # 只取需要的这一个键,不 `source .env`:那些 *_JSON 值含双引号, # 被 shell 解析后引号会消失,应用侧再读就成了非法 JSON。 DEEPSEEK_API_KEY=$(sed -n 's/^[[:space:]]*DEEPSEEK_API_KEY[[:space:]]*=[[:space:]]*//p' .env | tail -1) DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY%\"}; DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY#\"} DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY%\'}; DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY#\'} [[ -n "${DEEPSEEK_API_KEY}" ]] || fail "DEEPSEEK_API_KEY is empty in .env" [[ "${DEEPSEEK_API_KEY}" == sk-* ]] || fail "DEEPSEEK_API_KEY does not look like a real key" bold "1/7 Starting PostgreSQL (pgvector) and Redis" ${COMPOSE} up -d postgres redis for _ in $(seq 1 60); do if ${COMPOSE} exec -T postgres pg_isready -U meida -d meida >/dev/null 2>&1; then break; fi sleep 1 done ${COMPOSE} exec -T postgres pg_isready -U meida -d meida >/dev/null || fail "PostgreSQL never became ready" ${COMPOSE} exec -T redis redis-cli ping >/dev/null || fail "Redis never became ready" echo "postgres + redis ready" bold "2/7 Applying migrations" uv run alembic upgrade head ${COMPOSE} exec -T postgres psql -U meida -d meida -c "\dt" ${COMPOSE} exec -T postgres psql -U meida -d meida -tAc \ "SELECT extname FROM pg_extension WHERE extname='vector'" | grep -q vector \ || fail "pgvector extension missing" echo "pgvector extension present" bold "3/7 Starting the API" uv run uvicorn apps.api.main:app --host 127.0.0.1 --port "${API_PORT}" >"${API_LOG}" 2>&1 & API_PID=$! for _ in $(seq 1 60); do if curl -fsS "${BASE}/healthz" >/dev/null 2>&1; then break; fi sleep 0.5 done curl -fsS "${BASE}/healthz" >/dev/null || { cat "${API_LOG}"; fail "API did not start"; } curl -fsS "${BASE}/readyz" | tee /dev/null >/dev/null || { cat "${API_LOG}"; fail "API not ready"; } echo "api up on ${BASE} (logs: ${API_LOG})" bold "4/7 Issuing a dev token" DEVICE_KEY="smoke-$(date +%s)" TOKEN_JSON=$(curl -fsS -X POST "${BASE}/v1/auth/dev-token" \ -H 'Content-Type: application/json' \ -d "{\"device_key\":\"${DEVICE_KEY}\"}") TOKEN=$(printf '%s' "${TOKEN_JSON}" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["access_token"])') USER_ID=$(printf '%s' "${TOKEN_JSON}" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["user_id"])') [[ -n "${TOKEN}" ]] || fail "no access token issued" echo "user_id=${USER_ID}" bold "5/7 Calling POST /v1/agent/chat against the REAL DeepSeek API" CHAT_JSON=$(curl -fsS -X POST "${BASE}/v1/agent/chat" \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer ${TOKEN}" \ -d '{"message":"三十岁男生,第一次去互联网公司面试,穿什么比较稳妥?"}') printf '%s\n' "${CHAT_JSON}" | python3 -m json.tool # 走临时文件而不是管道:heredoc 会占用 stdin,管道里的 JSON 读不到 printf '%s' "${CHAT_JSON}" >"${LOG_DIR}/chat.json" python3 - "${LOG_DIR}/chat.json" <<'PY' import json import sys with open(sys.argv[1], encoding="utf-8") as fh: body = json.load(fh) assert body["code"] == 0, body data = body["data"] assert data["reply"].strip(), "empty model reply" usage = data["usage"] print("\n--- MODEL REPLY -------------------------------------------------") print(data["reply"]) print("-----------------------------------------------------------------") print("provider={} model={} prompt={}@{}".format( data["provider"], data["model"], data["prompt_id"], data["prompt_version"])) print("latency={}ms tokens_in={} tokens_out={} cached={} cost_cents={}".format( usage["latency_ms"], usage["tokens_in"], usage["tokens_out"], usage["tokens_cached"], usage["cost_cents"])) print("quota_remaining={}".format(data["quota_remaining"])) # 对话必须在 NFR-PERF-02b 的整轮 15s 之内。这里只量单次 LLM 调用, # 留出检索与后处理的余量,因此阈值取 10s。 assert usage["latency_ms"] < 10_000, f"LLM 调用耗时 {usage['latency_ms']}ms,超出预期" PY bold "6/7 ob_ai_call row (成本归因的唯一数据源, 架构文档 §13)" ${COMPOSE} exec -T postgres psql -U meida -d meida -x -c \ "SELECT call_id, user_id, scene, provider, model, prompt_id, prompt_version, tokens_in, tokens_out, tokens_cached, latency_ms, cost_cents, success, error_code, ref_type, created_at FROM ob_ai_call WHERE user_id = ${USER_ID} ORDER BY created_at DESC" ROWS=$(${COMPOSE} exec -T postgres psql -U meida -d meida -tAc \ "SELECT count(*) FROM ob_ai_call WHERE user_id = ${USER_ID} AND success") [[ "${ROWS// /}" -ge 1 ]] || fail "no successful ob_ai_call row was written" bold "7/7 bl_ledger rows (预扣三段式, 架构文档 §7.4)" ${COMPOSE} exec -T postgres psql -U meida -d meida -c \ "SELECT tx_id, delta, tx_type, status, quota_key, period_key, balance_after, idempotency_key, created_at FROM bl_ledger WHERE user_id = ${USER_ID} ORDER BY created_at" COMMITTED=$(${COMPOSE} exec -T postgres psql -U meida -d meida -tAc \ "SELECT count(*) FROM bl_ledger WHERE user_id = ${USER_ID} AND tx_type='consume' AND status='committed'") [[ "${COMMITTED// /}" -ge 1 ]] || fail "quota was not committed after a successful call" ${COMPOSE} exec -T postgres psql -U meida -d meida -c \ "SELECT quota_key, period_key, used, limit_value FROM bl_quota WHERE user_id = ${USER_ID}" printf '\n\033[32mSMOKE PASSED\033[0m — real DeepSeek reply received, ob_ai_call and bl_ledger populated.\n'