dsh-sessions-manager 3.4.1 → 3.5.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-sessions-manager",
3
3
  "description": "DSH 设置面板会话管理器:归档 / 恢复 / 彻底删除 / 移动到其他工作区,带工作区标签与会话日期;统一「会话管理」面板。Session manager for the DeepSeek Harness settings panel — archive / restore / permanently delete / move sessions across workspaces, with workspace tags & session dates in one unified panel.",
4
- "version": "3.4.1",
4
+ "version": "3.5.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -219,6 +219,7 @@ function installSettingsNavIcons(ctx) {
219
219
  function SessionPanel({ workspacesSvc }) {
220
220
  const [sessions, setSessions] = useState(null)
221
221
  const [workspaces, setWorkspaces] = useState([])
222
+ const [capabilities, setCapabilities] = useState(null)
222
223
  const initialPrefs = useRef(loadPanelPrefs()).current
223
224
  // 注意:'storage' 已从可持久化取值中移除(它不再是视图),旧版残留的
224
225
  // filter='storage' 会自动回落到 'all',避免落到一个已不存在的界面。
@@ -276,14 +277,16 @@ function SessionPanel({ workspacesSvc }) {
276
277
  Promise.all([
277
278
  postJSON('/archived-sessions/sessions', {}),
278
279
  postJSON('/archived-sessions/workspaces', {}),
280
+ postJSON('/archived-sessions/capabilities', {}),
279
281
  ])
280
- .then(([s, works]) => {
282
+ .then(([s, works, caps]) => {
281
283
  // Permanently-purged sessions must never re-appear here, even if DSH's
282
284
  // in-memory session index still lists them after the file was unlinked.
283
285
  const purged = dsmLoadPurged()
284
286
  const visible = (s.items || []).filter((x) => !purged.has(String(x.sessionId)))
285
287
  setSessions(visible)
286
288
  setWorkspaces(works.items || [])
289
+ setCapabilities(caps || null)
287
290
  setSelected({})
288
291
  setDelTarget(null)
289
292
  setConfirmBatch(false)
@@ -393,6 +396,9 @@ function SessionPanel({ workspacesSvc }) {
393
396
  const w = workspaces.find((x) => x.workspaceId === id)
394
397
  return w ? w.path : ''
395
398
  }
399
+ const actionCapability = (name) => (capabilities && capabilities.actions && capabilities.actions[name]) || { available: false, reason: '正在检查当前 DSH 的兼容能力…' }
400
+ const canPurge = actionCapability('purge')
401
+ const canMove = actionCapability('move')
396
402
 
397
403
  const archivedList = sessions ? sessions.filter((x) => x.archived) : []
398
404
  const activeList = sessions ? sessions.filter((x) => !x.archived) : []
@@ -473,7 +479,12 @@ function SessionPanel({ workspacesSvc }) {
473
479
  const verifyTrash = () => {
474
480
  setTrashBusy('__verify')
475
481
  postJSON('/archived-sessions/trash/verify', {})
476
- .then((r) => { setTrashBusy(null); setTrashCheck(r); showToast(r.missing ? `发现 ${r.missing} 条日志缺失` : '回收站校验通过') })
482
+ .then((r) => {
483
+ setTrashBusy(null); setTrashCheck(r)
484
+ if (r.missing) showToast(`发现 ${r.missing} 条日志缺失`)
485
+ else if (r.unverified) showToast(`${r.unverified} 条日志位置由当前 Runtime 管理,无法直接核验`)
486
+ else showToast('回收站校验通过')
487
+ })
477
488
  .catch((e) => { setTrashBusy(null); setError(String((e && e.message) || e)) })
478
489
  }
479
490
 
@@ -554,6 +565,7 @@ function SessionPanel({ workspacesSvc }) {
554
565
  }
555
566
 
556
567
  const doMove = (it) => {
568
+ if (!canMove.available) { setError(canMove.reason || '当前环境不支持跨工作区移动'); return }
557
569
  const targetPath = moveMode === 'new' ? newPath.trim() : wsPath(targetWs)
558
570
  if (!targetPath) { setError('请选择已有工作区或输入新的目标目录路径'); return }
559
571
  setBusy(it.sessionId)
@@ -591,6 +603,7 @@ function SessionPanel({ workspacesSvc }) {
591
603
  }
592
604
 
593
605
  const openMoveFor = (it) => {
606
+ if (!canMove.available) { setError(canMove.reason || '当前环境不支持跨工作区移动'); return }
594
607
  if (openMove === it.sessionId) { setOpenMove(null); return }
595
608
  setTargetWs(workspaces.length ? (targetWs || workspaces[0].workspaceId) : '')
596
609
  setMoveMode('existing')
@@ -663,15 +676,20 @@ function SessionPanel({ workspacesSvc }) {
663
676
  >⋯</button>
664
677
  {openMenu === it.sessionId && (
665
678
  <div className="more-menu" role="menu">
666
- {items.map(([id, label]) => (
679
+ {items.map(([id, label]) => {
680
+ const blocked = id === 'move' && !canMove.available
681
+ return (
667
682
  <button
668
683
  key={id}
669
684
  type="button"
670
685
  role="menuitem"
671
686
  className={'more-item' + (id === 'delete' ? ' more-item-danger' : '')}
687
+ disabled={blocked}
688
+ title={blocked ? canMove.reason : undefined}
672
689
  onClick={() => runMenu(id, it)}
673
690
  >{label}</button>
674
- ))}
691
+ )
692
+ })}
675
693
  </div>
676
694
  )}
677
695
  </div>
@@ -1034,9 +1052,10 @@ function SessionPanel({ workspacesSvc }) {
1034
1052
  </select>
1035
1053
  </div>
1036
1054
  <div className="sess-field"><label>数据检查</label><button type="button" className="archv-btn" disabled={trashBusy !== null} onClick={verifyTrash}>{trashBusy === '__verify' ? '校验中…' : '校验日志完整性'}</button></div>
1037
- <div className="sess-field"><label>永久清理</label><button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !trash.length} onClick={() => setPurgeTarget('__all')}>清空回收站</button></div>
1055
+ <div className="sess-field"><label>永久清理</label><button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !trash.length || !canPurge.available} title={!canPurge.available ? canPurge.reason : undefined} onClick={() => setPurgeTarget('__all')}>清空回收站</button></div>
1038
1056
  </div>
1039
- {trashCheck && <div className={trashCheck.missing ? 'archv-err' : 'archv-empty'} role="status">校验完成:{trashCheck.healthy} 条正常,{trashCheck.missing} 条日志缺失。</div>}
1057
+ {!canPurge.available && <div className="archv-empty" role="status">{canPurge.reason}</div>}
1058
+ {trashCheck && <div className={trashCheck.missing ? 'archv-err' : 'archv-empty'} role="status">校验完成:{trashCheck.healthy} 条正常,{trashCheck.missing} 条日志缺失,{trashCheck.unverified || 0} 条无法直接核验。</div>}
1040
1059
  {trash.length === 0 ? (
1041
1060
  <div className="dsm-trash-empty">回收站为空。删除的会话会先进入这里,可恢复或彻底删除。</div>
1042
1061
  ) : (
@@ -1047,7 +1066,7 @@ function SessionPanel({ workspacesSvc }) {
1047
1066
  <span className="dsm-trash-date">{fmtDate(t.deletedAt)}</span>
1048
1067
  <span className="dsm-trash-actions">
1049
1068
  <button type="button" className="archv-btn" disabled={trashBusy !== null} onClick={() => restoreTrash(t.sessionId)}>恢复</button>
1050
- <button type="button" className="archv-btn archv-del" disabled={trashBusy !== null} onClick={() => setPurgeTarget(t.sessionId)}>彻底删除</button>
1069
+ <button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !canPurge.available} title={!canPurge.available ? canPurge.reason : undefined} onClick={() => setPurgeTarget(t.sessionId)}>彻底删除</button>
1051
1070
  </span>
1052
1071
  </div>
1053
1072
  ))}
@@ -1169,6 +1188,14 @@ let dsmTrashIds = null
1169
1188
  let dsmServerPurgedIds = new Set()
1170
1189
  let dsmAuthoritativeTitles = new Map()
1171
1190
  let dsmTrashTick = 0
1191
+ let dsmCapabilities = null
1192
+ async function dsmLoadCapabilities() {
1193
+ try { dsmCapabilities = await postJSON('/archived-sessions/capabilities', {}) } catch (e) { /* unknown stays safely unavailable */ }
1194
+ return dsmCapabilities
1195
+ }
1196
+ function dsmActionCapability(name) {
1197
+ return dsmCapabilities && dsmCapabilities.actions && dsmCapabilities.actions[name]
1198
+ }
1172
1199
  async function dsmLoadTrashIds() {
1173
1200
  try {
1174
1201
  const r = await postJSON('/archived-sessions/sidebar-state', {})
@@ -1385,6 +1412,11 @@ function installSidebarSessionMenuAug() {
1385
1412
  const move = mk('移动会话', ICON_MOVE, false)
1386
1413
  const del = mk('删除会话', ICON_DEL, true)
1387
1414
  const mark = mk(dsmLoadManual().has(info.id) ? '标记已读' : '标记未读', ICON_UNREAD, false)
1415
+ const moveCapability = dsmActionCapability('move')
1416
+ if (!moveCapability || !moveCapability.available) {
1417
+ move.btn.disabled = true
1418
+ move.btn.title = (moveCapability && moveCapability.reason) || '正在检查当前版本的移动能力'
1419
+ }
1388
1420
  if (mark.btn.firstChild) mark.btn.firstChild.style.color = 'var(--dsw-alias-state-business-primary)'
1389
1421
  const chev = document.createElement('span')
1390
1422
  chev.style.cssText = 'margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:1'
@@ -1647,6 +1679,8 @@ function installSidebarWorkspaceDrag() {
1647
1679
  }, true)
1648
1680
 
1649
1681
  document.addEventListener('dragstart', (event) => {
1682
+ const moveCapability = dsmActionCapability('move')
1683
+ if (!moveCapability || !moveCapability.available) return
1650
1684
  const row = eventRow(event)
1651
1685
  const item = row && sessionForRow(row)
1652
1686
  if (!item || moving) return
@@ -1729,6 +1763,7 @@ function installSidebarWorkspaceDrag() {
1729
1763
  }
1730
1764
 
1731
1765
  export function apply(ctx) {
1766
+ dsmLoadCapabilities()
1732
1767
  installSettingsNavIcons(ctx)
1733
1768
  installSidebarSessionMenuAug()
1734
1769
  installSidebarStatusDots()
@@ -0,0 +1,34 @@
1
+ function action(available, reason = null) {
2
+ return { available: !!available, reason: available ? null : reason }
3
+ }
4
+
5
+ export function detectCapabilities({ persistence, workspaceRegistry }) {
6
+ const handleApi = !!(persistence && typeof persistence.open === 'function')
7
+ const legacyRead = !!(persistence && typeof persistence.readFrom === 'function')
8
+ const legacyLocate = !!(persistence && (typeof persistence.locate === 'function'
9
+ || (persistence.backend && typeof persistence.backend.locate === 'function')))
10
+ const workspaceInternals = !!(workspaceRegistry
11
+ && workspaceRegistry.headers && workspaceRegistry.sessionPaths
12
+ && typeof workspaceRegistry.replaceHeaderIndex === 'function')
13
+
14
+ return {
15
+ persistence: handleApi ? 'session-handle' : 'legacy',
16
+ actions: {
17
+ read: action(legacyRead || handleApi, '当前 DSH 未提供可识别的会话读取接口'),
18
+ archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === 'function'), '当前 DSH 未提供归档接口'),
19
+ trash: action(legacyRead || handleApi, '当前 DSH 无法读取会话,不能安全移入回收站'),
20
+ restoreTrash: action(true),
21
+ purge: action(!handleApi && legacyLocate, '当前 DSH 版本尚未提供经过验证的安全永久删除能力'),
22
+ move: action(!handleApi && legacyRead && legacyLocate && workspaceInternals, '当前 DSH 版本尚未提供经过验证的跨工作区迁移能力'),
23
+ },
24
+ }
25
+ }
26
+
27
+ export function requireCapability(capabilities, name) {
28
+ const value = capabilities && capabilities.actions && capabilities.actions[name]
29
+ if (value && value.available) return
30
+ const error = new Error((value && value.reason) || `当前环境不支持 ${name}`)
31
+ error.status = 409
32
+ error.code = 'DSM_CAPABILITY_UNAVAILABLE'
33
+ throw error
34
+ }
@@ -0,0 +1,72 @@
1
+ // Compatibility boundary for the two DSH persistence generations supported by
2
+ // dsh-sessions-manager. Business code consumes normalized headers and complete
3
+ // inspections; it never needs to know whether DSH returned a legacy header or
4
+ // a handle-era SessionPersistenceSnapshot.
5
+
6
+ function asHeader(value) {
7
+ if (!value || typeof value !== 'object') return null
8
+ const candidate = value.header && typeof value.header === 'object' ? value.header : value
9
+ return candidate.id == null ? null : candidate
10
+ }
11
+
12
+ export function normalizePersistenceEntry(value) {
13
+ const header = asHeader(value)
14
+ if (!header) return null
15
+ const snapshot = value && value.header === header ? value : null
16
+ return {
17
+ header,
18
+ snapshot,
19
+ id: String(header.id),
20
+ sizeBytes: snapshot && Number.isFinite(snapshot.sizeBytes) ? Number(snapshot.sizeBytes) : null,
21
+ eventCount: snapshot && Number.isSafeInteger(snapshot.eventCount) ? snapshot.eventCount : null,
22
+ revision: snapshot ? snapshot.revision : null,
23
+ }
24
+ }
25
+
26
+ export function normalizePersistenceList(values) {
27
+ if (!Array.isArray(values)) return []
28
+ return values.map(normalizePersistenceEntry).filter(Boolean)
29
+ }
30
+
31
+ export function createPersistenceAdapter(service) {
32
+ if (!service || typeof service.list !== 'function') throw new TypeError('sessionPersistence.list is required')
33
+
34
+ async function listEntries(options) {
35
+ return normalizePersistenceList(await service.list(options))
36
+ }
37
+
38
+ async function readSession(id, offset = 0) {
39
+ if (typeof service.readFrom === 'function') {
40
+ const result = await service.readFrom(id, offset)
41
+ return {
42
+ meta: result && result.meta ? result.meta : null,
43
+ inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
44
+ events: result && Array.isArray(result.events) ? result.events : [],
45
+ }
46
+ }
47
+ if (typeof service.open !== 'function') throw new Error('当前 DSH 持久化服务不支持读取会话')
48
+ const handle = await service.open(id, 'read')
49
+ if (!handle || typeof handle.read !== 'function' || typeof handle.close !== 'function') {
50
+ try { if (handle && typeof handle.close === 'function') await handle.close() } catch {}
51
+ throw new Error('DSH 返回了无效的 SessionHandle')
52
+ }
53
+ try {
54
+ const events = await handle.read(offset)
55
+ return {
56
+ meta: handle.header || handle.meta || null,
57
+ inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
58
+ events: Array.isArray(events) ? events : [...(events || [])],
59
+ }
60
+ } finally {
61
+ await handle.close()
62
+ }
63
+ }
64
+
65
+ function locate(header) {
66
+ if (typeof service.locate === 'function') return service.locate(header)
67
+ return null
68
+ }
69
+
70
+ const kind = typeof service.open === 'function' ? 'session-handle' : 'legacy'
71
+ return { kind, listEntries, readSession, locate }
72
+ }