DeerFlow 2.0 Windows LocalSandbox Shell 执行 Bug 完整排查修复报告

日期 : 2026-07-09

环境 : Windows 10 + Git for Windows (重装后位于 C:\Git) + Miniconda (deerflow conda 环境, Python 3.12.13) + Node v24.18.0 + 本地 nginx 静态托管

影响版本 : DeerFlow 2.0 backend/packages/harness/deerflow/sandbox/local/local_sandbox.py

排查耗时: 多阶段递进式排查,历经 6 个关键阶段


0. 写在前面

最近在本地部署了deerflow2.0,准备试用一下。奈何,本地电脑太拉,尤其是安装的系统太老,docker要求的系统版本不满足,只能本地部署一个非docker版本。一番折腾,成功运行之后,发现deerflow不能调用本地环境执行脚本和代码。经过排查才发现是deerflow本身的一个bug引起的,这篇文档就详细记录了排查的过程和修复的方法。


1. 问题概述

DeerFlow 2.0 在 Windows 本地开发环境中,用户通过聊天界面让 AI 助手(小鹿)执行 shell 命令时,命令始终无法正常执行,最终返回错误:

复制代码
/c: Is a directory
Exit Code: 126

该问题在 Linux/macOS 环境下不存在,仅在 Windows 上复现。排查过程中先后遇到了路径空格、Shell 路径转义、环境变量误导、第三方包搜索干扰等多个连环问题,最终定位到 Python subprocess 跨平台实现的 Windows 特定行为导致的 bug。


2. 完整排查时间线

阶段一:Git 安装路径包含空格问题

现象

最初在 Windows 上启动 DeerFlow 后,执行 shell 命令时出现与路径相关的错误(表现为找不到 git、sh 等命令,或路径解析错误)。

排查

检查 Git 安装位置,发现 Git for Windows 安装在默认路径:

复制代码
C:\Program Files\Git\

路径中包含空格(Program Files),在 subprocess 调用、Makefile 脚本、shell 命令拼接等场景下,空格是常见的问题根源------如果路径没有被正确引用,会被解析为多个参数。例如 C:\Program Files\Git\bin\bash.exe 如果不加引号,会被误解析为程序 C:\Program(不存在)和参数 Files\Git\bin\bash.exe

处理

重装 Git for Windows 到无空格路径 C:\Git\,确认目录结构:

  • C:\Git\bin\bash.exe --- Git Bash 主程序
  • C:\Git\usr\bin\sh.exe --- POSIX 兼容 sh
  • C:\Git\usr\bin\ --- 各种 GNU 工具集(ls, grep, find 等)

同时通过 Makefile(利用 Git 自带的 GNU Make 二进制)配置了本地 nginx 静态托管,用于部署 Shadow DOM 模型切换器组件。

阶段结果

重装 Git 解决了路径空格导致的"命令不存在"问题,但 shell 执行仍然失败,错误变为:

复制代码
/c: Is a directory
Exit Code: 126

阶段二:重装 Git 后错误变为 /c: Is a directory

现象

重装 Git 到 C:\Git 并重启 uvicorn、LangGraph 服务(pnpm 和 nginx 保持运行)后,shell 命令执行统一返回 /c: Is a directory,退出码 126。

退出码 126 在 POSIX 标准中表示"找到命令但无法执行"(Permission denied 或不是可执行文件)。错误信息表明系统试图"执行"名为 /c 的东西,而 /c 是一个目录。

初步分析

/c 在 MSYS/Git Bash 环境中是一个特殊路径------POSIX 风格的根路径 /c 被映射到 Windows 的 C:\ 盘根目录。这说明某个地方把 /c 当成了要执行的命令/脚本路径,但 /c 是目录不是可执行文件,因此报错。

阶段结果

错误从"命令找不到"变成了"试图执行 C 盘根目录",问题从路径空格问题转化为 shell 参数传递错误。


阶段三:Git Bash 反斜杠路径转义陷阱

现象

在验证 Git 二进制文件是否存在时,出现了"文件不存在"的假象。在 Git Bash 中执行 ls C:\Git\usr\bin\sh.exe 返回 No such file or directory,但文件在资源管理器中明明可见。

