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/lib/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.js
2
- import { mkdir as mkdir2, realpath, rename as rename2, stat, unlink, writeFile as writeFile2 } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join as join2 } from "node:path";
4
- import { readFileSync as readFileSync3 } from "node:fs";
5
- import { homedir as homedir2 } from "node:os";
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 || join2(homedir2(), ".dsh", "sessions-manager-trash");
258
- var TRASH_INDEX = join2(TRASH_DIR, "index.json");
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
- let authorityTitlesLoaded = false;
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
- async function resolveOne(id) {
376
- let title = null, createdAt = null, cwd = null;
377
- try {
378
- const o = await sq.readTitleSnapshot(id);
379
- if (o) {
380
- if (o.title && o.title.title) title = String(o.title.title);
381
- if (o.session) {
382
- cwd = o.session.cwd || null;
383
- createdAt = o.session.createdAt || null;
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
- const ws = cwd ? wsByPath[cwd] : void 0;
400
- const workspaceGone = !!(cwd && !ws);
401
- const display = title ? String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + "\u2026" : String(title) : null;
402
- return {
403
- sessionId: id,
404
- title: display,
405
- createdAt: createdAt || null,
406
- workspacePath: cwd || null,
407
- workspaceTitle: ws && ws.title ? ws.title : null,
408
- workspaceGone: workspaceGone ? true : false,
409
- hasWorkspace: !!cwd
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(readFileSync3(TRASH_INDEX, "utf8")));
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 mkdir2(TRASH_DIR, { recursive: true });
440
- const tmp = join2(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
441
- await writeFile2(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
442
- await rename2(tmp, TRASH_INDEX);
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 = join2(homedir2(), p.slice(2));
641
- if (!isAbsolute(p)) p = join2(homedir2(), 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 mkdir2(p, { recursive: true });
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 mkdir2(dirname(newPath), { recursive: true });
688
- await rename2(oldPath, backupPath);
998
+ await mkdir3(dirname(newPath), { recursive: true });
999
+ await rename3(oldPath, backupPath);
689
1000
  await rewriteFrame0Cwd(backupPath, canonical);
690
- await rename2(backupPath, newPath);
1001
+ await rename3(backupPath, newPath);
691
1002
  } catch (e) {
692
1003
  try {
693
- await rename2(backupPath, oldPath);
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 rename2(oldPath, backupPath);
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 rename2(backupPath, oldPath);
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 allSessionItems() {
842
- let materialized = /* @__PURE__ */ new Set();
843
- let live = ctx.get("sessions");
844
- let headersOk = false;
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 headers = await sp.list();
847
- materialized = new Set(headers.map((h) => String(h.id)));
848
- headersOk = true;
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
- const ids = [];
1165
+ return out;
1166
+ }
1167
+ async function allSessionItems(opts = {}) {
1168
+ let headers = [];
1169
+ let headersOk = false;
852
1170
  try {
853
- for (const header of await sp.list()) ids.push(String(header.id));
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
- if (!ids.includes(String(s.id))) ids.push(String(s.id));
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
- for (const header of await sp.list()) ids.push(String(header.id));
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
- if (!ids.includes(String(session.id))) ids.push(String(session.id));
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 (!authorityTitlesLoaded && ids.length && typeof sq.readTitleSnapshots === "function") {
908
- const results = await sq.readTitleSnapshots(ids);
909
- results.forEach((result, index) => {
910
- if (result && result.status === "fulfilled" && result.value && result.value.title && typeof result.value.title.title === "string") {
911
- authorityTitleCache.set(ids[index], result.value.title.title);
912
- }
913
- });
914
- authorityTitlesLoaded = true;
915
- } else if (!authorityTitlesLoaded) {
916
- for (const sid of ids) {
917
- try {
918
- const snapshot = await sq.readTitleSnapshot(sid);
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
- json(res, await deleteOne(sid));
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
- json(res, await restoreFromTrash(sid));
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
- json(res, await purgeFromTrash(sid));
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
  };