dsh-whale-widget 0.3.11 → 0.3.13
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/assets/whale-widget.js +69 -3
- package/lib/index.js +66 -10
- package/package.json +1 -1
package/assets/whale-widget.js
CHANGED
|
@@ -322,7 +322,12 @@ var css = [
|
|
|
322
322
|
'.dshwv-root.dshwv-left{transform:scaleX(-1)}',
|
|
323
323
|
'.dshwv-root.dshwv-dragging{cursor:grabbing;transition:none}',
|
|
324
324
|
'.dshwv-body{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:50% 100%;transition:transform .22s cubic-bezier(.34,1.56,.64,1)}',
|
|
325
|
-
|
|
325
|
+
// v757(issue #147):`.dshwv-img` 由 `pointer-events:none` 改为 `auto` —— 鲸鱼身体**自己接指针事件**。
|
|
326
|
+
// 原来靠"主文档监听 + isWhaleHit()"判定命中,而指针落在 `<iframe>`(如右侧栏 HTML 预览)上时
|
|
327
|
+
// 事件直接进入 iframe 自己的文档,主文档收不到 ⇒ 鲸鱼身体失联(点不动、拖不动,只有 ☰ 能用)。
|
|
328
|
+
// 代价是 img 的**矩形**(含透明边距)会吞掉点击 → 由 setupHitTest() 用命中图的**凸包**做 clip-path
|
|
329
|
+
// 裁掉透明区(凸包包含全部不透明像素,不会裁到角色本身),"点到透明处穿透到下层"的行为得以保留。
|
|
330
|
+
'.dshwv-img{position:absolute;right:0;bottom:0;width:59.45%;height:59.45%;display:block;pointer-events:auto;-webkit-user-drag:none;user-select:none;object-fit:contain;object-position:right bottom}',
|
|
326
331
|
// v751(PR #119):光标不再写 document.body.style.cursor —— cursor 是可继承属性,写 <body> 会让 Blink
|
|
327
332
|
// 失效**整棵文档树**的样式;而它在点击链路上按下/抬手各写一次,紧接着 isWhaleHit() 的
|
|
328
333
|
// getBoundingClientRect() 与泡泡行测量的 getComputedStyle()/scrollWidth 会强制刷新样式+布局,
|
|
@@ -14878,16 +14883,21 @@ function setupHitTest(url) {
|
|
|
14878
14883
|
if (ctx.getImageData(px, py, 1, 1).data[3] > 10) opaque++
|
|
14879
14884
|
}
|
|
14880
14885
|
}
|
|
14881
|
-
if (!opaque) { hitReady = false; hitFailed = true; return }
|
|
14886
|
+
if (!opaque) { hitReady = false; hitFailed = true; applyHitClip(''); return }
|
|
14882
14887
|
hitReady = true
|
|
14888
|
+
// v757(issue #147):命中图可用 → 用不透明区域的凸包裁掉 img 的透明边距,
|
|
14889
|
+
// 这样"身体自己接事件"不会把透明角落的点击也吞掉。
|
|
14890
|
+
applyHitClip(buildHitClipPath())
|
|
14883
14891
|
} catch (err) {
|
|
14884
14892
|
hitFailed = true
|
|
14893
|
+
applyHitClip('')
|
|
14885
14894
|
}
|
|
14886
14895
|
}
|
|
14887
14896
|
probe.onerror = function () {
|
|
14888
14897
|
// 图片加载失败:不能把整页当成鲸鱼命中区吞掉事件(会全页面点不动),
|
|
14889
14898
|
// 标记失败,命中判定退回图像矩形区域。
|
|
14890
14899
|
hitFailed = true
|
|
14900
|
+
applyHitClip('')
|
|
14891
14901
|
}
|
|
14892
14902
|
probe.src = url || IMG_URL
|
|
14893
14903
|
} catch (err) {}
|
|
@@ -14901,6 +14911,60 @@ function whaleRectHit(e) {
|
|
|
14901
14911
|
return e.clientX >= fr.left && e.clientX <= fr.right && e.clientY >= fr.top && e.clientY <= fr.bottom
|
|
14902
14912
|
} catch (err) { return false }
|
|
14903
14913
|
}
|
|
14914
|
+
// —— v757(issue #147):把命中图的不透明区域做成 clip-path ——
|
|
14915
|
+
// `img` 现在常驻 `pointer-events:auto`(压在任何 iframe 上都能接事件),所以要靠 clip-path
|
|
14916
|
+
// 把透明边距裁掉,保住"点到透明处穿透到下层"。用**凸包**而不是逐像素轮廓:凸包天然包含全部
|
|
14917
|
+
// 不透明像素(不会裁到角色本身),点数少(几十个),且左右镜像由外层的 scaleX(-1) 一起变换。
|
|
14918
|
+
function applyHitClip(clip) {
|
|
14919
|
+
try {
|
|
14920
|
+
img.style.clipPath = clip || ''
|
|
14921
|
+
img.style.webkitClipPath = clip || ''
|
|
14922
|
+
} catch (err) {}
|
|
14923
|
+
}
|
|
14924
|
+
function convexHull(pts) {
|
|
14925
|
+
try {
|
|
14926
|
+
var p = pts.slice().sort(function (a, b) { return a[0] - b[0] || a[1] - b[1] })
|
|
14927
|
+
if (p.length < 3) return null
|
|
14928
|
+
var cross = function (o, a, b) { return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) }
|
|
14929
|
+
var lower = [], upper = [], i
|
|
14930
|
+
for (i = 0; i < p.length; i++) {
|
|
14931
|
+
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p[i]) <= 0) lower.pop()
|
|
14932
|
+
lower.push(p[i])
|
|
14933
|
+
}
|
|
14934
|
+
for (i = p.length - 1; i >= 0; i--) {
|
|
14935
|
+
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p[i]) <= 0) upper.pop()
|
|
14936
|
+
upper.push(p[i])
|
|
14937
|
+
}
|
|
14938
|
+
lower.pop(); upper.pop()
|
|
14939
|
+
return lower.concat(upper)
|
|
14940
|
+
} catch (err) { return null }
|
|
14941
|
+
}
|
|
14942
|
+
function buildHitClipPath() {
|
|
14943
|
+
try {
|
|
14944
|
+
var c = hitCanvas && hitCanvas.getContext ? hitCanvas.getContext('2d') : null
|
|
14945
|
+
if (!c) return ''
|
|
14946
|
+
var data = c.getImageData(0, 0, 610, 610).data // 只读一次(1.5MB),别逐行读
|
|
14947
|
+
var pts = []
|
|
14948
|
+
for (var y = 0; y < 610; y++) {
|
|
14949
|
+
var l = -1, r = -1
|
|
14950
|
+
var base = y * 610 * 4
|
|
14951
|
+
for (var x = 0; x < 610; x++) {
|
|
14952
|
+
if (data[base + x * 4 + 3] > 10) { if (l < 0) l = x; r = x }
|
|
14953
|
+
}
|
|
14954
|
+
if (l >= 0) { pts.push([l, y]); pts.push([r, y]) }
|
|
14955
|
+
}
|
|
14956
|
+
if (pts.length < 8) return ''
|
|
14957
|
+
var hull = convexHull(pts)
|
|
14958
|
+
if (!hull || hull.length < 3) return ''
|
|
14959
|
+
var parts = []
|
|
14960
|
+
for (var k = 0; k < hull.length; k++) {
|
|
14961
|
+
parts.push((hull[k][0] / 610 * 100).toFixed(2) + '% ' + (hull[k][1] / 610 * 100).toFixed(2) + '%')
|
|
14962
|
+
}
|
|
14963
|
+
return 'polygon(' + parts.join(', ') + ')'
|
|
14964
|
+
} catch (err) {
|
|
14965
|
+
return '' // 读不出来 → 不裁(退回矩形行为,与旧版一致)
|
|
14966
|
+
}
|
|
14967
|
+
}
|
|
14904
14968
|
function isWhaleHit(e) {
|
|
14905
14969
|
// 命中图未就绪/失败时:绝不默认“全屏都是鲸鱼”。
|
|
14906
14970
|
// 加载中 → 返回 false(不拦截页面);加载失败 → 退回图像矩形区域,仅挂件区域可拖。
|
|
@@ -15141,7 +15205,9 @@ function setWidgetCursor(v) {
|
|
|
15141
15205
|
// 不碰页面级样式;找不到可滚动祖先时什么都不做(此时浏览器仍按默认把滚动交给页面滚动容器)。
|
|
15142
15206
|
function onWhaleWheel(e) {
|
|
15143
15207
|
try {
|
|
15144
|
-
|
|
15208
|
+
// v757(issue #147):img 现在**常驻** pointer-events:auto,所以不再用 widgetCursor 当闸门 ——
|
|
15209
|
+
// 这个监听挂在 root 上,只有挂件自己的子元素接住了事件才会触发;命中就转交。
|
|
15210
|
+
if (!e || !e.target) return
|
|
15145
15211
|
var hit = e.target
|
|
15146
15212
|
if (hit !== img && !(root && root.contains && root.contains(hit))) return
|
|
15147
15213
|
var prev = hit.style ? hit.style.pointerEvents : ''
|
package/lib/index.js
CHANGED
|
@@ -582,10 +582,50 @@ function loadWidgetJs() {
|
|
|
582
582
|
return widgetJsCache ? widgetJsCache.text : ''
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
// —— 桌面端(Electron)结构化注入行:**内联 script 行**,不是 script-src 行(issue #154)——
|
|
586
|
+
// 桌面壳的 index.html 从安装包静态 dist 直出,`tapIndex`(函数变换)永远过不去;唯一通道是
|
|
587
|
+
// `webserver/index-inject` 推的结构化行,经 IPC 交给页面侧解释器逐行应用。
|
|
588
|
+
// 但解释器对两种 script 行的处理**不对称**(desktop 前端 `web-boot`):
|
|
589
|
+
// case "script": createElement + textContent + append —— 没有 await,**不可能"加载失败"**
|
|
590
|
+
// case "script-src": await loadScript(src) —— 失败即 reject,而那个 reject 会
|
|
591
|
+
// reject 掉 __DSH_BOOT_READY__ ⇒ **应用起不来**
|
|
592
|
+
// 再加一层:外壳把这张表在**宿主启动时收集一次**后缓存,**没有任何刷新路径** ⇒ 插件在运行期被关掉时,
|
|
593
|
+
// 表里仍留着这一行,而 `/dsh-whale/*` 路由已注销 ⇒ 加载 404 ⇒ 就是 issue #154 那个
|
|
594
|
+
// "desktop web: failed to load /dsh-whale/widget.js" 致命错误。
|
|
595
|
+
// 所以改成内联行:由**我们自己**建 `<script src=…>` 并**吞掉** onerror —— 路由在就正常加载,
|
|
596
|
+
// 路由不在就静默失败,宿主永远不会因为我们起不来。
|
|
597
|
+
const DESKTOP_WIDGET_ROW_TEXT =
|
|
598
|
+
'(function(){try{var d=document.body||document.head||document.documentElement;if(!d)return;' +
|
|
599
|
+
'var s=document.createElement("script");s.src="/dsh-whale/widget.js";' +
|
|
600
|
+
's.onerror=function(){};d.appendChild(s)}catch(e){}})()'
|
|
601
|
+
|
|
585
602
|
export default {
|
|
586
603
|
name: 'whale-balance-widget',
|
|
587
|
-
inject: ['webServer',
|
|
588
|
-
apply(
|
|
604
|
+
// v758:**有意去掉对象级 `inject: ['webServer','credentials','connection']`**(issue #154 的隐患面)。
|
|
605
|
+
// 对象级 inject 会把整个 apply() 推迟到三个服务就绪之后;而桌面端的注入表是**宿主启动时一次性
|
|
606
|
+
// 收集**的(`dsh-desktop-host`:`collectIndexInjections()` → IPC → 渲染层),订阅一旦晚于那次收集,
|
|
607
|
+
// 这一行就永远进不了表 = 桌面端挂件完全不出现(#152/#153 那位报告人无法稳定复现的正是这个竞态)。
|
|
608
|
+
// 现在 apply() 立刻执行、**第一件事就是注册注入行**(它不依赖任何服务),其余逻辑原样放进
|
|
609
|
+
// `root.inject([...], cb)` 局部等待 —— 语义与原来的对象级 inject 等价,但行的注册不再被推迟。
|
|
610
|
+
apply(root) {
|
|
611
|
+
const rowDisposers = []
|
|
612
|
+
root.effect(() => () => { for (const d of rowDisposers) { try { d() } catch (err) {} } })
|
|
613
|
+
// ① 结构化注入行:桌面端唯一能生效的通道(web 形态另有 tapIndex,两条并存、各自去重)
|
|
614
|
+
rowDisposers.push(root.on('webserver/index-inject', (table) => {
|
|
615
|
+
try {
|
|
616
|
+
if (!Array.isArray(table)) return
|
|
617
|
+
for (const row of table) {
|
|
618
|
+
if (!row) continue
|
|
619
|
+
if (row.kind === 'script-src' && row.src === '/dsh-whale/widget.js') return // 旧版本推过 → 不重复
|
|
620
|
+
if (row.kind === 'script' && typeof row.text === 'string'
|
|
621
|
+
&& row.text.indexOf('/dsh-whale/widget.js') >= 0) return
|
|
622
|
+
}
|
|
623
|
+
table.push({ kind: 'script', placement: 'body', text: DESKTOP_WIDGET_ROW_TEXT })
|
|
624
|
+
} catch (err) {}
|
|
625
|
+
}))
|
|
626
|
+
|
|
627
|
+
// ② 其余逻辑:等齐三个服务后再跑(等价于原来的对象级 inject,但不再挡住 ①)
|
|
628
|
+
root.inject(['webServer', 'credentials', 'connection'], (ctx) => {
|
|
589
629
|
// —— 浏览器信任栅栏(issue #92 建立,issue #136 补 fail-closed)——
|
|
590
630
|
// dsh 的 connection 服务提供 requestRejection(req):Host/Origin 被伪造(DNS 重绑定)或未认证的
|
|
591
631
|
// 请求必须被拒。**但这个方法在较老的 dsh 上不存在**(#136 实测:0.1.5-rc.2/rc.3 有、0.1.0-rc.3 没有),
|
|
@@ -611,8 +651,11 @@ export default {
|
|
|
611
651
|
if (Number(m[1]) !== 127) return false
|
|
612
652
|
return [m[2], m[3], m[4]].every((x) => Number(x) <= 255)
|
|
613
653
|
}
|
|
614
|
-
// 通过 → null
|
|
615
|
-
|
|
654
|
+
// 通过 → null;否则返回应当拒绝的状态码。
|
|
655
|
+
// opts.fenceAvailable:宿主栅栏此刻可用时,**非回环 Host 交给它判** —— 它知道 dsh 自己的
|
|
656
|
+
// `--trusted-host` 声明了谁;我们只在"确实没有栅栏"时才回落到 DSHW_TRUSTED_HOSTS。
|
|
657
|
+
// (0.3.11 把自校验放在委托之前,导致宿主已信任的权威在插件这边被 403,见 issue #136 后续的 A/B。)
|
|
658
|
+
function selfRejection(req, opts) {
|
|
616
659
|
try {
|
|
617
660
|
const headers = (req && req.headers) || {}
|
|
618
661
|
let hostUrl = null
|
|
@@ -621,8 +664,10 @@ export default {
|
|
|
621
664
|
const authority = hostUrl.host
|
|
622
665
|
if (!isLoopbackHostname(hostname)) {
|
|
623
666
|
const listed = TRUSTED_AUTHORITIES.some((e) => (e.indexOf(':') >= 0 ? e === authority.toLowerCase() : e === hostname.toLowerCase()))
|
|
624
|
-
|
|
667
|
+
const fenceAvailable = !!(opts && opts.fenceAvailable)
|
|
668
|
+
if (!listed && !fenceAvailable) return 403
|
|
625
669
|
}
|
|
670
|
+
// 下面两条与"信任列表"无关,任何情况下都执行:跨站标记、Origin 与 Host 不同源。
|
|
626
671
|
const site = String(headers['sec-fetch-site'] || '').toLowerCase()
|
|
627
672
|
if (site === 'cross-site') return 403
|
|
628
673
|
const origin = headers.origin
|
|
@@ -652,10 +697,11 @@ export default {
|
|
|
652
697
|
try { res.statusCode = code || 403; res.end() } catch (err) {}
|
|
653
698
|
return true
|
|
654
699
|
}
|
|
655
|
-
const self = selfRejection(req)
|
|
656
|
-
if (self !== null) return deny(self)
|
|
657
700
|
const conn = connectionFence()
|
|
658
|
-
|
|
701
|
+
// 先算栅栏可用性,再自校验:非回环 Host 在"栅栏可用"时交给栅栏判(它知道 --trusted-host)
|
|
702
|
+
const self = selfRejection(req, { fenceAvailable: !!conn })
|
|
703
|
+
if (self !== null) return deny(self)
|
|
704
|
+
if (!conn) return false // 已通过自校验;此刻没有宿主栅栏可委托(加载时已 warn)
|
|
659
705
|
let code
|
|
660
706
|
try {
|
|
661
707
|
code = conn.requestRejection(req)
|
|
@@ -1046,7 +1092,7 @@ export default {
|
|
|
1046
1092
|
const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
|
|
1047
1093
|
for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
|
|
1048
1094
|
return {
|
|
1049
|
-
ok: true, version: '0.3.
|
|
1095
|
+
ok: true, version: '0.3.13', today: todayData, days7,
|
|
1050
1096
|
total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
|
|
1051
1097
|
all: {
|
|
1052
1098
|
days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
|
|
@@ -2246,7 +2292,7 @@ export default {
|
|
|
2246
2292
|
const summary = daySummary(led, todayKey())
|
|
2247
2293
|
const nowSec = Math.floor(Date.now() / 1000)
|
|
2248
2294
|
return {
|
|
2249
|
-
...visible, version: '0.3.
|
|
2295
|
+
...visible, version: '0.3.13', isPeak: isPeakTime(nowSec),
|
|
2250
2296
|
// 峰谷切换点与节假日清单:前端倒计时要与宿主同源(否则法定节假日会算错切换点)
|
|
2251
2297
|
peakNextChangeAt: nextPeakChangeAt(nowSec),
|
|
2252
2298
|
peakHolidays: HOLIDAY_VALLEY_LIST,
|
|
@@ -3525,10 +3571,20 @@ export default {
|
|
|
3525
3571
|
return html + tag
|
|
3526
3572
|
}))
|
|
3527
3573
|
|
|
3574
|
+
// —— 官方 DSH 桌面端(Electron)适配(issue #152 / #153 / #154)——
|
|
3575
|
+
// 桌面壳的 index.html 是**从安装包静态 dist 直接读盘**的(`dsh-app://app/`),永远不经过宿主的
|
|
3576
|
+
// renderIndex(),所以上面的 tapIndex 在桌面端不生效 —— 于是"宿主半区活着、客户端半区从未被请求"。
|
|
3577
|
+
// 桌面端唯一的注入通道是 `webserver/index-inject` 的结构化行(宿主启动时 collectIndexInjections()
|
|
3578
|
+
// 经 IPC 交给渲染层,渲染层按 kind 逐行应用)。
|
|
3579
|
+
// **该行的注册已上移到 apply() 开头**(见 `DESKTOP_WIDGET_ROW_TEXT` 上方那段注释):两个原因 ——
|
|
3580
|
+
// ① 桌面端的注入表是**一次性收集**的,行必须尽早进表;② 行本身改成了**内联 script 行**,
|
|
3581
|
+
// 因为页面侧解释器对 `script-src` 是"加载失败即 reject 整个 boot"(issue #154 的致命启动错误)。
|
|
3582
|
+
|
|
3528
3583
|
ctx.effect(() => () => {
|
|
3529
3584
|
for (const d of disposers) {
|
|
3530
3585
|
try { d() } catch (err) {}
|
|
3531
3586
|
}
|
|
3532
3587
|
})
|
|
3588
|
+
}) // ← 结束 root.inject(['webServer','credentials','connection'], cb)
|
|
3533
3589
|
},
|
|
3534
3590
|
}
|
package/package.json
CHANGED