@chance722/dsh-inbox 0.2.2 → 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 +179 -48
- package/lib/index.js +171 -54
- package/lib/types/client/messages.d.ts +42 -18
- package/lib/types/host/remote/merge.d.ts +17 -1
- package/lib/types/host/remote/pull.d.ts +27 -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 +79 -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 });
|
|
@@ -933,6 +955,42 @@ async function readFile(path, deps, absoluteUrl) {
|
|
|
933
955
|
return { bytes, contentType: declared.split(";")[0] ?? "application/octet-stream" };
|
|
934
956
|
}
|
|
935
957
|
|
|
958
|
+
// src/shared/panel-wire.ts
|
|
959
|
+
var INBOX_API_PREFIX = "/api/inbox";
|
|
960
|
+
var INBOX_ENDPOINT_CAPTURE = "capture";
|
|
961
|
+
var INBOX_ENDPOINT_LIST = "list";
|
|
962
|
+
var INBOX_ENDPOINT_DETAIL = "detail";
|
|
963
|
+
var INBOX_ENDPOINT_UPDATE = "update";
|
|
964
|
+
var INBOX_ENDPOINT_DELETE = "delete";
|
|
965
|
+
var INBOX_ENDPOINT_RESTORE = "restore";
|
|
966
|
+
var INBOX_ENDPOINT_PURGE = "purge";
|
|
967
|
+
var INBOX_ENDPOINT_ATTACHMENT = "attachment";
|
|
968
|
+
var INBOX_ENDPOINT_WEBDAV = "webdav";
|
|
969
|
+
var INBOX_ENDPOINT_PULL = "pull";
|
|
970
|
+
var INBOX_ENDPOINT_PROBE = "probe";
|
|
971
|
+
var INBOX_ENDPOINT_UI = "ui";
|
|
972
|
+
var INBOX_ENDPOINT_TAGS = "tags";
|
|
973
|
+
var INBOX_ENDPOINT_SECRET = "secret";
|
|
974
|
+
var INBOX_ENDPOINT_PUSH = "push";
|
|
975
|
+
var DEFAULT_SYNC_DIRECTORY = "inbox";
|
|
976
|
+
function syncDirectory(raw) {
|
|
977
|
+
const trimmed = (raw ?? "").trim().replace(/^\/+|\/+$/g, "");
|
|
978
|
+
return trimmed.length === 0 ? DEFAULT_SYNC_DIRECTORY : trimmed;
|
|
979
|
+
}
|
|
980
|
+
function syncRootFor(raw) {
|
|
981
|
+
return `${syncDirectory(raw)}/sync`;
|
|
982
|
+
}
|
|
983
|
+
var INBOX_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
984
|
+
var LIST_LIMIT = 50;
|
|
985
|
+
var UI_LIST_MODES = ["grid", "compact"];
|
|
986
|
+
var PREVIEW_CHARS = 140;
|
|
987
|
+
var MAX_ATTACHMENTS_PER_SUBMISSION = 20;
|
|
988
|
+
var MAX_NOTE_CHARS = 2e3;
|
|
989
|
+
var MAX_TAGS = 20;
|
|
990
|
+
var MAX_TAG_CHARS = 40;
|
|
991
|
+
var MAX_FILTER_CHARS = 200;
|
|
992
|
+
var MAX_TITLE_CHARS = 300;
|
|
993
|
+
|
|
936
994
|
// src/host/webdav/config.ts
|
|
937
995
|
import z from "@deepseek-ai/schemastery";
|
|
938
996
|
var SETTINGS_NAMESPACE = "dsh-inbox-webdav";
|
|
@@ -942,6 +1000,7 @@ var DEFAULT_SETTINGS = {
|
|
|
942
1000
|
protocol: "webdav",
|
|
943
1001
|
baseUrl: "",
|
|
944
1002
|
directory: "/inbox",
|
|
1003
|
+
adoptForeignRoots: false,
|
|
945
1004
|
username: "",
|
|
946
1005
|
endpoint: "",
|
|
947
1006
|
bucket: "",
|
|
@@ -955,6 +1014,7 @@ var WebdavSettingsSchema = z.object({
|
|
|
955
1014
|
protocol: z.union(["webdav", "s3"]).default("webdav"),
|
|
956
1015
|
baseUrl: z.string().default(""),
|
|
957
1016
|
directory: z.string().default("/inbox"),
|
|
1017
|
+
adoptForeignRoots: z.boolean().default(false),
|
|
958
1018
|
username: z.string().default(""),
|
|
959
1019
|
endpoint: z.string().default(""),
|
|
960
1020
|
bucket: z.string().default(""),
|
|
@@ -1026,6 +1086,7 @@ async function saveWebdav(ctx, vault, base, patch) {
|
|
|
1026
1086
|
const directory = patch.directory.trim();
|
|
1027
1087
|
config.directory = directory.startsWith("/") ? directory : `/${directory}`;
|
|
1028
1088
|
}
|
|
1089
|
+
if (patch.adoptForeignRoots !== void 0) config.adoptForeignRoots = patch.adoptForeignRoots;
|
|
1029
1090
|
if (patch.username !== void 0) config.username = patch.username.trim();
|
|
1030
1091
|
if (patch.endpoint !== void 0) config.endpoint = normalizeUrl(patch.endpoint);
|
|
1031
1092
|
if (patch.bucket !== void 0) config.bucket = patch.bucket.trim();
|
|
@@ -1070,36 +1131,6 @@ async function saveWebdav(ctx, vault, base, patch) {
|
|
|
1070
1131
|
|
|
1071
1132
|
// src/host/remote/pull.ts
|
|
1072
1133
|
import { admitEncodedFile, admitEncodedImages } from "@deepseek-ai/dsh-attachment";
|
|
1073
|
-
|
|
1074
|
-
// src/shared/panel-wire.ts
|
|
1075
|
-
var INBOX_API_PREFIX = "/api/inbox";
|
|
1076
|
-
var INBOX_ENDPOINT_CAPTURE = "capture";
|
|
1077
|
-
var INBOX_ENDPOINT_LIST = "list";
|
|
1078
|
-
var INBOX_ENDPOINT_DETAIL = "detail";
|
|
1079
|
-
var INBOX_ENDPOINT_UPDATE = "update";
|
|
1080
|
-
var INBOX_ENDPOINT_DELETE = "delete";
|
|
1081
|
-
var INBOX_ENDPOINT_RESTORE = "restore";
|
|
1082
|
-
var INBOX_ENDPOINT_PURGE = "purge";
|
|
1083
|
-
var INBOX_ENDPOINT_ATTACHMENT = "attachment";
|
|
1084
|
-
var INBOX_ENDPOINT_WEBDAV = "webdav";
|
|
1085
|
-
var INBOX_ENDPOINT_PULL = "pull";
|
|
1086
|
-
var INBOX_ENDPOINT_PROBE = "probe";
|
|
1087
|
-
var INBOX_ENDPOINT_UI = "ui";
|
|
1088
|
-
var INBOX_ENDPOINT_TAGS = "tags";
|
|
1089
|
-
var INBOX_ENDPOINT_SECRET = "secret";
|
|
1090
|
-
var INBOX_ENDPOINT_PUSH = "push";
|
|
1091
|
-
var INBOX_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
1092
|
-
var LIST_LIMIT = 50;
|
|
1093
|
-
var UI_LIST_MODES = ["grid", "compact"];
|
|
1094
|
-
var PREVIEW_CHARS = 140;
|
|
1095
|
-
var MAX_ATTACHMENTS_PER_SUBMISSION = 20;
|
|
1096
|
-
var MAX_NOTE_CHARS = 2e3;
|
|
1097
|
-
var MAX_TAGS = 20;
|
|
1098
|
-
var MAX_TAG_CHARS = 40;
|
|
1099
|
-
var MAX_FILTER_CHARS = 200;
|
|
1100
|
-
var MAX_TITLE_CHARS = 300;
|
|
1101
|
-
|
|
1102
|
-
// src/host/remote/pull.ts
|
|
1103
1134
|
var DEFAULT_DIRECTORY = "/inbox";
|
|
1104
1135
|
var TEXT_TYPES = ["text/", "application/json"];
|
|
1105
1136
|
var TEXT_SUFFIXES = [".txt", ".md", ".url", ".json"];
|
|
@@ -1108,8 +1139,11 @@ function looksTextual(name2, contentType) {
|
|
|
1108
1139
|
if (TEXT_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return true;
|
|
1109
1140
|
return contentType !== void 0 && TEXT_TYPES.some((prefix) => contentType.startsWith(prefix));
|
|
1110
1141
|
}
|
|
1111
|
-
function
|
|
1112
|
-
|
|
1142
|
+
function syncPrefixOf(path) {
|
|
1143
|
+
const parts = path.split("/").filter((part) => part.length > 0);
|
|
1144
|
+
const at = parts.lastIndexOf("sync");
|
|
1145
|
+
if (at === -1) return void 0;
|
|
1146
|
+
return parts.slice(0, at + 1).slice(-2).join("/");
|
|
1113
1147
|
}
|
|
1114
1148
|
function laterOf(current, candidate) {
|
|
1115
1149
|
if (candidate === void 0) return current;
|
|
@@ -1128,7 +1162,7 @@ function isNewer(entry, lastPullAt) {
|
|
|
1128
1162
|
if (Number.isNaN(seen) || Number.isNaN(remote)) return true;
|
|
1129
1163
|
return remote > seen;
|
|
1130
1164
|
}
|
|
1131
|
-
async function ingestFrom(vault, source, attachments) {
|
|
1165
|
+
async function ingestFrom(vault, source, attachments, syncRoot3 = syncRootFor(void 0), elsewhere = { roots: [] }) {
|
|
1132
1166
|
const lastPullAt = vault.global.sync.lastPullAt;
|
|
1133
1167
|
let entries;
|
|
1134
1168
|
try {
|
|
@@ -1141,15 +1175,40 @@ async function ingestFrom(vault, source, attachments) {
|
|
|
1141
1175
|
let pulled = 0;
|
|
1142
1176
|
let failed2 = 0;
|
|
1143
1177
|
let skipped = 0;
|
|
1178
|
+
let skippedSync = 0;
|
|
1179
|
+
let skippedOlder = 0;
|
|
1180
|
+
let skippedForeign = 0;
|
|
1181
|
+
let remoteRecords = 0;
|
|
1182
|
+
let remoteAttachments = 0;
|
|
1183
|
+
const foreignSyncRoots = new Set(elsewhere.roots);
|
|
1184
|
+
let foreignRecords = elsewhere.records ?? 0;
|
|
1144
1185
|
let newestSuccess;
|
|
1145
1186
|
const failures = [];
|
|
1146
1187
|
for (const entry of entries) {
|
|
1147
|
-
|
|
1188
|
+
const prefix = syncPrefixOf(entry.path);
|
|
1189
|
+
if (prefix !== void 0 && prefix !== syncRoot3) {
|
|
1190
|
+
skipped += 1;
|
|
1191
|
+
skippedForeign += 1;
|
|
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
|
+
}
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (prefix !== void 0) {
|
|
1148
1200
|
skipped += 1;
|
|
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;
|
|
1149
1207
|
continue;
|
|
1150
1208
|
}
|
|
1151
1209
|
if (!isNewer(entry, lastPullAt)) {
|
|
1152
1210
|
skipped += 1;
|
|
1211
|
+
skippedOlder += 1;
|
|
1153
1212
|
continue;
|
|
1154
1213
|
}
|
|
1155
1214
|
const name2 = nameOf(entry.path);
|
|
@@ -1207,6 +1266,14 @@ async function ingestFrom(vault, source, attachments) {
|
|
|
1207
1266
|
pulled,
|
|
1208
1267
|
failed: failed2,
|
|
1209
1268
|
skipped,
|
|
1269
|
+
skippedSync,
|
|
1270
|
+
skippedOlder,
|
|
1271
|
+
skippedForeign,
|
|
1272
|
+
remoteRecords,
|
|
1273
|
+
remoteAttachments,
|
|
1274
|
+
syncRoot: syncRoot3,
|
|
1275
|
+
...foreignSyncRoots.size === 0 ? {} : { foreignSyncRoots: [...foreignSyncRoots] },
|
|
1276
|
+
...foreignRecords === 0 ? {} : { foreignRecords },
|
|
1210
1277
|
listed,
|
|
1211
1278
|
lastPullAt: cursor,
|
|
1212
1279
|
...failures.length === 0 ? {} : { reason: failures.slice(0, 3).join("\uFF1B") }
|
|
@@ -1224,7 +1291,7 @@ async function pullRemote(vault, config, deps) {
|
|
|
1224
1291
|
listed: 0
|
|
1225
1292
|
};
|
|
1226
1293
|
}
|
|
1227
|
-
const directory = config.directory ?? DEFAULT_DIRECTORY
|
|
1294
|
+
const directory = `/${syncDirectory(config.directory ?? DEFAULT_DIRECTORY)}`;
|
|
1228
1295
|
return ingestFrom(
|
|
1229
1296
|
vault,
|
|
1230
1297
|
{
|
|
@@ -1235,10 +1302,12 @@ async function pullRemote(vault, config, deps) {
|
|
|
1235
1302
|
})),
|
|
1236
1303
|
read: async (entry) => readFile(entry.path, deps, entry.path.startsWith("http") ? entry.path : void 0)
|
|
1237
1304
|
},
|
|
1238
|
-
deps.attachments
|
|
1305
|
+
deps.attachments,
|
|
1306
|
+
// WebDAV resolves the sync root from the same directory rule the writer uses.
|
|
1307
|
+
syncRootFor(config.directory)
|
|
1239
1308
|
);
|
|
1240
1309
|
}
|
|
1241
|
-
async function pullS3(vault, config,
|
|
1310
|
+
async function pullS3(vault, config, directory, deps) {
|
|
1242
1311
|
if (config.endpoint.trim().length === 0 || config.bucket.trim().length === 0) {
|
|
1243
1312
|
return {
|
|
1244
1313
|
status: "unconfigured",
|
|
@@ -1249,18 +1318,44 @@ async function pullS3(vault, config, prefix, deps) {
|
|
|
1249
1318
|
listed: 0
|
|
1250
1319
|
};
|
|
1251
1320
|
}
|
|
1321
|
+
const scope = syncDirectory(directory);
|
|
1252
1322
|
return ingestFrom(
|
|
1253
1323
|
vault,
|
|
1254
1324
|
{
|
|
1255
|
-
list: async () => (await listPrefix(config,
|
|
1325
|
+
list: async () => (await listPrefix(config, `${scope}/`, deps)).map((object) => ({
|
|
1256
1326
|
path: object.key,
|
|
1257
1327
|
...object.lastModified === void 0 ? {} : { lastModified: object.lastModified }
|
|
1258
1328
|
})),
|
|
1259
1329
|
read: async (entry) => readObject(config, entry.path, deps)
|
|
1260
1330
|
},
|
|
1261
|
-
deps.attachments
|
|
1331
|
+
deps.attachments,
|
|
1332
|
+
// The vault's own tree, resolved by the same rule the writer uses.
|
|
1333
|
+
syncRootFor(directory),
|
|
1334
|
+
await s3RootsElsewhere(config, deps, scope)
|
|
1262
1335
|
);
|
|
1263
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
|
+
}
|
|
1264
1359
|
|
|
1265
1360
|
// src/host/remote/merge.ts
|
|
1266
1361
|
import { admitEncodedFile as admitEncodedFile2, admitEncodedImages as admitEncodedImages2 } from "@deepseek-ai/dsh-attachment";
|
|
@@ -1308,13 +1403,14 @@ function wins(remote, local) {
|
|
|
1308
1403
|
async function mergeOnce(vault, tree, prefix, admit) {
|
|
1309
1404
|
const failures = [];
|
|
1310
1405
|
let merged = 0;
|
|
1406
|
+
let added = 0;
|
|
1311
1407
|
let kept = 0;
|
|
1312
1408
|
let attachments = 0;
|
|
1313
1409
|
let objects;
|
|
1314
1410
|
try {
|
|
1315
1411
|
objects = await tree.list(`${prefix}/items/`);
|
|
1316
1412
|
} catch (error) {
|
|
1317
|
-
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)}`] };
|
|
1318
1414
|
}
|
|
1319
1415
|
const wanted = /* @__PURE__ */ new Map();
|
|
1320
1416
|
const bytesAt = /* @__PURE__ */ new Map();
|
|
@@ -1344,6 +1440,7 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1344
1440
|
}
|
|
1345
1441
|
await vault.import(remote);
|
|
1346
1442
|
merged += 1;
|
|
1443
|
+
if (local === void 0) added += 1;
|
|
1347
1444
|
for (const attachmentId of remote.attachmentIds) {
|
|
1348
1445
|
if (vault.getAttachment(attachmentId) === void 0) {
|
|
1349
1446
|
wanted.set(attachmentId, remote.id);
|
|
@@ -1378,7 +1475,7 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1378
1475
|
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A${reasonOf(error)}`);
|
|
1379
1476
|
}
|
|
1380
1477
|
}
|
|
1381
|
-
return { merged, kept, attachments, failures };
|
|
1478
|
+
return { merged, added, kept, attachments, failures };
|
|
1382
1479
|
}
|
|
1383
1480
|
function extensionOfName(path) {
|
|
1384
1481
|
return /\.([A-Za-z0-9]{1,8})$/.exec(nameOf2(path))?.[1]?.toLowerCase() ?? "bin";
|
|
@@ -1386,15 +1483,26 @@ function extensionOfName(path) {
|
|
|
1386
1483
|
function reasonOf(error) {
|
|
1387
1484
|
return error instanceof Error ? error.message : String(error);
|
|
1388
1485
|
}
|
|
1389
|
-
async function mergeRemote(ctx, vault, attachments) {
|
|
1486
|
+
async function mergeRemote(ctx, vault, attachments, extraRoots = []) {
|
|
1390
1487
|
const settings = readSettings(ctx);
|
|
1391
1488
|
const prefix = syncRoot(settings);
|
|
1392
1489
|
try {
|
|
1393
1490
|
const tree = settings.protocol === "s3" ? await s3Tree(ctx, settings) : await webdavTree(ctx, settings);
|
|
1394
|
-
if (tree === void 0) return { merged: 0, kept: 0, attachments: 0, failures: [] };
|
|
1395
|
-
|
|
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;
|
|
1396
1504
|
} catch (error) {
|
|
1397
|
-
return { merged: 0, kept: 0, attachments: 0, failures: [reasonOf(error)] };
|
|
1505
|
+
return { merged: 0, added: 0, kept: 0, attachments: 0, failures: [reasonOf(error)] };
|
|
1398
1506
|
}
|
|
1399
1507
|
}
|
|
1400
1508
|
function admitWith(store) {
|
|
@@ -1493,11 +1601,15 @@ async function runPull(ctx, vault, attachments) {
|
|
|
1493
1601
|
}
|
|
1494
1602
|
async function withMerge(ctx, vault, attachments, pulled) {
|
|
1495
1603
|
if (pulled.status !== "ok" || attachments === void 0) return pulled;
|
|
1496
|
-
const
|
|
1604
|
+
const settings = readSettings(ctx);
|
|
1605
|
+
const extraRoots = settings.adoptForeignRoots ? pulled.foreignSyncRoots ?? [] : [];
|
|
1606
|
+
const outcome = await mergeRemote(ctx, vault, attachments, extraRoots);
|
|
1497
1607
|
const troubles = [...pulled.failed > 0 && pulled.reason !== void 0 ? [pulled.reason] : [], ...outcome.failures];
|
|
1498
1608
|
return {
|
|
1499
1609
|
...pulled,
|
|
1500
1610
|
merged: outcome.merged,
|
|
1611
|
+
added: outcome.added,
|
|
1612
|
+
kept: outcome.kept,
|
|
1501
1613
|
attachments: outcome.attachments,
|
|
1502
1614
|
failed: pulled.failed + outcome.failures.length,
|
|
1503
1615
|
...troubles.length === 0 ? {} : { reason: troubles.slice(0, 3).join("\uFF1B") }
|
|
@@ -1546,7 +1658,10 @@ async function pullDropFolder(ctx, vault, attachments) {
|
|
|
1546
1658
|
signatureVersion: settings.signatureVersion,
|
|
1547
1659
|
userAgent: activeUserAgent(settings)
|
|
1548
1660
|
},
|
|
1549
|
-
|
|
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,
|
|
1550
1665
|
{
|
|
1551
1666
|
fetch: s3Fetch,
|
|
1552
1667
|
accessKeyId: settings.accessKeyId,
|
|
@@ -1570,8 +1685,7 @@ async function pullDropFolder(ctx, vault, attachments) {
|
|
|
1570
1685
|
|
|
1571
1686
|
// src/host/remote/writer.ts
|
|
1572
1687
|
function syncRoot(settings) {
|
|
1573
|
-
|
|
1574
|
-
return prefix.length === 0 ? "sync" : `${prefix}/sync`;
|
|
1688
|
+
return syncRootFor(settings.directory);
|
|
1575
1689
|
}
|
|
1576
1690
|
async function remoteWriter(ctx) {
|
|
1577
1691
|
const settings = readSettings(ctx);
|
|
@@ -3072,6 +3186,9 @@ async function handleWebdav(ctx, vault, payload) {
|
|
|
3072
3186
|
if (typeof request.protocol === "string") patch.protocol = request.protocol;
|
|
3073
3187
|
if (typeof request.baseUrl === "string") patch.baseUrl = request.baseUrl;
|
|
3074
3188
|
if (typeof request.directory === "string") patch.directory = request.directory;
|
|
3189
|
+
if (typeof request.adoptForeignRoots === "boolean") {
|
|
3190
|
+
patch.adoptForeignRoots = request.adoptForeignRoots;
|
|
3191
|
+
}
|
|
3075
3192
|
if (typeof request.username === "string") patch.username = request.username;
|
|
3076
3193
|
if (typeof request.password === "string") patch.password = request.password;
|
|
3077
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;
|
|
@@ -144,6 +147,7 @@ export declare const zh: {
|
|
|
144
147
|
'settings.save': string;
|
|
145
148
|
'settings.probe': string;
|
|
146
149
|
'settings.details': string;
|
|
150
|
+
'settings.syncRootNow': string;
|
|
147
151
|
'settings.noServiceBody': string;
|
|
148
152
|
'settings.bucketHint': string;
|
|
149
153
|
'notice.listFailed': string;
|
|
@@ -175,16 +179,20 @@ export declare const zh: {
|
|
|
175
179
|
'sync.pushUnconfigured': string;
|
|
176
180
|
'sync.pushFailed': string;
|
|
177
181
|
'sync.pushUnknown': string;
|
|
178
|
-
'sync.pushUpToDate': string;
|
|
179
|
-
'sync.pushDone': string;
|
|
180
182
|
'sync.pushPartial': string;
|
|
181
183
|
'sync.pullNoAddress': string;
|
|
182
184
|
'sync.pullFailed': string;
|
|
183
|
-
'sync.
|
|
184
|
-
'sync.
|
|
185
|
-
'sync.
|
|
186
|
-
'sync.
|
|
187
|
-
'sync.
|
|
185
|
+
'sync.detailPushedIdle': string;
|
|
186
|
+
'sync.detailPushed': string;
|
|
187
|
+
'sync.detailPulled': string;
|
|
188
|
+
'sync.detailAdded': string;
|
|
189
|
+
'sync.detailSkip': string;
|
|
190
|
+
'sync.pullForeignSync': string;
|
|
191
|
+
'sync.detailForeign': string;
|
|
192
|
+
'sync.shortPushed': string;
|
|
193
|
+
'sync.shortPulled': string;
|
|
194
|
+
'sync.shortIdle': string;
|
|
195
|
+
'sync.pullFailures': string;
|
|
188
196
|
'sync.thisPage': string;
|
|
189
197
|
'settings.status': string;
|
|
190
198
|
'settings.statusOn': string;
|
|
@@ -250,10 +258,14 @@ export declare const zh: {
|
|
|
250
258
|
'manual.5.auto.body': string;
|
|
251
259
|
'manual.5.manual.label': string;
|
|
252
260
|
'manual.5.manual.body': string;
|
|
261
|
+
'manual.5.twoMachines.label': string;
|
|
262
|
+
'manual.5.twoMachines.body': string;
|
|
253
263
|
'manual.5.delete.label': string;
|
|
254
264
|
'manual.5.delete.body': string;
|
|
255
|
-
'manual.5.
|
|
256
|
-
'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;
|
|
257
269
|
'manual.6.title': string;
|
|
258
270
|
'manual.6.query.label': string;
|
|
259
271
|
'manual.6.query.body': string;
|
|
@@ -386,6 +398,9 @@ export declare const MESSAGES: {
|
|
|
386
398
|
'settings.region': string;
|
|
387
399
|
'settings.bucketAddress': string;
|
|
388
400
|
'settings.bucketDir': string;
|
|
401
|
+
'settings.adoptForeign': string;
|
|
402
|
+
'settings.adoptForeignHint': string;
|
|
403
|
+
'settings.directoryPlaceholder': string;
|
|
389
404
|
'settings.dirS3.lead': string;
|
|
390
405
|
'settings.dirS3.mid': string;
|
|
391
406
|
'settings.dirS3.tail': string;
|
|
@@ -401,6 +416,7 @@ export declare const MESSAGES: {
|
|
|
401
416
|
'settings.save': string;
|
|
402
417
|
'settings.probe': string;
|
|
403
418
|
'settings.details': string;
|
|
419
|
+
'settings.syncRootNow': string;
|
|
404
420
|
'settings.noServiceBody': string;
|
|
405
421
|
'settings.bucketHint': string;
|
|
406
422
|
'notice.listFailed': string;
|
|
@@ -432,16 +448,20 @@ export declare const MESSAGES: {
|
|
|
432
448
|
'sync.pushUnconfigured': string;
|
|
433
449
|
'sync.pushFailed': string;
|
|
434
450
|
'sync.pushUnknown': string;
|
|
435
|
-
'sync.pushUpToDate': string;
|
|
436
|
-
'sync.pushDone': string;
|
|
437
451
|
'sync.pushPartial': string;
|
|
438
452
|
'sync.pullNoAddress': string;
|
|
439
453
|
'sync.pullFailed': string;
|
|
440
|
-
'sync.
|
|
441
|
-
'sync.
|
|
442
|
-
'sync.
|
|
443
|
-
'sync.
|
|
444
|
-
'sync.
|
|
454
|
+
'sync.detailPushedIdle': string;
|
|
455
|
+
'sync.detailPushed': string;
|
|
456
|
+
'sync.detailPulled': string;
|
|
457
|
+
'sync.detailAdded': string;
|
|
458
|
+
'sync.detailSkip': string;
|
|
459
|
+
'sync.pullForeignSync': string;
|
|
460
|
+
'sync.detailForeign': string;
|
|
461
|
+
'sync.shortPushed': string;
|
|
462
|
+
'sync.shortPulled': string;
|
|
463
|
+
'sync.shortIdle': string;
|
|
464
|
+
'sync.pullFailures': string;
|
|
445
465
|
'sync.thisPage': string;
|
|
446
466
|
'settings.status': string;
|
|
447
467
|
'settings.statusOn': string;
|
|
@@ -507,10 +527,14 @@ export declare const MESSAGES: {
|
|
|
507
527
|
'manual.5.auto.body': string;
|
|
508
528
|
'manual.5.manual.label': string;
|
|
509
529
|
'manual.5.manual.body': string;
|
|
530
|
+
'manual.5.twoMachines.label': string;
|
|
531
|
+
'manual.5.twoMachines.body': string;
|
|
510
532
|
'manual.5.delete.label': string;
|
|
511
533
|
'manual.5.delete.body': string;
|
|
512
|
-
'manual.5.
|
|
513
|
-
'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;
|
|
514
538
|
'manual.6.title': string;
|
|
515
539
|
'manual.6.query.label': string;
|
|
516
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 {};
|
|
@@ -43,7 +43,18 @@ export interface RemoteSource {
|
|
|
43
43
|
* @param attachments - the store that owns pulled bytes.
|
|
44
44
|
* @returns what happened.
|
|
45
45
|
*/
|
|
46
|
-
export declare function ingestFrom(vault: Vault, source: RemoteSource, attachments: AttachmentStore
|
|
46
|
+
export declare function ingestFrom(vault: Vault, source: RemoteSource, attachments: AttachmentStore,
|
|
47
|
+
/** This machine's own sync root; anything else under `sync/` is a stranger. */
|
|
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>;
|
|
47
58
|
/** What the user configures for WebDAV. */
|
|
48
59
|
export interface WebdavConfig {
|
|
49
60
|
baseUrl: string;
|
|
@@ -54,7 +65,20 @@ export interface WebdavConfig {
|
|
|
54
65
|
export declare function pullRemote(vault: Vault, config: WebdavConfig, deps: WebdavDeps & {
|
|
55
66
|
attachments: AttachmentStore;
|
|
56
67
|
}): Promise<PullResult>;
|
|
57
|
-
/**
|
|
58
|
-
|
|
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 & {
|
|
59
83
|
attachments: AttachmentStore;
|
|
60
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;
|