@yfwu2020/dsh-selection-explain 0.11.1 → 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.
Files changed (44) hide show
  1. package/README.md +25 -5
  2. package/lib/chat-context.js +86 -0
  3. package/lib/chat-context.js.map +1 -0
  4. package/lib/client.js +1978 -168
  5. package/lib/client.js.map +4 -4
  6. package/lib/config-values.js +6 -0
  7. package/lib/config-values.js.map +1 -1
  8. package/lib/host/analyze.js +199 -47
  9. package/lib/host/analyze.js.map +1 -1
  10. package/lib/host/chat-context.js +160 -0
  11. package/lib/host/chat-context.js.map +1 -0
  12. package/lib/host/chat-materials.js +64 -0
  13. package/lib/host/chat-materials.js.map +1 -0
  14. package/lib/host/config.js +8 -1
  15. package/lib/host/config.js.map +1 -1
  16. package/lib/host/history-attachments.js +17 -53
  17. package/lib/host/history-attachments.js.map +1 -1
  18. package/lib/host/history-store.js +93 -68
  19. package/lib/host/history-store.js.map +1 -1
  20. package/lib/host/http.js +2 -1
  21. package/lib/host/http.js.map +1 -1
  22. package/lib/host/plugin.js +31 -0
  23. package/lib/host/plugin.js.map +1 -1
  24. package/lib/host/promote.js +0 -1
  25. package/lib/host/promote.js.map +1 -1
  26. package/lib/host/settings-store.js +17 -18
  27. package/lib/host/settings-store.js.map +1 -1
  28. package/lib/host/shot.js +968 -0
  29. package/lib/host/shot.js.map +1 -0
  30. package/lib/prompts.js +54 -1
  31. package/lib/prompts.js.map +1 -1
  32. package/lib/settings.js +14 -1
  33. package/lib/settings.js.map +1 -1
  34. package/lib/types/chat-context.d.ts +53 -0
  35. package/lib/types/config-values.d.ts +30 -0
  36. package/lib/types/host/chat-context.d.ts +31 -0
  37. package/lib/types/host/chat-materials.d.ts +16 -0
  38. package/lib/types/host/config.d.ts +16 -2
  39. package/lib/types/host/history-attachments.d.ts +7 -40
  40. package/lib/types/host/history-store.d.ts +1 -0
  41. package/lib/types/host/shot.d.ts +73 -0
  42. package/lib/types/host/types.d.ts +17 -0
  43. package/lib/types/prompts.d.ts +21 -0
  44. package/package.json +4 -2
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";
@@ -151,7 +154,6 @@ var VOICE_SILENCE_RMS = 0.012;
151
154
  var CONTEXT_WINDOW = 1500;
152
155
  var KEY_CONTEXT_CHARS = 300;
153
156
  var MAX_QUOTES = 4;
154
- var MAX_QUOTE_CHARS = 3e3;
155
157
  var MAX_QUOTE_SELECTION = 2e4;
156
158
  var QUOTE_ONLY_QUESTION = "\u5C31\u4E0A\u9762\u5F15\u7528\u7684\u6587\u5B57\uFF0C\u8BF4\u8BF4\u5B83\u5728\u8FD9\u91CC\u662F\u4EC0\u4E48\u610F\u601D\u3002";
157
159
  var QUOTE_CONTEXT_API = "/selection-explain/api/quote-context";
@@ -283,6 +285,9 @@ var CSS = [
283
285
  // 可以自动换行、超过 84px 内部滚动,但**不显示滚动条**。
284
286
  // ⚠️ flex:0 0 auto 不能省:滚动区是 flex 列,而 overflow 非 visible 的 flex 项目
285
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}",
286
291
  ".dsh-sel-quote{margin:0;padding:7px 11px;border-radius:10px;flex:0 0 auto;border-left:3px solid var(--sel-a1,#0d9488);",
287
292
  "background:var(--dsw-alias-interactive-bg-hover,rgba(140,140,140,.11));color:var(--dsw-alias-label-secondary,inherit);",
288
293
  "font-size:12.5px;line-height:1.6;box-sizing:border-box;max-width:100%;max-height:84px;",
@@ -459,6 +464,10 @@ var CSS = [
459
464
  "display:grid;place-items:center;color:var(--dsw-alias-label-secondary,inherit)}",
460
465
  ".dsh-sel-thumb img{width:100%;height:100%;object-fit:cover;display:block}",
461
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)}',
462
471
  '.dsh-sel-thumb[data-state="error"]{border-color:color-mix(in srgb,var(--sel-bad,#e5534b) 70%,transparent)}',
463
472
  '.dsh-sel-thumb[data-state="error"] svg{opacity:.9;color:var(--sel-bad,#e5534b)}',
464
473
  // 转圈 + 不定态进度条:fetch 拿不到上传进度,所以**不假装知道百分比**
@@ -536,6 +545,50 @@ var CSS = [
536
545
  "border-radius:5px;line-height:15px;background:var(--sel-fill,#0f766e);color:var(--sel-fill-fg,#fff)}",
537
546
  ".dsh-sel-bqfilename{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px}",
538
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}}',
539
592
  // 「引用整条」:贴在助手气泡末尾,**平时不出现**(正文优先),鼠标移到这条消息上才浮出来。
540
593
  // 键盘 Tab 也能到(:focus-visible 同样点亮),触屏上另有浮标那条路。
541
594
  ".dsh-sel-bubquote{margin-top:7px;display:inline-flex;align-items:center;gap:4px;padding:2px 9px;",
@@ -693,6 +746,7 @@ var CSS = [
693
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}",
694
747
  ".dsh-sel-undo button{margin-left:auto;border:0;background:transparent;color:inherit;font:inherit;font-weight:600;cursor:pointer;text-decoration:underline}",
695
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;}",
696
750
  ".dsh-sel-historymeta{display:flex;align-items:center;gap:6px;font-size:11px;opacity:.5;font-variant-numeric:tabular-nums;min-width:0}",
697
751
  ".dsh-sel-historymeta>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
698
752
  ".dsh-sel-historymeta .dsh-sel-historypin{opacity:.9}",
@@ -957,6 +1011,32 @@ var CSS = [
957
1011
  ".dsh-sel-uline{position:fixed;height:2px;border-radius:1px;pointer-events:none}",
958
1012
  // 牵引线是 **SVG 贝塞尔**(原来是一根 2px 竖条 → 控制点和端点同 x,退化成直线,
959
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)}',
960
1040
  ".dsh-sel-thread{position:fixed;left:0;top:0;width:100%;height:100%;pointer-events:none;",
961
1041
  "z-index:2147482998;overflow:visible;display:none;opacity:0;transition:opacity .14s ease}",
962
1042
  ".dsh-sel-thread path{fill:none;stroke-width:1.6;stroke-dasharray:4 4;",
@@ -1836,6 +1916,13 @@ function adaptPreviewHtml(code, previewId) {
1836
1916
  }
1837
1917
  var previewSeq = 0;
1838
1918
  var previewFrames = {};
