惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Scott Helme
Scott Helme
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
P
Privacy & Cybersecurity Law Blog
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
罗磊的独立博客
MyScale Blog
MyScale Blog
博客园 - 【当耐特】
L
LangChain Blog
AWS News Blog
AWS News Blog
Security Latest
Security Latest
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Proofpoint News Feed
T
True Tiger Recordings
aimingoo的专栏
aimingoo的专栏
宝玉的分享
宝玉的分享
月光博客
月光博客
The Hacker News
The Hacker News
L
Lohrmann on Cybersecurity
The GitHub Blog
The GitHub Blog
Stack Overflow Blog
Stack Overflow Blog
S
SegmentFault 最新的问题
Recorded Future
Recorded Future
S
Security Archives - TechRepublic
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
Y
Y Combinator Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
大猫的无限游戏
大猫的无限游戏
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
博客园 - 聂微东
GbyAI
GbyAI
N
News and Events Feed by Topic
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
G
Google Developers Blog
A
About on SuperTechFans
K
Kaspersky official blog
NISL@THU
NISL@THU
S
Securelist
Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX - 技术

V2EX

[分享创造] 做了一个开源 SSH/SFTP 桌面工具,想解决人和 AI 共用远程上下文的问题 用 DeepSeek 做了个用量页面个小插件, 增加了几个维度的数据统计和计算 [体验分享] 从 TG、Signal 折腾一圈后,还是觉得 WhatsApp 最均衡 💔再见了!北京移动 18 元魔卡月底下架 [酷工作] 百度封控 / 渠道回传 / 无痕浏览 技术需求 [香港] 请问现在去香港还能开到银行卡吗 我开源了一个项目:把任何资料,安全的变成 AI 的上下文 我做了一个给超级个体 / OPC 用的 AI 智能体商业化平台 求推荐靠谱的海外 VPS [投资] 基金可以跑赢银行的贷款利率,那么基金一定靠谱么? 做了一个 Vibe coding 辅助小工具 claude|gpt 被封号的,或者还幸存的来 ai 时代的程序员 怪不得老板喜欢压迫员工,原来这么爽 [生活] 当系统判定“用户永远正确”时,老实人是不是只能认栽? [剧集] 凡人修仙传动画新年番 6 月 13 日上午 11 点开播! [推广] [追加福利!老板还没回] 偷偷再放 4 个独家 CDK, IP/500M 动态流量随缘自取,手慢无! 端午节准备去广州找个地方吃荔枝,大家有推荐的没 非招聘|佛系寻找远程同频开发者,先认识再合作 [问与答] 大伙儿推荐一个入口 IP 在境内的机场 币圈准备做复刻带单了,打不过就加入? 天塌了! Gemini 目前不支持你所在的地区。敬请期待! [问与答] 大佬们,有推荐比较好用的行李箱吗 我给 Claude Code 装了个“红绿灯”,再也不怕忘记确认状态了 Hermes Agent 通过 Webhook 收到消息后,再与用户进行交互会话,就分隔成两个会话了,丢失了上下文,如何解决? [Mac mini] QianPlayer — 给 macOS 写了一个原生视频播放器 年前决定戒烟到现在,顺便做了个小程序。 阿里百炼的自建 DeepSeek 限速是 TPM 1.2M,这限速是拍脑袋的吗? 想问一下上海拿工资的多少百分比租房? 换了个高刷 4k 显示器, c 口只有 15w [开源自荐] 悦心搜索 4.0,网盘搜索引擎,对接盘搜,快速搜索转存 [分享创造] [送会员] 搞了个专业文档转换, 翻译软件, 可一键批量翻译并保持格式,支持 PDF 等多种文档格式 原型设计是否可以直接让 AI 来做? [限时] 6 折招代理~阿里云国际|腾讯云国际 easy-tdx:接手停更的 pytdx,加了 CLI 和 30 个技术指标 [北京] 求租北京新能源指标 外包兼职(长期) [分享创造] 分享一个把微信步数变成修为的小程序:走路涨修为 求 codex、claude code 订阅账单每月$200 的,或者国内 coding 订阅,有偿 [生活] 鼻中隔偏曲术后第 9 天 Fractal Skills:给 AI Agent 一副不会过期的缰绳 vb 了一个图片工具箱,目前实现了拼图和切割图,大家看看怎么样,还花了 375 大洋买了个域名(10 年) 淘宝是不是发狂了?每天打开都要搞一个土鳖特效叫我立即领取 xx 元优惠券,实际上也没优惠什么 开启 Codex 桌宠 [上海] 上海有没有靠谱月嫂推荐? [问与答] 午休求救,要崩溃了 注册送 120 刀的周卡 分享一下我薅的站点 基于本地数据生成 ClaudeCode 热力图 小米大模型降智? 中转站免费 credit 就是电子鸡蛋 [问与答] 大家好,刚进 v 站,有没有大佬给我介绍 v 站的特色啊 阿里云 web 首页疑似会导致 Firefox 占用大量 CPU 资源 [AI Agent 智能体] 越来越怀疑,很多 Agent 现在根本进不了企业 [AI 独角兽团队] 内推直招 | 后端开发工程师,创业早期机会,升职加薪快 AI 写的代码你们是怎么保证质量的? 今天下午 Codex 每问个问题就报 429,大家都这样吗?是不是要出新模型了 [推广] 今天 pp 渠道死了,又是哀嚎遍野 claude 5 小时限额变少了,有没有同感? 安克创新咋了,一天 10 几个猎头狂推给我 Gps 坐标收藏夹 开发者平台,分成规则分享 [问与答] 麻醉是不是最接近死亡的体验? 这两天 Gemini 网页版开始胡说八道了吗? 关于海南求职的付费咨询 自建 VPS 推荐 [职场话题] 作为技术人如何和老板谈项目谈生意? claude code 工作中,切换不同中转站的 api 的不同模型,上下文记忆会丢失吗? [问与答] API 调用 chatgpt 的 这里是知乎吗? [VPS] 像搬瓦工、DMIT 等一般什么时候有优惠呢 [推广] Krill 福利加倍送,持续送,回贴就送,反正就是送~纯 pro 号池低至 0.13, image-2 免费用 , dp-v4-flash 官方 4 折! 大家拳皇和街霸玩的如何?做了一个帮助大家练招的训练工具 Crypto 交易所在线直招 实习生岗位开放 纯远程办公 BD 实习生/ 行研实习生/ 管培生 未来之星选拔计划 [华为] 大家怎么看这几天比较火的华为“韬定律芯片”逻辑折叠技术架构 [iPhone] 请问这个算是 iPhone 被 pdd 劫持了吗? 该走还是继续留 雨刮品牌-博世怎么样? [月末活动] 一个真正一目了然的自建 Codex 中转站 前端失业 2 个月了, 5 月份开始投简历,一共就约到 8 家 想做一个更轻的「友链网络」组件: LinkPals 被领导警告了 [AI Agent 智能体] 为什么我觉得 AI 真正的机会在“数字员工” 我自己感觉 codex 极大的扩展了个人的能力者不用说,但是用多了似乎也会有更多精神问题 把 10.8GB vLLM 镜像的 Pod Ready 从 4m35s 降到 14s: Hermes + SOCI lazy loading 实测 [程序员] 免费共享自己的 token 给大家一起用 极豹代理注册送 500M 动态住宅流量 静态住宅 3 刀起 [Codex] 好像没有人说 ChatGPT 账户登录的 Codex,不支持 GPT-5.3 Codex、GPT-5.4 等模型了 有没有一种工作能每天稳定收入 10-50 元 [分享创造] 谁还记得 K-MeleonCCF 网页浏览器 [分享发现] 从 0 开始 vibe coding,产品上线一个月 1500+用户,我对用户增长的一些思考 [Windows] 卡巴斯基安全软件和卡巴斯基标准版选择哪个? [程序员] 做了一个本地音频处理 + 伪知识库应用,强依赖本地 ASR 模型,这种项目开源有意义吗? [Linux] 国产 Touchpad 在 arch 下偶发无法用手势 [问与答] 有老哥用 Portainer 吗? webhook 调用成功但是没有重新部署,哪位有经验? [问与答] 现在比较可靠的国外手机卡选哪家? [路由器] MikroTik RB5009 在 2026 是否还值得入手 [硬件] 外置硬盘有什么散热的好方案,太热了 OpenCode 的压缩算法有建议的兄弟们吗?主要是写 LaTeX 论文。 今年 618 是凉了吗 现在各类所谓戒网瘾机构真的太吓人了.
坐在屏幕前有一种无力感,终于理解不得不在屎山上搭屎山的感觉了
HOMO114514 · 2025-04-15 · via V2EX

