release: v1.0.3 — P0 配置崩溃修复、Ollama 支持、检测升级

This commit is contained in:
10000ge10000
2026-03-05 12:17:27 +08:00
parent 823a693761
commit 649e73120f
5 changed files with 254 additions and 47 deletions

View File

@@ -4,6 +4,24 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
## [1.0.3] - 2026-03-02
### 修复
- **P0** 配置管理写入错误的 JSON 路径导致 Gateway 崩溃且无法恢复 (#1)
- `json_set models.openai.apiKey``openclaw.json` 创建了非法的顶层 `models`
- OpenClaw 2026.3.1 严格校验配置 schema拒绝启动并报 `Unknown config keys: models.openai`
- 修复: API Key 改写入 `auth-profiles.json`,模型注册到 `agents.defaults.models`
- 影响: 所有 11 个供应商的快速配置 (OpenAI/Anthropic/Gemini/OpenRouter/DeepSeek/GitHub Copilot/Qwen/xAI/Groq/SiliconFlow/自定义)
- **P0** 恢复默认配置 → "清除模型配置" 未清理 `auth-profiles.json` 认证信息
- **P1** 健康检查新增自动修复: 检测并移除旧版错误写入的顶层 `models` 无效键
- **P1** `set_active_model` 手动切换模型时未注册到 `agents.defaults.models`
### 新增
- `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`
## [1.0.2] - 2026-03-02
### 修复

View File

@@ -29,9 +29,10 @@
| x86_64 | glibc | [nodejs.org](https://nodejs.org/) 官方 | ✅ 支持 |
| aarch64 | musl | 项目自托管Alpine 打包,含完整依赖) | ✅ 已验证 |
| aarch64 | glibc | [nodejs.org](https://nodejs.org/) 官方 | ✅ 支持 |
| mips / mipsel | - | — | ❌ 不支持 |
| armv7l / armv6l | - | — | ❌ 不支持 |
> **说明**Node.js 22+ 不再提供 32 位 ARM 预编译包,因此不支持 armv7l/armv6l如 MT7621 路由器)
> **说明**Node.js 22+ 提供 x86_64 和 aarch64 预编译包,不支持 MIPS如 MT7620/MT7621 路由器)和 32 位 ARMarmv7l/armv6l。大部分老旧路由器MT76xx 系列)为 MIPS 架构,无法运行
#### 支持的 OpenWrt 版本
@@ -55,7 +56,8 @@
| Raspberry Pi 4/5 | aarch64 | OpenWrt 23.05+ | ✅ 应支持 |
| FriendlyElec R4S/R5S | aarch64 | OpenWrt / FriendlyWrt | ✅ 应支持 |
| 通用 x86 虚拟机 (PVE/ESXi) | x86_64 | OpenWrt 22.03+ | ✅ 应支持 |
| MT7621 路由器 (如 Redmi AC2100) | mipsel / armv7l | — | ❌ 不支持 |
| MT7621 路由器 (如 Redmi AC2100) | mipsel | — | ❌ 不支持 (MIPS) |
| MT7620/MT7628 路由器 | mipsel | — | ❌ 不支持 (MIPS) |
#### ARM64 musl 特别说明
@@ -64,7 +66,7 @@ ARM64 + musl 的 OpenWrt 设备(绝大多数 ARM64 路由器)使用**项目
- 基于 Alpine Linux 3.21 ARM64 环境打包
- 包含完整的共享库libstdc++、libssl、libicu 等)和 musl 动态链接器
- 包含完整 ICU 国际化数据(`icudt74l.dat`
- 通过 wrapper 脚本使用打包的 musl 链接器启动**不依赖系统库版本**
- 通过 `patchelf` 将 ELF interpreter 和 rpath 指向打包的 musl 链接器,**不依赖系统库版本**
- 因此即使系统是 OpenWrt 22.03musl 1.2.3)也能正常运行 Alpine 3.21 编译的 Node.js
## 📦 安装

View File

@@ -1 +1 @@
1.0.2
1.0.3

View File

@@ -487,6 +487,72 @@ do_upgrade() {
fi
}
# ── 恢复出厂设置 (非交互式) ──
do_factory_reset() {
local config_dir="${OC_DATA}/.openclaw"
local config_file="${config_dir}/openclaw.json"
local auth_file="${config_dir}/agents/main/agent/auth-profiles.json"
log_info "恢复出厂设置..."
# 1. 停止 Gateway
log_info "停止 Gateway..."
/etc/init.d/openclaw stop >/dev/null 2>&1 || true
sleep 2
# 2. 备份当前配置
if [ -f "$config_file" ]; then
local backup_dir="${config_dir}/backups"
local backup_ts=$(date +%Y%m%d_%H%M%S)
mkdir -p "$backup_dir"
cp "$config_file" "${backup_dir}/openclaw_${backup_ts}.json"
log_info "备份已保存: backups/openclaw_${backup_ts}.json"
fi
# 3. 重置配置
rm -f "$config_file" "${config_file}.bak" 2>/dev/null || true
echo '{}' > "$config_file"
chown openclaw:openclaw "$config_file" 2>/dev/null || true
# 4. 重置认证信息
if [ -f "$auth_file" ]; then
echo '{"version":1,"profiles":{},"usageStats":{}}' > "$auth_file"
chown openclaw:openclaw "$auth_file" 2>/dev/null || true
fi
# 5. 重新初始化
if [ -x "$NODE_BIN" ]; then
local oc_entry
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
fi
fi
# 6. 应用 OpenWrt 适配配置
if [ -x "$NODE_BIN" ] && [ -f "$config_file" ]; then
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
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
chown openclaw:openclaw "$config_file" 2>/dev/null || true
# 同步 token 到 UCI
. /lib/functions.sh 2>/dev/null || true
uci set openclaw.main.token="$new_token" 2>/dev/null || true
uci commit openclaw 2>/dev/null || true
log_info "新认证令牌: $new_token"
fi
# 7. 重启服务
/etc/init.d/openclaw start >/dev/null 2>&1 &
log_info "出厂设置已恢复Gateway 重启中..."
}
# ── 主入口 ──
case "${1:-}" in
setup)
@@ -501,13 +567,17 @@ case "${1:-}" in
node)
download_node "$NODE_VERSION"
;;
factory-reset)
do_factory_reset
;;
*)
echo "用法: openclaw-env {setup|check|upgrade|node}"
echo "用法: openclaw-env {setup|check|upgrade|node|factory-reset}"
echo ""
echo " setup — 完整安装 (下载 Node.js + pnpm + OpenClaw)"
echo " check — 检查环境状态"
echo " upgrade — 升级 OpenClaw 到最新版"
echo " node — 仅下载/更新 Node.js"
echo " factory-reset — 恢复出厂设置 (清除所有配置)"
exit 1
;;
esac

View File

@@ -124,6 +124,86 @@ enable_auth_plugins() {
" 2>/dev/null
}
# ── 模型认证: 将 API Key 写入 auth-profiles.json (而非 openclaw.json) ──
# 用法: auth_set_apikey <provider> <api_key> [profile_id]
# 例: auth_set_apikey openai sk-xxx
# auth_set_apikey anthropic sk-ant-xxx
# auth_set_apikey openai-compatible sk-xxx custom:manual
auth_set_apikey() {
local provider="$1" api_key="$2" profile_id="${3:-${1}:manual}"
local auth_dir="${OC_STATE_DIR}/agents/main/agent"
local auth_file="${auth_dir}/auth-profiles.json"
mkdir -p "$auth_dir"
_AP_PROVIDER="$provider" _AP_KEY="$api_key" _AP_PROFILE="$profile_id" "$NODE_BIN" -e "
const fs=require('fs'),f=process.env._AP_FILE||'${auth_file}';
let d={version:1,profiles:{},usageStats:{}};
try{d=JSON.parse(fs.readFileSync(f,'utf8'));}catch(e){}
if(!d.profiles)d.profiles={};
d.profiles[process.env._AP_PROFILE]={
type:'api_key',
provider:process.env._AP_PROVIDER,
token:process.env._AP_KEY
};
fs.writeFileSync(f,JSON.stringify(d,null,2));
" 2>/dev/null
chown openclaw:openclaw "$auth_file" 2>/dev/null || true
}
# ── 注册模型到 agents.defaults.models 并设为默认 ──
# 用法: register_and_set_model <model_id>
# 例: register_and_set_model openai/gpt-5.2
# register_and_set_model anthropic/claude-sonnet-4
# 注意: 模型 ID 可能含 "." (如 gpt-5.2),不能用 json_set (以 . 分割路径)
register_and_set_model() {
local model_id="$1"
_RSM_MID="$model_id" "$NODE_BIN" -e "
const fs=require('fs');
let d={};
try{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}
if(!d.agents)d.agents={};
if(!d.agents.defaults)d.agents.defaults={};
if(!d.agents.defaults.models)d.agents.defaults.models={};
if(!d.agents.defaults.model)d.agents.defaults.model={};
const mid=process.env._RSM_MID;
d.agents.defaults.models[mid]={};
d.agents.defaults.model.primary=mid;
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
" 2>/dev/null
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
}
# ── 注册自定义提供商 (需要 baseUrl 的 OpenAI 兼容提供商) ──
# 用法: register_custom_provider <provider_name> <base_url> <api_key> <model_id> [model_display_name]
# 例: register_custom_provider dashscope https://dashscope.aliyuncs.com/compatible-mode/v1 sk-xxx qwen-max "Qwen Max"
register_custom_provider() {
local provider_name="$1" base_url="$2" api_key="$3" model_id="$4" model_display="${5:-$4}"
_RCP_PROV="$provider_name" _RCP_URL="$base_url" _RCP_KEY="$api_key" _RCP_MID="$model_id" _RCP_MNAME="$model_display" "$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';
const prov=process.env._RCP_PROV;
d.models.providers[prov]={
baseUrl:process.env._RCP_URL,
apiKey:process.env._RCP_KEY,
api:'openai-responses',
models:[{
id:process.env._RCP_MID,
name:process.env._RCP_MNAME,
reasoning:false,
input:['text','image'],
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
}
# ── 辅助函数 ──
# 清理输入: 去除 ANSI 转义序列、不可见字符,只保留 ASCII 可打印字符
@@ -312,7 +392,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 OpenAI API Key (sk-...)" "" api_key
if [ -n "$api_key" ]; then
json_set models.openai.apiKey "$api_key"
auth_set_apikey openai "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} gpt-5.2 — 最强编程与代理旗舰 (推荐)"
@@ -334,8 +414,8 @@ configure_model() {
g) prompt_with_default "请输入模型名称" "gpt-5.2" model_name ;;
*) model_name="gpt-5.2" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ OpenAI 已配置,活跃模型: ${model_name}${NC}"
register_and_set_model "openai/${model_name}"
echo -e " ${GREEN}✅ OpenAI 已配置,活跃模型: openai/${model_name}${NC}"
fi
;;
3)
@@ -345,7 +425,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 Anthropic API Key (sk-ant-...)" "" api_key
if [ -n "$api_key" ]; then
json_set models.anthropic.apiKey "$api_key"
auth_set_apikey anthropic "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} claude-sonnet-4-20250514 — Claude Sonnet 4 (推荐)"
@@ -361,8 +441,8 @@ configure_model() {
d) prompt_with_default "请输入模型名称" "claude-sonnet-4-20250514" model_name ;;
*) model_name="claude-sonnet-4-20250514" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ Anthropic 已配置,活跃模型: ${model_name}${NC}"
register_and_set_model "anthropic/${model_name}"
echo -e " ${GREEN}✅ Anthropic 已配置,活跃模型: anthropic/${model_name}${NC}"
fi
;;
4)
@@ -372,7 +452,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 Google AI API Key" "" api_key
if [ -n "$api_key" ]; then
json_set models.google.apiKey "$api_key"
auth_set_apikey google "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} gemini-2.5-pro — 旗舰推理 (推荐)"
@@ -390,8 +470,8 @@ configure_model() {
e) prompt_with_default "请输入模型名称" "gemini-2.5-pro" model_name ;;
*) model_name="gemini-2.5-pro" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ Google Gemini 已配置,活跃模型: ${model_name}${NC}"
register_and_set_model "google/${model_name}"
echo -e " ${GREEN}✅ Google Gemini 已配置,活跃模型: google/${model_name}${NC}"
fi
;;
5)
@@ -402,7 +482,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 OpenRouter API Key" "" api_key
if [ -n "$api_key" ]; then
json_set models.openrouter.apiKey "$api_key"
auth_set_apikey openrouter "$api_key"
echo ""
echo -e " ${CYAN}常用模型 (格式: provider/model):${NC}"
echo -e " ${CYAN}a)${NC} anthropic/claude-sonnet-4 — Claude Sonnet 4 (推荐)"
@@ -424,8 +504,8 @@ configure_model() {
g) prompt_with_default "请输入模型名称" "anthropic/claude-sonnet-4" model_name ;;
*) model_name="anthropic/claude-sonnet-4" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ OpenRouter 已配置,活跃模型: ${model_name}${NC}"
register_and_set_model "openrouter/${model_name}"
echo -e " ${GREEN}✅ OpenRouter 已配置,活跃模型: openrouter/${model_name}${NC}"
fi
;;
6)
@@ -435,7 +515,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 DeepSeek API Key" "" api_key
if [ -n "$api_key" ]; then
json_set models.deepseek.apiKey "$api_key"
auth_set_apikey deepseek "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} deepseek-chat — DeepSeek-V3 (通用对话)"
@@ -449,8 +529,8 @@ configure_model() {
c) prompt_with_default "请输入模型名称" "deepseek-chat" model_name ;;
*) model_name="deepseek-chat" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ DeepSeek 已配置,活跃模型: ${model_name}${NC}"
register_and_set_model "deepseek/${model_name}"
echo -e " ${GREEN}✅ DeepSeek 已配置,活跃模型: deepseek/${model_name}${NC}"
fi
;;
7)
@@ -480,7 +560,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 GitHub Token (ghp_...)" "" api_key
if [ -n "$api_key" ]; then
json_set models.github-copilot.apiKey "$api_key"
auth_set_apikey github-copilot "$api_key" "github-copilot:github"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} github-copilot/claude-sonnet-4 — Claude Sonnet 4 (推荐)"
@@ -498,7 +578,7 @@ configure_model() {
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"
register_and_set_model "$model_name"
echo -e " ${GREEN}✅ GitHub Copilot 已配置,活跃模型: ${model_name}${NC}"
fi
;;
@@ -530,8 +610,6 @@ configure_model() {
echo ""
prompt_with_default "请输入阿里云 API Key (sk-...)" "" api_key
if [ -n "$api_key" ]; then
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 — 通义千问旗舰 (推荐)"
@@ -549,8 +627,10 @@ configure_model() {
e) prompt_with_default "请输入模型名称" "qwen-max" model_name ;;
*) model_name="qwen-max" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ 通义千问已配置,活跃模型: ${model_name}${NC}"
auth_set_apikey dashscope "$api_key"
register_custom_provider dashscope "https://dashscope.aliyuncs.com/compatible-mode/v1" "$api_key" "$model_name" "$model_name"
register_and_set_model "dashscope/${model_name}"
echo -e " ${GREEN}✅ 通义千问已配置,活跃模型: dashscope/${model_name}${NC}"
fi
;;
esac
@@ -562,7 +642,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 xAI API Key" "" api_key
if [ -n "$api_key" ]; then
json_set models.xai.apiKey "$api_key"
auth_set_apikey xai "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} grok-3 — Grok 3 旗舰"
@@ -576,8 +656,8 @@ configure_model() {
c) prompt_with_default "请输入模型名称" "grok-3" model_name ;;
*) model_name="grok-3" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ xAI Grok 已配置,活跃模型: ${model_name}${NC}"
register_and_set_model "xai/${model_name}"
echo -e " ${GREEN}✅ xAI Grok 已配置,活跃模型: xai/${model_name}${NC}"
fi
;;
10)
@@ -588,7 +668,7 @@ configure_model() {
echo ""
prompt_with_default "请输入 Groq API Key" "" api_key
if [ -n "$api_key" ]; then
json_set models.groq.apiKey "$api_key"
auth_set_apikey groq "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} llama-4-maverick-17b-128e — Llama 4 Maverick (推荐)"
@@ -604,8 +684,8 @@ configure_model() {
d) prompt_with_default "请输入模型名称" "llama-4-maverick-17b-128e" model_name ;;
*) model_name="llama-4-maverick-17b-128e" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ Groq 已配置,活跃模型: ${model_name}${NC}"
register_and_set_model "groq/${model_name}"
echo -e " ${GREEN}✅ Groq 已配置,活跃模型: groq/${model_name}${NC}"
fi
;;
11)
@@ -616,8 +696,6 @@ configure_model() {
echo ""
prompt_with_default "请输入 SiliconFlow API Key" "" api_key
if [ -n "$api_key" ]; then
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 (推荐)"
@@ -633,8 +711,10 @@ configure_model() {
d) prompt_with_default "请输入模型名称" "deepseek-ai/DeepSeek-V3" model_name ;;
*) model_name="deepseek-ai/DeepSeek-V3" ;;
esac
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ SiliconFlow 已配置,活跃模型: ${model_name}${NC}"
auth_set_apikey siliconflow "$api_key"
register_custom_provider siliconflow "https://api.siliconflow.cn/v1" "$api_key" "$model_name" "$model_name"
register_and_set_model "siliconflow/${model_name}"
echo -e " ${GREEN}✅ SiliconFlow 已配置,活跃模型: siliconflow/${model_name}${NC}"
fi
;;
12)
@@ -646,10 +726,10 @@ configure_model() {
prompt_with_default "API Key" "" api_key
prompt_with_default "模型名称" "" model_name
if [ -n "$base_url" ] && [ -n "$api_key" ] && [ -n "$model_name" ]; then
json_set models.custom.apiKey "$api_key"
json_set models.custom.baseUrl "$base_url"
json_set agents.defaults.model.primary "$model_name"
echo -e " ${GREEN}✅ 自定义模型已配置${NC}"
auth_set_apikey openai-compatible "$api_key" "openai-compatible:manual"
register_custom_provider openai-compatible "$base_url" "$api_key" "$model_name" "$model_name"
register_and_set_model "openai-compatible/${model_name}"
echo -e " ${GREEN}✅ 自定义模型已配置,活跃模型: openai-compatible/${model_name}${NC}"
fi
;;
0) return ;;
@@ -704,7 +784,7 @@ set_active_model() {
elif [ "$model_sel" = "m" ]; then
prompt_with_default "请输入模型 ID (如 openai/gpt-4o)" "${current_model:-}" manual_model
if [ -n "$manual_model" ]; then
json_set agents.defaults.model.primary "$manual_model"
register_and_set_model "$manual_model"
echo -e " ${GREEN}✅ 活跃模型已设为: ${manual_model}${NC}"
ask_restart
fi
@@ -715,7 +795,7 @@ set_active_model() {
console.log(m?m.key:'');
" "$models_json" "$model_sel" 2>/dev/null)
if [ -n "$selected" ]; then
json_set agents.defaults.model.primary "$selected"
register_and_set_model "$selected"
echo -e " ${GREEN}✅ 活跃模型已切换为: ${selected}${NC}"
ask_restart
else
@@ -728,7 +808,7 @@ set_active_model() {
echo ""
prompt_with_default "直接输入模型 ID 设置? (留空返回)" "" manual_model
if [ -n "$manual_model" ]; then
json_set agents.defaults.model.primary "$manual_model"
register_and_set_model "$manual_model"
echo -e " ${GREEN}✅ 活跃模型已设为: ${manual_model}${NC}"
ask_restart
fi
@@ -1028,6 +1108,37 @@ health_check() {
local gw_port=$(json_get gateway.port)
gw_port=${gw_port:-18789}
# ── 自动修复: 移除旧版错误写入的顶层 models.xxx 无效键 ──
if [ -f "$CONFIG_FILE" ]; then
local has_bad_models=$("$NODE_BIN" -e "
const d=JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8'));
const m=d.models;
if(m&&typeof m==='object'){
const bad=Object.keys(m).filter(k=>['openai','anthropic','google','openrouter','deepseek','github-copilot','dashscope','xai','groq','siliconflow','custom'].includes(k));
if(bad.length>0){console.log(bad.join(','));}
}
" 2>/dev/null)
if [ -n "$has_bad_models" ]; then
echo -e " ${YELLOW}⚠️ 检测到旧版配置错误: 顶层 models 包含无效键 (${has_bad_models})${NC}"
echo -e " ${CYAN}正在自动修复...${NC}"
"$NODE_BIN" -e "
const fs=require('fs');
const d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));
const bad=['openai','anthropic','google','openrouter','deepseek','github-copilot','dashscope','xai','groq','siliconflow','custom'];
if(d.models&&typeof d.models==='object'){
bad.forEach(k=>delete d.models[k]);
if(Object.keys(d.models).length===0||(Object.keys(d.models).length===1&&d.models.providers)){}
else if(Object.keys(d.models).filter(k=>k!=='mode'&&k!=='providers').length===0){}
if(!d.models.providers&&Object.keys(d.models).every(k=>bad.includes(k)||k==='mode'))delete d.models;
}
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
" 2>/dev/null
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
echo -e " ${GREEN}✅ 已移除无效的 models 配置键${NC}"
echo ""
fi
fi
if check_port_listening "$gw_port"; then
echo -e " ${GREEN}✅ Gateway 端口 ${gw_port} 正在监听${NC}"
else
@@ -1105,6 +1216,12 @@ reset_to_defaults() {
oc_cmd config unset models >/dev/null 2>&1 || true
oc_cmd config unset agents.defaults.model >/dev/null 2>&1 || true
oc_cmd config unset agents.defaults.models >/dev/null 2>&1 || true
# 同时清除 auth-profiles.json 中的认证信息
local auth_file="${OC_STATE_DIR}/agents/main/agent/auth-profiles.json"
if [ -f "$auth_file" ]; then
echo '{"version":1,"profiles":{},"usageStats":{}}' > "$auth_file"
chown openclaw:openclaw "$auth_file" 2>/dev/null || true
fi
echo -e " ${GREEN}✅ 模型配置已清除${NC}"
echo -e " ${YELLOW}请通过菜单 [2] 重新配置 AI 模型${NC}"
ask_restart