@izhimu/qq 0.2.3 → 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.
- package/README.md +62 -3
- package/dist/src/channel.d.ts +2 -2
- package/dist/src/channel.js +115 -41
- package/dist/src/core/config.d.ts +1 -1
- package/dist/src/core/config.js +8 -1
- package/dist/src/core/connection.d.ts +15 -3
- package/dist/src/core/connection.js +106 -16
- package/dist/src/core/dispatch.d.ts +1 -1
- package/dist/src/core/dispatch.js +32 -4
- package/dist/src/core/request.d.ts +6 -2
- package/dist/src/core/request.js +25 -2
- package/dist/src/core/runtime.d.ts +1 -1
- package/dist/src/types/index.d.ts +10 -0
- package/dist/src/utils/index.d.ts +8 -0
- package/dist/src/utils/index.js +32 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @izhimu/qq
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
+
<a href="https://github.com/izhimu/openclaw-channel-qq/releases">
|
|
5
|
+
<img src="https://img.shields.io/github/v/release/izhimu/openclaw-channel-qq?display_name=tag" alt="Release">
|
|
6
|
+
</a>
|
|
4
7
|
<a href="https://opensource.org/licenses/MIT">
|
|
5
8
|
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT">
|
|
6
9
|
</a>
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
- [API 文档](#api-文档)
|
|
37
40
|
- [开发指南](#开发指南)
|
|
38
41
|
- [故障排查](#故障排查)
|
|
42
|
+
- [更新日志](#更新日志)
|
|
39
43
|
|
|
40
44
|
---
|
|
41
45
|
|
|
@@ -44,7 +48,8 @@
|
|
|
44
48
|
- **多渠道支持** - 同时支持 QQ 私聊和群聊
|
|
45
49
|
- **消息类型** - 文本、@提及、图片、表情、语音、文件、回复
|
|
46
50
|
- **实时通信** - WebSocket 全双工连接,支持自动重连
|
|
47
|
-
- **心跳监测** -
|
|
51
|
+
- **心跳监测** - 内置健康检查、心跳超时检测和连接状态监控
|
|
52
|
+
- **媒体支持** - 支持图片、语音、文件等媒体消息的收发
|
|
48
53
|
- **交互式配置** - 提供向导式配置界面
|
|
49
54
|
- **TypeScript** - 完整的类型定义和类型安全
|
|
50
55
|
|
|
@@ -252,8 +257,8 @@ openclaw-channel-qq/
|
|
|
252
257
|
| `image` | ✓ | ✓ | 图片 |
|
|
253
258
|
| `face` | ✓ | - | QQ 表情 |
|
|
254
259
|
| `reply` | ✓ | ✓ | 消息回复 |
|
|
255
|
-
| `record` | ✓ |
|
|
256
|
-
| `file` | ✓ |
|
|
260
|
+
| `record` | ✓ | ✓ | 语音消息 |
|
|
261
|
+
| `file` | ✓ | ✓ | 文件 |
|
|
257
262
|
| `json` | ✓ | - | JSON 富文本 |
|
|
258
263
|
|
|
259
264
|
### OneBot 11 接口
|
|
@@ -364,6 +369,60 @@ npm run build
|
|
|
364
369
|
|
|
365
370
|
---
|
|
366
371
|
|
|
372
|
+
## 更新日志
|
|
373
|
+
|
|
374
|
+
### [0.3.0] - 2026-02-12
|
|
375
|
+
|
|
376
|
+
#### 新增
|
|
377
|
+
- 心跳超时检测和自动重连机制
|
|
378
|
+
- 媒体消息处理功能(图片、语音、文件等)
|
|
379
|
+
- 媒体文件发送功能
|
|
380
|
+
- 运行状态标记和探针功能
|
|
381
|
+
- 通道状态管理功能
|
|
382
|
+
|
|
383
|
+
#### 修复
|
|
384
|
+
- 修复连接状态管理逻辑
|
|
385
|
+
- 修复空消息过滤逻辑
|
|
386
|
+
|
|
387
|
+
#### 重构
|
|
388
|
+
- 重构通道状态管理逻辑
|
|
389
|
+
|
|
390
|
+
### [0.2.3] - 2026-02-09
|
|
391
|
+
|
|
392
|
+
#### 新增
|
|
393
|
+
- Markdown 文本转换功能
|
|
394
|
+
|
|
395
|
+
### [0.2.2] - 2026-02-08
|
|
396
|
+
|
|
397
|
+
#### 新增
|
|
398
|
+
- 账号启用和删除功能
|
|
399
|
+
- 消息回复功能
|
|
400
|
+
- Markdown 解析优化
|
|
401
|
+
|
|
402
|
+
#### 修复
|
|
403
|
+
- 修复多 Agent 通信消息回调问题
|
|
404
|
+
- 修复回复消息格式问题
|
|
405
|
+
- 修复字符串连接时换行符丢失问题
|
|
406
|
+
|
|
407
|
+
### [0.2.1] - 2026-02-08
|
|
408
|
+
|
|
409
|
+
#### 优化
|
|
410
|
+
- 优化 Markdown 转文本功能(移动端适配)
|
|
411
|
+
|
|
412
|
+
### [0.2.0] - 2026-02-07
|
|
413
|
+
|
|
414
|
+
#### 重构
|
|
415
|
+
- 重构 QQ 频道消息发送功能
|
|
416
|
+
|
|
417
|
+
### [0.1.1] - 2026-02-07
|
|
418
|
+
|
|
419
|
+
#### 优化
|
|
420
|
+
- 重构项目配置并更新文档
|
|
421
|
+
- 重构 QQ 频道插件的类型定义和配置管理
|
|
422
|
+
- 优化日志系统
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
367
426
|
## 相关链接
|
|
368
427
|
|
|
369
428
|
- [OpenClaw 官方文档](https://docs.openclaw.ai/)
|
package/dist/src/channel.d.ts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* QQ NapCat Plugin for OpenClaw
|
|
3
3
|
* Main plugin entry point
|
|
4
4
|
*/
|
|
5
|
-
import { ChannelPlugin } from "openclaw/plugin-sdk";
|
|
6
|
-
import type { QQConfig } from "./types
|
|
5
|
+
import type { ChannelPlugin } from "openclaw/plugin-sdk";
|
|
6
|
+
import type { QQConfig } from "./types";
|
|
7
7
|
export declare const qqPlugin: ChannelPlugin<QQConfig>;
|
package/dist/src/channel.js
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* QQ NapCat Plugin for OpenClaw
|
|
3
3
|
* Main plugin entry point
|
|
4
4
|
*/
|
|
5
|
-
import { setAccountEnabledInConfigSection, deleteAccountFromConfigSection } from "openclaw/plugin-sdk";
|
|
6
|
-
import {
|
|
7
|
-
import { messageIdToString, markdownToText, getFileType, getFileName, Logger as log } from "./utils/index.js";
|
|
5
|
+
import { buildChannelConfigSchema, setAccountEnabledInConfigSection, deleteAccountFromConfigSection, DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
6
|
+
import { messageIdToString, markdownToText, buildMediaMessage, Logger as log } from "./utils/index.js";
|
|
8
7
|
import { setContext, setContextStatus, clearContext, setConnection, getConnection, clearConnection } from "./core/runtime.js";
|
|
9
8
|
import { ConnectionManager } from "./core/connection.js";
|
|
10
9
|
import { openClawToNapCatMessage } from "./adapters/message.js";
|
|
11
10
|
import { listQQAccountIds, resolveQQAccount, QQConfigSchema, CHANNEL_ID } from "./core/config.js";
|
|
12
|
-
import { eventListener, sendMsg } from "./core/request.js";
|
|
11
|
+
import { eventListener, sendMsg, getStatus } from "./core/request.js";
|
|
13
12
|
import { qqOnboardingAdapter } from "./onboarding.js";
|
|
14
13
|
export const qqPlugin = {
|
|
15
14
|
id: CHANNEL_ID,
|
|
@@ -71,13 +70,66 @@ export const qqPlugin = {
|
|
|
71
70
|
sendMedia: outboundSend,
|
|
72
71
|
},
|
|
73
72
|
status: {
|
|
74
|
-
|
|
73
|
+
defaultRuntime: {
|
|
74
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
75
|
+
name: "QQ",
|
|
76
|
+
enabled: false,
|
|
77
|
+
configured: false,
|
|
78
|
+
linked: false,
|
|
79
|
+
running: false,
|
|
80
|
+
connected: false,
|
|
81
|
+
reconnectAttempts: 0,
|
|
82
|
+
lastConnectedAt: null,
|
|
83
|
+
lastStartAt: null,
|
|
84
|
+
lastStopAt: null,
|
|
85
|
+
lastError: null,
|
|
86
|
+
lastInboundAt: null,
|
|
87
|
+
lastOutboundAt: null,
|
|
88
|
+
},
|
|
89
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
90
|
+
enabled: snapshot.enabled ?? false,
|
|
91
|
+
configured: snapshot.configured ?? false,
|
|
92
|
+
linked: snapshot.linked ?? false,
|
|
93
|
+
running: snapshot.running ?? false,
|
|
94
|
+
connected: snapshot.connected ?? false,
|
|
95
|
+
reconnectAttempts: snapshot.reconnectAttempts ?? 0,
|
|
96
|
+
lastConnectedAt: snapshot.lastConnectedAt ?? null,
|
|
97
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
98
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
99
|
+
lastError: snapshot.lastError ?? null,
|
|
100
|
+
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
101
|
+
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
102
|
+
probe: snapshot.probe,
|
|
103
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
104
|
+
}),
|
|
105
|
+
probeAccount: async () => {
|
|
106
|
+
const status = await getStatus();
|
|
107
|
+
setContextStatus({
|
|
108
|
+
lastProbeAt: Date.now(),
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
ok: status.status === "ok",
|
|
112
|
+
status: status.retcode,
|
|
113
|
+
error: status.status === "failed" ? status.msg : null,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
|
75
117
|
return {
|
|
76
118
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
77
|
-
name:
|
|
78
|
-
enabled: account.enabled,
|
|
79
|
-
configured: Boolean(account.wsUrl),
|
|
80
|
-
|
|
119
|
+
name: "QQ",
|
|
120
|
+
enabled: account.enabled ?? false,
|
|
121
|
+
configured: Boolean(account.wsUrl?.trim()),
|
|
122
|
+
linked: runtime?.linked ?? false,
|
|
123
|
+
running: runtime?.running ?? false,
|
|
124
|
+
connected: runtime?.connected ?? false,
|
|
125
|
+
reconnectAttempts: runtime?.reconnectAttempts ?? 0,
|
|
126
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
127
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
128
|
+
lastError: runtime?.lastError ?? null,
|
|
129
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
130
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
131
|
+
probe,
|
|
132
|
+
lastProbeAt: runtime?.lastProbeAt ?? null,
|
|
81
133
|
};
|
|
82
134
|
},
|
|
83
135
|
},
|
|
@@ -86,12 +138,11 @@ export const qqPlugin = {
|
|
|
86
138
|
setContext(ctx);
|
|
87
139
|
const { account } = ctx;
|
|
88
140
|
log.info('gateway', `Starting gateway`);
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
141
|
+
// 检查是否已存在连接
|
|
142
|
+
const existingConnection = getConnection();
|
|
143
|
+
if (existingConnection) {
|
|
144
|
+
await stopAccount();
|
|
145
|
+
}
|
|
95
146
|
// Create new connection manager
|
|
96
147
|
const connection = new ConnectionManager(account);
|
|
97
148
|
connection.on("event", (event) => eventListener(event));
|
|
@@ -99,36 +150,68 @@ export const qqPlugin = {
|
|
|
99
150
|
log.info('gateway', `State: ${status.state}`);
|
|
100
151
|
if (status.state === "connected") {
|
|
101
152
|
setContextStatus({
|
|
153
|
+
linked: true,
|
|
102
154
|
connected: true,
|
|
103
155
|
lastConnectedAt: Date.now(),
|
|
104
156
|
});
|
|
105
157
|
}
|
|
106
158
|
else if (status.state === "disconnected" || status.state === "failed") {
|
|
107
159
|
setContextStatus({
|
|
160
|
+
linked: false,
|
|
108
161
|
connected: false,
|
|
109
162
|
lastError: status.error,
|
|
110
163
|
});
|
|
111
164
|
}
|
|
112
165
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
clearConnection();
|
|
122
|
-
}
|
|
123
|
-
setContextStatus({
|
|
124
|
-
running: false,
|
|
125
|
-
connected: false,
|
|
126
|
-
lastStopAt: Date.now(),
|
|
166
|
+
connection.on("reconnecting", (info) => {
|
|
167
|
+
log.info('gateway', `Reconnecting: ${info.reason}, attempt ${info.totalAttempts}`);
|
|
168
|
+
setContextStatus({
|
|
169
|
+
linked: false,
|
|
170
|
+
connected: false,
|
|
171
|
+
lastError: `Reconnecting (${info.reason})`,
|
|
172
|
+
reconnectAttempts: info.totalAttempts,
|
|
173
|
+
});
|
|
127
174
|
});
|
|
128
|
-
|
|
175
|
+
try {
|
|
176
|
+
await connection.start();
|
|
177
|
+
setConnection(connection);
|
|
178
|
+
// Update start time
|
|
179
|
+
setContextStatus({
|
|
180
|
+
running: true,
|
|
181
|
+
linked: true,
|
|
182
|
+
connected: true,
|
|
183
|
+
lastStartAt: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
log.info('gateway', `Started gateway`);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
log.error('gateway', `Failed to start gateway:`, error);
|
|
189
|
+
setContextStatus({
|
|
190
|
+
running: false,
|
|
191
|
+
linked: false,
|
|
192
|
+
connected: false,
|
|
193
|
+
lastError: error instanceof Error ? error.message : 'Failed to start gateway',
|
|
194
|
+
});
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
129
197
|
},
|
|
130
|
-
|
|
198
|
+
stopAccount,
|
|
199
|
+
}
|
|
131
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
|
+
}
|
|
132
215
|
async function outboundSend(ctx) {
|
|
133
216
|
const { to, text, mediaUrl, accountId, replyToId } = ctx;
|
|
134
217
|
log.debug("outbound", `send called - accountId: ${accountId}, to: ${to}, mediaUrl: ${mediaUrl ?? "null"}, replyToId: ${replyToId ?? "none"}`);
|
|
@@ -142,16 +225,7 @@ async function outboundSend(ctx) {
|
|
|
142
225
|
content.push({ type: "text", text: markdownToText(text) });
|
|
143
226
|
}
|
|
144
227
|
if (mediaUrl) {
|
|
145
|
-
|
|
146
|
-
case "image":
|
|
147
|
-
content.push({ type: "image", url: mediaUrl.trim() });
|
|
148
|
-
break;
|
|
149
|
-
case "audio":
|
|
150
|
-
content.push({ type: "audio", path: mediaUrl.trim(), url: mediaUrl.trim(), file: getFileName(mediaUrl.trim()) });
|
|
151
|
-
break;
|
|
152
|
-
default:
|
|
153
|
-
content.push({ type: "file", url: mediaUrl.trim(), file: getFileName(mediaUrl.trim()) });
|
|
154
|
-
}
|
|
228
|
+
content.push(buildMediaMessage(mediaUrl));
|
|
155
229
|
}
|
|
156
230
|
if (replyToId) {
|
|
157
231
|
content.push({ type: "reply", messageId: replyToId });
|
package/dist/src/core/config.js
CHANGED
|
@@ -25,8 +25,15 @@ export function resolveQQAccount(params) {
|
|
|
25
25
|
accessToken: config?.accessToken,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Custom Zod refinement to validate WebSocket URL format
|
|
30
|
+
*/
|
|
31
|
+
const wsUrlRegex = /^wss?:\/\/[\w.-]+(:\d+)?(\/[\w./-]*)?$/;
|
|
32
|
+
const wsUrlSchema = z.string()
|
|
33
|
+
.regex(wsUrlRegex, { message: "Invalid WebSocket URL format. Expected: ws://host:port or wss://host:port" })
|
|
34
|
+
.default("ws://127.0.0.1:3001");
|
|
28
35
|
export const QQConfigSchema = z.object({
|
|
29
|
-
wsUrl:
|
|
36
|
+
wsUrl: wsUrlSchema,
|
|
30
37
|
accessToken: z.string().default("access-token"),
|
|
31
38
|
enable: z.boolean().default(true)
|
|
32
39
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles per-account WebSocket connections with auto-reconnect and heartbeat
|
|
4
4
|
*/
|
|
5
5
|
import EventEmitter from 'events';
|
|
6
|
-
import type { NapCatResp, QQConfig, ConnectionStatus, NapCatAction } from '../types
|
|
6
|
+
import type { NapCatResp, QQConfig, ConnectionStatus, NapCatAction } from '../types';
|
|
7
7
|
/**
|
|
8
8
|
* Connection Manager for a single NapCat account
|
|
9
9
|
*/
|
|
@@ -12,9 +12,9 @@ export declare class ConnectionManager extends EventEmitter {
|
|
|
12
12
|
private ws;
|
|
13
13
|
private state;
|
|
14
14
|
private lastHeartbeatTime;
|
|
15
|
-
private
|
|
15
|
+
private heartbeatCheckTimer?;
|
|
16
16
|
private reconnectTimer?;
|
|
17
|
-
private
|
|
17
|
+
private totalReconnectAttempts;
|
|
18
18
|
private shouldReconnect;
|
|
19
19
|
private pendingRequests;
|
|
20
20
|
private healthStatus;
|
|
@@ -31,6 +31,18 @@ export declare class ConnectionManager extends EventEmitter {
|
|
|
31
31
|
* Establish WebSocket connection
|
|
32
32
|
*/
|
|
33
33
|
private connect;
|
|
34
|
+
/**
|
|
35
|
+
* Clear all pending requests and reject them with an error
|
|
36
|
+
*/
|
|
37
|
+
private clearPendingRequests;
|
|
38
|
+
/**
|
|
39
|
+
* Start heartbeat timeout detection
|
|
40
|
+
*/
|
|
41
|
+
private startHeartbeatCheck;
|
|
42
|
+
/**
|
|
43
|
+
* Stop heartbeat timeout detection
|
|
44
|
+
*/
|
|
45
|
+
private stopHeartbeatCheck;
|
|
34
46
|
/**
|
|
35
47
|
* Close WebSocket connection
|
|
36
48
|
*/
|
|
@@ -7,6 +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 = 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
|
|
10
12
|
/**
|
|
11
13
|
* Connection Manager for a single NapCat account
|
|
12
14
|
*/
|
|
@@ -16,11 +18,10 @@ export class ConnectionManager extends EventEmitter {
|
|
|
16
18
|
state = 'disconnected';
|
|
17
19
|
// Heartbeat - active ping + OneBot 11 meta_event based
|
|
18
20
|
lastHeartbeatTime = 0;
|
|
19
|
-
|
|
20
|
-
totalReconnectAttempts = 0;
|
|
21
|
+
heartbeatCheckTimer;
|
|
21
22
|
// Reconnection
|
|
22
23
|
reconnectTimer;
|
|
23
|
-
|
|
24
|
+
totalReconnectAttempts = 0;
|
|
24
25
|
shouldReconnect = true;
|
|
25
26
|
// Pending requests
|
|
26
27
|
pendingRequests = new Map();
|
|
@@ -46,7 +47,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
this.shouldReconnect = true;
|
|
49
|
-
this.reconnectAttempts = 0;
|
|
50
50
|
await this.connect();
|
|
51
51
|
log.info('connection', `Started connection`);
|
|
52
52
|
}
|
|
@@ -67,16 +67,34 @@ 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)
|
|
73
78
|
let wsUrl = this.config.wsUrl;
|
|
79
|
+
// Validate URL format before processing
|
|
80
|
+
if (!wsUrl || !wsUrl.match(/^wss?:\/\//)) {
|
|
81
|
+
log.error('connection', `Invalid WebSocket URL: ${wsUrl}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
74
84
|
if (this.config.accessToken) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(wsUrl);
|
|
87
|
+
url.searchParams.set('access_token', this.config.accessToken);
|
|
88
|
+
wsUrl = url.toString();
|
|
89
|
+
}
|
|
90
|
+
catch (urlError) {
|
|
91
|
+
log.error('connection', `Failed to parse WebSocket URL: ${wsUrl}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
78
94
|
}
|
|
79
|
-
|
|
95
|
+
// Sanitize URL for logging (hide access token)
|
|
96
|
+
const sanitizedUrl = wsUrl.replace(/access_token=[^&]+/, 'access_token=***');
|
|
97
|
+
log.info('connection', `Connecting to ${sanitizedUrl}`);
|
|
80
98
|
this.ws = new WebSocket(wsUrl);
|
|
81
99
|
this.ws.on('open', this.handleOpen.bind(this));
|
|
82
100
|
this.ws.on('message', this.handleMessage.bind(this));
|
|
@@ -102,10 +120,76 @@ export class ConnectionManager extends EventEmitter {
|
|
|
102
120
|
this.handleConnectionFailed(error instanceof Error ? error : new Error(String(error)));
|
|
103
121
|
}
|
|
104
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Clear all pending requests and reject them with an error
|
|
125
|
+
*/
|
|
126
|
+
clearPendingRequests(reason) {
|
|
127
|
+
if (this.pendingRequests.size === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
log.debug('connection', `Clearing ${this.pendingRequests.size} pending requests: ${reason}`);
|
|
131
|
+
for (const [_echo, pending] of this.pendingRequests) {
|
|
132
|
+
clearTimeout(pending.timeout);
|
|
133
|
+
pending.reject(new Error(`Connection closed: ${reason}`));
|
|
134
|
+
}
|
|
135
|
+
this.pendingRequests.clear();
|
|
136
|
+
}
|
|
137
|
+
// ==========================================================================
|
|
138
|
+
// Heartbeat Timeout Detection
|
|
139
|
+
// ==========================================================================
|
|
140
|
+
/**
|
|
141
|
+
* Start heartbeat timeout detection
|
|
142
|
+
*/
|
|
143
|
+
startHeartbeatCheck() {
|
|
144
|
+
this.stopHeartbeatCheck();
|
|
145
|
+
this.heartbeatCheckTimer = setInterval(() => {
|
|
146
|
+
const elapsed = Date.now() - this.lastHeartbeatTime;
|
|
147
|
+
if (elapsed > HEARTBEAT_TIMEOUT && this.isConnected()) {
|
|
148
|
+
log.warn('connection', `Heartbeat timeout (${elapsed}ms since last heartbeat), reconnecting...`);
|
|
149
|
+
this.healthStatus = {
|
|
150
|
+
healthy: false,
|
|
151
|
+
lastHeartbeatAt: this.lastHeartbeatTime,
|
|
152
|
+
consecutiveFailures: this.healthStatus.consecutiveFailures + 1,
|
|
153
|
+
};
|
|
154
|
+
this.emit('heartbeat', this.healthStatus);
|
|
155
|
+
// Close connection and trigger immediate reconnect
|
|
156
|
+
this.setState('disconnected');
|
|
157
|
+
this.close('Heartbeat timeout').then(() => {
|
|
158
|
+
if (this.shouldReconnect) {
|
|
159
|
+
// Increment total reconnect attempts
|
|
160
|
+
this.totalReconnectAttempts++;
|
|
161
|
+
// Emit reconnecting event for external status updates
|
|
162
|
+
this.emit('reconnecting', {
|
|
163
|
+
reason: 'heartbeat-timeout',
|
|
164
|
+
totalAttempts: this.totalReconnectAttempts,
|
|
165
|
+
});
|
|
166
|
+
this.connect().catch(error => {
|
|
167
|
+
log.error('connection', `Reconnect failed:`, error);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}, HEARTBEAT_CHECK_INTERVAL);
|
|
173
|
+
log.debug('connection', 'Started heartbeat timeout detection');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Stop heartbeat timeout detection
|
|
177
|
+
*/
|
|
178
|
+
stopHeartbeatCheck() {
|
|
179
|
+
if (this.heartbeatCheckTimer) {
|
|
180
|
+
clearInterval(this.heartbeatCheckTimer);
|
|
181
|
+
this.heartbeatCheckTimer = undefined;
|
|
182
|
+
log.debug('connection', 'Stopped heartbeat timeout detection');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
105
185
|
/**
|
|
106
186
|
* Close WebSocket connection
|
|
107
187
|
*/
|
|
108
188
|
async close(reason) {
|
|
189
|
+
// Stop heartbeat detection
|
|
190
|
+
this.stopHeartbeatCheck();
|
|
191
|
+
// Clear all pending requests before closing connection
|
|
192
|
+
this.clearPendingRequests(reason);
|
|
109
193
|
if (this.ws) {
|
|
110
194
|
log.info('connection', `Closing connection: ${reason}`);
|
|
111
195
|
// Clear event listeners to prevent further processing
|
|
@@ -122,8 +206,8 @@ export class ConnectionManager extends EventEmitter {
|
|
|
122
206
|
handleOpen() {
|
|
123
207
|
log.info('connection', `Connected to NapCat`);
|
|
124
208
|
this.setState('connected');
|
|
125
|
-
|
|
126
|
-
this.
|
|
209
|
+
// Start heartbeat timeout detection
|
|
210
|
+
this.startHeartbeatCheck();
|
|
127
211
|
this.emit('connected');
|
|
128
212
|
}
|
|
129
213
|
handleMessage(data) {
|
|
@@ -176,6 +260,12 @@ export class ConnectionManager extends EventEmitter {
|
|
|
176
260
|
handleClose(code, reason) {
|
|
177
261
|
const reasonStr = reason.toString() || getCloseCodeMessage(code);
|
|
178
262
|
log.warn('connection', `Connection closed: ${code} - ${reasonStr}`);
|
|
263
|
+
// 停止心跳检测
|
|
264
|
+
this.stopHeartbeatCheck();
|
|
265
|
+
// 如果 ws 已经为 null,说明是主动关闭(如心跳超时),不需要再处理
|
|
266
|
+
if (this.ws === null) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
179
269
|
if (this.shouldReconnect && !this.isNormalClosure(code)) {
|
|
180
270
|
this.scheduleReconnect();
|
|
181
271
|
}
|
|
@@ -220,17 +310,17 @@ export class ConnectionManager extends EventEmitter {
|
|
|
220
310
|
if (!this.shouldReconnect) {
|
|
221
311
|
return;
|
|
222
312
|
}
|
|
223
|
-
if (MAX_RECONNECT_ATTEMPTS != -1 && this.
|
|
313
|
+
if (MAX_RECONNECT_ATTEMPTS != -1 && this.totalReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
224
314
|
log.error('connection', `Max reconnect attempts reached`);
|
|
225
315
|
this.setState('failed', 'Max reconnect attempts reached');
|
|
226
316
|
this.emit('max-reconnect-attempts-reached');
|
|
227
317
|
return;
|
|
228
318
|
}
|
|
229
|
-
const delayMs = calculateBackoff(this.
|
|
230
|
-
log.info('connection', `Scheduling reconnect in ${delayMs}ms (attempt ${this.
|
|
319
|
+
const delayMs = calculateBackoff(this.totalReconnectAttempts);
|
|
320
|
+
log.info('connection', `Scheduling reconnect in ${delayMs}ms (attempt ${this.totalReconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
231
321
|
this.clearReconnectTimer();
|
|
232
322
|
this.reconnectTimer = setTimeout(async () => {
|
|
233
|
-
this.
|
|
323
|
+
this.totalReconnectAttempts++;
|
|
234
324
|
try {
|
|
235
325
|
await this.connect();
|
|
236
326
|
}
|
|
@@ -304,9 +394,9 @@ export class ConnectionManager extends EventEmitter {
|
|
|
304
394
|
return {
|
|
305
395
|
state: this.state,
|
|
306
396
|
lastConnected: this.lastHeartbeatTime || undefined,
|
|
307
|
-
lastAttempted: this.
|
|
397
|
+
lastAttempted: this.totalReconnectAttempts > 0 ? Date.now() : undefined,
|
|
308
398
|
error: this.state === 'failed' ? 'Connection failed' : undefined,
|
|
309
|
-
reconnectAttempts: this.
|
|
399
|
+
reconnectAttempts: this.totalReconnectAttempts > 0 ? this.totalReconnectAttempts : undefined,
|
|
310
400
|
};
|
|
311
401
|
}
|
|
312
402
|
// ==========================================================================
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Message Dispatch Module
|
|
3
3
|
* Handles routing and dispatching incoming messages to the AI
|
|
4
4
|
*/
|
|
5
|
-
import type { DispatchMessageParams } from '../types
|
|
5
|
+
import type { DispatchMessageParams } from '../types';
|
|
6
6
|
/**
|
|
7
7
|
* Dispatch an incoming message to the AI for processing
|
|
8
8
|
*/
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getRuntime, getContext } from './runtime.js';
|
|
6
6
|
import { getFile, sendMsg, setInputStatus } from './request.js';
|
|
7
|
-
import { napCatToOpenClawMessage } from '../adapters/message.js';
|
|
8
|
-
import { Logger as log, markdownToText } from '../utils/index.js';
|
|
7
|
+
import { napCatToOpenClawMessage, openClawToNapCatMessage } from '../adapters/message.js';
|
|
8
|
+
import { Logger as log, markdownToText, buildMediaMessage } from '../utils/index.js';
|
|
9
9
|
import { CHANNEL_ID } from "./config.js";
|
|
10
10
|
/**
|
|
11
11
|
* Convert OpenClaw message content array to plain text
|
|
@@ -24,7 +24,9 @@ async function contentToPlainText(content) {
|
|
|
24
24
|
case 'json':
|
|
25
25
|
return `[JSON]\n\`\`\`json\n${c.data}\n\`\`\``;
|
|
26
26
|
case 'reply':
|
|
27
|
-
|
|
27
|
+
const senderInfo = c.sender && c.senderId ? `${c.sender}(${c.senderId})` : '未知用户';
|
|
28
|
+
const replyMsg = c.message ?? '[无法获取原消息]';
|
|
29
|
+
let replyContent = `${senderInfo}:\n${replyMsg}`;
|
|
28
30
|
replyContent = replyContent.split('\n').map(line => `> ${line}`).join('\n');
|
|
29
31
|
return `[回复]\n${replyContent}\n`;
|
|
30
32
|
default:
|
|
@@ -83,6 +85,21 @@ async function sendText(isGroup, chatId, text) {
|
|
|
83
85
|
log.error('dispatch', `Send failed: ${error}`);
|
|
84
86
|
}
|
|
85
87
|
}
|
|
88
|
+
async function sendMedia(isGroup, chatId, mediaUrl) {
|
|
89
|
+
const content = [buildMediaMessage(mediaUrl)];
|
|
90
|
+
try {
|
|
91
|
+
await sendMsg({
|
|
92
|
+
message_type: isGroup ? 'group' : 'private',
|
|
93
|
+
group_id: isGroup ? chatId : undefined,
|
|
94
|
+
user_id: !isGroup ? chatId : undefined,
|
|
95
|
+
message: openClawToNapCatMessage(content),
|
|
96
|
+
});
|
|
97
|
+
log.info('dispatch', `Sent reply: ${mediaUrl.slice(0, 100)}`);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
log.error('dispatch', `Send failed: ${error}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
86
103
|
/**
|
|
87
104
|
* Dispatch an incoming message to the AI for processing
|
|
88
105
|
*/
|
|
@@ -168,9 +185,20 @@ export async function dispatchMessage(params) {
|
|
|
168
185
|
},
|
|
169
186
|
deliver: async (payload, info) => {
|
|
170
187
|
log.info('dispatch', `deliver(${info.kind}): ${JSON.stringify(payload)}`);
|
|
171
|
-
if (payload.text) {
|
|
188
|
+
if (payload.text && !payload.text.startsWith('MEDIA:')) {
|
|
172
189
|
await sendText(isGroup, chatId, payload.text);
|
|
173
190
|
}
|
|
191
|
+
if (payload.text && payload.text.startsWith('MEDIA:')) {
|
|
192
|
+
await sendMedia(isGroup, chatId, payload.text.replace('MEDIA:', ''));
|
|
193
|
+
}
|
|
194
|
+
if (payload.mediaUrl) {
|
|
195
|
+
await sendMedia(isGroup, chatId, payload.mediaUrl);
|
|
196
|
+
}
|
|
197
|
+
if (payload.mediaUrls && payload.mediaUrls.length > 0) {
|
|
198
|
+
for (const mediaUrl of payload.mediaUrls) {
|
|
199
|
+
await sendMedia(isGroup, chatId, mediaUrl);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
174
202
|
},
|
|
175
203
|
onError: async (err) => {
|
|
176
204
|
log.error('dispatch', `Dispatch error: ${err}`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { GetFileReq, GetFileResp, GetMsgReq, GetMsgResp, NapCatResp, SendMsgReq, SendMsgResp, SetInputStatusReq } from "../types
|
|
1
|
+
import type { GetFileReq, GetFileResp, GetMsgReq, GetMsgResp, GetStatusResp, NapCatResp, SendMsgReq, SendMsgResp, SetInputStatusReq } from "../types";
|
|
2
2
|
/**
|
|
3
3
|
* 事件监听
|
|
4
4
|
* @param event
|
|
5
5
|
*/
|
|
6
6
|
export declare function eventListener(event: any): Promise<void>;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* 发送消息(带限流)
|
|
9
9
|
* @param params
|
|
10
10
|
*/
|
|
11
11
|
export declare function sendMsg(params: SendMsgReq): Promise<NapCatResp<SendMsgResp>>;
|
|
@@ -24,3 +24,7 @@ export declare function getFile(params: GetFileReq): Promise<NapCatResp<GetFileR
|
|
|
24
24
|
* @param params
|
|
25
25
|
*/
|
|
26
26
|
export declare function setInputStatus(params: SetInputStatusReq): Promise<NapCatResp<void>>;
|
|
27
|
+
/**
|
|
28
|
+
* 获取状态
|
|
29
|
+
*/
|
|
30
|
+
export declare function getStatus(): Promise<NapCatResp<GetStatusResp>>;
|
package/dist/src/core/request.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import pLimit from 'p-limit';
|
|
1
2
|
import { Logger as log } from "../utils/index.js";
|
|
2
3
|
import { setContextStatus, getContext, getConnection } from "./runtime.js";
|
|
3
4
|
import { handleGroupMessage, handlePrivateMessage, handlePokeEvent } from "./dispatch.js";
|
|
4
5
|
import { failResp } from "./connection.js";
|
|
6
|
+
/**
|
|
7
|
+
* Rate limiter for sendMsg requests
|
|
8
|
+
* Limits concurrent messages to prevent API throttling
|
|
9
|
+
*/
|
|
10
|
+
const sendMsgLimiter = pLimit(1);
|
|
5
11
|
/**
|
|
6
12
|
* 事件监听
|
|
7
13
|
* @param event
|
|
@@ -20,6 +26,11 @@ export async function eventListener(event) {
|
|
|
20
26
|
}
|
|
21
27
|
switch (event.post_type) {
|
|
22
28
|
case "message":
|
|
29
|
+
// 过滤空消息
|
|
30
|
+
if (!event.raw_message || event.raw_message === '') {
|
|
31
|
+
log.debug("request", `Ignored empty message`);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
23
34
|
setContextStatus({
|
|
24
35
|
lastInboundAt: Date.now(),
|
|
25
36
|
});
|
|
@@ -66,7 +77,7 @@ export async function eventListener(event) {
|
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
/**
|
|
69
|
-
*
|
|
80
|
+
* 发送消息(带限流)
|
|
70
81
|
* @param params
|
|
71
82
|
*/
|
|
72
83
|
export async function sendMsg(params) {
|
|
@@ -75,7 +86,8 @@ export async function sendMsg(params) {
|
|
|
75
86
|
log.warn("request", `No connection available`);
|
|
76
87
|
return failResp();
|
|
77
88
|
}
|
|
78
|
-
|
|
89
|
+
// 使用限流器控制并发,避免触发 NapCat API 限流
|
|
90
|
+
return sendMsgLimiter(() => connection.sendRequest("send_msg", params));
|
|
79
91
|
}
|
|
80
92
|
/**
|
|
81
93
|
* 获取消息
|
|
@@ -113,3 +125,14 @@ export async function setInputStatus(params) {
|
|
|
113
125
|
}
|
|
114
126
|
return connection.sendRequest("set_input_status", params);
|
|
115
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* 获取状态
|
|
130
|
+
*/
|
|
131
|
+
export async function getStatus() {
|
|
132
|
+
const connection = getConnection();
|
|
133
|
+
if (!connection) {
|
|
134
|
+
log.warn("request", `No connection available`);
|
|
135
|
+
return failResp();
|
|
136
|
+
}
|
|
137
|
+
return connection.sendRequest("get_status");
|
|
138
|
+
}
|
|
@@ -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
|
|
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;
|
|
@@ -243,6 +243,11 @@ export interface SetInputStatusReq {
|
|
|
243
243
|
user_id: string;
|
|
244
244
|
event_type: 0 | 1 | 2;
|
|
245
245
|
}
|
|
246
|
+
export interface GetStatusResp {
|
|
247
|
+
online: boolean;
|
|
248
|
+
good: boolean;
|
|
249
|
+
stat: Record<any, any>;
|
|
250
|
+
}
|
|
246
251
|
export interface DispatchMessageMedia {
|
|
247
252
|
type?: string;
|
|
248
253
|
path?: string;
|
|
@@ -258,3 +263,8 @@ export interface DispatchMessageParams {
|
|
|
258
263
|
media?: DispatchMessageMedia;
|
|
259
264
|
timestamp: number;
|
|
260
265
|
}
|
|
266
|
+
export type QQProbe = {
|
|
267
|
+
ok: boolean;
|
|
268
|
+
status?: number | null;
|
|
269
|
+
error?: string | null;
|
|
270
|
+
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for QQ NapCat plugin
|
|
3
3
|
*/
|
|
4
|
+
import type { OpenClawMessage } from '../types';
|
|
4
5
|
/**
|
|
5
6
|
* Generate a unique message ID for OpenClaw
|
|
7
|
+
* Uses UUID for thread-safe and collision-free ID generation
|
|
6
8
|
*/
|
|
7
9
|
export declare function generateMessageId(): string;
|
|
8
10
|
/**
|
|
9
11
|
* Generate a unique echo ID for request correlation
|
|
12
|
+
* Uses UUID for thread-safe and collision-free ID generation
|
|
10
13
|
*/
|
|
11
14
|
export declare function generateEchoId(): string;
|
|
12
15
|
/**
|
|
@@ -49,6 +52,11 @@ export declare function getCloseCodeMessage(code: number): string;
|
|
|
49
52
|
export type FileCategory = 'image' | 'audio' | 'file';
|
|
50
53
|
export declare function getFileType(pathOrUrl: string): FileCategory;
|
|
51
54
|
export declare function getFileName(pathOrUrl: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Build an OpenClawMessage from a media URL
|
|
57
|
+
* Automatically detects file type (image, audio, or file)
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildMediaMessage(mediaUrl: string): OpenClawMessage;
|
|
52
60
|
export { CQCodeUtils, CQNode } from './cqcode.js';
|
|
53
61
|
export { Logger } from './log.js';
|
|
54
62
|
export { MarkdownToText, markdownToText, } from './markdown.js';
|
package/dist/src/utils/index.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for QQ NapCat plugin
|
|
3
3
|
*/
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// ID Generation
|
|
6
7
|
// =============================================================================
|
|
7
|
-
let idCounter = 0;
|
|
8
8
|
/**
|
|
9
9
|
* Generate a unique message ID for OpenClaw
|
|
10
|
+
* Uses UUID for thread-safe and collision-free ID generation
|
|
10
11
|
*/
|
|
11
12
|
export function generateMessageId() {
|
|
12
|
-
return `qq-${
|
|
13
|
+
return `qq-${randomUUID()}`;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Generate a unique echo ID for request correlation
|
|
17
|
+
* Uses UUID for thread-safe and collision-free ID generation
|
|
16
18
|
*/
|
|
17
19
|
export function generateEchoId() {
|
|
18
|
-
return `echo-${
|
|
20
|
+
return `echo-${randomUUID()}`;
|
|
19
21
|
}
|
|
20
22
|
// =============================================================================
|
|
21
23
|
// Message ID Conversion
|
|
@@ -296,6 +298,33 @@ export function getFileName(pathOrUrl) {
|
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
// =============================================================================
|
|
301
|
+
// Media Message Builder
|
|
302
|
+
// =============================================================================
|
|
303
|
+
/**
|
|
304
|
+
* Build an OpenClawMessage from a media URL
|
|
305
|
+
* Automatically detects file type (image, audio, or file)
|
|
306
|
+
*/
|
|
307
|
+
export function buildMediaMessage(mediaUrl) {
|
|
308
|
+
const trimmedUrl = mediaUrl.trim();
|
|
309
|
+
switch (getFileType(trimmedUrl)) {
|
|
310
|
+
case "image":
|
|
311
|
+
return { type: "image", url: trimmedUrl };
|
|
312
|
+
case "audio":
|
|
313
|
+
return {
|
|
314
|
+
type: "audio",
|
|
315
|
+
path: trimmedUrl,
|
|
316
|
+
url: trimmedUrl,
|
|
317
|
+
file: getFileName(trimmedUrl)
|
|
318
|
+
};
|
|
319
|
+
default:
|
|
320
|
+
return {
|
|
321
|
+
type: "file",
|
|
322
|
+
url: trimmedUrl,
|
|
323
|
+
file: getFileName(trimmedUrl)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// =============================================================================
|
|
299
328
|
// CQ Code Utilities
|
|
300
329
|
// =============================================================================
|
|
301
330
|
export { CQCodeUtils } from './cqcode.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izhimu/qq",
|
|
3
|
-
"version": "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",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"p-limit": "^7.3.0",
|
|
52
53
|
"ws": "^8.19.0",
|
|
53
54
|
"zod": "^4.3.6"
|
|
54
55
|
},
|