diff --git a/CHANGELOG.md b/CHANGELOG.md index 034b5ab..5526dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,37 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。 -## [1.0.3] - 2026-03-02 +## [1.0.4] - 2026-03-05 + +### 适配 OpenClaw 2026.3.2 + +#### 破坏性变更修复 +- **tools.profile 默认值变更**: 2026.3.2 将 `tools.profile` 默认从 `coding` 改为 `messaging` + - `sync_uci_to_json()` 每次启动强制写入 `tools.profile=coding` + - `openclaw-env init_openclaw()` onboard 命令添加 `--tools-profile coding` + - `openclaw-env do_factory_reset()` onboard 命令添加 `--tools-profile coding` + - `oc-config.sh` 工厂重置 onboard 命令添加 `--tools-profile coding` + - `oc-config.sh` 工厂重置配置写入新增 `tools.profile=coding` +- **ACP dispatch 默认启用**: 2026.3.2 默认开启 ACP dispatch,路由器内存有限可能导致 OOM + - `sync_uci_to_json()` 每次启动强制写入 `acp.dispatch.enabled=false` + - `openclaw-env do_factory_reset()` 配置写入新增 `acp.dispatch.enabled=false` + - `oc-config.sh` 工厂重置配置写入新增 `acp.dispatch.enabled=false` + +#### 新增 +- 健康检查集成 `openclaw config validate --json` 官方配置验证命令 +- 健康检查新增 `gateway health --json` CLI 深度检查 (v2026.3.2 HTTP `/health` 已被 SPA 接管) + +#### 修复 +- **Ollama 配置适配**: `api` 从废弃的 `openai-chat-completions` 改为原生 `ollama` API 类型 +- **Ollama baseUrl 格式**: 去掉 `/v1` 后缀,使用官方原生地址格式 (`http://host:11434`) +- **Ollama apiKey 对齐**: 从 `ollama` 改为官方默认值 `ollama-local` +- **启动自动迁移**: `sync_uci_to_json` 自动将旧版 Ollama 配置迁移到 v2026.3.2 格式 + +#### 改进 +- 配置管理页面移除「菜单功能说明」信息框,减少视觉干扰 +- `OC_TESTED_VERSION` 更新至 `2026.3.2` + +## [1.0.3] - 2026-03-05 ### 修复 - **P0** 配置管理写入错误的 JSON 路径导致 Gateway 崩溃且无法恢复 (#1) @@ -17,10 +47,20 @@ - **P1** `set_active_model` 手动切换模型时未注册到 `agents.defaults.models` ### 新增 +- **Ollama 本地模型支持**: 快速配置菜单新增 Ollama 选项 (12),支持 localhost/局域网连接、自动检测连通性、自动列出已安装模型、兼容 OpenAI chat completions 格式 - `openclaw-env factory-reset` 非交互式恢复出厂设置命令 - `auth_set_apikey` 函数: 正确写入 API Key 到 `auth-profiles.json` - `register_and_set_model` 函数: 注册模型到 `agents.defaults.models` 并设为默认 - `register_custom_provider` 函数: 为需要 `baseUrl` 的 OpenAI 兼容供应商注册 `models.providers` +- 「检测升级」同时检查 OpenClaw 和**插件版本** (通过 GitHub API 获取最新 release) +- 页面加载时自动静默检查更新,有新版本时「检测升级」按钮显示橙色小红点提醒 +- 状态面板显示当前安装的插件版本号 +- 构建/安装流程部署 `VERSION` 文件到 `/usr/share/openclaw/VERSION` +- `openclaw-env setup` 安装环境时自动安装 Gemini CLI (Google OAuth 依赖) + +### 改进 +- 使用指南顺序调整: ② 配置管理 → ③ Web 控制台 (首次使用更合理的引导顺序) +- Gemini CLI 安装从配置向导选项 1 移至环境安装阶段,避免进入向导时临时等待 ## [1.0.2] - 2026-03-02 diff --git a/Makefile b/Makefile index 9542dff..e166d55 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,7 @@ define Package/$(PKG_NAME)/install $(INSTALL_DATA) ./luasrc/view/openclaw/advanced.htm $(1)/usr/lib/lua/luci/view/openclaw/advanced.htm $(INSTALL_DATA) ./luasrc/view/openclaw/console.htm $(1)/usr/lib/lua/luci/view/openclaw/console.htm $(INSTALL_DIR) $(1)/usr/share/openclaw + $(INSTALL_DATA) ./VERSION $(1)/usr/share/openclaw/VERSION $(INSTALL_BIN) ./root/usr/share/openclaw/oc-config.sh $(1)/usr/share/openclaw/oc-config.sh $(INSTALL_DATA) ./root/usr/share/openclaw/web-pty.js $(1)/usr/share/openclaw/web-pty.js $(INSTALL_DIR) $(1)/usr/share/openclaw/ui diff --git a/VERSION b/VERSION index 21e8796..ee90284 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3 +1.0.4 diff --git a/luasrc/controller/openclaw.lua b/luasrc/controller/openclaw.lua index b09e80c..4ac4fb9 100644 --- a/luasrc/controller/openclaw.lua +++ b/luasrc/controller/openclaw.lua @@ -93,8 +93,16 @@ function action_status() uptime = "", node_version = "", openclaw_version = "", + plugin_version = "", } + -- 插件版本 + local pvf = io.open("/usr/share/openclaw/VERSION", "r") + if pvf then + result.plugin_version = pvf:read("*a"):gsub("%s+", "") + pvf:close() + end + -- 检查 Node.js local node_bin = "/opt/openclaw/node/bin/node" local f = io.open(node_bin, "r") @@ -278,10 +286,10 @@ function action_check_update() local http = require "luci.http" local sys = require "luci.sys" - -- 当前版本 + -- 当前 OpenClaw 版本 local current = get_openclaw_version() - -- 最新版本 (从 npm registry 查询) + -- 最新 OpenClaw 版本 (从 npm registry 查询) local latest = sys.exec("PATH=/opt/openclaw/node/bin:/opt/openclaw/global/bin:$PATH npm view openclaw version 2>/dev/null"):gsub("%s+", "") local has_update = false @@ -289,12 +297,42 @@ function action_check_update() has_update = true end + -- 插件版本检查 (从 GitHub API 获取最新 release tag) + local plugin_current = "" + local pf = io.open("/usr/share/openclaw/VERSION", "r") + or io.open("/root/luci-app-openclaw/VERSION", "r") + if pf then + plugin_current = pf:read("*a"):gsub("%s+", "") + pf:close() + end + + local plugin_latest = "" + local plugin_has_update = false + -- 仅在请求参数含 check_plugin=1 或 quick=1 时检查插件版本 + local check_plugin = http.formvalue("check_plugin") or "" + local quick = http.formvalue("quick") or "" + if check_plugin == "1" or quick == "1" then + -- 使用 GitHub API 获取最新 release tag (轻量, 不下载任何文件) + local gh_resp = sys.exec("curl -sf --connect-timeout 5 --max-time 10 'https://api.github.com/repos/10000ge10000/luci-app-openclaw/releases/latest' 2>/dev/null | grep -o '\"tag_name\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | head -1 | cut -d'\"' -f4") + gh_resp = gh_resp:gsub("%s+", "") + if gh_resp ~= "" then + -- tag 可能是 v1.0.3 或 1.0.3 + plugin_latest = gh_resp:gsub("^v", "") + end + if plugin_current ~= "" and plugin_latest ~= "" and plugin_current ~= plugin_latest then + plugin_has_update = true + end + end + http.prepare_content("application/json") http.write_json({ status = "ok", current = current, latest = latest, - has_update = has_update + has_update = has_update, + plugin_current = plugin_current, + plugin_latest = plugin_latest, + plugin_has_update = plugin_has_update }) end diff --git a/luasrc/model/cbi/openclaw/basic.lua b/luasrc/model/cbi/openclaw/basic.lua index e727ab0..580a13e 100644 --- a/luasrc/model/cbi/openclaw/basic.lua +++ b/luasrc/model/cbi/openclaw/basic.lua @@ -35,7 +35,7 @@ act.cfgvalue = function(self, section) html[#html+1] = '' html[#html+1] = '' html[#html+1] = '' - html[#html+1] = '' + html[#html+1] = '' html[#html+1] = '' html[#html+1] = '' html[#html+1] = '
' @@ -222,22 +222,37 @@ act.cfgvalue = function(self, section) html[#html+1] = '}catch(e){el.innerHTML="❌ 错误";}' html[#html+1] = '});}' - -- 检测升级 + -- 检测升级 (同时检查 OpenClaw + 插件版本) html[#html+1] = 'function ocCheckUpdate(){' html[#html+1] = 'var btn=document.getElementById("btn-check-update");' html[#html+1] = 'var el=document.getElementById("action-result");' html[#html+1] = 'var act=document.getElementById("oc-update-action");' html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在检测...";el.textContent="";act.style.display="none";' - html[#html+1] = '(new XHR()).get("' .. check_url .. '",null,function(x){' + html[#html+1] = '(new XHR()).get("' .. check_url .. '?check_plugin=1",null,function(x){' html[#html+1] = 'btn.disabled=false;btn.textContent="🔍 检测升级";' + html[#html+1] = 'var dot=document.getElementById("update-dot");if(dot)dot.style.display="none";' html[#html+1] = 'try{var r=JSON.parse(x.responseText);' - html[#html+1] = 'if(!r.current){el.innerHTML="⚠️ OpenClaw 未安装";return;}' + html[#html+1] = 'var msgs=[];' + -- OpenClaw 版本检查 + html[#html+1] = 'if(!r.current){msgs.push("⚠️ OpenClaw 运行环境未安装");}' + html[#html+1] = 'else if(r.has_update){msgs.push("📦 OpenClaw: v"+r.current+" → v"+r.latest+" (有新版本)");}' + html[#html+1] = 'else{msgs.push("✅ OpenClaw: v"+r.current+" (已是最新)");}' + -- 插件版本检查 + html[#html+1] = 'if(r.plugin_current){' + html[#html+1] = 'if(r.plugin_has_update){msgs.push("� 插件: v"+r.plugin_current+" → v"+r.plugin_latest+" (有新版本,请到 GitHub 下载更新)");}' + html[#html+1] = 'else if(r.plugin_latest){msgs.push("✅ 插件: v"+r.plugin_current+" (已是最新)");}' + html[#html+1] = 'else{msgs.push("🔌 插件: v"+r.plugin_current+" (无法检查最新版本)");}' + html[#html+1] = '}' + html[#html+1] = 'el.innerHTML=msgs.join("
");' + -- 显示 OpenClaw 升级按钮 html[#html+1] = 'if(r.has_update){' - html[#html+1] = 'el.innerHTML="📦 当前: v"+r.current+" → 最新: v"+r.latest+"";' html[#html+1] = 'act.style.display="block";' - html[#html+1] = 'act.innerHTML=\'\';' - html[#html+1] = '}else{' - html[#html+1] = 'el.innerHTML="✅ 已是最新版本 (v"+r.current+")";' + html[#html+1] = 'act.innerHTML=\'\';' + html[#html+1] = '}' + -- 插件有更新时添加 GitHub 链接 + html[#html+1] = 'if(r.plugin_has_update){' + html[#html+1] = 'act.style.display="block";' + html[#html+1] = 'act.innerHTML=(act.innerHTML||"")+\' 📥 下载插件 v\'+r.plugin_latest+\'\';' html[#html+1] = '}' html[#html+1] = '}catch(e){el.innerHTML="❌ 检测失败";}' html[#html+1] = '});}' @@ -334,6 +349,20 @@ act.cfgvalue = function(self, section) html[#html+1] = '}catch(e){el.innerHTML="❌ 请求失败";}' html[#html+1] = '});}' + -- 页面加载时静默检查是否有更新 (仅显示小红点提示) + html[#html+1] = '(function(){' + html[#html+1] = 'setTimeout(function(){' + html[#html+1] = '(new XHR()).get("' .. check_url .. '?quick=1",null,function(x){' + html[#html+1] = 'try{var r=JSON.parse(x.responseText);' + html[#html+1] = 'if(r.has_update||r.plugin_has_update){' + html[#html+1] = 'var dot=document.getElementById("update-dot");' + html[#html+1] = 'if(dot)dot.style.display="block";' + html[#html+1] = '}' + html[#html+1] = '}catch(e){}' + html[#html+1] = '});' + html[#html+1] = '},2000);' + html[#html+1] = '})();' + html[#html+1] = '' return table.concat(html, "\n") end @@ -351,8 +380,8 @@ guide.cfgvalue = function() html[#html+1] = '📖 使用指南
' html[#html+1] = '' html[#html+1] = '① 首次使用请点击 「安装运行环境」,安装完成后服务会自动启动
' - html[#html+1] = '② 进入 「Web 控制台」 配置 AI 模型、消息渠道,直接开始对话
' - html[#html+1] = '③ 进入 「配置管理」 可使用交互式向导进行高级配置
' + html[#html+1] = '② 进入 「配置管理」 使用交互式向导快速配置 AI 模型和 API Key
' + html[#html+1] = '③ 进入 「Web 控制台」 配置消息渠道,直接开始对话' html[#html+1] = '
' html[#html+1] = '有疑问?请关注B站并留言:' html[#html+1] = '' diff --git a/luasrc/view/openclaw/advanced.htm b/luasrc/view/openclaw/advanced.htm index 33faf45..cef6eba 100644 --- a/luasrc/view/openclaw/advanced.htm +++ b/luasrc/view/openclaw/advanced.htm @@ -13,20 +13,6 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793" .oc-page-header h2 { font-size: 18px; font-weight: 600; color: #333; margin: 0 0 6px 0; } .oc-page-header p { font-size: 13px; color: #666; margin: 0; line-height: 1.6; } -.oc-info-box { - padding: 12px 16px; - margin-bottom: 16px; - background: #f6f8fa; - border: 1px solid #e1e4e8; - border-left: 4px solid #4a90d9; - border-radius: 4px; - font-size: 13px; - line-height: 1.7; - color: #555; -} -.oc-info-box ul { margin: 6px 0 0 0; padding-left: 18px; list-style: none; } -.oc-info-box li { margin-bottom: 2px; } - .oc-terminal-wrap { border: 2px solid #2d333b; border-radius: 8px; @@ -47,15 +33,6 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"

通过内嵌的交互式终端 (oc-config) 进行 OpenClaw 的完整配置管理。支持 AI 模型配置、消息渠道设置、健康检查等。

-
- 💡 菜单功能说明: - -
-
diff --git a/luasrc/view/openclaw/status.htm b/luasrc/view/openclaw/status.htm index 1d2809b..fc4fca7 100644 --- a/luasrc/view/openclaw/status.htm +++ b/luasrc/view/openclaw/status.htm @@ -75,6 +75,7 @@ 运行时间- Node.js- OpenClaw- + 插件版本-
@@ -132,6 +133,7 @@ document.getElementById('oc-st-uptime').textContent = d.uptime || '-'; document.getElementById('oc-st-node').textContent = d.node_version || '未安装'; document.getElementById('oc-st-ocver').textContent = d.openclaw_version || '未安装'; + document.getElementById('oc-st-plugin').textContent = d.plugin_version ? ('v' + d.plugin_version) : '-'; } catch(e) { document.getElementById('oc-st-status').innerHTML = '查询失败'; diff --git a/root/etc/init.d/openclaw b/root/etc/init.d/openclaw index c556e68..e916929 100755 --- a/root/etc/init.d/openclaw +++ b/root/etc/init.d/openclaw @@ -114,6 +114,15 @@ d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true; 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'; +// 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';} fs.writeFileSync(f,JSON.stringify(d,null,2)); " 2>/dev/null fi diff --git a/root/usr/bin/openclaw-env b/root/usr/bin/openclaw-env index 49b3272..3b9b71b 100755 --- a/root/usr/bin/openclaw-env +++ b/root/usr/bin/openclaw-env @@ -13,7 +13,7 @@ set -e NODE_VERSION="${NODE_VERSION:-22.16.0}" # 经过验证的 OpenClaw 稳定版本 (更新此值需同步测试) -OC_TESTED_VERSION="2026.3.1" +OC_TESTED_VERSION="2026.3.2" # 用户可通过 OC_VERSION 环境变量覆盖安装版本 OC_VERSION="${OC_VERSION:-}" NODE_BASE="/opt/openclaw/node" @@ -296,6 +296,18 @@ install_openclaw() { log_error "OpenClaw 安装验证失败" exit 1 fi + + # 安装 Gemini CLI (官方模型配置向导的 Google Gemini OAuth 依赖) + if [ -x "$NPM_BIN" ]; then + echo "" + echo "=== 安装 Gemini CLI (Google OAuth 依赖) ===" + "$NPM_BIN" install -g @google/gemini-cli --prefix="$OC_GLOBAL" $install_flags 2>&1 | tail -3 + if command -v gemini >/dev/null 2>&1 || [ -x "$OC_GLOBAL/bin/gemini" ]; then + log_info "Gemini CLI 安装成功" + else + log_warn "Gemini CLI 安装失败 (不影响核心功能,仅影响 Google Gemini OAuth 登录)" + fi + fi } init_openclaw() { @@ -314,7 +326,7 @@ init_openclaw() { OPENCLAW_HOME="$OC_DATA" \ OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \ OPENCLAW_CONFIG_PATH="${OC_DATA}/.openclaw/openclaw.json" \ - "$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk 2>/dev/null || true + "$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk --tools-profile coding 2>/dev/null || true log_info "初始化完成" fi @@ -526,7 +538,7 @@ do_factory_reset() { oc_entry=$(find "$OC_GLOBAL" -name "openclaw.mjs" -path "*/openclaw/openclaw.mjs" 2>/dev/null | head -1) if [ -n "$oc_entry" ]; then log_info "重新初始化..." - OPENCLAW_HOME="$OC_DATA" "$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk >/dev/null 2>&1 || true + OPENCLAW_HOME="$OC_DATA" "$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk --tools-profile coding >/dev/null 2>&1 || true fi fi @@ -535,7 +547,7 @@ do_factory_reset() { local new_token new_token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48) _JS_KEY="gateway.port" _JS_VAL="18789" "$NODE_BIN" -e "const fs=require('fs');let d={};try{d=JSON.parse(fs.readFileSync('${config_file}','utf8'));}catch(e){}const ks=process.env._JS_KEY.split('.');let o=d;for(let i=0;i/dev/null - for kv in "gateway.bind=lan" "gateway.mode=local" "gateway.auth.mode=token" "gateway.auth.token=${new_token}" "gateway.controlUi.allowInsecureAuth=true" "gateway.controlUi.dangerouslyDisableDeviceAuth=true" "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true" "gateway.tailscale.mode=off"; do + for kv in "gateway.bind=lan" "gateway.mode=local" "gateway.auth.mode=token" "gateway.auth.token=${new_token}" "gateway.controlUi.allowInsecureAuth=true" "gateway.controlUi.dangerouslyDisableDeviceAuth=true" "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true" "gateway.tailscale.mode=off" "acp.dispatch.enabled=false" "tools.profile=coding"; do local k="${kv%%=*}" v="${kv#*=}" _JS_KEY="$k" _JS_VAL="$v" "$NODE_BIN" -e "const fs=require('fs');let d={};try{d=JSON.parse(fs.readFileSync('${config_file}','utf8'));}catch(e){}const ks=process.env._JS_KEY.split('.');let o=d;for(let i=0;i/dev/null done diff --git a/root/usr/share/openclaw/oc-config.sh b/root/usr/share/openclaw/oc-config.sh index 85b4fdd..7e9c6c3 100755 --- a/root/usr/share/openclaw/oc-config.sh +++ b/root/usr/share/openclaw/oc-config.sh @@ -359,7 +359,8 @@ configure_model() { echo -e " ${CYAN}9)${NC} xAI Grok (Grok-3/3-mini)" echo -e " ${CYAN}10)${NC} Groq (Llama 4, Llama 3.3)" echo -e " ${CYAN}11)${NC} 硅基流动 SiliconFlow" - echo -e " ${CYAN}12)${NC} 自定义 OpenAI 兼容 API" + echo -e " ${CYAN}12)${NC} Ollama (本地模型,无需 API Key)" + echo -e " ${CYAN}13)${NC} 自定义 OpenAI 兼容 API" echo -e " ${CYAN}0)${NC} 返回" echo "" prompt_with_default "请选择" "1" choice @@ -373,14 +374,6 @@ configure_model() { echo -e " ${CYAN}预启用模型认证插件...${NC}" enable_auth_plugins echo "" - # 检查并安装 gemini-cli (官方向导的 Google Gemini OAuth 依赖) - if ! command -v gemini >/dev/null 2>&1; then - local npm_bin="${NODE_BASE}/bin/npm" - if [ -x "$npm_bin" ]; then - echo -e " ${CYAN}安装 Gemini CLI (官方向导 Google OAuth 依赖)...${NC}" - "$npm_bin" install -g @google/gemini-cli --prefix="$OC_GLOBAL" >/dev/null 2>&1 || true - fi - fi (oc_cmd configure --section model) || echo -e " ${YELLOW}配置向导已退出${NC}" echo "" ask_restart @@ -718,6 +711,122 @@ configure_model() { fi ;; 12) + echo "" + echo -e " ${BOLD}🦙 Ollama 本地模型配置${NC}" + echo -e " ${YELLOW}Ollama 在本地或局域网运行大模型,无需 API Key${NC}" + echo -e " ${YELLOW}安装 Ollama: https://ollama.com${NC}" + echo "" + echo -e " ${CYAN}连接方式:${NC}" + echo -e " ${CYAN}a)${NC} 本机运行 (localhost:11434)" + echo -e " ${CYAN}b)${NC} 局域网其他设备" + echo "" + prompt_with_default "请选择" "a" ollama_mode + local ollama_url="" + case "$ollama_mode" in + b) + prompt_with_default "Ollama 地址 (如 192.168.1.100:11434)" "" ollama_host + if [ -n "$ollama_host" ]; then + # 补全协议前缀 + case "$ollama_host" in + http://*|https://*) ollama_url="${ollama_host}" ;; + *) ollama_url="http://${ollama_host}" ;; + esac + # v2026.3.2: Ollama 使用原生 API,baseUrl 不带 /v1 + ollama_url=$(echo "$ollama_url" | sed 's|/v1/*$||;s|/*$||') + fi + ;; + *) + ollama_url="http://127.0.0.1:11434" + ;; + esac + if [ -n "$ollama_url" ]; then + # 检测 Ollama 是否可达 + echo "" + echo -e " ${CYAN}检测 Ollama 连通性...${NC}" + local ollama_base=$(echo "$ollama_url" | sed 's|/v1$||') + local ollama_check=$(curl -sf --connect-timeout 3 --max-time 5 "${ollama_base}/api/tags" 2>/dev/null || echo "") + if [ -n "$ollama_check" ]; then + echo -e " ${GREEN}✅ Ollama 已连接${NC}" + # 列出已安装的模型 + local model_list=$("$NODE_BIN" -e " + try{ + const d=JSON.parse(process.argv[1]); + (d.models||[]).forEach((m,i)=>console.log(' '+(i+1)+') '+m.name)); + }catch(e){} + " "$ollama_check" 2>/dev/null) + if [ -n "$model_list" ]; then + echo -e " ${CYAN}已安装的模型:${NC}" + echo "$model_list" + echo -e " ${CYAN}m)${NC} 手动输入模型名" + echo "" + prompt_with_default "请选择模型" "1" ollama_sel + if [ "$ollama_sel" = "m" ]; then + prompt_with_default "请输入模型名称" "llama3.3" model_name + elif echo "$ollama_sel" | grep -qE '^[0-9]+$'; then + model_name=$("$NODE_BIN" -e " + try{ + const d=JSON.parse(process.argv[1]); + const m=(d.models||[])[parseInt(process.argv[2])-1]; + console.log(m?m.name:''); + }catch(e){console.log('');} + " "$ollama_check" "$ollama_sel" 2>/dev/null) + if [ -z "$model_name" ]; then + echo -e " ${YELLOW}无效选择,使用默认模型${NC}" + model_name="llama3.3" + fi + fi + else + echo -e " ${YELLOW}未检测到已安装模型,请先在 Ollama 中拉取模型:${NC}" + echo -e " ${CYAN} ollama pull llama3.3${NC}" + echo "" + prompt_with_default "请输入模型名称" "llama3.3" model_name + fi + else + echo -e " ${YELLOW}⚠️ 无法连接 Ollama (${ollama_base})${NC}" + echo -e " ${YELLOW} 请确认 Ollama 已启动并可访问${NC}" + echo -e " ${CYAN} 提示: 如果 Ollama 在其他设备上,需设置 OLLAMA_HOST=0.0.0.0${NC}" + echo "" + prompt_with_default "仍要继续配置? (y/n)" "n" force_continue + if ! confirm_yes "$force_continue"; then + return + fi + prompt_with_default "请输入模型名称" "llama3.3" model_name + fi + if [ -n "$model_name" ]; then + # Ollama 无需 API Key,使用占位符 + auth_set_apikey ollama "ollama-local" "ollama:local" + # v2026.3.2: Ollama 使用原生 ollama API,不再走 OpenAI 兼容层 + _RCP_PROV="ollama" _RCP_URL="$ollama_url" _RCP_KEY="ollama-local" _RCP_MID="$model_name" _RCP_MNAME="$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['ollama']={ + baseUrl:process.env._RCP_URL, + apiKey:process.env._RCP_KEY, + api:'ollama', + models:[{ + id:process.env._RCP_MID, + name:process.env._RCP_MNAME, + reasoning:false, + input:['text'], + cost:{input:0,output:0,cacheRead:0,cacheWrite:0}, + contextWindow:128000, + maxTokens:32000 + }] + }; + 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 "ollama/${model_name}" + echo -e " ${GREEN}✅ Ollama 已配置,活跃模型: ollama/${model_name}${NC}" + echo -e " ${CYAN} Ollama 地址: ${ollama_url}${NC}" + fi + fi + ;; + 13) echo "" echo -e " ${BOLD}自定义 OpenAI 兼容 API${NC}" echo -e " ${YELLOW}支持任何兼容 OpenAI API 格式的服务商${NC}" @@ -1108,6 +1217,34 @@ health_check() { local gw_port=$(json_get gateway.port) gw_port=${gw_port:-18789} + # ── v2026.3.2: 使用官方 config validate 验证配置 ── + if command -v openclaw >/dev/null 2>&1 || [ -n "$OC_ENTRY" ]; then + echo -e " ${CYAN}验证配置文件格式...${NC}" + local validate_out="" + validate_out=$(oc_cmd config validate --json 2>/dev/null) || true + if [ -n "$validate_out" ]; then + local has_errors=$("$NODE_BIN" -e " + try{const r=JSON.parse(process.argv[1]); + if(r.valid===true){console.log('OK');} + else if(r.errors&&r.errors.length>0){r.errors.forEach(e=>console.log('ERR:'+e.message));} + else{console.log('OK');}}catch(e){console.log('SKIP');} + " "$validate_out" 2>/dev/null) + if [ "$has_errors" = "OK" ]; then + echo -e " ${GREEN}✅ 配置文件格式有效${NC}" + elif [ "$has_errors" = "SKIP" ]; then + echo -e " ${YELLOW}⚠️ 无法解析验证结果,跳过${NC}" + else + echo -e " ${RED}❌ 配置文件存在错误:${NC}" + echo "$has_errors" | while IFS= read -r line; do + echo -e " ${YELLOW}• ${line#ERR:}${NC}" + done + fi + else + echo -e " ${YELLOW}⚠️ config validate 不可用,跳过格式验证${NC}" + fi + echo "" + fi + # ── 自动修复: 移除旧版错误写入的顶层 models.xxx 无效键 ── if [ -f "$CONFIG_FILE" ]; then local has_bad_models=$("$NODE_BIN" -e " @@ -1152,6 +1289,19 @@ health_check() { echo -e " ${RED}❌ HTTP 响应异常 (${http_code})${NC}" fi + # v2026.3.2: 使用 gateway health --json 做深度健康检查 (HTTP /health 已被 SPA 接管) + local health_resp=$(oc_cmd gateway health --json 2>/dev/null) + if [ -n "$health_resp" ]; then + local health_ok=$("$NODE_BIN" -e "try{const h=JSON.parse(process.argv[1]);console.log(h.ok?'ok':'fail');}catch(e){console.log('parse_error');}" "$health_resp" 2>/dev/null) + if [ "$health_ok" = "ok" ]; then + echo -e " ${GREEN}✅ Gateway 健康检查正常${NC}" + elif [ "$health_ok" = "parse_error" ]; then + echo -e " ${YELLOW}⚠️ Gateway 健康检查响应无法解析${NC}" + else + echo -e " ${YELLOW}⚠️ Gateway 健康检查异常${NC}" + fi + fi + if [ -f "$CONFIG_FILE" ]; then echo -e " ${GREEN}✅ 配置文件存在${NC}" else @@ -1296,9 +1446,9 @@ reset_to_defaults() { 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 + timeout 10 sh -c "\"$_node_bin\" \"$OC_ENTRY\" onboard --non-interactive --accept-risk --tools-profile coding" >/dev/null 2>&1 || true else - "$_node_bin" "$OC_ENTRY" onboard --non-interactive --accept-risk >/dev/null 2>&1 & + "$_node_bin" "$OC_ENTRY" onboard --non-interactive --accept-risk --tools-profile coding >/dev/null 2>&1 & local _ob_pid=$! sleep 10 kill "$_ob_pid" 2>/dev/null || true @@ -1318,6 +1468,8 @@ reset_to_defaults() { json_set gateway.controlUi.dangerouslyDisableDeviceAuth true json_set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback true json_set gateway.tailscale.mode off + json_set acp.dispatch.enabled false + json_set tools.profile coding # 同步 token 到 UCI . /lib/functions.sh 2>/dev/null || true diff --git a/scripts/build_ipk.sh b/scripts/build_ipk.sh index b6042a6..db5909c 100755 --- a/scripts/build_ipk.sh +++ b/scripts/build_ipk.sh @@ -60,6 +60,7 @@ cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$DATA_DIR/usr/lib/lua/luci/view/opencl # oc-config assets mkdir -p "$DATA_DIR/usr/share/openclaw" +cp "$PKG_DIR/VERSION" "$DATA_DIR/usr/share/openclaw/VERSION" cp "$PKG_DIR/root/usr/share/openclaw/oc-config.sh" "$DATA_DIR/usr/share/openclaw/" chmod +x "$DATA_DIR/usr/share/openclaw/oc-config.sh" cp "$PKG_DIR/root/usr/share/openclaw/web-pty.js" "$DATA_DIR/usr/share/openclaw/" diff --git a/scripts/build_run.sh b/scripts/build_run.sh index 079f86f..4115f61 100755 --- a/scripts/build_run.sh +++ b/scripts/build_run.sh @@ -62,6 +62,7 @@ install_files() { # oc-config assets mkdir -p "$dest/usr/share/openclaw" + cp "$PKG_DIR/VERSION" "$dest/usr/share/openclaw/VERSION" cp "$PKG_DIR/root/usr/share/openclaw/oc-config.sh" "$dest/usr/share/openclaw/" chmod +x "$dest/usr/share/openclaw/oc-config.sh" cp "$PKG_DIR/root/usr/share/openclaw/web-pty.js" "$dest/usr/share/openclaw/"