9 Commits
Author SHA1 Message Date
toor 2d3c322e34 feat(油猴): 行内推送按钮 + 已推送画廊标灰
需求两条:
  1) 列表页的复选框改成按钮,点一下直接推送到工具
  2) 推送过但没点进去的画廊,标题也要变灰(像浏览过的),
     这样刷新后还认得出哪些推过

一、行内推送按钮

复选框 + 底部「批量推送」两步走太啰嗦,列表页每行改成「📤 推送」
胶囊按钮,点一下直接调 /add。按钮三态:
  📤 推送  ->  ⏳ 推送中(禁用)  ->  ✓ 已推送(变绿)
底部悬浮条保留,改为「全部推送」+「重置状态」+ 本页计数。

二、标灰::visited 伪造不了,只能自己记

E-Hentai 的灰色来自自带样式
  a:visited .glink, a:active .glink { color:#8F6063 }
但 :visited 是浏览器**受保护**伪类:脚本查 matches(':visited')
永远返回 false,加 class 也触发不了站点的原生规则。

所以改成自己记一份「推送过」的清单,再照抄站点的色值手动染色,
观感与原生 :visited 一致(hover 恢复站点的 #8F4701)。

三、清单存哪:工具端为主,浏览器端为辅

新增 internal/core/pushed.go —— PushedStore 独立于 Store,
单独存 %APPDATA%\EH Downloader\pushed.json。

为什么单独一个文件:state.json 里的 tasks 是「当前要下载的东西」,
用户点「清除已完成」就删掉了;但「这个画廊我推送过」是历史事实,
删了任务不该忘掉。所以推送历史与任务列表解耦。

新增桥口 GET /pushed 返回推送历史(与 GET /tasks 语义不同:
后者是当前任务列表,会被清除)。

油猴脚本侧:
  - localStorage(ehdl_pushed_gids)先渲染,刷新后立刻可见,
    工具不在线也能用
  - 工具在线时用 /pushed 校正一次(localStorage 可能被清,
    工具才是权威),以工具为准整体覆盖
  - GET /tasks 保留给需要看当前任务列表的场合

四、附带:state.json 事故的恢复工具

tools/recoverstate —— 修复被写坏的 state.json:
  1) token 先从下载目录的 .ehdl.json 清单里找
  2) 找不到的用 E-Hentai 的 ?next=<gid> 定位法去站点抓
     (该 URL 会返回以 gid 开头的 25 条列表,正则取 token,
     offset 0..5 轮着试)
  3) 抓不到的说明画廊在站点上已被删除

-dry 预演不写盘,写盘前先备份原文件。

配套回归测试:
  - internal/core/pushed_test.go   推送历史读写、任务删除后仍保留
  - internal/core/recover_e2e_test.go  恢复出的任务能真下载(71/71 页)
  - internal/eh/recover_verify_test.go 恢复出的 token 能解析画廊
  - app_test.go                    点「清除已完成」后灰标不消失
  - internal/core/bridge_test.go   /tasks 与 /pushed 的 token 校验

后两者需要能连 e-hentai.org,设 EH_TEST_PROXY 才跑。
2026-10-25 00:16:03 +08:00
toor 77a422b9bf fix(core): BuildDir 只按模板里的分隔符分层
现象:默认模板 {title} 本该只出一层目录,但标题里带 | / : 的画廊
会凭空多出一层文件夹(6 个里 5 个中招)。

根因:把「非法文件名字符集」当成了「目录分隔符集」。两者只有 \ 和 /
重叠,剩下 : * ? " < > | 这 6 个字符被完全误当成目录分隔符。
更糟的是展开顺序——旧代码先把占位符全部替换完再切分,切分对象是
替换后的完整字符串,模板里的分隔符和值里的字符混在一起根本分不开。

EH 标题惯例是「原文 | 译名」,竖线极常见,所以中招率很高。

改动:改为先按模板文本切段(新增 reSep),再逐段展开占位符;
值里的非法字符一律交给 SanitizeSegment 清洗成 _,不再产生层级。
模板里自己写的 / 仍然正常分层,{artist}/{title} 行为不变。

