mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 04:15:06 +08:00
🌴 Sync 2026-07-08 20:36:07
This commit is contained in:
@@ -16,6 +16,10 @@ EXTRA_HELP=" setup 下载 Node.js 并安装 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
|
||||
@@ -311,6 +315,59 @@ fi
|
||||
# 同步 UCI 到 JSON
|
||||
sync_uci_to_json
|
||||
|
||||
# 微信 npm 插件自愈注册:
|
||||
# 有些环境里 npm projects 已安装 @tencent-weixin/openclaw-weixin,
|
||||
# 但 openclaw.json 缺少 plugins.installs / plugins.allow / channels 配置,
|
||||
# 会导致页面显示“已安装”而 Gateway 或登录流程无法稳定加载插件。
|
||||
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 wechat_plugin_dir heal_out
|
||||
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
|
||||
wechat_plugin_dir="${wechat_plugin_json%/openclaw.plugin.json}"
|
||||
heal_out=$(OC_CONFIG="$CONFIG_FILE" OC_WECHAT_PLUGIN_DIR="$wechat_plugin_dir" "$NODE_BIN" -e "
|
||||
const fs=require('fs');
|
||||
const p=process.env.OC_CONFIG;
|
||||
const pluginDir=process.env.OC_WECHAT_PLUGIN_DIR;
|
||||
let d={};
|
||||
try{d=JSON.parse(fs.readFileSync(p,'utf8'));}catch(e){}
|
||||
if(!d.plugins||typeof d.plugins!=='object')d.plugins={};
|
||||
if(!d.plugins.installs||typeof d.plugins.installs!=='object')d.plugins.installs={};
|
||||
if(!Array.isArray(d.plugins.allow))d.plugins.allow=[];
|
||||
if(!d.channels||typeof d.channels!=='object')d.channels={};
|
||||
let changed=false;
|
||||
const want={kind:'npm',installPath:pluginDir,packageName:'@tencent-weixin/openclaw-weixin'};
|
||||
const cur=d.plugins.installs['openclaw-weixin'];
|
||||
if(!cur||cur.kind!==want.kind||cur.installPath!==want.installPath||cur.packageName!==want.packageName){
|
||||
d.plugins.installs['openclaw-weixin']=want; 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(d.plugins.entries&&d.plugins.entries['openclaw-weixin']){
|
||||
delete d.plugins.entries['openclaw-weixin']; 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 npm plugin: $wechat_plugin_dir"
|
||||
fi
|
||||
}
|
||||
self_heal_wechat_npm_plugin_config
|
||||
|
||||
# v2026.3.13: 修复插件配置中的插件名称不匹配问题
|
||||
# OpenClaw 加强了配置验证,plugins.allow 中的名称必须与实际插件名完全匹配
|
||||
# 问题: 旧版本写入的是 "openclaw-qqbot",但实际插件名是 "@tencent-connect/openclaw-qqbot"
|
||||
@@ -361,7 +418,7 @@ fi
|
||||
fix_plugin_config
|
||||
|
||||
# 修复数据目录权限 (防止 root 用户操作后留下无法读取的文件)
|
||||
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
|
||||
# v2026.4.9: 修复插件目录权限 (OpenClaw 要求插件目录属主为 root)
|
||||
# 详见: https://github.com/nicepkg/openclaw/releases/tag/v2026.4.9
|
||||
@@ -389,17 +446,8 @@ fi
|
||||
mv "$stale_dir" "${archive_root}/${stale_name}" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
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
|
||||
if [ -d "$npm_projects_dir" ]; then
|
||||
chown -R root:root "$npm_projects_dir" 2>/dev/null || true
|
||||
chmod -R 755 "$npm_projects_dir" 2>/dev/null || true
|
||||
fi
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
|
||||
# v2026.4.x: 清理 jiti 缓存目录 (修复微信插件加载权限问题)
|
||||
# jiti 编译 TypeScript 时会在 /tmp/jiti 创建缓存,如果由 root 创建则 openclaw 用户无法写入
|
||||
rm -rf /tmp/jiti 2>/dev/null || true
|
||||
|
||||
# v2026.4.10: 优化配置迁移 - 只在版本变更时运行 doctor --fix
|
||||
@@ -425,6 +473,7 @@ fi
|
||||
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_ICU_DATA="${NODE_BASE}/share/icu" PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH" \
|
||||
"$NODE_BIN" "$oc_entry" doctor --fix 2>/dev/null && \
|
||||
logger -t openclaw "配置迁移完成" || \
|
||||
logger -t openclaw "配置迁移失败,请手动检查"
|
||||
@@ -439,14 +488,16 @@ fi
|
||||
|
||||
# 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
|
||||
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
|
||||
"$NODE_BIN" -e "
|
||||
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 = {};
|
||||
@@ -469,6 +520,9 @@ if (d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback !== true) d.gat
|
||||
// 确保认证配置
|
||||
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 = {};
|
||||
@@ -507,43 +561,26 @@ if(d.gateway&&d.gateway.auth&&d.gateway.auth.token)process.stdout.write(d.gatewa
|
||||
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: 最终权限修复 - 确保 Gateway 启动前所有数据目录权限正确
|
||||
# doctor --fix 或 _ensure_critical_config 可能创建新的目录/文件
|
||||
# 关键: agents 目录下的 agent 子目录必须可被 openclaw 用户写入
|
||||
find "${OC_DATA}/.openclaw" -user root \
|
||||
! -path "${OC_DATA}/.openclaw/extensions*" \
|
||||
! -path "${OC_DATA}/.openclaw/archived-extensions*" \
|
||||
! -path "${OC_DATA}/.openclaw/npm/projects*" \
|
||||
-type d -exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
find "${OC_DATA}/.openclaw" -user root \
|
||||
! -path "${OC_DATA}/.openclaw/extensions*" \
|
||||
! -path "${OC_DATA}/.openclaw/archived-extensions*" \
|
||||
! -path "${OC_DATA}/.openclaw/npm/projects*" \
|
||||
-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
|
||||
if [ -d "${OC_DATA}/.openclaw/archived-extensions" ]; then
|
||||
chown -R root:root "${OC_DATA}/.openclaw/archived-extensions" 2>/dev/null || true
|
||||
chmod -R 700 "${OC_DATA}/.openclaw/archived-extensions" 2>/dev/null || true
|
||||
fi
|
||||
if [ -d "${OC_DATA}/.openclaw/npm/projects" ]; then
|
||||
chown -R root:root "${OC_DATA}/.openclaw/npm/projects" 2>/dev/null || true
|
||||
chmod -R 755 "${OC_DATA}/.openclaw/npm/projects" 2>/dev/null || true
|
||||
fi
|
||||
# v2026.4.10: final permission fix before Gateway start
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
|
||||
# Patch iframe 安全头,允许 LuCI 嵌入
|
||||
patch_iframe_headers
|
||||
@@ -557,6 +594,9 @@ 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() {
|
||||
@@ -619,6 +659,7 @@ _ensure_port_free "$port"
|
||||
OPENCLAW_HOME="$OC_DATA" \
|
||||
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
||||
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
||||
OPENCLAW_GATEWAY_TOKEN="$gw_token" \
|
||||
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
|
||||
NODE_BASE="$NODE_BASE" \
|
||||
OC_GLOBAL="$OC_GLOBAL" \
|
||||
|
||||
@@ -146,9 +146,13 @@ probe_install_root() {
|
||||
cleanup_partial_install() {
|
||||
# 只在确认目标是规范化后的 openclaw 运行根目录时清理,避免误删用户目录。
|
||||
if command -v oc_safe_openclaw_root >/dev/null 2>&1 && oc_safe_openclaw_root "$OC_INSTALL_PATH"; then
|
||||
log_warn "安装失败,清理未完成的运行目录: $OC_INSTALL_PATH"
|
||||
rm -rf "$OC_INSTALL_PATH" 2>/dev/null || true
|
||||
[ -d /overlay/upper ] && rm -rf "/overlay/upper${OC_INSTALL_PATH}" 2>/dev/null || true
|
||||
if [ "${OC_SETUP_FRESH_ROOT:-0}" = "1" ]; then
|
||||
log_warn "安装失败,清理本次新建的运行目录: $OC_INSTALL_PATH"
|
||||
rm -rf "$OC_INSTALL_PATH" 2>/dev/null || true
|
||||
[ -d /overlay/upper ] && rm -rf "/overlay/upper${OC_INSTALL_PATH}" 2>/dev/null || true
|
||||
else
|
||||
log_warn "安装失败,检测到已有运行目录,已保留: $OC_INSTALL_PATH"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -332,34 +336,33 @@ download_node() {
|
||||
|
||||
# 解压
|
||||
echo " 正在解压到 ${NODE_BASE}..."
|
||||
# 先解压到临时目录并确认完整,再替换 NODE_BASE。
|
||||
# 这样即使 tar/xz 缺失或压缩包损坏,也不会删除已有 Node.js。
|
||||
local tmp_extract="/tmp/node-extract-$$"
|
||||
rm -rf "$tmp_extract" 2>/dev/null
|
||||
ensure_mkdir "$tmp_extract"
|
||||
if ! tar xf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
|
||||
log_error "Node.js 解压失败,请确认已安装 GNU tar(opkg install tar)且下载文件完整"
|
||||
rm -rf "$tmp_extract"
|
||||
exit 1
|
||||
fi
|
||||
# 找顶层目录 (node-vX.X.X-linux-xxx)
|
||||
local top_dir
|
||||
top_dir=$(ls "$tmp_extract" 2>/dev/null | head -1)
|
||||
if [ -z "$top_dir" ] || [ ! -d "$tmp_extract/$top_dir" ]; then
|
||||
log_error "解压后未找到顶层目录,安装失败"
|
||||
rm -rf "$tmp_extract"
|
||||
exit 1
|
||||
fi
|
||||
# OverlayFS 兼容: rm -rf 后可能因 whiteout 导致 mkdir 失败
|
||||
# 先尝试常规方式,失败则通过 overlay upper 层操作
|
||||
# 只有在临时目录已解压成功后才替换正式 Node 目录。
|
||||
rm -rf "$NODE_BASE" 2>/dev/null
|
||||
if [ -d /overlay/upper ]; then
|
||||
rm -rf "/overlay/upper${NODE_BASE}" 2>/dev/null
|
||||
fi
|
||||
ensure_mkdir "$NODE_BASE"
|
||||
# 兼容 BusyBox tar (不支持 --strip-components) 和 GNU tar
|
||||
# 方法: 先解压到临时目录,再移动顶层子目录内容到目标目录
|
||||
if tar --strip-components=1 -xf "$tmp_file" -C "$NODE_BASE" 2>/dev/null; then
|
||||
: # GNU tar 成功
|
||||
else
|
||||
# BusyBox tar 回退: 解压到临时目录后手动移动
|
||||
local tmp_extract="/tmp/node-extract-$$"
|
||||
ensure_mkdir "$tmp_extract"
|
||||
tar xf "$tmp_file" -C "$tmp_extract"
|
||||
# 找顶层目录 (node-vX.X.X-linux-xxx)
|
||||
local top_dir
|
||||
top_dir=$(ls "$tmp_extract" 2>/dev/null | head -1)
|
||||
if [ -n "$top_dir" ] && [ -d "$tmp_extract/$top_dir" ]; then
|
||||
cp -a "$tmp_extract/$top_dir/." "$NODE_BASE/"
|
||||
else
|
||||
log_error "解压后未找到顶层目录,安装失败"
|
||||
rm -rf "$tmp_extract"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$tmp_extract"
|
||||
fi
|
||||
cp -a "$tmp_extract/$top_dir/." "$NODE_BASE/"
|
||||
rm -rf "$tmp_extract"
|
||||
rm -f "$tmp_file"
|
||||
|
||||
# 验证
|
||||
@@ -542,6 +545,11 @@ do_setup() {
|
||||
|
||||
ensure_openclaw_user
|
||||
probe_install_root
|
||||
if [ -e "$OC_INSTALL_PATH" ]; then
|
||||
OC_SETUP_FRESH_ROOT=0
|
||||
else
|
||||
OC_SETUP_FRESH_ROOT=1
|
||||
fi
|
||||
OC_SETUP_CLEANUP=1
|
||||
trap 'rc=$?; if [ $rc -ne 0 ] && [ "${OC_SETUP_CLEANUP:-0}" = "1" ]; then cleanup_partial_install; fi; exit $rc' EXIT
|
||||
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
#!/bin/sh
|
||||
# OpenClaw state permission helper.
|
||||
# Keep runtime state writable by the openclaw user, while keeping plugin source
|
||||
# directories root-owned for OpenClaw's official ownership checks.
|
||||
|
||||
oc_perm_state_dir() {
|
||||
if [ -n "${1:-}" ]; then
|
||||
printf '%s\n' "$1"
|
||||
elif [ -n "${OPENCLAW_STATE_DIR:-}" ]; then
|
||||
printf '%s\n' "$OPENCLAW_STATE_DIR"
|
||||
elif [ -n "${OC_DATA:-}" ]; then
|
||||
printf '%s/.openclaw\n' "$OC_DATA"
|
||||
else
|
||||
printf '/opt/openclaw/data/.openclaw\n'
|
||||
fi
|
||||
}
|
||||
|
||||
oc_perm_data_dir_from_state() {
|
||||
local state_dir="$1"
|
||||
case "$state_dir" in
|
||||
*/.openclaw) printf '%s\n' "${state_dir%/.openclaw}" ;;
|
||||
*) printf '%s\n' "${OC_DATA:-/opt/openclaw/data}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
oc_fix_npm_projects_permissions() {
|
||||
local state_dir npm_projects plugin_json plugin_dir
|
||||
state_dir="$(oc_perm_state_dir "${1:-}")"
|
||||
npm_projects="${state_dir}/npm/projects"
|
||||
[ -d "$npm_projects" ] || return 0
|
||||
|
||||
# Gateway must be able to clean npm project generations.
|
||||
chown -R openclaw:openclaw "$npm_projects" 2>/dev/null || true
|
||||
chmod u+rwx,go+rx "$npm_projects" 2>/dev/null || true
|
||||
|
||||
# Actual OpenClaw plugin roots must stay root-owned, otherwise OpenClaw refuses
|
||||
# to load them with "suspicious ownership".
|
||||
find "$npm_projects" -name openclaw.plugin.json -type f 2>/dev/null | while IFS= read -r plugin_json; do
|
||||
plugin_dir="${plugin_json%/openclaw.plugin.json}"
|
||||
case "$plugin_dir" in
|
||||
*__openclaw-generation__*)
|
||||
# Retained generation dirs are temporary and must remain removable by openclaw.
|
||||
;;
|
||||
"$npm_projects"/*/node_modules/*)
|
||||
chown -R root:root "$plugin_dir" 2>/dev/null || true
|
||||
chmod -R 755 "$plugin_dir" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Compatibility for the WeChat plugin path even if its manifest scan fails.
|
||||
find "$npm_projects" -path '*/node_modules/@tencent-weixin/openclaw-weixin' -type d -prune \
|
||||
-exec chown -R root:root {} \; -exec chmod -R 755 {} \; 2>/dev/null || true
|
||||
}
|
||||
|
||||
oc_fix_state_permissions() {
|
||||
local state_dir data_dir ext_dir archive_dir config_file agent_dir
|
||||
state_dir="$(oc_perm_state_dir "${1:-}")"
|
||||
data_dir="$(oc_perm_data_dir_from_state "$state_dir")"
|
||||
ext_dir="${state_dir}/extensions"
|
||||
archive_dir="${state_dir}/archived-extensions"
|
||||
config_file="${state_dir}/openclaw.json"
|
||||
|
||||
[ -d "$data_dir" ] && chown openclaw:openclaw "$data_dir" 2>/dev/null || true
|
||||
[ -d "$state_dir" ] || mkdir -p "$state_dir" 2>/dev/null || true
|
||||
[ -d "$state_dir" ] && chown openclaw:openclaw "$state_dir" 2>/dev/null || true
|
||||
[ -d "$state_dir" ] && chmod 755 "$state_dir" 2>/dev/null || true
|
||||
|
||||
if [ -d "$state_dir" ]; then
|
||||
find "$state_dir" -user root \
|
||||
! -path "${ext_dir}*" \
|
||||
! -path "${archive_dir}*" \
|
||||
! -path "${state_dir}/npm/projects*" \
|
||||
-exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
fi
|
||||
|
||||
[ -f "$config_file" ] && chown openclaw:openclaw "$config_file" 2>/dev/null || true
|
||||
[ -f "${config_file}.bak" ] && chown openclaw:openclaw "${config_file}.bak" 2>/dev/null || true
|
||||
|
||||
agent_dir="${state_dir}/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
|
||||
|
||||
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
|
||||
if [ -d "$archive_dir" ]; then
|
||||
chown -R root:root "$archive_dir" 2>/dev/null || true
|
||||
chmod -R 700 "$archive_dir" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
oc_fix_npm_projects_permissions "$state_dir"
|
||||
}
|
||||
|
||||
oc_prepare_openclaw_workdirs() {
|
||||
local data_dir state_dir npm_dir
|
||||
data_dir="${1:-${OC_DATA:-/opt/openclaw/data}}"
|
||||
state_dir="${data_dir}/.openclaw"
|
||||
npm_dir="${state_dir}/npm/projects"
|
||||
|
||||
mkdir -p "${data_dir}/.npm" "${data_dir}/.tmp" "$npm_dir" "${state_dir}/extensions" 2>/dev/null || true
|
||||
chown -R openclaw:openclaw "${data_dir}/.npm" "${data_dir}/.tmp" 2>/dev/null || true
|
||||
chown openclaw:openclaw "$data_dir" "$state_dir" "${state_dir}/npm" "$npm_dir" 2>/dev/null || true
|
||||
chmod u+rwx,go+rx "${data_dir}/.npm" "${data_dir}/.tmp" "$state_dir" "${state_dir}/npm" "$npm_dir" 2>/dev/null || true
|
||||
|
||||
# During install/upgrade, allow openclaw to update npm generations. Call
|
||||
# fix-state after the install to restore plugin roots to root:root.
|
||||
[ -d "$npm_dir" ] && chown -R openclaw:openclaw "$npm_dir" 2>/dev/null || true
|
||||
}
|
||||
|
||||
if [ "${OPENCLAW_PERMISSIONS_SOURCED:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${1:-fix-state}" in
|
||||
fix-state)
|
||||
shift || true
|
||||
oc_fix_state_permissions "${1:-}"
|
||||
;;
|
||||
fix-npm-projects)
|
||||
shift || true
|
||||
oc_fix_npm_projects_permissions "${1:-}"
|
||||
;;
|
||||
prepare-workdirs)
|
||||
shift || true
|
||||
oc_prepare_openclaw_workdirs "${1:-}"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {fix-state [state_dir]|fix-npm-projects [state_dir]|prepare-workdirs [data_dir]}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { spawn, execSync } = require('child_process');
|
||||
const { spawn, execSync, execFileSync } = require('child_process');
|
||||
const menu = require('./oc-menu-engine');
|
||||
const { C, select, input, confirm, spinner, resetRenderCount } = menu;
|
||||
|
||||
@@ -31,6 +31,7 @@ const OC_DATA = process.env.OC_DATA || `${OC_INSTALL_PATH}/data`;
|
||||
const OC_STATE_DIR = process.env.OPENCLAW_STATE_DIR || `${OC_DATA}/.openclaw`;
|
||||
const CONFIG_FILE = process.env.OPENCLAW_CONFIG_PATH || `${OC_STATE_DIR}/openclaw.json`;
|
||||
const NODE_BIN = `${NODE_BASE}/bin/node`;
|
||||
const PERMISSIONS_HELPER = '/usr/libexec/openclaw-permissions.sh';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 辅助函数 (与 oc-config.sh 逻辑对应)
|
||||
@@ -73,6 +74,16 @@ function runCommand(cmd, args = [], options = {}) {
|
||||
/**
|
||||
* 读取 JSON 配置文件
|
||||
*/
|
||||
function fixStatePermissions() {
|
||||
try {
|
||||
execFileSync(PERMISSIONS_HELPER, ['fix-state', OC_STATE_DIR], { stdio: 'ignore', timeout: 10000 });
|
||||
} catch {
|
||||
try {
|
||||
execSync(`find "${OC_STATE_DIR}" -user root ! -path "*/extensions*" ! -path "*/archived-extensions*" ! -path "*/npm/projects*" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function readConfig() {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
@@ -95,7 +106,7 @@ function writeConfig(config) {
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||
try {
|
||||
execSync(`chown openclaw:openclaw "${CONFIG_FILE}"`, { stdio: 'ignore' });
|
||||
execSync(`find "${OC_STATE_DIR}" -user root ! -path "*/extensions*" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });
|
||||
fixStatePermissions();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -222,7 +233,7 @@ function authSetApikey(provider, apiKey, profileId) {
|
||||
fs.writeFileSync(authFile, JSON.stringify(authData, null, 2));
|
||||
try {
|
||||
execSync(`chown openclaw:openclaw "${authFile}"`, { stdio: 'ignore' });
|
||||
execSync(`find "${OC_STATE_DIR}" -user root ! -path "*/extensions*" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });
|
||||
fixStatePermissions();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -444,6 +455,7 @@ async function showModelMenu() {
|
||||
{ key: 'l', label: 'Ollama', desc: '本地模型,无需 API Key', value: 'ollama' },
|
||||
{ key: 'm', label: '自定义 OpenAI 兼容 API', desc: '', value: 'custom' },
|
||||
{ key: 'n', label: '自定义 Anthropic 兼容 API', desc: '', value: 'custom-anthropic' },
|
||||
{ key: 'o', label: '一万AI分享 粉丝专享 API', desc: '', value: 'yiwanai-fan' },
|
||||
|
||||
{ label: '', disabled: true },
|
||||
{ key: '0', label: '返回', desc: '', value: 'back' },
|
||||
@@ -1242,6 +1254,33 @@ async function configureCustomAnthropic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function configureYiwanAIFanAPI() {
|
||||
resetRenderCount();
|
||||
const providerName = 'yiwanai';
|
||||
const baseUrl = 'https://api.910501.xyz/v1';
|
||||
const modelName = 'gpt-5.5';
|
||||
|
||||
console.log(`\n${C.bold}一万AI分享 粉丝专享 API 配置${C.reset}`);
|
||||
console.log(`${C.yellow}OpenAI 兼容模式;Base URL 和模型已内置,只需要填写 API Key。${C.reset}`);
|
||||
console.log(`${C.dim}Base URL: ${baseUrl}${C.reset}`);
|
||||
console.log(`${C.dim}Model: ${modelName}${C.reset}\n`);
|
||||
|
||||
const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-...' });
|
||||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||||
|
||||
authSetApikey(providerName, apiKey, `${providerName}:fan`);
|
||||
registerCustomProvider(providerName, baseUrl, apiKey, modelName, modelName);
|
||||
const config = readConfig();
|
||||
const provider = config.models?.providers?.[providerName];
|
||||
if (provider?.models?.[0]) {
|
||||
provider.models[0].reasoning = true;
|
||||
}
|
||||
writeConfig(config);
|
||||
registerAndSetModel(`${providerName}/${modelName}`);
|
||||
console.log(`\n${C.green}✅ 一万AI分享粉丝专享 API 已配置,活跃模型: ${providerName}/${modelName}${C.reset}\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function launchWizard() {
|
||||
resetRenderCount();
|
||||
console.log(`\n${C.cyan}启动官方完整模型配置向导...${C.reset}\n`);
|
||||
@@ -1281,6 +1320,7 @@ async function handleModelConfig() {
|
||||
case 'ollama': configured = await configureOllama(); break;
|
||||
case 'custom': configured = await configureCustomAPI(); break;
|
||||
case 'custom-anthropic': configured = await configureCustomAnthropic(); break;
|
||||
case 'yiwanai-fan': configured = await configureYiwanAIFanAPI(); break;
|
||||
}
|
||||
|
||||
if (configured) await askRestart();
|
||||
|
||||
@@ -61,6 +61,14 @@ export OPENCLAW_CONFIG_PATH="$CONFIG_FILE"
|
||||
export NODE_ICU_DATA="${NODE_BASE}/share/icu"
|
||||
export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
fix_openclaw_state_permissions() {
|
||||
[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh fix-state "$OC_STATE_DIR"
|
||||
}
|
||||
|
||||
prepare_openclaw_workdirs() {
|
||||
[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh prepare-workdirs "$OC_DATA"
|
||||
}
|
||||
|
||||
# ── 查找 openclaw 入口 ──
|
||||
OC_PKG_DIR=""
|
||||
for d in "${OC_GLOBAL}/lib/node_modules/openclaw" "${OC_GLOBAL}/node_modules/openclaw" "${NODE_BASE}/lib/node_modules/openclaw"; do
|
||||
@@ -84,7 +92,7 @@ oc_cmd() {
|
||||
"$NODE_BIN" "$OC_ENTRY" "$@" 2>&1
|
||||
local rc=$?
|
||||
# 修复权限: oc_cmd 以 root 运行但配置文件应属于 openclaw 用户
|
||||
find "$OC_STATE_DIR" -user root ! -path "*/extensions*" -exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
chown openclaw:openclaw "${CONFIG_FILE}.bak" 2>/dev/null || true
|
||||
return $rc
|
||||
@@ -130,7 +138,7 @@ json_set() {
|
||||
fi
|
||||
|
||||
# 尝试修复所有权
|
||||
chown -R openclaw:openclaw "$OC_STATE_DIR" 2>/dev/null || true
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
|
||||
# 创建空配置文件
|
||||
if ! echo '{}' > "$CONFIG_FILE" 2>/dev/null; then
|
||||
@@ -548,7 +556,7 @@ restart_gateway() {
|
||||
echo -e " ${YELLOW}正在重启 Gateway...${NC}"
|
||||
|
||||
# 修复数据目录权限 (root 用户操作可能改变了文件属主)
|
||||
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
|
||||
local port
|
||||
port=$(json_get gateway.port)
|
||||
@@ -694,6 +702,8 @@ configure_model() {
|
||||
echo -e " ${BOLD}🏠 ── 本地模型 / 自定义 API ──${NC}"
|
||||
echo -e " ${CYAN}l)${NC} Ollama (本地模型,无需 API Key)"
|
||||
echo -e " ${CYAN}m)${NC} 自定义 OpenAI 兼容 API"
|
||||
echo -e " ${CYAN}n)${NC} 自定义 Anthropic 兼容 API"
|
||||
echo -e " ${CYAN}o)${NC} 一万AI分享 粉丝专享 API"
|
||||
echo ""
|
||||
echo -e " ${CYAN}q)${NC} 返回"
|
||||
echo ""
|
||||
@@ -1380,6 +1390,70 @@ configure_model() {
|
||||
echo -e " ${GREEN}✅ 自定义模型已配置,活跃模型: openai-compatible/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
n)
|
||||
echo ""
|
||||
echo -e " ${BOLD}自定义 Anthropic 兼容 API${NC}"
|
||||
echo -e " ${YELLOW}支持任何兼容 Anthropic Messages API 格式的服务商${NC}"
|
||||
echo ""
|
||||
prompt_with_default "API Base URL (如 https://api.anthropic.com)" "" base_url
|
||||
prompt_with_default "API Key" "" api_key
|
||||
prompt_with_default "模型名称" "claude-sonnet-4-20250514" model_name
|
||||
if [ -n "$base_url" ] && [ -n "$api_key" ] && [ -n "$model_name" ]; then
|
||||
_ACP_URL="${base_url%/}" _ACP_KEY="$api_key" _ACP_MID="$model_name" "$NODE_BIN" -e "
|
||||
const fs=require('fs');
|
||||
let d={};
|
||||
try{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}
|
||||
if(!d.models)d.models={};
|
||||
if(!d.models.providers)d.models.providers={};
|
||||
d.models.mode='merge';
|
||||
d.models.providers['anthropic-compatible']={
|
||||
baseUrl:process.env._ACP_URL,
|
||||
apiKey:process.env._ACP_KEY,
|
||||
api:'anthropic-messages',
|
||||
models:[{
|
||||
id:process.env._ACP_MID,
|
||||
name:process.env._ACP_MID,
|
||||
reasoning:false,
|
||||
input:['text','image'],
|
||||
cost:{input:0,output:0,cacheRead:0,cacheWrite:0},
|
||||
contextWindow:200000,
|
||||
maxTokens:16000
|
||||
}]
|
||||
};
|
||||
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
|
||||
" 2>/dev/null
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
auth_set_apikey anthropic-compatible "$api_key" "anthropic-compatible:manual"
|
||||
register_and_set_model "anthropic-compatible/${model_name}"
|
||||
echo -e " ${GREEN}✅ 自定义 Anthropic API 已配置,活跃模型: anthropic-compatible/${model_name}${NC}"
|
||||
fi
|
||||
;;
|
||||
o)
|
||||
echo ""
|
||||
echo -e " ${BOLD}一万AI分享 粉丝专享 API${NC}"
|
||||
echo -e " ${YELLOW}OpenAI 兼容模式;Base URL 和模型已内置,只需要填写 API Key。${NC}"
|
||||
echo -e " ${DIM}Base URL: https://api.910501.xyz/v1${NC}"
|
||||
echo -e " ${DIM}Model: gpt-5.5${NC}"
|
||||
echo ""
|
||||
prompt_with_default "API Key" "" api_key
|
||||
if [ -n "$api_key" ]; then
|
||||
auth_set_apikey yiwanai "$api_key" "yiwanai:fan"
|
||||
register_custom_provider yiwanai "https://api.910501.xyz/v1" "$api_key" "gpt-5.5" "gpt-5.5"
|
||||
_YW_PROV="yiwanai" "$NODE_BIN" -e "
|
||||
const fs=require('fs');
|
||||
let d={};
|
||||
try{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}
|
||||
const p=d.models&&d.models.providers&&d.models.providers[process.env._YW_PROV];
|
||||
if(p&&p.models&&p.models[0]){
|
||||
p.models[0].reasoning=true;
|
||||
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
|
||||
}
|
||||
" 2>/dev/null
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
register_and_set_model "yiwanai/gpt-5.5"
|
||||
echo -e " ${GREEN}✅ 一万AI分享粉丝专享 API 已配置,活跃模型: yiwanai/gpt-5.5${NC}"
|
||||
fi
|
||||
;;
|
||||
q) return ;;
|
||||
esac
|
||||
|
||||
@@ -2470,7 +2544,7 @@ backup_restore_menu() {
|
||||
# 提取 payload 到根目录 (还原到原始绝对路径)
|
||||
tar -xzf "$latest" --strip-components=3 -C / "${backup_name}/payload/posix/" 2>&1
|
||||
# 修复权限
|
||||
chown -R openclaw:openclaw "$OC_STATE_DIR" 2>/dev/null
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
echo -e " ${GREEN}✅ 配置和数据已完整恢复!原配置已保存为 openclaw.json.pre-restore${NC}"
|
||||
echo ""
|
||||
prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart
|
||||
@@ -2514,7 +2588,7 @@ launch_interactive_menu() {
|
||||
"$NODE_BIN" "$OC_INTERACTIVE" 2>&1
|
||||
local rc=$?
|
||||
|
||||
find "$OC_STATE_DIR" -user root ! -path "*/extensions*" -exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
# 返回后刷新配置权限
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
return $rc
|
||||
@@ -2532,7 +2606,7 @@ launch_interactive_model_config() {
|
||||
"$NODE_BIN" "$OC_INTERACTIVE" model 2>&1
|
||||
local rc=$?
|
||||
|
||||
find "$OC_STATE_DIR" -user root ! -path "*/extensions*" -exec chown openclaw:openclaw {} \; 2>/dev/null || true
|
||||
fix_openclaw_state_permissions 2>/dev/null || true
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
return $rc
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn, execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
@@ -36,12 +36,21 @@ try {
|
||||
const NODE_BASE = process.env.NODE_BASE || installPath + '/node';
|
||||
const OC_GLOBAL = process.env.OC_GLOBAL || installPath + '/global';
|
||||
const OC_DATA = process.env.OC_DATA || installPath + '/data';
|
||||
const OC_STATE_DIR = `${OC_DATA}/.openclaw`;
|
||||
const PERMISSIONS_HELPER = '/usr/libexec/openclaw-permissions.sh';
|
||||
const SCRIPT_PATH = process.env.OC_CONFIG_SCRIPT || '/usr/share/openclaw/oc-config.sh';
|
||||
const SSL_CERT = '/etc/uhttpd.crt';
|
||||
const SSL_KEY = '/etc/uhttpd.key';
|
||||
const MAX_SESSIONS = parseInt(process.env.OC_MAX_SESSIONS || '5', 10);
|
||||
|
||||
// ── 认证令牌 (从 UCI 或环境变量读取) ──
|
||||
|
||||
function fixStatePermissions() {
|
||||
try {
|
||||
execFileSync(PERMISSIONS_HELPER, ['fix-state', OC_STATE_DIR], { stdio: 'pipe', timeout: 10000 });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadAuthToken() {
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
@@ -220,7 +229,7 @@ class PtySession {
|
||||
if (!this.alive) return;
|
||||
// PTY 以 root 运行,子脚本可能创建了 root-owned 的目录
|
||||
// 修复权限,防止以 openclaw 用户运行的 Gateway 遇到 EACCES
|
||||
try { require('child_process').execFileSync('chown', ['-R', 'openclaw:openclaw', OC_DATA], { stdio: 'pipe', timeout: 5000 }); } catch(e) {}
|
||||
try { fixStatePermissions(); } catch(e) {}
|
||||
this._spawnFailCount++;
|
||||
if (this._spawnFailCount > this._MAX_SPAWN_RETRIES) {
|
||||
console.log(`[oc-config] Script failed ${this._spawnFailCount} times, stopping retries`);
|
||||
|
||||
Reference in New Issue
Block a user