@yfwu2020/dsh-selection-explain 0.12.0 → 0.13.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/lib/client.js CHANGED
@@ -119,6 +119,9 @@ var HISTORY = "/selection-explain/api/history";
119
119
  var MODELS = "/selection-explain/api/models";
120
120
  var ATTACH = "/selection-explain/api/attach";
121
121
  var ATTACHMENT = "/selection-explain/api/attachment";
122
+ var SHOT_START = "/selection-explain/api/shot/start";
123
+ var SHOT_PENDING = "/selection-explain/api/shot/pending";
124
+ var SHOT_IMAGE = "/selection-explain/api/shot/image";
122
125
  var ATTACH_MAX_ITEMS = 10;
123
126
  var ATTACH_MAX_ITEM_BYTES = 20 * 1024 * 1024;
124
127
  var SETTINGS = "/selection-explain/api/settings";
@@ -282,6 +285,9 @@ var CSS = [
282
285
  // 可以自动换行、超过 84px 内部滚动,但**不显示滚动条**。
283
286
  // ⚠️ flex:0 0 auto 不能省:滚动区是 flex 列,而 overflow 非 visible 的 flex 项目
284
287
  // 自动最小高度是 0 → 内容一多就被压扁,文字直接看不全(实测踩过)。
288
+ // 顶部那行是"生成的标题"时:两行封顶,超出用省略号(而不是出现滚动条)
289
+ '.dsh-sel-quote[data-title="1"]{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;',
290
+ "overflow:hidden;white-space:normal;max-height:none}",
285
291
  ".dsh-sel-quote{margin:0;padding:7px 11px;border-radius:10px;flex:0 0 auto;border-left:3px solid var(--sel-a1,#0d9488);",
286
292
  "background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.11));color:var(--dsw-alias-label-secondary,inherit);",
287
293
  "font-size:12.5px;line-height:1.6;box-sizing:border-box;max-width:100%;max-height:84px;",
@@ -458,6 +464,10 @@ var CSS = [
458
464
  "display:grid;place-items:center;color:var(--dsw-alias-label-secondary,inherit)}",
459
465
  ".dsh-sel-thumb img{width:100%;height:100%;object-fit:cover;display:block}",
460
466
  ".dsh-sel-thumb svg{width:15px;height:15px;opacity:.55}",
467
+ // 可预览的缩略图:放大镜光标 + 键盘焦点环(它是 role=button,能 Tab 到)
468
+ '.dsh-sel-thumb[data-preview="1"]{cursor:zoom-in}',
469
+ '.dsh-sel-thumb[data-preview="1"]:focus-visible{outline:2px solid var(--sel-a1,#0d9488);outline-offset:1px}',
470
+ '.dsh-sel-thumb[data-preview="1"]:hover img{filter:brightness(1.06)}',
461
471
  '.dsh-sel-thumb[data-state="error"]{border-color:color-mix(in srgb,var(--sel-bad,#e5534b) 70%,transparent)}',
462
472
  '.dsh-sel-thumb[data-state="error"] svg{opacity:.9;color:var(--sel-bad,#e5534b)}',
463
473
  // 转圈 + 不定态进度条:fetch 拿不到上传进度,所以**不假装知道百分比**
@@ -535,6 +545,50 @@ var CSS = [
535
545
  "border-radius:5px;line-height:15px;background:var(--sel-fill,#0f766e);color:var(--sel-fill-fg,#fff)}",
536
546
  ".dsh-sel-bqfilename{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px}",
537
547
  ".dsh-sel-bqfilemeta{flex:0 0 auto;font-size:10.5px;opacity:.6}",
548
+ // ── 图片预览(灯箱)──
549
+ // 挂在 layer 上、铺满整扇窗口:小窗只有 540 宽,把截图塞回小窗里预览等于没放大。
550
+ // 层级压过面板与语音卡;pointer-events 必须显式打开 —— layer 自己是穿透的,
551
+ // 不写这一行的话灯箱看得见却点不着。
552
+ ".dsh-sel-lightbox{position:fixed;inset:0;z-index:" + String(Z_PANEL + 2) + ";display:none;flex-direction:column;",
553
+ "align-items:center;justify-content:center;padding:48px 32px 34px;box-sizing:border-box;",
554
+ "background:rgba(9,11,13,.8);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);",
555
+ "pointer-events:auto;-webkit-app-region:no-drag;color:#fff;font:400 12.5px/1.5 " + FONT + "}",
556
+ '.dsh-sel-lightbox[data-open="1"]{display:flex;animation:dsh-sel-lightfade .12s ease}',
557
+ "@keyframes dsh-sel-lightfade{from{opacity:0}to{opacity:1}}",
558
+ // 顶栏:文件名(左,过长省略)+ 关闭键(右)。用 absolute 让它压在图片上方,
559
+ // 不用再给图片让出一条高度 —— 纵向空间本来就紧张。
560
+ ".dsh-sel-lightbox-bar{position:absolute;top:0;left:0;right:0;height:44px;display:flex;align-items:center;gap:12px;",
561
+ "padding:0 14px 0 20px;box-sizing:border-box;background:linear-gradient(rgba(0,0,0,.5),transparent)}",
562
+ ".dsh-sel-lightbox-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
563
+ "font-size:12.5px;opacity:.9}",
564
+ // 计数(3 / 7):只在多图时出现,等宽数字 —— 翻页时数字不跟着左右抖。
565
+ ".dsh-sel-lightbox-count{flex:0 0 auto;font-size:11.5px;opacity:.62;font-variant-numeric:tabular-nums;letter-spacing:.3px}",
566
+ '.dsh-sel-lightbox[data-gallery="0"] .dsh-sel-lightbox-count{display:none}',
567
+ ".dsh-sel-lightbox-x{flex:0 0 auto;width:28px;height:28px;padding:0;border:0;border-radius:14px;cursor:pointer;",
568
+ "display:grid;place-items:center;background:rgba(255,255,255,.12);color:#fff;font:inherit;opacity:.9}",
569
+ ".dsh-sel-lightbox-x:hover{background:rgba(255,255,255,.22);opacity:1}",
570
+ ".dsh-sel-lightbox-x svg{width:14px;height:14px;display:block}",
571
+ ".dsh-sel-lightbox-x:focus-visible{outline:2px solid #fff;outline-offset:2px}",
572
+ // 左右翻页:贴两侧垂直居中,和 ✕ 同一套半透明白底。
573
+ // 「到头」用 disabled 表示(22% 不透明)—— 小窗/窗口里没有滑动手势,全靠这两个键 + 方向键。
574
+ ".dsh-sel-lightbox-nav{position:absolute;top:50%;margin-top:-20px;width:40px;height:40px;padding:0;border:0;border-radius:20px;",
575
+ "display:grid;place-items:center;cursor:pointer;background:rgba(255,255,255,.12);color:#fff;opacity:.9;",
576
+ "box-shadow:0 4px 16px rgba(0,0,0,.3);transition:background .12s ease,opacity .12s ease}",
577
+ ".dsh-sel-lightbox-nav:hover:not(:disabled){background:rgba(255,255,255,.24);opacity:1}",
578
+ ".dsh-sel-lightbox-nav:focus-visible{outline:2px solid #fff;outline-offset:2px}",
579
+ ".dsh-sel-lightbox-nav:disabled{opacity:.22;cursor:default}",
580
+ '.dsh-sel-lightbox-nav[data-dir="prev"]{left:12px}',
581
+ '.dsh-sel-lightbox-nav[data-dir="next"]{right:12px}',
582
+ '.dsh-sel-lightbox[data-gallery="0"] .dsh-sel-lightbox-nav{display:none}',
583
+ // 舞台:占满可用高度,图 contain 到最大;点击图片本身不算"点空白",不会误关。
584
+ ".dsh-sel-lightbox-stage{flex:1 1 auto;width:100%;min-height:0;display:flex;align-items:center;justify-content:center}",
585
+ ".dsh-sel-lightbox-img{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;",
586
+ "border-radius:10px;background:rgba(255,255,255,.04);box-shadow:0 24px 64px rgba(0,0,0,.55);cursor:default}",
587
+ '.dsh-sel-lightbox[data-error="1"] .dsh-sel-lightbox-img{display:none}',
588
+ ".dsh-sel-lightbox-fail{display:none;margin:0;font-size:12.5px;opacity:.72}",
589
+ '.dsh-sel-lightbox[data-error="1"] .dsh-sel-lightbox-fail{display:block}',
590
+ ".dsh-sel-lightbox-hint{margin:0;font-size:11px;opacity:.5}",
591
+ '@media (prefers-reduced-motion:reduce){.dsh-sel-lightbox[data-open="1"]{animation:none}}',
538
592
  // 「引用整条」:贴在助手气泡末尾,**平时不出现**(正文优先),鼠标移到这条消息上才浮出来。
539
593
  // 键盘 Tab 也能到(:focus-visible 同样点亮),触屏上另有浮标那条路。
540
594
  ".dsh-sel-bubquote{margin-top:7px;display:inline-flex;align-items:center;gap:4px;padding:2px 9px;",
@@ -692,6 +746,7 @@ var CSS = [
692
746
  "background:var(--sel-warn-bg,rgba(255,166,87,.14));color:var(--sel-warn-fg,#b45309);font-size:11.5px;line-height:1.4}",
693
747
  ".dsh-sel-undo button{margin-left:auto;border:0;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;text-decoration:underline}",
694
748
  ".dsh-sel-historytext{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
749
+ ".dsh-sel-historysub{font-size:10.5px;line-height:1.4;opacity:.62;margin-top:1px;}",
695
750
  ".dsh-sel-historymeta{display:flex;align-items:center;gap:6px;font-size:11px;opacity:.5;font-variant-numeric:tabular-nums;min-width:0}",
696
751
  ".dsh-sel-historymeta>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
697
752
  ".dsh-sel-historymeta .dsh-sel-historypin{opacity:.9}",
@@ -956,6 +1011,32 @@ var CSS = [
956
1011
  ".dsh-sel-uline{position:fixed;height:2px;border-radius:1px;pointer-events:none}",
957
1012
  // 牵引线是 **SVG 贝塞尔**(原来是一根 2px 竖条 → 控制点和端点同 x,退化成直线,
958
1013
  // 用户一眼看出"这不是贝塞尔")。现在两端各给一个反向的横向控制量,做出可见的 S 形。
1014
+ // ── 截图提问:选框(选区落在窗口内时)/ 贴边缩略图(落在窗口外时)──
1015
+ ".dsh-sel-shotframe{position:fixed;z-index:2147482997;display:none;pointer-events:none;",
1016
+ "border:2px solid var(--sel-a1,#0d9488);border-radius:3px;",
1017
+ "box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 4px color-mix(in srgb,var(--sel-a1,#0d9488) 14%,transparent)}",
1018
+ '.dsh-sel-shotframe[data-show="1"]{display:block}',
1019
+ ".dsh-sel-shotframe-tag{position:absolute;left:-2px;bottom:-19px;padding:1px 6px;border-radius:5px;",
1020
+ "background:var(--sel-fill,#0f766e);color:var(--sel-fill-fg,#fff);font-size:10px;line-height:1.6;white-space:nowrap}",
1021
+ ".dsh-sel-shotchip{position:fixed;z-index:2147482997;display:none;align-items:center;gap:7px;box-sizing:border-box;",
1022
+ "padding:5px 9px 5px 5px;border-radius:11px;background:var(--dsw-alias-bg-base,#fff);",
1023
+ "border:.5px solid var(--dsw-alias-border-l3,rgba(140,140,140,.35));box-shadow:0 8px 22px rgba(0,0,0,.24)}",
1024
+ '.dsh-sel-shotchip[data-show="1"]{display:flex}',
1025
+ ".dsh-sel-shotchip img{display:block;width:56px;height:32px;object-fit:cover;border-radius:6px;",
1026
+ "border:1.5px solid #e5484d;flex:0 0 auto;background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.14))}",
1027
+ ".dsh-sel-shotchip-tx{display:flex;flex-direction:column;font-size:10.5px;line-height:1.35;white-space:nowrap;",
1028
+ "color:var(--dsw-alias-label-secondary,rgba(90,90,90,.9))}",
1029
+ ".dsh-sel-shotchip-tx b{font-size:10.5px;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}",
1030
+ // ── 截图失败的可见提示(Toast)──
1031
+ // 以前所有失败都只写进 /shot/status 诊断,用户按了快捷键没反应。这条浮层是唯一的用户可见面:
1032
+ // 位置放在底部中间(不挡卡片/面板),点击可立刻关掉,4.5 秒后自动消失。
1033
+ ".dsh-sel-shot-toast{position:fixed;left:50%;bottom:76px;transform:translate(-50%,6px);z-index:2147483005;",
1034
+ "box-sizing:border-box;max-width:min(82vw,460px);padding:9px 14px;border-radius:10px;",
1035
+ "background:rgba(28,32,36,.95);color:#fff;font-size:12px;line-height:1.55;text-align:left;",
1036
+ "box-shadow:0 10px 28px rgba(0,0,0,.28);opacity:0;pointer-events:none;",
1037
+ "transition:opacity .16s ease,transform .16s ease}",
1038
+ '.dsh-sel-shot-toast[data-show="1"]{opacity:1;pointer-events:auto;transform:translate(-50%,0)}',
1039
+ '.dsh-sel-shot-toast[data-tone="error"]{background:rgba(153,27,27,.96)}',
959
1040
  ".dsh-sel-thread{position:fixed;left:0;top:0;width:100%;height:100%;pointer-events:none;",
960
1041
  "z-index:2147482998;overflow:visible;display:none;opacity:0;transition:opacity .14s ease}",
961
1042
  ".dsh-sel-thread path{fill:none;stroke-width:1.6;stroke-dasharray:4 4;",
@@ -2106,7 +2187,17 @@ function createSettingsController(env) {
2106
2187
  var settingsRailButtons = [];
2107
2188
  var settingsPanes = [];
2108
2189
  var settingsSegs = [];
2109
- var SLOT_LABEL = { off: "\u5173", low: "\u4F4E", high: "\u9AD8", max: "\u6700\u9AD8" };
2190
+ var SLOT_LABEL = {
2191
+ off: "\u5173",
2192
+ low: "\u4F4E",
2193
+ high: "\u9AD8",
2194
+ max: "\u6700\u9AD8",
2195
+ // 截图取景范围(与推理档位共用 seg 标签表;值互不重叠)
2196
+ screen: "\u5168\u5C4F",
2197
+ window: "\u6574\u7A97",
2198
+ margin: "\u9009\u533A+\u7559\u767D",
2199
+ selection: "\u4EC5\u9009\u533A"
2200
+ };
2110
2201
  var SLOT_ORDER = ["off", "low", "high", "max"];
2111
2202
  var settingsEffortHints = [];
2112
2203
  var settingsMenus = [];
@@ -3159,7 +3250,8 @@ function createVoiceCardController(env) {
3159
3250
  var vcardHandoff = null;
3160
3251
  var panelGrowTimer = null;
3161
3252
  var panelGrowHandler = null;
3162
- var quoteVoiceHandled = false;
3253
+ var quoteVoiceHandled;
3254
+ var voiceSeenSinceOpen = false;
3163
3255
  var selMark = null;
3164
3256
  var selThread = null;
3165
3257
  var selThreadPath = null;
@@ -3368,11 +3460,13 @@ function createVoiceCardController(env) {
3368
3460
  function openVCard() {
3369
3461
  if (vcardState.open || env.panelOpen) return;
3370
3462
  if (!env.state.selection) return;
3371
- if (!env.state.selection.range && env.state.selection.source !== "iframe") return;
3372
- if (!hasFocusEvidence(env.state.selection)) return;
3373
- if (env.isTypingTarget(document.activeElement)) return;
3463
+ if (!env.state.selection.range && env.state.selection.source !== "iframe" && env.state.selection.source !== "shot") return;
3464
+ if (env.state.selection.source !== "shot" && !hasFocusEvidence(env.state.selection)) return;
3465
+ if (env.state.selection.source !== "shot" && env.isTypingTarget(document.activeElement)) return;
3466
+ if (env.ensureLayer) env.ensureLayer();
3374
3467
  ensureVCard();
3375
3468
  if (vcardState.closing || vcardState.crtTimer) finishCrtOff();
3469
+ voiceSeenSinceOpen = false;
3376
3470
  vcardState.range = env.state.selection.range || null;
3377
3471
  vcardState.anchorRect = env.state.selection.rect || null;
3378
3472
  var geo = vcardGeometry();
@@ -3417,7 +3511,7 @@ function createVoiceCardController(env) {
3417
3511
  vcard.setAttribute("data-voiced", on ? "1" : "0");
3418
3512
  if (!vcardBox) return;
3419
3513
  if (String(vcardBox.value || "").trim()) return;
3420
- vcardBox.placeholder = on ? "\u6B63\u5728\u542C\u2026" : env.settingsLive.voiceCancelOnSilence !== false ? "\u6B63\u5728\u5173\u95ED\u2026" : "\u76F4\u63A5\u6253\u5B57\u4E5F\u884C\u2026";
3514
+ vcardBox.placeholder = on ? "\u6B63\u5728\u542C\u2026" : isShotSelection() ? "\u5C31\u8FD9\u5F20\u622A\u56FE\u63D0\u95EE\u2026" : env.settingsLive.voiceCancelOnSilence !== false ? "\u6B63\u5728\u5173\u95ED\u2026" : "\u76F4\u63A5\u6253\u5B57\u4E5F\u884C\u2026";
3421
3515
  }
3422
3516
  function focusVCard() {
3423
3517
  if (!vcardBox) return;
@@ -3650,14 +3744,22 @@ function createVoiceCardController(env) {
3650
3744
  vcardState.settleStableAt = 0;
3651
3745
  vcardState.settleDeadline = 0;
3652
3746
  }
3747
+ function isShotSelection() {
3748
+ var sel = env.state && env.state.selection;
3749
+ return !!(sel && sel.source === "shot");
3750
+ }
3653
3751
  function finishVoiceSettle(text) {
3654
3752
  if (!vcardState.open) return;
3655
3753
  var spoken = String(text || "").trim();
3656
- if (spoken && env.settingsLive.voiceAutoSend !== false) {
3754
+ if (spoken && voiceSeenSinceOpen && env.settingsLive.voiceAutoSend !== false) {
3657
3755
  sendVCard();
3658
3756
  return;
3659
3757
  }
3660
3758
  if (!spoken && env.settingsLive.voiceCancelOnSilence !== false) {
3759
+ if (isShotSelection()) {
3760
+ markVCardListening(false);
3761
+ return;
3762
+ }
3661
3763
  closeVCard(true);
3662
3764
  return;
3663
3765
  }
@@ -3687,6 +3789,7 @@ function createVoiceCardController(env) {
3687
3789
  }
3688
3790
  function closeVCard(animate) {
3689
3791
  if (!vcardState.open) return;
3792
+ if (isShotSelection() && env.clearShotMarkers) env.clearShotMarkers();
3690
3793
  vcardState.open = false;
3691
3794
  if (vcardBox && !String(vcardBox.value || "").trim()) vcardBox.placeholder = "\u6B63\u5728\u5173\u95ED\u2026";
3692
3795
  vcardState.range = null;
@@ -3714,6 +3817,36 @@ function createVoiceCardController(env) {
3714
3817
  closeVCard();
3715
3818
  return;
3716
3819
  }
3820
+ if (selection.source === "shot") {
3821
+ var shotRect = selection.rect;
3822
+ var openShotPanel = function(ref) {
3823
+ if (!env.state.selection || env.state.selection !== selection) return;
3824
+ var why = env.shotFailed ? env.shotFailed() : "";
3825
+ if (!ref && why) {
3826
+ if (env.shotNotice) {
3827
+ env.shotNotice(
3828
+ why === "upload-failed" ? "\u622A\u56FE\u4E0A\u4F20\u5931\u8D25\uFF0C\u53EF\u4EE5\u518D\u6309\u4E00\u6B21\u53D1\u9001\u91CD\u8BD5" : "\u622A\u56FE\u6CA1\u6709\u751F\u6210\u6210\u529F\uFF0C\u8FD9\u6761\u5148\u4E0D\u53D1\u9001\uFF1B\u8BF7\u6309 Esc \u540E\u91CD\u65B0\u622A\u56FE"
3829
+ );
3830
+ }
3831
+ return;
3832
+ }
3833
+ vcardHandoff = vcardRect();
3834
+ closeVCard();
3835
+ env.openPanelWith("", "", "\u622A\u56FE", shotRect, "", {
3836
+ askFirst: asked,
3837
+ shotRef: ref,
3838
+ // 取景范围要跟着图一起进小窗:提示词要按"图里有没有红框 / 图是不是只有选区"换口径
3839
+ shotScope: selection.shotScope || "",
3840
+ shotFrame: selection.shotFrame !== false
3841
+ });
3842
+ if (env.clearShotMarkers) env.clearShotMarkers();
3843
+ env.state.selection = null;
3844
+ env.state.bridgeActive = false;
3845
+ };
3846
+ if (env.shotRefAsync) env.shotRefAsync().then(openShotPanel);
3847
+ else openShotPanel(selection.ref || null);
3848
+ return;
3849
+ }
3717
3850
  var contextInfo = selection.source === "iframe" || !selection.range ? {
3718
3851
  context: String(selection.context || ""),
3719
3852
  keyContext: String(selection.keyContext || ""),
@@ -3794,6 +3927,7 @@ function createVoiceCardController(env) {
3794
3927
  return;
3795
3928
  }
3796
3929
  if (vcardState.suppress) return;
3930
+ voiceSeenSinceOpen = true;
3797
3931
  stopVoiceSettle();
3798
3932
  if (!vcardState.open) openVCard();
3799
3933
  else markVCardListening(true);
@@ -3815,7 +3949,7 @@ function createVoiceCardController(env) {
3815
3949
  var selection = env.state.quoteSelection;
3816
3950
  if (!env.panelOpen || !selection) return;
3817
3951
  if (!hasFocusEvidence(selection)) return;
3818
- if (env.isTypingTarget(document.activeElement)) return;
3952
+ if (env.state.selection.source !== "shot" && env.isTypingTarget(document.activeElement)) return;
3819
3953
  if (env.voice && env.voice.phase !== "idle") return;
3820
3954
  env.addQuote(selection.text, selection.label, env.quoteContextFor(selection));
3821
3955
  hideQuoteButton();
@@ -4168,6 +4302,20 @@ function createAttachController(env) {
4168
4302
  env.attachNote.textContent = env.state.attachNote || "";
4169
4303
  env.attachNote.setAttribute("data-show", env.state.attachNote ? "1" : "0");
4170
4304
  }
4305
+ function ensureUploaded() {
4306
+ var pending = items().filter(function(item) {
4307
+ return !item.ref && item.file;
4308
+ });
4309
+ if (pending.length === 0) return Promise.resolve();
4310
+ var chain = Promise.resolve();
4311
+ pending.forEach(function(item) {
4312
+ chain = chain.then(function() {
4313
+ item.started = false;
4314
+ return upload(item);
4315
+ });
4316
+ });
4317
+ return chain;
4318
+ }
4171
4319
  function busy() {
4172
4320
  var list = items();
4173
4321
  for (var i = 0; i < list.length; i += 1) if (list[i].status === "uploading") return true;
@@ -4244,15 +4392,6 @@ function createAttachController(env) {
4244
4392
  note(rejected.join("\uFF1B"));
4245
4393
  render();
4246
4394
  env.refreshAskState();
4247
- var pending = list.filter(function(item) {
4248
- return item.status === "uploading" && !item.started;
4249
- });
4250
- var chain = Promise.resolve();
4251
- pending.forEach(function(item) {
4252
- chain = chain.then(function() {
4253
- return upload(item);
4254
- });
4255
- });
4256
4395
  }
4257
4396
  function createItem(file) {
4258
4397
  var isImage = /^image\//.test(String(file.type || ""));
@@ -4265,8 +4404,10 @@ function createAttachController(env) {
4265
4404
  file,
4266
4405
  // 缩略图先用本地 object URL 显示(不必等上传完),失败时回落到图标
4267
4406
  url: isImage && typeof URL !== "undefined" && URL.createObjectURL ? URL.createObjectURL(file) : "",
4268
- status: "uploading",
4269
- started: false,
4407
+ // 'local' = 只在内存里,还没落盘。用户要求:叉掉的不该在附件库留垃圾,
4408
+ // 所以**点发送时才真正上传**(见 ensureUploaded)。
4409
+ status: "local",
4410
+ started: true,
4270
4411
  ref: null,
4271
4412
  error: ""
4272
4413
  };
@@ -4278,6 +4419,7 @@ function createAttachController(env) {
4278
4419
  item.status = "uploading";
4279
4420
  item.error = "";
4280
4421
  render();
4422
+ env.refreshAskState();
4281
4423
  return fileToBase64(item.file).then(function(data) {
4282
4424
  return fetch(ATTACH, {
4283
4425
  method: "POST",
@@ -4347,10 +4489,44 @@ function createAttachController(env) {
4347
4489
  });
4348
4490
  return button;
4349
4491
  }
4492
+ function previewItem(item) {
4493
+ if (!item || item.status === "error") return;
4494
+ var list = items();
4495
+ var images = [];
4496
+ var start = 0;
4497
+ for (var i = 0; i < list.length; i += 1) {
4498
+ var other = list[i];
4499
+ if (!other || other.kind !== "image" || other.status === "error") continue;
4500
+ var src = other.url || (other.ref ? attachmentUrl("image", other.ref) : "");
4501
+ if (!src) continue;
4502
+ if (other === item) start = images.length;
4503
+ images.push({ src, name: other.name });
4504
+ }
4505
+ if (images.length === 0 || typeof env.previewImage !== "function") return;
4506
+ env.previewImage({ images, index: start });
4507
+ }
4350
4508
  function thumbNode(item) {
4351
4509
  var node = el("span", "dsh-sel-thumb");
4352
4510
  node.setAttribute("data-state", item.status);
4353
- node.title = item.name;
4511
+ var previewable = item.status !== "error" && !!(item.url || item.ref);
4512
+ if (previewable) {
4513
+ node.setAttribute("data-preview", "1");
4514
+ node.setAttribute("role", "button");
4515
+ node.tabIndex = 0;
4516
+ node.title = item.name + "\uFF08\u70B9\u51FB\u9884\u89C8\uFF09";
4517
+ node.setAttribute("aria-label", "\u9884\u89C8\u56FE\u7247 " + item.name);
4518
+ listen(node, "click", function() {
4519
+ previewItem(item);
4520
+ });
4521
+ listen(node, "keydown", function(event) {
4522
+ if (event.target !== node) return;
4523
+ if (event.key !== "Enter" && event.key !== " ") return;
4524
+ event.preventDefault();
4525
+ previewItem(item);
4526
+ });
4527
+ } else {
4528
+ node.title = item.name;
4529
+ }
4354
4530
  if (item.url) {
4355
4531
  var img = document.createElement("img");
4356
4532
  img.src = item.url;
@@ -4481,6 +4657,7 @@ function createAttachController(env) {
4481
4657
  retry,
4482
4658
  render,
4483
4659
  busy,
4660
+ ensureUploaded,
4484
4661
  payload,
4485
4662
  describe,
4486
4663
  note,
@@ -4490,6 +4667,891 @@ function createAttachController(env) {
4490
4667
  };
4491
4668
  }
4492
4669
 
4670
+ // src/client/lightbox.js
4671
+ var SVG_NS = "http://www.w3.org/2000/svg";
4672
+ function createLightboxController(env) {
4673
+ var root = null;
4674
+ var stage = null;
4675
+ var img = null;
4676
+ var nameEl = null;
4677
+ var failEl = null;
4678
+ var closeBtn = null;
4679
+ var prevBtn = null;
4680
+ var nextBtn = null;
4681
+ var countEl = null;
4682
+ var hintEl = null;
4683
+ var built = false;
4684
+ var open = false;
4685
+ var gallery = [];
4686
+ var index = 0;
4687
+ var lastFocus = null;
4688
+ function within(node, element) {
4689
+ if (!node || !element) return false;
4690
+ if (node === element) return true;
4691
+ if (typeof element.contains !== "function") return false;
4692
+ try {
4693
+ return element.contains(
4694
+ /** @type {Node} */
4695
+ node
4696
+ );
4697
+ } catch (error) {
4698
+ return false;
4699
+ }
4700
+ }
4701
+ function chevron(dir) {
4702
+ var svg = document.createElementNS(SVG_NS, "svg");
4703
+ svg.setAttribute("viewBox", "0 0 16 16");
4704
+ svg.setAttribute("width", "16");
4705
+ svg.setAttribute("height", "16");
4706
+ svg.setAttribute("fill", "none");
4707
+ svg.setAttribute("stroke", "currentColor");
4708
+ svg.setAttribute("stroke-width", "1.9");
4709
+ svg.setAttribute("stroke-linecap", "round");
4710
+ svg.setAttribute("stroke-linejoin", "round");
4711
+ svg.setAttribute("aria-hidden", "true");
4712
+ var path = document.createElementNS(SVG_NS, "path");
4713
+ path.setAttribute("d", dir === "prev" ? "M10 3.5L5.5 8l4.5 4.5" : "M6 3.5L10.5 8 6 12.5");
4714
+ svg.appendChild(path);
4715
+ return svg;
4716
+ }
4717
+ function navButton(dir) {
4718
+ var button = el("button", "dsh-sel-lightbox-nav");
4719
+ button.type = "button";
4720
+ button.setAttribute("data-dir", dir);
4721
+ var label = dir === "prev" ? "\u4E0A\u4E00\u5F20\u56FE\u7247\uFF08\u2190\uFF09" : "\u4E0B\u4E00\u5F20\u56FE\u7247\uFF08\u2192\uFF09";
4722
+ button.title = label;
4723
+ button.setAttribute("aria-label", label);
4724
+ button.appendChild(chevron(dir));
4725
+ listen(button, "click", function() {
4726
+ step(dir === "prev" ? -1 : 1);
4727
+ });
4728
+ return button;
4729
+ }
4730
+ function build() {
4731
+ if (built) return;
4732
+ built = true;
4733
+ root = el("div", "dsh-sel-lightbox");
4734
+ root.setAttribute("data-open", "0");
4735
+ root.setAttribute("data-error", "0");
4736
+ root.setAttribute("data-gallery", "0");
4737
+ root.setAttribute("role", "dialog");
4738
+ root.setAttribute("aria-label", "\u56FE\u7247\u9884\u89C8");
4739
+ var bar = el("div", "dsh-sel-lightbox-bar");
4740
+ nameEl = el("span", "dsh-sel-lightbox-name");
4741
+ countEl = el("span", "dsh-sel-lightbox-count");
4742
+ closeBtn = el("button", "dsh-sel-lightbox-x");
4743
+ closeBtn.type = "button";
4744
+ closeBtn.title = "\u5173\u95ED\u9884\u89C8\uFF08Esc\uFF09";
4745
+ closeBtn.setAttribute("aria-label", "\u5173\u95ED\u56FE\u7247\u9884\u89C8");
4746
+ closeBtn.appendChild(closeIcon());
4747
+ listen(closeBtn, "click", function() {
4748
+ close();
4749
+ });
4750
+ bar.appendChild(nameEl);
4751
+ bar.appendChild(countEl);
4752
+ bar.appendChild(closeBtn);
4753
+ stage = el("div", "dsh-sel-lightbox-stage");
4754
+ img = document.createElement("img");
4755
+ img.className = "dsh-sel-lightbox-img";
4756
+ img.alt = "";
4757
+ img.decoding = "async";
4758
+ img.draggable = false;
4759
+ img.addEventListener("error", function() {
4760
+ if (!open) return;
4761
+ root.setAttribute("data-error", "1");
4762
+ failEl.textContent = "\u8FD9\u5F20\u56FE\u6253\u4E0D\u5F00\u4E86\uFF08\u9644\u4EF6\u53EF\u80FD\u5DF2\u88AB\u6E05\u7406\uFF09";
4763
+ });
4764
+ stage.appendChild(img);
4765
+ failEl = el("p", "dsh-sel-lightbox-fail");
4766
+ prevBtn = navButton("prev");
4767
+ nextBtn = navButton("next");
4768
+ hintEl = el("p", "dsh-sel-lightbox-hint");
4769
+ root.appendChild(bar);
4770
+ root.appendChild(stage);
4771
+ root.appendChild(failEl);
4772
+ root.appendChild(prevBtn);
4773
+ root.appendChild(nextBtn);
4774
+ root.appendChild(hintEl);
4775
+ listen(root, "click", function(event) {
4776
+ if (!open) return;
4777
+ var target = event.target;
4778
+ if (target === img) return;
4779
+ if (within(target, bar) || within(target, prevBtn) || within(target, nextBtn)) return;
4780
+ close();
4781
+ });
4782
+ listen(root, "keydown", function(event) {
4783
+ if (!open || event.key !== "Tab") return;
4784
+ var order = [prevBtn, nextBtn, closeBtn].filter(function(button) {
4785
+ return !button.disabled;
4786
+ });
4787
+ var current = order.indexOf(document.activeElement);
4788
+ var next = event.shiftKey ? current - 1 : current + 1;
4789
+ if (next < 0) next = order.length - 1;
4790
+ if (next >= order.length) next = 0;
4791
+ event.preventDefault();
4792
+ if (order[next] && typeof order[next].focus === "function") order[next].focus();
4793
+ });
4794
+ env.layer.appendChild(root);
4795
+ }
4796
+ function paintImage() {
4797
+ var item = gallery[index] || { src: "", name: "\u56FE\u7247" };
4798
+ var many = gallery.length > 1;
4799
+ root.setAttribute("data-error", "0");
4800
+ root.setAttribute("data-gallery", many ? "1" : "0");
4801
+ failEl.textContent = "";
4802
+ nameEl.textContent = item.name || "\u56FE\u7247";
4803
+ img.alt = nameEl.textContent;
4804
+ img.src = item.src;
4805
+ countEl.textContent = many ? index + 1 + " / " + gallery.length : "";
4806
+ prevBtn.disabled = !many || index === 0;
4807
+ nextBtn.disabled = !many || index === gallery.length - 1;
4808
+ hintEl.textContent = many ? "\u2190 / \u2192 \u5207\u6362 \xB7 Esc \u6216\u70B9\u51FB\u7A7A\u767D\u5904\u5173\u95ED" : "Esc \u6216\u70B9\u51FB\u7A7A\u767D\u5904\u5173\u95ED";
4809
+ root.setAttribute("aria-label", "\u56FE\u7247\u9884\u89C8" + (many ? "\uFF08" + (index + 1) + " / " + gallery.length + "\uFF09" : ""));
4810
+ }
4811
+ function step(delta) {
4812
+ if (!open || gallery.length < 2) return;
4813
+ var next = index + delta;
4814
+ if (next < 0 || next >= gallery.length) return;
4815
+ index = next;
4816
+ paintImage();
4817
+ }
4818
+ function normalize(items, fallback) {
4819
+ var list = [];
4820
+ if (items && items.length) {
4821
+ for (var i = 0; i < items.length; i += 1) {
4822
+ var item = items[i] || {};
4823
+ if (!item.src) continue;
4824
+ list.push({ src: String(item.src), name: item.name ? String(item.name) : "\u56FE\u7247" });
4825
+ }
4826
+ }
4827
+ if (list.length === 0 && fallback && fallback.src) {
4828
+ list.push({ src: String(fallback.src), name: fallback.name ? String(fallback.name) : "\u56FE\u7247" });
4829
+ }
4830
+ return list;
4831
+ }
4832
+ function openImage(options) {
4833
+ build();
4834
+ var given = options && Array.isArray(options.images) ? options.images : null;
4835
+ var list = normalize(given, options);
4836
+ if (list.length === 0) return;
4837
+ var start = options && typeof options.index === "number" ? Math.floor(options.index) : 0;
4838
+ if (!(start >= 0 && start < list.length)) start = 0;
4839
+ gallery = list;
4840
+ index = start;
4841
+ lastFocus = document.activeElement;
4842
+ open = true;
4843
+ root.setAttribute("data-open", "1");
4844
+ root.setAttribute("aria-modal", "true");
4845
+ paintImage();
4846
+ if (typeof closeBtn.focus === "function") closeBtn.focus();
4847
+ }
4848
+ function close() {
4849
+ if (!open || !built) return;
4850
+ open = false;
4851
+ gallery = [];
4852
+ index = 0;
4853
+ root.setAttribute("data-open", "0");
4854
+ root.setAttribute("data-error", "0");
4855
+ root.setAttribute("data-gallery", "0");
4856
+ root.removeAttribute("aria-modal");
4857
+ img.removeAttribute("src");
4858
+ img.alt = "";
4859
+ countEl.textContent = "";
4860
+ prevBtn.disabled = true;
4861
+ nextBtn.disabled = true;
4862
+ var back = lastFocus;
4863
+ lastFocus = null;
4864
+ if (back && back.isConnected !== false && typeof back.focus === "function") {
4865
+ try {
4866
+ back.focus();
4867
+ } catch (error) {
4868
+ }
4869
+ }
4870
+ }
4871
+ function foreignModalOpen() {
4872
+ var modals = document.querySelectorAll('[role="dialog"][aria-modal="true"]');
4873
+ for (var i = 0; i < modals.length; i += 1) if (modals[i] !== root) return true;
4874
+ return false;
4875
+ }
4876
+ var offKey = listen(document, "keydown", function(event) {
4877
+ if (!open) return;
4878
+ if (event.defaultPrevented || event.isComposing || event.keyCode === 229) return;
4879
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
4880
+ if (gallery.length < 2 || foreignModalOpen()) return;
4881
+ event.preventDefault();
4882
+ event.stopPropagation();
4883
+ step(event.key === "ArrowLeft" ? -1 : 1);
4884
+ return;
4885
+ }
4886
+ if (event.key !== "Escape") return;
4887
+ if (foreignModalOpen()) return;
4888
+ event.preventDefault();
4889
+ event.stopPropagation();
4890
+ close();
4891
+ }, true);
4892
+ return {
4893
+ open: openImage,
4894
+ close,
4895
+ get isOpen() {
4896
+ return open;
4897
+ },
4898
+ /** 卸载时摘掉 document 上的监听(灯箱 DOM 随 layer 一起销毁)。 */
4899
+ dispose: function() {
4900
+ close();
4901
+ offKey();
4902
+ }
4903
+ };
4904
+ }
4905
+
4906
+ // src/client/shot.js
4907
+ var MAX_ANNOTATED = 2048;
4908
+ var FRAME_COLOR = "#e5484d";
4909
+ var FRAME_WIDTH_MIN_PX = 3;
4910
+ var SHOT_INSIDE_SLOP = 2;
4911
+ function pageRectOf(rect, origin) {
4912
+ if (!rect) return null;
4913
+ var sx = origin && typeof origin.screenX === "number" ? origin.screenX : 0;
4914
+ var sy = origin && typeof origin.screenY === "number" ? origin.screenY : 0;
4915
+ return asRect({
4916
+ left: rect.x - sx,
4917
+ top: rect.y - sy,
4918
+ width: rect.width,
4919
+ height: rect.height
4920
+ });
4921
+ }
4922
+ function asRect(box) {
4923
+ var left = typeof box.left === "number" ? box.left : 0;
4924
+ var top = typeof box.top === "number" ? box.top : 0;
4925
+ var width = typeof box.width === "number" ? box.width : 0;
4926
+ var height = typeof box.height === "number" ? box.height : 0;
4927
+ return { left, top, width, height, right: left + width, bottom: top + height, x: left, y: top };
4928
+ }
4929
+ function insideViewport(rect, viewport, slop) {
4930
+ if (!rect || !viewport) return false;
4931
+ var pad = typeof slop === "number" ? slop : SHOT_INSIDE_SLOP;
4932
+ return rect.left >= -pad && rect.top >= -pad && rect.left + rect.width <= viewport.width + pad && rect.top + rect.height <= viewport.height + pad;
4933
+ }
4934
+ function nearestEdge(rect, viewport) {
4935
+ var cx = rect.left + rect.width / 2;
4936
+ var cy = rect.top + rect.height / 2;
4937
+ var d = { left: Math.abs(cx), right: Math.abs(viewport.width - cx), top: Math.abs(cy), bottom: Math.abs(viewport.height - cy) };
4938
+ var best = "left";
4939
+ var bestValue = Infinity;
4940
+ for (var key in d) {
4941
+ if (!Object.prototype.hasOwnProperty.call(d, key)) continue;
4942
+ if (d[key] < bestValue) {
4943
+ bestValue = d[key];
4944
+ best = key;
4945
+ }
4946
+ }
4947
+ return best;
4948
+ }
4949
+ function chipRectFor(edge, anchorRect, viewport, size) {
4950
+ var w = size && size.width || 158;
4951
+ var h = size && size.height || 48;
4952
+ var pad = 12;
4953
+ var cx = anchorRect.left + anchorRect.width / 2;
4954
+ var cy = anchorRect.top + anchorRect.height / 2;
4955
+ var left;
4956
+ var top;
4957
+ if (edge === "left" || edge === "right") {
4958
+ left = edge === "left" ? pad : viewport.width - w - pad;
4959
+ top = Math.max(pad, Math.min(cy - h / 2, viewport.height - h - pad));
4960
+ } else {
4961
+ top = edge === "top" ? pad : viewport.height - h - pad;
4962
+ left = Math.max(pad, Math.min(cx - w / 2, viewport.width - w - pad));
4963
+ }
4964
+ return { left, top, width: w, height: h };
4965
+ }
4966
+ function findContentBox(g, width, height) {
4967
+ var probe = Math.min(400, width, height);
4968
+ function hasOpaque(data, count, stride, offset) {
4969
+ for (var i = 0; i < count; i += 1) {
4970
+ if (data[i * stride + offset + 3] > 128) return true;
4971
+ }
4972
+ return false;
4973
+ }
4974
+ var box = { left: 0, top: 0, right: width, bottom: height };
4975
+ var strip = g.getImageData(0, 0, width, probe);
4976
+ for (var y = 0; y < probe; y += 1) {
4977
+ if (hasOpaque(strip.data.subarray(y * width * 4), width, 4, 0)) {
4978
+ box.top = y;
4979
+ break;
4980
+ }
4981
+ }
4982
+ strip = g.getImageData(0, Math.max(0, height - probe), width, probe);
4983
+ for (var y2 = probe - 1; y2 >= 0; y2 -= 1) {
4984
+ if (hasOpaque(strip.data.subarray(y2 * width * 4), width, 4, 0)) {
4985
+ box.bottom = Math.max(0, height - probe) + y2 + 1;
4986
+ break;
4987
+ }
4988
+ }
4989
+ strip = g.getImageData(0, 0, probe, height);
4990
+ for (var x = 0; x < probe; x += 1) {
4991
+ if (hasOpaque(strip.data.subarray(x * 4), height, probe * 4, 0)) {
4992
+ box.left = x;
4993
+ break;
4994
+ }
4995
+ }
4996
+ strip = g.getImageData(Math.max(0, width - probe), 0, probe, height);
4997
+ for (var x2 = probe - 1; x2 >= 0; x2 -= 1) {
4998
+ if (hasOpaque(strip.data.subarray(x2 * 4), height, probe * 4, 0)) {
4999
+ box.right = Math.max(0, width - probe) + x2 + 1;
5000
+ break;
5001
+ }
5002
+ }
5003
+ if (box.right - box.left < 16 || box.bottom - box.top < 16) return null;
5004
+ return box;
5005
+ }
5006
+ function annotate(image, rect, scale, shot) {
5007
+ var iw = image.naturalWidth || image.width;
5008
+ var ih = image.naturalHeight || image.height;
5009
+ var k = iw > MAX_ANNOTATED ? MAX_ANNOTATED / iw : 1;
5010
+ var canvas = document.createElement("canvas");
5011
+ canvas.width = Math.max(1, Math.round(iw * k));
5012
+ canvas.height = Math.max(1, Math.round(ih * k));
5013
+ var g = canvas.getContext("2d");
5014
+ var needContentBox = !shot || !shot.scope || shot.scope === "window";
5015
+ var content = null;
5016
+ if (needContentBox) {
5017
+ g.imageSmoothingEnabled = false;
5018
+ g.drawImage(image, 0, 0, canvas.width, canvas.height);
5019
+ try {
5020
+ content = findContentBox(g, canvas.width, canvas.height);
5021
+ } catch (error) {
5022
+ content = null;
5023
+ }
5024
+ g.clearRect(0, 0, canvas.width, canvas.height);
5025
+ }
5026
+ g.imageSmoothingEnabled = true;
5027
+ g.drawImage(image, 0, 0, canvas.width, canvas.height);
5028
+ if (content && shot && shot.windowRect && shot.selectionRect) {
5029
+ var win = shot.windowRect;
5030
+ var sel = shot.selectionRect;
5031
+ var left = content.left / k;
5032
+ var top = content.top / k;
5033
+ var right = content.right / k;
5034
+ var realScale = (right - left) / win.width;
5035
+ if (realScale > 0.2 && realScale < 5) {
5036
+ rect = {
5037
+ x: left + (sel.x - win.x) * realScale,
5038
+ y: top + (sel.y - win.y) * realScale,
5039
+ width: sel.width * realScale,
5040
+ height: sel.height * realScale
5041
+ };
5042
+ scale = realScale;
5043
+ }
5044
+ }
5045
+ rect = { x: rect.x * k, y: rect.y * k, width: rect.width * k, height: rect.height * k };
5046
+ var line = Math.max(FRAME_WIDTH_MIN_PX, Math.round(3 * (scale || 1) * k));
5047
+ g.strokeStyle = FRAME_COLOR;
5048
+ g.lineWidth = line;
5049
+ var inset = line / 2;
5050
+ g.strokeRect(rect.x + inset, rect.y + inset, Math.max(0, rect.width - line), Math.max(0, rect.height - line));
5051
+ return new Promise(function(resolve) {
5052
+ canvas.toBlob(function(blob) {
5053
+ resolve(blob);
5054
+ }, "image/png");
5055
+ });
5056
+ }
5057
+ function report(payload) {
5058
+ try {
5059
+ fetch("/selection-explain/api/shot/hello", { method: "POST", body: JSON.stringify(payload) }).catch(function() {
5060
+ });
5061
+ } catch (error) {
5062
+ }
5063
+ }
5064
+ function createShotController(env) {
5065
+ var host = env.window || (typeof window !== "undefined" ? window : null) || {};
5066
+ var myEpoch = 0;
5067
+ try {
5068
+ host.__dshSelShotEpoch = (host.__dshSelShotEpoch || 0) + 1;
5069
+ myEpoch = host.__dshSelShotEpoch;
5070
+ } catch (error) {
5071
+ myEpoch = 1;
5072
+ }
5073
+ function superseded() {
5074
+ try {
5075
+ return host.__dshSelShotEpoch !== myEpoch;
5076
+ } catch (error) {
5077
+ return false;
5078
+ }
5079
+ }
5080
+ var timer = 0;
5081
+ var busy = false;
5082
+ var lastId = "";
5083
+ var current = null;
5084
+ var frameEl = null;
5085
+ var chipEl = null;
5086
+ var chipImg = null;
5087
+ var chipUrl = "";
5088
+ var toastEl = null;
5089
+ var toastTimer = 0;
5090
+ function errorText(info) {
5091
+ var code = info && (info.code || info.reason) ? String(info.code || info.reason) : "";
5092
+ if (info && typeof info.message === "string" && info.message.trim()) return info.message;
5093
+ if (code === "not-macos") return "\u622A\u56FE\u529F\u80FD\u76EE\u524D\u4EC5\u652F\u6301 macOS";
5094
+ if (code === "no-screen-permission") return "\u6CA1\u6709\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1A\u8BF7\u5728 \u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u5C4F\u5E55\u5F55\u5236 \u4E2D\u5141\u8BB8 DSH\uFF0C\u7136\u540E\u91CD\u8BD5";
5095
+ if (code === "busy") return "\u6B63\u5728\u622A\u56FE\uFF0C\u8BF7\u7A0D\u5019";
5096
+ if (code === "probe-missing") return "\u622A\u56FE\u7EC4\u4EF6\u7F3A\u5931\uFF0C\u8BF7\u91CD\u65B0\u6784\u5EFA\u63D2\u4EF6\u540E\u91CD\u8BD5";
5097
+ if (code === "spawn-failed") return "\u7CFB\u7EDF\u622A\u56FE\u547D\u4EE4\u542F\u52A8\u5931\u8D25";
5098
+ if (code === "timeout") return "\u6846\u9009\u8D85\u65F6\uFF0C\u5DF2\u53D6\u6D88\u8FD9\u6B21\u622A\u56FE";
5099
+ if (code === "capture-failed") return "\u622A\u56FE\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u5C4F\u5E55\u5F55\u5236\u6743\u9650\u540E\u91CD\u8BD5";
5100
+ if (code === "image-missing" || code === "finish-failed") return "\u622A\u56FE\u5DF2\u62CD\u4E0B\uFF0C\u4F46\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5";
5101
+ if (code.indexOf("tmp-failed") === 0) return "\u622A\u56FE\u4E34\u65F6\u76EE\u5F55\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u8BD5";
5102
+ if (code.indexOf("start-failed") === 0) return "\u622A\u56FE\u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5";
5103
+ return "\u622A\u56FE\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5";
5104
+ }
5105
+ function ensureToast() {
5106
+ if (toastEl) {
5107
+ if (env.layer && toastEl.parentNode !== env.layer) env.layer.appendChild(toastEl);
5108
+ return toastEl;
5109
+ }
5110
+ var d = doc();
5111
+ toastEl = d.createElement("div");
5112
+ toastEl.className = "dsh-sel-shot-toast";
5113
+ toastEl.setAttribute("data-show", "0");
5114
+ toastEl.setAttribute("role", "status");
5115
+ toastEl.setAttribute("aria-live", "polite");
5116
+ toastEl.addEventListener("click", function() {
5117
+ hideNotice();
5118
+ });
5119
+ env.layer.appendChild(toastEl);
5120
+ return toastEl;
5121
+ }
5122
+ function notice(text, tone) {
5123
+ if (!text) return;
5124
+ if (env.ensureLayer) env.ensureLayer();
5125
+ var el2 = ensureToast();
5126
+ el2.textContent = String(text);
5127
+ el2.setAttribute("data-tone", tone || "error");
5128
+ el2.setAttribute("data-show", "1");
5129
+ if (toastTimer) clearTimeout(toastTimer);
5130
+ toastTimer = setTimeout(function() {
5131
+ toastTimer = 0;
5132
+ hideNotice();
5133
+ }, 4500);
5134
+ }
5135
+ function hideNotice() {
5136
+ if (toastTimer) {
5137
+ clearTimeout(toastTimer);
5138
+ toastTimer = 0;
5139
+ }
5140
+ if (toastEl) toastEl.setAttribute("data-show", "0");
5141
+ }
5142
+ function doc() {
5143
+ return env.document;
5144
+ }
5145
+ function viewport() {
5146
+ var win = env.window || window;
5147
+ return { width: win && win.innerWidth || 1200, height: win && win.innerHeight || 800 };
5148
+ }
5149
+ function origin() {
5150
+ var win = env.window || window;
5151
+ return { screenX: win && win.screenX ? win.screenX : 0, screenY: win && win.screenY ? win.screenY : 0 };
5152
+ }
5153
+ function ensureFrame() {
5154
+ if (frameEl) return frameEl;
5155
+ var d = doc();
5156
+ frameEl = d.createElement("div");
5157
+ frameEl.className = "dsh-sel-shotframe";
5158
+ frameEl.setAttribute("data-show", "0");
5159
+ var tag = d.createElement("span");
5160
+ tag.className = "dsh-sel-shotframe-tag";
5161
+ tag.textContent = "\u5DF2\u9009\u4E2D\u7684\u533A\u57DF";
5162
+ frameEl.appendChild(tag);
5163
+ env.layer.appendChild(frameEl);
5164
+ return frameEl;
5165
+ }
5166
+ function ensureChip() {
5167
+ if (chipEl) return chipEl;
5168
+ var d = doc();
5169
+ chipEl = d.createElement("div");
5170
+ chipEl.className = "dsh-sel-shotchip";
5171
+ chipEl.setAttribute("data-show", "0");
5172
+ chipImg = d.createElement("img");
5173
+ chipImg.alt = "\u622A\u56FE";
5174
+ chipEl.appendChild(chipImg);
5175
+ var tx = d.createElement("span");
5176
+ tx.className = "dsh-sel-shotchip-tx";
5177
+ var b = d.createElement("b");
5178
+ b.textContent = "\u6574\u7A97 + \u7EA2\u6846";
5179
+ var meta = d.createElement("span");
5180
+ meta.className = "dsh-sel-shotchip-meta";
5181
+ tx.appendChild(b);
5182
+ tx.appendChild(meta);
5183
+ chipEl.appendChild(tx);
5184
+ env.layer.appendChild(chipEl);
5185
+ return chipEl;
5186
+ }
5187
+ function scopeText(shot) {
5188
+ var scope = shot && shot.scope;
5189
+ if (!shot || shot.frame === false || scope === "selection") return "\u4EC5\u9009\u533A";
5190
+ if (scope === "screen") return "\u5168\u5C4F + \u7EA2\u6846";
5191
+ if (scope === "margin") return "\u9009\u533A + \u7559\u767D";
5192
+ return "\u6574\u7A97 + \u7EA2\u6846";
5193
+ }
5194
+ function hideMarkers() {
5195
+ if (frameEl) frameEl.setAttribute("data-show", "0");
5196
+ if (chipEl) chipEl.setAttribute("data-show", "0");
5197
+ }
5198
+ function clearAll() {
5199
+ hideNotice();
5200
+ hideMarkers();
5201
+ if (chipUrl) {
5202
+ URL.revokeObjectURL(chipUrl);
5203
+ chipUrl = "";
5204
+ }
5205
+ if (chipImg) chipImg.removeAttribute("src");
5206
+ current = null;
5207
+ if (env.state.selection && env.state.selection.source === "shot") env.state.selection = null;
5208
+ if (env.vcardOpen && env.vcardOpen() && env.closeVCard) env.closeVCard(true);
5209
+ }
5210
+ var cancelBound = false;
5211
+ function bindCancel() {
5212
+ if (cancelBound) return;
5213
+ var d = doc();
5214
+ if (!d || !d.addEventListener) return;
5215
+ cancelBound = true;
5216
+ var showing = function() {
5217
+ return !!current || frameEl && frameEl.getAttribute("data-show") === "1" || chipEl && chipEl.getAttribute("data-show") === "1" || toastEl && toastEl.getAttribute("data-show") === "1";
5218
+ };
5219
+ d.addEventListener("keydown", function(event) {
5220
+ if (event.key === "Escape" && showing()) clearAll();
5221
+ }, true);
5222
+ d.addEventListener("pointerdown", function(event) {
5223
+ if (!showing()) return;
5224
+ var target = event.target;
5225
+ var card = env.vcardElement && env.vcardElement();
5226
+ if (card && target && card.contains && card.contains(target)) return;
5227
+ if (chipEl && target && chipEl.contains && chipEl.contains(target)) return;
5228
+ if (toastEl && target && toastEl.contains && toastEl.contains(target)) return;
5229
+ clearAll();
5230
+ }, true);
5231
+ }
5232
+ function startCapture() {
5233
+ clearAll();
5234
+ var mode = env.panelOpen ? "panel" : "card";
5235
+ fetch(SHOT_START + "?mode=" + mode, { method: "POST", body: "" }).then(function(response) {
5236
+ if (!response.ok) throw new Error("HTTP " + response.status);
5237
+ return response.json();
5238
+ }).then(function(info) {
5239
+ if (!info || info.ok !== true) notice(errorText(info));
5240
+ }).catch(function() {
5241
+ notice("\u622A\u56FE\u670D\u52A1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
5242
+ });
5243
+ }
5244
+ async function handle(shot) {
5245
+ if (!shot || shot.id === lastId) return;
5246
+ lastId = shot.id;
5247
+ var t0 = Date.now();
5248
+ current = { shot, file: null, blob: null, ref: null, refReady: null, uploading: null, error: "", t0 };
5249
+ var mine = current;
5250
+ var step = "geometry";
5251
+ try {
5252
+ if (env.ensureLayer) env.ensureLayer();
5253
+ var page = pageRectOf(shot.selectionRect, origin());
5254
+ var view = viewport();
5255
+ var inside = !!(page && insideViewport(page, view));
5256
+ var anchor = null;
5257
+ var panelMode = !!env.panelOpen && (!shot.mode || shot.mode === "panel");
5258
+ if (panelMode) {
5259
+ if (frameEl) frameEl.setAttribute("data-show", "0");
5260
+ if (chipEl) chipEl.setAttribute("data-show", "0");
5261
+ } else if (inside) {
5262
+ var frame = ensureFrame();
5263
+ frame.style.left = Math.round(page.left) + "px";
5264
+ frame.style.top = Math.round(page.top) + "px";
5265
+ frame.style.width = Math.round(page.width) + "px";
5266
+ frame.style.height = Math.round(page.height) + "px";
5267
+ frame.setAttribute("data-show", "1");
5268
+ if (chipEl) chipEl.setAttribute("data-show", "0");
5269
+ anchor = page;
5270
+ } else {
5271
+ var chip = ensureChip();
5272
+ var edge = nearestEdge(page || { left: 0, top: 0, width: 0, height: 0 }, view);
5273
+ var box = chipRectFor(edge, page || { left: 0, top: 0, width: 0, height: 0 }, view);
5274
+ chip.style.left = Math.round(box.left) + "px";
5275
+ chip.style.top = Math.round(box.top) + "px";
5276
+ var chipTitle = chip.querySelector(".dsh-sel-shotchip-tx b");
5277
+ if (chipTitle) chipTitle.textContent = scopeText(shot);
5278
+ var meta = chip.querySelector(".dsh-sel-shotchip-meta");
5279
+ if (meta) meta.textContent = Math.round(shot.selectionRect.width) + " \xD7 " + Math.round(shot.selectionRect.height);
5280
+ chip.setAttribute("data-show", "1");
5281
+ if (frameEl) frameEl.setAttribute("data-show", "0");
5282
+ anchor = asRect(box);
5283
+ }
5284
+ var md0 = Date.now();
5285
+ step = "annotate";
5286
+ var imageUrl = SHOT_IMAGE + "?id=" + encodeURIComponent(shot.id);
5287
+ var applyBlob = function(blob) {
5288
+ if (!blob) return null;
5289
+ if (current !== mine) return null;
5290
+ mine.blob = blob;
5291
+ mine.file = new File([blob], "\u622A\u56FE.png", { type: blob.type || "image/png" });
5292
+ mine.error = "";
5293
+ if (chipImg) {
5294
+ if (chipUrl) URL.revokeObjectURL(chipUrl);
5295
+ chipUrl = URL.createObjectURL(blob);
5296
+ chipImg.src = chipUrl;
5297
+ }
5298
+ report({ stage: "bg-done", ms: Date.now() - md0, frame: shot.frame !== false, scope: shot.scope || "" });
5299
+ return null;
5300
+ };
5301
+ var markWorkFailed = function(message, error) {
5302
+ if (current === mine) {
5303
+ mine.error = "image-failed";
5304
+ notice(message);
5305
+ }
5306
+ report({ stage: "bg-fail", ms: Date.now() - md0, error: String(error && error.message || error) });
5307
+ return null;
5308
+ };
5309
+ var startWork = function() {
5310
+ if (shot.frame === false) {
5311
+ return fetch(imageUrl, { cache: "no-store" }).then(function(response) {
5312
+ if (!response.ok) throw new Error("HTTP " + response.status);
5313
+ return response.blob();
5314
+ }).then(applyBlob).catch(function(error) {
5315
+ return markWorkFailed("\u622A\u56FE\u5DF2\u62CD\u4E0B\uFF0C\u4F46\u8BFB\u53D6\u56FE\u7247\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5", error);
5316
+ });
5317
+ }
5318
+ return loadImage(imageUrl).then(function(img) {
5319
+ return annotate(img, shot.rect, shot.scale, shot);
5320
+ }).then(applyBlob).catch(function(error) {
5321
+ return markWorkFailed("\u622A\u56FE\u5DF2\u62CD\u4E0B\uFF0C\u4F46\u5904\u7406\u56FE\u7247\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5", error);
5322
+ });
5323
+ };
5324
+ current.refReady = new Promise(function(resolve) {
5325
+ var kick = function() {
5326
+ resolve(startWork());
5327
+ };
5328
+ if (panelMode) {
5329
+ kick();
5330
+ return;
5331
+ }
5332
+ if (shot.frame === false) {
5333
+ kick();
5334
+ return;
5335
+ }
5336
+ if (typeof window !== "undefined" && window.requestIdleCallback) {
5337
+ setTimeout(function() {
5338
+ window.requestIdleCallback(kick, { timeout: 1200 });
5339
+ }, 350);
5340
+ } else {
5341
+ setTimeout(kick, 350);
5342
+ }
5343
+ });
5344
+ if (panelMode) {
5345
+ var ready = current.refReady;
5346
+ if (ready) ready.then(function() {
5347
+ if (current !== mine) return;
5348
+ if (mine.file) env.addFiles([mine.file]);
5349
+ else if (!mine.error) notice("\u622A\u56FE\u6CA1\u6709\u751F\u6210\u6210\u529F\uFF0C\u8BF7\u91CD\u8BD5");
5350
+ if (env.focusAskBox) env.focusAskBox();
5351
+ });
5352
+ report({ stage: "done", branch: "panel", scope: shot.scope || "", ms: Date.now() - t0 });
5353
+ return;
5354
+ }
5355
+ env.state.selection = {
5356
+ source: "shot",
5357
+ text: "",
5358
+ label: "\u622A\u56FE",
5359
+ context: "",
5360
+ keyContext: "",
5361
+ range: null,
5362
+ rect: anchor,
5363
+ shot: current,
5364
+ ref: null,
5365
+ // 后续提示词要按取景范围换口径(仅选区没有红框,留白/全屏/整窗口径不同)
5366
+ shotScope: shot.scope || "",
5367
+ shotFrame: shot.frame !== false
5368
+ };
5369
+ step = "openVCard";
5370
+ env.openVCard();
5371
+ var opened = !env.vcardOpen || env.vcardOpen();
5372
+ report({ stage: "done", branch: inside ? "frame" : "chip", opened, ms: Date.now() - t0 });
5373
+ setTimeout(function() {
5374
+ try {
5375
+ var card = env.vcardElement && env.vcardElement();
5376
+ var cinfo = null;
5377
+ if (card) {
5378
+ var cr = card.getBoundingClientRect();
5379
+ var cs = getComputedStyle(card);
5380
+ cinfo = { rect: [Math.round(cr.left), Math.round(cr.top), Math.round(cr.width), Math.round(cr.height)], display: cs.display, opacity: cs.opacity, z: cs.zIndex };
5381
+ }
5382
+ var layerEl = env.layer;
5383
+ report({
5384
+ stage: "probe",
5385
+ open: env.vcardOpen ? !!env.vcardOpen() : null,
5386
+ card: cinfo,
5387
+ layer: layerEl ? { connected: !!layerEl.isConnected, parent: layerEl.parentNode ? layerEl.parentNode.className || layerEl.parentNode.nodeName : null } : null,
5388
+ frame: frameEl ? { show: frameEl.getAttribute("data-show"), connected: !!frameEl.isConnected } : null,
5389
+ vp: [window.innerWidth, window.innerHeight]
5390
+ });
5391
+ } catch (error) {
5392
+ report({ stage: "probe", error: String(error && error.message || error) });
5393
+ }
5394
+ }, 500);
5395
+ if (!opened) {
5396
+ hideMarkers();
5397
+ env.state.selection = null;
5398
+ current = null;
5399
+ }
5400
+ } catch (error) {
5401
+ report({ stage: step, error: String(error && error.stack || error && error.message || error) });
5402
+ }
5403
+ }
5404
+ function uploadAnnotated(blob) {
5405
+ return new Promise(function(resolve) {
5406
+ var reader = new FileReader();
5407
+ reader.onload = function() {
5408
+ var data = String(reader.result || "");
5409
+ var base64 = data.indexOf(",") >= 0 ? data.slice(data.indexOf(",") + 1) : data;
5410
+ fetch(ATTACH, {
5411
+ method: "POST",
5412
+ headers: { "content-type": "application/json" },
5413
+ body: JSON.stringify({ items: [{ kind: "image", name: "\u622A\u56FE.png", mediaType: "image/png", data: base64 }] })
5414
+ }).then(function(r) {
5415
+ return r.ok ? r.json() : null;
5416
+ }).then(function(json) {
5417
+ var item = json && json.items && json.items[0];
5418
+ resolve(item && item.ref ? item.ref : null);
5419
+ }).catch(function() {
5420
+ resolve(null);
5421
+ });
5422
+ };
5423
+ reader.onerror = function() {
5424
+ resolve(null);
5425
+ };
5426
+ reader.readAsDataURL(blob);
5427
+ });
5428
+ }
5429
+ function loadImage(src, attempt) {
5430
+ var tries = attempt || 0;
5431
+ return new Promise(function(resolve, reject) {
5432
+ var img = new Image();
5433
+ img.onload = function() {
5434
+ resolve(img);
5435
+ };
5436
+ img.onerror = function(error) {
5437
+ if (tries >= 8) {
5438
+ reject(error);
5439
+ return;
5440
+ }
5441
+ setTimeout(function() {
5442
+ loadImage(src, tries + 1).then(resolve, reject);
5443
+ }, 120);
5444
+ };
5445
+ img.src = src + (src.indexOf("?") >= 0 ? "&" : "?") + "r=" + tries;
5446
+ });
5447
+ }
5448
+ async function poll() {
5449
+ if (busy) return;
5450
+ busy = true;
5451
+ try {
5452
+ var res = await fetch(SHOT_PENDING, { cache: "no-store" });
5453
+ if (res.ok) {
5454
+ var data = await res.json();
5455
+ if (data && data.error) notice(errorText(data.error));
5456
+ if (data && data.shot) await handle(data.shot);
5457
+ }
5458
+ } catch (error) {
5459
+ } finally {
5460
+ busy = false;
5461
+ }
5462
+ }
5463
+ function loop() {
5464
+ if (superseded()) return;
5465
+ timer = setTimeout(function() {
5466
+ timer = 0;
5467
+ if (superseded()) return;
5468
+ var d = doc();
5469
+ if (d && d.visibilityState === "hidden") return loop();
5470
+ poll().then(loop, loop);
5471
+ }, 30);
5472
+ }
5473
+ return {
5474
+ start: function() {
5475
+ bindCancel();
5476
+ if (timer) return;
5477
+ loop();
5478
+ },
5479
+ stop: function() {
5480
+ if (timer) clearTimeout(timer);
5481
+ timer = 0;
5482
+ hideNotice();
5483
+ },
5484
+ /** 快捷键按下时调用。 */
5485
+ capture: startCapture,
5486
+ clear: clearAll,
5487
+ clearMarkers: hideMarkers,
5488
+ /** 发送时用:当前这张已经画好红框的图(File 与持久引用)。 */
5489
+ takeFile: function() {
5490
+ return current ? current.file : null;
5491
+ },
5492
+ /**
5493
+ * 发送时用:等后台把红框图画好,**此刻才上传**并返回持久引用。
5494
+ *
5495
+ * 为什么不在截完就传:附件库(~/.dsh/attachments)是内容寻址、**只增不删**,
5496
+ * 没有 GC。用户按了 Esc / 点空白取消时,我们不该往里面塞一张永远不会被用到的图。
5497
+ */
5498
+ takeRefAsync: function() {
5499
+ var here = current;
5500
+ if (!here) return Promise.resolve(null);
5501
+ if (here.error && here.error !== "upload-failed") return Promise.resolve(null);
5502
+ if (here.ref) return Promise.resolve(here.ref);
5503
+ if (here.uploading) return here.uploading;
5504
+ if (!here.refReady) {
5505
+ here.error = "image-failed";
5506
+ notice("\u622A\u56FE\u5DF2\u62CD\u4E0B\uFF0C\u4F46\u8FD8\u6CA1\u6709\u51C6\u5907\u597D\uFF0C\u8BF7\u91CD\u8BD5");
5507
+ return Promise.resolve(null);
5508
+ }
5509
+ here.uploading = here.refReady.then(function() {
5510
+ if (here.ref) return here.ref;
5511
+ if (!here.blob) {
5512
+ if (!here.error) {
5513
+ here.error = "image-failed";
5514
+ notice("\u622A\u56FE\u5DF2\u62CD\u4E0B\uFF0C\u4F46\u8BFB\u53D6\u56FE\u7247\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
5515
+ }
5516
+ return null;
5517
+ }
5518
+ return uploadAnnotated(here.blob).then(function(ref) {
5519
+ if (!ref) {
5520
+ here.error = "upload-failed";
5521
+ here.uploading = null;
5522
+ return null;
5523
+ }
5524
+ here.ref = ref;
5525
+ here.blob = null;
5526
+ here.error = "";
5527
+ report({ stage: "uploaded", ref: true });
5528
+ return ref;
5529
+ });
5530
+ });
5531
+ return here.uploading;
5532
+ },
5533
+ /** 当前这张图是否已经失败(供语音卡在发送时决定"先别开小窗")。 */
5534
+ failed: function() {
5535
+ return current && current.error ? String(current.error) : "";
5536
+ },
5537
+ /** 给语音卡/其它控制器用的提示入口。 */
5538
+ notice,
5539
+ takeRef: function() {
5540
+ return current ? current.ref : null;
5541
+ },
5542
+ state: function() {
5543
+ return {
5544
+ polling: !!timer,
5545
+ lastId,
5546
+ hasFile: !!(current && current.file),
5547
+ error: current && current.error ? String(current.error) : "",
5548
+ frame: frameEl ? frameEl.getAttribute("data-show") === "1" : false,
5549
+ chip: chipEl ? chipEl.getAttribute("data-show") === "1" : false
5550
+ };
5551
+ }
5552
+ };
5553
+ }
5554
+
4493
5555
  // src/client/bridge.js
4494
5556
  function bridgeBody() {
4495
5557
  if (window.__dshSelBridge) return;
@@ -4635,7 +5697,7 @@ function bridgeBody() {
4635
5697
  if (!now) return "";
4636
5698
  return now.text + "\0" + now.rect.x + "," + now.rect.y + "," + now.rect.bottom + "," + now.rect.right;
4637
5699
  }
4638
- function report(allowClear) {
5700
+ function report2(allowClear) {
4639
5701
  var now = read();
4640
5702
  if (!now) {
4641
5703
  if (last && allowClear !== false) {
@@ -4681,10 +5743,10 @@ function bridgeBody() {
4681
5743
  }, true);
4682
5744
  window.addEventListener("mouseup", function() {
4683
5745
  pressed = false;
4684
- setTimeout(report, 0);
5746
+ setTimeout(report2, 0);
4685
5747
  }, true);
4686
5748
  window.addEventListener("keyup", function() {
4687
- setTimeout(report, 0);
5749
+ setTimeout(report2, 0);
4688
5750
  }, true);
4689
5751
  window.addEventListener("blur", function() {
4690
5752
  pressed = false;
@@ -4692,29 +5754,29 @@ function bridgeBody() {
4692
5754
  window.addEventListener("scroll", function() {
4693
5755
  if (pressed) return;
4694
5756
  setTimeout(function() {
4695
- report(false);
5757
+ report2(false);
4696
5758
  }, 60);
4697
5759
  }, true);
4698
5760
  window.addEventListener("resize", function() {
4699
- if (!pressed) report(false);
5761
+ if (!pressed) report2(false);
4700
5762
  });
4701
5763
  window.addEventListener("message", function(event) {
4702
5764
  var data = event && event.data;
4703
5765
  if (!data || data.__dshSel !== 1) return;
4704
5766
  if (data.kind === "ping") {
4705
5767
  send("hello", null);
4706
- report();
5768
+ report2();
4707
5769
  }
4708
- if (data.kind === "rescan") report();
5770
+ if (data.kind === "rescan") report2();
4709
5771
  });
4710
5772
  try {
4711
5773
  document.addEventListener("selectionchange", function() {
4712
- if (!pressed) report();
5774
+ if (!pressed) report2();
4713
5775
  });
4714
5776
  } catch (error) {
4715
5777
  }
4716
5778
  setInterval(tick, 250);
4717
- setTimeout(report, 60);
5779
+ setTimeout(report2, 60);
4718
5780
  }
4719
5781
  var BRIDGE_MARK = "data-dsh-sel-bridge";
4720
5782
  function bridgeIntoHtml(html) {
@@ -5036,7 +6098,82 @@ function createPanelController(env) {
5036
6098
  clearDocSelection();
5037
6099
  env.state.selection = null;
5038
6100
  }
6101
+ function shotScopeLabel(scope, frame) {
6102
+ if (frame === false || scope === "selection") return "\u4EC5\u9009\u533A";
6103
+ if (scope === "screen") return "\u5168\u5C4F + \u7EA2\u6846";
6104
+ if (scope === "margin") return "\u9009\u533A + \u7559\u767D";
6105
+ return "\u6574\u7A97 + \u7EA2\u6846";
6106
+ }
6107
+ function setQuoteLine(text) {
6108
+ if (!env.quote) return;
6109
+ var title = env.state.shotTitle;
6110
+ if (title) {
6111
+ env.quote.textContent = String(title);
6112
+ env.quote.setAttribute("data-title", "1");
6113
+ return;
6114
+ }
6115
+ env.quote.removeAttribute("data-title");
6116
+ env.quote.textContent = text;
6117
+ }
6118
+ function applyShotTitle(title) {
6119
+ if (!title) return;
6120
+ env.state.shotTitle = String(title);
6121
+ setQuoteLine(String(title));
6122
+ if (env.paintPill) env.paintPill();
6123
+ }
6124
+ function openPanelForShot(anchor, options) {
6125
+ env.panelSessionId = currentSessionId();
6126
+ env.state.contextState = null;
6127
+ env.state.skipInterpretation = true;
6128
+ env.state.payload = buildPayload(String(options.askFirst || ""), "", "\u622A\u56FE");
6129
+ env.state.payload.kind = "shot";
6130
+ env.state.shotScope = options.shotScope || "";
6131
+ env.state.shotFrame = options.shotFrame !== false;
6132
+ if (env.state.shotScope) {
6133
+ env.state.payload.shotScope = env.state.shotScope;
6134
+ env.state.payload.shotFrame = env.state.shotFrame;
6135
+ }
6136
+ env.state.kind = "shot";
6137
+ var shotIdentity = "";
6138
+ if (options && options.shotRef && typeof options.shotRef === "object") {
6139
+ var ref = options.shotRef;
6140
+ shotIdentity = String(ref.attachmentId || ref.id || ref.hash || "");
6141
+ }
6142
+ if (!shotIdentity) {
6143
+ shotIdentity = "shot-" + Date.now().toString(36);
6144
+ }
6145
+ env.state.cacheKey = cacheKeyOf(String(options.askFirst || ""), "shot:" + shotIdentity);
6146
+ env.hideButton();
6147
+ showPanel(anchor);
6148
+ env.state.shotTitle = "";
6149
+ setQuoteLine("\u622A\u56FE \xB7 " + shotScopeLabel(env.state.shotScope, env.state.shotFrame));
6150
+ if (env.paintPill) env.paintPill();
6151
+ env.clearQuotes();
6152
+ env.clearAttachments();
6153
+ env.state.turns = [];
6154
+ env.state.parts = { translation: "", detail: "" };
6155
+ env.state.stage = "";
6156
+ env.state.raw = "";
6157
+ env.state.phase = "done";
6158
+ env.state.chars = 0;
6159
+ env.state.error = "";
6160
+ env.state.fromCache = false;
6161
+ env.state.sharedCache = false;
6162
+ env.state.elapsed = 0;
6163
+ env.state.toolBusy = false;
6164
+ env.state.cacheState = "";
6165
+ env.state.toolDigest = "";
6166
+ env.renderTurns();
6167
+ paint();
6168
+ var refs = options.shotRef ? [{ kind: "image", ref: options.shotRef }] : [];
6169
+ var note = options.shotRef ? "\u3010\u672C\u6761\u6D88\u606F\u5E26\u4E86\u9644\u4EF6\u3011\u622A\u56FE\uFF08\u56FE\u7247\uFF09" : "";
6170
+ env.sendAsk(options.askFirst, null, refs, note);
6171
+ }
5039
6172
  function openPanelWithInner(text, context, label, anchor, keyContext, options) {
6173
+ if (options && options.shotRef !== void 0) {
6174
+ openPanelForShot(anchor, options);
6175
+ return;
6176
+ }
5040
6177
  env.state.skipInterpretation = !!(options && options.askFirst);
5041
6178
  env.panelSessionId = currentSessionId();
5042
6179
  env.state.contextState = null;
@@ -5044,8 +6181,10 @@ function createPanelController(env) {
5044
6181
  env.state.kind = env.state.payload.kind;
5045
6182
  env.hideButton();
5046
6183
  showPanel(anchor);
5047
- env.quote.textContent = text.length > 240 ? text.slice(0, 240) + " \u2026" : text;
6184
+ env.state.shotTitle = "";
6185
+ setQuoteLine(text.length > 240 ? text.slice(0, 240) + " \u2026" : text);
5048
6186
  applySectionTitle(env.state.kind, text);
6187
+ if (env.paintPill) env.paintPill();
5049
6188
  var cacheKey = cacheKeyOf(text, keyContext === void 0 ? context : keyContext);
5050
6189
  var cached = env.cache.get(cacheKey);
5051
6190
  env.state.cacheKey = cacheKey;
@@ -5329,6 +6468,7 @@ function createPanelController(env) {
5329
6468
  }
5330
6469
  function closePanel() {
5331
6470
  env.closeModelMenu();
6471
+ if (typeof env.closeLightbox === "function") env.closeLightbox();
5332
6472
  env.closeSettings();
5333
6473
  env.panelOpen = false;
5334
6474
  env.panel.style.display = "none";
@@ -5348,9 +6488,11 @@ function createPanelController(env) {
5348
6488
  env.hideQuoteButton();
5349
6489
  env.state.quoteSelection = null;
5350
6490
  env.cancelVoice();
5351
- if (env.state.request) env.state.aborted = true;
5352
- if (env.state.request) env.state.request.abort();
5353
- env.state.request = null;
6491
+ if (env.state.request) {
6492
+ env.state.aborted = true;
6493
+ env.state.request.abort();
6494
+ env.state.request = null;
6495
+ }
5354
6496
  if (env.state.askRequest) {
5355
6497
  env.state.stopped = true;
5356
6498
  env.state.askRequest.abort();
@@ -5596,6 +6738,7 @@ function createPanelController(env) {
5596
6738
  env.panel.style.top = Math.round(top) + "px";
5597
6739
  }
5598
6740
  return {
6741
+ applyShotTitle,
5599
6742
  releaseRenderedContent,
5600
6743
  openForSelection,
5601
6744
  currentSessionId,
@@ -5648,7 +6791,9 @@ function createPanelController(env) {
5648
6791
  // src/client/pill.js
5649
6792
  function createPillController(env) {
5650
6793
  function pillSnippet() {
5651
- var text = env.state.payload && env.state.payload.text ? String(env.state.payload.text) : "";
6794
+ var title = env.state.shotTitle ? String(env.state.shotTitle) : "";
6795
+ title = title.replace(/\s+/g, " ").trim();
6796
+ var text = title || (env.state.payload && env.state.payload.text ? String(env.state.payload.text) : "");
5652
6797
  text = text.replace(/\s+/g, " ").trim();
5653
6798
  if (!text) return "";
5654
6799
  return text.length > 12 ? text.slice(0, 12) + "\u2026" : text;
@@ -5684,7 +6829,7 @@ function createPillController(env) {
5684
6829
  var caret = env.panelOpen ? "\u25BE" : "\u25B4";
5685
6830
  if (env.pillCaret.textContent !== caret) env.pillCaret.textContent = caret;
5686
6831
  env.pill.title = [
5687
- snippet ? "\u6700\u8FD1\u4E00\u6B21\u5212\u8BCD\uFF1A" + snippet : "\u8FD8\u6CA1\u6709\u5212\u8FC7\u8BCD",
6832
+ snippet ? (env.state.shotTitle ? "\u6700\u8FD1\u4E00\u6B21\u622A\u56FE\uFF1A" : "\u6700\u8FD1\u4E00\u6B21\u5212\u8BCD\uFF1A") + snippet : "\u8FD8\u6CA1\u6709\u5212\u8FC7\u8BCD",
5688
6833
  status.text,
5689
6834
  env.panelOpen ? "\u70B9\u51FB\u6536\u8D77\u5C0F\u7A97" : "\u70B9\u51FB\u56DE\u5230\u8FD9\u4E2A\u5C0F\u7A97",
5690
6835
  "\u53EF\u62D6\u52A8\uFF08\u62D6\u5230\u53F3\u4E0B\u89D2\u9644\u8FD1\u4F1A\u5438\u9644\u56DE\u53BB\uFF09"
@@ -6249,6 +7394,7 @@ function prepareChatHistory(turns, rawState) {
6249
7394
  // src/client/history.js
6250
7395
  function createHistoryController(env) {
6251
7396
  var foldCount = 0;
7397
+ var shotTitlePendingKey = "";
6252
7398
  var HTML_VOID_TAGS = {
6253
7399
  area: 1,
6254
7400
  base: 1,
@@ -6583,30 +7729,40 @@ function createHistoryController(env) {
6583
7729
  } else return;
6584
7730
  renderTurns();
6585
7731
  }
7732
+ var savedBody = {
7733
+ key: env.state.cacheKey,
7734
+ text: payload.text,
7735
+ context: payload.context,
7736
+ // 截图有标题时写进 label(旧列表/旧客户端也能显示);标题本体另存 title
7737
+ label: payload.kind === "shot" && String(env.state.shotTitle || "").trim() ? "\u622A\u56FE \xB7 " + String(env.state.shotTitle).trim() : payload.label,
7738
+ ...payload.kind === "shot" && String(env.state.shotTitle || "").trim() ? { title: String(env.state.shotTitle).trim() } : {},
7739
+ parts: env.state.parts,
7740
+ turns,
7741
+ toolDigest: env.state.toolDigest,
7742
+ contextState: env.state.contextState || null,
7743
+ pinned: !!(options && options.pinned),
7744
+ // 这段对话有没有解读,必须跟着条目落盘:回放时靠它决定画不画「翻译」一节。
7745
+ // 不落盘的话,语音卡那条"跳过翻译与详解、直接追问"的对话下次开出来,
7746
+ // 会顶着一张空翻译卡片("这次没有返回内容")——用户实测。
7747
+ skipped: env.state.skipInterpretation === true,
7748
+ // 截图取景范围:标题那次请求要带上同样的 scope,才能得到同一个 SHOT_PROMPT 口径
7749
+ ...payload.kind === "shot" && env.state.shotScope ? { shotScope: env.state.shotScope, shotFrame: env.state.shotFrame !== false } : {}
7750
+ };
6586
7751
  try {
6587
7752
  fetch(HISTORY, {
6588
7753
  method: "POST",
6589
7754
  headers: { "content-type": "application/json" },
6590
- body: JSON.stringify({
6591
- key: env.state.cacheKey,
6592
- text: payload.text,
6593
- context: payload.context,
6594
- label: payload.label,
6595
- parts: env.state.parts,
6596
- turns,
6597
- toolDigest: env.state.toolDigest,
6598
- contextState: env.state.contextState || null,
6599
- pinned: !!(options && options.pinned),
6600
- // 这段对话有没有解读,必须跟着条目落盘:回放时靠它决定画不画「翻译」一节。
6601
- // 不落盘的话,语音卡那条"跳过翻译与详解、直接追问"的对话下次开出来,
6602
- // 会顶着一张空翻译卡片("这次没有返回内容")——用户实测。
6603
- skipped: env.state.skipInterpretation === true
6604
- })
7755
+ body: JSON.stringify(savedBody)
6605
7756
  }).then(function(response) {
6606
7757
  if (!response.ok) throw new Error("HTTP " + response.status);
6607
7758
  return response.json();
6608
7759
  }).then(function(result) {
6609
- noteSaved(!result || result.ok !== true);
7760
+ var failed = !result || result.ok !== true;
7761
+ noteSaved(failed);
7762
+ if (!failed && payload.kind === "shot" && !String(env.state.shotTitle || "").trim() && shotTitlePendingKey !== savedBody.key) {
7763
+ shotTitlePendingKey = savedBody.key;
7764
+ requestShotTitle(savedBody, payload.sessionId);
7765
+ }
6610
7766
  }).catch(function() {
6611
7767
  noteSaved(true);
6612
7768
  });
@@ -6614,6 +7770,99 @@ function createHistoryController(env) {
6614
7770
  noteSaved(true);
6615
7771
  }
6616
7772
  }
7773
+ function requestShotTitle(saved, sessionId, attempt) {
7774
+ var tries = attempt || 0;
7775
+ var userTurn = null;
7776
+ var list = saved.turns || [];
7777
+ for (var i = 0; i < list.length; i += 1) {
7778
+ if (list[i] && list[i].role === "user" && list[i].attachments && list[i].attachments.length > 0) {
7779
+ userTurn = list[i];
7780
+ break;
7781
+ }
7782
+ }
7783
+ if (!userTurn) return;
7784
+ var savedKey = saved.key;
7785
+ fetch(API, {
7786
+ method: "POST",
7787
+ headers: { "content-type": "application/json" },
7788
+ body: JSON.stringify({
7789
+ text: saved.text,
7790
+ kind: "shot",
7791
+ titleOnly: true,
7792
+ label: "\u622A\u56FE",
7793
+ attachments: userTurn.attachments,
7794
+ // 与回答那次同一个取景口径:标题是最好的"红框/仅选区"措辞,换口径会问错东西
7795
+ ...saved.shotScope ? { shotScope: saved.shotScope, shotFrame: saved.shotFrame !== false } : {},
7796
+ // ⚠️ 必须带 sessionId:不带会被上游 400 拒绝(MissingSessionID),
7797
+ // 而 `env.panelSessionId` 在历史模块里并不存在 —— 用的是保存时那份 payload 里的。
7798
+ ...sessionId ? { sessionId } : {},
7799
+ // ⚠️ 必须带上面板当前用的模型:不带的话 host 会退回"会话默认模型",
7800
+ // 那个路由可能不支持图片 → 上游报错 → 正文为空 → 标题永远写不回去(实测踩到)。
7801
+ ...env.state.modelChoice ? { provider: env.state.modelChoice.provider, model: env.state.modelChoice.model } : {}
7802
+ })
7803
+ }).then(function(response) {
7804
+ return response.ok ? response.text() : "";
7805
+ }).then(function(raw) {
7806
+ var title = cleanShotTitle(readSseText(raw));
7807
+ if (!title) throw new Error("empty-title");
7808
+ if (shotTitlePendingKey === savedKey) shotTitlePendingKey = "";
7809
+ if (env.state.cacheKey === savedKey && env.applyShotTitle) env.applyShotTitle(title);
7810
+ writeShotTitle(savedKey, title);
7811
+ }).catch(function() {
7812
+ if (tries >= 1) {
7813
+ if (shotTitlePendingKey === savedKey) shotTitlePendingKey = "";
7814
+ return;
7815
+ }
7816
+ setTimeout(function() {
7817
+ requestShotTitle(saved, sessionId, tries + 1);
7818
+ }, 2e3);
7819
+ });
7820
+ }
7821
+ function writeShotTitle(key, title) {
7822
+ fetch(HISTORY + "?key=" + encodeURIComponent(key)).then(function(response) {
7823
+ return response.ok ? response.json() : null;
7824
+ }).then(function(data) {
7825
+ var entry = data && data.entry;
7826
+ if (!entry) return;
7827
+ return fetch(HISTORY, {
7828
+ method: "POST",
7829
+ headers: { "content-type": "application/json" },
7830
+ // title 是权威字段;label 继续写成「截图 · 标题」兼容旧列表/旧版本
7831
+ body: JSON.stringify(Object.assign({}, entry, { title, label: "\u622A\u56FE \xB7 " + title }))
7832
+ });
7833
+ }).then(function(response) {
7834
+ if (!response || !response.ok) return;
7835
+ if (env.historyList && env.historyList.style.display === "flex") openHistoryList();
7836
+ }).catch(function() {
7837
+ });
7838
+ }
7839
+ function readSseText(raw) {
7840
+ var out = [];
7841
+ var lines = String(raw || "").split("\n");
7842
+ for (var i = 0; i < lines.length; i += 1) {
7843
+ var line = lines[i].trim();
7844
+ if (line.indexOf("data:") !== 0) continue;
7845
+ var chunk = line.slice(5).trim();
7846
+ if (!chunk || chunk === "[DONE]") continue;
7847
+ try {
7848
+ var obj = JSON.parse(chunk);
7849
+ if (obj.type === "thought") continue;
7850
+ if (typeof obj.delta === "string") out.push(obj.delta);
7851
+ else if (obj.type === "delta" && typeof obj.text === "string") out.push(obj.text);
7852
+ } catch (error) {
7853
+ }
7854
+ }
7855
+ return out.join("");
7856
+ }
7857
+ function cleanShotTitle(raw) {
7858
+ var title = String(raw || "").trim();
7859
+ title = title.replace(/^标题[::]\s*/, "");
7860
+ title = title.replace(/^["'「『【]+|["'」』】]+$/g, "");
7861
+ title = title.replace(/[。!?.!?,,、::;;…]+$/g, "");
7862
+ title = title.replace(/\s+/g, " ").trim();
7863
+ if (!title) return "";
7864
+ return title.length > 24 ? title.slice(0, 24) : title;
7865
+ }
6617
7866
  function applyHistoryEntry(entry, source) {
6618
7867
  var parts = entry.parts || {};
6619
7868
  env.state.parts = { translation: parts.translation || "", detail: parts.detail || "" };
@@ -6647,10 +7896,26 @@ function createHistoryController(env) {
6647
7896
  env.state.toolBusy = false;
6648
7897
  env.state.cacheState = source === "local" ? "local" : "history";
6649
7898
  env.state.chars = (env.state.parts.translation || "").length + (env.state.parts.detail || "").length;
6650
- env.quote.textContent = entry.text.length > 240 ? entry.text.slice(0, 240) + " \u2026" : entry.text;
7899
+ var replayTitle = typeof entry.title === "string" ? entry.title.trim() : "";
7900
+ if (!replayTitle) {
7901
+ var titleMatch = /^截图 · (.+)$/.exec(String(entry.label || ""));
7902
+ if (titleMatch) replayTitle = titleMatch[1];
7903
+ }
7904
+ env.state.shotTitle = replayTitle;
7905
+ if (replayTitle) {
7906
+ if (env.applyShotTitle) env.applyShotTitle(replayTitle);
7907
+ else {
7908
+ env.quote.textContent = replayTitle;
7909
+ env.quote.setAttribute("data-title", "1");
7910
+ }
7911
+ } else {
7912
+ env.quote.textContent = entry.text.length > 240 ? entry.text.slice(0, 240) + " \u2026" : entry.text;
7913
+ env.quote.removeAttribute("data-title");
7914
+ }
6651
7915
  env.applySectionTitle(env.state.kind, entry.text);
6652
7916
  hideHistoryList();
6653
7917
  env.paint();
7918
+ if (env.paintPill) env.paintPill();
6654
7919
  renderTurns();
6655
7920
  }
6656
7921
  function restoreFromHistory(cacheKey, onMiss) {
@@ -6757,8 +8022,11 @@ function createHistoryController(env) {
6757
8022
  }
6758
8023
  function historyRow(item) {
6759
8024
  var row = el("div", "dsh-sel-historyrow");
6760
- var title = String(item.text || "").trim();
6761
- row.appendChild(el("div", "dsh-sel-historytext", title || "\uFF08\u8FD9\u6B21\u6CA1\u9009\u4E2D\u6587\u5B57\uFF09"));
8025
+ var shotTitle = typeof item.title === "string" ? item.title.trim() : "";
8026
+ var shotMatch = /^截图 · (.+)$/.exec(String(item.label || ""));
8027
+ var isShot = !!shotTitle || !!shotMatch;
8028
+ var main = shotTitle || (shotMatch ? shotMatch[1] : String(item.text || "").trim());
8029
+ row.appendChild(el("div", "dsh-sel-historytext", main || "\uFF08\u8FD9\u6B21\u6CA1\u9009\u4E2D\u6587\u5B57\uFF09"));
6762
8030
  var del = el("button", "dsh-sel-historydel", "\u2715");
6763
8031
  del.type = "button";
6764
8032
  del.title = "\u5220\u9664\u8FD9\u6761\u8BB0\u5F55";
@@ -6769,7 +8037,8 @@ function createHistoryController(env) {
6769
8037
  deleteHistoryEntry(item, row);
6770
8038
  });
6771
8039
  row.appendChild(del);
6772
- var meta = (item.label ? shortLabel(item.label) + " \xB7 " : "") + (item.turns > 0 ? item.turns + " \u8F6E \xB7 " : "") + formatWhen(item.at);
8040
+ var metaLabel = isShot ? "\u622A\u56FE" : shortLabel(item.label);
8041
+ var meta = (metaLabel ? metaLabel + " \xB7 " : "") + (item.turns > 0 ? item.turns + " \u8F6E \xB7 " : "") + formatWhen(item.at);
6773
8042
  var metaNode = el("div", "dsh-sel-historymeta");
6774
8043
  metaNode.appendChild(el("span", null, meta));
6775
8044
  if (item.pinned) metaNode.appendChild(el("span", "dsh-sel-historypin", "\u5DF2\u5347\u683C"));
@@ -7139,13 +8408,35 @@ function createHistoryController(env) {
7139
8408
  }
7140
8409
  return strip;
7141
8410
  }
8411
+ function bubbleGallery(anchor) {
8412
+ var log = env.chatLog;
8413
+ var links = log && typeof log.querySelectorAll === "function" ? log.querySelectorAll(".dsh-sel-bqimgwrap") : [];
8414
+ var images = [];
8415
+ var index = 0;
8416
+ for (var i = 0; i < links.length; i += 1) {
8417
+ var wrap = links[i];
8418
+ var thumb = wrap.querySelector ? wrap.querySelector("img") : null;
8419
+ var src = String(wrap.getAttribute("href") || thumb && thumb.getAttribute("src") || "");
8420
+ if (!src) continue;
8421
+ if (wrap === anchor) index = images.length;
8422
+ images.push({ src, name: thumb && thumb.alt || "\u56FE\u7247" });
8423
+ }
8424
+ if (images.length === 0 && anchor) images.push({ src: anchor.href, name: "\u56FE\u7247" });
8425
+ return { images, index };
8426
+ }
7142
8427
  function attachmentImage(ref) {
7143
8428
  var link = document.createElement("a");
7144
8429
  link.className = "dsh-sel-bqimgwrap";
7145
8430
  link.href = attachmentUrl("image", ref);
7146
8431
  link.target = "_blank";
7147
8432
  link.rel = "noreferrer noopener";
7148
- link.title = "\u70B9\u51FB\u67E5\u770B\u539F\u56FE" + (ref.name ? "\uFF1A" + ref.name : "");
8433
+ link.title = "\u70B9\u51FB\u9884\u89C8" + (ref.name ? "\uFF1A" + ref.name : "");
8434
+ listen(link, "click", function(event) {
8435
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
8436
+ if (typeof env.previewImage !== "function") return;
8437
+ event.preventDefault();
8438
+ env.previewImage(bubbleGallery(link));
8439
+ });
7149
8440
  var img = document.createElement("img");
7150
8441
  img.className = "dsh-sel-bqimg";
7151
8442
  img.src = link.href;
@@ -7327,7 +8618,7 @@ function createConversationController(env) {
7327
8618
  }
7328
8619
  return false;
7329
8620
  }
7330
- function ask(question) {
8621
+ async function ask(question) {
7331
8622
  var generating = env.state.asking || env.state.phase === "loading" || env.state.phase === "streaming";
7332
8623
  if (generating) {
7333
8624
  env.setStatus("\u8FD8\u5728\u751F\u6210\uFF0C\u8BF7\u7A0D\u5019\u2026\uFF08\u751F\u6210\u5B8C\u5C31\u80FD\u8FFD\u95EE\uFF09");
@@ -7337,6 +8628,17 @@ function createConversationController(env) {
7337
8628
  env.cancelVoice({ note: "\u5DF2\u505C\u6B62\u5F55\u97F3\uFF08\u8FD9\u6761\u5148\u53D1\u51FA\u53BB\uFF09" });
7338
8629
  }
7339
8630
  var asked = String(question || "").trim();
8631
+ if (env.attachEnsureUploaded && env.state.uploadingAttachments) return;
8632
+ if (env.attachEnsureUploaded) {
8633
+ env.state.uploadingAttachments = true;
8634
+ env.setStatus("\u6B63\u5728\u4E0A\u4F20\u9644\u4EF6\u2026");
8635
+ try {
8636
+ await env.attachEnsureUploaded();
8637
+ } finally {
8638
+ env.state.uploadingAttachments = false;
8639
+ }
8640
+ if (env.state.asking || env.state.phase === "loading" || env.state.phase === "streaming") return;
8641
+ }
7340
8642
  var quotes = env.state.quotes.slice();
7341
8643
  var attachments = env.attachmentsPayload();
7342
8644
  if (!asked && quotes.length === 0 && attachments.length === 0 || !env.state.payload) return;
@@ -7375,6 +8677,7 @@ function createConversationController(env) {
7375
8677
  function sendAsk(asked, quotes, attachments, attachmentsNote) {
7376
8678
  var payload = env.state.payload;
7377
8679
  if (!payload) return;
8680
+ var firstShot = payload.kind === "shot" && env.state.turns.length === 0;
7378
8681
  var withAttachments = attachments || [];
7379
8682
  var sent = env.composeQuestion(quotes, asked);
7380
8683
  if (attachmentsNote) sent = attachmentsNote + "\n" + sent;
@@ -7438,7 +8741,7 @@ function createConversationController(env) {
7438
8741
  answerTurn.asking = false;
7439
8742
  answerTurn.streaming = false;
7440
8743
  clearWaiting(env.chatLog);
7441
- env.saveHistory();
8744
+ if (env.state.turns === requestTurns && env.state.cacheKey === requestKey) env.saveHistory();
7442
8745
  env.syncPanelStage();
7443
8746
  refreshAskState();
7444
8747
  env.state.askRequest = null;
@@ -7459,6 +8762,12 @@ function createConversationController(env) {
7459
8762
  text: payload.text,
7460
8763
  context: payload.context,
7461
8764
  label: payload.label,
8765
+ // 截图首轮:host 靠它走独立的 SHOT_PROMPT(不是"选中文字"模板),
8766
+ // 并据此描述图里到底是整窗、全屏、留白还是仅选区。
8767
+ ...firstShot ? {
8768
+ kind: "shot",
8769
+ ...env.state.shotScope ? { shotScope: env.state.shotScope, shotFrame: env.state.shotFrame !== false } : {}
8770
+ } : {},
7462
8771
  sessionId: env.panelOrCurrentSessionId(),
7463
8772
  question: sent,
7464
8773
  history,
@@ -7695,6 +9004,8 @@ function createConversationController(env) {
7695
9004
  }
7696
9005
  }
7697
9006
  function runRequest(payload, cacheKey, options) {
9007
+ var requestTurns = env.state.turns;
9008
+ var requestKey = cacheKey;
7698
9009
  env.state.cacheState = "miss";
7699
9010
  if (env.state.request) env.state.request.abort();
7700
9011
  var controller = new AbortController();
@@ -7763,7 +9074,7 @@ function createConversationController(env) {
7763
9074
  env.state.request = null;
7764
9075
  clearWaiting();
7765
9076
  if (cacheKey) env.cache.set(cacheKey, { parts: { ...env.state.parts }, turns: env.state.turns.slice() });
7766
- env.saveHistory();
9077
+ if (env.state.turns === requestTurns && env.state.cacheKey === requestKey) env.saveHistory();
7767
9078
  env.paint();
7768
9079
  }
7769
9080
  var dirty = false;
@@ -9925,6 +11236,18 @@ function apply(ctx) {
9925
11236
  }
9926
11237
  });
9927
11238
  var voiceCardController = createVoiceCardController({
11239
+ get shotRefAsync() {
11240
+ return shotController.takeRefAsync;
11241
+ },
11242
+ get shotFailed() {
11243
+ return shotController.failed;
11244
+ },
11245
+ get shotNotice() {
11246
+ return shotController.notice;
11247
+ },
11248
+ get clearShotMarkers() {
11249
+ return shotController.clearMarkers;
11250
+ },
9928
11251
  get layer() {
9929
11252
  return layer;
9930
11253
  },
@@ -10044,6 +11367,11 @@ function apply(ctx) {
10044
11367
  return later;
10045
11368
  }
10046
11369
  });
11370
+ var lightboxController = createLightboxController({
11371
+ get layer() {
11372
+ return layer;
11373
+ }
11374
+ });
10047
11375
  var panelController = createPanelController({
10048
11376
  get timerScope() {
10049
11377
  return timerScope;
@@ -10168,6 +11496,9 @@ function apply(ctx) {
10168
11496
  get closeSettings() {
10169
11497
  return settingsController.closeSettings;
10170
11498
  },
11499
+ get closeLightbox() {
11500
+ return lightboxController.close;
11501
+ },
10171
11502
  get hideHistoryList() {
10172
11503
  return historyController.hideHistoryList;
10173
11504
  },
@@ -10352,12 +11683,18 @@ function apply(ctx) {
10352
11683
  set panelSessionId(value) {
10353
11684
  panelSessionId = value;
10354
11685
  },
11686
+ get applyShotTitle() {
11687
+ return panelController.applyShotTitle;
11688
+ },
10355
11689
  get sentTextOf() {
10356
11690
  return quotesController.sentTextOf;
10357
11691
  },
10358
11692
  get panel() {
10359
11693
  return panel;
10360
11694
  },
11695
+ get previewImage() {
11696
+ return lightboxController.open;
11697
+ },
10361
11698
  get state() {
10362
11699
  return state;
10363
11700
  },
@@ -10382,6 +11719,9 @@ function apply(ctx) {
10382
11719
  get paint() {
10383
11720
  return panelController.paint;
10384
11721
  },
11722
+ get paintPill() {
11723
+ return pillController.paintPill;
11724
+ },
10385
11725
  get timerScope() {
10386
11726
  return timerScope;
10387
11727
  },
@@ -10483,6 +11823,9 @@ function apply(ctx) {
10483
11823
  get composeQuestion() {
10484
11824
  return quotesController.composeQuestion;
10485
11825
  },
11826
+ get attachEnsureUploaded() {
11827
+ return attachController.ensureUploaded;
11828
+ },
10486
11829
  get attachmentsPayload() {
10487
11830
  return attachController.payload;
10488
11831
  },
@@ -10617,6 +11960,9 @@ function apply(ctx) {
10617
11960
  get attachInput() {
10618
11961
  return attachInput;
10619
11962
  },
11963
+ get previewImage() {
11964
+ return lightboxController.open;
11965
+ },
10620
11966
  get setStatus() {
10621
11967
  return panelController.setStatus;
10622
11968
  },
@@ -10624,6 +11970,48 @@ function apply(ctx) {
10624
11970
  return conversationController.refreshAskState;
10625
11971
  }
10626
11972
  });
11973
+ var shotController = createShotController({
11974
+ get document() {
11975
+ return document;
11976
+ },
11977
+ get window() {
11978
+ return typeof window !== "undefined" ? window : null;
11979
+ },
11980
+ get layer() {
11981
+ return layer;
11982
+ },
11983
+ get state() {
11984
+ return state;
11985
+ },
11986
+ get panelOpen() {
11987
+ return panelOpen;
11988
+ },
11989
+ get addFiles() {
11990
+ return attachController.addFiles;
11991
+ },
11992
+ get focusAskBox() {
11993
+ return panelController.focusAskBox;
11994
+ },
11995
+ get openVCard() {
11996
+ return voiceCardController.openVCard;
11997
+ },
11998
+ get vcardOpen() {
11999
+ return function() {
12000
+ return voiceCardController.vcardState.open;
12001
+ };
12002
+ },
12003
+ get vcardElement() {
12004
+ return function() {
12005
+ return voiceCardController.vcard;
12006
+ };
12007
+ },
12008
+ get closeVCard() {
12009
+ return voiceCardController.closeVCard;
12010
+ },
12011
+ get ensureLayer() {
12012
+ return ensureLayer;
12013
+ }
12014
+ });
10627
12015
  var modelsController = createModelsController({
10628
12016
  get state() {
10629
12017
  return state;
@@ -10744,6 +12132,10 @@ function apply(ctx) {
10744
12132
  else layer.removeAttribute("data-full-view");
10745
12133
  }
10746
12134
  }
12135
+ function ensureLayer() {
12136
+ if (typeof document === "undefined" || !document.body) return;
12137
+ if (layer.parentNode !== document.body) document.body.appendChild(layer);
12138
+ }
10747
12139
  function mountFloatingLayer(node) {
10748
12140
  if (node) {
10749
12141
  if (layer.parentNode !== document.body) document.body.appendChild(layer);
@@ -11239,6 +12631,15 @@ function apply(ctx) {
11239
12631
  panelController.savePanelSize();
11240
12632
  });
11241
12633
  window.__dshSelectionExplain = {
12634
+ /** 截图提问的状态(调试/自动化)。 */
12635
+ shot: function() {
12636
+ return shotController.state();
12637
+ },
12638
+ /** 手动起一次截图(等同按 ⌘⇧X)。 */
12639
+ shotStart: function() {
12640
+ shotController.capture();
12641
+ return true;
12642
+ },
11242
12643
  /**
11243
12644
  * 语音卡(选中文字 + 输入法正在语音输入 → 就地追问)的状态。
11244
12645
  * 给自动化验证用:能不能弹、有没有焦点、几何对不对,全在这里读。
@@ -11752,6 +13153,95 @@ function apply(ctx) {
11752
13153
  });
11753
13154
  }
11754
13155
  };
13156
+ ctx.effect(function() {
13157
+ if (typeof window === "undefined" || !window.addEventListener) return function() {
13158
+ };
13159
+ function send(payload) {
13160
+ try {
13161
+ fetch("/selection-explain/api/shot/hello", {
13162
+ method: "POST",
13163
+ body: JSON.stringify(Object.assign({ at: Date.now() }, payload))
13164
+ }).catch(function() {
13165
+ });
13166
+ } catch (error) {
13167
+ }
13168
+ }
13169
+ function onError(event) {
13170
+ var err = event && (event.error || event);
13171
+ send({
13172
+ stage: "window-error",
13173
+ message: String(err && err.message || event.message || ""),
13174
+ stack: String(err && err.stack || "").slice(0, 2e3),
13175
+ source: String(event && event.filename || ""),
13176
+ line: event && event.lineno
13177
+ });
13178
+ }
13179
+ function onRejection(event) {
13180
+ var reason = event && event.reason;
13181
+ send({
13182
+ stage: "unhandled-rejection",
13183
+ message: String(reason && reason.message || reason || ""),
13184
+ stack: String(reason && reason.stack || "").slice(0, 2e3)
13185
+ });
13186
+ }
13187
+ window.addEventListener("error", onError);
13188
+ window.addEventListener("unhandledrejection", onRejection);
13189
+ return function() {
13190
+ window.removeEventListener("error", onError);
13191
+ window.removeEventListener("unhandledrejection", onRejection);
13192
+ };
13193
+ }, "selection-explain: error reporter");
13194
+ var shortcutsService = ctx.shortcuts;
13195
+ var shortcutReport = { loaded: true, shortcuts: !!shortcutsService, registered: false, error: "" };
13196
+ function reportShortcut() {
13197
+ try {
13198
+ fetch("/selection-explain/api/shot/hello", { method: "POST", body: JSON.stringify(shortcutReport) }).catch(function() {
13199
+ });
13200
+ } catch (error) {
13201
+ }
13202
+ }
13203
+ reportShortcut();
13204
+ if (shortcutsService && typeof shortcutsService.register === "function") {
13205
+ ctx.effect(function() {
13206
+ try {
13207
+ var off = shortcutsService.register({
13208
+ id: "selection-explain.screenshot",
13209
+ label: function() {
13210
+ return "\u622A\u56FE\u63D0\u95EE";
13211
+ },
13212
+ // ⚠️ 必须给数组:设置页渲染列表时会 `[...row.aliases]`,
13213
+ // 留成 undefined 会直接抛 TypeError,整个快捷键弹窗打不开(实测踩到)。
13214
+ aliases: [],
13215
+ // 键位是**物理键 + 修饰键**的结构(不是 "Mod+Shift+X" 这种字符串):
13216
+ // code 必须是 KeyA-Z / Digit0-9 / F1-24 这类物理 code,modifiers 是 meta/control/alt/shift/primary
13217
+ // (primary 在 macOS 上是 ⌘、Windows 上是 Ctrl)。
13218
+ defaults: {
13219
+ "desktop:macos": { code: "KeyX", modifiers: ["meta", "shift"] },
13220
+ "web:macos": { code: "KeyX", modifiers: ["meta", "shift"] },
13221
+ "desktop:windows": { code: "KeyX", modifiers: ["primary", "shift"] },
13222
+ "web:windows": { code: "KeyX", modifiers: ["primary", "shift"] }
13223
+ },
13224
+ resolve: function() {
13225
+ return {
13226
+ status: "handled",
13227
+ run: function() {
13228
+ shotController.capture();
13229
+ }
13230
+ };
13231
+ }
13232
+ });
13233
+ shortcutReport.registered = true;
13234
+ reportShortcut();
13235
+ return off;
13236
+ } catch (error) {
13237
+ shortcutReport.error = String(error && error.message || error);
13238
+ reportShortcut();
13239
+ return function() {
13240
+ };
13241
+ }
13242
+ }, "selection-explain: screenshot shortcut");
13243
+ shotController.start();
13244
+ }
11755
13245
  ctx.effect(function() {
11756
13246
  if (typeof console !== "undefined" && console.log) {
11757
13247
  console.log("[dsh-selection-explain] \u5212\u8BCD\u89E3\u8BFB\u5DF2\u5C31\u7EEA\uFF08\u9009\u4E2D\u6587\u5B57\u8BD5\u8BD5\uFF09");
@@ -11761,7 +13251,10 @@ function apply(ctx) {
11761
13251
  timerScope.dispose();
11762
13252
  cache.dispose();
11763
13253
  voiceCardController.closeVCard(false);
13254
+ lightboxController.dispose();
11764
13255
  attachController.clear();
13256
+ shotController.stop();
13257
+ shotController.clearMarkers();
11765
13258
  panelController.releaseRenderedContent();
11766
13259
  if (state.askRequest) state.askRequest.abort();
11767
13260
  offPopEnd();
@@ -11876,5 +13369,5 @@ function apply(ctx) {
11876
13369
  };
11877
13370
  }, "@yfwu2020/dsh-selection-explain: \u5212\u8BCD\u89E3\u8BFB UI");
11878
13371
  }
11879
- var inject = ["slots"];
13372
+ var inject = ["slots", "shortcuts"];
11880
13373
  return module.exports;}})