@tunnelbox/core 0.1.9 → 0.1.11

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 CHANGED
@@ -1,28 +1,28 @@
1
- # @tunnelbox/core
2
-
3
- [English](README.md) · [简体中文](README.zh-CN.md)
4
-
5
- Shared core for tunnelbox, reused by the agent adapters:
6
-
7
- | Module | Description |
8
- |---|---|
9
- | `types.ts` | Unified protocol types + `envelope()` (interoperable with relay/web mirrors) |
10
- | `relay.ts` | `RelayClient`: outbound WS with exponential-backoff **silent** reconnect |
11
- | `state.ts` | State persistence (`remote-state[.type].json`, isolated per agent) + pairing files |
12
- | `env.ts` | `injectedRelayUrl()`: default relay injected at build time via esbuild `__TB_DEFAULT_RELAY__` (no URL literals in source) |
13
- | `qr.ts` | `printPairing`: terminal QR code + pairing code printing |
14
-
15
- ## Usage
16
-
17
- ```ts
18
- import { RelayClient, envelope, loadState, printPairing, injectedRelayUrl } from "@tunnelbox/core";
19
-
20
- const state = loadState("dsh"); // multiple agents: each adapter keeps its own state file
21
- // Relay resolution: explicit (TUNNELBOX_RELAY_URL / config) > injectedRelayUrl() (default injected by build --env).
22
- // Config files live in plugin/env/.env.{development,test,production}; state.relayUrl is informational only.
23
- const ws = new RelayClient();
24
- ws.connect(`${injectedRelayUrl()}/ws/agent`, { Authorization: `Bearer ${state.agentId}` }, { … });
25
- ws.send(envelope("agent.info", { name: "host", type: "dsh", version: "0.1.0" }));
26
- ```
27
-
28
- Consumers (`plugin/opencode` / `plugin/dsh-tunnelbox` / the adapters) bundle this package's TS source directly with a bundler (esbuild/vite); no separate build step is needed.
1
+ # @tunnelbox/core
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md)
4
+
5
+ Shared core for tunnelbox, reused by the agent adapters:
6
+
7
+ | Module | Description |
8
+ |---|---|
9
+ | `types.ts` | Unified protocol types + `envelope()` (interoperable with relay/web mirrors) |
10
+ | `relay.ts` | `RelayClient`: outbound WS with exponential-backoff **silent** reconnect |
11
+ | `state.ts` | State persistence (`remote-state[.type].json`, isolated per agent) + pairing files |
12
+ | `env.ts` | `injectedRelayUrl()`: default relay injected at build time via esbuild `__TB_DEFAULT_RELAY__` (no URL literals in source) |
13
+ | `qr.ts` | `printPairing`: terminal QR code + pairing code printing |
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { RelayClient, envelope, loadState, printPairing, injectedRelayUrl } from "@tunnelbox/core";
19
+
20
+ const state = loadState("dsh"); // multiple agents: each adapter keeps its own state file
21
+ // Relay resolution: explicit (TUNNELBOX_RELAY_URL / config) > injectedRelayUrl() (default injected by build --env).
22
+ // Config files live in plugin/env/.env.{development,test,production}; state.relayUrl is informational only.
23
+ const ws = new RelayClient();
24
+ ws.connect(`${injectedRelayUrl()}/ws/agent`, { Authorization: `Bearer ${state.agentId}` }, { … });
25
+ ws.send(envelope("agent.info", { name: "host", type: "dsh", version: "0.1.0" }));
26
+ ```
27
+
28
+ Consumers (`plugin/opencode` / `plugin/dsh-tunnelbox` / the adapters) bundle this package's TS source directly with a bundler (esbuild/vite); no separate build step is needed.
package/README.zh-CN.md CHANGED
@@ -1,28 +1,28 @@
1
- # @tunnelbox/core
2
-
3
- [English](README.md) · [简体中文](README.zh-CN.md)
4
-
5
- tunnelbox 共享核心,供各智能体适配器复用:
6
-
7
- | 模块 | 说明 |
8
- |---|---|
9
- | `types.ts` | 统一协议类型 + `envelope()`(与 relay/web 镜像互通) |
10
- | `relay.ts` | `RelayClient`:出站 WS + 指数退避**静默**重连 |
11
- | `state.ts` | 状态持久化(`remote-state[.type].json`,多 agent 隔离)+ 配对文件 |
12
- | `env.ts` | `injectedRelayUrl()`:构建期 esbuild `__TB_DEFAULT_RELAY__` 注入的默认中继(源码无 URL 字面量) |
13
- | `qr.ts` | `printPairing`:终端二维码 + 配对码打印 |
14
-
15
- ## 使用
16
-
17
- ```ts
18
- import { RelayClient, envelope, loadState, printPairing, injectedRelayUrl } from "@tunnelbox/core";
19
-
20
- const state = loadState("dsh"); // 多智能体:各适配器独立状态文件
21
- // 中继地址解析:显式(TUNNELBOX_RELAY_URL/配置) > injectedRelayUrl()(构建 --env 注入的默认)。
22
- // 配置文件见 plugin/env/.env.{development,test,production};state.relayUrl 仅信息用途。
23
- const ws = new RelayClient();
24
- ws.connect(`${injectedRelayUrl()}/ws/agent`, { Authorization: `Bearer ${state.agentId}` }, { … });
25
- ws.send(envelope("agent.info", { name: "host", type: "dsh", version: "0.1.0" }));
26
- ```
27
-
28
- 消费方(`plugin/opencode` / `plugin/dsh-tunnelbox` / 各适配器)通过 bundler(esbuild/vite)直接打包本包 TS 源码;无需独立构建步骤。
1
+ # @tunnelbox/core
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md)
4
+
5
+ tunnelbox 共享核心,供各智能体适配器复用:
6
+
7
+ | 模块 | 说明 |
8
+ |---|---|
9
+ | `types.ts` | 统一协议类型 + `envelope()`(与 relay/web 镜像互通) |
10
+ | `relay.ts` | `RelayClient`:出站 WS + 指数退避**静默**重连 |
11
+ | `state.ts` | 状态持久化(`remote-state[.type].json`,多 agent 隔离)+ 配对文件 |
12
+ | `env.ts` | `injectedRelayUrl()`:构建期 esbuild `__TB_DEFAULT_RELAY__` 注入的默认中继(源码无 URL 字面量) |
13
+ | `qr.ts` | `printPairing`:终端二维码 + 配对码打印 |
14
+
15
+ ## 使用
16
+
17
+ ```ts
18
+ import { RelayClient, envelope, loadState, printPairing, injectedRelayUrl } from "@tunnelbox/core";
19
+
20
+ const state = loadState("dsh"); // 多智能体:各适配器独立状态文件
21
+ // 中继地址解析:显式(TUNNELBOX_RELAY_URL/配置) > injectedRelayUrl()(构建 --env 注入的默认)。
22
+ // 配置文件见 plugin/env/.env.{development,test,production};state.relayUrl 仅信息用途。
23
+ const ws = new RelayClient();
24
+ ws.connect(`${injectedRelayUrl()}/ws/agent`, { Authorization: `Bearer ${state.agentId}` }, { … });
25
+ ws.send(envelope("agent.info", { name: "host", type: "dsh", version: "0.1.0" }));
26
+ ```
27
+
28
+ 消费方(`plugin/opencode` / `plugin/dsh-tunnelbox` / 各适配器)通过 bundler(esbuild/vite)直接打包本包 TS 源码;无需独立构建步骤。
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ function envelope(type, payload, id) {
8
8
  // src/relay.ts
9
9
  var BASE_DELAY = 1e3;
10
10
  var MAX_DELAY = 15e3;
11
+ var KEEPALIVE_INTERVAL_MS = 15e3;
12
+ var KEEPALIVE_TIMEOUT_MS = 25e3;
11
13
  var RelayClient = class {
12
14
  ws = null;
13
15
  url = "";
@@ -17,6 +19,11 @@ var RelayClient = class {
17
19
  retry = 0;
18
20
  stopped = false;
19
21
  connected = false;
22
+ reconnectScheduled = false;
23
+ /** 保活心跳定时器(onopen 启动 / onclose 停止) */
24
+ keepaliveTimer = null;
25
+ /** 最近一次收到服务端帧的时间(含心跳回执 pong),用于静默断流检测 */
26
+ lastInbound = 0;
20
27
  get isConnected() {
21
28
  return this.connected;
22
29
  }
@@ -26,6 +33,8 @@ var RelayClient = class {
26
33
  this.handlers = handlers;
27
34
  this.stopped = false;
28
35
  this.retry = 0;
36
+ this.reconnectScheduled = false;
37
+ this.stopKeepalive();
29
38
  this.open();
30
39
  }
31
40
  send(env) {
@@ -41,6 +50,8 @@ var RelayClient = class {
41
50
  }
42
51
  close() {
43
52
  this.stopped = true;
53
+ this.reconnectScheduled = false;
54
+ this.stopKeepalive();
44
55
  if (this.timer) clearTimeout(this.timer);
45
56
  this.timer = null;
46
57
  if (this.ws) {
@@ -60,11 +71,14 @@ var RelayClient = class {
60
71
  ws.onopen = () => {
61
72
  this.connected = true;
62
73
  this.retry = 0;
74
+ this.startKeepalive();
63
75
  this.handlers?.onOpen();
64
76
  };
65
77
  ws.onmessage = (ev) => {
78
+ this.lastInbound = Date.now();
66
79
  try {
67
80
  const msg = JSON.parse(String(ev.data));
81
+ if (msg.type === "pong") return;
68
82
  this.handlers?.onMessage(msg);
69
83
  } catch {
70
84
  }
@@ -73,6 +87,7 @@ var RelayClient = class {
73
87
  ws.onclose = () => {
74
88
  this.connected = false;
75
89
  this.ws = null;
90
+ this.stopKeepalive();
76
91
  this.handlers?.onClose();
77
92
  this.scheduleReconnect();
78
93
  };
@@ -82,10 +97,52 @@ var RelayClient = class {
82
97
  }
83
98
  }
84
99
  scheduleReconnect() {
85
- if (this.stopped) return;
100
+ if (this.stopped || this.reconnectScheduled) return;
101
+ this.reconnectScheduled = true;
86
102
  const delay = Math.min(BASE_DELAY * 2 ** this.retry, MAX_DELAY);
87
103
  this.retry++;
88
- this.timer = setTimeout(() => this.open(), delay);
104
+ this.timer = setTimeout(() => {
105
+ this.reconnectScheduled = false;
106
+ this.open();
107
+ }, delay);
108
+ }
109
+ /** 启动保活心跳:每周期发 ping,长时间收不到任何帧则判定断流主动重连。 */
110
+ startKeepalive() {
111
+ this.stopKeepalive();
112
+ this.lastInbound = Date.now();
113
+ this.keepaliveTimer = setInterval(() => {
114
+ if (!this.connected || !this.ws) return;
115
+ const now = Date.now();
116
+ if (now - this.lastInbound > KEEPALIVE_TIMEOUT_MS) {
117
+ this.forceReconnect();
118
+ return;
119
+ }
120
+ try {
121
+ this.ws.send(JSON.stringify({ type: "ping", payload: {}, ts: now }));
122
+ } catch {
123
+ }
124
+ }, KEEPALIVE_INTERVAL_MS);
125
+ }
126
+ stopKeepalive() {
127
+ if (this.keepaliveTimer !== null) {
128
+ clearInterval(this.keepaliveTimer);
129
+ this.keepaliveTimer = null;
130
+ }
131
+ }
132
+ /** 判定连接已死:关闭当前 socket 并调度重连(沿用指数退避,onclose 去重)。 */
133
+ forceReconnect() {
134
+ if (this.stopped) return;
135
+ this.connected = false;
136
+ this.stopKeepalive();
137
+ const w = this.ws;
138
+ this.ws = null;
139
+ if (w) {
140
+ try {
141
+ w.close();
142
+ } catch {
143
+ }
144
+ }
145
+ this.scheduleReconnect();
89
146
  }
90
147
  };
91
148
 
@@ -112,6 +169,7 @@ var zh_CN_default = {
112
169
  "agent.log.pendingResent": "session.pending: \u8865\u53D1 {session} \u79EF\u538B\u8BF7\u6C42 {count} \u6761",
113
170
  "agent.log.unhandledType": "\u672A\u5904\u7406\u7684\u6D88\u606F\u7C7B\u578B: {type}",
114
171
  "events.log.fwdPermission": "\u8F6C\u53D1\u6743\u9650\u8BF7\u6C42\u5230\u624B\u673A (id={id} tool={tool} {version})",
172
+ "events.log.sessionDeleted": "\u8F6C\u53D1 session.deleted \u5230\u624B\u673A (session={session})",
115
173
  "events.log.permissionReplied": "opencode \u56DE\u6267 permission.replied (id={id} {response})",
116
174
  "events.log.questionRejected": "opencode \u56DE\u6267 question.rejected (id={id})",
117
175
  "events.log.questionReplied": "opencode \u56DE\u6267 question.replied (id={id})",
@@ -279,6 +337,7 @@ var zh_TW_default = {
279
337
  "agent.log.pendingResent": "session.pending: \u88DC\u767C {session} \u7A4D\u58D3\u8ACB\u6C42 {count} \u689D",
280
338
  "agent.log.unhandledType": "\u672A\u8655\u7406\u7684\u8A0A\u606F\u985E\u578B: {type}",
281
339
  "events.log.fwdPermission": "\u8F49\u767C\u6B0A\u9650\u8ACB\u6C42\u5230\u624B\u6A5F (id={id} tool={tool} {version})",
340
+ "events.log.sessionDeleted": "\u8F49\u767C session.deleted \u5230\u624B\u6A5F (session={session})",
282
341
  "events.log.permissionReplied": "opencode \u56DE\u57F7 permission.replied (id={id} {response})",
283
342
  "events.log.questionRejected": "opencode \u56DE\u57F7 question.rejected (id={id})",
284
343
  "events.log.questionReplied": "opencode \u56DE\u57F7 question.replied (id={id})",
@@ -446,6 +505,7 @@ var en_US_default = {
446
505
  "agent.log.pendingResent": "session.pending: resent {count} pending request(s) for {session}",
447
506
  "agent.log.unhandledType": "Unhandled message type: {type}",
448
507
  "events.log.fwdPermission": "Forwarded permission request to phone (id={id} tool={tool} {version})",
508
+ "events.log.sessionDeleted": "Forwarded session.deleted to phone (session={session})",
449
509
  "events.log.permissionReplied": "opencode acked permission.replied (id={id} {response})",
450
510
  "events.log.questionRejected": "opencode acked question.rejected (id={id})",
451
511
  "events.log.questionReplied": "opencode acked question.replied (id={id})",
@@ -613,6 +673,7 @@ var ja_JP_default = {
613
673
  "agent.log.pendingResent": "session.pending: {session} \u306E\u4FDD\u7559\u30EA\u30AF\u30A8\u30B9\u30C8 {count} \u4EF6\u3092\u518D\u9001",
614
674
  "agent.log.unhandledType": "\u672A\u51E6\u7406\u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u30BF\u30A4\u30D7: {type}",
615
675
  "events.log.fwdPermission": "\u6A29\u9650\u30EA\u30AF\u30A8\u30B9\u30C8\u3092\u643A\u5E2F\u3078\u8EE2\u9001 (id={id} tool={tool} {version})",
676
+ "events.log.sessionDeleted": "session.deleted \u3092\u643A\u5E2F\u3078\u8EE2\u9001 (session={session})",
616
677
  "events.log.permissionReplied": "opencode \u5FDC\u7B54 permission.replied (id={id} {response})",
617
678
  "events.log.questionRejected": "opencode \u5FDC\u7B54 question.rejected (id={id})",
618
679
  "events.log.questionReplied": "opencode \u5FDC\u7B54 question.replied (id={id})",
@@ -780,6 +841,7 @@ var ko_KR_default = {
780
841
  "agent.log.pendingResent": "session.pending: {session} \uBCF4\uB958 \uC694\uCCAD {count}\uAC74 \uC7AC\uC804\uC1A1",
781
842
  "agent.log.unhandledType": "\uCC98\uB9AC\uB418\uC9C0 \uC54A\uC740 \uBA54\uC2DC\uC9C0 \uC720\uD615: {type}",
782
843
  "events.log.fwdPermission": "\uAD8C\uD55C \uC694\uCCAD\uC744 \uD734\uB300\uD3F0\uC73C\uB85C \uC804\uB2EC (id={id} tool={tool} {version})",
844
+ "events.log.sessionDeleted": "session.deleted\uB97C \uD734\uB300\uD3F0\uC73C\uB85C \uC804\uB2EC (session={session})",
783
845
  "events.log.permissionReplied": "opencode \uC751\uB2F5 permission.replied (id={id} {response})",
784
846
  "events.log.questionRejected": "opencode \uC751\uB2F5 question.rejected (id={id})",
785
847
  "events.log.questionReplied": "opencode \uC751\uB2F5 question.replied (id={id})",
@@ -947,6 +1009,7 @@ var fr_FR_default = {
947
1009
  "agent.log.pendingResent": "session.pending : renvoi de {count} demande(s) en attente pour {session}",
948
1010
  "agent.log.unhandledType": "Type de message non g\xE9r\xE9 : {type}",
949
1011
  "events.log.fwdPermission": "Demande d'autorisation transmise au t\xE9l\xE9phone (id={id} tool={tool} {version})",
1012
+ "events.log.sessionDeleted": "session.deleted transmise au t\xE9l\xE9phone (session={session})",
950
1013
  "events.log.permissionReplied": "opencode a accus\xE9 permission.replied (id={id} {response})",
951
1014
  "events.log.questionRejected": "opencode a accus\xE9 question.rejected (id={id})",
952
1015
  "events.log.questionReplied": "opencode a accus\xE9 question.replied (id={id})",
@@ -1114,6 +1177,7 @@ var de_DE_default = {
1114
1177
  "agent.log.pendingResent": "session.pending: {count} ausstehende Anfrage(n) f\xFCr {session} erneut gesendet",
1115
1178
  "agent.log.unhandledType": "Nicht behandelter Nachrichtentyp: {type}",
1116
1179
  "events.log.fwdPermission": "Berechtigungsanfrage an Telefon weitergeleitet (id={id} tool={tool} {version})",
1180
+ "events.log.sessionDeleted": "session.deleted an Telefon weitergeleitet (session={session})",
1117
1181
  "events.log.permissionReplied": "opencode quittierte permission.replied (id={id} {response})",
1118
1182
  "events.log.questionRejected": "opencode quittierte question.rejected (id={id})",
1119
1183
  "events.log.questionReplied": "opencode quittierte question.replied (id={id})",
@@ -1281,6 +1345,7 @@ var es_ES_default = {
1281
1345
  "agent.log.pendingResent": "session.pending: reenviadas {count} solicitud(es) pendientes para {session}",
1282
1346
  "agent.log.unhandledType": "Tipo de mensaje no gestionado: {type}",
1283
1347
  "events.log.fwdPermission": "Solicitud de permiso reenviada al tel\xE9fono (id={id} tool={tool} {version})",
1348
+ "events.log.sessionDeleted": "session.deleted reenviada al tel\xE9fono (session={session})",
1284
1349
  "events.log.permissionReplied": "opencode confirm\xF3 permission.replied (id={id} {response})",
1285
1350
  "events.log.questionRejected": "opencode confirm\xF3 question.rejected (id={id})",
1286
1351
  "events.log.questionReplied": "opencode confirm\xF3 question.replied (id={id})",
@@ -1643,7 +1708,7 @@ async function printPairing(code, type, name, lang) {
1643
1708
  }
1644
1709
 
1645
1710
  // src/messages.ts
1646
- var DEFAULT_PAGE_SIZE = 50;
1711
+ var DEFAULT_PAGE_SIZE = 20;
1647
1712
  var MAX_PART_TEXT = 16e3;
1648
1713
  var MAX_PART_ARGS = 4e3;
1649
1714
  var MAX_PAGE_BYTES = 15e5;