@yfwu2020/dsh-selection-explain 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,6 +113,9 @@
113
113
  - **只挂引用、一个字不写也能发**:这时用一句兜底提问("就上面引用的文字,说说它在这里是什么意思。"),不会空发。
114
114
  - **只属于下一条提问**:发出即清空;换一段新的选中文字、或从「最近」回放另一段对话时也会清掉(那些引用属于上一次对话)。
115
115
  - **小窗开着时浮标就是「引用」**:要解读新的一段,先按 `Esc` 收起小窗再划词——避免两个小窗抢同一处选区。
116
+ - **点完「✦ 解读」那行高亮会立刻收掉**(`Esc`、重开也一样):小窗顶部本来就写着选中文字,留着高亮反而碍事——
117
+ 小窗亮着时它是「活的选区」,在输入框里按一下方向键就会又冒出一个「❝ 引用」,指的却是刚才解读的那段。
118
+ 要引用它,重新划一次即可(那时浮标才是「❝ 引用」)。
116
119
  - 引用会**进历史、也进升格**:回放这段对话时引用块跟着回来(以拼好的原文形式),升格出来的正式会话里同样带着。
117
120
 
118
121
  ### 输出偏好:Markdown ↔ 网页
@@ -479,9 +482,15 @@ git tag v0.2.0 && git push origin v0.2.0
479
482
 
480
483
  npm 侧只需配一次:包设置 → **Trusted Publisher** → GitHub Actions,填 `yfwu2020` / `dsh-selection-explain` / `publish.yml`,并勾选允许 **`npm publish`**(默认只允许 `npm stage publish`)。
481
484
 
482
- > 注意:`devDependencies` 里的 `@deepseek-ai/*` 钉在 **`0.1.5-rc.3`**(与插件开发所依据的运行时一致)。
485
+ > 注意:`devDependencies` 里的 `@deepseek-ai/*` 钉在 **`0.1.5-rc.3`**(CI 构建校验用的就是这份类型)。
483
486
  > npm 上这些包的 `latest` 还停在 `0.0.1-rc.x`,API 更旧(缺 `WebServer`、`createSystemMessage` 等),
484
487
  > 用 `latest` 会编译失败——升级运行时时要同步改这里。
488
+ >
489
+ > **本机运行时通常比它新**(如 `0.1.7-rc.2`),两者 API 会漂移:例如 `createSystemMessage` 在
490
+ > `0.1.5-rc.3` 是 `(text, plugin)` 两个必填参数,在 `0.1.7-rc.x` 只剩 `(text)`。此时**别只按本机类型写**,
491
+ > 走一层宽松别名兼容两边(见 `src/index.ts` 的 `systemMessageOf`)——v0.4.0 就是漏了这一步,
492
+ > 本机构建全绿、CI 构建报 TS2554,整条发布卡在第一步(与 Trusted Publisher 无关)。
493
+ > 想提前发现:把仓库复制到临时目录、`npm install --no-save`(会装到 `0.1.5-rc.3`)再 `bash scripts/build.sh`。
485
494
 
486
495
  ## 目录
487
496
 
package/lib/client.js CHANGED
@@ -3155,14 +3155,45 @@ window.__ModuleLoader__.load({
3155
3155
  }
3156
3156
  }
3157
3157
 
