@tencent-connect/openclaw-qqbot 1.6.4-alpha.6 → 1.6.4-alpha.8

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.
@@ -263,8 +263,10 @@ async function ensureImageServer(log, publicBaseUrl) {
263
263
  // 区分 gateway restart(进程重启)和 health-monitor 断线重连
264
264
  let isFirstReadyGlobal = true;
265
265
  const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
266
- const STARTUP_GREETING_TEXT = `Haha,我的'灵魂'已上线,随时等你吩咐。`;
267
266
  const STARTUP_GREETING_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
267
+ function getStartupGreetingText(version) {
268
+ return `🎉 QQBot 插件已更新至 v${version},在线等候你的吩咐。`;
269
+ }
268
270
  function readStartupMarker() {
269
271
  try {
270
272
  if (fs.existsSync(STARTUP_MARKER_FILE)) {
@@ -303,7 +305,7 @@ function getStartupGreetingPlan() {
303
305
  return { shouldSend: false, version: currentVersion, reason: "cooldown" };
304
306
  }
305
307
  }
306
- return { shouldSend: true, greeting: STARTUP_GREETING_TEXT, version: currentVersion };
308
+ return { shouldSend: true, greeting: getStartupGreetingText(currentVersion), version: currentVersion };
307
309
  }
308
310
  function markStartupGreetingSent(version) {
309
311
  writeStartupMarker({
@@ -14,7 +14,7 @@ import { createRequire } from "node:module";
14
14
  import { execFileSync, execFile } from "node:child_process";
15
15
  import path from "node:path";
16
16
  import fs from "node:fs";
17
- import { getUpdateInfo } from "./update-checker.js";
17
+ import { getUpdateInfo, checkVersionExists } from "./update-checker.js";
18
18
  import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js";
19
19
  import { fileURLToPath } from "node:url";
20
20
  const require = createRequire(import.meta.url);
@@ -65,6 +65,12 @@ function registerCommand(cmd) {
65
65
  registerCommand({
66
66
  name: "bot-ping",
67
67
  description: "测试当前 openclaw 与 QQ 连接的网络延迟",
68
+ usage: [
69
+ `/bot-ping`,
70
+ ``,
71
+ `测试 OpenClaw 主机与 QQ 服务器之间的网络延迟。`,
72
+ `返回网络传输耗时和插件处理耗时。`,
73
+ ].join("\n"),
68
74
  handler: (ctx) => {
69
75
  const now = Date.now();
70
76
  const eventTime = new Date(ctx.eventTimestamp).getTime();
@@ -90,6 +96,12 @@ registerCommand({
90
96
  registerCommand({
91
97
  name: "bot-version",
92
98
  description: "查看插件版本号",
99
+ usage: [
100
+ `/bot-version`,
101
+ ``,
102
+ `查看当前 QQBot 插件版本和 OpenClaw 框架版本。`,
103
+ `同时检查是否有新版本可用。`,
104
+ ].join("\n"),
93
105
  handler: () => {
94
106
  const frameworkVersion = getFrameworkVersion();
95
107
  const lines = [
@@ -116,6 +128,12 @@ registerCommand({
116
128
  registerCommand({
117
129
  name: "bot-help",
118
130
  description: "查看所有指令以及用途",
131
+ usage: [
132
+ `/bot-help`,
133
+ ``,
134
+ `列出所有可用的 QQBot 插件内置指令及其简要说明。`,
135
+ `使用 /指令名 ? 可查看某条指令的详细用法。`,
136
+ ].join("\n"),
119
137
  handler: () => {
120
138
  const lines = [`### QQBot插件内置调试指令`, ``];
121
139
  for (const [name, cmd] of commands) {
@@ -258,25 +276,40 @@ function fireHotUpgrade(targetVersion) {
258
276
  return { ok: true };
259
277
  }
260
278
  /**
261
- * /bot-upgrade — 统一升级入口:能热更就热更,失败则返回升级指引
279
+ * /bot-upgrade — 统一升级入口
262
280
  *
263
- * 支持参数:
264
- * /bot-upgrade 检查更新后自动热更
265
- * /bot-upgrade 1.6.4升级到指定版本
266
- * /bot-upgrade --force 强制升级(即使当前已是最新版)
281
+ * 产品流程:
282
+ * /bot-upgrade 展示版本信息+确认按钮(不直接升级)
283
+ * /bot-upgrade --latest确认升级到最新版本
284
+ * /bot-upgrade --version X 升级到指定版本
285
+ * /bot-upgrade --force — 强制升级(即使当前已是最新版)
267
286
  */
287
+ let _upgrading = false; // 升级锁
268
288
  registerCommand({
269
289
  name: "bot-upgrade",
270
290
  description: "检查更新并自动热更(失败则返回升级指引)",
271
- handler: (ctx) => {
291
+ usage: [
292
+ `/bot-upgrade 检查是否有新版本(展示信息+确认按钮)`,
293
+ `/bot-upgrade --latest 确认升级到最新版本`,
294
+ `/bot-upgrade --version X 升级到指定版本(如 1.6.4-alpha.7)`,
295
+ `/bot-upgrade --force 强制重新安装当前版本`,
296
+ ``,
297
+ `⚠️ 仅在私聊中可用。升级过程约 30~60 秒,期间服务短暂不可用。`,
298
+ ].join("\n"),
299
+ handler: async (ctx) => {
272
300
  // 升级相关指令仅在私聊中可用
273
301
  if (ctx.type !== "c2c") {
274
302
  return `💡 请在私聊中使用此指令`;
275
303
  }
304
+ // 升级锁:防止重复触发
305
+ if (_upgrading) {
306
+ return `⏳ 正在升级中,请稍候...`;
307
+ }
276
308
  const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
277
309
  const args = ctx.args.trim();
278
310
  const info = getUpdateInfo();
279
311
  let isForce = false;
312
+ let isLatest = false;
280
313
  let versionArg;
281
314
  const tokens = args ? args.split(/\s+/).filter(Boolean) : [];
282
315
  for (let i = 0; i < tokens.length; i += 1) {
@@ -285,6 +318,10 @@ registerCommand({
285
318
  isForce = true;
286
319
  continue;
287
320
  }
321
+ if (t === "--latest") {
322
+ isLatest = true;
323
+ continue;
324
+ }
288
325
  if (t === "--version") {
289
326
  const next = tokens[i + 1];
290
327
  if (!next || next.startsWith("--")) {
@@ -307,21 +344,65 @@ registerCommand({
307
344
  continue;
308
345
  }
309
346
  }
310
- if (!versionArg && !isForce) {
347
+ const GITHUB_URL = "https://github.com/tencent-connect/openclaw-qqbot/";
348
+ // ── 无参数(也没有 --latest / --version / --force):只展示信息+确认按钮 ──
349
+ if (!versionArg && !isLatest && !isForce) {
311
350
  if (info.checkedAt === 0) {
312
351
  return `⏳ 版本检查中,请稍后再试`;
313
352
  }
314
353
  if (info.error) {
315
- return `⚠️ 版本检查失败\n⬆️升级指引:[点击查看](${url})`;
354
+ return `❌ 当前我的主机网络访问 GitHub 异常,查看手动升级指引:[点击查看](${url})`;
316
355
  }
317
356
  if (!info.hasUpdate) {
318
- return `✅ 当前版本 v${PLUGIN_VERSION} 已是最新,无需升级\n\n> 💡 使用 /bot-upgrade --force 可强制重新安装`;
357
+ const lines = [
358
+ `🌟 GitHub:[tencent-connect/openclaw-qqbot](${GITHUB_URL})`,
359
+ `✅ 当前使用的已经是最新版本:v${PLUGIN_VERSION}`,
360
+ ];
361
+ return lines.join("\n");
362
+ }
363
+ // 有新版本:展示信息 + 确认按钮
364
+ const lines = [
365
+ `🌟 GitHub:[tencent-connect/openclaw-qqbot](${GITHUB_URL})`,
366
+ `📌 当前使用版本:v${PLUGIN_VERSION}`,
367
+ `🆕 最新可用版本:v${info.latest}`,
368
+ `📖 手动升级指引:[点击查看](${url})`,
369
+ `--`,
370
+ `**✅ 当前支持自动升级:**`,
371
+ `请确认机 OpenClaw 主机网络环境连接 GitHub 是否正常,插件更新会重启一次 OpenClaw 主机的 GateWay 服务,过程中会有服务不可用的状态。`,
372
+ `‼️点击确认 <qqbot-cmd-enter text="/bot-upgrade --latest" />`,
373
+ ];
374
+ return lines.join("\n");
375
+ }
376
+ // ── --version 指定版本:先校验版本号是否存在 ──
377
+ if (versionArg) {
378
+ const exists = await checkVersionExists(versionArg);
379
+ if (!exists) {
380
+ return `❌ 当前不存在 ${versionArg} 版本号,请检查 version 设置`;
381
+ }
382
+ // 检查是否就是当前版本
383
+ if (versionArg === PLUGIN_VERSION && !isForce) {
384
+ return `😀 当前已是 v${PLUGIN_VERSION} 版本,无需升级`;
385
+ }
386
+ }
387
+ // ── --latest:检查是否需要升级 ──
388
+ if (isLatest && !versionArg) {
389
+ if (info.checkedAt === 0) {
390
+ return `⏳ 版本检查中,请稍后再试`;
391
+ }
392
+ if (info.error) {
393
+ return `❌ 当前我的主机网络访问 GitHub 异常,查看手动升级指引:[点击查看](${url})`;
394
+ }
395
+ if (!info.hasUpdate && !isForce) {
396
+ return `😀 当前已是 v${PLUGIN_VERSION} 版本,无需升级`;
319
397
  }
320
398
  }
321
399
  const targetVersion = versionArg || info.latest || undefined;
400
+ // 加锁
401
+ _upgrading = true;
322
402
  // 异步执行升级
323
403
  const startResult = fireHotUpgrade(targetVersion);
324
404
  if (!startResult.ok) {
405
+ _upgrading = false;
325
406
  if (startResult.reason === "no-script") {
326
407
  return `❌ 未找到本地升级脚本\n⬆️升级指引:[点击查看](${url})`;
327
408
  }
@@ -452,6 +533,12 @@ function collectRecentLogFiles(logDirs) {
452
533
  registerCommand({
453
534
  name: "bot-logs",
454
535
  description: "导出本地日志文件",
536
+ usage: [
537
+ `/bot-logs`,
538
+ ``,
539
+ `导出最近的 OpenClaw 日志文件(最多 4 个)。`,
540
+ `每个文件最多保留最后 1000 行,以文件形式返回。`,
541
+ ].join("\n"),
455
542
  handler: () => {
456
543
  const logDirs = collectCandidateLogDirs();
457
544
  const recentFiles = collectRecentLogFiles(logDirs).slice(0, 4);
@@ -522,6 +609,13 @@ export async function matchSlashCommand(ctx) {
522
609
  const cmd = commands.get(cmdName);
523
610
  if (!cmd)
524
611
  return null; // 不是插件级指令,交给框架
612
+ // /指令 ? — 返回用法说明
613
+ if (args === "?") {
614
+ if (cmd.usage) {
615
+ return `📖 /${cmd.name} 用法:\n\n${cmd.usage}`;
616
+ }
617
+ return `/${cmd.name} — ${cmd.description}`;
618
+ }
525
619
  ctx.args = args;
526
620
  const result = await cmd.handler(ctx);
527
621
  return result;
@@ -20,3 +20,8 @@ export declare function triggerUpdateCheck(log?: {
20
20
  debug?: (msg: string) => void;
21
21
  }): void;
22
22
  export declare function getUpdateInfo(): UpdateInfo;
23
+ /**
24
+ * 检查指定版本是否存在于 npm registry
25
+ * 用于 /bot-upgrade --version 的前置校验
26
+ */
27
+ export declare function checkVersionExists(version: string): Promise<boolean>;
@@ -101,6 +101,24 @@ export function triggerUpdateCheck(log) {
101
101
  export function getUpdateInfo() {
102
102
  return { ..._lastInfo };
103
103
  }
104
+ /**
105
+ * 检查指定版本是否存在于 npm registry
106
+ * 用于 /bot-upgrade --version 的前置校验
107
+ */
108
+ export async function checkVersionExists(version) {
109
+ for (const baseUrl of REGISTRIES) {
110
+ try {
111
+ const url = `${baseUrl}/${version}`;
112
+ const json = await fetchJson(url, 10_000);
113
+ if (json && json.version === version)
114
+ return true;
115
+ }
116
+ catch {
117
+ // try next registry
118
+ }
119
+ }
120
+ return false;
121
+ }
104
122
  function compareVersions(a, b) {
105
123
  const parse = (v) => {
106
124
  const clean = v.replace(/^v/, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.4-alpha.6",
3
+ "version": "1.6.4-alpha.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/gateway.ts CHANGED
@@ -352,9 +352,12 @@ async function ensureImageServer(log?: GatewayContext["log"], publicBaseUrl?: st
352
352
  let isFirstReadyGlobal = true;
353
353
 
354
354
  const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
355
- const STARTUP_GREETING_TEXT = `Haha,我的'灵魂'已上线,随时等你吩咐。`;
356
355
  const STARTUP_GREETING_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
357
356
 
357
+ function getStartupGreetingText(version: string): string {
358
+ return `🎉 QQBot 插件已更新至 v${version},在线等候你的吩咐。`;
359
+ }
360
+
358
361
  type StartupMarkerData = {
359
362
  version?: string;
360
363
  startedAt?: string;
@@ -405,7 +408,7 @@ function getStartupGreetingPlan(): { shouldSend: boolean; greeting?: string; ver
405
408
  }
406
409
  }
407
410
 
408
- return { shouldSend: true, greeting: STARTUP_GREETING_TEXT, version: currentVersion };
411
+ return { shouldSend: true, greeting: getStartupGreetingText(currentVersion), version: currentVersion };
409
412
  }
410
413
 
411
414
  function markStartupGreetingSent(version: string): void {
@@ -16,7 +16,7 @@ import { createRequire } from "node:module";
16
16
  import { execFileSync, execFile } from "node:child_process";
17
17
  import path from "node:path";
18
18
  import fs from "node:fs";
19
- import { getUpdateInfo } from "./update-checker.js";
19
+ import { getUpdateInfo, checkVersionExists } from "./update-checker.js";
20
20
  import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js";
21
21
  import { fileURLToPath } from "node:url";
22
22
  const require = createRequire(import.meta.url);
@@ -116,6 +116,8 @@ interface SlashCommand {
116
116
  name: string;
117
117
  /** 简要描述 */
118
118
  description: string;
119
+ /** 详细用法说明(支持多行),用于 /指令 ? 查询 */
120
+ usage?: string;
119
121
  /** 处理函数 */
120
122
  handler: (ctx: SlashCommandContext) => SlashCommandResult | Promise<SlashCommandResult>;
121
123
  }
@@ -136,6 +138,12 @@ function registerCommand(cmd: SlashCommand): void {
136
138
  registerCommand({
137
139
  name: "bot-ping",
138
140
  description: "测试当前 openclaw 与 QQ 连接的网络延迟",
141
+ usage: [
142
+ `/bot-ping`,
143
+ ``,
144
+ `测试 OpenClaw 主机与 QQ 服务器之间的网络延迟。`,
145
+ `返回网络传输耗时和插件处理耗时。`,
146
+ ].join("\n"),
139
147
  handler: (ctx) => {
140
148
  const now = Date.now();
141
149
  const eventTime = new Date(ctx.eventTimestamp).getTime();
@@ -162,6 +170,12 @@ registerCommand({
162
170
  registerCommand({
163
171
  name: "bot-version",
164
172
  description: "查看插件版本号",
173
+ usage: [
174
+ `/bot-version`,
175
+ ``,
176
+ `查看当前 QQBot 插件版本和 OpenClaw 框架版本。`,
177
+ `同时检查是否有新版本可用。`,
178
+ ].join("\n"),
165
179
  handler: () => {
166
180
  const frameworkVersion = getFrameworkVersion();
167
181
  const lines = [
@@ -187,6 +201,12 @@ registerCommand({
187
201
  registerCommand({
188
202
  name: "bot-help",
189
203
  description: "查看所有指令以及用途",
204
+ usage: [
205
+ `/bot-help`,
206
+ ``,
207
+ `列出所有可用的 QQBot 插件内置指令及其简要说明。`,
208
+ `使用 /指令名 ? 可查看某条指令的详细用法。`,
209
+ ].join("\n"),
190
210
  handler: () => {
191
211
  const lines = [`### QQBot插件内置调试指令`, ``];
192
212
  for (const [name, cmd] of commands) {
@@ -346,27 +366,44 @@ function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
346
366
  }
347
367
 
348
368
  /**
349
- * /bot-upgrade — 统一升级入口:能热更就热更,失败则返回升级指引
369
+ * /bot-upgrade — 统一升级入口
350
370
  *
351
- * 支持参数:
352
- * /bot-upgrade 检查更新后自动热更
353
- * /bot-upgrade 1.6.4升级到指定版本
354
- * /bot-upgrade --force 强制升级(即使当前已是最新版)
371
+ * 产品流程:
372
+ * /bot-upgrade 展示版本信息+确认按钮(不直接升级)
373
+ * /bot-upgrade --latest确认升级到最新版本
374
+ * /bot-upgrade --version X 升级到指定版本
375
+ * /bot-upgrade --force — 强制升级(即使当前已是最新版)
355
376
  */
377
+ let _upgrading = false; // 升级锁
378
+
356
379
  registerCommand({
357
380
  name: "bot-upgrade",
358
381
  description: "检查更新并自动热更(失败则返回升级指引)",
359
- handler: (ctx) => {
382
+ usage: [
383
+ `/bot-upgrade 检查是否有新版本(展示信息+确认按钮)`,
384
+ `/bot-upgrade --latest 确认升级到最新版本`,
385
+ `/bot-upgrade --version X 升级到指定版本(如 1.6.4-alpha.7)`,
386
+ `/bot-upgrade --force 强制重新安装当前版本`,
387
+ ``,
388
+ `⚠️ 仅在私聊中可用。升级过程约 30~60 秒,期间服务短暂不可用。`,
389
+ ].join("\n"),
390
+ handler: async (ctx) => {
360
391
  // 升级相关指令仅在私聊中可用
361
392
  if (ctx.type !== "c2c") {
362
393
  return `💡 请在私聊中使用此指令`;
363
394
  }
364
395
 
396
+ // 升级锁:防止重复触发
397
+ if (_upgrading) {
398
+ return `⏳ 正在升级中,请稍候...`;
399
+ }
400
+
365
401
  const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
366
402
  const args = ctx.args.trim();
367
403
  const info = getUpdateInfo();
368
404
 
369
405
  let isForce = false;
406
+ let isLatest = false;
370
407
  let versionArg: string | undefined;
371
408
  const tokens = args ? args.split(/\s+/).filter(Boolean) : [];
372
409
  for (let i = 0; i < tokens.length; i += 1) {
@@ -375,6 +412,10 @@ registerCommand({
375
412
  isForce = true;
376
413
  continue;
377
414
  }
415
+ if (t === "--latest") {
416
+ isLatest = true;
417
+ continue;
418
+ }
378
419
  if (t === "--version") {
379
420
  const next = tokens[i + 1];
380
421
  if (!next || next.startsWith("--")) {
@@ -398,23 +439,73 @@ registerCommand({
398
439
  }
399
440
  }
400
441
 
401
- if (!versionArg && !isForce) {
442
+ const GITHUB_URL = "https://github.com/tencent-connect/openclaw-qqbot/";
443
+
444
+ // ── 无参数(也没有 --latest / --version / --force):只展示信息+确认按钮 ──
445
+ if (!versionArg && !isLatest && !isForce) {
402
446
  if (info.checkedAt === 0) {
403
447
  return `⏳ 版本检查中,请稍后再试`;
404
448
  }
405
449
  if (info.error) {
406
- return `⚠️ 版本检查失败\n⬆️升级指引:[点击查看](${url})`;
450
+ return `❌ 当前我的主机网络访问 GitHub 异常,查看手动升级指引:[点击查看](${url})`;
407
451
  }
408
452
  if (!info.hasUpdate) {
409
- return `✅ 当前版本 v${PLUGIN_VERSION} 已是最新,无需升级\n\n> 💡 使用 /bot-upgrade --force 可强制重新安装`;
453
+ const lines = [
454
+ `🌟 GitHub:[tencent-connect/openclaw-qqbot](${GITHUB_URL})`,
455
+ `✅ 当前使用的已经是最新版本:v${PLUGIN_VERSION}`,
456
+ ];
457
+ return lines.join("\n");
458
+ }
459
+
460
+ // 有新版本:展示信息 + 确认按钮
461
+ const lines = [
462
+ `🌟 GitHub:[tencent-connect/openclaw-qqbot](${GITHUB_URL})`,
463
+ `📌 当前使用版本:v${PLUGIN_VERSION}`,
464
+ `🆕 最新可用版本:v${info.latest}`,
465
+ `📖 手动升级指引:[点击查看](${url})`,
466
+ `--`,
467
+ `**✅ 当前支持自动升级:**`,
468
+ `请确认机 OpenClaw 主机网络环境连接 GitHub 是否正常,插件更新会重启一次 OpenClaw 主机的 GateWay 服务,过程中会有服务不可用的状态。`,
469
+ `‼️点击确认 <qqbot-cmd-enter text="/bot-upgrade --latest" />`,
470
+ ];
471
+ return lines.join("\n");
472
+ }
473
+
474
+ // ── --version 指定版本:先校验版本号是否存在 ──
475
+ if (versionArg) {
476
+ const exists = await checkVersionExists(versionArg);
477
+ if (!exists) {
478
+ return `❌ 当前不存在 ${versionArg} 版本号,请检查 version 设置`;
479
+ }
480
+
481
+ // 检查是否就是当前版本
482
+ if (versionArg === PLUGIN_VERSION && !isForce) {
483
+ return `😀 当前已是 v${PLUGIN_VERSION} 版本,无需升级`;
484
+ }
485
+ }
486
+
487
+ // ── --latest:检查是否需要升级 ──
488
+ if (isLatest && !versionArg) {
489
+ if (info.checkedAt === 0) {
490
+ return `⏳ 版本检查中,请稍后再试`;
491
+ }
492
+ if (info.error) {
493
+ return `❌ 当前我的主机网络访问 GitHub 异常,查看手动升级指引:[点击查看](${url})`;
494
+ }
495
+ if (!info.hasUpdate && !isForce) {
496
+ return `😀 当前已是 v${PLUGIN_VERSION} 版本,无需升级`;
410
497
  }
411
498
  }
412
499
 
413
500
  const targetVersion = versionArg || info.latest || undefined;
414
501
 
502
+ // 加锁
503
+ _upgrading = true;
504
+
415
505
  // 异步执行升级
416
506
  const startResult = fireHotUpgrade(targetVersion);
417
507
  if (!startResult.ok) {
508
+ _upgrading = false;
418
509
  if (startResult.reason === "no-script") {
419
510
  return `❌ 未找到本地升级脚本\n⬆️升级指引:[点击查看](${url})`;
420
511
  }
@@ -552,6 +643,12 @@ function collectRecentLogFiles(logDirs: string[]): LogCandidate[] {
552
643
  registerCommand({
553
644
  name: "bot-logs",
554
645
  description: "导出本地日志文件",
646
+ usage: [
647
+ `/bot-logs`,
648
+ ``,
649
+ `导出最近的 OpenClaw 日志文件(最多 4 个)。`,
650
+ `每个文件最多保留最后 1000 行,以文件形式返回。`,
651
+ ].join("\n"),
555
652
  handler: () => {
556
653
  const logDirs = collectCandidateLogDirs();
557
654
  const recentFiles = collectRecentLogFiles(logDirs).slice(0, 4);
@@ -628,6 +725,14 @@ export async function matchSlashCommand(ctx: SlashCommandContext): Promise<Slash
628
725
  const cmd = commands.get(cmdName);
629
726
  if (!cmd) return null; // 不是插件级指令,交给框架
630
727
 
728
+ // /指令 ? — 返回用法说明
729
+ if (args === "?") {
730
+ if (cmd.usage) {
731
+ return `📖 /${cmd.name} 用法:\n\n${cmd.usage}`;
732
+ }
733
+ return `/${cmd.name} — ${cmd.description}`;
734
+ }
735
+
631
736
  ctx.args = args;
632
737
  const result = await cmd.handler(ctx);
633
738
  return result;
@@ -117,6 +117,23 @@ export function getUpdateInfo(): UpdateInfo {
117
117
  return { ..._lastInfo };
118
118
  }
119
119
 
120
+ /**
121
+ * 检查指定版本是否存在于 npm registry
122
+ * 用于 /bot-upgrade --version 的前置校验
123
+ */
124
+ export async function checkVersionExists(version: string): Promise<boolean> {
125
+ for (const baseUrl of REGISTRIES) {
126
+ try {
127
+ const url = `${baseUrl}/${version}`;
128
+ const json = await fetchJson(url, 10_000);
129
+ if (json && json.version === version) return true;
130
+ } catch {
131
+ // try next registry
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+
120
137
  function compareVersions(a: string, b: string): number {
121
138
  const parse = (v: string) => {
122
139
  const clean = v.replace(/^v/, "");