Repository navigation
Releases: Buwrt/githup
Release list
githup v1.2.18
githup v1.2.16(Actions 日志:App 内直接看)
这一版做了什么:Actions 日志
以前在 Actions 里点「查看日志」,其实是被丢去了浏览器 ——
App 里那个按钮是个假动作。
原因不复杂:/actions/runs/{id}/logs 不是直接吐日志,
而是 302 跳到一段带签名的下载地址。前端既过不去跨域,
又不能在跳转后把令牌摘掉(带着令牌反而会被判成多余凭据,直接 403)。
两件事凑一起,就只有原生层能同时满足:带着令牌去要,跳走之后摘掉令牌再取。
这一版把这条路打通了:原生层把日志包取回、解压成文本交给前端,
前端按官网的样子呈现出来。
- 按步骤分栏:日志包里一个文件就是一个步骤,顶部横滑切换
- 还原 ANSI 颜色:红是报错、黄是警告、绿是通过,跟你在网页上看到的一样
- 关键字过滤:输入即筛,命中的行标黄,并告诉你命中了几行
- 一键复制:整段日志复制走,方便贴给人看
- 取不到时说人话:日志只在 GitHub 上保留一段时间,
过期或被清理时会明确提示,并给「在浏览器查看」兜底,不会把人困在空面板里
每个作业单独有一个「查看日志」(只看这一个作业),
右上角菜单里另有「查看完整日志」(整次运行),与官网两个入口对齐。
安装信息
| 项 | 值 |
|---|---|
| 大小 | 1,295,257 字节(约 1265 KB) |
| SHA-256 | 66574f3378a6f0b58d1a2d09e88b0b7e175deed4c8da0a752e925916a488a1b0 |
githup v1.2.17
githup v1.2.16(Actions 日志:App 内直接看)
这一版做了什么:Actions 日志
以前在 Actions 里点「查看日志」,其实是被丢去了浏览器 ——
App 里那个按钮是个假动作。
原因不复杂:/actions/runs/{id}/logs 不是直接吐日志,
而是 302 跳到一段带签名的下载地址。前端既过不去跨域,
又不能在跳转后把令牌摘掉(带着令牌反而会被判成多余凭据,直接 403)。
两件事凑一起,就只有原生层能同时满足:带着令牌去要,跳走之后摘掉令牌再取。
这一版把这条路打通了:原生层把日志包取回、解压成文本交给前端,
前端按官网的样子呈现出来。
- 按步骤分栏:日志包里一个文件就是一个步骤,顶部横滑切换
- 还原 ANSI 颜色:红是报错、黄是警告、绿是通过,跟你在网页上看到的一样
- 关键字过滤:输入即筛,命中的行标黄,并告诉你命中了几行
- 一键复制:整段日志复制走,方便贴给人看
- 取不到时说人话:日志只在 GitHub 上保留一段时间,
过期或被清理时会明确提示,并给「在浏览器查看」兜底,不会把人困在空面板里
每个作业单独有一个「查看日志」(只看这一个作业),
右上角菜单里另有「查看完整日志」(整次运行),与官网两个入口对齐。
安装信息
| 项 | 值 |
|---|---|
| 大小 | 1,295,257 字节(约 1265 KB) |
| SHA-256 | 66574f3378a6f0b58d1a2d09e88b0b7e175deed4c8da0a752e925916a488a1b0 |
githup v1.2.16
githup v1.2.16(Actions 日志:App 内直接看)
这一版做了什么:Actions 日志
以前在 Actions 里点「查看日志」,其实是被丢去了浏览器 ——
App 里那个按钮是个假动作。
原因不复杂:/actions/runs/{id}/logs 不是直接吐日志,
而是 302 跳到一段带签名的下载地址。前端既过不去跨域,
又不能在跳转后把令牌摘掉(带着令牌反而会被判成多余凭据,直接 403)。
两件事凑一起,就只有原生层能同时满足:带着令牌去要,跳走之后摘掉令牌再取。
这一版把这条路打通了:原生层把日志包取回、解压成文本交给前端,
前端按官网的样子呈现出来。
- 按步骤分栏:日志包里一个文件就是一个步骤,顶部横滑切换
- 还原 ANSI 颜色:红是报错、黄是警告、绿是通过,跟你在网页上看到的一样
- 关键字过滤:输入即筛,命中的行标黄,并告诉你命中了几行
- 一键复制:整段日志复制走,方便贴给人看
- 取不到时说人话:日志只在 GitHub 上保留一段时间,
过期或被清理时会明确提示,并给「在浏览器查看」兜底,不会把人困在空面板里
每个作业单独有一个「查看日志」(只看这一个作业),
右上角菜单里另有「查看完整日志」(整次运行),与官网两个入口对齐。
安装信息
| 项 | 值 |
|---|---|
| 大小 | 1,109,487 字节(约 1083 KB) |
| SHA-256 | 1ef6b291942ac9320a2ae96b0e5d4545bc738f5cf536c9ca1e7a74bec0738d51 |
这一版还做了什么:探索页换成官方趋势榜
「今日 / 本周 / 本月点来点去,内容都不变」—— 这不是刷新慢,是口径错了。
以前的趋势榜走搜索接口 created:>某日 sort=stars,排的是 star 总量。
总量是缓慢累积的量,排序天生就稳:实测 7 天窗口往后滑一天,top10 里
仍有 7 条不动。加上顶部「热门仓库」那 5 条写死成 stars:>50000,
既不认时间范围也不认语言,怎么看都是同一张脸。
官方 Trending 排的是窗口内 star 增量,GitHub 从没给过公开 API,
只有 github.com/trending 这一个入口 —— 这一版直接抓它,和网页版同一口径:
- 今日 / 本周 / 本月 现在真的对应官方的 daily / weekly / monthly
- 每条直接写出增量:
+2,716 今日,就是官网上那句 "2,716 stars today" - 语言筛选照旧:Python / Rust / Kotlin… 切过去就是官方对应语言的榜
- 抓不到会兜底:退回原来的搜索榜,并在顶部写明「这是按 star 总量排的兜底榜」
那条永远不变的「热门仓库」删掉了。
githup v1.2.15
githup v1.2.15(versionCode 1002015)
打包成功却看不到 APK —— 硬断言产物存在 + 空态给出原因。
实测数据
githup 库最近 40 次运行里,official-release 跑了 37 次,
其中 8 次「conclusion = success 但 artifacts = 0」
(#100 #97 #96 #86 #83 #82 #78 #73)。
绿勾出现了 8 次,下面一个 APK 都没有。
根因一:continue-on-error 让失败伪装成成功
- name: Build Release APK
id: b
continue-on-error: true # ← 为的是失败时仍能提交日志gradlew 失败时这条被"容错",整个 job 依然是绿色对勾。
用户看到「打包成功」,点进去却没有 APK。
根因二:if-no-files-found: warn
- name: Upload artifact
with:
if-no-files-found: warn # ← 匹配不到文件也只是警告产物路径匹配不到时步骤照样绿,又一次"成功但没东西"。
根因三:App 里没产物时整块消失
function artifactsBlock(artifacts) {
if (!artifacts.length) return ''; // ← 页面一片空白
}看的人只会以为"加载坏了",想不到是这次运行压根没上传产物。
而且 artifacts 请求失败时 .catch 静默返回空数组 ——
「确实没有」和「没查到」混为一谈,提示必然是错的。
改法
-
新增
Assert APK produced(硬断言)- 构建退出码非 0 → exit 1
- 找不到 APK → 打印目录树 + exit 1
- 小于 300KB → 判为残缺 + exit 1
绿色从此只代表一件事:确实产出了 APK。
-
Upload artifact 收紧
if-no-files-found: warn→errorretention-days: 90显式写死(不依赖可被改动的仓库默认值)
-
新增
Write APK summary
把文件名 / 大小 / sha256 / 证书指纹写进 Job Summary,
点开运行一眼可见,不用翻日志。 -
App 侧空态给出原因(page-detail.js)
- 没产物 → 说明「只有用了 upload-artifact 才会出现;
提交到分支或发到 Release 不会显示」 - 请求失败 → 单独提示「没查到产物列表(请求失败)」
两块不再混为一谈。
- 没产物 → 说明「只有用了 upload-artifact 才会出现;
为什么有些运行永远不会出现产物
比如那种只有 Download + Commit 到分支的工作流 ——
它没有 upload-artifact。GitHub 只在用了 upload-artifact 时才
生成产物区。以前这种运行在 App 里就是一片空白;现在会明说原因。
校验信息
| 项目 | 类型 | 完整值 |
|---|---|---|
| 源码指纹 | 源码快照 SHA-256 | c313a6029e0ab66290cb20339a0af4517f876101b7f333a29c8e8fa379e26026 |
| 签名指纹 | 证书 SHA-256 | 863dd1cd3752e59165e152bc4ab35fde478fcd296d724a6bc58cec0368184927 |
| APK 校验值 | 安装包 SHA-256 | 667f1506b36619abda4d0f7519b564091bd06c7739c8439f788fba5e7dc459c7 |
| 构建提交 | Commit SHA | 57dcfbc43167d68f318ba5dbc09f7581357bb641 |
均为完整值,可在表格里整段选中复制,与官方公布的比对一致再安装。
githup v1.2.14
githup v1.2.14(翻译速度还原 · 两步验证器:手机上直接出 6 位动态码)
上一版做错了什么
v1.2.13 说「免费引擎从 2 家扩到 5 家」,把 DeepL / Google / 设备端
直接放进了协作池。实测这条路错得离谱:
手动指定「只用有道」翻同一页 —— v1.2.11 是 624ms,v1.2.13 是 9633ms,慢了 15 倍。
有道本身一点没变慢,是它的活被连不上的引擎抢走了:有道只分到 1 批,
DeepL 被派了 6 次、Google 4 次、设备端 2 次,页面在等那些根本连不上的请求。
当初设想的三重措施全没兜住:
| 措施 | 为什么没用 |
|---|---|
| 3 秒闸门 | 把单次从 8 秒砍到 3 秒,但它们被派了十几次,接力下来整页照样 9.6 秒 |
| 权重垫后 | 只相对于有道一家垫后,转轮里「所有慢引擎加起来」占了三分之二的位子 |
| 连续失败摘牌 | 要连续失败 3 次才摘,前 3 批的学费已经交完了 |
闸门治的是「单次等多久」,治不了「反复派给死引擎」。
这一版改成了什么
先验证,再准入 —— 能不能用,探一次说了算。
- 探不过 → 压根不进池。国内用户永远走有道 + MyMemory 那条快车道
- 探过了 → 才准进池。此时它是真能干活的,不是来占位置的
探测在后台跑,不占首屏时间;结果记下来,30 分钟内不再重探
(换 WiFi / 开代理 / 出国之后会自动重探一次)。
| 场景(翻同一页 120 段) | v1.2.11 | v1.2.13 | v1.2.14 |
|---|---|---|---|
| 手动指定「有道」 | 624ms | 9633ms | 624ms |
| 手动指定「MyMemory」 | 612ms | 9621ms | 614ms |
| 自动(多引擎协作) | 608ms | 9612ms | 607ms |
同一版里还有:看代码 / 改代码变成编辑器的样子(PR #12,来自 @mymine)
- 长行不用左右拖了 —— 代码默认自动折行,顶上可以切回「不换行」
- 换行时行号不错位 —— 原来行号是单独一列,代码一折行就对不上;改成按行渲染后,
行号钉在每个逻辑行的开头 - 编辑文件时也有行号和语法高亮了 —— 原来是个光秃秃的输入框
「免费引擎变多」没有落空
它变成了「能用的才算多」:海外能通的环境、或者你挂了代理,
那几家探测一过就自动加入,和 1.2.11 里的有道 / MyMemory 一样是真在干活的引擎。
而国内用户不会再为这个「多」付 15 倍的代价。
顺带一提:首屏竞速(v1.2.13 的另一项改动)经查是无辜的 ——
纯竞速版实测 624ms,和 v1.2.11 一模一样。这一版没动它。
MyMemory 配额用完之后,不再每页都白撞一次
它的免费配额是按天算的,一天之内不会自己恢复。以前撞上配额只有
「连续失败 3 次就摘牌」管着,而那本账每次换页就清零重来 ——
于是配额早就用完时,每翻一页都要先白撞 3 批(每批几十条请求)
才又一次得出「它今天不行」。看到的就是每页都莫名慢一截。
现在记一笔账:确认耗尽后 6 小时内不让它自动进池,连那 3 批学费都省了。
取 6 小时而不是一整天,是因为它的配额按 UTC 日重置 ——
半天给一次机会,跨过零点自动回来。
顺带补了一个漏:配额打满时它也可能直接回 429(而不是把警告塞进译文里),
以前只认警告那一种,429 那一路会落回「每页白撞 3 批」的老路。
手动点名要用它时不受影响 —— 你在菜单里点了它,那就得上。
同一版里还有:议题里的图片,加载得跟 README 一样快
同一个 App,README 里的图划到就出来,议题(issue / PR)里贴的截图却要等一下。
根子不在网络,在预热名单漏了它们。README 的图渲染时会被丢给原生后台下载到磁盘,
划到时是读磁盘、零网络;议题里贴的图是 GitHub 附件链接,URL 不带扩展名,
于是既被当成 <video> 渲染、也进不了只认扩展名的预热名单 —— 每次都要现拉。
现在:GitHub 附件链接也进预热名单(最多 3 个、单张超过 4MB 的不预,
免得为一张大图把后台占满);探测用的 <video> 降级成 <img> 时
直接复用已经下载好的那一份,不再重新下一遍。
一处刻意没改:拦截仍然只接管带 image/* 的请求,<video> 一律放行 ——
视频靠 Range 请求做 seek,被整包 200 接过来会把播放弄坏。
同一版里还有:议题里的图,改成按官网的答案渲染(不再靠猜)
上面那一项治的是「进不了预热名单」,这一项治的是更靠前的根子:App 得猜它是什么。
官网不用猜 —— 它的正文是服务端渲染的。你拖一个文件进议题,GitHub 上传时就记下了
它是什么,渲染正文时直接写出来:
| 你贴的 | 官网渲染出来的 |
|---|---|
| 截图 | <img width="327" height="706" src="…"> |
| 录屏 | <video src="…"> |
类型和宽高都写在 HTML 里,浏览器一点都不用猜。
我们以前拿的是 Markdown 源码自己渲染,附件在里面是
github.com/user-attachments/assets/<uuid> —— 没有扩展名,看不出是截图还是录屏。
只能乐观当 <video> 渲染、拉不动再降级成 <img>。代价是:绝大多数其实是截图的
附件都要先白跑一趟视频请求;而且拿不到宽高,图一加载完页面就跳一下。
现在正文改要官方渲染好的那份(接口带 Accept: …full+json,一份请求里 Markdown
和 HTML 都有 —— Markdown 留给「引用 / 编辑」,HTML 用来渲染)。
地址就照官网的样子用:官方正文里给的是
private-user-images.githubusercontent.com/…<uuid>.png?jwt=… —— GitHub 自己的
CDN 域名,字节直出、不用跳。官网的图快,快就快在走的是这条直路,所以这一版
把它原样留着当主路径,只把不带签名的那份(user-attachments/assets/<uuid>)
记在旁边当第二条路 —— 那份要先吃一个 302 才到 S3,多一趟往返、多一次握手。
签名 5 分钟有效带来的两个坑:渲染完立刻预热(趁签名还热),
以及缓存不再按整条 URL 记(见下一节)。
同一版里还有:预热带回来的字节,得存进同一个抽屉
上面两条官方线路是同一份字节,可缓存以前是按整条 URL 做 key 的。带签名的
地址每 5 分钟、每次请求都不一样,于是:
预热按 CDN 地址下好一份 → 加载时签名已经换了 → 缓存里没有 → 又下第二份
预热带回来的那一趟等于白做,用户滑到的每一张都还在现拉。
现在缓存 key 改成认地址里那个 uuid(asset-<uuid>):两条官方地址落到磁盘上的
同一个文件,谁先下好谁说了算,另一个直接命中。下载锁也按 uuid 走,不会一个在
下载、另一个同一时刻也在下。没有 uuid 的地址(README 的 raw / camo / 头像)原样记,
不受影响。
同一版里还有:图片不再被我们「接走」—— 接走的那条路反而更慢
从 v1.2.9 起,图片比更早的版本慢:加的东西越来越多(缓存、预热、快车道),
体感却在往回走。真因不在缓存,在**「接管」这件事本身**。
v1.2.9 之前,图一直是 WebView 自己拉的 —— Chromium 的网络栈,HTTP/2 多路复用、
TLS 会话复用、它自己的 HTTP 缓存(304)、以及边下边渲染。v1.2.9 加了 ImageProxy
之后变成「WebView 要图,我们全接过来自己下」,而一旦被接住,Chromium 就不再发它自己
那趟请求 —— 等于把快车道换成了慢车道(HTTP/1.1、只有 gzip、整包到齐才出第一帧、
还同步占住 WebView 有限的几个 IO 线程,一张慢图把后面排队的图一起拖住)。
现在改成只接得住的才接:
- 磁盘上已经有 → 接,零网络
- 磁盘上没有、匿名就能拿到 → 放手,交回 WebView 自己拉(它那套比我们快)
- 非带令牌不可的(私有仓库的图,WebView 手上没令牌)→ 才由我们来,而且是学着来的:
第一次撞 404 补令牌成功后记下这个仓库,以后首趟就带,连那趟 401 都省了
预热也学会了让路:延后 1.2 秒再动(等屏幕上的图先拉完)、从 3 条并发改成 1 条、
并且不收屏幕上的图(它们此刻正由 WebView 拉,再下一遍是同一份字节买两次单)。
同一份附件还补了「双路竞速」:主路超过 2 秒没出图就把备用路点着,谁先回来用谁 ——
不用再等主路走到超时(十几秒)才换。主路先到的话第二趟压根不发起,快图不付两份流量。
同一版里还有:点议题里的图片,看的是大图;长按,才是打开那个页面
议题里点一张截图,弹出来的不是大图,是一个顶栏写着 GitHub、下面一片白的窗口
(github.com/user-attachments/assets/…)。两个原因叠在一起:
- 官方渲染的正文把上传的截图包在
<a target="_blank">里,而 App 的外链委托挂在
document 的捕获阶段,跑在图片自己的onclick之前 —— 一点图片,先被外链
委托接走、把内嵌浏览器打开,「查看大图」永远轮不到出场; - 链接规整还把附件地址站内化成了
#/user-attachments/assets/…,可它不是站内
页面,Router 不认识,最后照样掉进那个内嵌窗口。
现在:点图片 = 查看大图;长按图片(约半秒)= 打开它指向的那个页面;
点图片边缘(链接本体)仍是打开页面 —— 行为没丢,只是不再抢「看图」的默认位。
附件地址也从站内化规则里摘了出来,它本来就该是外链。
同一版里还有:收藏夹能分类了 —— 隔层就长在收藏夹里
官方客户端的「已添加星标」里有一层「我的列表」(Lists),把 Star 过的仓库
归进不同分类。这个功能 GitHub 至今没有开放 API(REST 里翻得到 /user/starred,
翻不到任何 list 端点),所以这版照着官方的交互做成了本机功能,并且直接
长在收藏夹里,不另立入口:
- 切到「收藏夹」→ 顶部多一排横滑分类:全部收藏 / 你建的各个列表 / +新建;
点哪个看哪个,长按某个分类可以重命名、删除(删除要二次确认); - 长按仓库条目(或仓库页「更多」菜单里的「加入列表」)弹出勾选层:
勾上 = 归进这个分类,再点 = 移出,勾完弹层不关,可以连着归几个; - 归进分类的仓库自动也算收藏 —— 单击「收藏」按钮的老习惯一点没变;
从分类里移出不影响收藏,从收藏夹移出也不影响分类; - 分类里存的是仓库快照(名字 / 描述 / 星数 / 语言),点开先画快照再悄悄刷新,
断网也不会白屏。
数据保存在这台设备上(按登录账号隔离),不占 GitHub 的任何请求额度;
换设备不跟着走 —— 这是「官方没开 API」之下唯一不偷密码、不走第三方的做法。
同一版里还有:已读通知,这次是真的能删干净了
「通知 → 已读」里点删除,通知当时没了,一刷新又全部回来 ——
这个幽灵现象查到最后,根子比想象深:
删除动作其实一直是成功的(GitHub 的「标记 Done」返回 204,没毛病)。
真正的坑是 GitHub REST API 不过滤 Done 状态:标了 Done 的通知,
拉通知列表(all=true)时照样原样返回,而且返回数据里没有任何
字段能认出它已被 Done(官方最新的 API 版本也没加)。网页版收件箱
用的是带 is:done 过滤的内部接口,第三方用不了 —— 所以「已读」
列表一刷新,刚删的全回来了。
REST 层面无解,这版用本机记账补上:
- 删除成功的通知记进本机「已清除清单」(按账号隔离,最多 500 条),
渲染列表时自动挡掉 —— 删除、刷新、重启 App,都不会再回来; - Done 之后那个议题/PR 有了新动静,GitHub 会把它复活成未读送回
收件箱 —— 未读的一律放行,新消息一条不漏; - 菜单新增**「清空已读」**:一键把所有已读通知标记为 Done 并记账,
未读通知一根汗毛都不动; - 多选删除只移除真正成功的条目,真失败的留在列表里告诉你;
- 顺带治好一个哑巴病:「点开通知后还是未读」—— 单条标已读之前
用错了请求方法(PUT),改回文档的 PATCH 后真正生效。
同一版里还有:发布列表支持长按删除
仓库页「发布」标签里,以前只能新建发布,想删掉一个发错的版本、
或者给已经发出的版本补传漏掉的 APK,都得去网页版操作。现在
长按任意一条发布弹出菜单:
- 上传 APK 附件 —— 从手机里选一个文件直传到这条发布,
走和创建发布同一条原生二进制通道(单个不超过 200MB,
APK / AAB / ZIP 等任意文件都行)。如果这条发布里已有同名附件,
会先删旧的再传新的,不会出现两份同名文件;传完自动刷新列表; - 删除此发布 —— 危险确认(红色按钮、写明会连带删掉几个附件)
后调用官方接口删除;删完这条就从列表里消失,全删光自动回到空态;
git 标签会保留(GitHub 的规矩:删 Release 不删 tag); - 查看发布详情 —— 和点条目进去是同一个页面;
- 没有仓库写权限时,长按只提示「需要仓库写权限」,不出菜单 ——
看得见管不着的按钮只会招人烦。
手势和通知、收藏夹是同一套:点一下照常进详情,长按 550ms 才出菜单
(比标准 500 稍长,快滚列表不容易误弹;长按松手带出的那次误触点击
会被吞掉,不会串进详情页)。
同一版里还有:两步验证器(TOTP)—— 手机上直接出 6 位动态码
登录 GitHub、Google 这类开了两步验证的网站,除了密码还要填一个
每 30 秒变一次的 6 位动态码。以前得切到另一个验证器 App 去看,
现在 githup 自己就能算:
入口 —— 「我的 → 两步验证器」,不占底部标签页。密钥只存在你手机上,
算法也在本机跑,不联网、不需要登录 GitHub,飞机上照样出码。
添加账户的三种方式:
- 扫一扫 —— 对准网站上的二维码,自动识别、自动填入,
实时码立刻开始跳动; - 从图片识别二维码 —— 先把二维码截图存下来,再从相册选图;
- 从剪贴板粘贴 —— 网站二维码下面通常有一行「密钥」或
「手动输入」的字母数字,复制过来粘进去就行。
二维码识别是自己写的纯 JS 解码器,没有引第三方库 —— 定位图案、
透视校正、RS 纠错都是本仓代码,整包只多了几十 KB。
后台也能看 —— App 退到后台时,通知栏会自动常驻一个实时刷新的动态码,
回到 App 里自动收起;一个账户都没添加时不占通知栏。这件事不需要去设置里
开开关,本来就是跟着账户走的。
顺带修掉两个真机上才会显形的问题:
① 扫码按钮根本不显示 —— 选文件的方法挂在window.Native上,
代码里却写成window.API,于是可用性自检恒为假,按钮被直接隐藏;
② 点保存没反应 —— 保存按钮在弹层的.sheet-foot里,而绑定代码
只在.sheet-body里找它,查到null后抛异常中断,处理器从未挂上。
密钥只存在这台设备上,卸载或清除数据后就没了。重要账户建议
同时在网站保存一份恢复码,免得手机丢了进不去。
版本号只动第三位(1.2.13 → 1.2.14),按 README 的规矩属于可选更新。
请更新这一版:保持 1.2.13 的话,App 会认为你已是最新版本,
拿不到这个修复。
githup v1.2.13(免费翻译引擎扩容)
githup v1.2.13(免费翻译引擎扩容)
这一版只动翻译引擎,没有界面改动。
免费引擎从 2 家扩到 5 家
以前真正在跑的免费引擎只有有道匿名接口和 MyMemory(有日限额,垫底兜底)。
可选项其实早就写好了 —— DeepL、Google、设备端(离线)—— 但被一句
「海外 / 需探测的引擎不进自动池」挡在外面。
当初挡得有道理:国内连 DeepL / Google 大概率不可达,设备端翻译在 WebView 上
直接不响应。一组文本摊上它们,整页就得陪着等满 15 秒的连接超时。
这次放回来,代价用三重措施兜住:
| 措施 | 作用 |
|---|---|
| 3 秒专属闸门 | 这三个引擎单独一道 3 秒超时,不是全局的 15 秒。3 秒没结果就判失败,立刻交给下一家接力 |
| 权重垫后 | 有道 5 / DeepL 4 / Google 3 / 设备端 2 / MyMemory 1。页面组数少时它们根本轮不到,组数多时只占小头 |
| 连续失败摘牌 | 真派到又没翻出来,走 poolFail,连续几次就把它摘出转轮,损害只落在头几组 |
付费优先
有道开放平台(权重 8)、小牛翻译(7)、百度(6)排在免费引擎之前。
不是靠 if 判断,是靠各家自己的 ready():没填密钥就不进池,
填了就自动当主力。所以免费用户看到的顺序是:
有道 → DeepL → Google → 设备端(离线)→ MyMemory(兜底)
顺序实际由 share 权重决定(转轮按权重降序展开),不是只看 ORDER 数组。
同一版里的另一项:首屏竞速
最靠近视口的那组文本同时交给两家,谁先回来谁占坑,慢到那家的译文照样写进
共享缓存给后面几组用。改善的是长尾(「有时候特别慢」的那一半),不是中位数。
版本号只动第三位(1.2.12 → 1.2.13),按 README 的规矩属于可选更新。
githup v1.2.12(返回上一层后,上一个页面不再赖在屏幕上)
githup v1.2.12(返回上一层后,上一个页面不再赖在屏幕上)
这一版没有新功能,只修一个 bug:从搜索页点进一个仓库(或任何详情页),
加载还没完成就按返回 —— 顶栏已经变回「搜索」,屏幕中间却还留着上一个页面的内容。
两个页面缝在一起。
版本号只动第三位(1.2.11 → 1.2.12),按 README 的规矩属于可选更新:
装了 v1.2.11 的会收到提示,不更也能继续用。
一、这个画面是怎么来的
不是返回键的问题。顶栏正确、内容没换,这个形状本身就说明路由已经退回去了 ——
是内容被人从底下顶掉的。
每个页面的 render 都是同一套节奏:
先同步铺一层骨架 → 发请求 → 响应回来把整块内容写进 #view
问题出在最后一步。真实时序:
| 时刻 | 发生的事 |
|---|---|
| t0 | 搜索页点进仓库 → 画出仓库骨架,发出 /repos/tisfeng/Easydict |
| t1 | 你按了返回 → 路由退回搜索页,顶栏变回「搜索」,#view 换成搜索页 |
| t2 | 仓库的响应这时才落地 → 回调照旧 host.innerHTML = ... |
| t3 | 刚画好的搜索页被整个覆盖 → 缝合画面 |
回调不知道页面已经换了人。所以这 bug 只在网络比手指慢的时候出现 ——
网速好的时候响应早就回来了,你永远撞不上它。
顺带一提:搜索页自己早就有 renderSeq 防这个,所以它不会自己盖自己;
但那是补丁不是制度,其余页面全是裸奔的。
二、改法:给 Router 加一把「渲染纪元」令牌
Router.render() 每次换一把新令牌(viewEpoch),页面在 render 里把当前纪元
拍下来,异步回调落地前先对一下纪元 —— 对不上就整份丢弃。
// app.js · Router.render()
Router.viewEpoch = (Router.viewEpoch || 0) + 1;
// page-repo.js · P.repo.render()
var epoch = window.Router.viewEpoch;
return window.API.get('/repos/' + full, ...).then(function (r) {
if (epoch !== window.Router.viewEpoch) return; // 页面已换人,整份丢弃
paint(r.data, true);
}).catch(function (e) {
if (epoch !== window.Router.viewEpoch) return; // 错误框一样会顶掉新页面
host.innerHTML = UI.errorBox(e);
});两个容易漏的点,都护住了:
- 错误框也要拦。请求失败时那个「请检查网络后下拉重试」的框同样是整块写进
#view,一样会把新页面顶掉。 - 改顶栏标题的回调也要拦。议题页、用户页的回调里会调
App.title(),
不拦的话内容没被覆盖、顶栏却被改回去了 —— 那是另一种缝合。
写子容器(#tabbody、#nlist 这类)的回调不需要这层保护:页面被清空后子容器
已经脱离文档,写了也看不见。
三、接入的页面
| 页面 | 文件 |
|---|---|
| 首页动态 | page-home.js |
| 仓库详情 | page-repo.js |
| 议题 / 提交 / Release / 运行详情 | page-detail.js |
| 用户主页 / Gist / 我的 | page-user.js |
另外顺手修了一处:Gist 页渲染时用了 CSS.escape,个别老 WebView 上没有这个 API,
缺了它整个 Gist 页会挂成错误框。加了字面量兜底。
四、验证
新建 jsdom 集成测试 41 项,用桩桥扣住响应来模拟「网络比返回键慢」:
| 结果 | |
|---|---|
| 修复后 | 41 / 41 全绿 |
| 反证(把 9 处防护全删掉,跑同一份测试) | 24 / 41,红 17 项 |
反证里 A1 精确复现了用户截图的画面 —— 迟到的仓库响应把搜索页顶掉,#view 里
冒出 repo-head。这条红了,才说明测试真的盯着这个 bug。
既有 14 个回归脚本无回退。
附:改动清单
| 文件 | 变化 |
|---|---|
app/src/main/assets/web/js/app.js |
Router:新增 viewEpoch 递增 + 说明竞态的注释 |
app/src/main/assets/web/js/page-home.js |
首页动态接入 |
app/src/main/assets/web/js/page-repo.js |
仓库详情接入 |
app/src/main/assets/web/js/page-detail.js |
议题 / 提交 / Release / 运行详情接入 |
app/src/main/assets/web/js/page-user.js |
用户主页 / Gist / 我的接入,另修 CSS.escape |
Java / Kotlin 未改业务逻辑(GuardKeys.java 由防护链脚本按新版本号重新生成)。
githup v1.2.11(液态玻璃:背景真的会弯)
githup v1.2.11(液态玻璃:背景真的会弯)
这一版没有任何新功能,只换了一件事:玻璃的材质。
以前的顶栏、底栏、弹层、卡片是毛玻璃——背景先糊掉(blur 20~30px)再透上来。
柔和是柔和,但有个硬伤:背景内容本身不弯折。水下的东西平着过去,再怎么看都不是水。
液态玻璃(liquid-glass)走另一条路:用内联 SVG 位移滤镜把背景扭过来,
玻璃给用户看的不只是「透」,而是「折射」。
版本号只动第三位(1.2.10 → 1.2.11),按 README 的规矩属于可选更新:
装了 v1.2.10 的会收到提示,不更也能继续用。
一、为什么必须整套换
一句话的物理约束:
模糊是折射的天敌。blur 超过 6px,形变就糊得看不见了。
原来的顶栏是 blur(20px)、底栏 blur(26px)。直接把位移滤镜叠上去不会有任何视觉效果,
水面当场死掉。所以这不是「加个效果」,是整套材质取值倒过来:
| 以前(毛玻璃) | 现在(液态) | |
|---|---|---|
| 模糊量 | 20~30px | 3px(必须 ≤ 4px) |
| 面罩透明度 | .62 | .30 |
| 噪点 | .22(毛玻璃的灵魂) | .06(液态的敌人) |
噪点从 .22 压到 .06 是关键:水面看着是水,全靠背后那块扭曲的图案看得清,
铺一层颗粒上去等于给它蒙雾——GPU 每帧照旧算位移,肉眼什么都看不见。
留 .06 只是防止大面积渐变出现色带。
二、一个差点让整件事白做的前提
原来的背景是全不透明的。
.card { background: var(--bg); } /* #ffffff 实色 */
.list { background: var(--bg); }
/* 画布 --canvas 也是实色 */实色透实色——不管挂什么滤镜,输入都是同一块颜色,把纯色扭多少像素它还是纯色。
所以先给画布铺了一层能被折射的内容:
- 三团很淡的径向色晕——提供大块明暗变化
- 一层极淡的斜纹——这是关键。折射看不看得出来取决于背后有没有高频图案,
光是平滑渐变,扭了也看不出来 background-attachment: fixed——让背景不跟着滚,卡片从它上面滚过去时图案相对移动,
才有「内容从水面下流过」的观感
深色模式配了另一组更冷的色晕。
三、覆盖范围,以及两处刻意的例外
顶栏、底栏胶囊、弹层、提示条、列表卡片统一到同一套语言。
其中两处故意不一样:
| 元素 | 为什么降力度 |
|---|---|
弹层(.sheet) |
它是要静下来读的东西,底下全是扭动会累。折射力度减半、面罩加厚,再补一层 brightness(1.18) 抵消遮罩的压暗 |
| 列表卡片 | 惯性滚动时每帧都要为每张卡片重算位移贴图,若干张同屏就是「每帧 × 张数」——这是实打实的掉帧,不是理论顾虑 |
另外两处小改:
- 面罩变薄、噪点去掉之后,撑住玻璃形状的就只剩边缘那两道 inset(上缘受光、下缘背光),这次把它加重了。
- 按压动效换成弹簧曲线
.34,1.56,.64,1(末段 >1 会冲过头再收回)。列表行不加:
它在overflow:hidden的卡片里,缩放会被裁出白边,一行缩放还会顶着相邻行,整列看着在抖。
四、踩到的两个 CSS 陷阱
陷阱一:含 url() 的折射行不能包进 var()。
把 saturate(...) blur(...) 抽成变量省几行——不行。自定义属性一旦替换出的值无效,
该声明就是 invalid at computed-value time,属性会掉回 inherit/initial,
不会回落到同属性的前一条声明。老 WebView 会直接得到一个「没有 backdrop-filter」的顶栏,
而不是兜底。字面写的话,不认的引擎当作解析失败整条丢掉,兜底自然留下。
于是每个元素写成四行:
backdrop-filter: saturate(...) blur(20px); /* 兜底 */
-webkit-backdrop-filter: saturate(...) blur(20px); /* 兜底(老 WebView 只认前缀) */
backdrop-filter: url(#lens) saturate(...) blur(3px); /* 真折射 */
-webkit-backdrop-filter: url(#lens) saturate(...) blur(3px);陷阱二:<defs> 不能用 display:none 藏。 被隐藏的元素不进渲染树,
引用它的滤镜会整条失效,玻璃会悄悄退化成「一层几乎不糊的半透明面」——
所以改用 0 尺寸 + 绝对定位躲起来。
五、性能(这句是真话)
液态玻璃是三档玻璃里最贵的一档:backdrop-filter 要逐帧重算它覆盖的那块背景,
位移滤镜更贵。已经做了两处降级(列表走 soft 档、朴素样式的隔离完好),
但中低端机型上滚动变卡是有可能的。
真觉得卡:设置里关掉「iOS 风格」,立刻回到从前的实体栏 + 纯色背景。
这个开关这次一行没动,是改动的第一约束。
附:改动清单
| 文件 | 变化 |
|---|---|
app/src/main/assets/web/index.html |
+26 行:内联两片位移滤镜 #lens / #lens-soft |
app/src/main/assets/web/css/app.css |
+173 行:材质取值、背景层、折射回落、弹簧曲线 |
Java / Kotlin 一行未动。 全部改动隔离在 html[data-nav="glass"] 这条轴上,
朴素样式完全不受影响(有脚本逐条校验)。
githup v1.2.10(Actions 运行记录:按工作流筛选 · 跑废的可以删)
这一版加的是 Actions 页面上一直缺的两件事:按工作流筛选,以及删除运行记录。
功能来自社区 PR #9(mymine 提的),合进来之前补了三处,下面单独说。
版本号只动第三位(1.2.9 → 1.2.10),按 README 的规矩属于可选更新:
装了 v1.2.9 的会收到提示,不更也能继续用。
一、按工作流筛选:一个仓库六个工作流时不用再翻页
以前 Actions 页只有一排状态分段控件(全部 / 成功 / 失败 / 运行中)。仓库里挂着
CI、发布、依赖更新好几个工作流时,想看其中某一个的构建记录,只能在一整页里翻。
现在状态分段下面多了一个「工作流:全部」按钮:
- 点开列出该仓库的全部工作流(停用的会标出来);
- 选中后只显示这个工作流的运行记录;
- 两个筛选同时生效,并且都写进地址 ——
?status=failure&wf=123。
写进地址这件事是有用的:按返回、或者把链接分享给同事,都能回到同一个筛选组合,
而不是「我刚才筛的是哪个来着」。
一个容易踩空的点:/actions/runs 不支持按工作流过滤。得换到
/actions/workflows/{id}/runs 才拿得到数据。这版走的就是后面这条端点 ——
PR 作者这一处判断是对的。
二、删除运行记录:跑废的那条不用留着
网页端早就有的「Delete workflow run」,App 里一直没有。现在两处都能删:
| 入口 | 位置 |
|---|---|
| 列表 | 每条运行记录行尾的 ⋮(只有对该仓库有写权限时才显示)→ 删除运行记录 / 在浏览器打开 |
| 详情 | 单条运行详情页右上角菜单 → 删除运行记录(分隔线下面那一条) |
删之前有二次确认,按钮是红色的「删除」,并且写明「日志与产物一起删、删了没法恢复」。
删除成功后列表自动刷新。
报错翻成人话,不再对着一句 Forbidden 发呆:
| 状态码 | 提示 |
|---|---|
| 403 | 没有权限(需要对该仓库的写权限) |
| 404 | 运行记录不存在或已被删除 |
| 409 | 该运行正在进行中,先取消再删除 |
顺带说一个实现上的坑(不写下来的话下一个人还会踩):全局的 data-go 委托跑在
捕获阶段,在冒泡里 stopPropagation() 是拦不住它的 —— 不特殊处理,点行尾的 ⋮
就会顺带跳进详情页。所以这一行按 App 的约定自己绑 onclick 并置 __bound,
让全局委托跳过,由它自己区分「点 ⋮ 开菜单 / 点行进详情」。
三、合并 PR #9 时补的三处
PR 本体的方向没问题,代码质量比一般的 PR 高(上面说的两个坑它都提前判断对了)。
合进来之前补了三处,都属于「功能在,但边界没兜住」:
1. ?status= 收回白名单
PR 把原来「三选一」的收敛改成了原样透传。链接里混进别的值(手敲的、旧版本分享
出去的、别的客户端生成的)会一路直通 GitHub,对方回 422,整页变成一个报错框。
现在认不出的值一律退回「全部」。
2. ?wf= 一并收敛
它会被拼进接口的路径里(/actions/workflows/{wf}/runs),等于让用户控制一段
URL 路径。现在只放行数字 id 和 xxx.yml 文件名,其余退回「全部」。
3. 删除的 promise 链 return 出去
原来 deleteRun() 没有 return,调用方(详情页、行尾菜单)拿不到结果,不知道什么
时候该刷新。现在成功 resolve true、失败 resolve false(失败已经提示过,不再
往外抛)。详情页再补一层「仓库页还没加载好」的判断 —— 不判断的话那种情况下点了
就是毫无反应,看着像卡死。
另外,PR 里夹带了一个误提交的文件:把这次改动的 git diff 整个存成了
githup-actions-filter-delete(206 行),没有合进来。
四、回归
新增 58 项针对性测试,覆盖筛选拼接、取数端点、⋮ 的分流、删除的确认与失败提示。
反证过(改一行代码就说修好了,是没法证明的):
| 被测的代码 | 结果 |
|---|---|
| 修复后 | 58 / 58 |
| 退回 PR 原样 | 53 / 58 —— 红的正是上面那三处 |
| 只补第一处的「半吊子」 | 55 / 58 |
既有回归全绿:tab 栏与返回 47 + 36、仓库页头部 22、Markdown 26 + 29、站内链接 48、
底栏贴底 26、安全区 46。第 3 环(前端资源完整性,21 个文件)21/21 通过。
五、下载与校验
| 项 | 值 |
|---|---|
| 文件 | githup-1.2.10.apk |
| 大小 | 926,286 字节(约 905 KB) |
| SHA-256 | 391ee29c05eac58ebdd9af7d0669e1cbf4b03b16680b9707c5ed6030f25c49cc |
| versionName / versionCode | 1.2.10 / 1002010 |
| 最低系统 | Android 7.0(API 24) |
包内自带两道自校验:签名自校验(证书指纹不符拒绝运行)+ 源码指纹
(对源码文件算整体 SHA-256 内嵌,被改过会报出来)。本次源码指纹 SRC_SHA256:
c017b60205f4a0e61a12e03cfad99f5a68c27b09604b8365809d02659273f8cc
签名证书与之前一致,可直接覆盖安装,不用卸载。
githup v1.2.9(仓库页按返回一律先回代码 · 朴素样式底栏贴回屏幕底边)
这一版修的是一个真实现象:在仓库页的议题 / 拉取请求 / Actions / 发布 / 更多,
以及「更多」里进去的提交记录、分支、标签……上按返回,会去哪儿全看你是怎么进来的。
一、以前:返回键把走过的 tab 倒着走一遍
从「代码」连点「议题 → 拉取请求 → 发布」,想退出去得按三次返回:
先退到「拉取请求」,再退到「议题」,最后才回到「代码」。用户想要的其实是
「退出这一个 tab」,不是把刚才走过的路倒着回放一遍。
更难受的是另一条路:从搜索结果或者通知里直接点进某个仓库的「议题」——
历史里压根没有「代码」这一条,一按返回整个仓库页就没了。明明人还在仓库里,
却被弹回了搜索页。
根因就一句话:返回键走的是 history 逐级回退,而 tab 之间每次切换都往历史里堆一条。
| 来路 | 以前按返回 |
|---|---|
| 代码 → 议题 → 拉取请求 → 发布 | 退到「拉取请求」→「议题」→ 三次才回到代码 |
| 搜索结果 → 议题 | 历史里没有「代码」,直接退出仓库页 |
| 通知 → 发布 | 同上 |
二、顺带修掉:回到代码了,那一排 tab 却还停在右边
修完上面那条,现象变成「页面确实回到代码了,可那一排还停在后面」——
高亮是「代码」,可它在视野外,看着像没生效。
根因在位置记忆:返回走的是 replace,不是点击,于是渲染时走「按记忆原样恢复」
那条路,而记忆还停在右边(你上次为了够到「发布」拉过去的位置)。
现在 高亮落在「代码」时无条件把这一排拉回最左,并把记忆一起清掉 ——
不然下次回来又被拽到右边。「代码」是这一排的第一个,停在右边没有任何意义。
只认代码 tab:在「发布」「议题」上刷新,位置照旧不动(那是「刷新后位置不跑」,
另一条需求,不能一起改没)。
三、现在:一次返回到代码,再一次返回出仓库
两处一起改:
handleBack()新增一步 —— 只要人在这个仓库、且不在代码 tab,返回就落到代码 tab;
已经在代码 tab 了,才继续往上退(回搜索、回首页……)。这里用replace而不是go:
再 push 一条的话,历史会攒成「代码 → 议题 → 代码 → 议题」,按返回变成在这两页之间来回跳,
永远退不出去。pushRoute()把「同一个仓库里换 tab」认定为横向切换,跟底部五个标签一样不堆历史。
那一排 tab 是仓库页内部的并列视图,来回切不该留下痕迹。
于是历史栈里始终只有「进仓库前那一页 + 当前 tab」两条:
一次返回到代码,再一次返回出仓库。
四、该逐级退的地方,一步都不能跳
新规则只管 tab 层,下面是刻意不动的:
| 位置 | 按返回 |
|---|---|
tree / blob 文件浏览 |
它们本来就在代码 tab 里,照旧逐级退回仓库根 |
| 议题详情 / PR 详情 / 运行详情 / 提交详情 / Release 详情 | 先退回它自己那个 tab |
| 搜索 / 通知 / 个人页 / 下载管理 | 与仓库无关,不受影响 |
判据是「这个路径是不是某个仓库的 tab 层」—— /owner/repo 后面最多只跟一个 tab 名,
带文件路径、议题号、commit sha、tag 的都不算。
五、同版追加:朴素样式下,底栏重新贴回屏幕底边
这一版发出之后又报来一个现象:关掉「iOS 风格」回到朴素那条实心底栏,
在有的机器上这一排没有贴着屏幕底边,和底之间留着一段空隙。
玻璃那一套从来没有这个问题。(版本号不变,直接重打包。)
为什么只有朴素样式看得出来。 底栏那一条是:
#tabbar { bottom: calc(var(--nav-lift) + var(--safe-b)); }玻璃下 --nav-lift 是 14px —— 它是浮在半空的胶囊,任何误差都混在这 14px 的悬浮
感里,眼睛认不出来。朴素下它是 0:底栏到屏幕底边的距离就等于 --safe-b 这一个
数,原生量错多少,这里就空多少。同一个 bug,一半的人看不见。
而那个数是这么错的。
第一层是重复让位。MainActivity 只声明了 LAYOUT_FULLSCREEN,没有声明
LAYOUT_HIDE_NAVIGATION —— 也就是说系统已经替我们把导航栏那条留白留出来了,
WebView 本身的矩形根本就不含它。这时候前端再照着「导航栏有多高」让一次,
等于凭空多出一块。
第二层在 minSdk 上。它是 24,于是 Android 7~10 一律走这条分支 —— 直接读资源:
int idBot = r.getIdentifier("navigation_bar_height", "dimen", "android");
if (idBot > 0) bottom = r.getDimensionPixelSize(idBot); // 恒为 48dp那是 ROM 写死的上限值,跟「这台机器当下画没画导航栏」毫无关系:
| 机器 | 屏幕底部实际情况 | 资源里写的 |
|---|---|---|
| Android 10 手势导航 | 只有一条细线 | 48dp |
| Android 9 三键导航 | 有一条 | 48dp(这条碰巧对了) |
| 实体 Home / mBack 的老机型 | 根本没有软导航栏 | 48dp |
同一份注释里还写着「手势导航下导航栏高度是很小的,不能写死 48dp」——
可那条约束只落实在 SDK ≥ 30 那一支,else 里照旧写死。「有的手机上」就是它们。
改法三处:
-
原生侧从「查配置」改成「量」。 拿 WebView 自己在屏幕坐标里的下沿去比屏幕
真实底边(getLocationOnScreenvsgetRealMetrics):- 够不到 → 系统已经留白了,页面自己不用再让,返回 0;
- 够得到 → 页面确实铺到了屏幕底,这时只看系统条这会儿画没画出来,
没画返回 0,画了才返回它真实高度; - 布局还没走完时不猜,同样返回 0 —— 宁可贴底,也不要让底栏飘在半空中。
这套判据不依赖任何「照厂商配置估算」的量,ROM 怎么改都一样成立。
-
CSS 把「底栏离屏幕底边」抽成
--nav-bottom单独一条。 玻璃照旧用
--safe-b,朴素钉死为 0 —— 朴素那一格的语义本来就是「贴着屏幕底边」,
从此不再受原生那个数的影响。 -
前端加一道防御上限。 异常 ROM 报出几百 px 时截断到
min(32px, 屏高的 8%)。
顺带把 .fab、#toast-root、.sel-bar、#view 的 padding-bottom 一起改了 ——
它们都按「底栏上沿」定位,必须跟着同一个量走,否则就变成「底栏贴住了底、
内容却多空出一截」。
会不会被系统条压住?只有在「页面真的铺到屏幕最底 + 三键导航」时才可能,而这时
系统条是浮在页面之上的一层(导航栏已被设为透明),底栏主体仍然是完整的。
相比之下「离底一大截」是实实在在的错位,两者不在一个量级上。
代码自校验(不需要服务器)
包内自带两道:签名自校验(证书指纹不符直接拒绝运行)+ 源码指纹
(对源码文件算整体 SHA-256 并内嵌,被改过就报出来)。本次源码指纹(SRC_SHA256):
270b231eb90d63da48be74fa51c7f2f260d36a5a4d85fb441888a6d9e0d420fa
安装信息
| 项 | 值 |
|---|---|
| 文件名 | githup-1.2.9.apk |
| 大小 | 923,690 字节(约 902 KB) |
| SHA-256 | 69ba30e1cb5bad00a2fba9a834d7fc9cb3b6671dc63d3457b2702a88cdda456d |
| versionName / versionCode | 1.2.9 / 1002009 |
| 应用名 / 包名 | githup / com.hubmobile.app |
| 最低系统 | Android 7.0(API 24) |
更新时才提醒,已是最新版完全静默。签名证书与之前一致,可直接覆盖安装,不用卸载。
更新时才提醒,已是最新版完全静默。签名证书与之前一致,可直接覆盖安装,不用卸载。