总结:我在自己的绿联 NAS 屎山上部署了我自己写的动漫爬虫,却因受到 Cloudflare 的 TOS 折磨最后把 aria2 换了 3 台宿主机重写了半个容器镜像的事

有些事情留意过我的 V 友应该有点印象,在 2 年前的今天我买了一台绿联 DX4600 Pro ,洋洋洒洒写了 4 篇文章进行了极其深度的体验和 DIY ,并且热爱看动漫的我拿这台机器架了个 Jellyfin ,还往开源项目里添了一个 HTTP 爬虫以每 30 分钟同步动画疯上游的版权动画,提交 aria 下载和刮削,这一套东西跑起来之后想追新番不再需要依赖一些广告盈利的盗版资源站。

image-20250411165253668

我们都知道绿联基于 OpenWRT 的 UGOS 系统是一坨屎,而随着绿联团队大刀阔斧推倒重做的 DXP 系列( with UGOS PRO powered by Debian )发布,这个系统的艺术性更是逼近失去了物业养护的公共厕所坑位里那一座风干了一年的草莓塔,连苍蝇蛆虫都散去,待在无人在意的角落里默默无闻,只等待那些:①像我这样因为正反馈(用户惯性)无法再找落脚点,别无选择只能在此处解决问题的人、②专门喜欢找史吃的数码时尚小垃圾爱好者发掘它,然后悄悄地恶心他们一下。