排查

这是 Git Bash(基于 MSYS2)的经典陷阱:反斜杠 \ 在 POSIX shell 中是转义字符 。当输入 C:\Git\usr\bin\sh.exe 时,shell 将 \G\u\b\s 解释为转义序列,路径被错误解析为类似 C:Gitusrbinsh.exe,自然找不到文件。

正确写法是使用正斜杠:

bash 复制代码
# 正确(正斜杠)
ls /c/git/usr/bin/sh.exe
ls C:/Git/usr/bin/sh.exe

# 错误(反斜杠被转义)
ls C:\Git\usr\bin\sh.exe

验证确认 /c/git/usr/bin/sh.exe/c/git/bin/bash.exe 均存在。

阶段结果

确认 Git 安装正确,sh.exe 和 bash.exe 都存在且可执行。之前的"文件不存在"是反斜杠转义造成的误判,后续验证路径统一使用正斜杠。


阶段四:怀疑 SHELL 环境变量,验证排除

假设

错误信息 /c: Is a directory 看起来像 shell 路径解析问题,推测可能是 SHELL 环境变量设置错误。

验证

  1. 检查 echo $SHELL/usr/bin/bash.exe(正确)
  2. unset SHELL 后重启服务 → 错误依旧
  3. 在独立 Git Bash 终端手动测试 Python subprocess 调用 → 完全正常
python 复制代码
import subprocess
r = subprocess.run(['bash', '-c', 'echo hello'], capture_output=True, text=True)
print(r.stdout, r.returncode)  # 正常输出 hello 0

阶段结果

排除 SHELL 环境变量问题。独立 Python subprocess 手动调用正常,说明问题出在 DeerFlow 代码调用 subprocess 的方式上。


阶段五:grep 搜索被 .venv 第三方包淹没

思路

既然问题出在 DeerFlow 代码调用 subprocess 的方式,搜索代码库中 subprocess 的使用位置。

遭遇问题

grep 搜索 subprocess 返回海量结果,绝大多数来自 backend\.venv\Lib\site-packages\ 下的第三方包(numpy、pandas、PIL、setuptools、sympy、pytest 等),这些与 DeerFlow 自身 bug 完全无关,但搜索结果被淹没。

处理

排除 .venv 目录后,定位到项目自有代码:

文件路径 是否相关
backend/packages/harness/deerflow/sandbox/local/local_sandbox.py 本地同步 shell 执行(核心)
backend/packages/harness/deerflow/community/aio_sandbox/local_backend.py Docker 容器管理(调用 docker CLI,列表参数)
backend/packages/harness/deerflow/community/aio_sandbox/aio_sandbox.py Docker 沙箱 HTTP 客户端(无本地 shell)

进一步确认:aio_sandbox.py 通过 HTTP API 连接 Docker 容器,不创建本地 shell 进程;local_backend.py 调用 ["docker", "run", ...] 使用列表参数,无 shell=True,均不受影响。

由于 grep 在 Windows 路径匹配上也有问题,最终使用自定义 Python os.walk() 脚本来精确过滤 .venv 目录。

阶段结果

排除 aio_sandbox 错误方向,唯一目标文件锁定为 local_sandbox.py


阶段六:定位核心源码,发现根因

代码审查

阅读 local_sandbox.py 完整源码(修复前已备份为 local_sandbox.py.bak),核心逻辑:

python 复制代码
class LocalSandbox(Sandbox):
    @staticmethod
    def _get_shell() -> str:
        for shell in ("/bin/zsh", "/bin/bash", "/bin/sh"):
            if os.path.isfile(shell) and os.access(shell, os.X_OK):
                return shell
        shell_from_path = shutil.which("sh")
        if shell_from_path is not None:
            return shell_from_path
        raise RuntimeError("No suitable shell executable found.")

    def execute_command(self, command: str) -> str:
        result = subprocess.run(
            command,
            executable=self._get_shell(),
            shell=True,
            capture_output=True,
            text=True,
            timeout=600,
        )

