Skip to content

v2.2.1

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Oct 11:14
· 13 commits to main since this release

v2.2.1

修剪贴板监控——macOS 和 Windows 上它从来就没工作过。用户 2026-10-28
实测发现的:两个平台都复现。

macOS:AppleScript 写错了,读取每轮都失败

代码写的是 osascript -e "pbpaste"。但 pbpaste 是 shell 命令,
不是 AppleScript 命令——AppleScript 解释器把它当成一个变量名,报

0:7: execution error: 变量"pbpaste"没有定义。 (-2753)

退出码非 0 → 读取函数返回 (None, -1) → 每一轮都失败。也就是说
「剪贴板链接自动识别」这个功能从上线到现在一次都没工作过。

改成直接调 /usr/bin/pbpaste:输出纯文本,剪贴板里是文件时输出空、
退出码 0(不会误报),而且不用每 0.5 秒起一个 AppleScript 解释器。
osascript -e "the clipboard" 留作兜底(那是 AppleScript 里读剪贴板的
正确写法,the clipboard 是 Standard Additions 的属性)。

Windows:句柄被截断,读出来是空的

GetClipboardData 返回的是 HANDLE(64 位)。代码没声明 restype,
而 ctypes 在没声明时默认按 c_int 返回——64 位 Windows 上句柄被截成
32 位
。低 32 位最高位是 1 的时候(现代 Windows 的句柄值经常落在
这个区间),ctypes.c_void_p(handle) 会把这个负数补成
0xFFFFFFFFxxxxxxxx 的错指针,GlobalLock 拿不到数据 → 读出来是空的。

现在给所有 Win32 调用都声明了 argtypes / restype,
GetClipboardData 和 GlobalLock 的 restype 是 wintypes.HANDLE。

开关打开后线程压根没起

监听线程之前是「启动那一刻设置已经开着才起」。于是:

  • 用户从设置页把开关打开 → 设置写对了,线程没跑 → 没反应
  • 用户从 Windows 托盘把开关打开 → 同上,而且托盘那条路连
    start() 都不调

现在线程无条件常驻,「开不开」由它每轮自己读设置决定
(_capture_enabled 本来就是这么设计的,只是从来没人保证线程在跑)。
任何入口改开关,下一轮就生效。

三个入口统一走一个出口

菜单栏、托盘、设置页之前各写各的设置键——写的是同一个键倒没错,但
只有设置页那条会推 settings_updated,菜单栏和托盘勾完之后,
设置页上的开关还是老样子,用户以为没生效。现在三个入口都走
GuiBridge.set_clipboard_monitor():写设置 + 推事件 + 状态一致。

读失败不再静默

之前读失败只有一条 debug 级日志,而 Windows 用户默认日志级别是
warning——那条根本看不到,「剪贴板监控用不了」在用户那边是一个完全
无从查起的问题。现在失败是 warning 级(30 秒节流,不刷屏)。

设置页补了一条说明

「剪贴板链接自动识别」旁边加了一句:只有扩展名能识别成文件类型
(zip / dmg / mp4 …)的链接才会提示,聊天里的普通链接不会打扰你。
这是 Swift v1.0.7 起就有的过滤规则,但从来没在界面上说过。


v2.2.0

浏览器下载捕获——Chrome / Edge / Firefox / Safari 四个浏览器的下载
可以交给本程序接管。

之前这套东西其实写好了(契约 §4 那一整套:协议、原生宿主、回环通道、
四个扩展目录都在仓库里),但用户没法用:扩展只躺在源码仓库里,
应用里没有任何地方告诉用户「扩展在哪、装没装、token 是多少」——
连 Safari 那份连配对入口都没有。这一版把缺的那一段补齐。

设置页新增「浏览器接管」板块

