dsh-session-cloud 0.1.6 → 0.1.7
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 +23 -0
- package/lib/dsh-types.d.ts +21 -0
- package/lib/index.js +16 -0
- package/lib/sync/restore.d.ts +12 -1
- package/lib/sync/restore.js +41 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
|
|
7
|
+
- **恢复成功后侧边栏出现工作区但会话列表为空,需刷新页面才显示**
|
|
8
|
+
根因:恢复只写了会话文件、更新了 workspace 成员关系,但 web 会话列表
|
|
9
|
+
(`useSessions` 的 `list.byId`)靠 Host 的 `api-session/added` 事件增量
|
|
10
|
+
更新;插件没有发这个事件,所以新会话要等刷新页面触发全量 `list()` 才
|
|
11
|
+
出现(工作区行与会员行是两个独立通道)。
|
|
12
|
+
修复:恢复落位成功后 emit `api-session/added`(携带 sessionId / 有效
|
|
13
|
+
cwd / running / blank 等列表所需字段),侧边栏立即出现新会话,无需刷新。
|
|
14
|
+
|
|
15
|
+
- **跨机恢复把会话误判「本地已存在」、无法重新落位**
|
|
16
|
+
根因:`existsLocal` 只看「本地持久化里有没有这个 session id」。旧版
|
|
17
|
+
相对路径 bug 把会话写到了错误 cwd,文件在盘上却没挂到工作区,于是被
|
|
18
|
+
判「本地已存在」且恢复弹窗禁用勾选,会话卡死无法归位。
|
|
19
|
+
修复:
|
|
20
|
+
- `existsLocal` 收紧为「本地有文件 **且** 已挂载到工作区」(以
|
|
21
|
+
`workspaceRegistry.list()` 的 `sessionIds` 并集判定);无工作区注册表
|
|
22
|
+
的环境退回旧语义,不回归 headless 场景;
|
|
23
|
+
- 重新恢复落位后清理同 id 残留在其它 cwd 的旧副本,避免同 id 出现两份
|
|
24
|
+
(一份在工作区、一份在未分组)。
|
|
25
|
+
|
|
3
26
|
## 0.1.6
|
|
4
27
|
|
|
5
28
|
### 修复
|
package/lib/dsh-types.d.ts
CHANGED
|
@@ -33,9 +33,28 @@ export interface SessionPersistenceLike {
|
|
|
33
33
|
/** @deepseek-ai/dsh-workspace 的 Workspace(恢复挂载用到 attachSession)。 */
|
|
34
34
|
export interface WorkspaceLike {
|
|
35
35
|
readonly path: string;
|
|
36
|
+
/**
|
|
37
|
+
* 过滤后的成员账号:只有 cwd realpath 等于本工作区 path 的会话才在此
|
|
38
|
+
* (dsh 侧同步过滤缺失/无效 cwd)。恢复目录据此判定「本地已存在」。
|
|
39
|
+
*/
|
|
40
|
+
readonly sessionIds: readonly string[];
|
|
36
41
|
/** 把会话记入本工作区成员(cwd 校验:realpath 后须等于工作区 path) */
|
|
37
42
|
attachSession(sessionId: string): Promise<void>;
|
|
38
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* `api-session/added` 事件的载荷(Host 恢复成功后 emit,供 web 侧边栏的
|
|
46
|
+
* `useSessions` 列表增量插入恢复的会话;字段与 dsh 的 SessionSummary 对齐,
|
|
47
|
+
* 仅覆盖恢复链路需要的子集)。
|
|
48
|
+
*/
|
|
49
|
+
export interface SessionSummaryLike {
|
|
50
|
+
readonly sessionId: string;
|
|
51
|
+
readonly updatedAt: number;
|
|
52
|
+
readonly running: boolean;
|
|
53
|
+
readonly blank: boolean;
|
|
54
|
+
readonly parentSessionId?: string;
|
|
55
|
+
readonly origin?: 'subagent';
|
|
56
|
+
readonly cwd?: string;
|
|
57
|
+
}
|
|
39
58
|
export interface WorkspaceRegistryLike {
|
|
40
59
|
list(): WorkspaceLike[];
|
|
41
60
|
/** 按本机 canonical path 查找已有 workspace;不存在返回 undefined */
|
|
@@ -70,5 +89,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
70
89
|
'session/flush'(session: {
|
|
71
90
|
id: string;
|
|
72
91
|
}): void;
|
|
92
|
+
/** 恢复会话后回发,让 web 会话列表无需刷新页面即可出现新会话。 */
|
|
93
|
+
'api-session/added'(summary: SessionSummaryLike): void;
|
|
73
94
|
}
|
|
74
95
|
}
|
package/lib/index.js
CHANGED
|
@@ -155,6 +155,16 @@ export async function apply(ctx, config) {
|
|
|
155
155
|
function workspacePaths() {
|
|
156
156
|
return ctx.get('workspaceRegistry')?.list().map((workspace) => workspace.path) ?? [];
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* 本机已正确挂载到工作区的会话 id 并集;无 workspaceRegistry 时返回
|
|
160
|
+
* undefined(fetchCatalog 退回旧 existsLocal 语义)。
|
|
161
|
+
*/
|
|
162
|
+
function attachedSessionIds() {
|
|
163
|
+
const registry = ctx.get('workspaceRegistry');
|
|
164
|
+
if (!registry)
|
|
165
|
+
return undefined;
|
|
166
|
+
return [...new Set(registry.list().flatMap((workspace) => [...workspace.sessionIds]))];
|
|
167
|
+
}
|
|
158
168
|
/** 构造恢复链路的 deps;配置不全返回 undefined(调用方回写 unconfigured 回显)。 */
|
|
159
169
|
async function catalogDeps() {
|
|
160
170
|
const cfg = current();
|
|
@@ -168,6 +178,7 @@ export async function apply(ctx, config) {
|
|
|
168
178
|
persistence: persistencePort(),
|
|
169
179
|
state: stateStore,
|
|
170
180
|
workspacePaths: workspacePaths(),
|
|
181
|
+
attachedSessionIds: attachedSessionIds(),
|
|
171
182
|
// 恢复落位后的 workspace 挂载(web 侧边栏可见性)。复刻 dsh 自己创建会话的
|
|
172
183
|
// 路径(session-controller commands.ts):resolveByPath 复用已有工作区,
|
|
173
184
|
// 无则 create(registry 自带 realpath + isDirectory 校验),再 attachSession。
|
|
@@ -181,6 +192,11 @@ export async function apply(ctx, config) {
|
|
|
181
192
|
await workspace.attachSession(sessionId);
|
|
182
193
|
})();
|
|
183
194
|
},
|
|
195
|
+
// 恢复成功后的会话列表增量:web 侧边栏 useSessions 靠 api-session/added
|
|
196
|
+
// 事件插入新会话,否则只写文件要到刷新页面(全量 list())才可见
|
|
197
|
+
onRestored: (summary) => {
|
|
198
|
+
ctx.emit('api-session/added', summary);
|
|
199
|
+
},
|
|
184
200
|
onWarn: (message) => logger.warn(message),
|
|
185
201
|
};
|
|
186
202
|
}
|
package/lib/sync/restore.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionHeaderLike, SessionPersistenceLike } from '../dsh-types.js';
|
|
1
|
+
import type { SessionHeaderLike, SessionPersistenceLike, SessionSummaryLike } from '../dsh-types.js';
|
|
2
2
|
import type { SyncClient } from './http-client.js';
|
|
3
3
|
import type { StateStore } from './state.js';
|
|
4
4
|
/**
|
|
@@ -103,6 +103,12 @@ export interface CatalogDeps {
|
|
|
103
103
|
state: StateStore;
|
|
104
104
|
/** 本机工作区 canonical path 列表(workspaceRegistry 缺失时传空数组) */
|
|
105
105
|
workspacePaths: string[];
|
|
106
|
+
/**
|
|
107
|
+
* 本机已正确挂载到工作区的会话 id 并集(workspaceRegistry.list() 的
|
|
108
|
+
* sessionIds)。缺省(undefined,即无 workspaceRegistry 环境)时退回旧
|
|
109
|
+
* 语义:仅按「会话 id 在本地持久化中存在」判 existsLocal。
|
|
110
|
+
*/
|
|
111
|
+
attachedSessionIds?: readonly string[];
|
|
106
112
|
/** 单个 meta 解密失败时回调(口令不匹配的兜底呈现走状态行,不打断目录) */
|
|
107
113
|
onWarn?: (message: string) => void;
|
|
108
114
|
}
|
|
@@ -121,6 +127,11 @@ export interface RestoreDeps {
|
|
|
121
127
|
* 实现负责 resolveByPath/create/attachSession;缺省跳过(无 workspaceRegistry 环境)。
|
|
122
128
|
*/
|
|
123
129
|
attachWorkspace?: (sessionId: string, cwd: string) => Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* 单条恢复成功后回发(Host 据此 emit api-session/added,让 web 会话列表
|
|
132
|
+
* 增量出现新会话,无需刷新页面)。仅在文件落位成功后调用,挂载失败也会调用。
|
|
133
|
+
*/
|
|
134
|
+
onRestored?: (summary: SessionSummaryLike) => void;
|
|
124
135
|
onWarn?: (message: string) => void;
|
|
125
136
|
}
|
|
126
137
|
/**
|
package/lib/sync/restore.js
CHANGED
|
@@ -151,6 +151,11 @@ export async function fetchCatalog(deps) {
|
|
|
151
151
|
const metaKeys = objects.filter((object) => object.key.endsWith(META_SUFFIX));
|
|
152
152
|
const localHeaders = await deps.persistence.list();
|
|
153
153
|
const localIds = new Set(localHeaders.map((header) => header.id));
|
|
154
|
+
// 已挂载工作区的会话 id:existsLocal 只有同时命中「本机有文件」且「已挂载到
|
|
155
|
+
// 工作区」才算本地已存在——残留在错误 cwd(旧版相对路径 bug)的会话文件仍可恢复。
|
|
156
|
+
const attachedIds = deps.attachedSessionIds === undefined
|
|
157
|
+
? undefined
|
|
158
|
+
: new Set(deps.attachedSessionIds);
|
|
154
159
|
// 兼容基准优先取本机会话 header 的 version(即本机运行时的 SESSION_FORMAT_VERSION,
|
|
155
160
|
// 上游 bump 后随本地新会话自适应);本地一个会话都没有才落到常量兜底
|
|
156
161
|
const localFormatVersion = localHeaders.reduce((max, header) => Math.max(max, header.version), LOCAL_FORMAT_VERSION);
|
|
@@ -169,7 +174,7 @@ export async function fetchCatalog(deps) {
|
|
|
169
174
|
deps.onWarn?.(`meta 解密失败,跳过 ${object.key}:${String(error)}`);
|
|
170
175
|
continue;
|
|
171
176
|
}
|
|
172
|
-
const existsLocal = localIds.has(sessionId);
|
|
177
|
+
const existsLocal = localIds.has(sessionId) && (attachedIds === undefined || attachedIds.has(sessionId));
|
|
173
178
|
const resolution = existsLocal || meta.cwd === ''
|
|
174
179
|
? { kind: 'none', cwd: null }
|
|
175
180
|
: resolveTargetCwd(meta.cwd, mappings, deps.workspacePaths);
|
|
@@ -255,11 +260,14 @@ async function restoreOne(deps, item) {
|
|
|
255
260
|
const tmp = `${location.path}.tmp-${process.pid}`;
|
|
256
261
|
await fsp.writeFile(tmp, plaintext);
|
|
257
262
|
await fsp.rename(tmp, location.path);
|
|
263
|
+
const effectiveCwd = locatedHeader.cwd ?? '';
|
|
264
|
+
// 清理同 id 的陈旧本地副本(旧版相对路径 bug 落错 cwd 的残留):新文件已就位,
|
|
265
|
+
// 只删 cwd 不同且落位路径不同的副本,避免恢复后同 id 出现两份。
|
|
266
|
+
await removeStaleLocalCopies(deps, item.sessionId, effectiveCwd, location.path);
|
|
258
267
|
// workspace 挂载(web 侧边栏按成员关系分组)。目录存在的落位 cwd 才尝试挂载:
|
|
259
268
|
// 原样落位到本机不存在的来源路径时按 §4.3 语义保持「未分组」,不算失败。
|
|
260
269
|
// 挂载失败仅告警不回滚——文件已就位,会话至少出现在「未分组」桶,重试 attach
|
|
261
270
|
// 也没有意义(同 cwd 再挂靠 registry 幂等,但失败原因通常是目录状态异常)
|
|
262
|
-
const effectiveCwd = locatedHeader.cwd ?? '';
|
|
263
271
|
if (deps.attachWorkspace && effectiveCwd !== '' && await isExistingDirectory(effectiveCwd)) {
|
|
264
272
|
try {
|
|
265
273
|
await deps.attachWorkspace(item.sessionId, effectiveCwd);
|
|
@@ -268,6 +276,17 @@ async function restoreOne(deps, item) {
|
|
|
268
276
|
deps.onWarn?.(`workspace 挂载失败 ${item.sessionId}(会话已落位,将出现在未分组):${String(error)}`);
|
|
269
277
|
}
|
|
270
278
|
}
|
|
279
|
+
// 通知 Host 回发 api-session/added:web 侧边栏的会话列表靠该事件增量更新,
|
|
280
|
+
// 只写文件不会让新会话在刷新前出现(workspace 成员关系与会话行是两个通道)。
|
|
281
|
+
deps.onRestored?.({
|
|
282
|
+
sessionId: item.sessionId,
|
|
283
|
+
updatedAt: Date.now(),
|
|
284
|
+
running: false,
|
|
285
|
+
blank: false,
|
|
286
|
+
...(effectiveCwd === '' ? {} : { cwd: effectiveCwd }),
|
|
287
|
+
...(header.parentSession === undefined ? {} : { parentSessionId: header.parentSession }),
|
|
288
|
+
...(header.origin === undefined ? {} : { origin: header.origin }),
|
|
289
|
+
});
|
|
271
290
|
return location.path;
|
|
272
291
|
}
|
|
273
292
|
async function isExistingDirectory(target) {
|
|
@@ -278,6 +297,26 @@ async function isExistingDirectory(target) {
|
|
|
278
297
|
return false;
|
|
279
298
|
}
|
|
280
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* 删除同 id、但落在其它 cwd 的陈旧本地副本(旧版相对路径 bug 落错位置后
|
|
302
|
+
* 再恢复会残留)。仅在新的正确落位文件写盘后调用:只删 cwd 与 keepCwd 不同、
|
|
303
|
+
* 且落位路径与 keepPath 不同的副本,绝不触碰刚写的文件。删除失败静默——残留
|
|
304
|
+
* 副本顶多出现在「未分组」,不影响本次恢复结果。
|
|
305
|
+
*/
|
|
306
|
+
async function removeStaleLocalCopies(deps, sessionId, keepCwd, keepPath) {
|
|
307
|
+
const locals = await deps.persistence.list();
|
|
308
|
+
for (const header of locals) {
|
|
309
|
+
if (header.id !== sessionId)
|
|
310
|
+
continue;
|
|
311
|
+
if ((header.cwd ?? '') === keepCwd)
|
|
312
|
+
continue;
|
|
313
|
+
const stale = deps.persistence.locate(header);
|
|
314
|
+
if (!stale || stale.path === keepPath)
|
|
315
|
+
continue;
|
|
316
|
+
await fsp.rm(stale.path, { force: true }).catch(() => { });
|
|
317
|
+
await fsp.rmdir(path.dirname(stale.path)).catch(() => { });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
281
320
|
/**
|
|
282
321
|
* 规范化用户改选的落位路径(§4.3):`~` 展开到本机 home,相对路径按本机
|
|
283
322
|
* home 解析(跨机恢复最常见的心智模型——「home 下的 my-app」在两台
|