@qzsy/dsh-bridge 3.4.3 → 3.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # 更新日志
2
2
 
3
+ ## [v3.5.0] - 2026-11-02
4
+
5
+ ### ✨ 新增
6
+
7
+ - **模型 → 思考级别连招**:在模型 sheet 里选中任意模型后,不再直接关闭整个菜单,而是自动重新打开设置菜单并进入「推理等级」子菜单,以同一个底部 sheet 呈现 Off / Low / High / Max,选完才关闭。
8
+ - **无闪跳过渡**:选中模型时先把旧 sheet 快照留在原地,宿主菜单在背后切换,推理 sheet 滑入后再移除快照;中间宿主菜单隐藏,不会闪出桌面 popover。
9
+ - 模型选中、推理等级选中仍由宿主完成,插件只负责串联和 sheet 化;桌面端不变。
10
+
11
+ ### 🧪 验证
12
+
13
+ - 独立宿主模拟:模型 sheet → 选模型 → 自动进入推理 sheet(Off/Low/High/Max)→ 选等级关闭并清理,4/4 通过。
14
+ - 运行中的 DSH:待 3.5.0 安装后验证完整链路。
15
+
16
+
17
+ ## [v3.4.4] - 2026-11-02
18
+
19
+ ### 🐛 修复
20
+
21
+ - **模型 sheet 不再因 blur 关闭**:3.4.3 在 sheet 套用后 `blur` 搜索框,宿主菜单会因 focusout 直接关闭,看起来像「点模型没反应」。现在改为给搜索框加 `readonly` + `inputmode=none`:保留焦点、不弹键盘,宿主菜单稳定打开。
22
+ - **用户主动搜索时恢复输入**:监听搜索框 `pointerdown` / `touchstart`,用户主动点的时候恢复 `readonly` 和 `inputmode` 并聚焦,需要搜索仍可正常输入。
23
+ - 模型列表默认完整显示,不再被 iOS 键盘遮挡。
24
+
25
+ ### 🧪 验证
26
+
27
+ - 模拟宿主自动 focus:sheet 保持打开,搜索框 `readOnly=true`、`inputmode=none`;用户 pointerdown 后 `readOnly=false` 且聚焦,4/4 通过。
28
+ - 运行中的 DSH:模型菜单打开后保持存在,sheet/遮罩正常。
29
+
30
+
3
31
  ## [v3.4.3] - 2026-11-02
4
32
 
5
33
  ### 🐛 修复
package/README.md CHANGED
@@ -48,7 +48,8 @@ dsh plugin --profile <name> add @qzsy/dsh-bridge
48
48
 
49
49
  - 内容区占满屏宽,左上角「菜单」按钮打开会话 / 工作区 / 插件入口;
50
50
  - 支持手势:左缘向右滑打开、抽屉内向左滑关闭,拖动时跟手,松手吸附;
51
- - 模型切换器在手机上改为底部 sheet:全宽、抓手、遮罩,支持下拉/点遮罩关闭;默认不聚焦搜索框(不弹键盘),需要搜索时再点;
51
+ - 模型切换器在手机上改为底部 sheet:全宽、抓手、遮罩,支持下拉/点遮罩关闭;默认不弹键盘(搜索框 `readonly`),需要搜索时再点;
52
+ - 选完模型自动继续进入「推理等级」sheet,Off / Low / High / Max 选完才关闭,手机上和桌面菜单一样连贯;
52
53
  - 全面屏自动避开安全区:刘海 / 状态栏 / 底部 home indicator 不遮挡内容;
53
54
  - 点遮罩、点会话行、按 Esc 或转回桌面宽度都会自动收起;
54
55
  - 桌面端保持 DSH 原生布局,不受影响。