很可惜,我不仅是①,而且还蹲在上面,为这个屎山耗费了 3 天的心血为其添砖加瓦,以至于当一切都跑通了的时候,我开始怀疑自己人生的意义到底在哪里……

1 开端:《兼容性问题》则眼不见为净

刚把这套东西搭起来我还是挺喜出望外的,kubespider套件本质上就是个定时任务,跑 xml 解析+aria2 调用器,如此成熟而又稳定的架构会在哪里出现问题?

答案是绿联的系统。它的系统自己的离线下载器是用 aria2 封装的,有可能是因为 openwrt 的内核兼容,或者是上面的 nas server 在捣鬼,导致自己使用 docker 架建的 aria2 实例,会每几分钟自动重启一次。找不到任何原因,换了数个作者封装的 docker 镜像,最后自己决定去镜像里面直接 strace 发现……

它真的什么错误都没发生,什么日志都没报,只是单纯的被不知道谁的 SIGKILL 干掉了,然后被 S6 守护进程自动拉起,conf 一切正常,rpc 监听一切正常,而 SIGKILL 在残核 openwrt 的容器内根本无法追溯,工具链缺失。追溯到某个镜像作者,只提了一嘴“绿联云兼容性相关”。

在支持断点续传的 URL 上,任务会保存下载进度,所以自动重启的影响不是很大,但 ANi Open 是一个很神奇的项目,它提供的 HTTP 地址不返回文件大小(因而也不支持断点续传),属于收到 EOF 就算完结的一次性链接:

image-20250411171057573

幸而我的机场足够快,纯 IEPL 专线,大多数时候一个 1GB 以内的视频都能够在 2 分钟的重启窗口内下载完成,但总会有遇到波动的时候,如果某个时间段速度起不来,而 aria2 又在反复自动重启,它就会不断地重新开始一个任务,在硬盘上留下数十个.1 .2 .3 ....的不完整的视频,被 jellyfin 录入后表现的现象是播放到某个时间后直接结束

image-20250411171331361

当时我的手头还没有这么多装备,所以想到的临时规避方法是偶尔打开 Aria2 管理器看一眼,如果遇到大量任务正在慢速重试,就直接停止,切到电脑手动下载,同时再往系统里挂一个定时任务每 3 小时清理一下,把最大的视频文件留下来,其它的删掉

image-20250411171551873

image-20250411171854881

从此处开始,一个庞大的屎山架构已经初具雏形。

2 转折:原来 Cloudflare 防爬虫防的是我

从今年开始,我突然发现有些时候新番没有准时同步,kubespider 的日志也没有请求和解析异常,但存储池里只会蹦出来一个 stream.mp4 ,点开来一看是一段 TOS 声明:

image-20250408211211590

解决的方法也很简单,重新下载几次,直到接收到正确的文件就 OK 。

