cortico-world-astrbot 1.0.2 → 1.0.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/CHANGELOG.md +9 -0
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/driver.ts +65 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.4
|
|
4
|
+
|
|
5
|
+
- README 修正协议说明:`capabilities.images` 只在 `hello` 帧里自报,`ready` 帧不带;并说明它只在重连时刷新。
|
|
6
|
+
- 补上实机验证环境(Cortico 0.2.8 / Coopanion 0.2.12)。
|
|
7
|
+
|
|
8
|
+
## 1.0.3
|
|
9
|
+
|
|
10
|
+
- 加心跳与静默检测:休眠唤醒、切换网络、对端进程消失时,socket 可能「开着但永远没有下文」,而 `close` 事件不会来。现在每 20 秒 ping 一次,60 秒没有任何往来就主动重建,不再静默丢消息。
|
|
11
|
+
|
|
3
12
|
## 1.0.2
|
|
4
13
|
|
|
5
14
|
- 桥的 `error` 事件只认现役 socket:重启时旧连接迟到的错误不再被当成新连接的错误记一条。
|
package/README.md
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
让 Cortico 的人格 Bot 进 QQ 群说话,同时把 QQ 账号与插件能力留在 AstrBot 手里。
|
|
6
6
|
|
|
7
|
-
**当前版本:1.0.
|
|
7
|
+
**当前版本:1.0.4**(更新内容见 [CHANGELOG.md](CHANGELOG.md))
|
|
8
8
|
|
|
9
9
|
项目地址:[as124da1231/cortico-world-astrbot](https://github.com/as124da1231/cortico-world-astrbot)。
|
|
10
10
|
|
|
11
|
+
已在 **Cortico 0.2.8 / Coopanion 0.2.12** 上实机验证:文字、图片(单图与多图)、撤回/戳一戳通知、断线重连。
|
|
12
|
+
|
|
11
13
|
## 为什么是这样一个形状
|
|
12
14
|
|
|
13
15
|
Cortico 自带的 `qq` World 能直接连 OneBot 协议端,但它只做收发消息。如果你的 QQ 号上已经有一整套 AstrBot 插件(画图、点歌、战绩查询、媒体解析……),换掉那套生态的代价很高。
|
|
@@ -96,12 +98,12 @@ corepack pnpm add cortico-world-astrbot
|
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
`capabilities.images` 表示本端模型能直接读图;为真时桥才附原图,否则自己转成文字。
|
|
101
|
+
这个值由 Cortico 侧按当前端点的多模态能力算出来,只在**每次重连握手时**刷新——中途改了端点设置要等重连(或重启进程)才生效。
|
|
99
102
|
|
|
100
103
|
桥推给客户端:
|
|
101
104
|
|
|
102
105
|
```json
|
|
103
|
-
{ "t": "ready", "v": 1, "self": { "id": "…", "name": "…" },
|
|
104
|
-
"admins": ["42"], "capabilities": { "images": false } }
|
|
106
|
+
{ "t": "ready", "v": 1, "self": { "id": "…", "name": "…" }, "admins": ["42"] }
|
|
105
107
|
|
|
106
108
|
{ "t": "msg", "id": "消息id", "ts": "2026-10-26T10:00:00+08:00",
|
|
107
109
|
"conv": { "kind": "group", "id": "123456", "name": "群名" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-astrbot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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)
|
|
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;
|