diff --git a/apps/README.md b/apps/README.md index f93eb3ba..86b1ca73 100644 --- a/apps/README.md +++ b/apps/README.md @@ -23,6 +23,20 @@ apps/ ← 你的应用(消费 deerflow,不反 > 还有第三种:LangGraph SDK(`langgraph_sdk.get_client(url=".../api")`,graph id `lead_agent`),用于接入 LangGraph 生态工具链。需要的话照 HTTP 示例的鉴权流程拿 cookie 即可。 +### 运行示例(每个示例自带 run.sh) + +```bash +# ① HTTP 模式:需要先起 Gateway(dev-gateway 或 dev-full 都行) +# run.sh 会自动探测网关地址:优先 :2026,回退 :8001 +./apps/examples/http-chat/run.sh +DF_BASE=http://localhost:8001 ./apps/examples/http-chat/run.sh # 也可手动指定 + +# ② 内嵌模式:不需要起任何服务,run.sh 自动进 backend uv 环境运行 +./apps/examples/embedded-chat/run.sh +``` + +http-chat 的 `run.sh` 优先用 `uv run --no-project --with requests`(临时环境,不污染系统),没有 uv 才回退到本地 `.venv` + pip;可用 `DF_BASE` / `DF_EMAIL` / `DF_PASSWORD` 覆盖。embedded-chat 的 `run.sh` 自动定位 `backend/`、加载 `.env` 后用 `uv run` 启动,依赖 `config.yaml` 里有可用模型。 + ## 前置:先把 DeerFlow 跑起来 在**仓库根目录**: @@ -33,6 +47,29 @@ make dev # 起 Gateway(8001) + 前端(3000) + nginx(2026),统一入 确保 `config.yaml` 里至少配了一个可用模型 + API key。 +### 本地调试脚本(推荐) + +`make dev` 是前台阻塞运行。日常调试更顺手的是仓库根 `scripts/` 下两个生命周期脚本,子命令统一为 `start / stop / restart / status / logs / run`: + +| 脚本 | 起什么 | 入口 | 适合 | +|---|---|---|---| +| `scripts/dev-gateway.sh` | 只起 Gateway | `http://localhost:8001` | 调后端 API / 接入示例,起得快 | +| `scripts/dev-full.sh` | Gateway + 前端 + nginx | `http://localhost:2026` | 连前端一起调,完整体验 | + +```bash +./scripts/dev-gateway.sh start # 后台启动,等就绪后返回 +./scripts/dev-gateway.sh status # PID / 端口 / HTTP 健康检查 +./scripts/dev-gateway.sh logs # tail -f 跟随日志(不影响服务) +./scripts/dev-gateway.sh stop + +./scripts/dev-full.sh start # 全量栈后台启动(首次装依赖) +SKIP_INSTALL=1 ./scripts/dev-full.sh start # 跳过依赖安装,重启更快 +./scripts/dev-full.sh status # 三服务一览 +./scripts/dev-full.sh run # 前台运行(= make dev,gateway 带热重载) +``` + +环境变量:`PORT=`(换端口)、`NO_RELOAD=1`(关热重载,断点更稳)、`SKIP_INSTALL=1`(全量栈跳过装依赖)。 + ## 鉴权(HTTP 模式必读) Gateway 是 **fail-closed** 的——除少数公开路径外所有请求都要带会话 cookie: diff --git a/apps/examples/embedded-chat/run.sh b/apps/examples/embedded-chat/run.sh new file mode 100755 index 00000000..6695ef77 --- /dev/null +++ b/apps/examples/embedded-chat/run.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# embedded-chat 示例启动脚本 +# ------------------------------------------------------------------ +# 内嵌 SDK 模式:进程内直接 import deerflow.*,不需要起任何服务。 +# 必须在 backend 的 uv 虚拟环境里跑(才能解析 deerflow-harness / app 包), +# 本脚本自动 cd 到 backend 并用 uv run 启动。 +# +# 用法: +# ./run.sh +# +# 前提: +# - 已 `cd backend && uv sync`(或跑过任意一个 dev 脚本,venv 已建好) +# - config.yaml 里配好至少一个可用模型 + API key +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +APP="$SCRIPT_DIR/app.py" +BACKEND="$REPO_ROOT/backend" + +# ── 前置检查 ────────────────────────────────────────────────────── +command -v uv >/dev/null 2>&1 || { echo "✗ 未找到 uv。安装:curl -LsSf https://astral.sh/uv/install.sh | sh" >&2; exit 1; } +[ -f "$REPO_ROOT/config.yaml" ] || echo "⚠ 未找到 $REPO_ROOT/config.yaml —— 没有可用模型会启动失败" >&2 + +if [ ! -d "$BACKEND/.venv" ]; then + echo "→ 未发现 backend/.venv,执行 uv sync" + (cd "$BACKEND" && uv sync) +fi + +# ── 加载 .env(模型 key / 数据库等)────────────────────────────── +if [ -f "$REPO_ROOT/.env" ]; then + set -a + # shellcheck disable=SC1091 + source "$REPO_ROOT/.env" + set +a +fi + +# ── 在 backend uv 环境里运行(config.yaml 解析依赖运行目录为 backend/)── +echo "→ 在 backend uv 环境中运行 embedded-chat" +cd "$BACKEND" +exec env PYTHONPATH=. uv run python "$APP" diff --git a/apps/examples/http-chat/run.sh b/apps/examples/http-chat/run.sh new file mode 100755 index 00000000..b61e0d1f --- /dev/null +++ b/apps/examples/http-chat/run.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# http-chat 示例启动脚本 +# ------------------------------------------------------------------ +# - 自动探测网关地址:优先 :2026(nginx 全量栈),回退 :8001(只起 Gateway) +# - 优先用 uv 临时虚拟环境带上 requests(--no-project,不污染系统/项目) +# 没有 uv 时回退到本地 .venv + pip +# +# 用法: +# ./run.sh # 自动探测网关并运行 +# DF_BASE=http://localhost:8001 ./run.sh # 手动指定网关 +# DF_EMAIL=a@b.com DF_PASSWORD=xxxx ./run.sh +# +# 前提:先起好 Gateway +# ../../../scripts/dev-gateway.sh start # → :8001 +# ../../../scripts/dev-full.sh start # → :2026 +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# ── 探测可用网关 ────────────────────────────────────────────────── +_alive() { curl -s -o /dev/null -w "%{http_code}" "$1/api/v1/auth/setup-status" 2>/dev/null | grep -qE "200|429"; } + +if [ -z "${DF_BASE:-}" ]; then + if _alive "http://localhost:2026"; then DF_BASE="http://localhost:2026" + elif _alive "http://localhost:8001"; then DF_BASE="http://localhost:8001" + else + echo "✗ 没探测到运行中的网关(:2026 / :8001 都不通)。" >&2 + echo " 先启动:scripts/dev-gateway.sh start 或 scripts/dev-full.sh start" >&2 + echo " 或手动指定:DF_BASE=http://your-host:port ./run.sh" >&2 + exit 1 + fi +fi +export DF_BASE +echo "→ 使用网关: $DF_BASE" + +# ── 运行:优先 uv,回退 venv+pip ───────────────────────────────── +if command -v uv >/dev/null 2>&1; then + echo "→ uv 临时环境运行(--with requests)" + exec uv run --no-project --with "requests>=2.31" python app.py +else + echo "→ 未找到 uv,使用本地 .venv + pip" + if [ ! -d .venv ]; then + python3 -m venv .venv + ./.venv/bin/pip install -q -r requirements.txt + fi + exec ./.venv/bin/python app.py +fi diff --git a/scripts/dev-full.sh b/scripts/dev-full.sh new file mode 100755 index 00000000..9080ea4b --- /dev/null +++ b/scripts/dev-full.sh @@ -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 diff --git a/scripts/dev-gateway.sh b/scripts/dev-gateway.sh new file mode 100755 index 00000000..3a125494 --- /dev/null +++ b/scripts/dev-gateway.sh @@ -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