但随着时间的推移,下载被重定向到这个文件的频率越来越高,越来越高,我开始有点无法忍受,在港台日新之间来回尝试节点、询问 ANi 的管理员并配合一起调试 HTTP 请求、试着模拟 UA/仿照 PC 浏览器的 Header 塞入 refer 和 cookie 参数,发现好像无济于事:难道我的 aria2 真的很像非法爬虫吗?(好像还真是)

image-20250408213700093

直到 4 月的现在,被重定向的概率已经到了 75%,现在新建的任务我先默认被重定向失效,守着 WebUI 等着重试,但是……

这 Aria2NG 它没有对已完成的任务提供重试按钮啊?

而且如果我要每次都在大量更新的时间段蹲守 WebUI ,等着手动 copy 链接和保存地址去重建任务,是不是有点太自动化了?

3 移植:缝缝补补,筑成巨大屎山

程序间歇性重启+请求下载被 ban 的概率越来越大,这两件事组合在一起彻底冲破了我的容忍度,我决定开始寻求解决方案。

既然间歇性重启是系统问题,那好办,把 aria2 换一个地方部署就好了。彼时我手头上能够 24*7 跑的机器,除了 NAS 以外,还有一台 N100 的迷你机,它在我的家庭网络定位里只干两件事情:连 ssh ,和连 todesk 。所以加个 WSL Docker 跑个 aria2 应该还是绰绰有余的。

image-20250414100218441

说干就干,安装 docker desktop ,然后直接跑 compose 拉起容器:

---
services:
  aria2:
    container_name: aria2
    image: superng6/aria2:latest
    privileged: true
    network_mode: host
    environment:
      PGID: 0
      PUID: 0
      BTPORT: 32516
      CACHE: 16M
      FA: falloc
      PORT: 6800
      QUIET: "true"
      RUT: "true"
      S6_CMD_WAIT_FOR_SERVICES_MAXTIME: 0
      S6_STAGE2_HOOK: /docker-mods
      S6_VERBOSITY: 1
      SECRET: kubespider
      SMD: "true"
      UMASK: "000"
      UT: "true"
      VIRTUAL_ENV: /lsiopy
      WEBUI: "false"
      WEBUI_PORT: 8080
    volumes:
      - ./config:/config
      - bahamut:/downloads/TV/ANi
    restart: always

volumes:
  bahamut:
    driver_opts:
      type: "cifs"
      device: "//192.168.1.20/nas_STORAGE_公共空间/Anime_Bahamut"
      o: rw,vers=3.0,iocharset=utf8,username=***,password=********,uid=911,gid=1001,file_mode=0777,dir_mode=0777

由于 Windows 的权限管理和 Linux 有巨大的区别,所以在 Windows 上跑原本为 Linux 设计的容器,直接给 UID GID 0 ,mask 也 000 ,挂上去的 SMB 权限也直接 777 ,拉起容器之后发现 ws 端口死活不通,排查了一轮后发现容器的 init.d 启动脚本里卡在了用户权限初始化,实际上 aria2c 就一直没启动:

image-20250414102718054

查阅了 docker 相关说明,得知在 Windows 上chown不可用,索性直接把/etc/cont-init.d挂出来,把所有启动脚本从原容器里复制,接着直接把 chown 注释掉,aria2c 就能正常启动了。

接着要解决下载失效的问题,aria2c 原生支持下载完成后回调脚本,我要做的是添加对被 TOS 文件的检测,然后 curl 构造重试请求,再在 aria2.conf 里把下载完成的脚本指定为自己修改的 sh 即可:

image-20250414102915815

image-20250414111216511

重启容器,添加任务进行调试,却发现下载到 stream.mp4 时没有进行重试,log 没有脚本运行的痕迹,打开 aria2.conf 观察到on-download-complete被还原为默认的/aria2/script/complete.sh。进一步探究发现每次启动容器时/etc/cont-init.d/30-config会强制把回调脚本刷成默认:

image-20250414112828568

改脚本再启,鉴定为勉强能跑,于是这套魔改了半个容器的 Windows Docker 方案就算阶段性投产。

4 愤怒:比不稳定更难以接受的,是不可用

启完容器之后,我手动加了一个下载任务进去,结果没什么问题,于是我就安心睡去了。

第二天早上起来到工位连上一看,在深夜创建的任务全部失败:

image-20250415174135644

image-20250415174153479

查看失败的原因是 IO 相关的问题,显示的是“File Exists”,于是进容器内排查,发现 SMB 的挂载已经掉了:

image-20250415173420130

但是ll挂载目录发现目标文件夹还是在的,只不过属性和权限全部变成?,并且无法 cd 。

因为挂载行为是在 dockerd 完成的,在容器内部没法完成重新挂载的行为,想要复位 smb 必须重启容器。查了一下挂载选项试着加一些内核参数保活:

bahamut:
  driver_opts:
    type: "cifs"
    device: "//192.168.1.20/nas_STORAGE_公共空间/Anime_Bahamut"
    o: rw,vers=3.0,iocharset=utf8,username=****,password=********,uid=911,gid=1001,file_mode=0777,dir_mode=0777,hard,intr

无效。不如试试直接在容器内跑挂载,不走 docker 宿主,同时再挂个脚本检测保活

image-20250414173853755

image-20250414174101563

image-20250414174633905

搞完之后我突然有点无语,我 tm 费这劲干什么?每 2 分钟掉进程跟每几小时之后掉 SMB 没有任何本质区别,与其面对吃力的 N100 、阿三写出来的 Win11 、强兼的 Docker Desktop 三座大屎山,我还不如趁早找台 unix 机器出来把东西移到那上面去。

5 落地:杀鸡就要用宰牛刀

于是我开始寻找身边有什么 unix 系统的设备能经得起连着 WiFi 24*7 地跑一个 docker 镜像,最后把目光瞄向了正放在阳台吃灰的 Mac Mini 。

这台 Mac 是我趁着国补之风买来备用的,配置是 16+256 ,配 10G 网口,打算等到 NAS 基础设施更换后拿来做 Jellyfin 服务器,因为更新设备需要跟着家里装修设计机柜走,所以到货激活之后一直在待命消耗保修期。。

想要把 Mac 从原厂系统捣鼓成服务器 ready 需要费不少功夫,brew 的初始化极其烧脑,再加上开启 ssh 、VNC 、关闭硬盘加密、开机自动登录、部分功能配置 logind 脚本、打通 Time Machine ,一套下来够摸索一天。不过 Meta Party 和 Docker 配置起来十分方便,一旦跑通会发现所有网络请求都自然而然地通了,不需要再做更多的环境变量配置,或者去各种不同的配置文件里改镜像源( brew 除外)。

clipboard_2025-04-14_21-12

根据在 Windows 上吃亏的经验,macOS 系统中,对cont-init.d进行相应的魔改内容应该保留,同时 PGID 尽量和当前用户保持一致,而 SMB 对应用的显示权限调整成 s6 的 abc 比较合适:

---
services:
  aria2:
    container_name: aria2
    image: superng6/aria2:latest
    privileged: true
    network_mode: host
    environment:
      PGID: 501
      PUID: 20
      TZ: Asia/Shanghai
      BTPORT: 32516
      CACHE: 16M
      PORT: 6800
      QUIET: "true"
      RUT: "true"
      SECRET: kubespider
      SMD: "true"
      UMASK: "000"
      UT: "false"
      WEBUI: "false"
      WEBUI_PORT: 8080
    volumes:
      - /Users/****/Appdata/aria2/start:/etc/services.d/aria2/run
      - /Users/****/Appdata/aria2/config:/config
      - bahamut:/downloads/TV/ANi
      - /Users/****/Appdata/aria2/cont-init.d:/etc/cont-init.d
    restart: always

volumes:
  bahamut:
    driver_opts:
      type: "cifs"
      device: "//192.168.1.20/nas_STORAGE_公共空间/Anime_Bahamut"
      o: rw,vers=3.0,iocharset=utf8,username=****,password=********,uid=911,gid=1001,file_mode=0777,dir_mode=0777,hard,intr

一番倒腾,aria2 终于趋于稳定,试点运行了 3 天后没有任何进程退出、SMB 掉挂载的问题,就是我发现下载完成回调脚本没有起到任何作用,它的 curl 重试操作实际上一直是失败的。在 shell 进行请求文本处理,特殊符号极其容易受到 shell 语言的影响导致实际传达的请求被转移后变成非常混乱的东西。于是我决定捡起我最擅长的语言——Python 。

在这几天密切跟踪 Aria2 的经历中,我发现一款安卓的 APP Aria2App对任务提供了重试功能,而且很惊喜地是这个 app 是开源的,所以我从源代码中直接找到了它对重试任务的实现方法

image-20250415114220104

