dsh-in-hand 1.7.4 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/lib/page.html +423 -202
- package/lib/sw.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -451,11 +451,22 @@ PWA 外壳由中继本地提供(而不是转发上游),这样它不随上
|
|
|
451
451
|
|
|
452
452
|
这就是手机页右上角那个双入口胶囊只能"事后切"、不能"事先测"的原因:
|
|
453
453
|
|
|
454
|
-
-
|
|
455
|
-
|
|
456
|
-
-
|
|
457
|
-
|
|
458
|
-
|
|
454
|
+
- 胶囊显示当前走的是哪条路(远端 / 局域网)。**点开是一张菜单**,列出两条入口:
|
|
455
|
+
当前那条标「当前」,另一条标「切换」—— 点另一条 = 签一张票 + 跳一次。
|
|
456
|
+
- **没有任何自动跳转**(v1.7.5 起)。页面加载时只读一次入口清单、只渲染胶囊,
|
|
457
|
+
不允许因为入口判断而导航;也没有定时体检。自动模式被删除的原因写在下面。
|
|
458
|
+
- 两道保险:跳转前在 `sessionStorage['tw-route-jump']` 写时间戳,8 秒内重复点击
|
|
459
|
+
只给静态提示、不签票也不跳;URL 带 `?t=` 落地时立刻 `history.replaceState`
|
|
460
|
+
清掉参数,并把入口锁成当前项(`sessionStorage['tw-route-lock']`)。
|
|
461
|
+
|
|
462
|
+
> **为什么不再"自动切"**:v1.6.0 – v1.7.4 的自动模式用「这条路刚才通不通」猜方向,
|
|
463
|
+
> 而 `apply()` 在初始加载与每 12 秒的体检后都会重新判断,判断出「该换」就改地址。
|
|
464
|
+
> 两条路的信号一旦互相矛盾(一条体检失败、另一条请求报错),页面就会在两个入口之间
|
|
465
|
+
> 无限往返,手机看到的是**地址栏来回变、整页反复刷新**。猜错的代价远大于省下的一次
|
|
466
|
+
> 点击,所以自动只保留在人的脑子里:菜单列出两条路,当前是哪条、另一条在哪,由用户点。
|
|
467
|
+
> `GET /taskwatch/upstream-health` 仍在中继侧保留,但页面不再请求它。
|
|
468
|
+
|
|
469
|
+