3158
+ /**
3159
+ * 点完「✦ 解读」就把**本文档**那次选区收掉。
3160
+ *
3161
+ * 为什么必须收:小窗一开,浮标就切成「❝ 引用」,而引用浮标是按"有没有活选区"判定的 ——
3162
+ * 留着解读用的那次选区,用户在小窗里敲字、按一下方向键(keyup 会触发一次检查),
3163
+ * 就会在小窗上方又冒出一个「❝ 引用」,指的是他刚才解读的那段文字,莫名其妙。
3164
+ *
3165
+ * 为什么能安全收掉:解读要的东西**在 mouseup 那一刻就都抓好了** ——
3166
+ * `state.selection` 里存的是文字副本 + **克隆的 Range**,上下文也是从克隆 Range 采的
3167
+ * (checkSelection → openForSelection),之后的请求、缓存 key、展开详解、历史回放、
3168
+ * 升格都不再读活选区。唯一的代价是那行字的高亮没了 —— 而小窗顶部本来就写着选中文字。
3169
+ *
3170
+ * 顺序要紧:必须在 openForSelection **采完上下文之后**调用 —— 万一环境没有 cloneRange,
3171
+ * 那边会退回读活选区(老浏览器),先清就采不到了。
3172
+ */
3173
+ function clearDocSelection() {
3174
+ try {
3175
+ var selection = window.getSelection && window.getSelection()
3176
+ if (selection && typeof selection.removeAllRanges === 'function' && selection.rangeCount > 0) {
3177
+ selection.removeAllRanges()
3178
+ }
3179
+ } catch (error) {
3180
+ /* 桩环境/老浏览器没有 removeAllRanges:留着就留着,不影响主流程 */
3181
+ }
3182
+ }
3183
+
3158
3184
  function openForSelection(selection) {
3159
3185
  // 侧边栏网页里的选区:父页面读不到那个文档,上下文只能由帧内桥就地采好带过来
3160
3186
  if (selection.source === 'iframe') {
3161
3187
  openPanelWith(selection.text, selection.context || '', selection.label, selection.rect, selection.keyContext)
3188
+ // 帧内那条高亮清不掉(不透明源,父页面碰不到它的选区)——只能让浮标自己收着;
3189
+ // 好在帧内的桥按指纹去重,同一条选区不会反复上报。
3162
3190
  return
3163
3191
  }
3164
3192
  var contextInfo = collectContext(selection.range ? rangeSelection(selection.range) : window.getSelection())
3165
3193
  openPanelWith(selection.text, contextInfo.context, contextInfo.label, selection.rect, contextInfo.keyContext)
3194
+ clearDocSelection()
3195
+ // 选区连同它的记录一起作废:之后要引用就重新划一段(那会走 checkQuoteSelection)
3196
+ state.selection = null
3166
3197
  }
3167
3198
 
3168
3199
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yfwu2020/dsh-selection-explain",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "DSH 划词解读:选中文字浮出按钮,点击弹窗流式显示专业中英翻译 + 该文字在当前上下文中的含义详解",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -161,6 +161,17 @@ function mouseUpOn(node) {
161
161
  function clickOn(node) {
162
162
  node.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }))
163
163
  }
164
+ /**
165
+ * 像用户那样点一下:mousedown → mouseup → click。
166
+ * 只派发 click 是"程序化点击",浏览器不会走聚焦/选区那套默认行为 ——
167
+ * 而浮标靠 mousedown 的 preventDefault 保住选区,正是那条路要验。
168
+ */
169
+ function realClick(node) {
170
+ var opts = { bubbles: true, cancelable: true, view: window, detail: 1 }
171
+ node.dispatchEvent(new MouseEvent("mousedown", opts))
172
+ node.dispatchEvent(new MouseEvent("mouseup", opts))
173
+ node.dispatchEvent(new MouseEvent("click", opts))
174
+ }
164
175
  /** 浮标是不是**最上层**(z-index 低于面板时 elementFromPoint 会返回面板)。 */