image-20250414214743922

我没有任何的 Java 经验,但能够勉强理解它在做什么:从旧任务的 GID 获取它的 options ,提取有效的 URL ,再把 URL 和 options 原封不动地送到新建任务的 API ,顺道再删除旧的 GID 。用 Python 实现类似如下:

import requests
import json
import argparse
import sys

def parse_arguments():
    """解析命令行参数"""
    parser = argparse.ArgumentParser(
        description="Aria2 任务重启工具",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    
    parser.add_argument(
        "-g", "--gid",
        required=True, type=str, help="要重启的任务 GID (必需)"
    )
    parser.add_argument(
        "-t", "--token",
        required=True, type=str, help="aria2 RPC 密钥 (必需)"
    )
    parser.add_argument(
        "-a", "--api-url",
        type=str, default="http://localhost:6800/jsonrpc", help="aria2 RPC 地址 (默认: %(default)s)"
    )
    
    return parser.parse_args()

def restart_download(gid, token, api_url):
    tell_status_params = {
        "jsonrpc": "2.0",
        "method": "aria2.tellStatus",
        "id": "restart",
        "params": [f"token:{token}", gid]
    }
    response = requests.post(api_url, json=tell_status_params)
    download_status = response.json()["result"]
    
    get_options_params = {
        "jsonrpc": "2.0",
        "method": "aria2.getOption",
        "id": "restart",
        "params": [f"token:{token}", gid]
    }
    response = requests.post(api_url, json=get_options_params)
    old_options = response.json()["result"]
    
    # 收集所有使用的 URLs
    new_urls = set()
    for file in download_status.get("files", []):
        for uri in file.get("uris", []):
            if uri.get("status") == "used":
                new_urls.add(uri.get("uri"))
    new_urls_list = list(new_urls)
    
    add_uri_params = {
        "jsonrpc": "2.0",
        "method": "aria2.addUri",
        "id": "restart",
        "params": [f"token:{token}", new_urls_list, old_options]
    }
    response = requests.post(api_url, json=add_uri_params)
    new_gid = response.json()["result"]
    
    remove_result_params = {
        "jsonrpc": "2.0",
        "method": "aria2.removeDownloadResult",
        "id": "restart",
        "params": [f"token:{token}", gid]
    }
    requests.post(api_url, json=remove_result_params)
    
    return new_gid

if __name__ == "__main__":
    try:
        args = parse_arguments()
        new_gid = restart_download(
            gid=args.gid,
            token=args.token,
            api_url=args.api_url
        )
        print(f"任务重启成功,新 GID: {new_gid}")
    except requests.exceptions.RequestException as e:
        print(f"网络请求失败: {str(e)}", file=sys.stderr)
        sys.exit(1)
    except KeyError as e:
        print(f"响应数据解析失败,缺失字段: {str(e)}", file=sys.stderr)
        sys.exit(2)
    except Exception as e:
        print(f"未知错误: {str(e)}", file=sys.stderr)
        sys.exit(3)%    

但是 aria2 使用的轻量基底镜像 Alpine 默认是不携带 Python 环境的,怎么办?

为了保持镜像本身的轻量化,同时降低运维复杂度,我决定不在原 Aria2 镜像中添加 python ,而是在同样的基底镜像里把这个 python 脚本编译成单个 executable 。

说干就干,写个 Dockerfile:

FROM superng6/alpine:3.21 AS builder
FROM superng6/alpine:3.21
LABEL maintainer="Homo"

ENV TZ=Asia/Shanghai PUID=0 PGID=0 UMASK=000

RUN mkdir /pythonenv
WORKDIR /pythonenv
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories && \
    apk add --no-cache python3 py3-pip gcc chrpath python3-dev build-base patchelf && \
    python3 -m venv .venv && \
    source .venv/bin/activate && \
    echo "source /pythonenv/.venv/bin/activate" >> /root/.bashrc && \
    echo "alias ll='ls -al'" >> /root/.bashrc && \
    pip3 config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple && \
    python3 -m pip install nuitka requests

VOLUME /workspace
WORKDIR /workspace

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

entrypoint

#!/bin/bash
set -e
if [ -f /workspace/requirements.txt ]; then
    echo "requirements.txt dected, installing dependencies..."
    source /pythonenv/.venv/bin/activate
    pip install -r /workspace/requirements.txt
fi

echo "Start your building simply with the command below:"
echo "python3 -m nuitka <your_options> <your_script.py>"
exec bash

build 后起容器,把 python 脚本挂在/workspace下,通过--onefile等参数最后编译出一个 12MB 的 python 单执行文件,在complete.sh下载完成回调中这样使用:

image-20250414222810911

#!/usr/bin/env bash

. "/aria2/script/setting"
. "/aria2/script/core"
. "/aria2/script/rpc_info"

TASK_GID=$1
FILE_NUM=$2
FILE_PATH=$3

MAX_RETRY=5
RETRY_FILE="/tmp/aria2_retry_${TASK_GID}.count"

GET_BASE_PATH
COMPLETED_PATH
GET_RPC_INFO
GET_FINAL_PATH

# 定位最终文件路径
if [ "${FILE_NUM}" -eq 1 ]; then
    final_path="${SOURCE_PATH}"
else
    if [ -n "${COMPLETED_DIR}" ]; then
        final_path="${COMPLETED_DIR}"
    else
        final_path="${TARGET_DIR}/${TASK_NAME}"
    fi
fi

# 提取最终文件名
final_name=$(basename "${final_path}")
file_md5=$(md5sum "${final_path}" 2>/dev/null | awk '{print $1}')
invalid_md5=5b16dfbf8e5d3edc9242b20b280500c3 # Cloudflare TOS 返回的视频 md5

delete_old_file() {
    if [ -e "${final_path}" ]; then
        echo -e "$(DATE_TIME) ${INFO} GID:${TASK_GID} 删除目标: ${final_path}"
        rm -rf "${final_path}"
    else
        echo -e "$(DATE_TIME) ${WARNING} GID:${TASK_GID} 目标不存在: ${final_path}"
    fi
}

if [ "${FILE_NUM}" -eq 0 ] || [ -z "${FILE_PATH}" ]; then
    exit 0
elif [ "${GET_PATH_INFO}" = "error" ]; then
    echo -e "$(DATE_TIME) ${ERROR} GID:${TASK_GID} 路径获取错误!"
    exit 1
else
    MOVE_FILE
    CHECK_TORRENT

    current_retry=$(cat "${RETRY_FILE}" 2>/dev/null || echo 0)

    # 判断是否为 stream.mp4 并且未达到最大重试次数
    if [[ "${final_name}" == "stream.mp4" || "$(basename "${FILE_PATH}")" == "stream.mp4" || "${file_md5}" == "${invalid_md5}" ]] && \
       [ "${current_retry}" -lt "${MAX_RETRY}" ]; then
        echo -e "$(DATE_TIME) ${WARNING} GID:${TASK_GID} 检测到 TOS ,10 秒后重试 ($((current_retry+1))/${MAX_RETRY})"
        echo $((current_retry+1)) > "${RETRY_FILE}"
        sleep 10
        delete_old_file
        /config/retry --gid "${TASK_GID}" --token "${SECRET}" --api-url "http://${RPC_ADDRESS}"
        exit $?
    else
        rm -f "${RETRY_FILE}"
    fi
fi

exit 0

至此一个完美的、支持自动重试的、不会宕机的 Aria2 终于调教完成,投入使用,现在一个只插着电的无头 Mac Mini 静静地放在我的 PC 上享受风冷散热,承载我 NAS 里最新的追番使命。

IMG_20250411_222617

6 戏剧:直面一切 3 天的努力,不如 3 分钟绕过它

然后我本以为一切都应该结束了,结果第二天起来一看我的机场 IP 仿佛被彻底拉黑了一样,所有的任务都陷入了无限重试:

image-20250414223808473

不得已直接停下了容器,再次研究对 ANi Open 服务器的请求,在我百思不得其解之时,突然注意到走电脑直接下载视频文件时,有概率被重定向到另一个域名:

image-20250414224147699

接下来尝试把所有下载域名的请求都替换成这个域名,什么 Header 、UA 、Refer 全都不用动,结果是不再出现拦截现象,所有下载任务都顺利执行。那我直接在解析器里替换域名不就好了?

image-20250415114517852

Cloudflare 拦截问题迎刃而解,之前做的所有努力都化为泡影,下载器转入稳定运行……

image-20250415114635648

在难绷人生的时间被浪费之余还有点想笑,唯一的收获就是大致学会了怎么写 Dockerfile build 一个镜像,捉瞎还是实力不够的证明,继续精进吧。