@tencent-connect/openclaw-qqbot 1.6.4-alpha.16 → 1.6.4-alpha.18

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.
@@ -7,7 +7,7 @@
7
7
  import { downloadFile } from "./image-server.js";
8
8
  import { convertSilkToWav, isVoiceAttachment, formatDuration } from "./utils/audio-convert.js";
9
9
  import { transcribeAudio, resolveSTTConfig } from "./stt.js";
10
- import { getQQBotDataDir } from "./utils/platform.js";
10
+ import { getQQBotMediaDir } from "./utils/platform.js";
11
11
  // ============ 空结果常量 ============
12
12
  const EMPTY_RESULT = {
13
13
  attachmentInfo: "",
@@ -33,7 +33,7 @@ export async function processAttachments(attachments, ctx) {
33
33
  if (!attachments?.length)
34
34
  return EMPTY_RESULT;
35
35
  const { accountId, cfg, log } = ctx;
36
- const downloadDir = getQQBotDataDir("downloads");
36
+ const downloadDir = getQQBotMediaDir("downloads");
37
37
  const prefix = `[qqbot:${accountId}]`;
38
38
  // 结果收集
39
39
  const imageUrls = [];
@@ -7,7 +7,7 @@ import { getAccessToken, sendC2CMessage, sendChannelMessage, sendGroupMessage, s
7
7
  import { isAudioFile, audioFileToSilkBase64, waitForFile, shouldTranscodeVoice } from "./utils/audio-convert.js";
8
8
  import { normalizeMediaTags } from "./utils/media-tags.js";
9
9
  import { checkFileSize, readFileAsync, fileExistsAsync, formatFileSize } from "./utils/file-utils.js";
10
- import { isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, getQQBotDataDir } from "./utils/platform.js";
10
+ import { isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, getQQBotMediaDir } from "./utils/platform.js";
11
11
  import { downloadFile } from "./image-server.js";
12
12
  // ============ 消息回复限流器 ============
13
13
  // 同一 message_id 1小时内最多回复 4 次,超过 1 小时无法被动回复(需改为主动消息)
@@ -269,7 +269,7 @@ export async function sendPhoto(ctx, imagePath) {
269
269
  */
270
270
  async function downloadAndRetrySendPhoto(ctx, httpUrl, prefix) {
271
271
  try {
272
- const downloadDir = getQQBotDataDir("downloads", "url-fallback");
272
+ const downloadDir = getQQBotMediaDir("downloads", "url-fallback");
273
273
  const localFile = await downloadFile(httpUrl, downloadDir);
274
274
  if (!localFile) {
275
275
  console.error(`${prefix} sendPhoto fallback: download also failed for ${httpUrl.slice(0, 80)}`);
@@ -566,7 +566,7 @@ async function sendDocumentFromLocal(ctx, mediaPath, prefix) {
566
566
  */
567
567
  async function downloadToFallbackDir(httpUrl, prefix, caller) {
568
568
  try {
569
- const downloadDir = getQQBotDataDir("downloads", "url-fallback");
569
+ const downloadDir = getQQBotMediaDir("downloads", "url-fallback");
570
570
  const localFile = await downloadFile(httpUrl, downloadDir);
571
571
  if (!localFile) {
572
572
  console.error(`${prefix} ${caller} fallback: download also failed for ${httpUrl.slice(0, 80)}`);
@@ -606,13 +606,14 @@ registerCommand({
606
606
  ].join("\n");
607
607
  }
608
608
  if (!info.hasUpdate) {
609
- return [
609
+ const lines = [
610
610
  `✅ 当前已是最新版本 v${PLUGIN_VERSION}`,
611
611
  ``,
612
612
  `项目地址:[GitHub](${GITHUB_URL})`,
613
- ].join("\n");
613
+ ];
614
+ return lines.join("\n");
614
615
  }
615
- // 有新版本:展示信息 + 确认按钮
616
+ // 有新版本:展示信息 + 确认按钮(同通道:alpha 只展示 alpha,正式版只展示正式版)
616
617
  return [
617
618
  `🆕 发现新版本`,
618
619
  ``,
@@ -656,6 +657,8 @@ registerCommand({
656
657
  }
657
658
  }
658
659
  const targetVersion = versionArg || info.latest || undefined;
660
+ // --force 时如果 targetVersion 等于当前版本,属于强制重装
661
+ const isReinstall = isForce && targetVersion === PLUGIN_VERSION;
659
662
  // ── 环境兼容性检查 ──
660
663
  const compat = checkUpgradeCompatibility();
661
664
  if (!compat.ok) {
@@ -704,16 +707,20 @@ registerCommand({
704
707
  ].join("\n");
705
708
  }
706
709
  saveUpgradeGreetingTarget(ctx.accountId, ctx.appId, ctx.senderId);
707
- const resultLines = [
708
- `🔄 正在升级...`,
709
- ``,
710
- `当前版本:v${PLUGIN_VERSION}`,
711
- ];
712
- if (targetVersion) {
713
- resultLines.push(`目标版本:v${targetVersion}`);
714
- }
715
- resultLines.push(``);
716
- resultLines.push(`预计 30~60 秒完成,届时会自动通知您`);
710
+ const resultLines = isReinstall
711
+ ? [
712
+ `🔄 正在重新安装 v${PLUGIN_VERSION}...`,
713
+ ``,
714
+ `预计 30~60 秒完成,届时会自动通知您`,
715
+ ]
716
+ : [
717
+ `🔄 正在升级...`,
718
+ ``,
719
+ `当前版本:v${PLUGIN_VERSION}`,
720
+ ...(targetVersion ? [`目标版本:v${targetVersion}`] : []),
721
+ ``,
722
+ `预计 30~60 秒完成,届时会自动通知您`,
723
+ ];
717
724
  return resultLines.join("\n");
718
725
  },
719
726
  });
@@ -9,7 +9,12 @@
9
9
  */
10
10
  export interface UpdateInfo {
11
11
  current: string;
12
+ /** 最佳升级目标(prerelease 用户优先 alpha,稳定版用户取 latest) */
12
13
  latest: string | null;
14
+ /** 稳定版 dist-tag */
15
+ stable: string | null;
16
+ /** alpha dist-tag */
17
+ alpha: string | null;
13
18
  hasUpdate: boolean;
14
19
  checkedAt: number;
15
20
  error?: string;
@@ -64,13 +64,21 @@ async function fetchDistTags() {
64
64
  }
65
65
  function buildUpdateInfo(tags) {
66
66
  const currentIsPrerelease = CURRENT_VERSION.includes("-");
67
- const compareTarget = currentIsPrerelease
68
- ? (tags.alpha || tags.latest || null)
69
- : (tags.latest || null);
67
+ const stableTag = tags.latest || null;
68
+ const alphaTag = tags.alpha || null;
69
+ // 严格隔离:alpha 只跟 alpha 比,正式版只跟正式版比,不交叉
70
+ const compareTarget = currentIsPrerelease ? alphaTag : stableTag;
70
71
  const hasUpdate = typeof compareTarget === "string"
71
72
  && compareTarget !== CURRENT_VERSION
72
73
  && compareVersions(compareTarget, CURRENT_VERSION) > 0;
73
- return { current: CURRENT_VERSION, latest: compareTarget, hasUpdate, checkedAt: Date.now() };
74
+ return {
75
+ current: CURRENT_VERSION,
76
+ latest: compareTarget,
77
+ stable: stableTag,
78
+ alpha: alphaTag,
79
+ hasUpdate,
80
+ checkedAt: Date.now(),
81
+ };
74
82
  }
75
83
  /** gateway 启动时调用,保存 log 引用 */
76
84
  export function triggerUpdateCheck(log) {
@@ -91,7 +99,7 @@ export async function getUpdateInfo() {
91
99
  }
92
100
  catch (err) {
93
101
  _log?.debug?.(`[qqbot:update-checker] check failed: ${err.message}`);
94
- return { current: CURRENT_VERSION, latest: null, hasUpdate: false, checkedAt: Date.now(), error: err.message };
102
+ return { current: CURRENT_VERSION, latest: null, stable: null, alpha: null, hasUpdate: false, checkedAt: Date.now(), error: err.message };
95
103
  }
96
104
  }
97
105
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.4-alpha.16",
3
+ "version": "1.6.4-alpha.18",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,12 +50,9 @@ cleanup_installation() {
50
50
  const ids = ['qqbot', 'openclaw-qq', '@sliverp/qqbot', '@tencent-connect/qqbot', '@tencent-connect/openclaw-qq', '@tencent-connect/openclaw-qqbot', 'openclaw-qqbot'];
51
51
 
52
52
  for (const id of ids) {
53
- // 删除 channels.<id>
54
- if (config.channels && config.channels[id]) {
55
- delete config.channels[id];
56
- console.log(' - 已删除 channels.' + id);
57
- }
58
-
53
+ // 注意: 不删除 channels.<id>,因为里面保存了用户的 appid/secret 凭证
54
+ // 凭证与插件版本无关,清理插件时不应清除凭证
55
+
59
56
  // 删除 plugins.entries.<id>
60
57
  if (config.plugins && config.plugins.entries && config.plugins.entries[id]) {
61
58
  delete config.plugins.entries[id];
@@ -186,6 +186,34 @@ INSTALL_LOG="/tmp/openclaw-install-$(date +%s).log"
186
186
  echo "安装日志文件: $INSTALL_LOG"
187
187
  echo "详细信息将记录到日志文件中..."
188
188
 
189
+ # ── 临时移除 channels.qqbot 配置 ──
190
+ # openclaw CLI 任何子命令(包括 gateway stop、plugins install)启动时都会校验 openclaw.json,
191
+ # 如果 channels.qqbot 存在但插件还未安装,CLI 不认识该 channel id,
192
+ # 导致 "Config invalid: unknown channel id: qqbot" 而命令失败(鸡生蛋问题)。
193
+ # 解决方案:在所有 openclaw 命令之前把 channels.qqbot 暂存,完成后恢复。
194
+ _QQBOT_CHANNEL_STASH=""
195
+ for _app in openclaw clawdbot moltbot; do
196
+ _cfg="$HOME/.$_app/$_app.json"
197
+ if [ -f "$_cfg" ]; then
198
+ _QQBOT_CHANNEL_STASH=$(node -e "
199
+ const fs = require('fs');
200
+ const cfg = JSON.parse(fs.readFileSync('$_cfg', 'utf8'));
201
+ if (cfg.channels && cfg.channels.qqbot) {
202
+ const stashed = JSON.stringify(cfg.channels.qqbot);
203
+ delete cfg.channels.qqbot;
204
+ fs.writeFileSync('$_cfg', JSON.stringify(cfg, null, 4) + '\n');
205
+ process.stdout.write(stashed);
206
+ }
207
+ " 2>/dev/null || true)
208
+ if [ -n "$_QQBOT_CHANNEL_STASH" ]; then
209
+ _STASH_APP="$_app"
210
+ _STASH_CFG="$_cfg"
211
+ echo " 已暂存 channels.qqbot 配置(避免 CLI 校验失败)"
212
+ fi
213
+ break
214
+ fi
215
+ done
216
+
189
217
  # 安装前先 stop gateway,防止 chokidar 在 plugins install 写入配置的中间状态
190
218
  # 触发 restart,导致 "unknown channel id: qqbot" 等错误
191
219
  _gw_was_running=0
@@ -251,6 +279,16 @@ if ! openclaw plugins install . 2>&1 | tee "$INSTALL_LOG"; then
251
279
  * )
252
280
  echo "安装失败,脚本退出。"
253
281
  echo "请先解决安装问题后再运行此脚本。"
282
+ # 恢复 channels.qqbot 后再退出
283
+ if [ -n "$_QQBOT_CHANNEL_STASH" ] && [ -n "$_STASH_CFG" ] && [ -f "$_STASH_CFG" ]; then
284
+ node -e "
285
+ const fs = require('fs');
286
+ const cfg = JSON.parse(fs.readFileSync('$_STASH_CFG', 'utf8'));
287
+ if (!cfg.channels) cfg.channels = {};
288
+ cfg.channels.qqbot = $_QQBOT_CHANNEL_STASH;
289
+ fs.writeFileSync('$_STASH_CFG', JSON.stringify(cfg, null, 4) + '\n');
290
+ " 2>/dev/null || true
291
+ fi
254
292
  exit 1
255
293
  ;;
256
294
  esac
@@ -424,6 +462,18 @@ else
424
462
  ' 2>/dev/null || echo "unknown")
425
463
  fi
426
464
 
465
+ # ── 恢复 channels.qqbot 配置 ──
466
+ # 安装完成(无论成功或失败),把之前暂存的 channels.qqbot 写回 openclaw.json
467
+ if [ -n "$_QQBOT_CHANNEL_STASH" ] && [ -n "$_STASH_CFG" ] && [ -f "$_STASH_CFG" ]; then
468
+ node -e "
469
+ const fs = require('fs');
470
+ const cfg = JSON.parse(fs.readFileSync('$_STASH_CFG', 'utf8'));
471
+ if (!cfg.channels) cfg.channels = {};
472
+ cfg.channels.qqbot = $_QQBOT_CHANNEL_STASH;
473
+ fs.writeFileSync('$_STASH_CFG', JSON.stringify(cfg, null, 4) + '\n');
474
+ " 2>/dev/null && echo " 已恢复 channels.qqbot 配置" || echo " ⚠️ 恢复 channels.qqbot 配置失败"
475
+ fi
476
+
427
477
  # 4. 配置机器人通道(仅在需要变更时写入配置,避免无意义覆盖)
428
478
  echo ""
429
479
  echo "[4/6] 配置机器人通道..."
@@ -8,7 +8,7 @@
8
8
  import { downloadFile } from "./image-server.js";
9
9
  import { convertSilkToWav, isVoiceAttachment, formatDuration } from "./utils/audio-convert.js";
10
10
  import { transcribeAudio, resolveSTTConfig } from "./stt.js";
11
- import { getQQBotDataDir } from "./utils/platform.js";
11
+ import { getQQBotMediaDir } from "./utils/platform.js";
12
12
 
13
13
  // ============ 类型定义 ============
14
14
 
@@ -85,7 +85,7 @@ export async function processAttachments(
85
85
  if (!attachments?.length) return EMPTY_RESULT;
86
86
 
87
87
  const { accountId, cfg, log } = ctx;
88
- const downloadDir = getQQBotDataDir("downloads");
88
+ const downloadDir = getQQBotMediaDir("downloads");
89
89
  const prefix = `[qqbot:${accountId}]`;
90
90
 
91
91
  // 结果收集
package/src/outbound.ts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  import { isAudioFile, audioFileToSilkBase64, waitForFile, shouldTranscodeVoice } from "./utils/audio-convert.js";
25
25
  import { normalizeMediaTags } from "./utils/media-tags.js";
26
26
  import { checkFileSize, readFileAsync, fileExistsAsync, isLargeFile, formatFileSize } from "./utils/file-utils.js";
27
- import { isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, getQQBotDataDir } from "./utils/platform.js";
27
+ import { isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, getQQBotDataDir, getQQBotMediaDir } from "./utils/platform.js";
28
28
  import { downloadFile } from "./image-server.js";
29
29
 
30
30
  // ============ 消息回复限流器 ============
@@ -378,7 +378,7 @@ async function downloadAndRetrySendPhoto(
378
378
  prefix: string,
379
379
  ): Promise<OutboundResult | null> {
380
380
  try {
381
- const downloadDir = getQQBotDataDir("downloads", "url-fallback");
381
+ const downloadDir = getQQBotMediaDir("downloads", "url-fallback");
382
382
  const localFile = await downloadFile(httpUrl, downloadDir);
383
383
  if (!localFile) {
384
384
  console.error(`${prefix} sendPhoto fallback: download also failed for ${httpUrl.slice(0, 80)}`);
@@ -702,7 +702,7 @@ async function sendDocumentFromLocal(ctx: MediaTargetContext, mediaPath: string,
702
702
  */
703
703
  async function downloadToFallbackDir(httpUrl: string, prefix: string, caller: string): Promise<string | null> {
704
704
  try {
705
- const downloadDir = getQQBotDataDir("downloads", "url-fallback");
705
+ const downloadDir = getQQBotMediaDir("downloads", "url-fallback");
706
706
  const localFile = await downloadFile(httpUrl, downloadDir);
707
707
  if (!localFile) {
708
708
  console.error(`${prefix} ${caller} fallback: download also failed for ${httpUrl.slice(0, 80)}`);
@@ -710,14 +710,15 @@ registerCommand({
710
710
  ].join("\n");
711
711
  }
712
712
  if (!info.hasUpdate) {
713
- return [
713
+ const lines = [
714
714
  `✅ 当前已是最新版本 v${PLUGIN_VERSION}`,
715
715
  ``,
716
716
  `项目地址:[GitHub](${GITHUB_URL})`,
717
- ].join("\n");
717
+ ];
718
+ return lines.join("\n");
718
719
  }
719
720
 
720
- // 有新版本:展示信息 + 确认按钮
721
+ // 有新版本:展示信息 + 确认按钮(同通道:alpha 只展示 alpha,正式版只展示正式版)
721
722
  return [
722
723
  `🆕 发现新版本`,
723
724
  ``,
@@ -766,6 +767,9 @@ registerCommand({
766
767
 
767
768
  const targetVersion = versionArg || info.latest || undefined;
768
769
 
770
+ // --force 时如果 targetVersion 等于当前版本,属于强制重装
771
+ const isReinstall = isForce && targetVersion === PLUGIN_VERSION;
772
+
769
773
  // ── 环境兼容性检查 ──
770
774
  const compat = checkUpgradeCompatibility();
771
775
  if (!compat.ok) {
@@ -819,16 +823,20 @@ registerCommand({
819
823
 
820
824
  saveUpgradeGreetingTarget(ctx.accountId, ctx.appId, ctx.senderId);
821
825
 
822
- const resultLines = [
823
- `🔄 正在升级...`,
824
- ``,
825
- `当前版本:v${PLUGIN_VERSION}`,
826
- ];
827
- if (targetVersion) {
828
- resultLines.push(`目标版本:v${targetVersion}`);
829
- }
830
- resultLines.push(``);
831
- resultLines.push(`预计 30~60 秒完成,届时会自动通知您`);
826
+ const resultLines = isReinstall
827
+ ? [
828
+ `🔄 正在重新安装 v${PLUGIN_VERSION}...`,
829
+ ``,
830
+ `预计 30~60 秒完成,届时会自动通知您`,
831
+ ]
832
+ : [
833
+ `🔄 正在升级...`,
834
+ ``,
835
+ `当前版本:v${PLUGIN_VERSION}`,
836
+ ...(targetVersion ? [`目标版本:v${targetVersion}`] : []),
837
+ ``,
838
+ `预计 30~60 秒完成,届时会自动通知您`,
839
+ ];
832
840
  return resultLines.join("\n");
833
841
  },
834
842
  });
@@ -31,7 +31,12 @@ try {
31
31
 
32
32
  export interface UpdateInfo {
33
33
  current: string;
34
+ /** 最佳升级目标(prerelease 用户优先 alpha,稳定版用户取 latest) */
34
35
  latest: string | null;
36
+ /** 稳定版 dist-tag */
37
+ stable: string | null;
38
+ /** alpha dist-tag */
39
+ alpha: string | null;
35
40
  hasUpdate: boolean;
36
41
  checkedAt: number;
37
42
  error?: string;
@@ -73,13 +78,24 @@ async function fetchDistTags(): Promise<Record<string, string>> {
73
78
 
74
79
  function buildUpdateInfo(tags: Record<string, string>): UpdateInfo {
75
80
  const currentIsPrerelease = CURRENT_VERSION.includes("-");
76
- const compareTarget = currentIsPrerelease
77
- ? (tags.alpha || tags.latest || null)
78
- : (tags.latest || null);
81
+ const stableTag = tags.latest || null;
82
+ const alphaTag = tags.alpha || null;
83
+
84
+ // 严格隔离:alpha 只跟 alpha 比,正式版只跟正式版比,不交叉
85
+ const compareTarget = currentIsPrerelease ? alphaTag : stableTag;
86
+
79
87
  const hasUpdate = typeof compareTarget === "string"
80
88
  && compareTarget !== CURRENT_VERSION
81
89
  && compareVersions(compareTarget, CURRENT_VERSION) > 0;
82
- return { current: CURRENT_VERSION, latest: compareTarget, hasUpdate, checkedAt: Date.now() };
90
+
91
+ return {
92
+ current: CURRENT_VERSION,
93
+ latest: compareTarget,
94
+ stable: stableTag,
95
+ alpha: alphaTag,
96
+ hasUpdate,
97
+ checkedAt: Date.now(),
98
+ };
83
99
  }
84
100
 
85
101
  /** gateway 启动时调用,保存 log 引用 */
@@ -104,7 +120,7 @@ export async function getUpdateInfo(): Promise<UpdateInfo> {
104
120
  return buildUpdateInfo(tags);
105
121
  } catch (err: any) {
106
122
  _log?.debug?.(`[qqbot:update-checker] check failed: ${err.message}`);
107
- return { current: CURRENT_VERSION, latest: null, hasUpdate: false, checkedAt: Date.now(), error: err.message };
123
+ return { current: CURRENT_VERSION, latest: null, stable: null, alpha: null, hasUpdate: false, checkedAt: Date.now(), error: err.message };
108
124
  }
109
125
  }
110
126