lightclawbot 1.1.2-beta.0 → 1.1.2

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,11 +1,11 @@
1
1
  {
2
- "generatedAt": "2026-04-09T11:45:37.286Z",
2
+ "generatedAt": "2026-04-10T11:27:01.831Z",
3
3
  "cdnBase": "https://cloudcache.tencent-cloud.com/qcloud/tea/app/data/scripts",
4
4
  "files": {
5
- "upgrade.sh": {
6
- "hashed": "upgrade.156d7999.sh",
7
- "hash": "156d7999",
8
- "size": 13296
5
+ "preflight.sh": {
6
+ "hashed": "preflight.9af62b39.sh",
7
+ "hash": "9af62b39",
8
+ "size": 7262
9
9
  }
10
10
  }
11
11
  }
@@ -0,0 +1,191 @@
1
+ #!/bin/bash
2
+
3
+ # ========== lightclawbot 插件前置检查脚本 ==========
4
+ # 用途:快速判断 lightclawbot 插件是否可用
5
+ # 特点:
6
+ # 1. 减少文件 I/O 次数(一次读取配置文件,复用解析结果)
7
+ # 2. 与 OpenClaw 源码逻辑保持一致(plugins.enabled/deny/allow/entries)
8
+ # 3. 支持传参指定插件 ID
9
+ #
10
+ # 输出:最后一行固定为 RESULT:{...} JSON,供前端解析
11
+ # - 可用: RESULT:{"status":"ok","version":"x.y.z"}
12
+ # - 缺 openclaw:RESULT:{"status":"need_openclaw","reason":"...","desc":"..."}
13
+ # - 需要安装/配置:RESULT:{"status":"need_install","reason":"...","desc":"..."}
14
+ # - reason: 固定错误码(前端用于判断分支)
15
+ # - desc: 检测命令的实际输出(可选,用于排查问题)
16
+ #
17
+ # 使用方式:
18
+ # 本地执行: bash preflight.sh [plugin_id]
19
+ # CDN 执行: bash <(curl -fsSL https://your-cdn.com/preflight.sh) [plugin_id]
20
+ # 默认检查: lightclawbot
21
+
22
+ # ---------- 配置 ----------
23
+ id="${1:-lightclawbot}" # 支持传参指定插件 ID
24
+ cfg="$HOME/.openclaw/openclaw.json"
25
+ plugin_dir="$HOME/.openclaw/extensions/${id}"
26
+
27
+ # ---------- 加载 nvm 环境 ----------
28
+ # bash xxx.sh 启动的是非交互式 shell,不会自动 source ~/.bashrc,
29
+ # 如果用户通过 nvm 安装 node,脚本中的 node / openclaw 命令都找不到。
30
+ # 提前加载 nvm,让后续所有步骤都能使用 nvm 管理的 node。
31
+ export NVM_DIR="$HOME/.nvm"
32
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
33
+
34
+ # ---------- 工具函数 ----------
35
+ result_exit() {
36
+ local status="$1"
37
+ local reason="$2"
38
+ local desc="${3:-}"
39
+ local exit_code="${4:-1}"
40
+ # 转义 desc 中的特殊字符,保证输出合法 JSON
41
+ desc="${desc//\\/\\\\}"
42
+ desc="${desc//\"/\\\"}"
43
+ desc="${desc//$'\n'/\\n}"
44
+ desc="${desc//$'\r'/}"
45
+ if [ -n "$desc" ]; then
46
+ echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\",\"desc\":\"${desc}\"}"
47
+ else
48
+ echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\"}"
49
+ fi
50
+ exit "$exit_code"
51
+ }
52
+
53
+ result_ok() {
54
+ echo "RESULT:{\"status\":\"ok\",\"version\":\"${1}\"}"
55
+ exit 0
56
+ }
57
+
58
+ # ---------- 1. 检查配置文件是否存在 ----------
59
+
60
+ if [ ! -f "$cfg" ]; then
61
+ result_exit "error" "config_not_found" "$(ls "$cfg" 2>&1)"
62
+ fi
63
+
64
+ # ---------- 2. 验证配置文件 JSON 格式 ----------
65
+ # openclaw 通过即 node 可用,用 node 做一次轻量校验
66
+
67
+ json_check_output=$(node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$cfg" 2>&1)
68
+ if [ $? -ne 0 ]; then
69
+ result_exit "error" "config_json_invalid" "$json_check_output"
70
+ fi
71
+
72
+ # ---------- 3. 检查 openclaw 是否可用 ----------
73
+
74
+ if ! command -v openclaw &>/dev/null; then
75
+ oc_which_output=$(which openclaw 2>&1) || true
76
+ result_exit "error" "openclaw_not_available" "${oc_which_output:-openclaw not found in PATH=${PATH}}"
77
+ fi
78
+
79
+ # ---------- 3a. 检查 openclaw 版本是否满足最低要求 ----------
80
+
81
+ min_version="2026.2.3"
82
+ # 兼容多种输出格式:
83
+ # "OpenClaw 2026.3.24 (cff6dc9)" → 2026.3.24
84
+ # "2026.2.3" → 2026.2.3
85
+ oc_version_output=$(openclaw -v 2>&1)
86
+ current_version=$(echo "$oc_version_output" | head -1 | sed 's/[()]//g' | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -1)
87
+
88
+ if [ -z "$current_version" ]; then
89
+ result_exit "error" "openclaw_version_unknown" "$oc_version_output"
90
+ fi
91
+
92
+ # 比较版本号:将 a.b.c 转为数值后逐段比较
93
+ version_lt() {
94
+ local IFS='.'
95
+ local i a=($1) b=($2)
96
+ for ((i = 0; i < 3; i++)); do
97
+ if (( ${a[i]:-0} < ${b[i]:-0} )); then return 0; fi
98
+ if (( ${a[i]:-0} > ${b[i]:-0} )); then return 1; fi
99
+ done
100
+ return 1 # 相等时不算小于
101
+ }
102
+
103
+ if version_lt "$current_version" "$min_version"; then
104
+ result_exit "error" "openclaw_version_too_old" "current: ${current_version}, required: >=${min_version}"
105
+ fi
106
+
107
+ # ---------- 3b. 检查 Gateway 是否可用 ----------
108
+
109
+ gateway_output=$(openclaw gateway health --json 2>&1)
110
+ if [ $? -ne 0 ]; then
111
+ result_exit "need_install" "gateway_not_healthy" "$gateway_output"
112
+ fi
113
+
114
+ # ---------- 4. 检查插件目录是否已安装 ----------
115
+
116
+ if [ ! -d "$plugin_dir" ] || [ ! -f "$plugin_dir/package.json" ]; then
117
+ result_exit "need_install" "plugin_not_installed" "$(ls -la "$plugin_dir/package.json" 2>&1)"
118
+ fi
119
+
120
+ # ---------- 5. 单次 node 调用完成所有配置项检查 ----------
121
+ # 一次启动 V8,顺序执行全部校验
122
+ # 通过环境变量安全传参,避免 shell 注入风险
123
+ # 输出格式:RESULT:{...} 与 bash 工具函数保持一致
124
+
125
+ PREFLIGHT_CFG="$cfg" PREFLIGHT_ID="$id" PREFLIGHT_PLUGIN_DIR="$plugin_dir" \
126
+ node -e '
127
+ const fs = require("fs");
128
+ const cfgPath = process.env.PREFLIGHT_CFG;
129
+ const pluginId = process.env.PREFLIGHT_ID;
130
+ const pluginDir = process.env.PREFLIGHT_PLUGIN_DIR;
131
+
132
+ function fail(status, reason, desc) {
133
+ const obj = { status, reason };
134
+ if (desc !== undefined) obj.desc = String(desc);
135
+ console.log("RESULT:" + JSON.stringify(obj));
136
+ process.exit(1);
137
+ }
138
+
139
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
140
+
141
+ // 5a. 全局插件系统检查(plugins.enabled 默认 true,仅显式 false 禁用)
142
+ if (cfg.plugins?.enabled === false) {
143
+ fail("need_install", "plugins_globally_disabled", "plugins.enabled = " + JSON.stringify(cfg.plugins?.enabled));
144
+ }
145
+
146
+ // 5b. deny 黑名单(优先级高于 allow,与 OpenClaw 源码一致)
147
+ if (Array.isArray(cfg.plugins?.deny) && cfg.plugins.deny.includes(pluginId)) {
148
+ fail("need_install", "plugin_in_denylist", "plugins.deny = " + JSON.stringify(cfg.plugins.deny));
149
+ }
150
+
151
+ // 5c. plugins.entries 启用状态
152
+ if (cfg.plugins?.entries?.[pluginId]?.enabled !== true) {
153
+ fail("need_install", "plugin_not_enabled", "plugins.entries." + pluginId + ".enabled = " + JSON.stringify(cfg.plugins?.entries?.[pluginId]?.enabled));
154
+ }
155
+
156
+ // 5d. allow 白名单(非空时必须在列表中)
157
+ const allow = cfg.plugins?.allow;
158
+ if (Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId)) {
159
+ fail("need_install", "plugin_not_in_allowlist", "plugins.allow = " + JSON.stringify(allow));
160
+ }
161
+
162
+ // 5e. plugins.installs 安装记录(可选,仅警告,默认不阻塞)
163
+ // 如需严格检查,取消下面的注释:
164
+ // if (!cfg.plugins?.installs?.[pluginId]) {
165
+ // fail("need_install", "plugin_install_record_missing", "plugins.installs." + pluginId + " = " + JSON.stringify(cfg.plugins?.installs?.[pluginId]));
166
+ // }
167
+
168
+ // 5f. channels 启用状态
169
+ if (cfg.channels?.[pluginId]?.enabled !== true) {
170
+ fail("need_install", "channel_not_enabled", "channels." + pluginId + ".enabled = " + JSON.stringify(cfg.channels?.[pluginId]?.enabled));
171
+ }
172
+
173
+ // 5g. channels apiKeys 配置
174
+ const apiKeys = cfg.channels?.[pluginId]?.apiKeys;
175
+ if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
176
+ fail("need_install", "apikeys_empty", "channels." + pluginId + ".apiKeys = " + JSON.stringify(apiKeys));
177
+ }
178
+
179
+ // 5h. 读取插件版本
180
+ let version = "unknown";
181
+ try {
182
+ const pkg = JSON.parse(fs.readFileSync(pluginDir + "/package.json", "utf8"));
183
+ version = pkg.version || "unknown";
184
+ } catch (e) {}
185
+
186
+ // 全部通过
187
+ console.log("RESULT:" + JSON.stringify({ status: "ok", version }));
188
+ '
189
+
190
+ # 将 node 的退出码传递给 bash
191
+ exit $?
@@ -0,0 +1,191 @@
1
+ #!/bin/bash
2
+
3
+ # ========== lightclawbot 插件前置检查脚本 ==========
4
+ # 用途:快速判断 lightclawbot 插件是否可用
5
+ # 特点:
6
+ # 1. 减少文件 I/O 次数(一次读取配置文件,复用解析结果)
7
+ # 2. 与 OpenClaw 源码逻辑保持一致(plugins.enabled/deny/allow/entries)
8
+ # 3. 支持传参指定插件 ID
9
+ #
10
+ # 输出:最后一行固定为 RESULT:{...} JSON,供前端解析
11
+ # - 可用: RESULT:{"status":"ok","version":"x.y.z"}
12
+ # - 缺 openclaw:RESULT:{"status":"need_openclaw","reason":"...","desc":"..."}
13
+ # - 需要安装/配置:RESULT:{"status":"need_install","reason":"...","desc":"..."}
14
+ # - reason: 固定错误码(前端用于判断分支)
15
+ # - desc: 检测命令的实际输出(可选,用于排查问题)
16
+ #
17
+ # 使用方式:
18
+ # 本地执行: bash preflight.sh [plugin_id]
19
+ # CDN 执行: bash <(curl -fsSL https://your-cdn.com/preflight.sh) [plugin_id]
20
+ # 默认检查: lightclawbot
21
+
22
+ # ---------- 配置 ----------
23
+ id="${1:-lightclawbot}" # 支持传参指定插件 ID
24
+ cfg="$HOME/.openclaw/openclaw.json"
25
+ plugin_dir="$HOME/.openclaw/extensions/${id}"
26
+
27
+ # ---------- 加载 nvm 环境 ----------
28
+ # bash xxx.sh 启动的是非交互式 shell,不会自动 source ~/.bashrc,
29
+ # 如果用户通过 nvm 安装 node,脚本中的 node / openclaw 命令都找不到。
30
+ # 提前加载 nvm,让后续所有步骤都能使用 nvm 管理的 node。
31
+ export NVM_DIR="$HOME/.nvm"
32
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
33
+
34
+ # ---------- 工具函数 ----------
35
+ result_exit() {
36
+ local status="$1"
37
+ local reason="$2"
38
+ local desc="${3:-}"
39
+ local exit_code="${4:-1}"
40
+ # 转义 desc 中的特殊字符,保证输出合法 JSON
41
+ desc="${desc//\\/\\\\}"
42
+ desc="${desc//\"/\\\"}"
43
+ desc="${desc//$'\n'/\\n}"
44
+ desc="${desc//$'\r'/}"
45
+ if [ -n "$desc" ]; then
46
+ echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\",\"desc\":\"${desc}\"}"
47
+ else
48
+ echo "RESULT:{\"status\":\"${status}\",\"reason\":\"${reason}\"}"
49
+ fi
50
+ exit "$exit_code"
51
+ }
52
+
53
+ result_ok() {
54
+ echo "RESULT:{\"status\":\"ok\",\"version\":\"${1}\"}"
55
+ exit 0
56
+ }
57
+
58
+ # ---------- 1. 检查配置文件是否存在 ----------
59
+
60
+ if [ ! -f "$cfg" ]; then
61
+ result_exit "error" "config_not_found" "$(ls "$cfg" 2>&1)"
62
+ fi
63
+
64
+ # ---------- 2. 验证配置文件 JSON 格式 ----------
65
+ # openclaw 通过即 node 可用,用 node 做一次轻量校验
66
+
67
+ json_check_output=$(node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$cfg" 2>&1)
68
+ if [ $? -ne 0 ]; then
69
+ result_exit "error" "config_json_invalid" "$json_check_output"
70
+ fi
71
+
72
+ # ---------- 3. 检查 openclaw 是否可用 ----------
73
+
74
+ if ! command -v openclaw &>/dev/null; then
75
+ oc_which_output=$(which openclaw 2>&1) || true
76
+ result_exit "error" "openclaw_not_available" "${oc_which_output:-openclaw not found in PATH=${PATH}}"
77
+ fi
78
+
79
+ # ---------- 3a. 检查 openclaw 版本是否满足最低要求 ----------
80
+
81
+ min_version="2026.2.3"
82
+ # 兼容多种输出格式:
83
+ # "OpenClaw 2026.3.24 (cff6dc9)" → 2026.3.24
84
+ # "2026.2.3" → 2026.2.3
85
+ oc_version_output=$(openclaw -v 2>&1)
86
+ current_version=$(echo "$oc_version_output" | head -1 | sed 's/[()]//g' | grep -oE '[0-9]{4}\.[0-9]+\.[0-9]+' | head -1)
87
+
88
+ if [ -z "$current_version" ]; then
89
+ result_exit "error" "openclaw_version_unknown" "$oc_version_output"
90
+ fi
91
+
92
+ # 比较版本号:将 a.b.c 转为数值后逐段比较
93
+ version_lt() {
94
+ local IFS='.'
95
+ local i a=($1) b=($2)
96
+ for ((i = 0; i < 3; i++)); do
97
+ if (( ${a[i]:-0} < ${b[i]:-0} )); then return 0; fi
98
+ if (( ${a[i]:-0} > ${b[i]:-0} )); then return 1; fi
99
+ done
100
+ return 1 # 相等时不算小于
101
+ }
102
+
103
+ if version_lt "$current_version" "$min_version"; then
104
+ result_exit "error" "openclaw_version_too_old" "current: ${current_version}, required: >=${min_version}"
105
+ fi
106
+
107
+ # ---------- 3b. 检查 Gateway 是否可用 ----------
108
+
109
+ gateway_output=$(openclaw gateway health --json 2>&1)
110
+ if [ $? -ne 0 ]; then
111
+ result_exit "need_install" "gateway_not_healthy" "$gateway_output"
112
+ fi
113
+
114
+ # ---------- 4. 检查插件目录是否已安装 ----------
115
+
116
+ if [ ! -d "$plugin_dir" ] || [ ! -f "$plugin_dir/package.json" ]; then
117
+ result_exit "need_install" "plugin_not_installed" "$(ls -la "$plugin_dir/package.json" 2>&1)"
118
+ fi
119
+
120
+ # ---------- 5. 单次 node 调用完成所有配置项检查 ----------
121
+ # 一次启动 V8,顺序执行全部校验
122
+ # 通过环境变量安全传参,避免 shell 注入风险
123
+ # 输出格式:RESULT:{...} 与 bash 工具函数保持一致
124
+
125
+ PREFLIGHT_CFG="$cfg" PREFLIGHT_ID="$id" PREFLIGHT_PLUGIN_DIR="$plugin_dir" \
126
+ node -e '
127
+ const fs = require("fs");
128
+ const cfgPath = process.env.PREFLIGHT_CFG;
129
+ const pluginId = process.env.PREFLIGHT_ID;
130
+ const pluginDir = process.env.PREFLIGHT_PLUGIN_DIR;
131
+
132
+ function fail(status, reason, desc) {
133
+ const obj = { status, reason };
134
+ if (desc !== undefined) obj.desc = String(desc);
135
+ console.log("RESULT:" + JSON.stringify(obj));
136
+ process.exit(1);
137
+ }
138
+
139
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
140
+
141
+ // 5a. 全局插件系统检查(plugins.enabled 默认 true,仅显式 false 禁用)
142
+ if (cfg.plugins?.enabled === false) {
143
+ fail("need_install", "plugins_globally_disabled", "plugins.enabled = " + JSON.stringify(cfg.plugins?.enabled));
144
+ }
145
+
146
+ // 5b. deny 黑名单(优先级高于 allow,与 OpenClaw 源码一致)
147
+ if (Array.isArray(cfg.plugins?.deny) && cfg.plugins.deny.includes(pluginId)) {
148
+ fail("need_install", "plugin_in_denylist", "plugins.deny = " + JSON.stringify(cfg.plugins.deny));
149
+ }
150
+
151
+ // 5c. plugins.entries 启用状态
152
+ if (cfg.plugins?.entries?.[pluginId]?.enabled !== true) {
153
+ fail("need_install", "plugin_not_enabled", "plugins.entries." + pluginId + ".enabled = " + JSON.stringify(cfg.plugins?.entries?.[pluginId]?.enabled));
154
+ }
155
+
156
+ // 5d. allow 白名单(非空时必须在列表中)
157
+ const allow = cfg.plugins?.allow;
158
+ if (Array.isArray(allow) && allow.length > 0 && !allow.includes(pluginId)) {
159
+ fail("need_install", "plugin_not_in_allowlist", "plugins.allow = " + JSON.stringify(allow));
160
+ }
161
+
162
+ // 5e. plugins.installs 安装记录(可选,仅警告,默认不阻塞)
163
+ // 如需严格检查,取消下面的注释:
164
+ // if (!cfg.plugins?.installs?.[pluginId]) {
165
+ // fail("need_install", "plugin_install_record_missing", "plugins.installs." + pluginId + " = " + JSON.stringify(cfg.plugins?.installs?.[pluginId]));
166
+ // }
167
+
168
+ // 5f. channels 启用状态
169
+ if (cfg.channels?.[pluginId]?.enabled !== true) {
170
+ fail("need_install", "channel_not_enabled", "channels." + pluginId + ".enabled = " + JSON.stringify(cfg.channels?.[pluginId]?.enabled));
171
+ }
172
+
173
+ // 5g. channels apiKeys 配置
174
+ const apiKeys = cfg.channels?.[pluginId]?.apiKeys;
175
+ if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
176
+ fail("need_install", "apikeys_empty", "channels." + pluginId + ".apiKeys = " + JSON.stringify(apiKeys));
177
+ }
178
+
179
+ // 5h. 读取插件版本
180
+ let version = "unknown";
181
+ try {
182
+ const pkg = JSON.parse(fs.readFileSync(pluginDir + "/package.json", "utf8"));
183
+ version = pkg.version || "unknown";
184
+ } catch (e) {}
185
+
186
+ // 全部通过
187
+ console.log("RESULT:" + JSON.stringify({ status: "ok", version }));
188
+ '
189
+
190
+ # 将 node 的退出码传递给 bash
191
+ exit $?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightclawbot",
3
- "version": "1.1.2-beta.0",
3
+ "version": "1.1.2",
4
4
  "description": "LightClawBot channel plugin with message support, cron jobs, and proactive messaging",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,384 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # ========== lightclawbot 插件快速安装/升级脚本 ==========
