dsh-sessions-manager 3.3.0 → 3.4.0
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 +161 -4
- package/lib/client.js.map +2 -2
- package/lib/index.js +305 -25
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/auto-archive.js +168 -0
- package/src/client/index.jsx +177 -5
- package/src/index.js +161 -5
- 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,179 @@ 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 isFresh(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, indexPath, dir };
|
|
412
|
+
}
|
|
413
|
+
|
|
253
414
|
// src/index.js
|
|
254
415
|
var name = "dsh-sessions-manager";
|
|
255
416
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
256
417
|
var MAX_TITLE = 80;
|
|
257
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
258
|
-
var TRASH_INDEX =
|
|
418
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join3(homedir3(), ".dsh", "sessions-manager-trash");
|
|
419
|
+
var TRASH_INDEX = join3(TRASH_DIR, "index.json");
|
|
259
420
|
var TRASH_SCHEMA_VERSION = 2;
|
|
260
421
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
261
422
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
262
423
|
var MAX_FETCHES = 12;
|
|
263
424
|
var MAX_FILES = 20;
|
|
425
|
+
var MAX_STORAGE_TOP = 50;
|
|
264
426
|
function json(res, value, status = 200) {
|
|
265
427
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
266
428
|
res.end(JSON.stringify(value));
|
|
@@ -372,7 +534,7 @@ function apply(ctx) {
|
|
|
372
534
|
return operation;
|
|
373
535
|
}
|
|
374
536
|
let wsByPath = {};
|
|
375
|
-
async function resolveOne(id) {
|
|
537
|
+
async function resolveOne(id, usage) {
|
|
376
538
|
let title = null, createdAt = null, cwd = null;
|
|
377
539
|
try {
|
|
378
540
|
const o = await sq.readTitleSnapshot(id);
|
|
@@ -399,7 +561,7 @@ function apply(ctx) {
|
|
|
399
561
|
const ws = cwd ? wsByPath[cwd] : void 0;
|
|
400
562
|
const workspaceGone = !!(cwd && !ws);
|
|
401
563
|
const display = title ? String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + "\u2026" : String(title) : null;
|
|
402
|
-
|
|
564
|
+
const base = {
|
|
403
565
|
sessionId: id,
|
|
404
566
|
title: display,
|
|
405
567
|
createdAt: createdAt || null,
|
|
@@ -408,6 +570,39 @@ function apply(ctx) {
|
|
|
408
570
|
workspaceGone: workspaceGone ? true : false,
|
|
409
571
|
hasWorkspace: !!cwd
|
|
410
572
|
};
|
|
573
|
+
if (usage) {
|
|
574
|
+
if (usage.sizeById && usage.sizeById.has(id)) base.sizeBytes = usage.sizeById.get(id);
|
|
575
|
+
if (usage.mtimeById && usage.mtimeById.has(id)) base.updatedAt = usage.mtimeById.get(id);
|
|
576
|
+
}
|
|
577
|
+
return base;
|
|
578
|
+
}
|
|
579
|
+
async function collectUsage() {
|
|
580
|
+
const sizeById = /* @__PURE__ */ new Map();
|
|
581
|
+
const mtimeById = /* @__PURE__ */ new Map();
|
|
582
|
+
let headers = [];
|
|
583
|
+
try {
|
|
584
|
+
headers = await sp.list();
|
|
585
|
+
} catch (e) {
|
|
586
|
+
headers = [];
|
|
587
|
+
}
|
|
588
|
+
if (!Array.isArray(headers)) headers = [];
|
|
589
|
+
const CHUNK = 8;
|
|
590
|
+
for (let i = 0; i < headers.length; i += CHUNK) {
|
|
591
|
+
await Promise.all(headers.slice(i, i + CHUNK).map(async (header) => {
|
|
592
|
+
const id = header && header.id != null ? String(header.id) : null;
|
|
593
|
+
if (!id) return;
|
|
594
|
+
try {
|
|
595
|
+
const loc = sp.locate(header);
|
|
596
|
+
if (!loc || typeof loc.path !== "string" || !loc.path) return;
|
|
597
|
+
const st = await stat(loc.path);
|
|
598
|
+
if (!st) return;
|
|
599
|
+
if (typeof st.size === "number") sizeById.set(id, st.size);
|
|
600
|
+
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) mtimeById.set(id, Math.floor(st.mtimeMs));
|
|
601
|
+
} catch (e) {
|
|
602
|
+
}
|
|
603
|
+
}));
|
|
604
|
+
}
|
|
605
|
+
return { sizeById, mtimeById };
|
|
411
606
|
}
|
|
412
607
|
async function restoreOne(sid) {
|
|
413
608
|
requireSessionId(sid);
|
|
@@ -427,7 +622,7 @@ function apply(ctx) {
|
|
|
427
622
|
}
|
|
428
623
|
async function readTrashStore() {
|
|
429
624
|
try {
|
|
430
|
-
return normalizeTrashStore(JSON.parse(
|
|
625
|
+
return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
|
|
431
626
|
} catch (e) {
|
|
432
627
|
return normalizeTrashStore(null);
|
|
433
628
|
}
|
|
@@ -436,10 +631,10 @@ function apply(ctx) {
|
|
|
436
631
|
return (await readTrashStore()).items;
|
|
437
632
|
}
|
|
438
633
|
async function writeTrashStore(store) {
|
|
439
|
-
await
|
|
440
|
-
const tmp =
|
|
441
|
-
await
|
|
442
|
-
await
|
|
634
|
+
await mkdir3(TRASH_DIR, { recursive: true });
|
|
635
|
+
const tmp = join3(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
636
|
+
await writeFile3(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
637
|
+
await rename3(tmp, TRASH_INDEX);
|
|
443
638
|
}
|
|
444
639
|
function mutateTrash(mutator) {
|
|
445
640
|
const operation = trashMutation.then(async () => {
|
|
@@ -453,6 +648,7 @@ function apply(ctx) {
|
|
|
453
648
|
return operation;
|
|
454
649
|
}
|
|
455
650
|
const stars = createStarIndex();
|
|
651
|
+
const autoArchive = createAutoArchiveStore();
|
|
456
652
|
async function gcStars(validIds) {
|
|
457
653
|
try {
|
|
458
654
|
const store = await stars.read();
|
|
@@ -637,8 +833,8 @@ function apply(ctx) {
|
|
|
637
833
|
async function moveTargetWorkspace(rawPath) {
|
|
638
834
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
639
835
|
let p = String(rawPath).trim();
|
|
640
|
-
if (p.startsWith("~/")) p =
|
|
641
|
-
if (!isAbsolute(p)) p =
|
|
836
|
+
if (p.startsWith("~/")) p = join3(homedir3(), p.slice(2));
|
|
837
|
+
if (!isAbsolute(p)) p = join3(homedir3(), p);
|
|
642
838
|
let canonical = null;
|
|
643
839
|
try {
|
|
644
840
|
canonical = await realpath(p);
|
|
@@ -646,7 +842,7 @@ function apply(ctx) {
|
|
|
646
842
|
canonical = null;
|
|
647
843
|
}
|
|
648
844
|
if (canonical === null) {
|
|
649
|
-
await
|
|
845
|
+
await mkdir3(p, { recursive: true });
|
|
650
846
|
canonical = await realpath(p);
|
|
651
847
|
}
|
|
652
848
|
return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
|
|
@@ -684,13 +880,13 @@ function apply(ctx) {
|
|
|
684
880
|
if (!oldPath || !newPath || oldPath === newPath) return false;
|
|
685
881
|
const backupPath = `${oldPath}.move-backup-${Date.now()}`;
|
|
686
882
|
try {
|
|
687
|
-
await
|
|
688
|
-
await
|
|
883
|
+
await mkdir3(dirname(newPath), { recursive: true });
|
|
884
|
+
await rename3(oldPath, backupPath);
|
|
689
885
|
await rewriteFrame0Cwd(backupPath, canonical);
|
|
690
|
-
await
|
|
886
|
+
await rename3(backupPath, newPath);
|
|
691
887
|
} catch (e) {
|
|
692
888
|
try {
|
|
693
|
-
await
|
|
889
|
+
await rename3(backupPath, oldPath);
|
|
694
890
|
} catch (_) {
|
|
695
891
|
}
|
|
696
892
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -736,7 +932,7 @@ function apply(ctx) {
|
|
|
736
932
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
737
933
|
if (backupPath) {
|
|
738
934
|
try {
|
|
739
|
-
await
|
|
935
|
+
await rename3(oldPath, backupPath);
|
|
740
936
|
} catch (e) {
|
|
741
937
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
742
938
|
}
|
|
@@ -744,7 +940,7 @@ function apply(ctx) {
|
|
|
744
940
|
const restore = async () => {
|
|
745
941
|
if (backupPath) {
|
|
746
942
|
try {
|
|
747
|
-
await
|
|
943
|
+
await rename3(backupPath, oldPath);
|
|
748
944
|
} catch (_) {
|
|
749
945
|
}
|
|
750
946
|
}
|
|
@@ -838,7 +1034,7 @@ function apply(ctx) {
|
|
|
838
1034
|
return { next: null, value: { ok: true, archived: true } };
|
|
839
1035
|
});
|
|
840
1036
|
}
|
|
841
|
-
async function allSessionItems() {
|
|
1037
|
+
async function allSessionItems(opts = {}) {
|
|
842
1038
|
let materialized = /* @__PURE__ */ new Set();
|
|
843
1039
|
let live = ctx.get("sessions");
|
|
844
1040
|
let headersOk = false;
|
|
@@ -876,9 +1072,10 @@ function apply(ctx) {
|
|
|
876
1072
|
}
|
|
877
1073
|
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
878
1074
|
const items = [];
|
|
1075
|
+
const usage = opts && opts.usage ? await collectUsage() : null;
|
|
879
1076
|
const CHUNK = 6;
|
|
880
1077
|
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
881
|
-
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map(resolveOne));
|
|
1078
|
+
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage)));
|
|
882
1079
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
883
1080
|
}
|
|
884
1081
|
let starredSet = /* @__PURE__ */ new Set();
|
|
@@ -890,6 +1087,40 @@ function apply(ctx) {
|
|
|
890
1087
|
if (headersOk) await gcStars(ids);
|
|
891
1088
|
return items;
|
|
892
1089
|
}
|
|
1090
|
+
async function buildStorage(opts = {}) {
|
|
1091
|
+
const items = await allSessionItems({ usage: true });
|
|
1092
|
+
const raw = Number(opts && opts.topN);
|
|
1093
|
+
const topN = Number.isInteger(raw) && raw > 0 ? Math.min(raw, MAX_STORAGE_TOP) : 10;
|
|
1094
|
+
return aggregateStorage(items, { topN });
|
|
1095
|
+
}
|
|
1096
|
+
async function autoArchiveSweep(opts = {}) {
|
|
1097
|
+
const store = await autoArchive.read();
|
|
1098
|
+
const days = store.settings.inactiveDays;
|
|
1099
|
+
if (!days) return { ok: true, skipped: "disabled", archived: 0 };
|
|
1100
|
+
const now = Date.now();
|
|
1101
|
+
if (!(opts && opts.force) && autoArchive.isFresh(store, now)) {
|
|
1102
|
+
return { ok: true, skipped: "throttled", archived: 0, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount };
|
|
1103
|
+
}
|
|
1104
|
+
const items = await allSessionItems({ usage: true });
|
|
1105
|
+
const candidates = pickInactiveCandidates(items, {
|
|
1106
|
+
inactiveDays: days,
|
|
1107
|
+
skipStarred: store.settings.skipStarred,
|
|
1108
|
+
activeSessionId: getActiveSessionId(ctx),
|
|
1109
|
+
now
|
|
1110
|
+
});
|
|
1111
|
+
let archived = 0;
|
|
1112
|
+
const failed = [];
|
|
1113
|
+
for (const sid of candidates) {
|
|
1114
|
+
try {
|
|
1115
|
+
const result = await archiveOne(sid);
|
|
1116
|
+
if (result && result.archived) archived++;
|
|
1117
|
+
} catch (e) {
|
|
1118
|
+
failed.push({ sessionId: sid, error: String(e && e.message || e) });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
await autoArchive.recordRun(archived, now);
|
|
1122
|
+
return { ok: true, archived, candidates: candidates.length, failed, lastRunAt: now };
|
|
1123
|
+
}
|
|
893
1124
|
async function sidebarAuthority() {
|
|
894
1125
|
const ids = [];
|
|
895
1126
|
try {
|
|
@@ -1096,7 +1327,7 @@ function apply(ctx) {
|
|
|
1096
1327
|
const items = [];
|
|
1097
1328
|
const CHUNK = 6;
|
|
1098
1329
|
for (let i = 0; i < idStrs.length; i += CHUNK) {
|
|
1099
|
-
const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map(resolveOne));
|
|
1330
|
+
const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map((id) => resolveOne(id)));
|
|
1100
1331
|
items.push.apply(items, res2);
|
|
1101
1332
|
}
|
|
1102
1333
|
json(res, { items });
|
|
@@ -1421,6 +1652,55 @@ function apply(ctx) {
|
|
|
1421
1652
|
}
|
|
1422
1653
|
}
|
|
1423
1654
|
}));
|
|
1655
|
+
disposers.push(ctx.webServer.register({
|
|
1656
|
+
kind: "exact",
|
|
1657
|
+
path: "/archived-sessions/storage",
|
|
1658
|
+
handler: async (req, res) => {
|
|
1659
|
+
try {
|
|
1660
|
+
const body = await readJsonBody(req);
|
|
1661
|
+
json(res, await buildStorage({ topN: body && body.topN }));
|
|
1662
|
+
} catch (e) {
|
|
1663
|
+
json(res, { error: String(e && e.message || e) }, 500);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}));
|
|
1667
|
+
disposers.push(ctx.webServer.register({
|
|
1668
|
+
kind: "exact",
|
|
1669
|
+
path: "/archived-sessions/auto-archive/settings",
|
|
1670
|
+
handler: async (req, res) => {
|
|
1671
|
+
try {
|
|
1672
|
+
const body = await readJsonBody(req);
|
|
1673
|
+
const patch = {};
|
|
1674
|
+
if (body && Object.prototype.hasOwnProperty.call(body, "inactiveDays")) patch.inactiveDays = body.inactiveDays;
|
|
1675
|
+
if (body && Object.prototype.hasOwnProperty.call(body, "skipStarred")) patch.skipStarred = body.skipStarred;
|
|
1676
|
+
const settings = Object.keys(patch).length ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
|
|
1677
|
+
const sweep = await autoArchiveSweep();
|
|
1678
|
+
const store = await autoArchive.read();
|
|
1679
|
+
json(res, {
|
|
1680
|
+
ok: true,
|
|
1681
|
+
settings,
|
|
1682
|
+
lastRunAt: store.lastRunAt,
|
|
1683
|
+
lastArchivedCount: store.lastArchivedCount,
|
|
1684
|
+
sweep
|
|
1685
|
+
});
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}));
|
|
1691
|
+
disposers.push(ctx.webServer.register({
|
|
1692
|
+
kind: "exact",
|
|
1693
|
+
path: "/archived-sessions/auto-archive/run",
|
|
1694
|
+
handler: async (req, res) => {
|
|
1695
|
+
try {
|
|
1696
|
+
const sweep = await autoArchiveSweep({ force: true });
|
|
1697
|
+
const store = await autoArchive.read();
|
|
1698
|
+
json(res, { ok: true, ...sweep, settings: store.settings, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount });
|
|
1699
|
+
} catch (e) {
|
|
1700
|
+
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}));
|
|
1424
1704
|
return () => {
|
|
1425
1705
|
for (const d of disposers) d();
|
|
1426
1706
|
};
|