Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

fix(server): `_require_server()` 是死代码——server extra 探针只查 fastapi,缺 cachetools/redis 时 `from plaita import FlowWorker` 抛裸 ModuleNotFoundError(无 pip install 提示)

Closed
#99 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
Half a day
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
fastapi, python, redis
Domain
tooling

Research direction

Start with the _EXTRAS_GUIDE table in plaita/__init__.py (around lines 7-16), where the server probe only checks fastapi, and compare it with the server list in pyproject.toml. Then decide whether to delete the dead _require_server() in plaita/server/__init__.py or wire it up, keeping only one probe list. Reproduce by blocking cachetools or redis with a builtins.__import__ hook and importing FlowWorker. Done when that import raises an ImportError that includes pip install plaita[server], with a unit test covering it.

Written by the indexing model from the issue text.

Description

优先级 P2 · 依赖:无

基线 HEAD = 65f48f2

Summary

server extra 的「缺依赖就给可操作报错」承诺,只由一个探针模块 fastapi 兑现;
而 plaita.server 在导入期真正需要的 cachetools / redis 无人探测。于是
from plaita import FlowWorker 在缺 cachetools(或 redis)时抛的是裸
ModuleNotFoundError,全文没有任何 pip install plaita[server] 提示。
同一根因还有第二半:仓库里确实存在一个写对了多探针的 _require_server(),
但它没有任何调用点,是死代码。

1)探针表把 server 的探针写成单个 fastapi(plaita/__init__.py:7-16):

_EXTRAS_GUIDE = {
    "redis": ("redis", "pip install plaita[redis]"),
    "server": ("fastapi", "pip install plaita[server]"),
    "code": ("execjs", "pip install plaita[code]"),
    # http extra 实际含 requests + aiohttp 两个依赖,只探测 requests 会让
    # "缺 extra 可操作报错"的承诺在只装 requests 时落空(运行时才炸)。
    "http": (("requests", "aiohttp"), "pip install plaita[http]"),
    # 观测适配(plaita.obs.LangfuseCallback,SDK v3 = OTel 内核一代)
    "langfuse": ("langfuse", "pip install plaita[langfuse]"),
}

探针机制本身支持多模块元组(http 一行就是先例),所以这里漏的是清单,不是能力。

2)真正消费探针的入口(plaita/__init__.py:167-173)——先探针放行,再导入真实模块:

    if name in _FEATURE_EXTRAS_MAP:
        # Raises an actionable ImportError if the extra is missing.
        _check_extra_available(_FEATURE_EXTRAS_MAP[name])
        module = importlib.import_module(_EXTRA_EXPORTS[name])
        value = getattr(module, name)
        globals()[name] = value
        return value

FlowWorker / ManagementAPI 在 _FEATURE_EXTRAS_MAP(:74-82)里都映射到 "server",
_EXTRA_EXPORTS(:132-140)指向 plaita.server.flow_worker 与 plaita.server.control。

3)plaita.server 的模块级依赖是 cachetools 与 redis:

# plaita/server/flow_worker.py:17-18
from cachetools import TTLCache
from redis import Redis
# plaita/server/control.py:11
from redis import Redis

plaita/server/ 下模块级 import cachetools/redis 的文件共 10 个:

$ grep -rln '^from cachetools\|^import cachetools\|^from redis\|^import redis' plaita/server/
plaita/server/task_queue.py
plaita/server/registry.py
plaita/server/event_filter.py
plaita/server/control.py
plaita/server/flow_worker.py
plaita/server/log_handler.py
plaita/server/services/service_manager.py
plaita/server/services/schedule_service.py
plaita/server/services/base_service.py
plaita/server/services/__main__.py

4)探针的 fastapi 在整个 plaita/ 包里其实没有任何运行时导入点——全包只有 3 处
命中,其中 2 处在死代码内部(见第 5 条):

$ grep -rn 'fastapi' --include='*.py' plaita/ | grep -v __pycache__
plaita/__init__.py:9:    "server": ("fastapi", "pip install plaita[server]"),
plaita/server/__init__.py:13:        import fastapi  # noqa: F401
plaita/server/__init__.py:15:        missing.append("fastapi")

即:探的是不用的包,用的是没探的包。同理 uvicorn / sqlalchemy 在 plaita/server/
下也没有模块级导入(grep -rn '^import uvicorn\|^from uvicorn' plaita/server/ 与
grep -rn '^import sqlalchemy\|^from sqlalchemy' plaita/server/ 均零命中)。

5)写对了多探针的那个函数没有任何调用点(plaita/server/__init__.py:9-29 全文):

