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 CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  /* 页面代码的版本号:宿主报的和这里不一致,说明页面还在跑旧模块,自己重载一次。 */
13
- window.__ADBM_BUILD = '151'
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
- const prev = img.dataset.url
591
+ if (pendingUrl !== '') URL.revokeObjectURL(pendingUrl)
592
+ pendingUrl = url
573
593
  img.onload = () => {
574
- if (prev !== undefined) URL.revokeObjectURL(prev)
575
- img.dataset.url = url
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
- const u = Math.min(1, Math.max(0, (point.x - rect.left) / rect.width))
689
- const v = Math.min(1, Math.max(0, (point.y - rect.top) / rect.height))
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 === '' || status === L('state.sent')) return
963
+ if (serial === '' || isSending.current) return
964
+ isSending.current = true
914
965
  setStatus(L('state.starting'))
915
966
  try {
916
- const response = await fetch(`${ROUTE}/frame?serial=${encodeURIComponent(serial)}`, { cache: 'no-store' })
917
- if (!response.ok) throw new Error('no frame')
918
- const bitmap = await createImageBitmap(await response.blob())
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 box = canvasRef.current.getBoundingClientRect()
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({ serial, session: sessionId, note, png, rect }),
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
- 151
1
+ 153