dsh-quote-annotate-pro 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  本项目的所有重要变更都记在这里。版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## 0.3.2 — 输入框装饰支持一条消息里多条批注
6
+
7
+ - **修复**:输入框里的「隐藏位置行 + 紧凑引用块」装饰改为**状态机逐行扫描**。此前实现遇到位置行后的第一个空行就 `break`,
8
+ 导致同一条消息里**只有第一条批注被装饰**,第二条开始位置行 `【批注 2 · …】` 仍然露在输入框里(用户实测截图确认)
9
+ - 现在按 `位置行 → 正文行 → 空行收尾` 循环扫描,多条批注全部折叠为紧凑引用块;空行、非单行内容都不会误判
10
+ - 编辑器若在 `contenteditable` 里再包一层容器,会自动向下一层找行元素(兼容不同 Lexical 挂载结构)
11
+ - 诊断通道改进:ping 失败后每 5 秒自动重试一次(此前一次探测失败就永久静默,接收端后启动时看不到任何事件)
12
+ - `npm test` 8/8、`node --check`、`check-manifest` 全绿
13
+
5
14
  ## 0.3.1 — 引用管线懒重试:页面刷新后不再退化成纯文本
6
15
 
7
16
  - **根因**:页面刷新时插件 `apply` 早于宿主 `inputTriggers` 服务就绪,`ctx.get('inputTriggers')` 拿到空值后
package/lib/client.js CHANGED
@@ -51,13 +51,23 @@ window.__ModuleLoader__.load({
51
51
  } catch (e) { /* diagnostics must never throw */ }
52
52
  }
53
53
  if (DIAG_ENABLED && typeof fetch === 'function') {
54
+ const pingOnce = () => {
55
+ try {
56
+ fetch(DIAG_URL + '/ping', { method: 'POST', body: 'ping' }).then(() => {
57
+ if (diagReady === true) return
58
+ diagReady = true
59
+ const pending = diagQueue.splice(0)
60
+ for (const text of pending) sendReport(text)
61
+ }).catch(() => { diagReady = false })
62
+ } catch (e) { diagReady = false }
63
+ }
64
+ pingOnce()
65
+ // 接收端比页面晚启动也来得及:探测未成功时每 5 秒重试(成功一次即停)
54
66
  try {
55
- fetch(DIAG_URL + '/ping', { method: 'POST', body: 'ping' }).then(() => {
56
- diagReady = true
57
- const pending = diagQueue.splice(0)
58
- for (const text of pending) sendReport(text)
59
- }).catch(() => { diagReady = false })
60
- } catch (e) { diagReady = false }
67
+ const retryTimer = setInterval(() => { if (diagReady !== true) pingOnce() }, 5000)
68
+ // Node(自检脚本里 import 本模块)不会自己退出 → 解除引用;浏览器里返回的是数字,跳过
69
+ if (retryTimer && typeof retryTimer.unref === 'function') retryTimer.unref()
70
+ } catch (e) { /* ignore */ }
61
71
  } else {
62
72
  diagReady = false
63
73
  }
