mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-28 06:12:52 +08:00
1093 lines
42 KiB
Bash
Executable File
1093 lines
42 KiB
Bash
Executable File
#!/bin/sh /etc/rc.common
|
|
# luci-app-openclaw — procd init 脚本
|
|
|
|
USE_PROCD=1
|
|
START=99
|
|
STOP=10
|
|
|
|
EXTRA_COMMANDS="setup status_service restart_gateway"
|
|
EXTRA_HELP=" setup 下载 Node.js 并安装 OpenClaw
|
|
status_service 显示服务状态
|
|
restart_gateway 仅重启 Gateway 实例 (不影响 Web PTY)"
|
|
|
|
# ── 安装路径 (支持自定义) ──
|
|
# 从 UCI 配置读取自定义路径,公开字段仍为 install_path。
|
|
# 统一路径解析可以兼容 /mnt/data/openclaw 输入,避免双拼路径。
|
|
[ -r /usr/libexec/openclaw-paths.sh ] && . /usr/libexec/openclaw-paths.sh
|
|
[ -r /usr/libexec/openclaw-node.sh ] && . /usr/libexec/openclaw-node.sh
|
|
OC_NODE_MIN_VERSION="${OC_NODE_MIN_VERSION:-22.19.0}"
|
|
|
|
fix_openclaw_state_permissions() {
|
|
[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh fix-state "${OC_DATA}/.openclaw"
|
|
}
|
|
OC_CONFIGURED_PATH="$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')"
|
|
if command -v oc_load_paths >/dev/null 2>&1; then
|
|
if ! oc_load_paths "$OC_CONFIGURED_PATH"; then
|
|
logger -t openclaw "安装路径无效: $OC_CONFIGURED_PATH"
|
|
OC_BASE_PATH="/opt"
|
|
OC_INSTALL_PATH="/opt/openclaw"
|
|
NODE_BASE="${OC_INSTALL_PATH}/node"
|
|
OC_GLOBAL="${OC_INSTALL_PATH}/global"
|
|
OC_DATA="${OC_INSTALL_PATH}/data"
|
|
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
|
else
|
|
OC_BASE_PATH="$OPENCLAW_INSTALL_PATH"
|
|
OC_INSTALL_PATH="$OC_ROOT"
|
|
fi
|
|
else
|
|
OC_BASE_PATH="${OC_CONFIGURED_PATH%/}"
|
|
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
|
|
NODE_BASE="${OC_INSTALL_PATH}/node"
|
|
OC_GLOBAL="${OC_INSTALL_PATH}/global"
|
|
OC_DATA="${OC_INSTALL_PATH}/data"
|
|
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
|
fi
|
|
|
|
# ── OverlayFS 兼容性修复 ──
|
|
# Docker bind mount (/overlay/upper/opt/docker) 会导致 /opt 不可写
|
|
# 解决: bind mount upper 层的 /opt 到合并视图的 /opt
|
|
# 注意: 仅当基础路径为 /opt 时才需要此修复
|
|
_oc_fix_opt() {
|
|
[ "$OC_BASE_PATH" != "/opt" ] && return 0
|
|
mkdir -p /opt/openclaw/.probe 2>/dev/null && { rmdir /opt/openclaw/.probe 2>/dev/null; return 0; }
|
|
if [ -d /overlay/upper/opt ]; then
|
|
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
|
|
mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
_oc_fix_opt
|
|
|
|
NODE_BIN="${NODE_BASE}/bin/node"
|
|
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
|
|
|
run_openclaw_cli_as_user() {
|
|
local oc_entry="$1"
|
|
shift
|
|
local pid_file="/tmp/openclaw-cli-$$.pid"
|
|
local rc
|
|
|
|
[ -x "$NODE_BIN" ] || return 127
|
|
id -u openclaw >/dev/null 2>&1 || return 127
|
|
rm -f "$pid_file"
|
|
ulimit -v unlimited 2>/dev/null || true
|
|
ulimit -m unlimited 2>/dev/null || true
|
|
ulimit -d unlimited 2>/dev/null || true
|
|
(
|
|
cd "${OC_DATA:-$OC_INSTALL_PATH}" 2>/dev/null || true
|
|
HOME="$OC_DATA" OPENCLAW_HOME="$OC_DATA" OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
|
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" NODE_ICU_DATA="${NODE_BASE}/share/icu" \
|
|
NPM_CONFIG_CACHE="${OC_DATA}/.npm" npm_config_cache="${OC_DATA}/.npm" TMPDIR="${OC_DATA}/.tmp" \
|
|
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH" \
|
|
start-stop-daemon -S -m -p "$pid_file" -c openclaw:openclaw \
|
|
-x "$NODE_BIN" -- "$oc_entry" "$@"
|
|
)
|
|
rc=$?
|
|
rm -f "$pid_file"
|
|
return "$rc"
|
|
}
|
|
|
|
get_oc_entry() {
|
|
local search_dirs="${OC_GLOBAL}/lib/node_modules/openclaw
|
|
${OC_GLOBAL}/node_modules/openclaw
|
|
${NODE_BASE}/lib/node_modules/openclaw"
|
|
|
|
# pnpm 全局安装路径形如: $OC_GLOBAL/5/node_modules/openclaw
|
|
for ver_dir in "${OC_GLOBAL}"/*/node_modules/openclaw; do
|
|
[ -d "$ver_dir" ] && search_dirs="$search_dirs
|
|
$ver_dir"
|
|
done
|
|
|
|
# v2026.3.8: 跟随符号链接解析真实包路径 (pnpm store / symlinked wrappers)
|
|
for link_dir in "${OC_GLOBAL}/lib/node_modules/openclaw" "${OC_GLOBAL}/node_modules/openclaw"; do
|
|
[ -L "$link_dir" ] && {
|
|
local real_dir=$(readlink -f "$link_dir" 2>/dev/null)
|
|
[ -n "$real_dir" ] && [ -d "$real_dir" ] && search_dirs="$search_dirs
|
|
$real_dir"
|
|
}
|
|
done
|
|
|
|
local d _tmpf
|
|
_tmpf=$(mktemp)
|
|
echo "$search_dirs" > "$_tmpf"
|
|
while read -r d; do
|
|
[ -z "$d" ] && continue
|
|
if [ -f "${d}/openclaw.mjs" ]; then
|
|
echo "${d}/openclaw.mjs"
|
|
rm -f "$_tmpf"
|
|
return
|
|
elif [ -f "${d}/dist/cli.js" ]; then
|
|
echo "${d}/dist/cli.js"
|
|
rm -f "$_tmpf"
|
|
return
|
|
fi
|
|
done < "$_tmpf"
|
|
rm -f "$_tmpf"
|
|
}
|
|
|
|
|
|
sync_uci_to_json() {
|
|
# 检查是否处于升级事务迁移期间,若是则暂停 UCI 覆盖迁移配置
|
|
local _sdir="${OPENCLAW_STATE_DIR:-${OC_DATA:+$OC_DATA/.openclaw}}"
|
|
local _pdir="$(dirname "$_sdir")"
|
|
local _rdir="$(dirname "$_pdir")"
|
|
local _status_file="" cand
|
|
for cand in \
|
|
"${_rdir}/.luci-openclaw-upgrade/status.json" \
|
|
"${_pdir}/.luci-openclaw-upgrade/status.json" \
|
|
"${_sdir}/.luci-openclaw-upgrade/status.json" \
|
|
"${OC_DATA:+$OC_DATA/.luci-openclaw-upgrade/status.json}" \
|
|
"${OC_BASE_PATH:-/opt}/openclaw/.luci-openclaw-upgrade/status.json"; do
|
|
if [ -n "$cand" ] && [ -f "$cand" ]; then
|
|
_status_file="$cand"
|
|
break
|
|
fi
|
|
done
|
|
if [ -n "$_status_file" ] && [ -f "$_status_file" ]; then
|
|
local _up_phase
|
|
_up_phase=$(grep -oE '"phase"[[:space:]]*:[[:space:]]*"[^"]+"' "$_status_file" 2>/dev/null | cut -d'"' -f4 || true)
|
|
case "$_up_phase" in
|
|
migrating|gateway_verifying|backing_up)
|
|
logger -t openclaw "检测到正在执行升级事务 (phase: $_up_phase),暂停 UCI 覆盖迁移配置"
|
|
return 0
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# 将 UCI 配置同步到 openclaw.json,同时确保 token 双向同步
|
|
local port bind token json_token
|
|
port=$(uci -q get openclaw.main.port || echo "18789")
|
|
bind=$(uci -q get openclaw.main.bind || echo "lan")
|
|
token=$(uci -q get openclaw.main.token || echo "")
|
|
|
|
# 确保配置目录和文件存在 (路径已在脚本开头做 OverlayFS 重映射)
|
|
mkdir -p "$(dirname "$CONFIG_FILE")" 2>/dev/null
|
|
if [ ! -f "$CONFIG_FILE" ]; then
|
|
echo '{}' > "$CONFIG_FILE"
|
|
fi
|
|
|
|
# 尝试从 JSON 读取 token (用于双向同步检测)
|
|
if [ -x "$NODE_BIN" ]; then
|
|
json_token=$("$NODE_BIN" -e "
|
|
try{const d=JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8'));
|
|
if(d.gateway&&d.gateway.auth&&d.gateway.auth.token)process.stdout.write(d.gateway.auth.token);}catch(e){}
|
|
" 2>/dev/null)
|
|
fi
|
|
|
|
# v2026.4.10: 双向同步策略 - JSON 中的 token 优先
|
|
# 原因: doctor --fix 或其他操作可能修改 JSON 中的 token
|
|
# 如果 JSON 有 token 且与 UCI 不同,以 JSON 为准
|
|
if [ -n "$json_token" ] && [ "$json_token" != "$token" ]; then
|
|
token="$json_token"
|
|
logger -t openclaw "Token 同步: JSON -> UCI"
|
|
fi
|
|
|
|
# UCI 和 JSON 都没有 token 时,生成一个新的
|
|
if [ -z "$token" ]; then
|
|
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48)
|
|
logger -t openclaw "Token 生成: 新 token"
|
|
fi
|
|
|
|
# 如果仍然没有 token,生成一个新的
|
|
if [ -z "$token" ]; then
|
|
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48)
|
|
fi
|
|
|
|
# 确保 token 写回 UCI
|
|
local uci_token
|
|
uci_token=$(uci -q get openclaw.main.token || echo "")
|
|
if [ "$uci_token" != "$token" ]; then
|
|
uci set openclaw.main.token="$token"
|
|
uci commit openclaw 2>/dev/null
|
|
fi
|
|
|
|
# 使用 Node.js 写入 JSON (如果可用)
|
|
if [ -x "$NODE_BIN" ]; then
|
|
OC_SYNC_PORT="$port" OC_SYNC_BIND="$bind" OC_SYNC_TOKEN="$token" OC_SYNC_FILE="$CONFIG_FILE" \
|
|
"$NODE_BIN" -e "
|
|
const fs=require('fs');
|
|
const f=process.env.OC_SYNC_FILE;
|
|
let d={};
|
|
// 配置存在但解析失败时必须中止,绝不能用 {} 继续往下写。
|
|
// 否则本函数会把空对象连同下面的默认值一起落盘,用户的
|
|
// models.providers(含 apiKey)、channels、plugins 全部丢失。
|
|
// 本函数在每次 start_service 时自动执行,一旦退化影响面比手工操作大得多。
|
|
let raw='';
|
|
try{raw=fs.readFileSync(f,'utf8');}catch(e){raw='';}
|
|
if(raw.trim()!==''){
|
|
try{
|
|
d=JSON.parse(raw);
|
|
}catch(e){
|
|
console.error('openclaw: 配置文件解析失败,已跳过 UCI 同步以避免覆盖: '+e.message);
|
|
process.exit(3);
|
|
}
|
|
if(d===null||typeof d!=='object'||Array.isArray(d)){
|
|
console.error('openclaw: 配置根节点不是对象,已跳过 UCI 同步');
|
|
process.exit(3);
|
|
}
|
|
}
|
|
if(!d.gateway)d.gateway={};
|
|
d.gateway.port=parseInt(process.env.OC_SYNC_PORT)||18789;
|
|
d.gateway.bind=process.env.OC_SYNC_BIND||'lan';
|
|
d.gateway.mode='local';
|
|
if(!d.gateway.auth)d.gateway.auth={};
|
|
d.gateway.auth.mode='token';
|
|
d.gateway.auth.token=process.env.OC_SYNC_TOKEN||'';
|
|
if(!d.gateway.controlUi)d.gateway.controlUi={};
|
|
d.gateway.controlUi.allowInsecureAuth=true;
|
|
d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true;
|
|
if(!Array.isArray(d.gateway.controlUi.allowedOrigins))d.gateway.controlUi.allowedOrigins=['*'];
|
|
// 清理 v2026.3.1+ 已废弃的字段,避免配置验证失败
|
|
delete d.gateway.name;
|
|
delete d.gateway.bonjour;
|
|
delete d.gateway.plugins;
|
|
// v2026.3.2: 禁用 ACP dispatch 防止路由器内存溢出
|
|
if(!d.acp)d.acp={};
|
|
if(!d.acp.dispatch)d.acp.dispatch={};
|
|
d.acp.dispatch.enabled=false;
|
|
// v2026.3.2: tools.profile 默认改为 messaging,路由器场景需强制 coding
|
|
if(!d.tools)d.tools={};
|
|
d.tools.profile='coding';
|
|
// 禁用 OpenClaw 内置更新检查,防止 Control UI 显示升级横幅
|
|
if(!d.update)d.update={};
|
|
d.update.checkOnStart=false;
|
|
// v2026.3.2: 迁移 Ollama provider 到原生 API
|
|
if(d.models&&d.models.providers&&d.models.providers.ollama){const ol=d.models.providers.ollama;if(ol.api==='openai-chat-completions'||ol.api==='openai-completions')ol.api='ollama';if(ol.baseUrl&&ol.baseUrl.endsWith('/v1'))ol.baseUrl=ol.baseUrl.replace(/\/v1$/,'');if(ol.apiKey==='ollama')ol.apiKey='ollama-local';}
|
|
// v2026.3.7: 清理已废弃的顶层配置键 (loadConfig() 现在会严格校验)
|
|
['cli','commands.native','commands.nativeSkills','commands.ownerDisplay'].forEach(k=>{const ks=k.split('.');let o=d;for(let i=0;i<ks.length-1;i++){if(!o[ks[i]])return;o=o[ks[i]];}delete o[ks[ks.length-1]];});
|
|
// v2026.4.5: 清理已废弃的配置别名 (BREAKING CHANGE)
|
|
// 1. talk.* 命名空间废弃 (voiceId, apiKey)
|
|
if(d.talk){delete d.talk.voiceId;delete d.talk.apiKey;}
|
|
// 2. browser.ssrfPolicy.allowPrivateNetwork 废弃
|
|
if(d.browser&&d.browser.ssrfPolicy){delete d.browser.ssrfPolicy.allowPrivateNetwork;}
|
|
// 3. hooks.internal.handlers 废弃
|
|
if(d.hooks&&d.hooks.internal){delete d.hooks.internal.handlers;}
|
|
// 4. channel/group/room 的 allow 字段迁移到 enabled
|
|
['channel','group','room'].forEach(k=>{if(d[k]){Object.keys(d[k]).forEach(id=>{if(d[k][id]&&typeof d[k][id].allow!=='undefined'){d[k][id].enabled=d[k][id].allow;delete d[k][id].allow;}});}});
|
|
// 5. agents.defaults.cliBackends 废弃
|
|
if(d.agents&&d.agents.defaults){delete d.agents.defaults.cliBackends;}
|
|
// v2026.4.9: 自动同步 plugins.allow (解决 "plugins.allow is empty" 警告)
|
|
if(!d.plugins)d.plugins={};
|
|
if(!Array.isArray(d.plugins.allow))d.plugins.allow=[];
|
|
delete d.plugins.installs;
|
|
// v2026.5.x: 微信插件渠道名统一为 openclaw-weixin
|
|
if(Array.isArray(d.plugins.allow)){
|
|
d.plugins.allow=d.plugins.allow.map(x=>x==='weixin'?'openclaw-weixin':x)
|
|
.filter((x,i,a)=>x&&a.indexOf(x)===i);
|
|
}
|
|
if(d.plugins&&d.plugins.entries&&d.plugins.entries.weixin){
|
|
if(!d.plugins.entries['openclaw-weixin'])d.plugins.entries['openclaw-weixin']=d.plugins.entries.weixin;
|
|
delete d.plugins.entries.weixin;
|
|
}
|
|
['channel','channels'].forEach(ns=>{
|
|
if(d[ns]&&d[ns].weixin){
|
|
if(!d[ns]['openclaw-weixin'])d[ns]['openclaw-weixin']=d[ns].weixin;
|
|
delete d[ns].weixin;
|
|
}
|
|
});
|
|
if(d.channels&&d.channels['openclaw-weixin']&&!d.plugins.allow.includes('openclaw-weixin')){
|
|
d.plugins.allow.push('openclaw-weixin');
|
|
}
|
|
// 始终信任 copilot-proxy (内置插件)
|
|
if(!d.plugins.allow.includes('copilot-proxy'))d.plugins.allow.push('copilot-proxy');
|
|
// 允许 LuCI 跨源与 Host-Header 访问 Control UI
|
|
if(!d.gateway)d.gateway={};
|
|
if(!d.gateway.controlUi)d.gateway.controlUi={};
|
|
d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true;
|
|
if(!Array.isArray(d.gateway.controlUi.allowedOrigins))d.gateway.controlUi.allowedOrigins=['*'];
|
|
fs.writeFileSync(f,JSON.stringify(d,null,2));
|
|
" 2>/dev/null
|
|
fi
|
|
|
|
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
start_service() {
|
|
local enabled port bind pty_port
|
|
config_load openclaw
|
|
config_get enabled main enabled "0"
|
|
config_get port main port "18789"
|
|
config_get bind main bind "lan"
|
|
config_get pty_port main pty_port "18793"
|
|
|
|
[ "$enabled" = "1" ] || {
|
|
echo "openclaw 已禁用。请在 /etc/config/openclaw 中设置 enabled 为 1"
|
|
return 0
|
|
}
|
|
|
|
# 检查 Node.js
|
|
if [ ! -x "$NODE_BIN" ]; then
|
|
echo "未找到 Node.js: $NODE_BIN"
|
|
echo "请运行: openclaw-env setup"
|
|
return 1
|
|
fi
|
|
if command -v oc_assert_node_min_version >/dev/null 2>&1; then
|
|
oc_assert_node_min_version "$NODE_BIN" "$OC_NODE_MIN_VERSION" >/dev/null || {
|
|
echo "Node.js 版本过低,OpenClaw 需要 >= v${OC_NODE_MIN_VERSION}"
|
|
echo "请运行: openclaw-env node"
|
|
return 1
|
|
}
|
|
fi
|
|
|
|
# 检查 openclaw 入口
|
|
local oc_entry
|
|
oc_entry=$(get_oc_entry)
|
|
if [ -z "$oc_entry" ]; then
|
|
echo "OpenClaw 未安装。请运行: openclaw-env setup"
|
|
return 1
|
|
fi
|
|
|
|
# 同步 UCI 到 JSON
|
|
sync_uci_to_json
|
|
|
|
# 微信 npm 插件自愈注册:OpenClaw 2026.6.11 已把安装记录从
|
|
# openclaw.json 的 plugins.installs 迁移到 SQLite installed_plugin_index。
|
|
# 所有官方 CLI 和 SQLite 注册表操作都以 openclaw 用户执行;完成配置写入后
|
|
# 强制 refresh 并检查 channel capability,避免重启后只加载 memory-core。
|
|
self_heal_wechat_npm_plugin_config() {
|
|
[ -x "$NODE_BIN" ] || return 0
|
|
[ -f "$CONFIG_FILE" ] || return 0
|
|
local npm_projects_dir="${OC_DATA}/.openclaw/npm/projects"
|
|
[ -d "$npm_projects_dir" ] || return 0
|
|
local wechat_plugin_json heal_out index_ok inspect_ok config_ok inspect_file
|
|
wechat_plugin_json=$(find "$npm_projects_dir" -path "*/node_modules/@tencent-weixin/openclaw-weixin/openclaw.plugin.json" -type f 2>/dev/null | head -n 1)
|
|
[ -n "$wechat_plugin_json" ] || return 0
|
|
inspect_file="/tmp/openclaw-wechat-self-heal.inspect"
|
|
rm -f "$inspect_file"
|
|
: > /tmp/openclaw-wechat-self-heal.log
|
|
index_ok=0
|
|
inspect_ok=0
|
|
config_ok=0
|
|
if run_openclaw_cli_as_user "$oc_entry" plugins inspect openclaw-weixin >"$inspect_file" 2>&1; then
|
|
grep -q '^Install:$' "$inspect_file" && grep -q '^Source: npm$' "$inspect_file" && index_ok=1
|
|
grep -q '^Status: loaded$' "$inspect_file" && grep -q '^channel: openclaw-weixin$' "$inspect_file" && inspect_ok=1
|
|
fi
|
|
OC_CONFIG="$CONFIG_FILE" "$NODE_BIN" -e "
|
|
try {
|
|
const d=JSON.parse(require('fs').readFileSync(process.env.OC_CONFIG,'utf8'));
|
|
const ok=d.plugins&&Array.isArray(d.plugins.allow)&&d.plugins.allow.includes('openclaw-weixin')&&
|
|
d.plugins.entries&&d.plugins.entries['openclaw-weixin']&&d.plugins.entries['openclaw-weixin'].enabled===true&&
|
|
d.channels&&d.channels['openclaw-weixin']&&d.channels['openclaw-weixin'].enabled===true&&
|
|
!(d.plugins.installs&&d.plugins.installs['openclaw-weixin']);
|
|
process.exit(ok?0:1);
|
|
} catch(e) { process.exit(1); }
|
|
" >/dev/null 2>&1 && config_ok=1
|
|
if [ "$index_ok" = "1" ] && [ "$inspect_ok" = "1" ] && [ "$config_ok" = "1" ]; then
|
|
cat "$inspect_file" >> /tmp/openclaw-wechat-self-heal.log
|
|
logger -t openclaw "微信插件注册状态已有效,跳过重复刷新"
|
|
rm -f "$inspect_file"
|
|
return 0
|
|
fi
|
|
if [ "$index_ok" != "1" ]; then
|
|
logger -t openclaw "检测到旧微信直装目录,正在迁移到 SQLite 插件索引"
|
|
mkdir -p "${OC_DATA}/.npm" "${OC_DATA}/.tmp" 2>/dev/null || true
|
|
if run_openclaw_cli_as_user "$oc_entry" plugins install --force --pin '@tencent-weixin/openclaw-weixin@2.4.8' \
|
|
>>/tmp/openclaw-wechat-self-heal.log 2>&1; then
|
|
logger -t openclaw "微信插件已迁移到 SQLite installed_plugin_index"
|
|
else
|
|
logger -t openclaw "微信插件 SQLite 索引迁移失败,请查看 /tmp/openclaw-wechat-self-heal.log"
|
|
rm -f "$inspect_file"
|
|
return 0
|
|
fi
|
|
fi
|
|
heal_out=$(OC_CONFIG="$CONFIG_FILE" "$NODE_BIN" -e "
|
|
const fs=require('fs');
|
|
const p=process.env.OC_CONFIG;
|
|
let d={};
|
|
try{d=JSON.parse(fs.readFileSync(p,'utf8'));}catch(e){}
|
|
if(!d.plugins||typeof d.plugins!=='object')d.plugins={};
|
|
if(!Array.isArray(d.plugins.allow))d.plugins.allow=[];
|
|
if(!d.channels||typeof d.channels!=='object')d.channels={};
|
|
let changed=false;
|
|
if(d.plugins.installs&&d.plugins.installs['openclaw-weixin']){
|
|
delete d.plugins.installs['openclaw-weixin']; changed=true;
|
|
}
|
|
if(!d.plugins.allow.includes('openclaw-weixin')){
|
|
d.plugins.allow.push('openclaw-weixin'); changed=true;
|
|
}
|
|
if(!d.channels['openclaw-weixin']||typeof d.channels['openclaw-weixin']!=='object'){
|
|
d.channels['openclaw-weixin']={}; changed=true;
|
|
}
|
|
if(d.channels['openclaw-weixin'].enabled!==true){
|
|
d.channels['openclaw-weixin'].enabled=true; changed=true;
|
|
}
|
|
if(changed){
|
|
fs.writeFileSync(p,JSON.stringify(d,null,2)+'\n');
|
|
console.log('FIXED');
|
|
}
|
|
" 2>/dev/null)
|
|
if echo "$heal_out" | grep -q "FIXED"; then
|
|
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
|
logger -t openclaw "已自愈启用 openclaw-weixin channel"
|
|
fi
|
|
if ! run_openclaw_cli_as_user "$oc_entry" plugins enable openclaw-weixin \
|
|
>>/tmp/openclaw-wechat-self-heal.log 2>&1; then
|
|
logger -t openclaw "微信插件启用失败,请查看 /tmp/openclaw-wechat-self-heal.log"
|
|
rm -f "$inspect_file"
|
|
return 0
|
|
fi
|
|
if ! run_openclaw_cli_as_user "$oc_entry" plugins registry --refresh \
|
|
>>/tmp/openclaw-wechat-self-heal.log 2>&1; then
|
|
logger -t openclaw "微信插件注册表刷新失败,请查看 /tmp/openclaw-wechat-self-heal.log"
|
|
rm -f "$inspect_file"
|
|
return 0
|
|
fi
|
|
if run_openclaw_cli_as_user "$oc_entry" plugins inspect openclaw-weixin >"$inspect_file" 2>&1 && \
|
|
grep -q '^Status: loaded$' "$inspect_file" && grep -q '^channel: openclaw-weixin$' "$inspect_file"; then
|
|
cat "$inspect_file" >> /tmp/openclaw-wechat-self-heal.log
|
|
logger -t openclaw "微信插件 SQLite 注册表已刷新并通过加载验证"
|
|
else
|
|
cat "$inspect_file" >> /tmp/openclaw-wechat-self-heal.log 2>/dev/null || true
|
|
logger -t openclaw "微信插件加载验证失败,请查看 /tmp/openclaw-wechat-self-heal.log"
|
|
fi
|
|
rm -f "$inspect_file"
|
|
}
|
|
self_heal_wechat_npm_plugin_config
|
|
|
|
# v2026.3.13: 修复插件配置中的插件名称不匹配问题
|
|
# OpenClaw 加强了配置验证,plugins.allow 中的名称必须与实际插件名完全匹配
|
|
# 问题: 旧版本写入的是 "openclaw-qqbot",但实际插件名是 "@tencent-connect/openclaw-qqbot"
|
|
fix_plugin_config() {
|
|
local qqbot_ext_dir="${OC_DATA}/.openclaw/extensions/openclaw-qqbot"
|
|
[ ! -d "$qqbot_ext_dir" ] && return
|
|
[ ! -f "$CONFIG_FILE" ] && return
|
|
|
|
"$NODE_BIN" -e "
|
|
const fs=require('fs');
|
|
try{
|
|
const d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));
|
|
if(!d.plugins)d.plugins={};
|
|
let modified=false;
|
|
|
|
// 修复 plugins.allow 数组中的插件名称
|
|
if(Array.isArray(d.plugins.allow)){
|
|
const oldName='openclaw-qqbot';
|
|
const newName='@tencent-connect/openclaw-qqbot';
|
|
const idx=d.plugins.allow.indexOf(oldName);
|
|
if(idx!==-1){
|
|
if(!d.plugins.allow.includes(newName)){
|
|
d.plugins.allow[idx]=newName;
|
|
modified=true;
|
|
}else{
|
|
d.plugins.allow.splice(idx,1);
|
|
modified=true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 同时修复 plugins.entries 中的键名
|
|
if(d.plugins.entries && d.plugins.entries['openclaw-qqbot']){
|
|
if(!d.plugins.entries['@tencent-connect/openclaw-qqbot']){
|
|
d.plugins.entries['@tencent-connect/openclaw-qqbot']=d.plugins.entries['openclaw-qqbot'];
|
|
}
|
|
delete d.plugins.entries['openclaw-qqbot'];
|
|
modified=true;
|
|
}
|
|
|
|
if(modified){
|
|
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
|
|
console.log('FIXED');
|
|
}
|
|
}catch(e){}
|
|
" 2>/dev/null && chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null
|
|
}
|
|
fix_plugin_config
|
|
|
|
# 修复数据目录权限 (防止 root 用户操作后留下无法读取的文件)
|
|
fix_openclaw_state_permissions 2>/dev/null || true
|
|
|
|
# 确保插件目录属主与权限统一 (openclaw:openclaw 755),确保 Gateway 可读取与管理插件
|
|
# 原因: npm/npx 以 root 创建目录时默认权限 700,其他用户无法读取
|
|
local ext_dir="${OC_DATA}/.openclaw/extensions"
|
|
local npm_projects_dir="${OC_DATA}/.openclaw/npm/projects"
|
|
local wechat_legacy_dir="${ext_dir}/openclaw-weixin"
|
|
local wechat_npm_plugin=""
|
|
if [ -d "$npm_projects_dir" ]; then
|
|
wechat_npm_plugin=$(find "$npm_projects_dir" -path "*/node_modules/@tencent-weixin/openclaw-weixin/openclaw.plugin.json" -type f 2>/dev/null | head -n 1)
|
|
fi
|
|
if [ -n "$wechat_npm_plugin" ] && [ -d "$wechat_legacy_dir" ]; then
|
|
local archive_root="${OC_DATA}/.openclaw/archived-extensions"
|
|
mkdir -p "$archive_root" 2>/dev/null || true
|
|
local archived_dir="${archive_root}/openclaw-weixin.legacy.$(date +%Y%m%d%H%M%S)"
|
|
mv "$wechat_legacy_dir" "$archived_dir" 2>/dev/null && logger -t openclaw "已归档旧微信扩展目录: $archived_dir"
|
|
fi
|
|
if [ -n "$wechat_npm_plugin" ] && [ -d "$ext_dir" ]; then
|
|
local archive_root="${OC_DATA}/.openclaw/archived-extensions"
|
|
mkdir -p "$archive_root" 2>/dev/null || true
|
|
for stale_dir in "$ext_dir"/openclaw-weixin.legacy.*; do
|
|
[ -d "$stale_dir" ] || continue
|
|
local stale_name="${stale_dir##*/}"
|
|
mv "$stale_dir" "${archive_root}/${stale_name}" 2>/dev/null || true
|
|
done
|
|
fi
|
|
fix_openclaw_state_permissions 2>/dev/null || true
|
|
|
|
# 不再每次启动都清理 /tmp/jiti。
|
|
# OpenClaw 在低性能 OpenWrt 设备上依赖 JITI/模块缓存降低冷启动成本;
|
|
# 每次 rm -rf 会让 gateway 在第一条 "starting..." 日志前重新编译/装载模块,
|
|
# 实测会额外拉长 20 秒以上。需要强制清理时再显式打开 UCI 开关。
|
|
if [ "$(uci -q get openclaw.main.clear_jiti_cache_on_start 2>/dev/null || echo 0)" = "1" ]; then
|
|
rm -rf /tmp/jiti 2>/dev/null || true
|
|
fi
|
|
|
|
# v2026.4.10: 优化配置迁移 - 只在版本变更时运行 doctor --fix
|
|
# 原因: doctor --fix 耗时 ~50 秒,每次重启都运行严重影响启动速度
|
|
# 策略: 使用标记文件记录已运行的版本,只在版本变化时才执行迁移
|
|
# v2026.7.1: doctor --fix 改为以 openclaw 服务用户执行。
|
|
# OpenClaw 2026.7.x 的插件安全检查按当前进程 uid 校验插件目录属主
|
|
# (属主必须等于进程 uid 或为 root)。root 身份运行 doctor 会把
|
|
# openclaw 属主的微信插件目录判为 suspicious ownership,进而自动
|
|
# 清理 plugins.allow / channels 中的微信配置。
|
|
_run_doctor_fix() {
|
|
local oc_entry="$1"
|
|
local _doc_cmd="cd \"${OC_DATA}\" && HOME=\"${OC_DATA}\" OPENCLAW_HOME=\"${OC_DATA}\" OPENCLAW_STATE_DIR=\"${OC_DATA}/.openclaw\" OPENCLAW_CONFIG_PATH=\"${CONFIG_FILE}\" NODE_ICU_DATA=\"${NODE_BASE}/share/icu\" OPENCLAW_SUPERVISOR_MODE=\"external\" OPENCLAW_SERVICE_REPAIR_POLICY=\"external\" \"${NODE_BIN}\" \"${oc_entry}\" doctor --fix"
|
|
if id openclaw >/dev/null 2>&1; then
|
|
if command -v su >/dev/null 2>&1; then
|
|
su -s /bin/sh openclaw -c "${_doc_cmd}" >/dev/null 2>&1
|
|
elif command -v runuser >/dev/null 2>&1; then
|
|
runuser -u openclaw -- sh -c "${_doc_cmd}" >/dev/null 2>&1
|
|
else
|
|
(cd "${OC_DATA}" 2>/dev/null || true; start-stop-daemon -S -m -p /tmp/openclaw-doctor.pid -c openclaw:openclaw -x /bin/sh -- -c "${_doc_cmd}" >/dev/null 2>&1)
|
|
rm -f /tmp/openclaw-doctor.pid 2>/dev/null || true
|
|
fi
|
|
else
|
|
# openclaw 用户不存在时退回 root 执行,保证迁移仍可运行
|
|
eval "${_doc_cmd}" >/dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
_run_config_migration() {
|
|
local oc_entry="$1"
|
|
[ -z "$oc_entry" ] && return
|
|
|
|
# 获取当前 OpenClaw 版本号 (含补丁后缀,如 2026.7.1-2)
|
|
local current_ver=""
|
|
current_ver=$("$NODE_BIN" "$oc_entry" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?' | head -1)
|
|
[ -z "$current_ver" ] && current_ver="unknown"
|
|
|
|
# 检查标记文件
|
|
local version_marker="${OC_DATA}/.openclaw/.doctor_ran_version"
|
|
local last_ver=""
|
|
if [ -f "$version_marker" ]; then
|
|
last_ver=$(cat "$version_marker" 2>/dev/null | tr -d '[:space:]')
|
|
fi
|
|
|
|
# 只在版本变化时才运行 doctor --fix
|
|
if [ "$current_ver" != "$last_ver" ]; then
|
|
logger -t openclaw "检测到版本变更 ($last_ver -> $current_ver),执行配置迁移 (doctor --fix)..."
|
|
if _run_doctor_fix "$oc_entry"; then
|
|
local _val_cmd="cd \"${OC_DATA}\" && HOME=\"${OC_DATA}\" OPENCLAW_HOME=\"${OC_DATA}\" OPENCLAW_STATE_DIR=\"${OC_DATA}/.openclaw\" OPENCLAW_CONFIG_PATH=\"${CONFIG_FILE}\" NODE_ICU_DATA=\"${NODE_BASE}/share/icu\" OPENCLAW_SUPERVISOR_MODE=\"external\" OPENCLAW_SERVICE_REPAIR_POLICY=\"external\" \"${NODE_BIN}\" \"${oc_entry}\" config validate --json"
|
|
local _val_ok=0
|
|
if run_openclaw_cli_as_user "$oc_entry" config validate --json >/dev/null 2>&1 || eval "${_val_cmd}" >/dev/null 2>&1; then
|
|
_val_ok=1
|
|
fi
|
|
if [ "$_val_ok" -eq 1 ]; then
|
|
_ensure_critical_config
|
|
_sync_token_after_doctor
|
|
echo "$current_ver" > "$version_marker" 2>/dev/null
|
|
chown openclaw:openclaw "$version_marker" 2>/dev/null || true
|
|
logger -t openclaw "配置迁移完成"
|
|
else
|
|
logger -t openclaw "配置校验 (config validate --json) 失败,未记录版本标记"
|
|
fi
|
|
else
|
|
logger -t openclaw "配置迁移失败,请手动检查"
|
|
fi
|
|
else
|
|
logger -t openclaw "版本未变化 ($current_ver),跳过 doctor --fix"
|
|
fi
|
|
}
|
|
_run_config_migration "$oc_entry"
|
|
|
|
# doctor --fix 后再次修复权限 (确保 extensions 与 state 目录保持 openclaw:openclaw 755)
|
|
fix_openclaw_state_permissions 2>/dev/null || true
|
|
|
|
# v2026.4.10: doctor --fix 后确保关键配置字段存在
|
|
# doctor --fix 可能会删除或遗漏某些必要字段,需要补充
|
|
_ensure_critical_config() {
|
|
[ ! -f "$CONFIG_FILE" ] && return
|
|
[ ! -x "$NODE_BIN" ] && return
|
|
local critical_token
|
|
critical_token=$(uci -q get openclaw.main.token || echo "")
|
|
OC_SYNC_TOKEN="$critical_token" "$NODE_BIN" -e "
|
|
const fs = require('fs');
|
|
const f = '${CONFIG_FILE}';
|
|
let d = {};
|
|
try { d = JSON.parse(fs.readFileSync(f, 'utf8')); } catch(e) {}
|
|
|
|
// 确保 gateway.mode 存在
|
|
if (!d.gateway) d.gateway = {};
|
|
if (!d.gateway.mode) d.gateway.mode = 'local';
|
|
|
|
// 确保端口和绑定配置
|
|
if (!d.gateway.port) d.gateway.port = 18789;
|
|
if (!d.gateway.bind) d.gateway.bind = 'lan';
|
|
|
|
// 确保 controlUi 安全配置存在 (允许非 loopback 绑定和 LuCI iframe 嵌入)
|
|
if (!d.gateway.controlUi) d.gateway.controlUi = {};
|
|
if (d.gateway.controlUi.allowInsecureAuth !== true) d.gateway.controlUi.allowInsecureAuth = true;
|
|
if (d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback !== true) d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true;
|
|
if (!Array.isArray(d.gateway.controlUi.allowedOrigins)) d.gateway.controlUi.allowedOrigins = ['*'];
|
|
|
|
// 确保认证配置
|
|
if (!d.gateway.auth) d.gateway.auth = {};
|
|
if (!d.gateway.auth.mode) d.gateway.auth.mode = 'token';
|
|
if (!d.gateway.auth.token && process.env.OC_SYNC_TOKEN) {
|
|
d.gateway.auth.token = process.env.OC_SYNC_TOKEN;
|
|
}
|
|
|
|
// 禁用 ACP dispatch (路由器内存有限)
|
|
if (!d.acp) d.acp = {};
|
|
if (!d.acp.dispatch) d.acp.dispatch = {};
|
|
d.acp.dispatch.enabled = false;
|
|
|
|
// 工具配置
|
|
if (!d.tools) d.tools = {};
|
|
d.tools.profile = 'coding';
|
|
|
|
// 禁用更新检查
|
|
if (!d.update) d.update = {};
|
|
d.update.checkOnStart = false;
|
|
|
|
fs.writeFileSync(f, JSON.stringify(d, null, 2));
|
|
" 2>/dev/null
|
|
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
|
}
|
|
_ensure_critical_config
|
|
|
|
# v2026.4.10: doctor --fix 后再次同步 token (关键!)
|
|
# doctor --fix 可能修改了 JSON 中的 token,必须同步回 UCI
|
|
# 否则 LuCI 显示的 token 与 Gateway 实际使用的 token 不一致
|
|
_sync_token_after_doctor() {
|
|
[ ! -f "$CONFIG_FILE" ] && return
|
|
[ ! -x "$NODE_BIN" ] && return
|
|
local json_token uci_token
|
|
json_token=$("$NODE_BIN" -e "
|
|
try{const d=JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8'));
|
|
if(d.gateway&&d.gateway.auth&&d.gateway.auth.token)process.stdout.write(d.gateway.auth.token);}catch(e){}
|
|
" 2>/dev/null)
|
|
uci_token=$(uci -q get openclaw.main.token || echo "")
|
|
|
|
# JSON 有 token 且与 UCI 不同时,以 JSON 为准更新 UCI
|
|
if [ -n "$json_token" ] && [ "$json_token" != "$uci_token" ]; then
|
|
uci set openclaw.main.token="$json_token"
|
|
uci commit openclaw 2>/dev/null
|
|
logger -t openclaw "Token 同步 (doctor 后): JSON -> UCI"
|
|
elif [ -z "$json_token" ] && [ -n "$uci_token" ]; then
|
|
OC_SYNC_TOKEN="$uci_token" OC_SYNC_FILE="$CONFIG_FILE" "$NODE_BIN" -e "
|
|
const fs=require('fs');
|
|
const f=process.env.OC_SYNC_FILE;
|
|
let d={};
|
|
try{d=JSON.parse(fs.readFileSync(f,'utf8'));}catch(e){}
|
|
if(!d.gateway)d.gateway={};
|
|
if(!d.gateway.auth)d.gateway.auth={};
|
|
d.gateway.auth.mode=d.gateway.auth.mode||'token';
|
|
d.gateway.auth.token=process.env.OC_SYNC_TOKEN;
|
|
fs.writeFileSync(f, JSON.stringify(d, null, 2));
|
|
" 2>/dev/null
|
|
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
|
logger -t openclaw "Token 同步 (doctor 后): UCI -> JSON"
|
|
fi
|
|
}
|
|
_sync_token_after_doctor
|
|
|
|
# v2026.4.10: final permission fix before Gateway start
|
|
fix_openclaw_state_permissions 2>/dev/null || true
|
|
|
|
# 将 UCI bind 映射到 openclaw gateway --bind 参数
|
|
local gw_bind="loopback"
|
|
case "$bind" in
|
|
lan) gw_bind="lan" ;;
|
|
loopback) gw_bind="loopback" ;;
|
|
all) gw_bind="custom" ;; # custom = 0.0.0.0
|
|
*) gw_bind="$bind" ;;
|
|
esac
|
|
|
|
local gw_token
|
|
gw_token=$(uci -q get openclaw.main.token || echo "")
|
|
|
|
# 确保网关端口未被残留进程占用 (防止 restart 时 crash loop)
|
|
# v2026.3.14 优化: 快速轮询 + 批量 kill
|
|
_ensure_port_free() {
|
|
local p="$1"
|
|
local i=0 max_wait=10 # 10 * 0.2 = 2 秒
|
|
|
|
# 先检查端口是否已被占用
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
fi
|
|
|
|
# 端口被占用,尝试清理
|
|
for occ_pid in $(pgrep -f "openclaw-gateway" 2>/dev/null); do
|
|
kill "$occ_pid" 2>/dev/null
|
|
done
|
|
|
|
while [ $i -lt $max_wait ]; do
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
fi
|
|
usleep 200000 2>/dev/null || sleep 1
|
|
i=$((i + 1))
|
|
done
|
|
|
|
# 最后手段: SIGKILL
|
|
local port_pid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
port_pid=$(ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | awk '{print $7}' | cut -d'/' -f1 | grep -E '^[0-9]+$' | head -n 1)
|
|
fi
|
|
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && usleep 300000 2>/dev/null
|
|
return 0
|
|
}
|
|
_ensure_port_free "$port"
|
|
|
|
# Node 22 原生编译缓存。OpenClaw 主入口在路由器上首次 import/compile 较慢;
|
|
# 保留该缓存可以显著缩短后续重启时 gateway 第一条日志前的等待时间。
|
|
local node_compile_cache="/tmp/openclaw-node-compile-cache"
|
|
mkdir -p "$node_compile_cache" 2>/dev/null || true
|
|
chown openclaw:openclaw "$node_compile_cache" 2>/dev/null || true
|
|
chmod 700 "$node_compile_cache" 2>/dev/null || true
|
|
|
|
# 启动 OpenClaw Gateway (主服务, 前台运行)
|
|
# v2026.4.7/4.9: 环境变量安全验证 (阻止危险变量注入)
|
|
_validate_path() {
|
|
local p="$1"
|
|
case "$p" in
|
|
*'$'*|*'`'*|*'|'*|*'&'*) return 1 ;;
|
|
/proc/*|/sys/*|/dev/*) return 1 ;;
|
|
*) return 0 ;;
|
|
esac
|
|
}
|
|
for _dir in "$NODE_BASE" "$OC_GLOBAL" "$OC_DATA"; do
|
|
_validate_path "$_dir" || { logger -t openclaw "安全警告: 路径验证失败: $_dir"; return 1; }
|
|
done
|
|
|
|
# 允许 LuCI 页面以 iframe 内嵌 OpenClaw 官方 Web 控制台
|
|
[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh patch-control-ui "$OC_GLOBAL"
|
|
|
|
procd_open_instance "gateway"
|
|
procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \
|
|
--port "$port" --bind "$gw_bind"
|
|
procd_set_param env \
|
|
HOME="$OC_DATA" \
|
|
OPENCLAW_HOME="$OC_DATA" \
|
|
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
|
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
|
OPENCLAW_GATEWAY_TOKEN="$gw_token" \
|
|
OPENCLAW_SUPERVISOR_MODE="external" \
|
|
OPENCLAW_SERVICE_REPAIR_POLICY="external" \
|
|
NODE_COMPILE_CACHE="$node_compile_cache" \
|
|
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
|
|
NODE_BASE="$NODE_BASE" \
|
|
OC_GLOBAL="$OC_GLOBAL" \
|
|
OC_DATA="$OC_DATA" \
|
|
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
procd_set_param user openclaw
|
|
procd_set_param term_timeout 2
|
|
# Gateway 的人工重载很常见,不能把 1 小时内的重启都计入 crash throttling。
|
|
# threshold=5 让正常运行超过 5 秒后的重载不累计失败;timeout=1 保持快速拉起;
|
|
# retry=-1 避免多次保存配置/扫码后 procd 停止 respawn。
|
|
procd_set_param respawn 5 1 -1
|
|
procd_set_param stdout 1
|
|
procd_set_param stderr 1
|
|
procd_set_param pidfile /var/run/openclaw.pid
|
|
procd_close_instance
|
|
|
|
# 启动 Web PTY 配置终端 (辅助服务)
|
|
# 复用已有 PTY token,只在不存在时才生成新的
|
|
# 这样避免服务重启时 PTY WebSocket 连接因 token 变化而断开
|
|
local pty_token
|
|
pty_token=$(uci -q get openclaw.main.pty_token || echo "")
|
|
if [ -z "$pty_token" ]; then
|
|
pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)")
|
|
uci set openclaw.main.pty_token="$pty_token"
|
|
uci commit openclaw 2>/dev/null
|
|
fi
|
|
|
|
procd_open_instance "pty"
|
|
procd_set_param command "$NODE_BIN" /usr/share/openclaw/web-pty.js
|
|
procd_set_param env \
|
|
OC_CONFIG_PORT="$pty_port" \
|
|
OC_PTY_TOKEN="$pty_token" \
|
|
OC_CONFIG_SCRIPT="/usr/share/openclaw/oc-config.sh" \
|
|
NODE_COMPILE_CACHE="$node_compile_cache" \
|
|
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
|
|
NODE_BASE="$NODE_BASE" \
|
|
OC_GLOBAL="$OC_GLOBAL" \
|
|
OC_DATA="$OC_DATA" \
|
|
HOME="$OC_DATA" \
|
|
OPENCLAW_HOME="$OC_DATA" \
|
|
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
|
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
|
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
procd_set_param respawn 3600 5 5
|
|
procd_set_param term_timeout 2
|
|
procd_set_param stdout 1
|
|
procd_set_param stderr 1
|
|
procd_set_param pidfile /var/run/openclaw-pty.pid
|
|
procd_close_instance
|
|
}
|
|
|
|
stop_service() {
|
|
# procd 会自动停止它管理的主进程 (openclaw)
|
|
# 但 openclaw 会 fork 出 openclaw-gateway 子进程实际监听端口
|
|
# procd 不一定能正确追踪并杀掉子进程树,需要手动清理
|
|
|
|
local port
|
|
port=$(uci -q get openclaw.main.port || echo "18789")
|
|
|
|
# v2026.3.14 优化: 快速终止进程树,减少等待时间
|
|
# 1) 先获取所有相关 PID
|
|
local gw_pids=""
|
|
gw_pids=$(pgrep -f "openclaw-gateway" 2>/dev/null)
|
|
gw_pids="$gw_pids $(pgrep -f "openclaw.*gateway.*run" 2>/dev/null)"
|
|
|
|
# 2) 同时发送 SIGTERM 给所有进程
|
|
for pid in $gw_pids; do
|
|
[ -n "$pid" ] && kill "$pid" 2>/dev/null
|
|
done
|
|
|
|
# 3) 快速轮询等待端口释放 (200ms 间隔,最多 3 秒)
|
|
local wait_count=0
|
|
local max_wait=15 # 15 * 0.2 = 3 秒
|
|
while [ $wait_count -lt $max_wait ]; do
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
fi
|
|
usleep 200000 2>/dev/null || sleep 1
|
|
wait_count=$((wait_count + 1))
|
|
done
|
|
|
|
# 4) 如果端口仍被占用,强制 SIGKILL
|
|
if [ $wait_count -ge $max_wait ]; then
|
|
local port_pid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | awk '{print $7}' | cut -d'/' -f1 | grep -E '^[0-9]+$' | head -n 1)
|
|
fi
|
|
if [ -n "$port_pid" ]; then
|
|
kill -9 "$port_pid" 2>/dev/null
|
|
# 等待内核回收 (缩短到 0.5 秒)
|
|
usleep 500000 2>/dev/null || sleep 1
|
|
fi
|
|
fi
|
|
|
|
# 5) 终极保险:使用暴力匹配端口杀死该名下任何相关占用进程
|
|
local _pty_port=$(uci -q get openclaw.main.pty_port || echo "18793")
|
|
for _p in "$port" "$_pty_port"; do
|
|
local _spid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
_spid=$(ss -tulnp 2>/dev/null | grep ":${_p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
_spid=$(netstat -tulnp 2>/dev/null | grep ":${_p} " | awk '{print $7}' | cut -d'/' -f1 | grep -E '^[0-9]+$' | head -n 1)
|
|
fi
|
|
[ -n "$_spid" ] && kill -9 "$_spid" 2>/dev/null || true
|
|
done
|
|
|
|
# 6) 清理该特定系统用户下任何悬挂或离群孤儿进程
|
|
if getent passwd openclaw >/dev/null 2>&1 || grep -q '^openclaw:' /etc/passwd 2>/dev/null; then
|
|
for _opcup in $(pgrep -u openclaw 2>/dev/null); do
|
|
kill -9 "$_opcup" 2>/dev/null || true
|
|
done
|
|
fi
|
|
}
|
|
|
|
service_triggers() {
|
|
procd_add_reload_trigger "openclaw"
|
|
|
|
}
|
|
|
|
reload_service() {
|
|
stop
|
|
# v2026.3.14: stop_service 已优化等待逻辑,缩短额外等待时间
|
|
usleep 300000 2>/dev/null || sleep 1
|
|
start
|
|
}
|
|
|
|
setup() {
|
|
echo "正在调用 openclaw-env setup..."
|
|
/usr/bin/openclaw-env setup
|
|
}
|
|
|
|
restart_gateway() {
|
|
# 仅重启 Gateway 进程,通过 kill 触发 procd respawn
|
|
# 绝对不能调用 start_service,否则会重启 PTY 实例
|
|
|
|
local lock_dir="/tmp/openclaw-restart-gateway.lock"
|
|
if ! mkdir "$lock_dir" 2>/dev/null; then
|
|
logger -t openclaw "restart_gateway 已在执行,跳过重复请求"
|
|
return 0
|
|
fi
|
|
|
|
local port
|
|
port=$(uci -q get openclaw.main.port || echo "18789")
|
|
logger -t openclaw "restart_gateway: 正在重启 Gateway"
|
|
|
|
# v2026.3.14 优化: 快速终止并重启
|
|
# 1) 同时获取端口 PID 和 procd 管理的 PID
|
|
local port_pid="" gw_pid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
|
|
fi
|
|
gw_pid=$(ubus call service list '{"name":"openclaw"}' 2>/dev/null | \
|
|
jsonfilter -e '$.openclaw.instances.gateway.pid' 2>/dev/null) || true
|
|
|
|
# 2) OpenClaw 支持 SIGUSR1 触发 in-process restart。
|
|
# 这比 SIGTERM/procd respawn 快很多,并能避免 stop 路径默认 25 秒 shutdown timeout。
|
|
# 注意要发给实际监听端口的 gateway 子进程,而不是 procd 记录的 node 父进程。
|
|
if [ -n "$port_pid" ] && kill -0 "$port_pid" 2>/dev/null; then
|
|
kill -USR1 "$port_pid" 2>/dev/null && {
|
|
logger -t openclaw "restart_gateway: 已通过 SIGUSR1 请求 Gateway 快速重载"
|
|
rmdir "$lock_dir" 2>/dev/null || true
|
|
return 0
|
|
}
|
|
fi
|
|
|
|
# 3) SIGUSR1 不可用时,回退到旧的 TERM + procd respawn 路径
|
|
[ -n "$port_pid" ] && kill "$port_pid" 2>/dev/null
|
|
[ -n "$gw_pid" ] && kill -0 "$gw_pid" 2>/dev/null && kill "$gw_pid" 2>/dev/null
|
|
|
|
# 4) 兜底: kill 所有残留进程
|
|
for pid in $(pgrep -f "openclaw-gateway" 2>/dev/null) $(pgrep -f "openclaw.*gateway.*run" 2>/dev/null); do
|
|
kill "$pid" 2>/dev/null
|
|
done
|
|
|
|
# 5) 快速轮询等待端口释放 (200ms 间隔,最多 2 秒)
|
|
local wait_count=0
|
|
while [ $wait_count -lt 10 ]; do
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
fi
|
|
usleep 200000 2>/dev/null || sleep 1
|
|
wait_count=$((wait_count + 1))
|
|
done
|
|
|
|
# 6) 如果端口仍被占用,强制 SIGKILL
|
|
if [ $wait_count -ge 10 ]; then
|
|
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null
|
|
usleep 300000 2>/dev/null || sleep 1
|
|
fi
|
|
|
|
# 7) 如果 procd 中没有 gateway 服务注册,调用 start
|
|
if [ -z "$gw_pid" ]; then
|
|
/etc/init.d/openclaw start >/dev/null 2>&1
|
|
fi
|
|
# 如果 gw_pid 存在,procd respawn 会自动重启 gateway
|
|
rmdir "$lock_dir" 2>/dev/null || true
|
|
}
|
|
|
|
status_service() {
|
|
local port pty_port enabled
|
|
port=$(uci -q get openclaw.main.port || echo "18789")
|
|
pty_port=$(uci -q get openclaw.main.pty_port || echo "18793")
|
|
enabled=$(uci -q get openclaw.main.enabled || echo "0")
|
|
|
|
echo "=== OpenClaw 服务状态 ==="
|
|
|
|
# Node.js
|
|
if [ -x "$NODE_BIN" ]; then
|
|
echo "Node.js: $($NODE_BIN --version 2>/dev/null)"
|
|
else
|
|
echo "Node.js: 未安装"
|
|
fi
|
|
|
|
# OpenClaw
|
|
local oc_entry
|
|
oc_entry=$(get_oc_entry)
|
|
if [ -n "$oc_entry" ]; then
|
|
local ver
|
|
ver=$("$NODE_BIN" "$oc_entry" --version 2>/dev/null | tr -d '[:space:]')
|
|
echo "OpenClaw: v${ver:-未知}"
|
|
else
|
|
echo "OpenClaw: 未安装"
|
|
fi
|
|
|
|
# 端口检测函数 (ss 优先, 回退 netstat)
|
|
_check_port() {
|
|
local p="$1"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} "
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} "
|
|
fi
|
|
}
|
|
|
|
_get_pid_by_port() {
|
|
local p="$1"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep ":${p} " | head -1 | sed -n 's/.*pid=\([0-9]*\).*/\1/p'
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep ":${p} " | head -1 | sed 's|.* \([0-9]*\)/.*|\1|'
|
|
fi
|
|
}
|
|
|
|
# Gateway port
|
|
if _check_port "$port"; then
|
|
echo "网关: 运行中 (端口 $port)"
|
|
# PID
|
|
local pid
|
|
pid=$(_get_pid_by_port "$port")
|
|
if [ -n "$pid" ]; then
|
|
echo "进程ID: $pid"
|
|
# Memory
|
|
local rss
|
|
rss=$(awk '/VmRSS/{print $2}' /proc/$pid/status 2>/dev/null)
|
|
[ -n "$rss" ] && echo "内存: ${rss} kB"
|
|
# Uptime
|
|
local start_time now_time
|
|
start_time=$(stat -c %Y /proc/$pid 2>/dev/null || echo "0")
|
|
now_time=$(date +%s)
|
|
if [ "$start_time" -gt 0 ]; then
|
|
local uptime=$((now_time - start_time))
|
|
local hours=$((uptime / 3600))
|
|
local mins=$(( (uptime % 3600) / 60 ))
|
|
echo "运行时间: ${hours}小时 ${mins}分钟"
|
|
fi
|
|
fi
|
|
else
|
|
local procd_running procd_pid procd_exit procd_pid_alive pidfile_pid pidfile_stale crash_loop
|
|
procd_running=$(ubus call service list '{"name":"openclaw"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.running' 2>/dev/null | tr -d '[:space:]')
|
|
procd_pid=$(ubus call service list '{"name":"openclaw"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.pid' 2>/dev/null | tr -d '[:space:]')
|
|
procd_exit=$(ubus call service list '{"name":"openclaw"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.exit_code' 2>/dev/null | tr -d '[:space:]')
|
|
[ "$procd_pid" = "null" ] && procd_pid=""
|
|
[ "$procd_exit" = "null" ] && procd_exit=""
|
|
procd_pid_alive=0
|
|
[ -n "$procd_pid" ] && [ -d "/proc/$procd_pid" ] && procd_pid_alive=1
|
|
pidfile_pid=$(cat /var/run/openclaw.pid 2>/dev/null | tr -d '[:space:]')
|
|
pidfile_stale=0
|
|
[ -n "$pidfile_pid" ] && [ ! -d "/proc/$pidfile_pid" ] && pidfile_stale=1
|
|
crash_loop=$(logread 2>/dev/null | grep -E 'Instance openclaw::gateway.*crash loop' | tail -1)
|
|
if [ "$enabled" != "1" ]; then
|
|
echo "网关: 已禁用"
|
|
elif [ -n "$procd_exit" ] && [ "$procd_exit" != "0" ] && [ "$procd_running" != "true" ]; then
|
|
echo "网关: 启动失败 (退出码 $procd_exit)"
|
|
elif [ -n "$crash_loop" ] && [ "$pidfile_stale" = "1" ] && [ "$procd_running" != "true" ] && [ "$procd_pid_alive" != "1" ]; then
|
|
echo "网关: 启动失败 (procd crash-loop 抑制)"
|
|
elif [ "$procd_running" = "true" ] || [ "$procd_pid_alive" = "1" ]; then
|
|
echo "网关: 正在启动 (通常 20~40 秒,首次安装或负载较高时会更久)"
|
|
[ -n "$procd_pid" ] && echo "进程ID: $procd_pid"
|
|
else
|
|
echo "网关: 已停止"
|
|
fi
|
|
fi
|
|
|
|
# PTY port
|
|
if _check_port "$pty_port"; then
|
|
echo "Web PTY: 运行中 (端口 $pty_port)"
|
|
else
|
|
echo "Web PTY: 已停止"
|
|
fi
|
|
}
|