打开「设置 → 浏览器接管」,能看到:

  • 配对 token:可复制、可重新生成(重新生成 = 旧 token 立刻作废,
    拿着旧 token 的扩展再也派不了单)
  • 每个浏览器的扩展目录:点「打开扩展目录」直接在访达/资源管理器里打开
  • 每个浏览器装没装:应用自己扫扩展目录、查 pluginkit 得到的真实状态
  • 每个浏览器的安装入口:点「打开扩展管理页」直接跳到浏览器的扩展页

扩展文件现在跟着应用一起打包(macOS 在 Contents/Resources/extensions/,
Windows 在 onefile 的 sys._MEIPASS/extensions/),不用再去源码仓库里找。

Safari:走官方的「未签名的临时扩展」路径

本项目没有 Apple Developer Program 开发者签名,所以 Safari 扩展
不签名,走 Apple 官方给未签名扩展留的路子(macOS 17 / Safari 17 起):

设置 → 高级 → 「显示网页开发者功能」→ 设置 → 开发者 → 「允许未签名的扩展」
→ 设置 → 开发者 → 「添加临时扩展…」→ 选扩展目录 → 设置 → 扩展里勾选启用。

不需要 Xcode,不需要开发者账号。

平台的硬限制如实标在界面和文档里:退出 Safari 会重置「允许未签名的扩展」,
临时扩展最长 24 小时。Safari 也只能手动派单——它没有
downloads.onDeterminingFilename 那种「下载开始前拦一道」的钩子,
硬做只能等下载开始后再取消,那时候文件已经在路上,用户会丢文件。

Safari 还必须手动粘一次 token:Safari 没有 Chrome 那种原生消息宿主,
只能走回环 HTTP 通道——而那个端口带着 Access-Control-Allow-Origin: *,
不能在上面开 /pair(那等于把 token 贴给任何网页,等于打开
「谁都能往下载器塞任务」的门)。扩展的选项页和弹窗里都加了 token 输入框。

Chrome / Edge:把扩展 ID 钉死

宿主清单里的 allowed_origins 必须跟扩展的 origin 精确一致,否则
Chrome 直接拒绝拉起宿主。而「加载已解压的扩展程序」装出来的扩展,
ID 默认按目录路径算哈希——换个机器、换个目录就变了,清单永远对不上。

所以 Chrome / Edge 的 manifest 里写死了一把公钥(key 字段),扩展 ID
由公钥算出来,恒定为 bangcjehjdodcpfekmnkfmbeffcclpih(Chrome 和 Edge 同一个)。
Firefox 不需要这一步:它的扩展 ID 是 manifest 里
browser_specific_settings.gecko.id 自己定的。

升级扩展时不要换这把公钥——一换 ID 就变了,所有装好的宿主清单全部失效。
有一条测试专门盯着「manifest 里的 key 算出来的 ID」和「代码里写死的 ID」
是否一致。

其他

  • Safari 扩展补了选项页(原来只有弹窗),版本号四个扩展统一到 2.2.0
  • 扩展 manifest 加了 options_ui,Chrome/Firefox 也能从扩展详情页进选项页

v2.1.3

修一个真正的架构 bug(下载任务根本跑不起来),再加上自动分类、全局 UA,
以及去掉手动分片数。

下载一直卡住、0 字节 —— 根因是事件循环被抹掉了

上一版把「卡在 0 KB/s」当成网络问题修了一轮(探测超时、重试预算)。
这次拿到进程内的 Python 栈才看清:下载任务压根没跑起来。

GuiBridge.set_window() 里有这么一段:从当前线程猜一个正在跑的
asyncio 循环,猜不到就把 _loop 置成 None。而窗口是在主线程里建的,
主线程上根本没有 asyncio 循环(引擎的循环在 sdm-engine 线程里)——
于是 set_window 把 set_loop() 刚设好的引擎循环抹成了 None。