@@ -1729,52 +1739,58 @@ window.__ModuleLoader__.load({
1729
1739
  // 编辑器重渲染会丢掉 class,因此用 MutationObserver 重新贴上。
1730
1740
  try {
1731
1741
  ctx.effect(() => {
1732
- let timer = null
1733
1742
  let observer = null
1734
- const isSingleLine = (t) => t !== '' && t.indexOf('\n') === -1
1735
- let lastSig = ''
1743
+ // 位置行:`【引用 · 第N轮 · DSH 回复】` 或 `【批注 N · 第N轮 · DSH 回复(…)】`
1744
+ const HEADER_RE = /^【(?:引用|批注)[^\n]*】$/
1745
+ // 编辑器根下的"行元素":通常是 <p>,个别宿主会再包一层容器
1746
+ const lineList = (ed) => {
1747
+ let kids = Array.from(ed.children || [])
1748
+ if (kids.length === 1 && kids[0] && kids[0].children && kids[0].children.length > 1) {
1749
+ kids = Array.from(kids[0].children)
1750
+ }
1751
+ return kids
1752
+ }
1753
+ /**
1754
+ * 把输入框里的正文"装饰"成紧凑块:
1755
+ * - 位置行 → `.dshq-cmp-header`(display:none,界面上看不到,但文本仍然在 → 发送后模型拿得到)
1756
+ * - 位置行后面紧跟的引用正文行 → `.dshq-cmp-quote`(浅色块;首行/末行分别圆角)
1757
+ * 一条消息里可以有多条批注,所以这里按"状态机"逐行扫描,遇到空行即结束当前块,
1758
+ * 然后继续扫后面的块(早期版本遇到第一个空行就 break,导致第二条批注不再装饰)。
1759
+ */
1736
1760
  const decorate = () => {
1737
- timer = null
1738
1761
  try {
1739
1762
  for (const ed of document.querySelectorAll('[contenteditable="true"]')) {
1740
- const kids = Array.from(ed.children || [])
1741
- let headerEl = null
1742
- const quoteEls = []
1763
+ const kids = lineList(ed)
1764
+ let block = null
1765
+ let changed = 0
1766
+ const flush = () => {
1767
+ if (block && block.lines.length > 0) {
1768
+ const lines = block.lines
1769
+ lines.forEach((el, i) => {
1770
+ if (!el.classList.contains('dshq-cmp-quote')) changed++
1771
+ el.classList.add('dshq-cmp-quote')
1772
+ el.classList.toggle('dshq-cmp-quote-first', i === 0)
1773
+ el.classList.toggle('dshq-cmp-quote-last', i === lines.length - 1)
1774
+ })
1775
+ }
1776
+ block = null
1777
+ }
1743
1778
  for (const el of kids) {
1744
1779
  const t = String(el.textContent || '').trim()
1745
- if (!headerEl) {
1746
- // 位置行必须"整行且只有一行",避免误把整个编辑器当成头部
1747
- if (isSingleLine(t) && /^【(?:引用|批注)[^\n]*】$/.test(t)) headerEl = el
1780
+ const single = t !== '' && t.indexOf('\n') === -1
1781
+ if (single && HEADER_RE.test(t)) {
1782
+ flush()
1783
+ if (!el.classList.contains('dshq-cmp-header')) changed++
1784
+ el.classList.add('dshq-cmp-header')
1785
+ block = { lines: [] }
1748
1786
  continue
1749
1787
  }
1750
- if (t === '') break
1751
- if (!isSingleLine(t)) break
1752
- quoteEls.push(el)
1788
+ if (!block) continue
1789
+ if (t === '' || !single) { flush(); continue }
1790
+ block.lines.push(el)
1753
1791
  }
1754
- if (!headerEl) {
1755
- // 诊断:编辑器里出现了"【引用/【批注"但没能匹配成"位置行 + 正文行"时,把结构打出来
1756
- const raw = String(ed.textContent || '')
1757
- if (raw.indexOf('【引用') >= 0 || raw.indexOf('【批注') >= 0) {
1758
- const sig = raw.slice(0, 40) + '|' + kids.length
1759
- if (sig !== lastSig) {
1760
- lastSig = sig
1761
- report('cmp:probe', {
1762
- ms: lastInsertAt ? Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - lastInsertAt) : null,
1763
- kids: kids.length,
1764
- singles: kids.map((el) => (isSingleLine(String(el.textContent || '').trim()) ? 1 : 0)).join(''),
1765
- first: String((kids[0] && kids[0].textContent) || '').slice(0, 24),
1766
- })
1767
- }
1768
- }
1769
- continue
1770
- }
1771
- headerEl.classList.add('dshq-cmp-header')
1772
- quoteEls.forEach((el, idx) => {
1773
- el.classList.add('dshq-cmp-quote')
1774
- el.classList.toggle('dshq-cmp-quote-first', idx === 0)
1775
- el.classList.toggle('dshq-cmp-quote-last', idx === quoteEls.length - 1)
1776
- })
1777
- report('cmp:decorated', { lines: quoteEls.length })
1792
+ flush()
1793
+ if (changed > 0) report('cmp:decorated', { changed })
1778
1794
  }
1779
1795
  } catch (e) { report('cmp:scan-throw', String((e && e.message) || e)) }
1780
1796
  }
@@ -1789,7 +1805,6 @@ window.__ModuleLoader__.load({
1789
1805
  } catch (e) { report('cmp:observer-throw', String((e && e.message) || e)) }
1790
1806
  return () => {
1791
1807
  if (composerDecorateNow === decorate) composerDecorateNow = null
1792
- if (timer !== null) clearTimeout(timer)
1793
1808
  if (observer) { try { observer.disconnect() } catch (e) { /* ignore */ } }
1794
1809
  }
1795
1810
  }, 'dsh-quote-annotate-pro: composer quote decoration')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quote-annotate-pro",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "划词批注与引用锚点:划词 → 批注(自动编号)→ 引用;引用块在输入框和已发送消息里始终可见(最多 3 行、位置行隐藏),点一下跳回原文并高亮;模型按「Annotation N:」逐条回应,回复里的标签可悬浮看原文与评语。纯客户端,锚点存本地 localStorage。",
5
5
  "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "quote", "annotate", "selection", "reference", "anchor"],
6
6
  "license": "MIT",