This commit is contained in:
fengxiangdebaozi committed 2026-06-01 15:11:14 +08:00
commit a5d877207e
10 files changed
+798 -4

No files matched your search

@@ -29,8 +29,15 @@ const INPUT_CLASS =
const DEFAULT_KEYWORD_COUNT = 1;
const DEFAULT_AUDIENCE = '普通读者';
const DEFAULT_MATERIAL_SOURCE: 'knowledge_base' | 'skill' = 'knowledge_base';
const MAX_INTENT_LENGTH = 500;
// 素材来源选项:知识库=素材专家默认 web/intranet 检索;技能检索=用技能编排去检索
const MATERIAL_SOURCE_OPTIONS: Array<{ value: 'knowledge_base' | 'skill'; label: string }> = [
{ value: 'knowledge_base', label: '知识库' },
{ value: 'skill', label: '技能检索' },
];
const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222]';
const FIELD_TRIGGER_CLASS =
'h-12 w-full rounded-xl border-[#e8dedd] bg-white text-sm shadow-sm';
@@ -45,6 +52,8 @@ export function AIWritingForm({
const [audience, setAudience] = useState('普通读者');
const [wordCountInput, setWordCountInput] = useState('800');
const [strictMode, setStrictMode] = useState(false);
const [materialSource, setMaterialSource] =
useState<'knowledge_base' | 'skill'>(DEFAULT_MATERIAL_SOURCE);
const [modelName, setModelName] = useState('');
const { models, isLoading: modelsLoading } = useModels();
@@ -111,6 +120,7 @@ export function AIWritingForm({
wordCountTarget: wordCount,
keywordCount: DEFAULT_KEYWORD_COUNT,
writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose',
materialSource,
modelName: selectedModel?.name || modelName || undefined,
});
}
@@ -178,6 +188,29 @@ export function AIWritingForm({
</div>
</div>
<div className="mt-4 space-y-2">
<label className={FIELD_LABEL_CLASS}>素材来源</label>
<Select
value={materialSource}
onValueChange={(v) => setMaterialSource(v as 'knowledge_base' | 'skill')}
disabled={disabled}
>
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
<SelectValue placeholder="选择素材来源" />
</SelectTrigger>
<SelectContent>
{MATERIAL_SOURCE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value} className="text-sm">
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs leading-relaxed text-muted-foreground/80">
知识库:素材专家默认检索方式;技能检索:用已启用的技能去编排检索。
</p>
</div>
<div className="mt-4 space-y-2">
<label className={FIELD_LABEL_CLASS}>写作模型</label>
<Select
@@ -747,6 +747,7 @@ export function useAIWriting() {
word_count_target: request.wordCountTarget,
keyword_count: Math.max(1, Math.min(10, request.keywordCount)),
writing_mode: request.writingMode === 'strict' ? 'strict' : 'loose',
material_source: request.materialSource === 'skill' ? 'skill' : 'knowledge_base',
revision_count: 0,
max_revisions: 3,
status: 'researching',
@@ -228,6 +228,8 @@ export interface AIWritingRequest {
keywordCount: number;
/** 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose) */
writingMode: 'strict' | 'loose';
/** 素材来源:knowledge_base=知识库(默认检索)/ skill=技能检索(默认 knowledge_base) */
materialSource: 'knowledge_base' | 'skill';
}
// ── 流式审核条目 ──────────────────────────────────────────────────────────────
@@ -0,0 +1,359 @@
# DeerFlow 后端 Linux Docker 部署指南
本文档说明如何在 **Linux + Docker** 环境下一键部署 DeerFlow 后端(Gateway API,默认端口 **8001**)。
部署目录:`offline-backend-20260512/`
---
## 1. 架构概览
```text
┌─────────────────────────────────────────────────────┐
│ Linux 宿主机 │
│ ┌───────────────────────────────────────────────┐ │
│ │ Docker 容器: deerflow-backend │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Gateway API (uvicorn, port 8001) │ │ │
│ │ │ + LangGraph Agent Runtime │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ 挂载卷: │ │
│ │ config.yaml / .env │ │
│ │ backend/ 源码(可热更新 Python 代码) │ │
│ │ data/ → .deer-flow 持久化数据 │ │
│ └───────────────────────────────────────────────┘ │
│ │ 8001 │
│ ▼ │
│ 前端 / Nginx / 其他服务 │
└─────────────────────────────────────────────────────┘
```
**持久化数据**(挂载到 `data/`):
| 路径 | 说明 |
|------|------|
| `data/data/deerflow.db` 或 `checkpoints.db` | LangGraph checkpoint(SQLite) |
| `data/data/` | 业务 SQLite 文件(未切 MySQL 时) |
| `data/python-vendor/` | 离线安装的 Python 包(asyncmy、croniter 等) |
| `data/users/` | 用户线程、上传、记忆等 |
| `data/archives/` | 会话归档 |
---
## 2. 服务器要求
| 项目 | 最低要求 |
|------|----------|
| 操作系统 | Linux x86_64(CentOS 7+、Ubuntu 20.04+、Debian 11+ 等) |
| Docker | 20.10+ |
| Docker Compose | V2(`docker compose` 命令可用) |
| 内存 | 建议 4 GB+ |
| 磁盘 | 建议 20 GB+(含镜像、数据、日志) |
| 网络 | 能访问 LLM API;内网部署可配置 Python 私服 |
### 2.1 安装 Docker(如未安装)
**Ubuntu / Debian:**
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
newgrp docker
```
**验证:**
```bash
docker --version
docker compose version
```
---
## 3. 部署包准备
将以下内容上传到服务器(例如 `/opt/deerflow/offline-backend-20260512`):
```text
offline-backend-20260512/
├── deploy.sh # 一键部署脚本(主入口)
├── start_backend.sh # 启动(deploy.sh 的别名)
├── stop_backend.sh # 停止
├── docker-compose.yaml
├── .env.example
├── config.yaml # 应用配置
├── skill_evolution.json
├── backend/ # 后端源码 + Dockerfile + wheelhouse
│ ├── Dockerfile
│ ├── wheelhouse/ # 离线 Python 包
│ └── ...
├── scripts/
│ └── container-entrypoint.sh
└── data/ # 首次部署后自动创建
```
---
## 4. 一键部署(推荐)
### 4.1 配置环境变量
```bash
cd /opt/deerflow/offline-backend-20260512
cp .env.example .env
vi .env
```
**必须修改的项:**
```env
# 至少配置一个可用模型的 API Key
DEEPSEEK_API_KEY=sk-xxxxxxxx
# JWT 密钥,部署后不要随意更改
AUTH_JWT_SECRET=your-long-random-secret
# 前端实际访问地址(CORS)
GATEWAY_CORS_ORIGINS=http://192.168.1.100:5174,http://localhost:5174
```
**使用 MySQL 时**(`config.yaml` 中 `database.backend: mysql`):
```env
# 不要用 127.0.0.1 指向宿主机 MySQL;密码特殊字符须 URL 编码
MYSQL_DATABASE_URL=mysql+asyncmy://user:pass@192.168.1.100:3306/deerflow?charset=utf8mb4
```
**内网无法访问公网 PyPI 时:**
```env
UV_INDEX_URL=http://your-python-mirror/simple
PIP_INDEX_URL=http://your-python-mirror/simple
UV_TRUSTED_HOST=your-python-mirror
PIP_TRUSTED_HOST=your-python-mirror
```
### 4.2 检查 config.yaml
确认模型配置、沙箱模式、数据库配置正确。`config.yaml` 中与 `$` 开头的值会从 `.env` 读取,例如 `$MYSQL_DATABASE_URL`。
### 4.3 执行一键部署
```bash
chmod +x deploy.sh start_backend.sh stop_backend.sh scripts/container-entrypoint.sh
bash deploy.sh
```
脚本会自动:
1. 检查 Docker 环境
2. 创建 `data/` 目录和兼容符号链接
3. 若无 `.env` 则从 `.env.example` 复制
4. 构建 Docker 镜像(或加载离线 tar)
5. 启动容器并做健康检查
### 4.4 验证
```bash
curl http://127.0.0.1:8001/health
# 期望返回 JSON,HTTP 200
curl -s http://127.0.0.1:8001/openapi.json | head
```
浏览器访问 API 文档:`http://<服务器IP>:8001/docs`
---
## 5. 常用运维命令
| 操作 | 命令 |
|------|------|
| 一键部署/启动 | `bash deploy.sh` 或 `bash start_backend.sh` |
| 停止 | `bash deploy.sh down` 或 `bash stop_backend.sh` |
| 重启 | `bash deploy.sh restart` |
| 查看状态 | `bash deploy.sh status` |
| 查看日志 | `bash deploy.sh logs` |
| 仅构建镜像 | `bash deploy.sh build` |
| 健康检查 | `bash deploy.sh health` |
### 5.1 自定义端口
```bash
HOST_PORT=9001 bash deploy.sh up
```
### 5.2 离线镜像模式(无法构建时)
若已提供预构建镜像 tar:
```bash
IMAGE_TAR=./deerflow-backend-offline-image-20260505.tar \
IMAGE_TAG=deerflow-backend-offline:20260505 \
DEPLOY_MODE=offline \
bash deploy.sh up
```
或分步:
```bash
bash deploy.sh load
IMAGE_TAG=deerflow-backend-offline:20260505 bash deploy.sh up
```
---
## 6. 前端对接
前端需配置后端地址(Vite 环境变量示例):
```env
VITE_BACKEND_BASE_URL=http://192.168.1.100:8001
VITE_LANGGRAPH_BASE_URL=http://192.168.1.100:8001/api
```
确保 `.env` 中 `GATEWAY_CORS_ORIGINS` 包含前端实际 Origin。
---
## 7. MySQL 切换(可选)
若初始使用 SQLite,后续可切换到 MySQL:
```bash
bash stop_backend.sh
MYSQL_DATABASE_URL='mysql+asyncmy://user:pass@192.168.1.100:3306/deerflow?charset=utf8mb4' \
bash switch_database_to_mysql.sh
bash start_backend.sh
```
详细说明见 [MYSQL_SWITCH_RELEASE_NOTES_LINUX_DOCKER_ZH.md](./MYSQL_SWITCH_RELEASE_NOTES_LINUX_DOCKER_ZH.md)。
---
## 8. 代码热更新
当前部署方式将 `backend/app`、`backend/packages` 等目录挂载进容器,**更新 Python 代码后重启即可生效**,无需重新构建镜像:
```bash
# 上传新代码后
bash deploy.sh restart
```
若修改了 `pyproject.toml` / 依赖,需重新构建:
```bash
bash deploy.sh build
bash deploy.sh restart
```
---
## 9. 目录与卷说明
| 宿主机 | 容器内 | 说明 |
|--------|--------|------|
| `./config.yaml` | `/app/config.yaml` | 主配置(只读) |
| `./.env` | 环境变量注入 | 密钥、CORS 等 |
| `./backend/app` | `/app/backend/app` | 应用层代码 |
| `./backend/packages` | `/app/backend/packages` | harness 框架 |
| `./backend/skills` | `/app/backend/skills` | Agent Skills |
| `./backend/wheelhouse` | `/app/wheelhouse` | 离线 wheel 包 |
| `./data` | `/app/backend/.deer-flow` | 全部运行时数据 |
---
## 10. 故障排查
### 10.1 容器启动后立即退出
```bash
bash deploy.sh logs
```
常见原因:
- `.env` 中 API Key 或 `AUTH_JWT_SECRET` 未配置
- `config.yaml` 语法错误
- MySQL 连接失败(检查 `MYSQL_DATABASE_URL` 是否用了容器可达的 IP)
- 缺少 `asyncmy`(MySQL 模式):确认 `backend/wheelhouse` 中有对应 wheel,或配置 Python 私服
### 10.2 健康检查失败
```bash
docker ps -a
docker logs deerflow-backend --tail 100
curl -v http://127.0.0.1:8001/health
```
确认端口未被占用:
```bash
ss -tlnp | grep 8001
```
### 10.3 前端报 CORS / Failed to fetch
检查 `.env`:
```env
GATEWAY_CORS_ORIGINS=http://你的前端地址:端口
```
修改后重启:
```bash
bash deploy.sh restart
```
### 10.4 Docker 权限问题
```bash
sudo usermod -aG docker "$USER"
newgrp docker
```
### 10.5 沙箱需要 Docker-in-Docker
若 `config.yaml` 使用 `AioSandboxProvider`,容器需挂载宿主机 Docker socket(默认已挂载 `/var/run/docker.sock`)。确认:
```bash
ls -l /var/run/docker.sock
```
---
## 11. 生产环境建议
1. **反向代理**:前面加 Nginx,配置 HTTPS 和限流
2. **防火墙**:仅开放必要端口(如 443、8001 内网)
3. **备份**:定期备份 `data/` 目录和 `config.yaml`、`.env`
4. **日志**:配置 Docker 日志轮转(`/etc/docker/daemon.json` 中 `log-opts`)
5. **资源**:根据并发调整 `GATEWAY_WORKERS`(`.env`,默认 2)
6. **文档**:生产环境建议 `GATEWAY_ENABLE_DOCS=false`
---
## 12. 回滚
```bash
bash stop_backend.sh
# 恢复配置和数据备份
cp data/backups/xxx/config.yaml.bak config.yaml
cp data/backups/xxx/env.bak .env
cp -a data.bak data
bash start_backend.sh
```
---
## 13. 相关文档
- [MYSQL_SWITCH_RELEASE_NOTES_LINUX_DOCKER_ZH.md](./MYSQL_SWITCH_RELEASE_NOTES_LINUX_DOCKER_ZH.md) — MySQL 切换
- [backend/docs/BACKEND_RUN_STOP_ZH.md](./backend/docs/BACKEND_RUN_STOP_ZH.md) — 本地非 Docker 启停
- [backend/docs/CONFIGURATION.md](./backend/docs/CONFIGURATION.md) — 配置项详解
@@ -1,13 +1,21 @@
# Linux Docker 版:业务库切换 MySQL 发版说明
适用场景:Linux 服务器上使用 `offline-backend-20260512/start_backend.sh` 启动 Docker 后端。
适用场景:Linux 服务器上使用 Docker 部署 DeerFlow 后端。
原启动脚本保持不变:
推荐一键部署:
```bash
bash deploy.sh
```
或使用兼容脚本:
```bash
bash start_backend.sh
```
详细步骤见 [DEPLOY_LINUX_DOCKER_ZH.md](./DEPLOY_LINUX_DOCKER_ZH.md)。
单独新增切换脚本:
```bash
@@ -116,11 +116,19 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
is_user_query_search = action == "re_search" and bool(user_query)
# 兼容旧版"追加关键词"调用方(如果还有 client 传 extra_keywords 数组而非 user_query)
is_append_only = action == "re_search" and not user_query and bool(extra_keywords)
# PAUSE-1 素材确认卡用户点「启用技能检索」:
# 用户在写作表单选择的素材来源:knowledge_base(默认 web/intranet 检索)/ skill(技能检索)。
# 首次检索(没有用户干预)时按它决定检索方式;缺省 knowledge_base。
material_source = (state.get("material_source") or "knowledge_base").strip()
is_first_run = not intervention
is_initial_skill_source = is_first_run and material_source == "skill"
# 技能检索触发条件(任一即可):
# - 首次检索且表单选了「技能检索」来源
# - PAUSE-1 素材确认卡用户点「启用技能检索」按钮
# 技能检索流程:
# - 不再走 web/intranet 检索,改用 SkillSearchProvider
# - 顺序调用候选技能 + 早停(第一个技能够就停,不够再加下一个)
# - 旧 material_package 完全丢弃(覆盖式替换,让用户对比 web vs skill 结果)
is_skill_search = action == "enable_skill_search"
is_skill_search = action == "enable_skill_search" or is_initial_skill_source
# 用户指定的检索关键词数量(1~10),决定生成多少个检索词、检索多少个方向
keyword_count = _resolve_keyword_count(state.get("keyword_count"))
@@ -132,6 +132,9 @@ class AIWritingState(TypedDict):
keyword_count: int
# 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose)
writing_mode: str
# 素材来源:knowledge_base=知识库(默认 web/intranet 检索)/ skill=技能检索。
# 用户在写作表单选择,首次检索时由 researcher 节点据此决定检索方式(缺省 knowledge_base)。
material_source: str
# 用户在写作表单选定的主模型。大纲/写作/审核严格使用它(不回退默认)。
# 由 intent_parser 在首次 invocation 从 configurable.model_name 捕获进 state,
# 经 checkpoint 持久化 —— interrupt/resume 是新 invocation、其 config 不带原
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env bash
#
# deploy.sh — DeerFlow 后端 Linux Docker 一键部署脚本
#
# 用法:
# bash deploy.sh # 首次部署: 检查环境 → 构建/加载镜像 → 启动
# bash deploy.sh up # 启动服务
# bash deploy.sh down # 停止并移除容器
# bash deploy.sh restart # 重启
# bash deploy.sh status # 查看状态
# bash deploy.sh logs # 跟踪日志
# bash deploy.sh build # 仅构建镜像
# bash deploy.sh load # 从离线 tar 加载镜像
# bash deploy.sh health # 健康检查
#
# 环境变量(可选):
# HOST_PORT 宿主机映射端口,默认 8001
# CONTAINER_NAME 容器名,默认 deerflow-backend
# IMAGE_TAG 镜像标签,默认 deerflow-backend:latest
# IMAGE_TAR 离线镜像 tar 路径
# DEPLOY_MODE build | offline,默认 build
# SKIP_BUILD 1 时跳过构建/加载,直接启动
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${SCRIPT_DIR}"
COMPOSE_PROJECT="${COMPOSE_PROJECT:-deerflow-backend}"
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yaml"
COMPOSE=(docker compose -p "${COMPOSE_PROJECT}" -f "${COMPOSE_FILE}")
HOST_PORT="${HOST_PORT:-8001}"
CONTAINER_NAME="${CONTAINER_NAME:-deerflow-backend}"
IMAGE_TAG="${IMAGE_TAG:-deerflow-backend:latest}"
IMAGE_TAR="${IMAGE_TAR:-${SCRIPT_DIR}/deerflow-backend-offline-image.tar}"
DEPLOY_MODE="${DEPLOY_MODE:-build}"
SKIP_BUILD="${SKIP_BUILD:-0}"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
fail() { echo -e "${RED}[FAIL]${NC} $*" >&2; exit 1; }
usage() {
sed -n '3,18p' "$0" | sed 's/^# \?//'
exit 0
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "未找到命令: $1"
}
ensure_layout() {
mkdir -p "${SCRIPT_DIR}/data/data" "${SCRIPT_DIR}/data/backups" "${SCRIPT_DIR}/data/archives/conversations"
# switch_database_to_mysql.sh 等脚本期望根目录有这些路径
[ -e "${SCRIPT_DIR}/wheelhouse" ] || ln -sfn backend/wheelhouse "${SCRIPT_DIR}/wheelhouse"
[ -e "${SCRIPT_DIR}/skills" ] || ln -sfn backend/skills "${SCRIPT_DIR}/skills"
if [ ! -e "${SCRIPT_DIR}/extensions_config.json" ]; then
ln -sfn backend/extensions_config.json "${SCRIPT_DIR}/extensions_config.json"
fi
chmod +x "${SCRIPT_DIR}/scripts/container-entrypoint.sh" 2>/dev/null || true
}
ensure_env() {
if [ ! -f "${SCRIPT_DIR}/.env" ]; then
if [ -f "${SCRIPT_DIR}/.env.example" ]; then
cp "${SCRIPT_DIR}/.env.example" "${SCRIPT_DIR}/.env"
warn "已从 .env.example 生成 .env,部署前请编辑 API Key 和 AUTH_JWT_SECRET"
else
fail "缺少 .env 文件,请先创建并配置环境变量"
fi
fi
if [ ! -f "${SCRIPT_DIR}/config.yaml" ]; then
fail "缺少 config.yaml,请复制并编辑 config.example.yaml"
fi
if [ ! -f "${SCRIPT_DIR}/skill_evolution.json" ]; then
echo '{"enabled":false,"curator":{"enabled":false}}' > "${SCRIPT_DIR}/skill_evolution.json"
warn "已创建默认 skill_evolution.json"
fi
}
check_docker() {
require_cmd docker
docker info >/dev/null 2>&1 || fail "Docker 未运行或无权限,请确认 docker 服务已启动且当前用户在 docker 组"
docker compose version >/dev/null 2>&1 || fail "未找到 docker compose,请安装 Docker Compose V2"
}
generate_secret_if_needed() {
local key="$1"
local file="$2"
if grep -qE "^[[:space:]]*${key}=" "${SCRIPT_DIR}/.env" 2>/dev/null; then
local val
val="$(grep -E "^[[:space:]]*${key}=" "${SCRIPT_DIR}/.env" | tail -n1 | sed 's/^[^=]*=//')"
if [ -n "${val}" ] && [ "${val}" != "change-me-to-a-long-random-string" ]; then
return 0
fi
fi
if [ -f "${file}" ]; then
return 0
fi
mkdir -p "$(dirname "${file}")"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32 > "${file}"
chmod 600 "${file}"
warn "已生成 ${key} → ${file},建议将其写入 .env 后删除该文件"
fi
}
load_offline_image() {
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
ok "镜像已存在: ${IMAGE_TAG}"
return 0
fi
[ -f "${IMAGE_TAR}" ] || fail "离线镜像不存在: ${IMAGE_TAR},请设置 IMAGE_TAR 或先执行 bash deploy.sh build"
info "加载离线镜像: ${IMAGE_TAR}"
docker load -i "${IMAGE_TAR}"
ok "镜像加载完成: ${IMAGE_TAG}"
}
build_image() {
info "构建 Docker 镜像: ${IMAGE_TAG}"
export IMAGE_TAG
"${COMPOSE[@]}" build gateway
ok "镜像构建完成"
}
prepare_image() {
if [ "${SKIP_BUILD}" = "1" ]; then
info "SKIP_BUILD=1,跳过构建/加载"
return 0
fi
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
ok "使用已有镜像: ${IMAGE_TAG}"
return 0
fi
case "${DEPLOY_MODE}" in
offline)
load_offline_image
;;
build|*)
build_image
;;
esac
}
wait_for_health() {
local url="http://127.0.0.1:${HOST_PORT}/health"
local retries="${HEALTH_RETRIES:-30}"
local i=1
info "等待服务就绪: ${url}"
while [ "${i}" -le "${retries}" ]; do
if curl -fsS "${url}" >/dev/null 2>&1; then
ok "健康检查通过"
return 0
fi
sleep 2
i=$((i + 1))
done
warn "健康检查超时,请执行: bash deploy.sh logs"
return 1
}
cmd_up() {
check_docker
ensure_layout
ensure_env
generate_secret_if_needed "AUTH_JWT_SECRET" "${SCRIPT_DIR}/data/.auth-jwt-secret"
export HOST_PORT CONTAINER_NAME IMAGE_TAG
if ! docker ps -a --format '{{.Names}}' | grep -qx "${CONTAINER_NAME}"; then
prepare_image
fi
info "启动容器..."
"${COMPOSE[@]}" up -d --remove-orphans
ok "容器已启动: ${CONTAINER_NAME}"
echo
echo " API 地址: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 127.0.0.1):${HOST_PORT}"
echo " 健康检查: http://127.0.0.1:${HOST_PORT}/health"
echo " API 文档: http://127.0.0.1:${HOST_PORT}/docs"
echo
wait_for_health || true
}
cmd_down() {
check_docker
export HOST_PORT CONTAINER_NAME IMAGE_TAG
info "停止容器..."
"${COMPOSE[@]}" down --remove-orphans
ok "已停止"
}
cmd_restart() {
cmd_down
cmd_up
}
cmd_status() {
check_docker
export HOST_PORT CONTAINER_NAME IMAGE_TAG
"${COMPOSE[@]}" ps
echo
if docker ps --format '{{.Names}}' | grep -qx "${CONTAINER_NAME}"; then
curl -fsS "http://127.0.0.1:${HOST_PORT}/health" 2>/dev/null && echo || warn "健康检查失败"
else
warn "容器未运行"
fi
}
cmd_logs() {
check_docker
export HOST_PORT CONTAINER_NAME IMAGE_TAG
"${COMPOSE[@]}" logs -f --tail=200 gateway
}
cmd_build() {
check_docker
ensure_layout
build_image
}
cmd_load() {
check_docker
DEPLOY_MODE=offline
load_offline_image
}
cmd_health() {
curl -fsS "http://127.0.0.1:${HOST_PORT}/health" && echo || fail "健康检查失败"
}
main() {
local cmd="${1:-up}"
case "${cmd}" in
-h|--help|help) usage ;;
up|start|"") cmd_up ;;
down|stop) cmd_down ;;
restart) cmd_restart ;;
status) cmd_status ;;
logs) cmd_logs ;;
build) cmd_build ;;
load) cmd_load ;;
health) cmd_health ;;
deploy) cmd_up ;;
*)
fail "未知命令: ${cmd}。运行 bash deploy.sh --help 查看帮助"
;;
esac
}
main "${1:-up}"
@@ -0,0 +1,50 @@
# DeerFlow 后端 — Linux Docker 部署
# 推荐使用 deploy.sh 管理;也可直接: docker compose up -d
services:
gateway:
build:
context: .
dockerfile: backend/Dockerfile
args:
APT_MIRROR: ${APT_MIRROR:-}
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:0.7.20}
UV_INDEX_URL: ${UV_INDEX_URL:-https://pypi.org/simple}
UV_EXTRAS: ${UV_EXTRAS:-}
image: ${IMAGE_TAG:-deerflow-backend:latest}
container_name: ${CONTAINER_NAME:-deerflow-backend}
ports:
- "${HOST_PORT:-8001}:8001"
env_file:
- .env
environment:
CI: "true"
DEER_FLOW_PROJECT_ROOT: /app
DEER_FLOW_HOME: /app/backend/.deer-flow
DEER_FLOW_CONFIG_PATH: /app/config.yaml
DEER_FLOW_EXTENSIONS_CONFIG_PATH: /app/backend/extensions_config.json
DEER_FLOW_SKILL_EVOLUTION_CONFIG_PATH: /app/skill_evolution.json
DEER_FLOW_SKILLS_PATH: /app/backend/skills
DEER_FLOW_CHANNELS_LANGGRAPH_URL: ${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api}
DEER_FLOW_CHANNELS_GATEWAY_URL: ${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001}
UV_LINK_MODE: copy
volumes:
- ./config.yaml:/app/config.yaml:ro
- ./skill_evolution.json:/app/skill_evolution.json:ro
- ./backend/app:/app/backend/app
- ./backend/packages:/app/backend/packages
- ./backend/scripts:/app/backend/scripts
- ./backend/extensions_config.json:/app/backend/extensions_config.json:ro
- ./backend/skills:/app/backend/skills:ro
- ./backend/wheelhouse:/app/wheelhouse:ro
- ./backend/pyproject.toml:/app/backend/pyproject.toml:ro
- ./backend/uv.lock:/app/backend/uv.lock:ro
- ./backend/langgraph.json:/app/backend/langgraph.json:ro
- ./scripts/container-entrypoint.sh:/app/scripts/container-entrypoint.sh:ro
- ./data:/app/backend/.deer-flow
- ${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
working_dir: /app
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
command: ["sh", "/app/scripts/container-entrypoint.sh"]
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Container startup: install optional offline deps, then launch Gateway.
set -eu
cd /app/backend
VENDOR_DIR="/app/backend/.deer-flow/python-vendor"
WHEELHOUSE="/app/wheelhouse"
mkdir -p "${VENDOR_DIR}"
export PYTHONPATH=".:${VENDOR_DIR}"
pip_install_offline() {
local packages="$1"
if [ -d "${WHEELHOUSE}" ]; then
uv pip install \
--target "${VENDOR_DIR}" \
--no-index \
--find-links "${WHEELHOUSE}" \
${packages}
elif [ -n "${UV_INDEX_URL:-}" ] || [ -n "${PIP_INDEX_URL:-}" ]; then
uv pip install --target "${VENDOR_DIR}" ${packages}
else
uv pip install --target "${VENDOR_DIR}" ${packages}
fi
}
pip_install_online() {
local packages="$1"
if [ -n "${UV_INDEX_URL:-}" ]; then
uv pip install --target "${VENDOR_DIR}" ${packages}
elif [ -n "${PIP_INDEX_URL:-}" ]; then
uv pip install --target "${VENDOR_DIR}" --index-url "${PIP_INDEX_URL}" ${packages}
else
uv pip install --target "${VENDOR_DIR}" ${packages}
fi
}
install_if_missing() {
local module="$1"
local packages="$2"
if uv run --no-sync python -c "import ${module}" >/dev/null 2>&1; then
return 0
fi
echo "Installing ${packages} into ${VENDOR_DIR}"
if [ -d "${WHEELHOUSE}" ] && ls "${WHEELHOUSE}"/*.whl >/dev/null 2>&1; then
pip_install_offline "${packages}"
else
pip_install_online "${packages}"
fi
}
if [ -n "${MYSQL_DATABASE_URL:-}" ]; then
install_if_missing "asyncmy" "${MYSQL_PIP_PACKAGES:-asyncmy==0.2.10}"
fi
if [ "${SCHEDULER_INSTALL_DEPS:-1}" = "1" ]; then
install_if_missing "croniter" "${SCHEDULER_PIP_PACKAGES:-croniter==6.2.2}"
fi
WORKERS="${GATEWAY_WORKERS:-2}"
echo "Starting Gateway on 0.0.0.0:8001 (workers=${WORKERS})"
exec env PYTHONPATH=".:${VENDOR_DIR}" uv run --no-sync uvicorn app.gateway.app:app \
--host 0.0.0.0 \
--port 8001 \
--workers "${WORKERS}"