首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >青龙面板踩坑实录:No module named 'notify' 的真正原因,和「改了没生效」的三层陷阱

青龙面板踩坑实录:No module named 'notify' 的真正原因,和「改了没生效」的三层陷阱

原创
作者头像
用户10817318
发布于 2026-10-22 19:44:42
发布于 2026-10-22 19:44:42
180
举报

背景

先交代环境,因为后面每个坑都和这些设定有关:

项

情况

宿主机

CM311-1A 电视盒子,Amlogic S905L3A,2G 内存

系统

Armbian(Debian 系)

青龙

Docker 部署,镜像自带 otask.sh / task 那套启动链

脚本

JS 与 Python 混用,Python 脚本依赖仓库里的 notify.py 等公共模块

通知

Bark 推送

青龙本身对环境变量的管理有好几层,加上它启动 Python 任务的方式比较特殊,几个坑叠加起来,报错信息会把你往完全错误的方向带。以下按「排查成本从高到低」排列,第一个坑我耗的时间最长。

坑一:No module named 'notify',第一反应 pip install 就错了

报错长这样:

代码语言:txt
复制
Traceback (most recent call last):
  File "/ql/data/scripts/xxx/yyy.py", line 5, in <module>
    import notify
ModuleNotFoundError: No module named 'notify'

90% 的教程会告诉你:依赖没装,pip install 一下。但对 notify 这个模块,装包是死路——PyPI 上根本没有(或不是你想要的那个)。notify.py 通常是脚本仓库自带的文件,和脚本在同一个目录,靠 Python 的路径搜索机制被找到。

所以真正的问题是:任务进程的模块搜索路径(sys.path)里没有脚本所在目录。

根因:otask.sh 重写了 PYTHONPATH,且顺序把你自己的补丁顶掉了

翻青龙容器里的 /ql/shell/otask.sh,对 .py 任务有类似这样的逻辑:

代码语言:sh
复制
export PYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"

其中 dir_preload 是 /ql/shell/preload,排在最前面。而这个目录里躺着一个青龙官方的 sitecustomize.py,末尾有一句模块级的 import task_before。

关键在于 Python 解释器启动时的规则:site 模块会按顺序尝试导入 sitecustomize,只导入第一个找到的。于是发生了这样的事:

  1. 我按常规思路,把补丁做成了 sitecustomize.py(往 sys.path 里 append 脚本目录),放在依赖缓存目录的 site-packages 里
  2. 任务启动时 PYTHONPATH 里 /ql/shell/preload 排最前,官方的 sitecustomize.py 先被找到并导入
  3. 同名模块只有一个能生效 → 我的 sitecustomize.py 永远不会执行
  4. task_before 钩子执行失败或者啥也没干,脚本目录照样不在 sys.path 里 → import notify 失败

也就是说,报错说「缺 notify」,实际缺的是一次正常的路径注入。报错信息和真实原因隔了一层,这是整个排查里最费时间的地方。

正确修法

两步,都不需要重启容器:

第一步:用官方钩子 task_before.py。 青龙支持 /ql/data/config/task_before.py,会在任务前被调用。往里写:

代码语言:python
复制
import sys
sys.path.append("/ql/data/scripts")

第二步:把补丁文件改名。 如果你在依赖目录放过自己的 sitecustomize.py,把它改名成 usercustomize.py。site 模块先 import sitecustomize 再 import usercustomize,两者可以共存,这样你的路径注入在所有进程(包括不走 otask.sh 的进程)里都能生效。

注意两点:

  • 路径注入用 sys.path.append,不要用 insert(0),防止遮蔽标准库
  • 改完不用重启容器,下一个任务立即生效

「裸 shell 能 import」不能作为判据

这是我踩过的最阴的一个坑:docker exec 进容器,python -c "import notify" 一切正常,但面板里跑任务就是报错。

原因:docker exec 起的 shell 是干净环境,PYTHONPATH 没被 otask.sh 改写;面板任务走的路径完全不同。唯一可信的验证方式是模拟面板的调用链:

代码语言:sh
复制
docker exec qinglong bash -c 'cd /ql/data/scripts && QL_DIR=/ql task <仓库目录>/<脚本路径>.py'

这条命令等价于你在面板里点「运行」。它通过了,面板里才会通过。

坑二:配置改了没生效——同一个变量有三层,你在改的可能不是生效那层

第二个大坑是环境变量。青龙的配置有三层,优先级从低到高:

  1. 容器层:compose 的 environment / docker run -e(进容器 Config.Env)
  2. 面板层:网页「环境变量」界面(存在 /ql/data/db 的 sqlite 里)
  3. 任务层:/ql/data/config/config.sh 和 extra.sh,任务启动前被 source,优先级最高

排查「改了没生效」的第一原则:先怀疑有更高优先级的同名变量在覆盖你。常见场景是三层里都有 HTTP_PROXY,你在面板里改了,但 config.sh 里还有一行旧的,任务实际用的是旧的。

验收不能只看文件,要用任务的真实视角看:

代码语言:sh
复制
docker exec qinglong bash -c 'source /ql/data/config/config.sh; env | grep -i proxy'

