feat(scripts): 新增本地调试与示例启动脚本,并登记进 apps/README
新增 4 个脚本,统一子命令风格(start/stop/restart/status/logs/run): - scripts/dev-gateway.sh:方式 B,只起 Gateway(:8001),用 backend/.venv 虚拟环境, 自动加载 .env、释放端口、等待就绪;带 PID/日志文件,可查状态与跟随日志。 - scripts/dev-full.sh:方式 A,复用 serve.sh 守护模式起全量栈(Gateway+前端+nginx), 补齐 serve.sh 缺失的 status 与 logs;restart 默认跳过依赖安装,统一入口 :2026。 - apps/examples/http-chat/run.sh:自动探测网关(:2026 优先,回退 :8001), 优先 uv 临时环境带 requests(--no-project,不污染系统),无 uv 时回退 venv+pip。 - apps/examples/embedded-chat/run.sh:自动定位 backend、加载 .env 后用 uv run 运行, 内嵌 SDK 模式无需起服务。 apps/README.md 增加「本地调试脚本」与「运行示例」两节,说明上述脚本用法。 全部脚本均已本地实跑验证:全量栈三服务 HTTP 200,两个示例多轮对话正常。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 本地调试全量栈管理脚本(方式 A:Gateway + Frontend + Nginx)
|
||||
# ------------------------------------------------------------------
|
||||
# 复用仓库已有的 scripts/serve.sh(处理 config-upgrade / postgres extras /
|
||||
# nginx 临时目录 / 依赖同步 / 端口等待),在其守护进程模式之上补齐
|
||||
# status 和 logs,子命令风格与 scripts/dev-gateway.sh 保持一致。
|
||||
#
|
||||
# 服务与端口:
|
||||
# Gateway localhost:8001 (REST API + agent runtime)
|
||||
# Frontend localhost:3000 (Next.js)
|
||||
# Nginx localhost:2026 (统一入口 / 反向代理) ← 浏览器访问这个
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/dev-full.sh start # 后台启动整套(首次会装依赖)
|
||||
# ./scripts/dev-full.sh stop # 关闭整套
|
||||
# ./scripts/dev-full.sh restart # 重启整套
|
||||
# ./scripts/dev-full.sh status # 三个服务的端口 / 健康检查
|
||||
# ./scripts/dev-full.sh logs [服务] # 跟随日志,默认三个一起;可指定 gateway|frontend|nginx
|
||||
# ./scripts/dev-full.sh run # 前台运行(= make dev,Ctrl-C 全停,gateway 带热重载)
|
||||
#
|
||||
# 环境变量:
|
||||
# SKIP_INSTALL=1 跳过依赖安装,重启更快
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SERVE="$REPO_ROOT/scripts/serve.sh"
|
||||
|
||||
# 服务清单:名称:端口:日志文件:健康检查路径(空=只查端口)
|
||||
SERVICES=(
|
||||
"Gateway:8001:gateway.log:/api/v1/auth/setup-status"
|
||||
"Frontend:3000:frontend.log:/"
|
||||
"Nginx:2026:nginx.log:/"
|
||||
)
|
||||
|
||||
# ── 工具函数 ──────────────────────────────────────────────────────
|
||||
_port_pid() { { lsof -ti tcp:"$1" 2>/dev/null || true; } | head -1; }
|
||||
|
||||
_any_running() {
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
local port="${svc#*:}"; port="${port%%:*}"
|
||||
[ -n "$(_port_pid "$port")" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_serve_flags() {
|
||||
# serve.sh 守护模式启动;可选跳过依赖安装
|
||||
local flags="--dev --daemon"
|
||||
[ "${SKIP_INSTALL:-0}" = "1" ] && flags="$flags --skip-install"
|
||||
echo "$flags"
|
||||
}
|
||||
|
||||
# ── 子命令 ────────────────────────────────────────────────────────
|
||||
cmd_start() {
|
||||
if _any_running; then
|
||||
echo "检测到已有服务在运行 —— 如需重启用:$0 restart"
|
||||
cmd_status || true
|
||||
return 0
|
||||
fi
|
||||
echo "→ 后台启动全量栈(serve.sh $(_serve_flags))"
|
||||
[ "${SKIP_INSTALL:-0}" = "1" ] || echo " 首次启动会执行 uv sync + pnpm install,可能较慢;重启可加 SKIP_INSTALL=1"
|
||||
# shellcheck disable=SC2046
|
||||
bash "$SERVE" $(_serve_flags)
|
||||
echo
|
||||
cmd_status || true
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
if ! _any_running; then
|
||||
echo "未在运行"
|
||||
return 0
|
||||
fi
|
||||
echo "→ 关闭全量栈(serve.sh --stop)"
|
||||
bash "$SERVE" --stop
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local all_up=0
|
||||
printf "%-10s %-7s %-9s %s\n" "服务" "端口" "状态" "健康"
|
||||
printf "%-10s %-7s %-9s %s\n" "----" "----" "----" "----"
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
local name port log path rest
|
||||
name="${svc%%:*}"; rest="${svc#*:}"
|
||||
port="${rest%%:*}"; rest="${rest#*:}"
|
||||
log="${rest%%:*}"; path="${rest#*:}"
|
||||
local pid; pid="$(_port_pid "$port")"
|
||||
if [ -z "$pid" ]; then
|
||||
printf "%-10s %-7s %-9s %s\n" "$name" "$port" "✗ 停止" "-"
|
||||
all_up=1
|
||||
else
|
||||
local code="-"
|
||||
if [ -n "$path" ]; then
|
||||
code="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port$path" 2>/dev/null || echo 000)"
|
||||
case "$code" in 200|429|301|302|307) code="✓ HTTP $code";; 000) code="⚠ 无响应";; *) code="⚠ HTTP $code";; esac
|
||||
fi
|
||||
printf "%-10s %-7s %-9s %s\n" "$name" "$port" "● 运行 ($pid)" "$code"
|
||||
fi
|
||||
done
|
||||
if [ "$all_up" = "0" ]; then
|
||||
echo
|
||||
echo " 🌐 统一入口: http://localhost:2026"
|
||||
fi
|
||||
return "$all_up"
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
local target="${1:-}"
|
||||
cd "$REPO_ROOT"
|
||||
local files=()
|
||||
if [ -n "$target" ]; then
|
||||
local f="logs/${target}.log"
|
||||
[ -f "$f" ] || { echo "暂无日志:$f(可选 gateway|frontend|nginx)" >&2; return 1; }
|
||||
files=("$f")
|
||||
else
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
local log; log="${svc#*:}"; log="${log#*:}"; log="${log%%:*}"
|
||||
[ -f "logs/$log" ] && files+=("logs/$log")
|
||||
done
|
||||
[ ${#files[@]} -gt 0 ] || { echo "暂无日志文件(logs/ 为空)" >&2; return 1; }
|
||||
fi
|
||||
echo "→ 跟随日志(Ctrl-C 退出,不影响服务):${files[*]}"
|
||||
tail -n 30 -f "${files[@]}"
|
||||
}
|
||||
|
||||
cmd_run() {
|
||||
if _any_running; then
|
||||
echo "已有后台实例在运行,先 $0 stop" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "→ 前台运行全量栈(= make dev,Ctrl-C 全停)"
|
||||
exec bash "$SERVE" --dev
|
||||
}
|
||||
|
||||
# ── 分发 ──────────────────────────────────────────────────────────
|
||||
case "${1:-status}" in
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_stop; echo; SKIP_INSTALL="${SKIP_INSTALL:-1}" cmd_start ;;
|
||||
status|"") cmd_status ;;
|
||||
logs) shift || true; cmd_logs "${1:-}" ;;
|
||||
run) cmd_run ;;
|
||||
-h|--help|help)
|
||||
sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//' ;;
|
||||
*)
|
||||
echo "未知命令: $1" >&2
|
||||
echo "可用: start | stop | restart | status | logs [服务] | run" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 本地调试 Gateway 管理脚本(方式 B:只起 Gateway,端口 8001)
|
||||
# ------------------------------------------------------------------
|
||||
# - 用 backend/.venv 虚拟环境运行(由 uv 管理)
|
||||
# - 自动加载仓库根 .env(数据库、模型 key 等)
|
||||
# - 支持 start / stop / restart / status / logs / run 子命令
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/dev-gateway.sh start # 后台启动(写 PID + 日志)
|
||||
# ./scripts/dev-gateway.sh stop # 关闭
|
||||
# ./scripts/dev-gateway.sh restart # 重启
|
||||
# ./scripts/dev-gateway.sh status # 查看状态(PID / 端口 / 健康检查)
|
||||
# ./scripts/dev-gateway.sh logs # 实时跟随日志(Ctrl-C 退出,不影响服务)
|
||||
# ./scripts/dev-gateway.sh run # 前台运行(断点调试,Ctrl-C 退出)
|
||||
#
|
||||
# 环境变量:
|
||||
# PORT=8002 换端口(默认 8001)
|
||||
# NO_RELOAD=1 关掉热重载(断点调试更稳)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# ── 定位仓库根 ────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PORT="${PORT:-8001}"
|
||||
PID_FILE="$REPO_ROOT/logs/gateway-dev.pid"
|
||||
LOG_FILE="$REPO_ROOT/logs/gateway-dev.log"
|
||||
|
||||
# ── 工具函数 ──────────────────────────────────────────────────────
|
||||
_running_pid() {
|
||||
# 打印存活的服务 PID(优先 PID 文件,回退到端口探测),否则空
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
local pid
|
||||
pid="$(cat "$PID_FILE" 2>/dev/null || true)"
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
echo "$pid"; return 0
|
||||
fi
|
||||
fi
|
||||
# lsof 在无监听时返回 1,配合 pipefail+set -e 会误终止脚本 → 用 || true 兜底
|
||||
{ lsof -ti tcp:"$PORT" 2>/dev/null || true; } | head -1
|
||||
}
|
||||
|
||||
_load_env() {
|
||||
if [ -f "$REPO_ROOT/.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$REPO_ROOT/.env"
|
||||
set +a
|
||||
else
|
||||
echo "⚠ 未找到 $REPO_ROOT/.env(数据库/模型 key 可能缺失)" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
_uvicorn_flags() {
|
||||
if [ "${NO_RELOAD:-0}" != "1" ]; then
|
||||
echo "--reload --reload-include=*.yaml --reload-include=.env --reload-exclude=*.pyc --reload-exclude=__pycache__/* --reload-exclude=sandbox/* --reload-exclude=.deer-flow/*"
|
||||
fi
|
||||
}
|
||||
|
||||
_preflight() {
|
||||
command -v uv >/dev/null 2>&1 || { echo "✗ 未找到 uv。安装:curl -LsSf https://astral.sh/uv/install.sh | sh" >&2; exit 1; }
|
||||
mkdir -p "$REPO_ROOT/logs"
|
||||
if [ ! -d "$REPO_ROOT/backend/.venv" ]; then
|
||||
echo "→ 未发现 backend/.venv,执行 uv sync 创建虚拟环境"
|
||||
(cd "$REPO_ROOT/backend" && uv sync)
|
||||
fi
|
||||
}
|
||||
|
||||
_wait_ready() {
|
||||
# 探测 setup-status,最多 30s;就绪返回 0
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$PORT/api/v1/auth/setup-status" 2>/dev/null | grep -qE "200|429"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── 子命令 ────────────────────────────────────────────────────────
|
||||
cmd_start() {
|
||||
local pid; pid="$(_running_pid)"
|
||||
if [ -n "$pid" ]; then
|
||||
echo "已在运行 (PID $pid, 端口 $PORT) —— 如需重启用:$0 restart"
|
||||
return 0
|
||||
fi
|
||||
_preflight
|
||||
_load_env
|
||||
echo "→ 后台启动 Gateway @localhost:${PORT}(venv: backend/.venv, 热重载: $([ "${NO_RELOAD:-0}" = "1" ] && echo off || echo on))"
|
||||
# shellcheck disable=SC2086
|
||||
( cd "$REPO_ROOT/backend" && exec env PYTHONPATH=. uv run uvicorn app.gateway.app:app \
|
||||
--host 0.0.0.0 --port "$PORT" $(_uvicorn_flags) ) > "$LOG_FILE" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
if _wait_ready; then
|
||||
echo "✓ 启动成功 (PID $(cat "$PID_FILE"))"
|
||||
echo " 日志: $0 logs 状态: $0 status 关闭: $0 stop"
|
||||
else
|
||||
echo "✗ 30s 内未就绪,最后 20 行日志:" >&2
|
||||
tail -n 20 "$LOG_FILE" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
local pid; pid="$(_running_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "未在运行"
|
||||
rm -f "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
echo "→ 关闭 Gateway (PID $pid)"
|
||||
# 优雅终止整组进程(uv → uvicorn → reloader 子进程)
|
||||
kill "$pid" 2>/dev/null || true
|
||||
for _ in $(seq 1 10); do kill -0 "$pid" 2>/dev/null || break; sleep 0.5; done
|
||||
# 兜底:按端口清残留(reload worker 偶尔不随父进程退出)
|
||||
lsof -ti tcp:"$PORT" 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
rm -f "$PID_FILE"
|
||||
echo "✓ 已停止,端口 $PORT 释放"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local pid; pid="$(_running_pid)"
|
||||
if [ -z "$pid" ]; then
|
||||
echo "● Gateway: 已停止 (端口 $PORT 空闲)"
|
||||
return 1
|
||||
fi
|
||||
echo "● Gateway: 运行中"
|
||||
echo " PID: $pid"
|
||||
echo " 端口: $PORT"
|
||||
local code
|
||||
code="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$PORT/api/v1/auth/setup-status" 2>/dev/null || echo "000")"
|
||||
case "$code" in
|
||||
200|429) echo " 健康: ✓ HTTP $code (REST API 响应中)";;
|
||||
000) echo " 健康: ⚠ 端口占用但 HTTP 无响应(可能仍在启动)";;
|
||||
*) echo " 健康: ⚠ HTTP $code";;
|
||||
esac
|
||||
echo " 日志: $LOG_FILE"
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
[ -f "$LOG_FILE" ] || { echo "暂无日志文件:$LOG_FILE"; return 1; }
|
||||
echo "→ 跟随日志(Ctrl-C 退出,不影响服务):$LOG_FILE"
|
||||
tail -n 50 -f "$LOG_FILE"
|
||||
}
|
||||
|
||||
cmd_run() {
|
||||
# 前台运行:日志直出终端,适合 IDE 断点 / 看实时堆栈
|
||||
local pid; pid="$(_running_pid)"
|
||||
[ -n "$pid" ] && { echo "已有后台实例在运行 (PID $pid),先 $0 stop" >&2; exit 1; }
|
||||
_preflight
|
||||
_load_env
|
||||
echo "→ 前台运行 @localhost:${PORT}(Ctrl-C 退出)"
|
||||
cd "$REPO_ROOT/backend"
|
||||
# shellcheck disable=SC2046,SC2086
|
||||
exec env PYTHONPATH=. uv run uvicorn app.gateway.app:app \
|
||||
--host 0.0.0.0 --port "$PORT" $(_uvicorn_flags)
|
||||
}
|
||||
|
||||
# ── 分发 ──────────────────────────────────────────────────────────
|
||||
case "${1:-status}" in
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_stop; echo; cmd_start ;;
|
||||
status|"") cmd_status ;;
|
||||
logs) cmd_logs ;;
|
||||
run) cmd_run ;;
|
||||
-h|--help|help)
|
||||
sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//' ;;
|
||||
*)
|
||||
echo "未知命令: $1" >&2
|
||||
echo "可用: start | stop | restart | status | logs | run" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user