news 2026/10/22 2:21:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
青龙面板踩坑实录:No module named ‘notify‘ 的真正原因,和「改了没生效」的三层陷阱

背景

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

项情况
宿主机CM311-1A 电视盒子,Amlogic S905L3A,2G 内存
系统Armbian(Debian 系)
青龙Docker 部署,镜像自带 otask.sh / task 那套启动链
脚本JS 与 Python 混用,Python 脚本依赖仓库里的 notify.py 等公共模块
通知Bark 推送

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

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

报错长这样:

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任务有类似这样的逻辑:

exportPYTHONPATH="${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,会在任务前被调用。往里写:

importsys 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 改写;面板任务走的路径完全不同。唯一可信的验证方式是模拟面板的调用链:

dockerexecqinglongbash-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 里还有一行旧的,任务实际用的是旧的。

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

dockerexecqinglongbash-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环境变量,指向仓库目录里的一个文件:

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 兜底,主机名逐字列出。

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 的真实判断:

importurllib.requestprint(urllib.request.proxy_bypass("qinglong"))# 必须是 Trueimportrequestsprint(requests.get("http://qinglong:5700/api/system").status_code)# 必须 200

两个都过,才算修好。

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

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

锁定真身的办法:

cat/ql/data/config/crontab.list

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

ls/ql/data/log/<任务log_name>/

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

写在最后

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

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

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

机器还在跑,欢迎交流。


本文同步发布于 xiaoyiweb.xyz,青龙面板的其他踩坑记录和这台 2G 盒子的完整部署手册都在站内。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 2:21:10

高温压力化成设备技术解析:温控、压力控制与工艺验证

1. 工艺概念高温压力化成通常是指将化成充放电程序与特定温度、压力条件结合起来执行的一类工艺配置。温度可能影响电芯内部的电化学反应和界面反应过程&#xff0c;压力则可能影响电芯的机械状态和受力情况。但具体作用与电芯材料体系、结构设计和工艺目标有关&#xff0c;不能…

作者头像 李华
网站建设 2026/10/22 2:20:26

95 跨进程抢资源怎么锁?这个Java Redis分布式锁工具类有8个方法

&#x1f4cc; 人工智能&#xff1a; 「AI大模型」AI数据分析助手保姆级文档 &#x1f4cb; 目录 一、工具类功能二、所需环境三、方法与调用示例&#xff08;按需调用&#xff09;四、工具类源码&#xff08;可直接复制&#xff09; 一、工具类功能 加锁走 SET key token …

作者头像 李华
网站建设 2026/10/22 2:20:24

HTTP协议安全入门:从请求走私到CRLF

一个让我加了两天班的“小问题”上个月排查一个线上问题&#xff0c;前端网关日志显示请求正常&#xff0c;后端服务却返回了不该返回的数据。查了两天&#xff0c;最后发现是一个被中间代理“吞掉”的请求边界问题——说白了&#xff0c;就是请求走私的变种。 这件事之后我把H…

作者头像 李华
网站建设 2026/10/22 2:17:53

品牌战略咨询公司怎么收费该怎么选?价格区间与交付内容一次说清

品牌战略咨询公司怎么收费、怎么选&#xff1f;四个价格区间与交付内容品牌战略咨询的收费&#xff0c;一直是企业在选择阶段最先问、也最难问清的问题。公开渠道上能查到的报价区间跨度很大&#xff0c;从每年几百万到每年数千万都有。差异来自服务范围、团队配置、项目周期与…

作者头像 李华
网站建设 2026/10/22 2:17:41

FDE模式深度解析:从Palantir到AI,小白程序员必备收藏帖!

本文深入剖析了FDE&#xff08;前沿部署工程&#xff09;模式的历史渊源、核心特征及其在AI时代的变革。从Palantir的案例出发&#xff0c;阐述了FDE如何通过嵌入式现场解决实际问题&#xff0c;并反哺平台。同时&#xff0c;文章揭示了当前FDE市场的真实状况&#xff0c;包括薪…

作者头像 李华
网站建设 2026/10/22 2:16:56

Redis 的原理和使用(6)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华