@izhimu/qq 0.3.0 → 0.3.1

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.
@@ -105,7 +105,6 @@ export const qqPlugin = {
105
105
  probeAccount: async () => {
106
106
  const status = await getStatus();
107
107
  setContextStatus({
108
- running: true,
109
108
  lastProbeAt: Date.now(),
110
109
  });
111
110
  return {
@@ -139,11 +138,11 @@ export const qqPlugin = {
139
138
  setContext(ctx);
140
139
  const { account } = ctx;
141
140
  log.info('gateway', `Starting gateway`);
142
- // Update start time
143
- setContextStatus({
144
- running: true,
145
- lastStartAt: Date.now(),
146
- });
141
+ // 检查是否已存在连接
142
+ const existingConnection = getConnection();
143
+ if (existingConnection) {
144
+ await stopAccount();
145
+ }
147
146
  // Create new connection manager
148
147
  const connection = new ConnectionManager(account);
149
148
  connection.on("event", (event) => eventListener(event));
@@ -151,12 +150,14 @@ export const qqPlugin = {
151
150
  log.info('gateway', `State: ${status.state}`);
152
151
  if (status.state === "connected") {
153
152
  setContextStatus({
153
+ linked: true,
154
154
  connected: true,
155
155
  lastConnectedAt: Date.now(),
156
156
  });
157
157
  }
158
158
  else if (status.state === "disconnected" || status.state === "failed") {
159
159
  setContextStatus({
160
+ linked: false,
160
161
  connected: false,
161
162
  lastError: status.error,
162
163
  });
@@ -165,54 +166,52 @@ export const qqPlugin = {
165
166
  connection.on("reconnecting", (info) => {
166
167
  log.info('gateway', `Reconnecting: ${info.reason}, attempt ${info.totalAttempts}`);
167
168
  setContextStatus({
169
+ linked: false,
168
170
  connected: false,
169
171
  lastError: `Reconnecting (${info.reason})`,
170
172
  reconnectAttempts: info.totalAttempts,
171
173
  });
172
174
  });
173
- await connection.start();
174
- setConnection(connection);
175
- log.info('gateway', `Started gateway`);
176
- },
177
- stopAccount: async (_ctx) => {
178
- const connection = getConnection();
179
- if (connection) {
180
- await connection.stop();
181
- clearConnection();
182
- }
183
- setContextStatus({
184
- running: false,
185
- connected: false,
186
- lastStopAt: Date.now(),
187
- });
188
- clearContext();
189
- },
190
- },
191
- heartbeat: {
192
- checkReady: async () => {
193
- const status = await getStatus();
194
- if (status.status === "ok" && status.data?.online && status.data?.good) {
175
+ try {
176
+ await connection.start();
177
+ setConnection(connection);
178
+ // Update start time
195
179
  setContextStatus({
180
+ running: true,
196
181
  linked: true,
182
+ connected: true,
183
+ lastStartAt: Date.now(),
197
184
  });
198
- return {
199
- ok: true,
200
- reason: 'ok'
201
- };
185
+ log.info('gateway', `Started gateway`);
202
186
  }
203
- else {
204
- log.warn('heartbeat', `Heartbeat failed, status: ${status.status}, data: ${status.data}`);
187
+ catch (error) {
188
+ log.error('gateway', `Failed to start gateway:`, error);
205
189
  setContextStatus({
190
+ running: false,
206
191
  linked: false,
192
+ connected: false,
193
+ lastError: error instanceof Error ? error.message : 'Failed to start gateway',
207
194
  });
208
- return {
209
- ok: false,
210
- reason: status.msg
211
- };
195
+ throw error;
212
196
  }
213
- }
197
+ },
198
+ stopAccount,
214
199
  }
215
200
  };
201
+ async function stopAccount() {
202
+ const connection = getConnection();
203
+ if (connection) {
204
+ await connection.stop();
205
+ clearConnection();
206
+ }
207
+ setContextStatus({
208
+ running: false,
209
+ linked: false,
210
+ connected: false,
211
+ lastStopAt: Date.now(),
212
+ });
213
+ clearContext();
214
+ }
216
215
  async function outboundSend(ctx) {
217
216
  const { to, text, mediaUrl, accountId, replyToId } = ctx;
218
217
  log.debug("outbound", `send called - accountId: ${accountId}, to: ${to}, mediaUrl: ${mediaUrl ?? "null"}, replyToId: ${replyToId ?? "none"}`);
@@ -7,8 +7,8 @@ import EventEmitter from 'events';
7
7
  import { Logger as log, generateEchoId, calculateBackoff, getCloseCodeMessage, } from '../utils/index.js';
8
8
  const MAX_RECONNECT_ATTEMPTS = -1;
9
9
  const REQUEST_TIMEOUT = 30000; // 30 seconds
10
- const HEARTBEAT_TIMEOUT = 60000; // 60 seconds - time without heartbeat before reconnecting
11
- const HEARTBEAT_CHECK_INTERVAL = 30000; // 30 seconds - how often to check for heartbeat timeout
10
+ const HEARTBEAT_TIMEOUT = 120000; // 120 seconds - time without heartbeat before reconnecting (increased for NapCat compatibility)
11
+ const HEARTBEAT_CHECK_INTERVAL = 60000; // 60 seconds - how often to check for heartbeat timeout
12
12
  /**
13
13
  * Connection Manager for a single NapCat account
14
14
  */
@@ -67,6 +67,11 @@ export class ConnectionManager extends EventEmitter {
67
67
  if (this.ws?.readyState === WebSocket.OPEN) {
68
68
  return;
69
69
  }
70
+ // 防御性清理:确保旧连接和监听器被清理,避免潜在的内存泄漏
71
+ if (this.ws) {
72
+ this.ws.removeAllListeners();
73
+ this.ws = null;
74
+ }
70
75
  this.setState('connecting');
71
76
  try {
72
77
  // Build WebSocket URL with access_token query parameter (NapCat OneBot 11 standard)
@@ -148,6 +153,7 @@ export class ConnectionManager extends EventEmitter {
148
153
  };
149
154
  this.emit('heartbeat', this.healthStatus);
150
155
  // Close connection and trigger immediate reconnect
156
+ this.setState('disconnected');
151
157
  this.close('Heartbeat timeout').then(() => {
152
158
  if (this.shouldReconnect) {
153
159
  // Increment total reconnect attempts
@@ -254,6 +260,12 @@ export class ConnectionManager extends EventEmitter {
254
260
  handleClose(code, reason) {
255
261
  const reasonStr = reason.toString() || getCloseCodeMessage(code);
256
262
  log.warn('connection', `Connection closed: ${code} - ${reasonStr}`);
263
+ // 停止心跳检测
264
+ this.stopHeartbeatCheck();
265
+ // 如果 ws 已经为 null,说明是主动关闭(如心跳超时),不需要再处理
266
+ if (this.ws === null) {
267
+ return;
268
+ }
257
269
  if (this.shouldReconnect && !this.isNormalClosure(code)) {
258
270
  this.scheduleReconnect();
259
271
  }
@@ -3,7 +3,7 @@
3
3
  * Stores the PluginRuntime for access in gateway handlers
4
4
  */
5
5
  import type { ChannelAccountSnapshot, ChannelGatewayContext, PluginRuntime } from "openclaw/plugin-sdk";
6
- import type { QQConfig } from "../types/index.js";
6
+ import type { QQConfig } from "../types";
7
7
  import { ConnectionManager } from "./connection.js";
8
8
  export declare function setRuntime(next: PluginRuntime): void;
9
9
  export declare function getRuntime(): PluginRuntime | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izhimu/qq",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A QQ channel plugin for OpenClaw using NapCat WebSocket",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",