diff --git a/docker/docker-compose-local-prod.yaml b/docker/docker-compose-local-prod.yaml new file mode 100644 index 00000000..b9cecf77 --- /dev/null +++ b/docker/docker-compose-local-prod.yaml @@ -0,0 +1,91 @@ +# DeerFlow Local Production Test +# Usage: docker compose -f docker-compose-local-prod.yaml up -d +# +# Prerequisites: +# 1. Build images first: +# docker build -f frontend/Dockerfile.prod -t deerflow-frontend:local . +# docker build -f backend/Dockerfile -t deerflow-backend:local . +# +# Services: +# - nginx: Reverse proxy (port 2026) +# - frontend: Frontend Next.js (production build) +# - gateway: Backend Gateway API +# - langgraph: LangGraph server + +name: deerflow2-local + +services: + nginx: + image: nginx:alpine + container_name: deer-flow-nginx + ports: + - "2026:2026" + volumes: + - ./nginx/nginx-local-prod.conf:/etc/nginx/nginx.conf:ro + depends_on: + - frontend + - gateway + - langgraph + networks: + - deer-flow-local + restart: unless-stopped + + frontend: + image: deerflow-frontend:local + container_name: deer-flow-frontend + environment: + - NODE_ENV=production + networks: + - deer-flow-local + restart: unless-stopped + + gateway: + image: deerflow-backend:local + container_name: deer-flow-gateway + command: sh -c "cd backend && uv run uvicorn src.gateway.app:app --host 0.0.0.0 --port 8001" + volumes: + - ../config.yaml:/app/config.yaml + - ../extensions_config.json:/app/extensions_config.json + - ../skills:/app/skills + - ../logs:/app/logs + - ../backend/.deer-flow:/app/backend/.deer-flow + - ~/.cache/uv:/root/.cache/uv + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - CI=true + - DOCKER_HOST=unix:///var/run/docker.sock + env_file: + - ../.env + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - deer-flow-local + restart: unless-stopped + + langgraph: + image: deerflow-backend:local + container_name: deer-flow-langgraph + command: sh -c "cd backend && exec uv run langgraph dev --no-browser --no-reload --allow-blocking --host 0.0.0.0 --port 2024" + volumes: + - ../backend/.langgraph_api:/app/backend/.langgraph_api + - ../config.yaml:/app/config.yaml + - ../extensions_config.json:/app/extensions_config.json + - ../skills:/app/skills + - ../logs:/app/logs + - ../backend/.deer-flow:/app/backend/.deer-flow + - ~/.cache/uv:/root/.cache/uv + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - CI=true + - DOCKER_HOST=unix:///var/run/docker.sock + env_file: + - ../.env + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - deer-flow-local + restart: unless-stopped + +networks: + deer-flow-local: + driver: bridge \ No newline at end of file diff --git a/docker/nginx/nginx-local-prod.conf b/docker/nginx/nginx-local-prod.conf new file mode 100644 index 00000000..d79936c3 --- /dev/null +++ b/docker/nginx/nginx-local-prod.conf @@ -0,0 +1,207 @@ +events { + worker_connections 1024; +} +pid /tmp/nginx.pid; +http { + # Basic settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Logging + access_log /dev/stdout; + error_log /dev/stderr; + + # Docker internal DNS (for resolving service names) + resolver 127.0.0.11 valid=10s ipv6=off; + + # Upstream servers (using Docker service names) + upstream gateway { + server gateway:8001; + } + + upstream langgraph { + server langgraph:2024; + } + + upstream frontend { + server frontend:3000; + } + + # ── Main server (path-based routing) ───────────────────────────────── + server { + listen 2026 default_server; + listen [::]:2026 default_server; + server_name _; + + # Hide CORS headers from upstream to prevent duplicates + proxy_hide_header 'Access-Control-Allow-Origin'; + proxy_hide_header 'Access-Control-Allow-Methods'; + proxy_hide_header 'Access-Control-Allow-Headers'; + proxy_hide_header 'Access-Control-Allow-Credentials'; + + # CORS headers for all responses (nginx handles CORS centrally) + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' '*' always; + + # Handle OPTIONS requests (CORS preflight) + if ($request_method = 'OPTIONS') { + return 204; + } + + # LangGraph API routes + # Rewrites /api/langgraph/* to /* before proxying + location /api/langgraph/ { + rewrite ^/api/langgraph/(.*) /$1 break; + proxy_pass http://langgraph; + proxy_http_version 1.1; + + # Headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ''; + + # SSE/Streaming support + proxy_buffering off; + proxy_cache off; + proxy_set_header X-Accel-Buffering no; + + # Timeouts for long-running requests + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + + # Chunked transfer encoding + chunked_transfer_encoding on; + } + + # Custom API: Models endpoint + location /api/models { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Custom API: Memory endpoint + location /api/memory { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Custom API: MCP configuration endpoint + location /api/mcp { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Custom API: Skills configuration endpoint + location /api/skills { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Custom API: Artifacts endpoint + location ~ ^/api/threads/[^/]+/artifacts { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Custom API: Uploads endpoint + location ~ ^/api/threads/[^/]+/uploads { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Large file upload support + client_max_body_size 100M; + proxy_request_buffering off; + } + + # API Documentation: Swagger UI + location /docs { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API Documentation: ReDoc + location /redoc { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API Documentation: OpenAPI Schema + location /openapi.json { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Health check endpoint (gateway) + location /health { + proxy_pass http://gateway; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # All other requests go to frontend + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + + # Headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_cache_bypass $http_upgrade; + + # Timeouts + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 7b9f9c4b..3654a6fa 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -130,6 +130,10 @@ export default function ChatPage() { fetchStateHistory: true, onFinish: (state) => { setFinalState(state); + // 新对话完成后导航到对话页面 + if (isNewThread && threadId) { + router.push(pathOfThread(threadId)); + } if (document.hidden || !document.hasFocus()) { let body = "Conversation finished"; const lastMessage = state.messages[state.messages.length - 1]; @@ -230,7 +234,7 @@ export default function ChatPage() { subagent_enabled: settings.context.mode === "ultra", }, afterSubmit() { - router.push(pathOfThread(threadId!)); + // 导航已在 onFinish 中处理,确保 stream 完成后再导航 }, }); const handleSubmit = useCallback( @@ -263,7 +267,7 @@ export default function ChatPage() {
@@ -333,12 +337,12 @@ export default function ChatPage() {
- {isNewThread && } + {isNewThread && !hasSubmitted && }
} disabled={ diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 68f3127a..b1aea4b8 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -90,6 +90,7 @@ export function InputBox({ context, extraHeader, isNewThread, + hasSubmitted, initialValue, onContextChange, onSubmit, @@ -107,6 +108,7 @@ export function InputBox({ }; extraHeader?: React.ReactNode; isNewThread?: boolean; + hasSubmitted?: boolean; initialValue?: string; onContextChange?: ( context: Omit< @@ -139,8 +141,8 @@ export function InputBox({ ); const [isFocused, setIsFocused] = useState(false); - // isNewThread 时禁用收缩,始终保持展开 - const effectiveIsFocused = (isNewThread ?? false) || isFocused; + // isNewThread 时禁用收缩,始终保持展开(除非已提交消息) + const effectiveIsFocused = (isNewThread ?? false) && !hasSubmitted || isFocused; // 点击外部区域时收起输入框 useEffect(() => { @@ -376,7 +378,7 @@ export function InputBox({ /> - {isNewThread && searchParams.get("mode") !== "skill" && ( + {isNewThread && !hasSubmitted && searchParams.get("mode") !== "skill" && ( diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index a94b6976..a6ca26c3 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -104,36 +104,41 @@ export const zhCN: Translations = { followupConfirmReplace: "替换并发送", suggestions: [ { - suggestion: "论文写作", - prompt: - "撰写一篇关于[主题]的学术论文,包含摘要、引言、正文和参考文献。", + suggestion: "自媒体文案", + prompt: "为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。", icon: PenLineIcon, - skill_id: "1", + skill_id: "432", }, { - suggestion: "报告生成", - prompt: "深入分析[主题],生成一份结构清晰的调研报告。", - icon: MicroscopeIcon, - skill_id: "2", + suggestion: "需求文档", + prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。", + icon: CompassIcon, + skill_id: "521", }, { - suggestion: "策划文案", - prompt: "为[项目/活动]撰写一份完整的策划方案和宣传文案。", - icon: ShapesIcon, - skill_id: "3", + suggestion: "使用指南", + prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。", + icon: GraduationCapIcon, + skill_id: "410", }, { suggestion: "PPT生成", prompt: "生成一个关于[主题]的PPT演示文稿大纲和内容。", icon: GraduationCapIcon, - skill_id: "4", + skill_id: "180", }, { - suggestion: "文档处理", - prompt: "对[文档]进行阅读、总结、翻译或格式转换等处理。", - icon: CompassIcon, + suggestion: "Excel数据分析", + prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。", + icon: MicroscopeIcon, skill_id: "5", }, + { + suggestion: "市场调研", + prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。", + icon: ShapesIcon, + skill_id: "31", + }, ], suggestionsCreate: [ {