根因推导(逐步追踪 Windows 执行流程):

  1. _get_shell() 在 Windows 上:/bin/zsh/bin/bash/bin/sh 均不存在,走 shutil.which("sh") 找到 C:\Git\usr\bin\sh.exe
  2. subprocess.run() 传入 shell=Trueexecutable=sh.exe
  3. 关键 bug :Python 在 Windows 上实现 shell=True 时,调用 CreateProcess API,无论 executable 指定什么 shell,Python 都硬编码使用 /c 参数(这是 cmd.exe 的命令执行语法)
  4. 实际执行等价于 sh.exe /c "echo hello"
  5. sh.exe(POSIX shell)期望的参数是 -c,收到 /c 后将其解释为脚本路径
  6. 在 MSYS 路径映射中 /c = C:\(C 盘根目录,是目录)
  7. 尝试执行目录 → 报错 /c: Is a directory,退出码 126

独立复现验证

python 复制代码
import subprocess

# 有 bug 的方式(模拟原代码)
r1 = subprocess.run('echo hello', executable='C:/Git/usr/bin/sh.exe',
                     shell=True, capture_output=True, text=True)
# stdout='', stderr='/c: Is a directory\n', rc=126  复现错误!

# 修复方式
r2 = subprocess.run(['C:/Git/bin/bash.exe', '-c', 'echo hello'],
                     shell=False, capture_output=True, text=True)
# stdout='hello\n', stderr='', rc=0  正常!

根因 100% 确认。


3. 根因深度分析

3.1 Bug 触发链条(完整链路)

复制代码
Git 安装在带空格路径 C:\Program Files\Git\
  └─→ 路径解析问题(重装到 C:\Git 解决)
       └─→ Git Bash 验证路径时反斜杠被转义(排查干扰项,已识别)
            └─→ 怀疑 SHELL 环境变量(方向错误,已排除)
                 └─→ grep 被 .venv 第三方包淹没(搜索干扰,已过滤)
                      └─→ _get_shell() 在 Windows 返回 sh.exe
                           └─→ subprocess.run(shell=True, executable=sh.exe)
                                └─→ Python Windows 硬编码 /c 参数
                                     └─→ sh.exe 收到 /c 而非 -c
                                          └─→ /c 在 MSYS 映射为 C:\(目录)
                                               └─→ /c: Is a directory (Exit Code 126)

3.2 为什么 Linux/macOS 没有这个问题

平台 shell=True 内部实现 分隔参数 结果
Linux/macOS /bin/sh -c command -c(POSIX 标准) 正常
Windows(默认) cmd.exe /c command /c(cmd.exe 专属) 正常(用 cmd.exe 时)
Windows + executable=sh.exe sh.exe /c command /c(硬编码,不随 executable 改变) 错误

Python 在 POSIX 和 Windows 上对 shell=True 的实现逻辑完全不同:

  • POSIX:调用 /bin/sh 并传入 -c 参数,executable 可以替换 shell 程序且 -c 参数保持
  • Windows:调用 cmd.exe 并传入 /c 参数,即使 executable 指定了其他 shell,/c 参数仍然硬编码不变

这是 Python 的设计行为而非 bug,但在 Windows 上使用非 cmd.exe 的 POSIX shell(Git Bash、WSL bash)时必然触发。

3.3 _get_shell() 原有逻辑的不足

_get_shell() 方法:

  1. 只检查 /bin/zsh/bin/bash/bin/sh 三个 POSIX 路径------在 Windows 上永远找不到
  2. 仅用 shutil.which("sh") 做兜底------可能找到 Windows 自带的 sh(如果有)或 Git 的 sh.exe,但永远不会优先找 bash.exe(功能更完整)
  3. 没有对 Windows 平台做特殊处理,没有搜索 Git for Windows、WSL 等常见安装位置

4. 修复方案

4.1 修改文件

文件 : backend/packages/harness/deerflow/sandbox/local/local_sandbox.py

备份 : local_sandbox.py.bak(修复前创建)

4.2 改动一:新增 import platform

在文件头部添加 import platform,用于平台检测。

4.3 改动二:重写 execute_command() 方法

修改前(有 bug):

python 复制代码
def execute_command(self, command: str) -> str:
    result = subprocess.run(
        command,
        executable=self._get_shell(),
        shell=True,
        capture_output=True,
        text=True,
        timeout=600,
    )

