TL;DR:DSH 在 commit 6830e1460d(首个发布的版本是 dsh-v0.1.6-alpha.2)把 SessionListState.current 移除了。插件从 ctx.sessions.list.getSnapshot().current 取当前会话 id,于是恒为 undefined;attachAndSend() 在 current === undefined 处静默 return false(早于自身的 try/catch),批注块从不进草稿,也无任何提示。1.4.10 与 1.4.11-preview.1 均受影响(我核对了 preview 的 tarball,同一写法仍有 9 处)。0.1.6-alpha.2 起内核也没有为页面级插件提供"当前会话"的公开读取面。
环境
- 插件:
@changfenhuang/dsh-annotation@1.4.10(npm latest);另核对 1.4.11-preview.1,受影响
- 宿主:DSH
0.1.6-alpha.2(tag dsh-v0.1.6-alpha.2,commit ddefc45fbc,源码 checkout 构建)
- 形态:Web GUI(
dsh web)
现象
选中助手文字 → 添加批注 →「批注 ×N」标签正常出现 → 回车发送:
- 消息里没有批注块,模型只收到正文;
- Console 没有
[annotation] 批注块已拼入草稿,回车将随消息发送(N 条);
- 没有任何 toast 或报错(选区、高亮、编号脚标、标签全部正常,只有"发送"这一环失效)。
根因
1.4.10 的 client.js 在 7 处读取 sessions.list.getSnapshot().current:第 879 / 1146 / 1518 / 1799 / 1802 / 2205 / 2208 行。
DSH commit 6830e1460d(refactor(session-controller): own Client Session generations,2026-09-17)把 current 从 SessionListState 移除:
| 内核版本 |
SessionListState.current |
插件表现 |
dsh-v0.1.5-rc.2(1.4.10 的验收宿主) |
存在 |
正常 |
dsh-v0.1.6-alpha.1(1.4.11-preview.1 的验收宿主) |
存在 |
正常 |
dsh-v0.1.6-alpha.2 |
已移除 |
批注不发送 |
attachAndSend() 的写法是先静默返回、后进 try/catch,所以整条链路没有任何提示:
function attachAndSend(e) {
var current = sessions.list.getSnapshot().current
if (current === undefined) return false // ← 命中这里:无 toast、无日志
var scoped = sessions.scope(current)
...
try { ... shell.setDraft(block + ...) } catch (err) { showToast(t('toast.attachFail')) }
}
同一原因还波及(可用同一种修法一起解决):
submitAttached():Cmd/Ctrl+Enter 发纯批注同样提前返回;
watchInputDraft():以"草稿由有变空"作为已发送的清理权威,现在每秒重试、永不挂上;
writeCurrentPendingQuotes() / readPendingQuotes():按会话持久化的 key 恒为 undefined,读写都失效;
- 会话切换检测:选择态已移出 list store,
sessions.list.subscribe(...) 不再于切换时触发(跨会话保留未发送批注的特性失效)。
内核侧没有公开替代读面
- 新的选中态在
packages/client/ui-workspace/src/client/navigation.ts 的 private readonly selection(持久化键 dsh.sessions.current);UiWorkspace 公开接口只有 openSession / startSession / archiveSession 这类动作,没有任何读取成员。
ISessions(packages/api/session-controller/src/client/contract/sessions.ts)也没有等价读取:scope(id) / scopeOf(ctx) 都需要你先知道 id,或已处于该会话的 ctx。
- 已核对
dsh-v0.1.7-alpha.2 与 master(2026-09-22):SessionListState 仍无 current,selection 仍为私有;0.1.7 还把 subagentsByParent / jobsBySession 换成了 projectionsBySession。
即:页面级(非 slot 内)插件目前没有任何官方途径知道"当前是哪个会话"。这也是本 issue 建议内核侧补齐的部分。
最小复现
手工:
- DSH
0.1.6-alpha.2 + 插件 1.4.10;
- 选中助手回复里一段文字 → 添加批注(内容可留空);
- 输入框写一句话 → 回车;
- 看消息内容与 Console:批注块缺失,且没有
[annotation] 批注块已拼入草稿… 日志。
代码级(沿用插件自带夹具的抽取方式,只把假内核换成 0.1.6 的真实形状):
var sessions = { list: { getSnapshot: () => ({ ids: ['session-a'], byId: {}, phase: 'ready' }) }, scope: () => ({}) }
attachAndSend({}) 返回 false,草稿保持原样。
建议修法
首选(内核侧):给页面级插件一个公开的只读选中态——例如把 uiWorkspace.selection 暴露为只读快照源,或在 ISessions 上恢复等价能力。这样插件只需改一处 id 来源,也不会再因内核内部重构而断裂。
临时(插件侧):读 localStorage['dsh.sessions.current'] 的 sessionId,再走 sessions.scope(id)。我在本地按此思路打了补丁并验证通过,改动共 3 处:
var sessions = ctx.sessions
+ // ---- 兼容 DSH 0.1.6-alpha.2+(内核 commit 6830e1460d 移除了 sessions.list.current)----
+ // 当前会话改由 uiWorkspace 的私有 selection store 持有,内核未提供公开读取面;
+ // 该 store 以 'dsh.sessions.current' 键持久化到 localStorage,故在此退化为读它。
+ function currentSessionId() {
+ try {
+ var raw = localStorage.getItem('dsh.sessions.current')
+ if (raw === null) return undefined
+ var parsed = JSON.parse(raw)
+ return parsed !== null && typeof parsed === 'object'
+ && typeof parsed.sessionId === 'string' ? parsed.sessionId : undefined
+ } catch (_) { return undefined }
+ }
另两处:7 个调用点 sessions.list.getSnapshot().current → currentSessionId();会话切换检测改为 sessions.list.subscribe(fn) + setInterval(fn, 300),并在已有清理函数里 clearInterval。
注意这是权宜之计:它依赖内核内部持久化键名,键名或持久化方式再变即失效。长期仍建议内核提供公开读面。
测试夹具需要同步(不是行为回归)
test/message-block.test.mjs 的 harness 把假内核硬编码为 { list: { getSnapshot: () => ({ current: 'session' }) } },并把函数单独 eval:换成读 helper 后,抽出的 attachAndSend 会报 currentSessionId is not defined,导致 4 项测试变红(需把 helper 一并注入 harness)。
test/pending-clear.test.mjs 的"会话切换作废旧会话未消费的发送暂存数据"依赖 sessions.list.subscribe(function () { 这一写法,改具名函数后该项变红。
相关但不同(避免混淆)
验证记录
同一夹具 + 真实 0.1.6 内核形状(list 快照无 current),打补丁前后对照:
|
attachAndSend() 返回 |
草稿是否含批注 |
sessions.scope() 收到 |
原始 1.4.10 |
false |
否 |
从未调用 |
| 打过补丁 |
true |
是(且保留原问题) |
"session-a" |
其余:node --check 通过;8 处 currentSessionId 引用经括号作用域分析全在 apply() 内;重复调用不重复拼稿;硬刷新后 Console 出现 [annotation] 批注块已拼入草稿,回车将随消息发送(1 条),模型按 Annotation 1:… 逐条回应。
环境
@changfenhuang/dsh-annotation@1.4.10(npmlatest);另核对1.4.11-preview.1,受影响0.1.6-alpha.2(tagdsh-v0.1.6-alpha.2,commitddefc45fbc,源码 checkout 构建)dsh web)现象
选中助手文字 → 添加批注 →「批注 ×N」标签正常出现 → 回车发送:
[annotation] 批注块已拼入草稿,回车将随消息发送(N 条);根因
1.4.10的client.js在 7 处读取sessions.list.getSnapshot().current:第 879 / 1146 / 1518 / 1799 / 1802 / 2205 / 2208 行。DSH commit
6830e1460d(refactor(session-controller): own Client Session generations,2026-09-17)把current从SessionListState移除:SessionListState.currentdsh-v0.1.5-rc.2(1.4.10 的验收宿主)dsh-v0.1.6-alpha.1(1.4.11-preview.1 的验收宿主)dsh-v0.1.6-alpha.2attachAndSend()的写法是先静默返回、后进try/catch,所以整条链路没有任何提示:同一原因还波及(可用同一种修法一起解决):
submitAttached():Cmd/Ctrl+Enter 发纯批注同样提前返回;watchInputDraft():以"草稿由有变空"作为已发送的清理权威,现在每秒重试、永不挂上;writeCurrentPendingQuotes()/readPendingQuotes():按会话持久化的 key 恒为undefined,读写都失效;sessions.list.subscribe(...)不再于切换时触发(跨会话保留未发送批注的特性失效)。内核侧没有公开替代读面
packages/client/ui-workspace/src/client/navigation.ts的private readonly selection(持久化键dsh.sessions.current);UiWorkspace公开接口只有openSession/startSession/archiveSession这类动作,没有任何读取成员。ISessions(packages/api/session-controller/src/client/contract/sessions.ts)也没有等价读取:scope(id)/scopeOf(ctx)都需要你先知道 id,或已处于该会话的 ctx。dsh-v0.1.7-alpha.2与 master(2026-09-22):SessionListState仍无current,selection仍为私有;0.1.7 还把subagentsByParent/jobsBySession换成了projectionsBySession。即:页面级(非 slot 内)插件目前没有任何官方途径知道"当前是哪个会话"。这也是本 issue 建议内核侧补齐的部分。
最小复现
手工:
0.1.6-alpha.2+ 插件1.4.10;[annotation] 批注块已拼入草稿…日志。代码级(沿用插件自带夹具的抽取方式,只把假内核换成 0.1.6 的真实形状):
attachAndSend({})返回false,草稿保持原样。建议修法
首选(内核侧):给页面级插件一个公开的只读选中态——例如把
uiWorkspace.selection暴露为只读快照源,或在ISessions上恢复等价能力。这样插件只需改一处 id 来源,也不会再因内核内部重构而断裂。临时(插件侧):读
localStorage['dsh.sessions.current']的sessionId,再走sessions.scope(id)。我在本地按此思路打了补丁并验证通过,改动共 3 处:另两处:7 个调用点
sessions.list.getSnapshot().current→currentSessionId();会话切换检测改为sessions.list.subscribe(fn)+setInterval(fn, 300),并在已有清理函数里clearInterval。注意这是权宜之计:它依赖内核内部持久化键名,键名或持久化方式再变即失效。长期仍建议内核提供公开读面。
测试夹具需要同步(不是行为回归)
test/message-block.test.mjs的 harness 把假内核硬编码为{ list: { getSnapshot: () => ({ current: 'session' }) } },并把函数单独eval:换成读 helper 后,抽出的attachAndSend会报currentSessionId is not defined,导致 4 项测试变红(需把 helper 一并注入 harness)。test/pending-clear.test.mjs的"会话切换作废旧会话未消费的发送暂存数据"依赖sessions.list.subscribe(function () {这一写法,改具名函数后该项变红。相关但不同(避免混淆)
textarea→contenteditable适配,不是同一原因。验证记录
同一夹具 + 真实 0.1.6 内核形状(
list快照无current),打补丁前后对照:attachAndSend()返回sessions.scope()收到1.4.10falsetrue"session-a"其余:
node --check通过;8 处currentSessionId引用经括号作用域分析全在apply()内;重复调用不重复拼稿;硬刷新后 Console 出现[annotation] 批注块已拼入草稿,回车将随消息发送(1 条),模型按Annotation 1:…逐条回应。