dsh-whale-widget 0.1.0 → 0.2.2
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 +140 -121
- package/assets/D1.mp3 +0 -0
- package/assets/D2.mp3 +0 -0
- package/assets/DSniang1.png +0 -0
- package/assets/Ya1.mp3 +0 -0
- package/assets/Ya2.mp3 +0 -0
- package/cordis.patch.yml +15 -15
- package/lib/index.js +1461 -627
- package/package.json +26 -26
- package/assets/DSniang02.png +0 -0
package/lib/index.js
CHANGED
|
@@ -1,627 +1,1461 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
|
|
6
|
-
// Package root: lib/index.js -> package root. This keeps the bundle relocatable
|
|
7
|
-
// when installed as a normal DSH npm plugin (node_modules or a local link).
|
|
8
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
-
|
|
10
|
-
// DSH home: used for the widget size memory
|
|
11
|
-
// read-only or cleaned on update.
|
|
12
|
-
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
13
|
-
|
|
14
|
-
// Resolve the whale image relative to the package first, so the bundle works
|
|
15
|
-
// no matter where it is installed. Legacy absolute paths remain as harmless
|
|
16
|
-
// fallbacks for older manual installs.
|
|
17
|
-
const IMAGE_CANDIDATES = [
|
|
18
|
-
path.join(PACKAGE_ROOT, 'assets', '
|
|
19
|
-
|
|
20
|
-
'D:/TestBox/deepseek/DSniang02.png',
|
|
21
|
-
'D:/TestBox/deepseek/skin/DSniang02.png',
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
// Size memory file: prefer writable DSH home locations, then legacy fallbacks.
|
|
25
|
-
const SIZE_FILE_CANDIDATES = [
|
|
26
|
-
path.join(DSH_HOME, '.dshw-size.json'),
|
|
27
|
-
path.join(DSH_HOME, 'profiles', 'web', '.dshw-size.json'),
|
|
28
|
-
'D:/TestBox/deepseek/.dshw-size.json',
|
|
29
|
-
'D:/TestBox/deepseek/skin/.dshw-size.json',
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
var
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
var
|
|
107
|
-
|
|
108
|
-
var
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
var
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
root.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
top: 0,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
var
|
|
141
|
-
var
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
function
|
|
184
|
-
var
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
function
|
|
206
|
-
var
|
|
207
|
-
var
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
var
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
// Package root: lib/index.js -> package root. This keeps the bundle relocatable
|
|
7
|
+
// when installed as a normal DSH npm plugin (node_modules or a local link).
|
|
8
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
|
|
10
|
+
// DSH home: used for the widget size/usage memory files, since node_modules may
|
|
11
|
+
// be read-only or cleaned on update.
|
|
12
|
+
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
13
|
+
|
|
14
|
+
// Resolve the whale image relative to the package first, so the bundle works
|
|
15
|
+
// no matter where it is installed. Legacy absolute paths remain as harmless
|
|
16
|
+
// fallbacks for older manual installs.
|
|
17
|
+
const IMAGE_CANDIDATES = [
|
|
18
|
+
path.join(PACKAGE_ROOT, 'assets', 'DSniang1.png'),
|
|
19
|
+
'D:/TestBox/deepseek/DSniang1.png',
|
|
20
|
+
'D:/TestBox/deepseek/DSniang02.png',
|
|
21
|
+
'D:/TestBox/deepseek/skin/DSniang02.png',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
// Size memory file: prefer writable DSH home locations, then legacy fallbacks.
|
|
25
|
+
const SIZE_FILE_CANDIDATES = [
|
|
26
|
+
path.join(DSH_HOME, '.dshw-size.json'),
|
|
27
|
+
path.join(DSH_HOME, 'profiles', 'web', '.dshw-size.json'),
|
|
28
|
+
'D:/TestBox/deepseek/.dshw-size.json',
|
|
29
|
+
'D:/TestBox/deepseek/skin/.dshw-size.json',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
// Usage ledger file (小鲸鱼记账 mode): same policy as the size file.
|
|
33
|
+
const USAGE_FILE_CANDIDATES = [
|
|
34
|
+
path.join(DSH_HOME, '.dshw-usage.json'),
|
|
35
|
+
path.join(DSH_HOME, 'profiles', 'web', '.dshw-usage.json'),
|
|
36
|
+
'D:/TestBox/deepseek/.dshw-usage.json',
|
|
37
|
+
'D:/TestBox/deepseek/skin/.dshw-usage.json',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
// Sound assets: package-relative first (optional — ship Ya1/Ya2/D1/D2.mp3 in
|
|
41
|
+
// assets/ if you want sounds to work out of the box), legacy paths as fallback.
|
|
42
|
+
const SOUND_SETS = {
|
|
43
|
+
duck: {
|
|
44
|
+
press: [path.join(PACKAGE_ROOT, 'assets', 'Ya1.mp3'), 'D:/TestBox/deepseek/skin/Ya1.mp3'],
|
|
45
|
+
release: [path.join(PACKAGE_ROOT, 'assets', 'Ya2.mp3'), 'D:/TestBox/deepseek/skin/Ya2.mp3'],
|
|
46
|
+
},
|
|
47
|
+
fx1: {
|
|
48
|
+
press: [path.join(PACKAGE_ROOT, 'assets', 'D1.mp3'), 'D:/TestBox/deepseek/skin/D1.mp3'],
|
|
49
|
+
release: [path.join(PACKAGE_ROOT, 'assets', 'D2.mp3'), 'D:/TestBox/deepseek/skin/D2.mp3'],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
function soundSetFromUrl(url) {
|
|
53
|
+
try {
|
|
54
|
+
const q = String(url || '').split('?')[1] || ''
|
|
55
|
+
const m = /(?:^|&)set=([^&]+)/.exec(q)
|
|
56
|
+
return m ? decodeURIComponent(m[1]) : ''
|
|
57
|
+
} catch (err) { return '' }
|
|
58
|
+
}
|
|
59
|
+
const BALANCE_URL = 'https://api.deepseek.com/user/balance'
|
|
60
|
+
const BALANCE_TTL_MS = 25000
|
|
61
|
+
// DeepSeek CNY prices per million tokens: [空闲时段价, 高峰时段价].
|
|
62
|
+
// 高峰时段:每日 9:00–12:00 和 14:00–18:00(北京时间)。Adjust here if DeepSeek changes pricing.
|
|
63
|
+
const PEAK_HOURS = [
|
|
64
|
+
[9, 12],
|
|
65
|
+
[14, 18],
|
|
66
|
+
]
|
|
67
|
+
const BASE_PRICE = { hit: [0.05, 0.1], miss: [1.5, 3.0], out: [4.5, 9.0] }
|
|
68
|
+
const PRICING = {
|
|
69
|
+
'deepseek-chat': BASE_PRICE,
|
|
70
|
+
'deepseek-reasoner': BASE_PRICE,
|
|
71
|
+
'deepseek-v4-flash': BASE_PRICE,
|
|
72
|
+
'deepseek-v4-pro': BASE_PRICE,
|
|
73
|
+
_default: BASE_PRICE,
|
|
74
|
+
}
|
|
75
|
+
function priceFor(model) {
|
|
76
|
+
const m = String(model || '').toLowerCase()
|
|
77
|
+
for (const key of Object.keys(PRICING)) {
|
|
78
|
+
if (key === '_default') continue
|
|
79
|
+
if (m.indexOf(key) !== -1) return PRICING[key]
|
|
80
|
+
}
|
|
81
|
+
return PRICING._default
|
|
82
|
+
}
|
|
83
|
+
// bucket time is an epoch second; derive the Beijing local hour to pick peak vs off-peak price.
|
|
84
|
+
function isPeakTime(timeSec) {
|
|
85
|
+
if (!isFinite(Number(timeSec))) return false
|
|
86
|
+
const hour = new Date(Number(timeSec) * 1000 + 8 * 3600 * 1000).getUTCHours()
|
|
87
|
+
for (const [start, end] of PEAK_HOURS) {
|
|
88
|
+
if (hour >= start && hour < end) return true
|
|
89
|
+
}
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const JSON_HEADERS = {
|
|
94
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
95
|
+
'Access-Control-Allow-Origin': '*',
|
|
96
|
+
'Cache-Control': 'no-store',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const WIDGET_JS = `(function () {
|
|
100
|
+
if (window.__dshWhaleWidget) return
|
|
101
|
+
window.__dshWhaleWidget = true
|
|
102
|
+
|
|
103
|
+
var MIN_SCALE = 0.6
|
|
104
|
+
var MAX_SCALE = 2.5
|
|
105
|
+
var STEP = 0.1
|
|
106
|
+
var CLICK_SQ = 9
|
|
107
|
+
var REFRESH_MS = 60000
|
|
108
|
+
var CHANGE_MS = 900
|
|
109
|
+
var ANIM_MS = 700
|
|
110
|
+
var BUBBLE_MS = 5000
|
|
111
|
+
var FETCH_TIMEOUT_MS = 25000
|
|
112
|
+
var BALANCE_URL = '/dsh-whale/balance.json'
|
|
113
|
+
var SIZE_URL = '/dsh-whale/size.json'
|
|
114
|
+
var IMG_URL = '/dsh-whale/image.png?v=2'
|
|
115
|
+
|
|
116
|
+
var css = [
|
|
117
|
+
'.dshwv-root{position:fixed;right:0;bottom:0;--dshw-scale:1;--dshw-base:clamp(122px,calc(min(250px,min(100vw,100vh) * 0.28) * var(--dshw-scale)),625px);width:var(--dshw-base);height:var(--dshw-base);pointer-events:none;user-select:none;-webkit-user-select:none;z-index:9999;font-family:inherit;transition:left .16s ease,top .16s ease,transform .3s ease}',
|
|
118
|
+
'.dshwv-root.dshwv-left{transform:scaleX(-1)}',
|
|
119
|
+
'.dshwv-root.dshwv-dragging{cursor:grabbing;transition:none}',
|
|
120
|
+
'.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)}',
|
|
121
|
+
'.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}',
|
|
122
|
+
'.dshwv-bubble{position:absolute;left:0;top:0;width:100%;aspect-ratio:1026/700;pointer-events:none;z-index:1}',
|
|
123
|
+
'.dshwv-bubble svg{display:block;width:100%;height:100%;pointer-events:none}',
|
|
124
|
+
'.dshwv-bubble svg path,.dshwv-bubble svg ellipse{pointer-events:none;cursor:pointer}',
|
|
125
|
+
'.dshwv-bubble.dshwv-bubble-open svg path,.dshwv-bubble.dshwv-bubble-open svg ellipse{pointer-events:visiblePainted}',
|
|
126
|
+
'.dshwv-bubble .dshwv-bshape,.dshwv-bubble .dshwv-b1,.dshwv-bubble .dshwv-b2{opacity:0;transform:scale(.7);transform-box:fill-box;transform-origin:50% 50%;transition:opacity .2s ease,transform .2s ease}',
|
|
127
|
+
'.dshwv-bubble.dshwv-bubble-open .dshwv-bshape,.dshwv-bubble.dshwv-bubble-open .dshwv-b1,.dshwv-bubble.dshwv-bubble-open .dshwv-b2{opacity:1;transform:none}',
|
|
128
|
+
'.dshwv-bubble.dshwv-bubble-open .dshwv-b2{transition-delay:0s}',
|
|
129
|
+
'.dshwv-bubble.dshwv-bubble-open .dshwv-b1{transition-delay:.13s}',
|
|
130
|
+
'.dshwv-bubble.dshwv-bubble-open .dshwv-bshape{transition-delay:.26s}',
|
|
131
|
+
'.dshwv-bubble .dshwv-bshape{transition-delay:.1s}',
|
|
132
|
+
'.dshwv-bubble .dshwv-b1{transition-delay:.2s}',
|
|
133
|
+
'.dshwv-bubble .dshwv-b2{transition-delay:.3s}',
|
|
134
|
+
'.dshwv-text{position:absolute;left:44.25%;top:38%;transform:translate(-50%,-50%);text-align:center;color:#536ba9;line-height:1.15;white-space:nowrap;--dshw-u:calc(var(--dshw-base) / 1026);pointer-events:none;opacity:0;transition:opacity .16s ease,transform .3s ease}',
|
|
135
|
+
'.dshwv-bubble.dshwv-bubble-open .dshwv-text{opacity:1;transition:opacity .16s ease .36s,transform .3s ease}',
|
|
136
|
+
'.dshwv-root.dshwv-left .dshwv-text{transform:translate(-50%,-50%) scaleX(-1)}',
|
|
137
|
+
'.dshwv-label{font-size:calc(var(--dshw-u) * 66);font-weight:600;letter-spacing:.06em}',
|
|
138
|
+
'.dshwv-amount{font-size:calc(var(--dshw-u) * 128);font-weight:800;line-height:1.05}',
|
|
139
|
+
'.dshwv-period{font-size:calc(var(--dshw-u) * 104);font-weight:800;line-height:1.05}',
|
|
140
|
+
'.dshwv-wrap{white-space:normal;max-width:calc(var(--dshw-u) * 560);line-height:1.2}',
|
|
141
|
+
'.dshwv-hint{font-size:calc(var(--dshw-u) * 56);color:#9fb0d9;letter-spacing:.02em;margin-top:calc(var(--dshw-u) * 9)}',
|
|
142
|
+
'.dshwv-menu-btn{position:absolute;top:calc(40.55% + 4px);right:4px;width:26px;height:26px;border:none;border-radius:6px;background:rgba(32,49,112,.85);cursor:pointer;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:0;z-index:2;opacity:0;transition:opacity .15s ease}',
|
|
143
|
+
'.dshwv-menu-btn.dshwv-menu-btn-visible{opacity:1}',
|
|
144
|
+
'.dshwv-menu-btn span{display:block;width:14px;height:2px;background:#fff;border-radius:1px}',
|
|
145
|
+
'.dshwv-menu-btn:hover{background:#203170}',
|
|
146
|
+
'.dshwv-menu{position:fixed;min-width:172px;background:rgba(255,255,255,.92);border:1px solid rgba(32,49,112,.35);border-radius:10px;padding:10px 12px;opacity:0;transform:scale(.92) translateY(-4px);transform-origin:top right;transition:opacity .18s ease,transform .2s cubic-bezier(.34,1.56,.64,1);pointer-events:none;z-index:10000;box-shadow:0 6px 18px rgba(0,0,0,.18);color-scheme:light}',
|
|
147
|
+
'.dshwv-menu.dshwv-menu-open{opacity:1;transform:scale(1) translateY(0);pointer-events:auto}',
|
|
148
|
+
'.dshwv-menu-row{display:flex;align-items:center;gap:8px;margin:5px 0;color:#203170;font-size:12px;white-space:nowrap}',
|
|
149
|
+
'.dshwv-range{flex:1;min-width:0;accent-color:#203170}',
|
|
150
|
+
'.dshwv-number{width:46px;border:1px solid rgba(32,49,112,.4);border-radius:6px;padding:2px 4px;font-size:12px;color:#203170;background:#fff}',
|
|
151
|
+
'.dshwv-sound{flex:1;border:1px solid rgba(32,49,112,.4);border-radius:6px;background:rgba(32,49,112,.08);color:#203170;font-size:12px;padding:3px 0;cursor:pointer}',
|
|
152
|
+
'.dshwv-sound:hover{background:rgba(32,49,112,.16)}',
|
|
153
|
+
'.dshwv-volpct{width:36px;text-align:right;color:#203170;font-size:12px}'
|
|
154
|
+
].join('\\n')
|
|
155
|
+
|
|
156
|
+
var styleEl = document.createElement('style')
|
|
157
|
+
styleEl.textContent = css
|
|
158
|
+
document.head.appendChild(styleEl)
|
|
159
|
+
|
|
160
|
+
var root = document.createElement('div')
|
|
161
|
+
root.className = 'dshwv-root'
|
|
162
|
+
|
|
163
|
+
var img = document.createElement('img')
|
|
164
|
+
img.className = 'dshwv-img'
|
|
165
|
+
img.src = IMG_URL
|
|
166
|
+
img.alt = 'DeepSeek 余额'
|
|
167
|
+
img.draggable = false
|
|
168
|
+
|
|
169
|
+
var menuBtn = document.createElement('button')
|
|
170
|
+
menuBtn.type = 'button'
|
|
171
|
+
menuBtn.className = 'dshwv-menu-btn'
|
|
172
|
+
menuBtn.title = '菜单'
|
|
173
|
+
menuBtn.innerHTML = '<span></span><span></span><span></span>'
|
|
174
|
+
menuBtn.addEventListener('click', function (e) { e.stopPropagation(); toggleMenu() })
|
|
175
|
+
|
|
176
|
+
var menuBox = document.createElement('div')
|
|
177
|
+
menuBox.className = 'dshwv-menu'
|
|
178
|
+
function menuLabel(text) {
|
|
179
|
+
var s = document.createElement('span')
|
|
180
|
+
s.textContent = text
|
|
181
|
+
return s
|
|
182
|
+
}
|
|
183
|
+
function menuRow() {
|
|
184
|
+
var r = document.createElement('div')
|
|
185
|
+
r.className = 'dshwv-menu-row'
|
|
186
|
+
return r
|
|
187
|
+
}
|
|
188
|
+
var scaleInput = document.createElement('input')
|
|
189
|
+
scaleInput.type = 'range'
|
|
190
|
+
scaleInput.min = String(MIN_SCALE)
|
|
191
|
+
scaleInput.max = String(MAX_SCALE)
|
|
192
|
+
scaleInput.step = '0.1'
|
|
193
|
+
scaleInput.className = 'dshwv-range'
|
|
194
|
+
scaleInput.value = '1.5'
|
|
195
|
+
var scaleNumber = document.createElement('input')
|
|
196
|
+
scaleNumber.type = 'number'
|
|
197
|
+
scaleNumber.min = '1'
|
|
198
|
+
scaleNumber.max = '20'
|
|
199
|
+
scaleNumber.step = '1'
|
|
200
|
+
scaleNumber.className = 'dshwv-number'
|
|
201
|
+
scaleNumber.value = '10'
|
|
202
|
+
scaleInput.addEventListener('pointerdown', function () { root.style.transition = 'none' })
|
|
203
|
+
scaleInput.addEventListener('input', function () { setScale(scaleInput.value) })
|
|
204
|
+
scaleInput.addEventListener('change', function () { root.style.transition = '' })
|
|
205
|
+
scaleNumber.addEventListener('change', function () {
|
|
206
|
+
var v = Math.round(Number(scaleNumber.value))
|
|
207
|
+
var s = MIN_SCALE + Math.max(0, Math.min(20, v) - 1) * (MAX_SCALE - MIN_SCALE) / 19
|
|
208
|
+
setScale(s)
|
|
209
|
+
root.style.transition = ''
|
|
210
|
+
})
|
|
211
|
+
var soundSelect = document.createElement('select')
|
|
212
|
+
soundSelect.className = 'dshwv-sound'
|
|
213
|
+
function soundOpt(value, label) {
|
|
214
|
+
var o = document.createElement('option')
|
|
215
|
+
o.value = value
|
|
216
|
+
o.textContent = label
|
|
217
|
+
return o
|
|
218
|
+
}
|
|
219
|
+
soundSelect.appendChild(soundOpt('duck', '小黄鸭'))
|
|
220
|
+
soundSelect.appendChild(soundOpt('fx1', '音效1'))
|
|
221
|
+
soundSelect.addEventListener('change', function () { setSoundSet(soundSelect.value) })
|
|
222
|
+
var usageSelect = document.createElement('select')
|
|
223
|
+
usageSelect.className = 'dshwv-sound'
|
|
224
|
+
usageSelect.appendChild(soundOpt('ledger', '小鲸鱼记账 (推荐)'))
|
|
225
|
+
usageSelect.appendChild(soundOpt('token', '实时·令牌 (用法:去问dsh)'))
|
|
226
|
+
usageSelect.addEventListener('change', function () { setUsageMode(usageSelect.value) })
|
|
227
|
+
var row1 = menuRow()
|
|
228
|
+
row1.appendChild(menuLabel('大小'))
|
|
229
|
+
row1.appendChild(scaleInput)
|
|
230
|
+
row1.appendChild(scaleNumber)
|
|
231
|
+
var row2 = menuRow()
|
|
232
|
+
row2.appendChild(menuLabel('音效'))
|
|
233
|
+
row2.appendChild(soundSelect)
|
|
234
|
+
var volInput = document.createElement('input')
|
|
235
|
+
volInput.type = 'range'
|
|
236
|
+
volInput.min = '0'
|
|
237
|
+
volInput.max = '1'
|
|
238
|
+
volInput.step = '0.05'
|
|
239
|
+
volInput.className = 'dshwv-range'
|
|
240
|
+
volInput.value = '0.9'
|
|
241
|
+
var volPct = document.createElement('span')
|
|
242
|
+
volPct.className = 'dshwv-volpct'
|
|
243
|
+
volPct.textContent = '90%'
|
|
244
|
+
volInput.addEventListener('input', function () { setVol(volInput.value) })
|
|
245
|
+
var row3 = menuRow()
|
|
246
|
+
row3.appendChild(menuLabel('音量'))
|
|
247
|
+
row3.appendChild(volInput)
|
|
248
|
+
row3.appendChild(volPct)
|
|
249
|
+
var row4 = menuRow()
|
|
250
|
+
row4.appendChild(menuLabel('用量'))
|
|
251
|
+
row4.appendChild(usageSelect)
|
|
252
|
+
menuBox.appendChild(row1)
|
|
253
|
+
menuBox.appendChild(row2)
|
|
254
|
+
menuBox.appendChild(row3)
|
|
255
|
+
menuBox.appendChild(row4)
|
|
256
|
+
|
|
257
|
+
var textBox = document.createElement('div')
|
|
258
|
+
textBox.className = 'dshwv-text'
|
|
259
|
+
var labelEl = document.createElement('div')
|
|
260
|
+
labelEl.className = 'dshwv-label'
|
|
261
|
+
labelEl.textContent = 'DeepSeek 余额'
|
|
262
|
+
var amountEl = document.createElement('div')
|
|
263
|
+
amountEl.className = 'dshwv-amount'
|
|
264
|
+
var hintEl = document.createElement('div')
|
|
265
|
+
hintEl.className = 'dshwv-hint'
|
|
266
|
+
textBox.appendChild(labelEl)
|
|
267
|
+
textBox.appendChild(amountEl)
|
|
268
|
+
textBox.appendChild(hintEl)
|
|
269
|
+
|
|
270
|
+
var bubbleBox = document.createElement('div')
|
|
271
|
+
bubbleBox.className = 'dshwv-bubble'
|
|
272
|
+
bubbleBox.innerHTML = '<svg viewBox="0 0 1026 700" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">' +
|
|
273
|
+
'<path class="dshwv-bshape" fill="#FFFFFF" stroke="#203170" stroke-width="18" stroke-linejoin="round" stroke-linecap="round" d="M 827 248 A 373 232 0 1 0 81 246 A 373 232 0 0 0 301 465 A 57 32 10 0 0 413 484 A 373 232 0 0 0 827 248 Z"/>' +
|
|
274
|
+
'<ellipse class="dshwv-b1" cx="352" cy="561" rx="37.5" ry="26" fill="#FFFFFF" stroke="#203170" stroke-width="18"/>' +
|
|
275
|
+
'<ellipse class="dshwv-b2" cx="442" cy="646" rx="24.5" ry="18" fill="#FFFFFF" stroke="#203170" stroke-width="18"/>' +
|
|
276
|
+
'</svg>'
|
|
277
|
+
bubbleBox.appendChild(textBox)
|
|
278
|
+
bubbleBox.addEventListener('click', function (e) {
|
|
279
|
+
e.stopPropagation()
|
|
280
|
+
if (!bubbleShown) return
|
|
281
|
+
if (bubbleRandomActive) {
|
|
282
|
+
// 再次点击:关闭
|
|
283
|
+
hideBubble()
|
|
284
|
+
} else {
|
|
285
|
+
// 首次点击:切到随机台词段(不延长总显示时长)
|
|
286
|
+
bubbleRandomActive = true
|
|
287
|
+
bubbleRandomLines = pickRandomLines()
|
|
288
|
+
swapBubbleContent(function () { applyBubbleLines(bubbleRandomLines) })
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
var body = document.createElement('div')
|
|
293
|
+
body.className = 'dshwv-body'
|
|
294
|
+
body.appendChild(img)
|
|
295
|
+
body.appendChild(bubbleBox)
|
|
296
|
+
root.appendChild(body)
|
|
297
|
+
root.appendChild(menuBtn)
|
|
298
|
+
document.body.appendChild(root)
|
|
299
|
+
document.body.appendChild(menuBox)
|
|
300
|
+
|
|
301
|
+
// Position model: the widget is ALWAYS expressed in left/top px (so edge snaps
|
|
302
|
+
// animate smoothly via the CSS transition on both sides — switching to
|
|
303
|
+
// right/auto cannot transition and flashes). The anchor info (h/v + offsets)
|
|
304
|
+
// lives in state and is used by settle() to recompute coordinates on window
|
|
305
|
+
// resize and size changes, keeping the widget glued to its anchored edge.
|
|
306
|
+
var state = {
|
|
307
|
+
scale: 1.5,
|
|
308
|
+
h: 'right',
|
|
309
|
+
hOff: 0,
|
|
310
|
+
v: 'bottom',
|
|
311
|
+
vOff: 0,
|
|
312
|
+
left: 0,
|
|
313
|
+
top: 0,
|
|
314
|
+
balance: null,
|
|
315
|
+
currency: null,
|
|
316
|
+
todayUsage: null,
|
|
317
|
+
isPeak: false,
|
|
318
|
+
status: 'loading',
|
|
319
|
+
message: ''
|
|
320
|
+
}
|
|
321
|
+
var busy = false
|
|
322
|
+
var settleTimer = null
|
|
323
|
+
var animDelayTimer = null
|
|
324
|
+
var drag = null
|
|
325
|
+
var shown = null
|
|
326
|
+
var animId = null
|
|
327
|
+
var bubbleShown = false
|
|
328
|
+
var bubbleTimer = null
|
|
329
|
+
var bubbleRandomActive = false
|
|
330
|
+
var bubbleRandomLines = null
|
|
331
|
+
var BUBBLE_STYLE_CLASS = { A: 'dshwv-label', B: 'dshwv-amount', P: 'dshwv-period', C: 'dshwv-hint' }
|
|
332
|
+
function pickOne(arr) { return arr[Math.floor(Math.random() * arr.length)] }
|
|
333
|
+
function singleCenter(style, text, color, wrap) { return [null, { t: text, s: style, c: color || '', w: !!wrap }, null] }
|
|
334
|
+
function buildGroup1() {
|
|
335
|
+
var peak = !!state.isPeak
|
|
336
|
+
return [
|
|
337
|
+
{ t: '当前时间段为:', s: 'A', c: '' },
|
|
338
|
+
{ t: peak ? '高峰时段' : '空闲时段', s: 'P', c: peak ? '#e0433f' : '#2fa24c' },
|
|
339
|
+
{ t: '今日已用 ' + fmt(state.todayUsage, state.currency), s: 'C', c: '' },
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
var RANDOM_GROUPS = [
|
|
343
|
+
{ w: 20, lines: buildGroup1 },
|
|
344
|
+
{ w: 7, lines: function () { return singleCenter('B', pickOne(['好模型... ↓', '好女孩...↓'])) } },
|
|
345
|
+
{ w: 7, lines: function () { return singleCenter('A', pickOne(['不知道用户有什么用,先赶走吧~', '我...我...我也要挣钱吗?', '我去吃饭啦,测完叫我', '压力一只蓝色大肥鱼?!', 'DeepSleep...', '坏了...用户彻底怒了!']), '', true) } },
|
|
346
|
+
{ w: 3, lines: function () { return singleCenter('A', pickOne(['你目录里的dsh是什么...大烧货吗...?', '恭喜你实现token自由!token全跑了!', '真当我是便宜货啊...']), '', true) } },
|
|
347
|
+
{ w: 1, lines: function () { return [{ t: '这个', s: 'A', c: '' }, { t: '凶', s: 'B', c: '' }, { t: '是什么意思呀...', s: 'A', c: '' }] } },
|
|
348
|
+
{ w: 1, lines: function () { return singleCenter('B', '哦鲸鲸... ') } },
|
|
349
|
+
]
|
|
350
|
+
function pickRandomLines() {
|
|
351
|
+
var total = 0
|
|
352
|
+
for (var i = 0; i < RANDOM_GROUPS.length; i++) total += RANDOM_GROUPS[i].w
|
|
353
|
+
var r = Math.random() * total
|
|
354
|
+
for (var i = 0; i < RANDOM_GROUPS.length; i++) {
|
|
355
|
+
r -= RANDOM_GROUPS[i].w
|
|
356
|
+
if (r < 0) return RANDOM_GROUPS[i].lines()
|
|
357
|
+
}
|
|
358
|
+
return RANDOM_GROUPS[RANDOM_GROUPS.length - 1].lines()
|
|
359
|
+
}
|
|
360
|
+
function applyBubbleLines(lines) {
|
|
361
|
+
var els = [labelEl, amountEl, hintEl]
|
|
362
|
+
for (var i = 0; i < 3; i++) {
|
|
363
|
+
var el = els[i]
|
|
364
|
+
var ln = lines && lines[i]
|
|
365
|
+
if (ln) {
|
|
366
|
+
el.style.display = ''
|
|
367
|
+
el.className = (BUBBLE_STYLE_CLASS[ln.s] || 'dshwv-label') + (ln.w ? ' dshwv-wrap' : '')
|
|
368
|
+
el.textContent = ln.t
|
|
369
|
+
el.style.color = ln.c || ''
|
|
370
|
+
} else {
|
|
371
|
+
el.style.display = 'none'
|
|
372
|
+
el.textContent = ''
|
|
373
|
+
el.style.color = ''
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
var bubbleSwapTimer = null
|
|
378
|
+
var hintFadeTimer = null
|
|
379
|
+
var lastHintText = null
|
|
380
|
+
function setHint(text) {
|
|
381
|
+
// 「加载中…」→「今日已用」等提示行变化时做淡出淡入,其余直接替换
|
|
382
|
+
if (text === lastHintText) return
|
|
383
|
+
lastHintText = text
|
|
384
|
+
if (hintFadeTimer) { clearTimeout(hintFadeTimer); hintFadeTimer = null }
|
|
385
|
+
if (!bubbleShown) {
|
|
386
|
+
hintEl.textContent = text
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
hintEl.style.transition = 'opacity .18s ease'
|
|
390
|
+
hintEl.style.opacity = '0'
|
|
391
|
+
hintFadeTimer = setTimeout(function () {
|
|
392
|
+
hintFadeTimer = null
|
|
393
|
+
hintEl.textContent = text
|
|
394
|
+
hintEl.style.opacity = '1'
|
|
395
|
+
setTimeout(function () {
|
|
396
|
+
hintEl.style.transition = ''
|
|
397
|
+
hintEl.style.opacity = ''
|
|
398
|
+
}, 220)
|
|
399
|
+
}, 190)
|
|
400
|
+
}
|
|
401
|
+
function swapBubbleContent(applyFn) {
|
|
402
|
+
if (bubbleSwapTimer) { clearTimeout(bubbleSwapTimer); bubbleSwapTimer = null }
|
|
403
|
+
textBox.style.transition = 'opacity .18s ease'
|
|
404
|
+
textBox.style.opacity = '0'
|
|
405
|
+
bubbleSwapTimer = setTimeout(function () {
|
|
406
|
+
bubbleSwapTimer = null
|
|
407
|
+
applyFn()
|
|
408
|
+
textBox.style.opacity = '1'
|
|
409
|
+
setTimeout(function () {
|
|
410
|
+
textBox.style.transition = ''
|
|
411
|
+
textBox.style.opacity = ''
|
|
412
|
+
}, 220)
|
|
413
|
+
}, 190)
|
|
414
|
+
}
|
|
415
|
+
function restoreBubbleLines() {
|
|
416
|
+
if (bubbleSwapTimer) { clearTimeout(bubbleSwapTimer); bubbleSwapTimer = null }
|
|
417
|
+
if (hintFadeTimer) { clearTimeout(hintFadeTimer); hintFadeTimer = null }
|
|
418
|
+
lastHintText = null
|
|
419
|
+
textBox.style.transition = ''
|
|
420
|
+
textBox.style.opacity = ''
|
|
421
|
+
labelEl.style.display = ''
|
|
422
|
+
labelEl.className = 'dshwv-label'
|
|
423
|
+
labelEl.textContent = 'DeepSeek 余额'
|
|
424
|
+
labelEl.style.color = ''
|
|
425
|
+
amountEl.className = 'dshwv-amount'
|
|
426
|
+
amountEl.style.color = ''
|
|
427
|
+
hintEl.className = 'dshwv-hint'
|
|
428
|
+
hintEl.style.color = ''
|
|
429
|
+
render()
|
|
430
|
+
}
|
|
431
|
+
function showBubble() {
|
|
432
|
+
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null }
|
|
433
|
+
bubbleShown = true
|
|
434
|
+
bubbleRandomActive = false
|
|
435
|
+
restoreBubbleLines()
|
|
436
|
+
bubbleBox.classList.add('dshwv-bubble-open')
|
|
437
|
+
// 默认展示当前内容;点击气泡切到随机台词段;总时长 5 秒自动关闭
|
|
438
|
+
bubbleTimer = setTimeout(hideBubble, BUBBLE_MS)
|
|
439
|
+
}
|
|
440
|
+
function hideBubble() {
|
|
441
|
+
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null }
|
|
442
|
+
if (bubbleSwapTimer) { clearTimeout(bubbleSwapTimer); bubbleSwapTimer = null }
|
|
443
|
+
if (hintFadeTimer) { clearTimeout(hintFadeTimer); hintFadeTimer = null }
|
|
444
|
+
textBox.style.transition = ''
|
|
445
|
+
textBox.style.opacity = ''
|
|
446
|
+
hintEl.style.transition = ''
|
|
447
|
+
hintEl.style.opacity = ''
|
|
448
|
+
bubbleRandomActive = false
|
|
449
|
+
bubbleRandomLines = null
|
|
450
|
+
bubbleShown = false
|
|
451
|
+
bubbleBox.classList.remove('dshwv-bubble-open')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v) }
|
|
455
|
+
function viewport() {
|
|
456
|
+
return {
|
|
457
|
+
w: window.innerWidth || document.documentElement.clientWidth || 1280,
|
|
458
|
+
h: window.innerHeight || document.documentElement.clientHeight || 800
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function fmt(balance, currency) {
|
|
462
|
+
var num = Number(balance)
|
|
463
|
+
var fixed = isFinite(num) ? num.toFixed(2) : '--'
|
|
464
|
+
return currency === 'CNY' ? '¥ ' + fixed : fixed + ' ' + currency
|
|
465
|
+
}
|
|
466
|
+
function animateAmount(from, to, currency, duration) {
|
|
467
|
+
if (animId) cancelAnimationFrame(animId)
|
|
468
|
+
if (from === null || !isFinite(from)) from = to
|
|
469
|
+
if (from === to) {
|
|
470
|
+
shown = to
|
|
471
|
+
amountEl.textContent = fmt(to, currency)
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
var startTime = null
|
|
475
|
+
function step(ts) {
|
|
476
|
+
if (startTime === null) startTime = ts
|
|
477
|
+
var t = Math.min(1, (ts - startTime) / duration)
|
|
478
|
+
var eased = 1 - Math.pow(1 - t, 3)
|
|
479
|
+
var val = from + (to - from) * eased
|
|
480
|
+
amountEl.textContent = fmt(val, currency)
|
|
481
|
+
if (t < 1) {
|
|
482
|
+
animId = requestAnimationFrame(step)
|
|
483
|
+
} else {
|
|
484
|
+
animId = null
|
|
485
|
+
shown = to
|
|
486
|
+
amountEl.textContent = fmt(to, currency)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
animId = requestAnimationFrame(step)
|
|
490
|
+
}
|
|
491
|
+
function render() {
|
|
492
|
+
var amount, hint
|
|
493
|
+
if (state.status === 'error') {
|
|
494
|
+
amount = shown !== null ? fmt(shown, state.currency) : '--'
|
|
495
|
+
hint = state.message ? state.message.slice(0, 14) : '获取失败 · 点击重试'
|
|
496
|
+
} else if (state.balance === null) {
|
|
497
|
+
amount = shown !== null ? fmt(shown, state.currency) : '…'
|
|
498
|
+
hint = '加载中…'
|
|
499
|
+
} else {
|
|
500
|
+
amount = shown !== null ? fmt(shown, state.currency) : fmt(state.balance, state.currency)
|
|
501
|
+
hint = '今日已用 ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
502
|
+
}
|
|
503
|
+
amountEl.textContent = amount
|
|
504
|
+
if (bubbleRandomActive && bubbleRandomLines) {
|
|
505
|
+
applyBubbleLines(bubbleRandomLines)
|
|
506
|
+
} else {
|
|
507
|
+
setHint(hint)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function express() {
|
|
511
|
+
root.style.right = 'auto'
|
|
512
|
+
root.style.bottom = 'auto'
|
|
513
|
+
root.style.left = state.left + 'px'
|
|
514
|
+
root.style.top = state.top + 'px'
|
|
515
|
+
root.classList.toggle('dshwv-left', state.h === 'left')
|
|
516
|
+
}
|
|
517
|
+
function settle() {
|
|
518
|
+
var vp = viewport()
|
|
519
|
+
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
520
|
+
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
521
|
+
if (drag && drag.active) {
|
|
522
|
+
// mid-drag resize: keep the pointer-follow position, just clamp into view
|
|
523
|
+
state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
|
|
524
|
+
state.top = clamp(state.top, 0, Math.max(0, vp.h - h))
|
|
525
|
+
express()
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
if (state.h === 'right') {
|
|
529
|
+
state.left = Math.max(0, vp.w - w - state.hOff)
|
|
530
|
+
} else if (state.h === 'left') {
|
|
531
|
+
state.left = state.hOff
|
|
532
|
+
} else {
|
|
533
|
+
state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
|
|
534
|
+
}
|
|
535
|
+
if (state.v === 'bottom') {
|
|
536
|
+
state.top = Math.max(0, vp.h - h - state.vOff)
|
|
537
|
+
} else if (state.v === 'top') {
|
|
538
|
+
state.top = state.vOff
|
|
539
|
+
} else {
|
|
540
|
+
state.top = clamp(state.top, 0, Math.max(0, vp.h - h))
|
|
541
|
+
}
|
|
542
|
+
express()
|
|
543
|
+
}
|
|
544
|
+
function refresh(manual) {
|
|
545
|
+
if (busy) return
|
|
546
|
+
busy = true
|
|
547
|
+
if (animDelayTimer) { clearTimeout(animDelayTimer); animDelayTimer = null }
|
|
548
|
+
if (manual || state.balance === null) { state.status = 'loading'; render() }
|
|
549
|
+
var ctrl = null
|
|
550
|
+
var timer = null
|
|
551
|
+
try {
|
|
552
|
+
ctrl = new AbortController()
|
|
553
|
+
timer = setTimeout(function () { try { ctrl.abort() } catch (err) {} }, FETCH_TIMEOUT_MS)
|
|
554
|
+
} catch (err) {}
|
|
555
|
+
fetch(BALANCE_URL, { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
|
|
556
|
+
.then(function (r) { return r.json() })
|
|
557
|
+
.then(function (data) {
|
|
558
|
+
if (data && data.ok) {
|
|
559
|
+
var nb = Number(data.totalBalance)
|
|
560
|
+
var nc = String(data.currency || 'CNY')
|
|
561
|
+
var changed = state.balance !== null && (nb !== state.balance || nc !== state.currency)
|
|
562
|
+
var currencyChanged = state.currency !== null && nc !== state.currency
|
|
563
|
+
state.balance = nb
|
|
564
|
+
state.currency = nc
|
|
565
|
+
state.message = ''
|
|
566
|
+
state.todayUsage = data.todayUsage !== undefined ? data.todayUsage : null
|
|
567
|
+
state.isPeak = !!data.isPeak
|
|
568
|
+
if (changed && !currencyChanged) {
|
|
569
|
+
if (!manual) {
|
|
570
|
+
showBubble()
|
|
571
|
+
state.status = 'changing'
|
|
572
|
+
// balance-change bubble: wait 0.3s after it floats out, then roll the number
|
|
573
|
+
if (animDelayTimer) clearTimeout(animDelayTimer)
|
|
574
|
+
animDelayTimer = setTimeout(function () {
|
|
575
|
+
animDelayTimer = null
|
|
576
|
+
animateAmount(shown, nb, nc, ANIM_MS)
|
|
577
|
+
}, 300)
|
|
578
|
+
if (settleTimer) clearTimeout(settleTimer)
|
|
579
|
+
settleTimer = setTimeout(function () {
|
|
580
|
+
settleTimer = null
|
|
581
|
+
if (state.status === 'changing') { state.status = 'ok'; render() }
|
|
582
|
+
}, CHANGE_MS + 300)
|
|
583
|
+
} else {
|
|
584
|
+
animateAmount(shown, nb, nc, ANIM_MS)
|
|
585
|
+
state.status = 'ok'
|
|
586
|
+
render()
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
if (animId === null) shown = nb
|
|
590
|
+
state.status = 'ok'
|
|
591
|
+
render()
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
state.status = 'error'
|
|
595
|
+
state.message = (data && data.error) ? String(data.error) : '获取失败'
|
|
596
|
+
render()
|
|
597
|
+
}
|
|
598
|
+
})
|
|
599
|
+
.catch(function () {
|
|
600
|
+
state.status = 'error'
|
|
601
|
+
state.message = '获取失败'
|
|
602
|
+
render()
|
|
603
|
+
})
|
|
604
|
+
.finally(function () {
|
|
605
|
+
busy = false
|
|
606
|
+
if (timer) clearTimeout(timer)
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
var soundOn = true
|
|
610
|
+
var soundVol = 0.9
|
|
611
|
+
var soundSet = 'duck'
|
|
612
|
+
var usageMode = 'ledger'
|
|
613
|
+
function saveConfig() {
|
|
614
|
+
try {
|
|
615
|
+
fetch(SIZE_URL, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode }) })
|
|
616
|
+
} catch (err) {}
|
|
617
|
+
}
|
|
618
|
+
function setUsageMode(v) {
|
|
619
|
+
usageMode = v === 'token' ? 'token' : 'ledger'
|
|
620
|
+
usageSelect.value = usageMode
|
|
621
|
+
saveConfig()
|
|
622
|
+
refresh(false)
|
|
623
|
+
}
|
|
624
|
+
function scaleToDisplay(s) {
|
|
625
|
+
return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
|
|
626
|
+
}
|
|
627
|
+
function setScale(v) {
|
|
628
|
+
var next = Math.round(Math.min(MAX_SCALE, Math.max(MIN_SCALE, Number(v))) * 10) / 10
|
|
629
|
+
var rect = root.getBoundingClientRect()
|
|
630
|
+
// fixed point: the whale's corner — bottom-right when unflipped, bottom-left
|
|
631
|
+
// when flipped. Growing extends the widget up-left / up-right from that
|
|
632
|
+
// corner; shrinking pulls it back toward the corner. The whale always hugs
|
|
633
|
+
// its corner while scaling.
|
|
634
|
+
var fx = state.h === 'left' ? rect.left : rect.right
|
|
635
|
+
var fy = rect.bottom
|
|
636
|
+
state.scale = next
|
|
637
|
+
root.style.setProperty('--dshw-scale', String(next))
|
|
638
|
+
scaleInput.value = String(next)
|
|
639
|
+
scaleNumber.value = String(scaleToDisplay(next))
|
|
640
|
+
saveConfig()
|
|
641
|
+
// keep the corner fixed while resizing; the position correction applies
|
|
642
|
+
// instantly because the caller disables the transition for the whole drag
|
|
643
|
+
var r2 = root.getBoundingClientRect()
|
|
644
|
+
var vp = viewport()
|
|
645
|
+
if (state.h === 'left') {
|
|
646
|
+
state.left = Math.min(Math.max(fx, 0), Math.max(0, vp.w - r2.width))
|
|
647
|
+
} else {
|
|
648
|
+
state.left = Math.min(Math.max(fx - r2.width, 0), Math.max(0, vp.w - r2.width))
|
|
649
|
+
}
|
|
650
|
+
state.top = Math.min(Math.max(fy - r2.height, 0), Math.max(0, vp.h - r2.height))
|
|
651
|
+
express()
|
|
652
|
+
}
|
|
653
|
+
function setVol(v) {
|
|
654
|
+
var next = Math.round(Math.min(1, Math.max(0, Number(v))) * 100) / 100
|
|
655
|
+
soundVol = next
|
|
656
|
+
soundOn = next > 0
|
|
657
|
+
volInput.value = String(next)
|
|
658
|
+
volPct.textContent = Math.round(next * 100) + '%'
|
|
659
|
+
try {
|
|
660
|
+
if (pressAudio) pressAudio.volume = next
|
|
661
|
+
if (releaseAudio) releaseAudio.volume = next
|
|
662
|
+
} catch (err) {}
|
|
663
|
+
saveConfig()
|
|
664
|
+
}
|
|
665
|
+
function setSoundSet(v) {
|
|
666
|
+
soundSet = v === 'fx1' ? 'fx1' : 'duck'
|
|
667
|
+
soundSelect.value = soundSet
|
|
668
|
+
applySoundSet()
|
|
669
|
+
saveConfig()
|
|
670
|
+
}
|
|
671
|
+
var SQUISH = 'scaleY(0.88) scaleX(1.05)'
|
|
672
|
+
var pressAudio = null
|
|
673
|
+
var releaseAudio = null
|
|
674
|
+
var pressing = false
|
|
675
|
+
var pressEnded = false
|
|
676
|
+
var releasePlayed = false
|
|
677
|
+
var releaseTimer = null
|
|
678
|
+
function applySoundSet() {
|
|
679
|
+
try {
|
|
680
|
+
pressAudio = new Audio('/dsh-whale/sound/press.mp3?set=' + soundSet)
|
|
681
|
+
pressAudio.preload = 'auto'
|
|
682
|
+
pressAudio.volume = soundVol
|
|
683
|
+
releaseAudio = new Audio('/dsh-whale/sound/release.mp3?set=' + soundSet)
|
|
684
|
+
releaseAudio.preload = 'auto'
|
|
685
|
+
releaseAudio.volume = soundVol
|
|
686
|
+
} catch (err) {}
|
|
687
|
+
}
|
|
688
|
+
function playPress() {
|
|
689
|
+
if (!pressAudio || !soundOn) return
|
|
690
|
+
try {
|
|
691
|
+
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
692
|
+
if (releaseAudio) {
|
|
693
|
+
releaseAudio.pause()
|
|
694
|
+
releaseAudio.currentTime = 0
|
|
695
|
+
}
|
|
696
|
+
pressEnded = false
|
|
697
|
+
releasePlayed = false
|
|
698
|
+
pressAudio.onended = function () {
|
|
699
|
+
pressEnded = true
|
|
700
|
+
// fallback (duration unknown): click → Ya2 right after Ya1 ends
|
|
701
|
+
if (!pressing && !releasePlayed) playRelease()
|
|
702
|
+
// hold: still pressed → wait for pressUp()
|
|
703
|
+
}
|
|
704
|
+
pressAudio.currentTime = 0
|
|
705
|
+
var p = pressAudio.play()
|
|
706
|
+
if (p && typeof p.catch === 'function') p.catch(function () {})
|
|
707
|
+
} catch (err) {}
|
|
708
|
+
}
|
|
709
|
+
function playRelease() {
|
|
710
|
+
if (releasePlayed || !releaseAudio || !soundOn) return
|
|
711
|
+
releasePlayed = true
|
|
712
|
+
try {
|
|
713
|
+
releaseAudio.currentTime = 0
|
|
714
|
+
var p = releaseAudio.play()
|
|
715
|
+
if (p && typeof p.catch === 'function') p.catch(function () {})
|
|
716
|
+
} catch (err) {}
|
|
717
|
+
}
|
|
718
|
+
function pressDown() {
|
|
719
|
+
body.style.transform = SQUISH
|
|
720
|
+
pressing = true
|
|
721
|
+
playPress()
|
|
722
|
+
}
|
|
723
|
+
function pressUp() {
|
|
724
|
+
body.style.transform = 'scaleY(1) scaleX(1)'
|
|
725
|
+
pressing = false
|
|
726
|
+
if (pressEnded) {
|
|
727
|
+
// hold (or released after Ya1 finished) → Ya2 now
|
|
728
|
+
playRelease()
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
// click: start Ya2 in the last 100ms of Ya1's playback
|
|
732
|
+
var durKnown = false
|
|
733
|
+
var remainMs = 0
|
|
734
|
+
try {
|
|
735
|
+
var dur = pressAudio ? pressAudio.duration : 0
|
|
736
|
+
if (isFinite(dur) && dur > 0) {
|
|
737
|
+
durKnown = true
|
|
738
|
+
remainMs = (dur - pressAudio.currentTime) * 1000
|
|
739
|
+
}
|
|
740
|
+
} catch (err) {}
|
|
741
|
+
if (durKnown) {
|
|
742
|
+
releaseTimer = setTimeout(function () {
|
|
743
|
+
releaseTimer = null
|
|
744
|
+
playRelease()
|
|
745
|
+
}, Math.max(0, remainMs - 100))
|
|
746
|
+
}
|
|
747
|
+
// duration unknown → pressAudio.onended fallback plays Ya2 after Ya1 ends
|
|
748
|
+
}
|
|
749
|
+
var menuOpen = false
|
|
750
|
+
function toggleMenu() {
|
|
751
|
+
menuOpen = !menuOpen
|
|
752
|
+
if (menuOpen) positionMenu()
|
|
753
|
+
menuBox.classList.toggle('dshwv-menu-open', menuOpen)
|
|
754
|
+
if (menuOpen) menuBtn.classList.add('dshwv-menu-btn-visible')
|
|
755
|
+
}
|
|
756
|
+
function closeMenu() {
|
|
757
|
+
menuOpen = false
|
|
758
|
+
menuBox.classList.remove('dshwv-menu-open')
|
|
759
|
+
root.style.transition = ''
|
|
760
|
+
snapCheck()
|
|
761
|
+
}
|
|
762
|
+
function snapCheck() {
|
|
763
|
+
var rect = root.getBoundingClientRect()
|
|
764
|
+
var vp = viewport()
|
|
765
|
+
var w = rect.width, h = rect.height
|
|
766
|
+
var left = rect.left, top = rect.top
|
|
767
|
+
var centerX = left + w / 2
|
|
768
|
+
var centerY = top + h / 2
|
|
769
|
+
var moved = false
|
|
770
|
+
if (centerX < vp.w / 4) {
|
|
771
|
+
state.h = 'left'
|
|
772
|
+
state.hOff = 0
|
|
773
|
+
left = 0
|
|
774
|
+
moved = true
|
|
775
|
+
} else if (centerX > vp.w * 3 / 4) {
|
|
776
|
+
state.h = 'right'
|
|
777
|
+
state.hOff = 0
|
|
778
|
+
left = vp.w - w
|
|
779
|
+
moved = true
|
|
780
|
+
} else {
|
|
781
|
+
state.h = null
|
|
782
|
+
state.hOff = left
|
|
783
|
+
}
|
|
784
|
+
if (centerY < vp.h / 4) {
|
|
785
|
+
state.v = 'top'
|
|
786
|
+
state.vOff = 0
|
|
787
|
+
top = 0
|
|
788
|
+
moved = true
|
|
789
|
+
} else {
|
|
790
|
+
state.v = 'bottom'
|
|
791
|
+
state.vOff = Math.max(0, vp.h - top - h)
|
|
792
|
+
}
|
|
793
|
+
if (moved) {
|
|
794
|
+
state.left = left
|
|
795
|
+
state.top = top
|
|
796
|
+
settle()
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function positionMenu() {
|
|
800
|
+
try {
|
|
801
|
+
var r = root.getBoundingClientRect()
|
|
802
|
+
var b = menuBtn.getBoundingClientRect()
|
|
803
|
+
var vp = viewport()
|
|
804
|
+
var onLeft = r.left + r.width / 2 < vp.w / 2
|
|
805
|
+
// the menu appears ABOVE the button, anchored to its side:
|
|
806
|
+
// right side → menu bottom-right aligns with the button's top-right;
|
|
807
|
+
// left side → menu bottom-left aligns with the button's top-left
|
|
808
|
+
if (onLeft) {
|
|
809
|
+
menuBox.style.left = b.left + 'px'
|
|
810
|
+
menuBox.style.right = 'auto'
|
|
811
|
+
menuBox.style.transformOrigin = 'bottom left'
|
|
812
|
+
} else {
|
|
813
|
+
menuBox.style.right = (vp.w - b.right) + 'px'
|
|
814
|
+
menuBox.style.left = 'auto'
|
|
815
|
+
menuBox.style.transformOrigin = 'bottom right'
|
|
816
|
+
}
|
|
817
|
+
menuBox.style.bottom = (vp.h - b.top) + 'px'
|
|
818
|
+
menuBox.style.top = 'auto'
|
|
819
|
+
} catch (err) {}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
var hitCanvas = null
|
|
823
|
+
var hitReady = false
|
|
824
|
+
function setupHitTest() {
|
|
825
|
+
try {
|
|
826
|
+
hitCanvas = document.createElement('canvas')
|
|
827
|
+
hitCanvas.width = 610
|
|
828
|
+
hitCanvas.height = 610
|
|
829
|
+
var probe = new Image()
|
|
830
|
+
probe.onload = function () {
|
|
831
|
+
try {
|
|
832
|
+
hitCanvas.getContext('2d').drawImage(probe, 0, 0)
|
|
833
|
+
hitReady = true
|
|
834
|
+
} catch (err) {}
|
|
835
|
+
}
|
|
836
|
+
probe.onerror = function () {}
|
|
837
|
+
probe.src = IMG_URL
|
|
838
|
+
} catch (err) {}
|
|
839
|
+
}
|
|
840
|
+
function isWhaleHit(e) {
|
|
841
|
+
if (!hitCanvas || !hitReady) return true
|
|
842
|
+
try {
|
|
843
|
+
var r = img.getBoundingClientRect()
|
|
844
|
+
if (!r || r.width <= 0 || r.height <= 0) return false
|
|
845
|
+
var lx = (e.clientX - r.left) / r.width * 610
|
|
846
|
+
var ly = (e.clientY - r.top) / r.height * 610
|
|
847
|
+
if (lx < 0 || ly < 0 || lx >= 610 || ly >= 610) return false
|
|
848
|
+
if (state.h === 'left') lx = 610 - lx
|
|
849
|
+
var data = hitCanvas.getContext('2d').getImageData(Math.floor(lx), Math.floor(ly), 1, 1).data
|
|
850
|
+
return data[3] > 10
|
|
851
|
+
} catch (err) {
|
|
852
|
+
return true
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function onDocPointerDown(e) {
|
|
856
|
+
if (e.target && e.target.closest) {
|
|
857
|
+
if (e.target.closest('.dshwv-bubble') || e.target.closest('.dshwv-menu') || e.target.closest('.dshwv-menu-btn')) return
|
|
858
|
+
}
|
|
859
|
+
if (menuOpen) {
|
|
860
|
+
closeMenu()
|
|
861
|
+
return
|
|
862
|
+
}
|
|
863
|
+
if (e.button !== 0 && e.pointerType === 'mouse') return
|
|
864
|
+
if (!isWhaleHit(e)) return
|
|
865
|
+
try { e.preventDefault(); e.stopPropagation() } catch (err) {}
|
|
866
|
+
var vp = viewport()
|
|
867
|
+
var rect = root.getBoundingClientRect()
|
|
868
|
+
drag = { active: true, startX: e.clientX, startY: e.clientY, origLeft: rect.left, origTop: rect.top, w: rect.width, h: rect.height, moved: false, vp: vp }
|
|
869
|
+
root.classList.add('dshwv-dragging')
|
|
870
|
+
pressDown()
|
|
871
|
+
setWidgetCursor('grabbing')
|
|
872
|
+
document.addEventListener('pointermove', onDocPointerMove, true)
|
|
873
|
+
document.addEventListener('pointerup', onDocPointerUp, true)
|
|
874
|
+
document.addEventListener('pointercancel', onDocPointerCancel, true)
|
|
875
|
+
document.addEventListener('click', onDocClickStopper, true)
|
|
876
|
+
}
|
|
877
|
+
function onDocPointerMove(e) {
|
|
878
|
+
if (!drag || !drag.active) return
|
|
879
|
+
var dx = e.clientX - drag.startX
|
|
880
|
+
var dy = e.clientY - drag.startY
|
|
881
|
+
if (dx * dx + dy * dy >= CLICK_SQ) drag.moved = true
|
|
882
|
+
// Keep the pre-drag flip orientation while dragging (state.h/v stay as they
|
|
883
|
+
// were); on release endDrag() recomputes the anchors and settle() flips the
|
|
884
|
+
// class with a smooth transition instead of reverting instantly.
|
|
885
|
+
state.left = clamp(drag.origLeft + dx, 0, Math.max(0, drag.vp.w - drag.w))
|
|
886
|
+
state.top = clamp(drag.origTop + dy, 0, Math.max(0, drag.vp.h - drag.h))
|
|
887
|
+
express()
|
|
888
|
+
}
|
|
889
|
+
function onDocPointerUp(e) { endDrag(e, true) }
|
|
890
|
+
function onDocPointerCancel(e) { endDrag(e, false) }
|
|
891
|
+
function onDocClickStopper(e) {
|
|
892
|
+
try { e.preventDefault(); e.stopPropagation() } catch (err) {}
|
|
893
|
+
}
|
|
894
|
+
document.addEventListener('pointerdown', onDocPointerDown, true)
|
|
895
|
+
|
|
896
|
+
var widgetCursor = ''
|
|
897
|
+
function setWidgetCursor(v) {
|
|
898
|
+
if (v !== widgetCursor) {
|
|
899
|
+
widgetCursor = v
|
|
900
|
+
try { document.body.style.cursor = v } catch (err) {}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function onDocPointerMoveCursor(e) {
|
|
904
|
+
if (drag && drag.active) { setWidgetCursor('grabbing'); return }
|
|
905
|
+
var el = null
|
|
906
|
+
try { el = document.elementFromPoint(e.clientX, e.clientY) } catch (err) {}
|
|
907
|
+
if (el && el.closest && (el.closest('.dshwv-bubble') || el.closest('.dshwv-menu') || el.closest('.dshwv-menu-btn'))) {
|
|
908
|
+
setWidgetCursor('')
|
|
909
|
+
menuBtn.classList.add('dshwv-menu-btn-visible')
|
|
910
|
+
return
|
|
911
|
+
}
|
|
912
|
+
var over = isWhaleHit(e)
|
|
913
|
+
setWidgetCursor(over ? 'grab' : '')
|
|
914
|
+
menuBtn.classList.toggle('dshwv-menu-btn-visible', over || menuOpen)
|
|
915
|
+
}
|
|
916
|
+
document.addEventListener('pointermove', onDocPointerMoveCursor, true)
|
|
917
|
+
|
|
918
|
+
function endDrag(e, clickAllowed) {
|
|
919
|
+
if (!drag || !drag.active) return
|
|
920
|
+
drag.active = false
|
|
921
|
+
document.removeEventListener('pointermove', onDocPointerMove, true)
|
|
922
|
+
document.removeEventListener('pointerup', onDocPointerUp, true)
|
|
923
|
+
document.removeEventListener('pointercancel', onDocPointerCancel, true)
|
|
924
|
+
document.removeEventListener('click', onDocClickStopper, true)
|
|
925
|
+
pressUp()
|
|
926
|
+
root.classList.remove('dshwv-dragging')
|
|
927
|
+
setWidgetCursor(isWhaleHit(e) ? 'grab' : '')
|
|
928
|
+
if (clickAllowed && !drag.moved) { showBubble(); refresh(true); return }
|
|
929
|
+
var dx = e.clientX - drag.startX
|
|
930
|
+
var dy = e.clientY - drag.startY
|
|
931
|
+
var left = clamp(drag.origLeft + dx, 0, Math.max(0, drag.vp.w - drag.w))
|
|
932
|
+
var top = clamp(drag.origTop + dy, 0, Math.max(0, drag.vp.h - drag.h))
|
|
933
|
+
var centerX = left + drag.w / 2
|
|
934
|
+
var centerY = top + drag.h / 2
|
|
935
|
+
if (centerX < drag.vp.w / 4) {
|
|
936
|
+
state.h = 'left'
|
|
937
|
+
state.hOff = 0
|
|
938
|
+
} else if (centerX > drag.vp.w * 3 / 4) {
|
|
939
|
+
state.h = 'right'
|
|
940
|
+
state.hOff = 0
|
|
941
|
+
} else {
|
|
942
|
+
state.h = null
|
|
943
|
+
state.hOff = left
|
|
944
|
+
}
|
|
945
|
+
if (centerY < drag.vp.h / 4) {
|
|
946
|
+
state.v = 'top'
|
|
947
|
+
state.vOff = 0
|
|
948
|
+
} else if (centerY > drag.vp.h * 3 / 4) {
|
|
949
|
+
state.v = 'bottom'
|
|
950
|
+
state.vOff = 0
|
|
951
|
+
} else {
|
|
952
|
+
state.v = null
|
|
953
|
+
state.vOff = top
|
|
954
|
+
}
|
|
955
|
+
state.left = left
|
|
956
|
+
state.top = top
|
|
957
|
+
settle()
|
|
958
|
+
}
|
|
959
|
+
window.addEventListener('resize', function () {
|
|
960
|
+
settle()
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
var rect0 = root.getBoundingClientRect()
|
|
964
|
+
state.left = rect0.left
|
|
965
|
+
state.top = rect0.top
|
|
966
|
+
express()
|
|
967
|
+
render()
|
|
968
|
+
applySoundSet()
|
|
969
|
+
setupHitTest()
|
|
970
|
+
fetch(SIZE_URL, { cache: 'no-store' })
|
|
971
|
+
.then(function (r) { return r.json() })
|
|
972
|
+
.then(function (d) {
|
|
973
|
+
if (d && typeof d.scale === 'number' && d.scale >= MIN_SCALE - 0.1 && d.scale <= MAX_SCALE + 0.1) {
|
|
974
|
+
state.scale = d.scale
|
|
975
|
+
root.style.setProperty('--dshw-scale', String(d.scale))
|
|
976
|
+
scaleInput.value = String(d.scale)
|
|
977
|
+
scaleNumber.value = String(scaleToDisplay(d.scale))
|
|
978
|
+
settle()
|
|
979
|
+
}
|
|
980
|
+
if (d && typeof d.vol === 'number') {
|
|
981
|
+
soundVol = d.vol
|
|
982
|
+
soundOn = soundVol > 0
|
|
983
|
+
volInput.value = String(soundVol)
|
|
984
|
+
volPct.textContent = Math.round(soundVol * 100) + '%'
|
|
985
|
+
try {
|
|
986
|
+
if (pressAudio) pressAudio.volume = soundVol
|
|
987
|
+
if (releaseAudio) releaseAudio.volume = soundVol
|
|
988
|
+
} catch (err) {}
|
|
989
|
+
}
|
|
990
|
+
if (d && typeof d.soundSet === 'string') {
|
|
991
|
+
soundSet = d.soundSet === 'fx1' ? 'fx1' : 'duck'
|
|
992
|
+
soundSelect.value = soundSet
|
|
993
|
+
applySoundSet()
|
|
994
|
+
}
|
|
995
|
+
if (d && typeof d.usageMode === 'string') {
|
|
996
|
+
usageMode = d.usageMode === 'token' ? 'token' : 'ledger'
|
|
997
|
+
usageSelect.value = usageMode
|
|
998
|
+
}
|
|
999
|
+
refresh(false)
|
|
1000
|
+
})
|
|
1001
|
+
.catch(function () { refresh(false) })
|
|
1002
|
+
setInterval(function () { refresh(false) }, REFRESH_MS)
|
|
1003
|
+
})()`
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
const name = 'whale-balance-widget'
|
|
1007
|
+
const inject = ['webServer', 'credentials']
|
|
1008
|
+
|
|
1009
|
+
function apply(ctx) {
|
|
1010
|
+
let imageBytes = null
|
|
1011
|
+
let balanceCache = null
|
|
1012
|
+
let balanceInFlight = null
|
|
1013
|
+
|
|
1014
|
+
function loadImage() {
|
|
1015
|
+
if (imageBytes) return imageBytes
|
|
1016
|
+
for (const p of IMAGE_CANDIDATES) {
|
|
1017
|
+
try {
|
|
1018
|
+
const bytes = fs.readFileSync(p)
|
|
1019
|
+
if (bytes && bytes.length > 0) {
|
|
1020
|
+
imageBytes = bytes
|
|
1021
|
+
return bytes
|
|
1022
|
+
}
|
|
1023
|
+
} catch (err) {}
|
|
1024
|
+
}
|
|
1025
|
+
throw new Error('whale image not found')
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function fetchBalance() {
|
|
1029
|
+
let cred
|
|
1030
|
+
try {
|
|
1031
|
+
cred = await ctx.credentials.resolve('DEEPSEEK_API_KEY')
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
return { ok: false, code: 'NO_KEY', error: '凭据读取失败: ' + String((err && err.message) || err).slice(0, 160) }
|
|
1034
|
+
}
|
|
1035
|
+
if (!cred) {
|
|
1036
|
+
return { ok: false, code: 'NO_KEY', error: '未配置 DEEPSEEK_API_KEY' }
|
|
1037
|
+
}
|
|
1038
|
+
let lastErr = null
|
|
1039
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1040
|
+
let res
|
|
1041
|
+
try {
|
|
1042
|
+
res = await fetch(BALANCE_URL, {
|
|
1043
|
+
headers: { Authorization: 'Bearer ' + cred.value },
|
|
1044
|
+
signal: AbortSignal.timeout(20000),
|
|
1045
|
+
})
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
lastErr = err
|
|
1048
|
+
if (attempt === 0) await new Promise((r) => setTimeout(r, 500))
|
|
1049
|
+
continue
|
|
1050
|
+
}
|
|
1051
|
+
if (!res.ok) {
|
|
1052
|
+
lastErr = new Error('HTTP ' + res.status)
|
|
1053
|
+
if (res.status < 500) break
|
|
1054
|
+
if (attempt === 0) await new Promise((r) => setTimeout(r, 500))
|
|
1055
|
+
continue
|
|
1056
|
+
}
|
|
1057
|
+
let data
|
|
1058
|
+
try {
|
|
1059
|
+
data = await res.json()
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
return { ok: false, code: 'PARSE', error: '余额接口返回不是合法 JSON' }
|
|
1062
|
+
}
|
|
1063
|
+
const info = data && Array.isArray(data.balance_infos) ? data.balance_infos[0] : null
|
|
1064
|
+
if (!info || info.total_balance === undefined) {
|
|
1065
|
+
return { ok: false, code: 'SHAPE', error: '余额接口返回结构异常' }
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
ok: true,
|
|
1069
|
+
totalBalance: Number(info.total_balance),
|
|
1070
|
+
currency: String(info.currency || 'CNY'),
|
|
1071
|
+
updatedAt: new Date().toISOString(),
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const transient = !(lastErr && /^HTTP 4\d\d/.test(lastErr.message))
|
|
1075
|
+
return {
|
|
1076
|
+
ok: false,
|
|
1077
|
+
code: 'HTTP',
|
|
1078
|
+
transient: transient,
|
|
1079
|
+
error: '余额接口请求失败: ' + String((lastErr && lastErr.message) || lastErr).slice(0, 200),
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function fetchUsage() {
|
|
1084
|
+
let cred
|
|
1085
|
+
try {
|
|
1086
|
+
cred = await ctx.credentials.resolve('DEEPSEEK_PLATFORM_TOKEN')
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
return { error: 'platform cred resolve failed' }
|
|
1089
|
+
}
|
|
1090
|
+
if (!cred) return { error: 'no platform token' }
|
|
1091
|
+
const token = String(cred.value).replace(/^Bearer\s+/i, '')
|
|
1092
|
+
try {
|
|
1093
|
+
const now = new Date()
|
|
1094
|
+
const tz = -now.getTimezoneOffset() * 60
|
|
1095
|
+
const start = Math.floor(new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() / 1000)
|
|
1096
|
+
const end = start + 86400
|
|
1097
|
+
const url = 'https://platform.deepseek.com/api/v0/usage/by_api_key/amount?start=' + start + '&end=' + end + '&tz=' + tz
|
|
1098
|
+
const res = await fetch(url, {
|
|
1099
|
+
headers: { Authorization: 'Bearer ' + token },
|
|
1100
|
+
signal: AbortSignal.timeout(15000),
|
|
1101
|
+
})
|
|
1102
|
+
if (!res.ok) return { error: 'http ' + res.status }
|
|
1103
|
+
const data = await res.json()
|
|
1104
|
+
const u = computeTodayUsage(data)
|
|
1105
|
+
if (u && isFinite(u.amount)) return { amount: u.amount, tokens: u.tokens }
|
|
1106
|
+
return { error: 'no usage' }
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
return { error: String((err && err.message) || err) }
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function computeTodayUsage(data) {
|
|
1113
|
+
// data.data.biz_data.series[]: [{model, buckets:[{time, usage:{RESPONSE_TOKEN, PROMPT_CACHE_HIT_TOKEN, PROMPT_CACHE_MISS_TOKEN}}]}]
|
|
1114
|
+
let d = data
|
|
1115
|
+
if (d && d.data && d.data.biz_data && Array.isArray(d.data.biz_data.series)) d = d.data.biz_data
|
|
1116
|
+
else if (d && d.data && Array.isArray(d.data.series)) d = d.data
|
|
1117
|
+
const series = Array.isArray(d.series) ? d.series : null
|
|
1118
|
+
if (!series || series.length === 0) return null
|
|
1119
|
+
let cost = 0
|
|
1120
|
+
let tokens = 0
|
|
1121
|
+
let found = false
|
|
1122
|
+
for (const s of series) {
|
|
1123
|
+
if (!s || typeof s !== 'object') continue
|
|
1124
|
+
const p = priceFor(s.model)
|
|
1125
|
+
const buckets = Array.isArray(s.buckets) ? s.buckets : []
|
|
1126
|
+
for (const b of buckets) {
|
|
1127
|
+
const u = b && b.usage
|
|
1128
|
+
if (!u || typeof u !== 'object') continue
|
|
1129
|
+
const hit = Number(u.PROMPT_CACHE_HIT_TOKEN) || 0
|
|
1130
|
+
const miss = Number(u.PROMPT_CACHE_MISS_TOKEN) || 0
|
|
1131
|
+
const out = Number(u.RESPONSE_TOKEN) || 0
|
|
1132
|
+
if (hit + miss + out === 0) continue
|
|
1133
|
+
found = true
|
|
1134
|
+
tokens += hit + miss + out
|
|
1135
|
+
const pi = isPeakTime(b.time) ? 1 : 0
|
|
1136
|
+
cost += (hit / 1e6) * p.hit[pi] + (miss / 1e6) * p.miss[pi] + (out / 1e6) * p.out[pi]
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return found ? { amount: cost, tokens: tokens } : null
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function todayKey() {
|
|
1143
|
+
const d = new Date()
|
|
1144
|
+
const p = (n) => String(n).padStart(2, '0')
|
|
1145
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
|
|
1146
|
+
}
|
|
1147
|
+
function readUsageLedger() {
|
|
1148
|
+
for (const p of USAGE_FILE_CANDIDATES) {
|
|
1149
|
+
try {
|
|
1150
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
1151
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.date === 'string') return parsed
|
|
1152
|
+
} catch (err) {}
|
|
1153
|
+
}
|
|
1154
|
+
return { date: todayKey(), lastBalance: null, todayUsage: 0, history: {} }
|
|
1155
|
+
}
|
|
1156
|
+
function writeUsageLedger(led) {
|
|
1157
|
+
const body = JSON.stringify(led)
|
|
1158
|
+
for (const p of USAGE_FILE_CANDIDATES) {
|
|
1159
|
+
try {
|
|
1160
|
+
fs.writeFileSync(p, body, 'utf8')
|
|
1161
|
+
return true
|
|
1162
|
+
} catch (err) {}
|
|
1163
|
+
}
|
|
1164
|
+
return false
|
|
1165
|
+
}
|
|
1166
|
+
// 记账模式:每次观测到余额后,用余额正差值累计当天用量(跨天自动归零并归档)
|
|
1167
|
+
function recordLedgerUsage(currentBalance) {
|
|
1168
|
+
const t = todayKey()
|
|
1169
|
+
let led = readUsageLedger()
|
|
1170
|
+
if (led.date !== t) {
|
|
1171
|
+
if (led.date && typeof led.todayUsage === 'number') {
|
|
1172
|
+
led.history = led.history || {}
|
|
1173
|
+
led.history[led.date] = led.todayUsage
|
|
1174
|
+
}
|
|
1175
|
+
led.date = t
|
|
1176
|
+
led.lastBalance = currentBalance
|
|
1177
|
+
led.todayUsage = 0
|
|
1178
|
+
} else {
|
|
1179
|
+
const prev = typeof led.lastBalance === 'number' ? led.lastBalance : currentBalance
|
|
1180
|
+
if (typeof prev === 'number' && typeof currentBalance === 'number' && currentBalance < prev) {
|
|
1181
|
+
led.todayUsage = (typeof led.todayUsage === 'number' ? led.todayUsage : 0) + (prev - currentBalance)
|
|
1182
|
+
}
|
|
1183
|
+
led.lastBalance = currentBalance
|
|
1184
|
+
}
|
|
1185
|
+
const keys = Object.keys(led.history || {}).sort()
|
|
1186
|
+
while (keys.length > 30) {
|
|
1187
|
+
delete led.history[keys.shift()]
|
|
1188
|
+
}
|
|
1189
|
+
writeUsageLedger(led)
|
|
1190
|
+
return led
|
|
1191
|
+
}
|
|
1192
|
+
function normalizeUsageMode(m) {
|
|
1193
|
+
return m === 'token' ? 'token' : 'ledger'
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function getBalancePayload() {
|
|
1197
|
+
const payload = await fetchBalance()
|
|
1198
|
+
if (!payload.ok) return payload
|
|
1199
|
+
// 无论哪种模式,都先把余额观测记入账本(自动累积「鲸鱼记账」数据)
|
|
1200
|
+
const led = recordLedgerUsage(Number(payload.totalBalance))
|
|
1201
|
+
const cfg = readSizeConfig() || {}
|
|
1202
|
+
const mode = normalizeUsageMode(cfg.usageMode)
|
|
1203
|
+
const full = { ...payload }
|
|
1204
|
+
full.isPeak = isPeakTime(Math.floor(Date.now() / 1000))
|
|
1205
|
+
if (mode === 'ledger') {
|
|
1206
|
+
full.todayUsage = led.todayUsage
|
|
1207
|
+
full.usageMode = 'ledger'
|
|
1208
|
+
return full
|
|
1209
|
+
}
|
|
1210
|
+
// token:尝试平台令牌实时计算
|
|
1211
|
+
let cred = null
|
|
1212
|
+
try {
|
|
1213
|
+
cred = await ctx.credentials.resolve('DEEPSEEK_PLATFORM_TOKEN')
|
|
1214
|
+
} catch (err) {}
|
|
1215
|
+
if (cred) {
|
|
1216
|
+
const u = await fetchUsage()
|
|
1217
|
+
if (u && u.amount !== undefined) {
|
|
1218
|
+
full.todayUsage = u.amount
|
|
1219
|
+
full.usageMode = 'token'
|
|
1220
|
+
return full
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
// 无令牌或令牌失败:回落记账模式
|
|
1224
|
+
full.todayUsage = led.todayUsage
|
|
1225
|
+
full.usageMode = 'ledger'
|
|
1226
|
+
return full
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function getBalance() {
|
|
1230
|
+
const now = Date.now()
|
|
1231
|
+
if (balanceCache && now - balanceCache.at < BALANCE_TTL_MS) {
|
|
1232
|
+
return Promise.resolve(balanceCache.payload)
|
|
1233
|
+
}
|
|
1234
|
+
if (balanceInFlight) return balanceInFlight
|
|
1235
|
+
balanceInFlight = getBalancePayload()
|
|
1236
|
+
.then((payload) => {
|
|
1237
|
+
if (payload.ok) {
|
|
1238
|
+
balanceCache = { at: now, payload }
|
|
1239
|
+
return payload
|
|
1240
|
+
}
|
|
1241
|
+
if (payload.transient && balanceCache) {
|
|
1242
|
+
// transient network/API blip: keep serving the last known balance
|
|
1243
|
+
return { ...balanceCache.payload, stale: true, error: payload.error }
|
|
1244
|
+
}
|
|
1245
|
+
if (!payload.transient) console.error('[whale-balance]', payload.code, payload.error)
|
|
1246
|
+
return payload
|
|
1247
|
+
})
|
|
1248
|
+
.catch((err) => ({
|
|
1249
|
+
ok: false,
|
|
1250
|
+
code: 'ERROR',
|
|
1251
|
+
error: '余额服务异常: ' + String((err && err.message) || err).slice(0, 200),
|
|
1252
|
+
}))
|
|
1253
|
+
.finally(() => {
|
|
1254
|
+
balanceInFlight = null
|
|
1255
|
+
})
|
|
1256
|
+
return balanceInFlight
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function readSizeConfig() {
|
|
1260
|
+
for (const p of SIZE_FILE_CANDIDATES) {
|
|
1261
|
+
try {
|
|
1262
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
1263
|
+
if (parsed && typeof parsed.scale === 'number') {
|
|
1264
|
+
return {
|
|
1265
|
+
scale: parsed.scale,
|
|
1266
|
+
sound: parsed.sound !== false,
|
|
1267
|
+
vol: typeof parsed.vol === 'number' ? parsed.vol : 0.9,
|
|
1268
|
+
soundSet: parsed.soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1269
|
+
usageMode: normalizeUsageMode(parsed.usageMode),
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} catch (err) {}
|
|
1273
|
+
}
|
|
1274
|
+
return null
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function writeSizeConfig(scale, sound, vol, soundSet, usageMode) {
|
|
1278
|
+
const um = normalizeUsageMode(usageMode)
|
|
1279
|
+
const body = JSON.stringify({
|
|
1280
|
+
scale: scale,
|
|
1281
|
+
sound: sound !== false,
|
|
1282
|
+
vol: typeof vol === 'number' ? vol : 0.9,
|
|
1283
|
+
soundSet: soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1284
|
+
usageMode: um,
|
|
1285
|
+
updatedAt: new Date().toISOString(),
|
|
1286
|
+
})
|
|
1287
|
+
for (const p of SIZE_FILE_CANDIDATES) {
|
|
1288
|
+
try {
|
|
1289
|
+
fs.writeFileSync(p, body, 'utf8')
|
|
1290
|
+
return {
|
|
1291
|
+
ok: true,
|
|
1292
|
+
scale: scale,
|
|
1293
|
+
sound: sound !== false,
|
|
1294
|
+
vol: typeof vol === 'number' ? vol : 0.9,
|
|
1295
|
+
soundSet: soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1296
|
+
usageMode: um,
|
|
1297
|
+
}
|
|
1298
|
+
} catch (err) {}
|
|
1299
|
+
}
|
|
1300
|
+
return { ok: false, error: '无法持久化挂件尺寸' }
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function readBody(req) {
|
|
1304
|
+
return new Promise((resolve, reject) => {
|
|
1305
|
+
const chunks = []
|
|
1306
|
+
let size = 0
|
|
1307
|
+
req.on('data', (c) => {
|
|
1308
|
+
size += c.length
|
|
1309
|
+
if (size > 8192) {
|
|
1310
|
+
reject(new Error('body too large'))
|
|
1311
|
+
req.destroy()
|
|
1312
|
+
return
|
|
1313
|
+
}
|
|
1314
|
+
chunks.push(c)
|
|
1315
|
+
})
|
|
1316
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
1317
|
+
req.on('error', reject)
|
|
1318
|
+
})
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const disposers = []
|
|
1322
|
+
|
|
1323
|
+
disposers.push(ctx.webServer.register({
|
|
1324
|
+
kind: 'exact',
|
|
1325
|
+
path: '/dsh-whale/image.png',
|
|
1326
|
+
handler: (req, res) => {
|
|
1327
|
+
try {
|
|
1328
|
+
const bytes = loadImage()
|
|
1329
|
+
res.writeHead(200, {
|
|
1330
|
+
'Content-Type': 'image/png',
|
|
1331
|
+
'Cache-Control': 'no-store',
|
|
1332
|
+
'Content-Length': String(bytes.length),
|
|
1333
|
+
})
|
|
1334
|
+
res.end(bytes)
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1337
|
+
res.end('whale image unavailable: ' + String((err && err.message) || err))
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
}))
|
|
1341
|
+
|
|
1342
|
+
disposers.push(ctx.webServer.register({
|
|
1343
|
+
kind: 'exact',
|
|
1344
|
+
path: '/dsh-whale/balance.json',
|
|
1345
|
+
handler: async (req, res) => {
|
|
1346
|
+
try {
|
|
1347
|
+
const payload = await getBalance()
|
|
1348
|
+
res.writeHead(200, JSON_HEADERS)
|
|
1349
|
+
res.end(JSON.stringify(payload))
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
res.writeHead(200, JSON_HEADERS)
|
|
1352
|
+
res.end(JSON.stringify({ ok: false, code: 'ERROR', error: String((err && err.message) || err).slice(0, 200) }))
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
}))
|
|
1356
|
+
|
|
1357
|
+
disposers.push(ctx.webServer.register({
|
|
1358
|
+
kind: 'exact',
|
|
1359
|
+
path: '/dsh-whale/size.json',
|
|
1360
|
+
handler: async (req, res) => {
|
|
1361
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
1362
|
+
try {
|
|
1363
|
+
const body = await readBody(req)
|
|
1364
|
+
const parsed = JSON.parse(body)
|
|
1365
|
+
const scale = typeof parsed.scale === 'number' ? parsed.scale : null
|
|
1366
|
+
if (scale === null) {
|
|
1367
|
+
res.writeHead(400, JSON_HEADERS)
|
|
1368
|
+
res.end(JSON.stringify({ ok: false, error: 'missing scale' }))
|
|
1369
|
+
return
|
|
1370
|
+
}
|
|
1371
|
+
// 用量模式变化时让余额缓存失效,下次请求立即按新模式计算
|
|
1372
|
+
if (typeof parsed.usageMode === 'string') {
|
|
1373
|
+
const old = readSizeConfig()
|
|
1374
|
+
if (!old || normalizeUsageMode(old.usageMode) !== normalizeUsageMode(parsed.usageMode)) {
|
|
1375
|
+
balanceCache = null
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode)
|
|
1379
|
+
res.writeHead(result.ok ? 200 : 500, JSON_HEADERS)
|
|
1380
|
+
res.end(JSON.stringify(result))
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
res.writeHead(400, JSON_HEADERS)
|
|
1383
|
+
res.end(JSON.stringify({ ok: false, error: String((err && err.message) || err) }))
|
|
1384
|
+
}
|
|
1385
|
+
return
|
|
1386
|
+
}
|
|
1387
|
+
res.writeHead(200, JSON_HEADERS)
|
|
1388
|
+
res.end(JSON.stringify(readSizeConfig() || {}))
|
|
1389
|
+
},
|
|
1390
|
+
}))
|
|
1391
|
+
|
|
1392
|
+
function loadSound(candidates) {
|
|
1393
|
+
for (const p of candidates) {
|
|
1394
|
+
try {
|
|
1395
|
+
const bytes = fs.readFileSync(p)
|
|
1396
|
+
if (bytes && bytes.length > 0) return bytes
|
|
1397
|
+
} catch (err) {}
|
|
1398
|
+
}
|
|
1399
|
+
return null
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function serveSound(req, res, candidates) {
|
|
1403
|
+
const bytes = loadSound(candidates)
|
|
1404
|
+
if (!bytes) {
|
|
1405
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1406
|
+
res.end('sound unavailable')
|
|
1407
|
+
return
|
|
1408
|
+
}
|
|
1409
|
+
res.writeHead(200, {
|
|
1410
|
+
'Content-Type': 'audio/mpeg',
|
|
1411
|
+
'Cache-Control': 'no-store',
|
|
1412
|
+
'Content-Length': String(bytes.length),
|
|
1413
|
+
})
|
|
1414
|
+
res.end(bytes)
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
disposers.push(ctx.webServer.register({
|
|
1418
|
+
kind: 'exact',
|
|
1419
|
+
path: '/dsh-whale/sound/press.mp3',
|
|
1420
|
+
handler: (req, res) => {
|
|
1421
|
+
const set = SOUND_SETS[soundSetFromUrl(req.url)] || SOUND_SETS.duck
|
|
1422
|
+
serveSound(req, res, set.press)
|
|
1423
|
+
},
|
|
1424
|
+
}))
|
|
1425
|
+
|
|
1426
|
+
disposers.push(ctx.webServer.register({
|
|
1427
|
+
kind: 'exact',
|
|
1428
|
+
path: '/dsh-whale/sound/release.mp3',
|
|
1429
|
+
handler: (req, res) => {
|
|
1430
|
+
const set = SOUND_SETS[soundSetFromUrl(req.url)] || SOUND_SETS.duck
|
|
1431
|
+
serveSound(req, res, set.release)
|
|
1432
|
+
},
|
|
1433
|
+
}))
|
|
1434
|
+
|
|
1435
|
+
disposers.push(ctx.webServer.register({
|
|
1436
|
+
kind: 'exact',
|
|
1437
|
+
path: '/dsh-whale/widget.js',
|
|
1438
|
+
handler: (req, res) => {
|
|
1439
|
+
res.writeHead(200, {
|
|
1440
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
1441
|
+
'Cache-Control': 'no-store',
|
|
1442
|
+
})
|
|
1443
|
+
res.end(WIDGET_JS)
|
|
1444
|
+
},
|
|
1445
|
+
}))
|
|
1446
|
+
|
|
1447
|
+
disposers.push(ctx.webServer.tapIndex((html) => {
|
|
1448
|
+
if (html.indexOf('/dsh-whale/widget.js') !== -1) return html
|
|
1449
|
+
const tag = '<script defer src="/dsh-whale/widget.js"></script>'
|
|
1450
|
+
if (html.indexOf('</body>') !== -1) return html.replace('</body>', tag + '</body>')
|
|
1451
|
+
return html + tag
|
|
1452
|
+
}))
|
|
1453
|
+
|
|
1454
|
+
ctx.effect(() => () => {
|
|
1455
|
+
for (const d of disposers) {
|
|
1456
|
+
try { d() } catch (err) {}
|
|
1457
|
+
}
|
|
1458
|
+
})
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
export { name, inject, apply }
|