@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.
- package/README.md +6 -1
- package/README.zh.md +6 -1
- package/dist/src/api.js +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/gateway.js +1 -1
- package/dist/src/request-context.d.ts +7 -0
- package/dist/src/request-context.js +7 -0
- package/dist/src/slash-commands.js +362 -38
- package/dist/src/tools/remind.js +17 -9
- package/dist/src/types.d.ts +9 -2
- package/dist/src/update-checker.d.ts +3 -1
- package/dist/src/update-checker.js +13 -2
- package/dist/src/utils/pkg-version.js +19 -9
- package/package.json +4 -1
- package/scripts/postinstall-link-sdk.js +22 -9
- package/scripts/upgrade-via-npm.ps1 +9 -0
- package/scripts/upgrade-via-npm.sh +154 -30
- package/scripts/upgrade-via-source.sh +124 -38
- package/skills/qqbot-remind/SKILL.md +21 -11
- package/src/api.ts +1 -1
- package/src/config.ts +1 -1
- package/src/gateway.ts +1 -1
- package/src/request-context.ts +10 -0
- package/src/slash-commands.ts +354 -36
- package/src/tools/remind.ts +17 -9
- package/src/types.ts +9 -2
- package/src/update-checker.ts +14 -2
- package/src/utils/pkg-version.ts +18 -8
package/dist/src/tools/remind.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
9
|
+
/** 已定位到的 package.json 路径,避免重复遍历目录树 */
|
|
10
|
+
let _resolvedPkgPath = null;
|
|
10
11
|
export function getPackageVersion(metaUrl) {
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
25
|
-
return
|
|
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
|
-
|
|
42
|
-
return _cached;
|
|
53
|
+
return pkg.version;
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
catch { /* next */ }
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
catch { /* fallback */ }
|
|
49
|
-
|
|
50
|
-
return _cached;
|
|
60
|
+
return "unknown";
|
|
51
61
|
}
|
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
23
|
+
// Check if already a valid symlink pointing to a directory with plugin-sdk/core
|
|
30
24
|
if (existsSync(linkTarget)) {
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
228
|
+
NEEDS_TEMP_CONFIG=false
|
|
168
229
|
|
|
169
230
|
if [ -f "$CONFIG_FILE" ]; then
|
|
170
|
-
|
|
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
|
-
|
|
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 [ "$
|
|
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
|
-
|
|
185
|
-
if (
|
|
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="$
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|