dsh-whale-widget 0.3.11 → 0.3.12
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 +31 -8
- 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
|
@@ -611,8 +611,11 @@ export default {
|
|
|
611
611
|
if (Number(m[1]) !== 127) return false
|
|
612
612
|
return [m[2], m[3], m[4]].every((x) => Number(x) <= 255)
|
|
613
613
|
}
|
|
614
|
-
// 通过 → null
|
|
615
|
-
|
|
614
|
+
// 通过 → null;否则返回应当拒绝的状态码。
|
|
615
|
+
// opts.fenceAvailable:宿主栅栏此刻可用时,**非回环 Host 交给它判** —— 它知道 dsh 自己的
|
|
616
|
+
// `--trusted-host` 声明了谁;我们只在"确实没有栅栏"时才回落到 DSHW_TRUSTED_HOSTS。
|
|
617
|
+
// (0.3.11 把自校验放在委托之前,导致宿主已信任的权威在插件这边被 403,见 issue #136 后续的 A/B。)
|
|
618
|
+
function selfRejection(req, opts) {
|
|
616
619
|
try {
|
|
617
620
|
const headers = (req && req.headers) || {}
|
|
618
621
|
let hostUrl = null
|
|
@@ -621,8 +624,10 @@ export default {
|
|
|
621
624
|
const authority = hostUrl.host
|
|
622
625
|
if (!isLoopbackHostname(hostname)) {
|
|
623
626
|
const listed = TRUSTED_AUTHORITIES.some((e) => (e.indexOf(':') >= 0 ? e === authority.toLowerCase() : e === hostname.toLowerCase()))
|
|
624
|
-
|
|
627
|
+
const fenceAvailable = !!(opts && opts.fenceAvailable)
|
|
628
|
+
if (!listed && !fenceAvailable) return 403
|
|
625
629
|
}
|
|
630
|
+
// 下面两条与"信任列表"无关,任何情况下都执行:跨站标记、Origin 与 Host 不同源。
|
|
626
631
|
const site = String(headers['sec-fetch-site'] || '').toLowerCase()
|
|
627
632
|
if (site === 'cross-site') return 403
|
|
628
633
|
const origin = headers.origin
|
|
@@ -652,10 +657,11 @@ export default {
|
|
|
652
657
|
try { res.statusCode = code || 403; res.end() } catch (err) {}
|
|
653
658
|
return true
|
|
654
659
|
}
|
|
655
|
-
const self = selfRejection(req)
|
|
656
|
-
if (self !== null) return deny(self)
|
|
657
660
|
const conn = connectionFence()
|
|
658
|
-
|
|
661
|
+
// 先算栅栏可用性,再自校验:非回环 Host 在"栅栏可用"时交给栅栏判(它知道 --trusted-host)
|
|
662
|
+
const self = selfRejection(req, { fenceAvailable: !!conn })
|
|
663
|
+
if (self !== null) return deny(self)
|
|
664
|
+
if (!conn) return false // 已通过自校验;此刻没有宿主栅栏可委托(加载时已 warn)
|
|
659
665
|
let code
|
|
660
666
|
try {
|
|
661
667
|
code = conn.requestRejection(req)
|
|
@@ -1046,7 +1052,7 @@ export default {
|
|
|
1046
1052
|
const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
|
|
1047
1053
|
for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
|
|
1048
1054
|
return {
|
|
1049
|
-
ok: true, version: '0.3.
|
|
1055
|
+
ok: true, version: '0.3.12', today: todayData, days7,
|
|
1050
1056
|
total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
|
|
1051
1057
|
all: {
|
|
1052
1058
|
days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
|
|
@@ -2246,7 +2252,7 @@ export default {
|
|
|
2246
2252
|
const summary = daySummary(led, todayKey())
|
|
2247
2253
|
const nowSec = Math.floor(Date.now() / 1000)
|
|
2248
2254
|
return {
|
|
2249
|
-
...visible, version: '0.3.
|
|
2255
|
+
...visible, version: '0.3.12', isPeak: isPeakTime(nowSec),
|
|
2250
2256
|
// 峰谷切换点与节假日清单:前端倒计时要与宿主同源(否则法定节假日会算错切换点)
|
|
2251
2257
|
peakNextChangeAt: nextPeakChangeAt(nowSec),
|
|
2252
2258
|
peakHolidays: HOLIDAY_VALLEY_LIST,
|
|
@@ -3525,6 +3531,23 @@ export default {
|
|
|
3525
3531
|
return html + tag
|
|
3526
3532
|
}))
|
|
3527
3533
|
|
|
3534
|
+
// —— 官方 DSH 桌面端(Electron)适配(issue #152 / #153)——
|
|
3535
|
+
// 桌面壳的 index.html 是**从安装包静态 dist 直接读盘**的(`dsh-app://app/`),
|
|
3536
|
+
// 永远不经过宿主的 renderIndex(),所以上面的 tapIndex 在桌面端不生效 —— 于是"宿主半区活着、
|
|
3537
|
+
// 客户端半区从未被请求"。桌面端走的是**结构化行注入**:宿主启动时 collectIndexInjections()
|
|
3538
|
+
// 经 IPC 把行表交给渲染层,渲染层按 kind 逐行应用(见 @deepseek-ai/dsh-host-webserver 的
|
|
3539
|
+
// injections.d.ts:`{ kind:'script-src'; placement:'head'|'body'; src:string }`,外部经典脚本)。
|
|
3540
|
+
// 这里补同一行;**两条并存**:Web 形态仍走 tapIndex,两边都只会注入一次(各自去重)。
|
|
3541
|
+
disposers.push(ctx.on('webserver/index-inject', (table) => {
|
|
3542
|
+
try {
|
|
3543
|
+
if (!Array.isArray(table)) return
|
|
3544
|
+
for (const row of table) {
|
|
3545
|
+
if (row && row.kind === 'script-src' && row.src === '/dsh-whale/widget.js') return
|
|
3546
|
+
}
|
|
3547
|
+
table.push({ kind: 'script-src', placement: 'body', src: '/dsh-whale/widget.js' })
|
|
3548
|
+
} catch (err) {}
|
|
3549
|
+
}))
|
|
3550
|
+
|
|
3528
3551
|
ctx.effect(() => () => {
|
|
3529
3552
|
for (const d of disposers) {
|
|
3530
3553
|
try { d() } catch (err) {}
|
package/package.json
CHANGED