package/client/client.js CHANGED
@@ -463,6 +463,15 @@ var MOBILE_DRAWER_CSS = `
463
463
  transition: transform 220ms ease, opacity 220ms ease !important;
464
464
  animation: none !important;
465
465
  }
466
+ /* \u9009\u4E2D\u6A21\u578B\u540E\u8FDB\u5165\u300C\u63A8\u7406\u7B49\u7EA7\u300D\u7684\u8FC7\u6E21\uFF1A\u65E7 sheet \u7684\u5FEB\u7167\u7559\u5728\u539F\u5730\uFF0C\u5BBF\u4E3B\u83DC\u5355\u5148\u9690\u85CF */
467
+ [data-qzsy-model-sheet-clone] {
468
+ z-index: 1199 !important;
469
+ pointer-events: none !important;
470
+ }
471
+ body.qzsy-model-advancing div[id$="-menu"]:not([data-qzsy-model-sheet]):not([data-qzsy-model-sheet-clone]) {
472
+ visibility: hidden !important;
473
+ pointer-events: none !important;
474
+ }
466
475
  body.qzsy-model-sheet-dragging [data-qzsy-model-sheet] {
467
476
  transform: translateY(var(--qzsy-model-sheet-y, 0px)) !important;
468
477
  transition: none !important;
@@ -516,6 +525,10 @@ function setupMobileDrawer() {
516
525
  let modelChainUntil = 0;
517
526
  let modelDrag = null;
518
527
  let modelDragHandlers = null;
528
+ let modelClone = null;
529
+ let modelAdvancing = false;
530
+ let reasoningStep = false;
531
+ let modelAdvanceToken = 0;
519
532
  const preventDrawerScroll = (event) => {
520
533
  if (!gesture || !event.cancelable) return;
521
534
  const touch = event.touches?.[0];
@@ -633,7 +646,7 @@ function setupMobileDrawer() {
633
646
  if (search) {
634
647
  const menu = search.closest('[role="menu"]') || search.closest('[class*="_menu"]');
635
648
  const portal = bodyPortalOf(menu || search);
636
- if (portal && visiblePortal(portal)) {
649
+ if (portal && visiblePortal(portal) && !portal.hasAttribute("data-qzsy-model-sheet-clone")) {
637
650
  const s = getComputedStyle(portal);
638
651
  if ((s.position === "fixed" || s.position === "absolute") && portal.getBoundingClientRect().width > 120) return portal;
639
652
  }
@@ -641,7 +654,8 @@ function setupMobileDrawer() {
641
654
  const candidates = [];
642
655
  for (const el of document.body.children) {
643
656
  if (!(el instanceof HTMLElement)) continue;
644
- if (el === modelBackdrop || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
657
+ if (el === modelBackdrop || el === modelClone || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
658
+ if (el.hasAttribute("data-qzsy-model-sheet-clone")) continue;
645
659
  if (!visiblePortal(el)) continue;
646
660
  const s = getComputedStyle(el);
647
661
  if (s.position !== "fixed" && s.position !== "absolute") continue;
@@ -661,6 +675,26 @@ function setupMobileDrawer() {
661
675
  candidates.sort((a, b) => Number(b.hasSearch) - Number(a.hasSearch) || Number(b.hasModelMarker) - Number(a.hasModelMarker) || b.rows - a.rows);
662
676
  return candidates[0]?.el || null;
663
677
  };
678
+ const findReasoningPortal = () => {
679
+ if (!isMobileViewport()) return null;
680
+ const candidates = [];
681
+ for (const el of document.body.children) {
682
+ if (!(el instanceof HTMLElement)) continue;
683
+ if (el === modelBackdrop || el === modelClone || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
684
+ if (el.hasAttribute("data-qzsy-model-sheet") || el.hasAttribute("data-qzsy-model-sheet-clone")) continue;
685
+ if (!visiblePortal(el)) continue;
686
+ const s = getComputedStyle(el);
687
+ if (s.position !== "fixed" && s.position !== "absolute") continue;
688
+ const menu = el.querySelector('[role="menu"]');
689
+ if (!menu) continue;
690
+ const radios = menu.querySelectorAll('button[role="menuitemradio"], [role="menuitemradio"], input[type="radio"]').length;
691
+ if (radios < 1) continue;
692
+ const text = menu.innerText || "";
693
+ if (/搜索模型|Search model|DeepSeek|claude|模型与推理等级/i.test(text)) continue;
694
+ candidates.push(el);
695
+ }
696
+ return candidates[candidates.length - 1] || null;
697
+ };
664
698
  const hostBackdropPresent = () => {
665
699
  for (const el of document.body.children) {
666
700
  if (!(el instanceof HTMLElement)) continue;
@@ -700,7 +734,23 @@ function setupMobileDrawer() {
700
734
  }, 450);
701
735
  }, 120);
702
736
  };
737
+ const dismissHostMenus = () => {
738
+ try {
739
+ document.body.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }));
740
+ } catch {
741
+ }
742
+ try {
743
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
744
+ } catch {
745
+ }
746
+ };
703
747
  const closeModelSheetNow = () => {
748
+ if (modelAdvancing) {
749
+ cancelAdvanceOverlay();
750
+ clearModelSheet();
751
+ dismissHostMenus();
752
+ return;
753
+ }
704
754
  const portal = modelPortal;
705
755
  if (!portal) return;
706
756
  document.body.classList.remove("qzsy-model-sheet-open");
@@ -709,6 +759,10 @@ function setupMobileDrawer() {
709
759
  requestHostModelClose(portal);
710
760
  };
711
761
  const closeModelSheetAnimated = () => {
762
+ if (modelAdvancing) {
763
+ closeModelSheetNow();
764
+ return;
765
+ }
712
766
  const portal = modelPortal;
713
767
  if (!portal) return;
714
768
  portal.classList.add("qzsy-model-sheet-closing");
@@ -778,68 +832,158 @@ function setupMobileDrawer() {
778
832
  portal.addEventListener("touchcancel", onEnd, { passive: true });
779
833
  modelDragHandlers = { onStart, onMove, onEnd };
780
834
  };
781
- const clearModelSheet = () => {
835
+ const clearModelSheet = ({ keepChrome = false } = {}) => {
782
836
  if (modelPortal) {
783
837
  detachModelDrag();
784
838
  modelPortal.removeAttribute("data-qzsy-model-sheet");
839
+ modelPortal.removeAttribute("data-qzsy-model-step");
785
840
  modelPortal.classList.remove("qzsy-model-sheet", "qzsy-model-sheet-closing");
786
841
  }
787
842
  modelPortal = null;
788
843
  modelDrag = null;
789
- document.body?.classList.remove("qzsy-model-sheet-open", "qzsy-model-sheet-dragging");
844
+ modelAdvanceToken += 1;
845
+ modelAdvancing = false;
846
+ reasoningStep = false;
847
+ if (modelClone) {
848
+ modelClone.remove();
849
+ modelClone = null;
850
+ }
851
+ document.body?.classList.remove("qzsy-model-advancing", "qzsy-model-sheet-dragging");
790
852
  document.body?.style.removeProperty("--qzsy-model-sheet-y");
791
853
  document.body?.style.removeProperty("--qzsy-model-sheet-progress");
792
- if (modelBackdrop) {
793
- modelBackdrop.remove();
794
- modelBackdrop = null;
854
+ if (!keepChrome) {
855
+ document.body?.classList.remove("qzsy-model-sheet-open");
856
+ if (modelBackdrop) {
857
+ modelBackdrop.remove();
858
+ modelBackdrop = null;
859
+ }
795
860
  }
796
861
  };
797
862
  const applyModelSheet = (portal) => {
798
863
  if (modelPortal === portal) return;
799
- clearModelSheet();
864
+ const step = reasoningStep ? "reasoning" : "model";
865
+ const keepChrome = modelAdvancing && Boolean(modelClone);
866
+ clearModelSheet({ keepChrome });
800
867
  modelPortal = portal;
801
868
  modelChainUntil = 0;
802
869
  portal.setAttribute("data-qzsy-model-sheet", "");
870
+ portal.setAttribute("data-qzsy-model-step", step);
803
871
  portal.classList.add("qzsy-model-sheet");
804
872
  attachModelDrag(portal);
805
873
  ensureModelBackdrop();
806
874
  const input = portal.querySelector('input[placeholder*="\u641C\u7D22\u6A21\u578B"], input[placeholder*="Search model"], input[type="search"], input');
807
875
  if (input) {
808
876
  let userIntent = false;
809
- const markUserIntent = () => {
877
+ const originalReadOnly = input.readOnly;
878
+ const originalInputMode = input.getAttribute("inputmode");
879
+ const enableSearch = () => {
880
+ if (userIntent) return;
810
881
  userIntent = true;
811
- };
812
- input.addEventListener("pointerdown", markUserIntent, { once: true });
813
- input.addEventListener("touchstart", markUserIntent, { once: true, passive: true });
814
- const blurIfAutoFocused = () => {
815
- if (!input.isConnected || userIntent) return;
816
- if (document.activeElement === input) {
817
- try {
818
- input.blur();
819
- } catch {
820
- }
882
+ input.readOnly = originalReadOnly;
883
+ if (originalInputMode === null) input.removeAttribute("inputmode");
884
+ else input.setAttribute("inputmode", originalInputMode);
885
+ try {
886
+ input.focus({ preventScroll: true });
887
+ } catch {
821
888
  }
822
889
  };
823
- const onFocus = () => {
824
- if (!userIntent) blurIfAutoFocused();
890
+ input.addEventListener("pointerdown", enableSearch);
891
+ input.addEventListener("touchstart", enableSearch, { passive: true });
892
+ const enforceNoKeyboard = () => {
893
+ if (!input.isConnected || userIntent) return;
894
+ if (!input.readOnly) input.readOnly = true;
895
+ if (!input.hasAttribute("readonly")) input.setAttribute("readonly", "");
896
+ if (input.getAttribute("inputmode") !== "none") input.setAttribute("inputmode", "none");
825
897
  };
826
- input.addEventListener("focus", onFocus);
827
- blurIfAutoFocused();
828
- window.requestAnimationFrame(blurIfAutoFocused);
829
- window.setTimeout(blurIfAutoFocused, 80);
830
- window.setTimeout(blurIfAutoFocused, 260);
831
- window.setTimeout(() => input.removeEventListener("focus", onFocus), 900);
898
+ input.addEventListener("focus", enforceNoKeyboard);
899
+ enforceNoKeyboard();
900
+ window.setTimeout(enforceNoKeyboard, 0);
901
+ window.setTimeout(enforceNoKeyboard, 80);
902
+ window.setTimeout(enforceNoKeyboard, 260);
903
+ window.setTimeout(() => input.removeEventListener("focus", enforceNoKeyboard), 1e3);
832
904
  }
833
905
  window.requestAnimationFrame(() => {
834
906
  if (!disposed && modelPortal === portal) document.body.classList.add("qzsy-model-sheet-open");
835
907
  });
836
908
  };
909
+ const cancelAdvanceOverlay = () => {
910
+ modelAdvanceToken += 1;
911
+ modelAdvancing = false;
912
+ reasoningStep = false;
913
+ document.body?.classList.remove("qzsy-model-advancing");
914
+ if (modelClone) {
915
+ modelClone.remove();
916
+ modelClone = null;
917
+ }
918
+ };
919
+ const beginReasoningAdvance = (portal) => {
920
+ if (disposed || modelAdvancing) return;
921
+ if (portal.getAttribute("data-qzsy-model-step") !== "model") return;
922
+ if (!portal.querySelector('button[role="menuitemradio"], [role="menuitemradio"]')) return;
923
+ modelAdvancing = true;
924
+ const token = ++modelAdvanceToken;
925
+ document.body.classList.add("qzsy-model-advancing");
926
+ modelClone = portal.cloneNode(true);
927
+ modelClone.setAttribute("data-qzsy-model-sheet-clone", "");
928
+ modelClone.setAttribute("aria-hidden", "true");
929
+ for (const el of modelClone.querySelectorAll("[id]")) el.removeAttribute("id");
930
+ document.body.appendChild(modelClone);
931
+ window.setTimeout(() => {
932
+ if (disposed || token !== modelAdvanceToken || !modelAdvancing) return;
933
+ const trigger = document.querySelector(
934
+ 'button[aria-haspopup="menu"][aria-label*="\u9009\u62E9\u6A21\u578B"], button[aria-haspopup="menu"][aria-label*="Select model"], button[aria-haspopup="menu"][aria-label*="\u6A21\u578B"]'
935
+ );
936
+ try {
937
+ trigger?.click();
938
+ } catch {
939
+ }
940
+ let attempts = 0;
941
+ const openReasonCell = () => {
942
+ if (disposed || token !== modelAdvanceToken || !modelAdvancing) return;
943
+ const cell = [...document.querySelectorAll('button[role="menuitem"]')].find((b) => /推理等级|Reasoning effort|Reasoning level|推理强度/i.test(b.innerText || ""));
944
+ if (cell) {
945
+ reasoningStep = true;
946
+ try {
947
+ cell.click();
948
+ } catch {
949
+ }
950
+ window.setTimeout(() => {
951
+ if (disposed || token !== modelAdvanceToken) return;
952
+ if (!modelPortal) {
953
+ cancelAdvanceOverlay();
954
+ clearModelSheet();
955
+ }
956
+ }, 900);
957
+ return;
958
+ }
959
+ attempts += 1;
960
+ if (attempts > 16) {
961
+ cancelAdvanceOverlay();
962
+ clearModelSheet();
963
+ return;
964
+ }
965
+ window.setTimeout(openReasonCell, 60);
966
+ };
967
+ window.setTimeout(openReasonCell, 120);
968
+ }, 140);
969
+ };
837
970
  const syncModelSheet = () => {
838
971
  if (disposed) return;
839
972
  if (!isMobileViewport()) {
840
973
  clearModelSheet();
841
974
  return;
842
975
  }
976
+ if (modelAdvancing) {
977
+ if (reasoningStep) {
978
+ const next = findReasoningPortal();
979
+ if (next) {
980
+ applyModelSheet(next);
981
+ return;
982
+ }
983
+ }
984
+ if (modelPortal && !visiblePortal(modelPortal)) modelPortal = null;
985
+ return;
986
+ }
843
987
  if (modelPortal && !visiblePortal(modelPortal)) clearModelSheet();
844
988
  const found = findModelPortal();
845
989
  if (!found) {
@@ -868,6 +1012,10 @@ function setupMobileDrawer() {
868
1012
  window.setTimeout(syncModelSheet, 30);
869
1013
  window.setTimeout(syncModelSheet, 180);
870
1014
  }
1015
+ const modelOption = target.closest('button[role="menuitemradio"], [role="menuitemradio"]');
1016
+ if (modelOption && modelPortal && modelPortal.getAttribute("data-qzsy-model-step") === "model" && modelPortal.contains(modelOption)) {
1017
+ beginReasoningAdvance(modelPortal);
1018
+ }
871
1019
  if (!isOpen()) return;
872
1020
  if (target.closest(`.${BACKDROP_CLASS}`)) {
873
1021
  closeDrawer();
package/client/mobile.js CHANGED
@@ -260,6 +260,15 @@ export const MOBILE_DRAWER_CSS = `
260
260
  transition: transform 220ms ease, opacity 220ms ease !important;