_loop 是 None 之后,桥接层所有调用退化成 asyncio.run(coro):
一次性循环。engine.start() 在这个一次性循环里把下载任务
create_task 出去,asyncio.run 一返回就拆循环,任务在跑第一行之前
就被取消。结果就是:任务躺在列表里永远不动、0 字节、连一个 socket
都没建过(lsof 上看不到任何对外连接)。暂停/继续/取消也都打在错误的
循环上,所以怎么点都没反应。添加框里的「解析」能成功,是因为它自己
开循环同步取结果,不依赖引擎循环。

修法:

  • set_window() 不再碰 _loop——引擎循环只有一个来源:set_loop()
  • main() 里 set_loop() 挪到 set_window() 之后
  • 桥接层加了兜底:真的没设上循环时,自己起一个常驻的后台循环,
    绝不再退化成一次性的 asyncio.run
  • 连接池等待加了超时上限(pool=None 的意思是「等连接池空位时永不
    超时」,池满了就永久挂起)

自动分类

  • 分类下拉第一个选项是「自动」并且默认选中。早先前端把第一个选项
    (视频)当成分类发给后端,后端再覆盖掉引擎按 URL 猜出来的正确分类
    ——用户加一个 .dmg,列表里却归在「视频」。现在「自动」传的是 null,
    由引擎判断
  • 按服务器给的文件名判分类:URL 常常是无扩展名的 CDN 路径
    (/download/12345),真正的文件名在 Content-Disposition 里。
    探测成功后按真实文件名回填分类(.dmg → 程序),保存目录也跟着换
  • 用户手动改过分类就不再自动改

全局 UA

  • 设置页新增「全局 User-Agent」:留空 = 用程序内置的默认值,
    随时可以改,改完立刻对后续所有下载生效
  • 优先级:站点规则里的 UA → 全局 UA → 内置默认 UA
  • 清空输入框是「恢复默认」,不是「发一个空的 UA」

去掉手动分片数

添加对话框里那个默认 8 的分片数输入框没有了。分片本来就是引擎动态
规划的(起始连接数按文件大小和实测速度自己长),挂一个数字框只会
让人以为那个数字决定实际分片数。

其它

  • 探测失败的日志从 notice 提到 warning——用户把日志级别调到 warning
    时,那恰恰是排查「卡在连接中」最关键的一条线索
  • 引擎探测之后会按真实文件名修正任务的分类(用户没手动选过时)

v2.1.2

修两件事:下载一直卡在 0 KB/s,以及下载列表的界面错位。

「0 KB/s」那条查出来是三件事叠在一起:探测探了两次、探测用的超时是
分片那个 60 秒的看门狗、重试预算只算退避不算请求本身的时间。三条
一起,服务器慢的时候就是几分钟的静默——界面全程显示 0 KB/s,
还没有报错。界面错位那条更严重:列表的 grid 模板压根没生效。

界面

  • 下载列表的列布局修好了:表头和数据行用的是同一套 grid 模板
    (--row-cols,名字列是 1fr),但这个变量被写在了列表容器上,
    而它里面的 var(--w-*) 只定义在行和表头上——CSS 自定义属性是在
    声明它的那个元素上做替换的,容器上找不到 --w-name,整个模板
    就成了无效值,grid-template-columns 退回 none,所有列竖着
    叠起来
    (实测一行 281px 高,八个格子竖排)。现在模板写在用它的
    元素上,列宽和窄屏自适应一起恢复
  • 表头和数据行不再差 36px:列选择器按钮原来跟表头并排(flex),
    把表头挤窄了 36px,名字之后的每一列都整体错开。现在按钮浮在表头
    右端,表头拿回满宽
  • 数字列右对齐:表头的「大小 / 速度 / 剩余时间」是右对齐的,行里
    的数字却是左对齐的,标签和数字永远对不上
  • 连接阶段显示「连接中」:从任务开跑到收到第一个字节之间不再显示
    「0 KB/s」——那段时间确实一个字节都没拿到,但「还没开始」和「卡死
    了」是两回事
  • 顺手修掉 i18n 里 7 处乱码文案(「SHA-256:(0)」这类被二次编码
    过的字符串)

