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.
@@ -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
- '.dshwv-img{position:absolute;right:0;bottom:0;width:59.45%;height:59.45%;display:block;pointer-events:none;-webkit-user-drag:none;user-select:none;object-fit:contain;object-position:right bottom}',
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
- if (!widgetCursor || !e || !e.target) return
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
- function selfRejection(req) {
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
- if (!listed) return 403
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
- if (!conn) return false // ① 已通过;此刻没有宿主栅栏可委托(加载时已 warn)
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.11', today: todayData, days7,
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.11', isPeak: isPeakTime(nowSec),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-widget",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义",
5
5
  "keywords": [
6
6
  "dsh",