dsh-sessions-manager 3.6.1 → 3.7.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 -10
- package/README.md +23 -12
- package/lib/client.js +1087 -57
- package/lib/client.js.map +2 -2
- package/lib/index.js +1103 -201
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +915 -56
- package/src/client/logic.js +314 -0
- package/src/empty-scan-index.js +118 -0
- package/src/index.js +566 -127
- package/src/lineage.js +18 -14
- package/src/move-notices.js +129 -0
- package/src/saved-filters.js +208 -0
- package/src/tag-index.js +308 -0
- package/src/title-persist-index.js +13 -3
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir as
|
|
3
|
-
import { basename as basename3, dirname as
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { homedir as
|
|
2
|
+
import { mkdir as mkdir10, readFile as readFile4, readdir as readdir4, realpath, rename as rename9, rm as rm3, stat as stat4, unlink, writeFile as writeFile10 } from "node:fs/promises";
|
|
3
|
+
import { basename as basename3, dirname as dirname4, isAbsolute, join as join12 } from "node:path";
|
|
4
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
5
|
+
import { homedir as homedir7 } from "node:os";
|
|
6
6
|
|
|
7
7
|
// src/zstd-frame.js
|
|
8
8
|
import zlib from "node:zlib";
|
|
@@ -303,13 +303,358 @@ function createStarIndex(options = {}) {
|
|
|
303
303
|
return { read, write, mutate, setStarred, removeIds, indexPath, dir };
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
// src/
|
|
306
|
+
// src/tag-index.js
|
|
307
|
+
import { mkdir as mkdir2, rename as fsRename, writeFile as writeFile2 } from "node:fs/promises";
|
|
307
308
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
308
|
-
import {
|
|
309
|
+
import { randomBytes } from "node:crypto";
|
|
309
310
|
import { homedir as homedir2 } from "node:os";
|
|
310
311
|
import { join as join2 } from "node:path";
|
|
312
|
+
var TAG_SCHEMA_VERSION = 4;
|
|
313
|
+
var MAX_TAGS = 200;
|
|
314
|
+
var MAX_TAGS_PER_SESSION = 10;
|
|
315
|
+
var MAX_TAG_NAME = 24;
|
|
316
|
+
var DEFAULT_TAG_DIR = join2(homedir2(), ".dsh", "sessions-manager");
|
|
317
|
+
var TAG_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
318
|
+
function randomSuffix() {
|
|
319
|
+
const bytes = randomBytes(8);
|
|
320
|
+
let out = "";
|
|
321
|
+
for (const b of bytes) out += TAG_ID_ALPHABET[b % TAG_ID_ALPHABET.length];
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
function isSafeSessionId2(value) {
|
|
325
|
+
return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
|
|
326
|
+
}
|
|
327
|
+
function isSafeTagId(value) {
|
|
328
|
+
return typeof value === "string" && value.length > 0 && value.length <= 64 && !/[\\/\0]/.test(value);
|
|
329
|
+
}
|
|
330
|
+
function tagError(message, status, code) {
|
|
331
|
+
const error = new Error(message);
|
|
332
|
+
error.status = status;
|
|
333
|
+
if (code) error.code = code;
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
function normalizeTagName(value) {
|
|
337
|
+
if (typeof value !== "string") return null;
|
|
338
|
+
const name2 = value.trim();
|
|
339
|
+
if (!name2 || /[\\/\0]/.test(name2)) return null;
|
|
340
|
+
if (Array.from(name2).length > MAX_TAG_NAME) return null;
|
|
341
|
+
return name2;
|
|
342
|
+
}
|
|
343
|
+
function nameKey(name2) {
|
|
344
|
+
return name2.toLocaleLowerCase();
|
|
345
|
+
}
|
|
346
|
+
function normalizeTagStore(raw) {
|
|
347
|
+
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
348
|
+
const tags = [];
|
|
349
|
+
const ids = /* @__PURE__ */ new Set();
|
|
350
|
+
const names = /* @__PURE__ */ new Set();
|
|
351
|
+
for (const item of source && Array.isArray(source.tags) ? source.tags : []) {
|
|
352
|
+
if (!item || typeof item !== "object") continue;
|
|
353
|
+
if (!isSafeTagId(item.id) || ids.has(item.id)) continue;
|
|
354
|
+
const name2 = normalizeTagName(item.name);
|
|
355
|
+
if (name2 === null) continue;
|
|
356
|
+
const key = nameKey(name2);
|
|
357
|
+
if (names.has(key)) continue;
|
|
358
|
+
ids.add(item.id);
|
|
359
|
+
names.add(key);
|
|
360
|
+
const createdAt = Number.isFinite(Number(item.createdAt)) ? Number(item.createdAt) : 0;
|
|
361
|
+
tags.push({ id: item.id, name: name2, createdAt });
|
|
362
|
+
if (tags.length >= MAX_TAGS) break;
|
|
363
|
+
}
|
|
364
|
+
const assignments = {};
|
|
365
|
+
const rawAssignments = source && source.assignments && typeof source.assignments === "object" && !Array.isArray(source.assignments) ? source.assignments : null;
|
|
366
|
+
if (rawAssignments) {
|
|
367
|
+
for (const [sid, value] of Object.entries(rawAssignments)) {
|
|
368
|
+
if (!isSafeSessionId2(sid) || !Array.isArray(value)) continue;
|
|
369
|
+
const kept = [];
|
|
370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
371
|
+
for (const tagId of value) {
|
|
372
|
+
if (!ids.has(tagId) || seen.has(tagId)) continue;
|
|
373
|
+
seen.add(tagId);
|
|
374
|
+
kept.push(tagId);
|
|
375
|
+
if (kept.length >= MAX_TAGS_PER_SESSION) break;
|
|
376
|
+
}
|
|
377
|
+
if (kept.length) assignments[sid] = kept;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { schemaVersion: TAG_SCHEMA_VERSION, tags, assignments };
|
|
381
|
+
}
|
|
382
|
+
function createTagIndex(options = {}) {
|
|
383
|
+
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || DEFAULT_TAG_DIR;
|
|
384
|
+
const indexPath = options.indexPath || join2(dir, "tags.json");
|
|
385
|
+
let mutation = Promise.resolve();
|
|
386
|
+
async function read() {
|
|
387
|
+
try {
|
|
388
|
+
return normalizeTagStore(JSON.parse(readFileSync2(indexPath, "utf8")));
|
|
389
|
+
} catch {
|
|
390
|
+
return normalizeTagStore(null);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function write(store) {
|
|
394
|
+
await mkdir2(dir, { recursive: true });
|
|
395
|
+
const tmp = join2(dir, `.tags-${process.pid}-${Date.now()}.tmp`);
|
|
396
|
+
await writeFile2(tmp, JSON.stringify(normalizeTagStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
397
|
+
await fsRename(tmp, indexPath);
|
|
398
|
+
}
|
|
399
|
+
function mutate(mutator) {
|
|
400
|
+
const operation = mutation.then(async () => {
|
|
401
|
+
const store = await read();
|
|
402
|
+
const result = await mutator(store);
|
|
403
|
+
await write(store);
|
|
404
|
+
return result;
|
|
405
|
+
});
|
|
406
|
+
mutation = operation.catch(() => {
|
|
407
|
+
});
|
|
408
|
+
return operation;
|
|
409
|
+
}
|
|
410
|
+
function newTagId(store) {
|
|
411
|
+
for (let i = 0; i < 16; i++) {
|
|
412
|
+
const id = `t_${randomSuffix()}`;
|
|
413
|
+
if (!store.tags.some((t) => t.id === id)) return id;
|
|
414
|
+
}
|
|
415
|
+
return tagError("\u65E0\u6CD5\u751F\u6210\u6807\u7B7E id\uFF08\u968F\u673A\u78B0\u649E\u5F02\u5E38\uFF09", 500, "DSM_TAG_ID_COLLISION");
|
|
416
|
+
}
|
|
417
|
+
async function create(rawName) {
|
|
418
|
+
const name2 = normalizeTagName(rawName);
|
|
419
|
+
if (name2 === null) tagError(`\u6807\u7B7E\u540D\u65E0\u6548\uFF08\u975E\u7A7A\u3001\u4E0D\u542B\u659C\u6760\u3001\u4E0D\u8D85\u8FC7 ${MAX_TAG_NAME} \u4E2A\u5B57\u7B26\uFF09`, 400, "DSM_TAG_NAME_INVALID");
|
|
420
|
+
const key = nameKey(name2);
|
|
421
|
+
return mutate((store) => {
|
|
422
|
+
if (store.tags.some((t) => nameKey(t.name) === key)) tagError("\u540C\u540D\u6807\u7B7E\u5DF2\u5B58\u5728", 409, "DSM_TAG_EXISTS");
|
|
423
|
+
if (store.tags.length >= MAX_TAGS) tagError(`\u6807\u7B7E\u603B\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF08${MAX_TAGS}\uFF09`, 409, "DSM_TAG_LIMIT");
|
|
424
|
+
const tag = { id: newTagId(store), name: name2, createdAt: Date.now() };
|
|
425
|
+
store.tags.push(tag);
|
|
426
|
+
return tag;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
async function rename10(id, rawName) {
|
|
430
|
+
const name2 = normalizeTagName(rawName);
|
|
431
|
+
if (name2 === null) tagError(`\u6807\u7B7E\u540D\u65E0\u6548\uFF08\u975E\u7A7A\u3001\u4E0D\u542B\u659C\u6760\u3001\u4E0D\u8D85\u8FC7 ${MAX_TAG_NAME} \u4E2A\u5B57\u7B26\uFF09`, 400, "DSM_TAG_NAME_INVALID");
|
|
432
|
+
const key = nameKey(name2);
|
|
433
|
+
return mutate((store) => {
|
|
434
|
+
const tag = store.tags.find((t) => t.id === id);
|
|
435
|
+
if (!tag) tagError("\u6807\u7B7E\u4E0D\u5B58\u5728", 404, "DSM_TAG_NOT_FOUND");
|
|
436
|
+
if (store.tags.some((t) => t.id !== tag.id && nameKey(t.name) === key)) tagError("\u540C\u540D\u6807\u7B7E\u5DF2\u5B58\u5728", 409, "DSM_TAG_EXISTS");
|
|
437
|
+
tag.name = name2;
|
|
438
|
+
return tag;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async function merge(fromId, toId) {
|
|
442
|
+
if (!isSafeTagId(fromId) || !isSafeTagId(toId) || fromId === toId) {
|
|
443
|
+
tagError("\u5408\u5E76\u7684\u6E90/\u76EE\u6807\u6807\u7B7E\u65E0\u6548\u6216\u76F8\u540C", 400, "DSM_TAG_INVALID");
|
|
444
|
+
}
|
|
445
|
+
return mutate((store) => {
|
|
446
|
+
if (!store.tags.some((t) => t.id === fromId) || !store.tags.some((t) => t.id === toId)) {
|
|
447
|
+
tagError("\u6807\u7B7E\u4E0D\u5B58\u5728", 404, "DSM_TAG_NOT_FOUND");
|
|
448
|
+
}
|
|
449
|
+
for (const [sid, list2] of Object.entries(store.assignments)) {
|
|
450
|
+
if (!list2.includes(fromId)) continue;
|
|
451
|
+
const merged = [...new Set(list2.filter((x) => x !== fromId).concat(toId))];
|
|
452
|
+
store.assignments[sid] = merged.slice(0, MAX_TAGS_PER_SESSION);
|
|
453
|
+
}
|
|
454
|
+
store.tags = store.tags.filter((t) => t.id !== fromId);
|
|
455
|
+
return { merged: true };
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function removeTag(id) {
|
|
459
|
+
return mutate((store) => {
|
|
460
|
+
if (isSafeTagId(id)) {
|
|
461
|
+
store.tags = store.tags.filter((t) => t.id !== id);
|
|
462
|
+
for (const [sid, list2] of Object.entries(store.assignments)) {
|
|
463
|
+
const kept = list2.filter((x) => x !== id);
|
|
464
|
+
if (kept.length) store.assignments[sid] = kept;
|
|
465
|
+
else delete store.assignments[sid];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { removed: true };
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async function setTags(sessionId, tagIds) {
|
|
472
|
+
if (!isSafeSessionId2(sessionId)) tagError("\u65E0\u6548\u7684 sessionId", 400, "DSM_TAG_SESSION_INVALID");
|
|
473
|
+
if (!Array.isArray(tagIds)) tagError("tagIds \u5FC5\u987B\u662F\u6570\u7EC4", 400, "DSM_TAG_IDS_INVALID");
|
|
474
|
+
const seen = /* @__PURE__ */ new Set();
|
|
475
|
+
const wanted = [];
|
|
476
|
+
for (const v of tagIds) {
|
|
477
|
+
if (!isSafeTagId(v)) tagError("\u65E0\u6548\u7684\u6807\u7B7E id", 400, "DSM_TAG_UNKNOWN");
|
|
478
|
+
if (seen.has(v)) continue;
|
|
479
|
+
seen.add(v);
|
|
480
|
+
wanted.push(v);
|
|
481
|
+
}
|
|
482
|
+
if (wanted.length > MAX_TAGS_PER_SESSION) tagError(`\u5355\u4E2A\u4F1A\u8BDD\u6700\u591A ${MAX_TAGS_PER_SESSION} \u4E2A\u6807\u7B7E`, 409, "DSM_TAG_LIMIT");
|
|
483
|
+
return mutate((store) => {
|
|
484
|
+
const known = new Set(store.tags.map((t) => t.id));
|
|
485
|
+
const unknown = wanted.filter((id) => !known.has(id));
|
|
486
|
+
if (unknown.length) tagError(`\u672A\u77E5\u7684\u6807\u7B7E id\uFF1A${unknown.join("\u3001")}`, 400, "DSM_TAG_UNKNOWN");
|
|
487
|
+
if (wanted.length) store.assignments[sessionId] = [...wanted];
|
|
488
|
+
else delete store.assignments[sessionId];
|
|
489
|
+
return store.assignments;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function removeIds(ids) {
|
|
493
|
+
const wanted = (Array.isArray(ids) ? ids : []).filter(isSafeSessionId2);
|
|
494
|
+
return mutate((store) => {
|
|
495
|
+
for (const sid of wanted) delete store.assignments[sid];
|
|
496
|
+
return store.assignments;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function list() {
|
|
500
|
+
const store = await read();
|
|
501
|
+
return { tags: store.tags, assignments: store.assignments };
|
|
502
|
+
}
|
|
503
|
+
return { read, write, mutate, list, create, rename: rename10, merge, removeTag, setTags, removeIds, indexPath, dir };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/saved-filters.js
|
|
507
|
+
import { mkdir as mkdir3, rename as rename2, writeFile as writeFile3 } from "node:fs/promises";
|
|
508
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
509
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
510
|
+
import { homedir as homedir3 } from "node:os";
|
|
511
|
+
import { join as join3 } from "node:path";
|
|
512
|
+
var FILTER_SCHEMA_VERSION = 1;
|
|
513
|
+
var MAX_FILTERS = 20;
|
|
514
|
+
var MAX_FILTER_NAME = 40;
|
|
515
|
+
var MAX_FILTER_JSON = 2048;
|
|
516
|
+
var DEFAULT_FILTER_DIR = join3(homedir3(), ".dsh", "sessions-manager");
|
|
517
|
+
var FILTER_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
518
|
+
function randomSuffix2() {
|
|
519
|
+
const bytes = randomBytes2(8);
|
|
520
|
+
let out = "";
|
|
521
|
+
for (const b of bytes) out += FILTER_ID_ALPHABET[b % FILTER_ID_ALPHABET.length];
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
function isSafeFilterId(value) {
|
|
525
|
+
return typeof value === "string" && value.length > 0 && value.length <= 64 && !/[\\/\0]/.test(value);
|
|
526
|
+
}
|
|
527
|
+
function filterError(message, status, code) {
|
|
528
|
+
const error = new Error(message);
|
|
529
|
+
error.status = status;
|
|
530
|
+
if (code) error.code = code;
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
function normalizeFilterName(value) {
|
|
534
|
+
if (typeof value !== "string") return null;
|
|
535
|
+
const name2 = value.trim();
|
|
536
|
+
if (!name2 || /[\\/\0]/.test(name2)) return null;
|
|
537
|
+
if (Array.from(name2).length > MAX_FILTER_NAME) return null;
|
|
538
|
+
return name2;
|
|
539
|
+
}
|
|
540
|
+
function nameKey2(name2) {
|
|
541
|
+
return name2.toLocaleLowerCase();
|
|
542
|
+
}
|
|
543
|
+
function normalizeFiltersPayload(value) {
|
|
544
|
+
let serialized;
|
|
545
|
+
try {
|
|
546
|
+
serialized = JSON.stringify(value);
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
if (typeof serialized !== "string" || serialized.length > MAX_FILTER_JSON) return null;
|
|
551
|
+
try {
|
|
552
|
+
return JSON.parse(serialized);
|
|
553
|
+
} catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function normalizeFilterStore(raw) {
|
|
558
|
+
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
559
|
+
const items = [];
|
|
560
|
+
const ids = /* @__PURE__ */ new Set();
|
|
561
|
+
const names = /* @__PURE__ */ new Set();
|
|
562
|
+
for (const item of source && Array.isArray(source.items) ? source.items : []) {
|
|
563
|
+
if (!item || typeof item !== "object") continue;
|
|
564
|
+
if (!isSafeFilterId(item.id) || ids.has(item.id)) continue;
|
|
565
|
+
const name2 = normalizeFilterName(item.name);
|
|
566
|
+
if (name2 === null) continue;
|
|
567
|
+
const key = nameKey2(name2);
|
|
568
|
+
if (names.has(key)) continue;
|
|
569
|
+
const filters = normalizeFiltersPayload(item.filters);
|
|
570
|
+
if (filters === null) continue;
|
|
571
|
+
ids.add(item.id);
|
|
572
|
+
names.add(key);
|
|
573
|
+
const createdAt = Number.isFinite(Number(item.createdAt)) ? Number(item.createdAt) : 0;
|
|
574
|
+
items.push({ id: item.id, name: name2, filters, createdAt });
|
|
575
|
+
if (items.length >= MAX_FILTERS) break;
|
|
576
|
+
}
|
|
577
|
+
return { schemaVersion: FILTER_SCHEMA_VERSION, items };
|
|
578
|
+
}
|
|
579
|
+
function createSavedFilters(options = {}) {
|
|
580
|
+
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || DEFAULT_FILTER_DIR;
|
|
581
|
+
const indexPath = options.indexPath || join3(dir, "saved-filters.json");
|
|
582
|
+
let mutation = Promise.resolve();
|
|
583
|
+
async function read() {
|
|
584
|
+
try {
|
|
585
|
+
return normalizeFilterStore(JSON.parse(readFileSync3(indexPath, "utf8")));
|
|
586
|
+
} catch {
|
|
587
|
+
return normalizeFilterStore(null);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function write(store) {
|
|
591
|
+
await mkdir3(dir, { recursive: true });
|
|
592
|
+
const tmp = join3(dir, `.saved-filters-${process.pid}-${Date.now()}.tmp`);
|
|
593
|
+
await writeFile3(tmp, JSON.stringify(normalizeFilterStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
594
|
+
await rename2(tmp, indexPath);
|
|
595
|
+
}
|
|
596
|
+
function mutate(mutator) {
|
|
597
|
+
const operation = mutation.then(async () => {
|
|
598
|
+
const store = await read();
|
|
599
|
+
const result = await mutator(store);
|
|
600
|
+
await write(store);
|
|
601
|
+
return result;
|
|
602
|
+
});
|
|
603
|
+
mutation = operation.catch(() => {
|
|
604
|
+
});
|
|
605
|
+
return operation;
|
|
606
|
+
}
|
|
607
|
+
function newFilterId(store) {
|
|
608
|
+
for (let i = 0; i < 16; i++) {
|
|
609
|
+
const id = `f_${randomSuffix2()}`;
|
|
610
|
+
if (!store.items.some((t) => t.id === id)) return id;
|
|
611
|
+
}
|
|
612
|
+
return filterError("\u65E0\u6CD5\u751F\u6210\u7B5B\u9009 id\uFF08\u968F\u673A\u78B0\u649E\u5F02\u5E38\uFF09", 500, "DSM_FILTER_ID_COLLISION");
|
|
613
|
+
}
|
|
614
|
+
async function save(rawName, filters) {
|
|
615
|
+
const name2 = normalizeFilterName(rawName);
|
|
616
|
+
if (name2 === null) filterError(`\u7B5B\u9009\u540D\u65E0\u6548\uFF08\u975E\u7A7A\u3001\u4E0D\u542B\u659C\u6760\u3001\u4E0D\u8D85\u8FC7 ${MAX_FILTER_NAME} \u4E2A\u5B57\u7B26\uFF09`, 400, "DSM_FILTER_NAME_INVALID");
|
|
617
|
+
let serialized;
|
|
618
|
+
try {
|
|
619
|
+
serialized = JSON.stringify(filters);
|
|
620
|
+
} catch {
|
|
621
|
+
serialized = void 0;
|
|
622
|
+
}
|
|
623
|
+
if (typeof serialized !== "string") filterError("\u7B5B\u9009\u6761\u4EF6\u5FC5\u987B\u662F\u53EF JSON \u5E8F\u5217\u5316\u7684\u6570\u636E", 400, "DSM_FILTER_INVALID");
|
|
624
|
+
if (serialized === "null") filterError("\u7B5B\u9009\u6761\u4EF6\u4E0D\u80FD\u4E3A null", 400, "DSM_FILTER_INVALID");
|
|
625
|
+
if (serialized.length > MAX_FILTER_JSON) filterError(`\u7B5B\u9009\u6761\u4EF6\u8FC7\u5927\uFF08\u5E8F\u5217\u5316\u540E\u6700\u591A ${MAX_FILTER_JSON} \u5B57\u7B26\uFF09`, 400, "DSM_FILTER_TOO_LARGE");
|
|
626
|
+
const payload = JSON.parse(serialized);
|
|
627
|
+
const key = nameKey2(name2);
|
|
628
|
+
return mutate((store) => {
|
|
629
|
+
if (store.items.some((t) => nameKey2(t.name) === key)) filterError("\u540C\u540D\u7B5B\u9009\u5DF2\u5B58\u5728", 409, "DSM_FILTER_EXISTS");
|
|
630
|
+
if (store.items.length >= MAX_FILTERS) filterError(`\u4FDD\u5B58\u7684\u7B5B\u9009\u5DF2\u8FBE\u4E0A\u9650\uFF08${MAX_FILTERS}\uFF09`, 409, "DSM_FILTER_LIMIT");
|
|
631
|
+
const item = { id: newFilterId(store), name: name2, filters: payload, createdAt: Date.now() };
|
|
632
|
+
store.items.push(item);
|
|
633
|
+
return item;
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function remove(ids) {
|
|
637
|
+
const wanted = new Set((Array.isArray(ids) ? ids : []).filter(isSafeFilterId).map(String));
|
|
638
|
+
return mutate((store) => {
|
|
639
|
+
const before = store.items.length;
|
|
640
|
+
store.items = store.items.filter((t) => !wanted.has(t.id));
|
|
641
|
+
return before - store.items.length;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
async function list() {
|
|
645
|
+
const store = await read();
|
|
646
|
+
return store.items;
|
|
647
|
+
}
|
|
648
|
+
return { read, write, mutate, list, save, remove, indexPath, dir };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/pending-moves.js
|
|
652
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
653
|
+
import { mkdir as mkdir4, rename as rename3, writeFile as writeFile4 } from "node:fs/promises";
|
|
654
|
+
import { homedir as homedir4 } from "node:os";
|
|
655
|
+
import { join as join4 } from "node:path";
|
|
311
656
|
var SCHEMA_VERSION = 1;
|
|
312
|
-
var DEFAULT_DIR = process.env.DSH_SESSIONS_MANAGER_PENDING_DIR || process.env.DSH_SESSIONS_MANAGER_STAR_DIR ||
|
|
657
|
+
var DEFAULT_DIR = process.env.DSH_SESSIONS_MANAGER_PENDING_DIR || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join4(homedir4(), ".dsh", "sessions-manager");
|
|
313
658
|
var MAX_ITEMS = 50;
|
|
314
659
|
var MAX_ATTEMPTS = 5;
|
|
315
660
|
function isSafeId(value) {
|
|
@@ -335,20 +680,20 @@ function normalizeStore(raw) {
|
|
|
335
680
|
}
|
|
336
681
|
function createPendingMoveStore(options = {}) {
|
|
337
682
|
const dir = options.dir || DEFAULT_DIR;
|
|
338
|
-
const indexPath = options.indexPath ||
|
|
683
|
+
const indexPath = options.indexPath || join4(dir, "pending-moves.json");
|
|
339
684
|
let mutation = Promise.resolve();
|
|
340
685
|
async function read() {
|
|
341
686
|
try {
|
|
342
|
-
return normalizeStore(JSON.parse(
|
|
687
|
+
return normalizeStore(JSON.parse(readFileSync4(indexPath, "utf8")));
|
|
343
688
|
} catch (e) {
|
|
344
689
|
return normalizeStore(null);
|
|
345
690
|
}
|
|
346
691
|
}
|
|
347
692
|
async function write(store) {
|
|
348
|
-
await
|
|
349
|
-
const tmp =
|
|
350
|
-
await
|
|
351
|
-
await
|
|
693
|
+
await mkdir4(dir, { recursive: true });
|
|
694
|
+
const tmp = join4(dir, `.pending-moves-${process.pid}-${Date.now()}.tmp`);
|
|
695
|
+
await writeFile4(tmp, JSON.stringify(normalizeStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
696
|
+
await rename3(tmp, indexPath);
|
|
352
697
|
}
|
|
353
698
|
function mutate(mutator) {
|
|
354
699
|
const operation = mutation.then(async () => {
|
|
@@ -398,9 +743,106 @@ function createPendingMoveStore(options = {}) {
|
|
|
398
743
|
};
|
|
399
744
|
}
|
|
400
745
|
|
|
746
|
+
// src/move-notices.js
|
|
747
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
748
|
+
import { mkdir as mkdir5, rename as rename4, writeFile as writeFile5 } from "node:fs/promises";
|
|
749
|
+
import { homedir as homedir5 } from "node:os";
|
|
750
|
+
import { join as join5 } from "node:path";
|
|
751
|
+
var SCHEMA_VERSION2 = 1;
|
|
752
|
+
var MAX_NOTICES = 20;
|
|
753
|
+
var NOTICE_MAX_AGE_MS = 7 * 24 * 3600 * 1e3;
|
|
754
|
+
var KINDS = /* @__PURE__ */ new Set(["moved", "abandoned"]);
|
|
755
|
+
function isSafeId2(value) {
|
|
756
|
+
return typeof value === "string" && value.length > 0 && value.length <= 200;
|
|
757
|
+
}
|
|
758
|
+
function str(v, max) {
|
|
759
|
+
return typeof v === "string" && v ? v.slice(0, max) : null;
|
|
760
|
+
}
|
|
761
|
+
function normalizeNotice(raw) {
|
|
762
|
+
if (!raw || typeof raw !== "object") return null;
|
|
763
|
+
if (!KINDS.has(raw.kind) || !isSafeId2(raw.sessionId)) return null;
|
|
764
|
+
const at = Number.isFinite(raw.at) && raw.at > 0 ? Math.floor(raw.at) : 0;
|
|
765
|
+
if (!at) return null;
|
|
766
|
+
const sessionId = String(raw.sessionId);
|
|
767
|
+
return {
|
|
768
|
+
id: str(raw.id, 400) || `${sessionId}:${raw.kind}:${at}`,
|
|
769
|
+
kind: raw.kind,
|
|
770
|
+
sessionId,
|
|
771
|
+
targetPath: str(raw.targetPath, 500),
|
|
772
|
+
attempts: Number.isSafeInteger(raw.attempts) && raw.attempts >= 0 ? raw.attempts : null,
|
|
773
|
+
reason: str(raw.reason, 300),
|
|
774
|
+
at
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function normalizeStore2(raw) {
|
|
778
|
+
const source = raw && Array.isArray(raw.items) ? raw.items : [];
|
|
779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
780
|
+
const items = [];
|
|
781
|
+
for (const entry of source) {
|
|
782
|
+
const n = normalizeNotice(entry);
|
|
783
|
+
if (!n || seen.has(n.id)) continue;
|
|
784
|
+
seen.add(n.id);
|
|
785
|
+
items.push(n);
|
|
786
|
+
}
|
|
787
|
+
items.sort((a, b) => a.at - b.at);
|
|
788
|
+
const trimmed = items.length > MAX_NOTICES ? items.slice(items.length - MAX_NOTICES) : items;
|
|
789
|
+
return { schemaVersion: SCHEMA_VERSION2, items: trimmed };
|
|
790
|
+
}
|
|
791
|
+
function createMoveNoticeStore(options = {}) {
|
|
792
|
+
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_PENDING_DIR || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join5(homedir5(), ".dsh", "sessions-manager");
|
|
793
|
+
const indexPath = options.indexPath || join5(dir, "move-notices.json");
|
|
794
|
+
let mutation = Promise.resolve();
|
|
795
|
+
function read() {
|
|
796
|
+
try {
|
|
797
|
+
return normalizeStore2(JSON.parse(readFileSync5(indexPath, "utf8")));
|
|
798
|
+
} catch (e) {
|
|
799
|
+
return normalizeStore2(null);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function write(store) {
|
|
803
|
+
await mkdir5(dir, { recursive: true });
|
|
804
|
+
const tmp = join5(dir, `.move-notices-${process.pid}-${Date.now()}.tmp`);
|
|
805
|
+
await writeFile5(tmp, JSON.stringify(normalizeStore2(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
806
|
+
await rename4(tmp, indexPath);
|
|
807
|
+
}
|
|
808
|
+
function mutate(mutator) {
|
|
809
|
+
const operation = mutation.then(async () => {
|
|
810
|
+
const store = read();
|
|
811
|
+
const result = await mutator(store);
|
|
812
|
+
await write(store);
|
|
813
|
+
return result;
|
|
814
|
+
});
|
|
815
|
+
mutation = operation.catch(() => {
|
|
816
|
+
});
|
|
817
|
+
return operation;
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
indexPath,
|
|
821
|
+
// 终局落一条通知;同 id(会话+类型+时刻)幂等去重。
|
|
822
|
+
append: (notice) => mutate((store) => {
|
|
823
|
+
const n = normalizeNotice(notice);
|
|
824
|
+
if (!n) return null;
|
|
825
|
+
if (store.items.some((x) => x.id === n.id)) return n;
|
|
826
|
+
store.items.push(n);
|
|
827
|
+
return n;
|
|
828
|
+
}),
|
|
829
|
+
// 只读列出未过期、未 ack 的通知(按时间升序)。过期项在下次写入时随归一化清掉。
|
|
830
|
+
async list() {
|
|
831
|
+
const now = Date.now();
|
|
832
|
+
return read().items.filter((n) => now - n.at <= NOTICE_MAX_AGE_MS);
|
|
833
|
+
},
|
|
834
|
+
ack: (ids) => mutate((store) => {
|
|
835
|
+
const drop = new Set((Array.isArray(ids) ? ids : []).filter((v) => typeof v === "string" && v.length <= 400).map(String));
|
|
836
|
+
const before = store.items.length;
|
|
837
|
+
store.items = store.items.filter((n) => !drop.has(n.id));
|
|
838
|
+
return before - store.items.length;
|
|
839
|
+
})
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
401
843
|
// src/state-temp-sweep.js
|
|
402
844
|
import { readdir, rm, stat } from "node:fs/promises";
|
|
403
|
-
import { join as
|
|
845
|
+
import { join as join6 } from "node:path";
|
|
404
846
|
var TEMP_NAME_RE = /^\..+-\d+-\d+\.tmp$/;
|
|
405
847
|
async function sweepStaleStateTemps(dirs, options = {}) {
|
|
406
848
|
const maxAgeMs = Number.isSafeInteger(options.maxAgeMs) && options.maxAgeMs >= 0 ? options.maxAgeMs : 36e5;
|
|
@@ -416,7 +858,7 @@ async function sweepStaleStateTemps(dirs, options = {}) {
|
|
|
416
858
|
}
|
|
417
859
|
for (const name2 of entries) {
|
|
418
860
|
if (!TEMP_NAME_RE.test(name2)) continue;
|
|
419
|
-
const path =
|
|
861
|
+
const path = join6(dir, name2);
|
|
420
862
|
let info;
|
|
421
863
|
try {
|
|
422
864
|
info = await stat(path);
|
|
@@ -487,15 +929,15 @@ function aggregateStorage(items, options = {}) {
|
|
|
487
929
|
}
|
|
488
930
|
|
|
489
931
|
// src/auto-archive.js
|
|
490
|
-
import { mkdir as
|
|
491
|
-
import { readFileSync as
|
|
492
|
-
import { homedir as
|
|
493
|
-
import { join as
|
|
932
|
+
import { mkdir as mkdir6, rename as rename5, writeFile as writeFile6 } from "node:fs/promises";
|
|
933
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
934
|
+
import { homedir as homedir6 } from "node:os";
|
|
935
|
+
import { join as join7 } from "node:path";
|
|
494
936
|
var AUTO_ARCHIVE_SCHEMA_VERSION = 4;
|
|
495
937
|
var INACTIVE_DAY_OPTIONS = Object.freeze([0, 30, 60, 90]);
|
|
496
938
|
var DAY_MS = 864e5;
|
|
497
939
|
var RUN_INTERVAL_MS = DAY_MS;
|
|
498
|
-
var DEFAULT_DIR2 =
|
|
940
|
+
var DEFAULT_DIR2 = join7(homedir6(), ".dsh", "sessions-manager");
|
|
499
941
|
function normalizeAutoArchiveStore(raw) {
|
|
500
942
|
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
501
943
|
const settings = source.settings && typeof source.settings === "object" ? source.settings : {};
|
|
@@ -540,20 +982,20 @@ function pickInactiveCandidates(items, options = {}) {
|
|
|
540
982
|
}
|
|
541
983
|
function createAutoArchiveStore(options = {}) {
|
|
542
984
|
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR2;
|
|
543
|
-
const indexPath = options.indexPath ||
|
|
985
|
+
const indexPath = options.indexPath || join7(dir, "auto-archive.json");
|
|
544
986
|
let mutation = Promise.resolve();
|
|
545
987
|
async function read() {
|
|
546
988
|
try {
|
|
547
|
-
return normalizeAutoArchiveStore(JSON.parse(
|
|
989
|
+
return normalizeAutoArchiveStore(JSON.parse(readFileSync6(indexPath, "utf8")));
|
|
548
990
|
} catch {
|
|
549
991
|
return normalizeAutoArchiveStore(null);
|
|
550
992
|
}
|
|
551
993
|
}
|
|
552
994
|
async function write(store) {
|
|
553
|
-
await
|
|
554
|
-
const tmp =
|
|
555
|
-
await
|
|
556
|
-
await
|
|
995
|
+
await mkdir6(dir, { recursive: true });
|
|
996
|
+
const tmp = join7(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`);
|
|
997
|
+
await writeFile6(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
998
|
+
await rename5(tmp, indexPath);
|
|
557
999
|
}
|
|
558
1000
|
function mutate(mutator) {
|
|
559
1001
|
const operation = mutation.then(async () => {
|
|
@@ -700,9 +1142,9 @@ function createSessionMetaCache(opts = {}) {
|
|
|
700
1142
|
}
|
|
701
1143
|
|
|
702
1144
|
// src/title-persist-index.js
|
|
703
|
-
import { mkdir as
|
|
704
|
-
import { dirname, join as
|
|
705
|
-
var TITLE_INDEX_SCHEMA_VERSION =
|
|
1145
|
+
import { mkdir as mkdir7, readFile, rename as rename6, writeFile as writeFile7 } from "node:fs/promises";
|
|
1146
|
+
import { dirname, join as join8 } from "node:path";
|
|
1147
|
+
var TITLE_INDEX_SCHEMA_VERSION = 2;
|
|
706
1148
|
var MAX_ENTRIES = 2e4;
|
|
707
1149
|
function normalizeEntry(raw) {
|
|
708
1150
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -711,9 +1153,13 @@ function normalizeEntry(raw) {
|
|
|
711
1153
|
const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
|
|
712
1154
|
const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
|
|
713
1155
|
const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
|
|
1156
|
+
const resolved = raw.resolved === true || raw.resolved === 1;
|
|
714
1157
|
if (!fingerprint || fingerprint.startsWith("rev:")) return null;
|
|
715
1158
|
if (!title && !cwd) return null;
|
|
716
|
-
|
|
1159
|
+
if (!title && !resolved) return null;
|
|
1160
|
+
const entry = { title, cwd, createdAt, fingerprint, updatedAt };
|
|
1161
|
+
if (resolved) entry.resolved = 1;
|
|
1162
|
+
return entry;
|
|
717
1163
|
}
|
|
718
1164
|
function normalizeTitleIndex(raw) {
|
|
719
1165
|
const entries = {};
|
|
@@ -739,7 +1185,7 @@ function mergeEntries(left, right) {
|
|
|
739
1185
|
function createTitleIndexStore({ dir, file }) {
|
|
740
1186
|
let cache = null;
|
|
741
1187
|
let chain = Promise.resolve();
|
|
742
|
-
const path = file ||
|
|
1188
|
+
const path = file || join8(dir, "title-index.json");
|
|
743
1189
|
async function readRaw() {
|
|
744
1190
|
try {
|
|
745
1191
|
return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
|
|
@@ -774,10 +1220,10 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
774
1220
|
if (!Object.keys(right).length) return false;
|
|
775
1221
|
await enqueue(async (store) => {
|
|
776
1222
|
const next = mergeEntries(store, right);
|
|
777
|
-
await
|
|
778
|
-
const tmp =
|
|
779
|
-
await
|
|
780
|
-
await
|
|
1223
|
+
await mkdir7(dirname(path), { recursive: true });
|
|
1224
|
+
const tmp = join8(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
1225
|
+
await writeFile7(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
|
|
1226
|
+
await rename6(tmp, path);
|
|
781
1227
|
cache = next;
|
|
782
1228
|
});
|
|
783
1229
|
return true;
|
|
@@ -794,10 +1240,110 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
794
1240
|
}
|
|
795
1241
|
}
|
|
796
1242
|
if (!changed) return;
|
|
797
|
-
await
|
|
798
|
-
const tmp =
|
|
799
|
-
await
|
|
800
|
-
await
|
|
1243
|
+
await mkdir7(dirname(path), { recursive: true });
|
|
1244
|
+
const tmp = join8(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
1245
|
+
await writeFile7(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
|
|
1246
|
+
await rename6(tmp, path);
|
|
1247
|
+
});
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/empty-scan-index.js
|
|
1254
|
+
import { mkdir as mkdir8, readFile as readFile2, rename as rename7, writeFile as writeFile8 } from "node:fs/promises";
|
|
1255
|
+
import { dirname as dirname2, join as join9 } from "node:path";
|
|
1256
|
+
var EMPTY_SCAN_SCHEMA_VERSION = 1;
|
|
1257
|
+
var MAX_ENTRIES2 = 2e4;
|
|
1258
|
+
function normalizeEntry2(raw) {
|
|
1259
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1260
|
+
const empty = raw.empty === 1 || raw.empty === true ? 1 : raw.empty === 0 || raw.empty === false ? 0 : null;
|
|
1261
|
+
const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
|
|
1262
|
+
const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
|
|
1263
|
+
if (empty === null || !fingerprint || fingerprint.startsWith("rev:")) return null;
|
|
1264
|
+
return { empty, fingerprint, updatedAt };
|
|
1265
|
+
}
|
|
1266
|
+
function normalizeEmptyScanIndex(raw) {
|
|
1267
|
+
const entries = {};
|
|
1268
|
+
if (raw && typeof raw === "object" && raw.entries && typeof raw.entries === "object") {
|
|
1269
|
+
for (const [id, entry] of Object.entries(raw.entries)) {
|
|
1270
|
+
if (typeof id !== "string" || !id || id.length > 200) continue;
|
|
1271
|
+
const normalized = normalizeEntry2(entry);
|
|
1272
|
+
if (normalized) entries[id] = normalized;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return { schemaVersion: EMPTY_SCAN_SCHEMA_VERSION, entries };
|
|
1276
|
+
}
|
|
1277
|
+
function mergeEntries2(left, right) {
|
|
1278
|
+
const merged = { ...left };
|
|
1279
|
+
for (const [id, entry] of Object.entries(right)) merged[id] = entry;
|
|
1280
|
+
const ids = Object.keys(merged);
|
|
1281
|
+
if (ids.length > MAX_ENTRIES2) {
|
|
1282
|
+
ids.sort((a, b) => (merged[a].updatedAt || 0) - (merged[b].updatedAt || 0));
|
|
1283
|
+
for (const id of ids.slice(0, ids.length - MAX_ENTRIES2)) delete merged[id];
|
|
1284
|
+
}
|
|
1285
|
+
return merged;
|
|
1286
|
+
}
|
|
1287
|
+
function createEmptyScanStore({ dir, file }) {
|
|
1288
|
+
let cache = null;
|
|
1289
|
+
let chain = Promise.resolve();
|
|
1290
|
+
const path = file || join9(dir, "empty-scan.json");
|
|
1291
|
+
async function readRaw() {
|
|
1292
|
+
try {
|
|
1293
|
+
return normalizeEmptyScanIndex(JSON.parse(await readFile2(path, "utf8")));
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
return normalizeEmptyScanIndex(null);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function enqueue(mutator) {
|
|
1299
|
+
const operation = chain.then(async () => {
|
|
1300
|
+
const store = cache || (cache = (await readRaw()).entries);
|
|
1301
|
+
await mutator(store);
|
|
1302
|
+
return store;
|
|
1303
|
+
});
|
|
1304
|
+
chain = operation.catch(() => {
|
|
1305
|
+
});
|
|
1306
|
+
return operation;
|
|
1307
|
+
}
|
|
1308
|
+
return {
|
|
1309
|
+
async entries() {
|
|
1310
|
+
if (cache) return cache;
|
|
1311
|
+
cache = (await readRaw()).entries;
|
|
1312
|
+
return cache;
|
|
1313
|
+
},
|
|
1314
|
+
async merge(batch) {
|
|
1315
|
+
const right = {};
|
|
1316
|
+
for (const [id, entry] of Object.entries(batch || {})) {
|
|
1317
|
+
const normalized = normalizeEntry2(entry);
|
|
1318
|
+
if (normalized) right[String(id)] = normalized;
|
|
1319
|
+
}
|
|
1320
|
+
if (!Object.keys(right).length) return false;
|
|
1321
|
+
await enqueue(async (store) => {
|
|
1322
|
+
const next = mergeEntries2(store, right);
|
|
1323
|
+
await mkdir8(dirname2(path), { recursive: true });
|
|
1324
|
+
const tmp = join9(dirname2(path), `.empty-scan-${process.pid}-${Date.now()}.tmp`);
|
|
1325
|
+
await writeFile8(tmp, JSON.stringify({ schemaVersion: EMPTY_SCAN_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
|
|
1326
|
+
await rename7(tmp, path);
|
|
1327
|
+
cache = next;
|
|
1328
|
+
});
|
|
1329
|
+
return true;
|
|
1330
|
+
},
|
|
1331
|
+
async remove(ids) {
|
|
1332
|
+
const wanted = new Set((ids || []).map(String));
|
|
1333
|
+
if (!wanted.size) return false;
|
|
1334
|
+
await enqueue(async (store) => {
|
|
1335
|
+
let changed = false;
|
|
1336
|
+
for (const id of wanted) {
|
|
1337
|
+
if (id in store) {
|
|
1338
|
+
delete store[id];
|
|
1339
|
+
changed = true;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (!changed) return;
|
|
1343
|
+
await mkdir8(dirname2(path), { recursive: true });
|
|
1344
|
+
const tmp = join9(dirname2(path), `.empty-scan-${process.pid}-${Date.now()}.tmp`);
|
|
1345
|
+
await writeFile8(tmp, JSON.stringify({ schemaVersion: EMPTY_SCAN_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
|
|
1346
|
+
await rename7(tmp, path);
|
|
801
1347
|
});
|
|
802
1348
|
return true;
|
|
803
1349
|
}
|
|
@@ -806,7 +1352,7 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
806
1352
|
|
|
807
1353
|
// src/handle-era-paths.js
|
|
808
1354
|
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
809
|
-
import { basename, join as
|
|
1355
|
+
import { basename, join as join10 } from "node:path";
|
|
810
1356
|
|
|
811
1357
|
// src/path-guard.js
|
|
812
1358
|
function splitSegments(target) {
|
|
@@ -879,8 +1425,8 @@ function resolveSessionRoot(sp) {
|
|
|
879
1425
|
return typeof root === "string" && root.length > 0 ? root : null;
|
|
880
1426
|
}
|
|
881
1427
|
function deriveSessionDir(root, cwd, id) {
|
|
882
|
-
const project = cwd === void 0 || cwd === null || cwd === "" ?
|
|
883
|
-
return
|
|
1428
|
+
const project = cwd === void 0 || cwd === null || cwd === "" ? join10(root, "_no-cwd") : join10(root, projectKeyFor(cwd));
|
|
1429
|
+
return join10(project, encodeSegmentFor(id));
|
|
884
1430
|
}
|
|
885
1431
|
function generationVersionOf(name2) {
|
|
886
1432
|
return Number((String(name2).match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
@@ -907,11 +1453,11 @@ async function locateSessionArtifacts(sp, header) {
|
|
|
907
1453
|
const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
|
|
908
1454
|
if (generationFiles.length === 0) return null;
|
|
909
1455
|
generationFiles.sort((a, b) => generationVersionOf(b) - generationVersionOf(a));
|
|
910
|
-
const logPath =
|
|
1456
|
+
const logPath = join10(sessionDir, generationFiles[0]);
|
|
911
1457
|
if (!pathOwnsSession(logPath, sid)) return null;
|
|
912
1458
|
return {
|
|
913
1459
|
root,
|
|
914
|
-
projectDir:
|
|
1460
|
+
projectDir: join10(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
|
|
915
1461
|
sessionDir,
|
|
916
1462
|
logPath,
|
|
917
1463
|
generationFiles,
|
|
@@ -1121,8 +1667,8 @@ function requireCapability(capabilities, name2) {
|
|
|
1121
1667
|
}
|
|
1122
1668
|
|
|
1123
1669
|
// src/handle-era-ops.js
|
|
1124
|
-
import { mkdir as
|
|
1125
|
-
import { basename as basename2, dirname as
|
|
1670
|
+
import { mkdir as mkdir9, readFile as readFile3, readdir as readdir3, rename as rename8, rm as rm2, stat as stat3, writeFile as writeFile9 } from "node:fs/promises";
|
|
1671
|
+
import { basename as basename2, dirname as dirname3, join as join11 } from "node:path";
|
|
1126
1672
|
var MOVE_BATCH = 400;
|
|
1127
1673
|
var REAL_LOG_RE = /^session.*\.jsonl(\.zst(d)?)?$/;
|
|
1128
1674
|
var MOVE_TEMP_RE = /\.move-(backup|stage)-/;
|
|
@@ -1145,7 +1691,7 @@ async function cleanSiblingCopies(root, sid, keepDir, keepVersion) {
|
|
|
1145
1691
|
const real = [];
|
|
1146
1692
|
for (const ent of projects) {
|
|
1147
1693
|
if (!ent.isDirectory()) continue;
|
|
1148
|
-
const candidate =
|
|
1694
|
+
const candidate = join11(root, ent.name, segment);
|
|
1149
1695
|
if (keepDir && candidate === keepDir) continue;
|
|
1150
1696
|
let entries;
|
|
1151
1697
|
try {
|
|
@@ -1260,7 +1806,7 @@ async function purgeSessionArtifacts(sp, sid, header) {
|
|
|
1260
1806
|
return artifacts;
|
|
1261
1807
|
}
|
|
1262
1808
|
async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts }) {
|
|
1263
|
-
const original = await
|
|
1809
|
+
const original = await readFile3(backupLogPath);
|
|
1264
1810
|
const frames = scanZstdFrames(original).frames;
|
|
1265
1811
|
if (frames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
1266
1812
|
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
@@ -1270,10 +1816,10 @@ async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifact
|
|
|
1270
1816
|
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1271
1817
|
}
|
|
1272
1818
|
const targetDir = deriveSessionDir(artifacts.root, canonical, sid);
|
|
1273
|
-
await
|
|
1274
|
-
const staged =
|
|
1275
|
-
await
|
|
1276
|
-
await
|
|
1819
|
+
await mkdir9(targetDir, { recursive: true });
|
|
1820
|
+
const staged = join11(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
|
|
1821
|
+
await writeFile9(staged, rewritten, { mode: 384 });
|
|
1822
|
+
await rename8(staged, join11(targetDir, artifacts.generationFiles[0]));
|
|
1277
1823
|
return targetDir;
|
|
1278
1824
|
}
|
|
1279
1825
|
async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
|
|
@@ -1325,11 +1871,11 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1325
1871
|
const firstSeq = events.length ? Number(events[0].seq) : 0;
|
|
1326
1872
|
const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
|
|
1327
1873
|
const createOptions = header.isSeeded && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? { inheritedEventCount } : void 0;
|
|
1328
|
-
const backupRoot =
|
|
1329
|
-
const backupDir =
|
|
1330
|
-
await
|
|
1331
|
-
const backupLogPath =
|
|
1332
|
-
await
|
|
1874
|
+
const backupRoot = join11(dirname3(artifacts.root), "sessions-manager-move-backup");
|
|
1875
|
+
const backupDir = join11(backupRoot, `${encodeSegmentFor(sid)}-${process.pid}-${Date.now()}`);
|
|
1876
|
+
await mkdir9(backupRoot, { recursive: true });
|
|
1877
|
+
const backupLogPath = join11(backupDir, basename2(artifacts.logPath));
|
|
1878
|
+
await rename8(artifacts.sessionDir, backupDir);
|
|
1333
1879
|
let writer = null;
|
|
1334
1880
|
try {
|
|
1335
1881
|
try {
|
|
@@ -1362,11 +1908,11 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1362
1908
|
} catch (_) {
|
|
1363
1909
|
}
|
|
1364
1910
|
try {
|
|
1365
|
-
await
|
|
1911
|
+
await mkdir9(dirname3(artifacts.sessionDir), { recursive: true });
|
|
1366
1912
|
} catch (_) {
|
|
1367
1913
|
}
|
|
1368
1914
|
try {
|
|
1369
|
-
await
|
|
1915
|
+
await rename8(backupDir, artifacts.sessionDir);
|
|
1370
1916
|
} catch (_) {
|
|
1371
1917
|
}
|
|
1372
1918
|
if (e && e.status) throw e;
|
|
@@ -1383,12 +1929,6 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
|
|
|
1383
1929
|
}
|
|
1384
1930
|
|
|
1385
1931
|
// src/lineage.js
|
|
1386
|
-
var EMPTY_BASE = 190;
|
|
1387
|
-
function isEmptyLogSize(sizeBytes, cwd) {
|
|
1388
|
-
if (!Number.isFinite(sizeBytes) || sizeBytes < 0) return false;
|
|
1389
|
-
const cwdLen = typeof cwd === "string" ? cwd.length : 0;
|
|
1390
|
-
return sizeBytes <= EMPTY_BASE + cwdLen;
|
|
1391
|
-
}
|
|
1392
1932
|
var EMPTY_DECODE_LIMIT = 8192;
|
|
1393
1933
|
var LIFECYCLE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1394
1934
|
"session",
|
|
@@ -1403,24 +1943,26 @@ function isEmptyEventTypes(types) {
|
|
|
1403
1943
|
}
|
|
1404
1944
|
return true;
|
|
1405
1945
|
}
|
|
1406
|
-
function classifyLineage(header
|
|
1946
|
+
function classifyLineage(header) {
|
|
1407
1947
|
if (!header || typeof header !== "object") return null;
|
|
1408
1948
|
const origin = header.origin === "subagent" ? "subagent" : null;
|
|
1409
1949
|
const parentSession = typeof header.parentSession === "string" && header.parentSession ? header.parentSession : null;
|
|
1410
1950
|
const delegationDepth = Number.isSafeInteger(header.delegationDepth) && header.delegationDepth > 0 ? header.delegationDepth : 0;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1951
|
+
if (!origin && !parentSession) return null;
|
|
1952
|
+
return { origin, parentSession, delegationDepth, empty: null };
|
|
1953
|
+
}
|
|
1954
|
+
function emptyScanCandidate(sizeBytes) {
|
|
1955
|
+
return Number.isFinite(sizeBytes) && sizeBytes <= EMPTY_DECODE_LIMIT;
|
|
1414
1956
|
}
|
|
1415
1957
|
|
|
1416
1958
|
// src/index.js
|
|
1417
|
-
var BUILD_STAMP = true ? "3.
|
|
1959
|
+
var BUILD_STAMP = true ? "3.7.0+95376789" : "dev";
|
|
1418
1960
|
var name = "dsh-sessions-manager";
|
|
1419
1961
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
1420
1962
|
var MAX_TITLE = 80;
|
|
1421
|
-
var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR ||
|
|
1422
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
1423
|
-
var TRASH_INDEX =
|
|
1963
|
+
var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join12(homedir7(), ".dsh", "sessions-manager");
|
|
1964
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join12(homedir7(), ".dsh", "sessions-manager-trash");
|
|
1965
|
+
var TRASH_INDEX = join12(TRASH_DIR, "index.json");
|
|
1424
1966
|
var TRASH_SCHEMA_VERSION = 2;
|
|
1425
1967
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
1426
1968
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -1467,14 +2009,14 @@ function parseIds(body) {
|
|
|
1467
2009
|
const raw = body && body.sessionIds;
|
|
1468
2010
|
if (!Array.isArray(raw)) return null;
|
|
1469
2011
|
const ids = [];
|
|
1470
|
-
for (const v of raw) if (typeof v === "string" &&
|
|
2012
|
+
for (const v of raw) if (typeof v === "string" && isSafeSessionId3(v)) ids.push(v);
|
|
1471
2013
|
return ids;
|
|
1472
2014
|
}
|
|
1473
|
-
function
|
|
2015
|
+
function isSafeSessionId3(value) {
|
|
1474
2016
|
return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
|
|
1475
2017
|
}
|
|
1476
2018
|
function requireSessionId(value) {
|
|
1477
|
-
if (!
|
|
2019
|
+
if (!isSafeSessionId3(value)) {
|
|
1478
2020
|
const error = new Error("\u65E0\u6548\u7684 sessionId");
|
|
1479
2021
|
error.status = 400;
|
|
1480
2022
|
throw error;
|
|
@@ -1499,20 +2041,6 @@ function getActiveSessionId(context) {
|
|
|
1499
2041
|
}
|
|
1500
2042
|
return null;
|
|
1501
2043
|
}
|
|
1502
|
-
function foldTitle(events) {
|
|
1503
|
-
let found = null;
|
|
1504
|
-
let firstUser = null;
|
|
1505
|
-
for (const ev of events) {
|
|
1506
|
-
if (ev.type === "session/title" && ev.data && typeof ev.data.title === "string" && ev.data.title.length) {
|
|
1507
|
-
found = ev.data.title;
|
|
1508
|
-
}
|
|
1509
|
-
if (firstUser === null && ev.type === "user/message" && ev.data && Array.isArray(ev.data.content)) {
|
|
1510
|
-
const txt = ev.data.content.filter((b) => b && b.type === "text").map((b) => b.text).filter(Boolean).join(" ").trim();
|
|
1511
|
-
if (txt) firstUser = txt;
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
return found || firstUser || null;
|
|
1515
|
-
}
|
|
1516
2044
|
function apply(ctx) {
|
|
1517
2045
|
const w = ctx.workspaceRegistry;
|
|
1518
2046
|
const sp = ctx.sessionPersistence;
|
|
@@ -1522,7 +2050,9 @@ function apply(ctx) {
|
|
|
1522
2050
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
1523
2051
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
1524
2052
|
const metaCache = createSessionMetaCache();
|
|
1525
|
-
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file:
|
|
2053
|
+
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join12(TRASH_DIR, "title-index.json") });
|
|
2054
|
+
const emptyIndex = createEmptyScanStore({ dir: TRASH_DIR });
|
|
2055
|
+
const EMPTY_VERDICT_TTL_MS = 14 * 24 * 3600 * 1e3;
|
|
1526
2056
|
async function hydrateFromPersist(ids, statsById) {
|
|
1527
2057
|
const hits = /* @__PURE__ */ new Map();
|
|
1528
2058
|
if (!ids || !ids.length) return hits;
|
|
@@ -1538,7 +2068,7 @@ function apply(ctx) {
|
|
|
1538
2068
|
if (!stat5 || !entry) continue;
|
|
1539
2069
|
const fp = persistFingerprintOf(stat5);
|
|
1540
2070
|
if (!fp) continue;
|
|
1541
|
-
if (fp && entry.
|
|
2071
|
+
if (entry.fingerprint === fp && (entry.title || entry.resolved)) {
|
|
1542
2072
|
hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
|
|
1543
2073
|
}
|
|
1544
2074
|
}
|
|
@@ -1548,10 +2078,12 @@ function apply(ctx) {
|
|
|
1548
2078
|
if (!decoded || !decoded.size) return;
|
|
1549
2079
|
const batch = {};
|
|
1550
2080
|
const now = Date.now();
|
|
1551
|
-
for (const [id,
|
|
2081
|
+
for (const [id, pack] of decoded) {
|
|
2082
|
+
if (!pack || !pack.resolved) continue;
|
|
2083
|
+
const meta = pack.meta;
|
|
1552
2084
|
const fp = persistFingerprintOf(statsById.get(id));
|
|
1553
2085
|
if (!fp) continue;
|
|
1554
|
-
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
|
|
2086
|
+
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now, resolved: 1 };
|
|
1555
2087
|
}
|
|
1556
2088
|
if (!Object.keys(batch).length) return;
|
|
1557
2089
|
try {
|
|
@@ -1634,6 +2166,60 @@ function apply(ctx) {
|
|
|
1634
2166
|
const warmQueue = /* @__PURE__ */ new Map();
|
|
1635
2167
|
let warmRunning = false;
|
|
1636
2168
|
let warmKickTimer = null;
|
|
2169
|
+
const WARM_MAX_ATTEMPTS = 5;
|
|
2170
|
+
const warmFail = /* @__PURE__ */ new Map();
|
|
2171
|
+
function warmFpOf(stat5) {
|
|
2172
|
+
return fingerprintOf(stat5) || persistFingerprintOf(stat5);
|
|
2173
|
+
}
|
|
2174
|
+
function warmBlocked(id, fp) {
|
|
2175
|
+
const rec = warmFail.get(id);
|
|
2176
|
+
if (!rec) return 0;
|
|
2177
|
+
if (fp && rec.fp && rec.fp !== fp) {
|
|
2178
|
+
warmFail.delete(id);
|
|
2179
|
+
return 0;
|
|
2180
|
+
}
|
|
2181
|
+
const now = Date.now();
|
|
2182
|
+
return rec.nextAt > now ? rec.nextAt - now : 0;
|
|
2183
|
+
}
|
|
2184
|
+
function warmMarkFailure(id, stat5, header) {
|
|
2185
|
+
const rec = warmFail.get(id) || { count: 0, fp: null, nextAt: 0 };
|
|
2186
|
+
rec.count += 1;
|
|
2187
|
+
rec.fp = warmFpOf(stat5) || rec.fp;
|
|
2188
|
+
if (stat5) rec.stat = stat5;
|
|
2189
|
+
if (header) rec.header = header;
|
|
2190
|
+
const backoff = rec.count >= WARM_MAX_ATTEMPTS ? 60 * 60 * 1e3 : Math.min(5 * 60 * 1e3, 15 * 1e3 * Math.pow(2, rec.count));
|
|
2191
|
+
rec.nextAt = Date.now() + backoff;
|
|
2192
|
+
warmFail.set(id, rec);
|
|
2193
|
+
}
|
|
2194
|
+
function warmPendingNow() {
|
|
2195
|
+
if (!warmHasApi) return false;
|
|
2196
|
+
if (warmRunning || warmQueue.size) return true;
|
|
2197
|
+
for (const rec of warmFail.values()) if (rec.nextAt > Date.now()) return true;
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
let warmRetryTimer = null;
|
|
2201
|
+
function scheduleWarmRetry() {
|
|
2202
|
+
if (warmRetryTimer) return;
|
|
2203
|
+
let soonest = 0;
|
|
2204
|
+
for (const rec of warmFail.values()) {
|
|
2205
|
+
const wait = rec.nextAt - Date.now();
|
|
2206
|
+
if (wait <= 0) {
|
|
2207
|
+
soonest = 1;
|
|
2208
|
+
break;
|
|
2209
|
+
}
|
|
2210
|
+
if (!soonest || wait < soonest) soonest = wait;
|
|
2211
|
+
}
|
|
2212
|
+
if (!soonest) return;
|
|
2213
|
+
warmRetryTimer = setTimeout(() => {
|
|
2214
|
+
warmRetryTimer = null;
|
|
2215
|
+
const now = Date.now();
|
|
2216
|
+
for (const [id, rec] of warmFail) {
|
|
2217
|
+
if (rec.nextAt <= now && !warmQueue.has(id)) warmQueue.set(id, { stat: rec.stat || null, header: rec.header || null });
|
|
2218
|
+
}
|
|
2219
|
+
if (warmQueue.size) scheduleWarm();
|
|
2220
|
+
}, soonest);
|
|
2221
|
+
if (typeof warmRetryTimer.unref === "function") warmRetryTimer.unref();
|
|
2222
|
+
}
|
|
1637
2223
|
function scheduleWarm() {
|
|
1638
2224
|
if (warmKickTimer || warmRunning) return;
|
|
1639
2225
|
warmKickTimer = setTimeout(() => {
|
|
@@ -1644,15 +2230,44 @@ function apply(ctx) {
|
|
|
1644
2230
|
}
|
|
1645
2231
|
function enqueueWarm(ids, statsById, entryById) {
|
|
1646
2232
|
if (!ids || !ids.length) return;
|
|
2233
|
+
if (!warmHasApi) return;
|
|
1647
2234
|
for (const id of ids) {
|
|
1648
2235
|
const key = String(id);
|
|
2236
|
+
const stat5 = statsById ? statsById.get(key) || null : null;
|
|
2237
|
+
const fp = warmFpOf(stat5);
|
|
2238
|
+
if (!fp) continue;
|
|
2239
|
+
const blockedMs = warmBlocked(key, fp);
|
|
1649
2240
|
const entry = entryById ? entryById.get(key) : null;
|
|
2241
|
+
const rec = warmFail.get(key);
|
|
2242
|
+
if (rec) {
|
|
2243
|
+
rec.stat = stat5 || rec.stat;
|
|
2244
|
+
if (entry) rec.header = entry.header || rec.header;
|
|
2245
|
+
}
|
|
2246
|
+
if (blockedMs > 0) continue;
|
|
1650
2247
|
warmQueue.set(key, {
|
|
1651
|
-
stat:
|
|
2248
|
+
stat: stat5,
|
|
1652
2249
|
header: entry && entry.header || null
|
|
1653
2250
|
});
|
|
1654
2251
|
}
|
|
1655
|
-
scheduleWarm();
|
|
2252
|
+
if (warmQueue.size) scheduleWarm();
|
|
2253
|
+
scheduleWarmRetry();
|
|
2254
|
+
}
|
|
2255
|
+
async function backfillTrashTitles(decoded) {
|
|
2256
|
+
if (!decoded || !decoded.size) return;
|
|
2257
|
+
try {
|
|
2258
|
+
const items = await readTrash();
|
|
2259
|
+
const worth = items.some((t) => !t.title && decoded.get(String(t.sessionId)) && decoded.get(String(t.sessionId)).meta && decoded.get(String(t.sessionId)).meta.title);
|
|
2260
|
+
if (!worth) return;
|
|
2261
|
+
await mutateTrash((store) => {
|
|
2262
|
+
for (const item of store.items) {
|
|
2263
|
+
if (item.title) continue;
|
|
2264
|
+
const pack = decoded.get(String(item.sessionId));
|
|
2265
|
+
const t = pack && pack.meta && pack.meta.title;
|
|
2266
|
+
if (t) item.title = String(t);
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
} catch (e) {
|
|
2270
|
+
}
|
|
1656
2271
|
}
|
|
1657
2272
|
async function runWarm() {
|
|
1658
2273
|
if (warmRunning) return;
|
|
@@ -1663,21 +2278,27 @@ function apply(ctx) {
|
|
|
1663
2278
|
for (const [id] of batch) warmQueue.delete(id);
|
|
1664
2279
|
try {
|
|
1665
2280
|
const ids = batch.map(([id]) => id);
|
|
1666
|
-
const
|
|
2281
|
+
const resultById = await projectTitlesStatus(ids);
|
|
1667
2282
|
const decoded = /* @__PURE__ */ new Map();
|
|
1668
2283
|
const statsById = /* @__PURE__ */ new Map();
|
|
1669
2284
|
for (const [id, desc] of batch) {
|
|
1670
|
-
const
|
|
1671
|
-
|
|
2285
|
+
const r = resultById.get(id);
|
|
2286
|
+
if (!r || !r.resolved) {
|
|
2287
|
+
warmMarkFailure(id, desc.stat, desc.header);
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
warmFail.delete(id);
|
|
2291
|
+
const meta = metaFromSnapshot(r.snapshot || null);
|
|
1672
2292
|
if (desc.header) {
|
|
1673
2293
|
if (typeof desc.header.cwd === "string") meta.cwd = desc.header.cwd;
|
|
1674
2294
|
if (desc.header.createdAt != null) meta.createdAt = desc.header.createdAt;
|
|
1675
2295
|
}
|
|
1676
2296
|
metaCache.set(id, desc.stat, meta);
|
|
1677
2297
|
statsById.set(id, desc.stat);
|
|
1678
|
-
|
|
2298
|
+
decoded.set(id, { meta, resolved: true });
|
|
1679
2299
|
}
|
|
1680
2300
|
await persistDecoded(decoded, statsById);
|
|
2301
|
+
await backfillTrashTitles(decoded);
|
|
1681
2302
|
} catch (e) {
|
|
1682
2303
|
}
|
|
1683
2304
|
await new Promise((resolve) => setImmediate(resolve));
|
|
@@ -1685,6 +2306,7 @@ function apply(ctx) {
|
|
|
1685
2306
|
} finally {
|
|
1686
2307
|
warmRunning = false;
|
|
1687
2308
|
if (warmQueue.size) scheduleWarm();
|
|
2309
|
+
scheduleWarmRetry();
|
|
1688
2310
|
}
|
|
1689
2311
|
}
|
|
1690
2312
|
async function collectUsage(preloadedEntries) {
|
|
@@ -1742,12 +2364,12 @@ function apply(ctx) {
|
|
|
1742
2364
|
schemaVersion: TRASH_SCHEMA_VERSION,
|
|
1743
2365
|
settings: { retentionDays },
|
|
1744
2366
|
items: raw && Array.isArray(raw.items) ? raw.items : [],
|
|
1745
|
-
purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(
|
|
2367
|
+
purgedSessionIds: raw && Array.isArray(raw.purgedSessionIds) ? [...new Set(raw.purgedSessionIds.filter(isSafeSessionId3).map(String))] : []
|
|
1746
2368
|
};
|
|
1747
2369
|
}
|
|
1748
2370
|
async function readTrashStore() {
|
|
1749
2371
|
try {
|
|
1750
|
-
return normalizeTrashStore(JSON.parse(
|
|
2372
|
+
return normalizeTrashStore(JSON.parse(readFileSync7(TRASH_INDEX, "utf8")));
|
|
1751
2373
|
} catch (e) {
|
|
1752
2374
|
return normalizeTrashStore(null);
|
|
1753
2375
|
}
|
|
@@ -1756,10 +2378,10 @@ function apply(ctx) {
|
|
|
1756
2378
|
return (await readTrashStore()).items;
|
|
1757
2379
|
}
|
|
1758
2380
|
async function writeTrashStore(store) {
|
|
1759
|
-
await
|
|
1760
|
-
const tmp =
|
|
1761
|
-
await
|
|
1762
|
-
await
|
|
2381
|
+
await mkdir10(TRASH_DIR, { recursive: true });
|
|
2382
|
+
const tmp = join12(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
2383
|
+
await writeFile10(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
2384
|
+
await rename9(tmp, TRASH_INDEX);
|
|
1763
2385
|
}
|
|
1764
2386
|
function mutateTrash(mutator) {
|
|
1765
2387
|
const operation = trashMutation.then(async () => {
|
|
@@ -1773,7 +2395,10 @@ function apply(ctx) {
|
|
|
1773
2395
|
return operation;
|
|
1774
2396
|
}
|
|
1775
2397
|
const stars = createStarIndex();
|
|
2398
|
+
const tags = createTagIndex();
|
|
2399
|
+
const savedFilters = createSavedFilters();
|
|
1776
2400
|
const pendingMoves = createPendingMoveStore();
|
|
2401
|
+
const moveNotices = createMoveNoticeStore();
|
|
1777
2402
|
const autoArchive = createAutoArchiveStore();
|
|
1778
2403
|
sweepStaleStateTemps([STATE_DIR, TRASH_DIR], {}).catch(() => {
|
|
1779
2404
|
});
|
|
@@ -1786,6 +2411,15 @@ function apply(ctx) {
|
|
|
1786
2411
|
} catch (e) {
|
|
1787
2412
|
}
|
|
1788
2413
|
}
|
|
2414
|
+
async function gcTags(validIds) {
|
|
2415
|
+
try {
|
|
2416
|
+
const store = await tags.read();
|
|
2417
|
+
const valid = new Set(validIds.map(String));
|
|
2418
|
+
const gone = Object.keys(store.assignments).filter((id) => !valid.has(id));
|
|
2419
|
+
if (gone.length) await tags.removeIds(gone);
|
|
2420
|
+
} catch (e) {
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
1789
2423
|
async function deleteOne(sid) {
|
|
1790
2424
|
requireSessionId(sid);
|
|
1791
2425
|
let header = null;
|
|
@@ -1804,30 +2438,19 @@ function apply(ctx) {
|
|
|
1804
2438
|
cwd = header.cwd || null;
|
|
1805
2439
|
title = header.title || header.meta && header.meta.title || null;
|
|
1806
2440
|
}
|
|
1807
|
-
if (!title) {
|
|
1808
|
-
if (typeof sq.readTitleSnapshot === "function") {
|
|
1809
|
-
try {
|
|
1810
|
-
const snap = unwrapSnapshot(await sq.readTitleSnapshot(sid));
|
|
1811
|
-
if (snap && snap.title && snap.title.title) title = String(snap.title.title);
|
|
1812
|
-
if (snap && snap.session) {
|
|
1813
|
-
if (!cwd) cwd = snap.session.cwd || null;
|
|
1814
|
-
}
|
|
1815
|
-
} catch (e) {
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
2441
|
if (!title) {
|
|
1820
2442
|
try {
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
});
|
|
1827
|
-
if (summary && summary.meta && !cwd) cwd = summary.meta.cwd || null;
|
|
1828
|
-
title = folded;
|
|
2443
|
+
const delStat = persistenceEntry && typeof persistenceEntry.revision === "string" && persistenceEntry.revision ? { revision: persistenceEntry.revision, sizeBytes: Number.isFinite(persistenceEntry.sizeBytes) ? Number(persistenceEntry.sizeBytes) : void 0 } : removedPath ? await stat4(removedPath).then((st) => ({ mtimeMs: st.mtimeMs, size: st.size })) : null;
|
|
2444
|
+
if (delStat) {
|
|
2445
|
+
const m = metaCache.get(sid, delStat) || (await hydrateFromPersist([sid], /* @__PURE__ */ new Map([[sid, delStat]]))).get(sid);
|
|
2446
|
+
if (m && m.title) title = String(m.title);
|
|
2447
|
+
}
|
|
1829
2448
|
} catch (e) {
|
|
1830
2449
|
}
|
|
2450
|
+
if (!title) {
|
|
2451
|
+
const at = authorityTitleCache.get(sid);
|
|
2452
|
+
if (at) title = String(at);
|
|
2453
|
+
}
|
|
1831
2454
|
}
|
|
1832
2455
|
} catch (e) {
|
|
1833
2456
|
}
|
|
@@ -1839,8 +2462,10 @@ function apply(ctx) {
|
|
|
1839
2462
|
const archived = await mutateArchived((list) => ({ next: null, value: list.includes(sid) })).catch(() => false);
|
|
1840
2463
|
await mutateTrash((store) => {
|
|
1841
2464
|
const entry = {
|
|
2465
|
+
// v3.6.2 #7:不再把 cwd/裸 id 冒充标题永久定格——存 null,预热解出后
|
|
2466
|
+
// backfillTrashTitles 补写;渲染端(t.title || t.sessionId)自然降级。
|
|
1842
2467
|
sessionId: sid,
|
|
1843
|
-
title: title ||
|
|
2468
|
+
title: title || null,
|
|
1844
2469
|
cwd: cwd || null,
|
|
1845
2470
|
header: header || null,
|
|
1846
2471
|
originalPath: removedPath || null,
|
|
@@ -2047,6 +2672,10 @@ function apply(ctx) {
|
|
|
2047
2672
|
await stars.removeIds([sid]);
|
|
2048
2673
|
} catch (e) {
|
|
2049
2674
|
}
|
|
2675
|
+
try {
|
|
2676
|
+
await tags.removeIds([sid]);
|
|
2677
|
+
} catch (e) {
|
|
2678
|
+
}
|
|
2050
2679
|
return { ok: true, purged: true };
|
|
2051
2680
|
}
|
|
2052
2681
|
async function trashSettings(next) {
|
|
@@ -2082,8 +2711,8 @@ function apply(ctx) {
|
|
|
2082
2711
|
async function moveTargetWorkspace(rawPath) {
|
|
2083
2712
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
2084
2713
|
let p = String(rawPath).trim();
|
|
2085
|
-
if (p.startsWith("~/")) p =
|
|
2086
|
-
if (!isAbsolute(p)) p =
|
|
2714
|
+
if (p.startsWith("~/")) p = join12(homedir7(), p.slice(2));
|
|
2715
|
+
if (!isAbsolute(p)) p = join12(homedir7(), p);
|
|
2087
2716
|
let canonical = null;
|
|
2088
2717
|
try {
|
|
2089
2718
|
canonical = await realpath(p);
|
|
@@ -2091,7 +2720,7 @@ function apply(ctx) {
|
|
|
2091
2720
|
canonical = null;
|
|
2092
2721
|
}
|
|
2093
2722
|
if (canonical === null) {
|
|
2094
|
-
await
|
|
2723
|
+
await mkdir10(p, { recursive: true });
|
|
2095
2724
|
canonical = await realpath(p);
|
|
2096
2725
|
}
|
|
2097
2726
|
return { canonical, entity: await w.create(canonical, basename3(canonical) || "workspace") };
|
|
@@ -2136,15 +2765,15 @@ function apply(ctx) {
|
|
|
2136
2765
|
const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
|
|
2137
2766
|
let destinationInstalled = false;
|
|
2138
2767
|
try {
|
|
2139
|
-
await
|
|
2768
|
+
await mkdir10(dirname4(newPath), { recursive: true });
|
|
2140
2769
|
try {
|
|
2141
2770
|
await stat4(newPath);
|
|
2142
2771
|
throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
|
|
2143
2772
|
} catch (e) {
|
|
2144
2773
|
if (e && e.code !== "ENOENT") throw e;
|
|
2145
2774
|
}
|
|
2146
|
-
await
|
|
2147
|
-
const original = await
|
|
2775
|
+
await rename9(oldPath, backupPath);
|
|
2776
|
+
const original = await readFile4(backupPath);
|
|
2148
2777
|
const originalFrames = scanZstdFrames(original).frames;
|
|
2149
2778
|
if (originalFrames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
2150
2779
|
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
@@ -2153,11 +2782,11 @@ function apply(ctx) {
|
|
|
2153
2782
|
const originalTail = original.subarray(originalFrames[0].end);
|
|
2154
2783
|
const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
|
|
2155
2784
|
if (!originalTail.equals(rewrittenTail)) throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
2156
|
-
await
|
|
2157
|
-
await
|
|
2785
|
+
await writeFile10(stagedPath, rewritten, { mode: 384 });
|
|
2786
|
+
await rename9(stagedPath, newPath);
|
|
2158
2787
|
destinationInstalled = true;
|
|
2159
2788
|
await unlink(backupPath);
|
|
2160
|
-
const keptInSource = await cleanupMovedSourceDir(
|
|
2789
|
+
const keptInSource = await cleanupMovedSourceDir(dirname4(oldPath));
|
|
2161
2790
|
if (keptInSource.length > 0) moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${keptInSource.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
|
|
2162
2791
|
} catch (e) {
|
|
2163
2792
|
try {
|
|
@@ -2171,7 +2800,7 @@ function apply(ctx) {
|
|
|
2171
2800
|
}
|
|
2172
2801
|
}
|
|
2173
2802
|
try {
|
|
2174
|
-
await
|
|
2803
|
+
await rename9(backupPath, oldPath);
|
|
2175
2804
|
} catch (_) {
|
|
2176
2805
|
}
|
|
2177
2806
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -2251,7 +2880,7 @@ function apply(ctx) {
|
|
|
2251
2880
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
2252
2881
|
if (backupPath) {
|
|
2253
2882
|
try {
|
|
2254
|
-
await
|
|
2883
|
+
await rename9(oldPath, backupPath);
|
|
2255
2884
|
} catch (e) {
|
|
2256
2885
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
2257
2886
|
}
|
|
@@ -2259,7 +2888,7 @@ function apply(ctx) {
|
|
|
2259
2888
|
const restore = async () => {
|
|
2260
2889
|
if (backupPath) {
|
|
2261
2890
|
try {
|
|
2262
|
-
await
|
|
2891
|
+
await rename9(backupPath, oldPath);
|
|
2263
2892
|
} catch (_) {
|
|
2264
2893
|
}
|
|
2265
2894
|
}
|
|
@@ -2278,7 +2907,7 @@ function apply(ctx) {
|
|
|
2278
2907
|
}
|
|
2279
2908
|
}
|
|
2280
2909
|
if (oldPath) {
|
|
2281
|
-
const keptInSource = await cleanupMovedSourceDir(
|
|
2910
|
+
const keptInSource = await cleanupMovedSourceDir(dirname4(oldPath));
|
|
2282
2911
|
if (keptInSource.length > 0) moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${keptInSource.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
|
|
2283
2912
|
}
|
|
2284
2913
|
} catch (e) {
|
|
@@ -2361,11 +2990,21 @@ function apply(ctx) {
|
|
|
2361
2990
|
metaCache.invalidate(item.sessionId);
|
|
2362
2991
|
moved++;
|
|
2363
2992
|
notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u5DF2\u5B8C\u6210\uFF1A${String(item.sessionId).slice(0, 18)}\u2026`);
|
|
2993
|
+
try {
|
|
2994
|
+
await moveNotices.append({ kind: "moved", sessionId: item.sessionId, targetPath: item.targetPath, at: Date.now() });
|
|
2995
|
+
} catch (e) {
|
|
2996
|
+
}
|
|
2364
2997
|
} catch (e) {
|
|
2365
2998
|
if (isBusyError(e)) continue;
|
|
2366
2999
|
if (Date.now() - bootedAt < BOOT_WARMUP_MS) continue;
|
|
2367
3000
|
const bumped = await pendingMoves.bumpAttempts(item.sessionId);
|
|
2368
|
-
if (bumped && bumped.dropped)
|
|
3001
|
+
if (bumped && bumped.dropped) {
|
|
3002
|
+
notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u591A\u6B21\u5931\u8D25\u5DF2\u653E\u5F03\uFF1A${String(item.sessionId).slice(0, 18)}\u2026\uFF08${String(e && e.message || e)}\uFF09`);
|
|
3003
|
+
try {
|
|
3004
|
+
await moveNotices.append({ kind: "abandoned", sessionId: item.sessionId, targetPath: item.targetPath, at: Date.now(), attempts: bumped.attempts, reason: String(e && e.message || e).split("\n")[0] });
|
|
3005
|
+
} catch (err) {
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
2369
3008
|
}
|
|
2370
3009
|
}
|
|
2371
3010
|
if (moved > 0) {
|
|
@@ -2455,19 +3094,50 @@ function apply(ctx) {
|
|
|
2455
3094
|
return { next: null, value: { ok: true, archived: true } };
|
|
2456
3095
|
});
|
|
2457
3096
|
}
|
|
2458
|
-
|
|
3097
|
+
const warmHasApi = typeof sq.readTitleSnapshots === "function" || typeof sq.readTitleSnapshot === "function";
|
|
3098
|
+
async function projectTitlesStatus(ids) {
|
|
2459
3099
|
const out = /* @__PURE__ */ new Map();
|
|
3100
|
+
const markAll = (resolved) => {
|
|
3101
|
+
for (const id of ids) out.set(String(id), { resolved, snapshot: null });
|
|
3102
|
+
};
|
|
2460
3103
|
if (!ids || !ids.length) return out;
|
|
2461
|
-
if (
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
3104
|
+
if (!warmHasApi) {
|
|
3105
|
+
markAll(false);
|
|
3106
|
+
return out;
|
|
3107
|
+
}
|
|
3108
|
+
if (typeof sq.readTitleSnapshots === "function") {
|
|
3109
|
+
let results = null;
|
|
3110
|
+
try {
|
|
3111
|
+
results = await sq.readTitleSnapshots(ids);
|
|
3112
|
+
} catch (e) {
|
|
3113
|
+
results = null;
|
|
3114
|
+
}
|
|
3115
|
+
if (Array.isArray(results)) {
|
|
3116
|
+
results.forEach((result, index) => {
|
|
3117
|
+
const id = String(ids[index]);
|
|
3118
|
+
if (result && result.status === "fulfilled") out.set(id, { resolved: true, snapshot: unwrapSnapshot(result) });
|
|
3119
|
+
else if (result && result.status === "rejected") out.set(id, { resolved: false, snapshot: null });
|
|
3120
|
+
else if (result) out.set(id, { resolved: true, snapshot: unwrapSnapshot(result) });
|
|
3121
|
+
else out.set(id, { resolved: false, snapshot: null });
|
|
3122
|
+
});
|
|
3123
|
+
for (const id of ids) if (!out.has(String(id))) out.set(String(id), { resolved: false, snapshot: null });
|
|
3124
|
+
return out;
|
|
3125
|
+
}
|
|
2470
3126
|
}
|
|
3127
|
+
if (typeof sq.readTitleSnapshot === "function") {
|
|
3128
|
+
for (const raw of ids) {
|
|
3129
|
+
const id = String(raw);
|
|
3130
|
+
try {
|
|
3131
|
+
const r = await sq.readTitleSnapshot(id);
|
|
3132
|
+
if (r && r.status === "rejected") out.set(id, { resolved: false, snapshot: null });
|
|
3133
|
+
else out.set(id, { resolved: true, snapshot: unwrapSnapshot(r) });
|
|
3134
|
+
} catch (e) {
|
|
3135
|
+
out.set(id, { resolved: false, snapshot: null });
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
return out;
|
|
3139
|
+
}
|
|
3140
|
+
markAll(false);
|
|
2471
3141
|
return out;
|
|
2472
3142
|
}
|
|
2473
3143
|
async function allSessionItemsDetailed(opts = {}) {
|
|
@@ -2553,12 +3223,23 @@ function apply(ctx) {
|
|
|
2553
3223
|
return { items: archivedItems, usage };
|
|
2554
3224
|
}
|
|
2555
3225
|
let starredSet = /* @__PURE__ */ new Set();
|
|
3226
|
+
let tagIdsBySession = /* @__PURE__ */ new Map();
|
|
2556
3227
|
try {
|
|
2557
3228
|
starredSet = new Set((await stars.read()).starredSessionIds);
|
|
3229
|
+
const tagStore = await tags.read();
|
|
3230
|
+
const knownTagIds = new Set(tagStore.tags.map((t) => t.id));
|
|
3231
|
+
for (const [sid, list] of Object.entries(tagStore.assignments)) {
|
|
3232
|
+
const kept = list.filter((id) => knownTagIds.has(id));
|
|
3233
|
+
if (kept.length) tagIdsBySession.set(sid, kept);
|
|
3234
|
+
}
|
|
2558
3235
|
} catch (e) {
|
|
2559
3236
|
}
|
|
2560
|
-
for (const it of items)
|
|
3237
|
+
for (const it of items) {
|
|
3238
|
+
it.starred = starredSet.has(String(it.sessionId));
|
|
3239
|
+
it.tags = tagIdsBySession.get(String(it.sessionId)) || [];
|
|
3240
|
+
}
|
|
2561
3241
|
if (headersOk) await gcStars(ids);
|
|
3242
|
+
if (headersOk) await gcTags(ids);
|
|
2562
3243
|
return { items, usage };
|
|
2563
3244
|
}
|
|
2564
3245
|
async function allSessionItems(opts = {}) {
|
|
@@ -2653,56 +3334,121 @@ function apply(ctx) {
|
|
|
2653
3334
|
}
|
|
2654
3335
|
}
|
|
2655
3336
|
const lineage = {};
|
|
3337
|
+
const refineWanted = [];
|
|
3338
|
+
let emptyStore = null;
|
|
3339
|
+
try {
|
|
3340
|
+
emptyStore = await emptyIndex.entries();
|
|
3341
|
+
} catch (e) {
|
|
3342
|
+
emptyStore = null;
|
|
3343
|
+
}
|
|
3344
|
+
const nowTs = Date.now();
|
|
2656
3345
|
for (const entry of entries) {
|
|
2657
|
-
const
|
|
2658
|
-
|
|
3346
|
+
const id = String(entry.id);
|
|
3347
|
+
const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
|
|
3348
|
+
const candidate = emptyScanCandidate(size);
|
|
3349
|
+
let verdict = null;
|
|
3350
|
+
if (!candidate) {
|
|
3351
|
+
if (size !== null) verdict = false;
|
|
3352
|
+
} else {
|
|
3353
|
+
const mem = emptyScanCache.get(id);
|
|
3354
|
+
if (mem && mem.sizeBytes === size) {
|
|
3355
|
+
verdict = !!mem.empty;
|
|
3356
|
+
} else {
|
|
3357
|
+
const rec = emptyStore && emptyStore[id];
|
|
3358
|
+
if (rec && rec.fingerprint === `sz:${size}` && nowTs - (rec.updatedAt || 0) <= EMPTY_VERDICT_TTL_MS) {
|
|
3359
|
+
verdict = rec.empty === 1;
|
|
3360
|
+
emptyScanCache.set(id, { sizeBytes: size, empty: verdict });
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
if (verdict === null && refineCapable() && !refineQueue.has(id)) refineWanted.push({ id, sizeBytes: size });
|
|
3364
|
+
}
|
|
3365
|
+
const info = classifyLineage(entry.header);
|
|
3366
|
+
if (info) lineage[id] = { origin: info.origin, parentSession: info.parentSession, delegationDepth: info.delegationDepth, empty: verdict };
|
|
3367
|
+
else if (verdict === true) lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: true };
|
|
2659
3368
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
3369
|
+
enqueueRefine(refineWanted);
|
|
3370
|
+
for (const key of emptyScanCache.keys()) {
|
|
3371
|
+
const rec = emptyScanCache.get(key);
|
|
3372
|
+
if (!rec || !emptyScanCandidate(rec.sizeBytes)) emptyScanCache.delete(key);
|
|
3373
|
+
}
|
|
3374
|
+
const payload = {
|
|
2662
3375
|
titles: Object.fromEntries(authorityTitleCache),
|
|
2663
3376
|
trashedSessionIds: store.items.map((item) => String(item.sessionId)),
|
|
2664
3377
|
purgedSessionIds: activeTombstones,
|
|
2665
|
-
lineage
|
|
3378
|
+
lineage,
|
|
3379
|
+
// v3.6.2:还有预热/退避重试在途 → client 缩短轮询节拍,预热一完成就把
|
|
3380
|
+
// 补齐的标题送回(含面板自动刷新)。无预热 API 时恒 false,绝不假忙。
|
|
3381
|
+
warmPending: warmPendingNow(),
|
|
3382
|
+
// T1:空白精判后台队列是否还有活。无精判能力(旧 runtime 无 inspect 通道)
|
|
3383
|
+
// 时恒 false——永久 busy 会让 client 疯轮询,这是唯一现实的假忙陷阱。
|
|
3384
|
+
refinePending: refinePendingNow()
|
|
2666
3385
|
};
|
|
3386
|
+
try {
|
|
3387
|
+
const noticeRows = await moveNotices.list();
|
|
3388
|
+
if (noticeRows.length) payload.moveNotices = noticeRows;
|
|
3389
|
+
} catch (e) {
|
|
3390
|
+
}
|
|
3391
|
+
return payload;
|
|
2667
3392
|
}
|
|
2668
3393
|
const emptyScanCache = /* @__PURE__ */ new Map();
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
3394
|
+
const REFINE_CHUNK = 2;
|
|
3395
|
+
const refineQueue = /* @__PURE__ */ new Map();
|
|
3396
|
+
let refineRunning = false;
|
|
3397
|
+
let refineKickTimer = null;
|
|
3398
|
+
function refineCapable() {
|
|
3399
|
+
return !!persistence && typeof persistence.inspectSession === "function";
|
|
3400
|
+
}
|
|
3401
|
+
function refinePendingNow() {
|
|
3402
|
+
return !!(refineRunning || refineQueue.size);
|
|
3403
|
+
}
|
|
3404
|
+
function scheduleRefine() {
|
|
3405
|
+
if (refineKickTimer || refineRunning || !refineQueue.size) return;
|
|
3406
|
+
refineKickTimer = setTimeout(() => {
|
|
3407
|
+
refineKickTimer = null;
|
|
3408
|
+
runRefine();
|
|
3409
|
+
}, 25);
|
|
3410
|
+
if (typeof refineKickTimer.unref === "function") refineKickTimer.unref();
|
|
3411
|
+
}
|
|
3412
|
+
function enqueueRefine(items) {
|
|
3413
|
+
if (!items || !items.length || !refineCapable()) return;
|
|
3414
|
+
for (const { id, sizeBytes } of items) {
|
|
3415
|
+
if (emptyScanCache.has(id) && emptyScanCache.get(id).sizeBytes === sizeBytes) continue;
|
|
3416
|
+
refineQueue.set(id, { sizeBytes });
|
|
3417
|
+
}
|
|
3418
|
+
scheduleRefine();
|
|
3419
|
+
}
|
|
3420
|
+
async function runRefine() {
|
|
3421
|
+
if (refineRunning) return;
|
|
3422
|
+
refineRunning = true;
|
|
3423
|
+
try {
|
|
3424
|
+
while (refineQueue.size) {
|
|
3425
|
+
const batch = [...refineQueue.entries()].slice(0, REFINE_CHUNK);
|
|
3426
|
+
for (const [id] of batch) refineQueue.delete(id);
|
|
3427
|
+
const persistBatch = {};
|
|
3428
|
+
for (const [id, desc] of batch) {
|
|
3429
|
+
try {
|
|
3430
|
+
const types = [];
|
|
3431
|
+
await persistence.inspectSession(id, { onEvents: (b) => {
|
|
3432
|
+
for (const ev of b || []) if (types.length < 64) types.push(ev && ev.type);
|
|
3433
|
+
} });
|
|
3434
|
+
const isEmpty = isEmptyEventTypes(types);
|
|
3435
|
+
emptyScanCache.set(id, { sizeBytes: desc.sizeBytes, empty: isEmpty });
|
|
3436
|
+
persistBatch[id] = { empty: isEmpty ? 1 : 0, fingerprint: `sz:${desc.sizeBytes}`, updatedAt: Date.now() };
|
|
3437
|
+
} catch (e) {
|
|
3438
|
+
emptyScanCache.set(id, { sizeBytes: desc.sizeBytes, empty: false });
|
|
3439
|
+
}
|
|
2686
3440
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
const info = lineage[id];
|
|
2695
|
-
info.empty = false;
|
|
2696
|
-
if (!info.origin && !info.parentSession) delete lineage[id];
|
|
3441
|
+
if (Object.keys(persistBatch).length) {
|
|
3442
|
+
try {
|
|
3443
|
+
await emptyIndex.merge(persistBatch);
|
|
3444
|
+
} catch (e) {
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2697
3448
|
}
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
|
|
2702
|
-
if (size !== null && size <= EMPTY_DECODE_LIMIT) live.add(String(entry.id));
|
|
2703
|
-
}
|
|
2704
|
-
for (const key of emptyScanCache.keys()) {
|
|
2705
|
-
if (!live.has(key)) emptyScanCache.delete(key);
|
|
3449
|
+
} finally {
|
|
3450
|
+
refineRunning = false;
|
|
3451
|
+
if (refineQueue.size) scheduleRefine();
|
|
2706
3452
|
}
|
|
2707
3453
|
}
|
|
2708
3454
|
async function buildDetails(sid, signal) {
|
|
@@ -3058,6 +3804,10 @@ function apply(ctx) {
|
|
|
3058
3804
|
await titleIndex.remove([sid]);
|
|
3059
3805
|
} catch (e) {
|
|
3060
3806
|
}
|
|
3807
|
+
try {
|
|
3808
|
+
await emptyIndex.remove([sid]);
|
|
3809
|
+
} catch (e) {
|
|
3810
|
+
}
|
|
3061
3811
|
json(res, out);
|
|
3062
3812
|
} catch (e) {
|
|
3063
3813
|
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
@@ -3079,6 +3829,8 @@ function apply(ctx) {
|
|
|
3079
3829
|
metaCache.invalidate(sid);
|
|
3080
3830
|
await titleIndex.remove([sid]).catch(() => {
|
|
3081
3831
|
});
|
|
3832
|
+
await emptyIndex.remove([sid]).catch(() => {
|
|
3833
|
+
});
|
|
3082
3834
|
} catch (e) {
|
|
3083
3835
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
3084
3836
|
}
|
|
@@ -3094,7 +3846,7 @@ function apply(ctx) {
|
|
|
3094
3846
|
path: "/archived-sessions/sessions",
|
|
3095
3847
|
handler: async (req, res) => {
|
|
3096
3848
|
try {
|
|
3097
|
-
json(res, { items: await allSessionItems() });
|
|
3849
|
+
json(res, { items: await allSessionItems({ usage: true }), warmPending: warmPendingNow() });
|
|
3098
3850
|
} catch (e) {
|
|
3099
3851
|
json(res, { error: String(e && e.message || e) }, 500);
|
|
3100
3852
|
}
|
|
@@ -3109,13 +3861,133 @@ function apply(ctx) {
|
|
|
3109
3861
|
const starred = !!(body && body.starred);
|
|
3110
3862
|
let ids = parseIds(body);
|
|
3111
3863
|
if ((!ids || ids.length === 0) && body && typeof body.sessionId === "string") {
|
|
3112
|
-
ids =
|
|
3864
|
+
ids = isSafeSessionId3(body.sessionId) ? [body.sessionId] : null;
|
|
3113
3865
|
}
|
|
3114
|
-
if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
3866
|
+
if (!ids || ids.length === 0) return json(res, { ok: false, code: "DSM_STAR_SESSION_INVALID", error: "missing sessionId" }, 400);
|
|
3115
3867
|
const starredSessionIds = await stars.setStarred(ids, starred);
|
|
3116
3868
|
json(res, { ok: true, starredSessionIds });
|
|
3117
3869
|
} catch (e) {
|
|
3118
|
-
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
3870
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}));
|
|
3874
|
+
disposers.push(ctx.webServer.register({
|
|
3875
|
+
kind: "exact",
|
|
3876
|
+
path: "/archived-sessions/tags/list",
|
|
3877
|
+
handler: async (req, res) => {
|
|
3878
|
+
try {
|
|
3879
|
+
const { tags: tagList, assignments } = await tags.list();
|
|
3880
|
+
json(res, { ok: true, tags: tagList, assignments });
|
|
3881
|
+
} catch (e) {
|
|
3882
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
}));
|
|
3886
|
+
disposers.push(ctx.webServer.register({
|
|
3887
|
+
kind: "exact",
|
|
3888
|
+
path: "/archived-sessions/tags/create",
|
|
3889
|
+
handler: async (req, res) => {
|
|
3890
|
+
try {
|
|
3891
|
+
const body = await readJsonBody(req);
|
|
3892
|
+
const tag = await tags.create(body && body.name);
|
|
3893
|
+
json(res, { ok: true, tag });
|
|
3894
|
+
} catch (e) {
|
|
3895
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
}));
|
|
3899
|
+
disposers.push(ctx.webServer.register({
|
|
3900
|
+
kind: "exact",
|
|
3901
|
+
path: "/archived-sessions/tags/rename",
|
|
3902
|
+
handler: async (req, res) => {
|
|
3903
|
+
try {
|
|
3904
|
+
const body = await readJsonBody(req);
|
|
3905
|
+
await tags.rename(body && body.id, body && body.name);
|
|
3906
|
+
json(res, { ok: true });
|
|
3907
|
+
} catch (e) {
|
|
3908
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}));
|
|
3912
|
+
disposers.push(ctx.webServer.register({
|
|
3913
|
+
kind: "exact",
|
|
3914
|
+
path: "/archived-sessions/tags/merge",
|
|
3915
|
+
handler: async (req, res) => {
|
|
3916
|
+
try {
|
|
3917
|
+
const body = await readJsonBody(req);
|
|
3918
|
+
await tags.merge(body && body.fromId, body && body.toId);
|
|
3919
|
+
json(res, { ok: true });
|
|
3920
|
+
} catch (e) {
|
|
3921
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
}));
|
|
3925
|
+
disposers.push(ctx.webServer.register({
|
|
3926
|
+
kind: "exact",
|
|
3927
|
+
path: "/archived-sessions/tags/delete",
|
|
3928
|
+
handler: async (req, res) => {
|
|
3929
|
+
try {
|
|
3930
|
+
const body = await readJsonBody(req);
|
|
3931
|
+
await tags.removeTag(body && body.id);
|
|
3932
|
+
json(res, { ok: true });
|
|
3933
|
+
} catch (e) {
|
|
3934
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}));
|
|
3938
|
+
disposers.push(ctx.webServer.register({
|
|
3939
|
+
kind: "exact",
|
|
3940
|
+
path: "/archived-sessions/tags/set",
|
|
3941
|
+
handler: async (req, res) => {
|
|
3942
|
+
try {
|
|
3943
|
+
const body = await readJsonBody(req);
|
|
3944
|
+
const sid = body && body.sessionId;
|
|
3945
|
+
if (!isSafeSessionId3(sid)) {
|
|
3946
|
+
return json(res, { ok: false, code: "DSM_TAG_SESSION_INVALID", error: "missing/invalid sessionId" }, 400);
|
|
3947
|
+
}
|
|
3948
|
+
const assignments = await tags.setTags(sid, body && body.tagIds);
|
|
3949
|
+
json(res, { ok: true, assignments });
|
|
3950
|
+
} catch (e) {
|
|
3951
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}));
|
|
3955
|
+
disposers.push(ctx.webServer.register({
|
|
3956
|
+
kind: "exact",
|
|
3957
|
+
path: "/archived-sessions/filters/list",
|
|
3958
|
+
handler: async (req, res) => {
|
|
3959
|
+
try {
|
|
3960
|
+
json(res, { ok: true, items: await savedFilters.list() });
|
|
3961
|
+
} catch (e) {
|
|
3962
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
}));
|
|
3966
|
+
disposers.push(ctx.webServer.register({
|
|
3967
|
+
kind: "exact",
|
|
3968
|
+
path: "/archived-sessions/filters/save",
|
|
3969
|
+
handler: async (req, res) => {
|
|
3970
|
+
try {
|
|
3971
|
+
const body = await readJsonBody(req);
|
|
3972
|
+
const item = await savedFilters.save(body && body.name, body && body.filters);
|
|
3973
|
+
json(res, { ok: true, item });
|
|
3974
|
+
} catch (e) {
|
|
3975
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}));
|
|
3979
|
+
disposers.push(ctx.webServer.register({
|
|
3980
|
+
kind: "exact",
|
|
3981
|
+
path: "/archived-sessions/filters/delete",
|
|
3982
|
+
handler: async (req, res) => {
|
|
3983
|
+
try {
|
|
3984
|
+
const body = await readJsonBody(req);
|
|
3985
|
+
const ids = body && Array.isArray(body.ids) ? body.ids : null;
|
|
3986
|
+
if (!ids || ids.length === 0) return json(res, { ok: false, code: "DSM_FILTER_IDS_INVALID", error: "missing ids" }, 400);
|
|
3987
|
+
const removed = await savedFilters.remove(ids);
|
|
3988
|
+
json(res, { ok: true, removed });
|
|
3989
|
+
} catch (e) {
|
|
3990
|
+
json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
|
|
3119
3991
|
}
|
|
3120
3992
|
}
|
|
3121
3993
|
}));
|
|
@@ -3174,7 +4046,7 @@ function apply(ctx) {
|
|
|
3174
4046
|
try {
|
|
3175
4047
|
const body = await readJsonBody(req);
|
|
3176
4048
|
const rootId = body && typeof body.sessionId === "string" ? body.sessionId : "";
|
|
3177
|
-
if (!rootId || !
|
|
4049
|
+
if (!rootId || !isSafeSessionId3(rootId)) return json(res, { error: "missing sessionId" }, 400);
|
|
3178
4050
|
const headerById = /* @__PURE__ */ new Map();
|
|
3179
4051
|
const sizeById = /* @__PURE__ */ new Map();
|
|
3180
4052
|
const trashedIds = /* @__PURE__ */ new Set();
|
|
@@ -3198,9 +4070,18 @@ function apply(ctx) {
|
|
|
3198
4070
|
});
|
|
3199
4071
|
if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
|
|
3200
4072
|
};
|
|
4073
|
+
const lineageStats = /* @__PURE__ */ new Map();
|
|
4074
|
+
const lineageHeaders = /* @__PURE__ */ new Map();
|
|
3201
4075
|
try {
|
|
3202
4076
|
for (const entry of await persistence.listEntries()) {
|
|
3203
4077
|
addHeader(entry.header, entry.sizeBytes);
|
|
4078
|
+
const eid = entry && entry.id != null ? String(entry.id) : null;
|
|
4079
|
+
if (eid) {
|
|
4080
|
+
lineageHeaders.set(eid, { header: entry && entry.header || null });
|
|
4081
|
+
if (entry && typeof entry.revision === "string" && entry.revision) {
|
|
4082
|
+
lineageStats.set(eid, Number.isFinite(entry.sizeBytes) ? { revision: entry.revision, sizeBytes: Number(entry.sizeBytes) } : { revision: entry.revision });
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
3204
4085
|
if (entry && entry.id != null) persistedIds.add(String(entry.id));
|
|
3205
4086
|
}
|
|
3206
4087
|
} catch (e) {
|
|
@@ -3252,11 +4133,17 @@ function apply(ctx) {
|
|
|
3252
4133
|
n.children.forEach(walk);
|
|
3253
4134
|
};
|
|
3254
4135
|
nodes.forEach(walk);
|
|
3255
|
-
const
|
|
4136
|
+
const lineageIds = allIds.map(String);
|
|
4137
|
+
const { cached: lineageCached, missing: lineageMissing } = metaCache.partition(lineageIds, lineageStats);
|
|
4138
|
+
const lineagePersisted = await hydrateFromPersist(lineageMissing, lineageStats);
|
|
4139
|
+
for (const [id, meta] of lineagePersisted) metaCache.set(id, lineageStats.get(id), meta);
|
|
4140
|
+
const lineageRest = lineageMissing.filter((id) => !lineagePersisted.has(id));
|
|
4141
|
+
if (lineageRest.length) enqueueWarm(lineageRest, lineageStats, lineageHeaders);
|
|
4142
|
+
const truncateTitle = (t) => t ? t.length > MAX_TITLE ? t.slice(0, MAX_TITLE) + "\u2026" : t : null;
|
|
3256
4143
|
const titleOf = (id) => {
|
|
3257
|
-
const
|
|
3258
|
-
const m =
|
|
3259
|
-
return m && m.title || null;
|
|
4144
|
+
const key = String(id);
|
|
4145
|
+
const m = lineageCached.get(key) || lineagePersisted.get(key);
|
|
4146
|
+
return truncateTitle(m && m.title || authorityTitleCache.get(key) || null);
|
|
3260
4147
|
};
|
|
3261
4148
|
const fill = (n) => {
|
|
3262
4149
|
n.title = titleOf(n.sessionId);
|
|
@@ -3371,6 +4258,21 @@ function apply(ctx) {
|
|
|
3371
4258
|
}
|
|
3372
4259
|
}
|
|
3373
4260
|
}));
|
|
4261
|
+
disposers.push(ctx.webServer.register({
|
|
4262
|
+
kind: "exact",
|
|
4263
|
+
path: "/archived-sessions/pending-moves/notices/ack",
|
|
4264
|
+
handler: async (req, res) => {
|
|
4265
|
+
try {
|
|
4266
|
+
const body = await readJsonBody(req);
|
|
4267
|
+
const ids = body && Array.isArray(body.ids) ? body.ids : null;
|
|
4268
|
+
if (!ids || !ids.length) return json(res, { ok: false, error: "missing ids" }, 400);
|
|
4269
|
+
const removed = await moveNotices.ack(ids);
|
|
4270
|
+
json(res, { ok: true, removed });
|
|
4271
|
+
} catch (e) {
|
|
4272
|
+
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}));
|
|
3374
4276
|
disposers.push(ctx.webServer.register({
|
|
3375
4277
|
kind: "exact",
|
|
3376
4278
|
path: "/archived-sessions/archive",
|