mirror of
https://github.com/hotwa/luci-app-openclaw.git
synced 2026-03-30 20:25:44 +00:00
release: v1.0.3 — P0 配置崩溃修复、Ollama 支持、检测升级
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -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
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -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 位 ARM(armv7l/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.03(musl 1.2.3)也能正常运行 Alpine 3.21 编译的 Node.js
|
||||
|
||||
## 📦 安装
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user