Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### 修复
- **修复「插件一运行客户端就卡死」**:装饰动作会改写宿主 DOM 并再次触发 `MutationObserver`,而越界编号在「不生成芯片、保留原文」的分支里仍然无条件 `replaceChild`——等于用一个内容相同的新节点替换原节点,mutation 永不收敛,装饰与观察者互相触发成自激,把主线程打满(表现为整个客户端失去响应)。现在一个可替换项都没有时**完全不碰 DOM**;另外给全量装饰加了 120ms 合并调度(首拍仍立即执行,保住「绘制前隐藏、零闪烁」),避免长会话里装饰自身滚成高频全量扫描。
- 适配 DSH 0.1.7:`sessions.list` 快照不再暴露 `current`,当前会话 id 改经 `uiSession.adapter.current`(`binding.key`)读取,旧内核保留原字段兜底,会话切换订阅同步跟随。修复 0.1.7+ 上「批注标签在、但回车/发送按钮都没反应」——拼稿前取不到会话 id 会直接返回,空草稿时宿主又禁用发送,只有手动输入正文才发得出去。
- 会话识别合成完整四级回退链(#68 与上条方案的合并,覆盖 DSH 0.1.1 ~ 0.2.0-rc.2,已对照 0.2.0-rc.2 内核源码逐项核实):`uiSession.adapter.current`(0.1.7+)→ `localStorage['dsh.sessions.current']`(0.1.6-alpha.2 ~ 0.1.6 上唯一可用路径)→ 旧 list 快照 `current`(≤0.1.6-alpha.1)→ `retainedBy.mainView > 0` 公开成员兜底。取不到会话 id 时 toast 提示且批注保留待下一条重试(不再静默丢弃);会话切换检测三层并行(uiSession source 订阅 / list.subscribe / 1s 轮询兜底),切换后重挂新会话草稿订阅,待发送批注跨会话、跨刷新正确恢复。方案与考证来自 @pinzza(#68),与 @fengbinmov 的 uiSession 读取(#65)合并。

### 交互
- 浮层底色改成「默认半透明、鼠标移入变实」:默认半透明,悬浮在正文上时能看穿下面的文字;鼠标移进去后**带 0.15s 过渡**换成不透明底色,方便专心读批注内容。半透明**不再依赖宿主变量自带 alpha**(Windows 上 `--dsw-specific-menu` 可能缺失或本身不透明),改用 `color-mix(in srgb, <主题色> 94%, transparent)` 自行生成半透明底,并做多级回退(`--dsw-alias-bg-layer-2` → `--dsw-alias-bg-base` → 深灰),保证明暗主题都能跟随。覆盖回复芯片、批注 ×N 标签、各类 hover 浮窗(回复芯片面板 / 批注清单 / 气泡标签面板)、选区工具条、编辑卡片与 toast。
Expand Down
129 changes: 103 additions & 26 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ window.__ModuleLoader__.load({
toast: {
attachFail: '批注拼稿失败,消息将不带批注发送:',
skipCommand: '本条是斜杠命令,未拼入批注;批注已保留,将随下一条消息发送',
noSession: '无法确定当前会话,批注未拼入(已保留,将随下一条消息重试)',
},
block: {
head: '我批注了以下 {n} 处内容(编号与原文对应),请针对它们回答我的问题:',
Expand Down Expand Up @@ -246,6 +247,7 @@ window.__ModuleLoader__.load({
toast: {
attachFail: 'Failed to attach annotations; the message will be sent without them: ',
skipCommand: 'Slash command detected — annotations stay pending and will attach to your next message',
noSession: 'Cannot determine the current session; annotations kept pending and will retry with your next message',
},
block: {
head: 'I annotated the following {n} passage(s) (the numbers match the quotes below); please respond to them when answering my question:',
Expand Down Expand Up @@ -922,13 +924,22 @@ window.__ModuleLoader__.load({
function apply(ctx) {
var sessions = ctx.sessions

// ---------- 当前会话 id 的跨版本读取 ----------
// DSH 0.1.7 起 `sessions.list` 快照不再暴露 `current`(selection 收进
// ClientSessions 私有 selection),改由 ui-session 服务的
// `adapter.current`(HostObservable<binding>,binding.key = sessionId)
// 发布。旧版两条路都在,故这里以 uiSession 优先、list.current 兜底:
// 若只认旧字段,0.1.7+ 上 current 恒为 undefined,attachAndSend 第一行
// 就 return false —— 表现为「有批注、标签也在,但回车/发送按钮都没反应」。
// ---------- 当前会话 id 的跨版本解析(覆盖 DSH 0.1.1 ~ 0.2.0-rc.2)----------
// 优先级(自上而下,命中即返回):
// ① uiSession 服务的 `adapter.current`(HostObservable<binding>,binding.key =
// sessionId)—— 0.1.7 起的公开选中态:selection 收进 ClientSessions 私有
// selection,list 快照不再带 current。已对照 0.1.7-rc.2 与 0.2.0-rc.2 内核
// 源码核实(packages/client/ui-session/src/client/index.ts:312 起暴露)。
// ② `localStorage['dsh.sessions.current'].sessionId` —— 内核 workspace 服务
// 持久化的选择态(`createSnapshotStore({}, { persist: { name: ... } })`,
// 写入形状 `{ sessionId }`,会话归档时由内核 clearArchivedCurrent 自行清理)。
// 0.1.6-alpha.2 ~ 0.1.6 上这是唯一真实可用的路径(#68 的考证)。
// ③ list 快照上的 `current` —— 0.1.6-alpha.2 之前的宿主,该字段是精确选中值。
// ④ list 行上的 `retainedBy.mainView > 0` —— 公开成员,兜底「尚未落盘 / 存储
// 被清」的场景。它是「主视图 retain 的会话」而非选中值:内核自己也只在
// id 未知时才用它猜,故排在精确值之后。
// (曾挂在 sessions 服务上的 `currentProvideInfo` 只存在于旧源码线
// packages/client/runtime,上游现契约已无此成员,不再尝试。)
var uiSessionService = null

/** ui-session 服务(惰性探测:apply 时机早于它注册时下次再取)。 */
Expand All @@ -939,31 +950,53 @@ window.__ModuleLoader__.load({
return uiSessionService
}

/** ① 命中时返回可订阅的选中态源(HostObservable),否则 null。 */
function currentSessionSource() {
try {
var face = uiSessionFace()
var adapter = face !== null ? face.adapter : undefined
var source = adapter !== undefined && adapter !== null ? adapter.current : undefined
if (source !== undefined && source !== null
&& typeof source.getSnapshot === 'function' && typeof source.subscribe === 'function') return source
} catch (_) { /* 旧版内核:退回 list store */ }
return sessions.list
} catch (_) { /* 旧版内核:无 uiSession */ }
return null
}

function readCurrentSessionId() {
try {
var face = uiSessionFace()
var adapter = face !== null ? face.adapter : undefined
var source = adapter !== undefined && adapter !== null ? adapter.current : undefined
if (source !== undefined && source !== null && typeof source.getSnapshot === 'function') {
var source = currentSessionSource()
if (source !== null) {
var binding = source.getSnapshot()
if (binding !== undefined && binding !== null && binding.key !== undefined) return binding.key
}
} catch (_) { /* 旧版内核:退回 list store */ }
} catch (_) { /* 落到下一级 */ }
try {
if (typeof localStorage !== 'undefined') {
var raw = localStorage.getItem('dsh.sessions.current')
if (raw !== null) {
var parsed = JSON.parse(raw)
if (parsed !== null && typeof parsed === 'object'
&& typeof parsed.sessionId === 'string' && parsed.sessionId !== '') return parsed.sessionId
}
}
} catch (_) { /* 落到下一级 */ }
try {
var snap = sessions.list.getSnapshot()
return snap !== undefined && snap !== null ? snap.current : undefined
} catch (_) { return undefined }
if (snap !== undefined && snap !== null) {
if (typeof snap.current === 'string' && snap.current !== '') return snap.current
var byId = snap.byId
if (byId !== undefined && byId !== null && typeof byId === 'object') {
var ids = Array.isArray(snap.ids) ? snap.ids : Object.keys(byId)
for (var i = 0; i < ids.length; i++) {
var retained = byId[ids[i]] !== undefined && byId[ids[i]] !== null
? byId[ids[i]].retainedBy : null
if (retained !== null && typeof retained === 'object'
&& typeof retained.mainView === 'number' && retained.mainView > 0) return ids[i]
}
}
}
} catch (_) { /* 落到下一级 */ }
return undefined
}

var host = document.createElement('div')
Expand Down Expand Up @@ -1123,7 +1156,8 @@ window.__ModuleLoader__.load({
var key = selectionKey(sel)
var rootEl = annotationRootOf(range.commonAncestorContainer)
var source = documentSourceOf(range.commonAncestorContainer)
if (source !== null && source.sessionId !== readCurrentSessionId()) rootEl = null
var selSessionId = readCurrentSessionId()
if (source !== null && selSessionId !== undefined && source.sessionId !== selSessionId) rootEl = null
if (rootEl === null) { clearSettle(); closeToolbar(); return }
if (ui.mode === 'actions' && key === ui.lastKey && text === ui.quote && rootEl === ui.selectionRoot
&& (ui.source && ui.source.sourceUrl) === (source && source.sourceUrl)) { clearSettle(); return }
Expand All @@ -1136,8 +1170,9 @@ window.__ModuleLoader__.load({
var r = s.getRangeAt(0)
if (annotationRootOf(r.commonAncestorContainer) !== rootEl) return
var currentSource = documentSourceOf(r.commonAncestorContainer)
var settleSessionId = readCurrentSessionId()
if ((currentSource && currentSource.sourceUrl) !== (source && source.sourceUrl)
|| (source !== null && source.sessionId !== readCurrentSessionId())) return
|| (source !== null && settleSessionId !== undefined && source.sessionId !== settleSessionId)) return
var rect = r.getBoundingClientRect()
if (rect.width === 0 || rect.height === 0) return
var p = placeAbove(rect, 40)
Expand Down Expand Up @@ -1299,9 +1334,15 @@ window.__ModuleLoader__.load({

function submitAttached() {
var current = readCurrentSessionId()
if (current === undefined) return
if (current === undefined) {
console.warn('[annotation] 无法确定当前会话,批注直接提交已跳过')
return
}
var scoped = sessions.scope(current)
if (scoped === undefined) return
if (scoped === undefined) {
console.warn('[annotation] 当前会话 scope 不可用,批注直接提交已跳过')
return
}
try {
ctx.conversation.input.for(scoped).submit('queue')
} catch (err) {
Expand Down Expand Up @@ -1765,10 +1806,18 @@ window.__ModuleLoader__.load({
* 返回 true 表示批注块已在草稿中(本次刚拼入,或之前已拼入未发送)。 */
function attachAndSend(e) {
var current = readCurrentSessionId()
if (current === undefined) return false
if (current === undefined) {
console.warn('[annotation] 无法确定当前会话,批注未拼入草稿')
showToast(t('toast.noSession'))
return false
}
try {
var scoped = sessions.scope(current)
if (scoped === undefined) return false
if (scoped === undefined) {
console.warn('[annotation] 当前会话 scope 不可用,批注未拼入草稿')
showToast(t('toast.noSession'))
return false
}
var shell = ctx.conversation.input.for(scoped)
var st = shell.state.getSnapshot()
var draft = st.draft || ''
Expand Down Expand Up @@ -2081,6 +2130,10 @@ window.__ModuleLoader__.load({
function watchInputDraft() {
if (inputWatchTimer !== null) { clearInterval(inputWatchTimer); inputWatchTimer = null }
if (typeof inputUnsub === 'function') { inputUnsub(); inputUnsub = null }
// 会话切换时旧 scope 的订阅由上层先释放,这里只负责挂上当前会话;
// 切换后若 scope 尚不可用,仍靠下方的 1s 重试补齐(同 watchInputDraft 原有语义)。
// 注意:切换后必须重新订阅新会话的草稿,否则“草稿有→空”的发送清空权威会
// 继续监听旧会话,新会话发送后待发送批注永远不清(每次 Enter 重复拼稿)。
var id = readCurrentSessionId()
if (id !== undefined && tryWatchInputDraft(id)) return
inputWatchTimer = setInterval(function () {
Expand Down Expand Up @@ -2503,11 +2556,15 @@ window.__ModuleLoader__.load({
}

// ---------- 待发送批注按会话恢复 ----------
// 0.1.6-alpha.2 起选择态已移出 list store:list.subscribe 不再于会话切换时触发。
// 切换检测三层并行(onSessionSwitch 幂等,重复触发无害):
// ① uiSession 的 current source(0.1.7+ 精确;uiSession 晚于插件注册时,
// onSessionSwitch 内的惰性探测仍能读到,订阅缺口由轮询兜底);
// ② list.subscribe(0.1.6-alpha.2 之前的宿主精确);
// ③ 1s 轮询(新宿主上 ② 永不触发、① 缺席时它是唯一触发点)。
var lastSessionId = readCurrentSessionId()
ui.quotes = readPendingQuotes(lastSessionId)
// 会话切换:0.1.7 起 selection 变化不再写进 list 快照,必须订阅
// ui-session 的 current source,否则切会话时待发送批注不会跟着切换。
var unsub = currentSessionSource().subscribe(function () {
function onSessionSwitch() {
var cur = readCurrentSessionId()
if (cur === lastSessionId) return
writePendingQuotes(lastSessionId)
Expand All @@ -2523,7 +2580,27 @@ window.__ModuleLoader__.load({
updateChip()
watchInputDraft()
renderMarkers()
})
}
var unsubList = null
var unsubSession = null
var switchTimer = null
try {
if (sessions.list !== undefined && sessions.list !== null
&& typeof sessions.list.subscribe === 'function') {
unsubList = sessions.list.subscribe(onSessionSwitch)
}
} catch (_) { unsubList = null }
try {
var uiSource = currentSessionSource()
if (uiSource !== null) unsubSession = uiSource.subscribe(onSessionSwitch)
} catch (_) { unsubSession = null }
switchTimer = setInterval(onSessionSwitch, 1000)
function unsubSessionSwitch() {
try { if (typeof unsubList === 'function') unsubList() } catch (_) { /* ignore */ }
try { if (typeof unsubSession === 'function') unsubSession() } catch (_) { /* ignore */ }
if (switchTimer !== null) { clearInterval(switchTimer); switchTimer = null }
}
var unsub = unsubSessionSwitch

watchInputDraft()
kickDecorate()
Expand Down
74 changes: 74 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading