release: v1.0.4 — 适配 OpenClaw 2026.3.2

This commit is contained in:
10000ge10000
2026-03-05 15:20:31 +08:00
parent 649e73120f
commit 5f381eebd7
12 changed files with 315 additions and 53 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -1 +1 @@
1.0.3
1.0.4

View File

@@ -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

View File

@@ -35,7 +35,7 @@ act.cfgvalue = function(self, section)
html[#html+1] = '<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocShowSetupDialog()" id="btn-setup" title="下载 Node.js 并安装 OpenClaw">📦 安装运行环境</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocServiceCtl(\'restart\')">🔄 重启服务</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocServiceCtl(\'stop\')">⏹️ 停止服务</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocCheckUpdate()" id="btn-check-update">🔍 检测升级</button>'
html[#html+1] = '<span style="position:relative;display:inline-block;" id="btn-check-update-wrap"><button class="btn cbi-button cbi-button-action" type="button" onclick="ocCheckUpdate()" id="btn-check-update">🔍 检测升级</button><span id="update-dot" style="display:none;position:absolute;top:-2px;right:-2px;width:10px;height:10px;background:#e36209;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px #e36209;"></span></span>'
html[#html+1] = '<button class="btn cbi-button cbi-button-remove" type="button" onclick="ocUninstall()" id="btn-uninstall" title="删除 Node.js、OpenClaw 运行环境及相关数据">🗑️ 卸载环境</button>'
html[#html+1] = '</div>'
html[#html+1] = '<div id="action-result" style="margin-top:8px;"></div>'
@@ -222,22 +222,37 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 错误</span>";}'
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="<span style=\\"color:#999\\">⚠️ OpenClaw 未安装</span>";return;}'
html[#html+1] = 'var msgs=[];'
-- OpenClaw 版本检查
html[#html+1] = 'if(!r.current){msgs.push("<span style=\\"color:#999\\">⚠️ OpenClaw 运行环境未安装</span>");}'
html[#html+1] = 'else if(r.has_update){msgs.push("<span style=\\"color:#e36209\\">📦 OpenClaw: v"+r.current+" → v"+r.latest+" (有新版本)</span>");}'
html[#html+1] = 'else{msgs.push("<span style=\\"color:green\\">✅ OpenClaw: v"+r.current+" (已是最新)</span>");}'
-- 插件版本检查
html[#html+1] = 'if(r.plugin_current){'
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\"color:#e36209\\"><3E> 插件: v"+r.plugin_current+" → v"+r.plugin_latest+" (有新版本,请到 GitHub 下载更新)</span>");}'
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\"color:green\\">✅ 插件: v"+r.plugin_current+" (已是最新)</span>");}'
html[#html+1] = 'else{msgs.push("<span style=\\"color:#999\\">🔌 插件: v"+r.plugin_current+" (无法检查最新版本)</span>");}'
html[#html+1] = '}'
html[#html+1] = 'el.innerHTML=msgs.join("<br/>");'
-- 显示 OpenClaw 升级按钮
html[#html+1] = 'if(r.has_update){'
html[#html+1] = 'el.innerHTML="<span style=\\"color:#e36209\\">📦 当前: v"+r.current+" → 最新: v"+r.latest+"</span>";'
html[#html+1] = 'act.style.display="block";'
html[#html+1] = 'act.innerHTML=\'<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocDoUpdate()" id="btn-do-update">⬆️ 立即升级</button>\';'
html[#html+1] = '}else{'
html[#html+1] = 'el.innerHTML="<span style=\\"color:green\\">✅ 已是最新版本 (v"+r.current+")</span>";'
html[#html+1] = 'act.innerHTML=\'<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocDoUpdate()" id="btn-do-update">⬆️ 立即升级 OpenClaw</button>\';'
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||"")+\' <a href="https://github.com/10000ge10000/luci-app-openclaw/releases/latest" target="_blank" rel="noopener" class="btn cbi-button cbi-button-action" style="text-decoration:none;">📥 下载插件 v\'+r.plugin_latest+\'</a>\';'
html[#html+1] = '}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}'
html[#html+1] = '});}'
@@ -334,6 +349,20 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 请求失败</span>";}'
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] = '</script>'
return table.concat(html, "\n")
end
@@ -351,8 +380,8 @@ guide.cfgvalue = function()
html[#html+1] = '<strong style="font-size:14px;">📖 使用指南</strong><br/>'
html[#html+1] = '<span style="color:#555;">'
html[#html+1] = '① 首次使用请点击 <b>「安装运行环境」</b>,安装完成后服务会自动启动<br/>'
html[#html+1] = '② 进入 <b>「Web 控制台」</b> 配置 AI 模型、消息渠道,直接开始对话<br/>'
html[#html+1] = '③ 进入 <b>「配置管理」</b> 可使用交互式向导进行高级配置</span>'
html[#html+1] = '② 进入 <b>「配置管理」</b> 使用交互式向导快速配置 AI 模型和 API Key<br/>'
html[#html+1] = '③ 进入 <b>「Web 控制台」</b> 配置消息渠道,直接开始对话</span>'
html[#html+1] = '<div style="margin-top:10px;padding-top:10px;border-top:1px solid #d0e8ff;">'
html[#html+1] = '<span style="color:#888;">有疑问请关注B站并留言</span>'
html[#html+1] = '<a href="https://space.bilibili.com/59438380" target="_blank" rel="noopener" style="color:#00a1d6;font-weight:bold;text-decoration:none;">'

View File

@@ -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"
<p>通过内嵌的交互式终端 (oc-config) 进行 OpenClaw 的完整配置管理。支持 AI 模型配置、消息渠道设置、健康检查等。</p>
</div>
<div class="oc-info-box">
<strong>💡 菜单功能说明:</strong>
<ul>
<li><strong>1)</strong> 查看当前配置 &nbsp; <strong>2)</strong> 配置 AI 模型提供商 &nbsp; <strong>3)</strong> 设定当前活跃模型</li>
<li><strong>4)</strong> 配置消息渠道 &nbsp; <strong>5)</strong> Telegram 配对向导 &nbsp; <strong>6)</strong> 健康检查 / 诊断</li>
<li><strong>7)</strong> 重启网关 &nbsp; <strong>8)</strong> 查看/编辑原始配置 &nbsp; <strong>9)</strong> 恢复默认配置</li>
</ul>
</div>
<div class="oc-terminal-wrap">
<div id="oc-terminal-container">
<div class="oc-terminal-loading" id="oc-terminal-loading">

View File

@@ -75,6 +75,7 @@
<tr><td>运行时间</td><td id="oc-st-uptime">-</td></tr>
<tr><td>Node.js</td><td id="oc-st-node">-</td></tr>
<tr><td>OpenClaw</td><td id="oc-st-ocver">-</td></tr>
<tr><td>插件版本</td><td id="oc-st-plugin">-</td></tr>
</table>
</div>
</div>
@@ -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 = '<span class="oc-badge oc-badge-unknown">查询失败</span>';

View File

@@ -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

View File

@@ -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<ks.length-1;i++){if(!o[ks[i]]||typeof o[ks[i]]!=='object')o[ks[i]]={};o=o[ks[i]];}let v=process.env._JS_VAL;try{v=JSON.parse(v);}catch(e){}o[ks[ks.length-1]]=v;fs.writeFileSync('${config_file}',JSON.stringify(d,null,2));" 2>/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<ks.length-1;i++){if(!o[ks[i]]||typeof o[ks[i]]!=='object')o[ks[i]]={};o=o[ks[i]];}let v=process.env._JS_VAL;try{v=JSON.parse(v);}catch(e){}o[ks[ks.length-1]]=v;fs.writeFileSync('${config_file}',JSON.stringify(d,null,2));" 2>/dev/null
done

View File

@@ -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 使用原生 APIbaseUrl 不带 /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

View File

@@ -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/"

View File

@@ -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/"