This commit is contained in:
Anders Hsueh 2026-05-20 03:02:15 +08:00 committed by GitHub
commit 6028fdf82d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 6223 additions and 0 deletions

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
corporate-ai-team/
# DeerFlow docker image cache
docker/.cache/
# oh-my-claudecode state
@ -55,6 +57,8 @@ web/
# Deployment artifacts
backend/Dockerfile.langgraph
config.yaml.bak
# asuka.sh service manager
.asuka_pids/
.playwright-mcp
/frontend/test-results/
/frontend/playwright-report/

167
CLAUDE.md Normal file
View file

@ -0,0 +1,167 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Root directory (full application)
```bash
make config # First-time setup: generate config.yaml from config.example.yaml (aborts if exists)
make config-upgrade # Merge new fields from config.example.yaml into existing config.yaml
make check # Verify Node.js 22+, pnpm, uv, nginx are installed
make install # Install all dependencies (backend uv sync + frontend pnpm install)
make dev # Start all services with hot-reload (LangGraph:2024, Gateway:8001, Frontend:3000, Nginx:2026)
make stop # Stop all running services
make clean # Stop services and remove backend/.deer-flow, .langgraph_api, logs
```
Service logs during `make dev`: `logs/langgraph.log`, `logs/gateway.log`, `logs/frontend.log`, `logs/nginx.log`
### Backend directory (`cd backend`)
```bash
make dev # Run LangGraph server only (port 2024)
make gateway # Run Gateway API only (port 8001)
make test # Run all backend tests
make lint # Lint with ruff
make format # Format code with ruff (--fix + format)
# Run a single test file
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v
```
### Frontend directory (`cd frontend`)
```bash
pnpm lint
pnpm typecheck
BETTER_AUTH_SECRET=local-dev-secret pnpm build # BETTER_AUTH_SECRET required for build
```
Note: `pnpm check` is broken — use `pnpm lint` and `pnpm typecheck` separately.
### Docker
```bash
make docker-init # Pull sandbox image (once)
make docker-start # Start dev services (mode-aware from config.yaml)
make up # Build + start production Docker services
make down # Stop production containers
```
## Architecture
DeerFlow is a full-stack super agent harness. Entry point: `http://localhost:2026` (nginx proxy).
**Service topology:**
- **LangGraph Server** (port 2024) — agent runtime, graph execution
- **Gateway API** (port 8001) — FastAPI REST API for models, skills, MCP, memory, uploads, threads
- **Frontend** (port 3000) — Next.js 16 + React 19 + TypeScript
- **Nginx** (port 2026) — unified reverse proxy: `/api/langgraph/*` → 2024, `/api/*` → 8001, `/` → 3000
**Backend split (strict dependency direction: app → deerflow, never reverse):**
- `backend/packages/harness/deerflow/` — publishable `deerflow-harness` package; contains agent orchestration, sandbox, tools, models, MCP, skills, config, memory
- `backend/app/` — unpublished application layer; FastAPI Gateway + IM channel integrations (Feishu, Slack, Telegram)
- Boundary enforced by `backend/tests/test_harness_boundary.py` (runs in CI)
**Frontend structure:**
- `frontend/src/app/` — Next.js routes/pages
- `frontend/src/components/` — UI components (workspace: chats, messages, artifacts, settings)
- `frontend/src/core/` — app logic modules (threads, models, tools, skills, MCP, memory, uploads, agents, i18n)
- `frontend/src/server/better-auth/` — authentication
## Key Backend Components
### Agent System
- **Lead Agent** (`deerflow/agents/lead_agent/agent.py`): entry point `make_lead_agent()`, registered in `backend/langgraph.json`
- **ThreadState** (`deerflow/agents/thread_state.py`): extends `AgentState` with sandbox, thread_data, title, artifacts, todos, uploaded_files, viewed_images
- **Runtime config** (via `config.configurable`): `thinking_enabled`, `model_name`, `is_plan_mode`, `subagent_enabled`
### Middleware Chain (execution order)
1. ThreadDataMiddleware — creates per-thread directories
2. UploadsMiddleware — injects newly uploaded files
3. SandboxMiddleware — acquires sandbox, stores sandbox_id
4. DanglingToolCallMiddleware — handles interrupted tool calls
5. GuardrailMiddleware — pre-tool-call authorization (optional)
6. SummarizationMiddleware — context reduction (optional)
7. TodoListMiddleware — task tracking with `write_todos` (plan_mode only)
8. TitleMiddleware — auto-generates thread title
9. MemoryMiddleware — queues conversations for async memory update
10. ViewImageMiddleware — injects base64 image data (vision models only)
11. SubagentLimitMiddleware — enforces MAX_CONCURRENT_SUBAGENTS=3 (subagent_enabled only)
12. ClarificationMiddleware — intercepts `ask_clarification`, interrupts via `Command(goto=END)` (must be last)
### Configuration
- `config.yaml` (project root) — main config: models, tools, sandbox, memory, summarization, channels
- `extensions_config.json` (project root) — MCP servers and skills enabled state
- Config values starting with `$` are resolved as environment variables
- `get_app_config()` auto-reloads when file mtime changes (no restart needed)
- Override config path via `DEER_FLOW_CONFIG_PATH` env var
- Bump `config_version` in `config.example.yaml` when changing the schema; run `make config-upgrade` to migrate
### Sandbox Virtual Path System
Agent sees virtual paths; physical paths are translated transparently:
- `/mnt/user-data/{workspace,uploads,outputs}``backend/.deer-flow/threads/{thread_id}/user-data/...`
- `/mnt/skills``deer-flow/skills/`
Sandbox modes: Local (host filesystem), Docker (isolated containers), Docker + Kubernetes (provisioner).
### Subagent System
- Max 3 concurrent subagents (`SubagentLimitMiddleware`)
- Built-in agents: `general-purpose` (all tools except `task`) and `bash` (command specialist)
- `task()` tool → `SubagentExecutor` → background thread → SSE events → result
- 15-minute timeout per subagent
### Memory System
- Stored in `backend/.deer-flow/memory.json`
- LLM extracts facts and context from conversations asynchronously (debounced 30s)
- Top 15 facts + context injected into system prompt via `<memory>` tags
- Deduplicates facts before appending
### Model Factory
- `create_chat_model(name, thinking_enabled)` in `deerflow/models/factory.py`
- Instantiates any LangChain-compatible LLM via reflection from `config.yaml` `use` field
- Supports CLI-backed providers: `CodexChatModel` (Codex CLI) and `ClaudeChatModel` (Claude Code OAuth)
### Gateway API Routers
| Path | Purpose |
|------|---------|
| `GET/PUT /api/mcp/config` | MCP server configuration |
| `GET /api/models` | List/get models |
| `GET/PUT /api/skills` | List, update, install skills |
| `GET /api/memory` | Memory data, reload, config, status |
| `POST /api/threads/{id}/uploads` | Upload files (auto-converts PDF/PPT/Excel/Word via markitdown) |
| `DELETE /api/threads/{id}` | Remove local thread data after LangGraph thread deletion |
| `GET /api/threads/{id}/artifacts/{path}` | Serve artifacts |
| `POST /api/threads/{id}/suggestions` | Generate follow-up questions |
## Development Rules
- **Every feature/bugfix must include tests** in `backend/tests/test_<feature>.py`
- **Always update `backend/README.md` and `backend/CLAUDE.md`** after code changes
- **harness never imports app** — enforced by CI (`tests/test_harness_boundary.py`)
- Code style: `ruff`, line length 240, Python 3.12+, double quotes
- Before opening PRs: `cd backend && make lint && make test`, plus frontend lint/typecheck if frontend was touched
## CI
`.github/workflows/backend-unit-tests.yml` runs on every PR:
1. `uv sync --group dev`
2. `make lint`
3. `make test`
Notable test files:
- `tests/test_harness_boundary.py` — import firewall
- `tests/test_client.py` — embedded client + Gateway conformance (77 tests)
- `tests/test_memory_updater.py` — memory deduplication
- `tests/test_docker_sandbox_mode_detection.py` — sandbox config parsing
- `tests/test_provisioner_kubeconfig.py` — kubeconfig handling