引擎

  • 探测不再探两次:引擎探一次拿真实文件名和总长度,下载流程又探
    一次——两个 RTT 的纯等待。现在探一次的结果直接交给流程层
  • 探测有自己的短超时:探测以前借用分片的 60 秒 read 超时(那是给
    慢连接用的空闲看门狗),服务器不回响应头时一次探测就静默 60 秒,
    重试几轮就是好几分钟。现在探测用 15 秒的独立超时
  • 重试预算真的约束总时间:预算以前只统计退避的 sleep,不统计请求
    本身耗掉的时间——服务器不响应时预算永远花不出去。现在请求耗时也
    计入预算,最长二十几秒就会给出明确的失败
  • 探测失败给一句人话:以前界面上显示的是 ReadTimeout 这种英文类名,
    现在是「连接服务器超时 / 连不上服务器 / 网络错误」

验证

  • 引擎侧新增 tests/test_connect_phase.py(探测预算、不重复探测、连接
    态生命周期),前端新增 tests/test_connecting_js.mjs 和
    tests/test_column_alignment_js.mjs
  • 列对齐用无头浏览器量像素验的(Chromium + WebKit 都跑过):修复
    前一行 281px 高、八格竖排;修复后行高 56px,表头和数据行的 grid
    轨道完全一致

已知限制

  • 仍不支持 BT / 磁力
  • 本版仍未经过大规模手测,按惯例发 Pre-release

v2.1.1

修三个用户实际撞上的问题:改设置后页面总跳回顶部、开启开机自启会立刻多出一个进程、
解析链接一直在报「所有重试都失败」。顺带修好进度条最后一段经常少报一截的问题。

界面

  • 改设置不再跳回页面顶部:改任何设置都会重画整个设置页,而重画会把滚动位置一起
    冲掉——用户改个设置就被弹回顶部。现在重画前记下位置、重画后放回去
  • 任务被删掉导致列表重画时,设置页的滚动位置同样不再丢

macOS

  • 开启开机自启不再立刻多出一个 SDM 进程:之前写完 plist 还顺手跑了一次
    launchctl load,而 load 不只是「注册」,它当场就把任务跑起来了。自启的意思是
    「下次登录时启动」,不是「现在启动」。现在只写 plist,登录时 launchd 自己会扫

引擎

  • 磁力链接这类地址立刻失败并说清楚:磁力链接(magnet: 开头)以前会被当成网络
    错误重试 5 次,最后报一句「所有重试都失败」——跟真实原因完全对不上。现在直接告诉你
    「这是磁力链接(BT),SDM 只下载 HTTP(S) 的普通文件,不支持 BT/磁力」
  • 不可重试的失败不再重试:404(文件不存在)、416(服务器不认这个 Range)这类重试
    多少次结果都一样,现在一次就报错,不再白等 7.5 秒
  • 两处错误分类对齐:新建任务对话框和任务列表现在用同一份分类。磁力链接在两边都
    归到「客户端错误」,不再在任务列表里显示成「网络连接中断,将自动重试」还标着可
    重试;404 归到「文件不存在」;429(被限流)归到「可重试」
  • 进度条最后一段不再少报:下载快结束时进度会停在 98.97% 再也不动(2 MB 的文件
    最终报成 2031616,正好少一个 64 KB 块)。根因是进度合并窗口到期时发的是上一个
    窗口攒的旧值,把最后那次上报直接抹掉了
  • 续传时进度不再倒退一次:续传的汇总基数没算盘上已有的字节,第一次上报会比实际
    少,进度条会往回跳一下,紧接着算出来的速度也是个巨大的假值

已知限制

  • 仍不支持 BT / 磁力(要做磁力得整个塞一个 BT 客户端进来,不在当前范围内);现在遇到
    这类地址会立刻说清楚原因,而不是重试到天荒地老
  • 本版仍未经过大规模手测,按惯例发 Pre-release

v2.1.0