165
176
  function onTop(node) {
166
177
  var rect = node.getBoundingClientRect()
@@ -345,6 +356,43 @@ async function main() {
345
356
  check('主界面引用标成会话里的', !!picked8 && picked8.session === true, picked8 && String(picked8.session))
346
357
  }
347
358
 
359
+ // ⑨ 点「✦ 解读」后立刻收掉本文档选区(用户报的:小窗亮起时旧选区又触发「❝ 引用」浮标)
360
+ {
361
+ hook.close()
362
+ await sleep(80)
363
+ var planBtn = document.querySelector('.dsh-sel-btn:not(.dsh-sel-quotebtn)')
364
+ var docp2 = document.getElementById('docp')
365
+ selectIn(docp2, 'the migration ran long')
366
+ mouseUpOn(docp2)
367
+ var planShown = await waitFor(function () {
368
+ return planBtn && planBtn.style.display !== 'none' && planBtn.getBoundingClientRect().width > 0
369
+ }, 1500)
370
+ check('主界面划词:浮出「✦ 解读」', planShown === true, planBtn ? planBtn.style.display : '未找到按钮')
371
+ if (planShown) {
372
+ realClick(planBtn)
373
+ // ⚠️ 这里**同步**检查,一下都不 await:开窗自动聚焦那一下是异步的(要等输入框可见),
374
+ // 所以"选区这会儿已经没了"只可能是我们自己在点的时候清的 —— 它骗不过这一条。
375
+ var live = window.getSelection()
376
+ check(
377
+ '点「解读」后选区立刻收掉(同步检查,不等聚焦那一下)',
378
+ !!live && (live.rangeCount === 0 || live.isCollapsed === true),
379
+ live ? 'rangeCount=' + live.rangeCount + ' collapsed=' + live.isCollapsed : 'null',
380
+ )
381
+ check('选区记录也一并作废(不再拿它当"当前选区")', hook.selection() === null, JSON.stringify(hook.selection()))
382
+ await sleep(120)
383
+ check('旧选区没有让「❝ 引用」浮标复活', !onTop(quoteBtn), String(quoteBtn.style.display))
384
+ // 用户报症状的那一步:小窗亮着时在输入框里按方向键(keyup 会触发一次选区检查)
385
+ var askBox = document.querySelector('.dsh-sel-askbox')
386
+ if (askBox) {
387
+ askBox.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft', bubbles: true }))
388
+ await sleep(120)
389
+ check('开窗后按方向键也不再冒出引用浮标', !onTop(quoteBtn), String(quoteBtn.style.display))
390
+ }
391
+ hook.close()
392
+ await sleep(60)
393
+ }
394
+ }
395
+
348
396
  finish()
349
397
  }
350
398
 
@@ -1044,11 +1044,16 @@ const textEl = new FakeEl('span')
1044
1044
  textEl._text = 'Dev: the migration ran long, so we ship Wednesday. PM: fine, freeze features.'
1045
1045
  container.appendChild(textEl)
1046
1046
  body.appendChild(container)
1047
+ /** 选区桩:记下 removeAllRanges 有没有被调用(点「解读」后要把本文档选区收掉)。 */
1048
+ const clearedSelections = { count: 0 }
1047
1049
  const selection = {
1048
1050
  isCollapsed: false,
1049
1051
  rangeCount: 1,
1050
1052
  toString: () => 'the migration ran long',
1051
1053
  getRangeAt: () => documentStub.createRange(),
1054
+ removeAllRanges() {
1055
+ clearedSelections.count += 1
1056
+ },
1052
1057
  }
1053
1058
  windowStub.getSelection = () => selection
1054
1059
  documentStub.dispatch('mouseup', { target: body })
