docker-compose brings up PostgreSQL 16 with pgvector and Redis 7 on non-standard host ports so it never collides with an existing local database. scripts/smoke.sh is the proof: compose, migrate, boot the API, issue a token, call the real DeepSeek API and print the resulting ob_ai_call and bl_ledger rows. services/vision and services/matting are placeholders noting that the local RTX 5090 makes BiRefNet and RTMPose developable without cloud GPU.
127 lines
5.2 KiB
Bash
Executable File
127 lines
5.2 KiB
Bash
Executable File
#!/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."
|
|
set -a; source .env; set +a
|
|
[[ -n "${DEEPSEEK_API_KEY:-}" ]] || fail "DEEPSEEK_API_KEY is empty in .env"
|
|
|
|
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' "${CHAT_JSON}" >"${LOG_DIR}/chat.json"
|
|
python3 -m json.tool --no-ensure-ascii "${LOG_DIR}/chat.json"
|
|
python3 - "${LOG_DIR}/chat.json" <<'PYEOF'
|
|
import json, 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"
|
|
print("\n--- MODEL REPLY -------------------------------------------------")
|
|
print(data["reply"])
|
|
print("-----------------------------------------------------------------")
|
|
print("provider={provider} model={model} prompt={prompt_id}@{prompt_version}".format(**data))
|
|
print("usage={}".format(data["usage"]))
|
|
print("quota_remaining={}".format(data["quota_remaining"]))
|
|
PYEOF
|
|
|
|
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'
|