@chance722/dsh-inbox 0.2.3 → 0.2.4
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 +141 -134
- package/README.zh.md +141 -134
- package/cordis.patch.yml +11 -11
- package/lib/client.js +156 -55
- package/lib/index.js +106 -19
- package/lib/types/client/messages.d.ts +36 -18
- package/lib/types/host/remote/merge.d.ts +17 -1
- package/lib/types/host/remote/pull.d.ts +25 -3
- package/lib/types/host/s3/client.d.ts +25 -1
- package/lib/types/host/webdav/config.d.ts +4 -0
- package/lib/types/shared/panel-wire.d.ts +34 -0
- package/package.json +1 -1
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.4" : "0.0.0-dev";
|
|
7
7
|
var DEFAULT_USER_AGENT = "dsh-inbox";
|
|
8
8
|
|
|
9
9
|
// src/shared/vocabulary.ts
|
|
@@ -749,6 +749,7 @@ var KEY_BLOCK = /<Key>([\s\S]*?)<\/Key>/i;
|
|
|
749
749
|
var LAST_MODIFIED = /<LastModified>([\s\S]*?)<\/LastModified>/i;
|
|
750
750
|
var SIZE = /<Size>([\s\S]*?)<\/Size>/i;
|
|
751
751
|
var CONTENTS = /<Contents>[\s\S]*?<\/Contents>/gi;
|
|
752
|
+
var COMMON_PREFIX = /<CommonPrefixes>[\s\S]*?<Prefix>([\s\S]*?)<\/Prefix>[\s\S]*?<\/CommonPrefixes>/gi;
|
|
752
753
|
function decode(value) {
|
|
753
754
|
return value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").trim();
|
|
754
755
|
}
|
|
@@ -768,15 +769,15 @@ function parseListing(xml) {
|
|
|
768
769
|
}
|
|
769
770
|
return objects;
|
|
770
771
|
}
|
|
771
|
-
async function listPrefix(config, prefix, deps) {
|
|
772
|
+
async function listPrefix(config, prefix, deps, extra = {}) {
|
|
772
773
|
const sign = signer(config);
|
|
773
|
-
const v2 = sign(config, deps, "GET", "", { "list-type": "2", prefix });
|
|
774
|
+
const v2 = sign(config, deps, "GET", "", { "list-type": "2", prefix, ...extra });
|
|
774
775
|
const first = await deps.fetch(v2.url, { method: "GET", headers: v2.headers });
|
|
775
776
|
if (first.ok) return parseListing(await first.text());
|
|
776
777
|
const refusal2 = await refused(first, "\u5217\u5BF9\u8C61");
|
|
777
778
|
if (first.status < 500) {
|
|
778
779
|
if (config.signatureVersion?.toLowerCase() !== "v2" && first.status === 401) {
|
|
779
|
-
const minimal = signRequestV4Minimal(config, deps, "GET", "", { "list-type": "2", prefix });
|
|
780
|
+
const minimal = signRequestV4Minimal(config, deps, "GET", "", { "list-type": "2", prefix, ...extra });
|
|
780
781
|
const retry = await deps.fetch(minimal.url, { method: "GET", headers: minimal.headers });
|
|
781
782
|
if (retry.ok) return parseListing(await retry.text());
|
|
782
783
|
throw new Error(
|
|
@@ -786,7 +787,7 @@ async function listPrefix(config, prefix, deps) {
|
|
|
786
787
|
}
|
|
787
788
|
throw refusal2;
|
|
788
789
|
}
|
|
789
|
-
const v1 = sign(config, deps, "GET", "", { prefix });
|
|
790
|
+
const v1 = sign(config, deps, "GET", "", { prefix, ...extra });
|
|
790
791
|
const second = await deps.fetch(v1.url, { method: "GET", headers: v1.headers });
|
|
791
792
|
if (!second.ok) {
|
|
792
793
|
throw new Error(`${await refused(second, "\u5217\u5BF9\u8C61\uFF08V1 \u56DE\u9000\uFF09").then((e) => e.message)}
|
|
@@ -794,6 +795,27 @@ async function listPrefix(config, prefix, deps) {
|
|
|
794
795
|
}
|
|
795
796
|
return parseListing(await second.text());
|
|
796
797
|
}
|
|
798
|
+
async function listTopLevel(config, deps) {
|
|
799
|
+
const sign = signer(config);
|
|
800
|
+
const v2 = sign(config, deps, "GET", "", { "list-type": "2", delimiter: "/", "max-keys": "100" });
|
|
801
|
+
const first = await deps.fetch(v2.url, { method: "GET", headers: v2.headers });
|
|
802
|
+
if (first.ok) return parsePrefixes(await first.text());
|
|
803
|
+
if (first.status >= 500) {
|
|
804
|
+
const v1 = sign(config, deps, "GET", "", { delimiter: "/", "max-keys": "100" });
|
|
805
|
+
const second = await deps.fetch(v1.url, { method: "GET", headers: v1.headers });
|
|
806
|
+
if (second.ok) return parsePrefixes(await second.text());
|
|
807
|
+
}
|
|
808
|
+
return [];
|
|
809
|
+
}
|
|
810
|
+
function parsePrefixes(xml) {
|
|
811
|
+
const prefixes = [];
|
|
812
|
+
for (const match of xml.matchAll(COMMON_PREFIX)) {
|
|
813
|
+
const value = decode(match[1] ?? "");
|
|
814
|
+
if (value.length === 0) continue;
|
|
815
|
+
prefixes.push(value.replace(/\/$/, ""));
|
|
816
|
+
}
|
|
817
|
+
return prefixes;
|
|
818
|
+
}
|
|
797
819
|
async function readObject(config, key, deps) {
|
|
798
820
|
const signed = signer(config)(config, deps, "GET", key);
|
|
799
821
|
const response = await deps.fetch(signed.url, { method: "GET", headers: signed.headers });
|
|
@@ -978,6 +1000,7 @@ var DEFAULT_SETTINGS = {
|
|
|
978
1000
|
protocol: "webdav",
|
|
979
1001
|
baseUrl: "",
|
|
980
1002
|
directory: "/inbox",
|
|
1003
|
+
adoptForeignRoots: false,
|
|
981
1004
|
username: "",
|
|
982
1005
|
endpoint: "",
|
|
983
1006
|
bucket: "",
|
|
@@ -991,6 +1014,7 @@ var WebdavSettingsSchema = z.object({
|
|
|
991
1014
|
protocol: z.union(["webdav", "s3"]).default("webdav"),
|
|
992
1015
|
baseUrl: z.string().default(""),
|
|
993
1016
|
directory: z.string().default("/inbox"),
|
|
1017
|
+
adoptForeignRoots: z.boolean().default(false),
|
|
994
1018
|
username: z.string().default(""),
|
|
995
1019
|
endpoint: z.string().default(""),
|
|
996
1020
|
bucket: z.string().default(""),
|
|
@@ -1062,6 +1086,7 @@ async function saveWebdav(ctx, vault, base, patch) {
|
|
|
1062
1086
|
const directory = patch.directory.trim();
|
|
1063
1087
|
config.directory = directory.startsWith("/") ? directory : `/${directory}`;
|
|
1064
1088
|
}
|
|
1089
|
+
if (patch.adoptForeignRoots !== void 0) config.adoptForeignRoots = patch.adoptForeignRoots;
|
|
1065
1090
|
if (patch.username !== void 0) config.username = patch.username.trim();
|
|
1066
1091
|
if (patch.endpoint !== void 0) config.endpoint = normalizeUrl(patch.endpoint);
|
|
1067
1092
|
if (patch.bucket !== void 0) config.bucket = patch.bucket.trim();
|
|
@@ -1137,7 +1162,7 @@ function isNewer(entry, lastPullAt) {
|
|
|
1137
1162
|
if (Number.isNaN(seen) || Number.isNaN(remote)) return true;
|
|
1138
1163
|
return remote > seen;
|
|
1139
1164
|
}
|
|
1140
|
-
async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(void 0)) {
|
|
1165
|
+
async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(void 0), elsewhere = { roots: [] }) {
|
|
1141
1166
|
const lastPullAt = vault.global.sync.lastPullAt;
|
|
1142
1167
|
let entries;
|
|
1143
1168
|
try {
|
|
@@ -1153,7 +1178,10 @@ async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(vo
|
|
|
1153
1178
|
let skippedSync = 0;
|
|
1154
1179
|
let skippedOlder = 0;
|
|
1155
1180
|
let skippedForeign = 0;
|
|
1156
|
-
|
|
1181
|
+
let remoteRecords = 0;
|
|
1182
|
+
let remoteAttachments = 0;
|
|
1183
|
+
const foreignSyncRoots = new Set(elsewhere.roots);
|
|
1184
|
+
let foreignRecords = elsewhere.records ?? 0;
|
|
1157
1185
|
let newestSuccess;
|
|
1158
1186
|
const failures = [];
|
|
1159
1187
|
for (const entry of entries) {
|
|
@@ -1162,11 +1190,20 @@ async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(vo
|
|
|
1162
1190
|
skipped += 1;
|
|
1163
1191
|
skippedForeign += 1;
|
|
1164
1192
|
foreignSyncRoots.add(prefix);
|
|
1193
|
+
const parts = entry.path.split("/").filter((part) => part.length > 0);
|
|
1194
|
+
if ((parts[parts.length - 2] ?? "") === "items" && (parts[parts.length - 1] ?? "").endsWith(".json")) {
|
|
1195
|
+
foreignRecords += 1;
|
|
1196
|
+
}
|
|
1165
1197
|
continue;
|
|
1166
1198
|
}
|
|
1167
1199
|
if (prefix !== void 0) {
|
|
1168
1200
|
skipped += 1;
|
|
1169
1201
|
skippedSync += 1;
|
|
1202
|
+
const parts = entry.path.split("/").filter((part) => part.length > 0);
|
|
1203
|
+
const folder = parts[parts.length - 2] ?? "";
|
|
1204
|
+
const name3 = parts[parts.length - 1] ?? "";
|
|
1205
|
+
if (folder === "items" && name3.endsWith(".json")) remoteRecords += 1;
|
|
1206
|
+
else if (folder === "attachments" && !name3.endsWith(".meta.json")) remoteAttachments += 1;
|
|
1170
1207
|
continue;
|
|
1171
1208
|
}
|
|
1172
1209
|
if (!isNewer(entry, lastPullAt)) {
|
|
@@ -1232,8 +1269,11 @@ async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(vo
|
|
|
1232
1269
|
skippedSync,
|
|
1233
1270
|
skippedOlder,
|
|
1234
1271
|
skippedForeign,
|
|
1272
|
+
remoteRecords,
|
|
1273
|
+
remoteAttachments,
|
|
1235
1274
|
syncRoot: syncRoot3,
|
|
1236
1275
|
...foreignSyncRoots.size === 0 ? {} : { foreignSyncRoots: [...foreignSyncRoots] },
|
|
1276
|
+
...foreignRecords === 0 ? {} : { foreignRecords },
|
|
1237
1277
|
listed,
|
|
1238
1278
|
lastPullAt: cursor,
|
|
1239
1279
|
...failures.length === 0 ? {} : { reason: failures.slice(0, 3).join("\uFF1B") }
|
|
@@ -1267,7 +1307,7 @@ async function pullRemote(vault, config, deps) {
|
|
|
1267
1307
|
syncRootFor(config.directory)
|
|
1268
1308
|
);
|
|
1269
1309
|
}
|
|
1270
|
-
async function pullS3(vault, config,
|
|
1310
|
+
async function pullS3(vault, config, directory, deps) {
|
|
1271
1311
|
if (config.endpoint.trim().length === 0 || config.bucket.trim().length === 0) {
|
|
1272
1312
|
return {
|
|
1273
1313
|
status: "unconfigured",
|
|
@@ -1278,20 +1318,44 @@ async function pullS3(vault, config, prefix, deps) {
|
|
|
1278
1318
|
listed: 0
|
|
1279
1319
|
};
|
|
1280
1320
|
}
|
|
1321
|
+
const scope = syncDirectory(directory);
|
|
1281
1322
|
return ingestFrom(
|
|
1282
1323
|
vault,
|
|
1283
1324
|
{
|
|
1284
|
-
list: async () => (await listPrefix(config,
|
|
1325
|
+
list: async () => (await listPrefix(config, `${scope}/`, deps)).map((object) => ({
|
|
1285
1326
|
path: object.key,
|
|
1286
1327
|
...object.lastModified === void 0 ? {} : { lastModified: object.lastModified }
|
|
1287
1328
|
})),
|
|
1288
1329
|
read: async (entry) => readObject(config, entry.path, deps)
|
|
1289
1330
|
},
|
|
1290
1331
|
deps.attachments,
|
|
1291
|
-
//
|
|
1292
|
-
|
|
1332
|
+
// The vault's own tree, resolved by the same rule the writer uses.
|
|
1333
|
+
syncRootFor(directory),
|
|
1334
|
+
await s3RootsElsewhere(config, deps, scope)
|
|
1293
1335
|
);
|
|
1294
1336
|
}
|
|
1337
|
+
var PROBE_CANDIDATES = 5;
|
|
1338
|
+
async function s3RootsElsewhere(config, deps, scope) {
|
|
1339
|
+
try {
|
|
1340
|
+
const tops = await listTopLevel(config, deps);
|
|
1341
|
+
const found = [];
|
|
1342
|
+
let records = 0;
|
|
1343
|
+
for (const top of tops.slice(0, PROBE_CANDIDATES)) {
|
|
1344
|
+
if (top === scope || top.startsWith(`${scope}/`)) continue;
|
|
1345
|
+
for (const probe2 of [`${top}/sync/items/`, `${top}/items/`]) {
|
|
1346
|
+
const inside = await listPrefix(config, probe2, deps, { "max-keys": "1000" });
|
|
1347
|
+
if (inside.length > 0) {
|
|
1348
|
+
found.push(probe2.replace(/\/items\/$/, ""));
|
|
1349
|
+
records += inside.filter((object) => object.key.endsWith(".json")).length;
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return found.length === 0 ? { roots: [] } : { roots: found, records };
|
|
1355
|
+
} catch {
|
|
1356
|
+
return { roots: [] };
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1295
1359
|
|
|
1296
1360
|
// src/host/remote/merge.ts
|
|
1297
1361
|
import { admitEncodedFile as admitEncodedFile2, admitEncodedImages as admitEncodedImages2 } from "@deepseek-ai/dsh-attachment";
|
|
@@ -1339,13 +1403,14 @@ function wins(remote, local) {
|
|
|
1339
1403
|
async function mergeOnce(vault, tree, prefix, admit) {
|
|
1340
1404
|
const failures = [];
|
|
1341
1405
|
let merged = 0;
|
|
1406
|
+
let added = 0;
|
|
1342
1407
|
let kept = 0;
|
|
1343
1408
|
let attachments = 0;
|
|
1344
1409
|
let objects;
|
|
1345
1410
|
try {
|
|
1346
1411
|
objects = await tree.list(`${prefix}/items/`);
|
|
1347
1412
|
} catch (error) {
|
|
1348
|
-
return { merged: 0, kept: 0, attachments: 0, failures: [`\u5217\u8FDC\u7AEF\u540C\u6B65\u76EE\u5F55\u5931\u8D25\uFF1A${reasonOf(error)}`] };
|
|
1413
|
+
return { merged: 0, added: 0, kept: 0, attachments: 0, failures: [`\u5217\u8FDC\u7AEF\u540C\u6B65\u76EE\u5F55\u5931\u8D25\uFF1A${reasonOf(error)}`] };
|
|
1349
1414
|
}
|
|
1350
1415
|
const wanted = /* @__PURE__ */ new Map();
|
|
1351
1416
|
const bytesAt = /* @__PURE__ */ new Map();
|
|
@@ -1375,6 +1440,7 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1375
1440
|
}
|
|
1376
1441
|
await vault.import(remote);
|
|
1377
1442
|
merged += 1;
|
|
1443
|
+
if (local === void 0) added += 1;
|
|
1378
1444
|
for (const attachmentId of remote.attachmentIds) {
|
|
1379
1445
|
if (vault.getAttachment(attachmentId) === void 0) {
|
|
1380
1446
|
wanted.set(attachmentId, remote.id);
|
|
@@ -1409,7 +1475,7 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1409
1475
|
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A${reasonOf(error)}`);
|
|
1410
1476
|
}
|
|
1411
1477
|
}
|
|
1412
|
-
return { merged, kept, attachments, failures };
|
|
1478
|
+
return { merged, added, kept, attachments, failures };
|
|
1413
1479
|
}
|
|
1414
1480
|
function extensionOfName(path) {
|
|
1415
1481
|
return /\.([A-Za-z0-9]{1,8})$/.exec(nameOf2(path))?.[1]?.toLowerCase() ?? "bin";
|
|
@@ -1417,15 +1483,26 @@ function extensionOfName(path) {
|
|
|
1417
1483
|
function reasonOf(error) {
|
|
1418
1484
|
return error instanceof Error ? error.message : String(error);
|
|
1419
1485
|
}
|
|
1420
|
-
async function mergeRemote(ctx, vault, attachments) {
|
|
1486
|
+
async function mergeRemote(ctx, vault, attachments, extraRoots = []) {
|
|
1421
1487
|
const settings = readSettings(ctx);
|
|
1422
1488
|
const prefix = syncRoot(settings);
|
|
1423
1489
|
try {
|
|
1424
1490
|
const tree = settings.protocol === "s3" ? await s3Tree(ctx, settings) : await webdavTree(ctx, settings);
|
|
1425
|
-
if (tree === void 0) return { merged: 0, kept: 0, attachments: 0, failures: [] };
|
|
1426
|
-
|
|
1491
|
+
if (tree === void 0) return { merged: 0, added: 0, kept: 0, attachments: 0, failures: [] };
|
|
1492
|
+
const admit = admitWith(attachments);
|
|
1493
|
+
const outcome = await mergeOnce(vault, tree, prefix, admit);
|
|
1494
|
+
for (const root of extraRoots) {
|
|
1495
|
+
if (root === prefix) continue;
|
|
1496
|
+
const extra = await mergeOnce(vault, tree, root, admit);
|
|
1497
|
+
outcome.merged += extra.merged;
|
|
1498
|
+
outcome.added += extra.added;
|
|
1499
|
+
outcome.kept += extra.kept;
|
|
1500
|
+
outcome.attachments += extra.attachments;
|
|
1501
|
+
outcome.failures.push(...extra.failures);
|
|
1502
|
+
}
|
|
1503
|
+
return outcome;
|
|
1427
1504
|
} catch (error) {
|
|
1428
|
-
return { merged: 0, kept: 0, attachments: 0, failures: [reasonOf(error)] };
|
|
1505
|
+
return { merged: 0, added: 0, kept: 0, attachments: 0, failures: [reasonOf(error)] };
|
|
1429
1506
|
}
|
|
1430
1507
|
}
|
|
1431
1508
|
function admitWith(store) {
|
|
@@ -1524,11 +1601,15 @@ async function runPull(ctx, vault, attachments) {
|
|
|
1524
1601
|
}
|
|
1525
1602
|
async function withMerge(ctx, vault, attachments, pulled) {
|
|
1526
1603
|
if (pulled.status !== "ok" || attachments === void 0) return pulled;
|
|
1527
|
-
const
|
|
1604
|
+
const settings = readSettings(ctx);
|
|
1605
|
+
const extraRoots = settings.adoptForeignRoots ? pulled.foreignSyncRoots ?? [] : [];
|
|
1606
|
+
const outcome = await mergeRemote(ctx, vault, attachments, extraRoots);
|
|
1528
1607
|
const troubles = [...pulled.failed > 0 && pulled.reason !== void 0 ? [pulled.reason] : [], ...outcome.failures];
|
|
1529
1608
|
return {
|
|
1530
1609
|
...pulled,
|
|
1531
1610
|
merged: outcome.merged,
|
|
1611
|
+
added: outcome.added,
|
|
1612
|
+
kept: outcome.kept,
|
|
1532
1613
|
attachments: outcome.attachments,
|
|
1533
1614
|
failed: pulled.failed + outcome.failures.length,
|
|
1534
1615
|
...troubles.length === 0 ? {} : { reason: troubles.slice(0, 3).join("\uFF1B") }
|
|
@@ -1577,7 +1658,10 @@ async function pullDropFolder(ctx, vault, attachments) {
|
|
|
1577
1658
|
signatureVersion: settings.signatureVersion,
|
|
1578
1659
|
userAgent: activeUserAgent(settings)
|
|
1579
1660
|
},
|
|
1580
|
-
|
|
1661
|
+
// The configured directory, not a pre-trimmed string: `/`, `''` and
|
|
1662
|
+
// "unset" all mean the default, and the ingest resolves the vault's own
|
|
1663
|
+
// root from the same rule the writer uses.
|
|
1664
|
+
settings.directory,
|
|
1581
1665
|
{
|
|
1582
1666
|
fetch: s3Fetch,
|
|
1583
1667
|
accessKeyId: settings.accessKeyId,
|
|
@@ -3102,6 +3186,9 @@ async function handleWebdav(ctx, vault, payload) {
|
|
|
3102
3186
|
if (typeof request.protocol === "string") patch.protocol = request.protocol;
|
|
3103
3187
|
if (typeof request.baseUrl === "string") patch.baseUrl = request.baseUrl;
|
|
3104
3188
|
if (typeof request.directory === "string") patch.directory = request.directory;
|
|
3189
|
+
if (typeof request.adoptForeignRoots === "boolean") {
|
|
3190
|
+
patch.adoptForeignRoots = request.adoptForeignRoots;
|
|
3191
|
+
}
|
|
3105
3192
|
if (typeof request.username === "string") patch.username = request.username;
|
|
3106
3193
|
if (typeof request.password === "string") patch.password = request.password;
|
|
3107
3194
|
if (typeof request.endpoint === "string") patch.endpoint = request.endpoint;
|
|
@@ -129,6 +129,9 @@ export declare const zh: {
|
|
|
129
129
|
'settings.region': string;
|
|
130
130
|
'settings.bucketAddress': string;
|
|
131
131
|
'settings.bucketDir': string;
|
|
132
|
+
'settings.adoptForeign': string;
|
|
133
|
+
'settings.adoptForeignHint': string;
|
|
134
|
+
'settings.directoryPlaceholder': string;
|
|
132
135
|
'settings.dirS3.lead': string;
|
|
133
136
|
'settings.dirS3.mid': string;
|
|
134
137
|
'settings.dirS3.tail': string;
|
|
@@ -176,17 +179,19 @@ export declare const zh: {
|
|
|
176
179
|
'sync.pushUnconfigured': string;
|
|
177
180
|
'sync.pushFailed': string;
|
|
178
181
|
'sync.pushUnknown': string;
|
|
179
|
-
'sync.pushUpToDate': string;
|
|
180
|
-
'sync.pushDone': string;
|
|
181
182
|
'sync.pushPartial': string;
|
|
182
183
|
'sync.pullNoAddress': string;
|
|
183
184
|
'sync.pullFailed': string;
|
|
184
|
-
'sync.
|
|
185
|
-
'sync.
|
|
186
|
-
'sync.
|
|
187
|
-
'sync.
|
|
188
|
-
'sync.
|
|
185
|
+
'sync.detailPushedIdle': string;
|
|
186
|
+
'sync.detailPushed': string;
|
|
187
|
+
'sync.detailPulled': string;
|
|
188
|
+
'sync.detailAdded': string;
|
|
189
|
+
'sync.detailSkip': string;
|
|
189
190
|
'sync.pullForeignSync': string;
|
|
191
|
+
'sync.detailForeign': string;
|
|
192
|
+
'sync.shortPushed': string;
|
|
193
|
+
'sync.shortPulled': string;
|
|
194
|
+
'sync.shortIdle': string;
|
|
190
195
|
'sync.pullFailures': string;
|
|
191
196
|
'sync.thisPage': string;
|
|
192
197
|
'settings.status': string;
|
|
@@ -253,10 +258,14 @@ export declare const zh: {
|
|
|
253
258
|
'manual.5.auto.body': string;
|
|
254
259
|
'manual.5.manual.label': string;
|
|
255
260
|
'manual.5.manual.body': string;
|
|
261
|
+
'manual.5.twoMachines.label': string;
|
|
262
|
+
'manual.5.twoMachines.body': string;
|
|
256
263
|
'manual.5.delete.label': string;
|
|
257
264
|
'manual.5.delete.body': string;
|
|
258
|
-
'manual.5.
|
|
259
|
-
'manual.5.
|
|
265
|
+
'manual.5.merge.label': string;
|
|
266
|
+
'manual.5.merge.body': string;
|
|
267
|
+
'manual.5.cloud.label': string;
|
|
268
|
+
'manual.5.cloud.body': string;
|
|
260
269
|
'manual.6.title': string;
|
|
261
270
|
'manual.6.query.label': string;
|
|
262
271
|
'manual.6.query.body': string;
|
|
@@ -389,6 +398,9 @@ export declare const MESSAGES: {
|
|
|
389
398
|
'settings.region': string;
|
|
390
399
|
'settings.bucketAddress': string;
|
|
391
400
|
'settings.bucketDir': string;
|
|
401
|
+
'settings.adoptForeign': string;
|
|
402
|
+
'settings.adoptForeignHint': string;
|
|
403
|
+
'settings.directoryPlaceholder': string;
|
|
392
404
|
'settings.dirS3.lead': string;
|
|
393
405
|
'settings.dirS3.mid': string;
|
|
394
406
|
'settings.dirS3.tail': string;
|
|
@@ -436,17 +448,19 @@ export declare const MESSAGES: {
|
|
|
436
448
|
'sync.pushUnconfigured': string;
|
|
437
449
|
'sync.pushFailed': string;
|
|
438
450
|
'sync.pushUnknown': string;
|
|
439
|
-
'sync.pushUpToDate': string;
|
|
440
|
-
'sync.pushDone': string;
|
|
441
451
|
'sync.pushPartial': string;
|
|
442
452
|
'sync.pullNoAddress': string;
|
|
443
453
|
'sync.pullFailed': string;
|
|
444
|
-
'sync.
|
|
445
|
-
'sync.
|
|
446
|
-
'sync.
|
|
447
|
-
'sync.
|
|
448
|
-
'sync.
|
|
454
|
+
'sync.detailPushedIdle': string;
|
|
455
|
+
'sync.detailPushed': string;
|
|
456
|
+
'sync.detailPulled': string;
|
|
457
|
+
'sync.detailAdded': string;
|
|
458
|
+
'sync.detailSkip': string;
|
|
449
459
|
'sync.pullForeignSync': string;
|
|
460
|
+
'sync.detailForeign': string;
|
|
461
|
+
'sync.shortPushed': string;
|
|
462
|
+
'sync.shortPulled': string;
|
|
463
|
+
'sync.shortIdle': string;
|
|
450
464
|
'sync.pullFailures': string;
|
|
451
465
|
'sync.thisPage': string;
|
|
452
466
|
'settings.status': string;
|
|
@@ -513,10 +527,14 @@ export declare const MESSAGES: {
|
|
|
513
527
|
'manual.5.auto.body': string;
|
|
514
528
|
'manual.5.manual.label': string;
|
|
515
529
|
'manual.5.manual.body': string;
|
|
530
|
+
'manual.5.twoMachines.label': string;
|
|
531
|
+
'manual.5.twoMachines.body': string;
|
|
516
532
|
'manual.5.delete.label': string;
|
|
517
533
|
'manual.5.delete.body': string;
|
|
518
|
-
'manual.5.
|
|
519
|
-
'manual.5.
|
|
534
|
+
'manual.5.merge.label': string;
|
|
535
|
+
'manual.5.merge.body': string;
|
|
536
|
+
'manual.5.cloud.label': string;
|
|
537
|
+
'manual.5.cloud.body': string;
|
|
520
538
|
'manual.6.title': string;
|
|
521
539
|
'manual.6.query.label': string;
|
|
522
540
|
'manual.6.query.body': string;
|
|
@@ -42,6 +42,14 @@ export interface SyncTree {
|
|
|
42
42
|
export interface MergeOutcome {
|
|
43
43
|
/** Records that were new here, or overwritten because the remote was newer. */
|
|
44
44
|
merged: number;
|
|
45
|
+
/**
|
|
46
|
+
* How many of {@link merged} were ids this vault did not have at all.
|
|
47
|
+
*
|
|
48
|
+
* "19 records came over" and "the vault grew by 6" are both true at once, and
|
|
49
|
+
* the reader who just watched a number expects them to match (asked
|
|
50
|
+
* 2026-09-21, after merging an abandoned tree whose copies mostly overlapped).
|
|
51
|
+
*/
|
|
52
|
+
added: number;
|
|
45
53
|
/** Records the remote had and this machine already had, newer or equal. */
|
|
46
54
|
kept: number;
|
|
47
55
|
/** Attachment objects pulled down (bytes plus their row). */
|
|
@@ -76,5 +84,13 @@ export declare function mergeOnce(vault: Vault, tree: SyncTree, prefix: string,
|
|
|
76
84
|
* @param attachments - where attachment bytes go.
|
|
77
85
|
* @returns the counts and the lines worth showing; never throws.
|
|
78
86
|
*/
|
|
79
|
-
export declare function mergeRemote(ctx: Context, vault: Vault, attachments: AttachmentStore
|
|
87
|
+
export declare function mergeRemote(ctx: Context, vault: Vault, attachments: AttachmentStore,
|
|
88
|
+
/**
|
|
89
|
+
* Further sync trees to merge, on top of this machine's own.
|
|
90
|
+
*
|
|
91
|
+
* The pull hands over the trees it found elsewhere in the bucket (a machine
|
|
92
|
+
* that used to sync under another directory leaves one behind). Each is read
|
|
93
|
+
* exactly like our own: per record, `id` + `updatedAt` decides.
|
|
94
|
+
*/
|
|
95
|
+
extraRoots?: readonly string[]): Promise<MergeOutcome>;
|
|
80
96
|
export {};
|
|
@@ -45,7 +45,16 @@ export interface RemoteSource {
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function ingestFrom(vault: Vault, source: RemoteSource, attachments: AttachmentStore,
|
|
47
47
|
/** This machine's own sync root; anything else under `sync/` is a stranger. */
|
|
48
|
-
syncRoot?: string
|
|
48
|
+
syncRoot?: string,
|
|
49
|
+
/**
|
|
50
|
+
* Sync roots found **outside** the listed scope, from a protocol-specific
|
|
51
|
+
* probe. A listing scoped to our directory cannot see them, and without them
|
|
52
|
+
* "the other machine's records never arrived" has no explanation to show.
|
|
53
|
+
*/
|
|
54
|
+
elsewhere?: {
|
|
55
|
+
roots: readonly string[];
|
|
56
|
+
records?: number;
|
|
57
|
+
}): Promise<PullResult>;
|
|
49
58
|
/** What the user configures for WebDAV. */
|
|
50
59
|
export interface WebdavConfig {
|
|
51
60
|
baseUrl: string;
|
|
@@ -56,7 +65,20 @@ export interface WebdavConfig {
|
|
|
56
65
|
export declare function pullRemote(vault: Vault, config: WebdavConfig, deps: WebdavDeps & {
|
|
57
66
|
attachments: AttachmentStore;
|
|
58
67
|
}): Promise<PullResult>;
|
|
59
|
-
/**
|
|
60
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Pull over S3.
|
|
70
|
+
*
|
|
71
|
+
* @param directory - the configured directory, exactly as the settings hold it.
|
|
72
|
+
* `/`, an empty string and "never set" all mean the default; the listing is
|
|
73
|
+
* scoped to it and the vault's own tree is `<directory>/sync`.
|
|
74
|
+
*
|
|
75
|
+
* This used to be handed the sync root as the *listing* prefix, and to treat
|
|
76
|
+
* that same string as "ours": with S3 every object the vault had uploaded
|
|
77
|
+
* therefore answered to a prefix that was not ours, so the panel reported
|
|
78
|
+
* 「自己的同步对象 0 项」 and warned about another machine's directory — which was
|
|
79
|
+
* this machine's own (measured 2026-09-21, a bucket holding both `sync/…` from
|
|
80
|
+
* the older build and `inbox/sync/…` from this one).
|
|
81
|
+
*/
|
|
82
|
+
export declare function pullS3(vault: Vault, config: S3Config, directory: string | undefined, deps: S3Deps & {
|
|
61
83
|
attachments: AttachmentStore;
|
|
62
84
|
}): Promise<PullResult>;
|
|
@@ -173,7 +173,31 @@ export declare function parseListing(xml: string): RemoteObject[];
|
|
|
173
173
|
* @param deps - credentials, fetch, clock.
|
|
174
174
|
* @returns the objects found.
|
|
175
175
|
*/
|
|
176
|
-
export declare function listPrefix(config: S3Config, prefix: string, deps: S3Deps
|
|
176
|
+
export declare function listPrefix(config: S3Config, prefix: string, deps: S3Deps,
|
|
177
|
+
/** Extra query parameters, e.g. `{ delimiter: '/', 'max-keys': '1' }`. */
|
|
178
|
+
extra?: Record<string, string>): Promise<RemoteObject[]>;
|
|
179
|
+
/**
|
|
180
|
+
* The top-level folders a bucket has.
|
|
181
|
+
*
|
|
182
|
+
* `delimiter=/` collapses everything below the first slash into
|
|
183
|
+
* `<CommonPrefixes>`, which is the cheap way to ask "what else lives up here?" —
|
|
184
|
+
* one request, however many objects the bucket holds. It is what makes "another
|
|
185
|
+
* machine synced into a different directory" visible: that machine's records are
|
|
186
|
+
* not under our directory, so a listing scoped to ours can never see them, and
|
|
187
|
+
* the silence looks exactly like "nothing new" (asked 2026-09-21).
|
|
188
|
+
*
|
|
189
|
+
* @param config - endpoint, bucket, region.
|
|
190
|
+
* @param deps - credentials, fetch, clock.
|
|
191
|
+
* @returns folder names without the trailing slash, e.g. `['inbox', 'sync']`.
|
|
192
|
+
*/
|
|
193
|
+
export declare function listTopLevel(config: S3Config, deps: S3Deps): Promise<string[]>;
|
|
194
|
+
/**
|
|
195
|
+
* Read the folder names out of a `delimiter=/` listing.
|
|
196
|
+
*
|
|
197
|
+
* @param xml - the response body.
|
|
198
|
+
* @returns each prefix without its trailing slash.
|
|
199
|
+
*/
|
|
200
|
+
export declare function parsePrefixes(xml: string): string[];
|
|
177
201
|
/** Fetch one object's bytes, with its content type when the server sends one. */
|
|
178
202
|
export declare function readObject(config: S3Config, key: string, deps: S3Deps): Promise<{
|
|
179
203
|
bytes: Uint8Array;
|
|
@@ -28,6 +28,7 @@ export declare const WebdavSettingsSchema: z<Schemastery.ObjectS<{
|
|
|
28
28
|
protocol: z<"webdav" | "s3", "webdav" | "s3">;
|
|
29
29
|
baseUrl: z<string, string>;
|
|
30
30
|
directory: z<string, string>;
|
|
31
|
+
adoptForeignRoots: z<boolean, boolean>;
|
|
31
32
|
username: z<string, string>;
|
|
32
33
|
endpoint: z<string, string>;
|
|
33
34
|
bucket: z<string, string>;
|
|
@@ -40,6 +41,7 @@ export declare const WebdavSettingsSchema: z<Schemastery.ObjectS<{
|
|
|
40
41
|
protocol: z<"webdav" | "s3", "webdav" | "s3">;
|
|
41
42
|
baseUrl: z<string, string>;
|
|
42
43
|
directory: z<string, string>;
|
|
44
|
+
adoptForeignRoots: z<boolean, boolean>;
|
|
43
45
|
username: z<string, string>;
|
|
44
46
|
endpoint: z<string, string>;
|
|
45
47
|
bucket: z<string, string>;
|
|
@@ -75,6 +77,8 @@ export interface WebdavPatch {
|
|
|
75
77
|
protocol?: string;
|
|
76
78
|
baseUrl?: string;
|
|
77
79
|
directory?: string;
|
|
80
|
+
/** Merge other sync trees in the same bucket, not just this directory's. */
|
|
81
|
+
adoptForeignRoots?: boolean;
|
|
78
82
|
username?: string;
|
|
79
83
|
/** Empty string clears the stored password; undefined leaves it. */
|
|
80
84
|
password?: string;
|
|
@@ -48,6 +48,8 @@ export interface WebdavRequest {
|
|
|
48
48
|
protocol?: RemoteProtocol;
|
|
49
49
|
baseUrl?: string;
|
|
50
50
|
directory?: string;
|
|
51
|
+
/** Merge other sync trees in the same bucket, not just this directory's. */
|
|
52
|
+
adoptForeignRoots?: boolean;
|
|
51
53
|
username?: string;
|
|
52
54
|
/** Empty string clears the stored password; absent leaves it alone. */
|
|
53
55
|
password?: string;
|
|
@@ -82,6 +84,18 @@ export interface WebdavSettings {
|
|
|
82
84
|
baseUrl: string;
|
|
83
85
|
/** Folder under the base URL; the convention every device drops into. */
|
|
84
86
|
directory: string;
|
|
87
|
+
/**
|
|
88
|
+
* Also merge `…/sync` trees found **outside** the configured directory.
|
|
89
|
+
*
|
|
90
|
+
* Off by default, because the directory is what tells two vaults apart and a
|
|
91
|
+
* bucket can be shared. On, this is what a person means by "it is all my
|
|
92
|
+
* cloud drive": a machine that used to sync somewhere else leaves its records
|
|
93
|
+
* behind, and they come back (asked 2026-09-21, 19 records). Merging settles
|
|
94
|
+
* per record by `id` + `updatedAt`, so an older tree cannot overwrite a newer
|
|
95
|
+
* copy — but it *can* bring back a record that was purged here, because a
|
|
96
|
+
* purge leaves nothing local to outrank it.
|
|
97
|
+
*/
|
|
98
|
+
adoptForeignRoots: boolean;
|
|
85
99
|
username: string;
|
|
86
100
|
/** S3: endpoint host, e.g. `https://s3.cstcloud.cn`. */
|
|
87
101
|
endpoint: string;
|
|
@@ -159,8 +173,19 @@ export interface PullResult {
|
|
|
159
173
|
* than the local copy). Only the merge half of a pull can produce these.
|
|
160
174
|
*/
|
|
161
175
|
merged?: number;
|
|
176
|
+
/** How many of {@link merged} were ids this vault did not have at all. */
|
|
177
|
+
added?: number;
|
|
162
178
|
/** Attachment objects the merge had to fetch and admit locally. */
|
|
163
179
|
attachments?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Records the cloud holds that this vault **already had** (same id, not older).
|
|
182
|
+
*
|
|
183
|
+
* This is the answer to "why is the cloud's copy not coming over?": it is the
|
|
184
|
+
* same record, and the merge settles per record by `id` + `updatedAt` rather
|
|
185
|
+
* than by which machine uploaded it — a record this machine pushed comes back
|
|
186
|
+
* with the timestamp it left with, so it is kept, not re-filed.
|
|
187
|
+
*/
|
|
188
|
+
kept?: number;
|
|
164
189
|
failed: number;
|
|
165
190
|
/** Files the server listed but we skipped as already-seen. */
|
|
166
191
|
skipped: number;
|
|
@@ -180,8 +205,17 @@ export interface PullResult {
|
|
|
180
205
|
skippedForeign?: number;
|
|
181
206
|
/** Those other prefixes, e.g. `['inbox/sync']`; the panel warns about them. */
|
|
182
207
|
foreignSyncRoots?: string[];
|
|
208
|
+
/** How many `items/*.json` records those other prefixes hold together. */
|
|
209
|
+
foreignRecords?: number;
|
|
183
210
|
/** This machine's own sync root, so the warning can name both sides. */
|
|
184
211
|
syncRoot?: string;
|
|
212
|
+
/**
|
|
213
|
+
* How many **records** the cloud holds, as opposed to how many objects that
|
|
214
|
+
* takes: `items/<id>.json` files under this machine's own sync root.
|
|
215
|
+
*/
|
|
216
|
+
remoteRecords?: number;
|
|
217
|
+
/** How many attachments the cloud holds (`attachments/<id>.<ext>`, no descriptors). */
|
|
218
|
+
remoteAttachments?: number;
|
|
185
219
|
/** How many entries the remote listed at all: distinguishes "empty folder"
|
|
186
220
|
* from "everything already ingested". */
|
|
187
221
|
listed: number;
|