@izhimu/qq 0.5.1 → 0.6.0
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 +12 -16
- package/dist/index.d.ts +5 -11
- package/dist/index.js +9 -18
- package/dist/src/adapters/message.d.ts +15 -4
- package/dist/src/adapters/message.js +179 -124
- package/dist/src/channel.d.ts +2 -7
- package/dist/src/channel.js +231 -312
- package/dist/src/core/auth.d.ts +67 -0
- package/dist/src/core/auth.js +154 -0
- package/dist/src/core/config.d.ts +5 -7
- package/dist/src/core/config.js +6 -8
- package/dist/src/core/connection.d.ts +6 -5
- package/dist/src/core/connection.js +17 -70
- package/dist/src/core/dispatch.d.ts +7 -54
- package/dist/src/core/dispatch.js +210 -398
- package/dist/src/core/event-handler.d.ts +42 -0
- package/dist/src/core/event-handler.js +171 -0
- package/dist/src/core/request.d.ts +3 -8
- package/dist/src/core/request.js +13 -126
- package/dist/src/core/runtime.d.ts +2 -11
- package/dist/src/core/runtime.js +0 -47
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/setup-surface.d.ts +2 -0
- package/dist/src/setup-surface.js +59 -0
- package/dist/src/types/index.d.ts +69 -25
- package/dist/src/types/index.js +3 -4
- package/dist/src/utils/cqcode.d.ts +0 -9
- package/dist/src/utils/cqcode.js +0 -17
- package/dist/src/utils/index.d.ts +0 -17
- package/dist/src/utils/index.js +17 -154
- package/dist/src/utils/log.js +2 -2
- package/dist/src/utils/markdown.d.ts +5 -0
- package/dist/src/utils/markdown.js +57 -5
- package/openclaw.plugin.json +3 -2
- package/package.json +9 -11
- package/dist/src/onboarding.d.ts +0 -10
- package/dist/src/onboarding.js +0 -98
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ 频道授权模块
|
|
3
|
+
*
|
|
4
|
+
* 使用三明治模式 (Sandwich Pattern) 集成 OpenClaw 原生授权系统:
|
|
5
|
+
*
|
|
6
|
+
* 1. 预处理层 (QQ 特有): denyFrom, policy='deny'
|
|
7
|
+
* 2. SDK 层 (OpenClaw 原生): commands.allowFrom 检查
|
|
8
|
+
* 3. 后处理层 (QQ 特有): allowFrom 覆盖, policy='allowlist'
|
|
9
|
+
*
|
|
10
|
+
* 授权优先级链 (从高到低):
|
|
11
|
+
* 1. denyFrom - 绝对拒绝
|
|
12
|
+
* 2. policy='deny' - 频道级全局拒绝
|
|
13
|
+
* 3. allowFrom - 频道级白名单 (最高优先授权)
|
|
14
|
+
* 4. commands.allowFrom.qq - 全局 QQ 专属授权
|
|
15
|
+
* 5. commands.allowFrom["*"] - 全局通配授权
|
|
16
|
+
* 6. policy='allow' - 频道级全局允许
|
|
17
|
+
* 7. policy='allowlist' 未匹配 - 拒绝
|
|
18
|
+
* 8. 默认 - 拒绝
|
|
19
|
+
*/
|
|
20
|
+
import { Logger as log } from "../utils/index.js";
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Helper Functions
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* 检查发送者是否在 allowFrom 列表中
|
|
26
|
+
*/
|
|
27
|
+
function isSenderInList(senderId, allowFrom) {
|
|
28
|
+
if (!allowFrom || allowFrom.length === 0)
|
|
29
|
+
return false;
|
|
30
|
+
return allowFrom.includes(senderId);
|
|
31
|
+
}
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Main Function
|
|
34
|
+
// ============================================================================
|
|
35
|
+
/**
|
|
36
|
+
* 解析 QQ 频道命令授权
|
|
37
|
+
*
|
|
38
|
+
* 使用三明治模式整合 QQ 特有配置与 OpenClaw 全局授权系统
|
|
39
|
+
*/
|
|
40
|
+
export function resolveQQCommandAuthorization(params) {
|
|
41
|
+
const { senderId, qqConfig } = params;
|
|
42
|
+
// ============================================================
|
|
43
|
+
// 第一层: 预处理 (QQ 特有的"硬拒绝"规则)
|
|
44
|
+
// ============================================================
|
|
45
|
+
// Level 1: denyFrom 黑名单 (绝对拒绝,不可被任何规则覆盖)
|
|
46
|
+
if (isSenderInList(senderId, qqConfig.denyFrom)) {
|
|
47
|
+
log.info("auth", `Authorization denied for user ${senderId}: in denyFrom list`);
|
|
48
|
+
return {
|
|
49
|
+
providerId: "qq",
|
|
50
|
+
ownerList: [],
|
|
51
|
+
senderId,
|
|
52
|
+
senderIsOwner: false,
|
|
53
|
+
isAuthorizedSender: false,
|
|
54
|
+
denialReason: "denyFrom",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Level 2: policy='deny' (频道级全局拒绝)
|
|
58
|
+
if (qqConfig.policy === "deny") {
|
|
59
|
+
log.info("auth", `Authorization denied for user ${senderId}: channel policy is deny`);
|
|
60
|
+
return {
|
|
61
|
+
providerId: "qq",
|
|
62
|
+
ownerList: [],
|
|
63
|
+
senderId,
|
|
64
|
+
senderIsOwner: false,
|
|
65
|
+
isAuthorizedSender: false,
|
|
66
|
+
denialReason: "policy_deny",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ============================================================
|
|
70
|
+
// 第二层: 频道级 allowFrom (最高优先级授权)
|
|
71
|
+
// 这一层优先于 SDK,符合"最小权限原则"
|
|
72
|
+
// ============================================================
|
|
73
|
+
// Level 3: 频道级 allowFrom 白名单
|
|
74
|
+
if (qqConfig.allowFrom?.length && qqConfig.allowFrom.includes(senderId)) {
|
|
75
|
+
log.debug("auth", `Authorization granted for user ${senderId}: in channel allowFrom`);
|
|
76
|
+
return {
|
|
77
|
+
providerId: "qq",
|
|
78
|
+
ownerList: qqConfig.allowFrom,
|
|
79
|
+
senderId,
|
|
80
|
+
senderIsOwner: true,
|
|
81
|
+
isAuthorizedSender: true,
|
|
82
|
+
matchedBy: "channel_allowFrom",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ============================================================
|
|
86
|
+
// 第三层: 后处理 (QQ 特有的覆盖规则)
|
|
87
|
+
// ============================================================
|
|
88
|
+
// Level 6: policy='allow' (频道级全局允许)
|
|
89
|
+
if (qqConfig.policy === "allow") {
|
|
90
|
+
log.debug("auth", `Authorization granted for user ${senderId}: channel policy is allow`);
|
|
91
|
+
return {
|
|
92
|
+
providerId: "qq",
|
|
93
|
+
ownerList: [],
|
|
94
|
+
senderId,
|
|
95
|
+
senderIsOwner: false,
|
|
96
|
+
isAuthorizedSender: true,
|
|
97
|
+
matchedBy: "policy_allow",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Level 7: policy='allowlist' 但未匹配 allowFrom
|
|
101
|
+
if (qqConfig.policy === "allowlist") {
|
|
102
|
+
log.info("auth", `Authorization denied for user ${senderId}: not in allowlist`);
|
|
103
|
+
return {
|
|
104
|
+
providerId: "qq",
|
|
105
|
+
ownerList: [],
|
|
106
|
+
senderId,
|
|
107
|
+
senderIsOwner: false,
|
|
108
|
+
isAuthorizedSender: false,
|
|
109
|
+
denialReason: "not_in_allowlist",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Level 8: 默认拒绝
|
|
113
|
+
log.info("auth", `Authorization denied for user ${senderId}: no matching authorization rule`);
|
|
114
|
+
return {
|
|
115
|
+
providerId: "qq",
|
|
116
|
+
ownerList: [],
|
|
117
|
+
senderId,
|
|
118
|
+
senderIsOwner: false,
|
|
119
|
+
isAuthorizedSender: false,
|
|
120
|
+
denialReason: "default_deny",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Utility Functions for dispatch.ts
|
|
125
|
+
// ============================================================================
|
|
126
|
+
/**
|
|
127
|
+
* 默认群组配置(用于私聊场景)
|
|
128
|
+
*/
|
|
129
|
+
const DEFAULT_GROUP_CONFIG = {
|
|
130
|
+
requireMention: false,
|
|
131
|
+
requirePoke: false,
|
|
132
|
+
historyLimit: 20,
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* 根据 chatType 获取对应的 QQ 配置
|
|
136
|
+
* 统一返回 QQGroupConfig 类型,私聊时使用默认值填充群组特有字段
|
|
137
|
+
*/
|
|
138
|
+
export function getQQConfigByChatType(isGroup, groupId, config) {
|
|
139
|
+
// 私聊:返回 messageDirect + 默认群组配置
|
|
140
|
+
if (!isGroup) {
|
|
141
|
+
return {
|
|
142
|
+
...DEFAULT_GROUP_CONFIG,
|
|
143
|
+
...config.messageDirect,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// 群聊:检查是否有特定群组配置
|
|
147
|
+
if (groupId && config.messageGroupsCustom[groupId]) {
|
|
148
|
+
return {
|
|
149
|
+
...config.messageGroup,
|
|
150
|
+
...config.messageGroupsCustom[groupId],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return config.messageGroup;
|
|
154
|
+
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
4
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
5
|
-
import type { QQConfig } from "../types";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { QQAccount } from "../types";
|
|
6
3
|
import { z } from "zod";
|
|
7
|
-
export declare const
|
|
4
|
+
export declare const QQ_CHANNEL = "qq";
|
|
8
5
|
export declare const DEBUG_MODE = false;
|
|
9
6
|
/**
|
|
10
7
|
* 列出所有 QQ 账户ID
|
|
@@ -15,7 +12,8 @@ export declare function listQQAccountIds(cfg: OpenClawConfig): string[];
|
|
|
15
12
|
*/
|
|
16
13
|
export declare function resolveQQAccount(params: {
|
|
17
14
|
cfg: OpenClawConfig;
|
|
18
|
-
|
|
15
|
+
accountId?: string | null;
|
|
16
|
+
}): QQAccount;
|
|
19
17
|
export declare const QQDirectConfigSchema: z.ZodObject<{
|
|
20
18
|
policy: z.ZodDefault<z.ZodEnum<{
|
|
21
19
|
allow: "allow";
|
package/dist/src/core/config.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
* QQ 配置管理
|
|
3
|
-
*/
|
|
4
|
-
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/core";
|
|
5
2
|
import { z } from "zod";
|
|
6
|
-
export const
|
|
3
|
+
export const QQ_CHANNEL = "qq";
|
|
7
4
|
export const DEBUG_MODE = false;
|
|
8
5
|
/**
|
|
9
6
|
* 列出所有 QQ 账户ID
|
|
10
7
|
*/
|
|
11
8
|
export function listQQAccountIds(cfg) {
|
|
12
|
-
const config = cfg.channels?.[
|
|
9
|
+
const config = cfg.channels?.[QQ_CHANNEL];
|
|
13
10
|
if (config?.wsUrl) {
|
|
14
|
-
return [
|
|
11
|
+
return ["default"];
|
|
15
12
|
}
|
|
16
13
|
return [];
|
|
17
14
|
}
|
|
@@ -19,8 +16,9 @@ export function listQQAccountIds(cfg) {
|
|
|
19
16
|
* 解析 QQ 账户配置
|
|
20
17
|
*/
|
|
21
18
|
export function resolveQQAccount(params) {
|
|
22
|
-
const config = params.cfg.channels?.[
|
|
19
|
+
const config = params.cfg.channels?.[QQ_CHANNEL];
|
|
23
20
|
return {
|
|
21
|
+
accountId: params.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
24
22
|
enabled: config?.enabled !== false,
|
|
25
23
|
wsUrl: config?.wsUrl ?? "",
|
|
26
24
|
token: config?.accessToken ?? "",
|
|
@@ -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,
|
|
6
|
+
import type { NapCatResp, QQAccount, ConnectionStatus, NapCatAction } from '../types';
|
|
7
7
|
/**
|
|
8
8
|
* Connection Manager for a single NapCat account
|
|
9
9
|
*/
|
|
@@ -14,11 +14,9 @@ export declare class ConnectionManager extends EventEmitter {
|
|
|
14
14
|
private lastHeartbeatTime;
|
|
15
15
|
private heartbeatCheckTimer?;
|
|
16
16
|
private reconnectTimer?;
|
|
17
|
-
private totalReconnectAttempts;
|
|
18
17
|
private shouldReconnect;
|
|
19
18
|
private pendingRequests;
|
|
20
|
-
|
|
21
|
-
constructor(config: QQConfig);
|
|
19
|
+
constructor(config: QQAccount);
|
|
22
20
|
/**
|
|
23
21
|
* Start the connection
|
|
24
22
|
*/
|
|
@@ -57,7 +55,6 @@ export declare class ConnectionManager extends EventEmitter {
|
|
|
57
55
|
private handleClose;
|
|
58
56
|
private handleConnectionFailed;
|
|
59
57
|
private handleResponse;
|
|
60
|
-
private isNormalClosure;
|
|
61
58
|
private scheduleReconnect;
|
|
62
59
|
private clearReconnectTimer;
|
|
63
60
|
/**
|
|
@@ -75,3 +72,7 @@ export declare class ConnectionManager extends EventEmitter {
|
|
|
75
72
|
isConnected(): boolean;
|
|
76
73
|
}
|
|
77
74
|
export declare function failResp<T>(msg?: string): Promise<NapCatResp<T>>;
|
|
75
|
+
/**
|
|
76
|
+
* Send API request via current connection
|
|
77
|
+
*/
|
|
78
|
+
export declare function sendRequest<T>(action: NapCatAction, params?: unknown): Promise<NapCatResp<T>>;
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import WebSocket from 'ws';
|
|
6
6
|
import EventEmitter from 'events';
|
|
7
|
-
import { Logger as log, generateEchoId,
|
|
8
|
-
|
|
7
|
+
import { Logger as log, generateEchoId, getCloseCodeMessage, } from '../utils/index.js';
|
|
8
|
+
import { getConnection } from './runtime.js';
|
|
9
9
|
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
10
|
-
const
|
|
10
|
+
const RECONNECT_INTERVAL = 5000; // 5 seconds - delay between reconnection attempts
|
|
11
|
+
const HEARTBEAT_TIMEOUT = 120000; // 120 seconds - time without heartbeat before reconnecting
|
|
11
12
|
const HEARTBEAT_CHECK_INTERVAL = 60000; // 60 seconds - how often to check for heartbeat timeout
|
|
12
13
|
/**
|
|
13
14
|
* Connection Manager for a single NapCat account
|
|
@@ -21,16 +22,9 @@ export class ConnectionManager extends EventEmitter {
|
|
|
21
22
|
heartbeatCheckTimer;
|
|
22
23
|
// Reconnection
|
|
23
24
|
reconnectTimer;
|
|
24
|
-
totalReconnectAttempts = 0;
|
|
25
25
|
shouldReconnect = true;
|
|
26
26
|
// Pending requests
|
|
27
27
|
pendingRequests = new Map();
|
|
28
|
-
// Health status
|
|
29
|
-
healthStatus = {
|
|
30
|
-
healthy: false,
|
|
31
|
-
lastHeartbeatAt: 0,
|
|
32
|
-
consecutiveFailures: 0,
|
|
33
|
-
};
|
|
34
28
|
constructor(config) {
|
|
35
29
|
super();
|
|
36
30
|
this.config = config;
|
|
@@ -43,7 +37,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
43
37
|
*/
|
|
44
38
|
async start() {
|
|
45
39
|
if (this.state === 'connected' || this.state === 'connecting') {
|
|
46
|
-
log.debug('connection', `Already ${this.state}`);
|
|
47
40
|
return;
|
|
48
41
|
}
|
|
49
42
|
this.shouldReconnect = true;
|
|
@@ -100,20 +93,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
100
93
|
this.ws.on('message', this.handleMessage.bind(this));
|
|
101
94
|
this.ws.on('error', this.handleError.bind(this));
|
|
102
95
|
this.ws.on('close', this.handleClose.bind(this));
|
|
103
|
-
// Wait for connection to be established or failed
|
|
104
|
-
await new Promise((resolve, reject) => {
|
|
105
|
-
const timeout = setTimeout(() => {
|
|
106
|
-
reject(new Error('Connection timeout'));
|
|
107
|
-
}, 30000);
|
|
108
|
-
this.once('connected', () => {
|
|
109
|
-
clearTimeout(timeout);
|
|
110
|
-
resolve();
|
|
111
|
-
});
|
|
112
|
-
this.once('failed', (error) => {
|
|
113
|
-
clearTimeout(timeout);
|
|
114
|
-
reject(error);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
96
|
}
|
|
118
97
|
catch (error) {
|
|
119
98
|
log.error('connection', `Connection failed:`, error);
|
|
@@ -127,7 +106,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
127
106
|
if (this.pendingRequests.size === 0) {
|
|
128
107
|
return;
|
|
129
108
|
}
|
|
130
|
-
log.debug('connection', `Clearing ${this.pendingRequests.size} pending requests: ${reason}`);
|
|
131
109
|
for (const [_echo, pending] of this.pendingRequests) {
|
|
132
110
|
clearTimeout(pending.timeout);
|
|
133
111
|
pending.reject(new Error(`Connection closed: ${reason}`));
|
|
@@ -146,23 +124,10 @@ export class ConnectionManager extends EventEmitter {
|
|
|
146
124
|
const elapsed = Date.now() - this.lastHeartbeatTime;
|
|
147
125
|
if (elapsed > HEARTBEAT_TIMEOUT && this.isConnected()) {
|
|
148
126
|
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
127
|
// Close connection and trigger immediate reconnect
|
|
156
128
|
this.setState('disconnected');
|
|
157
129
|
this.close('Heartbeat timeout').then(() => {
|
|
158
130
|
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
131
|
this.connect().catch(error => {
|
|
167
132
|
log.error('connection', `Reconnect failed:`, error);
|
|
168
133
|
});
|
|
@@ -170,7 +135,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
170
135
|
});
|
|
171
136
|
}
|
|
172
137
|
}, HEARTBEAT_CHECK_INTERVAL);
|
|
173
|
-
log.debug('connection', 'Started heartbeat timeout detection');
|
|
174
138
|
}
|
|
175
139
|
/**
|
|
176
140
|
* Stop heartbeat timeout detection
|
|
@@ -179,7 +143,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
179
143
|
if (this.heartbeatCheckTimer) {
|
|
180
144
|
clearInterval(this.heartbeatCheckTimer);
|
|
181
145
|
this.heartbeatCheckTimer = undefined;
|
|
182
|
-
log.debug('connection', 'Stopped heartbeat timeout detection');
|
|
183
146
|
}
|
|
184
147
|
}
|
|
185
148
|
/**
|
|
@@ -226,9 +189,7 @@ export class ConnectionManager extends EventEmitter {
|
|
|
226
189
|
// Handle event
|
|
227
190
|
if ('post_type' in message) {
|
|
228
191
|
this.emit('event', message);
|
|
229
|
-
return;
|
|
230
192
|
}
|
|
231
|
-
log.debug('connection', `Received unsolicited response:`, message);
|
|
232
193
|
}
|
|
233
194
|
catch (error) {
|
|
234
195
|
log.error('connection', `Failed to parse message:`, error);
|
|
@@ -239,19 +200,10 @@ export class ConnectionManager extends EventEmitter {
|
|
|
239
200
|
*/
|
|
240
201
|
handleMetaEvent(event) {
|
|
241
202
|
if (event.meta_event_type === 'heartbeat') {
|
|
242
|
-
// NapCat sent us a heartbeat - update health status
|
|
243
203
|
this.lastHeartbeatTime = Date.now();
|
|
244
|
-
this.healthStatus = {
|
|
245
|
-
healthy: true,
|
|
246
|
-
lastHeartbeatAt: this.lastHeartbeatTime,
|
|
247
|
-
consecutiveFailures: 0,
|
|
248
|
-
};
|
|
249
|
-
log.debug('connection', `Received heartbeat`);
|
|
250
|
-
this.emit('heartbeat', this.healthStatus);
|
|
251
204
|
}
|
|
252
205
|
else if (event.meta_event_type === 'lifecycle') {
|
|
253
206
|
log.info('connection', `Lifecycle event: ${event.sub_type}`);
|
|
254
|
-
this.emit('lifecycle', event);
|
|
255
207
|
}
|
|
256
208
|
}
|
|
257
209
|
handleError(error) {
|
|
@@ -266,7 +218,7 @@ export class ConnectionManager extends EventEmitter {
|
|
|
266
218
|
if (this.ws === null) {
|
|
267
219
|
return;
|
|
268
220
|
}
|
|
269
|
-
if (this.shouldReconnect
|
|
221
|
+
if (this.shouldReconnect) {
|
|
270
222
|
this.scheduleReconnect();
|
|
271
223
|
}
|
|
272
224
|
else {
|
|
@@ -298,10 +250,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
298
250
|
else {
|
|
299
251
|
pending.reject(new Error(response.msg || 'Request failed'));
|
|
300
252
|
}
|
|
301
|
-
log.debug('connection', `Received response for echo: ${echo}`);
|
|
302
|
-
}
|
|
303
|
-
isNormalClosure(code) {
|
|
304
|
-
return code === 1000 || code === 1001;
|
|
305
253
|
}
|
|
306
254
|
// ==========================================================================
|
|
307
255
|
// Reconnection Logic
|
|
@@ -310,24 +258,16 @@ export class ConnectionManager extends EventEmitter {
|
|
|
310
258
|
if (!this.shouldReconnect) {
|
|
311
259
|
return;
|
|
312
260
|
}
|
|
313
|
-
|
|
314
|
-
log.error('connection', `Max reconnect attempts reached`);
|
|
315
|
-
this.setState('failed', 'Max reconnect attempts reached');
|
|
316
|
-
this.emit('max-reconnect-attempts-reached');
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
const delayMs = calculateBackoff(this.totalReconnectAttempts);
|
|
320
|
-
log.info('connection', `Scheduling reconnect in ${delayMs}ms (attempt ${this.totalReconnectAttempts + 1}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
261
|
+
log.info('connection', `Scheduling reconnect in ${RECONNECT_INTERVAL}ms`);
|
|
321
262
|
this.clearReconnectTimer();
|
|
322
263
|
this.reconnectTimer = setTimeout(async () => {
|
|
323
|
-
this.totalReconnectAttempts++;
|
|
324
264
|
try {
|
|
325
265
|
await this.connect();
|
|
326
266
|
}
|
|
327
267
|
catch (error) {
|
|
328
268
|
log.error('connection', `Reconnect failed:`, error);
|
|
329
269
|
}
|
|
330
|
-
},
|
|
270
|
+
}, RECONNECT_INTERVAL);
|
|
331
271
|
}
|
|
332
272
|
clearReconnectTimer() {
|
|
333
273
|
if (this.reconnectTimer) {
|
|
@@ -366,7 +306,6 @@ export class ConnectionManager extends EventEmitter {
|
|
|
366
306
|
};
|
|
367
307
|
try {
|
|
368
308
|
this.ws?.send(JSON.stringify(request));
|
|
369
|
-
log.debug('connection', `Sent request: ${action} (echo: ${echo})`);
|
|
370
309
|
}
|
|
371
310
|
catch (error) {
|
|
372
311
|
this.pendingRequests.delete(echo);
|
|
@@ -394,9 +333,7 @@ export class ConnectionManager extends EventEmitter {
|
|
|
394
333
|
return {
|
|
395
334
|
state: this.state,
|
|
396
335
|
lastConnected: this.lastHeartbeatTime || undefined,
|
|
397
|
-
lastAttempted: this.totalReconnectAttempts > 0 ? Date.now() : undefined,
|
|
398
336
|
error: this.state === 'failed' ? 'Connection failed' : undefined,
|
|
399
|
-
reconnectAttempts: this.totalReconnectAttempts > 0 ? this.totalReconnectAttempts : undefined,
|
|
400
337
|
};
|
|
401
338
|
}
|
|
402
339
|
// ==========================================================================
|
|
@@ -416,3 +353,13 @@ export async function failResp(msg = '') {
|
|
|
416
353
|
msg
|
|
417
354
|
});
|
|
418
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Send API request via current connection
|
|
358
|
+
*/
|
|
359
|
+
export async function sendRequest(action, params) {
|
|
360
|
+
const connection = getConnection();
|
|
361
|
+
if (!connection) {
|
|
362
|
+
return failResp();
|
|
363
|
+
}
|
|
364
|
+
return connection.sendRequest(action, params);
|
|
365
|
+
}
|
|
@@ -1,54 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
export declare function dispatchMessage(params: DispatchMessageParams): Promise<void>;
|
|
10
|
-
/**
|
|
11
|
-
* Handle group message event
|
|
12
|
-
*/
|
|
13
|
-
export declare function handleGroupMessage(event: {
|
|
14
|
-
time: number;
|
|
15
|
-
self_id: number;
|
|
16
|
-
message_id: number;
|
|
17
|
-
group_id: number;
|
|
18
|
-
user_id: number;
|
|
19
|
-
message: Array<{
|
|
20
|
-
type: string;
|
|
21
|
-
data: Record<string, unknown>;
|
|
22
|
-
}>;
|
|
23
|
-
raw_message: string;
|
|
24
|
-
sender?: {
|
|
25
|
-
nickname?: string;
|
|
26
|
-
card?: string;
|
|
27
|
-
};
|
|
28
|
-
}): Promise<void>;
|
|
29
|
-
/**
|
|
30
|
-
* Handle private message event
|
|
31
|
-
*/
|
|
32
|
-
export declare function handlePrivateMessage(event: {
|
|
33
|
-
time: number;
|
|
34
|
-
self_id: number;
|
|
35
|
-
message_id: number;
|
|
36
|
-
user_id: number;
|
|
37
|
-
message: Array<{
|
|
38
|
-
type: string;
|
|
39
|
-
data: Record<string, unknown>;
|
|
40
|
-
}>;
|
|
41
|
-
raw_message: string;
|
|
42
|
-
sender?: {
|
|
43
|
-
nickname?: string;
|
|
44
|
-
};
|
|
45
|
-
}): Promise<void>;
|
|
46
|
-
export declare function handlePokeEvent(event: {
|
|
47
|
-
user_id: number;
|
|
48
|
-
target_id: number;
|
|
49
|
-
group_id?: number;
|
|
50
|
-
raw_info?: Array<{
|
|
51
|
-
type: string;
|
|
52
|
-
txt?: string;
|
|
53
|
-
}>;
|
|
54
|
-
}): Promise<void>;
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { QQAccount, InboundMessage } from '../types';
|
|
3
|
+
export declare function createInboundHandler(params: {
|
|
4
|
+
cfg: OpenClawConfig;
|
|
5
|
+
account: QQAccount;
|
|
6
|
+
runtime: PluginRuntime;
|
|
7
|
+
}): (msg: InboundMessage) => Promise<void>;
|