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/README.en.md +6 -0
- package/README.md +6 -0
- package/lib/client.js +60 -17
- package/lib/client.js.map +2 -2
- package/lib/index.js +375 -82
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +42 -7
- package/src/compat/capabilities.js +34 -0
- package/src/compat/persistence.js +72 -0
- package/src/index.js +167 -63
- package/src/title-persist-index.js +137 -0
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
|
+
"version": "3.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/client/index.jsx
CHANGED
|
@@ -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) => {
|
|
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
|
-
{
|
|
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
|
+
}
|