Merge 5063f2de42 into b69ca7ad97
4
.gitignore
vendored
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
25
app/src-tauri/Cargo.toml
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
11
app/src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
||||
BIN
app/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
app/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
app/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
app/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
app/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
app/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/src-tauri/icons/icon.icns
Normal file
BIN
app/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
app/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
16
app/src-tauri/src/lib.rs
Normal 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");
|
||||
}
|
||||
6
app/src-tauri/src/main.rs
Normal 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();
|
||||
}
|
||||
38
app/src-tauri/tauri.conf.json
Normal 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
|
|
@ -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
|
|
@ -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 "$@"
|
||||