@tunnelbox/core 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +115 -0
- package/locales/de-DE.json +1 -0
- package/locales/en-US.json +1 -0
- package/locales/es-ES.json +1 -0
- package/locales/fr-FR.json +1 -0
- package/locales/ja-JP.json +1 -0
- package/locales/ko-KR.json +1 -0
- package/locales/zh-CN.json +1 -0
- package/locales/zh-TW.json +1 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/messages.ts +171 -0
- package/src/types.ts +19 -0
package/dist/index.js
CHANGED
|
@@ -116,6 +116,7 @@ var zh_CN_default = {
|
|
|
116
116
|
"index.error.pairTimeout": "\u7B49\u5F85\u914D\u5BF9\u7801\u8D85\u65F6\uFF08\u4E2D\u7EE7\u672A\u54CD\u5E94\uFF09",
|
|
117
117
|
"index.log.claimed": "\u5DF2\u7ED1\u5B9A\u5230\u624B\u673A\u8D26\u53F7\uFF0C\u4E4B\u540E\u624B\u673A\u4ECE\u300C\u6211\u7684\u7535\u8111\u300D\u76F4\u63A5\u8FDE\u63A5\uFF0C\u65E0\u9700\u518D\u626B\u7801",
|
|
118
118
|
"index.log.connectedRelay": "\u5DF2\u8FDE\u63A5\u4E2D\u7EE7 {url}",
|
|
119
|
+
"index.log.loaded": "tunnelbox opencode \u63D2\u4EF6\u5DF2\u542F\u52A8\uFF08v{version}\uFF09",
|
|
119
120
|
"index.log.disconnected": "\u4E0E\u4E2D\u7EE7\u65AD\u5F00\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE\u2026",
|
|
120
121
|
"index.log.exit": "\u5DF2\u9000\u51FA",
|
|
121
122
|
"index.log.pairCode": "\u751F\u6210\u914D\u5BF9\u7801: {code}",
|
|
@@ -279,6 +280,7 @@ var zh_TW_default = {
|
|
|
279
280
|
"index.error.pairTimeout": "\u7B49\u5F85\u914D\u5C0D\u78BC\u903E\u6642\uFF08\u4E2D\u7E7C\u672A\u56DE\u61C9\uFF09",
|
|
280
281
|
"index.log.claimed": "\u5DF2\u7D81\u5B9A\u5230\u624B\u6A5F\u5E33\u865F\uFF0C\u4E4B\u5F8C\u624B\u6A5F\u76F4\u63A5\u5F9E\u300C\u6211\u7684\u96FB\u8166\u300D\u9023\u63A5\uFF0C\u4E0D\u9700\u518D\u6383\u78BC",
|
|
281
282
|
"index.log.connectedRelay": "\u5DF2\u9023\u63A5\u4E2D\u7E7C {url}",
|
|
283
|
+
"index.log.loaded": "tunnelbox opencode \u5916\u639B\u5DF2\u555F\u52D5\uFF08v{version}\uFF09",
|
|
282
284
|
"index.log.disconnected": "\u8207\u4E2D\u7E7C\u65B7\u7DDA\uFF0C\u6B63\u5728\u81EA\u52D5\u91CD\u9023\u2026",
|
|
283
285
|
"index.log.exit": "\u5DF2\u9000\u51FA",
|
|
284
286
|
"index.log.pairCode": "\u7522\u751F\u914D\u5C0D\u78BC: {code}",
|
|
@@ -442,6 +444,7 @@ var en_US_default = {
|
|
|
442
444
|
"index.error.pairTimeout": "Timed out waiting for a pairing code (the relay did not respond)",
|
|
443
445
|
"index.log.claimed": "Bound to your phone account. From now on the phone connects directly from My Computers; no need to scan again",
|
|
444
446
|
"index.log.connectedRelay": "Connected to relay {url}",
|
|
447
|
+
"index.log.loaded": "tunnelbox opencode plugin started (v{version})",
|
|
445
448
|
"index.log.disconnected": "Disconnected from relay, reconnecting automatically\u2026",
|
|
446
449
|
"index.log.exit": "Exited",
|
|
447
450
|
"index.log.pairCode": "Generated pairing code: {code}",
|
|
@@ -605,6 +608,7 @@ var ja_JP_default = {
|
|
|
605
608
|
"index.error.pairTimeout": "\u30DA\u30A2\u30EA\u30F3\u30B0\u30B3\u30FC\u30C9\u306E\u53D6\u5F97\u304C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F\uFF08\u30EA\u30EC\u30FC\u304C\u5FDC\u7B54\u3057\u307E\u305B\u3093\uFF09",
|
|
606
609
|
"index.log.claimed": "\u30B9\u30DE\u30DB\u306E\u30A2\u30AB\u30A6\u30F3\u30C8\u306B\u7D10\u4ED8\u3051\u307E\u3057\u305F\u3002\u4EE5\u964D\u306F\u300C\u30DE\u30A4 PC\u300D\u304B\u3089\u76F4\u63A5\u63A5\u7D9A\u3055\u308C\u308B\u305F\u3081\u3001\u518D\u30B9\u30AD\u30E3\u30F3\u306F\u4E0D\u8981\u3067\u3059",
|
|
607
610
|
"index.log.connectedRelay": "\u30EA\u30EC\u30FC\u306B\u63A5\u7D9A\u3057\u307E\u3057\u305F {url}",
|
|
611
|
+
"index.log.loaded": "tunnelbox opencode \u30D7\u30E9\u30B0\u30A4\u30F3\u3092\u8D77\u52D5\u3057\u307E\u3057\u305F\uFF08v{version}\uFF09",
|
|
608
612
|
"index.log.disconnected": "\u30EA\u30EC\u30FC\u304B\u3089\u5207\u65AD\u3001\u81EA\u52D5\u7684\u306B\u518D\u63A5\u7D9A\u3057\u3066\u3044\u307E\u3059\u2026",
|
|
609
613
|
"index.log.exit": "\u7D42\u4E86\u3057\u307E\u3057\u305F",
|
|
610
614
|
"index.log.pairCode": "\u30DA\u30A2\u30EA\u30F3\u30B0\u30B3\u30FC\u30C9\u3092\u751F\u6210: {code}",
|
|
@@ -768,6 +772,7 @@ var ko_KR_default = {
|
|
|
768
772
|
"index.error.pairTimeout": "\uD398\uC5B4\uB9C1 \uCF54\uB4DC \uB300\uAE30 \uC2DC\uAC04 \uCD08\uACFC(\uB9B4\uB808\uC774\uAC00 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC74C)",
|
|
769
773
|
"index.log.claimed": "\uD734\uB300\uD3F0 \uACC4\uC815\uC5D0 \uC5F0\uACB0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774\uC81C \uD734\uB300\uD3F0\uC740 \u300C\uB0B4 PC\u300D\uC5D0\uC11C \uC9C1\uC811 \uC5F0\uACB0\uD558\uBA70 \uB2E4\uC2DC \uC2A4\uCE94\uD560 \uD544\uC694\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
770
774
|
"index.log.connectedRelay": "\uB9B4\uB808\uC774\uC5D0 \uC5F0\uACB0\uB428 {url}",
|
|
775
|
+
"index.log.loaded": "tunnelbox opencode \uD50C\uB7EC\uADF8\uC778\uC744 \uC2DC\uC791\uD588\uC2B5\uB2C8\uB2E4\uFF08v{version}\uFF09",
|
|
771
776
|
"index.log.disconnected": "\uB9B4\uB808\uC774 \uC5F0\uACB0\uC774 \uB04A\uACA8 \uC790\uB3D9\uC73C\uB85C \uC7AC\uC5F0\uACB0 \uC911\u2026",
|
|
772
777
|
"index.log.exit": "\uC885\uB8CC\uB428",
|
|
773
778
|
"index.log.pairCode": "\uD398\uC5B4\uB9C1 \uCF54\uB4DC \uC0DD\uC131: {code}",
|
|
@@ -931,6 +936,7 @@ var fr_FR_default = {
|
|
|
931
936
|
"index.error.pairTimeout": "D\xE9lai d'attente du code d'appairage d\xE9pass\xE9 (le relais ne r\xE9pond pas)",
|
|
932
937
|
"index.log.claimed": "Li\xE9 \xE0 votre compte t\xE9l\xE9phone. Le t\xE9l\xE9phone se connecte d\xE9sormais directement depuis \xAB Mes PC \xBB ; plus besoin de scanner",
|
|
933
938
|
"index.log.connectedRelay": "Connect\xE9 au relais {url}",
|
|
939
|
+
"index.log.loaded": "Plugin tunnelbox opencode d\xE9marr\xE9 (v{version})",
|
|
934
940
|
"index.log.disconnected": "D\xE9connect\xE9 du relais, reconnexion automatique\u2026",
|
|
935
941
|
"index.log.exit": "Arr\xEAt\xE9",
|
|
936
942
|
"index.log.pairCode": "Code d'appairage g\xE9n\xE9r\xE9 : {code}",
|
|
@@ -1094,6 +1100,7 @@ var de_DE_default = {
|
|
|
1094
1100
|
"index.error.pairTimeout": "Zeit\xFCberschreitung beim Warten auf einen Pairing-Code (Relay antwortet nicht)",
|
|
1095
1101
|
"index.log.claimed": "An Ihr Telefonkonto gebunden. Das Telefon verbindet sich nun direkt \xFCber \u201CMeine PCs\u201D; kein erneutes Scannen n\xF6tig",
|
|
1096
1102
|
"index.log.connectedRelay": "Mit Relay verbunden {url}",
|
|
1103
|
+
"index.log.loaded": "tunnelbox-opencode-Plugin gestartet (v{version})",
|
|
1097
1104
|
"index.log.disconnected": "Vom Relay getrennt, verbinde automatisch neu\u2026",
|
|
1098
1105
|
"index.log.exit": "Beendet",
|
|
1099
1106
|
"index.log.pairCode": "Pairing-Code erzeugt: {code}",
|
|
@@ -1257,6 +1264,7 @@ var es_ES_default = {
|
|
|
1257
1264
|
"index.error.pairTimeout": "Tiempo de espera agotado al obtener el c\xF3digo de emparejamiento (el relay no responde)",
|
|
1258
1265
|
"index.log.claimed": "Vinculado a su cuenta de tel\xE9fono. El tel\xE9fono se conecta ahora directamente desde \xABMis PC\xBB; no hace falta volver a escanear",
|
|
1259
1266
|
"index.log.connectedRelay": "Conectado al relay {url}",
|
|
1267
|
+
"index.log.loaded": "Plugin tunnelbox opencode iniciado (v{version})",
|
|
1260
1268
|
"index.log.disconnected": "Desconectado del relay, reconectando autom\xE1ticamente\u2026",
|
|
1261
1269
|
"index.log.exit": "Salido",
|
|
1262
1270
|
"index.log.pairCode": "C\xF3digo de emparejamiento generado: {code}",
|
|
@@ -1609,20 +1617,127 @@ async function printPairing(code, type, name, lang) {
|
|
|
1609
1617
|
console.log(" " + tt("qr.fileWritten", { path: file }));
|
|
1610
1618
|
console.log("");
|
|
1611
1619
|
}
|
|
1620
|
+
|
|
1621
|
+
// src/messages.ts
|
|
1622
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
1623
|
+
var MAX_PART_TEXT = 16e3;
|
|
1624
|
+
var MAX_PART_ARGS = 4e3;
|
|
1625
|
+
var MAX_PAGE_BYTES = 15e5;
|
|
1626
|
+
function capStr(s, max) {
|
|
1627
|
+
if (s.length <= max) return s;
|
|
1628
|
+
return `${s.slice(0, max)}\u2026`;
|
|
1629
|
+
}
|
|
1630
|
+
function capMessage(m) {
|
|
1631
|
+
let parts;
|
|
1632
|
+
for (let i = 0; i < m.parts.length; i++) {
|
|
1633
|
+
const p = m.parts[i];
|
|
1634
|
+
const nText = typeof p.text === "string" && p.text.length > MAX_PART_TEXT ? capStr(p.text, MAX_PART_TEXT) : p.text;
|
|
1635
|
+
const nDelta = typeof p.delta === "string" && p.delta.length > MAX_PART_TEXT ? capStr(p.delta, MAX_PART_TEXT) : p.delta;
|
|
1636
|
+
const nArgs = typeof p.args === "string" && p.args.length > MAX_PART_ARGS ? capStr(p.args, MAX_PART_ARGS) : p.args;
|
|
1637
|
+
if (nText !== p.text || nDelta !== p.delta || nArgs !== p.args) {
|
|
1638
|
+
if (!parts) parts = m.parts.slice(0, i);
|
|
1639
|
+
parts.push({ ...p, text: nText, delta: nDelta, args: nArgs });
|
|
1640
|
+
} else if (parts) {
|
|
1641
|
+
parts.push(p);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return parts ? { ...m, parts } : m;
|
|
1645
|
+
}
|
|
1646
|
+
function capMessages(messages) {
|
|
1647
|
+
let changed = false;
|
|
1648
|
+
const out = messages.map((m) => {
|
|
1649
|
+
const c = capMessage(m);
|
|
1650
|
+
if (c !== m) changed = true;
|
|
1651
|
+
return c;
|
|
1652
|
+
});
|
|
1653
|
+
return changed ? out : messages;
|
|
1654
|
+
}
|
|
1655
|
+
function byteLength(messages) {
|
|
1656
|
+
try {
|
|
1657
|
+
return JSON.stringify(messages).length;
|
|
1658
|
+
} catch {
|
|
1659
|
+
return messages.length * MAX_PART_TEXT * 2;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function sliceWindow(full, query = {}) {
|
|
1663
|
+
const limit = Math.max(1, query.limit ?? DEFAULT_PAGE_SIZE);
|
|
1664
|
+
const maxBytes = query.maxBytes ?? MAX_PAGE_BYTES;
|
|
1665
|
+
let end = full.length;
|
|
1666
|
+
if (query.before !== void 0 && query.before !== "") {
|
|
1667
|
+
const idx = full.findIndex((m) => m.id === query.before);
|
|
1668
|
+
if (idx >= 0) end = idx;
|
|
1669
|
+
}
|
|
1670
|
+
let start = Math.max(0, end - limit);
|
|
1671
|
+
let messages = full.slice(start, end);
|
|
1672
|
+
while (messages.length > 1) {
|
|
1673
|
+
const capped = capMessages(messages);
|
|
1674
|
+
if (byteLength(capped) <= maxBytes) {
|
|
1675
|
+
messages = capped;
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
start++;
|
|
1679
|
+
messages = full.slice(start, end);
|
|
1680
|
+
}
|
|
1681
|
+
return { messages: capMessages(messages), hasMore: start > 0 };
|
|
1682
|
+
}
|
|
1683
|
+
var MessagesCache = class {
|
|
1684
|
+
ttlMs;
|
|
1685
|
+
maxEntries;
|
|
1686
|
+
store = /* @__PURE__ */ new Map();
|
|
1687
|
+
constructor(ttlMs = 2e3, maxEntries = 64) {
|
|
1688
|
+
this.ttlMs = ttlMs;
|
|
1689
|
+
this.maxEntries = maxEntries;
|
|
1690
|
+
}
|
|
1691
|
+
/** 命中且未过 TTL 时返回完整列表副本;否则 undefined。 */
|
|
1692
|
+
get(sessionID, now = Date.now()) {
|
|
1693
|
+
const e = this.store.get(sessionID);
|
|
1694
|
+
if (!e) return void 0;
|
|
1695
|
+
if (now - e.at > this.ttlMs) {
|
|
1696
|
+
this.store.delete(sessionID);
|
|
1697
|
+
return void 0;
|
|
1698
|
+
}
|
|
1699
|
+
return e.full;
|
|
1700
|
+
}
|
|
1701
|
+
put(sessionID, full) {
|
|
1702
|
+
if (this.store.size >= this.maxEntries && !this.store.has(sessionID)) {
|
|
1703
|
+
const oldest = this.store.keys().next().value;
|
|
1704
|
+
if (oldest !== void 0) this.store.delete(oldest);
|
|
1705
|
+
}
|
|
1706
|
+
this.store.set(sessionID, { full, at: Date.now() });
|
|
1707
|
+
}
|
|
1708
|
+
/** 会话内容变化(流式部件/状态/删除等)时调用,立即失效快照。 */
|
|
1709
|
+
invalidate(sessionID) {
|
|
1710
|
+
this.store.delete(sessionID);
|
|
1711
|
+
}
|
|
1712
|
+
/** 适配器重连/整体失效(如清除全部快照)。 */
|
|
1713
|
+
clear() {
|
|
1714
|
+
this.store.clear();
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
var messagesCache = new MessagesCache();
|
|
1612
1718
|
export {
|
|
1613
1719
|
DEFAULT_LANG,
|
|
1720
|
+
DEFAULT_PAGE_SIZE,
|
|
1614
1721
|
LANG_TAGS,
|
|
1722
|
+
MAX_PAGE_BYTES,
|
|
1723
|
+
MAX_PART_ARGS,
|
|
1724
|
+
MAX_PART_TEXT,
|
|
1725
|
+
MessagesCache,
|
|
1615
1726
|
RelayClient,
|
|
1727
|
+
capMessage,
|
|
1728
|
+
capMessages,
|
|
1616
1729
|
detectSystemLang,
|
|
1617
1730
|
envelope,
|
|
1618
1731
|
injectedRelayUrl,
|
|
1619
1732
|
loadState,
|
|
1620
1733
|
makeT,
|
|
1734
|
+
messagesCache,
|
|
1621
1735
|
normalizeLang,
|
|
1622
1736
|
printPairing,
|
|
1623
1737
|
renderPairingQrText,
|
|
1624
1738
|
resolveLang,
|
|
1625
1739
|
saveState,
|
|
1740
|
+
sliceWindow,
|
|
1626
1741
|
statePath,
|
|
1627
1742
|
t,
|
|
1628
1743
|
writePairingFile,
|
package/locales/de-DE.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "Zeitüberschreitung beim Warten auf einen Pairing-Code (Relay antwortet nicht)",
|
|
19
19
|
"index.log.claimed": "An Ihr Telefonkonto gebunden. Das Telefon verbindet sich nun direkt über “Meine PCs”; kein erneutes Scannen nötig",
|
|
20
20
|
"index.log.connectedRelay": "Mit Relay verbunden {url}",
|
|
21
|
+
"index.log.loaded": "tunnelbox-opencode-Plugin gestartet (v{version})",
|
|
21
22
|
"index.log.disconnected": "Vom Relay getrennt, verbinde automatisch neu…",
|
|
22
23
|
"index.log.exit": "Beendet",
|
|
23
24
|
"index.log.pairCode": "Pairing-Code erzeugt: {code}",
|
package/locales/en-US.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "Timed out waiting for a pairing code (the relay did not respond)",
|
|
19
19
|
"index.log.claimed": "Bound to your phone account. From now on the phone connects directly from My Computers; no need to scan again",
|
|
20
20
|
"index.log.connectedRelay": "Connected to relay {url}",
|
|
21
|
+
"index.log.loaded": "tunnelbox opencode plugin started (v{version})",
|
|
21
22
|
"index.log.disconnected": "Disconnected from relay, reconnecting automatically…",
|
|
22
23
|
"index.log.exit": "Exited",
|
|
23
24
|
"index.log.pairCode": "Generated pairing code: {code}",
|
package/locales/es-ES.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "Tiempo de espera agotado al obtener el código de emparejamiento (el relay no responde)",
|
|
19
19
|
"index.log.claimed": "Vinculado a su cuenta de teléfono. El teléfono se conecta ahora directamente desde «Mis PC»; no hace falta volver a escanear",
|
|
20
20
|
"index.log.connectedRelay": "Conectado al relay {url}",
|
|
21
|
+
"index.log.loaded": "Plugin tunnelbox opencode iniciado (v{version})",
|
|
21
22
|
"index.log.disconnected": "Desconectado del relay, reconectando automáticamente…",
|
|
22
23
|
"index.log.exit": "Salido",
|
|
23
24
|
"index.log.pairCode": "Código de emparejamiento generado: {code}",
|
package/locales/fr-FR.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "Délai d'attente du code d'appairage dépassé (le relais ne répond pas)",
|
|
19
19
|
"index.log.claimed": "Lié à votre compte téléphone. Le téléphone se connecte désormais directement depuis « Mes PC » ; plus besoin de scanner",
|
|
20
20
|
"index.log.connectedRelay": "Connecté au relais {url}",
|
|
21
|
+
"index.log.loaded": "Plugin tunnelbox opencode démarré (v{version})",
|
|
21
22
|
"index.log.disconnected": "Déconnecté du relais, reconnexion automatique…",
|
|
22
23
|
"index.log.exit": "Arrêté",
|
|
23
24
|
"index.log.pairCode": "Code d'appairage généré : {code}",
|
package/locales/ja-JP.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "ペアリングコードの取得がタイムアウトしました(リレーが応答しません)",
|
|
19
19
|
"index.log.claimed": "スマホのアカウントに紐付けました。以降は「マイ PC」から直接接続されるため、再スキャンは不要です",
|
|
20
20
|
"index.log.connectedRelay": "リレーに接続しました {url}",
|
|
21
|
+
"index.log.loaded": "tunnelbox opencode プラグインを起動しました(v{version})",
|
|
21
22
|
"index.log.disconnected": "リレーから切断、自動的に再接続しています…",
|
|
22
23
|
"index.log.exit": "終了しました",
|
|
23
24
|
"index.log.pairCode": "ペアリングコードを生成: {code}",
|
package/locales/ko-KR.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "페어링 코드 대기 시간 초과(릴레이가 응답하지 않음)",
|
|
19
19
|
"index.log.claimed": "휴대폰 계정에 연결되었습니다. 이제 휴대폰은 「내 PC」에서 직접 연결하며 다시 스캔할 필요가 없습니다",
|
|
20
20
|
"index.log.connectedRelay": "릴레이에 연결됨 {url}",
|
|
21
|
+
"index.log.loaded": "tunnelbox opencode 플러그인을 시작했습니다(v{version})",
|
|
21
22
|
"index.log.disconnected": "릴레이 연결이 끊겨 자동으로 재연결 중…",
|
|
22
23
|
"index.log.exit": "종료됨",
|
|
23
24
|
"index.log.pairCode": "페어링 코드 생성: {code}",
|
package/locales/zh-CN.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "等待配对码超时(中继未响应)",
|
|
19
19
|
"index.log.claimed": "已绑定到手机账号,之后手机从「我的电脑」直接连接,无需再扫码",
|
|
20
20
|
"index.log.connectedRelay": "已连接中继 {url}",
|
|
21
|
+
"index.log.loaded": "tunnelbox opencode 插件已启动(v{version})",
|
|
21
22
|
"index.log.disconnected": "与中继断开,正在自动重连…",
|
|
22
23
|
"index.log.exit": "已退出",
|
|
23
24
|
"index.log.pairCode": "生成配对码: {code}",
|
package/locales/zh-TW.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.error.pairTimeout": "等待配對碼逾時(中繼未回應)",
|
|
19
19
|
"index.log.claimed": "已綁定到手機帳號,之後手機直接從「我的電腦」連接,不需再掃碼",
|
|
20
20
|
"index.log.connectedRelay": "已連接中繼 {url}",
|
|
21
|
+
"index.log.loaded": "tunnelbox opencode 外掛已啟動(v{version})",
|
|
21
22
|
"index.log.disconnected": "與中繼斷線,正在自動重連…",
|
|
22
23
|
"index.log.exit": "已退出",
|
|
23
24
|
"index.log.pairCode": "產生配對碼: {code}",
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/messages.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话消息快照缓存与分页窗口工具(各智能体适配器共享)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:拉取历史(session.messages)在部分适配器开销可观——opencode 会对整段会话做
|
|
5
|
+
* 全量 hydrate、claude-code 需读盘解析 transcript;手机端蜂窝环境下还要把整份 JSON
|
|
6
|
+
* 经 WS 下发并一次性渲染。两点缓解:
|
|
7
|
+
* 1. 分页窗口:应答按 [before, limit] 只下发最近的 N 条,过长历史由手机端"滚动到顶
|
|
8
|
+
* 加载更早"再拉;构建出完整列表后切片(构建仍是最重一步,缓存命中可完全跳过)。
|
|
9
|
+
* 2. 短 TTL 快照:对重复/快速回看的打开直接从缓存切片,避免重复构建。快照在流式事件
|
|
10
|
+
* (message.part / session.status 等)到达时由适配器 invalidate,保证不长期陈旧。
|
|
11
|
+
*
|
|
12
|
+
* 消息数组统一约定为**时间升序(旧 → 新)**,与手机端展示顺序一致。
|
|
13
|
+
*
|
|
14
|
+
* 3. 载荷上界:超长会话/超大单条内容可能把一帧 push 撑到 relay 的连接读上限
|
|
15
|
+
* (旧 relay 4MB)而被误判断开。sliceWindow 统一在返回前截断单条文本/参数长度,
|
|
16
|
+
* 并做**字节预算**——超预算从窗口更旧侧继续裁剪(保留最新,缺口可经 before 续拉,
|
|
17
|
+
* 不丢内容),保证任何应答帧稳定远小于连接上限。截断只影响手机端展示。
|
|
18
|
+
*/
|
|
19
|
+
import type { ChatMessage, Part } from "./types";
|
|
20
|
+
|
|
21
|
+
/** 单页默认条数(手机端首屏展示窗口)。 */
|
|
22
|
+
export const DEFAULT_PAGE_SIZE = 50;
|
|
23
|
+
|
|
24
|
+
/** 单条 part 文本(text/thinking)展示上限(字符)。 */
|
|
25
|
+
export const MAX_PART_TEXT = 16000;
|
|
26
|
+
/** 单条 tool 参数展示上限(字符)。 */
|
|
27
|
+
export const MAX_PART_ARGS = 4000;
|
|
28
|
+
/** 单页串行化字节预算:预留明显余量,确保帧远小于 relay 连接读上限(4MB/32MB)。 */
|
|
29
|
+
export const MAX_PAGE_BYTES = 1_500_000;
|
|
30
|
+
|
|
31
|
+
export interface MessagesPageQuery {
|
|
32
|
+
/** 游标:只返回早于该消息 id 的更早消息(第一页不传)。 */
|
|
33
|
+
before?: string;
|
|
34
|
+
/** 每页条数(缺省 DEFAULT_PAGE_SIZE)。 */
|
|
35
|
+
limit?: number;
|
|
36
|
+
/** 字节预算(缺省 MAX_PAGE_BYTES)。 */
|
|
37
|
+
maxBytes?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MessagesPageResult {
|
|
41
|
+
sessionID: string;
|
|
42
|
+
messages: ChatMessage[];
|
|
43
|
+
/** 更早历史是否仍存在(手机端据此决定是否继续"加载更早")。 */
|
|
44
|
+
hasMore?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function capStr(s: string, max: number): string {
|
|
48
|
+
if (s.length <= max) return s;
|
|
49
|
+
return `${s.slice(0, max)}…`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 截断单条消息内过大部件(返回新对象;未超限原样返回)。只影响手机端展示体积。 */
|
|
53
|
+
export function capMessage(m: ChatMessage): ChatMessage {
|
|
54
|
+
let parts: Part[] | undefined;
|
|
55
|
+
for (let i = 0; i < m.parts.length; i++) {
|
|
56
|
+
const p = m.parts[i];
|
|
57
|
+
const nText =
|
|
58
|
+
typeof p.text === "string" && p.text.length > MAX_PART_TEXT
|
|
59
|
+
? capStr(p.text, MAX_PART_TEXT)
|
|
60
|
+
: p.text;
|
|
61
|
+
const nDelta =
|
|
62
|
+
typeof p.delta === "string" && p.delta.length > MAX_PART_TEXT
|
|
63
|
+
? capStr(p.delta, MAX_PART_TEXT)
|
|
64
|
+
: p.delta;
|
|
65
|
+
const nArgs =
|
|
66
|
+
typeof p.args === "string" && p.args.length > MAX_PART_ARGS
|
|
67
|
+
? capStr(p.args, MAX_PART_ARGS)
|
|
68
|
+
: p.args;
|
|
69
|
+
if (nText !== p.text || nDelta !== p.delta || nArgs !== p.args) {
|
|
70
|
+
if (!parts) parts = m.parts.slice(0, i);
|
|
71
|
+
parts.push({ ...p, text: nText, delta: nDelta, args: nArgs });
|
|
72
|
+
} else if (parts) {
|
|
73
|
+
parts.push(p);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return parts ? { ...m, parts } : m;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 整批截断(仅对确有超限的条目重建,避免无谓分配)。 */
|
|
80
|
+
export function capMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
81
|
+
let changed = false;
|
|
82
|
+
const out = messages.map((m) => {
|
|
83
|
+
const c = capMessage(m);
|
|
84
|
+
if (c !== m) changed = true;
|
|
85
|
+
return c;
|
|
86
|
+
});
|
|
87
|
+
return changed ? out : messages;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function byteLength(messages: ChatMessage[]): number {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(messages).length;
|
|
93
|
+
} catch {
|
|
94
|
+
// 循环引用等异常兜底:按上限放大估算,防止误超
|
|
95
|
+
return messages.length * MAX_PART_TEXT * 2;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 在完整(升序)消息列表上切出目标窗口:截断超大内容 + 字节预算裁剪最旧侧。 */
|
|
100
|
+
export function sliceWindow(
|
|
101
|
+
full: ChatMessage[],
|
|
102
|
+
query: MessagesPageQuery = {},
|
|
103
|
+
): Pick<MessagesPageResult, "messages" | "hasMore"> {
|
|
104
|
+
const limit = Math.max(1, query.limit ?? DEFAULT_PAGE_SIZE);
|
|
105
|
+
const maxBytes = query.maxBytes ?? MAX_PAGE_BYTES;
|
|
106
|
+
let end = full.length;
|
|
107
|
+
if (query.before !== undefined && query.before !== "") {
|
|
108
|
+
const idx = full.findIndex((m) => m.id === query.before);
|
|
109
|
+
// 游标消息本身不含在内;找不到(已被清理/不一致)时回退整段,避免丢消息。
|
|
110
|
+
if (idx >= 0) end = idx;
|
|
111
|
+
}
|
|
112
|
+
let start = Math.max(0, end - limit);
|
|
113
|
+
let messages = full.slice(start, end);
|
|
114
|
+
// 先按条数取窗口,再在「截断后」体积上做预算:超出才从最旧侧继续裁剪
|
|
115
|
+
// (单条超大内容经 cap 已大幅缩小,避免按原始长度过度裁剪)。
|
|
116
|
+
while (messages.length > 1) {
|
|
117
|
+
const capped = capMessages(messages);
|
|
118
|
+
if (byteLength(capped) <= maxBytes) {
|
|
119
|
+
messages = capped;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
start++;
|
|
123
|
+
messages = full.slice(start, end);
|
|
124
|
+
}
|
|
125
|
+
return { messages: capMessages(messages), hasMore: start > 0 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 会话消息快照缓存:仅缓存**完整列表**(窗口切片在各请求时现算)。 */
|
|
129
|
+
export class MessagesCache {
|
|
130
|
+
private ttlMs: number;
|
|
131
|
+
private maxEntries: number;
|
|
132
|
+
private store = new Map<string, { full: ChatMessage[]; at: number }>();
|
|
133
|
+
|
|
134
|
+
constructor(ttlMs = 2000, maxEntries = 64) {
|
|
135
|
+
this.ttlMs = ttlMs;
|
|
136
|
+
this.maxEntries = maxEntries;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 命中且未过 TTL 时返回完整列表副本;否则 undefined。 */
|
|
140
|
+
get(sessionID: string, now = Date.now()): ChatMessage[] | undefined {
|
|
141
|
+
const e = this.store.get(sessionID);
|
|
142
|
+
if (!e) return undefined;
|
|
143
|
+
if (now - e.at > this.ttlMs) {
|
|
144
|
+
this.store.delete(sessionID);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return e.full;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
put(sessionID: string, full: ChatMessage[]): void {
|
|
151
|
+
if (this.store.size >= this.maxEntries && !this.store.has(sessionID)) {
|
|
152
|
+
// 简单 FIFO 淘汰最旧条目,防内存膨胀
|
|
153
|
+
const oldest = this.store.keys().next().value;
|
|
154
|
+
if (oldest !== undefined) this.store.delete(oldest);
|
|
155
|
+
}
|
|
156
|
+
this.store.set(sessionID, { full, at: Date.now() });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 会话内容变化(流式部件/状态/删除等)时调用,立即失效快照。 */
|
|
160
|
+
invalidate(sessionID: string): void {
|
|
161
|
+
this.store.delete(sessionID);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 适配器重连/整体失效(如清除全部快照)。 */
|
|
165
|
+
clear(): void {
|
|
166
|
+
this.store.clear();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 共享会话消息快照缓存(各适配器复用同一实例)。 */
|
|
171
|
+
export const messagesCache = new MessagesCache();
|
package/src/types.ts
CHANGED
|
@@ -142,6 +142,25 @@ export interface CommandInfo {
|
|
|
142
142
|
description?: string;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* 会话消息分页拉取请求(session.messages):
|
|
147
|
+
* - 缺省(无 before):返回最近 limit 条(首屏);
|
|
148
|
+
* - before:返回早于该消息 id 的更早 limit 条(滚动到顶"加载更早")。
|
|
149
|
+
*/
|
|
150
|
+
export interface SessionMessagesQuery {
|
|
151
|
+
sessionID: string;
|
|
152
|
+
before?: string;
|
|
153
|
+
limit?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 会话消息应答(messages):hasMore 用于手机端分页续拉(游标由手机端取已加载最旧一条的 id)。 */
|
|
157
|
+
export interface SessionMessagesReply {
|
|
158
|
+
sessionID: string;
|
|
159
|
+
messages: ChatMessage[];
|
|
160
|
+
/** 更早历史是否仍存在(缺省 false)。 */
|
|
161
|
+
hasMore?: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
/** 账号体系(Phase 1):电脑被手机账号认领后 relay 下发。 */
|
|
146
165
|
export interface AgentClaimedMessage {
|
|
147
166
|
email?: string;
|