def _require_server():
    """Raise ImportError with actionable message if server dependencies are missing."""
    missing = []
    try:
        import fastapi  # noqa: F401
    except ImportError:
        missing.append("fastapi")
    try:
        import uvicorn  # noqa: F401
    except ImportError:
        missing.append("uvicorn")
    try:
        import sqlalchemy  # noqa: F401
    except ImportError:
        missing.append("sqlalchemy")

    if missing:
        raise ImportError(
            f"Server dependencies not installed: {', '.join(missing)}. "
            "Install them with: pip install plaita[server]"
        )

反证——全仓(含 plaita-console/、plaita-ai/、tests/、scripts/)零调用点,
唯一命中是定义行:

$ grep -rn '_require_server' . --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=__pycache__
./plaita/server/__init__.py:9:def _require_server():

注意这个死代码自身也没探 cachetools/redis(它只探 fastapi/uvicorn/sqlalchemy),
所以它既未被调用、清单也不对。

6)pyproject.toml 里 server extra 的真实清单是 5 个包(pyproject.toml:32-43):

server = [
    "fastapi>=0.68.0",
    "uvicorn>=0.15.0",
    # redis: plaita/server 下 8 个模块模块级 import redis——缺它则 server extra
    # 不自足(2026-09 打包审计 P1-1)
    "redis>=4.0.0",
    "SQLAlchemy>=1.4.0",
    "cachetools>=5.0.0",
    # python-jose/passlib/python-multipart 已裁撤(2026-09 打包审计 P2-1):
    # plaita 源码从不 import(编排台才用),且 python-jose 传递引入的 ecdsa
    # 有无修复版本的 CVSS 7.4 公告——它们属于 plaita-console 的依赖清单。
]

7)测试侧已经知道这个缺口,但只把测试遮成 skip,没回修用户侧报错
(tests/e2e/test_server_install.py:6-14):

# server extra 的依赖清单(pyproject [project.optional-dependencies].server):
# 缺任一项都意味着 extra 未装,应 SKIP 而非 FAIL——原先只认 error 文本里的
# "server"/"fastapi",缺 cachetools/redis 时直接抛错(干净环境假红)。
_SERVER_EXTRA_DEPS = ("fastapi", "redis", "cachetools", "sqlalchemy")


def _server_extra_missing(exc: ImportError) -> bool:
    missing = (getattr(exc, "name", "") or "").split(".")[0]
    return missing in _SERVER_EXTRA_DEPS or "server" in str(exc).lower()

8)复现(环境 /tmp/plaita-audit-venv/bin/python,Python 3.12,已 editable 装
plaita[dev,all];先确认五个包都在:fastapi: OK / uvicorn: OK / sqlalchemy: OK / cachetools: OK / redis: OK)。用 builtins.__import__ 钩子屏蔽单个包:

# /tmp/repro_13.py(屏蔽 cachetools)
import builtins, sys
BLOCK = {"cachetools"}
real_import = builtins.__import__

def fake_import(name, *a, **k):
    if name.split(".")[0] in BLOCK:
        raise ImportError("No module named '%s'" % name, name=name.split(".")[0])
    return real_import(name, *a, **k)

builtins.__import__ = fake_import
for m in [m for m in list(sys.modules) if m.split(".")[0] in BLOCK]:
    del sys.modules[m]

try:
    from plaita import FlowWorker
    print("OK ->", FlowWorker)
except Exception as e:
    print("FAILED ->", type(e).__name__, ":", e)

实测输出:

$ /tmp/plaita-audit-venv/bin/python /tmp/repro_13.py
FAILED -> ImportError : No module named 'cachetools'

把 BLOCK 换成 {"redis"}(同一脚本,循环访问 FlowWorker 与 ManagementAPI):

$ /tmp/plaita-audit-venv/bin/python /tmp/repro_13c.py
FlowWorker FAILED -> ImportError : No module named 'redis'
ManagementAPI FAILED -> ImportError : No module named 'redis'

三次输出里都没有 pip install plaita[server]。对照实验——屏蔽探针命中项 fastapi 时,
可操作报错是正常的:

$ /tmp/plaita-audit-venv/bin/python /tmp/repro_13d.py
FAILED -> ImportError :
The 'server' extra is required for this feature but is not installed. Install it with: pip install plaita[server]
Or install all extras: pip install plaita[all]

即 _check_extra_available() 的提示逻辑本身工作正常,唯一问题是 server 走不到它
(探针探的是 fastapi,装没装 cachetools/redis 不影响判定)。

影响