@@ -1092,6 +1097,17 @@ const chatLog = Array.from(walk(panel)).find((node) => node.className === 'dsh-s
1092
1097
  const askRow = Array.from(walk(panel)).find((node) => node.className === 'dsh-sel-ask')
1093
1098
  assert('弹窗打开', !!panel && panel.style.display === 'flex')
1094
1099
  assert('弹窗显示选中文字', !!panel && textOf(panel).indexOf('the migration ran long') >= 0)
1100
+ // 点完「解读」立刻把本文档选区收掉:留着它,小窗开着时按个方向键又会冒出一个「❝ 引用」,
1101
+ // 指的是刚才解读的那段文字(解读要的文字与上下文在 mouseup 那刻就抓好了,见 openForSelection)
1102
+ assert('点「解读」后本文档选区被收掉', clearedSelections.count >= 1, `removeAllRanges 调用 ${clearedSelections.count} 次`)
1103
+ {
1104
+ const quoteBtnNode = Array.from(walk(mount)).find((n) => String(n.className).indexOf('dsh-sel-quotebtn') >= 0)
1105
+ assert(
1106
+ '点「解读」后引用浮标不出现(旧选区不该复活它)',
1107
+ !quoteBtnNode || quoteBtnNode.style.display === 'none',
1108
+ quoteBtnNode ? String(quoteBtnNode.style.display) : '浮标节点还没建出来(= 没显示)',
1109
+ )
1110
+ }
1095
1111
 
1096
1112
  const hook = windowStub.__dshSelectionExplain
1097
1113
  assert('自检钩子可用', !!hook && typeof hook.state === 'function')
@@ -3155,14 +3155,45 @@ window.__ModuleLoader__.load({
3155
3155
  }
3156
3156
  }
3157
3157
 
3158
+ /**
3159
+ * 点完「✦ 解读」就把**本文档**那次选区收掉。
3160
+ *
3161
+ * 为什么必须收:小窗一开,浮标就切成「❝ 引用」,而引用浮标是按"有没有活选区"判定的 ——
3162
+ * 留着解读用的那次选区,用户在小窗里敲字、按一下方向键(keyup 会触发一次检查),
3163
+ * 就会在小窗上方又冒出一个「❝ 引用」,指的是他刚才解读的那段文字,莫名其妙。
3164
+ *
3165
+ * 为什么能安全收掉:解读要的东西**在 mouseup 那一刻就都抓好了** ——
3166
+ * `state.selection` 里存的是文字副本 + **克隆的 Range**,上下文也是从克隆 Range 采的
3167
+ * (checkSelection → openForSelection),之后的请求、缓存 key、展开详解、历史回放、
3168
+ * 升格都不再读活选区。唯一的代价是那行字的高亮没了 —— 而小窗顶部本来就写着选中文字。
3169
+ *
3170
+ * 顺序要紧:必须在 openForSelection **采完上下文之后**调用 —— 万一环境没有 cloneRange,
3171
+ * 那边会退回读活选区(老浏览器),先清就采不到了。
3172
+ */
3173
+ function clearDocSelection() {
3174
+ try {
3175
+ var selection = window.getSelection && window.getSelection()
3176
+ if (selection && typeof selection.removeAllRanges === 'function' && selection.rangeCount > 0) {
3177
+ selection.removeAllRanges()
3178
+ }
3179
+ } catch (error) {
3180
+ /* 桩环境/老浏览器没有 removeAllRanges:留着就留着,不影响主流程 */
3181
+ }
3182
+ }
3183
+
3158
3184
  function openForSelection(selection) {
3159
3185
  // 侧边栏网页里的选区:父页面读不到那个文档,上下文只能由帧内桥就地采好带过来
3160
3186
  if (selection.source === 'iframe') {
3161
3187
  openPanelWith(selection.text, selection.context || '', selection.label, selection.rect, selection.keyContext)
3188
+ // 帧内那条高亮清不掉(不透明源,父页面碰不到它的选区)——只能让浮标自己收着;
3189
+ // 好在帧内的桥按指纹去重,同一条选区不会反复上报。
3162
3190
  return
3163
3191
  }
3164
3192
  var contextInfo = collectContext(selection.range ? rangeSelection(selection.range) : window.getSelection())
3165
3193
  openPanelWith(selection.text, contextInfo.context, contextInfo.label, selection.rect, contextInfo.keyContext)
3194
+ clearDocSelection()
3195
+ // 选区连同它的记录一起作废:之后要引用就重新划一段(那会走 checkQuoteSelection)
3196
+ state.selection = null
3166
3197
  }
3167
3198
 
3168
3199
  /**