dsh-sessions-manager 3.5.1 → 3.5.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/README.en.md +10 -0
- package/README.md +10 -0
- package/lib/client.js +112 -88
- package/lib/client.js.map +2 -2
- package/lib/index.js +820 -220
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +138 -83
- package/src/client/logic.js +5 -1
- package/src/compat/capabilities.js +64 -8
- package/src/compat/persistence.js +137 -18
- package/src/handle-era-ops.js +185 -0
- package/src/handle-era-paths.js +126 -0
- package/src/index.js +347 -91
- package/src/markdown.js +122 -71
- package/src/path-guard.js +59 -0
- package/src/session-meta-cache.js +40 -13
- package/src/title-persist-index.js +6 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir as
|
|
3
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
2
|
+
import { mkdir as mkdir5, readFile as readFile3, realpath, rename as rename5, stat as stat2, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
3
|
+
import { basename as basename2, dirname as dirname2, isAbsolute, join as join6 } from "node:path";
|
|
4
4
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
|
|
@@ -136,82 +136,101 @@ function summarizeToolArguments(name2, rawArguments) {
|
|
|
136
136
|
}
|
|
137
137
|
return JSON.stringify(rest).slice(0, MAX_TOOL_ARG);
|
|
138
138
|
}
|
|
139
|
-
function
|
|
139
|
+
function createMarkdownFold(out, header, options) {
|
|
140
140
|
const includeReasoning = options.includeReasoning === true;
|
|
141
141
|
const includeToolResults = options.includeToolResults === true;
|
|
142
|
-
const header = meta && typeof meta === "object" ? meta : {};
|
|
143
|
-
const list = Array.isArray(events) ? events : [];
|
|
144
142
|
let title = typeof header.title === "string" && header.title.trim() ? header.title.trim() : null;
|
|
145
|
-
for (const ev of list) {
|
|
146
|
-
const data = ev && ev.data;
|
|
147
|
-
if (ev && ev.type === "session/title" && data && typeof data.title === "string" && data.title.trim()) {
|
|
148
|
-
title = data.title.trim();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const front = ["---"];
|
|
152
|
-
if (title) front.push(`title: ${yamlString(title)}`);
|
|
153
|
-
if (typeof header.id === "string" && header.id) front.push(`sessionId: ${yamlString(header.id)}`);
|
|
154
|
-
if (typeof header.cwd === "string" && header.cwd) front.push(`cwd: ${yamlString(header.cwd)}`);
|
|
155
|
-
const created = isoTime(header.createdAt);
|
|
156
|
-
if (created) front.push(`createdAt: ${created}`);
|
|
157
|
-
const exported = isoTime(options.exportedAt);
|
|
158
|
-
if (exported) front.push(`exportedAt: ${exported}`);
|
|
159
|
-
front.push("---");
|
|
160
|
-
const out = [front.join("\n")];
|
|
161
|
-
if (title) out.push("", `# ${title}`);
|
|
162
143
|
let turn = null;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
144
|
+
return {
|
|
145
|
+
get title() {
|
|
146
|
+
return title;
|
|
147
|
+
},
|
|
148
|
+
// The last session/title event wins — DSH may retitle a session later on.
|
|
149
|
+
add(events) {
|
|
150
|
+
const list = Array.isArray(events) ? events : [];
|
|
151
|
+
for (const ev of list) {
|
|
152
|
+
if (!ev || typeof ev !== "object") continue;
|
|
153
|
+
const data = ev.data && typeof ev.data === "object" ? ev.data : {};
|
|
154
|
+
const type = ev.type;
|
|
155
|
+
if (type === "session/title" && data && typeof data.title === "string" && data.title.trim()) {
|
|
156
|
+
title = data.title.trim();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (type === "turn/start") {
|
|
160
|
+
const next = Number.isInteger(data.turn) ? data.turn : null;
|
|
161
|
+
if (next !== null && next !== turn) {
|
|
162
|
+
turn = next;
|
|
163
|
+
out.push("", `## \u7B2C ${turn} \u8F6E`);
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (type === "user/message") {
|
|
168
|
+
const blocks = blocksOf(data.content);
|
|
169
|
+
const text = textFromBlocks(blocks);
|
|
170
|
+
const images = imageCountOf(blocks);
|
|
171
|
+
if (!text && images === 0) continue;
|
|
172
|
+
out.push("", "### \u7528\u6237", "");
|
|
173
|
+
if (text) out.push(text);
|
|
174
|
+
for (let i = 0; i < images; i++) out.push("", ``);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (type === "assistant/message") {
|
|
178
|
+
const message = data.message && typeof data.message === "object" ? data.message : {};
|
|
179
|
+
const blocks = blocksOf(message.content);
|
|
180
|
+
const text = textFromBlocks(blocks);
|
|
181
|
+
const reasoning = includeReasoning ? reasoningFromBlocks(blocks) : "";
|
|
182
|
+
if (!text && !reasoning) continue;
|
|
183
|
+
out.push("", "### \u52A9\u624B", "");
|
|
184
|
+
if (reasoning) out.push("> \u601D\u8003\uFF1A" + reasoning.split("\n").join("\n> "), "");
|
|
185
|
+
if (text) out.push(text);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (type === "tool/call") {
|
|
189
|
+
const name2 = typeof data.name === "string" && data.name ? data.name : "tool";
|
|
190
|
+
const summary = summarizeToolArguments(name2, data.arguments);
|
|
191
|
+
out.push("", `### \u5DE5\u5177\u8C03\u7528\uFF1A\`${name2}\``, "");
|
|
192
|
+
out.push(summary ? "```\n" + summary + "\n```" : "\uFF08\u65E0\u53C2\u6570\uFF09");
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (type === "tool/result" && includeToolResults) {
|
|
196
|
+
const message = data.message && typeof data.message === "object" ? data.message : {};
|
|
197
|
+
const blocks = blocksOf(message.content);
|
|
198
|
+
let text = "";
|
|
199
|
+
for (const block of blocks) {
|
|
200
|
+
if (block.type === "tool-result") text = textFromBlocks(blocksOf(block.content));
|
|
201
|
+
}
|
|
202
|
+
if (text) out.push("", "<details><summary>\u5DE5\u5177\u7ED3\u679C</summary>", "", "```\n" + text.slice(0, 2e3) + "\n```", "", "</details>");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function createSessionMarkdownBuilder(meta, options = {}) {
|
|
209
|
+
const header = meta && typeof meta === "object" ? meta : {};
|
|
210
|
+
const body = [];
|
|
211
|
+
const fold = createMarkdownFold(body, header, options);
|
|
212
|
+
return {
|
|
213
|
+
addEvents(events) {
|
|
214
|
+
fold.add(events);
|
|
215
|
+
},
|
|
216
|
+
finish(metaOverride) {
|
|
217
|
+
const effective = metaOverride && typeof metaOverride === "object" ? metaOverride : header;
|
|
218
|
+
const title = fold.title;
|
|
219
|
+
const front = ["---"];
|
|
220
|
+
if (title) front.push(`title: ${yamlString(title)}`);
|
|
221
|
+
if (typeof effective.id === "string" && effective.id) front.push(`sessionId: ${yamlString(effective.id)}`);
|
|
222
|
+
if (typeof effective.cwd === "string" && effective.cwd) front.push(`cwd: ${yamlString(effective.cwd)}`);
|
|
223
|
+
const created = isoTime(effective.createdAt);
|
|
224
|
+
if (created) front.push(`createdAt: ${created}`);
|
|
225
|
+
const exported = isoTime(options.exportedAt);
|
|
226
|
+
if (exported) front.push(`exportedAt: ${exported}`);
|
|
227
|
+
front.push("---");
|
|
228
|
+
const doc = [front.join("\n")];
|
|
229
|
+
if (title) doc.push("", `# ${title}`);
|
|
230
|
+
doc.push(...body, "");
|
|
231
|
+
return doc.join("\n");
|
|
232
|
+
}
|
|
233
|
+
};
|
|
215
234
|
}
|
|
216
235
|
|
|
217
236
|
// src/star-index.js
|
|
@@ -448,20 +467,28 @@ function createAutoArchiveStore(options = {}) {
|
|
|
448
467
|
// src/session-meta-cache.js
|
|
449
468
|
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
450
469
|
var DEFAULT_MAX = 4e3;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
470
|
+
var REVISION_PREFIX = "rev:";
|
|
471
|
+
function fingerprintOf(stat3) {
|
|
472
|
+
if (!stat3 || typeof stat3 !== "object") return null;
|
|
473
|
+
if (typeof stat3.revision === "string" && stat3.revision.length > 0) {
|
|
474
|
+
return REVISION_PREFIX + stat3.revision;
|
|
475
|
+
}
|
|
476
|
+
const mtimeMs = stat3.mtimeMs;
|
|
477
|
+
const size = stat3.size;
|
|
455
478
|
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs) || mtimeMs <= 0) return null;
|
|
456
479
|
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
457
480
|
return `${Math.floor(mtimeMs)}:${size}`;
|
|
458
481
|
}
|
|
459
|
-
function
|
|
482
|
+
function isPersistableFingerprint(fingerprint) {
|
|
483
|
+
return typeof fingerprint === "string" && fingerprint !== "" && !fingerprint.startsWith(REVISION_PREFIX);
|
|
484
|
+
}
|
|
485
|
+
function isFresh(entry, stat3, now, ttlMs = DEFAULT_TTL_MS) {
|
|
460
486
|
if (!entry) return false;
|
|
461
|
-
const fp = fingerprintOf(
|
|
487
|
+
const fp = fingerprintOf(stat3);
|
|
462
488
|
if (!fp) return false;
|
|
463
489
|
if (entry.fingerprint !== fp) return false;
|
|
464
490
|
if (typeof entry.at !== "number") return false;
|
|
491
|
+
if (fp.startsWith(REVISION_PREFIX)) return true;
|
|
465
492
|
return now - entry.at <= ttlMs;
|
|
466
493
|
}
|
|
467
494
|
function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAULT_TTL_MS) {
|
|
@@ -469,8 +496,8 @@ function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAU
|
|
|
469
496
|
const missing = [];
|
|
470
497
|
for (const id of ids) {
|
|
471
498
|
const entry = cache && cache.get(String(id));
|
|
472
|
-
const
|
|
473
|
-
if (isFresh(entry,
|
|
499
|
+
const stat3 = statsById && statsById.get(String(id));
|
|
500
|
+
if (isFresh(entry, stat3, now, ttlMs) && entry && entry.meta) {
|
|
474
501
|
cached.set(String(id), entry.meta);
|
|
475
502
|
} else {
|
|
476
503
|
missing.push(String(id));
|
|
@@ -486,10 +513,10 @@ function createSessionMetaCache(opts = {}) {
|
|
|
486
513
|
let misses = 0;
|
|
487
514
|
return {
|
|
488
515
|
// 命中返回 meta,未命中/无法校验返回 null。
|
|
489
|
-
get(id,
|
|
516
|
+
get(id, stat3) {
|
|
490
517
|
const key = String(id);
|
|
491
518
|
const entry = map.get(key);
|
|
492
|
-
if (isFresh(entry,
|
|
519
|
+
if (isFresh(entry, stat3, Date.now(), ttlMs)) {
|
|
493
520
|
hits++;
|
|
494
521
|
map.delete(key);
|
|
495
522
|
map.set(key, entry);
|
|
@@ -498,9 +525,9 @@ function createSessionMetaCache(opts = {}) {
|
|
|
498
525
|
misses++;
|
|
499
526
|
return null;
|
|
500
527
|
},
|
|
501
|
-
set(id,
|
|
528
|
+
set(id, stat3, meta) {
|
|
502
529
|
if (!meta) return null;
|
|
503
|
-
const fp = fingerprintOf(
|
|
530
|
+
const fp = fingerprintOf(stat3);
|
|
504
531
|
if (!fp) return null;
|
|
505
532
|
const key = String(id);
|
|
506
533
|
map.delete(key);
|
|
@@ -546,7 +573,8 @@ function normalizeEntry(raw) {
|
|
|
546
573
|
const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
|
|
547
574
|
const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
|
|
548
575
|
const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
|
|
549
|
-
if (!fingerprint ||
|
|
576
|
+
if (!fingerprint || fingerprint.startsWith("rev:")) return null;
|
|
577
|
+
if (!title && !cwd) return null;
|
|
550
578
|
return { title, cwd, createdAt, fingerprint, updatedAt };
|
|
551
579
|
}
|
|
552
580
|
function normalizeTitleIndex(raw) {
|
|
@@ -639,6 +667,19 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
639
667
|
}
|
|
640
668
|
|
|
641
669
|
// src/compat/persistence.js
|
|
670
|
+
var DEFAULT_CHUNK = 400;
|
|
671
|
+
var MAX_CHUNKS = 2e4;
|
|
672
|
+
function normalizeReadResult(events) {
|
|
673
|
+
if (Array.isArray(events)) return events;
|
|
674
|
+
if (events && typeof events[Symbol.iterator] === "function") return [...events];
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
async function closeQuietly(handle) {
|
|
678
|
+
try {
|
|
679
|
+
if (handle && typeof handle.close === "function") await handle.close();
|
|
680
|
+
} catch (e) {
|
|
681
|
+
}
|
|
682
|
+
}
|
|
642
683
|
function asHeader(value) {
|
|
643
684
|
if (!value || typeof value !== "object") return null;
|
|
644
685
|
const candidate = value.header && typeof value.header === "object" ? value.header : value;
|
|
@@ -654,7 +695,7 @@ function normalizePersistenceEntry(value) {
|
|
|
654
695
|
id: String(header.id),
|
|
655
696
|
sizeBytes: snapshot && Number.isFinite(snapshot.sizeBytes) ? Number(snapshot.sizeBytes) : null,
|
|
656
697
|
eventCount: snapshot && Number.isSafeInteger(snapshot.eventCount) ? snapshot.eventCount : null,
|
|
657
|
-
revision: snapshot ? snapshot.revision : null
|
|
698
|
+
revision: snapshot && typeof snapshot.revision === "string" && snapshot.revision ? snapshot.revision : null
|
|
658
699
|
};
|
|
659
700
|
}
|
|
660
701
|
function normalizePersistenceList(values) {
|
|
@@ -663,44 +704,112 @@ function normalizePersistenceList(values) {
|
|
|
663
704
|
}
|
|
664
705
|
function createPersistenceAdapter(service) {
|
|
665
706
|
if (!service || typeof service.list !== "function") throw new TypeError("sessionPersistence.list is required");
|
|
707
|
+
const hasStat = typeof service.stat === "function";
|
|
708
|
+
const kind = typeof service.open === "function" ? "session-handle" : "legacy";
|
|
666
709
|
async function listEntries(options) {
|
|
667
710
|
return normalizePersistenceList(await service.list(options));
|
|
668
711
|
}
|
|
669
|
-
async function
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
712
|
+
async function statSession(id) {
|
|
713
|
+
if (!hasStat) return null;
|
|
714
|
+
const snapshot = await service.stat(id);
|
|
715
|
+
return snapshot ? normalizePersistenceEntry(snapshot) : null;
|
|
716
|
+
}
|
|
717
|
+
async function readChunk(handle, offset, length, signal) {
|
|
718
|
+
if (signal && signal.aborted) {
|
|
719
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
720
|
+
error.code = "DSM_READ_ABORTED";
|
|
721
|
+
throw error;
|
|
677
722
|
}
|
|
723
|
+
const events = await handle.read(offset, length, signal ? { signal } : void 0);
|
|
724
|
+
return normalizeReadResult(events);
|
|
725
|
+
}
|
|
726
|
+
async function readChunks(id, { offset = 0, chunkSize = DEFAULT_CHUNK, signal, onEvents }) {
|
|
678
727
|
if (typeof service.open !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
|
|
679
728
|
const handle = await service.open(id, "read");
|
|
680
729
|
if (!handle || typeof handle.read !== "function" || typeof handle.close !== "function") {
|
|
681
|
-
|
|
682
|
-
if (handle && typeof handle.close === "function") await handle.close();
|
|
683
|
-
} catch {
|
|
684
|
-
}
|
|
730
|
+
await closeQuietly(handle);
|
|
685
731
|
throw new Error("DSH \u8FD4\u56DE\u4E86\u65E0\u6548\u7684 SessionHandle");
|
|
686
732
|
}
|
|
733
|
+
let cursor = Number.isSafeInteger(offset) && offset >= 0 ? offset : 0;
|
|
734
|
+
let total = 0;
|
|
687
735
|
try {
|
|
688
|
-
|
|
736
|
+
for (let round = 0; round < MAX_CHUNKS; round++) {
|
|
737
|
+
const events = await readChunk(handle, cursor, chunkSize, signal);
|
|
738
|
+
if (events.length === 0) break;
|
|
739
|
+
cursor += events.length;
|
|
740
|
+
total += events.length;
|
|
741
|
+
if (onEvents) await onEvents(events, { offset: cursor - events.length, total });
|
|
742
|
+
if (events.length < chunkSize) break;
|
|
743
|
+
}
|
|
744
|
+
} finally {
|
|
745
|
+
await closeQuietly(handle);
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
meta: handle.header || handle.meta || null,
|
|
749
|
+
inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
|
|
750
|
+
eventCount: total
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
async function inspectSession(id, opts = {}) {
|
|
754
|
+
const chunkSize = Number.isSafeInteger(opts.chunkSize) && opts.chunkSize > 0 ? opts.chunkSize : DEFAULT_CHUNK;
|
|
755
|
+
if (typeof service.open === "function") {
|
|
756
|
+
if (opts.signal && opts.signal.aborted) {
|
|
757
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
758
|
+
error.code = "DSM_READ_ABORTED";
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
return readChunks(id, { offset: opts.offset || 0, chunkSize, signal: opts.signal, onEvents: opts.onEvents });
|
|
762
|
+
}
|
|
763
|
+
if (typeof service.readFrom !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
|
|
764
|
+
if (opts.signal && opts.signal.aborted) {
|
|
765
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
766
|
+
error.code = "DSM_READ_ABORTED";
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
const result = await service.readFrom(id, opts.offset || 0);
|
|
770
|
+
const events = normalizeReadResult(result && result.events);
|
|
771
|
+
if (opts.onEvents && events.length) await opts.onEvents(events, { offset: opts.offset || 0, total: events.length });
|
|
772
|
+
return {
|
|
773
|
+
meta: result && result.meta ? result.meta : null,
|
|
774
|
+
inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
|
|
775
|
+
eventCount: events.length
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
async function readSession(id, offset = 0) {
|
|
779
|
+
if (typeof service.readFrom === "function") {
|
|
780
|
+
const result = await service.readFrom(id, offset);
|
|
689
781
|
return {
|
|
690
|
-
meta:
|
|
691
|
-
inheritedEventCount: Number.isSafeInteger(
|
|
692
|
-
events: Array.isArray(events) ? events : [
|
|
782
|
+
meta: result && result.meta ? result.meta : null,
|
|
783
|
+
inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
|
|
784
|
+
events: result && Array.isArray(result.events) ? result.events : []
|
|
693
785
|
};
|
|
694
|
-
} finally {
|
|
695
|
-
await handle.close();
|
|
696
786
|
}
|
|
787
|
+
const events = [];
|
|
788
|
+
const summary = await readChunks(id, { offset, onEvents: (batch) => {
|
|
789
|
+
events.push(...batch);
|
|
790
|
+
} });
|
|
791
|
+
return {
|
|
792
|
+
meta: summary.meta,
|
|
793
|
+
inheritedEventCount: summary.inheritedEventCount,
|
|
794
|
+
events
|
|
795
|
+
};
|
|
697
796
|
}
|
|
698
797
|
function locate(header) {
|
|
699
798
|
if (typeof service.locate === "function") return service.locate(header);
|
|
700
799
|
return null;
|
|
701
800
|
}
|
|
702
|
-
|
|
703
|
-
|
|
801
|
+
async function locateVerified(header) {
|
|
802
|
+
if (typeof service.locate === "function") {
|
|
803
|
+
try {
|
|
804
|
+
const loc = service.locate(header);
|
|
805
|
+
if (loc && typeof loc.path === "string") return { path: loc.path, sessionDir: null };
|
|
806
|
+
} catch (e) {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const artifacts = await locateSessionArtifacts(service, header);
|
|
810
|
+
return artifacts ? { path: artifacts.logPath, sessionDir: artifacts.sessionDir } : null;
|
|
811
|
+
}
|
|
812
|
+
return { kind, listEntries, readSession, inspectSession, statSession, locate, locateVerified, hasStat };
|
|
704
813
|
}
|
|
705
814
|
|
|
706
815
|
// src/compat/capabilities.js
|
|
@@ -711,17 +820,34 @@ function detectCapabilities({ persistence, workspaceRegistry }) {
|
|
|
711
820
|
const handleApi = !!(persistence && typeof persistence.open === "function");
|
|
712
821
|
const legacyRead = !!(persistence && typeof persistence.readFrom === "function");
|
|
713
822
|
const legacyLocate = !!(persistence && (typeof persistence.locate === "function" || persistence.backend && typeof persistence.backend.locate === "function"));
|
|
823
|
+
const handleEraRoot = !!(handleApi && persistence && typeof persistence.root === "string" && persistence.root.length > 0);
|
|
824
|
+
const canVerifyExistence = !!(handleApi || legacyRead || persistence && typeof persistence.stat === "function");
|
|
825
|
+
const readOk = legacyRead || handleApi;
|
|
714
826
|
const workspaceInternals = !!(workspaceRegistry && workspaceRegistry.headers && workspaceRegistry.sessionPaths && typeof workspaceRegistry.replaceHeaderIndex === "function");
|
|
827
|
+
const matrix = {
|
|
828
|
+
readInspection: action(readOk, "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u8BC6\u522B\u7684\u4F1A\u8BDD\u8BFB\u53D6\u63A5\u53E3"),
|
|
829
|
+
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === "function"), "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5F52\u6863\u63A5\u53E3"),
|
|
830
|
+
softTrash: action(readOk, "\u5F53\u524D DSH \u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5B89\u5168\u79FB\u5165\u56DE\u6536\u7AD9"),
|
|
831
|
+
// 恢复不再无条件宣称可用:必须能校验底层会话仍存在(stat 或 list),
|
|
832
|
+
// 否则恢复只会制造一条指向已消失日志的僵尸条目。
|
|
833
|
+
restoreIndexedSession: action(canVerifyExistence, "\u5F53\u524D DSH \u65E0\u6CD5\u6821\u9A8C\u5E95\u5C42\u4F1A\u8BDD\u662F\u5426\u5B58\u5728\uFF0C\u4E0D\u80FD\u5B89\u5168\u6062\u590D"),
|
|
834
|
+
physicalPurge: action(
|
|
835
|
+
!handleApi && legacyLocate || handleApi && handleEraRoot && canVerifyExistence,
|
|
836
|
+
handleApi && !handleEraRoot ? "\u65E0\u6CD5\u4ECE\u5F53\u524D DSH \u540E\u7AEF\u786E\u8BA4\u4F1A\u8BDD\u5B58\u50A8\u6839\u76EE\u5F55\uFF0C\u5DF2\u505C\u6B62\u7269\u7406\u5220\u9664\u4EE5\u4FDD\u62A4\u6570\u636E\u5B89\u5168" : "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u5B89\u5168\u6C38\u4E45\u5220\u9664\u80FD\u529B\uFF1B\u79FB\u5165\u56DE\u6536\u7AD9\u4E0D\u4F1A\u91CA\u653E\u78C1\u76D8\u7A7A\u95F4"
|
|
837
|
+
),
|
|
838
|
+
relocateSession: action(
|
|
839
|
+
!handleApi && legacyRead && legacyLocate && workspaceInternals || handleApi && handleEraRoot && readOk && workspaceInternals,
|
|
840
|
+
handleApi && !workspaceInternals ? "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5DE5\u4F5C\u533A\u6CE8\u518C\u8868\u5185\u90E8\u7ED3\u6784\uFF0C\u8DE8\u5DE5\u4F5C\u533A\u79FB\u52A8\u540E\u65E0\u6CD5\u5373\u65F6\u5237\u65B0\u5206\u7EC4" : handleApi && !handleEraRoot ? "\u65E0\u6CD5\u4ECE\u5F53\u524D DSH \u540E\u7AEF\u786E\u8BA4\u4F1A\u8BDD\u5B58\u50A8\u6839\u76EE\u5F55\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8\u4EE5\u4FDD\u62A4\u6570\u636E\u5B89\u5168" : "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u8DE8\u5DE5\u4F5C\u533A\u8FC1\u79FB\u80FD\u529B"
|
|
841
|
+
)
|
|
842
|
+
};
|
|
843
|
+
matrix.read = matrix.readInspection;
|
|
844
|
+
matrix.trash = matrix.softTrash;
|
|
845
|
+
matrix.restoreTrash = matrix.restoreIndexedSession;
|
|
846
|
+
matrix.purge = matrix.physicalPurge;
|
|
847
|
+
matrix.move = matrix.relocateSession;
|
|
715
848
|
return {
|
|
716
849
|
persistence: handleApi ? "session-handle" : "legacy",
|
|
717
|
-
actions:
|
|
718
|
-
read: action(legacyRead || handleApi, "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u8BC6\u522B\u7684\u4F1A\u8BDD\u8BFB\u53D6\u63A5\u53E3"),
|
|
719
|
-
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === "function"), "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5F52\u6863\u63A5\u53E3"),
|
|
720
|
-
trash: action(legacyRead || handleApi, "\u5F53\u524D DSH \u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5B89\u5168\u79FB\u5165\u56DE\u6536\u7AD9"),
|
|
721
|
-
restoreTrash: action(true),
|
|
722
|
-
purge: action(!handleApi && legacyLocate, "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u5B89\u5168\u6C38\u4E45\u5220\u9664\u80FD\u529B"),
|
|
723
|
-
move: action(!handleApi && legacyRead && legacyLocate && workspaceInternals, "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u8DE8\u5DE5\u4F5C\u533A\u8FC1\u79FB\u80FD\u529B")
|
|
724
|
-
}
|
|
850
|
+
actions: matrix
|
|
725
851
|
};
|
|
726
852
|
}
|
|
727
853
|
function requireCapability(capabilities, name2) {
|
|
@@ -733,12 +859,273 @@ function requireCapability(capabilities, name2) {
|
|
|
733
859
|
throw error;
|
|
734
860
|
}
|
|
735
861
|
|
|
862
|
+
// src/path-guard.js
|
|
863
|
+
function splitSegments(target) {
|
|
864
|
+
return String(target).replace(/[\\/]+$/, "").split(/[\\/]/).filter((seg) => seg.length > 0);
|
|
865
|
+
}
|
|
866
|
+
function stripDriveLetter(segments) {
|
|
867
|
+
return segments.length > 0 && /^[A-Za-z]:$/.test(segments[0]) ? segments.slice(1) : segments;
|
|
868
|
+
}
|
|
869
|
+
function pathOwnsSession(target, sid) {
|
|
870
|
+
if (typeof target !== "string" || target.length === 0) return false;
|
|
871
|
+
if (typeof sid !== "string" || sid.length === 0) return false;
|
|
872
|
+
const segments = stripDriveLetter(splitSegments(target));
|
|
873
|
+
if (segments.length === 0) return false;
|
|
874
|
+
const file = segments[segments.length - 1];
|
|
875
|
+
if (segments.length >= 2 && segments[segments.length - 2] === sid) return true;
|
|
876
|
+
if (file.includes(sid)) {
|
|
877
|
+
const ID_CHAR = /[A-Za-z0-9_-]/;
|
|
878
|
+
let from = 0;
|
|
879
|
+
while (true) {
|
|
880
|
+
const at = file.indexOf(sid, from);
|
|
881
|
+
if (at < 0) return false;
|
|
882
|
+
const before = at > 0 ? file[at - 1] : "";
|
|
883
|
+
const after = at + sid.length < file.length ? file[at + sid.length] : "";
|
|
884
|
+
if (!(before && ID_CHAR.test(before)) && !(after && ID_CHAR.test(after))) return true;
|
|
885
|
+
from = at + 1;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/handle-era-ops.js
|
|
892
|
+
import { mkdir as mkdir4, readFile as readFile2, rename as rename4, rm, unlink, writeFile as writeFile4 } from "node:fs/promises";
|
|
893
|
+
import { join as join5 } from "node:path";
|
|
894
|
+
|
|
895
|
+
// src/handle-era-paths.js
|
|
896
|
+
import { readdir, stat } from "node:fs/promises";
|
|
897
|
+
import { basename, join as join4 } from "node:path";
|
|
898
|
+
var GENERATION_LOG_RE = /^session\.v\d+\.jsonl(\.zst(d)?)?$/;
|
|
899
|
+
function isSafeChar(ch) {
|
|
900
|
+
return ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch);
|
|
901
|
+
}
|
|
902
|
+
function projectKeyFor(cwd) {
|
|
903
|
+
const s = String(cwd);
|
|
904
|
+
if (s.length === 0) throw new Error("cannot encode an empty project path");
|
|
905
|
+
let readable = "";
|
|
906
|
+
let separatorRun = false;
|
|
907
|
+
for (let i = 0; i < s.length; i++) {
|
|
908
|
+
const ch = s[i];
|
|
909
|
+
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
910
|
+
if (!separatorRun) readable += "-";
|
|
911
|
+
separatorRun = true;
|
|
912
|
+
} else if (isSafeChar(ch)) {
|
|
913
|
+
readable += ch;
|
|
914
|
+
separatorRun = false;
|
|
915
|
+
} else {
|
|
916
|
+
readable += "~" + s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
|
|
917
|
+
separatorRun = false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return "--" + (readable.replace(/^-+/, "") || "root").slice(0, 251) + "--";
|
|
921
|
+
}
|
|
922
|
+
function encodeSegmentFor(raw) {
|
|
923
|
+
const s = String(raw);
|
|
924
|
+
if (s.length === 0) throw new Error("cannot encode an empty path segment");
|
|
925
|
+
if (s === ".") return "~002E";
|
|
926
|
+
if (s === "..") return "~002E~002E";
|
|
927
|
+
let out = "";
|
|
928
|
+
for (let i = 0; i < s.length; i++) {
|
|
929
|
+
const ch = s[i];
|
|
930
|
+
out += isSafeChar(ch) ? ch : "~" + s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
|
|
931
|
+
}
|
|
932
|
+
return out;
|
|
933
|
+
}
|
|
934
|
+
function resolveSessionRoot(sp) {
|
|
935
|
+
const root = sp && typeof sp === "object" ? sp.root : void 0;
|
|
936
|
+
return typeof root === "string" && root.length > 0 ? root : null;
|
|
937
|
+
}
|
|
938
|
+
function deriveSessionDir(root, cwd, id) {
|
|
939
|
+
const project = cwd === void 0 || cwd === null || cwd === "" ? join4(root, "_no-cwd") : join4(root, projectKeyFor(cwd));
|
|
940
|
+
return join4(project, encodeSegmentFor(id));
|
|
941
|
+
}
|
|
942
|
+
async function locateSessionArtifacts2(sp, header) {
|
|
943
|
+
const root = resolveSessionRoot(sp);
|
|
944
|
+
if (!root || !header || header.id == null) return null;
|
|
945
|
+
const sid = String(header.id);
|
|
946
|
+
let sessionDir;
|
|
947
|
+
try {
|
|
948
|
+
sessionDir = deriveSessionDir(root, header.cwd, sid);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
if (basename(sessionDir) !== encodeSegmentFor(sid)) return null;
|
|
953
|
+
let entries;
|
|
954
|
+
try {
|
|
955
|
+
const st = await stat(sessionDir);
|
|
956
|
+
if (!st.isDirectory()) return null;
|
|
957
|
+
entries = await readdir(sessionDir);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
|
|
962
|
+
if (generationFiles.length === 0) return null;
|
|
963
|
+
generationFiles.sort((a, b) => {
|
|
964
|
+
const va = Number((a.match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
965
|
+
const vb = Number((b.match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
966
|
+
return vb - va;
|
|
967
|
+
});
|
|
968
|
+
const logPath = join4(sessionDir, generationFiles[0]);
|
|
969
|
+
if (!pathOwnsSession(logPath, sid)) return null;
|
|
970
|
+
return {
|
|
971
|
+
root,
|
|
972
|
+
projectDir: join4(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
|
|
973
|
+
sessionDir,
|
|
974
|
+
logPath,
|
|
975
|
+
generationFiles
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/handle-era-ops.js
|
|
980
|
+
var MOVE_BATCH = 400;
|
|
981
|
+
function conflictError(message) {
|
|
982
|
+
const error = new Error(message);
|
|
983
|
+
error.status = 409;
|
|
984
|
+
error.code = "DSM_SESSION_BUSY";
|
|
985
|
+
return error;
|
|
986
|
+
}
|
|
987
|
+
function failureText(e) {
|
|
988
|
+
return `${e && e.name || ""} ${e && e.message || e}`;
|
|
989
|
+
}
|
|
990
|
+
function isAlreadyOwned(e) {
|
|
991
|
+
return /already owned/i.test(failureText(e));
|
|
992
|
+
}
|
|
993
|
+
function isAlreadyExists(e) {
|
|
994
|
+
return /already exists/i.test(failureText(e));
|
|
995
|
+
}
|
|
996
|
+
async function closeQuietly2(handle) {
|
|
997
|
+
try {
|
|
998
|
+
if (handle && typeof handle.close === "function") await handle.close();
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function ensureNoActiveWriter(sp, sid) {
|
|
1003
|
+
let handle = null;
|
|
1004
|
+
try {
|
|
1005
|
+
handle = await sp.open(sid, "write");
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
if (isAlreadyOwned(e)) throw conflictError("\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FDB\u884C\u4E2D\uFF08\u5B58\u5728\u6D3B\u8DC3\u5199\u5165\uFF09\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u522B\u7684\u4F1A\u8BDD\u518D\u64CD\u4F5C\u3002");
|
|
1008
|
+
throw e;
|
|
1009
|
+
}
|
|
1010
|
+
await closeQuietly2(handle);
|
|
1011
|
+
}
|
|
1012
|
+
async function purgeSessionArtifacts(sp, sid, header) {
|
|
1013
|
+
const artifacts = await locateSessionArtifacts2(sp, header);
|
|
1014
|
+
if (!artifacts) {
|
|
1015
|
+
const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\u76EE\u5F55\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
1016
|
+
error.status = 409;
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
let writer = null;
|
|
1020
|
+
try {
|
|
1021
|
+
writer = await sp.open(sid, "write");
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
if (isAlreadyOwned(e)) throw conflictError("\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FDB\u884C\u4E2D\uFF08\u5B58\u5728\u6D3B\u8DC3\u5199\u5165\uFF09\uFF0C\u65E0\u6CD5\u5F7B\u5E95\u5220\u9664\u3002");
|
|
1024
|
+
throw e;
|
|
1025
|
+
}
|
|
1026
|
+
await closeQuietly2(writer);
|
|
1027
|
+
writer = null;
|
|
1028
|
+
try {
|
|
1029
|
+
await rm(artifacts.sessionDir, { recursive: true, force: true });
|
|
1030
|
+
} catch (e) {
|
|
1031
|
+
const error = new Error("\u5220\u9664\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1032
|
+
error.status = 500;
|
|
1033
|
+
throw error;
|
|
1034
|
+
}
|
|
1035
|
+
if (typeof sp.stat === "function") {
|
|
1036
|
+
const after = await sp.stat(sid).catch(() => void 0);
|
|
1037
|
+
if (after) {
|
|
1038
|
+
const error = new Error("\u5220\u9664\u540E\u5B98\u65B9 stat \u4ECD\u80FD\u770B\u5230\u8BE5\u4F1A\u8BDD\uFF0C\u5DF2\u4E2D\u6B62\uFF08\u76EE\u5F55\u53EF\u80FD\u88AB\u5E76\u53D1\u91CD\u5EFA\uFF09");
|
|
1039
|
+
error.status = 500;
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return artifacts;
|
|
1044
|
+
}
|
|
1045
|
+
async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }) {
|
|
1046
|
+
const original = await readFile2(backupPath);
|
|
1047
|
+
const frames = scanZstdFrames(original).frames;
|
|
1048
|
+
if (frames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
1049
|
+
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
1050
|
+
const rewrittenFrames = scanZstdFrames(rewritten).frames;
|
|
1051
|
+
if (rewrittenFrames.length !== frames.length) throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u5E27\u6570\u53D1\u751F\u53D8\u5316");
|
|
1052
|
+
if (!original.subarray(frames[0].end).equals(rewritten.subarray(rewrittenFrames[0].end))) {
|
|
1053
|
+
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1054
|
+
}
|
|
1055
|
+
const targetDir = deriveSessionDir(artifacts.root, canonical, sid);
|
|
1056
|
+
await mkdir4(targetDir, { recursive: true });
|
|
1057
|
+
const staged = join5(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
|
|
1058
|
+
await writeFile4(staged, rewritten, { mode: 384 });
|
|
1059
|
+
await rename4(staged, join5(targetDir, artifacts.generationFiles[0]));
|
|
1060
|
+
return targetDir;
|
|
1061
|
+
}
|
|
1062
|
+
async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
|
|
1063
|
+
const artifacts = await locateSessionArtifacts2(sp, header);
|
|
1064
|
+
if (!artifacts) {
|
|
1065
|
+
const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8");
|
|
1066
|
+
error.status = 409;
|
|
1067
|
+
throw error;
|
|
1068
|
+
}
|
|
1069
|
+
await ensureNoActiveWriter(sp, sid);
|
|
1070
|
+
const newHeader = Object.assign({}, header, { cwd: canonical });
|
|
1071
|
+
const firstSeq = events.length ? Number(events[0].seq) : 0;
|
|
1072
|
+
const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
|
|
1073
|
+
const createOptions = header.isSeeded && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? { inheritedEventCount } : void 0;
|
|
1074
|
+
const backupPath = `${artifacts.logPath}.move-backup-${process.pid}-${Date.now()}`;
|
|
1075
|
+
await rename4(artifacts.logPath, backupPath);
|
|
1076
|
+
let writer = null;
|
|
1077
|
+
try {
|
|
1078
|
+
try {
|
|
1079
|
+
writer = await sp.create(newHeader, createOptions);
|
|
1080
|
+
for (let i = 0; i < replay.length; i += MOVE_BATCH) {
|
|
1081
|
+
await writer.append(replay.slice(i, i + MOVE_BATCH));
|
|
1082
|
+
}
|
|
1083
|
+
await writer.flush();
|
|
1084
|
+
await writer.close();
|
|
1085
|
+
writer = null;
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
if (isAlreadyExists(e)) {
|
|
1088
|
+
await relocateRewrittenBackup({ sid, canonical, backupPath, artifacts });
|
|
1089
|
+
} else {
|
|
1090
|
+
throw e;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof sp.stat !== "function") throw new Error("\u79FB\u52A8\u540E\u65E0\u6CD5\u6821\u9A8C\uFF1A\u540E\u7AEF\u672A\u63D0\u4F9B stat");
|
|
1094
|
+
const after = await sp.stat(sid);
|
|
1095
|
+
if (!after || !after.header || after.header.cwd !== canonical) {
|
|
1096
|
+
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u672A\u6B63\u786E\u66F4\u65B0");
|
|
1097
|
+
}
|
|
1098
|
+
if (Number.isSafeInteger(after.eventCount) && events.length > 0 && after.eventCount !== events.length) {
|
|
1099
|
+
throw new Error(`\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4E8B\u4EF6\u6570\u4E0D\u4E00\u81F4\uFF08\u6E90 ${events.length}\uFF0C\u526F\u672C ${after.eventCount}\uFF09`);
|
|
1100
|
+
}
|
|
1101
|
+
} catch (e) {
|
|
1102
|
+
await closeQuietly2(writer);
|
|
1103
|
+
try {
|
|
1104
|
+
await rm(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
|
|
1105
|
+
} catch (_) {
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
await rename4(backupPath, artifacts.logPath);
|
|
1109
|
+
} catch (_) {
|
|
1110
|
+
}
|
|
1111
|
+
if (e && e.status) throw e;
|
|
1112
|
+
const error = new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1113
|
+
error.status = 500;
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
await unlink(backupPath);
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
}
|
|
1120
|
+
return { sessionDir: deriveSessionDir(artifacts.root, canonical, sid) };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
736
1123
|
// src/index.js
|
|
737
1124
|
var name = "dsh-sessions-manager";
|
|
738
1125
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
739
1126
|
var MAX_TITLE = 80;
|
|
740
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
741
|
-
var TRASH_INDEX =
|
|
1127
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join6(homedir3(), ".dsh", "sessions-manager-trash");
|
|
1128
|
+
var TRASH_INDEX = join6(TRASH_DIR, "index.json");
|
|
742
1129
|
var TRASH_SCHEMA_VERSION = 2;
|
|
743
1130
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
744
1131
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -752,6 +1139,21 @@ function json(res, value, status = 200) {
|
|
|
752
1139
|
function errorStatus(error) {
|
|
753
1140
|
return error && Number.isInteger(error.status) ? error.status : 500;
|
|
754
1141
|
}
|
|
1142
|
+
function createLimiter(max) {
|
|
1143
|
+
let active = 0;
|
|
1144
|
+
const queue = [];
|
|
1145
|
+
return async function run(fn) {
|
|
1146
|
+
if (active >= max) await new Promise((resolve) => queue.push(resolve));
|
|
1147
|
+
active++;
|
|
1148
|
+
try {
|
|
1149
|
+
return await fn();
|
|
1150
|
+
} finally {
|
|
1151
|
+
active--;
|
|
1152
|
+
const next = queue.shift();
|
|
1153
|
+
if (next) next();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
755
1157
|
async function readJsonBody(req) {
|
|
756
1158
|
const chunks = [];
|
|
757
1159
|
let total = 0;
|
|
@@ -825,7 +1227,7 @@ function apply(ctx) {
|
|
|
825
1227
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
826
1228
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
827
1229
|
const metaCache = createSessionMetaCache();
|
|
828
|
-
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file:
|
|
1230
|
+
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join6(TRASH_DIR, "title-index.json") });
|
|
829
1231
|
async function hydrateFromPersist(ids, statsById) {
|
|
830
1232
|
const hits = /* @__PURE__ */ new Map();
|
|
831
1233
|
if (!ids || !ids.length) return hits;
|
|
@@ -836,10 +1238,11 @@ function apply(ctx) {
|
|
|
836
1238
|
return hits;
|
|
837
1239
|
}
|
|
838
1240
|
for (const id of ids) {
|
|
839
|
-
const
|
|
1241
|
+
const stat3 = statsById.get(id);
|
|
840
1242
|
const entry = store && store[id];
|
|
841
|
-
if (!
|
|
842
|
-
const fp = fingerprintOf(
|
|
1243
|
+
if (!stat3 || !entry) continue;
|
|
1244
|
+
const fp = fingerprintOf(stat3);
|
|
1245
|
+
if (!isPersistableFingerprint(fp)) continue;
|
|
843
1246
|
if (fp && entry.fingerprint === fp) {
|
|
844
1247
|
hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
|
|
845
1248
|
}
|
|
@@ -852,7 +1255,7 @@ function apply(ctx) {
|
|
|
852
1255
|
const now = Date.now();
|
|
853
1256
|
for (const [id, meta] of decoded) {
|
|
854
1257
|
const fp = fingerprintOf(statsById.get(id));
|
|
855
|
-
if (!fp) continue;
|
|
1258
|
+
if (!isPersistableFingerprint(fp)) continue;
|
|
856
1259
|
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
|
|
857
1260
|
}
|
|
858
1261
|
if (!Object.keys(batch).length) return;
|
|
@@ -931,26 +1334,42 @@ function apply(ctx) {
|
|
|
931
1334
|
}
|
|
932
1335
|
async function resolveOne(id, usage, opts = {}) {
|
|
933
1336
|
const key = String(id);
|
|
934
|
-
const statInfo = usage ? { mtimeMs: usage.mtimeById.get(key), size: usage.sizeById.get(key) } : null;
|
|
1337
|
+
const statInfo = usage ? (usage.statsById ? usage.statsById.get(key) : null) || { mtimeMs: usage.mtimeById && usage.mtimeById.get(key), size: usage.sizeById && usage.sizeById.get(key) } : null;
|
|
935
1338
|
const cached = metaCache.get(key, statInfo);
|
|
936
1339
|
if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
|
|
937
1340
|
let meta = { title: null, cwd: null, createdAt: null };
|
|
1341
|
+
if (opts.listHeader) {
|
|
1342
|
+
if (typeof opts.listHeader.cwd === "string") meta.cwd = opts.listHeader.cwd;
|
|
1343
|
+
if (opts.listHeader.createdAt != null) meta.createdAt = opts.listHeader.createdAt;
|
|
1344
|
+
}
|
|
938
1345
|
if (opts.preloaded !== void 0) {
|
|
939
|
-
|
|
1346
|
+
const projected = metaFromSnapshot(unwrapSnapshot(opts.preloaded));
|
|
1347
|
+
if (projected.title) meta.title = projected.title;
|
|
1348
|
+
if (!meta.cwd && projected.cwd) meta.cwd = projected.cwd;
|
|
1349
|
+
if (!meta.createdAt && projected.createdAt) meta.createdAt = projected.createdAt;
|
|
940
1350
|
} else if (typeof sq.readTitleSnapshot === "function") {
|
|
941
1351
|
try {
|
|
942
|
-
|
|
1352
|
+
const projected = metaFromSnapshot(await sq.readTitleSnapshot(id));
|
|
1353
|
+
if (projected.title) meta.title = projected.title;
|
|
1354
|
+
if (!meta.cwd && projected.cwd) meta.cwd = projected.cwd;
|
|
1355
|
+
if (!meta.createdAt && projected.createdAt) meta.createdAt = projected.createdAt;
|
|
943
1356
|
} catch (e) {
|
|
944
1357
|
}
|
|
945
1358
|
}
|
|
946
|
-
|
|
1359
|
+
const projectionAvailable = typeof sq.readTitleSnapshot === "function" || typeof sq.readTitleSnapshots === "function";
|
|
1360
|
+
if (!meta.cwd || !meta.title && !projectionAvailable) {
|
|
947
1361
|
try {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1362
|
+
let foldedTitle = null;
|
|
1363
|
+
const summary = await persistence.inspectSession(key, {
|
|
1364
|
+
onEvents: (events) => {
|
|
1365
|
+
if (!foldedTitle) foldedTitle = foldTitle(events);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
if (summary && summary.meta) {
|
|
1369
|
+
if (!meta.cwd) meta.cwd = summary.meta.cwd || null;
|
|
1370
|
+
if (!meta.createdAt) meta.createdAt = summary.meta.createdAt || null;
|
|
952
1371
|
}
|
|
953
|
-
if (!meta.title &&
|
|
1372
|
+
if (!meta.title && foldedTitle) meta.title = foldedTitle;
|
|
954
1373
|
} catch (e2) {
|
|
955
1374
|
}
|
|
956
1375
|
}
|
|
@@ -958,11 +1377,12 @@ function apply(ctx) {
|
|
|
958
1377
|
if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
|
|
959
1378
|
return buildItem(key, meta, usage, opts.exposeUsage);
|
|
960
1379
|
}
|
|
961
|
-
async function collectUsage(
|
|
1380
|
+
async function collectUsage(preloadedEntries) {
|
|
962
1381
|
const sizeById = /* @__PURE__ */ new Map();
|
|
963
1382
|
const mtimeById = /* @__PURE__ */ new Map();
|
|
1383
|
+
const statsById = /* @__PURE__ */ new Map();
|
|
964
1384
|
let entries = null;
|
|
965
|
-
if (Array.isArray(
|
|
1385
|
+
if (Array.isArray(preloadedEntries)) entries = preloadedEntries;
|
|
966
1386
|
else {
|
|
967
1387
|
try {
|
|
968
1388
|
entries = await persistence.listEntries();
|
|
@@ -977,19 +1397,27 @@ function apply(ctx) {
|
|
|
977
1397
|
const header = entry && entry.header ? entry.header : entry;
|
|
978
1398
|
const id = entry && entry.id != null ? String(entry.id) : header && header.id != null ? String(header.id) : null;
|
|
979
1399
|
if (!id) return;
|
|
1400
|
+
if (entry && typeof entry.revision === "string" && entry.revision) {
|
|
1401
|
+
if (Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
|
|
1402
|
+
statsById.set(id, { revision: entry.revision });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
980
1405
|
if (entry && Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
|
|
981
1406
|
try {
|
|
982
1407
|
const loc = persistence.locate(header);
|
|
983
1408
|
if (!loc || typeof loc.path !== "string" || !loc.path) return;
|
|
984
|
-
const st = await
|
|
1409
|
+
const st = await stat2(loc.path);
|
|
985
1410
|
if (!st) return;
|
|
986
1411
|
if (typeof st.size === "number") sizeById.set(id, st.size);
|
|
987
|
-
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0)
|
|
1412
|
+
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) {
|
|
1413
|
+
mtimeById.set(id, Math.floor(st.mtimeMs));
|
|
1414
|
+
statsById.set(id, { mtimeMs: Math.floor(st.mtimeMs), size: typeof st.size === "number" ? st.size : void 0 });
|
|
1415
|
+
}
|
|
988
1416
|
} catch (e) {
|
|
989
1417
|
}
|
|
990
1418
|
}));
|
|
991
1419
|
}
|
|
992
|
-
return { sizeById, mtimeById };
|
|
1420
|
+
return { sizeById, mtimeById, statsById, hasActivityData: mtimeById.size > 0 };
|
|
993
1421
|
}
|
|
994
1422
|
async function restoreOne(sid) {
|
|
995
1423
|
requireSessionId(sid);
|
|
@@ -1018,10 +1446,10 @@ function apply(ctx) {
|
|
|
1018
1446
|
return (await readTrashStore()).items;
|
|
1019
1447
|
}
|
|
1020
1448
|
async function writeTrashStore(store) {
|
|
1021
|
-
await
|
|
1022
|
-
const tmp =
|
|
1023
|
-
await
|
|
1024
|
-
await
|
|
1449
|
+
await mkdir5(TRASH_DIR, { recursive: true });
|
|
1450
|
+
const tmp = join6(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
1451
|
+
await writeFile5(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
1452
|
+
await rename5(tmp, TRASH_INDEX);
|
|
1025
1453
|
}
|
|
1026
1454
|
function mutateTrash(mutator) {
|
|
1027
1455
|
const operation = trashMutation.then(async () => {
|
|
@@ -1064,10 +1492,28 @@ function apply(ctx) {
|
|
|
1064
1492
|
title = header.title || header.meta && header.meta.title || null;
|
|
1065
1493
|
}
|
|
1066
1494
|
if (!title) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1495
|
+
if (typeof sq.readTitleSnapshot === "function") {
|
|
1496
|
+
try {
|
|
1497
|
+
const snap = unwrapSnapshot(await sq.readTitleSnapshot(sid));
|
|
1498
|
+
if (snap && snap.title && snap.title.title) title = String(snap.title.title);
|
|
1499
|
+
if (snap && snap.session) {
|
|
1500
|
+
if (!cwd) cwd = snap.session.cwd || null;
|
|
1501
|
+
}
|
|
1502
|
+
} catch (e) {
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (!title) {
|
|
1507
|
+
try {
|
|
1508
|
+
let folded = null;
|
|
1509
|
+
const summary = await persistence.inspectSession(sid, {
|
|
1510
|
+
onEvents: (events) => {
|
|
1511
|
+
if (!folded) folded = foldTitle(events);
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
if (summary && summary.meta && !cwd) cwd = summary.meta.cwd || null;
|
|
1515
|
+
title = folded;
|
|
1516
|
+
} catch (e) {
|
|
1071
1517
|
}
|
|
1072
1518
|
}
|
|
1073
1519
|
} catch (e) {
|
|
@@ -1096,20 +1542,96 @@ function apply(ctx) {
|
|
|
1096
1542
|
});
|
|
1097
1543
|
return { ok: true, trashed: true };
|
|
1098
1544
|
}
|
|
1545
|
+
async function verifyTrashRestore(sid) {
|
|
1546
|
+
const sessions = ctx.get("sessions");
|
|
1547
|
+
if (sessions && sessions.get && sessions.get(sid)) {
|
|
1548
|
+
return { ok: true, workspaceGone: false, verified: true };
|
|
1549
|
+
}
|
|
1550
|
+
let exists = false;
|
|
1551
|
+
let header = null;
|
|
1552
|
+
try {
|
|
1553
|
+
const stat3 = await persistence.statSession(sid);
|
|
1554
|
+
if (stat3) {
|
|
1555
|
+
exists = true;
|
|
1556
|
+
header = stat3.header;
|
|
1557
|
+
}
|
|
1558
|
+
} catch (e) {
|
|
1559
|
+
}
|
|
1560
|
+
if (!exists) {
|
|
1561
|
+
try {
|
|
1562
|
+
const entries = await persistence.listEntries();
|
|
1563
|
+
const found = entries.find((entry2) => entry2.id === sid);
|
|
1564
|
+
if (found) {
|
|
1565
|
+
exists = true;
|
|
1566
|
+
header = found.header;
|
|
1567
|
+
}
|
|
1568
|
+
} catch (e) {
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (!exists) {
|
|
1572
|
+
const store2 = await readTrashStore();
|
|
1573
|
+
if (store2.purgedSessionIds.map(String).includes(sid)) {
|
|
1574
|
+
return { ok: false, status: 410, code: "DSM_SESSION_PURGED", message: "\u8BE5\u4F1A\u8BDD\u5DF2\u5F7B\u5E95\u5220\u9664\uFF0C\u65E0\u6CD5\u4ECE\u56DE\u6536\u7AD9\u6062\u590D" };
|
|
1575
|
+
}
|
|
1576
|
+
return { ok: false, status: 409, code: "DSM_SESSION_MISSING", message: "\u5E95\u5C42\u4F1A\u8BDD\u5DF2\u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u88AB\u5916\u90E8\u5220\u9664\u6216\u91CD\u5EFA\uFF09\uFF0C\u65E0\u6CD5\u6062\u590D" };
|
|
1577
|
+
}
|
|
1578
|
+
let verified = false;
|
|
1579
|
+
const store = await readTrashStore();
|
|
1580
|
+
const entry = store.items.find((t) => String(t.sessionId) === sid);
|
|
1581
|
+
let originalPath = entry && typeof entry.originalPath === "string" ? entry.originalPath : null;
|
|
1582
|
+
if (!originalPath && header) {
|
|
1583
|
+
const loc = persistence.locate(header);
|
|
1584
|
+
if (loc && typeof loc.path === "string") originalPath = loc.path;
|
|
1585
|
+
}
|
|
1586
|
+
if (originalPath) {
|
|
1587
|
+
const existsOnDisk = await stat2(originalPath).then(() => true).catch(() => false);
|
|
1588
|
+
if (!existsOnDisk) {
|
|
1589
|
+
return { ok: false, status: 409, code: "DSM_SESSION_LOG_MISSING", message: "\u56DE\u6536\u7AD9\u7D22\u5F15\u4ECD\u8BB0\u5F55\u8BE5\u4F1A\u8BDD\uFF0C\u4F46\u5176\u65E5\u5FD7\u6587\u4EF6\u5DF2\u6D88\u5931\uFF08\u53EF\u80FD\u88AB\u5916\u90E8\u79FB\u52A8\u6216\u5220\u9664\uFF09" };
|
|
1590
|
+
}
|
|
1591
|
+
verified = true;
|
|
1592
|
+
}
|
|
1593
|
+
let workspaceGone = false;
|
|
1594
|
+
const cwd = header && header.cwd || entry && entry.cwd || null;
|
|
1595
|
+
if (cwd) {
|
|
1596
|
+
try {
|
|
1597
|
+
workspaceGone = !w.list().some((ent) => ent.path === cwd);
|
|
1598
|
+
} catch (e) {
|
|
1599
|
+
workspaceGone = false;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { ok: true, workspaceGone, verified };
|
|
1603
|
+
}
|
|
1099
1604
|
async function restoreFromTrash(sid) {
|
|
1100
1605
|
requireSessionId(sid);
|
|
1606
|
+
requireCapability(capabilities, "restoreIndexedSession");
|
|
1607
|
+
let outcome = null;
|
|
1101
1608
|
await mutateTrash(async (store) => {
|
|
1102
1609
|
const entry = store.items.find((t) => String(t.sessionId) === sid);
|
|
1103
1610
|
if (!entry) {
|
|
1104
|
-
|
|
1611
|
+
if (store.purgedSessionIds.map(String).includes(sid)) {
|
|
1612
|
+
const error2 = new Error("\u8BE5\u4F1A\u8BDD\u5DF2\u5F7B\u5E95\u5220\u9664\uFF0C\u65E0\u6CD5\u4ECE\u56DE\u6536\u7AD9\u6062\u590D");
|
|
1613
|
+
error2.status = 410;
|
|
1614
|
+
error2.code = "DSM_SESSION_PURGED";
|
|
1615
|
+
throw error2;
|
|
1616
|
+
}
|
|
1617
|
+
const error = new Error("\u56DE\u6536\u7AD9\u4E2D\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\uFF08\u53EF\u80FD\u5DF2\u6062\u590D\u8FC7\uFF09");
|
|
1105
1618
|
error.status = 404;
|
|
1619
|
+
error.code = "DSM_TRASH_NOT_FOUND";
|
|
1620
|
+
throw error;
|
|
1621
|
+
}
|
|
1622
|
+
const verification = await verifyTrashRestore(sid);
|
|
1623
|
+
if (!verification.ok) {
|
|
1624
|
+
const error = new Error(verification.message);
|
|
1625
|
+
error.status = verification.status;
|
|
1626
|
+
error.code = verification.code;
|
|
1106
1627
|
throw error;
|
|
1107
1628
|
}
|
|
1108
1629
|
if (entry.wasArchived === false) await restoreOne(sid);
|
|
1109
1630
|
store.items = store.items.filter((t) => String(t.sessionId) !== sid);
|
|
1110
1631
|
store.purgedSessionIds = store.purgedSessionIds.filter((id) => id !== sid);
|
|
1632
|
+
outcome = { ok: true, restored: true, workspaceGone: verification.workspaceGone, verified: verification.verified };
|
|
1111
1633
|
});
|
|
1112
|
-
return { ok: true, restored: true };
|
|
1634
|
+
return outcome || { ok: true, restored: true };
|
|
1113
1635
|
}
|
|
1114
1636
|
async function purgeFromTrash(sid) {
|
|
1115
1637
|
requireSessionId(sid);
|
|
@@ -1136,7 +1658,7 @@ function apply(ctx) {
|
|
|
1136
1658
|
error.status = 409;
|
|
1137
1659
|
throw error;
|
|
1138
1660
|
}
|
|
1139
|
-
const targetOwnsSession =
|
|
1661
|
+
const targetOwnsSession = pathOwnsSession(target, sid);
|
|
1140
1662
|
if (target && !targetOwnsSession) {
|
|
1141
1663
|
const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
1142
1664
|
error.status = 409;
|
|
@@ -1158,9 +1680,11 @@ function apply(ctx) {
|
|
|
1158
1680
|
error.status = 409;
|
|
1159
1681
|
throw error;
|
|
1160
1682
|
}
|
|
1161
|
-
if (target) {
|
|
1683
|
+
if (target && persistence.kind === "session-handle" && locatedHeader) {
|
|
1684
|
+
await purgeSessionArtifacts(sp, sid, locatedHeader);
|
|
1685
|
+
} else if (target) {
|
|
1162
1686
|
try {
|
|
1163
|
-
await
|
|
1687
|
+
await unlink2(target);
|
|
1164
1688
|
} catch (e) {
|
|
1165
1689
|
if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1166
1690
|
}
|
|
@@ -1230,8 +1754,8 @@ function apply(ctx) {
|
|
|
1230
1754
|
async function moveTargetWorkspace(rawPath) {
|
|
1231
1755
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
1232
1756
|
let p = String(rawPath).trim();
|
|
1233
|
-
if (p.startsWith("~/")) p =
|
|
1234
|
-
if (!isAbsolute(p)) p =
|
|
1757
|
+
if (p.startsWith("~/")) p = join6(homedir3(), p.slice(2));
|
|
1758
|
+
if (!isAbsolute(p)) p = join6(homedir3(), p);
|
|
1235
1759
|
let canonical = null;
|
|
1236
1760
|
try {
|
|
1237
1761
|
canonical = await realpath(p);
|
|
@@ -1239,10 +1763,10 @@ function apply(ctx) {
|
|
|
1239
1763
|
canonical = null;
|
|
1240
1764
|
}
|
|
1241
1765
|
if (canonical === null) {
|
|
1242
|
-
await
|
|
1766
|
+
await mkdir5(p, { recursive: true });
|
|
1243
1767
|
canonical = await realpath(p);
|
|
1244
1768
|
}
|
|
1245
|
-
return { canonical, entity: await w.create(canonical,
|
|
1769
|
+
return { canonical, entity: await w.create(canonical, basename2(canonical) || "workspace") };
|
|
1246
1770
|
}
|
|
1247
1771
|
async function moveOne(sid, targetPath) {
|
|
1248
1772
|
requireCapability(capabilities, "move");
|
|
@@ -1280,15 +1804,15 @@ function apply(ctx) {
|
|
|
1280
1804
|
const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
|
|
1281
1805
|
let destinationInstalled = false;
|
|
1282
1806
|
try {
|
|
1283
|
-
await
|
|
1807
|
+
await mkdir5(dirname2(newPath), { recursive: true });
|
|
1284
1808
|
try {
|
|
1285
|
-
await
|
|
1809
|
+
await stat2(newPath);
|
|
1286
1810
|
throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1287
1811
|
} catch (e) {
|
|
1288
1812
|
if (e && e.code !== "ENOENT") throw e;
|
|
1289
1813
|
}
|
|
1290
|
-
await
|
|
1291
|
-
const original = await
|
|
1814
|
+
await rename5(oldPath, backupPath);
|
|
1815
|
+
const original = await readFile3(backupPath);
|
|
1292
1816
|
const originalFrames = scanZstdFrames(original).frames;
|
|
1293
1817
|
if (originalFrames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
1294
1818
|
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
@@ -1297,23 +1821,23 @@ function apply(ctx) {
|
|
|
1297
1821
|
const originalTail = original.subarray(originalFrames[0].end);
|
|
1298
1822
|
const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
|
|
1299
1823
|
if (!originalTail.equals(rewrittenTail)) throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1300
|
-
await
|
|
1301
|
-
await
|
|
1824
|
+
await writeFile5(stagedPath, rewritten, { mode: 384 });
|
|
1825
|
+
await rename5(stagedPath, newPath);
|
|
1302
1826
|
destinationInstalled = true;
|
|
1303
|
-
await
|
|
1827
|
+
await unlink2(backupPath);
|
|
1304
1828
|
} catch (e) {
|
|
1305
1829
|
try {
|
|
1306
|
-
await
|
|
1830
|
+
await unlink2(stagedPath);
|
|
1307
1831
|
} catch (_) {
|
|
1308
1832
|
}
|
|
1309
1833
|
if (destinationInstalled) {
|
|
1310
1834
|
try {
|
|
1311
|
-
await
|
|
1835
|
+
await unlink2(newPath);
|
|
1312
1836
|
} catch (_) {
|
|
1313
1837
|
}
|
|
1314
1838
|
}
|
|
1315
1839
|
try {
|
|
1316
|
-
await
|
|
1840
|
+
await rename5(backupPath, oldPath);
|
|
1317
1841
|
} catch (_) {
|
|
1318
1842
|
}
|
|
1319
1843
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -1337,7 +1861,21 @@ function apply(ctx) {
|
|
|
1337
1861
|
}
|
|
1338
1862
|
return null;
|
|
1339
1863
|
};
|
|
1340
|
-
if (
|
|
1864
|
+
if (persistence.kind === "session-handle") {
|
|
1865
|
+
const stat1 = await persistence.statSession(sid);
|
|
1866
|
+
if (stat1 && stat1.revision) {
|
|
1867
|
+
const stat22 = await persistence.statSession(sid);
|
|
1868
|
+
if (stat22 && stat22.revision !== stat1.revision) {
|
|
1869
|
+
throw new Error("\u8BE5\u4F1A\u8BDD\u5728\u79FB\u52A8\u51C6\u5907\u671F\u95F4\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
|
|
1874
|
+
} catch (e) {
|
|
1875
|
+
if (e && e.status) throw e;
|
|
1876
|
+
throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1877
|
+
}
|
|
1878
|
+
} else if (isOpen) {
|
|
1341
1879
|
if (!await relocateLog(meta, newHeader)) throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u65E5\u5FD7\u5DF2\u8FC1\u79FB\u5230\u76EE\u6807\u5DE5\u4F5C\u533A");
|
|
1342
1880
|
try {
|
|
1343
1881
|
const st = sp.states && sp.states.get && sp.states.get(sid);
|
|
@@ -1359,7 +1897,7 @@ function apply(ctx) {
|
|
|
1359
1897
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
1360
1898
|
if (backupPath) {
|
|
1361
1899
|
try {
|
|
1362
|
-
await
|
|
1900
|
+
await rename5(oldPath, backupPath);
|
|
1363
1901
|
} catch (e) {
|
|
1364
1902
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1365
1903
|
}
|
|
@@ -1367,7 +1905,7 @@ function apply(ctx) {
|
|
|
1367
1905
|
const restore = async () => {
|
|
1368
1906
|
if (backupPath) {
|
|
1369
1907
|
try {
|
|
1370
|
-
await
|
|
1908
|
+
await rename5(backupPath, oldPath);
|
|
1371
1909
|
} catch (_) {
|
|
1372
1910
|
}
|
|
1373
1911
|
}
|
|
@@ -1381,7 +1919,7 @@ function apply(ctx) {
|
|
|
1381
1919
|
}
|
|
1382
1920
|
if (backupPath) {
|
|
1383
1921
|
try {
|
|
1384
|
-
await
|
|
1922
|
+
await unlink2(backupPath);
|
|
1385
1923
|
} catch (e) {
|
|
1386
1924
|
}
|
|
1387
1925
|
}
|
|
@@ -1476,7 +2014,7 @@ function apply(ctx) {
|
|
|
1476
2014
|
}
|
|
1477
2015
|
return out;
|
|
1478
2016
|
}
|
|
1479
|
-
async function
|
|
2017
|
+
async function allSessionItemsDetailed(opts = {}) {
|
|
1480
2018
|
let entries = [];
|
|
1481
2019
|
let headersOk = false;
|
|
1482
2020
|
try {
|
|
@@ -1486,6 +2024,7 @@ function apply(ctx) {
|
|
|
1486
2024
|
} catch (e) {
|
|
1487
2025
|
entries = [];
|
|
1488
2026
|
}
|
|
2027
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1489
2028
|
let live = ctx.get("sessions");
|
|
1490
2029
|
const ids = entries.map((entry) => entry.id);
|
|
1491
2030
|
if (live) {
|
|
@@ -1500,7 +2039,11 @@ function apply(ctx) {
|
|
|
1500
2039
|
let hiddenIds = /* @__PURE__ */ new Set();
|
|
1501
2040
|
try {
|
|
1502
2041
|
const store = await readTrashStore();
|
|
1503
|
-
|
|
2042
|
+
const present = new Set(ids);
|
|
2043
|
+
hiddenIds = /* @__PURE__ */ new Set([
|
|
2044
|
+
...store.items.map((t) => String(t.sessionId)),
|
|
2045
|
+
...store.purgedSessionIds.map(String).filter((id) => !present.has(id))
|
|
2046
|
+
]);
|
|
1504
2047
|
} catch (e) {
|
|
1505
2048
|
}
|
|
1506
2049
|
const visibleIds = ids.filter((id) => !hiddenIds.has(id));
|
|
@@ -1513,8 +2056,11 @@ function apply(ctx) {
|
|
|
1513
2056
|
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
1514
2057
|
const items = [];
|
|
1515
2058
|
const usage = await collectUsage(entries);
|
|
1516
|
-
const statsById = new Map(visibleIds.map((id) => [
|
|
1517
|
-
|
|
2059
|
+
const statsById = new Map(visibleIds.map((id) => [
|
|
2060
|
+
id,
|
|
2061
|
+
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2062
|
+
]));
|
|
2063
|
+
const { missing } = metaCache.partition(visibleIds, statsById);
|
|
1518
2064
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1519
2065
|
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
1520
2066
|
const stillMissing = missing.filter((id) => !persisted.has(id));
|
|
@@ -1525,11 +2071,15 @@ function apply(ctx) {
|
|
|
1525
2071
|
};
|
|
1526
2072
|
const CHUNK = 6;
|
|
1527
2073
|
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
1528
|
-
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) =>
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2074
|
+
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => {
|
|
2075
|
+
const entry = entryById.get(id);
|
|
2076
|
+
return resolveOne(id, usage, {
|
|
2077
|
+
exposeUsage: !!(opts && opts.usage),
|
|
2078
|
+
listHeader: entry ? entry.header : null,
|
|
2079
|
+
preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
|
|
2080
|
+
collectDecoded
|
|
2081
|
+
});
|
|
2082
|
+
}));
|
|
1533
2083
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
1534
2084
|
}
|
|
1535
2085
|
persistDecoded(decoded, statsById);
|
|
@@ -1540,7 +2090,10 @@ function apply(ctx) {
|
|
|
1540
2090
|
}
|
|
1541
2091
|
for (const it of items) it.starred = starredSet.has(String(it.sessionId));
|
|
1542
2092
|
if (headersOk) await gcStars(ids);
|
|
1543
|
-
return items;
|
|
2093
|
+
return { items, usage };
|
|
2094
|
+
}
|
|
2095
|
+
async function allSessionItems(opts = {}) {
|
|
2096
|
+
return (await allSessionItemsDetailed(opts)).items;
|
|
1544
2097
|
}
|
|
1545
2098
|
async function buildStorage(opts = {}) {
|
|
1546
2099
|
const items = await allSessionItems({ usage: true });
|
|
@@ -1556,7 +2109,15 @@ function apply(ctx) {
|
|
|
1556
2109
|
if (!(opts && opts.force) && autoArchive.isFresh(store, now)) {
|
|
1557
2110
|
return { ok: true, skipped: "throttled", archived: 0, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount };
|
|
1558
2111
|
}
|
|
1559
|
-
const items = await
|
|
2112
|
+
const { items, usage } = await allSessionItemsDetailed({ usage: true });
|
|
2113
|
+
if (!usage.hasActivityData) {
|
|
2114
|
+
return {
|
|
2115
|
+
ok: true,
|
|
2116
|
+
skipped: "no-activity-data",
|
|
2117
|
+
archived: 0,
|
|
2118
|
+
note: "\u5F53\u524D DSH \u7248\u672C\u672A\u63D0\u4F9B\u53EF\u9760\u7684\u6700\u540E\u6D3B\u8DC3\u65F6\u95F4\uFF0C\u81EA\u52A8\u5F52\u6863\u5DF2\u8DF3\u8FC7\uFF1B\u4E0D\u4F1A\u57FA\u4E8E\u731C\u6D4B\u5F52\u6863\u4EFB\u4F55\u4F1A\u8BDD\u3002"
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
1560
2121
|
const candidates = pickInactiveCandidates(items, {
|
|
1561
2122
|
inactiveDays: days,
|
|
1562
2123
|
skipStarred: store.settings.skipStarred,
|
|
@@ -1595,9 +2156,14 @@ function apply(ctx) {
|
|
|
1595
2156
|
} catch (e) {
|
|
1596
2157
|
}
|
|
1597
2158
|
const store = await readTrashStore();
|
|
2159
|
+
const present = new Set(ids);
|
|
2160
|
+
const activeTombstones = store.purgedSessionIds.map(String).filter((id) => !present.has(id));
|
|
1598
2161
|
if (ids.length) {
|
|
1599
2162
|
const usage = await collectUsage(entries);
|
|
1600
|
-
const statsById = new Map(ids.map((id) => [
|
|
2163
|
+
const statsById = new Map(ids.map((id) => [
|
|
2164
|
+
id,
|
|
2165
|
+
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2166
|
+
]));
|
|
1601
2167
|
const { cached, missing } = metaCache.partition(ids, statsById);
|
|
1602
2168
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1603
2169
|
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
@@ -1610,8 +2176,13 @@ function apply(ctx) {
|
|
|
1610
2176
|
for (const id of ids) {
|
|
1611
2177
|
let meta = cached.get(id) || persisted.get(id) || null;
|
|
1612
2178
|
if (!meta) {
|
|
2179
|
+
const entry = entries.find((e) => e.id === id);
|
|
1613
2180
|
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
|
|
1614
2181
|
const next = metaFromSnapshot(snapshot);
|
|
2182
|
+
if (entry && entry.header) {
|
|
2183
|
+
if (!next.cwd && typeof entry.header.cwd === "string") next.cwd = entry.header.cwd;
|
|
2184
|
+
if (!next.createdAt && entry.header.createdAt != null) next.createdAt = entry.header.createdAt;
|
|
2185
|
+
}
|
|
1615
2186
|
metaCache.set(id, statsById.get(id), next);
|
|
1616
2187
|
if (statsById.get(id)) collectDecoded(id, next);
|
|
1617
2188
|
meta = next;
|
|
@@ -1623,38 +2194,14 @@ function apply(ctx) {
|
|
|
1623
2194
|
return {
|
|
1624
2195
|
titles: Object.fromEntries(authorityTitleCache),
|
|
1625
2196
|
trashedSessionIds: store.items.map((item) => String(item.sessionId)),
|
|
1626
|
-
purgedSessionIds:
|
|
2197
|
+
purgedSessionIds: activeTombstones
|
|
1627
2198
|
};
|
|
1628
2199
|
}
|
|
1629
|
-
async function buildDetails(sid) {
|
|
2200
|
+
async function buildDetails(sid, signal) {
|
|
1630
2201
|
const sessions = ctx.get("sessions");
|
|
1631
2202
|
const live = sessions && sessions.get(sid);
|
|
1632
2203
|
let meta = null;
|
|
1633
|
-
let
|
|
1634
|
-
if (live !== void 0) {
|
|
1635
|
-
meta = live && live.header || null;
|
|
1636
|
-
try {
|
|
1637
|
-
events = Array.isArray(live.events) ? [...live.events] : [];
|
|
1638
|
-
} catch (e) {
|
|
1639
|
-
events = [];
|
|
1640
|
-
}
|
|
1641
|
-
} else {
|
|
1642
|
-
const r = await persistence.readSession(sid, 0);
|
|
1643
|
-
if (!r || !r.meta) throw new Error("\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\u7684\u8BB0\u5F55\uFF08\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF09");
|
|
1644
|
-
meta = r.meta;
|
|
1645
|
-
events = Array.isArray(r.events) ? r.events : [];
|
|
1646
|
-
}
|
|
1647
|
-
let sizeBytes = null;
|
|
1648
|
-
try {
|
|
1649
|
-
const loc = persistence.locate(meta);
|
|
1650
|
-
if (loc && typeof loc.path === "string" && loc.path) {
|
|
1651
|
-
const st = await stat(loc.path);
|
|
1652
|
-
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
1653
|
-
}
|
|
1654
|
-
} catch (e) {
|
|
1655
|
-
sizeBytes = null;
|
|
1656
|
-
}
|
|
1657
|
-
let lastTime = typeof meta.createdAt === "number" ? meta.createdAt : 0;
|
|
2204
|
+
let lastTime = 0;
|
|
1658
2205
|
const fileSet = /* @__PURE__ */ new Map();
|
|
1659
2206
|
const stats = {
|
|
1660
2207
|
turns: 0,
|
|
@@ -1668,7 +2215,7 @@ function apply(ctx) {
|
|
|
1668
2215
|
};
|
|
1669
2216
|
const turnSeen = /* @__PURE__ */ new Set();
|
|
1670
2217
|
const stepSeen = /* @__PURE__ */ new Set();
|
|
1671
|
-
|
|
2218
|
+
const absorb = (ev) => {
|
|
1672
2219
|
if (ev && typeof ev.time === "number" && ev.time > lastTime) lastTime = ev.time;
|
|
1673
2220
|
const d = ev && ev.data && typeof ev.data === "object" ? ev.data : {};
|
|
1674
2221
|
const type = ev && ev.type;
|
|
@@ -1715,12 +2262,44 @@ function apply(ctx) {
|
|
|
1715
2262
|
break;
|
|
1716
2263
|
}
|
|
1717
2264
|
}
|
|
2265
|
+
};
|
|
2266
|
+
if (live !== void 0) {
|
|
2267
|
+
meta = live && live.header || null;
|
|
2268
|
+
try {
|
|
2269
|
+
(Array.isArray(live.events) ? [...live.events] : []).forEach(absorb);
|
|
2270
|
+
} catch (e) {
|
|
2271
|
+
}
|
|
2272
|
+
} else {
|
|
2273
|
+
const summary = await persistence.inspectSession(sid, { signal, onEvents: (batch) => {
|
|
2274
|
+
for (const ev of batch) absorb(ev);
|
|
2275
|
+
} });
|
|
2276
|
+
if (!summary || !summary.meta) throw new Error("\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\u7684\u8BB0\u5F55\uFF08\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF09");
|
|
2277
|
+
meta = summary.meta;
|
|
1718
2278
|
}
|
|
1719
2279
|
stats.turns = turnSeen.size;
|
|
1720
2280
|
stats.steps = stepSeen.size;
|
|
2281
|
+
let sizeBytes = null;
|
|
2282
|
+
if (live === void 0) {
|
|
2283
|
+
try {
|
|
2284
|
+
const stat3 = await persistence.statSession(sid);
|
|
2285
|
+
if (stat3 && Number.isFinite(stat3.sizeBytes)) sizeBytes = stat3.sizeBytes;
|
|
2286
|
+
} catch (e) {
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (sizeBytes === null) {
|
|
2290
|
+
try {
|
|
2291
|
+
const loc = persistence.locate(meta);
|
|
2292
|
+
if (loc && typeof loc.path === "string" && loc.path) {
|
|
2293
|
+
const st = await stat2(loc.path);
|
|
2294
|
+
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
2295
|
+
}
|
|
2296
|
+
} catch (e) {
|
|
2297
|
+
sizeBytes = null;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
1721
2300
|
if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
|
|
1722
2301
|
const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
|
|
1723
|
-
const exists = await Promise.all(fileEntries.map(([p]) =>
|
|
2302
|
+
const exists = await Promise.all(fileEntries.map(([p]) => stat2(p).then(() => true).catch(() => false)));
|
|
1724
2303
|
const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
|
|
1725
2304
|
const lineage = {
|
|
1726
2305
|
parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
|
|
@@ -1756,7 +2335,7 @@ function apply(ctx) {
|
|
|
1756
2335
|
sessionId: sid,
|
|
1757
2336
|
sizeBytes,
|
|
1758
2337
|
createdAt: meta && typeof meta.createdAt === "number" ? meta.createdAt : null,
|
|
1759
|
-
updatedAt: lastTime || null,
|
|
2338
|
+
updatedAt: Math.max(lastTime || 0, meta && typeof meta.createdAt === "number" ? meta.createdAt : 0) || null,
|
|
1760
2339
|
files,
|
|
1761
2340
|
stats,
|
|
1762
2341
|
lineage
|
|
@@ -1789,7 +2368,13 @@ function apply(ctx) {
|
|
|
1789
2368
|
} catch (e) {
|
|
1790
2369
|
}
|
|
1791
2370
|
const trashStore = await readTrashStore();
|
|
1792
|
-
const
|
|
2371
|
+
const present = /* @__PURE__ */ new Set([
|
|
2372
|
+
...materialized,
|
|
2373
|
+
...ids.map(String),
|
|
2374
|
+
...live && typeof live.list === "function" ? live.list().map((s) => String(s.id)) : []
|
|
2375
|
+
]);
|
|
2376
|
+
const tombstones = trashStore.purgedSessionIds.map(String).filter((id) => !present.has(id));
|
|
2377
|
+
const hidden = /* @__PURE__ */ new Set([...trashStore.items.map((item) => String(item.sessionId)), ...tombstones]);
|
|
1793
2378
|
const idStrs = ids.map(String).filter((id) => !hidden.has(id) && (materialized.has(id) || live && live.get(id)));
|
|
1794
2379
|
wsByPath = {};
|
|
1795
2380
|
try {
|
|
@@ -1922,7 +2507,7 @@ function apply(ctx) {
|
|
|
1922
2507
|
if (typeof item.originalPath !== "string" || !item.originalPath) {
|
|
1923
2508
|
return { sessionId: item.sessionId, status: "unverified", originalPath: null };
|
|
1924
2509
|
}
|
|
1925
|
-
const exists = await
|
|
2510
|
+
const exists = await stat2(item.originalPath).then(() => true).catch(() => false);
|
|
1926
2511
|
return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
|
|
1927
2512
|
}));
|
|
1928
2513
|
json(res, {
|
|
@@ -2026,6 +2611,7 @@ function apply(ctx) {
|
|
|
2026
2611
|
}
|
|
2027
2612
|
}
|
|
2028
2613
|
}));
|
|
2614
|
+
const exportLimiter = createLimiter(2);
|
|
2029
2615
|
disposers.push(ctx.webServer.register({
|
|
2030
2616
|
kind: "exact",
|
|
2031
2617
|
path: "/archived-sessions/export-md",
|
|
@@ -2034,13 +2620,23 @@ function apply(ctx) {
|
|
|
2034
2620
|
const url = new URL(req.url, "http://localhost");
|
|
2035
2621
|
const sid = url.searchParams.get("sessionId");
|
|
2036
2622
|
requireSessionId(sid);
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2623
|
+
const ac = new AbortController();
|
|
2624
|
+
res.on("close", () => {
|
|
2625
|
+
if (!res.writableEnded) ac.abort();
|
|
2626
|
+
});
|
|
2627
|
+
const md = await exportLimiter(async () => {
|
|
2628
|
+
const builder = createSessionMarkdownBuilder({ id: sid });
|
|
2629
|
+
const summary = await persistence.inspectSession(sid, {
|
|
2630
|
+
signal: ac.signal,
|
|
2631
|
+
onEvents: (batch) => builder.addEvents(batch)
|
|
2632
|
+
});
|
|
2633
|
+
if (!summary || !summary.meta) {
|
|
2634
|
+
const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
|
|
2635
|
+
error.status = 404;
|
|
2636
|
+
throw error;
|
|
2637
|
+
}
|
|
2638
|
+
return builder.finish({ ...summary.meta, id: sid });
|
|
2639
|
+
});
|
|
2044
2640
|
res.writeHead(200, {
|
|
2045
2641
|
"content-type": "text/markdown; charset=utf-8",
|
|
2046
2642
|
"content-disposition": `attachment; filename="dsh-session-${sid}.md"`,
|
|
@@ -2140,7 +2736,11 @@ function apply(ctx) {
|
|
|
2140
2736
|
const body = await readJsonBody(req);
|
|
2141
2737
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
2142
2738
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
2143
|
-
|
|
2739
|
+
const ac = new AbortController();
|
|
2740
|
+
res.on("close", () => {
|
|
2741
|
+
if (!res.writableEnded) ac.abort();
|
|
2742
|
+
});
|
|
2743
|
+
json(res, await buildDetails(sid, ac.signal));
|
|
2144
2744
|
} catch (e) {
|
|
2145
2745
|
json(res, { error: String(e && e.message || e) }, e && e.status ? e.status : 500);
|
|
2146
2746
|
}
|