修改后(修复):

python 复制代码
def execute_command(self, command: str) -> str:
    shell_exe = self._get_shell()
    # Use shell=False with explicit -c argument to avoid platform-specific issues.
    # On Windows, subprocess.run(shell=True) hardcodes /c (cmd.exe flag) regardless
    # of the executable specified, which causes POSIX shells to fail with
    # "/c: Is a directory". Using explicit [shell, "-c", command] works correctly
    # on all platforms.
    result = subprocess.run(
        [shell_exe, "-c", command],
        shell=False,
        capture_output=True,
        text=True,
        timeout=600,
    )

核心原理 :不用 shell=True 让 Python 自动组装命令行,而是显式传入参数列表 [shell_exe, "-c", command]shell=False 确保 Python 不做任何平台特定的参数转换,直接以 argv 数组方式启动进程。这在所有平台(Windows/Linux/macOS)上行为一致且正确。

4.4 改动三:增强 _get_shell() 方法

修改前

python 复制代码
@staticmethod
def _get_shell() -> str:
    for shell in ("/bin/zsh", "/bin/bash", "/bin/sh"):
        if os.path.isfile(shell) and os.access(shell, os.X_OK):
            return shell
    shell_from_path = shutil.which("sh")
    if shell_from_path is not None:
        return shell_from_path
    raise RuntimeError("No suitable shell executable found. Tried /bin/zsh, /bin/bash, /bin/sh, and `sh` on PATH.")

修改后

python 复制代码
@staticmethod
def _get_shell() -> str:
    is_windows = platform.system() == "Windows"

    if not is_windows:
        for shell in ("/bin/zsh", "/bin/bash", "/bin/sh"):
            if os.path.isfile(shell) and os.access(shell, os.X_OK):
                return shell
    else:
        program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
        program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
        local_appdata = os.environ.get("LOCALAPPDATA", "")
        windir = os.environ.get("WINDIR", r"C:\Windows")

        windows_candidates = [
            r"C:\Git\bin\bash.exe",
            r"C:\Git\usr\bin\bash.exe",
            os.path.join(program_files, "Git", "bin", "bash.exe"),
            os.path.join(program_files_x86, "Git", "bin", "bash.exe"),
            os.path.join(local_appdata, "Programs", "Git", "bin", "bash.exe") if local_appdata else None,
            r"C:\Git\bin\sh.exe",
            r"C:\Git\usr\bin\sh.exe",
            os.path.join(program_files, "Git", "bin", "sh.exe"),
            os.path.join(program_files_x86, "Git", "bin", "sh.exe"),
            os.path.join(windir, "System32", "bash.exe"),  # WSL
        ]
        for shell in windows_candidates:
            if shell and os.path.isfile(shell) and os.access(shell, os.X_OK):
                return shell

    shell_from_path = shutil.which("bash") or shutil.which("sh")
    if shell_from_path is not None:
        return shell_from_path
    raise RuntimeError(
        "No suitable shell executable found. "
        "On POSIX: tried /bin/zsh, /bin/bash, /bin/sh, and `sh` on PATH. "
        "On Windows: tried Git for Windows bash.exe/sh.exe in common locations, "
        "WSL bash.exe, and `bash`/`sh` on PATH."
    )

