dsh-quote-annotate-pro 0.3.0 → 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 +20 -0
- package/README.en.md +1 -0
- package/lib/client.js +115 -49
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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
|
+
|
|
14
|
+
## 0.3.1 — 引用管线懒重试:页面刷新后不再退化成纯文本
|
|
15
|
+
|
|
16
|
+
- **根因**:页面刷新时插件 `apply` 早于宿主 `inputTriggers` 服务就绪,`ctx.get('inputTriggers')` 拿到空值后
|
|
17
|
+
`pipelineAvailable` 被永久置为 `false` → 之后每次批注插入都走「纯文本回退」,输入框里出现 `【批注 1 …】` 裸文本块
|
|
18
|
+
(实测证据:localStorage 草稿记录为 `{"draft":{"text":"【批注 1 · …】\n钟)\n\n批注:1…","references":[]}}`,即没有 chip)
|
|
19
|
+
- **修复**:引用源注册改为幂等懒加载 `ensureQuoteSource()`,服务未就绪时按 500ms × 20 次(≈10s)自动重试,
|
|
20
|
+
每次插入前也会先试一次;重试成功即恢复正常 chip 插入,无需用户刷新页面或重启
|
|
21
|
+
- **可观测性**:`insert:draft` 诊断补充 `draftLen` / `pipeline`;`insert:fallback-plaintext` 补充 `draftLen` / `bodyLen` / `pipeline`;
|
|
22
|
+
编辑器提示按状态区分(`null` = 正在初始化引用管线,`false` = 回退为纯文本)
|
|
23
|
+
- 自检照旧:`npm test`(8 组纯函数断言)、`node --check`、`check-manifest` 全绿
|
|
24
|
+
|
|
5
25
|
## 0.3.0 — 编号批注 + 回复逐条对照
|
|
6
26
|
|
|
7
27
|
- **批注自动编号**:同一条消息里每次插入批注都会编号(`批注 1`、`批注 2`…,删掉中间某条不会重号,见 `nextAnnotationNo()`);
|
package/README.en.md
CHANGED
|
@@ -104,6 +104,7 @@ Official surfaces used: `slots` (`shell.overlay`, `conversation.chat.turnTail`,
|
|
|
104
104
|
|
|
105
105
|
```sh
|
|
106
106
|
npm run check # manifest self-check (package.json / patch / bundle registration id)
|
|
107
|
+
npm test # pure-function self-check for the client bundle (numbering, block parsing, snippets — no browser/network)
|
|
107
108
|
npm run diag # local diagnostics sink (http://127.0.0.1:47821)
|
|
108
109
|
```
|
|
109
110
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
}
|
|
@@ -215,6 +225,8 @@ window.__ModuleLoader__.load({
|
|
|
215
225
|
let currentSessionId = null
|
|
216
226
|
let refSeq = 0
|
|
217
227
|
let pipelineAvailable = null // null=未知 false=不可用 true=可用
|
|
228
|
+
// 引用 source 的注册函数(懒加载 + 重试):服务晚就绪、HMR 重载后仍能恢复 chip 插入
|
|
229
|
+
let ensureQuoteSource = null
|
|
218
230
|
// ref -> { sessionId, anchorKey, body, label } 用于点击 chip 跳回原文 / 悬停显示原文
|
|
219
231
|
const refTargets = new Map()
|
|
220
232
|
// ref -> { body, clipboardText } 提交时经 codec 序列化
|
|
@@ -910,6 +922,8 @@ window.__ModuleLoader__.load({
|
|
|
910
922
|
refCodecs.delete(oldest)
|
|
911
923
|
refTargets.delete(oldest)
|
|
912
924
|
}
|
|
925
|
+
// 懒重试:若 apply 时 inputTriggers 还没就绪,插入前再试一次(常见于"刚启动就点批注")
|
|
926
|
+
try { if (typeof ensureQuoteSource === 'function') ensureQuoteSource() } catch (e) { /* ignore */ }
|
|
913
927
|
// 事件派发目标:优先用 sessions.scope()(社区插件在本版本已验证的路径),退回 binding.ctx
|
|
914
928
|
let emitCtx = binding.ctx
|
|
915
929
|
try {
|
|
@@ -929,10 +943,13 @@ window.__ModuleLoader__.load({
|
|
|
929
943
|
try {
|
|
930
944
|
const snap2 = input.state.getSnapshot()
|
|
931
945
|
const occ2 = (snap2 && snap2.occurrences) || []
|
|
946
|
+
const draft2 = String((snap2 && snap2.draft) || '')
|
|
932
947
|
report('insert:draft', {
|
|
933
|
-
draft:
|
|
948
|
+
draft: draft2.replace(/\s+/g, ' ').slice(0, 400),
|
|
949
|
+
draftLen: draft2.length,
|
|
934
950
|
occ: occ2.length,
|
|
935
951
|
occLabels: occ2.map((o) => (o && o.label) || '').join('|').slice(0, 80),
|
|
952
|
+
pipeline: pipelineAvailable,
|
|
936
953
|
})
|
|
937
954
|
} catch (e) { /* ignore */ }
|
|
938
955
|
report('insert:chip', { ref, anchorKey: item.anchorKey || null, header, chars: item.text.length })
|
|
@@ -950,7 +967,9 @@ window.__ModuleLoader__.load({
|
|
|
950
967
|
refTargets.delete(ref)
|
|
951
968
|
refCodecs.delete(ref)
|
|
952
969
|
input.setDraft(base === '' ? quoteBody : base + '\n\n' + quoteBody)
|
|
953
|
-
|
|
970
|
+
let draftLen = 0
|
|
971
|
+
try { draftLen = String((input.state.getSnapshot() || {}).draft || '').length } catch (e) { /* ignore */ }
|
|
972
|
+
report('insert:fallback-plaintext', { ref, header, draftLen, bodyLen: quoteBody.length, pipeline: pipelineAvailable })
|
|
954
973
|
setStatus({ ok: false, text: 'chip 插入未生效,已回退为纯文本引用' })
|
|
955
974
|
editorStore.close()
|
|
956
975
|
} catch (e) {
|
|
@@ -1183,9 +1202,21 @@ window.__ModuleLoader__.load({
|
|
|
1183
1202
|
available: pipelineAvailable,
|
|
1184
1203
|
hasInsertReference: !!(inputTriggers && typeof inputTriggers.insertReference === 'function'),
|
|
1185
1204
|
})
|
|
1186
|
-
|
|
1205
|
+
// 注册机:幂等、可重试。宿主服务可能晚于插件就绪(或 HMR 重载后旧注册仍在),
|
|
1206
|
+
// 一次性判定会让整页退化成"纯文本回退",因此这里允许后续再次调用。
|
|
1207
|
+
let quoteSourceRegistered = false
|
|
1208
|
+
ensureQuoteSource = () => {
|
|
1209
|
+
if (quoteSourceRegistered) return true
|
|
1210
|
+
let triggers = inputTriggers
|
|
1211
|
+
if (!triggers) {
|
|
1212
|
+
try { triggers = ctx.get('inputTriggers') } catch (e) { triggers = undefined }
|
|
1213
|
+
}
|
|
1214
|
+
if (!triggers || typeof triggers.registerSource !== 'function') {
|
|
1215
|
+
pipelineAvailable = false
|
|
1216
|
+
return false
|
|
1217
|
+
}
|
|
1187
1218
|
try {
|
|
1188
|
-
ctx.effect(() =>
|
|
1219
|
+
ctx.effect(() => triggers.registerSource({
|
|
1189
1220
|
trigger: '@',
|
|
1190
1221
|
name: 'quote-ref',
|
|
1191
1222
|
order: 100,
|
|
@@ -1196,12 +1227,42 @@ window.__ModuleLoader__.load({
|
|
|
1196
1227
|
serialize: (ref) => Promise.resolve(refCodecs.get(ref) ? refCodecs.get(ref).body : ''),
|
|
1197
1228
|
},
|
|
1198
1229
|
}), 'dsh-quote-annotate-pro: quote-ref source')
|
|
1230
|
+
quoteSourceRegistered = true
|
|
1231
|
+
pipelineAvailable = true
|
|
1199
1232
|
report('register:quote-ref-source', 'ok')
|
|
1233
|
+
return true
|
|
1200
1234
|
} catch (e) {
|
|
1235
|
+
const msg = String((e && e.stack) || (e && e.message) || e)
|
|
1236
|
+
// 已注册(HMR 重载后宿主保留旧注册)不算失败:序列化仍会路由到当前模块的 codec
|
|
1237
|
+
if (/already|duplicate|exist|注册/i.test(msg)) {
|
|
1238
|
+
quoteSourceRegistered = true
|
|
1239
|
+
pipelineAvailable = true
|
|
1240
|
+
report('register:quote-ref-source', 'already')
|
|
1241
|
+
return true
|
|
1242
|
+
}
|
|
1201
1243
|
pipelineAvailable = false
|
|
1202
|
-
report('register:quote-ref-source', 'throw ' +
|
|
1244
|
+
report('register:quote-ref-source', 'throw ' + msg)
|
|
1245
|
+
return false
|
|
1203
1246
|
}
|
|
1204
1247
|
}
|
|
1248
|
+
if (!ensureQuoteSource()) {
|
|
1249
|
+
// 退避重试:最多 20 次、间隔 500ms(约 10s),把启动时序造成的退化救回来
|
|
1250
|
+
try {
|
|
1251
|
+
ctx.effect(() => {
|
|
1252
|
+
let tries = 0
|
|
1253
|
+
let timer = null
|
|
1254
|
+
const tick = () => {
|
|
1255
|
+
timer = null
|
|
1256
|
+
if (quoteSourceRegistered || tries >= 20) return
|
|
1257
|
+
tries += 1
|
|
1258
|
+
if (!ensureQuoteSource()) timer = setTimeout(tick, 500)
|
|
1259
|
+
}
|
|
1260
|
+
timer = setTimeout(tick, 500)
|
|
1261
|
+
report('register:quote-ref-source', 'retry-scheduled')
|
|
1262
|
+
return () => { if (timer !== null) clearTimeout(timer) }
|
|
1263
|
+
}, 'dsh-quote-annotate-pro: quote-ref retry')
|
|
1264
|
+
} catch (e) { /* ignore */ }
|
|
1265
|
+
}
|
|
1205
1266
|
// 全局监听(选区气泡 / chip 点击跳转 / chip 悬停提示;ctx.effect 保证卸载时全部清理)
|
|
1206
1267
|
try {
|
|
1207
1268
|
ctx.effect(() => {
|
|
@@ -1678,52 +1739,58 @@ window.__ModuleLoader__.load({
|
|
|
1678
1739
|
// 编辑器重渲染会丢掉 class,因此用 MutationObserver 重新贴上。
|
|
1679
1740
|
try {
|
|
1680
1741
|
ctx.effect(() => {
|
|
1681
|
-
let timer = null
|
|
1682
1742
|
let observer = null
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
+
*/
|
|
1685
1760
|
const decorate = () => {
|
|
1686
|
-
timer = null
|
|
1687
1761
|
try {
|
|
1688
1762
|
for (const ed of document.querySelectorAll('[contenteditable="true"]')) {
|
|
1689
|
-
const kids =
|
|
1690
|
-
let
|
|
1691
|
-
|
|
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
|
+
}
|
|
1692
1778
|
for (const el of kids) {
|
|
1693
1779
|
const t = String(el.textContent || '').trim()
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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: [] }
|
|
1697
1786
|
continue
|
|
1698
1787
|
}
|
|
1699
|
-
if (
|
|
1700
|
-
if (!
|
|
1701
|
-
|
|
1788
|
+
if (!block) continue
|
|
1789
|
+
if (t === '' || !single) { flush(); continue }
|
|
1790
|
+
block.lines.push(el)
|
|
1702
1791
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
const raw = String(ed.textContent || '')
|
|
1706
|
-
if (raw.indexOf('【引用') >= 0 || raw.indexOf('【批注') >= 0) {
|
|
1707
|
-
const sig = raw.slice(0, 40) + '|' + kids.length
|
|
1708
|
-
if (sig !== lastSig) {
|
|
1709
|
-
lastSig = sig
|
|
1710
|
-
report('cmp:probe', {
|
|
1711
|
-
ms: lastInsertAt ? Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - lastInsertAt) : null,
|
|
1712
|
-
kids: kids.length,
|
|
1713
|
-
singles: kids.map((el) => (isSingleLine(String(el.textContent || '').trim()) ? 1 : 0)).join(''),
|
|
1714
|
-
first: String((kids[0] && kids[0].textContent) || '').slice(0, 24),
|
|
1715
|
-
})
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
continue
|
|
1719
|
-
}
|
|
1720
|
-
headerEl.classList.add('dshq-cmp-header')
|
|
1721
|
-
quoteEls.forEach((el, idx) => {
|
|
1722
|
-
el.classList.add('dshq-cmp-quote')
|
|
1723
|
-
el.classList.toggle('dshq-cmp-quote-first', idx === 0)
|
|
1724
|
-
el.classList.toggle('dshq-cmp-quote-last', idx === quoteEls.length - 1)
|
|
1725
|
-
})
|
|
1726
|
-
report('cmp:decorated', { lines: quoteEls.length })
|
|
1792
|
+
flush()
|
|
1793
|
+
if (changed > 0) report('cmp:decorated', { changed })
|
|
1727
1794
|
}
|
|
1728
1795
|
} catch (e) { report('cmp:scan-throw', String((e && e.message) || e)) }
|
|
1729
1796
|
}
|
|
@@ -1738,7 +1805,6 @@ window.__ModuleLoader__.load({
|
|
|
1738
1805
|
} catch (e) { report('cmp:observer-throw', String((e && e.message) || e)) }
|
|
1739
1806
|
return () => {
|
|
1740
1807
|
if (composerDecorateNow === decorate) composerDecorateNow = null
|
|
1741
|
-
if (timer !== null) clearTimeout(timer)
|
|
1742
1808
|
if (observer) { try { observer.disconnect() } catch (e) { /* ignore */ } }
|
|
1743
1809
|
}
|
|
1744
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.
|
|
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",
|