@izhimu/qq 0.5.0 → 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 +21 -18
- 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 +247 -278
- package/dist/src/core/auth.d.ts +67 -0
- package/dist/src/core/auth.js +154 -0
- package/dist/src/core/config.d.ts +7 -8
- package/dist/src/core/config.js +9 -9
- 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 -50
- 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 -24
- 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 +6 -3
- 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
package/dist/src/channel.js
CHANGED
|
@@ -1,248 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { listQQAccountIds, resolveQQAccount, QQConfigSchema, CHANNEL_ID } from "./core/config.js";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, createChatChannelPlugin, } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { createPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup";
|
|
3
|
+
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
4
|
+
import { createScopedChannelConfigAdapter, adaptScopedAccountAccessor, } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
5
|
+
import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
|
|
6
|
+
import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
|
7
|
+
import { createAccountStatusSink, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
8
|
+
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
|
|
9
|
+
import { listQQAccountIds, QQ_CHANNEL, QQConfigSchema, resolveQQAccount } from "./core/config";
|
|
11
10
|
import { eventListener, sendMsg, getStatus, getLoginInfo, getFriendList, getGroupList } from "./core/request.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
})
|
|
49
|
-
},
|
|
50
|
-
configSchema: buildChannelConfigSchema(QQConfigSchema),
|
|
51
|
-
messaging: {
|
|
52
|
-
normalizeTarget: (target) => {
|
|
53
|
-
return target.replace(/^qq:/i, "");
|
|
54
|
-
},
|
|
55
|
-
targetResolver: {
|
|
56
|
-
looksLikeId: (id) => {
|
|
57
|
-
const normalized = id.replace(/^qq:/i, "");
|
|
58
|
-
// 支持 private:xxx, group:xxx 格式
|
|
59
|
-
if (normalized.startsWith("private:") || normalized.startsWith("group:"))
|
|
60
|
-
return true;
|
|
61
|
-
// 支持纯数字QQ号或群号
|
|
62
|
-
return /^\d+$/.test(normalized);
|
|
63
|
-
},
|
|
64
|
-
hint: "private:<qqId> or group:<groupId>",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
outbound: {
|
|
68
|
-
deliveryMode: "direct",
|
|
69
|
-
sendText: outboundSend,
|
|
70
|
-
sendMedia: outboundSend,
|
|
71
|
-
},
|
|
72
|
-
status: {
|
|
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
|
-
const ok = status.status === "ok";
|
|
108
|
-
setContextStatus({
|
|
109
|
-
linked: ok,
|
|
110
|
-
running: ok,
|
|
111
|
-
lastProbeAt: Date.now(),
|
|
112
|
-
});
|
|
113
|
-
return {
|
|
114
|
-
ok: ok,
|
|
115
|
-
status: status.retcode,
|
|
116
|
-
error: status.status === "failed" ? status.msg : null,
|
|
117
|
-
};
|
|
118
|
-
},
|
|
119
|
-
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
|
120
|
-
return {
|
|
121
|
-
accountId: DEFAULT_ACCOUNT_ID,
|
|
122
|
-
name: "QQ",
|
|
123
|
-
enabled: account.enabled ?? false,
|
|
124
|
-
configured: Boolean(account.wsUrl?.trim()),
|
|
125
|
-
linked: runtime?.linked ?? false,
|
|
126
|
-
running: runtime?.running ?? false,
|
|
127
|
-
connected: runtime?.connected ?? false,
|
|
128
|
-
reconnectAttempts: runtime?.reconnectAttempts ?? 0,
|
|
129
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
130
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
131
|
-
lastError: runtime?.lastError ?? null,
|
|
132
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
133
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
134
|
-
probe,
|
|
135
|
-
lastProbeAt: runtime?.lastProbeAt ?? null,
|
|
136
|
-
};
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
gateway: {
|
|
140
|
-
startAccount: async (ctx) => {
|
|
141
|
-
setContext(ctx);
|
|
142
|
-
const { account } = ctx;
|
|
143
|
-
log.info('gateway', `Starting gateway`);
|
|
144
|
-
// 检查是否已存在连接
|
|
145
|
-
const existingConnection = getConnection();
|
|
146
|
-
if (existingConnection) {
|
|
147
|
-
log.warn('gateway', `A connection is already running`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
// Create new connection manager
|
|
151
|
-
const connection = new ConnectionManager(account);
|
|
152
|
-
connection.on("event", (event) => eventListener(event));
|
|
153
|
-
connection.on("state-changed", (status) => {
|
|
154
|
-
log.info('gateway', `State: ${status.state}`);
|
|
155
|
-
if (status.state === "connected") {
|
|
156
|
-
setContextStatus({
|
|
157
|
-
linked: true,
|
|
158
|
-
connected: true,
|
|
159
|
-
lastConnectedAt: Date.now(),
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
else if (status.state === "disconnected" || status.state === "failed") {
|
|
163
|
-
setContextStatus({
|
|
164
|
-
linked: false,
|
|
165
|
-
connected: false,
|
|
166
|
-
lastError: status.error,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
connection.on("reconnecting", (info) => {
|
|
171
|
-
log.info('gateway', `Reconnecting: ${info.reason}, attempt ${info.totalAttempts}`);
|
|
172
|
-
setContextStatus({
|
|
173
|
-
linked: false,
|
|
174
|
-
connected: false,
|
|
175
|
-
lastError: `Reconnecting (${info.reason})`,
|
|
176
|
-
reconnectAttempts: info.totalAttempts,
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
try {
|
|
180
|
-
await connection.start();
|
|
181
|
-
setConnection(connection);
|
|
182
|
-
// 获取登录信息
|
|
183
|
-
const info = await getLoginInfo();
|
|
184
|
-
if (info.data) {
|
|
185
|
-
setLoginInfo({
|
|
186
|
-
userId: info.data.user_id.toString(),
|
|
187
|
-
nickname: info.data.nickname,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
// Update start time
|
|
191
|
-
setContextStatus({
|
|
192
|
-
running: true,
|
|
193
|
-
linked: true,
|
|
194
|
-
connected: true,
|
|
195
|
-
lastStartAt: Date.now(),
|
|
196
|
-
});
|
|
197
|
-
log.info('gateway', `Started gateway`);
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
log.error('gateway', `Failed to start gateway:`, error);
|
|
201
|
-
setContextStatus({
|
|
202
|
-
running: false,
|
|
203
|
-
linked: false,
|
|
204
|
-
connected: false,
|
|
205
|
-
lastError: error instanceof Error ? error.message : 'Failed to start gateway',
|
|
206
|
-
});
|
|
207
|
-
throw error;
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
stopAccount: async (_ctx) => {
|
|
211
|
-
const connection = getConnection();
|
|
212
|
-
if (connection) {
|
|
213
|
-
await connection.stop();
|
|
214
|
-
clearConnection();
|
|
215
|
-
}
|
|
216
|
-
setContextStatus({
|
|
217
|
-
running: false,
|
|
218
|
-
linked: false,
|
|
219
|
-
connected: false,
|
|
220
|
-
lastStopAt: Date.now(),
|
|
221
|
-
});
|
|
222
|
-
clearContext();
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
directory: {
|
|
226
|
-
self: async () => {
|
|
227
|
-
const info = await getLoginInfo();
|
|
228
|
-
if (!info.data) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
log.debug('directory', `self: ${JSON.stringify(info.data)}`);
|
|
232
|
-
return {
|
|
233
|
-
kind: "user",
|
|
234
|
-
id: info.data.user_id.toString(),
|
|
235
|
-
name: info.data.nickname,
|
|
236
|
-
};
|
|
237
|
-
},
|
|
238
|
-
listPeers: getFriends,
|
|
239
|
-
listPeersLive: getFriends,
|
|
240
|
-
listGroups: getGroups,
|
|
241
|
-
listGroupsLive: getGroups,
|
|
11
|
+
import { buildMediaMessage, Logger as log, markdownToText } from "./utils";
|
|
12
|
+
import { clearConnection, getConnection, setConnection, setLoginInfo } from "./core/runtime";
|
|
13
|
+
import { outboundMessageAdapter } from "./adapters/message";
|
|
14
|
+
import { ConnectionManager } from "./core/connection";
|
|
15
|
+
import { qqSetupWizard } from "./setup-surface";
|
|
16
|
+
import { createInboundHandler } from "./core/dispatch";
|
|
17
|
+
import { getQQRuntime } from "./runtime";
|
|
18
|
+
const formatAllowFromEntry = (entry) => entry
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/^(qq):/i, "")
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
async function getFriends() {
|
|
23
|
+
const friendList = await getFriendList();
|
|
24
|
+
log.debug('directory', `friendList: ${JSON.stringify(friendList.data)}`);
|
|
25
|
+
return (friendList.data || []).map((friend) => ({
|
|
26
|
+
kind: "user",
|
|
27
|
+
id: friend.user_id.toString(),
|
|
28
|
+
name: friend.nickname,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
async function getGroups() {
|
|
32
|
+
const groupList = await getGroupList();
|
|
33
|
+
log.debug('directory', `groupList: ${JSON.stringify(groupList.data)}`);
|
|
34
|
+
return (groupList.data || []).map((group) => ({
|
|
35
|
+
kind: "group",
|
|
36
|
+
id: group.group_id.toString(),
|
|
37
|
+
name: group.group_name,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
async function loadLoginInfo() {
|
|
41
|
+
// 获取登录信息
|
|
42
|
+
const info = await getLoginInfo();
|
|
43
|
+
if (info.data) {
|
|
44
|
+
setLoginInfo({
|
|
45
|
+
userId: info.data.user_id.toString(),
|
|
46
|
+
nickname: info.data.nickname,
|
|
47
|
+
});
|
|
242
48
|
}
|
|
243
|
-
}
|
|
49
|
+
}
|
|
244
50
|
async function outboundSend(ctx) {
|
|
245
|
-
const { to, text, mediaUrl, accountId, replyToId } = ctx;
|
|
51
|
+
const { cfg, to, text, mediaUrl, accountId, replyToId } = ctx;
|
|
246
52
|
log.debug("outbound", `send called - accountId: ${accountId}, to: ${to}, mediaUrl: ${mediaUrl ?? "null"}, replyToId: ${replyToId ?? "none"}`);
|
|
247
53
|
// Parse target (format: private:xxx or group:xxx)
|
|
248
54
|
const parts = to.split(":");
|
|
@@ -250,18 +56,9 @@ async function outboundSend(ctx) {
|
|
|
250
56
|
const chatType = type === "group" ? "group" : "private";
|
|
251
57
|
const chatId = id || to;
|
|
252
58
|
const content = [];
|
|
253
|
-
const
|
|
254
|
-
if (!context) {
|
|
255
|
-
log.warn('dispatch', `No gateway context`);
|
|
256
|
-
return {
|
|
257
|
-
channel: CHANNEL_ID,
|
|
258
|
-
messageId: "",
|
|
259
|
-
error: new Error(`No gateway context`),
|
|
260
|
-
deliveredAt: Date.now(),
|
|
261
|
-
};
|
|
262
|
-
}
|
|
59
|
+
const account = resolveQQAccount({ cfg, accountId });
|
|
263
60
|
if (text) {
|
|
264
|
-
content.push({ type: "text", text:
|
|
61
|
+
content.push({ type: "text", text: account.markdownFormat ? markdownToText(text) : text });
|
|
265
62
|
}
|
|
266
63
|
if (mediaUrl) {
|
|
267
64
|
content.push(buildMediaMessage(mediaUrl));
|
|
@@ -272,7 +69,7 @@ async function outboundSend(ctx) {
|
|
|
272
69
|
if (content.length === 0) {
|
|
273
70
|
log.warn("outbound", `send called with no content - accountId: ${accountId}, to: ${to}, mediaUrl: ${mediaUrl ?? "null"}, replyToId: ${replyToId ?? "none"}`);
|
|
274
71
|
return {
|
|
275
|
-
channel:
|
|
72
|
+
channel: QQ_CHANNEL,
|
|
276
73
|
messageId: "",
|
|
277
74
|
error: new Error(`No content to send`),
|
|
278
75
|
deliveredAt: Date.now(),
|
|
@@ -282,43 +79,215 @@ async function outboundSend(ctx) {
|
|
|
282
79
|
message_type: chatType,
|
|
283
80
|
user_id: chatType === "private" ? chatId : undefined,
|
|
284
81
|
group_id: chatType === "group" ? chatId : undefined,
|
|
285
|
-
message:
|
|
82
|
+
message: await outboundMessageAdapter(content, account),
|
|
286
83
|
});
|
|
287
84
|
if (response.status === "ok" && response.data) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
log.debug("outbound", `send successfully, messageId: ${data.message_id}`);
|
|
85
|
+
const { message_id } = response.data;
|
|
86
|
+
log.debug("outbound", `send successfully, messageId: ${message_id}`);
|
|
291
87
|
return {
|
|
292
|
-
channel:
|
|
293
|
-
messageId:
|
|
88
|
+
channel: QQ_CHANNEL,
|
|
89
|
+
messageId: message_id.toString(),
|
|
294
90
|
deliveredAt: Date.now(),
|
|
295
91
|
};
|
|
296
92
|
}
|
|
297
93
|
else {
|
|
298
94
|
log.warn("outbound", `send failed, status: ${response.status}, retcode: ${response.retcode}, msg: ${response.msg ?? "none"}`);
|
|
299
95
|
return {
|
|
300
|
-
channel:
|
|
96
|
+
channel: QQ_CHANNEL,
|
|
301
97
|
messageId: "",
|
|
302
98
|
error: new Error(response.msg || "Send failed"),
|
|
303
99
|
deliveredAt: Date.now(),
|
|
304
100
|
};
|
|
305
101
|
}
|
|
306
102
|
}
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
103
|
+
function onEvent(cfg, account, connection, statusSink) {
|
|
104
|
+
const handleInbound = createInboundHandler({
|
|
105
|
+
cfg,
|
|
106
|
+
account,
|
|
107
|
+
runtime: getQQRuntime(),
|
|
108
|
+
});
|
|
109
|
+
connection.on("event", (event) => eventListener(account, event, handleInbound));
|
|
110
|
+
connection.on("state-changed", (status) => {
|
|
111
|
+
log.info('gateway', `Connection state: ${status.state}`);
|
|
112
|
+
if (status.state === "connected") {
|
|
113
|
+
statusSink({
|
|
114
|
+
connected: true,
|
|
115
|
+
lastConnectedAt: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else if (status.state === "disconnected" || status.state === "failed") {
|
|
119
|
+
statusSink({
|
|
120
|
+
connected: false,
|
|
121
|
+
lastError: status.error,
|
|
122
|
+
lastDisconnect: {
|
|
123
|
+
at: Date.now(),
|
|
124
|
+
error: status.error,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
324
129
|
}
|
|
130
|
+
export const qqPlugin = createChatChannelPlugin({
|
|
131
|
+
base: {
|
|
132
|
+
id: QQ_CHANNEL,
|
|
133
|
+
meta: {
|
|
134
|
+
id: QQ_CHANNEL,
|
|
135
|
+
label: "QQ",
|
|
136
|
+
selectionLabel: "QQ",
|
|
137
|
+
detailLabel: "QQ",
|
|
138
|
+
docsPath: "extensions/qq",
|
|
139
|
+
docsLabel: "qq",
|
|
140
|
+
blurb: "Connect OpenClaw to QQ Chat",
|
|
141
|
+
systemImage: "message",
|
|
142
|
+
quickstartAllowFrom: true,
|
|
143
|
+
},
|
|
144
|
+
capabilities: {
|
|
145
|
+
chatTypes: ["direct", "group"],
|
|
146
|
+
reactions: true,
|
|
147
|
+
reply: true,
|
|
148
|
+
media: true,
|
|
149
|
+
blockStreaming: true,
|
|
150
|
+
},
|
|
151
|
+
reload: { configPrefixes: [`channels.${QQ_CHANNEL}`] },
|
|
152
|
+
config: {
|
|
153
|
+
...createScopedChannelConfigAdapter({
|
|
154
|
+
sectionKey: QQ_CHANNEL,
|
|
155
|
+
listAccountIds: listQQAccountIds,
|
|
156
|
+
resolveAccount: adaptScopedAccountAccessor(resolveQQAccount),
|
|
157
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
158
|
+
clearBaseFields: ["name"],
|
|
159
|
+
resolveAllowFrom: (account) => account.messageDirect.allowFrom,
|
|
160
|
+
formatAllowFrom: (allowFrom) => formatNormalizedAllowFromEntries({
|
|
161
|
+
allowFrom,
|
|
162
|
+
normalizeEntry: formatAllowFromEntry,
|
|
163
|
+
}),
|
|
164
|
+
}),
|
|
165
|
+
isConfigured: (account) => !!account.accessToken?.trim(),
|
|
166
|
+
},
|
|
167
|
+
configSchema: buildChannelConfigSchema(QQConfigSchema),
|
|
168
|
+
setup: createPatchedAccountSetupAdapter({
|
|
169
|
+
channelKey: QQ_CHANNEL,
|
|
170
|
+
validateInput: ({ input }) => {
|
|
171
|
+
if (input.useEnv) {
|
|
172
|
+
return "The use of environment variables is not supported at this time.";
|
|
173
|
+
}
|
|
174
|
+
if (!input.useEnv && !input.token) {
|
|
175
|
+
return "QQ Chat requires token";
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
},
|
|
179
|
+
buildPatch: (input) => input.token ? { botToken: input.token } : {},
|
|
180
|
+
}),
|
|
181
|
+
setupWizard: qqSetupWizard,
|
|
182
|
+
messaging: {
|
|
183
|
+
normalizeTarget: (target) => {
|
|
184
|
+
return target.replace(/^qq:/i, "");
|
|
185
|
+
},
|
|
186
|
+
targetResolver: {
|
|
187
|
+
looksLikeId: (id) => {
|
|
188
|
+
const normalized = id.replace(/^qq:/i, "");
|
|
189
|
+
// 支持 private:xxx, group:xxx 格式
|
|
190
|
+
if (normalized.startsWith("private:") || normalized.startsWith("group:"))
|
|
191
|
+
return true;
|
|
192
|
+
// 支持纯数字QQ号或群号
|
|
193
|
+
return /^\d+$/.test(normalized);
|
|
194
|
+
},
|
|
195
|
+
hint: "private:<qqId> or group:<groupId>",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
status: createComputedAccountStatusAdapter({
|
|
199
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
200
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
201
|
+
configured: snapshot.configured ?? false,
|
|
202
|
+
running: snapshot.running ?? false,
|
|
203
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
204
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
205
|
+
lastError: snapshot.lastError ?? null,
|
|
206
|
+
probe: snapshot.probe,
|
|
207
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
208
|
+
}),
|
|
209
|
+
probeAccount: async () => {
|
|
210
|
+
const status = await getStatus();
|
|
211
|
+
log.debug('gateway', `Probe status: ${status.status}`);
|
|
212
|
+
return {
|
|
213
|
+
ok: status.status === "ok",
|
|
214
|
+
status: status.retcode,
|
|
215
|
+
error: status.status === "failed" ? status.msg : null,
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
resolveAccountSnapshot: ({ account }) => ({
|
|
219
|
+
accountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
220
|
+
name: "QQ",
|
|
221
|
+
enabled: account.enabled,
|
|
222
|
+
configured: Boolean(account.wsUrl?.trim()),
|
|
223
|
+
})
|
|
224
|
+
}),
|
|
225
|
+
gateway: {
|
|
226
|
+
startAccount: async (ctx) => {
|
|
227
|
+
const account = ctx.account;
|
|
228
|
+
const statusSink = createAccountStatusSink({
|
|
229
|
+
accountId: account.accountId,
|
|
230
|
+
setStatus: ctx.setStatus,
|
|
231
|
+
});
|
|
232
|
+
log.info('gateway', `Starting QQ Chat`);
|
|
233
|
+
statusSink({
|
|
234
|
+
running: true,
|
|
235
|
+
lastStartAt: Date.now(),
|
|
236
|
+
});
|
|
237
|
+
await runPassiveAccountLifecycle({
|
|
238
|
+
abortSignal: ctx.abortSignal,
|
|
239
|
+
start: async () => {
|
|
240
|
+
const connection = new ConnectionManager(account);
|
|
241
|
+
onEvent(ctx.cfg, account, connection, statusSink);
|
|
242
|
+
await connection.start();
|
|
243
|
+
setConnection(connection);
|
|
244
|
+
await loadLoginInfo();
|
|
245
|
+
log.info('gateway', `Started gateway`);
|
|
246
|
+
},
|
|
247
|
+
stop: async () => {
|
|
248
|
+
const connection = getConnection();
|
|
249
|
+
if (connection) {
|
|
250
|
+
await connection.stop();
|
|
251
|
+
clearConnection();
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
onStop: async () => {
|
|
255
|
+
statusSink({
|
|
256
|
+
running: false,
|
|
257
|
+
lastStopAt: Date.now(),
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
directory: createChannelDirectoryAdapter({
|
|
264
|
+
self: async () => {
|
|
265
|
+
const info = await getLoginInfo();
|
|
266
|
+
if (!info.data) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
log.debug('directory', `self: ${JSON.stringify(info.data)}`);
|
|
270
|
+
return {
|
|
271
|
+
kind: "user",
|
|
272
|
+
id: info.data.user_id.toString(),
|
|
273
|
+
name: info.data.nickname,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
listPeers: getFriends,
|
|
277
|
+
listPeersLive: getFriends,
|
|
278
|
+
listGroups: getGroups,
|
|
279
|
+
listGroupsLive: getGroups,
|
|
280
|
+
}),
|
|
281
|
+
security: {},
|
|
282
|
+
},
|
|
283
|
+
outbound: {
|
|
284
|
+
base: {
|
|
285
|
+
deliveryMode: "direct",
|
|
286
|
+
},
|
|
287
|
+
attachedResults: {
|
|
288
|
+
channel: QQ_CHANNEL,
|
|
289
|
+
sendText: outboundSend,
|
|
290
|
+
sendMedia: outboundSend,
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
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 type { QQAccount, QQAllowConfig, QQGroupConfig } from "../types";
|
|
21
|
+
/**
|
|
22
|
+
* QQ 命令授权拒绝原因
|
|
23
|
+
*/
|
|
24
|
+
export type QQDenialReason = "denyFrom" | "policy_deny" | "not_in_allowlist" | "default_deny";
|
|
25
|
+
/**
|
|
26
|
+
* QQ 命令授权匹配来源
|
|
27
|
+
*/
|
|
28
|
+
export type QQMatchedBy = "channel_allowFrom" | "commands_qq" | "commands_wildcard" | "policy_allow";
|
|
29
|
+
/**
|
|
30
|
+
* QQ 命令授权结果
|
|
31
|
+
*/
|
|
32
|
+
export interface QQCommandAuthorization {
|
|
33
|
+
/** QQ 频道标识 */
|
|
34
|
+
providerId: "qq";
|
|
35
|
+
/** Owner 列表 */
|
|
36
|
+
ownerList: string[];
|
|
37
|
+
/** 发送者 ID */
|
|
38
|
+
senderId: string;
|
|
39
|
+
/** 发送者是否是 Owner */
|
|
40
|
+
senderIsOwner: boolean;
|
|
41
|
+
/** 是否授权 */
|
|
42
|
+
isAuthorizedSender: boolean;
|
|
43
|
+
/** 拒绝原因(如果被拒绝) */
|
|
44
|
+
denialReason?: QQDenialReason;
|
|
45
|
+
/** 授权匹配来源(如果被授权) */
|
|
46
|
+
matchedBy?: QQMatchedBy;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* resolveQQCommandAuthorization 参数
|
|
50
|
+
*/
|
|
51
|
+
export interface ResolveQQCommandAuthorizationParams {
|
|
52
|
+
/** 发送者 ID */
|
|
53
|
+
senderId: string;
|
|
54
|
+
/** QQ 频道配置 (群组或私聊) */
|
|
55
|
+
qqConfig: QQAllowConfig;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 解析 QQ 频道命令授权
|
|
59
|
+
*
|
|
60
|
+
* 使用三明治模式整合 QQ 特有配置与 OpenClaw 全局授权系统
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveQQCommandAuthorization(params: ResolveQQCommandAuthorizationParams): QQCommandAuthorization;
|
|
63
|
+
/**
|
|
64
|
+
* 根据 chatType 获取对应的 QQ 配置
|
|
65
|
+
* 统一返回 QQGroupConfig 类型,私聊时使用默认值填充群组特有字段
|
|
66
|
+
*/
|
|
67
|
+
export declare function getQQConfigByChatType(isGroup: boolean, groupId: string | undefined, config: QQAccount): QQGroupConfig;
|