改进点

  • Windows 平台优先搜索常见 Git for Windows 安装路径(C:\Git\Program Files\Git\%LOCALAPPDATA%\Programs\Git\ 等)
  • 优先使用 bash.exe(功能更完整,支持数组、关联数组等高级特性),sh.exe 作为备选
  • 包含 WSL bash(C:\Windows\System32\bash.exe)路径兜底
  • PATH 查找时优先 bashsh(原来只找 sh
  • 错误信息更详细,帮助用户排查安装问题

4.5 清理 Python 字节码缓存

修改源码后必须清理 __pycache__ 确保新代码加载:

bash 复制代码
find backend/packages/harness/deerflow/sandbox/local/__pycache__/ -name "*.pyc" -delete

然后重启 uvicorn 和 langgraph 服务(pnpm 和 nginx 无需重启)。


5. 验证结果

修复并重启服务后,通过 DeerFlow 聊天界面执行多轮 shell 命令测试:

测试项 命令/检查点 结果
基本命令 echo hello && pwd 输出 hello 和项目路径
Python 版本 python --version Python 3.12.13
Git 版本 git --version git version 2.55.0.windows.2
Node 版本 node --version v24.18.0
Conda 环境 echo $CONDA_DEFAULT_ENV deerflow
Shell 类型 echo $SHELL /usr/bin/bash.exe
Python 子进程调用 python -c "subprocess.run([...])" 正常执行,返回码 0
退出码 126 错误是否消失 已消失
/c: Is a directory 错误是否消失 已消失

所有测试项通过,shell 功能完全恢复正常。


6. 技术要点与经验总结

6.1 subprocess 跨平台调用最佳实践

在需要跨平台调用 POSIX shell 的场景下,始终使用 shell=False + 显式参数列表

python 复制代码
# 错误:Windows 上 shell=True + executable 会硬编码 /c 参数
subprocess.run(command, shell=True, executable="/path/to/bash")

# 正确:显式参数列表,跨平台一致
subprocess.run(["/path/to/bash", "-c", command], shell=False)

shell=True 只在确实需要使用系统默认 shell(Windows 上为 cmd.exe,Linux 上为 /bin/sh)且命令不需要跨平台时才适合使用。

6.2 Windows 开发环境常见陷阱

陷阱 说明 对策
路径含空格 C:\Program Files\ 等路径在命令行中会被拆分为多个参数 安装开发工具到无空格路径(如 C:\Git\C:\Python312\
反斜杠转义 Git Bash/MSYS 中 \ 是转义字符,C:\Git 被解析为错误路径 在 Git Bash 中使用正斜杠 /c/git/...C:/Git/...
grep 搜索虚拟环境 .venv/Lib/site-packages/ 下海量第三方包会淹没搜索结果 搜索时排除 .venvnode_modules 等依赖目录
SHELL 环境变量 在某些终端启动方式下 SHELL 可能未正确设置 不应依赖 SHELL 环境变量判断 shell 路径
Python 字节码缓存 修改 .py 文件后旧的 .pyc 可能仍被加载 修改后清理 __pycache__/ 并重启服务

6.3 aio_sandbox 不受影响的原因

backend/packages/harness/deerflow/community/aio_sandbox/ 目录:

  • aio_sandbox.py:Docker 沙箱的 HTTP 客户端,通过 HTTP 协议与容器内 sandbox 服务通信,不创建本地 shell 进程
  • local_backend.py:管理 Docker 容器启停,调用方式为 subprocess.run(["docker", "run", ...], shell=False),使用列表参数形式,本身就是正确写法

这两个文件均不受此 bug 影响,无需修改。

相关推荐
神奇小汤圆1 小时前
Loop Engineering 半年实战拆解 :自进化的开发系统已开源
人工智能
熊猫钓鱼>_>1 小时前
Electron:当 Web 技术统治桌面
大数据·前端·javascript·人工智能·架构·electron·agent
城事漫游Molly1 小时前
用AI提炼研究问题与假设的完整提示词——从模糊想法到精准RQ/Hypothesis,AI辅助精化全流程
人工智能·机器学习·prompt·ai for science·博士生必读·应用语言学·研究问题
AI刀刀2 小时前
豆包智能体对话导出后,如何构建长期归档与高效检索体系?
android·人工智能·word·excel·ai导出鸭
workflower2 小时前
室内外配送机器人-应用路径
人工智能·机器学习·设计模式·矩阵·自动化
Token炼金师2 小时前
生产化的鸿沟:流式、缓存、可观测、红蓝对抗、限流降级与评测 —— 工程化六渡
人工智能·llm·可观测性·红蓝对抗·缓存策略·限流降级·评测体系
DMD1682 小时前
AI海外外贸部:AI正在重新定义企业海外市场开发模式
大数据·人工智能·数字化转型·ai技术开发
PhotonixBay2 小时前
共聚焦成像核心原理:针孔、PSF与三维形貌测量技术
人工智能·测试工具·算法
橘子星2 小时前
MCP协议深度解析:从代码实例看跨进程工具调用
人工智能