顺带一提:值里的非法字符以前是「变成目录」,现在才是本该有的
「清洗成 _」——两处回归测试都锁住了。
2026-10-11 11:41:00 +08:00
toor 79ebc82227 chore: 适配 macOS 编译(测试期望改用平台路径分隔符、统一 LF 行尾) 2026-10-09 10:40:08 +08:00
toor 7243f73d33 fix: 解析大画廊时连接泄漏 + 解析阶段无进度反馈
用户反馈:点开始下载后 6 个画廊(都是 2000 张级)一直显示「解析中 0/?」,
看起来像卡死。排查发现两个问题:

1. 连接泄漏(真 bug)
   httpClient() 每次请求都新建 http.Transport,用完丢弃。Transport 被丢弃后
   它持有的 keep-alive 连接不会立刻释放(要等 IdleConnTimeout=90s),
   于是每发一个请求留下一条僵尸连接。
   现场实测:91 条 Established 连接,而解析是串行的,正常应该只有 1 条。

   修复:Transport 全局复用,只在代理配置变化时重建,
   重建时 CloseIdleConnections 关掉旧连接;新增 Client.Close()。

   实测:20 次请求从 20 条连接降到 1 条;100 次并发请求只用 22 条。

2. 解析阶段毫无反馈
   2000 张 = 100 屏串行翻页,整个解析阶段界面都显示「解析中 0/?」,
   完全没有进度,用户只能干等。

   修复:FetchGalleryPage 增加 ParseProgress 回调,每翻完一屏上报一次
   (已解析张数/总张数/当前屏),界面显示「已解析 340/2000 张」。

另外:用户设置里请求间隔是 1000ms(1 秒),全局限流让 5 个画廊共享
1 请求/秒 —— 500 次解析请求需要约 8 分钟。这是配置导致的慢,不是 bug,
但可以在界面上看到进度后就不会以为卡死了。

测试从 74 增至 80,新增:
- TestTransportIsReused / TestTransportReusedUnderConcurrency(连接复用)
- TestSetProxyRebuildsTransport(改代理要重建)
- TestParseProgressCallback(进度回调,含最后一屏)
2026-10-09 10:02:50 +08:00
toor 0623c55407 feat: 清单存 /s/ 路径,重开程序跳过画廊页解析
画廊页要翻 ?p=N 分页收集全部页(206 张 = 11 次请求)。之前每次运行都要
重新解析一遍,即使本地已经有完整清单。现改为:

- 清单升级到 v2,pageMap 存每页的 /s/ 路径与文件名
- 下载前先按 gid 找磁盘上的画廊目录,读到 v2 清单就直接下载,不请求画廊页
- 解析完立即写清单,崩溃/关程序也不丢

为什么存 /s/ 路径而不是 hath 直链:
hath 直链是「随机子域 + 随机端口 + 时间戳签名」,每次都变且会过期,
存下来也没法用(手动复制到浏览器打开,下次同样失效)。/s/ 路径是稳定的。

降级策略(都实测过):
- v1 旧清单(没有 pageMap)→ 重新解析,并把清单升级成 v2
- 清单 JSON 损坏 → 重新解析,跑完重写一份正常的
- 清单的 /s/ 路径过期(待下页几乎全失败)→ 自动重新解析一次再重试
- 找不到清单但目录存在(换了目录模板)→ 按新模板名/旧标题名找到目录继续

另外 pageMap 里存了 meta,否则从清单重建时目录模板(如 {artist}/...)
会退化到兜底值,算出来的目录名和之前对不上。

测试从 68 增至 74,其中 TestManifestSkipsReparse 实测第二次运行
画廊页请求数为 0 增量。
2026-10-09 08:58:18 +08:00
toor 9241c5dcd1 fix: 补 panic 兜底,防止将来任何内部错误导致整进程消失
上一提交修了并发 map 写的问题(fatal error,不可 recover)。
这次给引擎加一层普通 panic 的兜底:

- safeRunTask:画廊级别,panic 时把该画廊标记失败 + 记日志
- safeDownloadPage:页级别,一页出错不带走整个 worker
- 恢复代码本身再包一层 recover —— 万一 panic 根因就是 store 出问题,
  恢复逻辑不该二次崩溃逃逸出去

这不能替代修 bug(runtime fatal error 依然拦不住),
但能保证「将来再出别的内部错误」时用户能看到日志,
而不是应用无声消失、已下载的文件也白搭。