5
- # 用途:跳过 openclaw plugins install 的慢路径,直接安装/升级 lightclawbot 插件
6
- # 前提:openclaw 已安装且可用,pnpm 或 npm 已安装(不依赖 jq,JSON 操作统一用 node)
7
- # 支持:首次安装 / 重装 / 升级,自动保留已有的 channels 配置
8
- #
9
- # 输出规范:stdout 输出多行,供前端逐行解析
10
- # - 步骤进度:STEP:{"step":"step_id","message":"描述"}:
11
- # 7 处 step_log 调用 分别在环境检查、版本检查、下载、解压、配置更新、设备审批、重启网关前输出
12
- # STEP:{"step":"env_check","message":"checking environment"}
13
- # STEP:{"step":"version_check","message":"checking version info"}
14
- # STEP:{"step":"download","message":"downloading plugin"}
15
- # STEP:{"step":"extract","message":"extracting and installing"}
16
- # STEP:{"step":"config_update","message":"updating configuration"}
17
- # STEP:{"step":"devices_approve","message":"approving devices"}
18
- # STEP:{"step":"gateway_restart","message":"restarting gateway"}
19
- # - 最终成功:RESULT:{"status":"ok","version":"x.y.z"}
20
- # - 最终失败:RESULT:{"status":"error","reason":"..."}
21
- #
22
- # 使用方式:
23
- # 本地执行: bash scripts/upgrade.sh
24
- # CDN 执行: bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
25
- # 传入参数: APIKEY="sk-xxx" bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
26
- # 跳过重启: RESTART_GATEWAY=false APIKEY="sk-xxx" bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
27
-
28
- # ========== 工具函数 ==========
29
-
30
- # 输出步骤进度,供前端逐行解析
31
- step_log() {
32
- local step="$1"
33
- local message="$2"
34
- echo "STEP:{\"step\":\"${step}\",\"message\":\"${message}\"}"
35
- }
36
-
37
- result_exit() {
38
- local reason="$1"
39
- echo "RESULT:{\"status\":\"error\",\"reason\":\"${reason}\"}"
40
- exit 1
41
- }
42
-
43
- result_ok() {
44
- echo "RESULT:{\"status\":\"ok\",\"version\":\"${1}\"}"
45
- exit 0
46
- }
47
-
48
- # semver 比较:left >= right 返回 0,否则返回 1,解析失败返回 2
49
- version_gte() {
50
- local left="$1"
51
- local right="$2"
52
- VERSION_LEFT="$left" VERSION_RIGHT="$right" node - <<'NODE'
53
- function parseSemver(value) {
54
- const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
55
- if (!match) return null;
56
- return [Number(match[1]), Number(match[2]), Number(match[3])];
57
- }
58
- const left = parseSemver(process.env.VERSION_LEFT);
59
- const right = parseSemver(process.env.VERSION_RIGHT);
60
- if (!left || !right) process.exit(2);
61
- for (let i = 0; i < 3; i++) {
62
- if (left[i] > right[i]) process.exit(0);
63
- if (left[i] < right[i]) process.exit(1);
64
- }
65
- process.exit(0);
66
- NODE
67
- }
68
-
69
- # 获取本机 openclaw 版本号(纯数字 x.y.z 格式)
70
- get_openclaw_version() {
71
- if ! command -v openclaw &>/dev/null; then
72
- echo ""
73
- return
74
- fi
75
- local raw
76
- raw=$(openclaw -v 2>/dev/null || true)
77
- echo "$raw" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1
78
- }
79
-
80
- # ========== 配置 ==========
81
-
82
- id="lightclawbot"
83
- spec="lightclawbot"
84
- cfg="$HOME/.openclaw/openclaw.json"
85
- plugin_dir="$HOME/.openclaw/extensions/${id}"
86
-
87
- # npm 源:使用腾讯镜像源(可通过 NPM_REGISTRY 环境变量覆盖)
88
- NPM_REGISTRY="${NPM_REGISTRY:-https://mirrors.tencent.com/npm/}"
89
- # fallback 源:首选失败时兜底(可通过 NPM_FALLBACK 环境变量覆盖)
90
- NPM_FALLBACK="${NPM_FALLBACK:-https://registry.npmjs.org}"
91
-
92
- # API Key:优先读取环境变量,为空则保留配置文件中已有的值
93
- # 用法:APIKEY="sk-xxx" bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
94
- APIKEY="${APIKEY:-}"
95
-
96
- # 是否在安装/升级后重启 Gateway:默认 true,传入 false 可跳过重启
97
- # 用法:RESTART_GATEWAY=false bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
98
- RESTART_GATEWAY="${RESTART_GATEWAY:-true}"
99
-
100
- # openclaw 新老版本分界线:>= 此版本使用最新插件,< 此版本锁定 LEGACY_PLUGIN_VERSION
101
- OPENCLAW_BREAKING_VERSION="2026.3.22"
102
- LEGACY_PLUGIN_VERSION="1.0.8"
103
-
104
- # openclaw = 此版本需要执行 devices approve
105
- OPENCLAW_DEVICES_APPROVE_VERSION="2026.3.28"
106
-
107
- # ========== 环境加载 ==========
108
- # bash xxx.sh 启动的是非交互式 shell,不会自动 source ~/.bashrc,
109
- # 如果用户通过 nvm 安装 node,脚本中的 node/pnpm/openclaw 都找不到。
110
-
111
- export NVM_DIR="$HOME/.nvm"
112
- [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
113
-
114
- if [ -z "$PNPM_HOME" ] && [ -d "$HOME/.local/share/pnpm" ]; then
115
- export PNPM_HOME="$HOME/.local/share/pnpm"
116
- export PATH="$PNPM_HOME:$PATH"
117
- fi
118
-
119
- [ ! -t 1 ] && export CI=true
120
-
121
- # ========== 前置检查 ==========
122
-
123
- step_log "env_check" "checking environment"
124
-
125
- # 1) 探测包管理器:优先 pnpm,fallback npm,都没有则报错
126
- if command -v pnpm &>/dev/null; then
127
- PKG_MGR="pnpm"
128
- elif command -v npm &>/dev/null; then
129
- PKG_MGR="npm"
130
- else
131
- result_exit "npm_not_found"
132
- fi
133
-
134
- # 2) 检查 openclaw 是否可用
135
- if ! openclaw -v &>/dev/null; then
136
- result_exit "openclaw_not_available"
137
- fi
138
-
139
- # 3) 设置 npm/pnpm 源(确保后续 pack/install 都走镜像源)
140
- npm config set registry "$NPM_REGISTRY" 2>/dev/null || true
141
- [ "$PKG_MGR" = "pnpm" ] && pnpm config set registry "$NPM_REGISTRY" 2>/dev/null || true
142
-
143
- # 4) 检查 APIKEY
144
- if [ -z "$APIKEY" ]; then
145
- result_exit "apikey_empty"
146
- fi
147
-
148
- # ========== 安装 / 升级 lightclawbot ==========
149
-
150
- step_log "version_check" "checking version info"
151
-
152
- # ---------- 检查是否已安装,获取本地版本 ----------
153
- local_version=""
154
- if [ -d "$plugin_dir" ] && [ -f "$plugin_dir/package.json" ]; then
155
- local_version=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version||'')}catch(e){console.log('')}" "$plugin_dir/package.json" 2>/dev/null || true)
156
- fi
157
-
158
- # ---------- 获取 openclaw 版本,确定目标插件版本 ----------
159
- openclaw_version=$(get_openclaw_version)
160
- target_spec="$spec"
161
-
162
- if [ -n "$openclaw_version" ]; then
163
- if version_gte "$openclaw_version" "$OPENCLAW_BREAKING_VERSION"; then
164
- # openclaw >= 2026.3.22 → 安装最新版插件
165
- target_spec="$spec"
166
- else
167
- # openclaw < 2026.3.22 → 锁定旧版插件
168
- target_spec="${spec}@${LEGACY_PLUGIN_VERSION}"
169
- fi
170
- fi
171
-
172
- # ---------- 查询远程目标版本 ----------
173
- remote_version=""
174
-
175
- if [ -n "$openclaw_version" ] && ! version_gte "$openclaw_version" "$OPENCLAW_BREAKING_VERSION"; then
176
- # 旧版 openclaw:版本已锁定
177
- remote_version="$LEGACY_PLUGIN_VERSION"
178
- else
179
- remote_version=$(npm view "$spec" version 2>/dev/null || true)
180
-
181
- if [ -z "$remote_version" ]; then
182
- remote_version=$(npm view "$spec" version --registry "$NPM_FALLBACK" 2>/dev/null || true)
183
- fi
184
- fi
185
-
186
- # ---------- 判断安装模式 ----------
187
- install_mode="install"
188
- if [ -n "$local_version" ]; then
189
- if [ -n "$remote_version" ] && [ "$local_version" = "$remote_version" ]; then
190
- install_mode="up-to-date"
191
- else
192
- install_mode="upgrade"
193
- fi
194
- fi
195
-
196
- if [ "$install_mode" != "up-to-date" ]; then
197
- # ---------- 清理旧版本 ----------
198
- rm -rf "$plugin_dir"
199
-
200
- step_log "download" "downloading plugin"
201
-
202
- # ---------- npm pack 下载 tgz ----------
203
- tmp_dir=$(mktemp -d /tmp/openclaw-plugin-XXXXXX)
204
-
205
- tgz_file=$(cd "$tmp_dir" && npm pack "$target_spec" 2>/dev/null | tail -n1)
206
- if [ -z "$tgz_file" ] || [ ! -f "$tmp_dir/$tgz_file" ]; then
207
- tgz_file=$(cd "$tmp_dir" && npm pack "$target_spec" --registry "$NPM_FALLBACK" 2>/dev/null | tail -n1)
208
- fi
209
- if [ -z "$tgz_file" ] || [ ! -f "$tmp_dir/$tgz_file" ]; then
210
- rm -rf "$tmp_dir"
211
- result_exit "download_failed"
212
- fi
213
-
214
- step_log "extract" "extracting and installing"
215
-
216
- # ---------- 解压到插件目录 ----------
217
- mkdir -p "$plugin_dir"
218
- tar -xzf "$tmp_dir/$tgz_file" -C "$plugin_dir" --strip-components=1
219
- rm -rf "$tmp_dir"
220
-
221
- # ---------- 安装生产依赖 ----------
222
- # lightclawbot 声明了 bundledDependencies,npm pack 产出的 tgz 已包含所有依赖
223
- has_unbundled_deps=$(node -e '
224
- const pkg = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
225
- const deps = Object.keys(pkg.dependencies || {});
226
- const bundled = pkg.bundledDependencies || pkg.bundleDependencies || [];
227
- console.log(deps.length > 0 && bundled.length < deps.length ? "true" : "false");
228
- ' "$plugin_dir/package.json")
229
-
230
- if [ "$has_unbundled_deps" = true ]; then
231
- (cd "$plugin_dir" && CI=true $PKG_MGR install --prod) >/dev/null 2>&1
232
- fi
233
- fi
234
-
235
- # ========== 更新配置文件 ==========
236
-
237
- step_log "config_update" "updating configuration"
238
-
239
- # 确保配置文件存在
240
- if [ ! -f "$cfg" ]; then
241
- mkdir -p "$(dirname "$cfg")"
242
- echo '{}' > "$cfg"
243
- fi
244
-
245
- # 校验配置文件是合法 JSON(用户可能手动编辑弄坏了格式)
246
- if ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$cfg" 2>/dev/null; then
247
- result_exit "config_json_invalid"
248
- fi
249
-
250
- # ---------- 单次 node 调用完成所有配置更新 ----------
251
- # 一次启动 V8,通过环境变量安全传参,
252
- # 顺序完成 plugins.entries / installs / deny / allow / channels 全部配置修改,
253
- # 减少文件 I/O 次数(只读一次 + 写一次)。
254
-
255
- version=$(UPGRADE_CFG="$cfg" UPGRADE_ID="$id" UPGRADE_SPEC="$spec" \
256
- UPGRADE_PLUGIN_DIR="$plugin_dir" UPGRADE_APIKEY="$APIKEY" \
257
- node -e '
258
- const fs = require("fs");
259
- const cfgPath = process.env.UPGRADE_CFG;
260
- const pluginId = process.env.UPGRADE_ID;
261
- const spec = process.env.UPGRADE_SPEC;
262
- const pluginDir = process.env.UPGRADE_PLUGIN_DIR;
263
- const envApiKey = process.env.UPGRADE_APIKEY || "";
264
-
265
- function fail(reason) {
266
- process.exit(1);
267
- }
268
-
269
- // 读取配置
270
- let cfg;
271
- try {
272
- cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
273
- } catch (e) {
274
- fail("config_read_failed");
275
- }
276
-
277
- // 读取插件版本
278
- let version = "unknown";
279
- try {
280
- const pkg = JSON.parse(fs.readFileSync(pluginDir + "/package.json", "utf8"));
281
- version = pkg.version || "unknown";
282
- } catch (e) {}
283
-
284
- // ---------- 1) 确保 plugins 基础结构存在 & 全局插件系统启用 ----------
285
- if (!cfg.plugins) cfg.plugins = {};
286
- // 确保全局插件系统生效(plugins.enabled 默认 true,仅显式 false 时需修复)
287
- if (cfg.plugins.enabled === false) {
288
- cfg.plugins.enabled = true;
289
- }
290
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
291
- if (!cfg.plugins.installs) cfg.plugins.installs = {};
292
-
293
- // plugins.entries:启用插件
294
- cfg.plugins.entries[pluginId] = { enabled: true };
295
-
296
- // plugins.installs:安装记录
297
- cfg.plugins.installs[pluginId] = {
298
- source: "npm",
299
- spec: spec,
300
- installPath: pluginDir,
301
- version: version,
302
- installedAt: new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z")
303
- };
304
-
305
- // ---------- 2) 黑名单修复 ----------
306
- if (Array.isArray(cfg.plugins.deny)) {
307
- const idx = cfg.plugins.deny.indexOf(pluginId);
308
- if (idx !== -1) {
309
- cfg.plugins.deny.splice(idx, 1);
310
- if (cfg.plugins.deny.length === 0) {
311
- delete cfg.plugins.deny;
312
- }
313
- }
314
- }
315
-
316
- // ---------- 3) 白名单添加 ----------
317
- if (Array.isArray(cfg.plugins.allow)) {
318
- if (!cfg.plugins.allow.includes(pluginId)) {
319
- cfg.plugins.allow.push(pluginId);
320
- }
321
- }
322
-
323
- // ---------- 4) channel 配置:智能合并 ----------
324
- if (!cfg.channels) cfg.channels = {};
325
- const ch = cfg.channels[pluginId] || {};
326
-
327
- // 迁移旧的 apiKey -> apiKeys
328
- let apiKeys = Array.isArray(ch.apiKeys) ? [...ch.apiKeys] : [];
329
- if (ch.apiKey && typeof ch.apiKey === "string") {
330
- if (!apiKeys.includes(ch.apiKey)) {
331
- apiKeys.push(ch.apiKey);
332
- }
333
- delete ch.apiKey;
334
- }
335
-
336
- // 如果通过环境变量传入了 APIKEY,确保它在 apiKeys 数组中(去重)
337
- if (envApiKey && !apiKeys.includes(envApiKey)) {
338
- apiKeys.push(envApiKey);
339
- }
340
-
341
- ch.enabled = true;
342
- ch.apiKeys = apiKeys;
343
- cfg.channels[pluginId] = ch;
344
-
345
- // ---------- 5) 写回配置文件 ----------
346
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
347
-
348
- // 输出版本号到 stdout,供 shell 接收
349
- process.stdout.write(version);
350
- ') || result_exit "config_update_failed"
351
-
352
- # ========== 插件 >= 1.1.0 时需要重新安装依赖并注册 ==========
353
-
354
- # if version_gte "$version" "1.1.0"; then
355
- # (cd "$plugin_dir" && pnpm install) >/dev/null 2>&1 || true
356
- # openclaw gateway install --force >/dev/null 2>&1 || true
357
-
358
- # # 小内存机器(<3GB)自动注入 NODE_OPTIONS 限制堆内存,避免 OOM
359
- # GATEWAY_SERVICE="$HOME/.config/systemd/user/openclaw-gateway.service"
360
- # TOTAL_MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")
361
- # if [ "$TOTAL_MEM_KB" -gt 0 ] && [ "$TOTAL_MEM_KB" -lt 3000000 ] \
362
- # && [ -f "$GATEWAY_SERVICE" ] \
363
- # && ! grep -q 'max-old-space-size' "$GATEWAY_SERVICE"; then
364
- # sed -i '/\[Service\]/a Environment="NODE_OPTIONS=--max-old-space-size=1500"' "$GATEWAY_SERVICE"
365
- # systemctl --user daemon-reload
366
- # fi
367
- # fi
368
-
369
- # ========== 设备审批(openclaw == 2026.3.28)==========
370
-
371
- if [ -n "$openclaw_version" ] && [ "$openclaw_version" = "$OPENCLAW_DEVICES_APPROVE_VERSION" ]; then
372
- step_log "devices_approve" "approving devices"
373
- openclaw devices approve >/dev/null 2>&1 || true
374
- fi
375
-
376
- # ========== 重启 Gateway ==========
377
-
378
- if [ "$RESTART_GATEWAY" = "true" ]; then
379
- step_log "gateway_restart" "restarting gateway"
380
- openclaw gateway restart >/dev/null 2>&1 || true
381
- fi
382
-
383
- # stdout 最后一行 RESULT,供前端解析最终结果
384
- result_ok "$version"
@@ -1,384 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # ========== lightclawbot 插件快速安装/升级脚本 ==========
5
- # 用途:跳过 openclaw plugins install 的慢路径,直接安装/升级 lightclawbot 插件
6
- # 前提:openclaw 已安装且可用,pnpm 或 npm 已安装(不依赖 jq,JSON 操作统一用 node)
7
- # 支持:首次安装 / 重装 / 升级,自动保留已有的 channels 配置
8
- #
9
- # 输出规范:stdout 输出多行,供前端逐行解析
10
- # - 步骤进度:STEP:{"step":"step_id","message":"描述"}:
11
- # 7 处 step_log 调用 分别在环境检查、版本检查、下载、解压、配置更新、设备审批、重启网关前输出
12
- # STEP:{"step":"env_check","message":"checking environment"}
13
- # STEP:{"step":"version_check","message":"checking version info"}
14
- # STEP:{"step":"download","message":"downloading plugin"}
15
- # STEP:{"step":"extract","message":"extracting and installing"}
16
- # STEP:{"step":"config_update","message":"updating configuration"}
17
- # STEP:{"step":"devices_approve","message":"approving devices"}
18
- # STEP:{"step":"gateway_restart","message":"restarting gateway"}
19
- # - 最终成功:RESULT:{"status":"ok","version":"x.y.z"}
20
- # - 最终失败:RESULT:{"status":"error","reason":"..."}
21
- #
22
- # 使用方式:
23
- # 本地执行: bash scripts/upgrade.sh
24
- # CDN 执行: bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
25
- # 传入参数: APIKEY="sk-xxx" bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
26
- # 跳过重启: RESTART_GATEWAY=false APIKEY="sk-xxx" bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
27
-
28
- # ========== 工具函数 ==========
29
-
30
- # 输出步骤进度,供前端逐行解析
31
- step_log() {
32
- local step="$1"
33
- local message="$2"
34
- echo "STEP:{\"step\":\"${step}\",\"message\":\"${message}\"}"
35
- }
36
-
37
- result_exit() {
38
- local reason="$1"
39
- echo "RESULT:{\"status\":\"error\",\"reason\":\"${reason}\"}"
40
- exit 1
41
- }
42
-
43
- result_ok() {
44
- echo "RESULT:{\"status\":\"ok\",\"version\":\"${1}\"}"
45
- exit 0
46
- }
47
-
48
- # semver 比较:left >= right 返回 0,否则返回 1,解析失败返回 2
49
- version_gte() {
50
- local left="$1"
51
- local right="$2"
52
- VERSION_LEFT="$left" VERSION_RIGHT="$right" node - <<'NODE'
53
- function parseSemver(value) {
54
- const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
55
- if (!match) return null;
56
- return [Number(match[1]), Number(match[2]), Number(match[3])];
57
- }
58
- const left = parseSemver(process.env.VERSION_LEFT);
59
- const right = parseSemver(process.env.VERSION_RIGHT);
60
- if (!left || !right) process.exit(2);
61
- for (let i = 0; i < 3; i++) {
62
- if (left[i] > right[i]) process.exit(0);
63
- if (left[i] < right[i]) process.exit(1);
64
- }
65
- process.exit(0);
66
- NODE
67
- }
68
-
69
- # 获取本机 openclaw 版本号(纯数字 x.y.z 格式)
70
- get_openclaw_version() {
71
- if ! command -v openclaw &>/dev/null; then
72
- echo ""
73
- return
74
- fi
75
- local raw
76
- raw=$(openclaw -v 2>/dev/null || true)
77
- echo "$raw" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1
78
- }
79
-
80
- # ========== 配置 ==========
81
-
82
- id="lightclawbot"
83
- spec="lightclawbot"
84
- cfg="$HOME/.openclaw/openclaw.json"
85
- plugin_dir="$HOME/.openclaw/extensions/${id}"
86
-
87
- # npm 源:使用腾讯镜像源(可通过 NPM_REGISTRY 环境变量覆盖)
88
- NPM_REGISTRY="${NPM_REGISTRY:-https://mirrors.tencent.com/npm/}"
89
- # fallback 源:首选失败时兜底(可通过 NPM_FALLBACK 环境变量覆盖)
90
- NPM_FALLBACK="${NPM_FALLBACK:-https://registry.npmjs.org}"
91
-
92
- # API Key:优先读取环境变量,为空则保留配置文件中已有的值
93
- # 用法:APIKEY="sk-xxx" bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
94
- APIKEY="${APIKEY:-}"
95
-
96
- # 是否在安装/升级后重启 Gateway:默认 true,传入 false 可跳过重启
97
- # 用法:RESTART_GATEWAY=false bash <(curl -fsSL https://your-cdn.com/upgrade.sh)
98
- RESTART_GATEWAY="${RESTART_GATEWAY:-true}"
99
-
100
- # openclaw 新老版本分界线:>= 此版本使用最新插件,< 此版本锁定 LEGACY_PLUGIN_VERSION
101
- OPENCLAW_BREAKING_VERSION="2026.3.22"
102
- LEGACY_PLUGIN_VERSION="1.0.8"
103
-
104
- # openclaw = 此版本需要执行 devices approve
105
- OPENCLAW_DEVICES_APPROVE_VERSION="2026.3.28"
106
-
107
- # ========== 环境加载 ==========
108
- # bash xxx.sh 启动的是非交互式 shell,不会自动 source ~/.bashrc,
109
- # 如果用户通过 nvm 安装 node,脚本中的 node/pnpm/openclaw 都找不到。
110
-
111
- export NVM_DIR="$HOME/.nvm"
112
- [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
113
-
114
- if [ -z "$PNPM_HOME" ] && [ -d "$HOME/.local/share/pnpm" ]; then
115
- export PNPM_HOME="$HOME/.local/share/pnpm"
116
- export PATH="$PNPM_HOME:$PATH"
117
- fi
118
-
119
- [ ! -t 1 ] && export CI=true
120
-
121
- # ========== 前置检查 ==========
122
-
123
- step_log "env_check" "checking environment"
124
-
125
- # 1) 探测包管理器:优先 pnpm,fallback npm,都没有则报错
126
- if command -v pnpm &>/dev/null; then
127
- PKG_MGR="pnpm"
128
- elif command -v npm &>/dev/null; then
129
- PKG_MGR="npm"
130
- else
131
- result_exit "npm_not_found"
132
- fi
133
-
134
- # 2) 检查 openclaw 是否可用
135
- if ! openclaw -v &>/dev/null; then
136
- result_exit "openclaw_not_available"
137
- fi
138
-
139
- # 3) 设置 npm/pnpm 源(确保后续 pack/install 都走镜像源)
140
- npm config set registry "$NPM_REGISTRY" 2>/dev/null || true
141
- [ "$PKG_MGR" = "pnpm" ] && pnpm config set registry "$NPM_REGISTRY" 2>/dev/null || true
142
-
143
- # 4) 检查 APIKEY
144
- if [ -z "$APIKEY" ]; then
145
- result_exit "apikey_empty"
146
- fi
147
-
148
- # ========== 安装 / 升级 lightclawbot ==========
149
-
150
- step_log "version_check" "checking version info"
151
-
152
- # ---------- 检查是否已安装,获取本地版本 ----------
153
- local_version=""
154
- if [ -d "$plugin_dir" ] && [ -f "$plugin_dir/package.json" ]; then
155
- local_version=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version||'')}catch(e){console.log('')}" "$plugin_dir/package.json" 2>/dev/null || true)
156
- fi
157
-
158
- # ---------- 获取 openclaw 版本,确定目标插件版本 ----------
159
- openclaw_version=$(get_openclaw_version)
160
- target_spec="$spec"
161
-
162
- if [ -n "$openclaw_version" ]; then
163
- if version_gte "$openclaw_version" "$OPENCLAW_BREAKING_VERSION"; then
164
- # openclaw >= 2026.3.22 → 安装最新版插件
165
- target_spec="$spec"
166
- else
167
- # openclaw < 2026.3.22 → 锁定旧版插件
168
- target_spec="${spec}@${LEGACY_PLUGIN_VERSION}"
169
- fi
170
- fi
171
-
172
- # ---------- 查询远程目标版本 ----------
173
- remote_version=""
174
-
175
- if [ -n "$openclaw_version" ] && ! version_gte "$openclaw_version" "$OPENCLAW_BREAKING_VERSION"; then
176
- # 旧版 openclaw:版本已锁定
177
- remote_version="$LEGACY_PLUGIN_VERSION"
178
- else
179
- remote_version=$(npm view "$spec" version 2>/dev/null || true)
180
-
181
- if [ -z "$remote_version" ]; then
182
- remote_version=$(npm view "$spec" version --registry "$NPM_FALLBACK" 2>/dev/null || true)
183
- fi
184
- fi
185
-
186
- # ---------- 判断安装模式 ----------
187
- install_mode="install"
188
- if [ -n "$local_version" ]; then
189
- if [ -n "$remote_version" ] && [ "$local_version" = "$remote_version" ]; then
190
- install_mode="up-to-date"
191
- else
192
- install_mode="upgrade"
193
- fi
194
- fi
195
-
196
- if [ "$install_mode" != "up-to-date" ]; then
197
- # ---------- 清理旧版本 ----------
198
- rm -rf "$plugin_dir"
199
-
200
- step_log "download" "downloading plugin"
201
-
202
- # ---------- npm pack 下载 tgz ----------
203
- tmp_dir=$(mktemp -d /tmp/openclaw-plugin-XXXXXX)
204
-
205
- tgz_file=$(cd "$tmp_dir" && npm pack "$target_spec" 2>/dev/null | tail -n1)
206
- if [ -z "$tgz_file" ] || [ ! -f "$tmp_dir/$tgz_file" ]; then
207
- tgz_file=$(cd "$tmp_dir" && npm pack "$target_spec" --registry "$NPM_FALLBACK" 2>/dev/null | tail -n1)
208
- fi
209
- if [ -z "$tgz_file" ] || [ ! -f "$tmp_dir/$tgz_file" ]; then
210
- rm -rf "$tmp_dir"
211
- result_exit "download_failed"
212
- fi
213
-
214
- step_log "extract" "extracting and installing"
215
-
216
- # ---------- 解压到插件目录 ----------
217
- mkdir -p "$plugin_dir"
218
- tar -xzf "$tmp_dir/$tgz_file" -C "$plugin_dir" --strip-components=1
219
- rm -rf "$tmp_dir"
220
-
221
- # ---------- 安装生产依赖 ----------
222
- # lightclawbot 声明了 bundledDependencies,npm pack 产出的 tgz 已包含所有依赖
223
- has_unbundled_deps=$(node -e '
224
- const pkg = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
225
- const deps = Object.keys(pkg.dependencies || {});
226
- const bundled = pkg.bundledDependencies || pkg.bundleDependencies || [];
227
- console.log(deps.length > 0 && bundled.length < deps.length ? "true" : "false");
228
- ' "$plugin_dir/package.json")
229
-
230
- if [ "$has_unbundled_deps" = true ]; then
231
- (cd "$plugin_dir" && CI=true $PKG_MGR install --prod) >/dev/null 2>&1
232
- fi
233
- fi
234
-
235
- # ========== 更新配置文件 ==========
236
-
237
- step_log "config_update" "updating configuration"
238
-
239
- # 确保配置文件存在
240
- if [ ! -f "$cfg" ]; then
241
- mkdir -p "$(dirname "$cfg")"
242
- echo '{}' > "$cfg"
243
- fi
244
-
245
- # 校验配置文件是合法 JSON(用户可能手动编辑弄坏了格式)
246
- if ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$cfg" 2>/dev/null; then
247
- result_exit "config_json_invalid"
248
- fi
249
-
250
- # ---------- 单次 node 调用完成所有配置更新 ----------
251
- # 一次启动 V8,通过环境变量安全传参,
252
- # 顺序完成 plugins.entries / installs / deny / allow / channels 全部配置修改,
253
- # 减少文件 I/O 次数(只读一次 + 写一次)。
254
-
255
- version=$(UPGRADE_CFG="$cfg" UPGRADE_ID="$id" UPGRADE_SPEC="$spec" \
256
- UPGRADE_PLUGIN_DIR="$plugin_dir" UPGRADE_APIKEY="$APIKEY" \
257
- node -e '
258
- const fs = require("fs");
259
- const cfgPath = process.env.UPGRADE_CFG;
260
- const pluginId = process.env.UPGRADE_ID;
261
- const spec = process.env.UPGRADE_SPEC;
262
- const pluginDir = process.env.UPGRADE_PLUGIN_DIR;
263
- const envApiKey = process.env.UPGRADE_APIKEY || "";
264
-
265
- function fail(reason) {
266
- process.exit(1);
267
- }
268
-
269
- // 读取配置
270
- let cfg;
271
- try {
272
- cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
273
- } catch (e) {
274
- fail("config_read_failed");
275
- }
276
-
277
- // 读取插件版本
278
- let version = "unknown";
279
- try {
280
- const pkg = JSON.parse(fs.readFileSync(pluginDir + "/package.json", "utf8"));
281
- version = pkg.version || "unknown";
282
- } catch (e) {}
283
-
284
- // ---------- 1) 确保 plugins 基础结构存在 & 全局插件系统启用 ----------
285
- if (!cfg.plugins) cfg.plugins = {};
286
- // 确保全局插件系统生效(plugins.enabled 默认 true,仅显式 false 时需修复)
287
- if (cfg.plugins.enabled === false) {
288
- cfg.plugins.enabled = true;
289
- }
290
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
291
- if (!cfg.plugins.installs) cfg.plugins.installs = {};
292
-
293
- // plugins.entries:启用插件
294
- cfg.plugins.entries[pluginId] = { enabled: true };
295
-
296
- // plugins.installs:安装记录
297
- cfg.plugins.installs[pluginId] = {
298
- source: "npm",
299
- spec: spec,
300
- installPath: pluginDir,
301
- version: version,
302
- installedAt: new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z")
303
- };
304
-
305
- // ---------- 2) 黑名单修复 ----------
306
- if (Array.isArray(cfg.plugins.deny)) {
307
- const idx = cfg.plugins.deny.indexOf(pluginId);
308
- if (idx !== -1) {
309
- cfg.plugins.deny.splice(idx, 1);
310
- if (cfg.plugins.deny.length === 0) {
311
- delete cfg.plugins.deny;
312
- }
313
- }
314
- }
315
-
316
- // ---------- 3) 白名单添加 ----------
317
- if (Array.isArray(cfg.plugins.allow)) {
318
- if (!cfg.plugins.allow.includes(pluginId)) {
319
- cfg.plugins.allow.push(pluginId);
320
- }
321
- }
322
-
323
- // ---------- 4) channel 配置:智能合并 ----------
324
- if (!cfg.channels) cfg.channels = {};
325
- const ch = cfg.channels[pluginId] || {};
326
-
327
- // 迁移旧的 apiKey -> apiKeys
328
- let apiKeys = Array.isArray(ch.apiKeys) ? [...ch.apiKeys] : [];
329
- if (ch.apiKey && typeof ch.apiKey === "string") {
330
- if (!apiKeys.includes(ch.apiKey)) {
331
- apiKeys.push(ch.apiKey);
332
- }
333
- delete ch.apiKey;
334
- }
335
-
336
- // 如果通过环境变量传入了 APIKEY,确保它在 apiKeys 数组中(去重)
337
- if (envApiKey && !apiKeys.includes(envApiKey)) {
338
- apiKeys.push(envApiKey);
339
- }
340
-
341
- ch.enabled = true;
342
- ch.apiKeys = apiKeys;
343
- cfg.channels[pluginId] = ch;
344
-
345
- // ---------- 5) 写回配置文件 ----------
346
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
347
-
348
- // 输出版本号到 stdout,供 shell 接收
349
- process.stdout.write(version);
350
- ') || result_exit "config_update_failed"
351
-
352
- # ========== 插件 >= 1.1.0 时需要重新安装依赖并注册 ==========
353
-
354
- # if version_gte "$version" "1.1.0"; then
355
- # (cd "$plugin_dir" && pnpm install) >/dev/null 2>&1 || true
356
- # openclaw gateway install --force >/dev/null 2>&1 || true
357
-
358
- # # 小内存机器(<3GB)自动注入 NODE_OPTIONS 限制堆内存,避免 OOM
359
- # GATEWAY_SERVICE="$HOME/.config/systemd/user/openclaw-gateway.service"
360
- # TOTAL_MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")
361
- # if [ "$TOTAL_MEM_KB" -gt 0 ] && [ "$TOTAL_MEM_KB" -lt 3000000 ] \
362
- # && [ -f "$GATEWAY_SERVICE" ] \
363
- # && ! grep -q 'max-old-space-size' "$GATEWAY_SERVICE"; then
364
- # sed -i '/\[Service\]/a Environment="NODE_OPTIONS=--max-old-space-size=1500"' "$GATEWAY_SERVICE"
365
- # systemctl --user daemon-reload
366
- # fi
367
- # fi
368
-
369
- # ========== 设备审批(openclaw == 2026.3.28)==========
370
-
371
- if [ -n "$openclaw_version" ] && [ "$openclaw_version" = "$OPENCLAW_DEVICES_APPROVE_VERSION" ]; then
372
- step_log "devices_approve" "approving devices"
373
- openclaw devices approve >/dev/null 2>&1 || true
374
- fi
375
-
376
- # ========== 重启 Gateway ==========
377
-
378
- if [ "$RESTART_GATEWAY" = "true" ]; then
379
- step_log "gateway_restart" "restarting gateway"
380
- openclaw gateway restart >/dev/null 2>&1 || true
381
- fi
382
-
383
- # stdout 最后一行 RESULT,供前端解析最终结果
384
- result_ok "$version"