261
261
  animation: none !important;
262
262
  }
263
+ /* 选中模型后进入「推理等级」的过渡:旧 sheet 的快照留在原地,宿主菜单先隐藏 */
264
+ [data-qzsy-model-sheet-clone] {
265
+ z-index: 1199 !important;
266
+ pointer-events: none !important;
267
+ }
268
+ body.qzsy-model-advancing div[id$="-menu"]:not([data-qzsy-model-sheet]):not([data-qzsy-model-sheet-clone]) {
269
+ visibility: hidden !important;
270
+ pointer-events: none !important;
271
+ }
263
272
  body.qzsy-model-sheet-dragging [data-qzsy-model-sheet] {
264
273
  transform: translateY(var(--qzsy-model-sheet-y, 0px)) !important;
265
274
  transition: none !important;
@@ -322,6 +331,10 @@ export function setupMobileDrawer() {
322
331
  let modelChainUntil = 0;
323
332
  let modelDrag = null;
324
333
  let modelDragHandlers = null;
334
+ let modelClone = null;
335
+ let modelAdvancing = false;
336
+ let reasoningStep = false;
337
+ let modelAdvanceToken = 0;
325
338
 
326
339
  const preventDrawerScroll = (event) => {
327
340
  if (!gesture || !event.cancelable) return;
@@ -451,7 +464,7 @@ export function setupMobileDrawer() {
451
464
  if (search) {
452
465
  const menu = search.closest('[role="menu"]') || search.closest('[class*="_menu"]');
453
466
  const portal = bodyPortalOf(menu || search);
454
- if (portal && visiblePortal(portal)) {
467
+ if (portal && visiblePortal(portal) && !portal.hasAttribute('data-qzsy-model-sheet-clone')) {
455
468
  const s = getComputedStyle(portal);
456
469
  if ((s.position === 'fixed' || s.position === 'absolute') && portal.getBoundingClientRect().width > 120) return portal;
457
470
  }
@@ -460,7 +473,8 @@ export function setupMobileDrawer() {
460
473
  const candidates = [];
461
474
  for (const el of document.body.children) {
462
475
  if (!(el instanceof HTMLElement)) continue;
463
- if (el === modelBackdrop || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
476
+ if (el === modelBackdrop || el === modelClone || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
477
+ if (el.hasAttribute('data-qzsy-model-sheet-clone')) continue;
464
478
  if (!visiblePortal(el)) continue;
465
479
  const s = getComputedStyle(el);
466
480
  if (s.position !== 'fixed' && s.position !== 'absolute') continue;
@@ -485,6 +499,28 @@ export function setupMobileDrawer() {
485
499
  return candidates[0]?.el || null;
486
500
  };
487
501
 
502
+ const findReasoningPortal = () => {
503
+ if (!isMobileViewport()) return null;
504
+ const candidates = [];
505
+ for (const el of document.body.children) {
506
+ if (!(el instanceof HTMLElement)) continue;
507
+ if (el === modelBackdrop || el === modelClone || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
508
+ if (el.hasAttribute('data-qzsy-model-sheet') || el.hasAttribute('data-qzsy-model-sheet-clone')) continue;
509
+ if (!visiblePortal(el)) continue;
510
+ const s = getComputedStyle(el);
511
+ if (s.position !== 'fixed' && s.position !== 'absolute') continue;
512
+ const menu = el.querySelector('[role="menu"]');
513
+ if (!menu) continue;
514
+ const radios = menu.querySelectorAll('button[role="menuitemradio"], [role="menuitemradio"], input[type="radio"]').length;
515
+ if (radios < 1) continue;
516
+ const text = menu.innerText || '';
517
+ // 推理等级菜单只含 Off / Low / High / Max 等档位,不含模型搜索与模型分组
518
+ if (/搜索模型|Search model|DeepSeek|claude|模型与推理等级/i.test(text)) continue;
519
+ candidates.push(el);
520
+ }
521
+ return candidates[candidates.length - 1] || null;
522
+ };
523
+
488
524
  const hostBackdropPresent = () => {
489
525
  for (const el of document.body.children) {
490
526
  if (!(el instanceof HTMLElement)) continue;
@@ -517,7 +553,18 @@ export function setupMobileDrawer() {
517
553
  }, 120);
518
554
  };
519
555
 
556
+ const dismissHostMenus = () => {
557
+ try { document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true })); } catch { /* 忽略 */ }
558
+ try { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); } catch { /* 忽略 */ }
559
+ };
560
+
520
561
  const closeModelSheetNow = () => {
562
+ if (modelAdvancing) {
563
+ cancelAdvanceOverlay();
564
+ clearModelSheet();
565
+ dismissHostMenus();
566
+ return;
567
+ }
521
568
  const portal = modelPortal;
522
569
  if (!portal) return;
523
570
  document.body.classList.remove('qzsy-model-sheet-open');
@@ -527,6 +574,10 @@ export function setupMobileDrawer() {
527
574
  };
528
575
 
529
576
  const closeModelSheetAnimated = () => {
577
+ if (modelAdvancing) {
578
+ closeModelSheetNow();
579
+ return;
580
+ }
530
581
  const portal = modelPortal;
531
582
  if (!portal) return;
532
583
  portal.classList.add('qzsy-model-sheet-closing');
@@ -600,54 +651,77 @@ export function setupMobileDrawer() {
600
651
  modelDragHandlers = { onStart, onMove, onEnd };
601
652
  };
602
653
 
603
- const clearModelSheet = () => {
654
+ const clearModelSheet = ({ keepChrome = false } = {}) => {
604
655
  if (modelPortal) {
605
656
  detachModelDrag();
606
657
  modelPortal.removeAttribute('data-qzsy-model-sheet');
658
+ modelPortal.removeAttribute('data-qzsy-model-step');
607
659
  modelPortal.classList.remove('qzsy-model-sheet', 'qzsy-model-sheet-closing');
608
660
  }
609
661
  modelPortal = null;
610
662
  modelDrag = null;
611
- document.body?.classList.remove('qzsy-model-sheet-open', 'qzsy-model-sheet-dragging');
663
+ modelAdvanceToken += 1;
664
+ modelAdvancing = false;
665
+ reasoningStep = false;
666
+ if (modelClone) {
667
+ modelClone.remove();
668
+ modelClone = null;
669
+ }
670
+ document.body?.classList.remove('qzsy-model-advancing', 'qzsy-model-sheet-dragging');
612
671
  document.body?.style.removeProperty('--qzsy-model-sheet-y');
613
672
  document.body?.style.removeProperty('--qzsy-model-sheet-progress');
614
- if (modelBackdrop) {
615
- modelBackdrop.remove();
616
- modelBackdrop = null;
673
+ if (!keepChrome) {
674
+ document.body?.classList.remove('qzsy-model-sheet-open');
675
+ if (modelBackdrop) {
676
+ modelBackdrop.remove();
677
+ modelBackdrop = null;
678
+ }
617
679
  }
618
680
  };
619
681
 
620
682
  const applyModelSheet = (portal) => {
621
683
  if (modelPortal === portal) return;
622
- clearModelSheet();
684
+ const step = reasoningStep ? 'reasoning' : 'model';
685
+ const keepChrome = modelAdvancing && Boolean(modelClone);
686
+ clearModelSheet({ keepChrome });
623
687
  modelPortal = portal;
624
688
  modelChainUntil = 0;
625
689
  portal.setAttribute('data-qzsy-model-sheet', '');
690
+ portal.setAttribute('data-qzsy-model-step', step);
626
691
  portal.classList.add('qzsy-model-sheet');
627
692
  attachModelDrag(portal);
628
693
  ensureModelBackdrop();
629
694
 
630
695
  // 宿主菜单默认会聚焦「搜索模型」,手机上会直接弹起键盘把 sheet 顶掉一半。
631
- // 移动端模型数量通常很少,默认不聚焦;用户主动点搜索框时再允许聚焦。
696
+ // 移动端模型数量通常很少:先把搜索框设为 readonly + inputmode=none,阻止键盘;
697
+ // 保留焦点,避免宿主因 focusout 关闭菜单。用户主动点搜索框时再恢复可输入。
632
698
  const input = portal.querySelector('input[placeholder*="搜索模型"], input[placeholder*="Search model"], input[type="search"], input');
633
699
  if (input) {
634
700
  let userIntent = false;
635
- const markUserIntent = () => { userIntent = true; };
636
- input.addEventListener('pointerdown', markUserIntent, { once: true });
637
- input.addEventListener('touchstart', markUserIntent, { once: true, passive: true });
638
- const blurIfAutoFocused = () => {
701
+ const originalReadOnly = input.readOnly;
702
+ const originalInputMode = input.getAttribute('inputmode');
703
+ const enableSearch = () => {
704
+ if (userIntent) return;
705
+ userIntent = true;
706
+ input.readOnly = originalReadOnly;
707
+ if (originalInputMode === null) input.removeAttribute('inputmode');
708
+ else input.setAttribute('inputmode', originalInputMode);
709
+ try { input.focus({ preventScroll: true }); } catch { /* 忽略 */ }
710
+ };
711
+ input.addEventListener('pointerdown', enableSearch);
712
+ input.addEventListener('touchstart', enableSearch, { passive: true });
713
+ const enforceNoKeyboard = () => {
639
714
  if (!input.isConnected || userIntent) return;
640
- if (document.activeElement === input) {
641
- try { input.blur(); } catch { /* 忽略 */ }
642
- }
715
+ if (!input.readOnly) input.readOnly = true;
716
+ if (!input.hasAttribute('readonly')) input.setAttribute('readonly', '');
717
+ if (input.getAttribute('inputmode') !== 'none') input.setAttribute('inputmode', 'none');
643
718
  };
644
- const onFocus = () => { if (!userIntent) blurIfAutoFocused(); };
645
- input.addEventListener('focus', onFocus);
646
- blurIfAutoFocused();
647
- window.requestAnimationFrame(blurIfAutoFocused);
648
- window.setTimeout(blurIfAutoFocused, 80);
649
- window.setTimeout(blurIfAutoFocused, 260);
650
- window.setTimeout(() => input.removeEventListener('focus', onFocus), 900);
719
+ input.addEventListener('focus', enforceNoKeyboard);
720
+ enforceNoKeyboard();
721
+ window.setTimeout(enforceNoKeyboard, 0);
722
+ window.setTimeout(enforceNoKeyboard, 80);
723
+ window.setTimeout(enforceNoKeyboard, 260);
724
+ window.setTimeout(() => input.removeEventListener('focus', enforceNoKeyboard), 1000);
651
725
  }
652
726
 
653
727
  window.requestAnimationFrame(() => {
@@ -655,12 +729,83 @@ export function setupMobileDrawer() {
655
729
  });
656
730
  };
657
731
 
732
+ const cancelAdvanceOverlay = () => {
733
+ modelAdvanceToken += 1;
734
+ modelAdvancing = false;
735
+ reasoningStep = false;
736
+ document.body?.classList.remove('qzsy-model-advancing');
737
+ if (modelClone) {
738
+ modelClone.remove();
739
+ modelClone = null;
740
+ }
741
+ };
742
+
743
+ // 选完模型后自动进入推理等级:保留旧 sheet 快照,宿主菜单在背后切换,再滑入推理 sheet
744
+ const beginReasoningAdvance = (portal) => {
745
+ if (disposed || modelAdvancing) return;
746
+ if (portal.getAttribute('data-qzsy-model-step') !== 'model') return;
747
+ if (!portal.querySelector('button[role="menuitemradio"], [role="menuitemradio"]')) return;
748
+ modelAdvancing = true;
749
+ const token = ++modelAdvanceToken;
750
+ document.body.classList.add('qzsy-model-advancing');
751
+ modelClone = portal.cloneNode(true);
752
+ modelClone.setAttribute('data-qzsy-model-sheet-clone', '');
753
+ modelClone.setAttribute('aria-hidden', 'true');
754
+ for (const el of modelClone.querySelectorAll('[id]')) el.removeAttribute('id');
755
+ document.body.appendChild(modelClone);
756
+
757
+ window.setTimeout(() => {
758
+ if (disposed || token !== modelAdvanceToken || !modelAdvancing) return;
759
+ const trigger = document.querySelector(
760
+ 'button[aria-haspopup="menu"][aria-label*="选择模型"], button[aria-haspopup="menu"][aria-label*="Select model"], button[aria-haspopup="menu"][aria-label*="模型"]',
761
+ );
762
+ try { trigger?.click(); } catch { /* 忽略 */ }
763
+ let attempts = 0;
764
+ const openReasonCell = () => {
765
+ if (disposed || token !== modelAdvanceToken || !modelAdvancing) return;
766
+ const cell = [...document.querySelectorAll('button[role="menuitem"]')].find((b) => /推理等级|Reasoning effort|Reasoning level|推理强度/i.test(b.innerText || ''));
767
+ if (cell) {
768
+ reasoningStep = true;
769
+ try { cell.click(); } catch { /* 忽略 */ }
770
+ window.setTimeout(() => {
771
+ if (disposed || token !== modelAdvanceToken) return;
772
+ if (!modelPortal) {
773
+ // 推理菜单没出现:撤掉 overlay,避免卡在半路
774
+ cancelAdvanceOverlay();
775
+ clearModelSheet();
776
+ }
777
+ }, 900);
778
+ return;
779
+ }
780
+ attempts += 1;
781
+ if (attempts > 16) {
782
+ cancelAdvanceOverlay();
783
+ clearModelSheet();
784
+ return;
785
+ }
786
+ window.setTimeout(openReasonCell, 60);
787
+ };
788
+ window.setTimeout(openReasonCell, 120);
789
+ }, 140);
790
+ };
791
+
658
792
  const syncModelSheet = () => {
659
793
  if (disposed) return;
660
794
  if (!isMobileViewport()) {
661
795
  clearModelSheet();
662
796
  return;
663
797
  }
798
+ if (modelAdvancing) {
799
+ if (reasoningStep) {
800
+ const next = findReasoningPortal();
801
+ if (next) {
802
+ applyModelSheet(next);
803
+ return;
804
+ }
805
+ }
806
+ if (modelPortal && !visiblePortal(modelPortal)) modelPortal = null;
807
+ return;
808
+ }
664
809
  if (modelPortal && !visiblePortal(modelPortal)) clearModelSheet();
665
810
  const found = findModelPortal();
666
811
  if (!found) {
@@ -694,6 +839,14 @@ export function setupMobileDrawer() {
694
839
  window.setTimeout(syncModelSheet, 180);
695
840
  }
696
841
 
842
+ // 选完模型继续进入推理等级,而不是直接关掉整个菜单
843
+ const modelOption = target.closest('button[role="menuitemradio"], [role="menuitemradio"]');
844
+ if (modelOption && modelPortal
845
+ && modelPortal.getAttribute('data-qzsy-model-step') === 'model'
846
+ && modelPortal.contains(modelOption)) {
847
+ beginReasoningAdvance(modelPortal);
848
+ }
849
+
697
850
  if (!isOpen()) return;
698
851
 
699
852
  if (target.closest(`.${BACKDROP_CLASS}`)) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@qzsy/dsh-bridge",
3
- "version": "3.4.3",
3
+ "version": "3.5.0",
4
4
  "description": "DSH 远程隧道云官方客户端插件:一条命令把本机 DeepSeek Harness 接入云端隧道(设备码绑定、短期令牌、本地门禁代理、HTTP/SSE/WebSocket 反向隧道)。",
5
- "releaseNotes": "【v3.4.3】模型 sheet 默认不弹键盘\\n• 宿主打开模型菜单会自动聚焦搜索框,iOS 键盘把 sheet 顶掉一半;现在自动 blur\\n• 模型数量少时默认直接看到完整列表;需要搜索时主动点搜索框仍可输入\\n• 立即 / 80ms / 260ms 三次兜底 blur,不误伤用户主动点击",
5
+ "releaseNotes": "【v3.5.0】模型 → 思考级别连招\\n• 选完模型自动进入推理等级 sheet(Off / Low / High / Max),选完才关闭\\n• 旧 sheet 快照过渡,宿主菜单在背后切换,不闪桌面 popover\\n• 模型和推理等级仍由宿主选择;桌面端不变",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
8
8
  "exports": {