fix(server): `_require_server()` 是死代码——server extra 探针只查 fastapi,缺 cachetools/redis 时 `from plaita import FlowWorker` 抛裸 ModuleNotFoundError(无 pip install 提示)
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- Half a day
- Newbie friendliness
- 72/100
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() 死代码——后来者读代码会以为覆盖已经做了。
建议
修复方向(两处都要动,缺一不完整):
-
补齐探针清单。
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 更稳,可避免下次再漂移。 -
处理死代码
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 加新语义。 - 不扩到
codeextra: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.tomlextras 的失真属另一条单的范围,本单只动代码。 - 查重结果:
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from jeffkit/plaita
-
Difficulty 4/5 3-5 days Newbie friendliness 42/100
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
-
Difficulty 5/5 Over a week Newbie friendliness 24/100
-
Difficulty 4/5 3-5 days Newbie friendliness 8/100
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Graphify-Labs/graphify#4524 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
topoteretes/cognee#5982 ·
Maintainers usually reply within 1 day
-
Patches of derivations with `__structuredAttrs` are ignoredPossibly taken @KiaraGrouwstra claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 81/100
nix-community/vulnix#184 ·
-
New InternshipOpennew_internship
Difficulty 1/5 Under an hour Newbie friendliness 65/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
aws-samples/sample-per-model-bedrock#295 ·
Maintainers usually reply within 1 day