build: local stack, container images and the end-to-end smoke script
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.
This commit is contained in:
Executable
+22
@@ -0,0 +1,22 @@
|
||||
# `scripts/`
|
||||
|
||||
| 脚本 | 用途 |
|
||||
| --- | --- |
|
||||
| `smoke.sh` | 端到端验证:compose up → migrate → 起 API → 发 dev token → 调 **真实 DeepSeek** → 打印 `ob_ai_call` 与 `bl_ledger` |
|
||||
|
||||
```bash
|
||||
make smoke # 或 bash scripts/smoke.sh
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- **`smoke.sh` 会真的调用 DeepSeek,产生真实费用。** 这是仓库里唯一会花钱的
|
||||
入口;`make test` 全程离线。
|
||||
- 需要 `.env` 里有可用的 `DEEPSEEK_API_KEY`。
|
||||
- 它在 8010 端口起一个临时 API 进程,结束时自动清理;日志留在 `.smoke/api.log`。
|
||||
|
||||
## 写新脚本的规矩
|
||||
|
||||
- `set -Eeuo pipefail`,并且失败要给出**可操作的**错误信息,不要只是退出码。
|
||||
- 不要把密钥写进脚本或 echo 出来。从 `.env` / 环境变量读。
|
||||
- 任何会改数据或花钱的脚本,在文件头第一行注释里写清楚。
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user