dsh-whale-widget 0.2.5 → 0.2.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MeteorNOX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -192,3 +192,7 @@ curl http://127.0.0.1:3080/dsh-whale/last-turn.json
192
192
  ## 开发与维护
193
193
 
194
194
  完整规格、视觉参数、架构结论和生成提示词见 `whale-widget-prompt.md`。修改文字位置、颜色、动画、吸附逻辑、台词组或定价表时参考该文件。
195
+
196
+ ## 许可证
197
+
198
+ 本项目基于 **MIT License** 开源,详见 [LICENSE](LICENSE)。
package/lib/index.js CHANGED
@@ -336,6 +336,8 @@ gifEl.src = GIF_URL
336
336
  gifEl.alt = ''
337
337
  gifEl.draggable = false
338
338
  bubbleBox.appendChild(gifEl)
339
+ var gifFailed = false
340
+ gifEl.onerror = function () { gifFailed = true }
339
341
  bubbleBox.appendChild(textBox)
340
342
  bubbleBox.addEventListener('click', function (e) {
341
343
  e.stopPropagation()
@@ -436,13 +438,18 @@ function pickRandomLines() {
436
438
  function applyBubbleLines(lines) {
437
439
  if (lines && lines.gif) {
438
440
  // gif 台词组:只显示 gif,隐藏三行文字(display 必须显式覆盖 CSS 的 none)
439
- if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
440
- gifEl.style.display = 'block'
441
- gifEl.style.opacity = ''
442
- labelEl.style.display = 'none'
443
- amountEl.style.display = 'none'
444
- hintEl.style.display = 'none'
445
- return
441
+ if (gifFailed) {
442
+ // gif 加载失败/路由缺失:降级为文字台词,避免空白白色气泡
443
+ lines = singleCenter('A', pickOne(['gif 加载失败了...', '今天没有动图给你看~', '呜呜 动图不见了...']), '', true)
444
+ } else {
445
+ if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
446
+ gifEl.style.display = 'block'
447
+ gifEl.style.opacity = ''
448
+ labelEl.style.display = 'none'
449
+ amountEl.style.display = 'none'
450
+ hintEl.style.display = 'none'
451
+ return
452
+ }
446
453
  }
447
454
  if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
448
455
  gifEl.style.display = 'none'
@@ -605,6 +612,11 @@ function viewport() {
605
612
  h: window.innerHeight || document.documentElement.clientHeight || 800
606
613
  }
607
614
  }
615
+ function rightGap() {
616
+ var sw = window.innerWidth - document.documentElement.clientWidth
617
+ if (sw < 8) sw = 10 // 悬浮滚动条测量为0时兜底
618
+ return sw + 2 // 滚动条宽度 + 2px
619
+ }
608
620
  function fmt(balance, currency) {
609
621
  var num = Number(balance)
610
622
  var fixed = isFinite(num) ? num.toFixed(2) : '--'
@@ -671,17 +683,17 @@ function settle() {
671
683
  var h = root.offsetHeight || root.getBoundingClientRect().height || 0
672
684
  if (drag && drag.active) {
673
685
  // mid-drag resize: keep the pointer-follow position, just clamp into view
674
- state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
686
+ state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
675
687
  state.top = clamp(state.top, 0, Math.max(0, vp.h - h))
676
688
  express()
677
689
  return
678
690
  }
679
691
  if (state.h === 'right') {
680
- state.left = Math.max(0, vp.w - w - state.hOff)
692
+ state.left = Math.max(0, vp.w - w - state.hOff - rightGap())
681
693
  } else if (state.h === 'left') {
682
694
  state.left = state.hOff
683
695
  } else {
684
- state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
696
+ state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
685
697
  }
686
698
  if (state.v === 'bottom') {
687
699
  state.top = Math.max(0, vp.h - h - state.vOff)
@@ -765,11 +777,23 @@ var peakMode = 'default'
765
777
  var bubbleOn = true
766
778
  var turnCostOn = true
767
779
  var turnCostCloseMs = 5000
768
- var lastCostSeq = 0
769
780
  var costBubbleActive = false
770
781
  function saveConfig() {
771
782
  try {
772
783
  fetch(SIZE_URL, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode, peakMode: peakMode, bubbleOn: bubbleOn, turnCostOn: turnCostOn, turnCostCloseMs: turnCostCloseMs }) })
784
+ var vp = viewport()
785
+ var w = root.offsetWidth || root.getBoundingClientRect().width || 0
786
+ var h = root.offsetHeight || root.getBoundingClientRect().height || 0
787
+ var leftDist = state.left
788
+ var rightDist = vp.w - state.left - w
789
+ var topDist = state.top
790
+ var bottomDist = vp.h - state.top - h
791
+ localStorage.setItem('dshw-pos', JSON.stringify({
792
+ hAnchor: leftDist <= rightDist ? 'left' : 'right',
793
+ hDist: Math.round(Math.min(leftDist, rightDist)),
794
+ vAnchor: topDist <= bottomDist ? 'top' : 'bottom',
795
+ vDist: Math.round(Math.min(topDist, bottomDist))
796
+ }))
773
797
  } catch (err) {}
774
798
  }
775
799
  function setUsageMode(v) {
@@ -787,7 +811,8 @@ function setBubbleOn(v) {
787
811
  bubbleOn = !!v
788
812
  bubbleToggle.checked = bubbleOn
789
813
  saveConfig()
790
- if (!bubbleOn) hideBubble()
814
+ // 必须走 hideCostBubble:残留的 costBubbleActive 会让 render()/showBubble() 永久早退
815
+ if (!bubbleOn) hideCostBubble()
791
816
  }
792
817
  function setTurnCostOn(v) {
793
818
  turnCostOn = !!v
@@ -955,7 +980,7 @@ function snapCheck() {
955
980
  } else if (centerX > vp.w * 3 / 4) {
956
981
  state.h = 'right'
957
982
  state.hOff = 0
958
- left = vp.w - w
983
+ left = vp.w - w - rightGap()
959
984
  moved = true
960
985
  } else {
961
986
  state.h = null
@@ -1009,7 +1034,9 @@ function setupHitTest() {
1009
1034
  var probe = new Image()
1010
1035
  probe.onload = function () {
1011
1036
  try {
1012
- hitCanvas.getContext('2d').drawImage(probe, 0, 0)
1037
+ // 拉伸到 610×610 与 isWhaleHit 的坐标映射对齐;不指定尺寸会按原图大小绘制,
1038
+ // 回退到非 610×610 素材(如 DSniang02.png)时命中区域会错位
1039
+ hitCanvas.getContext('2d').drawImage(probe, 0, 0, 610, 610)
1013
1040
  hitReady = true
1014
1041
  } catch (err) {}
1015
1042
  }
@@ -1136,7 +1163,28 @@ function endDrag(e, clickAllowed) {
1136
1163
  state.top = top
1137
1164
  settle()
1138
1165
  }
1166
+ // 窗口尺寸变化时:自由位置的鲸鱼按相对边框锚点重算(保持离边距离,窗口恢复原状即回原位);
1167
+ // 贴边吸附的鲸鱼走 settle()(保持贴边)
1168
+ function applyAnchorPos() {
1169
+ try {
1170
+ var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
1171
+ if (!a || (a.hAnchor !== 'left' && a.hAnchor !== 'right') || typeof a.hDist !== 'number' ||
1172
+ (a.vAnchor !== 'top' && a.vAnchor !== 'bottom') || typeof a.vDist !== 'number') return false
1173
+ var vp = viewport()
1174
+ var w = root.offsetWidth || root.getBoundingClientRect().width || 0
1175
+ var h = root.offsetHeight || root.getBoundingClientRect().height || 0
1176
+ var l = a.hAnchor === 'left' ? a.hDist : vp.w - a.hDist - w
1177
+ var t = a.vAnchor === 'top' ? a.vDist : vp.h - a.vDist - h
1178
+ state.left = clamp(l, 0, Math.max(0, vp.w - w))
1179
+ state.top = clamp(t, 0, Math.max(0, vp.h - h))
1180
+ state.h = null
1181
+ state.v = null
1182
+ express()
1183
+ return true
1184
+ } catch (err) { return false }
1185
+ }
1139
1186
  window.addEventListener('resize', function () {
1187
+ if (state.h === null && state.v === null && applyAnchorPos()) return
1140
1188
  settle()
1141
1189
  })
1142
1190
 
@@ -1192,6 +1240,23 @@ fetch(SIZE_URL, { cache: 'no-store' })
1192
1240
  turnCostCloseMs = d.turnCostCloseMs > 0 ? d.turnCostCloseMs : 0
1193
1241
  turnCostCloseInput.value = String(Math.round(turnCostCloseMs / 1000))
1194
1242
  }
1243
+ // 相对边框恢复(localStorage 锚点):窗口变化后保持离边距离
1244
+ try {
1245
+ var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
1246
+ if (a && (a.hAnchor === 'left' || a.hAnchor === 'right') && typeof a.hDist === 'number' &&
1247
+ (a.vAnchor === 'top' || a.vAnchor === 'bottom') && typeof a.vDist === 'number') {
1248
+ var vpA = viewport()
1249
+ var wA = root.offsetWidth || root.getBoundingClientRect().width || 0
1250
+ var hA = root.offsetHeight || root.getBoundingClientRect().height || 0
1251
+ var lA = a.hAnchor === 'left' ? a.hDist : vpA.w - a.hDist - wA
1252
+ var tA = a.vAnchor === 'top' ? a.vDist : vpA.h - a.vDist - hA
1253
+ state.left = clamp(lA, 0, Math.max(0, vpA.w - wA))
1254
+ state.top = clamp(tA, 0, Math.max(0, vpA.h - hA))
1255
+ state.h = null // 恢复为自由位置,避免 settle() 重新吸附覆盖
1256
+ state.v = null
1257
+ settle()
1258
+ }
1259
+ } catch (err) {}
1195
1260
  refresh(false)
1196
1261
  })
1197
1262
  .catch(function () { refresh(false) })
@@ -1287,10 +1352,6 @@ function apply(ctx) {
1287
1352
 
1288
1353
  // 监听所有会话的追加事件;turn/end 时结算本轮
1289
1354
  disposers.push(ctx.on('session/event', (session, event) => {
1290
- // [debug] 确认事件是否到达(下个版本移除)
1291
- if (event && (event.type === 'turn/end' || event.type === 'assistant/message')) {
1292
- try { console.error('[whale-balance:event]', event.type, 'turn=' + (event.data && event.data.turn), 'usage=' + (event.data && event.data.usage ? 'yes' : 'no')) } catch (err) {}
1293
- }
1294
1355
  handleSessionEvent(event)
1295
1356
  }))
1296
1357
 
@@ -1322,6 +1383,17 @@ function apply(ctx) {
1322
1383
  throw new Error('whale image not found')
1323
1384
  }
1324
1385
 
1386
+ function pickBalanceInfo(infos) {
1387
+ if (!Array.isArray(infos) || infos.length === 0) return null
1388
+ const num = (x) => (x && x.total_balance !== undefined ? Number(x.total_balance) : NaN)
1389
+ return (
1390
+ infos.find((x) => x && x.currency === 'CNY' && num(x) > 0) ||
1391
+ infos.find((x) => num(x) > 0) ||
1392
+ infos.find((x) => x && x.currency === 'CNY') ||
1393
+ infos[0]
1394
+ )
1395
+ }
1396
+
1325
1397
  async function fetchBalance() {
1326
1398
  let cred
1327
1399
  try {
@@ -1357,7 +1429,7 @@ function apply(ctx) {
1357
1429
  } catch (err) {
1358
1430
  return { ok: false, code: 'PARSE', error: '余额接口返回不是合法 JSON' }
1359
1431
  }
1360
- const info = data && Array.isArray(data.balance_infos) ? data.balance_infos[0] : null
1432
+ const info = pickBalanceInfo(data && data.balance_infos)
1361
1433
  if (!info || info.total_balance === undefined) {
1362
1434
  return { ok: false, code: 'SHAPE', error: '余额接口返回结构异常' }
1363
1435
  }
@@ -1580,7 +1652,8 @@ function apply(ctx) {
1580
1652
  const pm = peakMode === 'liangwen' || peakMode === 'qiangqiang' ? peakMode : 'default'
1581
1653
  const bo = bubbleOn !== false
1582
1654
  const tco = turnCostOn !== false
1583
- const tcc = typeof turnCostCloseMs === 'number' && turnCostCloseMs > 0 ? turnCostCloseMs : 5000
1655
+ // 0 是合法值(不自动关闭),只有缺失/非法时才回落默认 5
1656
+ const tcc = typeof turnCostCloseMs === 'number' && turnCostCloseMs >= 0 ? turnCostCloseMs : 5000
1584
1657
  const body = JSON.stringify({
1585
1658
  scale: scale,
1586
1659
  sound: sound !== false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-widget",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件(含今日已用、峰谷定价、随机台词、音效与每轮消耗统计)",
5
5
  "keywords": [
6
6
  "dsh",