Repository navigation
Releases
v5.2.0 — Word(.docx)也能在手机上预览了(下载照旧)
手机桥 5.2.0 —— Word(.docx)也能在手机上预览了(下载照旧)。
5.2.0 — 2026-10-23
主题:Word(.docx)也能在手机上预览了(下载照旧) ✓
(用户 2026-10-23:「可以下载,预览」✓)。
为什么不是「再调一次 Word COM」
- 试过 Word COM:
Documents.Open能开 ✓,但SaveAs2(..., wdFormatFilteredHTML)会一直挂着60 秒(实测两次,像卡在不可见对话框)✗ —— 手机等不起 ✓;
- 改走 docx = ZIP + XML:
lib/docx.js自己读word/document.xml✓,
解析段落 / 加粗 / 斜体 / 下划线 / 标题(Heading + 中文「标题 1」)/ 表格(首行当表头)/
内联图片(从word/media/*转 data URI ✓);几百 KB 的文档毫秒级 ✓(实测 40ms / 19ms)。
改了什么
- 新路由
GET /api/preview/word?path=<绝对路径>→ 返回一段静态 HTML ✓
(文本全转义 ✓、无脚本 ✓、图片 data URI ✓);路径 / 密钥 / 大小边界和/api/file一样 ✓; /api/preview?path=对 docx 回{ok:true, kind:'word', url}✓(前端可用);- 手机页面:
docKindOf认.docx/.docm/.dotx/.dotm→renderWord✓;
显示用<iframe sandbox="allow-same-origin" srcdoc=...>(样式隔离 ✓、不执行文档里的脚本 ✓),
按内容自动长高 ✓;右上角「下载」照旧 ✓(用户明确要的 ✓); - 老式二进制
.doc仍不给预览(照旧下载 ✓);失败时写清原因 + 指向下载 ✓; - 说明页 / README 的「Word 只能下载」文案一起改 ✓;
WHATS_NEW_THEMES那条扩成「PPT · 表格 · Word」✓; - 新增
lib/docx.js(ZIP 读取 / XML 解析 / HTML 生成 / 移动端纸面样式)✓,未新增任何依赖 ✓。
测试
- package 186/186 ✓(+3:Word 路由与边界 ✓ / docx 自己解(含 inflateRawSync 与 document.xml)✓ /
前端 sandbox iframe 与下载入口 ✓)、harness 614 ✓、qr 12 ✓、ui 852/852 ✓(+4:Word 可点条目 /
srcdoc 排版内容 / sandbox 不带 allow-scripts / 下载入口 ✓)、security 54 ✓、crashprobe 17 ✓、
addresses 16 ✓、offline 17 ✓、platform 27 ✓ → 九个套件 1795 项全绿 ✓;check-countsexit 0 ✓。 - 线上实测:
sample-min.docx(标题+加粗+表格)→ 200、40ms、h1/strong/table 都在 ✓;
sample-image.docx→ 200、19ms、data:image/png✓;/api/preview元信息 ✓;拿 pptx 调 word 路由 → 415 ✓。
用户要做的
- 手机上刷新一次页面(版本 5.2.0)→ 会话里出现
.docx时点「打开 ›」直接看排版;右上角仍可下载 ✓。
v5.1.0 — PPT 与表格在手机上直接渲染(电脑转一次、手机看图片/真表格)
手机桥 5.1.0 —— PPT 与表格在手机上直接渲染(电脑转一次、手机看图片/真表格)。
5.1.0 — 2026-10-23
主题:PPT 与表格在手机上直接渲染(电脑转一次、手机看图片/真表格) ✓
(用户 2026-10-23:「手机桥帮我加上 ppt 和表格渲染功能」✓)。
原来什么样、为什么不能直接做
- 文档预览(2.1.0)里,
pptx/xlsx落在「Office 文档」那一档:页面只说「浏览器渲染不了」,
给「下载」让用户用手机上的 WPS/Office 打开 ✗ —— 用户要的是「在手机桥里就能看」✓; - 手机浏览器没有 pptx/xlsx 渲染器 ✗,所以只能在电脑上转 ✓:
- PPT → PowerPoint COM 逐页导出 JPG(1280×720,按页面比例 ✓;最多 40 页 ✓);
- Excel → Excel COM 读
UsedRange.Value2→ 行×列(最多 300×30 ✓,多张表给页签 ✓); - CSV/TSV → 服务端直接解析(引号/逗号/换行 ✓,不用启 Excel ✓)。
改了什么
- 新接口(都要登录 cookie,和
/api/file同一条边界 ✓):GET /api/preview?path=<绝对路径>→ PPT 回{count,exported,truncated,...}、表格回{sheets:[{name,rows,...}]};GET /api/preview/asset?path=...&n=<第几页>→ 幻灯片 JPG;
- 安全:只认
ppt/pptx/pptm、xls/xlsx/xlsm/xlsb、csv/tsv白名单 ✓;必须在桥认识的目录里 ✓;
密钥类文件 403 ✓;超过 25MB 文档上限 413 ✓;资产只能从这个接口算出来的缓存目录取 ✓; - 转换在
tools/office-preview.ps1(PowerPoint / Excel COM),串行执行 ✓(COM 并发会互卡 ✗),
产物缓存到~/.dsh/mobile-bridge/preview-cache/<sha1(路径+大小+mtime)>✓ —— 第二次打开不再启 Office ✓
(实测 PPT 首开 2.9s、缓存 27ms ✓)、缓存自动清理(>60 份或 >7 天 ✓); - 手机页面:PPT → 左右滑 / 上一页下一页 的逐页图片 + 页码 ✓;Excel/CSV → 真表格
(表头固定、宽表横滑、多表页签 ✓);失败时说清原因 + 指向上角「下载」 ✓(不留白屏 ✗); - 说明页 / README 的「Office 只能下载」文案一起改掉 ✓;
WHATS_NEW_THEMES加「📊 PPT · 表格」一条 ✓。
测试
- package 183/183 ✓(+6:预览路由 ✓ / 边界检查 ✓ / 转换脚本 COM+JPG+result.json ✓ / 缓存与串行 ✓ /
前端两种渲染 ✓ / 失败不沉默 ✓)、harness 614 ✓、qr 12 ✓、ui 848/848 ✓(+7:Excel 真表格 / 截断照实说 /
横滑容器 / PPT 逐页图片 / 页码 / 翻页 / 页数截断 ✓)、security 54 ✓、crashprobe 17 ✓、addresses 16 ✓、
offline 17 ✓、platform 27 ✓ → 九个套件 1788 项全绿 ✓;check-countsexit 0 ✓。 - 线上实测(真文件):
motion-probe.pptx→ 2 页、首开 2.9s / 缓存 27ms、/api/preview/asset回image/jpeg✓;
rawdata.xlsx→ 1 张表 170 行 × 20 列(超上限会标truncated)✓;CSV → 4×4 表格 ✓;
未登录 401 ✓、密钥文件 403 ✓、目录外 403 ✓、不支持类型 415 ✓、不存在 404 ✓。
用户要做的
- 手机上刷新一次页面(版本变 5.1.0)→ 会话里出现 PPT / Excel / CSV 时点「打开 ›」就能直接看 ✓;
- 第一次打开 PPT/Excel 会慢几秒(要叫起 PowerPoint/Excel);之后同一份文件秒开 ✓。
v5.0.5 — 狼人杀面板黑屏(iOS 上同页 iframe 不画)+ 黑屏兜底
手机桥 5.0.5 —— 狼人杀面板黑屏(iOS 上同页 iframe 不画)+ 黑屏兜底。
5.0.5 — 2026-10-20
主题:狼人杀面板黑屏(iOS 上同页 iframe 不画)+ 黑屏兜底 ✓
(用户 2026-10-20:「这个新增的狼人杀是黑屏的,你来帮我继续做」✓)。
现场与判断(从桥日志对出来的 ✓)
- 手机确实打开了面板 ✓:19:52:32
GET /panel/wolf/index.html(来自手机 IP ✓、200 ✓); - 但之后没有一条
state.json请求 ✓ —— 桌面 Chrome 打开同一个面板时,面板脚本立刻会轮询
state.json(实测 4 次 ✓)→ 手机那边要么脚本没跑 ✗、要么整块 iframe 没画 ✗; - 桌面面板本身是好的 ✓(headless Chrome 加载同一文件:空状态/牌桌都正常 ✓、无 JS 异常 ✓);
- 这台机器上复现不了 iOS 的绘制差异 ✓ —— 但这类"桌面正常、iPhone 黑屏"最常见的一条是:
iOS Safari 在祖先带backdrop-filter时可能整块不绘制 iframe ✗;而面板那张.sheet正好带毛玻璃 ✓。
改(两条一起上 ✓)
- 面板这张纸撤掉毛玻璃 + 给不透明底 ✓:
#panelopen { background: var(--bg); backdrop-filter: none }、
#panelFrame { background: var(--bg) !important }—— 面板是全屏交互页,玻璃模糊本来就没意义 ✗; - 同源父页黑屏兜底 ✓(面板与手机页同源 → 父页能读
contentDocument✓):
打开 4 秒后里面一个字都没有 → 自动重载 iframe 一次 ✓(并reportToBridge('panel-blank-retry')✓);
重载后还是空 → 面板下面明说「面板没显示出来」+「重试」✓ —— 别让用户对着黑屏猜 ✗;
readyState !== 'complete'不算空 ✓(别把"正在下载"当黑屏重载 ✗)。 - 面板自己(
~/.dsh/mobile-panels/wolf/index.html✓ 用户目录,不在插件包里)也补了自查 ✓:
未捕获错误画成看得见的一屏说明 ✓ + 报/api/clientlog✓;打开时报 UA / 视口 / iframe 高度 ✓,
第一次读局面无论成败都报一行 ✓ —— 下次真出问题,电脑mobile-bridge.log里直接能看到原因 ✓。
测试
- package 177/177 ✓(+3:撤模糊 + 不透明底 ✓ / 黑屏兜底与重试 ✓ / 不误伤加载中 + 写日志 ✓)、
harness 614 ✓、qr 12 ✓、ui 841 ✓、security 54 ✓、crashprobe 17 ✓、addresses 16 ✓、offline 17 ✓、platform 27 ✓
→ 九个套件 1775 项全绿 ✓;check-countsexit 0 ✓。
用户要做的
- 手机上重新打开一次 🐺 ✓;如果面板还是黑的,现在它会自己说出来(并且重载一次 ✓),
把屏幕上那行字(或「没显示出来」提示)发我,我按mobile-bridge.log里的client wolf-panel-*继续查 ✓。
v5.0.4 — 本地 IndexedDB 卡住时,启动必须让路(修「打开了但一直转,桥一条 `/api/boot` 都没有」)
手机桥 5.0.4 —— 本地 IndexedDB 卡住时,启动必须让路(修「打开了但一直转,桥一条 /api/boot 都没有」)。
5.0.4 — 2026-10-20
主题:本地 IndexedDB 卡住时,启动必须让路(修「打开了但一直转,桥一条 /api/boot 都没有」) ✓
(用户 2026-10-20:「我新加了狼人杀以后手机桥又打不开了」✓)。
现场(从访问日志 + 真浏览器复现对出来的 ✓,不是猜的 ✗)
- 手机请求到了桥 ✓:
GET /(页面)✓、GET /manifest.webmanifest✓ —— 页面能打开 ✓; - 但桥这边一条
/api/boot都没有 ✗(正常页面打开后第一个动作就是它 ✓)→ 所以卡在页面自己的 JS ✓,
不是网络、不是 Funnel(本轮用真 Chrome 走公网入口实测:/api/boot200 ✓、/api/stream200 ✓); - 页面启动顺序(
bootRun()✓):先await loadTranscriptSnapshot(remembered)(读本地对话缓存 ✓),
然后才发/api/boot✓; - 真凶:
openBgDb()里indexedDB.open('dshm-media', 2)在另一个连接还开着旧版本时,
浏览器只发onblocked✓ —— 既不success也不error✗,而代码没处理onblocked、也没有超时 ✗
→ 这个await永远不返回 ✗ →/api/boot根本没发出去 ✓。
现场很容易出现:PWA 在后台还留着一份旧页面 ✓ / 旧缓存页开着 ✓ / DB 从 v1 升 v2 之后(v2 才加 transcripts ✓); - 所以"新加了狼人杀"不是原因 ✓(只是恰好同一段时间里桥重启过 ✓、旧页面实例还在 ✓)——
但确实是添加新面板 / 多开页面之后更容易撞上 ✓。
改(一条主线:本地缓存只配加速 ✓,绝不能卡启动 ✗)
openBgDb():加request.onblocked✓ + 硬超时 2 秒(IDB_TIMEOUT_MS✓);idbRun():事务也加超时 ✓ +tx.onabort✓(页面被系统挂起 / 有连接卡着时不会永远等 ✗);bootRun():读快照外面再包一层 1.5 秒总超时 ✓ —— 超时就当没有缓存 ✓,
照常发/api/boot✓(缓存读不出来必须让路 ✓,不能为了"秒开"把整个页面卡死 ✗)。
测试
- package 174/174 ✓(+3:
onblocked+ 硬超时 ✓ / 事务超时 +onabort✓ / 启动最多等 1.5 秒 ✓)、
harness 614/614 ✓、qr 12 ✓、ui 841 ✓、security 54 ✓、crashprobe 17 ✓、addresses 16 ✓、offline 17 ✓、platform 27 ✓
→ 九个套件 1772 项全绿 ✓;check-countsexit 0 ✓。 - 真机等价复现(Chrome headless + CDP ✓):故意用一个旧连接占着 DB v1 ✓ → 打开手机页 →
旧代码会永远卡在"正在启动" ✗;新代码 1.5 秒后继续 ✓、/api/boot200 ✓、页面进得去 ✓。
用户要做的
- 手机重新打开一次(PWA 会把新页面换掉 ✓);如果还卡在加载页,下拉刷新一次 ✓。
v5.0.3 — 外网自检不再被 MagicDNS 骗 + 探不通时直接给修法(重开 Funnel)
手机桥 5.0.3 —— 外网自检不再被 MagicDNS 骗 + 探不通时直接给修法(重开 Funnel)。
5.0.3 — 2026-10-19
主题:外网自检不再被 MagicDNS 骗 + 探不通时直接给修法(重开 Funnel) ✓
(用户 2026-10-19:「连了几秒后跳转连不上电脑页面」✓、「你修好他」✓)。
现场(22:00 左右对出来的 ✓,不是猜的 ✗)
- 手机:页面外壳能出来(缓存 ✓),几秒后跳「连不上电脑」✓ —— 说明
/api/boot到不了桥 ✓; - 桥这边:20:15 之后一条手机请求都没有 ✓(日志里只剩每 2 分钟一次的隧道探针 ✓);
- 深挖 Tailscale:
tailscale funnel status一直显示 Funnel on → 127.0.0.1:3081 ✓(看起来正常 ✗);- 公网 ingress IP(
103.84.155.153/.217、IPv62403:2500:400:20::e8e/::25a):
TCP 443 能连、TLS 握手被 reset ✗; - 直连 tailnet IP
100.122.188.30:443的 TLS 是好的 ✓(证书crmer.tailc2a211.ts.net,有效期到 2027-01-02 ✓)
→ 节点没坏,坏的是 ingress → node 那段转发 ✓(WiFi 漫游 / 换 IP 之后卡死的 ✓); - 外部旁证:让 hackertarget(美国)代取
https://crmer.tailc2a211.ts.net/→ 连不上 ✗;
- 修复(现场做的 ✓,不需要管理员 ✓):
tailscale funnel --https=443 off+tailscale funnel --bg 3081
→ 强制走公网 ingress IP 再取整页:HTTP 200 / 831012 字节 / 3.5s ✓;hackertarget 再到 → 拿到桥的
HTTP/1.1 404+ 头 ✓(外网真的通了 ✓)。
🔴 桥这边的假绿(这一版修的就是它 ✗)
桥原来的隧道自检是从本机 fetch publicUrl ✓ —— 本机 *.ts.net 走 MagicDNS,解析成 100.x ✗
→ 绕过了 Funnel 的 ingress ✓ → 所以它一直报「外网可达 ✓」,而手机根本连不上 ✗✗。
改法(probeTunnel ✓):
- 先看本机解析结果 ✓:解析到 Tailscale 网段(
100.64/10/fd7a:115c:a1e0::/48✓)
⇒ 只是 MagicDNS 抄近路 ✓ → 改用公网 DoH(AliDNS223.5.5.5/ doh.pub ✓)查出真正的 ingress IP ✓; - 再用
https.request强制连那个 IP、servername/Host仍写域名 ✓(=curl --resolve✓)——
探的就是"外网进来那一段" ✓; - 公网 DNS 也不通 ⇒ 记
unverifiable✓,自检/徽标说「没法从本机验证外网入口」✓ ——
宁可说"不知道",也不许报"可达" ✗; - 探不通时直接给修法 ✓:自检/徽标/地址卡都写「在电脑上跑
tailscale funnel --bg <端口>重开一次」✓
(这次现场就是它救回来的 ✓);同时保留"手机可能仍能连"(本机网络问题也会让探测失败 ✓); - 探 成功 时说清楚是从公网入口 IP 实测的 ✓(不再是 MagicDNS 近路 ✓)。
测试
- package 171/171 ✓(+3:MagicDNS 时走公网 DoH + 强制 ingress ✓ / 公网 DNS 不通不许假绿 ✓ / 失败给 Funnel 修法 ✓)、
harness 614/614 ✓、qr 12 ✓、ui 841 ✓、security 54 ✓、crashprobe 17 ✓、addresses 16 ✓、offline 17 ✓、platform 27 ✓
→ 九个套件 1769 项全绿 ✓;check-countsexit 0 ✓。 - harness 那条"配了外网时自检必须报不通"仍然绿 ✓(
example.ts.net解析不到 → 走普通 fetch 路径 → 桩拒绝 ✓)。
如实说明(没做 / 不能保证的 ✗)
- 不会自动帮你重开 Funnel ✗ —— 那要动系统服务/外部命令 ✓,插件只把命令写清楚 ✓(要不要自动,另说 ✓);
- 本机网络抖动/校园网抽风仍可能让外网探测失败 ✓(所以保留"手机可能仍能连" ✓,不把话说死 ✗);
- 公网 DNS(223.5.5.5 / doh.pub)被墙/被校园网挡时,只能报
unverifiable✓ —— 这是诚实的上限 ✓。
v5.0.2 — 加载页超时 60 秒 → 120 秒;等过 30 秒就说人话
手机桥 5.0.2 —— 加载页超时 60 秒 → 120 秒;等过 30 秒就说人话。
5.0.2 — 2026-10-19
主题:加载页超时 60 秒 → 120 秒;等过 30 秒就说人话 ✓
(用户 2026-10-19 原话:「加载页面又改回30秒了?不是还可以缓存到手机吗,应该是这个问题,
你要把加载时间放宽,今天改过一次了」✓)。
先对一下事实(不是抬杠 ✓,是免得改错地方 ✗)
- 线上页面里的值不是 30 秒 ✓:实测
GET /<secret>/返回的 5.0.1 页面里
REQUEST_TIMEOUT_MS = 60000、API_TIMEOUT_MS = 60000✓(test/ui.mjs也钉着 ✓)。 - "30 秒"有两个来路 ✓:
- 手机上的 PWA(Service Worker)还停在旧版页面 ✓ —— 4.16.2 那版的
API_TIMEOUT_MS就是 30000 ✗,而页面里的常量是跟着版本走的 ✓;
手机连不上时自然拿不到新页面 ✓,于是它一直按旧数字掐 ✓; - 加载页上"已等 N 秒"的读秒 ✓ —— 它本来只是"还活着"的证据 ✓,不是已经超时了 ✓。
- 手机上的 PWA(Service Worker)还停在旧版页面 ✓ —— 4.16.2 那版的
- "缓存到手机"也早就有 ✓:页面本身有 SW 缓存 + ETag(4.16.2 ✓)、对话快照也存本机(切回来秒开 ✓)。
但缓存救不了"请求根本到不了" ✗:手机若还停在旧 IP(20:15 那次 DHCP 换 IP ✓),
连页面都打不开 ✓,缓存里那份旧页面只会按旧超时反复失败 ✓(5.0.1 已把"IP 变了"这件事说清 ✓)。
改(一条主线 + 一处人话 ✓)
REQUEST_TIMEOUT_MS60000 → 120000 ✓(boot / 大快照那条 ✓);API_TIMEOUT_MS60000 → 120000 ✓(prompt 这类长任务 ✓ —— 别再被 30/60 秒掐 ✗);- 加载页等过 30 秒时多一句人话 ✓:「网络很慢,但还在走(最多等 120 秒)」✓
—— 数据显示一直有的 ✓("已等 N 秒" ✓),但以前没人告诉用户"这不是死了" ✓; test/ui.mjs那条"超时 ≤ 60 秒"的旧上限跟着改成 120 秒 ✓(那是注释里写死的既有决定 ✓,
用户第二次点名要放宽 ✓);test/package.mjs加 2 条钉住 120 秒与那句人话 ✓。
测试
- package 168/168 ✓(+2)、harness 614/614 ✓、qr 12 ✓、ui 841 ✓、security 54 ✓、crashprobe 17 ✓、
addresses 16 ✓、offline 17 ✓、platform 27 ✓ → 九个套件 1766 项全绿 ✓;check-countsexit 0 ✓。
用户接下来要做的(只有一步 ✓)
- 手机用固定外网地址(
https://……ts.net/<密钥段>/)打开一次 ✓ —— 旧缓存的页面会拿到新版本 ✓,
超时数字也跟着变成 120 秒 ✓;之后加载页就没那么容易被"慢"打败 ✓。 - 若还是打不开:那说明不是超时 ✗(请求没到桥 ✓),看桌面徽标二维码 / 固定地址 ✓(同 5.0.1 ✓)。
v5.0.1 — 修「手机桥又登不上去了」—— 本机的局域网 IP 变了,手机上的旧网址连不上
手机桥 5.0.1 —— 修「手机桥又登不上去了」—— 本机的局域网 IP 变了,手机上的旧网址连不上。
5.0.1 — 2026-10-19
主题:修「手机桥又登不上去了」—— 本机的局域网 IP 变了,手机上的旧网址连不上 ✓
(用户 2026-10-19 原话:「手机桥又登不上去了,你改一下bug」✓)。
现场(时间线是从系统事件与访问日志里对出来的 ✓,不是猜的 ✗)
- 20:12:47 网卡
Netwtw06报 WiFi 漫游完成(BSS 切换)✓; - 20:15:51 WLAN 的 DHCP 续租拿到新地址 ✓:
113.54.216.122→113.54.205.169✓
(LeaseObtained时间就在这儿 ✓); - 手机最后一次到达桥的时间是 20:04 ✓(
mobile-bridge.log里那个 iPhone 的 IPv6)——
20:15 之后桥一条来自手机的请求都没有 ✓; - 也就是说:手机"加到主屏幕"的那个网址还写着 旧 IP ✗ → 请求在网络上就丢了 ✓,
桥根本没收到 ✓ —— 所以这不是登录接口坏了 ✗(本机 PIN 登录当场 200 ✓、公网*.ts.net也 200 ✓)。
改了什么(桥这边能自救的三处 ✓)
- 启动日志先打「固定地址」 ✓:配了
publicUrl(Tailscale Funnel 的*.ts.net)时,
phone url:先打那条地址固定的 ✓(标着「外网 · 地址固定 ✓ 推荐加到主屏幕」✓);
局域网那几行标清「局域网,本机 IP 一变就失效」✓ —— 以前只打局域网地址 ✗,
用户照着加到主屏幕,一换 IP 就永久打不开 ✓。 phoneHost那条"上次手机连上的地址"会被报出来**(但不偷偷改配置 ✓):
桥每次算地址时比一下 ✓(启动时 +/api/bootstrap✓(回包里多一个phoneHostStale字段 ✓)+
每 2 分钟的隧道探针 ✓);变了就在日志里留一行"旧地址 → 现在不属于本机" ✓、徽标上点名 ✓。
⚠️ 为什么不自动改 ✗:phoneHost的语义是"手机真的从哪个地址连上来" ✓ ——
它可以是 USB 网段 / 热点网段 ✓,手机拔了线就不在网卡列表里** ✓,"不在当前网卡 ≠ 记错了" ✗。
我第一版写成"自动改成当前地址" ✓ → harness 里三条 Host 头学习用例当场红 ✓(把学习机制顶掉了 ✗)→ 收回 ✓。- 桌面徽标把话说清 ✓:有外网地址 → 「✅ 上面那条「外网」地址是固定的 —— 手机加到主屏幕请用它
(下面这条局域网地址会随 IP 变 ✗)」;只有局域网 → 「⚠️ 这条局域网地址会变…换 IP 后扫下面二维码重新打开」✓;
记着的旧地址真没了 → 直接点名「⚠️ 上次手机连上的地址 X 本机已经没有了」✓。 - README 里补了同一句恢复办法 ✓("手机之前能打开、突然打不开了 → 先看本机 IP 有没有变"✓)。
用户这次的恢复办法(一条就够 ✓)
- 电脑左下角蓝色徽标里有一条「🌍 外网 https://……ts.net/…」✓ —— 用手机扫徽标的二维码重新打开一次 ✓;
或者直接把那条 https 地址加到主屏幕 ✓(它地址固定 ✓,以后换 IP / 换 WiFi 都不用改 ✓)。 - 想继续用局域网地址也行 ✓:现在是
http://113.54.205.169:3081/<密钥段>/✓(但这个 IP 以后还会变 ✓)。
测试
- package 166/166 ✓(+3:启动先打固定地址 ✓ / 旧地址只报不改 +
phoneHostStale✓ / 徽标写清"哪条会变"✓)、
harness 614/614 ✓(+2:phoneHostStale真的报出来 ✓、而且配置没被偷偷改✓)、
qr 12 ✓、ui 841 ✓、security 54 ✓、crashprobe 17 ✓、addresses 16 ✓、offline 17 ✓、platform 27 ✓
→ 九个套件 1764 项全绿 ✓;check-countsexit 0 ✓。
如实说明(做不到的那一半 ✗)
- 手机上的旧网址,桥这边推不回去 ✗ —— 它连不上,任何"通知 / 重定向 / 推送"都到不了它 ✓。
所以恢复必须由用户做一次(扫二维码 / 用固定地址重开 ✓);这一版能做的是让旧地址不再被当成推荐 ✓
和把变化记下来 ✓。 - 校园 WiFi 的 DHCP 会一直换 IP ✓ —— 要长期不再遇到,加到主屏幕请用固定外网地址 ✓(
*.ts.net);
局域网 IP 那条只能当"当场用一下"✓。
v5.0.0 — 分享出去的安装文案改成「外网也能连」—— Tailscale 必装 + Funnel + publicUrl + 关 WiFi 验一次
手机桥 5.0.0 —— 分享出去的安装文案改成「外网也能连」—— Tailscale 必装 + Funnel + publicUrl + 关 WiFi 验一次。
5.0.0 — 2026-10-19
主题:分享出去的安装文案改成「外网也能连」—— Tailscale 必装 + Funnel + publicUrl + 关 WiFi 验一次 ✓
(用户 2026-10-19 原话:「这个分享功能里面的 tails 软件必须装上,你改一下,要能从外网连,
然后发布5.0.0版本我就分享给朋友」✓ + 「发布5.0.0的意思是等你做完直接按照现有功能发就行,改一下版本号」✓)。
改了什么(一份内容源 ✓,文本版 / 网页版一起变 ✓)
- Tailscale 从"可选"改成"必装" ✓(
need4 样 → 5 样 ✓):
原来第 4 条写的是「手机和电脑在同一个 WiFi,想在外面用就装 Tailscale(可选)」✗ ——
分享给朋友之后,人家只能同一 WiFi 用 ✓;现在明确写 「Tailscale:必装(电脑)」 ✓,
并说清"不装它就只能同一 WiFi" ✓。 - 步骤 7 小步 → 10 小步 ✓(
steps✓),补上外网那一段:
① 装 Tailscale 并登录(tailscale status能看到本机 ✓);
② 开外网入口tailscale funnel --bg 3081✓(第一次按它给的链接开一次 HTTPS ✓);
③ 把固定 https 地址写进%USERPROFILE%\.dsh\mobile-bridge.json的publicUrl✓、重启 DSH ✓;
④ 手机用流量验一次 ✓(关掉 WiFi、只用流量打开那条 https 地址 ✓ —— 这一步别跳过 ✓);
⑤ 可选替代:手机也装 Tailscale(同一账号)走100.x.x.x的 tailnet 地址 ✓。 - 排错 5 条 → 8 条 ✓(
trouble✓):tailscale不是命令(重开终端 / 完整路径 ✓)、
Funnel 要先开 HTTPS ✓、桌面徽标上没有「🌍 外网 …」那一行(publicUrl 没写对 ✓)、
手机流量打不开但 WiFi 能开(查 Funnel 状态 / DNS ✓)。 - 网页版里裸网址现在点得动 ✓(
rich()把https://…渲染成<a>✓ ——
原来 Tailscale 下载地址只能手抄 ✗)。 - 说明页 / README 同一套说法 ✓:说明页「📤 分享」那一条写明 5.0.0 起文案按"外网也能连"写 ✓;
README 的「想分享给同学」从三步变四步(第 3 步 = Tailscale 必装 + Funnel + publicUrl ✓),
《外网访问》标题从"(可选)"改成"(要分享给朋友 → 这一步必做)" ✓。 - 版本号 4.17.0 → 5.0.0 ✓(用户点名 ✓;源码树与发布树两处一起改 ✓ —— 发版预检会查 ✓)。
测试
- package 163/163 ✓、harness 612/612 ✓、qr 12/12 ✓、ui 841/841 ✓、security 54/54 ✓、
crashprobe 17/17 ✓、addresses 16/16 ✓、offline 17/17 ✓、platform 27/27 ✓
→ 九个套件 1759 项全绿 ✓;check-countsexit 0 ✓。 - 新增钉住(harness +4 ✓):Tailscale 写成必装 ✓(不再出现"可选"那句 ✗)、
外网步骤真有命令 ✓(tailscale funnel+ts.net+ 关 WiFi)、
真写了publicUrl✓、网页版里 Tailscale 下载地址是可点的<a>✓。 - 判据口径同步放宽 ✓(只放宽到"公开域名"):凭据检查仍然一个都不许有 ✗;
白名单只多了tailscale.com(下载地址 ✓)与ts.net(地址占位后缀 ✓,真实电脑名不出现 ✓)。 ⚠️ 顺手修了一条假红 ✓:文案里现在有"/<电脑名>/&✓,网页版会转成"/</&✓;
同源比对原来只剥标签 ✗ → 实体还留着 ⇒ 真机没问题、测试却红 ✓。现在先解实体再比 ✓。
如实说明(没做的 ✗ / 不能保证的)
-
这份文案是说明,不是安装器 ✗ —— 装还得在朋友自己电脑上做 ✓(这一条 4.1.0 起就写在文案里 ✓);
-
Funnel 是公网入口 ✓:网址本身带密钥段 + PIN 双重保护 ✓,但它确实能被公网访问到 ✓
—— 不想暴露就用"手机也装 Tailscale"的 tailnet 地址方案 ✓(文案第 10 步 ✓); -
我没有在另一台朋友的机器上真跑过这份文案 ✗(这台机器上没有第二台环境 ✓)——
命令都是 Tailscale 官方用法 ✓,但真到别人电脑上如果卡住,把报错发回来 ✓。 -
⚠️ 顺手修了一条测试竞态 ✓:/api/welcomed落盘是异步、不阻塞请求的 ✓(persistTokens的串行化设计 ✓),
而那条断言原来在 POST 回来后立刻读文件 ✗ —— 发版负载下红了一次 ✓(真机没问题 ✓、判据也没错 ✓)。
现在最多等 1.5 秒(每 50ms 读一次)✓:等的是"落盘这个动作" ✓,判据一个字没放松 ✓。
v4.17.0 — 刷新回原色 ✓ · 每个功能单独一页 ✓ · 便签/计时器/周报跟着背景上色 ✓ · 手机桌宠下线 ✓
手机桥 4.17.0 —— 刷新回原色 ✓ · 每个功能单独一页 ✓ · 便签/计时器/周报跟着背景上色 ✓ · 手机桌宠下线 ✓。
4.17.0 — 2026-10-19
主题:刷新回原色 ✓ · 每个功能单独一页 ✓ · 便签/计时器/周报跟着背景上色 ✓ · 手机桌宠下线 ✓
(用户 2026-10-19 原话:「而且刷新后界面又回原来颜色了,这个也改」✓、
「然后更多功能里面点击每一个功能时有的没有出现对应的界面要么就是界面被覆盖,统一改为点功能键后出现对应的界面,
每个功能的界面单独做一页」✓、「便签计时器和每日做了什么没有和背景风格一致,你调一下吧」✓、
「手机桌宠不要了,电脑桌宠不用动」✓、「我的余额界面被挡住了」✓)。
① 刷新回原色(根因:配色和"背景能播"被绑在一起 ✗)
- 旧代码:
applyBackdrop()先摘掉主题类 ✓,等背景媒体真的能播 / 能显示了才在reveal()里
加hasbg+bg-live✓ —— 而那套暖色配色正是挂在body.hasbg.bg-live上的 ✓; - 背景视频 428KB / 静止图 141KB ✗ —— 弱网下下不完 ✓ →
reveal()永远不跑 ✗ →
配色一直是默认的 ✓✓ = 用户看到的"刷新后界面又回原来颜色" ✓; - 修法(一处 ✓):两件事分开 ✓ —— 配色一拿到背景信息就挂 ✓(
wantsLive✓);
背景层另给一个bg-shown✓:只有媒体真能显示了才显示 ✓("别露黑屏"那条老规矩不动 ✓); reveal()/conceal()/ CSS 预设(赛博朋克)三处都跟着补/摘bg-shown✓;- 连带改一条 ui 断言:原来钉"一上来不许有 hasbg" ✓ → 改成"配色要在、背景层先不显示" ✓(意图不变 ✓)。
② 每个功能单独一页(根因:层级打平 + 手写清单 ✗)
- 被盖住:
.sheet全是z-index: 50✓,打平时后写的赢 ✗ ——#moreopen写在中间 ✗,
排在它前面的功能页(目标 / 电脑状态 / 背景 / 搜一搜 / 工作流 / 面板 / 自检 / 备份 / 键盘 / 安全)
一点开就被盖住 ✓,排在后面(定时 / 导出 / 控制电脑 / 剪贴板 / 关机…)就正常 ✗
—— 同一个动作一半能用一半不能用 ✓(最难查的那种 ✓);
修:#moreopen { z-index: 49 }✓ —— 它只是入口那一页 ✓,任何功能页都盖得住它 ✓; - 余额被挡住:切页签时只收"最上面那张纸" ✗ —— 从「更多」(49) 点进功能页(50) 是两张叠着 ✓,
只收功能页 ⇒ 底下的「更多」还盖着余额 ✓;而且"收哪些纸"靠一份手写清单 ✗(漏登一张就盖住 ✓,
2.8.0 的secopen/ 2.9.0 的backupopen都漏过 ✓);
修:openToolPanel()改成扫.sheet自动取最上面那张 ✓(moreopen只有当兜底 ✓);
goPage()翻到页面本身(任务 / 新建 / 余额)时closeAllSheets()全收 ✓; ⚠️ 「正在录音不让走」「新建 / 生图草稿先问一句」两条守卫原样保留 ✓(先问、后收 ✓)。
③ 便签 / 计时器 / 周报跟着背景上色
- 面板是独立 iframe ✗ —— 它不继承手机页那套暖色变量 ✓,挂上背景之后手机页是暖色半透明的、
面板却是"深蓝灰一整块" ✗(一眼就看出来 ✓); - 修:面板与手机页同源 ✓ → 父页往面板 document 里注一份主题 ✓
(PANEL_THEME_ID✓:背景透明 + 暖白字 + 金色标题 + 暖色描边 ✓),对所有面板都生效 ✓
(包括以后用户自己装的 ✓ —— 不用改人家的文件 ✗);而且只在挂了背景时才注 ✓、
断线摘背景时会自己撤掉 ✓(MutationObserver盯 body 的 class ✓);iframe 自己的底也改成透明 ✓; - 顺带把写死的深蓝灰换成主题变量 ✓(
.goalcard目标卡 ✓、搜一搜 / 这周学了什么 的卡片 ✓)——
它们原来在暖色背景下就是"一块蓝板子" ✗。
④ 手机桌宠下线(用户:「手机桌宠不要了,电脑桌宠不用动」✓)
- 页面:浮层节点 ✓、CSS ✓、拖动 / 缩放 / 跑动 IIFE ✓、「更多」里的 🐳 键 ✓、发成功 / 失败变表情 ✓、
主脚本拉清单那一半 ✓ —— 全删 ✓; - 宿主:
petsDir/petDir✓、整个 4.15.0 服务段(清单 + 素材 + 白名单)✓、两条路由 ✓、
能力清单里那段介绍 ✓ —— 全删 ✓; - 用例:ui / package / harness 里钉桌宠的整块都拿掉 ✓(历史 CHANGELOG 不动 ✓ —— 那是记录 ✓);
⚠️ 用户本机~/.dsh/mobile-bridge-pets/里的素材一张没删 ✓(那是用户自己的东西 ✓ ——
删功能不删素材 ✓,除非他点名 ✓);电脑网页端那只没动 ✓(用户明确要求 ✓)。- 📌 顺手把超时那段注释里"跟桌宠的对比"改掉 ✓(免得以后有人去找一个不存在的功能 ✓)。
测试
- package 163/163 ✓、harness 608/608 ✓、qr 12/12 ✓、ui 841/841 ✓、security 54/54 ✓、
crashprobe 17/17 ✓、addresses 16/16 ✓、offline 17/17 ✓、platform 27/27 ✓
→ 九个套件 1755 项全绿 ✓;check-countsexit 0 ✓。 - 新增钉住的:配色类不等媒体 ✓ +
bg-shown开关 ✓(package);面板主题注入 / 摘除 ✓ +
「更多」层级 49 ✓ + 抽屉自动取最上面 ✓ + 切页签全收 ✓(package / ui);桌宠整块下线 ✓(package)。
界面上看得到的变化
- 刷新 / 弱网打开:配色直接是那套暖色 ✓(背景照旧等能播才铺 ✓,不会黑屏 ✓);
- 「更多」里点任何一项:对应的界面就在最上面 ✓,按「← 返回」回到「更多」✓;
- 点「余额」:开着的纸全收 ✓,余额页不再被盖住 ✓;
- 「便签」「计时器」、目标卡、搜一搜 / 周报卡:都跟背景同一套暖色 ✓;
- 「更多」里没有 🐳 桌宠了 ✓。
v4.16.3 — 修「数据变大后连到一半就断」—— 客户端超时太短(20 秒)
手机桥 4.16.3 —— 修「数据变大后连到一半就断」—— 客户端超时太短(20 秒)。
4.16.3 — 2026-10-19
主题:修「数据变大后连到一半就断」—— 客户端超时太短(20 秒) ✓
(用户 2026-10-19:「数据变大了后就连上桥手机容易连到一半就断,能不能把数据缓存到手机上,
然后延长连接时间让手机可以顺利连上而不是不到一半就断」✓)。
先澄清两件不是原因的(都实测过 ✓)
- 隧道 ✓:
tailscale funnel status→ Funnel on ✓;走外网取页面 → HTTP 200 / 599823 字节 ✓;
隧道健康探针每 2 分钟成功一次 ✓ → 隧道是好的 ✓(面板上那句红字是 4.16.1 记过的已知误报 ✗,
前提是手机跑的是旧页面 ✓ → 硬刷新一次即可 ✓); - 桌宠 ✗:它现在正加载着 ✓ 而隧道正通着 ✓ —— 直接反证 ✓;而且它是界面插件 ✓,不碰网络 ✓。
真凶(两个数字 ✗)
- 页面 gzip 后 295446 字节 ✓(原 601658 ✓)、会话快照两百多 KB ✓(代码注释里自己写着 ✓);
- 弱网实测 20~70 KB/s ✓ → 一次 boot 要十几到几十秒 ✓;
- 而客户端
REQUEST_TIMEOUT_MS = 20000✗ → 传到一半ontimeout掐掉 ✗✓ = "连到一半就断" ✓✓。
改(两个常量 ✓)
REQUEST_TIMEOUT_MS20000 → 60000 ✓(boot / 大快照那条 ✓);API_TIMEOUT_MS30000 → 60000 ✓(prompt 这种长任务也别被 30 秒掐 ✗);⚠️ 取 60 秒、不是 90 ✓:test/ui.mjs里那条断言早就定下上限 "有超时且 ≤ 60000" ✓ ——
那是项目的既有决定 ✓,我不该为了自己方便去改它 ✓;而 60 秒对 200KB 快照(20KB/s ≈ 10 秒)绰绰有余 ✓✓。
(我第一版写 90 秒 ✓ → ui 那条当场红 ✓ → 按既有上限收回 60 ✓ —— 安全网又救了一次 ✓。)
另一半"缓存到手机":已经在 4.16.2 里做完了 ✓
- 那个提交就是「页面不再每次重下:原来是
cache-control: no-store✗」✓; - 实测线上现在:
cache-control: no-cache, must-revalidate✓ +etag✓ +content-encoding: gzip✓; - ⇒ 手机硬刷新一次拿到新页面之后 ✓,以后每次打开都会带 etag 换 304 ✓ → 几乎零下载 ✓✓。
测试
- ui 849/849 ✓(含那条
boot 请求设了超时✓ —— 现在实测timeout=60000✓); - package 170/170 ✓;页面脚本语法 ✓。
下一步(用户第二件事 ✓)
- 删掉手机桌宠 ✓(用户原话:「顺便把手机桌宠删掉,没什么用」✓)——
足迹已摸清:mobile.html约 8 处(更多纸上的「🐳 桌宠」键 ✓、浮层节点 ✓、跑动模式 CSS ✓、
开心/懊恼反应 ✓、主脚本 IIFE ✓)+index.js的/api/pet那一段 ✓ + 相应用例 ✓。
⚠️ 这属于"多点改动 + 测试要跟着删" ✓ —— 不塞进这一版末尾 ✗,单独一版做 ✓(一次一处 ✓)。
📌~/.dsh/mobile-bridge-pets/里的素材是用户自己的 ✓,删功能不删素材 ✗(除非他点名 ✓)。
修(用户:「还是连接不上」「可以缓存到手机上吗」)
- 🔴 页面不再每次重下:原来是
cache-control: no-store✗ —— 手机每打开一次就重下整页
(gzip 后 285 KB ✗)。慢线路上(Funnel 实测 1–2 KB/s ✗)等于永远打不开 ✓。
现在no-cache+ 强 ETag ✓:内容没变回 304(几十字节 ✓),变了才重下 ✓
—— 既不会拿到旧页面 ✗,也不用重复下载 ✓。 - 🔴 发图片发不出去的真凶:内联发送失败时,离线发件箱先把这条收走了 ✗
→ 于是"自动改走切片上传重发"那条路永远走不到 ✗(最后一条 prompt 还是images: 1✓、
切片 0 片 ✓、屏幕停在「正在发送…」✗)。现在:带图的失败一律先走切片重发 ✓,
切片也失败才进离线发件箱 ✓(带着已经传好的 fileRefs ✓)。 - token 上限收到 14 天(
MAX_TOKEN_TTL_HOURS10,080 → 336 ✓,配置里的 720 一并夹到 336 ✓,
改前已备份 ✓)—— 和测试/文档的口径一致 ✓。
测试
- harness +4:页面带强 ETag ✓/
no-cache且不再是no-store✗/内容没变回 304 且正文 0 字节 ✓/
ETag 不对照常 200 全量 ✓;旧的 "forbids caching outright" 断言改成新口径 ✓。 - ui:那 7 条(切片重发 / 进度 / fileRefs / 三类日志 ✓)全部转绿 ✓。