谁:任何「宿主环境已装 fastapi(或 uvicorn/sqlalchemy)但 plaita 本体只按
pip install plaita 装」的部署。这在实践中很常见——plaita-console 这类 FastAPI 应用、
或用户自己的 FastAPI 服务里顺带装 plaita,都会让探针误判「server extra 已就绪」。
条件:pip install plaita(不带 [server]),且宿主恰好有 fastapi。

踩到什么:from plaita import FlowWorker(或 plaita.ManagementAPI)抛
ImportError: No module named 'cachetools' / 'redis'。报错文本既不说这是 extra 缺失,
也不给安装命令,用户看到的是「plaita 的依赖声明不全 / 包是坏的」,而不是「我该
pip install plaita[server]」。

生产可达:可达(非库嵌入限定)。代价不是数据错或挂起,而是不可自助定位的启动期
失败
:故障信号指向错误的层(裸模块缺失 vs. 明确缺 extra),运维/用户必须自己去读
pyproject.toml 或源码才能得出正确动作;如果这条错误出现在 worker 启动脚本里
(launchd/systemd 拉起),还会被记成「plaita 启动崩溃」而不是「环境缺 extra」。

CI 不会兜住:.github/workflows/ci.yml 用 .[dev,lint,all] 安装,server extra 的包
全在,所以门禁永远是绿的;tests/e2e/test_server_install.py 又把同样的缺口按 exc.name
显式识别成 SKIP。两边都不会提醒这个报错还缺指引。

不修的后果:_EXTRAS_GUIDE 里唯一一个多依赖 extra 的「可操作报错」承诺长期落空,
且仓库里同时躺着一份写对了的 _require_server() 死代码——后来者读代码会以为覆盖已经做了。

建议

修复方向(两处都要动,缺一不完整):

  1. 补齐探针清单。plaita/__init__.py:9 把 server 的探针改成与
    pyproject.toml:32-43 一致的元组;校验机制已支持元组(http 一行是先例):

    "server": (("fastapi", "uvicorn", "redis", "cachetools", "sqlalchemy"), "pip install plaita[server]"),
    

    最小修法(只堵住本次复现的两条裸报错路径)是把 "fastapi" 换成
    ("redis", "cachetools")——plaita/server/ 下模块级真正 import 的只有这两个;
    但完整对齐 pyproject 更稳,可避免下次再漂移。

  2. 处理死代码 plaita/server/__init__.py:9-29:要么删掉它,要么让它在
    plaita/server/__init__.py 被导入时调用、并补齐 redis/cachetools 探针。
    两条路选一条,不要两处各留一份探针清单(这正是本次漂移的成因)。

验收条件(可执行、可断言):

  • 新增单测(建议 tests/unit/test_extras_guidance.py):用 builtins.__import__ 钩子
    或 sys.meta_path 屏蔽 cachetools(以及 redis),断言
    getattr(plaita, "FlowWorker") 抛出的 ImportError 文本同时包含 server 与
    pip install plaita[server];屏蔽 fastapi 时不强制(它本就不是导入期依赖)。
  • 跑通现有 e2e:tests/e2e/test_server_install.py 在「只缺 cachetools/redis」的环境下
    仍然是 skip 语义(这条不回归),但新增断言要求错误文本含安装指引。
  • 复现脚本 /tmp/repro_13.py 与 /tmp/repro_13c.py 在上述修复后应输出
    「The 'server' extra is required for this feature but is not installed. Install it with:
    pip install plaita[server]」,而不是 No module named 'cachetools' / 'redis'。

边界

  • 只修 server extra 的探针清单与那份死代码。不改 _check_extra_available() 的判定
    机制(按 extra 名查表本身没问题),不给其他 extra 加新语义。
  • 不扩到 code extra:RestrictedPython 缺失时 plaita/node/code.py:108-113 会在
    模块级降级为 _RESTRICTED_AVAILABLE = False,使用点 :432-436 给出
    Install it with: pip install RestrictedPython,from plaita import CodeNode 实测仍成功——
    不存在同类裸报错,不在本单范围。
  • 不顺手重构 plaita/server/__init__.py 的包结构(该文件目前除死代码外无内容)。
  • 不碰文档侧漂移:docs-site/docs/reference/configuration.md 等配置参考页与
    pyproject.toml extras 的失真属另一条单的范围,本单只动代码。
  • 查重结果:gh issue list -R jeffkit/plaita --state all --limit 100 共 33 条,
    无与本条同根的单(#46 同属 server 依赖面的报错,但那是 services 启动器缺 logging 配置,
    与本条的 extras 探针清单无关,不合并)。
Dominant language
Python
Stars
0
Forks
1
Avg merge
3h 53m
Merged PRs (30d)
3

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from jeffkit/plaita

All issues in jeffkit/plaita

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.