122
app/README.md Normal file
View file

@ -0,0 +1,122 @@
# DeerFlow Desktop App
A [Tauri](https://tauri.app/) wrapper that packages DeerFlow into a native desktop application for macOS, Linux, and Windows. The app window loads the DeerFlow workspace directly — no browser required.
## How It Works
```
┌──────────────────────────────────────┐
│ Tauri Window (native desktop app) │
│ └── WebView → localhost:2026 │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ DeerFlow services (local) │
│ ├── nginx :2026 (unified) │
│ ├── Next.js :3000 │
│ ├── Gateway API :8001 │
│ └── LangGraph :2024 │
└──────────────────────────────────────┘
```
The Tauri shell is intentionally thin — it provides a native window, titlebar, and OS integration while all application logic remains in the existing DeerFlow stack. No code duplication, and every DeerFlow update is automatically reflected in the desktop app.
## Directory Structure
```
app/
├── src/
│ └── index.html # WebView entry: loads localhost:2026/workspace
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json # productName, window size, devUrl
│ └── src/
│ ├── main.rs
│ └── lib.rs
├── init.sh # Start / restart all DeerFlow backend services
└── README.md
```
## Prerequisites
- [Rust](https://rustup.rs/) 1.77.2+
- All DeerFlow prerequisites: Node.js 22+, pnpm, uv, nginx (see root [README](../README.md))
## Getting Started
### 1. Start the backend services
From the `app/` directory:
```bash
./init.sh
```
This starts LangGraph, Gateway API, Frontend, and nginx (or restarts them if already running). Wait until you see the services ready message before launching the desktop window.
### 2. Development mode (hot-reload)
```bash
cd app/src-tauri
source "$HOME/.cargo/env"
cargo tauri dev
```
The Tauri window opens and loads `http://localhost:2026/workspace` directly. Changes to the frontend are reflected live.
### 3. Build a release bundle
```bash
cd app/src-tauri
source "$HOME/.cargo/env"
cargo tauri build
```
Output locations:
| Platform | Path |
|----------|------|
| macOS | `src-tauri/target/release/bundle/macos/Asuka.app` |
| Linux | `src-tauri/target/release/bundle/appimage/asuka_*.AppImage` |
| Windows | `src-tauri/target/release/bundle/msi/Asuka_*.msi` |
> The app bundles only the Tauri shell. DeerFlow backend services still need to be running locally when you launch the app.
## Updating DeerFlow
Pull the latest code and rebuild:
```bash
git pull
make install # reinstall frontend + backend deps if needed
cd app/src-tauri
cargo tauri build
```
## Troubleshooting
**Blank window** — Backend services are not running. Run `./init.sh` first.
**Build fails** — Make sure the full Rust toolchain is installed:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Linux also needs:
sudo apt-get install -y build-essential libwebkit2gtk-4.1-dev libssl-dev
```
**Port conflict** — If any of the ports (2024, 2026, 3000, 8001) are in use, stop existing services with `make stop` from the project root.
**Chinese text or emoji not rendering correctly on Linux** — The WebView relies on system fonts. Install the required CJK and emoji fonts:
```bash
sudo apt-get install -y fonts-noto-cjk
sudo apt-get install -y fonts-noto-color-emoji fonts-symbola
```
## Acknowledgements
This desktop wrapper is built on top of [DeerFlow](https://github.com/bytedance/deer-flow) by [ByteDance](https://github.com/bytedance), an open-source super agent harness licensed under MIT. All core agent logic, frontend, and backend code belong to the DeerFlow project and its contributors.
## Author
[Anders Hsueh](https://github.com/andershsueh)

88
app/init.sh Executable file
View file

@ -0,0 +1,88 @@
#!/bin/bash
# DeerFlow 后端服务管理脚本
# 用法: ./init.sh
# 功能:
# - 如果服务未运行 → 启动所有服务
# - 如果服务已运行 → 重启所有服务
set -e
DEER_FLOW_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$DEER_FLOW_DIR"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 检查服务是否运行
check_service() {
if pgrep -f "langgraph dev" > /dev/null 2>&1 || \
pgrep -f "uvicorn app.gateway.app:app" > /dev/null 2>&1 || \
pgrep -f "next dev" > /dev/null 2>&1; then
return 0 # 服务运行中
fi
return 1 # 服务未运行
}
# 停止所有服务
stop_services() {
echo -e "${YELLOW}停止所有服务...${NC}"
pkill -f "langgraph dev" 2>/dev/null || true
pkill -f "uvicorn app.gateway.app:app" 2>/dev/null || true
pkill -f "next dev" 2>/dev/null || true
pkill -f "next-server" 2>/dev/null || true
pkill -f "nginx" 2>/dev/null || true
sleep 2
echo -e "${GREEN}所有服务已停止${NC}"
}
# 启动所有服务
start_services() {
echo -e "${GREEN}启动 DeerFlow 后端服务...${NC}"
# 确保 PATH 包含 uv
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
# 设置环境变量
if [ -f "$DEER_FLOW_DIR/.env" ]; then
set -a
source "$DEER_FLOW_DIR/.env"
set +a
fi
# 启动服务
echo -e "${GREEN}启动 LangGraph Server, Gateway API, Frontend 和 nginx...${NC}"
make dev &
# 等待服务启动
sleep 5
# 检查服务是否成功启动
if curl -s http://localhost:2026 > /dev/null 2>&1; then
echo -e "${GREEN}✓ 所有服务已启动成功!${NC}"
echo -e "${GREEN}访问地址: http://localhost:2026${NC}"
else
echo -e "${YELLOW}⚠ 服务可能未完全启动,请检查日志${NC}"
fi
}
# 主逻辑
main() {
echo "========================================"
echo -e "${GREEN}DeerFlow 服务管理${NC}"
echo "========================================"
if check_service; then
echo -e "${YELLOW}检测到服务正在运行${NC}"
stop_services
sleep 2
start_services
else
echo -e "${YELLOW}检测到服务未运行${NC}"
start_services
fi
}
main "$@"

4
app/src-tauri/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

5281
app/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
app/src-tauri/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.5.6", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.10.3", features = [] }
tauri-plugin-log = "2"

3
app/src-tauri/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View file

@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

16
app/src-tauri/src/lib.rs Normal file
View file

@ -0,0 +1,16 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

View file

@ -0,0 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Asuka",
"version": "0.1.0",
"identifier": "com.deerflow.desktop",
"build": {
"frontendDist": "../src",
"devUrl": "http://localhost:2026/workspace",
"beforeDevCommand": "",
"beforeBuildCommand": ""
},
"app": {
"windows": [
{
"title": "Asuka",
"width": 1280,
"height": 800,
"resizable": true,
"fullscreen": false,
"center": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

36
app/src/index.html Normal file
View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Asuka</title>
<style>
@font-face {
font-family: 'Noto Color Emoji';
src: url('https://fonts.gstatic.com/s/notocoloremoji/v25/Yzyinc9s7QA__6yWRJbPkHdsO97fLOaFbL60D5ikDYoG7QYo7BuPqZ6g7BEoJgTYwL3ZsttsNgKi48jBg.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
/* Fallback for emojis */
:root {
font-family: 'Noto Color Emoji', 'Noto Sans CJK SC', system-ui, sans-serif;
}
</style>
</head>
<body>
<iframe id="app" src="http://localhost:2026/workspace"></iframe>
</body>
</html>

422
asuka.sh Executable file
View file

@ -0,0 +1,422 @@
#!/usr/bin/env bash
#
# asuka.sh - DeerFlow 服务管理脚本
#
# 用法:
# ./asuka.sh --start-all 启动所有服务
# ./asuka.sh --restart-all 重启所有服务
# ./asuka.sh --stop-all 停止所有服务
# ./asuka.sh --start:FE 启动前端
# ./asuka.sh --start:BE 启动后端
# ./asuka.sh --start:LG 启动 LangGraph
# ./asuka.sh --stop:FE 停止前端
# ./asuka.sh --stop:BE 停止后端
# ./asuka.sh --stop:LG 停止 LangGraph
# ./asuka.sh --help 显示帮助
set -e
# ── 颜色定义 ─────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# ── 路径配置 ─────────────────────────────────────────────────────────────────
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPTS_DIR="$REPO_ROOT/scripts"
LOGS_DIR="$REPO_ROOT/logs"
PID_DIR="$REPO_ROOT/.asuka_pids"
# 创建必要目录
mkdir -p "$LOGS_DIR" "$PID_DIR"
# ── 服务定义 ─────────────────────────────────────────────────────────────────
# FE: Frontend (Next.js)
SVC_FE_NAME="Frontend"
SVC_FE_PORT="3000"
SVC_FE_PATTERN="next dev|next-server|node.*next"
# BE: Backend (Gateway)
SVC_BE_NAME="Backend"
SVC_BE_PORT="8001"
SVC_BE_PATTERN="uvicorn app.gateway.app:app"
# LG: LangGraph
SVC_LG_NAME="LangGraph"
SVC_LG_PORT="2024"
SVC_LG_PATTERN="langgraph dev"
# NG: Nginx reverse proxy
SVC_NG_NAME="Nginx"
SVC_NG_PORT="2026"
SVC_NG_PATTERN="nginx"
# ── 帮助信息 ─────────────────────────────────────────────────────────────────
show_help() {
echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ DeerFlow 服务管理工具 ║${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${YELLOW}━━━ 全部服务 ━━━${NC}"
echo " ./asuka.sh --start-all 启动所有服务"
echo " ./asuka.sh --restart-all 重启所有服务"
echo " ./asuka.sh --stop-all 停止所有服务"
echo ""
echo -e "${YELLOW}━━━ 单个服务 ━━━${NC}"
echo " ./asuka.sh --start:FE 启动前端 (Next.js)"
echo " ./asuka.sh --start:BE 启动后端 (Gateway)"
echo " ./asuka.sh --start:LG 启动 LangGraph"
echo " ./asuka.sh --start:NG 启动 Nginx 反向代理"
echo ""
echo " ./asuka.sh --stop:FE 停止前端"
echo " ./asuka.sh --stop:BE 停止后端"
echo " ./asuka.sh --stop:LG 停止 LangGraph"
echo " ./asuka.sh --stop:NG 停止 Nginx"
echo ""
echo -e "${YELLOW}━━━ 其他 ━━━${NC}"
echo " ./asuka.sh --help 显示帮助"
echo ""
echo -e "${YELLOW}━━━ 服务端口 ━━━${NC}"
echo " FE (Frontend): localhost:3000"
echo " BE (Backend): localhost:8001"
echo " LG (LangGraph): localhost:2024"
echo " NG (Nginx): localhost:2026"
echo ""
echo -e "${YELLOW}━━━ 日志位置 ━━━${NC}"
echo " $LOGS_DIR/frontend.log"
echo " $LOGS_DIR/gateway.log"
echo " $LOGS_DIR/langgraph.log"
echo " $LOGS_DIR/nginx.log"
echo ""
}
# ── 工具函数 ─────────────────────────────────────────────────────────────────
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
check_port() {
local port=$1
if lsof -i -P 2>/dev/null | grep -q ":${port} (LISTEN)"; then
return 0 # 端口被占用
fi
return 1 # 端口空闲
}
wait_for_port() {
local service_name=$1
local port=$2
local timeout=${3:-60}
local elapsed=0
log_info "等待 $service_name 就绪 (端口 $port)..."
while [ $elapsed -lt $timeout ]; do
if check_port $port; then
log_info "$service_name 已就绪"
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
done
log_error "$service_name 启动超时 (等待 ${timeout}s)"
return 1
}
get_pid() {
local service=$1
local pid_file="$PID_DIR/${service}.pid"
if [ -f "$pid_file" ]; then
cat "$pid_file"
else
echo ""
fi
}
save_pid() {
local service=$1
local pid=$2
local pid_file="$PID_DIR/${service}.pid"
echo "$pid" > "$pid_file"
}
clear_pid() {
local service=$1
local pid_file="$PID_DIR/${service}.pid"
rm -f "$pid_file"
}
is_running() {
local pattern=$1
pgrep -f "$pattern" > /dev/null 2>&1
}
# ── 停止服务函数 ─────────────────────────────────────────────────────────────
stop_service() {
local service=$1
local name=$2
local pattern=$3
local port=$4
log_info "停止 $name..."
# 方法1: 通过 PID 文件
local pid_file="$PID_DIR/${service}.pid"
if [ -f "$pid_file" ]; then
local pid=$(cat "$pid_file")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
sleep 1
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$pid_file"
fi
# 方法2: 通过进程名匹配
local pids=$(pgrep -f "$pattern" 2>/dev/null || true)
if [ -n "$pids" ]; then
echo "$pids" | xargs kill -TERM 2>/dev/null || true
sleep 1
echo "$pids" | xargs kill -9 2>/dev/null || true
fi
# 等待端口释放
local wait_count=0
while check_port "$port" && [ $wait_count -lt 10 ]; do
sleep 1
wait_count=$((wait_count + 1))
done
log_info "$name 已停止"
}
# ── 启动服务函数 ─────────────────────────────────────────────────────────────
start_frontend() {
if is_running "$SVC_FE_PATTERN"; then
log_warn "$SVC_FE_NAME 已在运行"
return 0
fi
if check_port "$SVC_FE_PORT"; then
log_error "端口 $SVC_FE_PORT 已被占用,无法启动 $SVC_FE_NAME"
return 1
fi
log_info "启动 $SVC_FE_NAME..."
cd "$REPO_ROOT/frontend"
nohup pnpm run dev > "$LOGS_DIR/frontend.log" 2>&1 &
local pid=$!
cd - > /dev/null
save_pid "FE" "$pid"
wait_for_port "$SVC_FE_NAME" "$SVC_FE_PORT" 120
}
start_backend() {
if is_running "$SVC_BE_PATTERN"; then
log_warn "$SVC_BE_NAME 已在运行"
return 0
fi
if check_port "$SVC_BE_PORT"; then
log_error "端口 $SVC_BE_PORT 已被占用,无法启动 $SVC_BE_NAME"
return 1
fi
log_info "启动 $SVC_BE_NAME..."
cd "$REPO_ROOT/backend"
nohup env PYTHONPATH=. uv run uvicorn app.gateway.app:app \
--host 0.0.0.0 --port 8001 \
> "$LOGS_DIR/gateway.log" 2>&1 &
local pid=$!
cd - > /dev/null
save_pid "BE" "$pid"
wait_for_port "$SVC_BE_NAME" "$SVC_BE_PORT" 30
}
start_langgraph() {
if is_running "$SVC_LG_PATTERN"; then
log_warn "$SVC_LG_NAME 已在运行"
return 0
fi
if check_port "$SVC_LG_PORT"; then
log_error "端口 $SVC_LG_PORT 已被占用,无法启动 $SVC_LG_NAME"
return 1
fi
log_info "启动 $SVC_LG_NAME..."
cd "$REPO_ROOT/backend"
nohup env NO_COLOR=1 uv run langgraph dev \
--no-browser --allow-blocking \
> "$LOGS_DIR/langgraph.log" 2>&1 &
local pid=$!
cd - > /dev/null
save_pid "LG" "$pid"
wait_for_port "$SVC_LG_NAME" "$SVC_LG_PORT" 60
}
start_nginx() {
if is_running "$SVC_NG_PATTERN"; then
log_warn "$SVC_NG_NAME 已在运行"
return 0
fi
if check_port "$SVC_NG_PORT"; then
log_error "端口 $SVC_NG_PORT 已被占用,无法启动 $SVC_NG_NAME"
return 1
fi
log_info "启动 $SVC_NG_NAME..."
local nginx_conf="$REPO_ROOT/docker/nginx/nginx.local.conf"
if [ ! -f "$nginx_conf" ]; then
log_error "Nginx 配置文件不存在: $nginx_conf"
return 1
fi
nohup nginx -g "daemon off;" -c "$nginx_conf" -p "$REPO_ROOT" \
> "$LOGS_DIR/nginx.log" 2>&1 &
local pid=$!
save_pid "NG" "$pid"
wait_for_port "$SVC_NG_NAME" "$SVC_NG_PORT" 10
}
# ── 全部服务操作 ─────────────────────────────────────────────────────────────
start_all() {
log_info "启动所有服务..."
echo ""
start_langgraph
echo ""
start_backend
echo ""
start_frontend
echo ""
start_nginx
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} ✓ 所有服务已启动${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo " 🌐 Frontend: http://localhost:3000"
echo " 📡 Backend: http://localhost:8001"
echo " 🤖 LangGraph: http://localhost:2024"
echo " 🌍 Nginx: http://localhost:2026 (统一入口)"
echo ""
}
stop_all() {
log_info "停止所有服务..."
echo ""
stop_service "FE" "$SVC_FE_NAME" "$SVC_FE_PATTERN" "$SVC_FE_PORT"
stop_service "BE" "$SVC_BE_NAME" "$SVC_BE_PATTERN" "$SVC_BE_PORT"
stop_service "LG" "$SVC_LG_NAME" "$SVC_LG_PATTERN" "$SVC_LG_PORT"
stop_service "NG" "$SVC_NG_NAME" "$SVC_NG_PATTERN" "$SVC_NG_PORT"
echo ""
log_info "✓ 所有服务已停止"
}
restart_all() {
log_info "重启所有服务..."
stop_all
echo ""
sleep 2
start_all
}
# ── 解析命令 ─────────────────────────────────────────────────────────────────
parse_command() {
local cmd=$1
case "$cmd" in
--help|-h)
show_help
;;
--start-all)
start_all
;;
--stop-all)
stop_all
;;
--restart-all)
restart_all
;;
--start:FE)
start_frontend
;;
--start:BE)
start_backend
;;
--start:LG)
start_langgraph
;;
--start:NG)
start_nginx
;;
--stop:FE)
stop_service "FE" "$SVC_FE_NAME" "$SVC_FE_PATTERN" "$SVC_FE_PORT"
;;
--stop:BE)
stop_service "BE" "$SVC_BE_NAME" "$SVC_BE_PATTERN" "$SVC_BE_PORT"
;;
--stop:LG)
stop_service "LG" "$SVC_LG_NAME" "$SVC_LG_PATTERN" "$SVC_LG_PORT"
;;
--stop:NG)
stop_service "NG" "$SVC_NG_NAME" "$SVC_NG_PATTERN" "$SVC_NG_PORT"
;;
*)
log_error "未知命令: $cmd"
echo ""
show_help
exit 1
;;
esac
}
# ── 主入口 ───────────────────────────────────────────────────────────────────
main() {
if [ $# -eq 0 ]; then
show_help
exit 0
fi
for arg in "$@"; do
parse_command "$arg"
done
}
main "$@"