release: v1.0.1 — 14 项 bug 修复、ARM64 musl Node.js 构建

This commit is contained in:
10000ge10000
2026-03-02 20:24:10 +08:00
parent c1c3151a9f
commit 6e55b3f12d
16 changed files with 496 additions and 119 deletions

View File

@@ -23,10 +23,13 @@ NODE_BIN="${NODE_BASE}/bin/node"
NPM_BIN="${NODE_BASE}/bin/npm"
PNPM_BIN="${OC_GLOBAL}/bin/pnpm"
# Node.js 官方镜像 + musl 非官方构建
# Node.js 官方镜像 + musl 非官方构建
NODE_MIRROR="${NODE_MIRROR:-https://nodejs.org/dist}"
NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
# 项目自托管 ARM64 musl Node.js (unofficial-builds 仅提供 x64 musl)
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH"
@@ -99,26 +102,7 @@ download_node() {
local libc_type
libc_type=$(detect_libc)
local tarball=""
local url="" url_fallback=""
if [ "$libc_type" = "musl" ]; then
tarball="node-v${node_ver}-${node_arch}-musl.tar.xz"
url="${NODE_MUSL_MIRROR}/v${node_ver}/${tarball}"
url_fallback=""
echo ""
echo "=== 下载 Node.js v${node_ver} (${node_arch}, musl libc) ==="
else
tarball="node-v${node_ver}-${node_arch}.tar.xz"
url="${NODE_MIRROR}/v${node_ver}/${tarball}"
url_fallback="${NODE_MIRROR_CN}/v${node_ver}/${tarball}"
echo ""
echo "=== 下载 Node.js v${node_ver} (${node_arch}, glibc) ==="
fi
local tmp_file="/tmp/${tarball}"
# 如果已存在且版本正确, 跳过
# 如果已存在且版本兼容, 跳过
if [ -x "$NODE_BIN" ]; then
local current_ver
current_ver=$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//')
@@ -126,19 +110,67 @@ download_node() {
log_info "Node.js v${node_ver} 已安装, 跳过下载"
return 0
fi
# ARM64 musl 使用 Alpine 打包,版本号可能不完全匹配
# 只要主版本号相同即认为兼容 (如 22.15.1 vs 22.16.0)
local cur_major=$(echo "$current_ver" | cut -d. -f1)
local want_major=$(echo "$node_ver" | cut -d. -f1)
if [ "$cur_major" = "$want_major" ]; then
log_info "Node.js v${current_ver} 已安装 (兼容 v${node_ver}), 跳过下载"
return 0
fi
log_warn "当前 Node.js v${current_ver}, 将更新到 v${node_ver}"
fi
# 下载 (带重试)
# ── 构建下载 URL 列表 (按优先级排列) ──
local mirror_list=""
local musl_tarball="node-v${node_ver}-${node_arch}-musl.tar.xz"
local glibc_tarball="node-v${node_ver}-${node_arch}.tar.xz"
if [ "$libc_type" = "musl" ]; then
echo ""
echo "=== 下载 Node.js v${node_ver} (${node_arch}, musl libc) ==="
if [ "$node_arch" = "linux-arm64" ]; then
# ARM64 musl: unofficial-builds 不提供,从项目自托管下载
# 1) 项目自托管 ARM64 musl 构建
mirror_list="${NODE_SELF_HOST}/${musl_tarball}"
# 2) unofficial-builds (留作将来可能支持)
mirror_list="$mirror_list ${NODE_MUSL_MIRROR}/v${node_ver}/${musl_tarball}"
else
# x64 musl: unofficial-builds 提供
# 1) unofficial-builds
mirror_list="${NODE_MUSL_MIRROR}/v${node_ver}/${musl_tarball}"
# 2) npmmirror 镜像
mirror_list="$mirror_list ${NODE_MIRROR_CN}/v${node_ver}/${musl_tarball}"
fi
else
echo ""
echo "=== 下载 Node.js v${node_ver} (${node_arch}, glibc) ==="
mirror_list="${NODE_MIRROR}/v${node_ver}/${glibc_tarball}"
mirror_list="$mirror_list ${NODE_MIRROR_CN}/v${node_ver}/${glibc_tarball}"
fi
# ── 逐个尝试下载 ──
local downloaded=0
local mirror_list="$url"
[ -n "$url_fallback" ] && mirror_list="$url $url_fallback"
local tmp_file="/tmp/node-v${node_ver}.tar.xz"
local attempt=0
local total=$(echo "$mirror_list" | wc -w)
for mirror_url in $mirror_list; do
echo " 正在从 ${mirror_url} 下载..."
attempt=$((attempt + 1))
echo " 正在从 ${mirror_url} 下载... (${attempt}/${total})"
if curl -fSL --connect-timeout 15 --max-time 300 -o "$tmp_file" "$mirror_url" 2>/dev/null || \
wget -q --timeout=15 -O "$tmp_file" "$mirror_url" 2>/dev/null; then
downloaded=1
break
# 校验文件大小 (Node.js xz 压缩包至少 5MB; Alpine 精简包约 12MB, 官方完整包约 30MB)
local fsize=$(wc -c < "$tmp_file" 2>/dev/null || echo 0)
if [ "$fsize" -gt 5000000 ] 2>/dev/null; then
downloaded=1
break
else
log_warn "文件大小异常 (${fsize} bytes), 跳过"
rm -f "$tmp_file"
fi
fi
log_warn "下载失败, 尝试备用镜像..."
done
@@ -158,7 +190,9 @@ download_node() {
# 验证
if [ -x "$NODE_BIN" ]; then
log_info "Node.js $($NODE_BIN --version) 安装成功"
local installed_ver
installed_ver=$("$NODE_BIN" --version 2>/dev/null || echo "unknown")
log_info "Node.js ${installed_ver} 安装成功"
else
log_error "Node.js 安装验证失败"
exit 1

View File

@@ -164,14 +164,13 @@ restart_gateway() {
port=$(json_get gateway.port)
port=${port:-18789}
# ── 使用 init.d restart_gateway: 只重启 Gateway 实例,不影响 PTY 终端 ──
# 发 SIGTERM 给 gateway 进程procd 自动 respawn避免 crash loop 计数累积
# ── kill gateway 进程,让 procd respawn ──
/etc/init.d/openclaw restart_gateway >/dev/null 2>&1
# ── 等待端口监听,最多 60 秒 (Node.js 冷启动实测约 40 秒) ──
echo -e " ${YELLOW}⏳ Gateway 启动中,请稍候 (约 40 秒)...${NC}"
# ── 等待端口恢复 (最多 30 秒,含端口释放 + Node.js 冷启动) ──
echo -e " ${YELLOW}⏳ Gateway 启动中,请稍候 (约 15-30 秒)...${NC}"
local waited=0
while [ $waited -lt 60 ]; do
while [ $waited -lt 30 ]; do
sleep 3
waited=$((waited + 3))
if check_port_listening "$port"; then
@@ -180,28 +179,10 @@ restart_gateway() {
fi
done
# ── 超时仍未就绪: 先确认进程是否其实已经在运行 ──
# (可能端口刚好在轮询间隙启动完成)
if check_port_listening "$port"; then
echo -e " ${GREEN}✅ Gateway 已重启成功${NC}"
return 0
fi
# ── 确实未启动: 可能 procd crash loop 保护触发,用 start 解除 ──
echo -e " ${YELLOW}⏳ 正在尝试再次启动...${NC}"
/etc/init.d/openclaw start >/dev/null 2>&1 &
waited=0
while [ $waited -lt 30 ]; do
sleep 3
waited=$((waited + 3))
if check_port_listening "$port"; then
echo -e " ${GREEN}✅ Gateway 已重启成功${NC}"
return 0
fi
done
echo -e " ${RED}❌ Gateway 可能未正常启动${NC}"
# 30 秒内没就绪,提示用户但不继续阻塞
echo -e " ${YELLOW}⏳ Gateway 仍在启动中,请稍后确认${NC}"
echo -e " ${CYAN} 查看日志: logread -e openclaw${NC}"
echo ""
}
ask_restart() {
@@ -474,32 +455,53 @@ configure_model() {
7)
echo ""
echo -e " ${BOLD}GitHub Copilot 配置${NC}"
echo -e " ${YELLOW}需要有效的 GitHub Copilot 订阅${NC}"
echo -e " ${YELLOW}获取 Token: https://github.com/settings/tokens (需 copilot 权限)${NC}"
echo -e " ${YELLOW}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${NC}"
echo -e " ${YELLOW}使用 OAuth 自动认证,无需手动输入 Token${NC}"
echo ""
prompt_with_default "请输入 GitHub Token (ghp_...)" "" api_key
if [ -n "$api_key" ]; then
json_set models.github-copilot.apiKey "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} github-copilot/claude-sonnet-4 — Claude Sonnet 4 (推荐)"
echo -e " ${CYAN}b)${NC} github-copilot/gpt-5.2 — GPT-5.2"
echo -e " ${CYAN}c)${NC} github-copilot/gemini-2.5-pro — Gemini 2.5 Pro"
echo -e " ${CYAN}d)${NC} github-copilot/o3 — o3"
echo -e " ${CYAN}e)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="github-copilot/claude-sonnet-4" ;;
b) model_name="github-copilot/gpt-5.2" ;;
c) model_name="github-copilot/gemini-2.5-pro" ;;
d) model_name="github-copilot/o3" ;;
e) prompt_with_default "请输入模型名称" "github-copilot/claude-sonnet-4" model_name ;;
*) model_name="github-copilot/claude-sonnet-4" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ GitHub Copilot 已配置,活跃模型: ${model_name}${NC}"
fi
echo -e " ${CYAN}配置方式:${NC}"
echo -e " ${CYAN}a)${NC} 通过 OAuth 授权登录 (推荐)"
echo -e " ${CYAN}b)${NC} 手动输入 GitHub Token (ghp_...)"
echo ""
prompt_with_default "请选择" "a" copilot_mode
case "$copilot_mode" in
a)
echo ""
echo -e " ${CYAN}启用 Copilot Proxy 插件...${NC}"
enable_auth_plugins
echo -e " ${CYAN}启动 GitHub Copilot OAuth 授权...${NC}"
oc_cmd models auth login --provider copilot-proxy --set-default || echo -e " ${YELLOW}OAuth 授权已退出${NC}"
echo ""
ask_restart
;;
b|*)
echo ""
echo -e " ${YELLOW}获取 Token: https://github.com/settings/tokens (需 copilot 权限)${NC}"
echo ""
prompt_with_default "请输入 GitHub Token (ghp_...)" "" api_key
if [ -n "$api_key" ]; then
json_set models.github-copilot.apiKey "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} github-copilot/claude-sonnet-4 — Claude Sonnet 4 (推荐)"
echo -e " ${CYAN}b)${NC} github-copilot/gpt-5.2 — GPT-5.2"
echo -e " ${CYAN}c)${NC} github-copilot/gemini-2.5-pro — Gemini 2.5 Pro"
echo -e " ${CYAN}d)${NC} github-copilot/o3 — o3"
echo -e " ${CYAN}e)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="github-copilot/claude-sonnet-4" ;;
b) model_name="github-copilot/gpt-5.2" ;;
c) model_name="github-copilot/gemini-2.5-pro" ;;
d) model_name="github-copilot/o3" ;;
e) prompt_with_default "请输入模型名称" "github-copilot/claude-sonnet-4" model_name ;;
*) model_name="github-copilot/claude-sonnet-4" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ GitHub Copilot 已配置,活跃模型: ${model_name}${NC}"
fi
;;
esac
;;
8)
echo ""
@@ -527,8 +529,8 @@ configure_model() {
echo ""
prompt_with_default "请输入阿里云 API Key (sk-...)" "" api_key
if [ -n "$api_key" ]; then
json_set models.custom.apiKey "$api_key"
json_set models.custom.baseUrl "https://dashscope.aliyuncs.com/compatible-mode/v1"
json_set models.dashscope.apiKey "$api_key"
json_set models.dashscope.baseUrl "https://dashscope.aliyuncs.com/compatible-mode/v1"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} qwen-max — 通义千问旗舰 (推荐)"
@@ -613,8 +615,8 @@ configure_model() {
echo ""
prompt_with_default "请输入 SiliconFlow API Key" "" api_key
if [ -n "$api_key" ]; then
json_set models.custom.apiKey "$api_key"
json_set models.custom.baseUrl "https://api.siliconflow.cn/v1"
json_set models.siliconflow.apiKey "$api_key"
json_set models.siliconflow.baseUrl "https://api.siliconflow.cn/v1"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} deepseek-ai/DeepSeek-V3 — DeepSeek V3 (推荐)"
@@ -924,8 +926,10 @@ telegram_pairing() {
fi
if [ -n "$codes" ]; then
# ash 不支持 <<<, 使用 echo | while
echo "$codes" | while IFS= read -r code; do
# 逐个处理配对码 (避免管道子 shell 变量丢失问题)
local _codes_tmp="/tmp/oc_pair_codes_$$"
echo "$codes" > "$_codes_tmp"
while IFS= read -r code; do
[ -z "$code" ] && continue
echo -e " ${CYAN}发现配对请求: ${code}${NC}"
local approve=$(oc_cmd pairing approve telegram "$code" 2>&1)
@@ -933,7 +937,8 @@ telegram_pairing() {
echo ""
echo -e " ${GREEN}${BOLD}🎉 Telegram 配对成功!${NC}"
fi
done
done < "$_codes_tmp"
rm -f "$_codes_tmp"
# 检查是否还有待配对的
local re_check=$(oc_cmd pairing list telegram --json 2>/dev/null || echo "")
local re_codes=$(echo "$re_check" | grep -o '"code"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
@@ -1170,7 +1175,17 @@ reset_to_defaults() {
echo -e " ${CYAN}[4/5] 重新初始化...${NC}"
# 尝试 onboard超时 10 秒避免阻塞
timeout 10 sh -c "$(which node 2>/dev/null || echo "$NODE_BIN") \"$OC_ENTRY\" onboard --non-interactive --accept-risk" >/dev/null 2>&1 || true
local _node_bin
_node_bin=$(which node 2>/dev/null || echo "$NODE_BIN")
if command -v timeout >/dev/null 2>&1; then
timeout 10 sh -c "\"$_node_bin\" \"$OC_ENTRY\" onboard --non-interactive --accept-risk" >/dev/null 2>&1 || true
else
"$_node_bin" "$OC_ENTRY" onboard --non-interactive --accept-risk >/dev/null 2>&1 &
local _ob_pid=$!
sleep 10
kill "$_ob_pid" 2>/dev/null || true
wait "$_ob_pid" 2>/dev/null || true
fi
echo -e " ${GREEN} 初始化完成${NC}"
echo -e " ${CYAN}[5/5] 应用 OpenWrt 适配配置...${NC}"

View File

@@ -135,7 +135,7 @@
let ws = null;
let connected = false;
let retryCount = 0;
const MAX_RETRY = 20;
const MAX_RETRY = Infinity;
let retryTimer = null;
let wasEverConnected = false;

View File

@@ -29,7 +29,7 @@ const MAX_SESSIONS = parseInt(process.env.OC_MAX_SESSIONS || '5', 10);
function loadAuthToken() {
try {
const { execSync } = require('child_process');
const t = execSync('uci -q get openclaw.main.luci_token 2>/dev/null', { encoding: 'utf8', timeout: 3000 }).trim();
const t = execSync('uci -q get openclaw.main.pty_token 2>/dev/null', { encoding: 'utf8', timeout: 3000 }).trim();
return t || '';
} catch { return ''; }
}
@@ -263,6 +263,7 @@ function handleUpgrade(req, socket, head) {
// ── 服务器实例 ──
const httpServer = http.createServer(handleRequest);
httpServer.on('upgrade', handleUpgrade);
let httpsServer = null;
httpServer.listen(PORT, HOST, () => {
console.log(`[oc-config] HTTP listening on ${HOST}:${PORT}`);
@@ -273,7 +274,7 @@ httpServer.listen(PORT, HOST, () => {
const HTTPS_PORT = PORT + 1;
try {
if (fs.existsSync(SSL_CERT) && fs.existsSync(SSL_KEY)) {
const httpsServer = https.createServer({ cert: fs.readFileSync(SSL_CERT), key: fs.readFileSync(SSL_KEY) }, handleRequest);
httpsServer = https.createServer({ cert: fs.readFileSync(SSL_CERT), key: fs.readFileSync(SSL_KEY) }, handleRequest);
httpsServer.on('upgrade', handleUpgrade);
httpsServer.listen(HTTPS_PORT, HOST, () => console.log(`[oc-config] HTTPS listening on ${HOST}:${HTTPS_PORT}`));
httpsServer.on('error', (e) => console.log(`[oc-config] HTTPS port ${HTTPS_PORT}: ${e.message}`));
@@ -281,5 +282,6 @@ try {
} catch (e) { console.log(`[oc-config] SSL init: ${e.message}`); }
httpServer.on('error', (e) => { console.error(`[oc-config] Fatal: ${e.message}`); process.exit(1); });
process.on('SIGTERM', () => { console.log('[oc-config] Shutdown'); httpServer.close(); process.exit(0); });
process.on('SIGINT', () => { httpServer.close(); process.exit(0); });
function shutdown() { console.log('[oc-config] Shutdown'); httpServer.close(); if (httpsServer) httpsServer.close(); process.exit(0); }
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);