@izhimu/qq 0.5.0 → 0.5.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 +9 -2
- package/dist/src/channel.js +125 -75
- package/dist/src/core/config.d.ts +2 -1
- package/dist/src/core/config.js +3 -1
- package/dist/src/core/runtime.js +1 -4
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/utils/log.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
29
|
---
|
|
30
|
-

|
|
31
31
|
## 目录
|
|
32
32
|
|
|
33
33
|
- [功能特性](#功能特性)
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
openclaw plugins install @izhimu/qq
|
|
70
70
|
|
|
71
71
|
# 更新插件
|
|
72
|
-
openclaw plugins update
|
|
72
|
+
openclaw plugins update qq
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
### 本地开发安装
|
|
@@ -175,6 +175,8 @@ openclaw gateway restart
|
|
|
175
175
|
}
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+

|
|
179
|
+
|
|
178
180
|
---
|
|
179
181
|
|
|
180
182
|
## 使用方法
|
|
@@ -391,6 +393,11 @@ npm run build
|
|
|
391
393
|
|
|
392
394
|
## 更新日志
|
|
393
395
|
|
|
396
|
+
### [0.5.1] - 2026-03-12
|
|
397
|
+
|
|
398
|
+
#### 修复
|
|
399
|
+
- **连接状态显示**:修复了插件面板中连接状态及错误信息显示不准确的问题。
|
|
400
|
+
|
|
394
401
|
### [0.5.0] - 2026-03-11
|
|
395
402
|
|
|
396
403
|
#### 新增
|
package/dist/src/channel.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* QQ NapCat Plugin for OpenClaw
|
|
3
3
|
* Main plugin entry point
|
|
4
4
|
*/
|
|
5
|
-
import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, setAccountEnabledInConfigSection, deleteAccountFromConfigSection } from "openclaw/plugin-sdk";
|
|
5
|
+
import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, setAccountEnabledInConfigSection, deleteAccountFromConfigSection, applyAccountNameToChannelSection, migrateBaseNameToDefaultAccount, normalizeAccountId, waitUntilAbort } from "openclaw/plugin-sdk";
|
|
6
6
|
import { messageIdToString, markdownToText, buildMediaMessage, Logger as log } from "./utils/index.js";
|
|
7
7
|
import { setContext, setContextStatus, clearContext, setConnection, getConnection, clearConnection, setLoginInfo, getContext } from "./core/runtime.js";
|
|
8
8
|
import { ConnectionManager } from "./core/connection.js";
|
|
@@ -32,8 +32,7 @@ export const qqPlugin = {
|
|
|
32
32
|
config: {
|
|
33
33
|
listAccountIds: (cfg) => listQQAccountIds(cfg),
|
|
34
34
|
resolveAccount: (cfg) => resolveQQAccount({ cfg }),
|
|
35
|
-
|
|
36
|
-
isConfigured: (account) => Boolean(account?.wsUrl),
|
|
35
|
+
isConfigured: (account) => !!account.accessToken?.trim(),
|
|
37
36
|
setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
|
|
38
37
|
cfg,
|
|
39
38
|
sectionKey: "qq",
|
|
@@ -46,8 +45,62 @@ export const qqPlugin = {
|
|
|
46
45
|
sectionKey: "qq",
|
|
47
46
|
accountId,
|
|
48
47
|
}),
|
|
48
|
+
describeAccount: (account) => ({
|
|
49
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
50
|
+
tokenSource: account.accessToken ? "config" : "none"
|
|
51
|
+
}),
|
|
49
52
|
},
|
|
50
53
|
configSchema: buildChannelConfigSchema(QQConfigSchema),
|
|
54
|
+
setup: {
|
|
55
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
56
|
+
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
57
|
+
cfg: cfg,
|
|
58
|
+
channelKey: "qq",
|
|
59
|
+
accountId,
|
|
60
|
+
name,
|
|
61
|
+
}),
|
|
62
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
63
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
64
|
+
cfg,
|
|
65
|
+
channelKey: "qq",
|
|
66
|
+
accountId,
|
|
67
|
+
name: input.name,
|
|
68
|
+
});
|
|
69
|
+
const next = accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
|
|
70
|
+
cfg: namedConfig,
|
|
71
|
+
channelKey: "qq",
|
|
72
|
+
}) : namedConfig;
|
|
73
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
74
|
+
return {
|
|
75
|
+
...next,
|
|
76
|
+
channels: {
|
|
77
|
+
...next.channels,
|
|
78
|
+
qq: {
|
|
79
|
+
...next.channels?.["qq"],
|
|
80
|
+
enabled: true,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...next,
|
|
87
|
+
channels: {
|
|
88
|
+
...next.channels,
|
|
89
|
+
qq: {
|
|
90
|
+
...next.channels?.["qq"],
|
|
91
|
+
enabled: true,
|
|
92
|
+
accounts: {
|
|
93
|
+
...next.channels?.["qq"]?.accounts,
|
|
94
|
+
[accountId]: {
|
|
95
|
+
...next.channels?.["qq"]?.accounts?.[accountId],
|
|
96
|
+
enabled: true,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
},
|
|
51
104
|
messaging: {
|
|
52
105
|
normalizeTarget: (target) => {
|
|
53
106
|
return target.replace(/^qq:/i, "");
|
|
@@ -73,45 +126,32 @@ export const qqPlugin = {
|
|
|
73
126
|
defaultRuntime: {
|
|
74
127
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
75
128
|
name: "QQ",
|
|
76
|
-
enabled: false,
|
|
77
|
-
configured: false,
|
|
78
|
-
linked: false,
|
|
79
129
|
running: false,
|
|
80
130
|
connected: false,
|
|
81
131
|
reconnectAttempts: 0,
|
|
82
132
|
lastConnectedAt: null,
|
|
133
|
+
lastDisconnect: null,
|
|
83
134
|
lastStartAt: null,
|
|
84
135
|
lastStopAt: null,
|
|
85
136
|
lastError: null,
|
|
86
|
-
lastInboundAt: null,
|
|
87
|
-
lastOutboundAt: null,
|
|
88
137
|
},
|
|
89
138
|
buildChannelSummary: ({ snapshot }) => ({
|
|
90
|
-
enabled: snapshot.enabled ?? false,
|
|
91
139
|
configured: snapshot.configured ?? false,
|
|
92
|
-
linked: snapshot.linked ?? false,
|
|
93
140
|
running: snapshot.running ?? false,
|
|
94
|
-
connected: snapshot.connected ?? false,
|
|
95
|
-
reconnectAttempts: snapshot.reconnectAttempts ?? 0,
|
|
96
|
-
lastConnectedAt: snapshot.lastConnectedAt ?? null,
|
|
97
141
|
lastStartAt: snapshot.lastStartAt ?? null,
|
|
98
142
|
lastStopAt: snapshot.lastStopAt ?? null,
|
|
99
143
|
lastError: snapshot.lastError ?? null,
|
|
100
|
-
lastInboundAt: snapshot.lastInboundAt ?? null,
|
|
101
|
-
lastOutboundAt: snapshot.lastOutboundAt ?? null,
|
|
102
144
|
probe: snapshot.probe,
|
|
103
145
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
104
146
|
}),
|
|
105
147
|
probeAccount: async () => {
|
|
106
148
|
const status = await getStatus();
|
|
107
|
-
|
|
149
|
+
log.debug('gateway', `Probe status: ${status.status}`);
|
|
108
150
|
setContextStatus({
|
|
109
|
-
linked: ok,
|
|
110
|
-
running: ok,
|
|
111
151
|
lastProbeAt: Date.now(),
|
|
112
152
|
});
|
|
113
153
|
return {
|
|
114
|
-
ok: ok,
|
|
154
|
+
ok: status.status === "ok",
|
|
115
155
|
status: status.retcode,
|
|
116
156
|
error: status.status === "failed" ? status.msg : null,
|
|
117
157
|
};
|
|
@@ -120,12 +160,11 @@ export const qqPlugin = {
|
|
|
120
160
|
return {
|
|
121
161
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
122
162
|
name: "QQ",
|
|
123
|
-
enabled: account.enabled
|
|
163
|
+
enabled: account.enabled,
|
|
124
164
|
configured: Boolean(account.wsUrl?.trim()),
|
|
125
|
-
linked:
|
|
165
|
+
linked: Boolean(account.wsUrl?.trim()),
|
|
126
166
|
running: runtime?.running ?? false,
|
|
127
167
|
connected: runtime?.connected ?? false,
|
|
128
|
-
reconnectAttempts: runtime?.reconnectAttempts ?? 0,
|
|
129
168
|
lastStartAt: runtime?.lastStartAt ?? null,
|
|
130
169
|
lastStopAt: runtime?.lastStopAt ?? null,
|
|
131
170
|
lastError: runtime?.lastError ?? null,
|
|
@@ -139,69 +178,30 @@ export const qqPlugin = {
|
|
|
139
178
|
gateway: {
|
|
140
179
|
startAccount: async (ctx) => {
|
|
141
180
|
setContext(ctx);
|
|
142
|
-
const { account } = ctx;
|
|
181
|
+
const { account, abortSignal } = ctx;
|
|
143
182
|
log.info('gateway', `Starting gateway`);
|
|
183
|
+
setContextStatus({
|
|
184
|
+
running: true,
|
|
185
|
+
lastStartAt: Date.now(),
|
|
186
|
+
});
|
|
144
187
|
// 检查是否已存在连接
|
|
145
188
|
const existingConnection = getConnection();
|
|
146
189
|
if (existingConnection) {
|
|
147
|
-
log.
|
|
148
|
-
return;
|
|
190
|
+
log.debug('gateway', `A connection is already running`);
|
|
191
|
+
return waitUntilAbort(abortSignal);
|
|
149
192
|
}
|
|
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
193
|
try {
|
|
194
|
+
const connection = new ConnectionManager(account);
|
|
195
|
+
onEvent(connection);
|
|
180
196
|
await connection.start();
|
|
181
197
|
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
|
-
});
|
|
198
|
+
await loadLoginInfo();
|
|
197
199
|
log.info('gateway', `Started gateway`);
|
|
200
|
+
return waitUntilAbort(abortSignal);
|
|
198
201
|
}
|
|
199
202
|
catch (error) {
|
|
200
203
|
log.error('gateway', `Failed to start gateway:`, error);
|
|
201
204
|
setContextStatus({
|
|
202
|
-
running: false,
|
|
203
|
-
linked: false,
|
|
204
|
-
connected: false,
|
|
205
205
|
lastError: error instanceof Error ? error.message : 'Failed to start gateway',
|
|
206
206
|
});
|
|
207
207
|
throw error;
|
|
@@ -215,8 +215,6 @@ export const qqPlugin = {
|
|
|
215
215
|
}
|
|
216
216
|
setContextStatus({
|
|
217
217
|
running: false,
|
|
218
|
-
linked: false,
|
|
219
|
-
connected: false,
|
|
220
218
|
lastStopAt: Date.now(),
|
|
221
219
|
});
|
|
222
220
|
clearContext();
|
|
@@ -239,7 +237,20 @@ export const qqPlugin = {
|
|
|
239
237
|
listPeersLive: getFriends,
|
|
240
238
|
listGroups: getGroups,
|
|
241
239
|
listGroupsLive: getGroups,
|
|
242
|
-
}
|
|
240
|
+
},
|
|
241
|
+
heartbeat: {
|
|
242
|
+
checkReady: async ({ cfg }) => {
|
|
243
|
+
const account = resolveQQAccount({ cfg });
|
|
244
|
+
if (!account.wsUrl) {
|
|
245
|
+
return { ok: false, reason: "not-configured" };
|
|
246
|
+
}
|
|
247
|
+
const connection = getConnection();
|
|
248
|
+
if (!connection?.isConnected) {
|
|
249
|
+
return { ok: false, reason: "not-connected" };
|
|
250
|
+
}
|
|
251
|
+
return { ok: true, reason: "ok" };
|
|
252
|
+
},
|
|
253
|
+
},
|
|
243
254
|
};
|
|
244
255
|
async function outboundSend(ctx) {
|
|
245
256
|
const { to, text, mediaUrl, accountId, replyToId } = ctx;
|
|
@@ -322,3 +333,42 @@ async function getGroups() {
|
|
|
322
333
|
name: group.group_name,
|
|
323
334
|
}));
|
|
324
335
|
}
|
|
336
|
+
function onEvent(connection) {
|
|
337
|
+
connection.on("event", (event) => eventListener(event));
|
|
338
|
+
connection.on("state-changed", (status) => {
|
|
339
|
+
log.info('gateway', `Connection state: ${status.state}`);
|
|
340
|
+
if (status.state === "connected") {
|
|
341
|
+
setContextStatus({
|
|
342
|
+
connected: true,
|
|
343
|
+
lastConnectedAt: Date.now(),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else if (status.state === "disconnected" || status.state === "failed") {
|
|
347
|
+
setContextStatus({
|
|
348
|
+
connected: false,
|
|
349
|
+
lastError: status.error,
|
|
350
|
+
lastDisconnect: {
|
|
351
|
+
at: Date.now(),
|
|
352
|
+
error: status.error,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
connection.on("reconnecting", (info) => {
|
|
358
|
+
log.info('gateway', `Reconnecting: ${info.reason}, attempt ${info.totalAttempts}`);
|
|
359
|
+
setContextStatus({
|
|
360
|
+
lastError: `Reconnecting (${info.reason})`,
|
|
361
|
+
reconnectAttempts: info.totalAttempts,
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async function loadLoginInfo() {
|
|
366
|
+
// 获取登录信息
|
|
367
|
+
const info = await getLoginInfo();
|
|
368
|
+
if (info.data) {
|
|
369
|
+
setLoginInfo({
|
|
370
|
+
userId: info.data.user_id.toString(),
|
|
371
|
+
nickname: info.data.nickname,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
|
5
5
|
import type { QQConfig } from "../types";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
export declare const CHANNEL_ID = "qq";
|
|
8
|
+
export declare const DEBUG_MODE = false;
|
|
8
9
|
/**
|
|
9
10
|
* 列出所有 QQ 账户ID
|
|
10
11
|
*/
|
|
@@ -40,7 +41,7 @@ export declare const QQGroupConfigSchema: z.ZodObject<{
|
|
|
40
41
|
export declare const QQConfigSchema: z.ZodObject<{
|
|
41
42
|
wsUrl: z.ZodDefault<z.ZodString>;
|
|
42
43
|
accessToken: z.ZodDefault<z.ZodString>;
|
|
43
|
-
|
|
44
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
44
45
|
markdownFormat: z.ZodDefault<z.ZodBoolean>;
|
|
45
46
|
messageDirect: z.ZodObject<{
|
|
46
47
|
policy: z.ZodDefault<z.ZodEnum<{
|
package/dist/src/core/config.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
export const CHANNEL_ID = "qq";
|
|
7
|
+
export const DEBUG_MODE = false;
|
|
7
8
|
/**
|
|
8
9
|
* 列出所有 QQ 账户ID
|
|
9
10
|
*/
|
|
@@ -22,6 +23,7 @@ export function resolveQQAccount(params) {
|
|
|
22
23
|
return {
|
|
23
24
|
enabled: config?.enabled !== false,
|
|
24
25
|
wsUrl: config?.wsUrl ?? "",
|
|
26
|
+
token: config?.accessToken ?? "",
|
|
25
27
|
accessToken: config?.accessToken,
|
|
26
28
|
markdownFormat: config?.markdownFormat ?? true,
|
|
27
29
|
messageDirect: {
|
|
@@ -66,7 +68,7 @@ export const QQGroupConfigSchema = z.object({
|
|
|
66
68
|
export const QQConfigSchema = z.object({
|
|
67
69
|
wsUrl: wsUrlSchema,
|
|
68
70
|
accessToken: z.string().default("access-token").describe("NapCat Websocket Token"),
|
|
69
|
-
|
|
71
|
+
enabled: z.boolean().default(true).describe("是否启用"),
|
|
70
72
|
markdownFormat: z.boolean().default(true).describe("是否启动 Markdown 格式化转换"),
|
|
71
73
|
messageDirect: QQDirectConfigSchema,
|
|
72
74
|
messageGroup: QQGroupConfigSchema,
|
package/dist/src/core/runtime.js
CHANGED
|
@@ -27,10 +27,7 @@ export function clearContext() {
|
|
|
27
27
|
}
|
|
28
28
|
export function setContextStatus(next) {
|
|
29
29
|
if (context) {
|
|
30
|
-
context.setStatus(
|
|
31
|
-
...context.getStatus(),
|
|
32
|
-
...next,
|
|
33
|
-
});
|
|
30
|
+
context.setStatus(next);
|
|
34
31
|
}
|
|
35
32
|
}
|
|
36
33
|
// =============================================================================
|
package/dist/src/utils/log.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEBUG_MODE } from "../core/config";
|
|
1
2
|
import { getRuntime } from "../core/runtime.js";
|
|
2
3
|
function log() {
|
|
3
4
|
return getRuntime()?.logging.getChildLogger({ module: 'channel/qq' }) ?? console;
|
|
@@ -9,7 +10,9 @@ function param(args) {
|
|
|
9
10
|
}
|
|
10
11
|
export class Logger {
|
|
11
12
|
static debug(category, message, ...args) {
|
|
12
|
-
|
|
13
|
+
if (DEBUG_MODE) {
|
|
14
|
+
log().debug?.(`[${category}] ${message}${param(args)}`);
|
|
15
|
+
}
|
|
13
16
|
}
|
|
14
17
|
static info(category, message, ...args) {
|
|
15
18
|
log().info?.(`[${category}] ${message}${param(args)}`);
|