@chance722/dsh-inbox 0.2.6 → 0.2.8
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/LICENSE +21 -21
- package/README.md +170 -187
- package/README.zh.md +168 -186
- package/cordis.patch.yml +32 -32
- package/lib/cli.js +1 -0
- package/lib/client.js +4 -1
- package/lib/index.js +93 -12
- package/lib/types/client/messages.d.ts +2 -0
- package/lib/types/host/remote/merge.d.ts +10 -0
- package/lib/types/host/vault/spec.d.ts +38 -0
- package/lib/types/host/vault/vault.d.ts +20 -2
- package/lib/types/shared/panel-wire.d.ts +25 -2
- package/package.json +104 -104
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
|
|
3
3
|
|
|
4
4
|
// src/shared/constants.ts
|
|
5
5
|
var PACKAGE_NAME = "@chance722/dsh-inbox";
|
|
6
|
-
var VERSION = true ? "0.2.
|
|
6
|
+
var VERSION = true ? "0.2.8" : "0.0.0-dev";
|
|
7
7
|
var DEFAULT_USER_AGENT = "dsh-inbox";
|
|
8
8
|
|
|
9
9
|
// src/shared/vocabulary.ts
|
|
@@ -1251,8 +1251,13 @@ function isNewer(entry, lastPullAt) {
|
|
|
1251
1251
|
if (Number.isNaN(seen) || Number.isNaN(remote)) return true;
|
|
1252
1252
|
return remote > seen;
|
|
1253
1253
|
}
|
|
1254
|
+
function isFolder(path, dropFolder) {
|
|
1255
|
+
if (path.endsWith("/")) return true;
|
|
1256
|
+
return dropFolder.length > 0 && path.replace(/\/+$/, "") === dropFolder;
|
|
1257
|
+
}
|
|
1254
1258
|
async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(void 0), elsewhere = { roots: [] }) {
|
|
1255
1259
|
const lastPullAt = vault.global.sync.lastPullAt;
|
|
1260
|
+
const dropFolder = syncRoot3.endsWith("/sync") ? syncRoot3.slice(0, -"/sync".length) : "";
|
|
1256
1261
|
let entries;
|
|
1257
1262
|
try {
|
|
1258
1263
|
entries = await source.list();
|
|
@@ -1267,6 +1272,7 @@ async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(vo
|
|
|
1267
1272
|
let skippedSync = 0;
|
|
1268
1273
|
let skippedOlder = 0;
|
|
1269
1274
|
let skippedForeign = 0;
|
|
1275
|
+
let skippedFolders = 0;
|
|
1270
1276
|
let remoteRecords = 0;
|
|
1271
1277
|
let remoteAttachments = 0;
|
|
1272
1278
|
const foreignSyncRoots = new Set(elsewhere.roots);
|
|
@@ -1295,6 +1301,11 @@ async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(vo
|
|
|
1295
1301
|
else if (folder === "attachments" && !name3.endsWith(".meta.json")) remoteAttachments += 1;
|
|
1296
1302
|
continue;
|
|
1297
1303
|
}
|
|
1304
|
+
if (isFolder(entry.path, dropFolder)) {
|
|
1305
|
+
skipped += 1;
|
|
1306
|
+
skippedFolders += 1;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1298
1309
|
if (!isNewer(entry, lastPullAt)) {
|
|
1299
1310
|
skipped += 1;
|
|
1300
1311
|
skippedOlder += 1;
|
|
@@ -1358,6 +1369,7 @@ async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(vo
|
|
|
1358
1369
|
skippedSync,
|
|
1359
1370
|
skippedOlder,
|
|
1360
1371
|
skippedForeign,
|
|
1372
|
+
skippedFolders,
|
|
1361
1373
|
remoteRecords,
|
|
1362
1374
|
remoteAttachments,
|
|
1363
1375
|
syncRoot: syncRoot3,
|
|
@@ -1489,18 +1501,32 @@ function wins(remote, local) {
|
|
|
1489
1501
|
if (Number.isNaN(incoming) || Number.isNaN(current)) return false;
|
|
1490
1502
|
return incoming > current;
|
|
1491
1503
|
}
|
|
1504
|
+
function newerThanPurge(vault, remote) {
|
|
1505
|
+
const purged = vault.purgedAt(remote.id);
|
|
1506
|
+
if (purged === void 0) return true;
|
|
1507
|
+
return Date.parse(remote.updatedAt) > Date.parse(purged);
|
|
1508
|
+
}
|
|
1492
1509
|
async function mergeOnce(vault, tree, prefix, admit) {
|
|
1493
1510
|
const failures = [];
|
|
1494
1511
|
let merged = 0;
|
|
1495
1512
|
let added = 0;
|
|
1496
1513
|
let deletions = 0;
|
|
1514
|
+
let purged = 0;
|
|
1497
1515
|
let kept = 0;
|
|
1498
1516
|
let attachments = 0;
|
|
1499
1517
|
let objects;
|
|
1500
1518
|
try {
|
|
1501
1519
|
objects = await tree.list(`${prefix}/items/`);
|
|
1502
1520
|
} catch (error) {
|
|
1503
|
-
return {
|
|
1521
|
+
return {
|
|
1522
|
+
merged: 0,
|
|
1523
|
+
added: 0,
|
|
1524
|
+
deletions: 0,
|
|
1525
|
+
purged: 0,
|
|
1526
|
+
kept: 0,
|
|
1527
|
+
attachments: 0,
|
|
1528
|
+
failures: [`\u5217\u8FDC\u7AEF\u540C\u6B65\u76EE\u5F55\u5931\u8D25\uFF1A${reasonOf(error)}`]
|
|
1529
|
+
};
|
|
1504
1530
|
}
|
|
1505
1531
|
const wanted = /* @__PURE__ */ new Map();
|
|
1506
1532
|
const bytesAt = /* @__PURE__ */ new Map();
|
|
@@ -1524,6 +1550,10 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1524
1550
|
continue;
|
|
1525
1551
|
}
|
|
1526
1552
|
const local = vault.get(remote.id);
|
|
1553
|
+
if (local === void 0 && !newerThanPurge(vault, remote)) {
|
|
1554
|
+
purged += 1;
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1527
1557
|
if (!wins(remote, local)) {
|
|
1528
1558
|
kept += 1;
|
|
1529
1559
|
continue;
|
|
@@ -1566,7 +1596,7 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1566
1596
|
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A${reasonOf(error)}`);
|
|
1567
1597
|
}
|
|
1568
1598
|
}
|
|
1569
|
-
return { merged, added, deletions, kept, attachments, failures };
|
|
1599
|
+
return { merged, added, deletions, purged, kept, attachments, failures };
|
|
1570
1600
|
}
|
|
1571
1601
|
function extensionOfName(path) {
|
|
1572
1602
|
return /\.([A-Za-z0-9]{1,8})$/.exec(nameOf2(path))?.[1]?.toLowerCase() ?? "bin";
|
|
@@ -1579,7 +1609,9 @@ async function mergeRemote(ctx, vault, attachments, extraRoots = []) {
|
|
|
1579
1609
|
const prefix = syncRoot(settings);
|
|
1580
1610
|
try {
|
|
1581
1611
|
const tree = settings.protocol === "s3" ? await s3Tree(ctx, settings) : await webdavTree(ctx, settings);
|
|
1582
|
-
if (tree === void 0)
|
|
1612
|
+
if (tree === void 0) {
|
|
1613
|
+
return { merged: 0, added: 0, deletions: 0, purged: 0, kept: 0, attachments: 0, failures: [] };
|
|
1614
|
+
}
|
|
1583
1615
|
const admit = admitWith(attachments);
|
|
1584
1616
|
const outcome = await mergeOnce(vault, tree, prefix, admit);
|
|
1585
1617
|
for (const root of extraRoots) {
|
|
@@ -1588,13 +1620,22 @@ async function mergeRemote(ctx, vault, attachments, extraRoots = []) {
|
|
|
1588
1620
|
outcome.merged += extra.merged;
|
|
1589
1621
|
outcome.added += extra.added;
|
|
1590
1622
|
outcome.deletions += extra.deletions;
|
|
1623
|
+
outcome.purged += extra.purged;
|
|
1591
1624
|
outcome.kept += extra.kept;
|
|
1592
1625
|
outcome.attachments += extra.attachments;
|
|
1593
1626
|
outcome.failures.push(...extra.failures);
|
|
1594
1627
|
}
|
|
1595
1628
|
return outcome;
|
|
1596
1629
|
} catch (error) {
|
|
1597
|
-
return {
|
|
1630
|
+
return {
|
|
1631
|
+
merged: 0,
|
|
1632
|
+
added: 0,
|
|
1633
|
+
deletions: 0,
|
|
1634
|
+
purged: 0,
|
|
1635
|
+
kept: 0,
|
|
1636
|
+
attachments: 0,
|
|
1637
|
+
failures: [reasonOf(error)]
|
|
1638
|
+
};
|
|
1598
1639
|
}
|
|
1599
1640
|
}
|
|
1600
1641
|
function admitWith(store) {
|
|
@@ -1702,6 +1743,7 @@ async function withMerge(ctx, vault, attachments, pulled) {
|
|
|
1702
1743
|
merged: outcome.merged,
|
|
1703
1744
|
added: outcome.added,
|
|
1704
1745
|
deletions: outcome.deletions,
|
|
1746
|
+
...outcome.purged === 0 ? {} : { purged: outcome.purged },
|
|
1705
1747
|
kept: outcome.kept,
|
|
1706
1748
|
attachments: outcome.attachments,
|
|
1707
1749
|
failed: pulled.failed + outcome.failures.length,
|
|
@@ -2466,6 +2508,12 @@ var attachmentSchema = z3.object({
|
|
|
2466
2508
|
/** Present when the medium identifies the bytes by digest. */
|
|
2467
2509
|
sha256: z3.string().optional()
|
|
2468
2510
|
});
|
|
2511
|
+
var graveSchema = z3.object({
|
|
2512
|
+
/** The record's own key, repeated inside the document (see `attachments`). */
|
|
2513
|
+
id: z3.string().min(1),
|
|
2514
|
+
/** When the user emptied it out of the bin. */
|
|
2515
|
+
purgedAt: timestamp
|
|
2516
|
+
});
|
|
2469
2517
|
var vaultGlobalSchema = z3.object({
|
|
2470
2518
|
sync: z3.object({
|
|
2471
2519
|
lastPullAt: z3.string().optional(),
|
|
@@ -2518,9 +2566,17 @@ var vaultSpec = defineDomain({
|
|
|
2518
2566
|
* Version 7 adds `sync.lastPushAt`: the cursor that keeps a push to "what
|
|
2519
2567
|
* changed since last time" instead of re-uploading the vault on every pass.
|
|
2520
2568
|
* A `global` field, so no record shape changes at all.
|
|
2569
|
+
*
|
|
2570
|
+
* Version 8 adds the `graves` table: one row per record the user emptied out
|
|
2571
|
+
* of the bin. A purge leaves no local row, so the merge had nothing to
|
|
2572
|
+
* outrank the cloud's copy with — with 「同时合并别的同步目录」 on, the older
|
|
2573
|
+
* tree in the same bucket filed all thirteen of them back into the bin on
|
|
2574
|
+
* every restart (measured 2026-09-21). A new table, so no record shape
|
|
2575
|
+
* changes; an older vault simply has no graves, and there is nothing to
|
|
2576
|
+
* protect until the next purge.
|
|
2521
2577
|
*/
|
|
2522
|
-
version:
|
|
2523
|
-
compatibleVersions: [1, 2, 3, 4, 5, 6],
|
|
2578
|
+
version: 8,
|
|
2579
|
+
compatibleVersions: [1, 2, 3, 4, 5, 6, 7],
|
|
2524
2580
|
layout: "per-record",
|
|
2525
2581
|
global: {
|
|
2526
2582
|
schema: vaultGlobalSchema,
|
|
@@ -2528,7 +2584,8 @@ var vaultSpec = defineDomain({
|
|
|
2528
2584
|
},
|
|
2529
2585
|
tables: {
|
|
2530
2586
|
items: domainTable(itemSchema),
|
|
2531
|
-
attachments: domainTable(attachmentSchema)
|
|
2587
|
+
attachments: domainTable(attachmentSchema),
|
|
2588
|
+
graves: domainTable(graveSchema)
|
|
2532
2589
|
}
|
|
2533
2590
|
});
|
|
2534
2591
|
|
|
@@ -2599,6 +2656,9 @@ var Vault = class _Vault {
|
|
|
2599
2656
|
get attachments() {
|
|
2600
2657
|
return this.domain.table("attachments");
|
|
2601
2658
|
}
|
|
2659
|
+
get graves() {
|
|
2660
|
+
return this.domain.table("graves");
|
|
2661
|
+
}
|
|
2602
2662
|
/** Where the key state stands; what the panel shows and the tools consult. */
|
|
2603
2663
|
get lockState() {
|
|
2604
2664
|
return { configured: this.global.master !== void 0, unlocked: this.key !== void 0 };
|
|
@@ -2853,16 +2913,26 @@ var Vault = class _Vault {
|
|
|
2853
2913
|
});
|
|
2854
2914
|
}
|
|
2855
2915
|
/**
|
|
2856
|
-
* Delete one record for good, together with its attachment rows
|
|
2916
|
+
* Delete one record for good, together with its attachment rows, and leave a
|
|
2917
|
+
* grave behind so nothing that is still in the cloud can file it again.
|
|
2857
2918
|
*
|
|
2858
2919
|
* The bytes behind an attachment live in dsh's own store, which never deletes
|
|
2859
2920
|
* automatically — emptying the recycle bin drops our references, not their
|
|
2860
2921
|
* objects. A row another record still references is left alone.
|
|
2861
2922
|
*
|
|
2923
|
+
* The grave is the other half of that sentence, and it is why this is not
|
|
2924
|
+
* called `remove` any more: deleting the row really does take this machine's
|
|
2925
|
+
* copy away, but a copy under a *different* `sync/` tree in the same bucket
|
|
2926
|
+
* survives the purge of our own tree (`../remote/remove.ts` only ever deletes
|
|
2927
|
+
* under this vault's root), and the merge has no local row to outrank it with.
|
|
2928
|
+
* Measured 2026-09-21: thirteen emptied tombstones came back into the bin on
|
|
2929
|
+
* every `dsh` restart. The grave is what makes "gone" stick; it holds the id
|
|
2930
|
+
* and the moment, no content.
|
|
2931
|
+
*
|
|
2862
2932
|
* @param id - record key.
|
|
2863
2933
|
* @returns whether the record existed.
|
|
2864
2934
|
*/
|
|
2865
|
-
async
|
|
2935
|
+
async purge(id) {
|
|
2866
2936
|
const item = this.items.get(id);
|
|
2867
2937
|
if (item === void 0) return false;
|
|
2868
2938
|
const stillReferenced = /* @__PURE__ */ new Set();
|
|
@@ -2873,7 +2943,18 @@ var Vault = class _Vault {
|
|
|
2873
2943
|
for (const attachmentId of item.attachmentIds) {
|
|
2874
2944
|
if (!stillReferenced.has(attachmentId)) await this.attachments.delete(attachmentId);
|
|
2875
2945
|
}
|
|
2876
|
-
|
|
2946
|
+
await this.items.delete(id);
|
|
2947
|
+
await this.graves.put(id, { id, purgedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2948
|
+
return true;
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* When this id was emptied out of the bin, if it ever was.
|
|
2952
|
+
*
|
|
2953
|
+
* The merge asks this before taking a copy of a record this vault does not
|
|
2954
|
+
* have (see `newerThanPurge` in `../remote/merge.ts`).
|
|
2955
|
+
*/
|
|
2956
|
+
purgedAt(id) {
|
|
2957
|
+
return this.graves.get(id)?.purgedAt;
|
|
2877
2958
|
}
|
|
2878
2959
|
/**
|
|
2879
2960
|
* Record an attachment's metadata. The bytes stay in the store named by
|
|
@@ -3253,7 +3334,7 @@ async function handlePurge(ctx, vault) {
|
|
|
3253
3334
|
}
|
|
3254
3335
|
let removed = 0;
|
|
3255
3336
|
for (const item of bin) {
|
|
3256
|
-
if (await vault.
|
|
3337
|
+
if (await vault.purge(item.id)) removed += 1;
|
|
3257
3338
|
}
|
|
3258
3339
|
const stillReferenced = new Set(
|
|
3259
3340
|
vault.list({ includeDeleted: true }).flatMap((item) => [...item.attachmentIds])
|
|
@@ -181,6 +181,7 @@ export declare const zh: {
|
|
|
181
181
|
'sync.detailPulled': string;
|
|
182
182
|
'sync.detailAdded': string;
|
|
183
183
|
'sync.detailDeleted': string;
|
|
184
|
+
'sync.detailPurged': string;
|
|
184
185
|
'sync.pullForeignSync': string;
|
|
185
186
|
'sync.detailForeign': string;
|
|
186
187
|
'sync.shortPushed': string;
|
|
@@ -444,6 +445,7 @@ export declare const MESSAGES: {
|
|
|
444
445
|
'sync.detailPulled': string;
|
|
445
446
|
'sync.detailAdded': string;
|
|
446
447
|
'sync.detailDeleted': string;
|
|
448
|
+
'sync.detailPurged': string;
|
|
447
449
|
'sync.pullForeignSync': string;
|
|
448
450
|
'sync.detailForeign': string;
|
|
449
451
|
'sync.shortPushed': string;
|
|
@@ -59,6 +59,16 @@ export interface MergeOutcome {
|
|
|
59
59
|
* 2026-09-21, after merging an abandoned tree whose copies mostly overlapped).
|
|
60
60
|
*/
|
|
61
61
|
added: number;
|
|
62
|
+
/**
|
|
63
|
+
* Copies of records this vault **emptied out of the bin** — left alone, on
|
|
64
|
+
* purpose, because a purge is not a deletion to be argued with.
|
|
65
|
+
*
|
|
66
|
+
* Counted apart from {@link kept}: "the cloud has a copy you already have" and
|
|
67
|
+
* "the cloud has a copy of something you threw away" are different sentences,
|
|
68
|
+
* and only the second one answers "so why did the emptied records stay empty
|
|
69
|
+
* this time?" (2026-09-21).
|
|
70
|
+
*/
|
|
71
|
+
purged: number;
|
|
62
72
|
/** Records the remote had and this machine already had, newer or equal. */
|
|
63
73
|
kept: number;
|
|
64
74
|
/** Attachment objects pulled down (bytes plus their row). */
|
|
@@ -67,6 +67,31 @@ export declare const attachmentSchema: z.ZodObject<{
|
|
|
67
67
|
height: z.ZodOptional<z.ZodNumber>;
|
|
68
68
|
sha256: z.ZodOptional<z.ZodString>;
|
|
69
69
|
}, z.core.$strip>;
|
|
70
|
+
/**
|
|
71
|
+
* One grave: the id the user emptied out of the recycle bin, and when.
|
|
72
|
+
*
|
|
73
|
+
* Emptying the bin is not "deleted" — it is *gone*, and a copy of the record
|
|
74
|
+
* living somewhere else (another `sync/` tree in the same bucket, a device that
|
|
75
|
+
* has not pulled the tombstone yet) must not be able to file it back in.
|
|
76
|
+
* `updatedAt` cannot express that on its own: the merge compares the incoming
|
|
77
|
+
* copy against the local one, and after a purge there is no local row at all,
|
|
78
|
+
* so any copy won. That is measured, not theoretical — thirteen tombstones came
|
|
79
|
+
* back into the bin on every restart (2026-09-21).
|
|
80
|
+
*
|
|
81
|
+
* It carries an id and a time and nothing else: no text, no note, no
|
|
82
|
+
* attachment. What the row buys is the one comparison the merge needs — a copy
|
|
83
|
+
* **at or before** this moment is dead, a copy strictly newer still wins, the
|
|
84
|
+
* same way it does for a record that was never deleted.
|
|
85
|
+
*
|
|
86
|
+
* Nothing ever collects these, and that is deliberate rather than an oversight:
|
|
87
|
+
* a grave that has been pruned is a hole reopened — the copy it was holding out
|
|
88
|
+
* is exactly as old as it was, and the next pull takes it. One row per emptied
|
|
89
|
+
* record, six fields of JSON, for as long as the vault lives.
|
|
90
|
+
*/
|
|
91
|
+
export declare const graveSchema: z.ZodObject<{
|
|
92
|
+
id: z.ZodString;
|
|
93
|
+
purgedAt: z.ZodString;
|
|
94
|
+
}, z.core.$strip>;
|
|
70
95
|
/**
|
|
71
96
|
* One global slot per domain. Sync state lives here because it belongs to the
|
|
72
97
|
* vault as a whole, not to any item; M6 fills it in.
|
|
@@ -96,6 +121,7 @@ export declare const vaultGlobalSchema: z.ZodObject<{
|
|
|
96
121
|
}, z.core.$strip>;
|
|
97
122
|
export type Item = z.infer<typeof itemSchema>;
|
|
98
123
|
export type Attachment = z.infer<typeof attachmentSchema>;
|
|
124
|
+
export type Grave = z.infer<typeof graveSchema>;
|
|
99
125
|
export type VaultGlobal = z.infer<typeof vaultGlobalSchema>;
|
|
100
126
|
/**
|
|
101
127
|
* Domain name doubles as the backend unit name: `<DSH_HOME>/storages/dsh_inbox/…`.
|
|
@@ -122,6 +148,14 @@ export declare const vaultSpec: {
|
|
|
122
148
|
* Version 7 adds `sync.lastPushAt`: the cursor that keeps a push to "what
|
|
123
149
|
* changed since last time" instead of re-uploading the vault on every pass.
|
|
124
150
|
* A `global` field, so no record shape changes at all.
|
|
151
|
+
*
|
|
152
|
+
* Version 8 adds the `graves` table: one row per record the user emptied out
|
|
153
|
+
* of the bin. A purge leaves no local row, so the merge had nothing to
|
|
154
|
+
* outrank the cloud's copy with — with 「同时合并别的同步目录」 on, the older
|
|
155
|
+
* tree in the same bucket filed all thirteen of them back into the bin on
|
|
156
|
+
* every restart (measured 2026-09-21). A new table, so no record shape
|
|
157
|
+
* changes; an older vault simply has no graves, and there is nothing to
|
|
158
|
+
* protect until the next purge.
|
|
125
159
|
*/
|
|
126
160
|
version: number;
|
|
127
161
|
compatibleVersions: number[];
|
|
@@ -188,6 +222,10 @@ export declare const vaultSpec: {
|
|
|
188
222
|
height?: number | undefined;
|
|
189
223
|
sha256?: string | undefined;
|
|
190
224
|
}>;
|
|
225
|
+
graves: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string, {
|
|
226
|
+
id: string;
|
|
227
|
+
purgedAt: string;
|
|
228
|
+
}>;
|
|
191
229
|
};
|
|
192
230
|
};
|
|
193
231
|
export type VaultSpec = typeof vaultSpec;
|
|
@@ -96,6 +96,7 @@ export declare class Vault {
|
|
|
96
96
|
private migrateOnce;
|
|
97
97
|
private get items();
|
|
98
98
|
private get attachments();
|
|
99
|
+
private get graves();
|
|
99
100
|
/** Where the key state stands; what the panel shows and the tools consult. */
|
|
100
101
|
get lockState(): VaultLockState;
|
|
101
102
|
/**
|
|
@@ -211,16 +212,33 @@ export declare class Vault {
|
|
|
211
212
|
/** Undo a soft delete. */
|
|
212
213
|
restore(id: string): Promise<Item>;
|
|
213
214
|
/**
|
|
214
|
-
* Delete one record for good, together with its attachment rows
|
|
215
|
+
* Delete one record for good, together with its attachment rows, and leave a
|
|
216
|
+
* grave behind so nothing that is still in the cloud can file it again.
|
|
215
217
|
*
|
|
216
218
|
* The bytes behind an attachment live in dsh's own store, which never deletes
|
|
217
219
|
* automatically — emptying the recycle bin drops our references, not their
|
|
218
220
|
* objects. A row another record still references is left alone.
|
|
219
221
|
*
|
|
222
|
+
* The grave is the other half of that sentence, and it is why this is not
|
|
223
|
+
* called `remove` any more: deleting the row really does take this machine's
|
|
224
|
+
* copy away, but a copy under a *different* `sync/` tree in the same bucket
|
|
225
|
+
* survives the purge of our own tree (`../remote/remove.ts` only ever deletes
|
|
226
|
+
* under this vault's root), and the merge has no local row to outrank it with.
|
|
227
|
+
* Measured 2026-09-21: thirteen emptied tombstones came back into the bin on
|
|
228
|
+
* every `dsh` restart. The grave is what makes "gone" stick; it holds the id
|
|
229
|
+
* and the moment, no content.
|
|
230
|
+
*
|
|
220
231
|
* @param id - record key.
|
|
221
232
|
* @returns whether the record existed.
|
|
222
233
|
*/
|
|
223
|
-
|
|
234
|
+
purge(id: string): Promise<boolean>;
|
|
235
|
+
/**
|
|
236
|
+
* When this id was emptied out of the bin, if it ever was.
|
|
237
|
+
*
|
|
238
|
+
* The merge asks this before taking a copy of a record this vault does not
|
|
239
|
+
* have (see `newerThanPurge` in `../remote/merge.ts`).
|
|
240
|
+
*/
|
|
241
|
+
purgedAt(id: string): string | undefined;
|
|
224
242
|
/**
|
|
225
243
|
* Record an attachment's metadata. The bytes stay in the store named by
|
|
226
244
|
* `storeId`; this row is our own index over them, keyed by a generated id
|
|
@@ -92,8 +92,10 @@ export interface WebdavSettings {
|
|
|
92
92
|
* cloud drive": a machine that used to sync somewhere else leaves its records
|
|
93
93
|
* behind, and they come back (asked 2026-09-21, 19 records). Merging settles
|
|
94
94
|
* per record by `id` + `updatedAt`, so an older tree cannot overwrite a newer
|
|
95
|
-
* copy —
|
|
96
|
-
* purge leaves
|
|
95
|
+
* copy — and a record the user **emptied out of the bin** stays gone: the
|
|
96
|
+
* grave that `purge` leaves is the thing such a copy cannot outrank (it used
|
|
97
|
+
* to, and thirteen emptied tombstones came back into the bin on every restart
|
|
98
|
+
* — measured 2026-09-21).
|
|
97
99
|
*/
|
|
98
100
|
adoptForeignRoots: boolean;
|
|
99
101
|
username: string;
|
|
@@ -175,6 +177,15 @@ export interface PullResult {
|
|
|
175
177
|
merged?: number;
|
|
176
178
|
/** How many of {@link merged} were ids this vault did not have at all. */
|
|
177
179
|
added?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Copies the cloud still holds of records this vault **emptied out of the bin**.
|
|
182
|
+
*
|
|
183
|
+
* Not taken in, on purpose: `purge` leaves a grave, and a copy that is not
|
|
184
|
+
* *newer* than it does not come back (see `graves` in `src/host/vault/spec.ts`).
|
|
185
|
+
* Reported because a reader who just emptied the bin is owed the sight of the
|
|
186
|
+
* older tree trying and being refused.
|
|
187
|
+
*/
|
|
188
|
+
purged?: number;
|
|
178
189
|
/** How many of {@link merged} were deletions (they land in the recycle bin). */
|
|
179
190
|
deletions?: number;
|
|
180
191
|
/** Attachment objects the merge had to fetch and admit locally. */
|
|
@@ -200,6 +211,18 @@ export interface PullResult {
|
|
|
200
211
|
skippedSync?: number;
|
|
201
212
|
/** How many of {@link skipped} were older than the last pull's cursor. */
|
|
202
213
|
skippedOlder?: number;
|
|
214
|
+
/**
|
|
215
|
+
* How many of {@link skipped} were **folders**, not files.
|
|
216
|
+
*
|
|
217
|
+
* S3 has no folders, so a folder the user made in the cloud console is a
|
|
218
|
+
* placeholder object whose key ends with `/` — and the listing hands the drop
|
|
219
|
+
* folder's own placeholder back like any other object. Reading one is not a
|
|
220
|
+
* file read: 数据胶囊 answers `HTTP 500 {"msg":"未知运行时异常"}`, which the
|
|
221
|
+
* panel showed as `失败 1:inbox:取对象失败:HTTP 500 …` on every refresh,
|
|
222
|
+
* because a failed entry pins the pull cursor (measured 2026-09-21). They are
|
|
223
|
+
* skipped by shape now; the count is here for whoever is debugging a listing.
|
|
224
|
+
*/
|
|
225
|
+
skippedFolders?: number;
|
|
203
226
|
/**
|
|
204
227
|
* How many of {@link skipped} sat under a `…/sync/` prefix that is **not**
|
|
205
228
|
* ours — another machine syncing under a different directory.
|
package/package.json
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@chance722/dsh-inbox",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"private": false,
|
|
5
|
-
"description": "Personal paste inbox for DeepSeek Harness (dsh): capture, classify, browse and retrieve.",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"dsh",
|
|
8
|
-
"dsh-plugin",
|
|
9
|
-
"deepseek-harness",
|
|
10
|
-
"inbox",
|
|
11
|
-
"clipboard",
|
|
12
|
-
"vault"
|
|
13
|
-
],
|
|
14
|
-
"repository": {
|
|
15
|
-
"type": "git",
|
|
16
|
-
"url": "git+https://github.com/Chance722/dsh-inbox.git"
|
|
17
|
-
},
|
|
18
|
-
"homepage": "https://github.com/Chance722/dsh-inbox#readme",
|
|
19
|
-
"bugs": {
|
|
20
|
-
"url": "https://github.com/Chance722/dsh-inbox/issues"
|
|
21
|
-
},
|
|
22
|
-
"publishConfig": {
|
|
23
|
-
"access": "public"
|
|
24
|
-
},
|
|
25
|
-
"type": "module",
|
|
26
|
-
"license": "MIT",
|
|
27
|
-
"main": "lib/index.js",
|
|
28
|
-
"types": "lib/types/host/index.d.ts",
|
|
29
|
-
"exports": {
|
|
30
|
-
".": {
|
|
31
|
-
"types": "./lib/types/host/index.d.ts",
|
|
32
|
-
"default": "./lib/index.js"
|
|
33
|
-
},
|
|
34
|
-
"./client": {
|
|
35
|
-
"types": "./lib/types/client/index.d.ts",
|
|
36
|
-
"default": "./lib/client.js"
|
|
37
|
-
},
|
|
38
|
-
"./package.json": "./package.json"
|
|
39
|
-
},
|
|
40
|
-
"files": [
|
|
41
|
-
"lib/index.js",
|
|
42
|
-
"lib/client.js",
|
|
43
|
-
"lib/cli.js",
|
|
44
|
-
"cordis.patch.yml",
|
|
45
|
-
"lib/types/**/*.d.ts"
|
|
46
|
-
],
|
|
47
|
-
"bin": {
|
|
48
|
-
"dsh-inbox": "./lib/cli.js"
|
|
49
|
-
},
|
|
50
|
-
"dsh": {
|
|
51
|
-
"bundle": {
|
|
52
|
-
"patch": "./cordis.patch.yml"
|
|
53
|
-
},
|
|
54
|
-
"client": {
|
|
55
|
-
"platform": "web",
|
|
56
|
-
"inject": [
|
|
57
|
-
"@deepseek-ai/dsh-client-connection",
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-layout",
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"build": "node scripts/build.mjs && tsc -p tsconfig.build.json",
|
|
65
|
-
"typecheck": "tsc --noEmit",
|
|
66
|
-
"test": "vitest run",
|
|
67
|
-
"prepublishOnly": "npm run build",
|
|
68
|
-
"dev:status": "node scripts/dev.mjs status",
|
|
69
|
-
"dev:local": "node scripts/dev.mjs local",
|
|
70
|
-
"dev:npm": "node scripts/dev.mjs npm"
|
|
71
|
-
},
|
|
72
|
-
"dependencies": {
|
|
73
|
-
"lucide-react": "^1.47.0",
|
|
74
|
-
"zod": "^4.4.3"
|
|
75
|
-
},
|
|
76
|
-
"peerDependencies": {
|
|
77
|
-
"@deepseek-ai/cordis": "^4.0.2",
|
|
78
|
-
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.2",
|
|
79
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
80
|
-
"@deepseek-ai/dsh-commands": "^0.1.5-rc.2",
|
|
81
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
|
82
|
-
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
83
|
-
"@deepseek-ai/dsh-web": "^0.1.5-rc.2"
|
|
84
|
-
},
|
|
85
|
-
"devDependencies": {
|
|
86
|
-
"@deepseek-ai/cordis": "^4.0.2",
|
|
87
|
-
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.2",
|
|
88
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
89
|
-
"@deepseek-ai/dsh-commands": "^0.1.5-rc.2",
|
|
90
|
-
"@deepseek-ai/dsh-storage": "^0.1.5-rc.2",
|
|
91
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
|
92
|
-
"@deepseek-ai/dsh-storage-json": "^0.1.5-rc.2",
|
|
93
|
-
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
94
|
-
"@deepseek-ai/dsh-web": "0.1.5-rc.2",
|
|
95
|
-
"@deepseek-ai/schemastery": "3.18.2",
|
|
96
|
-
"@types/node": "^22.10.0",
|
|
97
|
-
"@types/react": "~18.3.1",
|
|
98
|
-
"esbuild": "^0.25.0",
|
|
99
|
-
"react": "^18.2.0",
|
|
100
|
-
"typescript": "^5.7.0",
|
|
101
|
-
"vitest": "^3.0.0",
|
|
102
|
-
"zod": "^4.4.3"
|
|
103
|
-
}
|
|
104
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@chance722/dsh-inbox",
|
|
3
|
+
"version": "0.2.8",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Personal paste inbox for DeepSeek Harness (dsh): capture, classify, browse and retrieve.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"inbox",
|
|
11
|
+
"clipboard",
|
|
12
|
+
"vault"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Chance722/dsh-inbox.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/Chance722/dsh-inbox#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/Chance722/dsh-inbox/issues"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"main": "lib/index.js",
|
|
28
|
+
"types": "lib/types/host/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./lib/types/host/index.d.ts",
|
|
32
|
+
"default": "./lib/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./client": {
|
|
35
|
+
"types": "./lib/types/client/index.d.ts",
|
|
36
|
+
"default": "./lib/client.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"lib/index.js",
|
|
42
|
+
"lib/client.js",
|
|
43
|
+
"lib/cli.js",
|
|
44
|
+
"cordis.patch.yml",
|
|
45
|
+
"lib/types/**/*.d.ts"
|
|
46
|
+
],
|
|
47
|
+
"bin": {
|
|
48
|
+
"dsh-inbox": "./lib/cli.js"
|
|
49
|
+
},
|
|
50
|
+
"dsh": {
|
|
51
|
+
"bundle": {
|
|
52
|
+
"patch": "./cordis.patch.yml"
|
|
53
|
+
},
|
|
54
|
+
"client": {
|
|
55
|
+
"platform": "web",
|
|
56
|
+
"inject": [
|
|
57
|
+
"@deepseek-ai/dsh-client-connection",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "node scripts/build.mjs && tsc -p tsconfig.build.json",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"prepublishOnly": "npm run build",
|
|
68
|
+
"dev:status": "node scripts/dev.mjs status",
|
|
69
|
+
"dev:local": "node scripts/dev.mjs local",
|
|
70
|
+
"dev:npm": "node scripts/dev.mjs npm"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"lucide-react": "^1.47.0",
|
|
74
|
+
"zod": "^4.4.3"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
78
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-commands": "^0.1.5-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-web": "^0.1.5-rc.2"
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
87
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-commands": "^0.1.5-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-storage": "^0.1.5-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
|
|
92
|
+
"@deepseek-ai/dsh-storage-json": "^0.1.5-rc.2",
|
|
93
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
94
|
+
"@deepseek-ai/dsh-web": "0.1.5-rc.2",
|
|
95
|
+
"@deepseek-ai/schemastery": "3.18.2",
|
|
96
|
+
"@types/node": "^22.10.0",
|
|
97
|
+
"@types/react": "~18.3.1",
|
|
98
|
+
"esbuild": "^0.25.0",
|
|
99
|
+
"react": "^18.2.0",
|
|
100
|
+
"typescript": "^5.7.0",
|
|
101
|
+
"vitest": "^3.0.0",
|
|
102
|
+
"zod": "^4.4.3"
|
|
103
|
+
}
|
|
104
|
+
}
|