cortico-world-astrbot 1.0.2 → 1.0.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.3
4
+
5
+ - 加心跳与静默检测:休眠唤醒、切换网络、对端进程消失时,socket 可能「开着但永远没有下文」,而 `close` 事件不会来。现在每 20 秒 ping 一次,60 秒没有任何往来就主动重建,不再静默丢消息。
6
+
3
7
  ## 1.0.2
4
8
 
5
9
  - 桥的 `error` 事件只认现役 socket:重启时旧连接迟到的错误不再被当成新连接的错误记一条。
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  让 Cortico 的人格 Bot 进 QQ 群说话,同时把 QQ 账号与插件能力留在 AstrBot 手里。
6
6
 
7
- **当前版本:1.0.2**(更新内容见 [CHANGELOG.md](CHANGELOG.md))
7
+ **当前版本:1.0.3**(更新内容见 [CHANGELOG.md](CHANGELOG.md))
8
8
 
9
9
  项目地址:[as124da1231/cortico-world-astrbot](https://github.com/as124da1231/cortico-world-astrbot)。
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-astrbot",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "Cortico World: join QQ conversations through an AstrBot bridge, so the bot keeps AstrBot's plugins while a Cortico persona drives the talking",
6
6
  "license": "MIT",
package/src/driver.ts CHANGED
@@ -94,6 +94,10 @@ export interface BridgeDriverOptions {
94
94
  reconnectMaxMs?: number;
95
95
  /** 一次发送等确认的超时。 */
96
96
  sendTimeoutMs?: number;
97
+ /** 心跳间隔;0 表示不做活性检测。 */
98
+ heartbeatMs?: number;
99
+ /** 超过这么久没有任何往来就重建连接(休眠唤醒后的死连接靠它发现)。 */
100
+ idleTimeoutMs?: number;
97
101
  log?: Logger;
98
102
  }
99
103
 
@@ -105,6 +109,12 @@ interface PendingSend {
105
109
  /** 发送确认的默认超时。 */
106
110
  export const DEFAULT_SEND_TIMEOUT_MS = 15_000;
107
111
 
112
+ /** 心跳间隔:主动 ping 一次,顺便刷新活跃时间。 */
113
+ export const DEFAULT_HEARTBEAT_MS = 20_000;
114
+
115
+ /** 多久没有任何往来就认定连接已死。桥每 20 秒 ping 一次,留三倍余量。 */
116
+ export const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
117
+
108
118
  export class AstrbotBridgeDriver implements BridgeDriver {
109
119
  private readonly opts: BridgeDriverOptions;
110
120
  private readonly log: Logger;
@@ -112,6 +122,8 @@ export class AstrbotBridgeDriver implements BridgeDriver {
112
122
  private stopped = true;
113
123
  private backoffMs: number;
114
124
  private reconnectTimer?: ReturnType<typeof setTimeout>;
125
+ private heartbeat?: ReturnType<typeof setInterval>;
126
+ private lastActivity = 0;
115
127
  private ridSeq = 0;
116
128
  private pending = new Map<string, PendingSend>();
117
129
  private messageCbs: Array<(m: BridgeMessage) => void> = [];
@@ -167,6 +179,7 @@ export class AstrbotBridgeDriver implements BridgeDriver {
167
179
 
168
180
  async stop(): Promise<void> {
169
181
  this.stopped = true;
182
+ this.stopHeartbeat();
170
183
  if (this.reconnectTimer) {
171
184
  clearTimeout(this.reconnectTimer);
172
185
  this.reconnectTimer = undefined;
@@ -273,11 +286,24 @@ export class AstrbotBridgeDriver implements BridgeDriver {
273
286
  });
274
287
  }
275
288
  this.emitState(true);
289
+ this.startHeartbeat(ws);
276
290
  settle();
277
291
  });
278
292
 
279
293
  ws.on('message', (data) => {
280
- if (!this.stopped && this.ws === ws) this.handleFrame(String(data));
294
+ if (!this.stopped && this.ws === ws) {
295
+ this.lastActivity = Date.now();
296
+ this.handleFrame(String(data));
297
+ }
298
+ });
299
+
300
+ // 桥每 20 秒 ping 一次;收到任何一帧都算「还活着」。
301
+ ws.on('ping', () => {
302
+ if (this.ws === ws) this.lastActivity = Date.now();
303
+ });
304
+
305
+ ws.on('pong', () => {
306
+ if (this.ws === ws) this.lastActivity = Date.now();
281
307
  });
282
308
 
283
309
  ws.on('error', (err) => {
@@ -290,6 +316,7 @@ export class AstrbotBridgeDriver implements BridgeDriver {
290
316
  const isCurrent = this.ws === ws;
291
317
  const wasConnected = this.connected;
292
318
  if (isCurrent) {
319
+ this.stopHeartbeat();
293
320
  this.ws = undefined;
294
321
  this.connected = false;
295
322
  this.rejectAllPending('与桥的连接已断开');
@@ -303,6 +330,43 @@ export class AstrbotBridgeDriver implements BridgeDriver {
303
330
  });
304
331
  }
305
332
 
333
+ /**
334
+ * 连接死掉时 socket 不一定给我们 close:机器休眠、网络切换、对端进程消失都可能留下
335
+ * 一条「开着但永远没有下文」的连接。定期 ping,超过 idleTimeoutMs 没有任何往来就主动
336
+ * terminate,让 close 走正常重连。
337
+ */
338
+ private startHeartbeat(ws: WebSocket): void {
339
+ this.stopHeartbeat();
340
+ const every = this.opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
341
+ if (every <= 0) return;
342
+ const idleLimit = this.opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
343
+ this.lastActivity = Date.now();
344
+ this.heartbeat = setInterval(() => {
345
+ if (this.ws !== ws) return;
346
+ const idle = Date.now() - this.lastActivity;
347
+ if (idle > idleLimit) {
348
+ this.log.warn('桥连接长时间没有任何动静,重建连接', {
349
+ idleMs: idle,
350
+ idleLimitMs: idleLimit,
351
+ });
352
+ ws.terminate();
353
+ return;
354
+ }
355
+ try {
356
+ ws.ping();
357
+ } catch {
358
+ // 发不出去就等 close 接手,这里不再多记一条。
359
+ }
360
+ }, every);
361
+ }
362
+
363
+ private stopHeartbeat(): void {
364
+ if (this.heartbeat) {
365
+ clearInterval(this.heartbeat);
366
+ this.heartbeat = undefined;
367
+ }
368
+ }
369
+
306
370
  private scheduleReconnect(): void {
307
371
  if (this.reconnectTimer || this.stopped) return;
308
372
  const delay = this.backoffMs;