dsh-session-manager 0.7.1 → 0.7.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 +8 -0
- package/README.md +18 -16
- package/README.zh.md +19 -17
- package/lib/client.js +29 -30
- package/lib/index.js +20 -9
- package/lib/session-files.js +73 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.7.2 — 2026-10-17
|
|
6
|
+
|
|
7
|
+
- **fix(encoding)**: Correct session directory encoding and candidate matching for IM and non-UUID sessions (`encodeFsSessionSegment` matching DSH `encodeSegment` with `~XXXX` hex escapes). Support colons and non-alphanumeric characters in session IDs without directory traversal risks, distinguish project keys from session directory segments, and maintain multi-candidate fallback compatibility for existing sessions (#39).
|
|
8
|
+
- **fix(ui)**: Scope custom dropdown interceptor to Session Manager elements only (`getTargetSmSelect`), preventing global interception of third-party plugin selects and DSH native dropdowns and eliminating unwanted `preventDefault` / `stopPropagation` calls (#36).
|
|
9
|
+
- **fix(ui)**: Prevent scrolling within the custom select menu from closing the menu, enabling smooth wheel scrolling and scrollbar dragging across long option lists (PR #37).
|
|
10
|
+
- **fix(ui)**: Improve sidebar footer layout compatibility when multiple plugins inject actions into `sidebar.footer.action`. Apply scoped `flex-wrap: wrap` on the footer actions container and update `.sm-footerBtn-wide` to `flex: 1 1 100%`, preventing overlapping buttons and horizontal squishing without fragile CSS module hashes or targeting other plugins' private classes (#38).
|
|
11
|
+
- **compat**: Compatible with DSH Desktop V0.2.1-rc.2 (and V0.2.0-rc.2).
|
|
12
|
+
|
|
5
13
|
## 0.7.1 — 2026-10-15
|
|
6
14
|
|
|
7
15
|
- **fix(cache)**: Decouple boot-time workspace session membership reconciliation from projection cache reconstruction, preventing background event-loop blocking and watchdog recovery timeouts on large session libraries (#32).
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-session-manager)
|
|
6
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
7
|
[](https://github.com/hkkz9522/dsh-session-manager)
|
|
7
8
|
[](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
|
|
8
9
|
[](https://awesome-dsh-plugin.com)
|
|
@@ -51,7 +52,7 @@ Verified with the official DSH Desktop client; see [5 Compatibility](#5-compatib
|
|
|
51
52
|
- **Theme**: Automatically follows the active DSH theme (including third-party theme plugins). Dialogs, settings cards, title-bar buttons, menus, inputs, and all other UI elements seamlessly adapt to light and dark themes.
|
|
52
53
|
- **Unified dropdown styling**: All select dropdowns (sort, filter, priority) use unified shadow-free rounded custom floating menus matching active themes, strictly aligned with trigger widths, eliminating Chromium duplicate native popups.
|
|
53
54
|
|
|
54
|
-
### 1.5 Workspace Archiving and Management
|
|
55
|
+
### 1.5 Workspace Archiving and Management
|
|
55
56
|
|
|
56
57
|
- **Two-page tab switching**: Seamlessly switch between "Session Management" and "Workspace Management" in the dialog header, preserving each page's distinct search, filter, and sort states.
|
|
57
58
|
- **Workspace list**: Displays all registered workspaces with title/name, physical path, session count, latest activity timestamp computed from member sessions, and right-side toggle switch.
|
|
@@ -140,21 +141,22 @@ After changing plugin dependencies, `package.json`, bundle configuration, or oth
|
|
|
140
141
|
|
|
141
142
|
## 5 Compatibility
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
> Verification note
|
|
146
|
-
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
151
|
-
| v0.1.
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
155
|
-
| v0.1.
|
|
156
|
-
|
|
|
157
|
-
|
|
|
144
|
+
The table below lists verified version combinations between DeepSeek Harness (DSH) and `dsh-session-manager`.
|
|
145
|
+
|
|
146
|
+
> **Verification note**: From version 0.6.3 onwards, this plugin is verified with the official DSH Desktop client only (the Web UI shares the same Host/client codebase but is no longer covered by the verification matrix).
|
|
147
|
+
|
|
148
|
+
| DeepSeek Harness (DSH) | dsh-session-manager | Verified Environment |
|
|
149
|
+
| :--- | :--- | :--- |
|
|
150
|
+
| **v0.2.1-rc.2** | `0.7.2` | Desktop |
|
|
151
|
+
| **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
|
|
152
|
+
| **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
|
|
153
|
+
| **v0.1.7-rc.1** | `0.5.2` | Desktop / Web UI |
|
|
154
|
+
| **v0.1.6-alpha.2** | `0.5.1` | Web UI |
|
|
155
|
+
| **v0.1.5-rc.2** | `0.4.11` | Web UI |
|
|
156
|
+
| **v0.1.5-rc.1** | `0.4.10`, `0.4.9`, `0.4.7` | Web UI |
|
|
157
|
+
| **v0.1.3-alpha.2** | `0.4.6`, `0.4.4`, `0.4.1` | Web UI |
|
|
158
|
+
| **v0.1.2-rc.1** | `0.4.0` | Web UI |
|
|
159
|
+
| **v0.1.0-rc.7** | `0.1.2`, `0.1.1`, `0.1.0` | Web UI |
|
|
158
160
|
|
|
159
161
|
The version combinations above have been tested with either the official Web UI or Desktop app. Other version combinations may also work but have not been individually verified.
|
|
160
162
|
|
package/README.zh.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-session-manager)
|
|
6
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
7
|
[](https://github.com/hkkz9522/dsh-session-manager)
|
|
7
8
|
[](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
|
|
8
9
|
[](https://awesome-dsh-plugin.com)
|
|
@@ -42,14 +43,14 @@ DSH 会话与工作区管理——工作区:归档与显示。会话:跨工
|
|
|
42
43
|
|
|
43
44
|
### 1.4 插件更新与设置卡片
|
|
44
45
|
|
|
45
|
-
-
|
|
46
|
+
- **检查更新**:会话管理窗口顶部提供 🐋(鲸鱼)图标按钮,点击可检查新版本并在有更新时展示小红点提示;点击弹出更新窗口,展示当前版本与最新版本,支持一键调用插件管理器安装更新。
|
|
46
47
|
- **设置卡片**:注册在 DSH 设置的插件配置页(`settings.plugin.item`)。展示当前安装版本、最新版本状态、卡片内检查更新与更新按钮、自动检查偏好开关以及 GitHub 仓库链接。
|
|
47
48
|
- **安装源选择**:在设置卡片中可随时切换安装源,可选 **npm 官方源**(`registry.npmjs.org`,默认)与 **中国大陆镜像源**(`registry.npmmirror.com`);检查更新与版本下载将直接请求所选源。
|
|
48
49
|
- **弹窗透明度**:设置卡片「偏好设置」区域提供 50%–100% 滑块(步长 5%,默认 100%)与「恢复默认」按钮,支持调节插件弹窗的背景填充透明度,文字、按钮、输入框、标签与边框等内容保持不透明并即时生效。
|
|
49
50
|
- **主题**:全面跟随 DSH 当前主题(包含第三方主题插件),弹窗、设置卡片、标题栏按钮、下拉菜单、输入框等界面元素自动适配浅色与深色主题。
|
|
50
51
|
- **统一下拉样式**:所有下拉选择框(排序、筛选、优先级等)采用无四周阴影的统一圆角自定义浮层,严格对齐触发框宽度,并解决 Chromium 原生系统弹层与自定义浮层重叠的双层问题。
|
|
51
52
|
|
|
52
|
-
### 1.5
|
|
53
|
+
### 1.5 工作区归档与管理
|
|
53
54
|
|
|
54
55
|
- **双页面切换**:在管理弹窗顶部可在「会话管理」与「工作区管理」之间无缝切换,保留各自的搜索与筛选状态。
|
|
55
56
|
- **工作区列表**:完整展示全部已注册工作区,包含工作区名称、物理路径、所属会话数、基于所属会话计算的最近更新时间及状态开关。
|
|
@@ -138,21 +139,22 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
138
139
|
|
|
139
140
|
## 5 兼容性
|
|
140
141
|
|
|
141
|
-
DSH
|
|
142
|
-
|
|
143
|
-
>
|
|
144
|
-
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
149
|
-
| v0.1.
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
153
|
-
| v0.1.
|
|
154
|
-
|
|
|
155
|
-
|
|
|
142
|
+
下表列出 DeepSeek Harness(DSH)与 `dsh-session-manager` 已验证的版本组合。
|
|
143
|
+
|
|
144
|
+
> **版本验证说明**:自 0.6.3 起,本插件仅在官方 DSH Desktop 客户端上进行验证(Web UI 与本插件共用同一套 Host / 客户端代码,但不再纳入常规验证范围)。
|
|
145
|
+
|
|
146
|
+
| DeepSeek Harness (DSH) | dsh-session-manager | 验证环境 |
|
|
147
|
+
| :--- | :--- | :--- |
|
|
148
|
+
| **v0.2.1-rc.2** | `0.7.2` | Desktop |
|
|
149
|
+
| **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
|
|
150
|
+
| **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
|
|
151
|
+
| **v0.1.7-rc.1** | `0.5.2` | Desktop / Web UI |
|
|
152
|
+
| **v0.1.6-alpha.2** | `0.5.1` | Web UI |
|
|
153
|
+
| **v0.1.5-rc.2** | `0.4.11` | Web UI |
|
|
154
|
+
| **v0.1.5-rc.1** | `0.4.10`, `0.4.9`, `0.4.7` | Web UI |
|
|
155
|
+
| **v0.1.3-alpha.2** | `0.4.6`, `0.4.4`, `0.4.1` | Web UI |
|
|
156
|
+
| **v0.1.2-rc.1** | `0.4.0` | Web UI |
|
|
157
|
+
| **v0.1.0-rc.7** | `0.1.2`, `0.1.1`, `0.1.0` | Web UI |
|
|
156
158
|
|
|
157
159
|
以上版本组合已在官方 Web UI 或 Desktop 客户端中完成测试。其他版本组合可能同样兼容,但未逐一验证。
|
|
158
160
|
|
package/lib/client.js
CHANGED
|
@@ -862,7 +862,8 @@ function isTopmostDialog(z) {
|
|
|
862
862
|
".sm-bulkResultItemReason{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;word-break:break-word}",
|
|
863
863
|
".sm-bulkResultItemReasonSkip{color:var(--dsw-alias-label-tertiary,#888)}",
|
|
864
864
|
".sm-bulkResultFooter{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
865
|
-
".sm-footerBtn-wide
|
|
865
|
+
"[class*=footerActions]:has(.sm-footerBtn-wide),[class*=footer-actions]:has(.sm-footerBtn-wide),[class*=footer]:has(> .sm-footerBtn-wide),[class*=footer] > div:has(> .sm-footerBtn-wide){flex-wrap:wrap;align-content:flex-start;height:auto}",
|
|
866
|
+
".sm-footerBtn-wide{flex:1 1 100%;display:flex;align-items:center;gap:8px;width:calc(100% + 8px);height:34px;margin:4px -4px 4px;padding:6px 2px 6px 10px;box-sizing:border-box;border:none;border-radius:12px;cursor:pointer;overflow:hidden;font-size:14px;line-height:22px}",
|
|
866
867
|
".sm-footerBtn-wide:hover,.sm-footerBtn-wide:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
867
868
|
".sm-footerBtn-rail{width:36px;height:36px;padding:0;justify-content:center;border-radius:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;display:inline-flex;align-items:center}",
|
|
868
869
|
".sm-footerBtn-rail:hover,.sm-footerBtn-rail:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
@@ -2100,6 +2101,18 @@ function isTopmostDialog(z) {
|
|
|
2100
2101
|
}
|
|
2101
2102
|
}
|
|
2102
2103
|
|
|
2104
|
+
function getTargetSmSelect(target) {
|
|
2105
|
+
if (!target || typeof target.closest !== "function") return null;
|
|
2106
|
+
const direct = target.closest(SM_SELECT_SELECTOR);
|
|
2107
|
+
if (direct && direct.tagName === "SELECT") return direct;
|
|
2108
|
+
const container = target.closest(".sm-wsSortField, .sm-annotationPriority, .sm-viewField, .sm-moveSection, .sm-settingsRow, .sm-customSelectWrapper");
|
|
2109
|
+
if (container) {
|
|
2110
|
+
const sel = container.querySelector("select");
|
|
2111
|
+
if (sel && sel.tagName === "SELECT") return sel;
|
|
2112
|
+
}
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2103
2116
|
function installCustomDropdownInterceptor() {
|
|
2104
2117
|
if (typeof document === "undefined" || typeof document.addEventListener !== "function") {
|
|
2105
2118
|
return () => {};
|
|
@@ -2113,24 +2126,8 @@ function isTopmostDialog(z) {
|
|
|
2113
2126
|
if (activeCustomMenu && activeCustomMenu.contains(e.target)) return;
|
|
2114
2127
|
if (e.target?.closest?.("button, input, textarea, a")) return;
|
|
2115
2128
|
|
|
2116
|
-
|
|
2117
|
-
if (
|
|
2118
|
-
select = e.target;
|
|
2119
|
-
} else if (e.target?.closest?.("select")) {
|
|
2120
|
-
select = e.target.closest("select");
|
|
2121
|
-
} else {
|
|
2122
|
-
const container = e.target?.closest?.(".sm-wsSortField, .sm-annotationPriority, .sm-viewField, .sm-moveSection, .sm-settingsRow, .sm-customSelectWrapper");
|
|
2123
|
-
if (container) {
|
|
2124
|
-
select = container.querySelector("select");
|
|
2125
|
-
} else {
|
|
2126
|
-
const nearest = e.target?.closest?.(SM_SELECT_SELECTOR);
|
|
2127
|
-
if (nearest && nearest.tagName === "SELECT") {
|
|
2128
|
-
select = nearest;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
if (select && select.tagName === "SELECT") {
|
|
2129
|
+
const select = getTargetSmSelect(e.target);
|
|
2130
|
+
if (select) {
|
|
2134
2131
|
e.preventDefault();
|
|
2135
2132
|
e.stopPropagation();
|
|
2136
2133
|
if (select.disabled) return;
|
|
@@ -2146,14 +2143,8 @@ function isTopmostDialog(z) {
|
|
|
2146
2143
|
|
|
2147
2144
|
const onClick = (e) => {
|
|
2148
2145
|
if (e.target?.closest?.("button, input, textarea, a")) return;
|
|
2149
|
-
|
|
2150
|
-
if (
|
|
2151
|
-
select = e.target;
|
|
2152
|
-
} else {
|
|
2153
|
-
const container = e.target?.closest?.(".sm-wsSortField, .sm-annotationPriority, .sm-viewField, .sm-moveSection, .sm-settingsRow, .sm-customSelectWrapper");
|
|
2154
|
-
if (container) select = container.querySelector("select");
|
|
2155
|
-
}
|
|
2156
|
-
if (select && select.tagName === "SELECT") {
|
|
2146
|
+
const select = getTargetSmSelect(e.target);
|
|
2147
|
+
if (select) {
|
|
2157
2148
|
e.preventDefault();
|
|
2158
2149
|
e.stopPropagation();
|
|
2159
2150
|
}
|
|
@@ -2170,7 +2161,7 @@ function isTopmostDialog(z) {
|
|
|
2170
2161
|
}
|
|
2171
2162
|
|
|
2172
2163
|
const focused = document.activeElement;
|
|
2173
|
-
const targetSelect = activeCustomSelect || (focused
|
|
2164
|
+
const targetSelect = activeCustomSelect || getTargetSmSelect(focused);
|
|
2174
2165
|
if (!targetSelect || targetSelect.tagName !== "SELECT") return;
|
|
2175
2166
|
|
|
2176
2167
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -2196,7 +2187,14 @@ function isTopmostDialog(z) {
|
|
|
2196
2187
|
}
|
|
2197
2188
|
};
|
|
2198
2189
|
|
|
2199
|
-
const onResizeOrScroll = () => {
|
|
2190
|
+
const onResizeOrScroll = (e) => {
|
|
2191
|
+
// Ignore scrolls that originate inside the open menu: the listener is bound on
|
|
2192
|
+
// window with capture=true, so scrolling the menu's own option list (wheel or
|
|
2193
|
+
// scrollbar drag) would otherwise close it immediately.
|
|
2194
|
+
// Reference: PR #37
|
|
2195
|
+
if (activeCustomMenu && e && e.type === "scroll" && e.target && typeof activeCustomMenu.contains === "function" && activeCustomMenu.contains(e.target)) {
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2200
2198
|
if (activeCustomMenu) closeCustomSelect();
|
|
2201
2199
|
};
|
|
2202
2200
|
|
|
@@ -3749,7 +3747,7 @@ function isTopmostDialog(z) {
|
|
|
3749
3747
|
));
|
|
3750
3748
|
}
|
|
3751
3749
|
|
|
3752
|
-
const CURRENT_VERSION = "0.7.
|
|
3750
|
+
const CURRENT_VERSION = "0.7.2";
|
|
3753
3751
|
|
|
3754
3752
|
function compareSemver(v1, v2) {
|
|
3755
3753
|
if (typeof v1 !== "string" || typeof v2 !== "string") return 0;
|
|
@@ -6856,6 +6854,7 @@ function isTopmostDialog(z) {
|
|
|
6856
6854
|
exports.openCustomSelect = openCustomSelect;
|
|
6857
6855
|
exports.closeCustomSelect = closeCustomSelect;
|
|
6858
6856
|
exports.installCustomDropdownInterceptor = installCustomDropdownInterceptor;
|
|
6857
|
+
exports.getTargetSmSelect = getTargetSmSelect;
|
|
6859
6858
|
exports.SM_SELECT_SELECTOR = SM_SELECT_SELECTOR;
|
|
6860
6859
|
return module.exports;
|
|
6861
6860
|
}
|
package/lib/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import { randomBytes } from "node:crypto";
|
|
|
28
28
|
import { createAnnotationStore } from "./annotation-store.js";
|
|
29
29
|
import { createWorkspaceArchiveStore, assertWorkspaceId } from "./workspace-archive-store.js";
|
|
30
30
|
import { createDshAdapter, normalizeSessionHeader, compareSemver } from "./compat/dsh-adapter.js";
|
|
31
|
-
import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
|
|
31
|
+
import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, candidateSessionDirs, encodeFsSessionSegment, decodeFsSessionSegment, encodeProjectSegment, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
|
|
32
32
|
|
|
33
33
|
export const name = "dsh-session-manager";
|
|
34
34
|
|
|
@@ -125,10 +125,16 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
125
125
|
}
|
|
126
126
|
const result = () => ({ headers, complete: listed && errors.length === 0, errors });
|
|
127
127
|
// Skip known files before fopen; retain the orphan/raw-id compatibility path.
|
|
128
|
+
// Only add canonical session directories that do not start and end with "--"
|
|
129
|
+
// to avoid colliding with legacy projectKey candidates (e.g. "--s1--").
|
|
128
130
|
const knownDirs = new Set();
|
|
129
131
|
for (const id of seen) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
try {
|
|
133
|
+
const canonical = encodeFsSessionSegment(id);
|
|
134
|
+
if (!canonical.startsWith("--") || !canonical.endsWith("--")) {
|
|
135
|
+
knownDirs.add(canonical);
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {}
|
|
132
138
|
}
|
|
133
139
|
if (typeof dshHome !== "function") return result();
|
|
134
140
|
const root = dshHome("sessions");
|
|
@@ -150,7 +156,7 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
150
156
|
for (const filename of ARTIFACT_NAMES) {
|
|
151
157
|
try {
|
|
152
158
|
const header = await readSessionHeader(join(root, proj.name, entry.name, filename));
|
|
153
|
-
if (!
|
|
159
|
+
if (!candidateSessionDirs(header.id).includes(entry.name)) {
|
|
154
160
|
throw new Error("会话目录与 header id 不一致");
|
|
155
161
|
}
|
|
156
162
|
if (!seen.has(header.id)) { seen.add(header.id); headers.push(header); }
|
|
@@ -170,7 +176,7 @@ const listSessionHeaders = async (persistence, dshHome) => (await scanSessionHea
|
|
|
170
176
|
|
|
171
177
|
export function apply(ctx) {
|
|
172
178
|
try {
|
|
173
|
-
const buildId = "dsh-session-manager v0.7.
|
|
179
|
+
const buildId = "dsh-session-manager v0.7.2 (workspace-archive)";
|
|
174
180
|
if (typeof ctx?.logger?.info === "function") ctx.logger.info(buildId);
|
|
175
181
|
else if (typeof console !== "undefined" && console?.info) console.info(buildId);
|
|
176
182
|
} catch (_) { /* ignore logging failures */ }
|
|
@@ -1252,14 +1258,19 @@ ${rest}`, "utf8");
|
|
|
1252
1258
|
catch (error) { if (error.code === "ENOENT") return undefined; throw error; }
|
|
1253
1259
|
for (const proj of projects) {
|
|
1254
1260
|
if (!proj.isDirectory() || proj.isSymbolicLink()) continue;
|
|
1255
|
-
for (const candidate of
|
|
1261
|
+
for (const candidate of candidateSessionDirs(sessionId)) {
|
|
1256
1262
|
const dir = join(root, proj.name, candidate);
|
|
1257
1263
|
try { await assertSessionDirectory(root, dir, sessionId); }
|
|
1258
1264
|
catch (error) { if (error.code === "ENOENT") continue; throw error; }
|
|
1259
1265
|
for (const filename of ARTIFACT_NAMES) {
|
|
1260
1266
|
try {
|
|
1261
1267
|
const artifact = await read(join(dir, filename));
|
|
1262
|
-
if (artifact.meta.id !== sessionId)
|
|
1268
|
+
if (artifact.meta.id !== sessionId) {
|
|
1269
|
+
if (candidate === candidateSessionDirs(sessionId)[0]) {
|
|
1270
|
+
throw new Error("会话工件 header id 与请求不一致");
|
|
1271
|
+
}
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1263
1274
|
return artifact;
|
|
1264
1275
|
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
1265
1276
|
}
|
|
@@ -2013,7 +2024,7 @@ ${rest}`, "utf8");
|
|
|
2013
2024
|
return send(res, 200, { ok: true, result });
|
|
2014
2025
|
}
|
|
2015
2026
|
if (req.method === "GET" && path === "/update/check") {
|
|
2016
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
2027
|
+
const currentVersion = adapter.pluginVersion() || "0.7.2";
|
|
2017
2028
|
let latestVersion = currentVersion;
|
|
2018
2029
|
let hasUpdate = false;
|
|
2019
2030
|
try {
|
|
@@ -2111,4 +2122,4 @@ ${rest}`, "utf8");
|
|
|
2111
2122
|
}), "session-manager: http api");
|
|
2112
2123
|
}
|
|
2113
2124
|
|
|
2114
|
-
export { listSessionHeaders, scanSessionHeaders, encodeSessionSegment, compareSemver };
|
|
2125
|
+
export { listSessionHeaders, scanSessionHeaders, candidateSessionDirs, encodeFsSessionSegment, decodeFsSessionSegment, encodeProjectSegment, encodeSessionSegment, compareSemver };
|
package/lib/session-files.js
CHANGED
|
@@ -21,7 +21,7 @@ const MAX_HEADER_BYTES = 1024 * 1024;
|
|
|
21
21
|
|
|
22
22
|
export function assertSessionId(id) {
|
|
23
23
|
if (typeof id !== "string" || id.trim() === "" || id === "." || id === ".."
|
|
24
|
-
|| /[\\/\x00-\x1f
|
|
24
|
+
|| /[\\/\x00-\x1f<>"|?*]/.test(id) || /[. ]$/.test(id)) {
|
|
25
25
|
throw Object.assign(new Error("sessionId 必须是合法的会话标识,不能包含路径或特殊目录名"), { code: "bad-request" });
|
|
26
26
|
}
|
|
27
27
|
return id;
|
|
@@ -46,12 +46,56 @@ export function parseSessionGenerationFromFilename(filename) {
|
|
|
46
46
|
return match[1] !== undefined ? Number(match[1]) : 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Encode a session ID into its on-disk session directory name,
|
|
51
|
+
* matching DSH kernel's `encodeSegment` contract in
|
|
52
|
+
* packages/session/session-persistence-jsonl/src/format.ts:
|
|
53
|
+
* - "." -> "~002E"
|
|
54
|
+
* - ".." -> "~002E~002E"
|
|
55
|
+
* - Safe chars [A-Za-z0-9._-] (except "~") remain unchanged
|
|
56
|
+
* - Other characters (including ":", "~", non-ASCII) are escaped as ~XXXX (4-digit uppercase hex)
|
|
57
|
+
*/
|
|
58
|
+
export function encodeFsSessionSegment(raw) {
|
|
59
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
60
|
+
throw new Error("cannot encode an empty path segment");
|
|
61
|
+
}
|
|
62
|
+
if (raw === ".") return "~002E";
|
|
63
|
+
if (raw === "..") return "~002E~002E";
|
|
64
|
+
let out = "";
|
|
65
|
+
for (let i = 0; i < raw.length; i++) {
|
|
66
|
+
const code = raw.charCodeAt(i);
|
|
67
|
+
const ch = String.fromCharCode(code);
|
|
68
|
+
if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
|
|
69
|
+
out += ch;
|
|
70
|
+
} else {
|
|
71
|
+
out += "~" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Decode an on-disk session directory name back to raw session ID.
|
|
79
|
+
*/
|
|
80
|
+
export function decodeFsSessionSegment(encoded) {
|
|
81
|
+
if (typeof encoded !== "string" || encoded.length === 0) return encoded;
|
|
82
|
+
if (encoded === "~002E") return ".";
|
|
83
|
+
if (encoded === "~002E~002E") return "..";
|
|
84
|
+
return encoded.replace(/~([0-9A-Fa-f]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Encode a project directory path (cwd) into its on-disk project directory key,
|
|
89
|
+
* matching DSH kernel's `projectKey` contract:
|
|
90
|
+
* Wrapping "--...--" markers with path separators collapsed into "-".
|
|
91
|
+
*/
|
|
92
|
+
export function encodeProjectSegment(path) {
|
|
93
|
+
if (typeof path !== "string") return "--root--";
|
|
50
94
|
let readable = "";
|
|
51
95
|
let separatorRun = false;
|
|
52
|
-
for (let i = 0; i <
|
|
53
|
-
const ch =
|
|
54
|
-
const code =
|
|
96
|
+
for (let i = 0; i < path.length; i++) {
|
|
97
|
+
const ch = path[i];
|
|
98
|
+
const code = path.charCodeAt(i);
|
|
55
99
|
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
56
100
|
if (!separatorRun) readable += "-";
|
|
57
101
|
separatorRun = true;
|
|
@@ -66,6 +110,28 @@ export function encodeSessionSegment(id) {
|
|
|
66
110
|
return "--" + (readable.replace(/^-+/, "") || "root").slice(0, 251) + "--";
|
|
67
111
|
}
|
|
68
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Legacy alias for `encodeProjectSegment`.
|
|
115
|
+
* @deprecated Use `encodeProjectSegment` for project cwd keys, or `encodeFsSessionSegment` for session directories.
|
|
116
|
+
*/
|
|
117
|
+
export const encodeSessionSegment = encodeProjectSegment;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Return strictly verified candidate directory names for a given session ID.
|
|
121
|
+
* - Primary: DSH's true `encodeFsSessionSegment(id)` (e.g. "im~003Afeishu...")
|
|
122
|
+
* - Raw `id`: matches when raw ID equals encoded ID (common UUIDs), or if raw dir was used
|
|
123
|
+
* - Legacy projectKey format: transitional compatibility for `--id--` created by older plugin versions
|
|
124
|
+
*/
|
|
125
|
+
export function candidateSessionDirs(id) {
|
|
126
|
+
assertSessionId(id);
|
|
127
|
+
const primary = encodeFsSessionSegment(id);
|
|
128
|
+
const candidates = [primary];
|
|
129
|
+
if (id !== primary) candidates.push(id);
|
|
130
|
+
const legacyProject = encodeProjectSegment(id);
|
|
131
|
+
if (legacyProject !== primary && legacyProject !== id) candidates.push(legacyProject);
|
|
132
|
+
return candidates;
|
|
133
|
+
}
|
|
134
|
+
|
|
69
135
|
/** Only an actual <sessions>/<project>/<session> directory may be mutated.
|
|
70
136
|
* The root itself may be configured through a symlink, but neither child may
|
|
71
137
|
* be a symlink/junction. Check again immediately before destructive cleanup.
|
|
@@ -79,8 +145,9 @@ export async function assertSessionDirectory(root, directory, id, { allowMissing
|
|
|
79
145
|
const target = resolve(directory);
|
|
80
146
|
const rel = relative(base, target);
|
|
81
147
|
const parts = rel.split(sep);
|
|
148
|
+
const validDirs = candidateSessionDirs(id);
|
|
82
149
|
if (isAbsolute(rel) || parts.length !== 2 || parts.some(p => p === ".." || p === "." || p === "")
|
|
83
|
-
|| !
|
|
150
|
+
|| !validDirs.includes(parts[1])) {
|
|
84
151
|
throw new Error("会话目录超出允许范围,已停止文件操作");
|
|
85
152
|
}
|
|
86
153
|
const canonicalRoot = await fs.realpath(base);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-manager",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "DeepSeek Harness session and workspace manager —— Workspaces: archive and visibility. Sessions: move, migrate presets, delete, favorite, review later, prioritize, tag, and annotate. Batch operations. | DSH 会话与工作区管理——工作区:归档与显示。会话:跨工作区移动、预设迁移、删除;收藏、待回看、优先级、标签、备注。批量操作。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|