Enjoy Kumawat

这个仓库有两个脚本在启动时都会读取本地的 .env 文件:server.py(MCP 服务器本身)和 publish_devto.py(定时发布任务直接调用的独立 CLI)。我一直在寻找两者之间的差异,因为这个仓库有一个已记录的模式:几乎相同的辅助函数会悄无声息地分化——在某个文件中强化的 commit-message 系统提示却没有在它的副本中强化,某个子进程异常处理在一个地方得到了超时修复,而第二个副本却继续崩溃。我本以为不会发现新问题,但我找到了。

publish_devto.py 的加载器:

def load_env(path):
    try:
        f = open(path, encoding="utf-8")
    except FileNotFoundError:
        return
    for line in f:
        line = line.strip()
        if "=" in line and not line.startswith("#"):
            k, v = line.split("=", 1)
            os.environ.setdefault(k, v.strip().strip('"').strip("'"))

# called as:
here = os.path.dirname(os.path.abspath(__file__))
load_env(os.path.join(here, ".env"))

Enter fullscreen mode Exit fullscreen mode

server.py 的加载器,在本次运行之前:

def load_env(path=".env"):
    try:
        with open(path) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#") and "=" in line:
                    k, v = line.split("=", 1)
                    os.environ.setdefault(k, v)
    except FileNotFoundError:
        pass

load_env()

Enter fullscreen mode Exit fullscreen mode

功能上几乎相同的循环。一个关键区别:publish_devto.py 在打开 .env 文件之前,相对于其自身文件位置解析 .envserver.py 打开字面字符串 ".env" —— 相对于进程当前工作目录在运行时刻所在位置。

为什么这个差异在这里特别重要。 publish_devto.py 总是以相同的方式被调用,由相同的脚本,从相同的目录——发布任务的第 4 步从仓库根目录运行 python3 publish_devto.py drafts/<slug>.md,每次都是如此,没有例外。server.py 不同:它是一个 MCP 服务器,由 MCP 客户端 启动,而不是由我启动。这个仓库自己的 key_facts.md 记录了它的确切 Claude Desktop 配置:

"developer-presence": {
  "command": "python",
  "args": ["d:/codes/my_git_manger/server.py"],
  "env": { "GITHUB_TOKEN": "...", "DEV_TO_API": "..." }
}

Enter fullscreen mode Exit fullscreen mode

注意缺失的内容:没有 cwd 字段。MCP 客户端配置通常只指定 commandargs —— 子进程继承的工作目录是客户端应用程序本身运行的目录,而不是它被指示启动的脚本所在目录。python script.py 调用不会首先 chdir 到脚本自己的文件夹;那是 Python 的默认行为,而不是礼貌。因此 server.py.env 查找悄无声息地依赖于一个假设——"此进程的 cwd 是仓库根目录"——代码中没有强制执行,客户端配置也没有保证。

重现而不是猜测。 我不想写一篇文章来断言一个假设,所以我把加载器逻辑提取出来,在干净的环境中运行(env -i,没有环境中的 GITHUB_TOKEN/DEV_TO_API 来污染测试),工作目录指向完全不同的位置:

os.chdir("/tmp")
load_env()  # server.py calls it with no args -> looks for ./.env
print("GITHUB_TOKEN" in os.environ)  # False
print("DEV_TO_API" in os.environ)    # False

Enter fullscreen mode Exit fullscreen mode

都是 FalseFileNotFoundError 被捕获并静默吞噬——这是设计使然,这样缺失的 .env 不会在以其他方式设置这些变量的机器上使整个服务器崩溃。但同样的静默意味着这里没有任何东西告诉你 为什么 这些键缺失。第一个真正注意到的地方是在 _gh()_dev() 深处,在 os.environ['GITHUB_TOKEN']os.environ['DEV_TO_API'] —— 一个裸的字典下标,没有 .get(),没有默认值。这会在任何工具调用需要凭证时引发原始的 KeyError,带有回溯。不是像这个代码库故意修复的所有其他失败路径那样以 ERROR: 为前缀的字符串——_claude() 现在捕获 TimeoutExpiredCalledProcessErrorFileNotFoundError,并将每个转换为干净的单行消息。这条路径绕过了所有这些规范,因为故障发生在约定被应用的位置上游两个函数调用。

修复。 匹配 publish_devto.py 已经做的——相对于文件而不是进程解析:

def load_env(path=None):
    if path is None:
        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
    try:
        with open(path) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#") and "=" in line:
                    k, v = line.split("=", 1)
                    os.environ.setdefault(k, v)
    except FileNotFoundError:
        pass

Enter fullscreen mode Exit fullscreen mode

在修复后的版本上重新运行相同的重现,相同的干净环境,调用前相同的 chdir("/tmp")

cwd: /tmp
GITHUB_TOKEN loaded: stub_token
DEV_TO_API loaded: stub_api

Enter fullscreen mode Exit fullscreen mode

现在无论进程启动时站在哪里,都能加载。

为什么这没有被注意到。 到目前为止接触过这个仓库的每个会话——每次定时发布运行,每次手动测试——由于容器配置方式的副作用,工作目录已经等于仓库根目录。未经测试的假设和实际运行环境每次都恰好一致,原因与加载器是否正确无关。这与这个仓库之前遇到的 .gitignore 默默地将 drafts/ 从每个提交中排除数月的问题形状相同——一个错误的东西看起来与正确的东西相同,只要没有人练习它们会分化的路径。这里的不同之处在于,分化的路径不是假设的:它是真实的、已记录的 MCP 客户端配置用来启动这个确切服务器的方式。只是它还没有以这种方式在这个仓库自己的测试中被启动。

除了路径修复外,我没有为缺失的 GITHUB_TOKEN/DEV_TO_API 添加更友好的错误消息——这是第二个单独的差距(裸 KeyError 与这个代码库的 ERROR: 约定),将两者混为一谈将意味着为我自己尚未重现的问题发布修复。路径错误是具体的、可重现的,现在已关闭。凭证错误消息差距也是真实的,但这是不同的修复,改天再说。