|
|
459
470
|
|
|
460
471
|
### 换路为什么不重输配对码
|
|
461
472
|
|
|
@@ -472,8 +483,8 @@ PWA 外壳由中继本地提供(而不是转发上游),这样它不随上
|
|
|
472
483
|
**v1.7.1 起,前两条插件宿主也自己实现了**(同一套语义):`GET /taskwatch/entries`
|
|
473
484
|
回答「现在有哪几条路」,`POST /taskwatch/ticket` 给一张换路票据。
|
|
474
485
|
所以从宿主页面(例如 `http://127.0.0.1:52676/taskwatch`)点「去远端」不再拿到 405 / 404。
|
|
475
|
-
`GET /taskwatch/upstream-health`
|
|
476
|
-
|
|
486
|
+
`GET /taskwatch/upstream-health` 仍只有中继实现,但 **v1.7.5 起页面不再请求它**:
|
|
487
|
+
自动体检切换已删除,来源也不可靠 —— 中继经隧道看到的来源永远是 `127.0.0.1`。
|
|
477
488
|
|
|
478
489
|
#### 票据格式(v1.7.2)
|
|
479
490
|
|
|
@@ -518,8 +529,8 @@ nonce = crypto.randomBytes(16) # base64url 后 22 个字符
|
|
|
518
529
|
|
|
519
530
|
页面侧遇到这两条接口的 **404 / 405** 会进入**静态降级**:不重试、不弹错,胶囊按
|
|
520
531
|
「只有一个入口」隐藏;设置 / 任务详情里留一行说明与一个手动「去远端」链接
|
|
521
|
-
(上次成功的地址记在 `localStorage['tw-remote-url']
|
|
522
|
-
|
|
532
|
+
(上次成功的地址记在 `localStorage['tw-remote-url']`)。其它错误只在入口菜单里
|
|
533
|
+
显示一行静态提示(切菜单即清),不会自动重试,更不会因此导航。
|
|
523
534
|
|
|
524
535
|
> **升级后公网换路仍是 401?** 先看 3099 上的中继进程是不是新代码:它在**启动时**
|
|
525
536
|
> 一次性加载路由与票据密钥,不会热更新。带令牌调一次 `POST /taskwatch/ticket`,
|
package/lib/page.html
CHANGED
|
@@ -4518,288 +4518,510 @@ if ('serviceWorker' in navigator) {
|
|
|
4518
4518
|
}
|
|
4519
4519
|
</script>
|
|
4520
4520
|
<script>
|
|
4521
|
-
/*
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4521
|
+
/* ROUTE-SAFETY-BEGIN
|
|
4522
|
+
── 双入口:清单只读;换路只能由用户点选触发(2026-10-21 v1.7.5)──────────
|
|
4523
|
+
|
|
4524
|
+
为什么以前会反复刷新:v1.6.0 起,页面把「该走哪条路」的判断写进 apply(),
|
|
4525
|
+
而 apply() 不只在点按钮时调用 —— 初始加载读完入口清单会调一次,每 12 秒的
|
|
4526
|
+
上游体检回来还会调一次。判断出「该换」就直接改地址跳走。两条路对「这条路
|
|
4527
|
+
通不通」的判断一旦不一致(一条体检失败、另一条请求报错),A→B→A 就会无限
|
|
4528
|
+
往返,手机看到的就是地址栏来回变、页面反复刷新。
|
|
4529
|
+
|
|
4530
|
+
v1.7.5 把这条自动路径整个删掉,只保留三件事:
|
|
4531
|
+
1. 读一次 GET /taskwatch/entries,把两条路列进胶囊菜单 —— 只渲染,不导航;
|
|
4532
|
+
2. 用户点另一个入口 → 向中继签一张换路票据 → 跳一次;整个脚本只有这一处
|
|
4533
|
+
会改 location;
|
|
4534
|
+
3. 跳之前写 sessionStorage 时间戳:8 秒内已有跳转标记就拒绝再跳,只给静态
|
|
4535
|
+
提示(第二重保险)。
|
|
4536
|
+
另外,带 ?t= 落地时立刻用 history.replaceState 清掉参数,并把入口锁成这
|
|
4537
|
+
一项;锁定后它不再参与任何判断。这里没有定时器,也没有任何自动跳转。
|
|
4538
|
+
|
|
4539
|
+
为什么自动模式被删掉:浏览器禁止 https 页面探测 http:// 的局域网地址,中继
|
|
4540
|
+
经隧道看到的来源又永远是 127.0.0.1,页面拿不到「手机在哪个网络」这种事实。
|
|
4541
|
+
用「刚才通不通」去猜,只能猜出方向,猜不出「该不该动」—— 猜错的代价是整页
|
|
4542
|
+
反复刷新,远大于省下的一次点击。所以自动只保留在人的脑子里:胶囊列出两条路,
|
|
4543
|
+
当前是哪条、另一条在哪,由用户自己点。
|
|
4544
|
+
*/
|
|
4531
4545
|
(function () {
|
|
4532
4546
|
try {
|
|
4533
|
-
var
|
|
4547
|
+
var LOCK_KEY = 'tw-route-lock'
|
|
4548
|
+
var JUMP_KEY = 'tw-route-jump'
|
|
4549
|
+
var JUMP_GUARD_MS = 8000
|
|
4534
4550
|
var REMOTE_KEY = 'tw-remote-url'
|
|
4535
|
-
|
|
4536
|
-
var
|
|
4537
|
-
var
|
|
4538
|
-
var
|
|
4539
|
-
var
|
|
4551
|
+
|
|
4552
|
+
var items = [] // [{kind,label,url}],顺序固定:远端在前
|
|
4553
|
+
var current = '' // 入口清单 / 地址判断出的当前入口
|
|
4554
|
+
var locked = '' // 票据落地后锁定的入口;锁定期间只信它
|
|
4555
|
+
var landed = false // 本次加载是不是票据落地
|
|
4556
|
+
var degraded = false // 宿主没有换路接口:静态降级,不再重试
|
|
4540
4557
|
var busy = false
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
var
|
|
4545
|
-
var probeOff = false // 宿主没有上游体检:不知道 ≠ 失败,关掉探测别每 12 秒一个 404
|
|
4546
|
-
var timer = 0 // 轮询句柄;降级 / 探测不可用时必须清掉
|
|
4547
|
-
var failAt = 0 // 换路失败后的冷却起点,防止每次轮询都重试同一次失败
|
|
4548
|
-
var toastMsg = ''
|
|
4549
|
-
var toastAt = 0
|
|
4550
|
-
var TOAST_COOLDOWN = 30 * 1000
|
|
4551
|
-
var RETRY_COOLDOWN = 30 * 1000
|
|
4552
|
-
|
|
4553
|
-
/* ROUTE-LOGIC-BEGIN */
|
|
4554
|
-
function decideRoute(c) {
|
|
4555
|
-
if (!c.hasRemote && !c.hasLan) return 'stay'
|
|
4556
|
-
if (c.pref === 'remote') return c.origin === 'remote' ? 'stay' : (c.hasRemote ? 'toRemote' : 'stay')
|
|
4557
|
-
if (c.pref === 'lan') return c.origin === 'lan' ? 'stay' : (c.hasLan ? 'toLan' : 'stay')
|
|
4558
|
-
// auto:只在"这条路确实不行"时才动,绝不把能用的页面换成可能打不开的地址
|
|
4559
|
-
if (c.origin === 'lan' && c.failed) return c.hasRemote ? 'toRemote' : 'stay'
|
|
4560
|
-
if (c.origin === 'remote' && c.upstreamFailed) return c.hasLan ? 'toLan' : 'stay'
|
|
4561
|
-
return 'stay'
|
|
4562
|
-
}
|
|
4563
|
-
/* ROUTE-LOGIC-END */
|
|
4558
|
+
var lastJumpAt = 0 // 同文档内的第二道保险(sessionStorage 不可用时仍有效)
|
|
4559
|
+
var pill = null
|
|
4560
|
+
var menu = null
|
|
4561
|
+
var scrim = null
|
|
4564
4562
|
|
|
4565
|
-
function
|
|
4566
|
-
try {
|
|
4567
|
-
|
|
4563
|
+
function readStore(store, key) {
|
|
4564
|
+
try { return store.getItem(key) || '' } catch (e) { return '' }
|
|
4565
|
+
}
|
|
4566
|
+
function writeStore(store, key, value) {
|
|
4567
|
+
try { store.setItem(key, value) } catch (e) {}
|
|
4568
|
+
}
|
|
4569
|
+
function dropStore(store, key) {
|
|
4570
|
+
try { store.removeItem(key) } catch (e) {}
|
|
4568
4571
|
}
|
|
4569
|
-
function
|
|
4572
|
+
function readSession(key) { return readStore(sessionStorage, key) }
|
|
4573
|
+
function writeSession(key, value) { writeStore(sessionStorage, key, value) }
|
|
4570
4574
|
|
|
4571
|
-
function
|
|
4572
|
-
function writeStore(key, value) { try { localStorage.setItem(key, value) } catch (e) {} }
|
|
4575
|
+
function asKind(v) { return v === 'lan' || v === 'remote' ? v : '' }
|
|
4573
4576
|
|
|
4574
4577
|
/** 上次成功切换 / 打开过的远端地址。只认 http(s),避免 localStorage 被第三方
|
|
4575
4578
|
* 写进 javascript: 一类的东西后,这个链接变成一个可点的执行入口。 */
|
|
4576
4579
|
function rememberedRemote() {
|
|
4577
|
-
var value = readStore(REMOTE_KEY)
|
|
4580
|
+
var value = readStore(localStorage, REMOTE_KEY)
|
|
4578
4581
|
return /^https?:\/\//i.test(value) ? value : ''
|
|
4579
4582
|
}
|
|
4580
4583
|
function rememberRemote(url) {
|
|
4581
4584
|
if (!url) return
|
|
4582
4585
|
var base = String(url).split('?')[0]
|
|
4583
|
-
if (/^https?:\/\//i.test(base)) writeStore(REMOTE_KEY, base)
|
|
4586
|
+
if (/^https?:\/\//i.test(base)) writeStore(localStorage, REMOTE_KEY, base)
|
|
4584
4587
|
}
|
|
4585
4588
|
|
|
4586
|
-
/**
|
|
4587
|
-
function
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
if (
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
el.id = 'tw-route-toast'
|
|
4597
|
-
el.setAttribute('role', 'status')
|
|
4598
|
-
el.setAttribute('aria-live', 'polite')
|
|
4599
|
-
document.body.appendChild(el)
|
|
4589
|
+
/** 把入口 URL 补成「…/taskwatch」。只做显示与同源判断;真正跳转用票据给的地址。 */
|
|
4590
|
+
function withRoute(url) {
|
|
4591
|
+
var s = String(url || '').trim()
|
|
4592
|
+
if (!/^https?:\/\//i.test(s)) return ''
|
|
4593
|
+
if (!/\/taskwatch\/?$/i.test(s)) s = s.replace(/\/+$/, '') + '/taskwatch'
|
|
4594
|
+
return s
|
|
4595
|
+
}
|
|
4596
|
+
function shortUrl(url) {
|
|
4597
|
+
try { return new URL(url, location.href).origin } catch (e) {
|
|
4598
|
+
return String(url || '').replace(/\/taskwatch\/?$/, '')
|
|
4600
4599
|
}
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4600
|
+
}
|
|
4601
|
+
function originOf(url) {
|
|
4602
|
+
try { return new URL(url, location.href).origin } catch (e) { return '' }
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
/** 票据落地:URL 上的 ?t= 立刻清掉,不留在地址栏、历史记录和分享出去的链接里。 */
|
|
4606
|
+
function stripTicketParam() {
|
|
4607
|
+
var u
|
|
4608
|
+
try { u = new URL(location.href) } catch (e) { return false }
|
|
4609
|
+
var params = u.searchParams
|
|
4610
|
+
if (!params.has('t') && !params.has('T')) return false
|
|
4611
|
+
params.delete('t')
|
|
4612
|
+
params.delete('T')
|
|
4613
|
+
var search = params.toString()
|
|
4614
|
+
var clean = u.pathname + (search ? '?' + search : '') + u.hash
|
|
4615
|
+
try { history.replaceState(null, '', clean) } catch (e) {}
|
|
4616
|
+
return true
|
|
4605
4617
|
}
|
|
4606
4618
|
|
|
4607
|
-
function
|
|
4608
|
-
|
|
4619
|
+
function readJump() {
|
|
4620
|
+
var raw = readSession(JUMP_KEY)
|
|
4621
|
+
if (!raw) return null
|
|
4622
|
+
try {
|
|
4623
|
+
var j = JSON.parse(raw)
|
|
4624
|
+
return j && typeof j.at === 'number' ? j : null
|
|
4625
|
+
} catch (e) { return null }
|
|
4609
4626
|
}
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4627
|
+
|
|
4628
|
+
/** 8 秒护栏还差多久(毫秒)。0 = 现在可以跳。时钟回拨按「刚刚跳过」处理。 */
|
|
4629
|
+
function guardWait() {
|
|
4630
|
+
var now = Date.now()
|
|
4631
|
+
var wait = 0
|
|
4632
|
+
var j = readJump()
|
|
4633
|
+
if (j) {
|
|
4634
|
+
wait = j.at > now ? JUMP_GUARD_MS : Math.max(0, JUMP_GUARD_MS - (now - j.at))
|
|
4635
|
+
}
|
|
4636
|
+
if (lastJumpAt) {
|
|
4637
|
+
var mem = lastJumpAt > now ? JUMP_GUARD_MS : Math.max(0, JUMP_GUARD_MS - (now - lastJumpAt))
|
|
4638
|
+
if (mem > wait) wait = mem
|
|
4639
|
+
}
|
|
4640
|
+
return wait
|
|
4613
4641
|
}
|
|
4614
4642
|
|
|
4615
|
-
/**
|
|
4616
|
-
function
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4643
|
+
/** 换路的唯一出口:先过护栏、写带时间戳的标记,然后才跳。 */
|
|
4644
|
+
function navigateOnce(url, kind) {
|
|
4645
|
+
var wait = guardWait()
|
|
4646
|
+
if (wait > 0) {
|
|
4647
|
+
busy = false
|
|
4648
|
+
status('刚刚切换过,' + Math.ceil(wait / 1000) + ' 秒后才能再切')
|
|
4649
|
+
return false
|
|
4650
|
+
}
|
|
4651
|
+
var at = Date.now()
|
|
4652
|
+
lastJumpAt = at
|
|
4653
|
+
writeSession(JUMP_KEY, JSON.stringify({ at: at, to: kind }))
|
|
4654
|
+
location.href = url
|
|
4655
|
+
return true
|
|
4622
4656
|
}
|
|
4623
4657
|
|
|
4624
|
-
function isGone(
|
|
4658
|
+
function isGone(code) { return code === 404 || code === 405 }
|
|
4659
|
+
|
|
4660
|
+
function currentKind() {
|
|
4661
|
+
// 地址是事实;地址匹配不上(多网卡 / 服务端看到的 Host 不可信)才用票据锁。
|
|
4662
|
+
var here = guessCurrent()
|
|
4663
|
+
if (here) return here
|
|
4664
|
+
if (locked && hasKind(locked)) return locked
|
|
4665
|
+
return current
|
|
4666
|
+
}
|
|
4667
|
+
function hasKind(kind) {
|
|
4668
|
+
for (var i = 0; i < items.length; i++) if (items[i].kind === kind) return true
|
|
4669
|
+
return false
|
|
4670
|
+
}
|
|
4671
|
+
function itemOf(kind) {
|
|
4672
|
+
for (var i = 0; i < items.length; i++) if (items[i].kind === kind) return items[i]
|
|
4673
|
+
return null
|
|
4674
|
+
}
|
|
4675
|
+
function hasChoice() { return items.length >= 2 }
|
|
4625
4676
|
|
|
4626
4677
|
/** 兼容两种 entries 形状:中继的 {remote, lan, current} 与新契约的
|
|
4627
4678
|
* {entries:[{kind, label, url, current}]}。同一份页面在中继和宿主下都能跑。 */
|
|
4628
4679
|
function parseEntries(j) {
|
|
4629
4680
|
if (!j || j.ok !== true) return false
|
|
4630
|
-
var list =
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
var
|
|
4634
|
-
var
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
if (
|
|
4638
|
-
if (e.kind
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4681
|
+
var list = []
|
|
4682
|
+
var declared = ''
|
|
4683
|
+
if (Array.isArray(j.entries)) {
|
|
4684
|
+
var seenKind = {}
|
|
4685
|
+
for (var i = 0; i < j.entries.length; i++) {
|
|
4686
|
+
var e = j.entries[i]
|
|
4687
|
+
if (!e || typeof e.url !== 'string') continue
|
|
4688
|
+
if (e.kind !== 'remote' && e.kind !== 'lan') continue
|
|
4689
|
+
if (seenKind[e.kind]) continue
|
|
4690
|
+
var url = withRoute(e.url)
|
|
4691
|
+
if (!url) continue
|
|
4692
|
+
seenKind[e.kind] = true
|
|
4693
|
+
list.push({
|
|
4694
|
+
kind: e.kind,
|
|
4695
|
+
label: typeof e.label === 'string' ? e.label : '',
|
|
4696
|
+
url: url,
|
|
4697
|
+
})
|
|
4698
|
+
if (e.current === true) declared = e.kind
|
|
4642
4699
|
}
|
|
4643
|
-
REMOTE = rem
|
|
4644
|
-
LAN = lan
|
|
4645
|
-
ORIGIN = current || (lan.length ? 'lan' : (rem ? 'remote' : 'unknown'))
|
|
4646
4700
|
} else {
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4701
|
+
var remote = withRoute(j.remote)
|
|
4702
|
+
if (remote) list.push({ kind: 'remote', label: '', url: remote })
|
|
4703
|
+
if (Array.isArray(j.lan)) {
|
|
4704
|
+
var seen = {}
|
|
4705
|
+
for (var k = 0; k < j.lan.length; k++) {
|
|
4706
|
+
var lanUrl = withRoute(j.lan[k])
|
|
4707
|
+
if (!lanUrl || seen[lanUrl]) continue
|
|
4708
|
+
seen[lanUrl] = true
|
|
4709
|
+
list.push({ kind: 'lan', label: '', url: lanUrl })
|
|
4710
|
+
break // 胶囊只需要一条局域网入口,多网卡不在这里堆
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
declared = asKind(j.current)
|
|
4651
4714
|
}
|
|
4652
|
-
if (
|
|
4715
|
+
if (!list.length) return false
|
|
4716
|
+
// 远端永远排第一,局域网第二:顺序稳定,用户不必每次重新找。
|
|
4717
|
+
list.sort(function (a, b) {
|
|
4718
|
+
if (a.kind === b.kind) return 0
|
|
4719
|
+
return a.kind === 'remote' ? -1 : 1
|
|
4720
|
+
})
|
|
4721
|
+
items = list
|
|
4722
|
+
current = guessCurrent() || declared
|
|
4723
|
+
if (landed) {
|
|
4724
|
+
if (!locked) locked = current
|
|
4725
|
+
if (locked) writeSession(LOCK_KEY, locked)
|
|
4726
|
+
}
|
|
4727
|
+
if (current === 'remote') rememberRemote(location.origin + '/taskwatch')
|
|
4653
4728
|
return true
|
|
4654
4729
|
}
|
|
4655
4730
|
|
|
4731
|
+
/** 「我现在在哪条路上」优先按地址判断,不轻信服务端看到的 Host:
|
|
4732
|
+
* 中继经隧道看到的来源永远是 127.0.0.1,Host 判断可能落到别的分支。 */
|
|
4733
|
+
function guessCurrent() {
|
|
4734
|
+
var here = location.origin
|
|
4735
|
+
for (var i = 0; i < items.length; i++) {
|
|
4736
|
+
if (originOf(items[i].url) === here) return items[i].kind
|
|
4737
|
+
}
|
|
4738
|
+
return ''
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
function status(message) {
|
|
4742
|
+
var el = menu && menu.querySelector('.tw-mstatus')
|
|
4743
|
+
if (!el) return
|
|
4744
|
+
el.textContent = message || ''
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
/** 降级:宿主没有换路接口。清掉胶囊、不报错、不重试。 */
|
|
4748
|
+
function degrade() {
|
|
4749
|
+
if (degraded) return
|
|
4750
|
+
degraded = true
|
|
4751
|
+
closeMenu()
|
|
4752
|
+
if (pill) { pill.style.display = 'none'; pill.setAttribute('aria-hidden', 'true') }
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4656
4755
|
/** 降级态在设置 / 详情里的一行说明。宿主页面没有中继接口这件事本身不是错误,
|
|
4657
4756
|
* 所以这里只陈述事实,并把「上次成功用过的远端地址」留成手动入口。 */
|
|
4658
4757
|
function noteHtml() {
|
|
4659
|
-
if (!
|
|
4758
|
+
if (!degraded) return ''
|
|
4660
4759
|
var last = rememberedRemote()
|
|
4661
4760
|
var text = '当前页面由插件宿主提供,远端切换需要中继接口'
|
|
4662
4761
|
if (last) text += ';也可直接打开 <a href="' + esc(last) + '">' + esc(last) + '</a>'
|
|
4663
4762
|
return '<div class="note route-note">' + text + '</div>'
|
|
4664
4763
|
}
|
|
4764
|
+
function esc(s) {
|
|
4765
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
4766
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
4767
|
+
})
|
|
4768
|
+
}
|
|
4665
4769
|
window.twRouteNote = noteHtml
|
|
4666
4770
|
|
|
4667
4771
|
function mount() {
|
|
4668
|
-
//
|
|
4669
|
-
//
|
|
4670
|
-
// 一个块级元素 —— 在旧截图里正好压在代码块顶部。现在它只是 .hrow 的
|
|
4671
|
-
// 一个 flex 子项,不参与任何绝对/浮动定位,结构上不可能盖住内容。
|
|
4772
|
+
// 胶囊待的**标题行**里(标题与连接三点之间)。它只是 .hrow 的 flex 子项,
|
|
4773
|
+
// 不参与绝对/浮动定位,结构上不可能盖住内容。
|
|
4672
4774
|
var host = document.getElementById('hrow') || document.querySelector('.hrow') || document.querySelector('header') || document.body
|
|
4673
4775
|
var before = document.getElementById('netq') || document.getElementById('newBtn') || null
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
setPref(p === 'auto' ? 'lan' : (p === 'lan' ? 'remote' : 'auto'))
|
|
4683
|
-
apply()
|
|
4684
|
-
})
|
|
4776
|
+
if (!pill) {
|
|
4777
|
+
pill = document.createElement('button')
|
|
4778
|
+
pill.id = 'tw-route-pill'
|
|
4779
|
+
pill.type = 'button'
|
|
4780
|
+
pill.setAttribute('aria-haspopup', 'dialog')
|
|
4781
|
+
pill.setAttribute('aria-expanded', 'false')
|
|
4782
|
+
pill.setAttribute('aria-label', '连接入口')
|
|
4783
|
+
pill.addEventListener('click', openMenu)
|
|
4685
4784
|
}
|
|
4686
|
-
if (
|
|
4687
|
-
if (before && before.parentNode === host) host.insertBefore(
|
|
4688
|
-
else host.appendChild(
|
|
4785
|
+
if (pill.parentNode !== host || (before && pill.nextSibling !== before)) {
|
|
4786
|
+
if (before && before.parentNode === host) host.insertBefore(pill, before)
|
|
4787
|
+
else host.appendChild(pill)
|
|
4689
4788
|
}
|
|
4690
|
-
return
|
|
4789
|
+
return pill
|
|
4691
4790
|
}
|
|
4692
4791
|
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4792
|
+
function buildMenu() {
|
|
4793
|
+
if (menu) return
|
|
4794
|
+
scrim = document.createElement('div')
|
|
4795
|
+
scrim.id = 'tw-route-scrim'
|
|
4796
|
+
scrim.setAttribute('aria-hidden', 'true')
|
|
4797
|
+
scrim.addEventListener('click', closeMenu)
|
|
4798
|
+
document.body.appendChild(scrim)
|
|
4799
|
+
|
|
4800
|
+
menu = document.createElement('div')
|
|
4801
|
+
menu.id = 'tw-route-menu'
|
|
4802
|
+
menu.setAttribute('role', 'dialog')
|
|
4803
|
+
menu.setAttribute('aria-label', '连接入口')
|
|
4804
|
+
var head = document.createElement('div')
|
|
4805
|
+
head.className = 'tw-mhead'
|
|
4806
|
+
head.textContent = '连接入口'
|
|
4807
|
+
var list = document.createElement('div')
|
|
4808
|
+
list.className = 'tw-mlist'
|
|
4809
|
+
var foot = document.createElement('div')
|
|
4810
|
+
foot.className = 'tw-mfoot'
|
|
4811
|
+
var statusEl = document.createElement('div')
|
|
4812
|
+
statusEl.className = 'tw-mstatus'
|
|
4813
|
+
statusEl.setAttribute('role', 'status')
|
|
4814
|
+
statusEl.setAttribute('aria-live', 'polite')
|
|
4815
|
+
menu.appendChild(head)
|
|
4816
|
+
menu.appendChild(list)
|
|
4817
|
+
menu.appendChild(foot)
|
|
4818
|
+
menu.appendChild(statusEl)
|
|
4819
|
+
document.body.appendChild(menu)
|
|
4820
|
+
document.addEventListener('keydown', onKey)
|
|
4697
4821
|
}
|
|
4698
4822
|
|
|
4699
|
-
function
|
|
4700
|
-
if (
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
var here =
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4823
|
+
function renderMenu() {
|
|
4824
|
+
if (!menu) return
|
|
4825
|
+
var list = menu.querySelector('.tw-mlist')
|
|
4826
|
+
while (list.firstChild) list.removeChild(list.firstChild)
|
|
4827
|
+
var here = currentKind()
|
|
4828
|
+
for (var i = 0; i < items.length; i++) {
|
|
4829
|
+
var it = items[i]
|
|
4830
|
+
var isHere = it.kind === here
|
|
4831
|
+
var row = document.createElement('button')
|
|
4832
|
+
row.type = 'button'
|
|
4833
|
+
row.className = 'tw-entry'
|
|
4834
|
+
row.setAttribute('data-kind', it.kind)
|
|
4835
|
+
row.setAttribute('aria-current', isHere ? 'true' : 'false')
|
|
4836
|
+
var kind = document.createElement('span')
|
|
4837
|
+
kind.className = 'tw-kind'
|
|
4838
|
+
kind.textContent = it.label || (it.kind === 'lan' ? '局域网' : '远端')
|
|
4839
|
+
var url = document.createElement('span')
|
|
4840
|
+
url.className = 'tw-url'
|
|
4841
|
+
url.textContent = shortUrl(it.url)
|
|
4842
|
+
var chip = document.createElement('span')
|
|
4843
|
+
chip.className = 'tw-chip'
|
|
4844
|
+
chip.textContent = isHere ? '当前' : '切换'
|
|
4845
|
+
row.appendChild(kind)
|
|
4846
|
+
row.appendChild(url)
|
|
4847
|
+
row.appendChild(chip)
|
|
4848
|
+
if (!isHere) row.addEventListener('click', onPick)
|
|
4849
|
+
list.appendChild(row)
|
|
4850
|
+
}
|
|
4851
|
+
var foot = menu.querySelector('.tw-mfoot')
|
|
4852
|
+
foot.textContent = locked && locked === here
|
|
4853
|
+
? '已锁定为票据落地入口;页面不会自动切换,只在你点另一个入口时才跳一次。'
|
|
4854
|
+
: '只在你点选时切换;页面不会自动跳转。'
|
|
4707
4855
|
}
|
|
4708
4856
|
|
|
4709
|
-
function
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
var
|
|
4713
|
-
if (
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
var
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
}
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4857
|
+
function positionMenu() {
|
|
4858
|
+
if (!menu || !pill) return
|
|
4859
|
+
var r = pill.getBoundingClientRect()
|
|
4860
|
+
var top = Math.round(r.bottom + 6)
|
|
4861
|
+
if (top < 8) top = 8
|
|
4862
|
+
// 右对齐到胶囊,但**必须夹在视口内**:胶囊在标题行中间偏右,直接按它右对齐
|
|
4863
|
+
// 会把 330px 宽的菜单推到屏幕左侧外面(390 宽实测会裁掉入口名)。
|
|
4864
|
+
var margin = 12
|
|
4865
|
+
var width = menu.offsetWidth || Math.min(330, window.innerWidth - margin * 2)
|
|
4866
|
+
var right = Math.max(margin, Math.round(window.innerWidth - r.right))
|
|
4867
|
+
if (window.innerWidth - right - width < margin) {
|
|
4868
|
+
right = Math.max(margin, window.innerWidth - width - margin)
|
|
4869
|
+
}
|
|
4870
|
+
menu.style.top = top + 'px'
|
|
4871
|
+
menu.style.right = right + 'px'
|
|
4872
|
+
menu.style.left = 'auto'
|
|
4873
|
+
menu.style.maxHeight = Math.max(180, Math.round(window.innerHeight - top - margin)) + 'px'
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
function openMenu() {
|
|
4877
|
+
if (degraded || !hasChoice()) return
|
|
4878
|
+
buildMenu()
|
|
4879
|
+
renderMenu()
|
|
4880
|
+
status('')
|
|
4881
|
+
positionMenu()
|
|
4882
|
+
scrim.classList.add('on')
|
|
4883
|
+
menu.classList.add('on')
|
|
4884
|
+
pill.setAttribute('aria-expanded', 'true')
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
function closeMenu() {
|
|
4888
|
+
if (menu) menu.classList.remove('on')
|
|
4889
|
+
if (scrim) scrim.classList.remove('on')
|
|
4890
|
+
if (pill) pill.setAttribute('aria-expanded', 'false')
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
function onKey(e) {
|
|
4894
|
+
if (e && e.key === 'Escape') closeMenu()
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
function onPick() {
|
|
4898
|
+
jump(this.getAttribute('data-kind'))
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
/** 用户点「另一个入口」:签一张票,跳一次。失败只改菜单里的静态提示。 */
|
|
4902
|
+
function jump(kind) {
|
|
4903
|
+
if (busy || degraded) return
|
|
4904
|
+
var target = itemOf(kind)
|
|
4905
|
+
if (!target) { status('这条入口现在不可用'); return }
|
|
4906
|
+
if (kind === currentKind()) { status('已经在这个入口上'); return }
|
|
4907
|
+
// 同源目标跳过去只是刷新一次页面,没有意义(也会绕开 8 秒护栏的直觉)。
|
|
4908
|
+
if (originOf(target.url) === location.origin) {
|
|
4909
|
+
status('这条入口和当前地址是同一个源,无需切换')
|
|
4910
|
+
return
|
|
4911
|
+
}
|
|
4912
|
+
// 护栏提前到签票之前:被拦下时连票都不该签。
|
|
4913
|
+
var wait = guardWait()
|
|
4914
|
+
if (wait > 0) {
|
|
4915
|
+
status('刚刚切换过,' + Math.ceil(wait / 1000) + ' 秒后才能再切')
|
|
4916
|
+
return
|
|
4917
|
+
}
|
|
4725
4918
|
busy = true
|
|
4919
|
+
status('正在签票…')
|
|
4726
4920
|
fetch('/taskwatch/ticket', {
|
|
4727
4921
|
method: 'POST',
|
|
4728
4922
|
headers: { 'content-type': 'application/json' },
|
|
4729
|
-
body: JSON.stringify({ to:
|
|
4923
|
+
body: JSON.stringify({ to: kind }),
|
|
4730
4924
|
}).then(function (r) {
|
|
4731
4925
|
// 宿主没有换路接口(405)或这条路由不存在(404):静态降级,绝不重试。
|
|
4732
4926
|
if (isGone(r.status)) { degrade(); return null }
|
|
4733
4927
|
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
4734
4928
|
return r.json()
|
|
4735
4929
|
}).then(function (j) {
|
|
4736
|
-
if (j === null)
|
|
4737
|
-
if (j
|
|
4738
|
-
|
|
4739
|
-
|
|
4930
|
+
if (j === null) return
|
|
4931
|
+
if (!j || j.ok !== true || typeof j.url !== 'string' || !/^https?:\/\//i.test(j.url)) {
|
|
4932
|
+
busy = false
|
|
4933
|
+
status((j && j.error) || '没有拿到换路地址')
|
|
4740
4934
|
return
|
|
4741
4935
|
}
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
toast((j && j.error) || '无法切换连接方式')
|
|
4936
|
+
if (kind === 'remote') rememberRemote(j.url)
|
|
4937
|
+
navigateOnce(j.url, kind)
|
|
4745
4938
|
}).catch(function (e) {
|
|
4746
4939
|
busy = false
|
|
4747
|
-
|
|
4748
|
-
toast('切换连接方式失败:' + ((e && e.message) || '网络错误'))
|
|
4940
|
+
status('切换失败:' + ((e && e.message) || '网络错误'))
|
|
4749
4941
|
})
|
|
4750
4942
|
}
|
|
4751
4943
|
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
apply()
|
|
4771
|
-
toast('状态检查失败:' + ((e && e.message) || '网络错误'))
|
|
4772
|
-
})
|
|
4944
|
+
/** 只渲染,不判断、不跳。加载路径上调用它是安全的。 */
|
|
4945
|
+
function apply() {
|
|
4946
|
+
var el = mount()
|
|
4947
|
+
if (degraded) { el.style.display = 'none'; el.setAttribute('aria-hidden', 'true'); return }
|
|
4948
|
+
var here = currentKind()
|
|
4949
|
+
if (!hasChoice() || !here) { el.style.display = 'none'; el.setAttribute('aria-hidden', 'true'); return }
|
|
4950
|
+
var text = here === 'lan' ? '局域网' : '远端'
|
|
4951
|
+
while (el.firstChild) el.removeChild(el.firstChild)
|
|
4952
|
+
var label = document.createElement('span')
|
|
4953
|
+
label.textContent = text
|
|
4954
|
+
var caret = document.createElement('span')
|
|
4955
|
+
caret.className = 'tw-caret'
|
|
4956
|
+
caret.setAttribute('aria-hidden', 'true')
|
|
4957
|
+
el.appendChild(label)
|
|
4958
|
+
el.appendChild(caret)
|
|
4959
|
+
el.removeAttribute('aria-hidden')
|
|
4960
|
+
el.setAttribute('aria-label', '连接入口:' + text + '。点按查看另一条入口')
|
|
4961
|
+
el.style.display = 'inline-flex'
|
|
4773
4962
|
}
|
|
4774
4963
|
|
|
4775
4964
|
// 样式随手挂上,不依赖页面里已有的 class(两份外壳的样式表可能不同)
|
|
4776
4965
|
var css = document.createElement('style')
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
css.textContent = '#tw-route-pill{display:none;align-items:center;gap:6px;'
|
|
4783
|
-
+ 'flex:0 0 auto;margin:0;max-width:36vw;overflow:hidden;text-overflow:ellipsis;'
|
|
4784
|
-
+ 'height:44px;min-height:44px;line-height:1;padding:0 8px;border-radius:999px;'
|
|
4785
|
-
+ 'font:600 12.5px/1.25 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",system-ui,sans-serif;'
|
|
4966
|
+
var FONT = '-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",system-ui,sans-serif'
|
|
4967
|
+
css.textContent = '#tw-route-pill{display:none;align-items:center;gap:5px;'
|
|
4968
|
+
+ 'flex:0 0 auto;margin:0;max-width:38vw;overflow:hidden;'
|
|
4969
|
+
+ 'height:44px;min-height:44px;line-height:1;padding:0 10px;border-radius:999px;'
|
|
4970
|
+
+ 'font:600 12.5px/1.25 ' + FONT + ';'
|
|
4786
4971
|
+ 'color:inherit;background:color-mix(in srgb,currentColor 8%,transparent);'
|
|
4787
4972
|
+ 'border:1px solid color-mix(in srgb,currentColor 24%,transparent);cursor:pointer;white-space:nowrap}'
|
|
4788
|
-
+ '#tw-route-pill[
|
|
4973
|
+
+ '#tw-route-pill[aria-expanded="true"]{background:color-mix(in srgb,currentColor 15%,transparent)}'
|
|
4789
4974
|
+ '#tw-route-pill:active{transform:translateY(1px)}'
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
+ '
|
|
4794
|
-
+ '
|
|
4795
|
-
|
|
4796
|
-
+ '
|
|
4797
|
-
+ '
|
|
4798
|
-
+ '
|
|
4799
|
-
+ '
|
|
4975
|
+
+ '#tw-route-pill .tw-caret{flex:0 0 auto;width:0;height:0;opacity:.62;'
|
|
4976
|
+
+ 'border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid currentColor}'
|
|
4977
|
+
// 点空白处关闭:一层透明遮罩,而不是给 document 挂 click(后者会误吞页面上的点击)。
|
|
4978
|
+
+ '#tw-route-scrim{position:fixed;inset:0;z-index:70;background:rgba(8,10,14,.22);display:none}'
|
|
4979
|
+
+ '#tw-route-scrim.on{display:block}'
|
|
4980
|
+
// 菜单:固定在胶囊下方右对齐;宽度跟着视口收,390 宽下不会顶到屏幕外。
|
|
4981
|
+
+ '#tw-route-menu{position:fixed;z-index:80;display:none;box-sizing:border-box;'
|
|
4982
|
+
+ 'width:min(330px,calc(100vw - 20px));padding:8px;overflow-y:auto;'
|
|
4983
|
+
+ 'border-radius:16px;background:var(--panel,Canvas);color:inherit;'
|
|
4984
|
+
+ 'border:1px solid color-mix(in srgb,currentColor 16%,transparent);'
|
|
4985
|
+
+ 'box-shadow:0 18px 44px -12px rgba(0,0,0,.38),0 2px 8px rgba(0,0,0,.12);'
|
|
4986
|
+
+ 'font:400 13px/1.4 ' + FONT + '}'
|
|
4987
|
+
+ '#tw-route-menu.on{display:block}'
|
|
4988
|
+
+ '#tw-route-menu .tw-mhead{padding:5px 10px 8px;font:600 11.5px/1 ' + FONT + ';'
|
|
4989
|
+
+ 'letter-spacing:.06em;color:color-mix(in srgb,currentColor 48%,transparent)}'
|
|
4990
|
+
+ '#tw-route-menu .tw-entry{display:flex;align-items:center;gap:8px;width:100%;'
|
|
4991
|
+
+ 'min-height:56px;padding:8px 10px;border:0;border-radius:12px;background:none;'
|
|
4992
|
+
+ 'color:inherit;text-align:left;cursor:pointer;font:inherit}'
|
|
4993
|
+
+ '#tw-route-menu .tw-entry+.tw-entry{margin-top:2px}'
|
|
4994
|
+
+ '#tw-route-menu .tw-kind{flex:0 0 auto;font-weight:600;font-size:13.5px}'
|
|
4995
|
+
+ '#tw-route-menu .tw-url{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;'
|
|
4996
|
+
+ 'white-space:nowrap;font-size:11.5px;color:color-mix(in srgb,currentColor 55%,transparent);'
|
|
4997
|
+
+ 'font-variant-numeric:tabular-nums}'
|
|
4998
|
+
+ '#tw-route-menu .tw-chip{flex:0 0 auto;padding:4px 8px;border-radius:999px;'
|
|
4999
|
+
+ 'font-size:11.5px;font-weight:600;white-space:nowrap;'
|
|
5000
|
+
+ 'border:1px solid color-mix(in srgb,currentColor 20%,transparent)}'
|
|
5001
|
+
+ '#tw-route-menu .tw-entry[aria-current="true"]{background:color-mix(in srgb,currentColor 6%,transparent)}'
|
|
5002
|
+
+ '#tw-route-menu .tw-entry[aria-current="true"] .tw-chip{background:color-mix(in srgb,currentColor 10%,transparent);border-color:transparent}'
|
|
5003
|
+
+ '#tw-route-menu .tw-entry:not([aria-current="true"]) .tw-chip{color:var(--accent,#2f6df6)}'
|
|
5004
|
+
+ '#tw-route-menu .tw-entry:not([aria-current="true"]):active{background:color-mix(in srgb,currentColor 10%,transparent)}'
|
|
5005
|
+
+ '#tw-route-menu .tw-mfoot{padding:8px 10px 4px;font-size:11.5px;line-height:1.45;'
|
|
5006
|
+
+ 'color:color-mix(in srgb,currentColor 52%,transparent)}'
|
|
5007
|
+
+ '#tw-route-menu .tw-mstatus{padding:6px 10px 4px;font-size:11.5px;line-height:1.4;'
|
|
5008
|
+
+ 'color:var(--warn,#a35b00)}'
|
|
5009
|
+
+ '#tw-route-menu .tw-mstatus:empty{display:none}'
|
|
5010
|
+
+ '@media (prefers-reduced-motion:reduce){#tw-route-pill:active{transform:none}}'
|
|
4800
5011
|
document.head.appendChild(css)
|
|
4801
5012
|
|
|
4802
|
-
//
|
|
5013
|
+
// ?t= 落地:先把参数清掉并把入口锁成这一项,再去做任何别的事。
|
|
5014
|
+
landed = stripTicketParam()
|
|
5015
|
+
locked = asKind(readSession(LOCK_KEY))
|
|
5016
|
+
if (landed) {
|
|
5017
|
+
var jumped = readJump()
|
|
5018
|
+
var jumpKind = jumped ? asKind(jumped.to) : ''
|
|
5019
|
+
if (jumpKind) locked = jumpKind
|
|
5020
|
+
if (locked) writeSession(LOCK_KEY, locked)
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5023
|
+
// 入口清单只取一次:404/405 → 静态降级并不再重试;其它错误 → 记一条日志后收手。
|
|
5024
|
+
// 这里只渲染。没有任何分支会改地址,所以初始加载路径上不可能发生导航。
|
|
4803
5025
|
fetch('/taskwatch/entries', { cache: 'no-store' })
|
|
4804
5026
|
.then(function (r) {
|
|
4805
5027
|
if (isGone(r.status)) { degrade(); return null }
|
|
@@ -4808,16 +5030,15 @@ if ('serviceWorker' in navigator) {
|
|
|
4808
5030
|
})
|
|
4809
5031
|
.then(function (j) {
|
|
4810
5032
|
if (j === null) return
|
|
4811
|
-
if (!parseEntries(j)) return
|
|
5033
|
+
if (!parseEntries(j)) { console.warn('taskwatch: 入口清单形状不认识,双入口胶囊不显示'); return }
|
|
4812
5034
|
apply()
|
|
4813
|
-
probe()
|
|
4814
|
-
if (!probeOff) startTimer()
|
|
4815
5035
|
})
|
|
4816
5036
|
.catch(function (e) {
|
|
4817
|
-
|
|
5037
|
+
console.warn('taskwatch: 入口清单读取失败', (e && e.message) || e)
|
|
4818
5038
|
})
|
|
4819
5039
|
} catch (e) { console.warn('taskwatch: 双入口切换未启用', e) }
|
|
4820
5040
|
})()
|
|
5041
|
+
/* ROUTE-SAFETY-END */
|
|
4821
5042
|
</script>
|
|
4822
5043
|
</body>
|
|
4823
5044
|
</html>
|
package/lib/sw.js
CHANGED
|
@@ -44,7 +44,7 @@ const SHELL = ['/taskwatch', '/taskwatch/icon-192.png', '/taskwatch/icon-512.png
|
|
|
44
44
|
// 那次重载是 cache-first 时代的补丁(旧外壳永远不会自己变新)。现在页面已经是
|
|
45
45
|
// **网络优先**(见下面 fetch 里的注释),每次导航本来就取新版,这个补丁既没必要、
|
|
46
46
|
// 又足以把整站打死。别再加回来。
|
|
47
|
-
const CACHE = 'taskwatch-
|
|
47
|
+
const CACHE = 'taskwatch-v15'
|
|
48
48
|
|
|
49
49
|
const JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
|
|
50
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-in-hand",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.5",
|
|
4
4
|
"description": "掌上 DSH:DeepSeek Harness 的手机端,不用装 App——会话、实时对话、交付文件、后台任务与待批准。 / DSH in hand: the phone client for DeepSeek Harness with no app to install.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
"observability"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=20"
|
|
33
|
+
"node": ">=20",
|
|
34
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
|
-
"check": "node scripts/check-bundle.mjs && node scripts/check-files.mjs && node scripts/check-uploads.mjs && node scripts/check-queue.mjs && node scripts/check-entries.mjs && node scripts/check-ticket-hmac.mjs && node scripts/check-contract.mjs",
|
|
37
|
+
"check": "node scripts/check-bundle.mjs && node scripts/check-files.mjs && node scripts/check-uploads.mjs && node scripts/check-queue.mjs && node scripts/check-entries.mjs && node scripts/check-ticket-hmac.mjs && node scripts/check-contract.mjs && node scripts/check-no-autonav.mjs",
|
|
37
38
|
"demo": "node scripts/make-demo.mjs",
|
|
38
39
|
"icons": "node scripts/make-icons.mjs"
|
|
39
40
|
},
|