op-packages/luci-app-openclaw/root/etc/init.d/openclaw
github-actions[bot] ccfd18b795 🍉 Sync 2026-05-16 08:37:13
2026-05-16 08:37:13 +08:00

839 lines
30 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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.14.0}"
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"
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"
}
patch_iframe_headers() {
# 移除 OpenClaw 网关的 X-Frame-Options 和 frame-ancestors 限制,允许 LuCI iframe 嵌入
# v2026.4.x: 安全头设置迁移到 server.impl-*.js 文件
local patched=0
# 1. 搜索 server.impl-*.js (v2026.4.x 新位置)
for f in $(find "${OC_GLOBAL}/lib/node_modules/openclaw/dist" -name "server.impl-*.js" -type f 2>/dev/null); do
if grep -q "X-Frame-Options.*DENY\|frame-ancestors.*none" "$f" 2>/dev/null; then
sed -i 's|res\.setHeader("X-Frame-Options", "DENY")|res.setHeader("X-Frame-Options", "ALLOW-FROM *") // patched by luci-app-openclaw|g' "$f"
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
logger -t openclaw "Patched iframe headers in $f"
patched=1
fi
done
# 2. 兼容旧版本: 搜索 gateway-cli-*.js (v2026.3.x)
for search_root in "${OC_GLOBAL}" "${NODE_BASE}/lib"; do
[ -d "$search_root" ] || continue
for f in $(find "$search_root" -name "gateway-cli-*.js" -type f 2>/dev/null); do
if grep -q "X-Frame-Options.*DENY" "$f" 2>/dev/null; then
sed -i "s|res.setHeader(\"X-Frame-Options\", \"DENY\");|// res.setHeader(\"X-Frame-Options\", \"DENY\"); // patched by luci-app-openclaw|g" "$f"
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
logger -t openclaw "Patched iframe headers in $f"
patched=1
fi
done
done
[ $patched -eq 1 ] && logger -t openclaw "iframe headers patched successfully"
}
sync_uci_to_json() {
# 将 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={};
try{d=JSON.parse(fs.readFileSync(f,'utf8'));}catch(e){}
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.dangerouslyDisableDeviceAuth=true;
d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true;
// 清理 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" 警告)
// 扫描已安装的插件并添加到 allow 列表
if(!d.plugins)d.plugins={};
if(!Array.isArray(d.plugins.allow))d.plugins.allow=[];
// 从 plugins.installs 中提取已安装插件
// 注意: 使用 installPath 中的目录名作为插件 ID (与 openclaw.plugin.json 中的 id 一致)
if(d.plugins.installs){
Object.keys(d.plugins.installs).forEach(key=>{
const inst=d.plugins.installs[key];
if(inst&&inst.installPath){
// 从 installPath 提取插件目录名 (如 openclaw-weixin)
const match=inst.installPath.match(/\/([^\/]+)\/?$/);
if(match&&match[1]&&!d.plugins.allow.includes(match[1])){
d.plugins.allow.push(match[1]);
}
}
});
}
// v2026.5.x: 微信插件渠道名统一为 openclaw-weixin
// 旧配置里可能残留 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);
}
['entries','installs'].forEach(ns=>{
if(d.plugins&&d.plugins[ns]&&d.plugins[ns].weixin){
if(!d.plugins[ns]['openclaw-weixin'])d.plugins[ns]['openclaw-weixin']=d.plugins[ns].weixin;
delete d.plugins[ns].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.plugins.allow.includes('openclaw-weixin') && d.plugins.installs && d.plugins.installs['openclaw-weixin']){
d.plugins.allow.push('openclaw-weixin');
}
// 始终信任 copilot-proxy (内置插件)
if(!d.plugins.allow.includes('copilot-proxy'))d.plugins.allow.push('copilot-proxy');
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
# 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 用户操作后留下无法读取的文件)
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
# v2026.4.9: 修复插件目录权限 (OpenClaw 要求插件目录属主为 root)
# 详见: https://github.com/nicepkg/openclaw/releases/tag/v2026.4.9
# v2026.4.10 补充: 同时修复权限模式为 755确保 Gateway 可读取插件
# 原因: npm/npx 以 root 创建目录时默认权限 700其他用户无法读取
local ext_dir="${OC_DATA}/.openclaw/extensions"
if [ -d "$ext_dir" ]; then
chown -R root:root "$ext_dir" 2>/dev/null || true
chmod -R 755 "$ext_dir" 2>/dev/null || true
fi
# v2026.4.x: 清理 jiti 缓存目录 (修复微信插件加载权限问题)
# jiti 编译 TypeScript 时会在 /tmp/jiti 创建缓存,如果由 root 创建则 openclaw 用户无法写入
rm -rf /tmp/jiti 2>/dev/null || true
# v2026.4.10: 优化配置迁移 - 只在版本变更时运行 doctor --fix
# 原因: doctor --fix 耗时 ~50 秒,每次重启都运行严重影响启动速度
# 策略: 使用标记文件记录已运行的版本,只在版本变化时才执行迁移
_run_config_migration() {
local oc_entry="$1"
[ -z "$oc_entry" ] && return
# 获取当前 OpenClaw 版本号 (只取数字版本,如 2026.4.9)
local current_ver=""
current_ver=$("$NODE_BIN" "$oc_entry" --version 2>/dev/null | grep -oE '[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)..."
OPENCLAW_HOME="$OC_DATA" OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
"$NODE_BIN" "$oc_entry" doctor --fix 2>/dev/null && \
logger -t openclaw "配置迁移完成" || \
logger -t openclaw "配置迁移失败,请手动检查"
# 更新标记文件
echo "$current_ver" > "$version_marker" 2>/dev/null
chown openclaw:openclaw "$version_marker" 2>/dev/null || true
else
logger -t openclaw "版本未变化 ($current_ver),跳过 doctor --fix"
fi
}
_run_config_migration "$oc_entry"
# doctor --fix 后再次修复权限 (防止 root 创建的文件导致 EACCES)
# 注意: extensions 目录保持 root 权限 (OpenClaw 安全要求)
find "$OC_DATA" -user root ! -path "${OC_DATA}/.openclaw/extensions*" -exec chown openclaw:openclaw {} \; 2>/dev/null || true
# v2026.4.10: doctor --fix 后确保关键配置字段存在
# doctor --fix 可能会删除或遗漏某些必要字段,需要补充
_ensure_critical_config() {
[ ! -f "$CONFIG_FILE" ] && return
[ ! -x "$NODE_BIN" ] && return
"$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.dangerouslyDisableDeviceAuth !== true) d.gateway.controlUi.dangerouslyDisableDeviceAuth = true;
if (d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback !== true) d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true;
// 确保认证配置
if (!d.gateway.auth) d.gateway.auth = {};
if (!d.gateway.auth.mode) d.gateway.auth.mode = '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"
fi
}
_sync_token_after_doctor
# v2026.4.10: 最终权限修复 - 确保 Gateway 启动前所有数据目录权限正确
# doctor --fix 或 _ensure_critical_config 可能创建新的目录/文件
# 关键: agents 目录下的 agent 子目录必须可被 openclaw 用户写入
find "${OC_DATA}/.openclaw" -user root -type d -exec chown openclaw:openclaw {} \; 2>/dev/null || true
find "${OC_DATA}/.openclaw" -user root -type f -exec chown openclaw:openclaw {} \; 2>/dev/null || true
# 特别确保 agents/main/agent 目录权限 (模型配置写入目录)
local agent_dir="${OC_DATA}/.openclaw/agents/main/agent"
if [ -d "$agent_dir" ]; then
chown openclaw:openclaw "$agent_dir" 2>/dev/null || true
chmod 755 "$agent_dir" 2>/dev/null || true
fi
# extensions 目录除外 - 必须保持 root 权限 (OpenClaw 安全要求)
# 同时修复权限模式为 755确保 Gateway 可读取插件
if [ -d "${OC_DATA}/.openclaw/extensions" ]; then
chown -R root:root "${OC_DATA}/.openclaw/extensions" 2>/dev/null || true
chmod -R 755 "${OC_DATA}/.openclaw/extensions" 2>/dev/null || true
fi
# Patch iframe 安全头,允许 LuCI 嵌入
patch_iframe_headers
# 将 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
# 确保网关端口未被残留进程占用 (防止 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"
# 启动 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
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" \
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 respawn 3600 10 5
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_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 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 port
port=$(uci -q get openclaw.main.port || echo "18789")
# 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) 同时发送 SIGTERM 给所有相关进程
[ -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
# 3) 兜底: 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
# 4) 快速轮询等待端口释放 (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
# 5) 如果端口仍被占用,强制 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
# 6) 如果 procd 中没有 gateway 服务注册,调用 start
if [ -z "$gw_pid" ]; then
/etc/init.d/openclaw start >/dev/null 2>&1
fi
# 如果 gw_pid 存在procd respawn 会自动重启 gateway
}
status_service() {
local port pty_port
port=$(uci -q get openclaw.main.port || echo "18789")
pty_port=$(uci -q get openclaw.main.pty_port || echo "18793")
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
elif pgrep -f "openclaw.*gateway" >/dev/null 2>&1; then
echo "网关: 正在启动 (首次启动可能需要 2~5 分钟)"
else
echo "网关: 已停止"
fi
# PTY port
if _check_port "$pty_port"; then
echo "Web PTY: 运行中 (端口 $pty_port)"
else
echo "Web PTY: 已停止"
fi
}