@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 CHANGED
@@ -27,7 +27,7 @@
27
27
  </p>
28
28
 
29
29
  ---
30
- ![demo.png](docs/demo.png)
30
+ ![demo.jpg](docs/demo.jpg)
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 @izhimu/qq
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
+ ![config.png](docs/config.png)
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
  #### 新增
@@ -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
- isEnabled: (account) => Boolean(account?.enabled),
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
- const ok = status.status === "ok";
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 ?? false,
163
+ enabled: account.enabled,
124
164
  configured: Boolean(account.wsUrl?.trim()),
125
- linked: runtime?.linked ?? false,
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.warn('gateway', `A connection is already running`);
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
- enable: z.ZodDefault<z.ZodBoolean>;
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<{
@@ -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
- enable: z.boolean().default(true).describe("是否启用"),
71
+ enabled: z.boolean().default(true).describe("是否启用"),
70
72
  markdownFormat: z.boolean().default(true).describe("是否启动 Markdown 格式化转换"),
71
73
  messageDirect: QQDirectConfigSchema,
72
74
  messageGroup: QQGroupConfigSchema,
@@ -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
  // =============================================================================
@@ -278,6 +278,7 @@ export interface DispatchMessageParams {
278
278
  }
279
279
  export interface QQConfig {
280
280
  wsUrl: string;
281
+ token?: string;
281
282
  accessToken?: string;
282
283
  enabled: boolean;
283
284
  markdownFormat: boolean;
@@ -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
- log().debug?.(`[${category}] ${message}${param(args)}`);
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)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izhimu/qq",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A QQ channel plugin for OpenClaw using NapCat WebSocket",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",