dsh-sessions-manager 3.3.0 → 3.4.1
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 +21 -21
- package/README.md +21 -21
- package/assets/screenshot-session-autoarch.png +0 -0
- package/assets/screenshot-session-details.png +0 -0
- package/assets/screenshot-session-settings.png +0 -0
- package/assets/screenshot-session-starred.png +0 -0
- package/assets/screenshot-session-storage.png +0 -0
- package/assets/screenshot-session-trash.png +0 -0
- package/lib/client.js +192 -8
- package/lib/client.js.map +2 -2
- package/lib/index.js +506 -80
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/auto-archive.js +168 -0
- package/src/client/index.jsx +224 -9
- package/src/index.js +311 -58
- package/src/session-meta-cache.js +117 -0
- package/src/storage-stats.js +94 -0
- package/assets/screenshot-session-settingsmenu.png +0 -0
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir as
|
|
3
|
-
import { basename, dirname, isAbsolute, join as
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { homedir as
|
|
2
|
+
import { mkdir as mkdir3, realpath, rename as rename3, stat, unlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, join as join3 } from "node:path";
|
|
4
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
5
|
+
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
|
|
7
7
|
// src/zstd-frame.js
|
|
8
8
|
import zlib from "node:zlib";
|
|
@@ -250,17 +250,268 @@ function createStarIndex(options = {}) {
|
|
|
250
250
|
return { read, write, mutate, setStarred, removeIds, indexPath, dir };
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// src/storage-stats.js
|
|
254
|
+
var UNGROUPED_KEY = "__ungrouped__";
|
|
255
|
+
function isFiniteSize(value) {
|
|
256
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
257
|
+
}
|
|
258
|
+
function aggregateStorage(items, options = {}) {
|
|
259
|
+
const topN = Number.isInteger(options.topN) && options.topN > 0 ? options.topN : 10;
|
|
260
|
+
const list = Array.isArray(items) ? items : [];
|
|
261
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
262
|
+
const sized = [];
|
|
263
|
+
let totalBytes = 0;
|
|
264
|
+
let unknownSessions = 0;
|
|
265
|
+
let counted = 0;
|
|
266
|
+
for (const item of list) {
|
|
267
|
+
if (!item || item.sessionId == null) continue;
|
|
268
|
+
counted++;
|
|
269
|
+
const id = String(item.sessionId);
|
|
270
|
+
const path = item.workspacePath ? String(item.workspacePath) : null;
|
|
271
|
+
const key = path || UNGROUPED_KEY;
|
|
272
|
+
let bucket = buckets.get(key);
|
|
273
|
+
if (!bucket) {
|
|
274
|
+
bucket = { key, path, title: item.workspaceTitle ? String(item.workspaceTitle) : null, bytes: 0, sessions: 0 };
|
|
275
|
+
buckets.set(key, bucket);
|
|
276
|
+
}
|
|
277
|
+
bucket.sessions++;
|
|
278
|
+
if (isFiniteSize(item.sizeBytes)) {
|
|
279
|
+
bucket.bytes += item.sizeBytes;
|
|
280
|
+
totalBytes += item.sizeBytes;
|
|
281
|
+
sized.push({
|
|
282
|
+
sessionId: id,
|
|
283
|
+
title: item.title || null,
|
|
284
|
+
workspacePath: path,
|
|
285
|
+
workspaceTitle: bucket.title,
|
|
286
|
+
sizeBytes: item.sizeBytes
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
unknownSessions++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const workspaces = [...buckets.values()].sort((a, b) => b.bytes - a.bytes || b.sessions - a.sessions || a.key.localeCompare(b.key)).map((bucket) => ({ ...bucket, share: totalBytes > 0 ? bucket.bytes / totalBytes : 0 }));
|
|
293
|
+
const top = sized.sort((a, b) => b.sizeBytes - a.sizeBytes || a.sessionId.localeCompare(b.sessionId)).slice(0, topN);
|
|
294
|
+
return {
|
|
295
|
+
totalBytes,
|
|
296
|
+
sessionCount: counted,
|
|
297
|
+
sizedSessions: sized.length,
|
|
298
|
+
unknownSessions,
|
|
299
|
+
workspaces,
|
|
300
|
+
top
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/auto-archive.js
|
|
305
|
+
import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
306
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
307
|
+
import { homedir as homedir2 } from "node:os";
|
|
308
|
+
import { join as join2 } from "node:path";
|
|
309
|
+
var AUTO_ARCHIVE_SCHEMA_VERSION = 4;
|
|
310
|
+
var INACTIVE_DAY_OPTIONS = Object.freeze([0, 30, 60, 90]);
|
|
311
|
+
var DAY_MS = 864e5;
|
|
312
|
+
var RUN_INTERVAL_MS = DAY_MS;
|
|
313
|
+
var DEFAULT_DIR = join2(homedir2(), ".dsh", "sessions-manager");
|
|
314
|
+
function normalizeAutoArchiveStore(raw) {
|
|
315
|
+
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
316
|
+
const settings = source.settings && typeof source.settings === "object" ? source.settings : {};
|
|
317
|
+
const inactiveDays = INACTIVE_DAY_OPTIONS.includes(settings.inactiveDays) ? settings.inactiveDays : 0;
|
|
318
|
+
return {
|
|
319
|
+
schemaVersion: AUTO_ARCHIVE_SCHEMA_VERSION,
|
|
320
|
+
settings: {
|
|
321
|
+
inactiveDays,
|
|
322
|
+
// Starred sessions are an explicit "keep" mark, so they are skipped
|
|
323
|
+
// unless the user opts out.
|
|
324
|
+
skipStarred: settings.skipStarred !== false
|
|
325
|
+
},
|
|
326
|
+
lastRunAt: Number.isFinite(source.lastRunAt) ? source.lastRunAt : null,
|
|
327
|
+
lastArchivedCount: Number.isInteger(source.lastArchivedCount) && source.lastArchivedCount >= 0 ? source.lastArchivedCount : 0
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function pickInactiveCandidates(items, options = {}) {
|
|
331
|
+
const days = options.inactiveDays;
|
|
332
|
+
if (!INACTIVE_DAY_OPTIONS.includes(days) || days === 0) return [];
|
|
333
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
334
|
+
const cutoff = now - days * DAY_MS;
|
|
335
|
+
const skipStarred = options.skipStarred !== false;
|
|
336
|
+
const activeId = options.activeSessionId != null ? String(options.activeSessionId) : null;
|
|
337
|
+
const list = Array.isArray(items) ? items : [];
|
|
338
|
+
const out = [];
|
|
339
|
+
const seen = /* @__PURE__ */ new Set();
|
|
340
|
+
for (const item of list) {
|
|
341
|
+
if (!item || item.sessionId == null) continue;
|
|
342
|
+
const id = String(item.sessionId);
|
|
343
|
+
if (seen.has(id)) continue;
|
|
344
|
+
if (item.archived) continue;
|
|
345
|
+
if (skipStarred && item.starred) continue;
|
|
346
|
+
if (activeId !== null && id === activeId) continue;
|
|
347
|
+
const updatedAt = Number(item.updatedAt);
|
|
348
|
+
if (!Number.isFinite(updatedAt) || updatedAt <= 0) continue;
|
|
349
|
+
if (updatedAt < cutoff) {
|
|
350
|
+
seen.add(id);
|
|
351
|
+
out.push(id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
function createAutoArchiveStore(options = {}) {
|
|
357
|
+
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR;
|
|
358
|
+
const indexPath = options.indexPath || join2(dir, "auto-archive.json");
|
|
359
|
+
let mutation = Promise.resolve();
|
|
360
|
+
async function read() {
|
|
361
|
+
try {
|
|
362
|
+
return normalizeAutoArchiveStore(JSON.parse(readFileSync3(indexPath, "utf8")));
|
|
363
|
+
} catch {
|
|
364
|
+
return normalizeAutoArchiveStore(null);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function write(store) {
|
|
368
|
+
await mkdir2(dir, { recursive: true });
|
|
369
|
+
const tmp = join2(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`);
|
|
370
|
+
await writeFile2(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
371
|
+
await rename2(tmp, indexPath);
|
|
372
|
+
}
|
|
373
|
+
function mutate(mutator) {
|
|
374
|
+
const operation = mutation.then(async () => {
|
|
375
|
+
const store = await read();
|
|
376
|
+
const result = await mutator(store);
|
|
377
|
+
await write(store);
|
|
378
|
+
return result;
|
|
379
|
+
});
|
|
380
|
+
mutation = operation.catch(() => {
|
|
381
|
+
});
|
|
382
|
+
return operation;
|
|
383
|
+
}
|
|
384
|
+
function update(patch = {}) {
|
|
385
|
+
return mutate((store) => {
|
|
386
|
+
if (Object.prototype.hasOwnProperty.call(patch, "inactiveDays")) {
|
|
387
|
+
const days = Number(patch.inactiveDays);
|
|
388
|
+
if (!INACTIVE_DAY_OPTIONS.includes(days)) {
|
|
389
|
+
const error = new Error(`inactiveDays \u4EC5\u652F\u6301 ${INACTIVE_DAY_OPTIONS.join("\u3001")}`);
|
|
390
|
+
error.status = 400;
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
store.settings.inactiveDays = days;
|
|
394
|
+
}
|
|
395
|
+
if (Object.prototype.hasOwnProperty.call(patch, "skipStarred")) {
|
|
396
|
+
store.settings.skipStarred = !!patch.skipStarred;
|
|
397
|
+
}
|
|
398
|
+
return store.settings;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function recordRun(count, at = Date.now()) {
|
|
402
|
+
return mutate((store) => {
|
|
403
|
+
store.lastRunAt = at;
|
|
404
|
+
store.lastArchivedCount = Number.isInteger(count) && count >= 0 ? count : 0;
|
|
405
|
+
return store;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function isFresh2(store, now = Date.now()) {
|
|
409
|
+
return Number.isFinite(store && store.lastRunAt) && now - store.lastRunAt < RUN_INTERVAL_MS;
|
|
410
|
+
}
|
|
411
|
+
return { read, write, mutate, update, recordRun, isFresh: isFresh2, indexPath, dir };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/session-meta-cache.js
|
|
415
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
416
|
+
var DEFAULT_MAX = 4e3;
|
|
417
|
+
function fingerprintOf(stat2) {
|
|
418
|
+
if (!stat2 || typeof stat2 !== "object") return null;
|
|
419
|
+
const mtimeMs = stat2.mtimeMs;
|
|
420
|
+
const size = stat2.size;
|
|
421
|
+
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs) || mtimeMs <= 0) return null;
|
|
422
|
+
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
423
|
+
return `${Math.floor(mtimeMs)}:${size}`;
|
|
424
|
+
}
|
|
425
|
+
function isFresh(entry, stat2, now, ttlMs = DEFAULT_TTL_MS) {
|
|
426
|
+
if (!entry) return false;
|
|
427
|
+
const fp = fingerprintOf(stat2);
|
|
428
|
+
if (!fp) return false;
|
|
429
|
+
if (entry.fingerprint !== fp) return false;
|
|
430
|
+
if (typeof entry.at !== "number") return false;
|
|
431
|
+
return now - entry.at <= ttlMs;
|
|
432
|
+
}
|
|
433
|
+
function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAULT_TTL_MS) {
|
|
434
|
+
const cached = /* @__PURE__ */ new Map();
|
|
435
|
+
const missing = [];
|
|
436
|
+
for (const id of ids) {
|
|
437
|
+
const entry = cache && cache.get(String(id));
|
|
438
|
+
const stat2 = statsById && statsById.get(String(id));
|
|
439
|
+
if (isFresh(entry, stat2, now, ttlMs) && entry && entry.meta) {
|
|
440
|
+
cached.set(String(id), entry.meta);
|
|
441
|
+
} else {
|
|
442
|
+
missing.push(String(id));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { cached, missing };
|
|
446
|
+
}
|
|
447
|
+
function createSessionMetaCache(opts = {}) {
|
|
448
|
+
const ttlMs = Number.isFinite(opts.ttlMs) ? opts.ttlMs : DEFAULT_TTL_MS;
|
|
449
|
+
const max = Number.isInteger(opts.max) && opts.max > 0 ? opts.max : DEFAULT_MAX;
|
|
450
|
+
const map = /* @__PURE__ */ new Map();
|
|
451
|
+
let hits = 0;
|
|
452
|
+
let misses = 0;
|
|
453
|
+
return {
|
|
454
|
+
// 命中返回 meta,未命中/无法校验返回 null。
|
|
455
|
+
get(id, stat2) {
|
|
456
|
+
const key = String(id);
|
|
457
|
+
const entry = map.get(key);
|
|
458
|
+
if (isFresh(entry, stat2, Date.now(), ttlMs)) {
|
|
459
|
+
hits++;
|
|
460
|
+
map.delete(key);
|
|
461
|
+
map.set(key, entry);
|
|
462
|
+
return entry.meta;
|
|
463
|
+
}
|
|
464
|
+
misses++;
|
|
465
|
+
return null;
|
|
466
|
+
},
|
|
467
|
+
set(id, stat2, meta) {
|
|
468
|
+
if (!meta) return null;
|
|
469
|
+
const fp = fingerprintOf(stat2);
|
|
470
|
+
if (!fp) return null;
|
|
471
|
+
const key = String(id);
|
|
472
|
+
map.delete(key);
|
|
473
|
+
map.set(key, { fingerprint: fp, at: Date.now(), meta });
|
|
474
|
+
if (map.size > max) {
|
|
475
|
+
const oldest = map.keys().next().value;
|
|
476
|
+
if (oldest !== void 0) map.delete(oldest);
|
|
477
|
+
}
|
|
478
|
+
return meta;
|
|
479
|
+
},
|
|
480
|
+
// 批量判定:一次算出「命中缓存」与「需要解码」两组,供列表构建做批量投影。
|
|
481
|
+
partition(ids, statsById) {
|
|
482
|
+
return partitionByCache(ids, statsById, map, Date.now(), ttlMs);
|
|
483
|
+
},
|
|
484
|
+
invalidate(id) {
|
|
485
|
+
if (id == null) return false;
|
|
486
|
+
const key = String(id);
|
|
487
|
+
const had = map.has(key);
|
|
488
|
+
map.delete(key);
|
|
489
|
+
return had;
|
|
490
|
+
},
|
|
491
|
+
clear() {
|
|
492
|
+
map.clear();
|
|
493
|
+
},
|
|
494
|
+
get size() {
|
|
495
|
+
return map.size;
|
|
496
|
+
},
|
|
497
|
+
stats() {
|
|
498
|
+
return { size: map.size, hits, misses, ttlMs };
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
253
503
|
// src/index.js
|
|
254
504
|
var name = "dsh-sessions-manager";
|
|
255
505
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
256
506
|
var MAX_TITLE = 80;
|
|
257
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
258
|
-
var TRASH_INDEX =
|
|
507
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join3(homedir3(), ".dsh", "sessions-manager-trash");
|
|
508
|
+
var TRASH_INDEX = join3(TRASH_DIR, "index.json");
|
|
259
509
|
var TRASH_SCHEMA_VERSION = 2;
|
|
260
510
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
261
511
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
262
512
|
var MAX_FETCHES = 12;
|
|
263
513
|
var MAX_FILES = 20;
|
|
514
|
+
var MAX_STORAGE_TOP = 50;
|
|
264
515
|
function json(res, value, status = 200) {
|
|
265
516
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
266
517
|
res.end(JSON.stringify(value));
|
|
@@ -338,7 +589,24 @@ function apply(ctx) {
|
|
|
338
589
|
const sq = ctx.sessionQuery;
|
|
339
590
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
340
591
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
341
|
-
|
|
592
|
+
const metaCache = createSessionMetaCache();
|
|
593
|
+
function metaFromSnapshot(o) {
|
|
594
|
+
let title = null, createdAt = null, cwd = null;
|
|
595
|
+
if (o) {
|
|
596
|
+
if (o.title && o.title.title) title = String(o.title.title);
|
|
597
|
+
if (o.session) {
|
|
598
|
+
cwd = o.session.cwd || null;
|
|
599
|
+
createdAt = o.session.createdAt || null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return { title, cwd, createdAt };
|
|
603
|
+
}
|
|
604
|
+
function unwrapSnapshot(result) {
|
|
605
|
+
if (!result) return null;
|
|
606
|
+
if (result.status === "fulfilled") return result.value || null;
|
|
607
|
+
if (result.status === "rejected") return null;
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
342
610
|
async function archivedState() {
|
|
343
611
|
const d = dom();
|
|
344
612
|
if (!d) throw new Error("workspace domain is not open");
|
|
@@ -372,42 +640,84 @@ function apply(ctx) {
|
|
|
372
640
|
return operation;
|
|
373
641
|
}
|
|
374
642
|
let wsByPath = {};
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
643
|
+
function buildItem(key, meta, usage, exposeUsage) {
|
|
644
|
+
const cwd = meta.cwd || null;
|
|
645
|
+
const ws = cwd ? wsByPath[cwd] : void 0;
|
|
646
|
+
const title = meta.title || null;
|
|
647
|
+
const display = title ? String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + "\u2026" : String(title) : null;
|
|
648
|
+
const base = {
|
|
649
|
+
sessionId: key,
|
|
650
|
+
title: display,
|
|
651
|
+
createdAt: meta.createdAt || null,
|
|
652
|
+
workspacePath: cwd,
|
|
653
|
+
workspaceTitle: ws && ws.title ? ws.title : null,
|
|
654
|
+
workspaceGone: !!(cwd && !ws),
|
|
655
|
+
hasWorkspace: !!cwd
|
|
656
|
+
};
|
|
657
|
+
if (exposeUsage && usage) {
|
|
658
|
+
if (usage.sizeById && usage.sizeById.has(key)) base.sizeBytes = usage.sizeById.get(key);
|
|
659
|
+
if (usage.mtimeById && usage.mtimeById.has(key)) base.updatedAt = usage.mtimeById.get(key);
|
|
660
|
+
}
|
|
661
|
+
return base;
|
|
662
|
+
}
|
|
663
|
+
async function resolveOne(id, usage, opts = {}) {
|
|
664
|
+
const key = String(id);
|
|
665
|
+
const statInfo = usage ? { mtimeMs: usage.mtimeById.get(key), size: usage.sizeById.get(key) } : null;
|
|
666
|
+
const cached = metaCache.get(key, statInfo);
|
|
667
|
+
if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
|
|
668
|
+
let meta = { title: null, cwd: null, createdAt: null };
|
|
669
|
+
if (opts.preloaded !== void 0) {
|
|
670
|
+
meta = metaFromSnapshot(unwrapSnapshot(opts.preloaded));
|
|
671
|
+
} else if (typeof sq.readTitleSnapshot === "function") {
|
|
672
|
+
try {
|
|
673
|
+
meta = metaFromSnapshot(await sq.readTitleSnapshot(id));
|
|
674
|
+
} catch (e) {
|
|
385
675
|
}
|
|
386
|
-
} catch (e) {
|
|
387
676
|
}
|
|
388
|
-
if (!title || !cwd) {
|
|
677
|
+
if (!meta.title || !meta.cwd) {
|
|
389
678
|
try {
|
|
390
679
|
const r = await sp.readFrom(id, 0);
|
|
391
680
|
if (r.meta) {
|
|
392
|
-
if (!cwd) cwd = r.meta.cwd || null;
|
|
393
|
-
if (!createdAt) createdAt = r.meta.createdAt || null;
|
|
681
|
+
if (!meta.cwd) meta.cwd = r.meta.cwd || null;
|
|
682
|
+
if (!meta.createdAt) meta.createdAt = r.meta.createdAt || null;
|
|
394
683
|
}
|
|
395
|
-
if (!title && Array.isArray(r.events)) title = foldTitle(r.events);
|
|
684
|
+
if (!meta.title && Array.isArray(r.events)) meta.title = foldTitle(r.events);
|
|
396
685
|
} catch (e2) {
|
|
397
686
|
}
|
|
398
687
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
688
|
+
metaCache.set(key, statInfo, meta);
|
|
689
|
+
return buildItem(key, meta, usage, opts.exposeUsage);
|
|
690
|
+
}
|
|
691
|
+
async function collectUsage(preloadedHeaders) {
|
|
692
|
+
const sizeById = /* @__PURE__ */ new Map();
|
|
693
|
+
const mtimeById = /* @__PURE__ */ new Map();
|
|
694
|
+
let headers = null;
|
|
695
|
+
if (Array.isArray(preloadedHeaders)) headers = preloadedHeaders;
|
|
696
|
+
else {
|
|
697
|
+
try {
|
|
698
|
+
headers = await sp.list();
|
|
699
|
+
} catch (e) {
|
|
700
|
+
headers = [];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (!Array.isArray(headers)) headers = [];
|
|
704
|
+
const CHUNK = 8;
|
|
705
|
+
for (let i = 0; i < headers.length; i += CHUNK) {
|
|
706
|
+
await Promise.all(headers.slice(i, i + CHUNK).map(async (header) => {
|
|
707
|
+
const id = header && header.id != null ? String(header.id) : null;
|
|
708
|
+
if (!id) return;
|
|
709
|
+
try {
|
|
710
|
+
const loc = sp.locate(header);
|
|
711
|
+
if (!loc || typeof loc.path !== "string" || !loc.path) return;
|
|
712
|
+
const st = await stat(loc.path);
|
|
713
|
+
if (!st) return;
|
|
714
|
+
if (typeof st.size === "number") sizeById.set(id, st.size);
|
|
715
|
+
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) mtimeById.set(id, Math.floor(st.mtimeMs));
|
|
716
|
+
} catch (e) {
|
|
717
|
+
}
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
return { sizeById, mtimeById };
|
|
411
721
|
}
|
|
412
722
|
async function restoreOne(sid) {
|
|
413
723
|
requireSessionId(sid);
|
|
@@ -427,7 +737,7 @@ function apply(ctx) {
|
|
|
427
737
|
}
|
|
428
738
|
async function readTrashStore() {
|
|
429
739
|
try {
|
|
430
|
-
return normalizeTrashStore(JSON.parse(
|
|
740
|
+
return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
|
|
431
741
|
} catch (e) {
|
|
432
742
|
return normalizeTrashStore(null);
|
|
433
743
|
}
|
|
@@ -436,10 +746,10 @@ function apply(ctx) {
|
|
|
436
746
|
return (await readTrashStore()).items;
|
|
437
747
|
}
|
|
438
748
|
async function writeTrashStore(store) {
|
|
439
|
-
await
|
|
440
|
-
const tmp =
|
|
441
|
-
await
|
|
442
|
-
await
|
|
749
|
+
await mkdir3(TRASH_DIR, { recursive: true });
|
|
750
|
+
const tmp = join3(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
751
|
+
await writeFile3(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
752
|
+
await rename3(tmp, TRASH_INDEX);
|
|
443
753
|
}
|
|
444
754
|
function mutateTrash(mutator) {
|
|
445
755
|
const operation = trashMutation.then(async () => {
|
|
@@ -453,6 +763,7 @@ function apply(ctx) {
|
|
|
453
763
|
return operation;
|
|
454
764
|
}
|
|
455
765
|
const stars = createStarIndex();
|
|
766
|
+
const autoArchive = createAutoArchiveStore();
|
|
456
767
|
async function gcStars(validIds) {
|
|
457
768
|
try {
|
|
458
769
|
const store = await stars.read();
|
|
@@ -637,8 +948,8 @@ function apply(ctx) {
|
|
|
637
948
|
async function moveTargetWorkspace(rawPath) {
|
|
638
949
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
639
950
|
let p = String(rawPath).trim();
|
|
640
|
-
if (p.startsWith("~/")) p =
|
|
641
|
-
if (!isAbsolute(p)) p =
|
|
951
|
+
if (p.startsWith("~/")) p = join3(homedir3(), p.slice(2));
|
|
952
|
+
if (!isAbsolute(p)) p = join3(homedir3(), p);
|
|
642
953
|
let canonical = null;
|
|
643
954
|
try {
|
|
644
955
|
canonical = await realpath(p);
|
|
@@ -646,7 +957,7 @@ function apply(ctx) {
|
|
|
646
957
|
canonical = null;
|
|
647
958
|
}
|
|
648
959
|
if (canonical === null) {
|
|
649
|
-
await
|
|
960
|
+
await mkdir3(p, { recursive: true });
|
|
650
961
|
canonical = await realpath(p);
|
|
651
962
|
}
|
|
652
963
|
return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
|
|
@@ -684,13 +995,13 @@ function apply(ctx) {
|
|
|
684
995
|
if (!oldPath || !newPath || oldPath === newPath) return false;
|
|
685
996
|
const backupPath = `${oldPath}.move-backup-${Date.now()}`;
|
|
686
997
|
try {
|
|
687
|
-
await
|
|
688
|
-
await
|
|
998
|
+
await mkdir3(dirname(newPath), { recursive: true });
|
|
999
|
+
await rename3(oldPath, backupPath);
|
|
689
1000
|
await rewriteFrame0Cwd(backupPath, canonical);
|
|
690
|
-
await
|
|
1001
|
+
await rename3(backupPath, newPath);
|
|
691
1002
|
} catch (e) {
|
|
692
1003
|
try {
|
|
693
|
-
await
|
|
1004
|
+
await rename3(backupPath, oldPath);
|
|
694
1005
|
} catch (_) {
|
|
695
1006
|
}
|
|
696
1007
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -736,7 +1047,7 @@ function apply(ctx) {
|
|
|
736
1047
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
737
1048
|
if (backupPath) {
|
|
738
1049
|
try {
|
|
739
|
-
await
|
|
1050
|
+
await rename3(oldPath, backupPath);
|
|
740
1051
|
} catch (e) {
|
|
741
1052
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
742
1053
|
}
|
|
@@ -744,7 +1055,7 @@ function apply(ctx) {
|
|
|
744
1055
|
const restore = async () => {
|
|
745
1056
|
if (backupPath) {
|
|
746
1057
|
try {
|
|
747
|
-
await
|
|
1058
|
+
await rename3(backupPath, oldPath);
|
|
748
1059
|
} catch (_) {
|
|
749
1060
|
}
|
|
750
1061
|
}
|
|
@@ -838,25 +1149,38 @@ function apply(ctx) {
|
|
|
838
1149
|
return { next: null, value: { ok: true, archived: true } };
|
|
839
1150
|
});
|
|
840
1151
|
}
|
|
841
|
-
async function
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1152
|
+
async function projectTitles(ids) {
|
|
1153
|
+
const out = /* @__PURE__ */ new Map();
|
|
1154
|
+
if (!ids || !ids.length) return out;
|
|
1155
|
+
if (typeof sq.readTitleSnapshots !== "function") return out;
|
|
845
1156
|
try {
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
1157
|
+
const results = await sq.readTitleSnapshots(ids);
|
|
1158
|
+
if (!Array.isArray(results)) return out;
|
|
1159
|
+
results.forEach((result, index) => {
|
|
1160
|
+
const id = String(ids[index]);
|
|
1161
|
+
out.set(id, unwrapSnapshot(result));
|
|
1162
|
+
});
|
|
849
1163
|
} catch (e) {
|
|
850
1164
|
}
|
|
851
|
-
|
|
1165
|
+
return out;
|
|
1166
|
+
}
|
|
1167
|
+
async function allSessionItems(opts = {}) {
|
|
1168
|
+
let headers = [];
|
|
1169
|
+
let headersOk = false;
|
|
852
1170
|
try {
|
|
853
|
-
|
|
1171
|
+
headers = await sp.list();
|
|
1172
|
+
headersOk = Array.isArray(headers);
|
|
1173
|
+
if (!headersOk) headers = [];
|
|
854
1174
|
} catch (e) {
|
|
1175
|
+
headers = [];
|
|
855
1176
|
}
|
|
1177
|
+
let live = ctx.get("sessions");
|
|
1178
|
+
const ids = headers.map((h) => String(h.id));
|
|
856
1179
|
if (live) {
|
|
857
1180
|
try {
|
|
858
1181
|
live.list().forEach((s) => {
|
|
859
|
-
|
|
1182
|
+
const sid = String(s.id);
|
|
1183
|
+
if (!ids.includes(sid)) ids.push(sid);
|
|
860
1184
|
});
|
|
861
1185
|
} catch (e) {
|
|
862
1186
|
}
|
|
@@ -876,9 +1200,16 @@ function apply(ctx) {
|
|
|
876
1200
|
}
|
|
877
1201
|
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
878
1202
|
const items = [];
|
|
1203
|
+
const usage = await collectUsage(headers);
|
|
1204
|
+
const statsById = new Map(visibleIds.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
|
|
1205
|
+
const { cached, missing } = metaCache.partition(visibleIds, statsById);
|
|
1206
|
+
const snapshotById = await projectTitles(missing);
|
|
879
1207
|
const CHUNK = 6;
|
|
880
1208
|
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
881
|
-
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map(resolveOne
|
|
1209
|
+
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage, {
|
|
1210
|
+
exposeUsage: !!(opts && opts.usage),
|
|
1211
|
+
preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0
|
|
1212
|
+
})));
|
|
882
1213
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
883
1214
|
}
|
|
884
1215
|
let starredSet = /* @__PURE__ */ new Set();
|
|
@@ -890,37 +1221,74 @@ function apply(ctx) {
|
|
|
890
1221
|
if (headersOk) await gcStars(ids);
|
|
891
1222
|
return items;
|
|
892
1223
|
}
|
|
1224
|
+
async function buildStorage(opts = {}) {
|
|
1225
|
+
const items = await allSessionItems({ usage: true });
|
|
1226
|
+
const raw = Number(opts && opts.topN);
|
|
1227
|
+
const topN = Number.isInteger(raw) && raw > 0 ? Math.min(raw, MAX_STORAGE_TOP) : 10;
|
|
1228
|
+
return aggregateStorage(items, { topN });
|
|
1229
|
+
}
|
|
1230
|
+
async function autoArchiveSweep(opts = {}) {
|
|
1231
|
+
const store = await autoArchive.read();
|
|
1232
|
+
const days = store.settings.inactiveDays;
|
|
1233
|
+
if (!days) return { ok: true, skipped: "disabled", archived: 0 };
|
|
1234
|
+
const now = Date.now();
|
|
1235
|
+
if (!(opts && opts.force) && autoArchive.isFresh(store, now)) {
|
|
1236
|
+
return { ok: true, skipped: "throttled", archived: 0, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount };
|
|
1237
|
+
}
|
|
1238
|
+
const items = await allSessionItems({ usage: true });
|
|
1239
|
+
const candidates = pickInactiveCandidates(items, {
|
|
1240
|
+
inactiveDays: days,
|
|
1241
|
+
skipStarred: store.settings.skipStarred,
|
|
1242
|
+
activeSessionId: getActiveSessionId(ctx),
|
|
1243
|
+
now
|
|
1244
|
+
});
|
|
1245
|
+
let archived = 0;
|
|
1246
|
+
const failed = [];
|
|
1247
|
+
for (const sid of candidates) {
|
|
1248
|
+
try {
|
|
1249
|
+
const result = await archiveOne(sid);
|
|
1250
|
+
if (result && result.archived) archived++;
|
|
1251
|
+
} catch (e) {
|
|
1252
|
+
failed.push({ sessionId: sid, error: String(e && e.message || e) });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
await autoArchive.recordRun(archived, now);
|
|
1256
|
+
return { ok: true, archived, candidates: candidates.length, failed, lastRunAt: now };
|
|
1257
|
+
}
|
|
893
1258
|
async function sidebarAuthority() {
|
|
894
1259
|
const ids = [];
|
|
1260
|
+
let headers = [];
|
|
895
1261
|
try {
|
|
896
|
-
|
|
1262
|
+
headers = await sp.list();
|
|
897
1263
|
} catch (e) {
|
|
1264
|
+
headers = [];
|
|
898
1265
|
}
|
|
1266
|
+
if (!Array.isArray(headers)) headers = [];
|
|
1267
|
+
for (const header of headers) ids.push(String(header.id));
|
|
899
1268
|
const sessions = ctx.get("sessions");
|
|
900
1269
|
try {
|
|
901
1270
|
if (sessions) sessions.list().forEach((session) => {
|
|
902
|
-
|
|
1271
|
+
const sid = String(session.id);
|
|
1272
|
+
if (!ids.includes(sid)) ids.push(sid);
|
|
903
1273
|
});
|
|
904
1274
|
} catch (e) {
|
|
905
1275
|
}
|
|
906
1276
|
const store = await readTrashStore();
|
|
907
|
-
if (
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (snapshot && snapshot.title && typeof snapshot.title.title === "string") authorityTitleCache.set(sid, snapshot.title.title);
|
|
920
|
-
} catch (e) {
|
|
1277
|
+
if (ids.length) {
|
|
1278
|
+
const usage = await collectUsage(headers);
|
|
1279
|
+
const statsById = new Map(ids.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
|
|
1280
|
+
const { cached, missing } = metaCache.partition(ids, statsById);
|
|
1281
|
+
const snapshotById = await projectTitles(missing);
|
|
1282
|
+
for (const id of ids) {
|
|
1283
|
+
let meta = cached.get(id) || null;
|
|
1284
|
+
if (!meta) {
|
|
1285
|
+
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
|
|
1286
|
+
const next = metaFromSnapshot(snapshot);
|
|
1287
|
+
metaCache.set(id, statsById.get(id), next);
|
|
1288
|
+
meta = next;
|
|
921
1289
|
}
|
|
1290
|
+
if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
|
|
922
1291
|
}
|
|
923
|
-
authorityTitlesLoaded = true;
|
|
924
1292
|
}
|
|
925
1293
|
return {
|
|
926
1294
|
titles: Object.fromEntries(authorityTitleCache),
|
|
@@ -1096,7 +1464,7 @@ function apply(ctx) {
|
|
|
1096
1464
|
const items = [];
|
|
1097
1465
|
const CHUNK = 6;
|
|
1098
1466
|
for (let i = 0; i < idStrs.length; i += CHUNK) {
|
|
1099
|
-
const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map(resolveOne));
|
|
1467
|
+
const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map((id) => resolveOne(id)));
|
|
1100
1468
|
items.push.apply(items, res2);
|
|
1101
1469
|
}
|
|
1102
1470
|
json(res, { items });
|
|
@@ -1149,7 +1517,9 @@ function apply(ctx) {
|
|
|
1149
1517
|
const body = await readJsonBody(req);
|
|
1150
1518
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
1151
1519
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1152
|
-
|
|
1520
|
+
const out = await deleteOne(sid);
|
|
1521
|
+
metaCache.invalidate(sid);
|
|
1522
|
+
json(res, out);
|
|
1153
1523
|
} catch (e) {
|
|
1154
1524
|
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
1155
1525
|
}
|
|
@@ -1167,6 +1537,7 @@ function apply(ctx) {
|
|
|
1167
1537
|
for (const sid of ids) {
|
|
1168
1538
|
try {
|
|
1169
1539
|
results.push({ sessionId: sid, ok: true, ...await deleteOne(sid) });
|
|
1540
|
+
metaCache.invalidate(sid);
|
|
1170
1541
|
} catch (e) {
|
|
1171
1542
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
1172
1543
|
}
|
|
@@ -1230,7 +1601,9 @@ function apply(ctx) {
|
|
|
1230
1601
|
const body = await readJsonBody(req);
|
|
1231
1602
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
1232
1603
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1233
|
-
|
|
1604
|
+
const out = await restoreFromTrash(sid);
|
|
1605
|
+
metaCache.invalidate(sid);
|
|
1606
|
+
json(res, out);
|
|
1234
1607
|
} catch (e) {
|
|
1235
1608
|
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
1236
1609
|
}
|
|
@@ -1244,7 +1617,9 @@ function apply(ctx) {
|
|
|
1244
1617
|
const body = await readJsonBody(req);
|
|
1245
1618
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
1246
1619
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1247
|
-
|
|
1620
|
+
const out = await purgeFromTrash(sid);
|
|
1621
|
+
metaCache.invalidate(sid);
|
|
1622
|
+
json(res, out);
|
|
1248
1623
|
} catch (e) {
|
|
1249
1624
|
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
1250
1625
|
}
|
|
@@ -1262,6 +1637,7 @@ function apply(ctx) {
|
|
|
1262
1637
|
for (const sid of ids) {
|
|
1263
1638
|
try {
|
|
1264
1639
|
results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
|
|
1640
|
+
metaCache.invalidate(sid);
|
|
1265
1641
|
} catch (e) {
|
|
1266
1642
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
1267
1643
|
}
|
|
@@ -1361,6 +1737,7 @@ function apply(ctx) {
|
|
|
1361
1737
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1362
1738
|
if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
|
|
1363
1739
|
const moved = await moveOne(sid, target);
|
|
1740
|
+
metaCache.invalidate(sid);
|
|
1364
1741
|
try {
|
|
1365
1742
|
await reindexRegistry();
|
|
1366
1743
|
} catch (e) {
|
|
@@ -1421,6 +1798,55 @@ function apply(ctx) {
|
|
|
1421
1798
|
}
|
|
1422
1799
|
}
|
|
1423
1800
|
}));
|
|
1801
|
+
disposers.push(ctx.webServer.register({
|
|
1802
|
+
kind: "exact",
|
|
1803
|
+
path: "/archived-sessions/storage",
|
|
1804
|
+
handler: async (req, res) => {
|
|
1805
|
+
try {
|
|
1806
|
+
const body = await readJsonBody(req);
|
|
1807
|
+
json(res, await buildStorage({ topN: body && body.topN }));
|
|
1808
|
+
} catch (e) {
|
|
1809
|
+
json(res, { error: String(e && e.message || e) }, 500);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}));
|
|
1813
|
+
disposers.push(ctx.webServer.register({
|
|
1814
|
+
kind: "exact",
|
|
1815
|
+
path: "/archived-sessions/auto-archive/settings",
|
|
1816
|
+
handler: async (req, res) => {
|
|
1817
|
+
try {
|
|
1818
|
+
const body = await readJsonBody(req);
|
|
1819
|
+
const patch = {};
|
|
1820
|
+
if (body && Object.prototype.hasOwnProperty.call(body, "inactiveDays")) patch.inactiveDays = body.inactiveDays;
|
|
1821
|
+
if (body && Object.prototype.hasOwnProperty.call(body, "skipStarred")) patch.skipStarred = body.skipStarred;
|
|
1822
|
+
const settings = Object.keys(patch).length ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
|
|
1823
|
+
const sweep = await autoArchiveSweep();
|
|
1824
|
+
const store = await autoArchive.read();
|
|
1825
|
+
json(res, {
|
|
1826
|
+
ok: true,
|
|
1827
|
+
settings,
|
|
1828
|
+
lastRunAt: store.lastRunAt,
|
|
1829
|
+
lastArchivedCount: store.lastArchivedCount,
|
|
1830
|
+
sweep
|
|
1831
|
+
});
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}));
|
|
1837
|
+
disposers.push(ctx.webServer.register({
|
|
1838
|
+
kind: "exact",
|
|
1839
|
+
path: "/archived-sessions/auto-archive/run",
|
|
1840
|
+
handler: async (req, res) => {
|
|
1841
|
+
try {
|
|
1842
|
+
const sweep = await autoArchiveSweep({ force: true });
|
|
1843
|
+
const store = await autoArchive.read();
|
|
1844
|
+
json(res, { ok: true, ...sweep, settings: store.settings, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount });
|
|
1845
|
+
} catch (e) {
|
|
1846
|
+
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}));
|
|
1424
1850
|
return () => {
|
|
1425
1851
|
for (const d of disposers) d();
|
|
1426
1852
|
};
|