dsh-mobile-use 1.0.1 → 1.0.4
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/client.js +94 -16
- package/client.rev +1 -1
- package/index.js +335 -83
- package/mirror.js +157 -32
- package/ocr.js +88 -17
- package/package.json +7 -3
- package/selftest.mjs +64 -12
- package/stream.js +214 -39
- package/test/coords.test.mjs +129 -0
- package/test/symbols.test.mjs +73 -0
- package/test/timers.test.mjs +60 -0
package/client.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/* 页面代码的版本号:宿主报的和这里不一致,说明页面还在跑旧模块,自己重载一次。 */
|
|
13
|
-
window.__ADBM_BUILD = '
|
|
13
|
+
window.__ADBM_BUILD = '152'
|
|
14
14
|
|
|
15
15
|
window.__ModuleLoader__.load({
|
|
16
16
|
id: 'dsh-mobile-use',
|
|
@@ -338,6 +338,11 @@ window.__ModuleLoader__.load({
|
|
|
338
338
|
const stageRef = React.useRef(null) // ui-copy-ignore
|
|
339
339
|
const canvasRef = React.useRef(null) // ui-copy-ignore
|
|
340
340
|
const strokes = React.useRef([])
|
|
341
|
+
/* 标注要绑住"当时看到的那一帧":动笔画第一下就把画面冻住,
|
|
342
|
+
之后输入说明的几秒里页面继续动,送回会话的图、圈、控件仍然是同一时刻的。 */
|
|
343
|
+
const frozen = React.useRef(null) // ui-copy-ignore
|
|
344
|
+
/* 防重复提交用独立标志:状态行显示"已发送"之后人还能继续圈下一处。 */
|
|
345
|
+
const isSending = React.useRef(false) // ui-copy-ignore
|
|
341
346
|
/* 点下去先在本地画一个圈:设备那一帧要 0.7 s 才回来,圈让手感立刻有回应。 */
|
|
342
347
|
const taps = React.useRef([]) // ui-copy-ignore
|
|
343
348
|
const drawing = React.useRef(null) // ui-copy-ignore
|
|
@@ -545,6 +550,20 @@ window.__ModuleLoader__.load({
|
|
|
545
550
|
let timer
|
|
546
551
|
let since = 0
|
|
547
552
|
let misses = 0
|
|
553
|
+
/* 正在显示与正在解码的 Blob URL 各留一个:替换时先释放旧的待解码,
|
|
554
|
+
卸载时两个都释放。旧写法只靠 onload 回收,换帧快时会漏掉整条 URL。 */
|
|
555
|
+
let shownUrl = ''
|
|
556
|
+
let pendingUrl = ''
|
|
557
|
+
const releaseUrls = () => {
|
|
558
|
+
if (pendingUrl !== '') {
|
|
559
|
+
URL.revokeObjectURL(pendingUrl)
|
|
560
|
+
pendingUrl = ''
|
|
561
|
+
}
|
|
562
|
+
if (shownUrl !== '') {
|
|
563
|
+
URL.revokeObjectURL(shownUrl)
|
|
564
|
+
shownUrl = ''
|
|
565
|
+
}
|
|
566
|
+
}
|
|
548
567
|
const visible = () => {
|
|
549
568
|
const node = frameRef.current
|
|
550
569
|
const host = node === null ? null : (node.parentElement ?? node)
|
|
@@ -569,13 +588,21 @@ window.__ModuleLoader__.load({
|
|
|
569
588
|
const img = imgRef.current
|
|
570
589
|
if (img !== null) {
|
|
571
590
|
const url = URL.createObjectURL(blob)
|
|
572
|
-
|
|
591
|
+
if (pendingUrl !== '') URL.revokeObjectURL(pendingUrl)
|
|
592
|
+
pendingUrl = url
|
|
573
593
|
img.onload = () => {
|
|
574
|
-
if (
|
|
575
|
-
|
|
594
|
+
if (pendingUrl !== url) return
|
|
595
|
+
pendingUrl = ''
|
|
596
|
+
if (shownUrl !== '') URL.revokeObjectURL(shownUrl)
|
|
597
|
+
shownUrl = url
|
|
576
598
|
setReady(true)
|
|
577
599
|
paint()
|
|
578
600
|
}
|
|
601
|
+
img.onerror = () => {
|
|
602
|
+
if (pendingUrl !== url) return
|
|
603
|
+
pendingUrl = ''
|
|
604
|
+
URL.revokeObjectURL(url)
|
|
605
|
+
}
|
|
579
606
|
img.src = url
|
|
580
607
|
}
|
|
581
608
|
misses = 0
|
|
@@ -585,7 +612,7 @@ window.__ModuleLoader__.load({
|
|
|
585
612
|
if (live) timer = setTimeout(pull, misses === 0 ? 0 : Math.min(3000, 300 * misses))
|
|
586
613
|
}
|
|
587
614
|
void pull()
|
|
588
|
-
return () => { live = false; clearTimeout(timer) }
|
|
615
|
+
return () => { live = false; clearTimeout(timer); releaseUrls() }
|
|
589
616
|
}, [serial, paint, engine])
|
|
590
617
|
|
|
591
618
|
/* 画面尺寸与是否在直播:标注坐标要换算到设备像素,必须知道这两个数。 */
|
|
@@ -683,10 +710,15 @@ window.__ModuleLoader__.load({
|
|
|
683
710
|
*/
|
|
684
711
|
const devicePoint = (point) => {
|
|
685
712
|
if (screen.w < 1 || screen.h < 1) return { x: 0, y: 0 }
|
|
713
|
+
const canvas = canvasRef.current
|
|
686
714
|
const rect = pictureRect()
|
|
687
|
-
if (rect.width < 1 || rect.height < 1) return { x: 0, y: 0 }
|
|
688
|
-
|
|
689
|
-
|
|
715
|
+
if (canvas === null || rect.width < 1 || rect.height < 1) return { x: 0, y: 0 }
|
|
716
|
+
/* point 来自 pointOf,是画布内的局部坐标;pictureRect 是视口坐标。
|
|
717
|
+
混用两个坐标系会把侧栏整体算成负偏移(右栏越靠右,误差越大),
|
|
718
|
+
所以这里补一次画布自身在视口里的偏移,整条换算只做一次。 */
|
|
719
|
+
const box = canvas.getBoundingClientRect()
|
|
720
|
+
const u = Math.min(1, Math.max(0, (point.x + box.left - rect.left) / rect.width))
|
|
721
|
+
const v = Math.min(1, Math.max(0, (point.y + box.top - rect.top) / rect.height))
|
|
690
722
|
const x = turned() ? Math.round(v * screen.w) : Math.round(u * screen.w)
|
|
691
723
|
const y = turned() ? Math.round(u * screen.h) : Math.round(v * screen.h)
|
|
692
724
|
return { x, y }
|
|
@@ -728,6 +760,23 @@ window.__ModuleLoader__.load({
|
|
|
728
760
|
if (mode === 'view') return
|
|
729
761
|
const point = pointOf(event)
|
|
730
762
|
if (mode === 'mark') {
|
|
763
|
+
/* 第一笔落下就把画面冻住:之后几秒输入说明时页面再动,
|
|
764
|
+
送回会话的仍是"人看到问题的那一帧"。 */
|
|
765
|
+
if (frozen.current === null && serial !== '') {
|
|
766
|
+
const box = canvasRef.current === null ? null : canvasRef.current.getBoundingClientRect()
|
|
767
|
+
const width = box === null ? 0 : box.width
|
|
768
|
+
fetch(`${ROUTE}/frame?serial=${encodeURIComponent(serial)}`, { cache: 'no-store' })
|
|
769
|
+
.then((response) => (response.ok ? response.blob() : Promise.reject(new Error('no frame'))))
|
|
770
|
+
.then((blob) => createImageBitmap(blob))
|
|
771
|
+
.then((bitmap) => {
|
|
772
|
+
if (frozen.current === null && strokes.current.length > 0) {
|
|
773
|
+
frozen.current = { bitmap, width, at: Date.now() }
|
|
774
|
+
} else if (bitmap !== null && typeof bitmap.close === 'function') {
|
|
775
|
+
bitmap.close()
|
|
776
|
+
}
|
|
777
|
+
})
|
|
778
|
+
.catch(() => { setStatus(L('state.failed')) })
|
|
779
|
+
}
|
|
731
780
|
drawing.current = [point]
|
|
732
781
|
strokes.current.push(drawing.current)
|
|
733
782
|
paint()
|
|
@@ -896,6 +945,7 @@ window.__ModuleLoader__.load({
|
|
|
896
945
|
|
|
897
946
|
const clear = () => {
|
|
898
947
|
strokes.current = []
|
|
948
|
+
frozen.current = null
|
|
899
949
|
paint()
|
|
900
950
|
setStatus('')
|
|
901
951
|
}
|
|
@@ -910,20 +960,34 @@ window.__ModuleLoader__.load({
|
|
|
910
960
|
}
|
|
911
961
|
|
|
912
962
|
const send = async () => {
|
|
913
|
-
if (serial === '' ||
|
|
963
|
+
if (serial === '' || isSending.current) return
|
|
964
|
+
isSending.current = true
|
|
914
965
|
setStatus(L('state.starting'))
|
|
915
966
|
try {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const
|
|
967
|
+
/* 优先用画第一笔时冻住的那一帧:保证图、圈、控件是同一时刻的现场。
|
|
968
|
+
冻帧失败(当时没网/没帧)才退回取当前帧,并在证据里标明。 */
|
|
969
|
+
const still = frozen.current
|
|
970
|
+
let bitmap = null
|
|
971
|
+
let frozenAt = 0
|
|
972
|
+
let canvasWidth = 0
|
|
973
|
+
if (still !== null) {
|
|
974
|
+
bitmap = still.bitmap
|
|
975
|
+
frozenAt = still.at
|
|
976
|
+
canvasWidth = still.width
|
|
977
|
+
} else {
|
|
978
|
+
const response = await fetch(`${ROUTE}/frame?serial=${encodeURIComponent(serial)}`, { cache: 'no-store' })
|
|
979
|
+
if (!response.ok) throw new Error('no frame')
|
|
980
|
+
bitmap = await createImageBitmap(await response.blob())
|
|
981
|
+
frozenAt = Date.now()
|
|
982
|
+
canvasWidth = canvasRef.current.getBoundingClientRect().width
|
|
983
|
+
}
|
|
919
984
|
const out = document.createElement('canvas')
|
|
920
985
|
out.width = bitmap.width
|
|
921
986
|
out.height = bitmap.height
|
|
922
987
|
const ink = out.getContext('2d')
|
|
923
988
|
if (ink === null) throw new Error('no canvas')
|
|
924
989
|
ink.drawImage(bitmap, 0, 0)
|
|
925
|
-
const
|
|
926
|
-
const scale = box.width < 1 ? 1 : out.width / box.width
|
|
990
|
+
const scale = canvasWidth < 1 ? 1 : out.width / canvasWidth
|
|
927
991
|
ink.strokeStyle = '#ff3b30'
|
|
928
992
|
ink.lineWidth = Math.max(3, 3 * scale)
|
|
929
993
|
ink.lineCap = 'round'
|
|
@@ -959,11 +1023,20 @@ window.__ModuleLoader__.load({
|
|
|
959
1023
|
const answer = await fetch(`${ROUTE}/annotate`, {
|
|
960
1024
|
method: 'POST',
|
|
961
1025
|
headers: { 'content-type': 'application/json' },
|
|
962
|
-
body: JSON.stringify({
|
|
1026
|
+
body: JSON.stringify({
|
|
1027
|
+
serial,
|
|
1028
|
+
session: sessionId,
|
|
1029
|
+
note,
|
|
1030
|
+
png,
|
|
1031
|
+
rect,
|
|
1032
|
+
frozen_at: frozenAt,
|
|
1033
|
+
frozen_ms: frozenAt < 1 ? null : Math.max(0, Date.now() - frozenAt),
|
|
1034
|
+
}),
|
|
963
1035
|
}).then((res) => res.json())
|
|
964
1036
|
if (answer?.ok === true) {
|
|
965
1037
|
setStatus(L('state.sent'))
|
|
966
1038
|
strokes.current = []
|
|
1039
|
+
frozen.current = null
|
|
967
1040
|
setNote('')
|
|
968
1041
|
paint()
|
|
969
1042
|
} else {
|
|
@@ -971,13 +1044,16 @@ window.__ModuleLoader__.load({
|
|
|
971
1044
|
}
|
|
972
1045
|
} catch {
|
|
973
1046
|
setStatus(L('state.failed'))
|
|
1047
|
+
} finally {
|
|
1048
|
+
isSending.current = false
|
|
974
1049
|
}
|
|
975
1050
|
}
|
|
976
1051
|
|
|
977
1052
|
/* 不画任何东西,直接把最近几秒的画面 + 操作时间线推给 agent:
|
|
978
1053
|
桌宠变大这类「刚才那几下之后不对劲」的问题,用这个最快。 */
|
|
979
1054
|
const sendRecent = async () => {
|
|
980
|
-
if (serial === '') return
|
|
1055
|
+
if (serial === '' || isSending.current) return
|
|
1056
|
+
isSending.current = true
|
|
981
1057
|
setStatus(L('state.starting'))
|
|
982
1058
|
try {
|
|
983
1059
|
const answer = await fetch(`${ROUTE}/annotate`, {
|
|
@@ -993,6 +1069,8 @@ window.__ModuleLoader__.load({
|
|
|
993
1069
|
}
|
|
994
1070
|
} catch {
|
|
995
1071
|
setStatus(L('state.failed'))
|
|
1072
|
+
} finally {
|
|
1073
|
+
isSending.current = false
|
|
996
1074
|
}
|
|
997
1075
|
}
|
|
998
1076
|
|
package/client.rev
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
153
|