@michengai/dsh-archive-manager 1.1.13 → 1.1.16
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 +41 -0
- package/CHANGELOG.zh-CN.md +41 -0
- package/lib/client.js +5 -5
- package/lib/explorer-path-policy.js +29 -0
- package/lib/index.js +42 -1
- package/lib/native-explorer.js +34 -0
- package/lib/session-move.js +346 -0
- package/lib/workspace.js +60 -1
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
function isWindowsPath(p) {
|
|
3
|
+
return /^[a-zA-Z]:[\\/]/.test(p) || /^\\\\/.test(p) || process.platform === "win32";
|
|
4
|
+
}
|
|
5
|
+
function isPathAbsolute(p) {
|
|
6
|
+
return path.win32.isAbsolute(p) || path.posix.isAbsolute(p);
|
|
7
|
+
}
|
|
8
|
+
function normalizeComparable(target) {
|
|
9
|
+
const win = isWindowsPath(target);
|
|
10
|
+
const p = win ? path.win32 : path.posix;
|
|
11
|
+
const normalized = p.normalize(target).replace(/[\\/]+$/, "");
|
|
12
|
+
return win ? normalized.toLowerCase() : normalized;
|
|
13
|
+
}
|
|
14
|
+
function authorizeExplorerPath(targetPath, registeredPaths) {
|
|
15
|
+
if (typeof targetPath !== "string" || targetPath.trim() === "") return void 0;
|
|
16
|
+
const trimmed = targetPath.trim();
|
|
17
|
+
if (!isPathAbsolute(trimmed)) return void 0;
|
|
18
|
+
const targetComparable = normalizeComparable(trimmed);
|
|
19
|
+
for (const candidate of registeredPaths) {
|
|
20
|
+
if (typeof candidate !== "string" || candidate.trim() === "") continue;
|
|
21
|
+
if (normalizeComparable(candidate) === targetComparable) {
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
authorizeExplorerPath
|
|
29
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { getOnlineUpdate, getSidebarEntry, setOnlineUpdate, setSidebarEntry } from "./plugin-settings.js";
|
|
2
2
|
import { isTrustedUpdateRequest, rawService, registerPluginUpdater, releaseUpdateRoute } from "./plugin-updater.js";
|
|
3
3
|
import { apply as installHost, inject as hostInject } from "./host-install.js";
|
|
4
|
+
import { authorizeExplorerPath } from "./explorer-path-policy.js";
|
|
5
|
+
import { revealPathInSystem } from "./native-explorer.js";
|
|
4
6
|
const inject = ["webServer", ...hostInject];
|
|
5
7
|
const SIDEBAR_ENTRY_ENDPOINT = "/api/michengai/dsh-archive-manager/sidebar-entry";
|
|
6
8
|
const ONLINE_UPDATE_ENDPOINT = "/api/michengai/dsh-archive-manager/online-update";
|
|
9
|
+
const OPEN_IN_EXPLORER_ENDPOINT = "/api/michengai/dsh-archive-manager/open-in-explorer";
|
|
7
10
|
function sendJson(response, statusCode, value) {
|
|
8
11
|
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
9
12
|
response.end(JSON.stringify(value));
|
|
@@ -34,6 +37,7 @@ function registerSettingsRoutes(ctx, hostUpdateDisabled) {
|
|
|
34
37
|
const server = rawService(ctx.webServer);
|
|
35
38
|
releaseUpdateRoute(server, SIDEBAR_ENTRY_ENDPOINT);
|
|
36
39
|
releaseUpdateRoute(server, ONLINE_UPDATE_ENDPOINT);
|
|
40
|
+
releaseUpdateRoute(server, OPEN_IN_EXPLORER_ENDPOINT);
|
|
37
41
|
const disposeSidebar = ctx.webServer.register({
|
|
38
42
|
kind: "exact",
|
|
39
43
|
path: SIDEBAR_ENTRY_ENDPOINT,
|
|
@@ -92,6 +96,37 @@ function registerSettingsRoutes(ctx, hostUpdateDisabled) {
|
|
|
92
96
|
response.end();
|
|
93
97
|
}
|
|
94
98
|
});
|
|
99
|
+
const disposeOpenExplorer = ctx.webServer.register({
|
|
100
|
+
kind: "exact",
|
|
101
|
+
path: OPEN_IN_EXPLORER_ENDPOINT,
|
|
102
|
+
handler: async (request, response) => {
|
|
103
|
+
if (request.method !== "POST") {
|
|
104
|
+
response.writeHead(405, { allow: "POST" });
|
|
105
|
+
response.end();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!isTrustedUpdateRequest(request)) {
|
|
109
|
+
sendJson(response, 403, { ok: false, error: "\u5DF2\u62D2\u7EDD\u975E\u672C\u673A\u540C\u6E90\u8BF7\u6C42\u3002" });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const body = await readJsonBody(request);
|
|
113
|
+
const path = typeof body?.path === "string" ? body.path : "";
|
|
114
|
+
const registry = ctx.get("workspaceRegistry");
|
|
115
|
+
const registeredPaths = typeof registry?.list === "function" ? registry.list().map((w) => w.path) : [];
|
|
116
|
+
const authorizedPath = authorizeExplorerPath(path, registeredPaths);
|
|
117
|
+
if (authorizedPath === void 0) {
|
|
118
|
+
sendJson(response, 403, { ok: false, error: "\u4EC5\u5141\u8BB8\u6253\u5F00\u5DF2\u6CE8\u518C\u7684\u5DE5\u4F5C\u533A\u76EE\u5F55\u3002" });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const result = await revealPathInSystem(authorizedPath);
|
|
123
|
+
sendJson(response, 200, { ok: true, opened: true, via: result.via });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
ctx.logger?.warn?.(`open in explorer failed: ${String(error)}`);
|
|
126
|
+
sendJson(response, 500, { ok: false, error: "\u6253\u5F00\u7CFB\u7EDF\u8D44\u6E90\u7BA1\u7406\u5668\u5931\u8D25\u3002" });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
95
130
|
return () => {
|
|
96
131
|
try {
|
|
97
132
|
disposeSidebar();
|
|
@@ -101,6 +136,10 @@ function registerSettingsRoutes(ctx, hostUpdateDisabled) {
|
|
|
101
136
|
disposeUpdate();
|
|
102
137
|
} catch {
|
|
103
138
|
}
|
|
139
|
+
try {
|
|
140
|
+
disposeOpenExplorer();
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
104
143
|
};
|
|
105
144
|
}
|
|
106
145
|
async function apply(ctx, config) {
|
|
@@ -142,6 +181,8 @@ async function apply(ctx, config) {
|
|
|
142
181
|
};
|
|
143
182
|
}
|
|
144
183
|
export {
|
|
184
|
+
OPEN_IN_EXPLORER_ENDPOINT,
|
|
145
185
|
apply,
|
|
146
|
-
inject
|
|
186
|
+
inject,
|
|
187
|
+
registerSettingsRoutes
|
|
147
188
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
async function revealPathInSystem(targetPath, injectedModule, spawner) {
|
|
3
|
+
try {
|
|
4
|
+
const spec = "@deepseek-ai/dsh-native-command";
|
|
5
|
+
const mod = injectedModule ?? await import(spec).catch(() => ({}));
|
|
6
|
+
if (typeof mod.revealNativePath === "function") {
|
|
7
|
+
await mod.revealNativePath(targetPath);
|
|
8
|
+
return { ok: true, via: "native-command" };
|
|
9
|
+
}
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
const spawnFn = spawner ?? ((file, args) => {
|
|
13
|
+
if (process.env.DSH_ARCHIVE_MANAGER_MOCK_EXPLORER === "1") {
|
|
14
|
+
return { unref() {
|
|
15
|
+
} };
|
|
16
|
+
}
|
|
17
|
+
return spawn(file, args, { detached: true, stdio: "ignore" });
|
|
18
|
+
});
|
|
19
|
+
const platform = process.platform;
|
|
20
|
+
if (platform === "win32") {
|
|
21
|
+
const cp = spawnFn("explorer.exe", [targetPath]);
|
|
22
|
+
cp.unref();
|
|
23
|
+
} else if (platform === "darwin") {
|
|
24
|
+
const cp = spawnFn("open", [targetPath]);
|
|
25
|
+
cp.unref();
|
|
26
|
+
} else {
|
|
27
|
+
const cp = spawnFn("xdg-open", [targetPath]);
|
|
28
|
+
cp.unref();
|
|
29
|
+
}
|
|
30
|
+
return { ok: true, via: "spawn" };
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
revealPathInSystem
|
|
34
|
+
};
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
3
|
+
import zlib from "node:zlib";
|
|
4
|
+
const ZSTD_MAGIC = 4247762216;
|
|
5
|
+
const CHECKSUM_OPTS = { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } };
|
|
6
|
+
function normalizePath(p) {
|
|
7
|
+
if (typeof p !== "string" || p.trim() === "") return void 0;
|
|
8
|
+
const norm = normalize(resolve(p)).replace(/[\\/]+$/, "");
|
|
9
|
+
return process.platform === "win32" ? norm.toLowerCase() : norm;
|
|
10
|
+
}
|
|
11
|
+
function rawWorkspaceSessionIds(ws) {
|
|
12
|
+
const raw = ws.record?.sessionIds;
|
|
13
|
+
return Array.isArray(raw) ? raw : Array.isArray(ws.sessionIds) ? ws.sessionIds : [];
|
|
14
|
+
}
|
|
15
|
+
function scanFirstZstdFrame(buf) {
|
|
16
|
+
if (buf.length < 4) throw new Error("\u4F1A\u8BDD\u5DE5\u4EF6\u6570\u636E\u8FC7\u77ED\uFF0C\u65E0\u6CD5\u8BFB\u53D6 zstd \u5934\u90E8");
|
|
17
|
+
if (buf.readUInt32LE(0) !== ZSTD_MAGIC) throw new Error("\u4F1A\u8BDD\u5DE5\u4EF6\u4E0D\u662F\u5408\u6CD5\u7684 zstd \u683C\u5F0F");
|
|
18
|
+
let offset = 4;
|
|
19
|
+
if (offset >= buf.length) throw new Error("\u4F1A\u8BDD\u5DE5\u4EF6 zstd \u5E27\u4E0D\u5B8C\u6574");
|
|
20
|
+
const descriptor = buf.readUInt8(offset++);
|
|
21
|
+
if ((descriptor & 24) !== 0) throw new Error("zstd \u5E27\u5305\u542B\u4FDD\u7559\u4F4D");
|
|
22
|
+
const contentSizeFlag = descriptor >>> 6;
|
|
23
|
+
const singleSegment = (descriptor & 32) !== 0;
|
|
24
|
+
const checksum = (descriptor & 4) !== 0;
|
|
25
|
+
const dictionaryFlag = descriptor & 3;
|
|
26
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
27
|
+
const contentSizeBytes = contentSizeFlag === 0 ? singleSegment ? 1 : 0 : 1 << contentSizeFlag;
|
|
28
|
+
const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
29
|
+
if (buf.length - offset < remainingHeaderBytes) throw new Error("zstd \u5934\u90E8\u88AB\u622A\u65AD");
|
|
30
|
+
offset += remainingHeaderBytes;
|
|
31
|
+
for (; ; ) {
|
|
32
|
+
if (buf.length - offset < 3) throw new Error("zstd \u5757\u7ED3\u6784\u622A\u65AD");
|
|
33
|
+
const blockHeader = buf.readUIntLE(offset, 3);
|
|
34
|
+
offset += 3;
|
|
35
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
36
|
+
const blockType = blockHeader >>> 1 & 3;
|
|
37
|
+
const blockSize = blockHeader >>> 3;
|
|
38
|
+
if (blockType === 3) throw new Error("zstd \u5305\u542B\u672A\u77E5\u4FDD\u7559\u5757\u7C7B\u578B");
|
|
39
|
+
const payloadBytes = blockType === 1 ? 1 : blockSize;
|
|
40
|
+
if (buf.length - offset < payloadBytes) throw new Error("zstd \u5757\u8D1F\u8F7D\u88AB\u622A\u65AD");
|
|
41
|
+
offset += payloadBytes;
|
|
42
|
+
if (lastBlock) break;
|
|
43
|
+
}
|
|
44
|
+
if (checksum) {
|
|
45
|
+
if (buf.length - offset < 4) throw new Error("zstd \u6821\u9A8C\u548C\u88AB\u622A\u65AD");
|
|
46
|
+
offset += 4;
|
|
47
|
+
}
|
|
48
|
+
return { start: 0, end: offset };
|
|
49
|
+
}
|
|
50
|
+
function rewriteSessionArtifactCwd(sourceBuffer, targetCwd, isZstd) {
|
|
51
|
+
if (isZstd) {
|
|
52
|
+
const frame0Boundary = scanFirstZstdFrame(sourceBuffer);
|
|
53
|
+
const frame0Bytes = sourceBuffer.subarray(0, frame0Boundary.end);
|
|
54
|
+
const decompressedHeader = zlib.zstdDecompressSync(frame0Bytes).toString("utf8");
|
|
55
|
+
const nlIndex = decompressedHeader.indexOf("\n");
|
|
56
|
+
const headerLine = nlIndex >= 0 ? decompressedHeader.slice(0, nlIndex) : decompressedHeader;
|
|
57
|
+
const headerObj = JSON.parse(headerLine);
|
|
58
|
+
headerObj.cwd = targetCwd;
|
|
59
|
+
const newFrame0 = zlib.zstdCompressSync(Buffer.from(JSON.stringify(headerObj) + "\n", "utf8"), CHECKSUM_OPTS);
|
|
60
|
+
const rest = sourceBuffer.subarray(frame0Boundary.end);
|
|
61
|
+
return Buffer.concat([newFrame0, rest]);
|
|
62
|
+
} else {
|
|
63
|
+
const text = sourceBuffer.toString("utf8");
|
|
64
|
+
const nlIndex = text.indexOf("\n");
|
|
65
|
+
const headerLine = nlIndex >= 0 ? text.slice(0, nlIndex) : text;
|
|
66
|
+
const headerObj = JSON.parse(headerLine);
|
|
67
|
+
headerObj.cwd = targetCwd;
|
|
68
|
+
const rest = nlIndex >= 0 ? text.slice(nlIndex + 1) : "";
|
|
69
|
+
return Buffer.from(JSON.stringify(headerObj) + "\n" + rest, "utf8");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function moveSessionToWorkspace(ctx, sessionId, targetWorkspaceId) {
|
|
73
|
+
if (typeof sessionId !== "string" || sessionId.trim() === "") {
|
|
74
|
+
throw new TypeError("sessionId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
75
|
+
}
|
|
76
|
+
if (typeof targetWorkspaceId !== "string" || targetWorkspaceId.trim() === "") {
|
|
77
|
+
throw new TypeError("targetWorkspaceId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
78
|
+
}
|
|
79
|
+
const cleanSessionId = sessionId.trim();
|
|
80
|
+
const cleanTargetWsId = targetWorkspaceId.trim();
|
|
81
|
+
const registry = ctx.workspaceRegistry;
|
|
82
|
+
if (!registry || typeof registry.list !== "function") {
|
|
83
|
+
throw new Error("\u5DE5\u4F5C\u533A\u6CE8\u518C\u8868\u670D\u52A1\u4E0D\u53EF\u7528");
|
|
84
|
+
}
|
|
85
|
+
const targetWorkspace = registry.list().find((w) => w.id === cleanTargetWsId || w.workspaceId === cleanTargetWsId || w.path === cleanTargetWsId);
|
|
86
|
+
if (!targetWorkspace) {
|
|
87
|
+
throw new Error(`\u76EE\u6807\u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728: ${cleanTargetWsId}`);
|
|
88
|
+
}
|
|
89
|
+
const targetPath = targetWorkspace.path ?? targetWorkspace.record?.path;
|
|
90
|
+
const liveSession = ctx.sessions?.get?.(cleanSessionId);
|
|
91
|
+
const agent = ctx.agents?.get?.(cleanSessionId);
|
|
92
|
+
const isExecuting = liveSession?.running === true || liveSession?.status === "running" || liveSession?.status === "ongoing" || agent?.busy === true || agent?.status === "running";
|
|
93
|
+
if (isExecuting) {
|
|
94
|
+
throw new Error("\u8BE5\u4F1A\u8BDD\u6B63\u5904\u4E8E\u6267\u884C\u6216\u8FD0\u884C\u72B6\u6001\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u540E\u518D\u79FB\u52A8\u3002");
|
|
95
|
+
}
|
|
96
|
+
let originalEntry;
|
|
97
|
+
if (liveSession !== void 0) {
|
|
98
|
+
if (typeof agent?.cancel === "function") {
|
|
99
|
+
try {
|
|
100
|
+
agent.cancel({ kind: "disposed" });
|
|
101
|
+
await agent.whenIdle?.();
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (typeof ctx.sessions?.flush === "function") {
|
|
106
|
+
try {
|
|
107
|
+
await ctx.sessions.flush(liveSession);
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
await agent?.scope?.dispose?.();
|
|
113
|
+
ctx.agents?.store?.delete(cleanSessionId);
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
originalEntry = ctx.sessions?.store?.get?.(cleanSessionId) ?? (typeof ctx.sessions?.liveEntryFor === "function" ? (() => {
|
|
117
|
+
try {
|
|
118
|
+
return ctx.sessions.liveEntryFor(liveSession);
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
})() : void 0);
|
|
123
|
+
if (typeof originalEntry?.detach === "function") {
|
|
124
|
+
try {
|
|
125
|
+
originalEntry.detach();
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const persistence = ctx.sessionPersistence;
|
|
131
|
+
let header;
|
|
132
|
+
if (typeof registry.readSessionHeader === "function") {
|
|
133
|
+
header = await registry.readSessionHeader(cleanSessionId);
|
|
134
|
+
}
|
|
135
|
+
if (!header && persistence && typeof persistence.stat === "function") {
|
|
136
|
+
header = (await persistence.stat(cleanSessionId))?.header;
|
|
137
|
+
}
|
|
138
|
+
if (!header && persistence && typeof persistence.open === "function") {
|
|
139
|
+
try {
|
|
140
|
+
const readHandle = await persistence.open(cleanSessionId, "read");
|
|
141
|
+
header = readHandle.header;
|
|
142
|
+
await readHandle.close();
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!header) {
|
|
147
|
+
throw new Error(`\u65E0\u6CD5\u83B7\u53D6\u4F1A\u8BDD ${cleanSessionId} \u7684\u5143\u6570\u636E\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002`);
|
|
148
|
+
}
|
|
149
|
+
if (header.origin === "subagent") {
|
|
150
|
+
throw new Error("\u5B50\u4EE3\u7406\uFF08subagent\uFF09\u4F1A\u8BDD\u4E0D\u652F\u6301\u8DE8\u5DE5\u4F5C\u533A\u79FB\u52A8\u3002");
|
|
151
|
+
}
|
|
152
|
+
const currentNormalizedCwd = normalizePath(header.cwd);
|
|
153
|
+
const targetNormalizedCwd = normalizePath(targetPath);
|
|
154
|
+
const targetNorm = normalizePath(targetWorkspace.path);
|
|
155
|
+
const sourceWorkspaces = registry.list().filter((ws) => {
|
|
156
|
+
const wsNorm = normalizePath(ws.path);
|
|
157
|
+
const isOther = ws.id && targetWorkspace.id ? ws.id !== targetWorkspace.id : wsNorm !== targetNorm;
|
|
158
|
+
return isOther && rawWorkspaceSessionIds(ws).includes(cleanSessionId);
|
|
159
|
+
});
|
|
160
|
+
if (currentNormalizedCwd && targetNormalizedCwd && currentNormalizedCwd === targetNormalizedCwd) {
|
|
161
|
+
for (const ws of sourceWorkspaces) {
|
|
162
|
+
await ws.detachSession(cleanSessionId);
|
|
163
|
+
}
|
|
164
|
+
await targetWorkspace.attachSession(cleanSessionId);
|
|
165
|
+
registry.sessionPaths?.set(cleanSessionId, targetPath);
|
|
166
|
+
if (ctx.sessionProjectionCache && typeof ctx.sessionProjectionCache.coldSnapshot === "function") {
|
|
167
|
+
try {
|
|
168
|
+
if (ctx.sessionProjectionCache.coldSnapshot.length === 1) {
|
|
169
|
+
await ctx.sessionProjectionCache.coldSnapshot(cleanSessionId);
|
|
170
|
+
} else if (persistence && typeof persistence.open === "function") {
|
|
171
|
+
const readHandle = await persistence.open(cleanSessionId, "read");
|
|
172
|
+
try {
|
|
173
|
+
const { events } = typeof readHandle.read === "function" ? await readHandle.read(0) : { events: [] };
|
|
174
|
+
const coldMeta = readHandle.header ?? header;
|
|
175
|
+
const inherited = readHandle.inheritedEventCount ?? 0;
|
|
176
|
+
await ctx.sessionProjectionCache.coldSnapshot(
|
|
177
|
+
coldMeta,
|
|
178
|
+
inherited,
|
|
179
|
+
events ?? []
|
|
180
|
+
);
|
|
181
|
+
} finally {
|
|
182
|
+
await readHandle.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
ctx.logger?.warn?.(`archive-manager: post-move coldSnapshot failed: ${String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
moved: true,
|
|
192
|
+
alreadyCwd: true,
|
|
193
|
+
sessionId: cleanSessionId,
|
|
194
|
+
toWorkspaceId: cleanTargetWsId,
|
|
195
|
+
toWorkspaceTitle: targetWorkspace.title || cleanTargetWsId,
|
|
196
|
+
toWorkspacePath: targetPath
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (!persistence || typeof persistence.locate !== "function") {
|
|
200
|
+
throw new Error("\u5F53\u524D\u6301\u4E45\u5316\u540E\u7AEF\u4E0D\u652F\u6301\u7269\u7406\u5B9A\u4F4D\uFF0C\u65E0\u6CD5\u8DE8\u5DE5\u4F5C\u533A\u79FB\u52A8\u4F1A\u8BDD\u3002");
|
|
201
|
+
}
|
|
202
|
+
const sourceLoc = persistence.locate({ id: cleanSessionId, cwd: header.cwd });
|
|
203
|
+
const targetLoc = persistence.locate({ id: cleanSessionId, cwd: targetPath });
|
|
204
|
+
if (!sourceLoc || !targetLoc || !sourceLoc.path || !targetLoc.path) {
|
|
205
|
+
throw new Error("\u6301\u4E45\u5316\u540E\u7AEF\u65E0\u6CD5\u5B9A\u4F4D\u4F1A\u8BDD\u5DE5\u4EF6\u8DEF\u5F84\u3002");
|
|
206
|
+
}
|
|
207
|
+
const sourceLogPath = sourceLoc.path;
|
|
208
|
+
const targetLogPath = targetLoc.path;
|
|
209
|
+
const sourceSessionDir = dirname(sourceLogPath);
|
|
210
|
+
const targetSessionDir = dirname(targetLogPath);
|
|
211
|
+
let targetOccupied = false;
|
|
212
|
+
try {
|
|
213
|
+
await stat(targetLogPath);
|
|
214
|
+
targetOccupied = true;
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
if (targetOccupied) {
|
|
218
|
+
throw new Error(`\u76EE\u6807\u5DE5\u4F5C\u533A\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u5DE5\u4EF6\uFF0C\u62D2\u7EDD\u8986\u76D6: ${targetLogPath}`);
|
|
219
|
+
}
|
|
220
|
+
const persistenceRoot = persistence.root ?? persistence.config?.root;
|
|
221
|
+
const backupRoot = persistenceRoot ? join(dirname(resolve(persistenceRoot)), "dsham-move-backup") : join(dirname(sourceSessionDir), ".dsham-move-backup");
|
|
222
|
+
await mkdir(backupRoot, { recursive: true });
|
|
223
|
+
const safeSessionName = encodeURIComponent(cleanSessionId).replace(/%/g, "~");
|
|
224
|
+
const backupDir = join(backupRoot, `${safeSessionName}-${process.pid}-${Date.now()}`);
|
|
225
|
+
const isZstd = sourceLogPath.endsWith(".zstd");
|
|
226
|
+
const sourceBytes = await readFile(sourceLogPath);
|
|
227
|
+
const newBytes = rewriteSessionArtifactCwd(sourceBytes, targetPath, isZstd);
|
|
228
|
+
if (isZstd) {
|
|
229
|
+
const f0 = zlib.zstdDecompressSync(newBytes);
|
|
230
|
+
const f0Obj = JSON.parse(f0.toString("utf8").split("\n")[0]);
|
|
231
|
+
if (f0Obj.id !== cleanSessionId || f0Obj.cwd !== targetPath) {
|
|
232
|
+
throw new Error("\u8FC1\u79FB\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u6570\u636E\u6539\u5199\u9A8C\u8BC1\u672A\u901A\u8FC7\u3002");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const detachedWorkspaces = [];
|
|
236
|
+
let targetAttached = false;
|
|
237
|
+
let stageFile = null;
|
|
238
|
+
let sourceDirMoved = false;
|
|
239
|
+
try {
|
|
240
|
+
await rename(sourceSessionDir, backupDir);
|
|
241
|
+
sourceDirMoved = true;
|
|
242
|
+
await mkdir(targetSessionDir, { recursive: true });
|
|
243
|
+
stageFile = join(targetSessionDir, `.stage-${process.pid}-${Date.now()}`);
|
|
244
|
+
await writeFile(stageFile, newBytes);
|
|
245
|
+
await rename(stageFile, targetLogPath);
|
|
246
|
+
stageFile = null;
|
|
247
|
+
const readbackBytes = await readFile(targetLogPath);
|
|
248
|
+
if (readbackBytes.length === 0) throw new Error("\u76EE\u6807\u5199\u5165\u5DE5\u4EF6\u4E3A\u7A7A");
|
|
249
|
+
const targetHeader = { ...header, cwd: targetPath };
|
|
250
|
+
registry.sessionPaths?.set(cleanSessionId, targetPath);
|
|
251
|
+
registry.headers?.set(cleanSessionId, targetHeader);
|
|
252
|
+
registry.invalidSessionPaths?.delete(cleanSessionId);
|
|
253
|
+
if (liveSession !== void 0) {
|
|
254
|
+
try {
|
|
255
|
+
if ("header" in liveSession && liveSession.header) {
|
|
256
|
+
liveSession.header.cwd = targetPath;
|
|
257
|
+
}
|
|
258
|
+
if ("cwd" in liveSession) {
|
|
259
|
+
liveSession.cwd = targetPath;
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const ws of sourceWorkspaces) {
|
|
265
|
+
await ws.detachSession(cleanSessionId);
|
|
266
|
+
detachedWorkspaces.push(ws);
|
|
267
|
+
}
|
|
268
|
+
await targetWorkspace.attachSession(cleanSessionId);
|
|
269
|
+
targetAttached = true;
|
|
270
|
+
if (ctx.sessionProjectionCache && typeof ctx.sessionProjectionCache.coldSnapshot === "function") {
|
|
271
|
+
try {
|
|
272
|
+
if (ctx.sessionProjectionCache.coldSnapshot.length === 1) {
|
|
273
|
+
await ctx.sessionProjectionCache.coldSnapshot(cleanSessionId);
|
|
274
|
+
} else if (persistence && typeof persistence.open === "function") {
|
|
275
|
+
const readHandle = await persistence.open(cleanSessionId, "read");
|
|
276
|
+
try {
|
|
277
|
+
const { events } = typeof readHandle.read === "function" ? await readHandle.read(0) : { events: [] };
|
|
278
|
+
const coldMeta = readHandle.header ?? targetHeader;
|
|
279
|
+
const inherited = readHandle.inheritedEventCount ?? 0;
|
|
280
|
+
await ctx.sessionProjectionCache.coldSnapshot(
|
|
281
|
+
coldMeta,
|
|
282
|
+
inherited,
|
|
283
|
+
events ?? []
|
|
284
|
+
);
|
|
285
|
+
} finally {
|
|
286
|
+
await readHandle.close();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
ctx.logger?.warn?.(`archive-manager: post-move coldSnapshot failed: ${String(err)}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (header) {
|
|
295
|
+
registry.headers?.set(cleanSessionId, header);
|
|
296
|
+
if (header.cwd) registry.sessionPaths?.set(cleanSessionId, header.cwd);
|
|
297
|
+
}
|
|
298
|
+
if (liveSession !== void 0 && header) {
|
|
299
|
+
try {
|
|
300
|
+
if ("header" in liveSession && liveSession.header) {
|
|
301
|
+
liveSession.header.cwd = header.cwd;
|
|
302
|
+
}
|
|
303
|
+
if ("cwd" in liveSession && header.cwd) {
|
|
304
|
+
liveSession.cwd = header.cwd;
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (stageFile) await rm(stageFile, { force: true }).catch(() => {
|
|
310
|
+
});
|
|
311
|
+
await rm(targetSessionDir, { recursive: true, force: true }).catch(() => {
|
|
312
|
+
});
|
|
313
|
+
if (sourceDirMoved) {
|
|
314
|
+
try {
|
|
315
|
+
await mkdir(dirname(sourceSessionDir), { recursive: true });
|
|
316
|
+
await rename(backupDir, sourceSessionDir);
|
|
317
|
+
} catch (restoreErr) {
|
|
318
|
+
throw new AggregateError([error, restoreErr], `\u79FB\u52A8\u5931\u8D25\u4E14\u539F\u5DE5\u4EF6\u56DE\u6EDA\u5F02\u5E38\uFF0C\u5907\u4EFD\u6682\u5B58\u4E8E: ${backupDir}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (targetAttached) {
|
|
322
|
+
await targetWorkspace.detachSession(cleanSessionId).catch(() => {
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
for (const ws of detachedWorkspaces) {
|
|
326
|
+
await ws.attachSession(cleanSessionId).catch(() => {
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
await rm(backupDir, { recursive: true, force: true }).catch(() => {
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
moved: true,
|
|
336
|
+
alreadyCwd: false,
|
|
337
|
+
sessionId: cleanSessionId,
|
|
338
|
+
toWorkspaceId: cleanTargetWsId,
|
|
339
|
+
toWorkspaceTitle: targetWorkspace.title || cleanTargetWsId,
|
|
340
|
+
toWorkspacePath: targetPath
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
export {
|
|
344
|
+
moveSessionToWorkspace,
|
|
345
|
+
rewriteSessionArtifactCwd
|
|
346
|
+
};
|
package/lib/workspace.js
CHANGED
|
@@ -10,6 +10,7 @@ import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
10
10
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
11
11
|
import { defineDomain } from "@deepseek-ai/dsh-storage-domain";
|
|
12
12
|
import { favoriteInputSchema, favoriteStateSchema, favoriteInvocations } from "./archive-organizer.js";
|
|
13
|
+
import { moveSessionToWorkspace } from "./session-move.js";
|
|
13
14
|
const favoriteDomainSpec = defineDomain({
|
|
14
15
|
name: "archive_manager_favorites",
|
|
15
16
|
version: 1,
|
|
@@ -105,7 +106,8 @@ const ARCHIVE_REMOTE_METHODS = [
|
|
|
105
106
|
"diagnoseOrphanSubagents",
|
|
106
107
|
"diagnoseSessionCaches",
|
|
107
108
|
"rebuildSessionCaches",
|
|
108
|
-
"cleanOrphanSubagents"
|
|
109
|
+
"cleanOrphanSubagents",
|
|
110
|
+
"moveSessionToWorkspace"
|
|
109
111
|
];
|
|
110
112
|
function markRemoteMethod(instance, method) {
|
|
111
113
|
const context = {
|
|
@@ -212,6 +214,22 @@ const orphanCleanResultSchema = {
|
|
|
212
214
|
return val;
|
|
213
215
|
}
|
|
214
216
|
};
|
|
217
|
+
const sessionMoveInputSchema = {
|
|
218
|
+
parse(value) {
|
|
219
|
+
const val = record(value);
|
|
220
|
+
if (!val || typeof val.sessionId !== "string" || !val.sessionId || typeof val.targetWorkspaceId !== "string" || !val.targetWorkspaceId) {
|
|
221
|
+
throw new TypeError("sessionId and targetWorkspaceId must be non-empty strings");
|
|
222
|
+
}
|
|
223
|
+
return { sessionId: val.sessionId, targetWorkspaceId: val.targetWorkspaceId };
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const sessionMoveResultSchema = {
|
|
227
|
+
parse(value) {
|
|
228
|
+
const val = record(value);
|
|
229
|
+
if (!val || val.ok !== true) throw new TypeError("session move result must be ok: true");
|
|
230
|
+
return val;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
215
233
|
const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
216
234
|
...cacheRebuildInvocations(),
|
|
217
235
|
...favoriteInvocations(),
|
|
@@ -338,6 +356,27 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
338
356
|
line: 1,
|
|
339
357
|
column: 1
|
|
340
358
|
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "@michengai/dsh-archive-manager#workspaceRegistry/moveSessionToWorkspace",
|
|
362
|
+
service: "workspaceRegistry",
|
|
363
|
+
namespace: "workspaceRegistry",
|
|
364
|
+
method: "moveSessionToWorkspace",
|
|
365
|
+
invocation: { kind: "direct" },
|
|
366
|
+
parameters: [
|
|
367
|
+
{
|
|
368
|
+
name: "input",
|
|
369
|
+
wire: "input",
|
|
370
|
+
source: "json",
|
|
371
|
+
codec: strictCodec("@michengai/dsh-archive-manager/types#SessionMoveInput", sessionMoveInputSchema)
|
|
372
|
+
}
|
|
373
|
+
],
|
|
374
|
+
result: strictCodec("@michengai/dsh-archive-manager/types#SessionMoveResult", sessionMoveResultSchema),
|
|
375
|
+
sourceLocation: {
|
|
376
|
+
file: "@michengai/dsh-archive-manager/lib/workspace.js",
|
|
377
|
+
line: 1,
|
|
378
|
+
column: 1
|
|
379
|
+
}
|
|
341
380
|
}
|
|
342
381
|
];
|
|
343
382
|
const ARCHIVE_MANAGER_TYPERT = {
|
|
@@ -1055,6 +1094,26 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
1055
1094
|
return { cleanedSessionIds, skippedSessionIds, failures };
|
|
1056
1095
|
});
|
|
1057
1096
|
}
|
|
1097
|
+
/**
|
|
1098
|
+
* 安全跨工作区会话移动:支持未分组归并及带备份的跨目录物理迁移。
|
|
1099
|
+
*/
|
|
1100
|
+
async moveSessionToWorkspace(input) {
|
|
1101
|
+
const { sessionId, targetWorkspaceId } = sessionMoveInputSchema.parse(input);
|
|
1102
|
+
return this.enqueueOperation(async () => {
|
|
1103
|
+
return moveSessionToWorkspace(
|
|
1104
|
+
{
|
|
1105
|
+
workspaceRegistry: this,
|
|
1106
|
+
sessionPersistence: this.ctx.get("sessionPersistence"),
|
|
1107
|
+
sessionProjectionCache: this.ctx.get("sessionProjectionCache"),
|
|
1108
|
+
sessions: this.ctx.get("sessions"),
|
|
1109
|
+
agents: this.ctx.get("agents"),
|
|
1110
|
+
logger: this.ctx.logger
|
|
1111
|
+
},
|
|
1112
|
+
sessionId,
|
|
1113
|
+
targetWorkspaceId
|
|
1114
|
+
);
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1058
1117
|
};
|
|
1059
1118
|
const workspaceOverlay = Symbol.for("dsh-archive-manager.workspace-overlay");
|
|
1060
1119
|
const remoteMethodDescriptor = "@deepseek-ai/dsh-typert-protocol/remote-methods";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michengai/dsh-archive-manager",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "归档、恢复、收藏和永久删除会话,并诊断损坏的会话日志。 · Archive, restore, favorite, and permanently delete sessions, and diagnose damaged session logs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|