@tencent-connect/openclaw-qqbot 1.6.6 → 1.6.7

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.
@@ -1,4 +1,4 @@
1
- import { getRequestTarget } from "../request-context.js";
1
+ import { getRequestTarget, getRequestAccountId } from "../request-context.js";
2
2
  // ========== JSON Schema ==========
3
3
  const RemindSchema = {
4
4
  type: "object",
@@ -100,7 +100,7 @@ function generateJobName(content) {
100
100
  /**
101
101
  * 构建一次性提醒的 cron 工具参数
102
102
  */
103
- function buildOnceJob(params, delayMs, to) {
103
+ function buildOnceJob(params, delayMs, to, accountId) {
104
104
  const atMs = Date.now() + delayMs;
105
105
  const content = params.content;
106
106
  const name = params.name || generateJobName(content);
@@ -115,9 +115,12 @@ function buildOnceJob(params, delayMs, to) {
115
115
  payload: {
116
116
  kind: "agentTurn",
117
117
  message: buildReminderPrompt(content),
118
- deliver: true,
118
+ },
119
+ delivery: {
120
+ mode: "announce",
119
121
  channel: "qqbot",
120
122
  to,
123
+ accountId,
121
124
  },
122
125
  },
123
126
  };
@@ -125,7 +128,7 @@ function buildOnceJob(params, delayMs, to) {
125
128
  /**
126
129
  * 构建周期提醒的 cron 工具参数
127
130
  */
128
- function buildCronJob(params, to) {
131
+ function buildCronJob(params, to, accountId) {
129
132
  const content = params.content;
130
133
  const name = params.name || generateJobName(content);
131
134
  const tz = params.timezone || "Asia/Shanghai";
@@ -139,9 +142,12 @@ function buildCronJob(params, to) {
139
142
  payload: {
140
143
  kind: "agentTurn",
141
144
  message: buildReminderPrompt(content),
142
- deliver: true,
145
+ },
146
+ delivery: {
147
+ mode: "announce",
143
148
  channel: "qqbot",
144
149
  to,
150
+ accountId,
145
151
  },
146
152
  },
147
153
  };
@@ -211,15 +217,17 @@ export function registerRemindTool(api) {
211
217
  if (!resolvedTo) {
212
218
  return json({ error: "action=add 时无法确定投递目标地址。请稍后重试。" });
213
219
  }
220
+ // 从请求上下文获取当前账户 ID(多账户场景),fallback 到 "default"
221
+ const resolvedAccountId = getRequestAccountId() || "default";
214
222
  if (!p.time) {
215
223
  return json({ error: "action=add 时 time(时间)为必填参数。示例:\"5m\"、\"1h30m\"、\"0 8 * * *\"" });
216
224
  }
217
225
  // 判断是 cron 表达式还是相对时间
218
226
  if (isCronExpression(p.time)) {
219
227
  // 周期提醒
220
- const cronJob = buildCronJob(p, resolvedTo);
228
+ const cronJob = buildCronJob(p, resolvedTo, resolvedAccountId);
221
229
  return json({
222
- _instruction: "请立即使用 cron 工具执行以下参数,然后回复用户提醒已设置",
230
+ _instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
223
231
  cronParams: cronJob,
224
232
  summary: `⏰ 周期提醒: "${p.content}" (${p.time}, tz=${p.timezone || "Asia/Shanghai"})`,
225
233
  });
@@ -236,9 +244,9 @@ export function registerRemindTool(api) {
236
244
  if (delayMs < 30_000) {
237
245
  return json({ error: "提醒时间不能少于 30 秒" });
238
246
  }
239
- const onceJob = buildOnceJob(p, delayMs, resolvedTo);
247
+ const onceJob = buildOnceJob(p, delayMs, resolvedTo, resolvedAccountId);
240
248
  return json({
241
- _instruction: "请立即使用 cron 工具执行以下参数,然后回复用户提醒已设置",
249
+ _instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
242
250
  cronParams: onceJob,
243
251
  summary: `⏰ ${formatDelay(delayMs)}后提醒: "${p.content}"`,
244
252
  });
@@ -92,10 +92,17 @@ export interface QQBotAccountConfig {
92
92
  upgradeUrl?: string;
93
93
  /**
94
94
  * /bot-upgrade 指令的行为模式
95
- * - "doc":展示升级文档链接(默认,安全模式)
96
- * - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新
95
+ * - "doc":展示升级文档链接(安全模式)
96
+ * - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新(默认)
97
97
  */
98
98
  upgradeMode?: "doc" | "hot-reload";
99
+ /**
100
+ * /bot-upgrade 热更新时使用的 npm 包名
101
+ * 支持 "scope/name"(自动补 @)或 "@scope/name" 格式
102
+ * 默认: "@tencent-connect/openclaw-qqbot"
103
+ * 示例: "ryantest/openclaw-qqbot"
104
+ */
105
+ upgradePkg?: string;
99
106
  /**
100
107
  * 出站消息合并回复(debounce)配置
101
108
  * 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
@@ -30,5 +30,7 @@ export declare function getUpdateInfo(): Promise<UpdateInfo>;
30
30
  /**
31
31
  * 检查指定版本是否存在于 npm registry
32
32
  * 用于 /bot-upgrade --version 的前置校验
33
+ * @param version 要检查的版本号
34
+ * @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
33
35
  */
34
- export declare function checkVersionExists(version: string): Promise<boolean>;
36
+ export declare function checkVersionExists(version: string, pkgName?: string): Promise<boolean>;
@@ -99,9 +99,12 @@ export async function getUpdateInfo() {
99
99
  /**
100
100
  * 检查指定版本是否存在于 npm registry
101
101
  * 用于 /bot-upgrade --version 的前置校验
102
+ * @param version 要检查的版本号
103
+ * @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
102
104
  */
103
- export async function checkVersionExists(version) {
104
- for (const baseUrl of REGISTRIES) {
105
+ export async function checkVersionExists(version, pkgName) {
106
+ const registries = pkgName ? buildRegistries(pkgName) : REGISTRIES;
107
+ for (const baseUrl of registries) {
105
108
  try {
106
109
  const url = `${baseUrl}/${version}`;
107
110
  const json = await fetchJson(url, 10_000);
@@ -114,6 +117,14 @@ export async function checkVersionExists(version) {
114
117
  }
115
118
  return false;
116
119
  }
120
+ /** 根据自定义包名构建 registry URL 列表 */
121
+ function buildRegistries(pkgName) {
122
+ const encoded = encodeURIComponent(pkgName);
123
+ return [
124
+ `https://registry.npmjs.org/${encoded}`,
125
+ `https://registry.npmmirror.com/${encoded}`,
126
+ ];
127
+ }
117
128
  function compareVersions(a, b) {
118
129
  const parse = (v) => {
119
130
  const clean = v.replace(/^v/, "");
@@ -6,10 +6,22 @@ import { fileURLToPath } from "node:url";
6
6
  import { createRequire } from "node:module";
7
7
  import path from "node:path";
8
8
  import fs from "node:fs";
9
- let _cached = null;
9
+ /** 已定位到的 package.json 路径,避免重复遍历目录树 */
10
+ let _resolvedPkgPath = null;
10
11
  export function getPackageVersion(metaUrl) {
11
- if (_cached !== null)
12
- return _cached;
12
+ // 如果之前已定位到 package.json 路径,直接重新读取(快速路径)
13
+ if (_resolvedPkgPath) {
14
+ try {
15
+ const pkg = JSON.parse(fs.readFileSync(_resolvedPkgPath, "utf8"));
16
+ if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
17
+ return pkg.version;
18
+ }
19
+ }
20
+ catch {
21
+ // 文件可能已被删除(升级过程中),清除路径缓存,走完整查找
22
+ _resolvedPkgPath = null;
23
+ }
24
+ }
13
25
  // Strategy 1: 从调用者的 import.meta.url(或本模块)向上遍历找 package.json
14
26
  const startFile = metaUrl ? fileURLToPath(metaUrl) : fileURLToPath(import.meta.url);
15
27
  let dir = path.dirname(startFile);
@@ -21,8 +33,8 @@ export function getPackageVersion(metaUrl) {
21
33
  const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
22
34
  // 确认是我们自己的包(避免找到其他 package.json)
23
35
  if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
24
- _cached = pkg.version;
25
- return _cached;
36
+ _resolvedPkgPath = candidate;
37
+ return pkg.version;
26
38
  }
27
39
  }
28
40
  }
@@ -38,14 +50,12 @@ export function getPackageVersion(metaUrl) {
38
50
  try {
39
51
  const pkg = require(rel);
40
52
  if (pkg?.version) {
41
- _cached = pkg.version;
42
- return _cached;
53
+ return pkg.version;
43
54
  }
44
55
  }
45
56
  catch { /* next */ }
46
57
  }
47
58
  }
48
59
  catch { /* fallback */ }
49
- _cached = "unknown";
50
- return _cached;
60
+ return "unknown";
51
61
  }
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.6",
3
+ "version": "1.6.7",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
4
7
  "type": "module",
5
8
  "main": "dist/index.js",
6
9
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  // globally installed openclaw package, allowing Node's native ESM resolver
11
11
  // (used by jiti with tryNative:true for .js files) to find `openclaw/plugin-sdk`.
12
12
 
13
- import { existsSync, symlinkSync, mkdirSync, realpathSync } from "node:fs";
13
+ import { existsSync, lstatSync, symlinkSync, unlinkSync, rmSync, mkdirSync, realpathSync } from "node:fs";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { execSync } from "node:child_process";
@@ -18,17 +18,30 @@ import { execSync } from "node:child_process";
18
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
19
  const pluginRoot = resolve(__dirname, "..");
20
20
 
21
- // Only run when installed under an openclaw-like extensions directory
22
- // (supports openclaw, clawdbot, moltbot, etc.)
23
- if (!pluginRoot.includes("extensions")) {
24
- process.exit(0);
25
- }
26
-
27
21
  const linkTarget = join(pluginRoot, "node_modules", "openclaw");
28
22
 
29
- // Already linked or exists
23
+ // Check if already a valid symlink pointing to a directory with plugin-sdk/core
30
24
  if (existsSync(linkTarget)) {
31
- process.exit(0);
25
+ try {
26
+ const stat = lstatSync(linkTarget);
27
+ if (stat.isSymbolicLink()) {
28
+ // Symlink exists — verify it has plugin-sdk/core
29
+ if (existsSync(join(linkTarget, "plugin-sdk", "core.js"))) {
30
+ process.exit(0);
31
+ }
32
+ // Symlink is stale or points to wrong target, remove and re-create
33
+ unlinkSync(linkTarget);
34
+ } else if (existsSync(join(linkTarget, "plugin-sdk", "core.js"))) {
35
+ // Real directory with correct structure (e.g. npm installed a good version)
36
+ process.exit(0);
37
+ } else {
38
+ // Real directory from npm install but missing plugin-sdk/core — replace with symlink
39
+ rmSync(linkTarget, { recursive: true, force: true });
40
+ }
41
+ } catch {
42
+ // If stat fails, try to remove and re-create
43
+ try { rmSync(linkTarget, { recursive: true, force: true }); } catch {}
44
+ }
32
45
  }
33
46
 
34
47
  // CLI names to try (openclaw and its aliases)
@@ -17,6 +17,7 @@ param(
17
17
  [string]$Secret = "",
18
18
  [switch]$NoRestart,
19
19
  [string]$Tag = "",
20
+ [string]$Pkg = "",
20
21
  [switch]$Help
21
22
  )
22
23
 
@@ -25,6 +26,13 @@ $PKG_NAME = "@tencent-connect/openclaw-qqbot"
25
26
  $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition
26
27
  $PROJECT_DIR = Split-Path -Parent $SCRIPT_DIR
27
28
 
29
+ # -Pkg 覆盖包名(支持 "scope/name" 自动补 @)
30
+ if ($Pkg) {
31
+ $Pkg = $Pkg.Trim()
32
+ if (-not $Pkg.StartsWith("@")) { $Pkg = "@$Pkg" }
33
+ $PKG_NAME = $Pkg
34
+ }
35
+
28
36
  # Read local version
29
37
  $LOCAL_VERSION = ""
30
38
  try {
@@ -41,6 +49,7 @@ if ($Help) {
41
49
  Write-Host " .\upgrade-via-npm.ps1 -Version [version] # upgrade to specific version"
42
50
  Write-Host " .\upgrade-via-npm.ps1 -SelfVersion # upgrade to repo version ($LOCAL_VERSION)"
43
51
  Write-Host ""
52
+ Write-Host " -Pkg [scope/name] Custom npm package (e.g. ryantest/openclaw-qqbot)"
44
53
  Write-Host " -AppId [appid] QQ bot appid (required on first install)"
45
54
  Write-Host " -Secret [secret] QQ bot secret (required on first install)"
46
55
  exit 0
@@ -18,14 +18,68 @@
18
18
 
19
19
  set -eo pipefail
20
20
 
21
- # 异常退出时清理临时配置文件(防止泄露或残留)
21
+ # ⚠️ 必须在 cd 之前解析脚本路径,否则相对路径的 $0 在 cd 后无法正确解析
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
24
+
25
+ # 确保 cwd 是一个存在的目录。
26
+ # 当从 gateway 进程 fork 时,继承的 cwd 可能已被删除(如旧插件目录被 mv/rm),
27
+ # 导致 openclaw CLI 启动时 process.cwd() 报 ENOENT: uv_cwd 错误。
28
+ cd "$HOME" 2>/dev/null || cd / 2>/dev/null || true
29
+
30
+ # 异常退出时清理临时文件并回滚(防止泄露或残留)
31
+ INSTALL_COMPLETED=false # 标记 install 是否已完成(用于区分正常退出和异常退出)
22
32
  cleanup_on_exit() {
33
+ local exit_code=$?
34
+
35
+ # 异常退出时同步临时配置中的 install 记录回真实配置
23
36
  if [ -n "$TEMP_CONFIG_FILE" ] && [ -f "$TEMP_CONFIG_FILE" ]; then
37
+ # 尝试同步 install 记录(即使异常退出也要保留)
38
+ node -e "
39
+ try {
40
+ const fs = require('fs');
41
+ const tmp = JSON.parse(fs.readFileSync('$TEMP_CONFIG_FILE', 'utf8'));
42
+ const real = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
43
+ if (tmp.plugins && tmp.plugins.installs) {
44
+ if (!real.plugins) real.plugins = {};
45
+ real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
46
+ }
47
+ if (tmp.plugins && tmp.plugins.entries) {
48
+ if (!real.plugins) real.plugins = {};
49
+ real.plugins.entries = { ...(real.plugins.entries || {}), ...tmp.plugins.entries };
50
+ }
51
+ fs.writeFileSync('$CONFIG_FILE', JSON.stringify(real, null, 4) + '\n');
52
+ } catch {}
53
+ " 2>/dev/null || true
24
54
  rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true
25
55
  fi
56
+
57
+ # 异常退出且 install 未完成时,回滚备份目录(而非删除)
58
+ if [ "$INSTALL_COMPLETED" != "true" ] && [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
59
+ if [ ! -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] || [ ! -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
60
+ # 插件目录不存在或不完整,回滚
61
+ rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
62
+ mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || \
63
+ mv "$BACKUP_DIR"/* "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
64
+ echo " ↩️ [cleanup] 异常退出,已回滚到旧版本"
65
+ else
66
+ # 插件目录完整,清理备份
67
+ rm -rf "$BACKUP_DIR" 2>/dev/null || true
68
+ fi
69
+ elif [ "$INSTALL_COMPLETED" = "true" ] && [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
70
+ # 正常完成,清理备份
71
+ rm -rf "$BACKUP_DIR" 2>/dev/null || true
72
+ fi
73
+
74
+ # 清理 openclaw install 可能残留的暂存目录(extensions 和 /tmp 中都可能存在)
75
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
76
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
26
77
  }
27
78
  trap cleanup_on_exit EXIT
28
79
 
80
+ # 清理上次升级可能遗留的备份目录(如上次脚本被 kill 等极端情况)
81
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -exec rm -rf {} + 2>/dev/null || true
82
+
29
83
  PKG_NAME="@tencent-connect/openclaw-qqbot"
30
84
  PLUGIN_ID="openclaw-qqbot"
31
85
  INSTALL_SRC=""
@@ -33,8 +87,6 @@ TARGET_VERSION=""
33
87
  APPID=""
34
88
  SECRET=""
35
89
  NO_RESTART=false
36
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
37
- PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
38
90
 
39
91
  LOCAL_VERSION="$(node -e "
40
92
  try {
@@ -56,6 +108,7 @@ print_usage() {
56
108
  echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本"
57
109
  fi
58
110
  echo ""
111
+ echo " --pkg <scope/name> 指定 npm 包名(如 ryantest/openclaw-qqbot)"
59
112
  echo " --appid <appid> QQ机器人 appid(首次安装时必填)"
60
113
  echo " --secret <secret> QQ机器人 secret(首次安装时必填)"
61
114
  echo ""
@@ -69,22 +122,17 @@ while [[ $# -gt 0 ]]; do
69
122
  case "$1" in
70
123
  --tag)
71
124
  [ -z "$2" ] && echo "❌ --tag 需要参数" && exit 1
72
- _ver="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
73
- TARGET_VERSION="$_ver"
74
- INSTALL_SRC="${PKG_NAME}@$_ver"
125
+ TARGET_VERSION="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
75
126
  shift 2
76
127
  ;;
77
128
  --version)
78
129
  [ -z "$2" ] && echo "❌ --version 需要参数" && exit 1
79
- _ver="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
80
- TARGET_VERSION="$_ver"
81
- INSTALL_SRC="${PKG_NAME}@$_ver"
130
+ TARGET_VERSION="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
82
131
  shift 2
83
132
  ;;
84
133
  --self-version)
85
134
  [ -z "$LOCAL_VERSION" ] && echo "❌ 无法从 package.json 读取版本" && exit 1
86
135
  TARGET_VERSION="$LOCAL_VERSION"
87
- INSTALL_SRC="${PKG_NAME}@${LOCAL_VERSION}"
88
136
  shift 1
89
137
  ;;
90
138
  --appid)
@@ -97,6 +145,14 @@ while [[ $# -gt 0 ]]; do
97
145
  SECRET="$2"
98
146
  shift 2
99
147
  ;;
148
+ --pkg)
149
+ [ -z "$2" ] && echo "❌ --pkg 需要参数" && exit 1
150
+ _pkg="$2"
151
+ # 支持 "scope/name" 自动补 @
152
+ if [[ "$_pkg" != @* ]]; then _pkg="@$_pkg"; fi
153
+ PKG_NAME="$_pkg"
154
+ shift 2
155
+ ;;
100
156
  --no-restart)
101
157
  NO_RESTART=true
102
158
  shift 1
@@ -108,7 +164,12 @@ while [[ $# -gt 0 ]]; do
108
164
  *) echo "未知选项: $1"; print_usage; exit 1 ;;
109
165
  esac
110
166
  done
111
- INSTALL_SRC="${INSTALL_SRC:-${PKG_NAME}@latest}"
167
+ # 参数解析完毕后统一拼接 INSTALL_SRC(确保 --pkg 无论在 --version 前后都能生效)
168
+ if [ -n "$TARGET_VERSION" ]; then
169
+ INSTALL_SRC="${PKG_NAME}@${TARGET_VERSION}"
170
+ else
171
+ INSTALL_SRC="${PKG_NAME}@latest"
172
+ fi
112
173
 
113
174
  # 环境变量 fallback
114
175
  APPID="${APPID:-$QQBOT_APPID}"
@@ -164,30 +225,46 @@ echo "[1/4] 安装/升级插件..."
164
225
  # 环境变量让 plugins install/update 使用临时配置,真实配置文件不受影响。
165
226
  CONFIG_FILE="$HOME/.$CMD/$CMD.json"
166
227
  TEMP_CONFIG_FILE=""
167
- HAS_QQBOT_CHANNEL=false
228
+ NEEDS_TEMP_CONFIG=false
168
229
 
169
230
  if [ -f "$CONFIG_FILE" ]; then
170
- HAS_QQBOT_CHANNEL="$(node -e "
231
+ NEEDS_TEMP_CONFIG="$(node -e "
171
232
  try {
172
233
  const fs = require('fs');
173
234
  const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
174
- if (cfg.channels && cfg.channels.qqbot) process.stdout.write('true');
235
+ const hasChannel = !!(cfg.channels && cfg.channels.qqbot);
236
+ const hasAllow = Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.includes('$PLUGIN_ID');
237
+ const hasEntry = !!(cfg.plugins?.entries?.['$PLUGIN_ID']);
238
+ if (hasChannel || hasAllow || hasEntry) process.stdout.write('true');
175
239
  } catch {}
176
240
  " 2>/dev/null || true)"
177
241
 
178
- if [ "$HAS_QQBOT_CHANNEL" = "true" ]; then
242
+ if [ "$NEEDS_TEMP_CONFIG" = "true" ]; then
179
243
  TEMP_CONFIG_FILE="$(mktemp)"
180
244
  node -e "
181
245
  try {
182
246
  const fs = require('fs');
183
247
  const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
184
- delete cfg.channels.qqbot;
185
- if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
248
+ // 移除 channels.qqbot(插件自定义通道,校验时会 unknown channel id)
249
+ if (cfg.channels?.qqbot) {
250
+ delete cfg.channels.qqbot;
251
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
252
+ }
253
+ // 移除 plugins.allow 中的 openclaw-qqbot(插件目录被备份后校验找不到)
254
+ if (Array.isArray(cfg.plugins?.allow)) {
255
+ cfg.plugins.allow = cfg.plugins.allow.filter(p => p !== '$PLUGIN_ID');
256
+ if (cfg.plugins.allow.length === 0) delete cfg.plugins.allow;
257
+ }
258
+ // 移除 plugins.entries 中的 openclaw-qqbot(同理)
259
+ if (cfg.plugins?.entries?.['$PLUGIN_ID']) {
260
+ delete cfg.plugins.entries['$PLUGIN_ID'];
261
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
262
+ }
186
263
  fs.writeFileSync('$TEMP_CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
187
264
  } catch(e) { process.exit(1); }
188
265
  " 2>/dev/null
189
266
  if [ $? -eq 0 ]; then
190
- echo " [兼容] 创建临时配置副本(不含 channels.qqbot)以通过配置校验"
267
+ echo " [兼容] 创建临时配置副本(不含 channels.qqbot / plugins.allow / plugins.entries)以通过配置校验"
191
268
  export OPENCLAW_CONFIG_PATH="$TEMP_CONFIG_FILE"
192
269
  else
193
270
  echo " ⚠️ 创建临时配置失败,继续使用原配置"
@@ -200,22 +277,32 @@ fi
200
277
  # plugins install/update 可能把 install 记录写入了临时配置,需要同步回真实配置
201
278
  restore_qqbot_channel() {
202
279
  if [ -n "$TEMP_CONFIG_FILE" ] && [ -f "$TEMP_CONFIG_FILE" ]; then
203
- # 将临时配置中 plugins.installs 的变更同步回真实配置
280
+ # 将临时配置中 plugins.installs 和 plugins.entries 的变更同步回真实配置
204
281
  node -e "
205
282
  try {
206
283
  const fs = require('fs');
207
284
  const tmp = JSON.parse(fs.readFileSync('$TEMP_CONFIG_FILE', 'utf8'));
208
285
  const real = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
286
+ let changed = false;
209
287
  if (tmp.plugins && tmp.plugins.installs) {
210
288
  if (!real.plugins) real.plugins = {};
211
289
  real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
290
+ changed = true;
291
+ }
292
+ // 同步 plugins.entries(openclaw plugins install 会写入 entries)
293
+ if (tmp.plugins && tmp.plugins.entries) {
294
+ if (!real.plugins) real.plugins = {};
295
+ real.plugins.entries = { ...(real.plugins.entries || {}), ...tmp.plugins.entries };
296
+ changed = true;
297
+ }
298
+ if (changed) {
212
299
  fs.writeFileSync('$CONFIG_FILE', JSON.stringify(real, null, 4) + '\n');
213
300
  }
214
301
  } catch {}
215
302
  " 2>/dev/null || true
216
303
  rm -f "$TEMP_CONFIG_FILE"
217
304
  unset OPENCLAW_CONFIG_PATH
218
- echo " [兼容] 已同步 install 记录并清理临时配置副本"
305
+ echo " [兼容] 已同步 install/entries 记录并清理临时配置副本"
219
306
  fi
220
307
  }
221
308
 
@@ -291,7 +378,7 @@ if [ "$UPGRADE_OK" != "true" ]; then
291
378
  # 备份旧目录(而非直接删除),install 失败时可回滚
292
379
  BACKUP_DIR=""
293
380
  if [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ]; then
294
- BACKUP_DIR="$EXTENSIONS_DIR/.openclaw-qqbot-backup-$$"
381
+ BACKUP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-upgrade-backup-XXXXXX")"
295
382
  mv "$EXTENSIONS_DIR/$PLUGIN_ID" "$BACKUP_DIR"
296
383
  echo " 已备份旧目录: $BACKUP_DIR"
297
384
  fi
@@ -304,20 +391,57 @@ if [ "$UPGRADE_OK" != "true" ]; then
304
391
  echo " 执行 install: $INSTALL_SRC"
305
392
 
306
393
  if $CMD plugins install "$INSTALL_SRC" --pin 2>&1; then
307
- UPGRADE_OK=true
308
- echo " install 成功"
309
- # install 成功,清理备份
310
- if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
311
- rm -rf "$BACKUP_DIR"
312
- echo " 已清理旧版备份"
394
+ # install 返回 0,但需要验证插件目录是否真的存在且完整
395
+ if [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
396
+ UPGRADE_OK=true
397
+ INSTALL_COMPLETED=true
398
+ echo " ✅ install 成功"
399
+ # install 成功,清理备份
400
+ if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
401
+ rm -rf "$BACKUP_DIR"
402
+ echo " 已清理旧版备份"
403
+ fi
404
+ # 清理 openclaw CLI install 可能留下的额外 backup 目录(extensions 内遗留 + 新路径)
405
+ find "$EXTENSIONS_DIR" -maxdepth 1 -name ".openclaw-qqbot-backup-*" -exec rm -rf {} + 2>/dev/null || true
406
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
407
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
408
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -exec rm -rf {} + 2>/dev/null || true
409
+ else
410
+ echo " ❌ install 命令返回成功但插件目录不完整"
411
+ echo " [诊断] 目录存在: $([ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && echo '是' || echo '否')"
412
+ echo " [诊断] package.json 存在: $([ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && echo '是' || echo '否')"
413
+ # 清理可能残留的暂存目录(extensions 和 /tmp 中都可能存在)
414
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
415
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
416
+ # 回滚
417
+ if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
418
+ rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
419
+ # 备份目录内可能是 PLUGIN_ID 子目录或直接是内容
420
+ if [ -d "$BACKUP_DIR/$PLUGIN_ID" ]; then
421
+ mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID"
422
+ else
423
+ mv "$BACKUP_DIR" "$EXTENSIONS_DIR/$PLUGIN_ID"
424
+ fi
425
+ echo " ↩️ 已回滚到旧版本"
426
+ fi
427
+ restore_qqbot_channel
428
+ echo "QQBOT_NEW_VERSION=unknown"
429
+ echo "QQBOT_REPORT=❌ QQBot 安装异常(目录不完整,已回滚),请重试或手动安装"
430
+ exit 1
313
431
  fi
314
- # 清理 openclaw CLI install 可能留下的额外 backup 目录
315
- find "$EXTENSIONS_DIR" -maxdepth 1 -name ".openclaw-qqbot-backup-*" -exec rm -rf {} + 2>/dev/null || true
316
432
  else
317
433
  echo " ❌ install 失败"
434
+ # 清理可能残留的暂存目录(extensions 和 /tmp 中都可能存在)
435
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
436
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
318
437
  # 回滚:恢复旧目录
319
438
  if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
320
- mv "$BACKUP_DIR" "$EXTENSIONS_DIR/$PLUGIN_ID"
439
+ rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
440
+ if [ -d "$BACKUP_DIR/$PLUGIN_ID" ]; then
441
+ mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID"
442
+ else
443
+ mv "$BACKUP_DIR" "$EXTENSIONS_DIR/$PLUGIN_ID"
444
+ fi
321
445
  echo " ↩️ 已回滚到旧版本"
322
446
  fi
323
447
  restore_qqbot_channel