@tensorix/metahub 0.5.2 → 0.5.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/README.md +8 -4
- package/README.zh-CN.md +9 -5
- package/dist/cli.js +5005 -4033
- package/dist/cli.js.map +66 -60
- package/dist/core/compact.d.ts.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/crdt.d.ts +12 -4
- package/dist/core/crdt.d.ts.map +1 -1
- package/dist/core/databases.d.ts +3 -2
- package/dist/core/databases.d.ts.map +1 -1
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/driver.d.ts +5 -1
- package/dist/core/driver.d.ts.map +1 -1
- package/dist/core/errors.d.ts +19 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/guest-intent.d.ts +1 -1
- package/dist/core/guest-intent.d.ts.map +1 -1
- package/dist/core/history-cache.d.ts +13 -0
- package/dist/core/history-cache.d.ts.map +1 -0
- package/dist/core/history.d.ts.map +1 -1
- package/dist/core/hlc.d.ts +13 -0
- package/dist/core/hlc.d.ts.map +1 -1
- package/dist/core/integrity.d.ts +15 -3
- package/dist/core/integrity.d.ts.map +1 -1
- package/dist/core/md/grammar.d.ts +5 -0
- package/dist/core/md/grammar.d.ts.map +1 -1
- package/dist/core/md/heal.d.ts.map +1 -1
- package/dist/core/resolve.d.ts +8 -0
- package/dist/core/resolve.d.ts.map +1 -1
- package/dist/core/schema-init.d.ts +1 -1
- package/dist/core/schema-init.d.ts.map +1 -1
- package/dist/core/search.d.ts +1 -1
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/site-channel-store.d.ts +1 -1
- package/dist/core/site-channel-store.d.ts.map +1 -1
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/sync/auth.d.ts +2 -4
- package/dist/core/sync/auth.d.ts.map +1 -1
- package/dist/core/sync/cf-oauth.d.ts.map +1 -1
- package/dist/core/sync/client.d.ts +4 -0
- package/dist/core/sync/client.d.ts.map +1 -1
- package/dist/core/sync/drop-protocol.d.ts.map +1 -1
- package/dist/core/sync/drop-pull.d.ts +1 -1
- package/dist/core/sync/drop-pull.d.ts.map +1 -1
- package/dist/core/sync/http-status.d.ts.map +1 -1
- package/dist/core/sync/locale.d.ts +8 -0
- package/dist/core/sync/locale.d.ts.map +1 -0
- package/dist/core/sync/page-messages.d.ts +106 -0
- package/dist/core/sync/page-messages.d.ts.map +1 -0
- package/dist/core/sync/pairing.d.ts +4 -1
- package/dist/core/sync/pairing.d.ts.map +1 -1
- package/dist/core/sync/peers.d.ts +1 -1
- package/dist/core/sync/peers.d.ts.map +1 -1
- package/dist/core/sync/protocol.d.ts +1 -0
- package/dist/core/sync/protocol.d.ts.map +1 -1
- package/dist/core/sync/remote-shares-cache.d.ts +32 -0
- package/dist/core/sync/remote-shares-cache.d.ts.map +1 -0
- package/dist/core/sync/room-client.d.ts +1 -1
- package/dist/core/sync/room-client.d.ts.map +1 -1
- package/dist/core/sync/room-protocol.d.ts +1 -1
- package/dist/core/sync/room-protocol.d.ts.map +1 -1
- package/dist/core/sync/room-serve.d.ts.map +1 -1
- package/dist/core/sync/routes.d.ts +1 -1
- package/dist/core/sync/routes.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts +2 -0
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/core/sync/share-actions.d.ts +7 -2
- package/dist/core/sync/share-actions.d.ts.map +1 -1
- package/dist/core/sync/share-copy.d.ts +7 -0
- package/dist/core/sync/share-copy.d.ts.map +1 -0
- package/dist/core/sync/share-export.d.ts +3 -1
- package/dist/core/sync/share-export.d.ts.map +1 -1
- package/dist/core/sync/share-serve.d.ts.map +1 -1
- package/dist/core/sync/sites-serve.d.ts.map +1 -1
- package/dist/core/sync/storage.d.ts +1 -1
- package/dist/core/sync/storage.d.ts.map +1 -1
- package/dist/db-worker.js +115 -88
- package/dist/edge-worker.js +319 -84
- package/dist/index.js +1637 -418
- package/dist/index.js.map +55 -48
- package/dist/metahub-sdk.js +5 -5
- package/dist/mh-runtime.js +9 -1
- package/dist/sw.js +15 -7
- package/dist/webui/server/assets.d.ts.map +1 -1
- package/dist/webui/server/changes-route.d.ts +2 -0
- package/dist/webui/server/changes-route.d.ts.map +1 -1
- package/dist/webui/server/edge-routes.d.ts.map +1 -1
- package/dist/webui/server/icons.d.ts +12 -3
- package/dist/webui/server/icons.d.ts.map +1 -1
- package/dist/webui/server/routes.d.ts +2 -0
- package/dist/webui/server/routes.d.ts.map +1 -1
- package/dist/webui-fmt.js +8 -8
- package/dist/webui.js +190 -215
- package/package.json +1 -1
package/dist/edge-worker.js
CHANGED
|
@@ -69,14 +69,38 @@ function normalizeRow(row) {
|
|
|
69
69
|
// src/core/errors.ts
|
|
70
70
|
class MhError extends Error {
|
|
71
71
|
code;
|
|
72
|
-
|
|
72
|
+
i18n;
|
|
73
|
+
constructor(code, message, i18n) {
|
|
73
74
|
super(message);
|
|
74
75
|
this.code = code;
|
|
76
|
+
this.i18n = i18n;
|
|
75
77
|
this.name = "MhError";
|
|
76
78
|
}
|
|
77
79
|
}
|
|
80
|
+
function interpolateMsg(s, params) {
|
|
81
|
+
return params ? s.replace(/\{(\w+)\}/g, (m, k) => (k in params) ? String(params[k]) : m) : s;
|
|
82
|
+
}
|
|
83
|
+
function mhError(code, key, params) {
|
|
84
|
+
return new MhError(code, interpolateMsg(key, params), { key, params });
|
|
85
|
+
}
|
|
86
|
+
var SQLITE_BUSY_RE = /^SQLITE_(BUSY|LOCKED)/;
|
|
87
|
+
function isSqliteBusy(e) {
|
|
88
|
+
if (!(e instanceof Error))
|
|
89
|
+
return false;
|
|
90
|
+
const code = e.code;
|
|
91
|
+
if (typeof code === "string" && SQLITE_BUSY_RE.test(code))
|
|
92
|
+
return true;
|
|
93
|
+
return e.message === "database is locked";
|
|
94
|
+
}
|
|
95
|
+
function asMhError(e) {
|
|
96
|
+
if (e instanceof MhError)
|
|
97
|
+
return e;
|
|
98
|
+
if (isSqliteBusy(e))
|
|
99
|
+
return mhError("busy", "数据库正被另一个进程写入,请稍后重试");
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
78
102
|
function errorCode(e) {
|
|
79
|
-
return e
|
|
103
|
+
return asMhError(e)?.code;
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
// src/core/sync/http-status.ts
|
|
@@ -87,7 +111,9 @@ var STATUS = {
|
|
|
87
111
|
stale: 409,
|
|
88
112
|
conflict: 409,
|
|
89
113
|
auth: 401,
|
|
90
|
-
rate_limited: 429
|
|
114
|
+
rate_limited: 429,
|
|
115
|
+
busy: 503,
|
|
116
|
+
clock_skew: 409
|
|
91
117
|
};
|
|
92
118
|
function httpStatusForCode(code) {
|
|
93
119
|
return code ? STATUS[code] ?? 400 : 400;
|
|
@@ -132,6 +158,12 @@ function idKind(id) {
|
|
|
132
158
|
return KINDS.has(prefix) ? prefix : null;
|
|
133
159
|
}
|
|
134
160
|
|
|
161
|
+
// src/core/driver.ts
|
|
162
|
+
function writeTx(db, fn) {
|
|
163
|
+
const tx = db.transaction(fn);
|
|
164
|
+
return (tx.immediate ?? tx)();
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
// src/core/node.ts
|
|
136
168
|
function getNodeId(db) {
|
|
137
169
|
const row = db.query("SELECT value FROM meta WHERE key = 'node_id'").get();
|
|
@@ -144,6 +176,13 @@ function getNodeId(db) {
|
|
|
144
176
|
|
|
145
177
|
// src/core/hlc.ts
|
|
146
178
|
var MAX_HLC_COUNTER = 65535;
|
|
179
|
+
var HLC_MAX_SKEW_MS = 5 * 60000;
|
|
180
|
+
function hlcSkewBound(now) {
|
|
181
|
+
return formatHlc({ millis: now + HLC_MAX_SKEW_MS, counter: MAX_HLC_COUNTER, node: "~~~~~~~~" });
|
|
182
|
+
}
|
|
183
|
+
function isSkewedHlc(hlc, now) {
|
|
184
|
+
return hlc > hlcSkewBound(now);
|
|
185
|
+
}
|
|
147
186
|
function carryCounter(millis, counter) {
|
|
148
187
|
if (counter <= MAX_HLC_COUNTER)
|
|
149
188
|
return { millis, counter };
|
|
@@ -174,6 +213,8 @@ function writeLast(db, h) {
|
|
|
174
213
|
}
|
|
175
214
|
function nextHlc(db, node, now = Date.now()) {
|
|
176
215
|
const last = readLast(db, node);
|
|
216
|
+
if (last.millis > now + HLC_MAX_SKEW_MS)
|
|
217
|
+
throw mhError("clock_skew", "本机逻辑时钟比系统时间超前 {min} 分钟(系统时钟曾被调错,或收到过时钟错误设备的数据),运行 mh repair --clock 修复", { min: Math.ceil((last.millis - now) / 60000) });
|
|
177
218
|
const millis = Math.max(last.millis, now);
|
|
178
219
|
const counter = millis === last.millis ? last.counter + 1 : 0;
|
|
179
220
|
const h = { ...carryCounter(millis, counter), node };
|
|
@@ -185,6 +226,8 @@ function observeHlc(db, node, remote, now = Date.now()) {
|
|
|
185
226
|
const r = parseHlc(remote);
|
|
186
227
|
if (!Number.isFinite(r.millis) || !Number.isFinite(r.counter))
|
|
187
228
|
return;
|
|
229
|
+
if (r.millis > now + HLC_MAX_SKEW_MS)
|
|
230
|
+
return;
|
|
188
231
|
const millis = Math.max(last.millis, r.millis, now);
|
|
189
232
|
let counter;
|
|
190
233
|
if (millis === last.millis && millis === r.millis)
|
|
@@ -299,6 +342,39 @@ function lcs(a, b) {
|
|
|
299
342
|
return pairs;
|
|
300
343
|
}
|
|
301
344
|
|
|
345
|
+
// src/core/history-cache.ts
|
|
346
|
+
var caches = new WeakMap;
|
|
347
|
+
function dropDoc(c, docId) {
|
|
348
|
+
const e = c.docs.get(docId);
|
|
349
|
+
if (!e)
|
|
350
|
+
return;
|
|
351
|
+
c.docs.delete(docId);
|
|
352
|
+
for (const b of e.blockIds)
|
|
353
|
+
if (c.blockToDoc.get(b) === docId)
|
|
354
|
+
c.blockToDoc.delete(b);
|
|
355
|
+
}
|
|
356
|
+
function noteChange(db, dataset, rowId, col, value) {
|
|
357
|
+
const c = caches.get(db);
|
|
358
|
+
if (!c)
|
|
359
|
+
return;
|
|
360
|
+
if (dataset === "documents") {
|
|
361
|
+
dropDoc(c, rowId);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (dataset !== "doc_blocks")
|
|
365
|
+
return;
|
|
366
|
+
const owner = c.blockToDoc.get(rowId);
|
|
367
|
+
if (owner)
|
|
368
|
+
dropDoc(c, owner);
|
|
369
|
+
if (col === "doc_id" && value) {
|
|
370
|
+
try {
|
|
371
|
+
const target = JSON.parse(value);
|
|
372
|
+
if (typeof target === "string")
|
|
373
|
+
dropDoc(c, target);
|
|
374
|
+
} catch {}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
302
378
|
// src/core/intent-retention.ts
|
|
303
379
|
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
304
380
|
var DROP_ENVELOPE_RETENTION_MS = 30 * DAY_MS;
|
|
@@ -338,27 +414,27 @@ function withNodeId(node, fn) {
|
|
|
338
414
|
currentNodeOverride = prev;
|
|
339
415
|
}
|
|
340
416
|
}
|
|
341
|
-
function withChangeGroup(label, fn) {
|
|
417
|
+
function withChangeGroup(db, label, fn) {
|
|
342
418
|
if (currentTxn !== null)
|
|
343
419
|
return fn();
|
|
344
420
|
currentTxn = (currentActor ? currentActor + "/" : "") + (label ? label + ":" : "") + randomSuffix(8);
|
|
345
421
|
try {
|
|
346
|
-
return fn
|
|
422
|
+
return writeTx(db, fn);
|
|
347
423
|
} finally {
|
|
348
424
|
currentTxn = null;
|
|
349
425
|
}
|
|
350
426
|
}
|
|
351
|
-
function withTxnId(txn, fn) {
|
|
427
|
+
function withTxnId(db, txn, fn) {
|
|
352
428
|
const prev = currentTxn;
|
|
353
429
|
currentTxn = txn;
|
|
354
430
|
try {
|
|
355
|
-
return fn
|
|
431
|
+
return writeTx(db, fn);
|
|
356
432
|
} finally {
|
|
357
433
|
currentTxn = prev;
|
|
358
434
|
}
|
|
359
435
|
}
|
|
360
436
|
function grouped(fn, label = null) {
|
|
361
|
-
return (...args) => withChangeGroup(label, () => fn(...args));
|
|
437
|
+
return (db, ...args) => withChangeGroup(db, label, () => fn(db, ...args));
|
|
362
438
|
}
|
|
363
439
|
var DOMAIN = {
|
|
364
440
|
databases: {
|
|
@@ -480,6 +556,16 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
480
556
|
}
|
|
481
557
|
return;
|
|
482
558
|
}
|
|
559
|
+
if (dataset === "databases" && col.startsWith("meta.")) {
|
|
560
|
+
ensureRow(db, "databases", rowId);
|
|
561
|
+
const key = col.slice(5);
|
|
562
|
+
if (valueJson === null) {
|
|
563
|
+
db.query(`UPDATE databases SET meta = json_remove(coalesce(meta, '{}'), '$."' || ? || '"') WHERE id = ?`).run(key, rowId);
|
|
564
|
+
} else {
|
|
565
|
+
db.query(`UPDATE databases SET meta = json_set(coalesce(meta, '{}'), '$."' || ? || '"', json(?)) WHERE id = ?`).run(key, valueJson, rowId);
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
483
569
|
const d = DOMAIN[dataset];
|
|
484
570
|
if (!d || !d.cols.has(col))
|
|
485
571
|
return;
|
|
@@ -497,6 +583,8 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
497
583
|
function applyChange(db, c) {
|
|
498
584
|
const inserted = db.query("INSERT OR IGNORE INTO crdt_changes (hlc, node_id, dataset, row_id, col, value, txn) VALUES (?, ?, ?, ?, ?, ?, ?)").run(c.hlc, c.node_id, c.dataset, c.row_id, c.col, c.value, c.txn ?? null).changes > 0;
|
|
499
585
|
const cur = db.query("SELECT MAX(hlc) AS h FROM crdt_changes WHERE dataset = ? AND row_id = ? AND col = ?").get(c.dataset, c.row_id, c.col);
|
|
586
|
+
if (inserted)
|
|
587
|
+
noteChange(db, c.dataset, c.row_id, c.col, c.value);
|
|
500
588
|
if (cur.h !== c.hlc)
|
|
501
589
|
return { inserted, winner: false };
|
|
502
590
|
try {
|
|
@@ -510,30 +598,41 @@ function applyChange(db, c) {
|
|
|
510
598
|
return { inserted, winner: true };
|
|
511
599
|
}
|
|
512
600
|
function emit(db, dataset, rowId, col, value) {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
601
|
+
const mint = () => {
|
|
602
|
+
const node = currentNodeOverride ?? getNodeId(db);
|
|
603
|
+
const change = {
|
|
604
|
+
hlc: nextHlc(db, node),
|
|
605
|
+
node_id: node,
|
|
606
|
+
dataset,
|
|
607
|
+
row_id: rowId,
|
|
608
|
+
col,
|
|
609
|
+
value: value === undefined ? null : JSON.stringify(value),
|
|
610
|
+
txn: currentTxn
|
|
611
|
+
};
|
|
612
|
+
applyChange(db, change);
|
|
613
|
+
return change;
|
|
522
614
|
};
|
|
523
|
-
|
|
524
|
-
return change;
|
|
615
|
+
return currentTxn !== null ? mint() : writeTx(db, mint);
|
|
525
616
|
}
|
|
526
617
|
function ingest(db, changes, opts = {}) {
|
|
618
|
+
return ingestDetailed(db, changes, opts).received;
|
|
619
|
+
}
|
|
620
|
+
function ingestDetailed(db, changes, opts = {}) {
|
|
527
621
|
const now = opts.now ?? Date.now();
|
|
528
622
|
pruneExpiredIntentReceipts(db, now);
|
|
529
623
|
const node = getNodeId(db);
|
|
530
624
|
let received = 0;
|
|
531
625
|
let poisoned = 0;
|
|
532
|
-
const
|
|
533
|
-
|
|
626
|
+
const skewed = {};
|
|
627
|
+
const tx = () => {
|
|
628
|
+
for (const c of changes) {
|
|
534
629
|
if (isExpiredIntentReceipt(c, now))
|
|
535
630
|
continue;
|
|
536
|
-
|
|
631
|
+
if (isSkewedHlc(c.hlc, now)) {
|
|
632
|
+
skewed[c.node_id] = (skewed[c.node_id] ?? 0) + 1;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
observeHlc(db, node, c.hlc, now);
|
|
537
636
|
try {
|
|
538
637
|
if (applyChange(db, c).inserted)
|
|
539
638
|
received++;
|
|
@@ -542,11 +641,13 @@ function ingest(db, changes, opts = {}) {
|
|
|
542
641
|
console.error(`[ingest] skipped a change that failed to apply (dataset=${c.dataset} col=${c.col} row=${c.row_id}): ${e.message}`);
|
|
543
642
|
}
|
|
544
643
|
}
|
|
545
|
-
}
|
|
546
|
-
tx
|
|
644
|
+
};
|
|
645
|
+
writeTx(db, tx);
|
|
547
646
|
if (poisoned)
|
|
548
647
|
console.error(`[ingest] ${poisoned} change(s) skipped this batch (see above)`);
|
|
549
|
-
|
|
648
|
+
for (const [n, k] of Object.entries(skewed))
|
|
649
|
+
console.error(`[ingest] refused ${k} change(s) from node ${n}: stamped more than 5 min in the future (that device should run mh repair --clock)`);
|
|
650
|
+
return { received, skewed };
|
|
550
651
|
}
|
|
551
652
|
|
|
552
653
|
// src/core/indexing.ts
|
|
@@ -1204,7 +1305,19 @@ var DATABASE_SELECT = DATABASE_COLS.join(", ");
|
|
|
1204
1305
|
function rowOut(r) {
|
|
1205
1306
|
if (!r)
|
|
1206
1307
|
return null;
|
|
1207
|
-
|
|
1308
|
+
const meta = r.meta === null ? null : JSON.parse(r.meta);
|
|
1309
|
+
return { ...r, meta: meta && Object.keys(meta).length ? meta : null };
|
|
1310
|
+
}
|
|
1311
|
+
function emitMetaPatch(db, id, cur, patch) {
|
|
1312
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1313
|
+
if (k.includes("."))
|
|
1314
|
+
throw new MhError("invalid_input", `meta key must not contain ".": ${k}`);
|
|
1315
|
+
const next = v === undefined ? null : v;
|
|
1316
|
+
const prev = cur && k in cur ? cur[k] : null;
|
|
1317
|
+
if (JSON.stringify(prev ?? null) === JSON.stringify(next ?? null))
|
|
1318
|
+
continue;
|
|
1319
|
+
emit(db, "databases", id, `meta.${k}`, next === null ? undefined : next);
|
|
1320
|
+
}
|
|
1208
1321
|
}
|
|
1209
1322
|
var createDatabase = grouped(function createDatabase(db, opts) {
|
|
1210
1323
|
const id = newId("db", opts.name);
|
|
@@ -1224,7 +1337,9 @@ var updateDatabase = grouped(function updateDatabase(db, id, fields) {
|
|
|
1224
1337
|
if (fields.meta !== undefined) {
|
|
1225
1338
|
if (fields.meta !== null && (typeof fields.meta !== "object" || Array.isArray(fields.meta)))
|
|
1226
1339
|
throw new MhError("invalid_input", "meta must be a JSON object or null");
|
|
1227
|
-
|
|
1340
|
+
const cur = getDatabase(db, id).meta;
|
|
1341
|
+
const patch = fields.meta ?? Object.fromEntries(Object.keys(cur ?? {}).map((k) => [k, null]));
|
|
1342
|
+
emitMetaPatch(db, id, cur, patch);
|
|
1228
1343
|
}
|
|
1229
1344
|
return getDatabase(db, id);
|
|
1230
1345
|
});
|
|
@@ -1238,8 +1353,7 @@ var duplicateDatabase = grouped(function duplicateDatabase(db, id, opts = {}) {
|
|
|
1238
1353
|
});
|
|
1239
1354
|
if (src.meta !== null) {
|
|
1240
1355
|
const { collapsed: _hidden, ...meta } = src.meta;
|
|
1241
|
-
|
|
1242
|
-
emit(db, "databases", dup.id, "meta", meta);
|
|
1356
|
+
emitMetaPatch(db, dup.id, null, meta);
|
|
1243
1357
|
}
|
|
1244
1358
|
const propIdMap = new Map;
|
|
1245
1359
|
const selfRelNew = new Set;
|
|
@@ -1563,8 +1677,151 @@ function escapeHtml(s) {
|
|
|
1563
1677
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1564
1678
|
}
|
|
1565
1679
|
|
|
1680
|
+
// src/core/sync/locale.ts
|
|
1681
|
+
var LANG_COOKIE = "mh-lang";
|
|
1682
|
+
var isLocale = (v) => v === "zh-CN" || v === "en";
|
|
1683
|
+
function localeFromTags(tags) {
|
|
1684
|
+
if (!tags || tags.length === 0)
|
|
1685
|
+
return "zh-CN";
|
|
1686
|
+
for (const tag of tags) {
|
|
1687
|
+
if (!tag)
|
|
1688
|
+
continue;
|
|
1689
|
+
if (/^zh\b/i.test(tag))
|
|
1690
|
+
return "zh-CN";
|
|
1691
|
+
if (/^en\b/i.test(tag))
|
|
1692
|
+
return "en";
|
|
1693
|
+
}
|
|
1694
|
+
return "en";
|
|
1695
|
+
}
|
|
1696
|
+
function localeFromAcceptLanguage(header) {
|
|
1697
|
+
if (!header)
|
|
1698
|
+
return "zh-CN";
|
|
1699
|
+
return localeFromTags(header.split(",").map((s) => s.split(";")[0].trim()));
|
|
1700
|
+
}
|
|
1701
|
+
function localeFromCookie(header) {
|
|
1702
|
+
if (!header)
|
|
1703
|
+
return null;
|
|
1704
|
+
for (const part of header.split(";")) {
|
|
1705
|
+
const eq = part.indexOf("=");
|
|
1706
|
+
if (eq < 0)
|
|
1707
|
+
continue;
|
|
1708
|
+
if (part.slice(0, eq).trim() !== LANG_COOKIE)
|
|
1709
|
+
continue;
|
|
1710
|
+
const v = decodeURIComponent(part.slice(eq + 1).trim());
|
|
1711
|
+
return isLocale(v) ? v : null;
|
|
1712
|
+
}
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
function pickLocale(req) {
|
|
1716
|
+
return localeFromCookie(req.headers.get("cookie")) ?? localeFromAcceptLanguage(req.headers.get("accept-language"));
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/core/sync/page-messages.ts
|
|
1720
|
+
var EN = {
|
|
1721
|
+
"本机逻辑时钟比系统时间超前 {min} 分钟(系统时钟曾被调错,或收到过时钟错误设备的数据),运行 mh repair --clock 修复": "This device's logical clock is {min} min ahead of wall time (the system clock was wrong, or data arrived from a device with a wrong clock); run mh repair --clock",
|
|
1722
|
+
"数据库正被另一个进程写入,请稍后重试": "The database is being written by another process; retry shortly",
|
|
1723
|
+
"通过 metahub 分享": "Shared via metahub",
|
|
1724
|
+
"由 metahub 托管": "Hosted by metahub",
|
|
1725
|
+
"(空文档)": "(empty document)",
|
|
1726
|
+
文档: "Document",
|
|
1727
|
+
表格: "Table",
|
|
1728
|
+
编辑: "Edit",
|
|
1729
|
+
编辑文档: "Edit document",
|
|
1730
|
+
保存: "Save",
|
|
1731
|
+
"保存中…": "Saving…",
|
|
1732
|
+
保存失败: "Save failed",
|
|
1733
|
+
"已保存 ✓": "Saved ✓",
|
|
1734
|
+
"可编辑文本/数字/URL/日期单元格,失焦自动保存": "Text, number, URL and date cells are editable; changes save on blur",
|
|
1735
|
+
"编辑站点文件(文本文件可在线编辑;保存即对所有访问者生效)。": "Edit site files (text files can be edited here; saving applies to every visitor).",
|
|
1736
|
+
"(暂无文件)": "(no files yet)",
|
|
1737
|
+
"预览 ↗": "Preview ↗",
|
|
1738
|
+
"(二进制文件,不可在线编辑)": "(binary file, cannot be edited here)",
|
|
1739
|
+
"{name} · 文件": "{name} · Files",
|
|
1740
|
+
"\uD83D\uDD12 此分享受口令保护": "\uD83D\uDD12 This share is protected by a passcode",
|
|
1741
|
+
"口令错误,请重试。": "Wrong passcode, try again.",
|
|
1742
|
+
口令: "Passcode",
|
|
1743
|
+
解锁: "Unlock",
|
|
1744
|
+
受保护的分享: "Protected share",
|
|
1745
|
+
复制全文: "Copy all",
|
|
1746
|
+
已复制: "Copied",
|
|
1747
|
+
复制失败: "Copy failed",
|
|
1748
|
+
站点尚未同步: "Site not synced yet",
|
|
1749
|
+
"分享方设备上线同步后即可访问。": "Available once the sharing device comes online and syncs.",
|
|
1750
|
+
页面不存在: "Page not found",
|
|
1751
|
+
"这个站点里没有这个页面或文件。": "This site has no such page or file.",
|
|
1752
|
+
资源暂不可用: "Resource temporarily unavailable",
|
|
1753
|
+
"文件内容尚未同步到房间,请稍后重试。": "The file content has not synced to the room yet. Try again later.",
|
|
1754
|
+
"文件的内容暂时无法取得,请稍后重试。": "The file content cannot be retrieved right now. Try again later.",
|
|
1755
|
+
站点不存在: "Site not found",
|
|
1756
|
+
"没有名为 “{name}” 的站点。": "There is no site named “{name}”.",
|
|
1757
|
+
返回站点首页: "Back to site home",
|
|
1758
|
+
"验证访问令牌 · metahub": "Verify access token · metahub",
|
|
1759
|
+
输入访问令牌: "Enter the access token",
|
|
1760
|
+
"此服务器已开启访问保护。在已登录设备的「设置 → 设备与授权」获取登录链接或二维码,或在服务器上运行 {cmd}。": "This server requires an access token. Get a login link or QR code from Settings → Devices on a signed-in device, or run {cmd} on the server.",
|
|
1761
|
+
粘贴访问令牌或登录链接: "Paste the access token or login link",
|
|
1762
|
+
显示令牌: "Show token",
|
|
1763
|
+
隐藏令牌: "Hide token",
|
|
1764
|
+
解锁并进入: "Unlock and enter",
|
|
1765
|
+
"请输入访问令牌。": "Enter the access token.",
|
|
1766
|
+
"验证中…": "Verifying…",
|
|
1767
|
+
"令牌无效,请检查后重试。": "Invalid token. Check it and try again.",
|
|
1768
|
+
"登录已过期,请重新输入令牌。": "Session expired. Enter the token again.",
|
|
1769
|
+
附件已失效: "Attachment unavailable",
|
|
1770
|
+
链接缺少分享数据: "The link is missing share data",
|
|
1771
|
+
请输入分享口令: "Enter the share passcode",
|
|
1772
|
+
链接缺少密钥: "The link is missing its key",
|
|
1773
|
+
口令错误或链接已失效: "Wrong passcode or the link has expired",
|
|
1774
|
+
链接已失效或密钥不正确: "The link has expired or the key is wrong",
|
|
1775
|
+
分享: "Share",
|
|
1776
|
+
正在加载: "Loading",
|
|
1777
|
+
"✅ 已授权": "✅ Authorized",
|
|
1778
|
+
"可以关闭此标签页,回到 MetaHub 继续部署。": "You can close this tab and return to MetaHub to continue deploying.",
|
|
1779
|
+
站点入口健康响应过大: "Site entry health response is too large",
|
|
1780
|
+
站点入口不能指向云平台元数据服务: "Site entry must not point at a cloud metadata service",
|
|
1781
|
+
"Desktop sidecar 仅用于本机预览;请启动带鉴权的 mh --server,或使用 Edge": "The desktop sidecar is for local preview only; start an authenticated mh --server, or use Edge",
|
|
1782
|
+
"站点入口健康检查失败(HTTP {status})": "Site entry health check failed (HTTP {status})",
|
|
1783
|
+
站点入口健康响应无效: "Site entry health response is invalid",
|
|
1784
|
+
"站点入口节点不匹配(期望 {expected},实际 {actual});这是防误配检查,不是身份认证": "Site entry node mismatch (expected {expected}, got {actual}); this is a misconfiguration guard, not authentication",
|
|
1785
|
+
"目标设备版本不支持定向发布渠道;请先升级该设备,再重新验证入口": "The target device does not support targeted publishing channels; upgrade it, then verify the entry again",
|
|
1786
|
+
"无法验证站点入口:连接超时": "Could not verify the site entry: connection timed out",
|
|
1787
|
+
"无法验证站点入口:{msg}": "Could not verify the site entry: {msg}",
|
|
1788
|
+
"请先在“设置 → 站点托管”配置并验证当前设备的公网或局域网入口": "Configure and verify this device's public or LAN entry under Settings → Site hosting first",
|
|
1789
|
+
"配对设备缺少节点身份,重新配对后再试": "The paired device has no node identity; pair again and retry",
|
|
1790
|
+
"该站点仍在等待目标设备确认回滚,请先重试回滚": "This site is still waiting for the target device to confirm the rollback; retry the rollback first",
|
|
1791
|
+
配对设备同步失败: "Sync with the paired device failed",
|
|
1792
|
+
没有待确认的站点回滚: "No site rollback is awaiting confirmation",
|
|
1793
|
+
"目标配对设备不存在,请重新配对后重试": "The target paired device does not exist; pair again and retry",
|
|
1794
|
+
站点渠道已不存在: "The site channel no longer exists",
|
|
1795
|
+
"OAuth 流程不存在或已过期,请重新登录": "The OAuth flow does not exist or has expired; sign in again",
|
|
1796
|
+
"OAuth 流程不存在或已过期,请重试": "The OAuth flow does not exist or has expired; try again",
|
|
1797
|
+
"Cloudflare 授权失败": "Cloudflare authorization failed",
|
|
1798
|
+
"Cloudflare 授权尚未完成,请稍候": "Cloudflare authorization is not finished yet; please wait",
|
|
1799
|
+
"缺少 Cloudflare 账号 id": "Missing Cloudflare account id",
|
|
1800
|
+
"缺少 Cloudflare 凭据(请先登录或提供 API Token)": "Missing Cloudflare credentials (sign in first or provide an API token)",
|
|
1801
|
+
"Cloudflare 授权失败:{detail}": "Cloudflare authorization failed: {detail}",
|
|
1802
|
+
"未返回 access token": "no access token returned",
|
|
1803
|
+
"无法连接 Cloudflare 授权服务:{msg}": "Could not reach the Cloudflare authorization service: {msg}",
|
|
1804
|
+
"Cloudflare 令牌交换失败(HTTP {status})": "Cloudflare token exchange failed (HTTP {status})",
|
|
1805
|
+
"无法读取 Cloudflare 账号列表:{msg}": "Could not read the Cloudflare account list: {msg}",
|
|
1806
|
+
"读取 Cloudflare 账号失败(HTTP {status})": "Reading Cloudflare accounts failed (HTTP {status})",
|
|
1807
|
+
"未配置 Cloudflare OAuth 客户端,请改用 API Token 部署": "No Cloudflare OAuth client is configured; deploy with an API token instead",
|
|
1808
|
+
"Cloudflare 授权超时,请重试": "Cloudflare authorization timed out; try again",
|
|
1809
|
+
"Cloudflare 授权被拒绝:{detail}": "Cloudflare authorization was denied: {detail}",
|
|
1810
|
+
"OAuth state 不匹配,疑似伪造回调,已中止": "OAuth state mismatch; the callback looks forged, aborted",
|
|
1811
|
+
"Cloudflare 未返回授权码": "Cloudflare did not return an authorization code",
|
|
1812
|
+
"请输入完整的 http(s) 地址": "Enter a complete http(s) address",
|
|
1813
|
+
"站点入口只支持 HTTP 或 HTTPS": "Site entry supports HTTP or HTTPS only",
|
|
1814
|
+
"站点入口不能包含凭据、查询参数或片段": "Site entry must not contain credentials, query parameters or fragments",
|
|
1815
|
+
"站点入口必须是域名根地址,不能包含路径": "Site entry must be a domain root without a path",
|
|
1816
|
+
站点入口必须是可访问的主机地址: "Site entry must be a reachable host address",
|
|
1817
|
+
"公网入口必须使用 HTTPS;HTTP 仅允许本机或局域网地址": "A public entry must use HTTPS; HTTP is only allowed for local or LAN addresses"
|
|
1818
|
+
};
|
|
1819
|
+
function pm(locale, key, params) {
|
|
1820
|
+
const s = locale === "en" ? EN[key] : key;
|
|
1821
|
+
return params ? s.replace(/\{(\w+)\}/g, (m, k) => (k in params) ? String(params[k]) : m) : s;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1566
1824
|
// src/core/guest-intent.ts
|
|
1567
|
-
var HLC_SKEW_MS = 5 * 60000;
|
|
1568
1825
|
var INTENT_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
1569
1826
|
var RECEIPT_COL = "result";
|
|
1570
1827
|
function applyGuestIntent(db, policy, session, intent, opts) {
|
|
@@ -1576,7 +1833,7 @@ function applyGuestIntent(db, policy, session, intent, opts) {
|
|
|
1576
1833
|
kind: policy.audience,
|
|
1577
1834
|
guestNode: session.guestNode
|
|
1578
1835
|
};
|
|
1579
|
-
|
|
1836
|
+
return writeTx(db, () => {
|
|
1580
1837
|
const prepared = prepareIntent(db, policy, principal, intent, opts.clock);
|
|
1581
1838
|
const txn = intentTxnPrefix(session.guestNode, intent.intentId) + prepared.fingerprint;
|
|
1582
1839
|
const receipt = readReceipt(db, session.guestNode, intent.intentId, acceptedNow);
|
|
@@ -1596,7 +1853,6 @@ function applyGuestIntent(db, policy, session, intent, opts) {
|
|
|
1596
1853
|
return applySubmitted(db, policy, principal, intent, prepared, txn, acceptedNow);
|
|
1597
1854
|
return applyAuthority(db, policy, principal, intent, prepared, txn, acceptedNow);
|
|
1598
1855
|
});
|
|
1599
|
-
return tx();
|
|
1600
1856
|
}
|
|
1601
1857
|
function prepareIntent(db, policy, principal, intent, clock) {
|
|
1602
1858
|
if (typeof intent.payload !== "object" || intent.payload === null || Array.isArray(intent.payload))
|
|
@@ -1634,7 +1890,7 @@ function prepareIntent(db, policy, principal, intent, clock) {
|
|
|
1634
1890
|
};
|
|
1635
1891
|
}
|
|
1636
1892
|
function applyAuthority(db, policy, principal, intent, prepared, txn, acceptedNow) {
|
|
1637
|
-
return withTxnId(txn, () => withNodeId(principal.guestNode, () => {
|
|
1893
|
+
return withTxnId(db, txn, () => withNodeId(principal.guestNode, () => {
|
|
1638
1894
|
if (intent.action === "createRecord")
|
|
1639
1895
|
assertGuestCreateCapacity(db, prepared.database, policy.limits);
|
|
1640
1896
|
const current = intent.action === "createRecord" ? createRecordPrepared(db, prepared.database, prepared.cells) : updateRecordPrepared(db, intent.recordId, prepared.cells);
|
|
@@ -1758,7 +2014,7 @@ function isRecordRow(value) {
|
|
|
1758
2014
|
}
|
|
1759
2015
|
function clampMillis(submittedAt, now) {
|
|
1760
2016
|
const safeNow = Math.max(0, Math.trunc(Number.isFinite(now) ? now : Date.now()));
|
|
1761
|
-
const ceil = safeNow +
|
|
2017
|
+
const ceil = safeNow + HLC_MAX_SKEW_MS;
|
|
1762
2018
|
if (!Number.isFinite(submittedAt))
|
|
1763
2019
|
return safeNow;
|
|
1764
2020
|
return Math.max(0, Math.min(Math.trunc(submittedAt), ceil));
|
|
@@ -2790,25 +3046,6 @@ var editDocument = grouped(function editDocument(db, id, opts) {
|
|
|
2790
3046
|
throw new MhError("invalid_input", `anchor not found: no match for --old`);
|
|
2791
3047
|
if (count > 1 && !opts.replaceAll)
|
|
2792
3048
|
throw new MhError("ambiguous", `${count} matches for --old; add surrounding context or use --replace-all`);
|
|
2793
|
-
if (!opts.old.includes(`
|
|
2794
|
-
|
|
2795
|
-
`)) {
|
|
2796
|
-
const affected = blocks.filter((b) => b.text.includes(opts.old));
|
|
2797
|
-
const next = affected.map((b) => opts.replaceAll ? b.text.replaceAll(opts.old, () => opts.new) : b.text.replace(opts.old, () => opts.new));
|
|
2798
|
-
if (next.every((t) => t.length > 0 && !t.includes(`
|
|
2799
|
-
|
|
2800
|
-
`))) {
|
|
2801
|
-
const targets = opts.replaceAll ? affected : affected.slice(0, 1);
|
|
2802
|
-
let wrote = false;
|
|
2803
|
-
targets.forEach((b, i) => {
|
|
2804
|
-
if (next[i] !== b.text) {
|
|
2805
|
-
emit(db, "doc_blocks", b.id, "text", next[i]);
|
|
2806
|
-
wrote = true;
|
|
2807
|
-
}
|
|
2808
|
-
});
|
|
2809
|
-
return { id, changed: wrote, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
3049
|
const newBody = opts.replaceAll ? body.replaceAll(opts.old, () => opts.new) : body.replace(opts.old, () => opts.new);
|
|
2813
3050
|
reconcileBody(db, id, newBody);
|
|
2814
3051
|
return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
@@ -2905,7 +3142,7 @@ function readPolicy(db) {
|
|
|
2905
3142
|
return { fullNodes, redundancy };
|
|
2906
3143
|
}
|
|
2907
3144
|
var setFullNodes = (db, nodeIds) => {
|
|
2908
|
-
withChangeGroup(null, () => {
|
|
3145
|
+
withChangeGroup(db, null, () => {
|
|
2909
3146
|
const seen = new Set;
|
|
2910
3147
|
const list = nodeIds.filter((n) => n && !seen.has(n) && seen.add(n) != null);
|
|
2911
3148
|
emit(db, "blob_policy", POLICY_ID, "full_nodes", list);
|
|
@@ -2959,7 +3196,7 @@ function migrateRecords(db) {
|
|
|
2959
3196
|
db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
|
|
2960
3197
|
if (!tableExists3(db, "record_values"))
|
|
2961
3198
|
return;
|
|
2962
|
-
|
|
3199
|
+
writeTx(db, () => {
|
|
2963
3200
|
db.exec(`
|
|
2964
3201
|
UPDATE records SET data = coalesce((
|
|
2965
3202
|
SELECT json_group_object(rv.property_id, json(rv.value))
|
|
@@ -2970,7 +3207,6 @@ function migrateRecords(db) {
|
|
|
2970
3207
|
db.exec("DROP TABLE record_values");
|
|
2971
3208
|
backfillRecordOrderKeys(db);
|
|
2972
3209
|
});
|
|
2973
|
-
tx();
|
|
2974
3210
|
backfillRecordOrderKeys(db);
|
|
2975
3211
|
}
|
|
2976
3212
|
function migrateShares(db) {
|
|
@@ -3028,7 +3264,7 @@ function migrateOplog(db) {
|
|
|
3028
3264
|
function migrateCrdtChangesSeq(db) {
|
|
3029
3265
|
if (hasColumn(db, "crdt_changes", "seq"))
|
|
3030
3266
|
return;
|
|
3031
|
-
|
|
3267
|
+
writeTx(db, () => {
|
|
3032
3268
|
db.exec(`
|
|
3033
3269
|
DROP TABLE IF EXISTS crdt_changes_new;
|
|
3034
3270
|
CREATE TABLE crdt_changes_new (
|
|
@@ -3058,7 +3294,6 @@ function migrateCrdtChangesSeq(db) {
|
|
|
3058
3294
|
if (tableExists3(db, "peers"))
|
|
3059
3295
|
db.exec("UPDATE peers SET push_cursor = 0, pull_cursor = 0");
|
|
3060
3296
|
});
|
|
3061
|
-
tx();
|
|
3062
3297
|
}
|
|
3063
3298
|
function migrateStoragePeerUrls(db) {
|
|
3064
3299
|
if (!tableExists3(db, "peers"))
|
|
@@ -3091,7 +3326,7 @@ function migrateStoragePeerUrls(db) {
|
|
|
3091
3326
|
if (renames.length === 0)
|
|
3092
3327
|
return;
|
|
3093
3328
|
const map = new Map(renames.map(({ oldUrl, newUrl }) => [oldUrl, newUrl]));
|
|
3094
|
-
|
|
3329
|
+
writeTx(db, () => {
|
|
3095
3330
|
for (const { oldUrl, newUrl } of renames) {
|
|
3096
3331
|
if (tableExists3(db, "storage_cursors"))
|
|
3097
3332
|
db.query("UPDATE storage_cursors SET peer_url = ? WHERE peer_url = ?").run(newUrl, oldUrl);
|
|
@@ -3103,7 +3338,6 @@ function migrateStoragePeerUrls(db) {
|
|
|
3103
3338
|
setFullNodes(db, fullNodes.map((n) => map.get(n) ?? n));
|
|
3104
3339
|
}
|
|
3105
3340
|
});
|
|
3106
|
-
tx();
|
|
3107
3341
|
}
|
|
3108
3342
|
function migrateDatabases(db) {
|
|
3109
3343
|
if (hasColumn(db, "databases", "meta"))
|
|
@@ -3156,11 +3390,11 @@ function replayDataset(db, dataset) {
|
|
|
3156
3390
|
WHERE dataset = ? AND seq > ? ORDER BY seq`).all(dataset, since);
|
|
3157
3391
|
if (changes.length === 0)
|
|
3158
3392
|
return;
|
|
3159
|
-
db
|
|
3160
|
-
for (const change of
|
|
3393
|
+
writeTx(db, () => {
|
|
3394
|
+
for (const change of changes)
|
|
3161
3395
|
applyChange(db, change);
|
|
3162
|
-
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, String(
|
|
3163
|
-
})
|
|
3396
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, String(changes[changes.length - 1].seq));
|
|
3397
|
+
});
|
|
3164
3398
|
}
|
|
3165
3399
|
function migrateSiteChannels(db) {
|
|
3166
3400
|
replayDataset(db, "site_channels");
|
|
@@ -3268,12 +3502,11 @@ function roomMissingBlobs(db, limit = NEED_BLOBS_LIMIT) {
|
|
|
3268
3502
|
return rows.map((r) => r.hash);
|
|
3269
3503
|
}
|
|
3270
3504
|
function roomPutBlobChunk(db, hash, idx, total, bytes) {
|
|
3271
|
-
|
|
3505
|
+
writeTx(db, () => {
|
|
3272
3506
|
if (idx === 0)
|
|
3273
3507
|
db.query("DELETE FROM room_blobs WHERE hash = ?").run(hash);
|
|
3274
3508
|
db.query("INSERT OR REPLACE INTO room_blobs (hash, idx, total, bytes) VALUES (?, ?, ?, ?)").run(hash, idx, total, bytes);
|
|
3275
3509
|
});
|
|
3276
|
-
tx();
|
|
3277
3510
|
}
|
|
3278
3511
|
function roomBlobBytes(db, hash) {
|
|
3279
3512
|
const rows = db.query("SELECT idx, total, bytes FROM room_blobs WHERE hash = ? ORDER BY idx").all(hash);
|
|
@@ -3303,6 +3536,7 @@ function evictRow(db, key, ackSeq, guestBase) {
|
|
|
3303
3536
|
if (!table)
|
|
3304
3537
|
return;
|
|
3305
3538
|
db.query(`DELETE FROM ${table} WHERE id = ?`).run(key.row_id);
|
|
3539
|
+
noteChange(db, key.dataset, key.row_id, "", null);
|
|
3306
3540
|
db.query(`DELETE FROM crdt_changes WHERE dataset = ? AND row_id = ?
|
|
3307
3541
|
AND NOT (${GUEST_NODE_SQL} AND seq > ?)`).run(key.dataset, key.row_id, guestBase, guestBase, ackSeq);
|
|
3308
3542
|
const remaining = db.query("SELECT 1 FROM crdt_changes WHERE dataset = ? AND row_id = ? LIMIT 1").get(key.dataset, key.row_id) != null;
|
|
@@ -3319,6 +3553,7 @@ function sweepEvictPending(db, ackSeq, guestBase) {
|
|
|
3319
3553
|
db.query("DELETE FROM evict_pending WHERE dataset = ? AND row_id = ?").run(k.dataset, k.row_id);
|
|
3320
3554
|
continue;
|
|
3321
3555
|
}
|
|
3556
|
+
noteChange(db, k.dataset, k.row_id, "", null);
|
|
3322
3557
|
db.query(`DELETE FROM crdt_changes WHERE dataset = ? AND row_id = ? AND seq <= ? AND (${GUEST_NODE_SQL})`).run(k.dataset, k.row_id, ackSeq, guestBase, guestBase);
|
|
3323
3558
|
const remaining = db.query("SELECT 1 FROM crdt_changes WHERE dataset = ? AND row_id = ? LIMIT 1").get(k.dataset, k.row_id) != null;
|
|
3324
3559
|
if (!remaining)
|
|
@@ -3652,9 +3887,9 @@ function createRoomFetch(deps) {
|
|
|
3652
3887
|
if (req.method !== "GET")
|
|
3653
3888
|
return plain404();
|
|
3654
3889
|
if (locked) {
|
|
3655
|
-
return wantsHtml(req) ? new Response(unlockPage(cfg, false), { headers: HTML }) : json({ error: "unauthorized" }, 401);
|
|
3890
|
+
return wantsHtml(req) ? new Response(unlockPage(pickLocale(req), cfg, false), { headers: HTML }) : json({ error: "unauthorized" }, 401);
|
|
3656
3891
|
}
|
|
3657
|
-
return serveRoomFile(db, cfg, sub);
|
|
3892
|
+
return serveRoomFile(db, cfg, sub, pickLocale(req));
|
|
3658
3893
|
};
|
|
3659
3894
|
}
|
|
3660
3895
|
async function handleUnlock(db, cfg, req, url) {
|
|
@@ -3668,20 +3903,20 @@ async function handleUnlock(db, cfg, req, url) {
|
|
|
3668
3903
|
}
|
|
3669
3904
|
const ok = !cfg.pwHash || !cfg.pwSalt ? true : await verifyPasswordVerifier(cfg.pwHash, cfg.pwSalt, pw);
|
|
3670
3905
|
if (!ok)
|
|
3671
|
-
return new Response(unlockPage(cfg, true), { headers: HTML, status: 401 });
|
|
3906
|
+
return new Response(unlockPage(pickLocale(req), cfg, true), { headers: HTML, status: 401 });
|
|
3672
3907
|
const minted = await mintRoomSession(db, cfg, url);
|
|
3673
3908
|
return new Response(null, {
|
|
3674
3909
|
status: 303,
|
|
3675
3910
|
headers: { location: `/r/${cfg.slug}/`, "set-cookie": minted.cookie }
|
|
3676
3911
|
});
|
|
3677
3912
|
}
|
|
3678
|
-
function serveRoomFile(db, cfg, path) {
|
|
3913
|
+
function serveRoomFile(db, cfg, path, locale) {
|
|
3679
3914
|
const site = db.query("SELECT id, spa FROM sites WHERE __deleted = 0 ORDER BY created_hlc LIMIT 1").get();
|
|
3680
3915
|
if (!site)
|
|
3681
|
-
return roomMessagePage("站点尚未同步", "分享方设备上线同步后即可访问。", 404);
|
|
3916
|
+
return roomMessagePage(locale, pm(locale, "站点尚未同步"), pm(locale, "分享方设备上线同步后即可访问。"), 404);
|
|
3682
3917
|
const resolved = resolveSiteFileRow(db, site.id, path, { spa: site.spa === 1 });
|
|
3683
3918
|
if (!resolved)
|
|
3684
|
-
return roomMessagePage("页面不存在", "这个站点里没有这个页面或文件。", 404);
|
|
3919
|
+
return roomMessagePage(locale, pm(locale, "页面不存在"), pm(locale, "这个站点里没有这个页面或文件。"), 404);
|
|
3685
3920
|
const { row, status } = resolved;
|
|
3686
3921
|
const headers = {
|
|
3687
3922
|
"content-type": row.content_type,
|
|
@@ -3697,7 +3932,7 @@ function serveRoomFile(db, cfg, path) {
|
|
|
3697
3932
|
}
|
|
3698
3933
|
const bytes = row.content ? roomBlobBytes(db, row.content) : null;
|
|
3699
3934
|
if (!bytes)
|
|
3700
|
-
return roomMessagePage("资源暂不可用", "文件内容尚未同步到房间,请稍后重试。", 404);
|
|
3935
|
+
return roomMessagePage(locale, pm(locale, "资源暂不可用"), pm(locale, "文件内容尚未同步到房间,请稍后重试。"), 404);
|
|
3701
3936
|
const body = new Uint8Array(bytes.byteLength);
|
|
3702
3937
|
body.set(bytes);
|
|
3703
3938
|
return new Response(body.buffer, { status, headers });
|
|
@@ -3716,28 +3951,28 @@ var PAGE_CSS = `
|
|
|
3716
3951
|
.muted{color:var(--muted);font-size:14px}
|
|
3717
3952
|
h1{font-size:20px;margin:0 0 10px;text-align:center}
|
|
3718
3953
|
footer{margin-top:34px;color:var(--muted);font-size:12px;text-align:center}`;
|
|
3719
|
-
function unlockPage(cfg, error) {
|
|
3720
|
-
return `<!doctype html><html lang="
|
|
3954
|
+
function unlockPage(locale, cfg, error) {
|
|
3955
|
+
return `<!doctype html><html lang="${locale}"><head>
|
|
3721
3956
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3722
|
-
<meta name="robots" content="noindex"><title
|
|
3957
|
+
<meta name="robots" content="noindex"><title>${escapeHtml(pm(locale, "受保护的分享"))}</title>
|
|
3723
3958
|
<style>${PAGE_CSS}</style></head><body><main>
|
|
3724
3959
|
<form method="post" action="/r/${escapeHtml(cfg.slug)}/unlock" class="pw">
|
|
3725
|
-
<h1
|
|
3726
|
-
${error ?
|
|
3727
|
-
<input type="password" name="password" placeholder="口令" autofocus autocomplete="current-password">
|
|
3728
|
-
<button type="submit"
|
|
3960
|
+
<h1>${escapeHtml(pm(locale, "\uD83D\uDD12 此分享受口令保护"))}</h1>
|
|
3961
|
+
${error ? `<p class="err">${escapeHtml(pm(locale, "口令错误,请重试。"))}</p>` : ""}
|
|
3962
|
+
<input type="password" name="password" placeholder="${escapeHtml(pm(locale, "口令"))}" autofocus autocomplete="current-password">
|
|
3963
|
+
<button type="submit">${escapeHtml(pm(locale, "解锁"))}</button>
|
|
3729
3964
|
</form>
|
|
3730
|
-
<footer
|
|
3965
|
+
<footer>${escapeHtml(pm(locale, "通过 metahub 分享"))}</footer>
|
|
3731
3966
|
</main></body></html>`;
|
|
3732
3967
|
}
|
|
3733
|
-
function roomMessagePage(title, detail, status) {
|
|
3734
|
-
const html = `<!doctype html><html lang="
|
|
3968
|
+
function roomMessagePage(locale, title, detail, status) {
|
|
3969
|
+
const html = `<!doctype html><html lang="${locale}"><head>
|
|
3735
3970
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3736
3971
|
<meta name="robots" content="noindex"><title>${escapeHtml(title)}</title>
|
|
3737
3972
|
<style>${PAGE_CSS}</style></head><body><main>
|
|
3738
3973
|
<h1>${escapeHtml(title)}</h1>
|
|
3739
3974
|
<p class="muted" style="text-align:center">${escapeHtml(detail)}</p>
|
|
3740
|
-
<footer
|
|
3975
|
+
<footer>${escapeHtml(pm(locale, "由 metahub 托管"))}</footer>
|
|
3741
3976
|
</main></body></html>`;
|
|
3742
3977
|
return new Response(html, { status, headers: { ...HTML, "cache-control": "no-store" } });
|
|
3743
3978
|
}
|