这一版按 DeepSeek 规格做了整轮补齐,重点在引擎和错误处理——这两块是真正
的壁垒,其余是体验放大器。

引擎

  • 动态分段:分片布局在下载过程中会长大,不再是开跑前一次性切死。从 2 条
    连接起步,有实打实的进度才逐级放开到设定上限;单个分片跑到一半会被拆成两半,
    拆出来的另一半另开连接继续拉
  • 无 Range 自动回退:服务器对 Range 请求返回 200 而不是 206 时,自动退化成
    单流下载,不再把整份文件当成分片数据写坏
  • 续传校验:会话文件里记了 ETag / Last-Modified / Content-Length,资源变了
    就不再接着旧进度写,任务转成「资源已变更」而不是悄悄写出一个坏文件
  • 会话落盘:分片布局写成 <文件名>.sdm.session.json,崩溃或被杀之后能按原
    布局接着下
  • 进度报增量:文件级进度是所有分片增量之和。修好了进度虚高 2.5 倍的 bug
    (2 MB 的文件一度报成 5 MB)
  • 代理:支持 http / https 原生代理,SOCKS4 / SOCKS5 走 httpx-socks
  • 校验和:可选 md5 / sha1 / sha256,完成后自动算出校验值
  • 完成后行为:打开文件、打开文件夹、跑脚本、休眠,可按任务设置
  • 磁盘空间预检:开跑前检查剩余空间,不够就直接失败,不下到一半才发现
  • 名字冲突:默认保留两者(foo (1).zip),覆盖要走「移到废纸篓」,不会
    静默删掉用户已有文件
  • 任务队列:同时活跃的任务数有上限(默认 5),超出的排队,可置顶置底
  • 重试与改选项:失败任务可重试,代理 / 校验算法 / 完成后行为可以中途改

错误处理

  • 12 类结构化错误分类:网络、磁盘、权限、空间不足、认证、链接过期、服务器、
    限流、协议、不支持、用户取消、未知
  • 每类错误都带能不能自动重试和用户该做什么的中文说明
  • HTTP 状态码自动归类,403 / 410 / 451 归到「链接失效」,任务转「等待新链接」
  • 尊重 Retry-After,重试用指数退避 + 抖动,最多 5 次
  • 错误详情可以复制出来,粘到反馈里能看懂

浏览器接管

  • Native Messaging 宿主:Chrome / Edge / Firefox / Safari 扩展通过原生消息
    把下载派给应用,自动带上页面的 Cookie、User-Agent、Referer
  • 配对令牌:扩展第一次连接要跟应用配对,令牌存在本地,回环 HTTP 通道强制
    校验
  • Chrome / Edge 扩展重写:取消浏览器自带的下载,接管文件名、按分类分目录
  • Firefox 修好:以前把文件名当 URL 发出去,压根下不动
  • Safari 改成纯手动:右键菜单 + 工具栏按钮,不做自动拦截

界面

  • 底部状态栏:全局速度、限速、活跃任务数
  • 分类列 + 列显隐选择器
  • 新建任务对话框重写:多行 URL、保存目录、分类、代理、请求头键值行、分块数、
    开始时机(立即 / 排队 / 定时),带解析状态和重新解析
  • 详情面板:分片可视化 + 最近 60 秒的速度曲线
  • 浅色 / 深色主题切换,跟随系统
  • 设置里补齐 7 个新键(并发任务数、主题、代理、配对令牌、初始连接数、分片上限等)

macOS / Windows 系统集成

  • 菜单栏状态行:实时显示「下载中 N 个 · 速度」,1.5 秒刷一次;带暂停全部、
    继续全部、剪贴板监控勾选
  • Dock 图标显示整体下载进度
  • AppleScript 词典:可在 Script Editor 里 add download、pause all downloads、
    resume all downloads、get download list
  • Windows 托盘图标 + 与 macOS 一致的菜单
  • Windows URL Scheme 注册表项

