@tunnelbox/core 0.1.7 → 0.1.9

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 CHANGED
@@ -105,6 +105,9 @@ var zh_CN_default = {
105
105
  "agent.log.gotPermReply": "\u6536\u5230\u624B\u673A\u6743\u9650\u56DE\u590D (id={id} {reply} {version})",
106
106
  "agent.log.gotQuestionReply": "\u6536\u5230\u624B\u673A\u63D0\u95EE\u56DE\u590D (id={id} {status} {version})",
107
107
  "agent.log.handleFailed": "\u5904\u7406 {type} \u5931\u8D25: {message}",
108
+ "agent.log.msgOk": "\u62C9\u53D6\u4F1A\u8BDD\u6D88\u606F\u6210\u529F session={session} data={count} \u65B9\u5F0F={limited} win={win} ms={ms}",
109
+ "agent.log.msgFailed": "\u62C9\u53D6\u4F1A\u8BDD\u6D88\u606F\u5931\u8D25 session={session} err={err} ms={ms}",
110
+ "agent.log.msgLimitFallback": "\u5F53\u524D opencode \u4E0D\u652F\u6301 limit\uFF0C\u56DE\u9000\u5168\u91CF\u62C9\u53D6\uFF08session={session}\uFF09",
108
111
  "agent.log.httpOk": "opencode HTTP {status} {path}",
109
112
  "agent.log.pendingResent": "session.pending: \u8865\u53D1 {session} \u79EF\u538B\u8BF7\u6C42 {count} \u6761",
110
113
  "agent.log.unhandledType": "\u672A\u5904\u7406\u7684\u6D88\u606F\u7C7B\u578B: {type}",
@@ -116,6 +119,7 @@ var zh_CN_default = {
116
119
  "index.error.pairTimeout": "\u7B49\u5F85\u914D\u5BF9\u7801\u8D85\u65F6\uFF08\u4E2D\u7EE7\u672A\u54CD\u5E94\uFF09",
117
120
  "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
121
  "index.log.connectedRelay": "\u5DF2\u8FDE\u63A5\u4E2D\u7EE7 {url}",
122
+ "index.log.loaded": "tunnelbox opencode \u63D2\u4EF6\u5DF2\u542F\u52A8\uFF08v{version}\uFF09",
119
123
  "index.log.disconnected": "\u4E0E\u4E2D\u7EE7\u65AD\u5F00\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE\u2026",
120
124
  "index.log.exit": "\u5DF2\u9000\u51FA",
121
125
  "index.log.pairCode": "\u751F\u6210\u914D\u5BF9\u7801: {code}",
@@ -268,6 +272,9 @@ var zh_TW_default = {
268
272
  "agent.log.gotPermReply": "\u6536\u5230\u624B\u6A5F\u6B0A\u9650\u56DE\u8986 (id={id} {reply} {version})",
269
273
  "agent.log.gotQuestionReply": "\u6536\u5230\u624B\u6A5F\u63D0\u554F\u56DE\u8986 (id={id} {status} {version})",
270
274
  "agent.log.handleFailed": "\u8655\u7406 {type} \u5931\u6557: {message}",
275
+ "agent.log.msgOk": "\u53D6\u5F97\u6703\u8A71\u8A0A\u606F\u6210\u529F session={session} data={count} \u65B9\u5F0F={limited} win={win} ms={ms}",
276
+ "agent.log.msgFailed": "\u53D6\u5F97\u6703\u8A71\u8A0A\u606F\u5931\u6557 session={session} err={err} ms={ms}",
277
+ "agent.log.msgLimitFallback": "\u76EE\u524D\u7684 opencode \u4E0D\u652F\u63F4 limit\uFF0C\u6539\u70BA\u5168\u91CF\u53D6\u5F97\uFF08session={session}\uFF09",
271
278
  "agent.log.httpOk": "opencode HTTP {status} {path}",
272
279
  "agent.log.pendingResent": "session.pending: \u88DC\u767C {session} \u7A4D\u58D3\u8ACB\u6C42 {count} \u689D",
273
280
  "agent.log.unhandledType": "\u672A\u8655\u7406\u7684\u8A0A\u606F\u985E\u578B: {type}",
@@ -279,6 +286,7 @@ var zh_TW_default = {
279
286
  "index.error.pairTimeout": "\u7B49\u5F85\u914D\u5C0D\u78BC\u903E\u6642\uFF08\u4E2D\u7E7C\u672A\u56DE\u61C9\uFF09",
280
287
  "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
288
  "index.log.connectedRelay": "\u5DF2\u9023\u63A5\u4E2D\u7E7C {url}",
289
+ "index.log.loaded": "tunnelbox opencode \u5916\u639B\u5DF2\u555F\u52D5\uFF08v{version}\uFF09",
282
290
  "index.log.disconnected": "\u8207\u4E2D\u7E7C\u65B7\u7DDA\uFF0C\u6B63\u5728\u81EA\u52D5\u91CD\u9023\u2026",
283
291
  "index.log.exit": "\u5DF2\u9000\u51FA",
284
292
  "index.log.pairCode": "\u7522\u751F\u914D\u5C0D\u78BC: {code}",
@@ -431,6 +439,9 @@ var en_US_default = {
431
439
  "agent.log.gotPermReply": "Received permission reply from phone (id={id} {reply} {version})",
432
440
  "agent.log.gotQuestionReply": "Received question reply from phone (id={id} {status} {version})",
433
441
  "agent.log.handleFailed": "Failed to handle {type}: {message}",
442
+ "agent.log.msgOk": "session.messages OK session={session} data={count} limited={limited} win={win} ms={ms}",
443
+ "agent.log.msgFailed": "session.messages FAILED session={session} err={err} ms={ms}",
444
+ "agent.log.msgLimitFallback": "limit unsupported, fell back to full session fetch (session={session})",
434
445
  "agent.log.httpOk": "opencode HTTP {status} {path}",
435
446
  "agent.log.pendingResent": "session.pending: resent {count} pending request(s) for {session}",
436
447
  "agent.log.unhandledType": "Unhandled message type: {type}",
@@ -442,6 +453,7 @@ var en_US_default = {
442
453
  "index.error.pairTimeout": "Timed out waiting for a pairing code (the relay did not respond)",
443
454
  "index.log.claimed": "Bound to your phone account. From now on the phone connects directly from My Computers; no need to scan again",
444
455
  "index.log.connectedRelay": "Connected to relay {url}",
456
+ "index.log.loaded": "tunnelbox opencode plugin started (v{version})",
445
457
  "index.log.disconnected": "Disconnected from relay, reconnecting automatically\u2026",
446
458
  "index.log.exit": "Exited",
447
459
  "index.log.pairCode": "Generated pairing code: {code}",
@@ -594,6 +606,9 @@ var ja_JP_default = {
594
606
  "agent.log.gotPermReply": "\u643A\u5E2F\u304B\u3089\u6A29\u9650\u306E\u8FD4\u4FE1\u3092\u53D7\u4FE1 (id={id} {reply} {version})",
595
607
  "agent.log.gotQuestionReply": "\u643A\u5E2F\u304B\u3089\u8CEA\u554F\u306E\u8FD4\u4FE1\u3092\u53D7\u4FE1 (id={id} {status} {version})",
596
608
  "agent.log.handleFailed": "{type} \u306E\u51E6\u7406\u306B\u5931\u6557: {message}",
609
+ "agent.log.msgOk": "\u30BB\u30C3\u30B7\u30E7\u30F3\u30E1\u30C3\u30BB\u30FC\u30B8\u53D6\u5F97\u6210\u529F session={session} data={count} \u65B9\u5F0F={limited} win={win} ms={ms}",
610
+ "agent.log.msgFailed": "\u30BB\u30C3\u30B7\u30E7\u30F3\u30E1\u30C3\u30BB\u30FC\u30B8\u53D6\u5F97\u5931\u6557 session={session} err={err} ms={ms}",
611
+ "agent.log.msgLimitFallback": "\u3053\u306E opencode \u306F limit \u975E\u5BFE\u5FDC\u306E\u305F\u3081\u5168\u91CF\u53D6\u5F97\u306B\u30D5\u30A9\u30FC\u30EB\u30D0\u30C3\u30AF\uFF08session={session}\uFF09",
597
612
  "agent.log.httpOk": "opencode HTTP {status} {path}",
598
613
  "agent.log.pendingResent": "session.pending: {session} \u306E\u4FDD\u7559\u30EA\u30AF\u30A8\u30B9\u30C8 {count} \u4EF6\u3092\u518D\u9001",
599
614
  "agent.log.unhandledType": "\u672A\u51E6\u7406\u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u30BF\u30A4\u30D7: {type}",
@@ -605,6 +620,7 @@ var ja_JP_default = {
605
620
  "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
621
  "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
622
  "index.log.connectedRelay": "\u30EA\u30EC\u30FC\u306B\u63A5\u7D9A\u3057\u307E\u3057\u305F {url}",
623
+ "index.log.loaded": "tunnelbox opencode \u30D7\u30E9\u30B0\u30A4\u30F3\u3092\u8D77\u52D5\u3057\u307E\u3057\u305F\uFF08v{version}\uFF09",
608
624
  "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
625
  "index.log.exit": "\u7D42\u4E86\u3057\u307E\u3057\u305F",
610
626
  "index.log.pairCode": "\u30DA\u30A2\u30EA\u30F3\u30B0\u30B3\u30FC\u30C9\u3092\u751F\u6210: {code}",
@@ -757,6 +773,9 @@ var ko_KR_default = {
757
773
  "agent.log.gotPermReply": "\uD734\uB300\uD3F0\uC5D0\uC11C \uAD8C\uD55C \uC751\uB2F5 \uC218\uC2E0 (id={id} {reply} {version})",
758
774
  "agent.log.gotQuestionReply": "\uD734\uB300\uD3F0\uC5D0\uC11C \uC9C8\uBB38 \uC751\uB2F5 \uC218\uC2E0 (id={id} {status} {version})",
759
775
  "agent.log.handleFailed": "{type} \uCC98\uB9AC \uC2E4\uD328: {message}",
776
+ "agent.log.msgOk": "\uC138\uC158 \uBA54\uC2DC\uC9C0 \uC870\uD68C \uC131\uACF5 session={session} data={count} \uBC29\uC2DD={limited} win={win} ms={ms}",
777
+ "agent.log.msgFailed": "\uC138\uC158 \uBA54\uC2DC\uC9C0 \uC870\uD68C \uC2E4\uD328 session={session} err={err} ms={ms}",
778
+ "agent.log.msgLimitFallback": "\uC774 opencode\uB294 limit \uBBF8\uC9C0\uC6D0 \u2192 \uC804\uCCB4 \uC870\uD68C\uB85C \uD3F4\uBC31 (session={session})",
760
779
  "agent.log.httpOk": "opencode HTTP {status} {path}",
761
780
  "agent.log.pendingResent": "session.pending: {session} \uBCF4\uB958 \uC694\uCCAD {count}\uAC74 \uC7AC\uC804\uC1A1",
762
781
  "agent.log.unhandledType": "\uCC98\uB9AC\uB418\uC9C0 \uC54A\uC740 \uBA54\uC2DC\uC9C0 \uC720\uD615: {type}",
@@ -768,6 +787,7 @@ var ko_KR_default = {
768
787
  "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
788
  "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
789
  "index.log.connectedRelay": "\uB9B4\uB808\uC774\uC5D0 \uC5F0\uACB0\uB428 {url}",
790
+ "index.log.loaded": "tunnelbox opencode \uD50C\uB7EC\uADF8\uC778\uC744 \uC2DC\uC791\uD588\uC2B5\uB2C8\uB2E4\uFF08v{version}\uFF09",
771
791
  "index.log.disconnected": "\uB9B4\uB808\uC774 \uC5F0\uACB0\uC774 \uB04A\uACA8 \uC790\uB3D9\uC73C\uB85C \uC7AC\uC5F0\uACB0 \uC911\u2026",
772
792
  "index.log.exit": "\uC885\uB8CC\uB428",
773
793
  "index.log.pairCode": "\uD398\uC5B4\uB9C1 \uCF54\uB4DC \uC0DD\uC131: {code}",
@@ -920,6 +940,9 @@ var fr_FR_default = {
920
940
  "agent.log.gotPermReply": "R\xE9ponse d'autorisation re\xE7ue du t\xE9l\xE9phone (id={id} {reply} {version})",
921
941
  "agent.log.gotQuestionReply": "R\xE9ponse de question re\xE7ue du t\xE9l\xE9phone (id={id} {status} {version})",
922
942
  "agent.log.handleFailed": "\xC9chec du traitement de {type} : {message}",
943
+ "agent.log.msgOk": "R\xE9cup\xE9ration des messages OK session={session} data={count} mode={limited} win={win} ms={ms}",
944
+ "agent.log.msgFailed": "\xC9chec de la r\xE9cup\xE9ration des messages session={session} err={err} ms={ms}",
945
+ "agent.log.msgLimitFallback": "limit non pris en charge, repli sur la r\xE9cup\xE9ration compl\xE8te (session={session})",
923
946
  "agent.log.httpOk": "opencode HTTP {status} {path}",
924
947
  "agent.log.pendingResent": "session.pending : renvoi de {count} demande(s) en attente pour {session}",
925
948
  "agent.log.unhandledType": "Type de message non g\xE9r\xE9 : {type}",
@@ -931,6 +954,7 @@ var fr_FR_default = {
931
954
  "index.error.pairTimeout": "D\xE9lai d'attente du code d'appairage d\xE9pass\xE9 (le relais ne r\xE9pond pas)",
932
955
  "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
956
  "index.log.connectedRelay": "Connect\xE9 au relais {url}",
957
+ "index.log.loaded": "Plugin tunnelbox opencode d\xE9marr\xE9 (v{version})",
934
958
  "index.log.disconnected": "D\xE9connect\xE9 du relais, reconnexion automatique\u2026",
935
959
  "index.log.exit": "Arr\xEAt\xE9",
936
960
  "index.log.pairCode": "Code d'appairage g\xE9n\xE9r\xE9 : {code}",
@@ -1083,6 +1107,9 @@ var de_DE_default = {
1083
1107
  "agent.log.gotPermReply": "Berechtigungsantwort vom Telefon erhalten (id={id} {reply} {version})",
1084
1108
  "agent.log.gotQuestionReply": "Frageantwort vom Telefon erhalten (id={id} {status} {version})",
1085
1109
  "agent.log.handleFailed": "Fehler beim Verarbeiten von {type}: {message}",
1110
+ "agent.log.msgOk": "Session-Nachrichten OK session={session} data={count} Modus={limited} win={win} ms={ms}",
1111
+ "agent.log.msgFailed": "Session-Nachrichten fehlgeschlagen session={session} err={err} ms={ms}",
1112
+ "agent.log.msgLimitFallback": "limit nicht unterst\xFCtzt, R\xFCckfall auf vollst\xE4ndigen Abruf (session={session})",
1086
1113
  "agent.log.httpOk": "opencode HTTP {status} {path}",
1087
1114
  "agent.log.pendingResent": "session.pending: {count} ausstehende Anfrage(n) f\xFCr {session} erneut gesendet",
1088
1115
  "agent.log.unhandledType": "Nicht behandelter Nachrichtentyp: {type}",
@@ -1094,6 +1121,7 @@ var de_DE_default = {
1094
1121
  "index.error.pairTimeout": "Zeit\xFCberschreitung beim Warten auf einen Pairing-Code (Relay antwortet nicht)",
1095
1122
  "index.log.claimed": "An Ihr Telefonkonto gebunden. Das Telefon verbindet sich nun direkt \xFCber \u201CMeine PCs\u201D; kein erneutes Scannen n\xF6tig",
1096
1123
  "index.log.connectedRelay": "Mit Relay verbunden {url}",
1124
+ "index.log.loaded": "tunnelbox-opencode-Plugin gestartet (v{version})",
1097
1125
  "index.log.disconnected": "Vom Relay getrennt, verbinde automatisch neu\u2026",
1098
1126
  "index.log.exit": "Beendet",
1099
1127
  "index.log.pairCode": "Pairing-Code erzeugt: {code}",
@@ -1246,6 +1274,9 @@ var es_ES_default = {
1246
1274
  "agent.log.gotPermReply": "Respuesta de permiso recibida del tel\xE9fono (id={id} {reply} {version})",
1247
1275
  "agent.log.gotQuestionReply": "Respuesta de pregunta recibida del tel\xE9fono (id={id} {status} {version})",
1248
1276
  "agent.log.handleFailed": "Error al procesar {type}: {message}",
1277
+ "agent.log.msgOk": "Mensajes de sesi\xF3n OK session={session} data={count} modo={limited} win={win} ms={ms}",
1278
+ "agent.log.msgFailed": "Error al obtener mensajes de sesi\xF3n session={session} err={err} ms={ms}",
1279
+ "agent.log.msgLimitFallback": "limit no compatible; se vuelve a la obtenci\xF3n completa (session={session})",
1249
1280
  "agent.log.httpOk": "opencode HTTP {status} {path}",
1250
1281
  "agent.log.pendingResent": "session.pending: reenviadas {count} solicitud(es) pendientes para {session}",
1251
1282
  "agent.log.unhandledType": "Tipo de mensaje no gestionado: {type}",
@@ -1257,6 +1288,7 @@ var es_ES_default = {
1257
1288
  "index.error.pairTimeout": "Tiempo de espera agotado al obtener el c\xF3digo de emparejamiento (el relay no responde)",
1258
1289
  "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
1290
  "index.log.connectedRelay": "Conectado al relay {url}",
1291
+ "index.log.loaded": "Plugin tunnelbox opencode iniciado (v{version})",
1260
1292
  "index.log.disconnected": "Desconectado del relay, reconectando autom\xE1ticamente\u2026",
1261
1293
  "index.log.exit": "Salido",
1262
1294
  "index.log.pairCode": "C\xF3digo de emparejamiento generado: {code}",
@@ -1609,20 +1641,127 @@ async function printPairing(code, type, name, lang) {
1609
1641
  console.log(" " + tt("qr.fileWritten", { path: file }));
1610
1642
  console.log("");
1611
1643
  }
1644
+
1645
+ // src/messages.ts
1646
+ var DEFAULT_PAGE_SIZE = 50;
1647
+ var MAX_PART_TEXT = 16e3;
1648
+ var MAX_PART_ARGS = 4e3;
1649
+ var MAX_PAGE_BYTES = 15e5;
1650
+ function capStr(s, max) {
1651
+ if (s.length <= max) return s;
1652
+ return `${s.slice(0, max)}\u2026`;
1653
+ }
1654
+ function capMessage(m) {
1655
+ let parts;
1656
+ for (let i = 0; i < m.parts.length; i++) {
1657
+ const p = m.parts[i];
1658
+ const nText = typeof p.text === "string" && p.text.length > MAX_PART_TEXT ? capStr(p.text, MAX_PART_TEXT) : p.text;
1659
+ const nDelta = typeof p.delta === "string" && p.delta.length > MAX_PART_TEXT ? capStr(p.delta, MAX_PART_TEXT) : p.delta;
1660
+ const nArgs = typeof p.args === "string" && p.args.length > MAX_PART_ARGS ? capStr(p.args, MAX_PART_ARGS) : p.args;
1661
+ if (nText !== p.text || nDelta !== p.delta || nArgs !== p.args) {
1662
+ if (!parts) parts = m.parts.slice(0, i);
1663
+ parts.push({ ...p, text: nText, delta: nDelta, args: nArgs });
1664
+ } else if (parts) {
1665
+ parts.push(p);
1666
+ }
1667
+ }
1668
+ return parts ? { ...m, parts } : m;
1669
+ }
1670
+ function capMessages(messages) {
1671
+ let changed = false;
1672
+ const out = messages.map((m) => {
1673
+ const c = capMessage(m);
1674
+ if (c !== m) changed = true;
1675
+ return c;
1676
+ });
1677
+ return changed ? out : messages;
1678
+ }
1679
+ function byteLength(messages) {
1680
+ try {
1681
+ return JSON.stringify(messages).length;
1682
+ } catch {
1683
+ return messages.length * MAX_PART_TEXT * 2;
1684
+ }
1685
+ }
1686
+ function sliceWindow(full, query = {}) {
1687
+ const limit = Math.max(1, query.limit ?? DEFAULT_PAGE_SIZE);
1688
+ const maxBytes = query.maxBytes ?? MAX_PAGE_BYTES;
1689
+ let end = full.length;
1690
+ if (query.before !== void 0 && query.before !== "") {
1691
+ const idx = full.findIndex((m) => m.id === query.before);
1692
+ if (idx >= 0) end = idx;
1693
+ }
1694
+ let start = Math.max(0, end - limit);
1695
+ let messages = full.slice(start, end);
1696
+ while (messages.length > 1) {
1697
+ const capped = capMessages(messages);
1698
+ if (byteLength(capped) <= maxBytes) {
1699
+ messages = capped;
1700
+ break;
1701
+ }
1702
+ start++;
1703
+ messages = full.slice(start, end);
1704
+ }
1705
+ return { messages: capMessages(messages), hasMore: start > 0 };
1706
+ }
1707
+ var MessagesCache = class {
1708
+ ttlMs;
1709
+ maxEntries;
1710
+ store = /* @__PURE__ */ new Map();
1711
+ constructor(ttlMs = 2e3, maxEntries = 64) {
1712
+ this.ttlMs = ttlMs;
1713
+ this.maxEntries = maxEntries;
1714
+ }
1715
+ /** 命中且未过 TTL 时返回完整列表副本;否则 undefined。 */
1716
+ get(sessionID, now = Date.now()) {
1717
+ const e = this.store.get(sessionID);
1718
+ if (!e) return void 0;
1719
+ if (now - e.at > this.ttlMs) {
1720
+ this.store.delete(sessionID);
1721
+ return void 0;
1722
+ }
1723
+ return e.full;
1724
+ }
1725
+ put(sessionID, full) {
1726
+ if (this.store.size >= this.maxEntries && !this.store.has(sessionID)) {
1727
+ const oldest = this.store.keys().next().value;
1728
+ if (oldest !== void 0) this.store.delete(oldest);
1729
+ }
1730
+ this.store.set(sessionID, { full, at: Date.now() });
1731
+ }
1732
+ /** 会话内容变化(流式部件/状态/删除等)时调用,立即失效快照。 */
1733
+ invalidate(sessionID) {
1734
+ this.store.delete(sessionID);
1735
+ }
1736
+ /** 适配器重连/整体失效(如清除全部快照)。 */
1737
+ clear() {
1738
+ this.store.clear();
1739
+ }
1740
+ };
1741
+ var messagesCache = new MessagesCache();
1612
1742
  export {
1613
1743
  DEFAULT_LANG,
1744
+ DEFAULT_PAGE_SIZE,
1614
1745
  LANG_TAGS,
1746
+ MAX_PAGE_BYTES,
1747
+ MAX_PART_ARGS,
1748
+ MAX_PART_TEXT,
1749
+ MessagesCache,
1615
1750
  RelayClient,
1751
+ capMessage,
1752
+ capMessages,
1616
1753
  detectSystemLang,
1617
1754
  envelope,
1618
1755
  injectedRelayUrl,
1619
1756
  loadState,
1620
1757
  makeT,
1758
+ messagesCache,
1621
1759
  normalizeLang,
1622
1760
  printPairing,
1623
1761
  renderPairingQrText,
1624
1762
  resolveLang,
1625
1763
  saveState,
1764
+ sliceWindow,
1626
1765
  statePath,
1627
1766
  t,
1628
1767
  writePairingFile,
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "Berechtigungsantwort vom Telefon erhalten (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "Frageantwort vom Telefon erhalten (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "Fehler beim Verarbeiten von {type}: {message}",
10
+ "agent.log.msgOk": "Session-Nachrichten OK session={session} data={count} Modus={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "Session-Nachrichten fehlgeschlagen session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "limit nicht unterstützt, Rückfall auf vollständigen Abruf (session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending: {count} ausstehende Anfrage(n) für {session} erneut gesendet",
12
15
  "agent.log.unhandledType": "Nicht behandelter Nachrichtentyp: {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "Zeitüberschreitung beim Warten auf einen Pairing-Code (Relay antwortet nicht)",
19
22
  "index.log.claimed": "An Ihr Telefonkonto gebunden. Das Telefon verbindet sich nun direkt über “Meine PCs”; kein erneutes Scannen nötig",
20
23
  "index.log.connectedRelay": "Mit Relay verbunden {url}",
24
+ "index.log.loaded": "tunnelbox-opencode-Plugin gestartet (v{version})",
21
25
  "index.log.disconnected": "Vom Relay getrennt, verbinde automatisch neu…",
22
26
  "index.log.exit": "Beendet",
23
27
  "index.log.pairCode": "Pairing-Code erzeugt: {code}",
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "Received permission reply from phone (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "Received question reply from phone (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "Failed to handle {type}: {message}",
10
+ "agent.log.msgOk": "session.messages OK session={session} data={count} limited={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "session.messages FAILED session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "limit unsupported, fell back to full session fetch (session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending: resent {count} pending request(s) for {session}",
12
15
  "agent.log.unhandledType": "Unhandled message type: {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "Timed out waiting for a pairing code (the relay did not respond)",
19
22
  "index.log.claimed": "Bound to your phone account. From now on the phone connects directly from My Computers; no need to scan again",
20
23
  "index.log.connectedRelay": "Connected to relay {url}",
24
+ "index.log.loaded": "tunnelbox opencode plugin started (v{version})",
21
25
  "index.log.disconnected": "Disconnected from relay, reconnecting automatically…",
22
26
  "index.log.exit": "Exited",
23
27
  "index.log.pairCode": "Generated pairing code: {code}",
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "Respuesta de permiso recibida del teléfono (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "Respuesta de pregunta recibida del teléfono (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "Error al procesar {type}: {message}",
10
+ "agent.log.msgOk": "Mensajes de sesión OK session={session} data={count} modo={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "Error al obtener mensajes de sesión session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "limit no compatible; se vuelve a la obtención completa (session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending: reenviadas {count} solicitud(es) pendientes para {session}",
12
15
  "agent.log.unhandledType": "Tipo de mensaje no gestionado: {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "Tiempo de espera agotado al obtener el código de emparejamiento (el relay no responde)",
19
22
  "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
23
  "index.log.connectedRelay": "Conectado al relay {url}",
24
+ "index.log.loaded": "Plugin tunnelbox opencode iniciado (v{version})",
21
25
  "index.log.disconnected": "Desconectado del relay, reconectando automáticamente…",
22
26
  "index.log.exit": "Salido",
23
27
  "index.log.pairCode": "Código de emparejamiento generado: {code}",
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "Réponse d'autorisation reçue du téléphone (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "Réponse de question reçue du téléphone (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "Échec du traitement de {type} : {message}",
10
+ "agent.log.msgOk": "Récupération des messages OK session={session} data={count} mode={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "Échec de la récupération des messages session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "limit non pris en charge, repli sur la récupération complète (session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending : renvoi de {count} demande(s) en attente pour {session}",
12
15
  "agent.log.unhandledType": "Type de message non géré : {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "Délai d'attente du code d'appairage dépassé (le relais ne répond pas)",
19
22
  "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
23
  "index.log.connectedRelay": "Connecté au relais {url}",
24
+ "index.log.loaded": "Plugin tunnelbox opencode démarré (v{version})",
21
25
  "index.log.disconnected": "Déconnecté du relais, reconnexion automatique…",
22
26
  "index.log.exit": "Arrêté",
23
27
  "index.log.pairCode": "Code d'appairage généré : {code}",
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "携帯から権限の返信を受信 (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "携帯から質問の返信を受信 (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "{type} の処理に失敗: {message}",
10
+ "agent.log.msgOk": "セッションメッセージ取得成功 session={session} data={count} 方式={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "セッションメッセージ取得失敗 session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "この opencode は limit 非対応のため全量取得にフォールバック(session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending: {session} の保留リクエスト {count} 件を再送",
12
15
  "agent.log.unhandledType": "未処理のメッセージタイプ: {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "ペアリングコードの取得がタイムアウトしました(リレーが応答しません)",
19
22
  "index.log.claimed": "スマホのアカウントに紐付けました。以降は「マイ PC」から直接接続されるため、再スキャンは不要です",
20
23
  "index.log.connectedRelay": "リレーに接続しました {url}",
24
+ "index.log.loaded": "tunnelbox opencode プラグインを起動しました(v{version})",
21
25
  "index.log.disconnected": "リレーから切断、自動的に再接続しています…",
22
26
  "index.log.exit": "終了しました",
23
27
  "index.log.pairCode": "ペアリングコードを生成: {code}",
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "휴대폰에서 권한 응답 수신 (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "휴대폰에서 질문 응답 수신 (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "{type} 처리 실패: {message}",
10
+ "agent.log.msgOk": "세션 메시지 조회 성공 session={session} data={count} 방식={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "세션 메시지 조회 실패 session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "이 opencode는 limit 미지원 → 전체 조회로 폴백 (session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending: {session} 보류 요청 {count}건 재전송",
12
15
  "agent.log.unhandledType": "처리되지 않은 메시지 유형: {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "페어링 코드 대기 시간 초과(릴레이가 응답하지 않음)",
19
22
  "index.log.claimed": "휴대폰 계정에 연결되었습니다. 이제 휴대폰은 「내 PC」에서 직접 연결하며 다시 스캔할 필요가 없습니다",
20
23
  "index.log.connectedRelay": "릴레이에 연결됨 {url}",
24
+ "index.log.loaded": "tunnelbox opencode 플러그인을 시작했습니다(v{version})",
21
25
  "index.log.disconnected": "릴레이 연결이 끊겨 자동으로 재연결 중…",
22
26
  "index.log.exit": "종료됨",
23
27
  "index.log.pairCode": "페어링 코드 생성: {code}",
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "收到手机权限回复 (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "收到手机提问回复 (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "处理 {type} 失败: {message}",
10
+ "agent.log.msgOk": "拉取会话消息成功 session={session} data={count} 方式={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "拉取会话消息失败 session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "当前 opencode 不支持 limit,回退全量拉取(session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending: 补发 {session} 积压请求 {count} 条",
12
15
  "agent.log.unhandledType": "未处理的消息类型: {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "等待配对码超时(中继未响应)",
19
22
  "index.log.claimed": "已绑定到手机账号,之后手机从「我的电脑」直接连接,无需再扫码",
20
23
  "index.log.connectedRelay": "已连接中继 {url}",
24
+ "index.log.loaded": "tunnelbox opencode 插件已启动(v{version})",
21
25
  "index.log.disconnected": "与中继断开,正在自动重连…",
22
26
  "index.log.exit": "已退出",
23
27
  "index.log.pairCode": "生成配对码: {code}",
@@ -7,6 +7,9 @@
7
7
  "agent.log.gotPermReply": "收到手機權限回覆 (id={id} {reply} {version})",
8
8
  "agent.log.gotQuestionReply": "收到手機提問回覆 (id={id} {status} {version})",
9
9
  "agent.log.handleFailed": "處理 {type} 失敗: {message}",
10
+ "agent.log.msgOk": "取得會話訊息成功 session={session} data={count} 方式={limited} win={win} ms={ms}",
11
+ "agent.log.msgFailed": "取得會話訊息失敗 session={session} err={err} ms={ms}",
12
+ "agent.log.msgLimitFallback": "目前的 opencode 不支援 limit,改為全量取得(session={session})",
10
13
  "agent.log.httpOk": "opencode HTTP {status} {path}",
11
14
  "agent.log.pendingResent": "session.pending: 補發 {session} 積壓請求 {count} 條",
12
15
  "agent.log.unhandledType": "未處理的訊息類型: {type}",
@@ -18,6 +21,7 @@
18
21
  "index.error.pairTimeout": "等待配對碼逾時(中繼未回應)",
19
22
  "index.log.claimed": "已綁定到手機帳號,之後手機直接從「我的電腦」連接,不需再掃碼",
20
23
  "index.log.connectedRelay": "已連接中繼 {url}",
24
+ "index.log.loaded": "tunnelbox opencode 外掛已啟動(v{version})",
21
25
  "index.log.disconnected": "與中繼斷線,正在自動重連…",
22
26
  "index.log.exit": "已退出",
23
27
  "index.log.pairCode": "產生配對碼: {code}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tunnelbox/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "tunnelbox 共享核心:协议类型、中继客户端、状态持久化、二维码配对",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -9,3 +9,4 @@ export * from "./state";
9
9
  export * from "./env";
10
10
  export * from "./qr";
11
11
  export * from "./i18n";
12
+ export * from "./messages";
@@ -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;