TestSafeRunTaskRecoversPanic 验证 recover 生效
(该测试在写第一版时还真抓出了恢复代码的二次 panic 隐患)。
2026-10-09 08:41:42 +08:00
toor 27ca6e08a0 fix: 修复大画廊下载时进程静默闪退(fatal error: concurrent map write)
现象:点「开始下载」后,单个画廊图片较多时整个程序无报错直接退出。
只在图片多的画廊上偶发。

根因:pageState.snapshotLocked 用浅拷贝把 Task 交给 UI:
    t := *s.task          // Pages map 仍是同一块内存
快照被 EventsEmit 做 JSON 序列化时,其他页 worker 正在写同一个 map,
撞上就抛 Go runtime 的致命错误:

    fatal error: concurrent map iteration and map write

这是 runtime 级 fatal error,不可 recover,直接杀进程 —— 所以没有任何
报错提示。图片越多、页并发越高,并发写窗口越长,越容易触发。

同一问题还有第二处:Store.UpsertTask 存的是原值(含活的 map),
而 Flush() 会在后台 json.Marshal 整个 state,同样会撞上。

修复:
- snapshotLocked 深拷贝 Pages map 与 FailedList slice
- UpsertTask 存前先 clone(),让 store 完全拥有自己的数据
- 另加一个兜底 recover,防止将来再有 panic 直接杀掉进程

测试:
- TestSnapshotDoesNotSharePagesMap:验证快照隔离(修复前失败)
- TestSnapshotRaceWindowClosed:持续写原 task + 持续序列化快照,
  精准复现崩溃窗口 —— 实测修复前 fatal error 崩进程,修复后通过
- 大画廊压测:200 页 × 页并发 20,以及 5 画廊 × 60 页 × 并发 16

测试从 62 增至 67。
2026-10-09 08:39:27 +08:00
toor 0447875b6d feat: 双层并发 + 全局限流 + 整轮扫尾 + 画廊目录清单
原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。
按用户多年使用习惯(5 画廊 × 每画廊 8 张)改造为双层并发。

- 双层并发:画廊并发(默认5) × 页并发(默认8) = 最多 40 张同时下载,
  两个值都在设置面板可调(1-20)。页级 worker 池 + mutex 保护计数。
- 全局限流:对齐参考实现的 nextRequestTime 算法,限所有请求
  (含 /s/ 解析与图片本体),默认 0 秒关闭。
- 整轮扫尾:跑完后自动重扫失败页 1 轮(同样走完整 4 次退避重试),
  仍失败则停下并标记 partial,换线路后手动点开始即可续传。
- 画廊目录清单 .ehdl.json:记录 gid/token/总页数/已完成页,
  即使 state.json 丢失也能算出「应该多少张、还差多少张」。

顺带修复:
- Start() 双重加锁导致死锁(引擎完全无法启动)
- clampInt 越界时返回默认值而非边界值(设 30 会静默变 8)
- 直链正则写死 hath.network,CDN 换域名会全军覆没 -> 加图片 URL 兜底
- ReconcileTask 会误报「有改动」

测试从 37 增至 62,其中并发/限流/扫尾用 httptest 模拟 EH 实测:
- 页并发峰值 = 8、画廊并发峰值 = 5(完全生效)
- 12 次请求限流 80ms 实测 966ms(期望 880ms)
- 断点续传 4 张已存在时只发 5 次请求
2026-10-09 00:11:38 +08:00
toor 0f927147cd feat: EH Downloader 用 Go + Wails v2 重写
原 Electron 版在这台 Windows 机器上无法启动(0x80000003 崩在 V8 初始化),
整个业务逻辑用 Go 重写,前端界面与协议结论完全保留。

- internal/eh: 协议核心,画廊页分页 -> /s/ -> hath 直链 -> 下载
- internal/core: 并发引擎 / 状态存储 / 目录模板 / 油猴桥口
- app.go: Wails 绑定层,替代 Electron IPC
- frontend/js/api.js: 适配层,renderer.js 一行未改
- 37 个测试,含 httptest 模拟 EH 的端到端链路验证

相比 Electron 版:90MB -> 12.4MB,180MB 内存 -> 27MB,构建 90s -> 2s。

顺带修掉原版代理探测的 bug(永远返回第一个端口,从不等连接结果)。
2026-10-08 23:28:16 +08:00