打包

  • 新增 sdmd-native-host 产物,浏览器按清单拉起它
  • macOS 首次启动自动装原生消息宿主清单,Windows 解包后自动装
  • AppleScript 词典打进 .app,Info.plist 里写 OSAScriptingDefinition

已知限制

  • Safari 只做手动接管,不做自动拦截
  • 不支持 BT / 磁力
  • 没有 App Intents 原生集成
  • SOCKS 代理需要额外装 httpx-socks
  • 本版仍未经过大规模手测,按惯例发 Pre-release

v2.0.6

  • 下载列表可以按文件名、大小、速度、状态排序,未知大小的任务排在最后
  • 速度显示改成平滑曲线,不再忽快忽慢;任务结束或暂停时速度归零
  • 下载完成后自动写进历史记录
  • 日志每天自动清理一次,改动日志设置时也会立即清理
  • 设置里可以直接打开 macOS 的 Console.app 看日志
  • 下载完成和失败时发系统通知;应用在前台时也弹横幅;点通知会把主窗口拉到前面
  • 站点规则对探测请求也生效,需要 Referer 或特定 UA 的站点不再一启动就失败
  • 命令行和后台守护支持用环境变量指定 socket 路径
  • 后台守护必须由系统服务启动,手动运行会给出提示而不是挂在前台
  • 剪贴板候选弹窗可以随时停止监听;已经在下载队列里的链接不再重复提示
  • 关闭自动分类后,所有下载落进下载目录根目录
  • 关掉主窗口后应用继续在菜单栏待着,可从菜单栏唤回
  • 关闭窗口时按设置隐藏 Dock 图标,从菜单栏唤回时恢复
  • 启动时自动同步开机自启状态
  • 带宽限制输入框的单位改成 KB/s,之前填的数值实际是按字节算的
  • 支持 superdownloadmanager:// 链接唤起下载

v2.0.5

  • 修好分片计数:详情页的「已完成分片 / 总片数」以前一直是 0/0,现在正确显示
  • 最后一个分片完成后状态不再停留在未完成
  • 已完成的任务也能查看分片明细
  • 修好 socket 路径过长时 daemon 起不来:路径超过系统上限自动改用短路径
  • 打包时校验应用图标,没打进 bundle 直接让构建失败

v2.0.4

  • 详情页分片表可以展开看全部,默认仍只列前 64 个
  • 换任务时自动收起

v2.0.3

  • 修好应用图标:之前打包没传图标参数,访达和 Dock 里显示的是打包工具的默认图标
  • macOS 菜单栏常驻图标,菜单项跟桌面版一致:打开主窗口、加 URL、在访达显示下载目录、关于、设置、退出
  • Windows 也用同一个图标

v2.0.2

  • 一个包就够:命令行和后台守护都装在 app 里,首次启动自动装好
  • macOS 把 sdm 软链到 PATH 并注册开机自启
  • Windows 首次运行时释放命令行和守护进程,加进 PATH 并注册计划任务
  • macOS DMG 生成(build.py --mac)

v2.0.1

Python 重写版。跨 macOS、Windows、Linux,界面换成液态玻璃。

  • 引擎改用 httpx 真流式传输,字节到达即落盘
  • 分片下载默认 32 段,上限 128
  • 断点续传按已落盘字节继续
  • 链接过期(403/410/451)时任务转「等待新链接」,换新 URL 后从已完成分段续传
  • 全局限速、单任务限速、时间段限速规则
  • 自动文件分类,无扩展名文件按 magic byte 识别
  • 站点规则支持 Referer、User-Agent、Cookie、Basic Auth
  • 下载历史、计划任务、站点规则、日志、通知
  • 剪贴板候选确认、浏览器扩展派单
  • sdm 命令行,每个命令返回 JSON
  • 后台 daemon,开机自启
  • 窗口自由缩放,布局重排不裁切
  • 详情页显示真实分片状态
  • macOS DMG 生成(build.py --mac)

Windows 22H2 与 macOS 12+。