这打印出来的才是任务进程实际拿到的值。

还有两个附带的小坑:

  • export VAR="" 不是「恢复默认值」,而是显式把变量覆盖成空串。config.sh 里要填就填真值,不要的整行删掉,绝不留空赋值
  • 改了 compose 的 environment 或 command 后,必须 docker compose up -d --force-recreate。否则 docker compose up -d 只会说一句 Running 0.0s——容器没重建,配置当然没变

坑三:command 被镜像 entrypoint 吞掉,是死代码

有个需求想在容器启动时执行 git config,我写进了 compose 的 command:,重启后 docker inspect 一看——根本没执行。

原因:青龙的镜像入口脚本(entrypoint)不会执行你传入的 command,那段配置是死代码,不报错、不生效,纯摆设。

替代方案是给任务进程配 GIT_CONFIG_GLOBAL 环境变量,指向仓库目录里的一个文件:

代码语言:ini
复制
GIT_CONFIG_GLOBAL=/ql/data/.gitconfig

注意把 user.name、user.email、http.postBuffer 一起写进这个文件。另外不要用 bind mount 单文件去覆盖容器里的 /root/.gitconfig——单文件挂载在容器重建后容易出 inode 问题,目录挂载更稳。

坑四:NO_PROXY 写法不对,容器互访被代理截走,症状是 503

这套环境里配了 HTTP 代理(HTTP_PROXY / HTTPS_PROXY),容器里跑的任务会继承这几个变量。但容器之间互访不能走代理,NO_PROXY 写错就会被截走。症状很有迷惑性:

容器 A 用容器名访问容器 B,回 503,不是超时也不是 404。

看到 503 第一反应是「服务挂了」,实际上是请求被代理截走了——代理服务器不认识内网容器名,回了 503。

NO_PROXY 有三种常见的无效写法,全都见过:

  1. glob 通配:NO_PROXY=172.*|192.168.*|10.* —— curl 和 Python requests 都不解析这种写法,整条丢弃
  2. 只写 CIDR 不写主机名:NO_PROXY=172.16.0.0/12 —— 对 IP 地址有效,但对主机名无效
  3. 空赋值:NO_PROXY="" —— 赋值动作本身就会覆盖上层的正确值

第 2 条的根因值得单独说:Python 的 requests 不做 DNS 解析来匹配 NO_PROXY,只做字符串的 hostname.endswith(条目) 判断。主机名永远不会匹配上 CIDR。curl 会解析 DNS,所以同一个 NO_PROXY 下 curl 正常、Python 不正常,又是一个两边行为不一致的陷阱。

正确口径一句话:IP 用 CIDR 兜底,主机名逐字列出。

代码语言:ini
复制
NO_PROXY=localhost,127.0.0.1,qinglong,yyb-go,host.docker.internal,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8

验收标准也不止是「env 值对了」,要看 Python 的真实判断:

代码语言:python
复制
import urllib.request
print(urllib.request.proxy_bypass("qinglong"))   # 必须是 True
import requests
print(requests.get("http://qinglong:5700/api/system").status_code)  # 必须 200

两个都过,才算修好。

坑五:同一个脚本多份副本,你看的日志可能不是它

青龙里跑签到类脚本,同一个脚本常常存在多份副本(不同仓库目录各一份)。排查问题时你打开的日志,可能对应的是另一份禁用着的副本,你在那份里改半天当然没用。

锁定真身的办法:

代码语言:sh
复制
cat /ql/data/config/crontab.list

每行任务后面带的路径才是实际执行的文件。再拿日志目录名对一下:

代码语言:sh
复制
ls /ql/data/log/<任务log_name>/

先确认「跑的是哪一份」,再去改代码。另外 crontab.list 行首的 # 注释状态和面板里的启用状态可能不一致,以面板为准。

写在最后

回头看,这五个坑里有三个的共性是:报错信息 / 表面现象,和真实原因之间隔了一层。

  • No module named 'notify' → 不是缺模块,是路径注入被打断
  • 配置「改对了没生效」 → 改的那层不是生效的那层
  • 容器互访 503 → 不是服务问题,是代理截胡
  • curl 正常 Python 不正常 → 不是网络问题,是两家对 NO_PROXY 的解析规则不同

所以排查这类问题,我的习惯变成了:先找一条能复现任务真实环境的验证命令(比如 task 命令全链路跑一遍),再动配置。验证命令不存在的话,先造一条,再排查——不然每改一次都只能「面板点运行 + 祈祷」,排查效率天差地别。

机器还在跑,欢迎交流。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 背景
  • 坑一:No module named 'notify',第一反应 pip install 就错了
    • 根因:otask.sh 重写了 PYTHONPATH,且顺序把你自己的补丁顶掉了
    • 正确修法
    • 「裸 shell 能 import」不能作为判据
  • 坑二:配置改了没生效——同一个变量有三层,你在改的可能不是生效那层
  • 坑三:command 被镜像 entrypoint 吞掉,是死代码
  • 坑四:NO_PROXY 写法不对,容器互访被代理截走,症状是 503
  • 坑五:同一个脚本多份副本,你看的日志可能不是它
  • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档