1919
+ function releasePreviewFrames(root) {
1920
+ if (!root) return;
1921
+ for (var key in previewFrames) {
1922
+ var frame = previewFrames[key].frame;
1923
+ if (frame === root || root.contains && root.contains(frame)) delete previewFrames[key];
1924
+ }
1925
+ }
1839
1926
  function registerPreviewFrame(frame) {
1840
1927
  previewSeq += 1;
1841
1928
  var id = "pv" + previewSeq;
@@ -1919,6 +2006,7 @@ function webPreviewBlock(code) {
1919
2006
  return { root: wrap, pre, frame };
1920
2007
  }
1921
2008
  function renderRich(parent2, text, options) {
2009
+ releasePreviewFrames(parent2);
1922
2010
  parent2.textContent = "";
1923
2011
  var lines = String(text || "").replace(/\r/g, "").split("\n");
1924
2012
  var current = null;
@@ -2099,7 +2187,17 @@ function createSettingsController(env) {
2099
2187
  var settingsRailButtons = [];
2100
2188
  var settingsPanes = [];
2101
2189
  var settingsSegs = [];
2102
- 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
+ };
2103
2201
  var SLOT_ORDER = ["off", "low", "high", "max"];
2104
2202
  var settingsEffortHints = [];
2105
2203
  var settingsMenus = [];
@@ -3152,7 +3250,8 @@ function createVoiceCardController(env) {
3152
3250
  var vcardHandoff = null;
3153
3251
  var panelGrowTimer = null;
3154
3252
  var panelGrowHandler = null;
3155
- var quoteVoiceHandled = false;
3253
+ var quoteVoiceHandled;
3254
+ var voiceSeenSinceOpen = false;
3156
3255
  var selMark = null;
3157
3256
  var selThread = null;
3158
3257
  var selThreadPath = null;
@@ -3259,7 +3358,7 @@ function createVoiceCardController(env) {
3259
3358
  }
3260
3359
  function unpinVCardBox() {
3261
3360
  if (vcardState.unpinTimer) {
3262
- clearTimeout(vcardState.unpinTimer);
3361
+ env.timerScope.clear(vcardState.unpinTimer);
3263
3362
  vcardState.unpinTimer = null;
3264
3363
  }
3265
3364
  if (vcard && vcardState.unpinHandler) {
@@ -3273,7 +3372,7 @@ function createVoiceCardController(env) {
3273
3372
  function scheduleVCardUnpin() {
3274
3373
  if (!vcard) return;
3275
3374
  if (vcardState.unpinTimer) {
3276
- clearTimeout(vcardState.unpinTimer);
3375
+ env.timerScope.clear(vcardState.unpinTimer);
3277
3376
  vcardState.unpinTimer = null;
3278
3377
  }
3279
3378
  if (vcardState.unpinHandler) {
@@ -3361,11 +3460,13 @@ function createVoiceCardController(env) {
3361
3460
  function openVCard() {
3362
3461
  if (vcardState.open || env.panelOpen) return;
3363
3462
  if (!env.state.selection) return;
3364
- if (!env.state.selection.range && env.state.selection.source !== "iframe") return;
3365
- if (!hasFocusEvidence(env.state.selection)) return;
3366
- 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();
3367
3467
  ensureVCard();
3368
3468
  if (vcardState.closing || vcardState.crtTimer) finishCrtOff();
3469
+ voiceSeenSinceOpen = false;
3369
3470
  vcardState.range = env.state.selection.range || null;
3370
3471
  vcardState.anchorRect = env.state.selection.rect || null;
3371
3472
  var geo = vcardGeometry();
@@ -3410,7 +3511,7 @@ function createVoiceCardController(env) {
3410
3511
  vcard.setAttribute("data-voiced", on ? "1" : "0");
3411
3512
  if (!vcardBox) return;
3412
3513
  if (String(vcardBox.value || "").trim()) return;
3413
- 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";
3414
3515
  }
3415
3516
  function focusVCard() {
3416
3517
  if (!vcardBox) return;
@@ -3582,7 +3683,7 @@ function createVoiceCardController(env) {
3582
3683
  line.setAttribute("data-off", "a");
3583
3684
  var done = function() {
3584
3685
  if (vcardState.crtTimer) {
3585
- clearTimeout(vcardState.crtTimer);
3686
+ env.timerScope.clear(vcardState.crtTimer);
3586
3687
  vcardState.crtTimer = null;
3587
3688
  }
3588
3689
  if (vcard && vcardState.crtHandler) {
@@ -3601,7 +3702,7 @@ function createVoiceCardController(env) {
3601
3702
  function finishCrtOff() {
3602
3703
  vcardState.closing = false;
3603
3704
  if (vcardState.crtTimer) {
3604
- clearTimeout(vcardState.crtTimer);
3705
+ env.timerScope.clear(vcardState.crtTimer);
3605
3706
  vcardState.crtTimer = null;
3606
3707
  }
3607
3708
  if (vcard && vcardState.crtHandler) {
@@ -3643,14 +3744,22 @@ function createVoiceCardController(env) {
3643
3744
  vcardState.settleStableAt = 0;
3644
3745
  vcardState.settleDeadline = 0;
3645
3746
  }
3747
+ function isShotSelection() {
3748
+ var sel = env.state && env.state.selection;
3749
+ return !!(sel && sel.source === "shot");
3750
+ }
3646
3751
  function finishVoiceSettle(text) {
3647
3752
  if (!vcardState.open) return;
3648
3753
  var spoken = String(text || "").trim();
3649
- if (spoken && env.settingsLive.voiceAutoSend !== false) {
3754
+ if (spoken && voiceSeenSinceOpen && env.settingsLive.voiceAutoSend !== false) {
3650
3755
  sendVCard();
3651
3756
  return;
3652
3757
  }
3653
3758
  if (!spoken && env.settingsLive.voiceCancelOnSilence !== false) {
3759
+ if (isShotSelection()) {
3760
+ markVCardListening(false);
3761
+ return;
3762
+ }
3654
3763
  closeVCard(true);
3655
3764
  return;
3656
3765
  }
@@ -3680,6 +3789,7 @@ function createVoiceCardController(env) {
3680
3789
  }
3681
3790
  function closeVCard(animate) {
3682
3791
  if (!vcardState.open) return;
3792
+ if (isShotSelection() && env.clearShotMarkers) env.clearShotMarkers();
3683
3793
  vcardState.open = false;
3684
3794
  if (vcardBox && !String(vcardBox.value || "").trim()) vcardBox.placeholder = "\u6B63\u5728\u5173\u95ED\u2026";
3685
3795
  vcardState.range = null;
@@ -3707,6 +3817,36 @@ function createVoiceCardController(env) {
3707
3817
  closeVCard();
3708
3818
  return;
3709
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
+ }
3710
3850
  var contextInfo = selection.source === "iframe" || !selection.range ? {
3711
3851
  context: String(selection.context || ""),
3712
3852
  keyContext: String(selection.keyContext || ""),
@@ -3755,14 +3895,13 @@ function createVoiceCardController(env) {
3755
3895
  function startMicWatch() {
3756
3896
  if (vcardState.timer) return;
3757
3897
  if (!env.state.selection && !env.state.quoteSelection) return;
3758
- vcardState.timer = setInterval(micTick, MIC_POLL_MS);
3759
- env.timers.push(vcardState.timer);
3898
+ vcardState.timer = env.timerScope.interval(micTick, MIC_POLL_MS);
3760
3899
  micTick();
3761
3900
  }
3762
3901
  function stopMicWatch() {
3763
3902
  quoteVoiceHandled = false;
3764
3903
  if (!vcardState.timer) return;
3765
- clearInterval(vcardState.timer);
3904
+ env.timerScope.clear(vcardState.timer);
3766
3905
  vcardState.timer = null;
3767
3906
  }
3768
3907
  function micTick() {
@@ -3788,6 +3927,7 @@ function createVoiceCardController(env) {
3788
3927
  return;
3789
3928
  }
3790
3929
  if (vcardState.suppress) return;
3930
+ voiceSeenSinceOpen = true;
3791
3931
  stopVoiceSettle();
3792
3932
  if (!vcardState.open) openVCard();
3793
3933
  else markVCardListening(true);
@@ -3809,7 +3949,7 @@ function createVoiceCardController(env) {
3809
3949
  var selection = env.state.quoteSelection;
3810
3950
  if (!env.panelOpen || !selection) return;
3811
3951
  if (!hasFocusEvidence(selection)) return;
3812
- if (env.isTypingTarget(document.activeElement)) return;
3952
+ if (env.state.selection.source !== "shot" && env.isTypingTarget(document.activeElement)) return;
3813
3953
  if (env.voice && env.voice.phase !== "idle") return;
3814
3954
  env.addQuote(selection.text, selection.label, env.quoteContextFor(selection));
3815
3955
  hideQuoteButton();
@@ -3882,8 +4022,6 @@ function createQuotesController(env) {
3882
4022
  function addQuote(text, label, extra) {
3883
4023
  var clean = String(text || "").replace(/\u00a0/g, " ").trim();
3884
4024
  if (!clean) return false;
3885
- var truncated = clean.length > MAX_QUOTE_CHARS;
3886
- if (truncated) clean = clean.slice(0, MAX_QUOTE_CHARS) + "\u2026";
3887
4025
  for (var i = 0; i < env.state.quotes.length; i += 1) {
3888
4026
  if (env.state.quotes[i].text === clean) {
3889
4027
  env.setStatus("\u8FD9\u6BB5\u5DF2\u7ECF\u5728\u5F15\u7528\u91CC\u4E86");
@@ -3896,19 +4034,27 @@ function createQuotesController(env) {
3896
4034
  }
3897
4035
  quoteSeq += 1;
3898
4036
  var context = extra && typeof extra.context === "string" ? extra.context.trim() : "";
3899
- env.state.quotes.push({
4037
+ var quote = {
3900
4038
  id: quoteSeq,
3901
4039
  label: label || "\u5F15\u7528",
3902
4040
  text: clean,
3903
4041
  context,
3904
- session: !!(extra && extra.session === true)
3905
- });
4042
+ session: !!(extra && extra.session === true),
4043
+ sessionId: extra && extra.sessionId || env.currentSessionId(),
4044
+ contextJob: null
4045
+ };
4046
+ env.state.quotes.push(quote);
4047
+ if (quote.session && quote.sessionId) {
4048
+ quote.contextJob = fetchQuoteContext(quote.sessionId, quote.text).then(function(context2) {
4049
+ if (context2) quote.context = context2;
4050
+ });
4051
+ }
3906
4052
  renderQuotes();
3907
4053
  env.refreshAskState();
3908
4054
  env.askRow.style.display = "";
3909
4055
  if (env.composerVisible()) env.focusAskBox();
3910
4056
  env.setStatus(
3911
- "\u5DF2\u52A0\u5165\u5F15\u7528\uFF08" + quoteLabelOf(env.state.quotes[env.state.quotes.length - 1]) + " \xB7 " + clean.length + " \u5B57" + (truncated ? "\uFF08\u539F\u6587\u66F4\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09" : "") + (context ? " \xB7 \u5E26\u4E0A\u4E0B\u6587 " + context.length + " \u5B57" : "") + "\uFF09"
4057
+ "\u5DF2\u52A0\u5165\u5F15\u7528\uFF08" + quoteLabelOf(env.state.quotes[env.state.quotes.length - 1]) + " \xB7 " + clean.length + " \u5B57" + (context ? " \xB7 \u5E26\u4E0A\u4E0B\u6587 " + context.length + " \u5B57" : "") + "\uFF09"
3912
4058
  );
3913
4059
  return true;
3914
4060
  }
@@ -3973,6 +4119,22 @@ function createQuotesController(env) {
3973
4119
  function quoteContextFor(selection) {
3974
4120
  if (!selection) return { context: "", session: false };
3975
4121
  if (selection.source === "panel") {
4122
+ var index = selection.turnIndex;
4123
+ var through = env.state.contextState && env.state.contextState.through || 0;
4124
+ var visibleBefore = env.state.turns.slice(0, index).filter(function(t) {
4125
+ return !t.hidden;
4126
+ }).length;
4127
+ if (index >= 0 && visibleBefore < through) {
4128
+ var pieces = env.state.turns.slice(Math.max(0, index - 1), index + 2).filter(function(t) {
4129
+ return !t.hidden;
4130
+ }).map(function(t) {
4131
+ var text = t.role === "assistant" ? env.foldForHistory(t.text || "") : sentTextOf(t);
4132
+ var at = text.indexOf(selection.text);
4133
+ var start = at >= 0 ? Math.max(0, at - 500) : 0;
4134
+ return (t.role === "user" ? "\u7528\u6237\uFF1A" : "\u52A9\u624B\uFF1A") + text.slice(start, start + Math.max(1500, selection.text.length + 500));
4135
+ });
4136
+ return { context: pieces.join("\n"), session: false };
4137
+ }
3976
4138
  return { context: "", session: false };
3977
4139
  }
3978
4140
  if (selection.source === "iframe") {
@@ -3993,22 +4155,16 @@ function createQuotesController(env) {
3993
4155
  for (var i = 0; i < quotes.length; i += 1) {
3994
4156
  if (quotes[i].session === true) jobs.push(quotes[i]);
3995
4157
  }
3996
- var sessionId = env.currentSessionId();
3997
- if (jobs.length === 0 || !sessionId) return Promise.resolve(quotes);
3998
- return Promise.all(
3999
- jobs.map(function(quote) {
4000
- return fetchQuoteContext(sessionId, quote.text).then(function(context) {
4001
- if (context) quote.context = context;
4002
- return quote;
4003
- });
4004
- })
4005
- ).then(function() {
4158
+ if (jobs.length === 0) return Promise.resolve(quotes);
4159
+ return Promise.all(jobs.map(function(quote) {
4160
+ return quote.contextJob || Promise.resolve();
4161
+ })).then(function() {
4006
4162
  return quotes;
4007
4163
  });
4008
4164
  }
4009
4165
  function fetchQuoteContext(sessionId, text) {
4010
4166
  var controller = typeof AbortController === "function" ? new AbortController() : null;
4011
- var timer = setTimeout(function() {
4167
+ var timer = env.timerScope.later(function() {
4012
4168
  if (controller) {
4013
4169
  try {
4014
4170
  controller.abort();
@@ -4016,7 +4172,6 @@ function createQuotesController(env) {
4016
4172
  }
4017
4173
  }
4018
4174
  }, QUOTE_CONTEXT_TIMEOUT);
4019
- env.timers.push(timer);
4020
4175
  return fetch(QUOTE_CONTEXT_API, {
4021
4176
  method: "POST",
4022
4177
  headers: { "content-type": "application/json" },
@@ -4025,11 +4180,11 @@ function createQuotesController(env) {
4025
4180
  }).then(function(response) {
4026
4181
  return response.ok ? response.json() : null;
4027
4182
  }).then(function(data) {
4028
- clearTimeout(timer);
4183
+ env.timerScope.clear(timer);
4029
4184
  if (data && data.ok === true && data.matched === true && typeof data.context === "string") return data.context;
4030
4185
  return "";
4031
4186
  }).catch(function() {
4032
- clearTimeout(timer);
4187
+ env.timerScope.clear(timer);
4033
4188
  return "";
4034
4189
  });
4035
4190
  }
@@ -4071,6 +4226,7 @@ function createQuotesController(env) {
4071
4226
  out.push({
4072
4227
  role: turn.role,
4073
4228
  text: sentTextOf(turn),
4229
+ ...turn.contextExcluded === true ? { contextExcluded: true } : {},
4074
4230
  ...turn.attachments && turn.attachments.length > 0 ? { attachments: turn.attachments } : {}
4075
4231
  });
4076
4232
  }
@@ -4146,6 +4302,20 @@ function createAttachController(env) {
4146
4302
  env.attachNote.textContent = env.state.attachNote || "";
4147
4303
  env.attachNote.setAttribute("data-show", env.state.attachNote ? "1" : "0");
4148
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
+ }
4149
4319
  function busy() {
4150
4320
  var list = items();
4151
4321
  for (var i = 0; i < list.length; i += 1) if (list[i].status === "uploading") return true;
@@ -4222,15 +4392,6 @@ function createAttachController(env) {
4222
4392
  note(rejected.join("\uFF1B"));
4223
4393
  render();
4224
4394
  env.refreshAskState();
4225
- var pending = list.filter(function(item) {
4226
- return item.status === "uploading" && !item.started;
4227
- });
4228
- var chain = Promise.resolve();
4229
- pending.forEach(function(item) {
4230
- chain = chain.then(function() {
4231
- return upload(item);
4232
- });
4233
- });
4234
4395
  }
4235
4396
  function createItem(file) {
4236
4397
  var isImage = /^image\//.test(String(file.type || ""));
@@ -4243,8 +4404,10 @@ function createAttachController(env) {
4243
4404
  file,
4244
4405
  // 缩略图先用本地 object URL 显示(不必等上传完),失败时回落到图标
4245
4406
  url: isImage && typeof URL !== "undefined" && URL.createObjectURL ? URL.createObjectURL(file) : "",
4246
- status: "uploading",
4247
- started: false,
4407
+ // 'local' = 只在内存里,还没落盘。用户要求:叉掉的不该在附件库留垃圾,
4408
+ // 所以**点发送时才真正上传**(见 ensureUploaded)。
4409
+ status: "local",
4410
+ started: true,
4248
4411
  ref: null,
4249
4412
  error: ""
4250
4413
  };
@@ -4256,6 +4419,7 @@ function createAttachController(env) {
4256
4419
  item.status = "uploading";
4257
4420
  item.error = "";
4258
4421
  render();
4422
+ env.refreshAskState();
4259
4423
  return fileToBase64(item.file).then(function(data) {
4260
4424
  return fetch(ATTACH, {
4261
4425
  method: "POST",
@@ -4325,10 +4489,44 @@ function createAttachController(env) {
4325
4489
  });
4326
4490
  return button;
4327
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
+ }
4328
4508
  function thumbNode(item) {
4329
4509
  var node = el("span", "dsh-sel-thumb");
4330
4510
  node.setAttribute("data-state", item.status);
4331
- 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
+ }
4332
4530
  if (item.url) {
4333
4531
  var img = document.createElement("img");
4334
4532
  img.src = item.url;
@@ -4459,6 +4657,7 @@ function createAttachController(env) {
4459
4657
  retry,
4460
4658
  render,
4461
4659
  busy,
4660
+ ensureUploaded,
4462
4661
  payload,
4463
4662
  describe,
4464
4663
  note,
@@ -4468,6 +4667,891 @@ function createAttachController(env) {
4468
4667
  };
4469
4668
  }
4470
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
+
4471
5555
  // src/client/bridge.js
4472
5556
  function bridgeBody() {
4473
5557
  if (window.__dshSelBridge) return;
@@ -4613,7 +5697,7 @@ function bridgeBody() {
4613
5697
  if (!now) return "";
4614
5698
  return now.text + "\0" + now.rect.x + "," + now.rect.y + "," + now.rect.bottom + "," + now.rect.right;
4615
5699
  }
4616
- function report(allowClear) {
5700
+ function report2(allowClear) {
4617
5701
  var now = read();
4618
5702
  if (!now) {
4619
5703
  if (last && allowClear !== false) {
@@ -4659,10 +5743,10 @@ function bridgeBody() {
4659
5743
  }, true);
4660
5744
  window.addEventListener("mouseup", function() {
4661
5745
  pressed = false;
4662
- setTimeout(report, 0);
5746
+ setTimeout(report2, 0);
4663
5747
  }, true);
4664
5748
  window.addEventListener("keyup", function() {
4665
- setTimeout(report, 0);
5749
+ setTimeout(report2, 0);
4666
5750
  }, true);
4667
5751
  window.addEventListener("blur", function() {
4668
5752
  pressed = false;
@@ -4670,29 +5754,29 @@ function bridgeBody() {
4670
5754
  window.addEventListener("scroll", function() {
4671
5755
  if (pressed) return;
4672
5756
  setTimeout(function() {
4673
- report(false);
5757
+ report2(false);
4674
5758
  }, 60);
4675
5759
  }, true);
4676
5760
  window.addEventListener("resize", function() {
4677
- if (!pressed) report(false);
5761
+ if (!pressed) report2(false);
4678
5762
  });
4679
5763
  window.addEventListener("message", function(event) {
4680
5764
  var data = event && event.data;
4681
5765
  if (!data || data.__dshSel !== 1) return;
4682
5766
  if (data.kind === "ping") {
4683
5767
  send("hello", null);
4684
- report();
5768
+ report2();
4685
5769
  }
4686
- if (data.kind === "rescan") report();
5770
+ if (data.kind === "rescan") report2();
4687
5771
  });
4688
5772
  try {
4689
5773
  document.addEventListener("selectionchange", function() {
4690
- if (!pressed) report();
5774
+ if (!pressed) report2();
4691
5775
  });
4692
5776
  } catch (error) {
4693
5777
  }
4694
5778
  setInterval(tick, 250);
4695
- setTimeout(report, 60);
5779
+ setTimeout(report2, 60);
4696
5780
  }
4697
5781
  var BRIDGE_MARK = "data-dsh-sel-bridge";
4698
5782
  function bridgeIntoHtml(html) {
@@ -4919,7 +6003,7 @@ function createPreviewController(env) {
4919
6003
  // src/client/panel.js
4920
6004
  function createPanelController(env) {
4921
6005
  function panelOrCurrentSessionId() {
4922
- return env.panelSessionId || currentSessionId();
6006
+ return env.state.contextState && typeof env.state.contextState.sessionId === "string" ? env.state.contextState.sessionId : env.panelSessionId || currentSessionId();
4923
6007
  }
4924
6008
  function currentSessionId() {
4925
6009
  try {
@@ -5014,15 +6098,93 @@ function createPanelController(env) {
5014
6098
  clearDocSelection();
5015
6099
  env.state.selection = null;
5016
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
+ }
5017
6172
  function openPanelWithInner(text, context, label, anchor, keyContext, options) {
6173
+ if (options && options.shotRef !== void 0) {
6174
+ openPanelForShot(anchor, options);
6175
+ return;
6176
+ }
5018
6177
  env.state.skipInterpretation = !!(options && options.askFirst);
5019
6178
  env.panelSessionId = currentSessionId();
6179
+ env.state.contextState = null;
5020
6180
  env.state.payload = buildPayload(text, context, label);
5021
6181
  env.state.kind = env.state.payload.kind;
5022
6182
  env.hideButton();
5023
6183
  showPanel(anchor);
5024
- 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);
5025
6186
  applySectionTitle(env.state.kind, text);
6187
+ if (env.paintPill) env.paintPill();
5026
6188
  var cacheKey = cacheKeyOf(text, keyContext === void 0 ? context : keyContext);
5027
6189
  var cached = env.cache.get(cacheKey);
5028
6190
  env.state.cacheKey = cacheKey;
@@ -5282,12 +6444,12 @@ function createPanelController(env) {
5282
6444
  env.panel.style.transition = "transform " + PANEL_GROW_MS + "ms " + PANEL_GROW_EASE;
5283
6445
  env.panel.style.transform = "none";
5284
6446
  if (env.panelGrowTimer) {
5285
- clearTimeout(env.panelGrowTimer);
6447
+ env.timerScope.clear(env.panelGrowTimer);
5286
6448
  env.panelGrowTimer = null;
5287
6449
  }
5288
6450
  var settle = function() {
5289
6451
  if (env.panelGrowTimer) {
5290
- clearTimeout(env.panelGrowTimer);
6452
+ env.timerScope.clear(env.panelGrowTimer);
5291
6453
  env.panelGrowTimer = null;
5292
6454
  }
5293
6455
  if (env.panelGrowHandler) {
@@ -5306,11 +6468,12 @@ function createPanelController(env) {
5306
6468
  }
5307
6469
  function closePanel() {
5308
6470
  env.closeModelMenu();
6471
+ if (typeof env.closeLightbox === "function") env.closeLightbox();
5309
6472
  env.closeSettings();
5310
6473
  env.panelOpen = false;
5311
6474
  env.panel.style.display = "none";
5312
6475
  if (env.panelGrowTimer) {
5313
- clearTimeout(env.panelGrowTimer);
6476
+ env.timerScope.clear(env.panelGrowTimer);
5314
6477
  env.panelGrowTimer = null;
5315
6478
  }
5316
6479
  if (env.panelGrowHandler) {
@@ -5325,9 +6488,16 @@ function createPanelController(env) {
5325
6488
  env.hideQuoteButton();
5326
6489
  env.state.quoteSelection = null;
5327
6490
  env.cancelVoice();
5328
- if (env.state.request) env.state.aborted = true;
5329
- if (env.state.request) env.state.request.abort();
5330
- 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
+ }
6496
+ if (env.state.askRequest) {
6497
+ env.state.stopped = true;
6498
+ env.state.askRequest.abort();
6499
+ }
6500
+ releaseRenderedContent();
5331
6501
  env.paintPill();
5332
6502
  env.syncRouteWatch();
5333
6503
  }
@@ -5356,7 +6526,20 @@ function createPanelController(env) {
5356
6526
  var what = env.state.toolBusy ? "\u6B63\u5728\u68C0\u7D22\u8D44\u6599\u2026" : env.state.phase === "loading" ? "\u6B63\u5728\u5206\u6790\u2026" : "\u751F\u6210\u4E2D\u2026";
5357
6527
  return what + " " + seconds + "s";
5358
6528
  }
6529
+ function releaseRenderedContent() {
6530
+ env.clearWaiting();
6531
+ env.clearRenderedTurns();
6532
+ for (var content of [env.translationSection.content, env.detailSection.content]) {
6533
+ releasePreviewFrames(content);
6534
+ content.textContent = "";
6535
+ }
6536
+ }
5359
6537
  function paint() {
6538
+ if (!env.panelOpen) {
6539
+ releaseRenderedContent();
6540
+ env.paintPill();
6541
+ return;
6542
+ }
5360
6543
  var streaming = env.state.phase === "loading" || env.state.phase === "streaming";
5361
6544
  var stage = env.state.stage;
5362
6545
  var selected = env.state.payload && env.state.payload.text ? env.state.payload.text : env.state.selection ? env.state.selection.text : "";
@@ -5555,6 +6738,8 @@ function createPanelController(env) {
5555
6738
  env.panel.style.top = Math.round(top) + "px";
5556
6739
  }
5557
6740
  return {
6741
+ applyShotTitle,
6742
+ releaseRenderedContent,
5558
6743
  openForSelection,
5559
6744
  currentSessionId,
5560
6745
  loadPanelSize,
@@ -5606,7 +6791,9 @@ function createPanelController(env) {
5606
6791
  // src/client/pill.js
5607
6792
  function createPillController(env) {
5608
6793
  function pillSnippet() {
5609
- 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) : "");
5610
6797
  text = text.replace(/\s+/g, " ").trim();
5611
6798
  if (!text) return "";
5612
6799
  return text.length > 12 ? text.slice(0, 12) + "\u2026" : text;
@@ -5642,7 +6829,7 @@ function createPillController(env) {
5642
6829
  var caret = env.panelOpen ? "\u25BE" : "\u25B4";
5643
6830
  if (env.pillCaret.textContent !== caret) env.pillCaret.textContent = caret;
5644
6831
  env.pill.title = [
5645
- 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",
5646
6833
  status.text,
5647
6834
  env.panelOpen ? "\u70B9\u51FB\u6536\u8D77\u5C0F\u7A97" : "\u70B9\u51FB\u56DE\u5230\u8FD9\u4E2A\u5C0F\u7A97",
5648
6835
  "\u53EF\u62D6\u52A8\uFF08\u62D6\u5230\u53F3\u4E0B\u89D2\u9644\u8FD1\u4F1A\u5438\u9644\u56DE\u53BB\uFF09"
@@ -6143,10 +7330,71 @@ function createPillController(env) {
6143
7330
  };
6144
7331
  }
6145
7332
 
7333
+ // src/chat-context.ts
7334
+ var CHAT_REQUEST_BYTES = 512 * 1024;
7335
+ var EMPTY_DIGEST = "811c9dc5";
7336
+ function readContextState(raw) {
7337
+ if (!raw || typeof raw !== "object") return null;
7338
+ const s = raw;
7339
+ if (s.version !== 1 || !Number.isSafeInteger(s.through) || s.through < 0 || !/^[a-f0-9]{8}$/.test(s.source) || !/^[a-f0-9]{8}$/.test(s.digest) || typeof s.summary !== "string" || s.summary.length > 12e3 || s.through > 0 && !s.summary.trim()) return null;
7340
+ if (s.background !== null && (!s.background || typeof s.background.transcript !== "string" || s.background.transcript.length > 2e5 || typeof s.background.marked !== "boolean")) return null;
7341
+ if (s.sessionId !== void 0 && (typeof s.sessionId !== "string" || s.sessionId.length > 500)) return null;
7342
+ return {
7343
+ version: 1,
7344
+ ...typeof s.sessionId === "string" ? { sessionId: s.sessionId } : {},
7345
+ source: s.source,
7346
+ through: s.through,
7347
+ digest: s.digest,
7348
+ summary: s.summary,
7349
+ background: s.background ? { transcript: s.background.transcript, marked: s.background.marked } : null
7350
+ };
7351
+ }
7352
+ function modelTurn(turn) {
7353
+ if (turn.contextExcluded) return {
7354
+ role: turn.role === "assistant" ? "assistant" : "user",
7355
+ text: turn.role === "assistant" ? "\uFF08\u8BE5\u8F6E\u672A\u751F\u6210\u56DE\u7B54\u3002\uFF09" : "\uFF08\u8BE5\u63D0\u95EE\u672A\u63D0\u4EA4\u7ED9\u6A21\u578B\uFF0C\u539F\u59CB\u5185\u5BB9\u4ECD\u4FDD\u5B58\u5728\u754C\u9762\u5386\u53F2\u4E2D\uFF1B\u9700\u8981\u65F6\u8BF7\u91CD\u65B0\u5F15\u7528\u5FC5\u8981\u90E8\u5206\u3002\uFF09"
7356
+ };
7357
+ return {
7358
+ role: turn.role === "assistant" ? "assistant" : "user",
7359
+ text: turn.sent || turn.text || "",
7360
+ ...turn.attachments?.length ? { attachments: turn.attachments } : {}
7361
+ };
7362
+ }
7363
+ function historyDigest(turns, initial = EMPTY_DIGEST) {
7364
+ let hash = Number.parseInt(initial, 16) >>> 0;
7365
+ for (const raw of turns) {
7366
+ const t = modelTurn(raw);
7367
+ const text = JSON.stringify([t.role, t.text, (t.attachments || []).map((a) => [a.kind, a.ref.attachmentId])]) + "\n";
7368
+ for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 16777619) >>> 0;
7369
+ }
7370
+ return hash.toString(16).padStart(8, "0");
7371
+ }
7372
+ function contextSource(session, text, context, initial) {
7373
+ return historyDigest([{ role: "user", text: JSON.stringify([session, text, context, initial]) }]);
7374
+ }
7375
+ function prepareChatHistory(turns, rawState) {
7376
+ const visible = turns.filter((t) => !t.hidden);
7377
+ let state = readContextState(rawState);
7378
+ if (state && (state.through > visible.length || historyDigest(visible.slice(0, state.through)) !== state.digest)) state = null;
7379
+ const start = state?.through || 0;
7380
+ const materials = [];
7381
+ const seen = /* @__PURE__ */ new Set();
7382
+ visible.forEach((turn, index) => {
7383
+ for (const item of turn.attachments || []) {
7384
+ const key = item.kind + ":" + item.ref.attachmentId;
7385
+ if (!seen.has(key)) {
7386
+ seen.add(key);
7387
+ materials.push({ kind: item.kind, ref: item.ref, turn: index });
7388
+ }
7389
+ }
7390
+ });
7391
+ return { history: visible.slice(start).map(modelTurn), historyStart: start, contextState: state, materials };
7392
+ }
7393
+
6146
7394
  // src/client/history.js
6147
7395
  function createHistoryController(env) {
6148
- var FOLD_HISTORY_KEEP_LATEST = true;
6149
7396
  var foldCount = 0;
7397
+ var shotTitlePendingKey = "";
6150
7398
  var HTML_VOID_TAGS = {
6151
7399
  area: 1,
6152
7400
  base: 1,
@@ -6426,16 +7674,6 @@ function createHistoryController(env) {
6426
7674
  src = src.replace(/```html[ \t]*\n([\s\S]*)$/, one);
6427
7675
  return src;
6428
7676
  }
6429
- function historyTextOf(turn, keepRaw) {
6430
- if (!turn) return "";
6431
- if (turn.role === "user") return env.sentTextOf(turn);
6432
- if (keepRaw) return turn.text;
6433
- if (typeof turn.foldedCache === "string") return turn.foldedCache;
6434
- if (turn.streaming === true) return turn.text;
6435
- var folded = foldForHistory(turn.text);
6436
- turn.foldedCache = folded;
6437
- return folded;
6438
- }
6439
7677
  function currentSurfaceTheme() {
6440
7678
  try {
6441
7679
  var bodyEl = document.body;
@@ -6464,6 +7702,7 @@ function createHistoryController(env) {
6464
7702
  function visibleRaw() {
6465
7703
  return env.state.raw;
6466
7704
  }
7705
+ var historySaveSequence = 0;
6467
7706
  function saveHistory(options) {
6468
7707
  var payload = env.state.payload;
6469
7708
  if (!payload || !env.state.cacheKey) return;
@@ -6474,28 +7713,155 @@ function createHistoryController(env) {
6474
7713
  if (turn.hidden === true) continue;
6475
7714
  turns.push(exported[i]);
6476
7715
  }
7716
+ var owner = env.state.turns;
7717
+ var ownerKey = env.state.cacheKey;
7718
+ var sequence = ++historySaveSequence;
7719
+ function noteSaved(error) {
7720
+ if (sequence !== historySaveSequence || env.state.turns !== owner || env.state.cacheKey !== ownerKey) return;
7721
+ var last = owner[owner.length - 1];
7722
+ if (!last || last.role !== "assistant") return;
7723
+ if (error) {
7724
+ last.notice = "\u5386\u53F2\u8BB0\u5F55\u4FDD\u5B58\u5931\u8D25\uFF0C\u5F53\u524D\u539F\u6587\u4ECD\u5728\u5C0F\u7A97\u4E2D\uFF1B\u5237\u65B0\u6216\u9000\u51FA\u524D\u8BF7\u5148\u590D\u5236\u91CD\u8981\u5185\u5BB9\u3002";
7725
+ last.noticeCode = "history-save-failed";
7726
+ } else if (last.noticeCode === "history-save-failed") {
7727
+ last.notice = "";
7728
+ last.noticeCode = "";
7729
+ } else return;
7730
+ renderTurns();
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
+ };
6477
7751
  try {
6478
7752
  fetch(HISTORY, {
6479
7753
  method: "POST",
6480
7754
  headers: { "content-type": "application/json" },
6481
- body: JSON.stringify({
6482
- key: env.state.cacheKey,
6483
- text: payload.text,
6484
- context: payload.context,
6485
- label: payload.label,
6486
- parts: env.state.parts,
6487
- turns,
6488
- toolDigest: env.state.toolDigest,
6489
- pinned: !!(options && options.pinned),
6490
- // 这段对话有没有解读,必须跟着条目落盘:回放时靠它决定画不画「翻译」一节。
6491
- // 不落盘的话,语音卡那条"跳过翻译与详解、直接追问"的对话下次开出来,
6492
- // 会顶着一张空翻译卡片("这次没有返回内容")——用户实测。
6493
- skipped: env.state.skipInterpretation === true
6494
- })
7755
+ body: JSON.stringify(savedBody)
7756
+ }).then(function(response) {
7757
+ if (!response.ok) throw new Error("HTTP " + response.status);
7758
+ return response.json();
7759
+ }).then(function(result) {
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
+ }
6495
7766
  }).catch(function() {
7767
+ noteSaved(true);
6496
7768
  });
6497
7769
  } catch (error) {
7770
+ noteSaved(true);
7771
+ }
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
+ }
6498
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;
6499
7865
  }
6500
7866
  function applyHistoryEntry(entry, source) {
6501
7867
  var parts = entry.parts || {};
@@ -6510,6 +7876,8 @@ function createHistoryController(env) {
6510
7876
  }
6511
7877
  var noInterpretation = !String(parts.translation || "").trim() && !String(parts.detail || "").trim();
6512
7878
  env.state.skipInterpretation = entry.skipped === true || entry.skipped === void 0 && noInterpretation && hasAskTurn;
7879
+ env.state.contextState = readContextState(entry.contextState);
7880
+ if (env.state.contextState && typeof env.state.contextState.sessionId === "string") env.panelSessionId = env.state.contextState.sessionId;
6513
7881
  env.state.turns = Array.isArray(entry.turns) ? entry.turns.slice() : [];
6514
7882
  env.clearQuotes();
6515
7883
  env.state.toolDigest = typeof entry.toolDigest === "string" ? entry.toolDigest : "";
@@ -6528,10 +7896,26 @@ function createHistoryController(env) {
6528
7896
  env.state.toolBusy = false;
6529
7897
  env.state.cacheState = source === "local" ? "local" : "history";
6530
7898
  env.state.chars = (env.state.parts.translation || "").length + (env.state.parts.detail || "").length;
6531
- 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
+ }
6532
7915
  env.applySectionTitle(env.state.kind, entry.text);
6533
7916
  hideHistoryList();
6534
7917
  env.paint();
7918
+ if (env.paintPill) env.paintPill();
6535
7919
  renderTurns();
6536
7920
  }
6537
7921
  function restoreFromHistory(cacheKey, onMiss) {
@@ -6541,8 +7925,7 @@ function createHistoryController(env) {
6541
7925
  settled = true;
6542
7926
  if (env.state.cacheKey === cacheKey && onMiss) onMiss();
6543
7927
  }
6544
- var timer = setTimeout(miss, 1500);
6545
- env.timers.push(timer);
7928
+ var timer = env.timerScope.later(miss, 1500);
6546
7929
  try {
6547
7930
  fetch(HISTORY + "?key=" + encodeURIComponent(cacheKey)).then(function(response) {
6548
7931
  return response.json();
@@ -6554,7 +7937,7 @@ function createHistoryController(env) {
6554
7937
  }
6555
7938
  if (data && data.ok === true && data.entry) {
6556
7939
  settled = true;
6557
- clearTimeout(timer);
7940
+ env.timerScope.clear(timer);
6558
7941
  applyHistoryEntry(data.entry, "history");
6559
7942
  return;
6560
7943
  }
@@ -6639,8 +8022,11 @@ function createHistoryController(env) {
6639
8022
  }
6640
8023
  function historyRow(item) {
6641
8024
  var row = el("div", "dsh-sel-historyrow");
6642
- var title = String(item.text || "").trim();
6643
- 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"));
6644
8030
  var del = el("button", "dsh-sel-historydel", "\u2715");
6645
8031
  del.type = "button";
6646
8032
  del.title = "\u5220\u9664\u8FD9\u6761\u8BB0\u5F55";
@@ -6651,7 +8037,8 @@ function createHistoryController(env) {
6651
8037
  deleteHistoryEntry(item, row);
6652
8038
  });
6653
8039
  row.appendChild(del);
6654
- 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);
6655
8042
  var metaNode = el("div", "dsh-sel-historymeta");
6656
8043
  metaNode.appendChild(el("span", null, meta));
6657
8044
  if (item.pinned) metaNode.appendChild(el("span", "dsh-sel-historypin", "\u5DF2\u5347\u683C"));
@@ -6709,7 +8096,7 @@ function createHistoryController(env) {
6709
8096
  });
6710
8097
  bar.appendChild(undo);
6711
8098
  env.historyList.appendChild(bar);
6712
- setTimeout(function() {
8099
+ env.timerScope.later(function() {
6713
8100
  if (bar.parentNode) bar.parentNode.removeChild(bar);
6714
8101
  }, 5e3);
6715
8102
  }
@@ -6841,24 +8228,81 @@ function createHistoryController(env) {
6841
8228
  if (Date.now() - stickAt < 150) return;
6842
8229
  env.state.follow = nearBottom();
6843
8230
  }
8231
+ var turnWindowSize = 40;
8232
+ var turnWindowKey = null;
8233
+ var renderedTurns = [];
8234
+ function clearRenderedTurns() {
8235
+ env.clearWaiting(env.chatLog);
8236
+ releasePreviewFrames(env.chatLog);
8237
+ env.chatLog.textContent = "";
8238
+ renderedTurns = [];
8239
+ turnWindowSize = 40;
8240
+ turnWindowKey = null;
8241
+ }
6844
8242
  function renderTurns(options) {
8243
+ if (!env.panelOpen) {
8244
+ clearRenderedTurns();
8245
+ return;
8246
+ }
6845
8247
  var force = !!(options && options.stick === true);
6846
- var stick = force || nearBottom();
8248
+ var stick = !(options && options.preservePosition) && (force || nearBottom());
6847
8249
  var visible = [];
6848
8250
  for (var i = 0; i < env.state.turns.length; i++) {
6849
8251
  if (env.state.turns[i].hidden !== true) visible.push(env.state.turns[i]);
6850
8252
  }
6851
- if (env.chatLog.children.length !== visible.length) {
8253
+ if (turnWindowKey !== env.state.cacheKey) {
8254
+ turnWindowKey = env.state.cacheKey;
8255
+ turnWindowSize = 40;
8256
+ }
8257
+ var earlier = Math.max(0, visible.length - turnWindowSize);
8258
+ visible = visible.slice(earlier);
8259
+ var sameWindow = renderedTurns.length === visible.length && visible.every(function(turn2, index) {
8260
+ return renderedTurns[index] === turn2;
8261
+ });
8262
+ var prependCount = visible.length - renderedTurns.length;
8263
+ var canPrepend = options && options.prepend && renderedTurns.length > 0 && prependCount > 0 && renderedTurns.every(function(turn2, index) {
8264
+ return visible[prependCount + index] === turn2;
8265
+ });
8266
+ if (canPrepend) {
8267
+ var moreButton = env.chatLog.children[0];
8268
+ var firstBubble = env.chatLog.children[1];
8269
+ if (earlier > 0) moreButton.textContent = "\u663E\u793A\u66F4\u65E9\u7684\u6D88\u606F\uFF08\u8FD8\u6709 " + earlier + " \u6761\uFF09";
8270
+ else env.chatLog.removeChild(moreButton);
8271
+ for (var p = 0; p < prependCount; p++) {
8272
+ var prepended = bubbleFor(visible[p]);
8273
+ prepended.__turn = visible[p];
8274
+ env.chatLog.insertBefore(prepended, firstBubble);
8275
+ }
8276
+ renderedTurns = visible.slice();
8277
+ } else if (!sameWindow || env.chatLog.children.length !== visible.length + (earlier > 0 ? 1 : 0)) {
6852
8278
  env.clearWaiting(env.chatLog);
8279
+ releasePreviewFrames(env.chatLog);
6853
8280
  env.chatLog.textContent = "";
8281
+ if (earlier > 0) {
8282
+ var more = el("button", "dsh-sel-expand", "\u663E\u793A\u66F4\u65E9\u7684\u6D88\u606F\uFF08\u8FD8\u6709 " + earlier + " \u6761\uFF09");
8283
+ more.type = "button";
8284
+ more.setAttribute("data-history-more", "1");
8285
+ listen(more, "click", function(event) {
8286
+ event.preventDefault();
8287
+ event.stopPropagation();
8288
+ var top = env.body.scrollTop;
8289
+ var height = env.body.scrollHeight;
8290
+ turnWindowSize += 40;
8291
+ renderTurns({ preservePosition: true, prepend: true });
8292
+ env.body.scrollTop = top + Math.max(0, env.body.scrollHeight - height);
8293
+ env.state.follow = false;
8294
+ });
8295
+ env.chatLog.appendChild(more);
8296
+ }
6854
8297
  for (var j = 0; j < visible.length; j++) {
6855
8298
  var bubble = bubbleFor(visible[j]);
6856
8299
  bubble.__turn = visible[j];
6857
8300
  env.chatLog.appendChild(bubble);
6858
8301
  }
8302
+ renderedTurns = visible.slice();
6859
8303
  var fresh = visible[visible.length - 1];
6860
8304
  if (fresh && fresh.role === "assistant" && fresh.asking === true && !fresh.text) {
6861
- env.showWaiting(env.chatLog.children[visible.length - 1], "\u6B63\u5728\u56DE\u7B54\u2026", {
8305
+ env.showWaiting(env.chatLog.children[env.chatLog.children.length - 1], "\u6B63\u5728\u56DE\u7B54\u2026", {
6862
8306
  startedAt: fresh.startedAt,
6863
8307
  effort: fresh.effort,
6864
8308
  effortOf: function() {
@@ -6870,7 +8314,7 @@ function createHistoryController(env) {
6870
8314
  });
6871
8315
  }
6872
8316
  } else if (visible.length > 0) {
6873
- var node = env.chatLog.children[visible.length - 1];
8317
+ var node = env.chatLog.children[env.chatLog.children.length - 1];
6874
8318
  var turn = visible[visible.length - 1];
6875
8319
  var painted = false;
6876
8320
  node.className = "dsh-sel-bubble " + (turn.role === "user" ? "dsh-sel-bubble-user" : "dsh-sel-bubble-bot") + (turn.error ? " dsh-sel-bubble-err" : "");
@@ -6964,13 +8408,35 @@ function createHistoryController(env) {
6964
8408
  }
6965
8409
  return strip;
6966
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
+ }
6967
8427
  function attachmentImage(ref) {
6968
8428
  var link = document.createElement("a");
6969
8429
  link.className = "dsh-sel-bqimgwrap";
6970
8430
  link.href = attachmentUrl("image", ref);
6971
8431
  link.target = "_blank";
6972
8432
  link.rel = "noreferrer noopener";
6973
- 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
+ });
6974
8440
  var img = document.createElement("img");
6975
8441
  img.className = "dsh-sel-bqimg";
6976
8442
  img.src = link.href;
@@ -7053,6 +8519,7 @@ function createHistoryController(env) {
7053
8519
  return seed.length > 1500 ? seed.slice(0, 1500) + "\u2026" : seed;
7054
8520
  }
7055
8521
  return {
8522
+ clearRenderedTurns,
7056
8523
  hideHistoryList,
7057
8524
  placeHistoryList,
7058
8525
  toggleHistoryList,
@@ -7085,13 +8552,6 @@ function createHistoryController(env) {
7085
8552
  loadHistoryEntry,
7086
8553
  seedHistoryFromExplanation,
7087
8554
  ensureChatFollow,
7088
- get FOLD_HISTORY_KEEP_LATEST() {
7089
- return FOLD_HISTORY_KEEP_LATEST;
7090
- },
7091
- set FOLD_HISTORY_KEEP_LATEST(value) {
7092
- FOLD_HISTORY_KEEP_LATEST = value;
7093
- },
7094
- historyTextOf,
7095
8555
  clearTransientNotice,
7096
8556
  saveHistory,
7097
8557
  applyToolEvent,
@@ -7158,7 +8618,7 @@ function createConversationController(env) {
7158
8618
  }
7159
8619
  return false;
7160
8620
  }
7161
- function ask(question) {
8621
+ async function ask(question) {
7162
8622
  var generating = env.state.asking || env.state.phase === "loading" || env.state.phase === "streaming";
7163
8623
  if (generating) {
7164
8624
  env.setStatus("\u8FD8\u5728\u751F\u6210\uFF0C\u8BF7\u7A0D\u5019\u2026\uFF08\u751F\u6210\u5B8C\u5C31\u80FD\u8FFD\u95EE\uFF09");
@@ -7168,6 +8628,17 @@ function createConversationController(env) {
7168
8628
  env.cancelVoice({ note: "\u5DF2\u505C\u6B62\u5F55\u97F3\uFF08\u8FD9\u6761\u5148\u53D1\u51FA\u53BB\uFF09" });
7169
8629
  }
7170
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
+ }
7171
8642
  var quotes = env.state.quotes.slice();
7172
8643
  var attachments = env.attachmentsPayload();
7173
8644
  if (!asked && quotes.length === 0 && attachments.length === 0 || !env.state.payload) return;
@@ -7206,13 +8677,15 @@ function createConversationController(env) {
7206
8677
  function sendAsk(asked, quotes, attachments, attachmentsNote) {
7207
8678
  var payload = env.state.payload;
7208
8679
  if (!payload) return;
8680
+ var firstShot = payload.kind === "shot" && env.state.turns.length === 0;
7209
8681
  var withAttachments = attachments || [];
7210
8682
  var sent = env.composeQuestion(quotes, asked);
7211
8683
  if (attachmentsNote) sent = attachmentsNote + "\n" + sent;
7212
8684
  if (env.state.turns.length === 0 && (env.state.raw.trim() || env.state.parts.translation || env.state.parts.detail)) {
7213
8685
  env.state.turns.push({ role: "assistant", text: env.seedHistoryFromExplanation(), seed: true, hidden: true });
7214
8686
  }
7215
- env.state.turns.push({ role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent, attachments: withAttachments });
8687
+ var questionTurn = { role: "user", text: asked || QUOTE_ONLY_QUESTION, quotes, sent, attachments: withAttachments };
8688
+ env.state.turns.push(questionTurn);
7216
8689
  var answerTurn = {
7217
8690
  role: "assistant",
7218
8691
  text: "",
@@ -7234,23 +8707,22 @@ function createConversationController(env) {
7234
8707
  if (env.state.sessionOpen !== void 0) {
7235
8708
  }
7236
8709
  var startedAt = Date.now();
7237
- var timer = setInterval(function() {
8710
+ var timer = env.timerScope.interval(function() {
7238
8711
  env.paintPill();
7239
8712
  env.setStatus(
7240
8713
  (env.state.toolBusy ? "\u6B63\u5728\u68C0\u7D22\u8D44\u6599\u2026 " : "\u8FFD\u95EE\u4E2D\u2026 ") + ((Date.now() - startedAt) / 1e3).toFixed(1) + "s"
7241
8714
  );
7242
8715
  updateWaitClocks();
7243
8716
  }, 200);
7244
- env.timers.push(timer);
7245
8717
  var historyTurns = env.state.turns.slice(0, env.state.turns.length - 2);
7246
- var history = historyTurns.map(function(turn, index) {
7247
- var keepRaw = env.FOLD_HISTORY_KEEP_LATEST && index === historyTurns.length - 1;
7248
- return {
7249
- role: turn.role,
7250
- text: env.historyTextOf(turn, keepRaw),
7251
- ...turn.attachments && turn.attachments.length > 0 ? { attachments: turn.attachments } : {}
7252
- };
7253
- });
8718
+ var initialExplanation = env.state.skipInterpretation ? "" : env.seedHistoryFromExplanation();
8719
+ var source = contextSource(env.panelOrCurrentSessionId(), payload.text.trim(), payload.context, initialExplanation);
8720
+ if (env.state.contextState && env.state.contextState.source !== source) env.state.contextState = null;
8721
+ var prepared = prepareChatHistory(historyTurns, env.state.contextState);
8722
+ env.state.contextState = prepared.contextState;
8723
+ var history = prepared.history;
8724
+ var requestTurns = env.state.turns;
8725
+ var requestKey = env.state.cacheKey;
7254
8726
  env.state.stopped = false;
7255
8727
  var askController = new AbortController();
7256
8728
  env.state.askRequest = {
@@ -7262,14 +8734,14 @@ function createConversationController(env) {
7262
8734
  }
7263
8735
  };
7264
8736
  function finish(error) {
7265
- clearInterval(timer);
8737
+ env.timerScope.clear(timer);
7266
8738
  env.state.asking = false;
7267
8739
  env.state.toolBusy = false;
7268
8740
  env.clearTransientNotice(answerTurn);
7269
8741
  answerTurn.asking = false;
7270
8742
  answerTurn.streaming = false;
7271
8743
  clearWaiting(env.chatLog);
7272
- env.saveHistory();
8744
+ if (env.state.turns === requestTurns && env.state.cacheKey === requestKey) env.saveHistory();
7273
8745
  env.syncPanelStage();
7274
8746
  refreshAskState();
7275
8747
  env.state.askRequest = null;
@@ -7286,24 +8758,42 @@ function createConversationController(env) {
7286
8758
  );
7287
8759
  env.askFocusAfterTurn();
7288
8760
  }
8761
+ var requestBody = JSON.stringify({
8762
+ text: payload.text,
8763
+ context: payload.context,
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
+ } : {},
8771
+ sessionId: env.panelOrCurrentSessionId(),
8772
+ question: sent,
8773
+ history,
8774
+ key: requestKey,
8775
+ historyStart: prepared.historyStart,
8776
+ contextState: prepared.contextState,
8777
+ materials: prepared.materials,
8778
+ initialExplanation,
8779
+ // 附件只带**引用**(图片/文件都已经在 host 的附件服务里),body 不会因为一张截图胖几 MB
8780
+ ...withAttachments.length > 0 ? { attachments: withAttachments } : {},
8781
+ // 网页输出要跟着当前深浅走:深色 Harness 里给一张白底页面会很刺眼
8782
+ ...env.state.webAnswer ? { webAnswer: true, theme: env.currentSurfaceTheme() } : {},
8783
+ ...env.state.modelChoice ? { provider: env.state.modelChoice.provider, model: env.state.modelChoice.model } : {},
8784
+ ...env.state.effort ? { effort: env.state.effort } : {},
8785
+ ...env.state.toolDigest ? { toolDigest: env.state.toolDigest } : {}
8786
+ });
8787
+ if (new TextEncoder().encode(requestBody).byteLength > CHAT_REQUEST_BYTES) {
8788
+ questionTurn.contextExcluded = true;
8789
+ answerTurn.contextExcluded = true;
8790
+ finish("\u672C\u8F6E\u6750\u6599\u8D85\u8FC7\u8BF7\u6C42\u5BB9\u91CF\uFF0C\u8BF7\u7F29\u5C0F\u63D0\u95EE\u6216\u5F15\u7528\u8303\u56F4\uFF1B\u539F\u6587\u4ECD\u4FDD\u7559\u5728\u5F53\u524D\u5386\u53F2\u4E2D");
8791
+ return;
8792
+ }
7289
8793
  fetch(API, {
7290
8794
  method: "POST",
7291
8795
  headers: { "content-type": "application/json" },
7292
- body: JSON.stringify({
7293
- text: payload.text,
7294
- context: payload.context,
7295
- label: payload.label,
7296
- sessionId: env.panelOrCurrentSessionId(),
7297
- question: sent,
7298
- history,
7299
- // 附件只带**引用**(图片/文件都已经在 host 的附件服务里),body 不会因为一张截图胖几 MB
7300
- ...withAttachments.length > 0 ? { attachments: withAttachments } : {},
7301
- // 网页输出要跟着当前深浅走:深色 Harness 里给一张白底页面会很刺眼
7302
- ...env.state.webAnswer ? { webAnswer: true, theme: env.currentSurfaceTheme() } : {},
7303
- ...env.state.modelChoice ? { provider: env.state.modelChoice.provider, model: env.state.modelChoice.model } : {},
7304
- ...env.state.effort ? { effort: env.state.effort } : {},
7305
- ...env.state.toolDigest ? { toolDigest: env.state.toolDigest } : {}
7306
- }),
8796
+ body: requestBody,
7307
8797
  signal: askController.signal
7308
8798
  }).then(function(response) {
7309
8799
  if (!response.ok) {
@@ -7329,7 +8819,18 @@ function createConversationController(env) {
7329
8819
  } catch (error) {
7330
8820
  continue;
7331
8821
  }
7332
- if (message.type === "delta") {
8822
+ if (message.type === "context-state") {
8823
+ if (!askController.signal.aborted && env.state.turns === requestTurns && env.state.cacheKey === requestKey) {
8824
+ env.state.contextState = readContextState(message.state);
8825
+ }
8826
+ } else if (message.type === "material-restored") {
8827
+ var material = message.attachment;
8828
+ if (!askController.signal.aborted && material && (material.kind === "image" || material.kind === "file") && material.ref && material.ref.attachmentId) {
8829
+ if (!questionTurn.attachments.some(function(a) {
8830
+ return a.kind === material.kind && a.ref.attachmentId === material.ref.attachmentId;
8831
+ })) questionTurn.attachments.push(material);
8832
+ }
8833
+ } else if (message.type === "delta") {
7333
8834
  env.clearTransientNotice(answerTurn);
7334
8835
  if (!answerTurn.text.trim()) answerTurn.asking = false;
7335
8836
  answerTurn.text += message.text;
@@ -7368,6 +8869,10 @@ function createConversationController(env) {
7368
8869
  }
7369
8870
  scheduleTurnsRender();
7370
8871
  } else if (message.type === "error") {
8872
+ if (message.code === "context-rejected") {
8873
+ questionTurn.contextExcluded = true;
8874
+ answerTurn.contextExcluded = true;
8875
+ }
7371
8876
  finish(message.message || "\u6A21\u578B\u8FD4\u56DE\u9519\u8BEF");
7372
8877
  }
7373
8878
  }
@@ -7499,6 +9004,8 @@ function createConversationController(env) {
7499
9004
  }
7500
9005
  }
7501
9006
  function runRequest(payload, cacheKey, options) {
9007
+ var requestTurns = env.state.turns;
9008
+ var requestKey = cacheKey;
7502
9009
  env.state.cacheState = "miss";
7503
9010
  if (env.state.request) env.state.request.abort();
7504
9011
  var controller = new AbortController();
@@ -7525,16 +9032,15 @@ function createConversationController(env) {
7525
9032
  env.state.startedAt = Date.now();
7526
9033
  env.state.elapsed = 0;
7527
9034
  env.paint();
7528
- var ticker = setInterval(function() {
9035
+ var ticker = env.timerScope.interval(function() {
7529
9036
  env.paintPill();
7530
9037
  if (env.state.phase === "loading" || env.state.phase === "streaming") {
7531
9038
  env.setStatus(env.progressText());
7532
9039
  updateWaitClocks();
7533
9040
  }
7534
9041
  }, 200);
7535
- env.timers.push(ticker);
7536
9042
  function stopTicker() {
7537
- clearInterval(ticker);
9043
+ env.timerScope.clear(ticker);
7538
9044
  }
7539
9045
  function fail(message) {
7540
9046
  if (env.state.phase === "done") return;
@@ -7568,7 +9074,7 @@ function createConversationController(env) {
7568
9074
  env.state.request = null;
7569
9075
  clearWaiting();
7570
9076
  if (cacheKey) env.cache.set(cacheKey, { parts: { ...env.state.parts }, turns: env.state.turns.slice() });
7571
- env.saveHistory();
9077
+ if (env.state.turns === requestTurns && env.state.cacheKey === requestKey) env.saveHistory();
7572
9078
  env.paint();
7573
9079
  }
7574
9080
  var dirty = false;
@@ -9123,6 +10629,101 @@ function createVoiceController(env) {
9123
10629
  };
9124
10630
  }
9125
10631
 
10632
+ // src/client/memory.js
10633
+ function createResultCache(maxEntries = 20, maxBytes = 8 * 1024 * 1024) {
10634
+ const entries = /* @__PURE__ */ new Map();
10635
+ let textBytes = 0;
10636
+ let disposed = false;
10637
+ function remove(key) {
10638
+ const entry = entries.get(key);
10639
+ if (!entry) return;
10640
+ textBytes -= entry.bytes;
10641
+ entries.delete(key);
10642
+ }
10643
+ const cache = {
10644
+ get size() {
10645
+ return entries.size;
10646
+ },
10647
+ // Conservative UTF-16 size of serialized text, not a measurement of JS heap size.
10648
+ get textBytes() {
10649
+ return textBytes;
10650
+ },
10651
+ keys() {
10652
+ return entries.keys();
10653
+ },
10654
+ get(key) {
10655
+ const entry = entries.get(key);
10656
+ if (!entry) return void 0;
10657
+ entries.delete(key);
10658
+ entries.set(key, entry);
10659
+ return JSON.parse(entry.json);
10660
+ },
10661
+ set(key, value) {
10662
+ if (disposed) return cache;
10663
+ remove(key);
10664
+ let json;
10665
+ try {
10666
+ json = JSON.stringify(value);
10667
+ } catch {
10668
+ return cache;
10669
+ }
10670
+ if (typeof json !== "string") return cache;
10671
+ const bytes = (String(key).length + json.length) * 2;
10672
+ if (bytes > maxBytes || maxEntries <= 0) return cache;
10673
+ entries.set(key, { json, bytes });
10674
+ textBytes += bytes;
10675
+ while (entries.size > maxEntries || textBytes > maxBytes) remove(entries.keys().next().value);
10676
+ return cache;
10677
+ },
10678
+ clear() {
10679
+ entries.clear();
10680
+ textBytes = 0;
10681
+ },
10682
+ dispose() {
10683
+ disposed = true;
10684
+ cache.clear();
10685
+ }
10686
+ };
10687
+ return cache;
10688
+ }
10689
+ function createTimerScope() {
10690
+ const active = /* @__PURE__ */ new Set();
10691
+ let disposed = false;
10692
+ return {
10693
+ get size() {
10694
+ return active.size;
10695
+ },
10696
+ later(fn, ms) {
10697
+ if (disposed) return null;
10698
+ const id = setTimeout(() => {
10699
+ active.delete(id);
10700
+ if (!disposed) fn();
10701
+ }, ms);
10702
+ active.add(id);
10703
+ return id;
10704
+ },
10705
+ interval(fn, ms) {
10706
+ if (disposed) return null;
10707
+ const id = setInterval(fn, ms);
10708
+ active.add(id);
10709
+ return id;
10710
+ },
10711
+ clear(id) {
10712
+ clearTimeout(id);
10713
+ clearInterval(id);
10714
+ active.delete(id);
10715
+ },
10716
+ dispose() {
10717
+ disposed = true;
10718
+ active.forEach((id) => {
10719
+ clearTimeout(id);
10720
+ clearInterval(id);
10721
+ });
10722
+ active.clear();
10723
+ }
10724
+ };
10725
+ }
10726
+
9126
10727
  // src/client/index.js
9127
10728
  function apply(ctx) {
9128
10729
  var state = {
@@ -9235,7 +10836,8 @@ function apply(ctx) {
9235
10836
  /** 升格请求进行中。 */
9236
10837
  promoting: false
9237
10838
  };
9238
- var cache = /* @__PURE__ */ new Map();
10839
+ var cache = createResultCache();
10840
+ var timerScope = createTimerScope();
9239
10841
  var styleEl = el("style");
9240
10842
  styleEl.setAttribute("data-plugin", "@yfwu2020/dsh-selection-explain");
9241
10843
  styleEl.textContent = CSS;
@@ -9634,6 +11236,18 @@ function apply(ctx) {
9634
11236
  }
9635
11237
  });
9636
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
+ },
9637
11251
  get layer() {
9638
11252
  return layer;
9639
11253
  },
@@ -9670,8 +11284,8 @@ function apply(ctx) {
9670
11284
  get frameTouchedAt() {
9671
11285
  return previewController.frameTouchedAt;
9672
11286
  },
9673
- get timers() {
9674
- return timers;
11287
+ get timerScope() {
11288
+ return timerScope;
9675
11289
  },
9676
11290
  get voice() {
9677
11291
  return voiceController.voice;
@@ -9696,6 +11310,9 @@ function apply(ctx) {
9696
11310
  }
9697
11311
  });
9698
11312
  var quotesController = createQuotesController({
11313
+ get foldForHistory() {
11314
+ return historyController.foldForHistory;
11315
+ },
9699
11316
  get state() {
9700
11317
  return state;
9701
11318
  },
@@ -9729,8 +11346,8 @@ function apply(ctx) {
9729
11346
  get currentSessionId() {
9730
11347
  return panelController.currentSessionId;
9731
11348
  },
9732
- get timers() {
9733
- return timers;
11349
+ get timerScope() {
11350
+ return timerScope;
9734
11351
  }
9735
11352
  });
9736
11353
  var previewController = createPreviewController({
@@ -9750,7 +11367,15 @@ function apply(ctx) {
9750
11367
  return later;
9751
11368
  }
9752
11369
  });
11370
+ var lightboxController = createLightboxController({
11371
+ get layer() {
11372
+ return layer;
11373
+ }
11374
+ });
9753
11375
  var panelController = createPanelController({
11376
+ get timerScope() {
11377
+ return timerScope;
11378
+ },
9754
11379
  get panelSessionId() {
9755
11380
  return panelSessionId;
9756
11381
  },
@@ -9787,6 +11412,12 @@ function apply(ctx) {
9787
11412
  get renderTurns() {
9788
11413
  return historyController.renderTurns;
9789
11414
  },
11415
+ get clearRenderedTurns() {
11416
+ return historyController.clearRenderedTurns;
11417
+ },
11418
+ get clearWaiting() {
11419
+ return conversationController.clearWaiting;
11420
+ },
9790
11421
  get sendAsk() {
9791
11422
  return conversationController.sendAsk;
9792
11423
  },
@@ -9865,6 +11496,9 @@ function apply(ctx) {
9865
11496
  get closeSettings() {
9866
11497
  return settingsController.closeSettings;
9867
11498
  },
11499
+ get closeLightbox() {
11500
+ return lightboxController.close;
11501
+ },
9868
11502
  get hideHistoryList() {
9869
11503
  return historyController.hideHistoryList;
9870
11504
  },
@@ -10046,12 +11680,21 @@ function apply(ctx) {
10046
11680
  }
10047
11681
  });
10048
11682
  var historyController = createHistoryController({
11683
+ set panelSessionId(value) {
11684
+ panelSessionId = value;
11685
+ },
11686
+ get applyShotTitle() {
11687
+ return panelController.applyShotTitle;
11688
+ },
10049
11689
  get sentTextOf() {
10050
11690
  return quotesController.sentTextOf;
10051
11691
  },
10052
11692
  get panel() {
10053
11693
  return panel;
10054
11694
  },
11695
+ get previewImage() {
11696
+ return lightboxController.open;
11697
+ },
10055
11698
  get state() {
10056
11699
  return state;
10057
11700
  },
@@ -10076,8 +11719,11 @@ function apply(ctx) {
10076
11719
  get paint() {
10077
11720
  return panelController.paint;
10078
11721
  },
10079
- get timers() {
10080
- return timers;
11722
+ get paintPill() {
11723
+ return pillController.paintPill;
11724
+ },
11725
+ get timerScope() {
11726
+ return timerScope;
10081
11727
  },
10082
11728
  get panelOpen() {
10083
11729
  return panelOpen;
@@ -10177,6 +11823,9 @@ function apply(ctx) {
10177
11823
  get composeQuestion() {
10178
11824
  return quotesController.composeQuestion;
10179
11825
  },
11826
+ get attachEnsureUploaded() {
11827
+ return attachController.ensureUploaded;
11828
+ },
10180
11829
  get attachmentsPayload() {
10181
11830
  return attachController.payload;
10182
11831
  },
@@ -10207,14 +11856,8 @@ function apply(ctx) {
10207
11856
  get paintPill() {
10208
11857
  return pillController.paintPill;
10209
11858
  },
10210
- get timers() {
10211
- return timers;
10212
- },
10213
- get FOLD_HISTORY_KEEP_LATEST() {
10214
- return historyController.FOLD_HISTORY_KEEP_LATEST;
10215
- },
10216
- get historyTextOf() {
10217
- return historyController.historyTextOf;
11859
+ get timerScope() {
11860
+ return timerScope;
10218
11861
  },
10219
11862
  get clearTransientNotice() {
10220
11863
  return historyController.clearTransientNotice;
@@ -10317,6 +11960,9 @@ function apply(ctx) {
10317
11960
  get attachInput() {
10318
11961
  return attachInput;
10319
11962
  },
11963
+ get previewImage() {
11964
+ return lightboxController.open;
11965
+ },
10320
11966
  get setStatus() {
10321
11967
  return panelController.setStatus;
10322
11968
  },
@@ -10324,6 +11970,48 @@ function apply(ctx) {
10324
11970
  return conversationController.refreshAskState;
10325
11971
  }
10326
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
+ });
10327
12015
  var modelsController = createModelsController({
10328
12016
  get state() {
10329
12017
  return state;
@@ -10444,6 +12132,10 @@ function apply(ctx) {
10444
12132
  else layer.removeAttribute("data-full-view");
10445
12133
  }
10446
12134
  }
12135
+ function ensureLayer() {
12136
+ if (typeof document === "undefined" || !document.body) return;
12137
+ if (layer.parentNode !== document.body) document.body.appendChild(layer);
12138
+ }
10447
12139
  function mountFloatingLayer(node) {
10448
12140
  if (node) {
10449
12141
  if (layer.parentNode !== document.body) document.body.appendChild(layer);
@@ -10488,7 +12180,6 @@ function apply(ctx) {
10488
12180
  var STREAM_RENDER_MS = 90;
10489
12181
  var turnsRenderPending = false;
10490
12182
  var lastTurnsRenderAt = 0;
10491
- var timers = [];
10492
12183
  var PILL_POLL_MIN = 2e3;
10493
12184
  var PILL_POLL_MAX = 6e4;
10494
12185
  var pillPollDelay = PILL_POLL_MIN;
@@ -10508,9 +12199,7 @@ function apply(ctx) {
10508
12199
  var pillObserver = null;
10509
12200
  var waitList = [];
10510
12201
  function later(fn, ms) {
10511
- var id = setTimeout(fn, ms);
10512
- timers.push(id);
10513
- return id;
12202
+ return timerScope.later(fn, ms);
10514
12203
  }
10515
12204
  var offBridgeMessage = listen(window, "message", function(event) {
10516
12205
  var data = event && event.data;
@@ -10942,6 +12631,15 @@ function apply(ctx) {
10942
12631
  panelController.savePanelSize();
10943
12632
  });
10944
12633
  window.__dshSelectionExplain = {
12634
+ /** 截图提问的状态(调试/自动化)。 */
12635
+ shot: function() {
12636
+ return shotController.state();
12637
+ },
12638
+ /** 手动起一次截图(等同按 ⌘⇧X)。 */
12639
+ shotStart: function() {
12640
+ shotController.capture();
12641
+ return true;
12642
+ },
10945
12643
  /**
10946
12644
  * 语音卡(选中文字 + 输入法正在语音输入 → 就地追问)的状态。
10947
12645
  * 给自动化验证用:能不能弹、有没有焦点、几何对不对,全在这里读。
@@ -11080,6 +12778,14 @@ function apply(ctx) {
11080
12778
  turns: function() {
11081
12779
  return state.turns.slice();
11082
12780
  },
12781
+ memory: function() {
12782
+ return {
12783
+ cacheEntries: cache.size,
12784
+ cacheTextBytes: cache.textBytes,
12785
+ timers: timerScope.size,
12786
+ previewFrames: Object.keys(previewFrames).length
12787
+ };
12788
+ },
11083
12789
  /** 自检用:当前待发送的引用(引用区里那几张卡片)。 */
11084
12790
  quotes: function() {
11085
12791
  return state.quotes.map(function(quote2) {
@@ -11424,6 +13130,7 @@ function apply(ctx) {
11424
13130
  state.selection = null;
11425
13131
  state.bridgeActive = false;
11426
13132
  state.parts = { translation: "", detail: "" };
13133
+ state.contextState = null;
11427
13134
  state.turns = [];
11428
13135
  quotesController.clearQuotes();
11429
13136
  state.raw = "";
@@ -11446,11 +13153,110 @@ function apply(ctx) {
11446
13153
  });
11447
13154
  }
11448
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
+ }
11449
13245
  ctx.effect(function() {
11450
13246
  if (typeof console !== "undefined" && console.log) {
11451
13247
  console.log("[dsh-selection-explain] \u5212\u8BCD\u89E3\u8BFB\u5DF2\u5C31\u7EEA\uFF08\u9009\u4E2D\u6587\u5B57\u8BD5\u8BD5\uFF09");
11452
13248
  }
11453
13249
  return function() {
13250
+ panelOpen = false;
13251
+ timerScope.dispose();
13252
+ cache.dispose();
13253
+ voiceCardController.closeVCard(false);
13254
+ lightboxController.dispose();
13255
+ attachController.clear();
13256
+ shotController.stop();
13257
+ shotController.clearMarkers();
13258
+ panelController.releaseRenderedContent();
13259
+ if (state.askRequest) state.askRequest.abort();
11454
13260
  offPopEnd();
11455
13261
  offQuotePopEnd();
11456
13262
  offMouseUp();
@@ -11538,13 +13344,17 @@ function apply(ctx) {
11538
13344
  }
11539
13345
  });
11540
13346
  previewController.bridged.clear();
13347
+ previewController.frameTouchedAt.clear();
11541
13348
  state.turns = [];
11542
13349
  state.quotes = [];
13350
+ state.selection = null;
13351
+ state.quoteSelection = null;
13352
+ state.payload = null;
13353
+ state.parts = { translation: "", detail: "" };
13354
+ state.raw = "";
11543
13355
  if (state.request) state.request.abort();
11544
- for (var i = 0; i < timers.length; i++) clearTimeout(timers[i]);
11545
13356
  if (pillIdleId) clearTimeout(pillIdleId);
11546
13357
  if (pillController.pillStarTimer) clearTimeout(pillController.pillStarTimer);
11547
- cache.clear();
11548
13358
  if (layer.parentNode) layer.parentNode.removeChild(layer);
11549
13359
  if (button.parentNode) button.parentNode.removeChild(button);
11550
13360
  if (quoteButton.parentNode) quoteButton.parentNode.removeChild(quoteButton);
@@ -11559,5 +13369,5 @@ function apply(ctx) {
11559
13369
  };
11560
13370
  }, "@yfwu2020/dsh-selection-explain: \u5212\u8BCD\u89E3\u8BFB UI");
11561
13371
  }
11562
- var inject = ["slots"];
13372
+ var inject = ["slots", "shortcuts"];
11563
13373
  return module.exports;}})