Files
luci-app-openclaw/root/usr/share/openclaw/oc-config.sh

2138 lines
90 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# ============================================================================
# OpenClaw 配置管理工具 — OpenWrt 适配版
# 基于原始 oc-config.sh 移植,适配 ash/busybox 环境
# ============================================================================
# ── 颜色 ──
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
# ── 端口检查兼容函数 (ss 或 netstat) ──
# check_port_listening <port> — 检查端口是否在监听,返回 0/1
check_port_listening() {
local p="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | grep -q ":${p} "
else
netstat -tln 2>/dev/null | grep -q ":${p} "
fi
}
# get_pid_by_port <port> — 获取监听指定端口的进程 PID
get_pid_by_port() {
local p="$1"
if command -v ss >/dev/null 2>&1; then
ss -tlnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1
else
netstat -tlnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1
fi
}
# ── 路径 (OpenWrt 适配) ──
NODE_BASE="${NODE_BASE:-/opt/openclaw/node}"
OC_GLOBAL="${OC_GLOBAL:-/opt/openclaw/global}"
OC_DATA="${OC_DATA:-/opt/openclaw/data}"
NODE_BIN="${NODE_BASE}/bin/node"
OC_STATE_DIR="${OC_DATA}/.openclaw"
CONFIG_FILE="${OC_STATE_DIR}/openclaw.json"
export HOME="$OC_DATA"
export OPENCLAW_HOME="$OC_DATA"
export OPENCLAW_STATE_DIR="$OC_STATE_DIR"
export OPENCLAW_CONFIG_PATH="$CONFIG_FILE"
export NODE_ICU_DATA="${NODE_BASE}/share/icu"
export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# ── 查找 openclaw 入口 ──
OC_PKG_DIR=""
for d in "${OC_GLOBAL}/lib/node_modules/openclaw" "${OC_GLOBAL}/node_modules/openclaw" "${NODE_BASE}/lib/node_modules/openclaw"; do
if [ -d "$d" ]; then
OC_PKG_DIR="$d"
break
fi
done
OC_ENTRY=""
if [ -n "$OC_PKG_DIR" ]; then
if [ -f "${OC_PKG_DIR}/openclaw.mjs" ]; then
OC_ENTRY="${OC_PKG_DIR}/openclaw.mjs"
elif [ -f "${OC_PKG_DIR}/dist/cli.js" ]; then
OC_ENTRY="${OC_PKG_DIR}/dist/cli.js"
fi
fi
oc_cmd() {
if [ -n "$OC_ENTRY" ] && [ -x "$NODE_BIN" ]; then
"$NODE_BIN" "$OC_ENTRY" "$@" 2>&1
local rc=$?
# 修复权限: oc_cmd 以 root 运行但配置文件应属于 openclaw 用户
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
chown openclaw:openclaw "${CONFIG_FILE}.bak" 2>/dev/null || true
return $rc
else
echo "ERROR: OpenClaw 未安装或 Node.js 不可用"
return 1
fi
}
# ── JSON 读写 (使用 Node.js) ──
json_get() {
if [ ! -f "$CONFIG_FILE" ]; then echo ""; return; fi
_JS_KEY="$1" "$NODE_BIN" -e "
const fs=require('fs');
try{
const d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));
const ks=process.env._JS_KEY.split('.');let v=d;
for(const k of ks){v=v[k];if(v===undefined){console.log('');process.exit(0);}}
if(typeof v==='object')console.log(JSON.stringify(v));else console.log(v);
}catch(e){console.log('');}
" 2>/dev/null
}
json_set() {
local key="$1" value="$2"
if [ ! -f "$CONFIG_FILE" ]; then
mkdir -p "$(dirname "$CONFIG_FILE")"
chown -R openclaw:openclaw "$OC_STATE_DIR" 2>/dev/null || true
echo '{}' > "$CONFIG_FILE"
fi
_JS_KEY="$key" _JS_VAL="$value" "$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
}
# ── 启用 auth 插件 ──
enable_auth_plugins() {
[ ! -f "$CONFIG_FILE" ] && return
"$NODE_BIN" -e "
const fs=require('fs');
try{
const d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));
if(!d.plugins)d.plugins={};if(!d.plugins.entries)d.plugins.entries={};
const e=d.plugins.entries;
['qwen-portal-auth','copilot-proxy','google-gemini-cli-auth','minimax-portal-auth'].forEach(p=>{
if(!e[p])e[p]={};e[p].enabled=true;
});
delete e['google-antigravity-auth'];
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
}catch(e){}
" 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"
chown -R openclaw:openclaw "${OC_STATE_DIR}/agents" 2>/dev/null || true
_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,
key:process.env._AP_KEY
};
fs.writeFileSync(f,JSON.stringify(d,null,2));
" 2>/dev/null
chown openclaw:openclaw "$auth_file" 2>/dev/null || true
}
# ── GitHub Copilot Token 写入 auth-profiles.json (type:token) ──
# GitHub Copilot 使用 token 类型而非 api_keyOpenClaw 会自动兑换 Copilot session token
# 用法: auth_set_copilot_token <github_token>
auth_set_copilot_token() {
local github_token="$1"
local auth_dir="${OC_STATE_DIR}/agents/main/agent"
local auth_file="${auth_dir}/auth-profiles.json"
mkdir -p "$auth_dir"
chown -R openclaw:openclaw "${OC_STATE_DIR}/agents" 2>/dev/null || true
_AP_TOKEN="$github_token" "$NODE_BIN" -e "
const fs=require('fs'),f='${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['github-copilot:github']={
type:'token',
provider:'github-copilot',
token:process.env._AP_TOKEN
};
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] [context_window] [max_tokens]
# 例: register_custom_provider dashscope https://dashscope.aliyuncs.com/compatible-mode/v1 sk-xxx qwen-max "Qwen Max"
# 例: register_custom_provider bailian https://coding.dashscope.aliyuncs.com/v1 sk-sp-xxx qwen3.5-plus "qwen3.5-plus" 1000000 65536
register_custom_provider() {
local provider_name="$1" base_url="$2" api_key="$3" model_id="$4" model_display="${5:-$4}"
local ctx_window="${6:-128000}" max_tok="${7:-32000}"
_RCP_PROV="$provider_name" _RCP_URL="$base_url" _RCP_KEY="$api_key" _RCP_MID="$model_id" _RCP_MNAME="$model_display" _RCP_CTX="$ctx_window" _RCP_MAXTOK="$max_tok" "$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-completions',
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:parseInt(process.env._RCP_CTX)||128000,
maxTokens:parseInt(process.env._RCP_MAXTOK)||32000
}]
};
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
" 2>/dev/null
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
}
# ── 注册 Coding Plan 提供商 (多模型批量注册) ──
# 用法: register_codingplan_provider <api_key>
# 按阿里云官方文档注册 bailian 提供商,包含所有 Coding Plan 套餐支持的模型
register_codingplan_provider() {
local api_key="$1"
_RCP_KEY="$api_key" "$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['bailian']={
baseUrl:'https://coding.dashscope.aliyuncs.com/v1',
apiKey:process.env._RCP_KEY,
api:'openai-completions',
models:[
{id:'qwen3.5-plus',name:'qwen3.5-plus',reasoning:false,input:['text','image'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:1000000,maxTokens:65536},
{id:'qwen3-coder-plus',name:'qwen3-coder-plus',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:1000000,maxTokens:65536},
{id:'qwen3-coder-next',name:'qwen3-coder-next',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:262144,maxTokens:65536},
{id:'qwen3-max-2026-01-23',name:'qwen3-max-2026-01-23',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:262144,maxTokens:65536},
{id:'MiniMax-M2.5',name:'MiniMax-M2.5',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:204800,maxTokens:131072},
{id:'glm-5',name:'glm-5',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:202752,maxTokens:16384},
{id:'glm-4.7',name:'glm-4.7',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:202752,maxTokens:16384},
{id:'kimi-k2.5',name:'kimi-k2.5',reasoning:false,input:['text','image'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:262144,maxTokens:32768}
]
};
if(!d.agents)d.agents={};
if(!d.agents.defaults)d.agents.defaults={};
if(!d.agents.defaults.models)d.agents.defaults.models={};
['qwen3.5-plus','qwen3-coder-plus','qwen3-coder-next','qwen3-max-2026-01-23','MiniMax-M2.5','glm-5','glm-4.7','kimi-k2.5'].forEach(m=>{
d.agents.defaults.models['bailian/'+m]={};
});
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
" 2>/dev/null
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
}
# ── 注册腾讯云 Coding Plan 提供商 (多模型批量注册) ──
# 用法: register_lkeap_codingplan_provider <api_key>
# 按腾讯云官方文档 (https://cloud.tencent.com/document/product/1772/128949) 注册
# provider name: lkeap, Base URL: https://api.lkeap.cloud.tencent.com/coding/v3
register_lkeap_codingplan_provider() {
local api_key="$1"
_RCP_KEY="$api_key" "$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['lkeap']={
baseUrl:'https://api.lkeap.cloud.tencent.com/coding/v3',
apiKey:process.env._RCP_KEY,
api:'openai-completions',
models:[
{id:'tc-code-latest',name:'Auto (智能匹配最优模型)',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:128000,maxTokens:8192},
{id:'hunyuan-2.0-instruct',name:'Tencent HY 2.0 Instruct',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:128000,maxTokens:16000},
{id:'hunyuan-2.0-thinking',name:'Tencent HY 2.0 Think',reasoning:true,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:128000,maxTokens:64000},
{id:'hunyuan-t1',name:'Hunyuan-T1',reasoning:true,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:32000,maxTokens:64000},
{id:'hunyuan-turbos',name:'Hunyuan-TurboS',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:32000,maxTokens:16000},
{id:'minimax-m2.5',name:'MiniMax-M2.5',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:204800,maxTokens:131072},
{id:'kimi-k2.5',name:'Kimi-K2.5',reasoning:false,input:['text','image'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:262144,maxTokens:32768},
{id:'glm-5',name:'GLM-5',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:202752,maxTokens:8192}
]
};
if(!d.agents)d.agents={};
if(!d.agents.defaults)d.agents.defaults={};
if(!d.agents.defaults.models)d.agents.defaults.models={};
['tc-code-latest','hunyuan-2.0-instruct','hunyuan-2.0-thinking','hunyuan-t1','hunyuan-turbos','minimax-m2.5','kimi-k2.5','glm-5'].forEach(m=>{
d.agents.defaults.models['lkeap/'+m]={};
});
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
" 2>/dev/null
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
}
# ── 辅助函数 ──
# 清理输入: 去除 ANSI 转义序列、不可见字符,只保留 ASCII 可打印字符
sanitize_input() {
# 1) tr -cd ' -~' : 只保留 ASCII 0x20-0x7E (去除 ESC/控制字符/Unicode 不可见字符)
# 2) sed : 去除 ESC 被剥离后残余的 CSI 序列 (如 bracketed paste 的 [200~ [201~)
# 3) sed : 去除首尾空白
printf '%s' "$1" | tr -cd ' -~' | sed 's/\[[0-9;]*[a-zA-Z~]//g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
prompt_with_default() {
local prompt="$1" default="$2" varname="$3"
if [ -n "$default" ]; then
printf " ${CYAN}${prompt} [${default}]:${NC} " >&2
else
printf " ${CYAN}${prompt}:${NC} " >&2
fi
read input
input=$(sanitize_input "$input")
local _result="${input:-$default}"
export "__prompt_result__=$_result"
eval "$varname=\"\$__prompt_result__\""
unset __prompt_result__
}
confirm_yes() {
local ans="$1"
case "$ans" in y|Y|yes|YES|Yes) return 0 ;; *) return 1 ;; esac
}
restart_gateway() {
echo ""
echo -e " ${YELLOW}正在重启 Gateway...${NC}"
# 修复数据目录权限 (root 用户操作可能改变了文件属主)
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
local port
port=$(json_get gateway.port)
port=${port:-18789}
# ── kill gateway 进程,让 procd respawn ──
/etc/init.d/openclaw restart_gateway >/dev/null 2>&1
# ── 等待端口恢复 (最多 30 秒,含端口释放 + Node.js 冷启动) ──
echo -e " ${YELLOW}⏳ Gateway 启动中,请稍候 (约 15-30 秒)...${NC}"
local waited=0
while [ $waited -lt 30 ]; do
sleep 3
waited=$((waited + 3))
if check_port_listening "$port"; then
echo -e " ${GREEN}✅ Gateway 已重启成功 (${waited}秒)${NC}"
return 0
fi
done
# 30 秒内没就绪,提示用户但不继续阻塞
echo -e " ${YELLOW}⏳ Gateway 仍在启动中,请稍后确认${NC}"
echo -e " ${CYAN} 查看日志: logread -e openclaw${NC}"
echo ""
}
ask_restart() {
prompt_with_default "是否立即重启 Gateway? (y/n)" "y" do_restart
confirm_yes "$do_restart" && restart_gateway
}
# ── 查看当前配置 ──
show_current_config() {
echo ""
echo -e "${GREEN}┌──────────────────────────────────────────────────────────┐${NC}"
echo -e "${GREEN}${NC} 📋 ${BOLD}当前配置概览${NC}"
echo -e "${GREEN}├──────────────────────────────────────────────────────────┤${NC}"
local port=$(json_get gateway.port)
local bind=$(json_get gateway.bind)
local mode=$(json_get gateway.mode)
echo -e "${GREEN}${NC} 网关端口 ............ ${CYAN}${port:-18789}${NC}"
echo -e "${GREEN}${NC} 绑定模式 ............ ${CYAN}${bind:-lan}${NC}"
echo -e "${GREEN}${NC} 运行模式 ............ ${CYAN}${mode:-local}${NC}"
local model=$(json_get agents.defaults.model.primary)
if [ -n "$model" ]; then
echo -e "${GREEN}${NC} 活跃模型 ............ ${CYAN}${model}${NC}"
else
echo -e "${GREEN}${NC} 活跃模型 ............ ${YELLOW}未配置${NC}"
fi
echo -e "${GREEN}├──────────────────────────────────────────────────────────┤${NC}"
echo -e "${GREEN}${NC} ${BOLD}渠道配置状态${NC}"
local tg_token=$(json_get channels.telegram.botToken)
local dc_token=$(json_get channels.discord.botToken)
local fs_appid=$(json_get channels.feishu.appId)
local sk_token=$(json_get channels.slack.botToken)
local qq_appid=$(json_get channels.qqbot.appId)
if [ -n "$qq_appid" ]; then
local qq_short=$(echo "$qq_appid" | cut -c1-8)
echo -e "${GREEN}${NC} QQ (qqbot) ......... ${GREEN}✅ 已配置${NC} (AppID: ${qq_short}...)"
else
echo -e "${GREEN}${NC} QQ (qqbot) ......... ${YELLOW}❌ 未配置${NC}"
fi
if [ -n "$tg_token" ]; then
local tg_short=$(echo "$tg_token" | cut -c1-12)
echo -e "${GREEN}${NC} Telegram ............ ${GREEN}✅ 已配置${NC} (${tg_short}...)"
else
echo -e "${GREEN}${NC} Telegram ............ ${YELLOW}❌ 未配置${NC}"
fi
if [ -n "$dc_token" ]; then
echo -e "${GREEN}${NC} Discord ............. ${GREEN}✅ 已配置${NC}"
else
echo -e "${GREEN}${NC} Discord ............. ${YELLOW}❌ 未配置${NC}"
fi
if [ -n "$fs_appid" ]; then
local fs_short=$(echo "$fs_appid" | cut -c1-6)
echo -e "${GREEN}${NC} 飞书 ................ ${GREEN}✅ 已配置${NC} (AppID: ${fs_short}...)"
else
echo -e "${GREEN}${NC} 飞书 ................ ${YELLOW}❌ 未配置${NC}"
fi
if [ -n "$sk_token" ]; then
echo -e "${GREEN}${NC} Slack ............... ${GREEN}✅ 已配置${NC}"
else
echo -e "${GREEN}${NC} Slack ............... ${YELLOW}❌ 未配置${NC}"
fi
echo -e "${GREEN}└──────────────────────────────────────────────────────────┘${NC}"
echo ""
echo -e " ${BOLD}系统信息:${NC}"
echo -e " Node.js: $("$NODE_BIN" -v 2>/dev/null || echo '未安装')"
if [ -n "$OC_ENTRY" ]; then
echo -e " OpenClaw: $(oc_cmd --version 2>/dev/null || echo '未知')"
else
echo -e " OpenClaw: 未安装"
fi
echo -e " 架构: $(uname -m)"
local mem_total=$(awk '/MemTotal/{printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null || echo "?")
echo -e " 内存: ${mem_total} MB"
}
# ══════════════════════════════════════════════════════════════
# 配置 AI 模型
# ══════════════════════════════════════════════════════════════
configure_model() {
echo ""
echo -e " ${BOLD}🤖 配置 AI 模型提供商${NC}"
echo ""
echo -e " ${GREEN}${BOLD}--- 推荐 ---${NC}"
echo -e " ${CYAN}1)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}"
echo ""
echo -e " ${BOLD}--- 快速配置 ---${NC}"
echo -e " ${CYAN}2)${NC} OpenAI (GPT-5.2, GPT-5 mini, GPT-4.1)"
echo -e " ${CYAN}3)${NC} Anthropic (Claude Sonnet 4, Opus 4, Haiku)"
echo -e " ${CYAN}4)${NC} Google Gemini (Gemini 2.5 Pro/Flash, Gemini 3)"
echo -e " ${CYAN}5)${NC} OpenRouter (聚合多家模型)"
echo -e " ${CYAN}6)${NC} DeepSeek (DeepSeek-V3/R1)"
echo -e " ${CYAN}7)${NC} GitHub Copilot (需要 Copilot 订阅)"
echo -e " ${CYAN}8)${NC} 阿里云通义千问 Qwen (Portal/API/Coding Plan)"
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} Ollama (本地模型,无需 API Key)"
echo -e " ${CYAN}13)${NC} 腾讯云 Coding Plan (HY T1/TurboS/GLM-5/Kimi)"
echo -e " ${CYAN}14)${NC} 自定义 OpenAI 兼容 API"
echo -e " ${CYAN}0)${NC} 返回"
echo ""
prompt_with_default "请选择" "1" choice
case "$choice" in
1)
echo ""
echo -e " ${CYAN}启动官方完整模型配置向导...${NC}"
echo -e " ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}"
echo ""
echo -e " ${CYAN}预启用模型认证插件...${NC}"
enable_auth_plugins
echo ""
(oc_cmd configure --section model) || echo -e " ${YELLOW}配置向导已退出${NC}"
echo ""
ask_restart
;;
2)
echo ""
echo -e " ${BOLD}OpenAI 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://platform.openai.com/api-keys${NC}"
echo ""
prompt_with_default "请输入 OpenAI API Key (sk-...)" "" api_key
if [ -n "$api_key" ]; then
auth_set_apikey openai "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} gpt-5.2 — 最强编程与代理旗舰 (推荐)"
echo -e " ${CYAN}b)${NC} gpt-5-mini — 高性价比推理"
echo -e " ${CYAN}c)${NC} gpt-5-nano — 极速低成本"
echo -e " ${CYAN}d)${NC} gpt-4.1 — 最强非推理模型"
echo -e " ${CYAN}e)${NC} o3 — 推理模型"
echo -e " ${CYAN}f)${NC} o4-mini — 推理轻量"
echo -e " ${CYAN}g)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="gpt-5.2" ;;
b) model_name="gpt-5-mini" ;;
c) model_name="gpt-5-nano" ;;
d) model_name="gpt-4.1" ;;
e) model_name="o3" ;;
f) model_name="o4-mini" ;;
g) prompt_with_default "请输入模型名称" "gpt-5.2" model_name ;;
*) model_name="gpt-5.2" ;;
esac
register_and_set_model "openai/${model_name}"
echo -e " ${GREEN}✅ OpenAI 已配置,活跃模型: openai/${model_name}${NC}"
fi
;;
3)
echo ""
echo -e " ${BOLD}Anthropic 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.anthropic.com/settings/keys${NC}"
echo ""
prompt_with_default "请输入 Anthropic API Key (sk-ant-...)" "" api_key
if [ -n "$api_key" ]; then
auth_set_apikey anthropic "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} claude-sonnet-4-20250514 — Claude Sonnet 4 (推荐)"
echo -e " ${CYAN}b)${NC} claude-opus-4-20250514 — Claude Opus 4 顶级推理"
echo -e " ${CYAN}c)${NC} claude-haiku-4-5 — Claude Haiku 4.5 轻量快速"
echo -e " ${CYAN}d)${NC} claude-sonnet-4.5 — Claude Sonnet 4.5"
echo -e " ${CYAN}e)${NC} claude-sonnet-4.6 — Claude Sonnet 4.6"
echo -e " ${CYAN}f)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="claude-sonnet-4-20250514" ;;
b) model_name="claude-opus-4-20250514" ;;
c) model_name="claude-haiku-4-5" ;;
d) model_name="claude-sonnet-4-5" ;;
e) model_name="claude-sonnet-4-6" ;;
f) prompt_with_default "请输入模型名称" "claude-sonnet-4-20250514" model_name ;;
*) model_name="claude-sonnet-4-20250514" ;;
esac
register_and_set_model "anthropic/${model_name}"
echo -e " ${GREEN}✅ Anthropic 已配置,活跃模型: anthropic/${model_name}${NC}"
fi
;;
4)
echo ""
echo -e " ${BOLD}Google Gemini 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://aistudio.google.com/apikey${NC}"
echo ""
prompt_with_default "请输入 Google AI API Key" "" api_key
if [ -n "$api_key" ]; then
auth_set_apikey google "$api_key"
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} gemini-2.5-pro — 旗舰推理 (推荐)"
echo -e " ${CYAN}b)${NC} gemini-2.5-flash — 快速均衡"
echo -e " ${CYAN}c)${NC} gemini-2.5-flash-lite — 极速低成本"
echo -e " ${CYAN}d)${NC} gemini-3-flash-preview — Gemini 3 Flash 预览"
echo -e " ${CYAN}e)${NC} gemini-3-pro-preview — Gemini 3 Pro 预览"
echo -e " ${CYAN}f)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="gemini-2.5-pro" ;;
b) model_name="gemini-2.5-flash" ;;
c) model_name="gemini-2.5-flash-lite" ;;
d) model_name="gemini-3-flash-preview" ;;
e) model_name="gemini-3-pro-preview" ;;
f) prompt_with_default "请输入模型名称" "gemini-2.5-pro" model_name ;;
*) model_name="gemini-2.5-pro" ;;
esac
register_and_set_model "google/${model_name}"
echo -e " ${GREEN}✅ Google Gemini 已配置,活跃模型: google/${model_name}${NC}"
fi
;;
5)
echo ""
echo -e " ${BOLD}OpenRouter 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://openrouter.ai/keys${NC}"
echo -e " ${YELLOW}聚合多家模型,一个 Key 可调用所有主流模型${NC}"
echo ""
prompt_with_default "请输入 OpenRouter API Key" "" api_key
if [ -n "$api_key" ]; then
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 (推荐)"
echo -e " ${CYAN}b)${NC} anthropic/claude-opus-4 — Claude Opus 4"
echo -e " ${CYAN}c)${NC} openai/gpt-5.2 — GPT-5.2"
echo -e " ${CYAN}d)${NC} google/gemini-2.5-pro — Gemini 2.5 Pro"
echo -e " ${CYAN}e)${NC} deepseek/deepseek-r1 — DeepSeek R1"
echo -e " ${CYAN}f)${NC} meta-llama/llama-4-maverick — Meta Llama 4"
echo -e " ${CYAN}g)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="anthropic/claude-sonnet-4" ;;
b) model_name="anthropic/claude-opus-4" ;;
c) model_name="openai/gpt-5.2" ;;
d) model_name="google/gemini-2.5-pro" ;;
e) model_name="deepseek/deepseek-r1" ;;
f) model_name="meta-llama/llama-4-maverick" ;;
g) prompt_with_default "请输入模型名称" "anthropic/claude-sonnet-4" model_name ;;
*) model_name="anthropic/claude-sonnet-4" ;;
esac
register_and_set_model "openrouter/${model_name}"
echo -e " ${GREEN}✅ OpenRouter 已配置,活跃模型: openrouter/${model_name}${NC}"
fi
;;
6)
echo ""
echo -e " ${BOLD}DeepSeek 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://platform.deepseek.com/api_keys${NC}"
echo ""
prompt_with_default "请输入 DeepSeek API Key" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} deepseek-chat — DeepSeek-V3 (通用对话)"
echo -e " ${CYAN}b)${NC} deepseek-reasoner — DeepSeek-R1 (深度推理)"
echo -e " ${CYAN}c)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="deepseek-chat" ;;
b) model_name="deepseek-reasoner" ;;
c) prompt_with_default "请输入模型名称" "deepseek-chat" model_name ;;
*) model_name="deepseek-chat" ;;
esac
auth_set_apikey deepseek "$api_key"
register_custom_provider deepseek "https://api.deepseek.com/v1" "$api_key" "$model_name" "$model_name"
register_and_set_model "deepseek/${model_name}"
echo -e " ${GREEN}✅ DeepSeek 已配置,活跃模型: deepseek/${model_name}${NC}"
fi
;;
7)
echo ""
echo -e " ${BOLD}GitHub Copilot 配置${NC}"
echo -e " ${YELLOW}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${NC}"
echo ""
echo -e " ${CYAN}启动 GitHub Copilot OAuth 登录 (Device Flow)...${NC}"
echo -e " ${DIM}请在浏览器中打开显示的 URL输入授权码完成登录${NC}"
echo ""
if oc_cmd models auth login-github-copilot --yes; then
echo ""
echo -e " ${GREEN}✅ GitHub Copilot OAuth 认证成功${NC}"
echo ""
echo -e " ${CYAN}选择默认模型:${NC}"
echo ""
echo -e " ${CYAN}── GPT 系列 ──${NC}"
echo -e " ${CYAN}a)${NC} github-copilot/gpt-4.1 — GPT-4.1 ${GREEN}(推荐)${NC}"
echo -e " ${CYAN}b)${NC} github-copilot/gpt-4o — GPT-4o"
echo -e " ${CYAN}c)${NC} github-copilot/gpt-5 — GPT-5"
echo -e " ${CYAN}d)${NC} github-copilot/gpt-5-mini — GPT-5 mini"
echo -e " ${CYAN}e)${NC} github-copilot/gpt-5.1 — GPT-5.1"
echo -e " ${CYAN}f)${NC} github-copilot/gpt-5.2 — GPT-5.2"
echo -e " ${CYAN}g)${NC} github-copilot/gpt-5.2-codex — GPT-5.2 Codex"
echo ""
echo -e " ${CYAN}── Claude 系列 ──${NC}"
echo -e " ${CYAN}h)${NC} github-copilot/claude-sonnet-4 — Claude Sonnet 4"
echo -e " ${CYAN}i)${NC} github-copilot/claude-sonnet-4.5 — Claude Sonnet 4.5"
echo -e " ${CYAN}j)${NC} github-copilot/claude-sonnet-4.6 — Claude Sonnet 4.6"
echo ""
echo -e " ${CYAN}── Gemini 系列 ──${NC}"
echo -e " ${CYAN}k)${NC} github-copilot/gemini-2.5-pro — Gemini 2.5 Pro"
echo ""
echo -e " ${CYAN}m)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="github-copilot/gpt-4.1" ;;
b) model_name="github-copilot/gpt-4o" ;;
c) model_name="github-copilot/gpt-5" ;;
d) model_name="github-copilot/gpt-5-mini" ;;
e) model_name="github-copilot/gpt-5.1" ;;
f) model_name="github-copilot/gpt-5.2" ;;
g) model_name="github-copilot/gpt-5.2-codex" ;;
h) model_name="github-copilot/claude-sonnet-4" ;;
i) model_name="github-copilot/claude-sonnet-4.5" ;;
j) model_name="github-copilot/claude-sonnet-4.6" ;;
k) model_name="github-copilot/gemini-2.5-pro" ;;
m) prompt_with_default "请输入模型名称" "github-copilot/gpt-4.1" model_name ;;
*) model_name="github-copilot/gpt-4.1" ;;
esac
register_and_set_model "$model_name"
echo -e " ${GREEN}✅ 活跃模型已设置: ${model_name}${NC}"
else
echo -e " ${YELLOW}OAuth 授权已退出或失败${NC}"
fi
;;
8)
echo ""
echo -e " ${BOLD}阿里云通义千问 Qwen 配置${NC}"
echo ""
echo -e " ${CYAN}配置方式:${NC}"
echo -e " ${CYAN}a)${NC} 通过官方向导配置 (Qwen Portal OAuth)"
echo -e " ${CYAN}b)${NC} 百炼按量付费 API Key (sk-xxx, 按 token 计费)"
echo -e " ${CYAN}c)${NC} ${GREEN}Coding Plan 套餐${NC} (sk-sp-xxx, 按套餐抵扣额度) ${GREEN}★ 推荐${NC}"
echo ""
echo -e " ${DIM}提示: Coding Plan 套餐和百炼按量付费的 API Key / Base URL 不互通,请勿混用${NC}"
echo ""
prompt_with_default "请选择" "c" qwen_mode
case "$qwen_mode" in
a)
echo ""
echo -e " ${CYAN}启用 Qwen Portal Auth 插件...${NC}"
enable_auth_plugins
echo -e " ${CYAN}启动 Qwen OAuth 授权...${NC}"
oc_cmd models auth login --provider qwen-portal --set-default || echo -e " ${YELLOW}OAuth 授权已退出${NC}"
echo ""
ask_restart
return
;;
b)
echo ""
echo -e " ${BOLD}百炼按量付费配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://dashscope.console.aliyun.com/apiKey${NC}"
echo -e " ${DIM}Base URL: https://dashscope.aliyuncs.com/compatible-mode/v1${NC}"
echo ""
prompt_with_default "请输入百炼 API Key (sk-...)" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}── 千问商业版 ──${NC}"
echo -e " ${CYAN}a)${NC} qwen-max — 千问Max 旗舰模型 (推荐)"
echo -e " ${CYAN}b)${NC} qwen-plus — 千问Plus 均衡之选 (已升级Qwen3.5)"
echo -e " ${CYAN}c)${NC} qwen-flash — 千问Flash 速度最快 (已升级Qwen3.5)"
echo -e " ${CYAN}d)${NC} qwen-turbo — 千问Turbo 经济实惠"
echo -e " ${CYAN}e)${NC} qwen-long — 千问Long 超长上下文 (1000万Token)"
echo -e " ${CYAN}── 千问Coder ──${NC}"
echo -e " ${CYAN}f)${NC} qwen3-coder-plus — 代码专用旗舰 (100万上下文)"
echo -e " ${CYAN}g)${NC} qwen3-coder-flash — 代码专用极速"
echo -e " ${CYAN}── 推理模型 ──${NC}"
echo -e " ${CYAN}h)${NC} qwq-plus — QwQ推理模型 (数学/代码强化)"
echo -e " ${CYAN}── 千问开源版 ──${NC}"
echo -e " ${CYAN}i)${NC} qwen3-235b-a22b — Qwen3 235B MoE"
echo -e " ${CYAN}j)${NC} qwen3-32b — Qwen3 32B"
echo -e " ${CYAN}k)${NC} qwen3-30b-a3b — Qwen3 30B MoE"
echo -e " ${CYAN}── 第三方模型 ──${NC}"
echo -e " ${CYAN}l)${NC} deepseek-r1 — DeepSeek R1 推理"
echo -e " ${CYAN}m)${NC} deepseek-v3 — DeepSeek V3"
echo -e " ${CYAN}n)${NC} kimi-k2.5 — Kimi K2.5"
echo -e " ${CYAN}o)${NC} glm-5 — 智谱 GLM-5"
echo -e " ${CYAN}p)${NC} MiniMax-M2.5 — MiniMax M2.5"
echo -e " ${CYAN}────────────${NC}"
echo -e " ${CYAN}z)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="qwen-max" ;;
b) model_name="qwen-plus" ;;
c) model_name="qwen-flash" ;;
d) model_name="qwen-turbo" ;;
e) model_name="qwen-long" ;;
f) model_name="qwen3-coder-plus" ;;
g) model_name="qwen3-coder-flash" ;;
h) model_name="qwq-plus" ;;
i) model_name="qwen3-235b-a22b" ;;
j) model_name="qwen3-32b" ;;
k) model_name="qwen3-30b-a3b" ;;
l) model_name="deepseek-r1" ;;
m) model_name="deepseek-v3" ;;
n) model_name="kimi-k2.5" ;;
o) model_name="glm-5" ;;
p) model_name="MiniMax-M2.5" ;;
z) prompt_with_default "请输入模型名称" "qwen-max" model_name ;;
*) model_name="qwen-max" ;;
esac
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
;;
c|*)
echo ""
echo -e " ${BOLD}Coding Plan 套餐配置${NC}"
echo -e " ${YELLOW}订阅套餐: https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/coding_plan${NC}"
echo -e " ${YELLOW}获取专属 API Key: 在上方页面获取 Coding Plan 专属 Key (sk-sp-...)${NC}"
echo -e " ${DIM}Base URL: https://coding.dashscope.aliyuncs.com/v1${NC}"
echo -e " ${DIM}文档: https://help.aliyun.com/zh/model-studio/openclaw-coding-plan${NC}"
echo ""
prompt_with_default "请输入 Coding Plan 专属 API Key (sk-sp-...)" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} qwen3.5-plus — Qwen3.5 Plus (推荐, 100万上下文)"
echo -e " ${CYAN}b)${NC} qwen3-coder-plus — Qwen3 Coder Plus (代码专用, 100万上下文)"
echo -e " ${CYAN}c)${NC} qwen3-coder-next — Qwen3 Coder Next"
echo -e " ${CYAN}d)${NC} qwen3-max-2026-01-23 — Qwen3 Max"
echo -e " ${CYAN}e)${NC} MiniMax-M2.5 — MiniMax M2.5"
echo -e " ${CYAN}f)${NC} glm-5 — 智谱 GLM-5"
echo -e " ${CYAN}g)${NC} kimi-k2.5 — Kimi K2.5"
echo -e " ${CYAN}h)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择默认模型" "a" model_choice
case "$model_choice" in
a) model_name="qwen3.5-plus" ;;
b) model_name="qwen3-coder-plus" ;;
c) model_name="qwen3-coder-next" ;;
d) model_name="qwen3-max-2026-01-23" ;;
e) model_name="MiniMax-M2.5" ;;
f) model_name="glm-5" ;;
g) model_name="kimi-k2.5" ;;
h) prompt_with_default "请输入模型名称" "qwen3.5-plus" model_name ;;
*) model_name="qwen3.5-plus" ;;
esac
echo ""
echo -e " ${CYAN}正在注册 Coding Plan 提供商 (含全部可用模型)...${NC}"
auth_set_apikey bailian "$api_key"
register_codingplan_provider "$api_key"
register_and_set_model "bailian/${model_name}"
echo -e " ${GREEN}✅ Coding Plan 已配置,活跃模型: bailian/${model_name}${NC}"
echo -e " ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}"
fi
;;
esac
;;
9)
echo ""
echo -e " ${BOLD}xAI Grok 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.x.ai${NC}"
echo ""
prompt_with_default "请输入 xAI API Key" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} grok-4 — Grok 4 旗舰 (推荐)"
echo -e " ${CYAN}b)${NC} grok-4-fast — Grok 4 Fast"
echo -e " ${CYAN}c)${NC} grok-3 — Grok 3"
echo -e " ${CYAN}d)${NC} grok-3-fast — Grok 3 Fast"
echo -e " ${CYAN}e)${NC} grok-3-mini — Grok 3 Mini"
echo -e " ${CYAN}f)${NC} grok-3-mini-fast — Grok 3 Mini Fast"
echo -e " ${CYAN}g)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="grok-4" ;;
b) model_name="grok-4-fast" ;;
c) model_name="grok-3" ;;
d) model_name="grok-3-fast" ;;
e) model_name="grok-3-mini" ;;
f) model_name="grok-3-mini-fast" ;;
g) prompt_with_default "请输入模型名称" "grok-4" model_name ;;
*) model_name="grok-4" ;;
esac
auth_set_apikey xai "$api_key"
register_and_set_model "xai/${model_name}"
echo -e " ${GREEN}✅ xAI Grok 已配置,活跃模型: xai/${model_name}${NC}"
fi
;;
10)
echo ""
echo -e " ${BOLD}Groq 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://console.groq.com/keys${NC}"
echo -e " ${YELLOW}Groq 提供超快推理速度${NC}"
echo ""
prompt_with_default "请输入 Groq API Key" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} meta-llama/llama-4-maverick-17b-128e-instruct — Llama 4 Maverick (推荐)"
echo -e " ${CYAN}b)${NC} meta-llama/llama-4-scout-17b-16e-instruct — Llama 4 Scout"
echo -e " ${CYAN}c)${NC} moonshotai/kimi-k2-instruct — Kimi K2"
echo -e " ${CYAN}d)${NC} qwen/qwen3-32b — 通义千问 Qwen3 32B"
echo -e " ${CYAN}e)${NC} llama-3.3-70b-versatile — Llama 3.3 70B"
echo -e " ${CYAN}f)${NC} llama-3.1-8b-instant — Llama 3.1 8B (极速)"
echo -e " ${CYAN}g)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="meta-llama/llama-4-maverick-17b-128e-instruct" ;;
b) model_name="meta-llama/llama-4-scout-17b-16e-instruct" ;;
c) model_name="moonshotai/kimi-k2-instruct" ;;
d) model_name="qwen/qwen3-32b" ;;
e) model_name="llama-3.3-70b-versatile" ;;
f) model_name="llama-3.1-8b-instant" ;;
g) prompt_with_default "请输入模型名称" "meta-llama/llama-4-maverick-17b-128e-instruct" model_name ;;
*) model_name="meta-llama/llama-4-maverick-17b-128e-instruct" ;;
esac
auth_set_apikey groq "$api_key"
register_and_set_model "groq/${model_name}"
echo -e " ${GREEN}✅ Groq 已配置,活跃模型: groq/${model_name}${NC}"
fi
;;
11)
echo ""
echo -e " ${BOLD}硅基流动 SiliconFlow 配置${NC}"
echo -e " ${YELLOW}获取 API Key: https://cloud.siliconflow.cn/account/ak${NC}"
echo -e " ${YELLOW}国内推理平台,支持多种开源模型${NC}"
echo ""
prompt_with_default "请输入 SiliconFlow API Key" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型:${NC}"
echo -e " ${CYAN}a)${NC} deepseek-ai/DeepSeek-V3 — DeepSeek V3 (推荐)"
echo -e " ${CYAN}b)${NC} deepseek-ai/DeepSeek-R1 — DeepSeek R1"
echo -e " ${CYAN}c)${NC} Qwen/Qwen3-235B-A22B — 通义千问 Qwen3 235B"
echo -e " ${CYAN}d)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择模型" "a" model_choice
case "$model_choice" in
a) model_name="deepseek-ai/DeepSeek-V3" ;;
b) model_name="deepseek-ai/DeepSeek-R1" ;;
c) model_name="Qwen/Qwen3-235B-A22B" ;;
d) prompt_with_default "请输入模型名称" "deepseek-ai/DeepSeek-V3" model_name ;;
*) model_name="deepseek-ai/DeepSeek-V3" ;;
esac
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)
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}腾讯云大模型 Coding Plan 套餐配置${NC}"
echo ""
echo -e " ${YELLOW}订阅/管理套餐: https://hunyuan.cloud.tencent.com/#/app/subscription${NC}"
echo -e " ${YELLOW}获取 API Key: 在上方页面创建 Coding Plan 专属 Key (sk-sp-...)${NC}"
echo -e " ${DIM}文档: https://cloud.tencent.com/document/product/1772/128947${NC}"
echo ""
prompt_with_default "请输入 Coding Plan API Key (sk-sp-...)" "" api_key
if [ -n "$api_key" ]; then
echo ""
echo -e " ${CYAN}可用模型 (Coding Plan 套餐内):${NC}"
echo -e " ${CYAN}── 智能推荐 ──${NC}"
echo -e " ${CYAN}a)${NC} tc-code-latest — 自动路由 (由平台选择最佳模型) ${GREEN}★ 推荐${NC}"
echo -e " ${CYAN}── 推理模型 ──${NC}"
echo -e " ${CYAN}b)${NC} hunyuan-t1 — 混元 T1 深度推理"
echo -e " ${CYAN}c)${NC} hunyuan-2.0-thinking — 混元 2.0 Thinking"
echo -e " ${CYAN}── 旗舰模型 ──${NC}"
echo -e " ${CYAN}d)${NC} hunyuan-turbos — 混元 TurboS 旗舰"
echo -e " ${CYAN}e)${NC} hunyuan-2.0-instruct — 混元 2.0 Instruct"
echo -e " ${CYAN}── 第三方模型 ──${NC}"
echo -e " ${CYAN}f)${NC} glm-5 — 智谱 GLM-5"
echo -e " ${CYAN}g)${NC} kimi-k2.5 — Moonshot Kimi K2.5"
echo -e " ${CYAN}h)${NC} minimax-m2.5 — MiniMax M2.5"
echo -e " ${CYAN}────────────${NC}"
echo -e " ${CYAN}z)${NC} 手动输入模型名"
echo ""
prompt_with_default "请选择默认模型" "a" model_choice
case "$model_choice" in
a) model_name="tc-code-latest" ;;
b) model_name="hunyuan-t1" ;;
c) model_name="hunyuan-2.0-thinking" ;;
d) model_name="hunyuan-turbos" ;;
e) model_name="hunyuan-2.0-instruct" ;;
f) model_name="glm-5" ;;
g) model_name="kimi-k2.5" ;;
h) model_name="minimax-m2.5" ;;
z) prompt_with_default "请输入模型名称" "tc-code-latest" model_name ;;
*) model_name="tc-code-latest" ;;
esac
echo ""
echo -e " ${CYAN}正在注册腾讯云 Coding Plan 提供商 (含全部套餐模型)...${NC}"
auth_set_apikey lkeap "$api_key"
register_lkeap_codingplan_provider "$api_key"
register_and_set_model "lkeap/${model_name}"
echo -e " ${GREEN}✅ 腾讯云 Coding Plan 已配置,活跃模型: lkeap/${model_name}${NC}"
echo -e " ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}"
fi
;;
14)
echo ""
echo -e " ${BOLD}自定义 OpenAI 兼容 API${NC}"
echo -e " ${YELLOW}支持任何兼容 OpenAI API 格式的服务商${NC}"
echo ""
prompt_with_default "API Base URL (如 https://api.example.com/v1)" "" base_url
prompt_with_default "API Key" "" api_key
prompt_with_default "模型名称" "" model_name
if [ -n "$base_url" ] && [ -n "$api_key" ] && [ -n "$model_name" ]; then
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 ;;
esac
if [ "$choice" != "0" ] && [ "$choice" != "1" ]; then
echo ""
ask_restart
fi
}
# ══════════════════════════════════════════════════════════════
# 设定当前活跃模型
# ══════════════════════════════════════════════════════════════
set_active_model() {
echo ""
echo -e " ${BOLD}🔄 设定当前活跃模型${NC}"
echo ""
local current_model=$(json_get agents.defaults.model.primary)
echo -e " 当前活跃模型: ${GREEN}${BOLD}${current_model:-未设置}${NC}"
echo ""
local models_json=""
models_json=$(oc_cmd models list --json 2>/dev/null || echo "")
local model_count=0
if [ -n "$models_json" ]; then
model_count=$("$NODE_BIN" -e "
try{const d=JSON.parse(process.argv[1]);console.log((d.models||[]).length);}catch(e){console.log(0);}
" "$models_json" 2>/dev/null || echo "0")
fi
if [ "$model_count" -gt 0 ] 2>/dev/null; then
echo -e " ${CYAN}已配置的模型:${NC}"
echo ""
"$NODE_BIN" -e "
const d=JSON.parse(process.argv[1]);
(d.models||[]).forEach((m,i)=>{
const mark=m.key===process.argv[2]?' ← 当前活跃':'';
const n=m.name&&m.name!==m.key?' ('+m.name+')':'';
console.log(' '+(i+1)+') '+m.key+n+mark);
});
" "$models_json" "$current_model" 2>/dev/null
echo ""
echo -e " ${CYAN}m)${NC} 手动输入模型 ID"
echo -e " ${CYAN}0)${NC} 返回"
echo ""
prompt_with_default "请选择" "0" model_sel
if [ "$model_sel" = "0" ]; then
return
elif [ "$model_sel" = "m" ]; then
prompt_with_default "请输入模型 ID (如 openai/gpt-4o)" "${current_model:-}" manual_model
if [ -n "$manual_model" ]; then
register_and_set_model "$manual_model"
echo -e " ${GREEN}✅ 活跃模型已设为: ${manual_model}${NC}"
ask_restart
fi
elif echo "$model_sel" | grep -qE '^[0-9]+$'; then
local selected=$("$NODE_BIN" -e "
const d=JSON.parse(process.argv[1]);
const m=(d.models||[])[parseInt(process.argv[2])-1];
console.log(m?m.key:'');
" "$models_json" "$model_sel" 2>/dev/null)
if [ -n "$selected" ]; then
register_and_set_model "$selected"
echo -e " ${GREEN}✅ 活跃模型已切换为: ${selected}${NC}"
ask_restart
else
echo -e " ${YELLOW}无效选择${NC}"
fi
fi
else
echo -e " ${YELLOW}尚未配置任何模型。${NC}"
echo -e " ${YELLOW}请先通过「配置 AI 模型提供商」(菜单 2) 添加模型。${NC}"
echo ""
prompt_with_default "直接输入模型 ID 设置? (留空返回)" "" manual_model
if [ -n "$manual_model" ]; then
register_and_set_model "$manual_model"
echo -e " ${GREEN}✅ 活跃模型已设为: ${manual_model}${NC}"
ask_restart
fi
fi
}
# ══════════════════════════════════════════════════════════════
# 配置 QQ 机器人 (通过 qqbot 插件 @tencent-connect/openclaw-qqbot)
# ══════════════════════════════════════════════════════════════
configure_qq() {
echo ""
echo -e " ${BOLD}🐧 QQ 机器人配置${NC}"
echo ""
# 检查 qqbot 插件是否已安装
local plugin_installed=0
if [ -n "$OC_ENTRY" ] && [ -x "$NODE_BIN" ]; then
local plugin_check=$(oc_cmd plugins list 2>/dev/null | grep -i "qqbot" | grep -i "loaded\|disabled")
if [ -n "$plugin_check" ]; then
plugin_installed=1
echo -e " ${GREEN}✅ qqbot 插件已安装${NC}"
fi
fi
if [ "$plugin_installed" -eq 0 ]; then
echo -e " ${YELLOW}⚠️ qqbot 插件尚未安装${NC}"
echo -e " QQ 渠道需要先安装 qqbot 插件才能使用。"
echo ""
prompt_with_default "是否立即安装 qqbot 插件? (y/n)" "y" install_qqbot
if confirm_yes "$install_qqbot"; then
echo -e " ${CYAN}正在安装 @tencent-connect/openclaw-qqbot ...${NC}"
echo -e " ${DIM}(首次安装可能需要几分钟)${NC}"
local install_out
install_out=$(oc_cmd plugins install @tencent-connect/openclaw-qqbot@latest 2>&1)
local install_rc=$?
if [ $install_rc -eq 0 ]; then
echo -e " ${GREEN}✅ qqbot 插件安装成功${NC}"
plugin_installed=1
else
echo -e " ${RED}❌ 插件安装失败 (exit: $install_rc)${NC}"
echo -e " ${DIM}${install_out}${NC}" | tail -5
echo ""
echo -e " ${YELLOW}请手动安装: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}"
return
fi
else
echo -e " ${YELLOW}已跳过插件安装。请先安装 qqbot 插件后再配置。${NC}"
echo -e " ${CYAN}安装命令: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}"
return
fi
fi
echo ""
echo -e " ${YELLOW}获取 App ID 和 App Secret 步骤:${NC}"
echo -e " 1. 前往 ${CYAN}QQ 开放平台${NC}: ${CYAN}https://q.qq.com/qqbot/openclaw/login.html${NC}"
echo -e " 2. 用手机 QQ 扫码注册/登录"
echo -e " 3. 进入 QQ 机器人页面 → 点击「创建机器人」"
echo -e " 4. 创建完成后,复制页面中的 ${CYAN}App ID${NC}${CYAN}App Secret${NC}"
echo ""
echo -e " ${RED}⚠️ 注意: App Secret 不支持二次查看(会强制重置),请妥善保存!${NC}"
echo ""
local current_appid=$(json_get channels.qqbot.appId)
if [ -n "$current_appid" ]; then
echo -e " ${GREEN}当前已配置 App ID: ${current_appid}${NC}"
fi
prompt_with_default "请输入 QQ 机器人 App ID" "" qq_appid
prompt_with_default "请输入 QQ 机器人 App Secret" "" qq_secret
qq_appid=$(sanitize_input "$qq_appid" | tr -d '[:space:]')
qq_secret=$(sanitize_input "$qq_secret" | tr -d '[:space:]')
if [ -n "$qq_appid" ] && [ -n "$qq_secret" ]; then
# App ID 应为纯数字
if ! printf '%s' "$qq_appid" | grep -qE '^[0-9]+$'; then
echo -e " ${RED}❌ App ID 格式错误,应为纯数字${NC}"
return
fi
# App Secret 基本格式检查 (非空即可,长度通常 32 位)
if [ ${#qq_secret} -lt 10 ]; then
echo -e " ${YELLOW}⚠️ App Secret 长度过短(${#qq_secret} 字符),请确认是否完整粘贴。${NC}"
prompt_with_default "是否仍然保存? (y/n)" "n" force_save
if ! confirm_yes "$force_save"; then
echo -e " ${YELLOW}已取消,配置未保存。${NC}"
return
fi
fi
# 使用 openclaw CLI 一键配置 (推荐方式)
echo -e " ${CYAN}正在配置 qqbot 渠道...${NC}"
local add_out
add_out=$(oc_cmd channels add --channel qqbot --token "${qq_appid}:${qq_secret}" 2>&1)
local add_rc=$?
if [ $add_rc -ne 0 ]; then
echo -e " ${YELLOW}CLI 配置未成功,尝试直接写入配置文件...${NC}"
# 回退: 直接写入 JSON 配置
json_set channels.qqbot.enabled true
json_set channels.qqbot.appId "$qq_appid"
json_set channels.qqbot.clientSecret "$qq_secret"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
fi
# 保存后验证
local saved_appid=$(json_get channels.qqbot.appId)
if [ -z "$saved_appid" ]; then
echo -e " ${RED}❌ 配置保存异常! 请检查配置文件${NC}"
return
fi
echo -e " ${GREEN}✅ QQ 机器人配置已保存${NC}"
echo -e " ${CYAN} App ID: ${qq_appid}${NC}"
echo ""
echo -e " ${YELLOW}提示:${NC}"
echo -e " • 配置完成后需重启 Gateway 使配置生效"
echo -e " • 如果机器人回复「该机器人去火星了」,请检查 App ID 和 App Secret 是否正确"
echo -e " • 当前不建议将 QQ 机器人添加进 QQ 群聊"
echo -e " • 插件升级: ${CYAN}openclaw plugins update openclaw-qqbot${NC}"
echo ""
# 重启 Gateway 使配置生效
ask_restart
else
echo -e " ${YELLOW}信息不完整,已取消。${NC}"
fi
}
# ══════════════════════════════════════════════════════════════
# 配置 Telegram
# ══════════════════════════════════════════════════════════════
configure_telegram() {
echo ""
echo -e " ${BOLD}📱 Telegram Bot 配置${NC}"
echo ""
echo -e " ${YELLOW}获取 Bot Token 步骤:${NC}"
echo -e " 1. 打开 Telegram → 搜索 ${CYAN}@BotFather${NC}"
echo -e " 2. 发送 ${CYAN}/newbot${NC} → 按提示创建"
echo -e " 3. 复制生成的 Token"
echo ""
local current_token=$(json_get channels.telegram.botToken)
if [ -n "$current_token" ]; then
local ct_short=$(echo "$current_token" | cut -c1-12)
echo -e " ${GREEN}当前已配置 Token: ${ct_short}...${NC}"
fi
prompt_with_default "请输入 Telegram Bot Token" "" tg_token
if [ -n "$tg_token" ]; then
# ── 强力清洗: 先用 sanitize_input 去除 ANSI 转义序列,再白名单过滤 ──
tg_token=$(sanitize_input "$tg_token")
tg_token=$(printf '%s' "$tg_token" | tr -cd 'A-Za-z0-9:_-')
# ── 格式验证: 使用 grep 正则匹配 "数字:字母数字" ──
if ! printf '%s' "$tg_token" | grep -qE '^[0-9]+:[A-Za-z0-9_-]+$'; then
echo -e " ${RED}❌ Token 格式错误${NC}"
echo -e " ${YELLOW} 正确格式: 123456789:ABCdefGHIjklMNOpqr${NC}"
echo -e " ${YELLOW} 请检查粘贴是否完整,重试。${NC}"
return
fi
echo -e " ${CYAN}验证 Token...${NC}"
local verify=""
verify=$(curl -s --connect-timeout 5 --max-time 10 "https://api.telegram.org/bot${tg_token}/getMe" 2>/dev/null || echo '{"ok":false}')
if echo "$verify" | grep -q '"ok":true'; then
local bot_name=$(echo "$verify" | grep -o '"username":"[^"]*"' | head -1 | cut -d'"' -f4)
echo -e " ${GREEN}✅ Token 验证成功 — @${bot_name}${NC}"
else
echo -e " ${RED}❌ Token 验证失败${NC}"
echo -e " ${YELLOW} 可能原因: Token 不正确 或 无法连接 Telegram API${NC}"
prompt_with_default "是否仍然保存此 Token? (y/n)" "n" force_save
if ! confirm_yes "$force_save"; then
echo -e " ${YELLOW}已取消Token 未保存。${NC}"
return
fi
fi
# ── 使用 json_set 直接写入 (避免 oc_cmd CLI 参数解析问题) ──
json_set channels.telegram.botToken "$tg_token"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
chown openclaw:openclaw "${CONFIG_FILE}.bak" 2>/dev/null || true
# ── 保存后验证: 读回检查 Token 是否完整 ──
local saved_token=$(json_get channels.telegram.botToken)
if [ "$saved_token" != "$tg_token" ]; then
echo -e " ${RED}❌ Token 保存异常! 已保存的值与输入不一致${NC}"
echo -e " ${YELLOW} 期望: ${tg_token}${NC}"
echo -e " ${YELLOW} 实际: ${saved_token}${NC}"
echo -e " ${YELLOW} 请重新配置。${NC}"
return
fi
echo -e " ${GREEN}✅ Telegram Bot Token 已保存${NC}"
# 重启 Gateway 使 Token 生效 (必须重启,否则 Bot 无法连接 Telegram)
echo -e " ${CYAN}正在重启 Gateway 使 Token 生效...${NC}"
restart_gateway
# Token 保存且 Gateway 重启后,自动进入配对流程
echo ""
echo -e " ${CYAN}接下来进行 Telegram 配对,让 Bot 关联您的账号。${NC}"
prompt_with_default "是否现在进行配对? (y/n)" "y" do_pair
if confirm_yes "$do_pair"; then
telegram_pairing
fi
else
echo -e " ${YELLOW}未输入 Token已取消。${NC}"
fi
}
# ── 配置 Discord ──
configure_discord() {
echo ""
echo -e " ${BOLD}🎮 Discord Bot 配置${NC}"
echo ""
echo -e " ${YELLOW}获取 Bot Token:${NC} ${CYAN}https://discord.com/developers/applications${NC}"
echo ""
prompt_with_default "请输入 Discord Bot Token" "" dc_token
dc_token=$(sanitize_input "$dc_token" | tr -d '[:space:]')
if [ -n "$dc_token" ]; then
json_set channels.discord.botToken "$dc_token"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
echo -e " ${GREEN}✅ Discord Bot Token 已保存${NC}"
ask_restart
fi
}
# ── 配置飞书 ──
configure_feishu() {
echo ""
echo -e " ${BOLD}🐦 飞书 Bot 配置${NC}"
echo ""
echo -e " ${YELLOW}获取凭据: ${CYAN}https://open.feishu.cn${NC} → 创建企业自建应用"
echo ""
local current_appid=$(json_get channels.feishu.appId)
if [ -n "$current_appid" ]; then
echo -e " ${GREEN}当前 App ID: ${current_appid}${NC}"
fi
prompt_with_default "请输入飞书 App ID" "" fs_appid
prompt_with_default "请输入飞书 App Secret" "" fs_secret
fs_appid=$(sanitize_input "$fs_appid" | tr -d '[:space:]')
fs_secret=$(sanitize_input "$fs_secret" | tr -d '[:space:]')
if [ -n "$fs_appid" ] && [ -n "$fs_secret" ]; then
json_set channels.feishu.appId "$fs_appid"
json_set channels.feishu.appSecret "$fs_secret"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
echo -e " ${GREEN}✅ 飞书配置已保存${NC}"
ask_restart
else
echo -e " ${YELLOW}信息不完整,已取消。${NC}"
fi
}
# ── 配置 Slack ──
configure_slack() {
echo ""
echo -e " ${BOLD}💬 Slack Bot 配置${NC}"
echo ""
echo -e " ${YELLOW}获取 Bot Token:${NC} ${CYAN}https://api.slack.com/apps${NC} → Create App"
echo ""
prompt_with_default "请输入 Slack Bot Token (xoxb-...)" "" sk_token
sk_token=$(sanitize_input "$sk_token" | tr -d '[:space:]')
if [ -n "$sk_token" ]; then
json_set channels.slack.botToken "$sk_token"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
echo -e " ${GREEN}✅ Slack Bot Token 已保存${NC}"
ask_restart
fi
}
# ══════════════════════════════════════════════════════════════
# Telegram 配对助手
# ══════════════════════════════════════════════════════════════
telegram_pairing() {
echo ""
echo -e " ${BOLD}🤝 Telegram 配对助手${NC}"
echo ""
local tg_token=$(json_get channels.telegram.botToken)
if [ -z "$tg_token" ]; then
echo -e " ${YELLOW}未检测到 Telegram Bot Token请先配置 Telegram。${NC}"
return
fi
echo -e " ${CYAN}诊断 Telegram API 连通性...${NC}"
local verify=""
verify=$(curl -s --connect-timeout 5 --max-time 10 "https://api.telegram.org/bot${tg_token}/getMe" 2>/dev/null || echo '{"ok":false}')
if echo "$verify" | grep -q '"ok":true'; then
local bot_name=$(echo "$verify" | grep -o '"username":"[^"]*"' | head -1 | cut -d'"' -f4)
echo -e " ${GREEN}✅ Telegram API 连通正常 — @${bot_name}${NC}"
else
echo -e " ${RED}❌ Telegram API 连通检测未通过${NC}"
echo -e " ${YELLOW} 可能原因: Token 不正确、网络不通 或 Telegram 被屏蔽${NC}"
echo -e " ${CYAN} 建议: 返回重新配置 Token 或检查代理/网络设置${NC}"
return
fi
echo ""
echo -e " ${GREEN}╔══════════════════════════════════════════════════╗${NC}"
echo -e " ${GREEN}║ 请在 Telegram 中向 Bot 发送 /start ║${NC}"
echo -e " ${GREEN}║ 然后回到这里按回车,脚本自动检测配对请求 ║${NC}"
echo -e " ${GREEN}╚══════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${YELLOW}发送 /start 后按回车继续 (输入 q 退出)...${NC}"
read _wait
case "$_wait" in q|Q) return ;; esac
local paired=0
local attempt=1
while [ $attempt -le 3 ]; do
echo -e " ${CYAN}检测配对请求... (第 ${attempt}/3 轮)${NC}"
local pair_json=$(oc_cmd pairing list telegram --json 2>/dev/null || echo "")
local codes=""
if [ -n "$pair_json" ]; then
codes=$(echo "$pair_json" | grep -o '"code"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
fi
if [ -n "$codes" ]; then
# 逐个处理配对码 (避免管道子 shell 变量丢失问题)
local _codes_tmp="/tmp/oc_pair_codes_$$"
echo "$codes" > "$_codes_tmp"
while IFS= read -r code; do
[ -z "$code" ] && continue
echo -e " ${CYAN}发现配对请求: ${code}${NC}"
local approve=$(oc_cmd pairing approve telegram "$code" 2>&1)
if echo "$approve" | grep -qi "approved\|success\|ok"; then
echo ""
echo -e " ${GREEN}${BOLD}🎉 Telegram 配对成功!${NC}"
fi
done < "$_codes_tmp"
rm -f "$_codes_tmp"
# 检查是否还有待配对的
local re_check=$(oc_cmd pairing list telegram --json 2>/dev/null || echo "")
local re_codes=$(echo "$re_check" | grep -o '"code"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
if [ -z "$re_codes" ]; then
paired=1
break
fi
fi
if [ $attempt -lt 3 ] && [ $paired -eq 0 ]; then
echo -e " ${YELLOW}未检测到,等待 8 秒后重试...${NC}"
sleep 8
fi
attempt=$((attempt + 1))
done
if [ "$paired" -eq 0 ]; then
echo ""
echo -e " ${YELLOW}未自动检测到配对请求。${NC}"
echo -e " ${CYAN}如果 Bot 已回复配对码,请手动输入 (回车跳过):${NC}"
prompt_with_default "配对码" "" manual_code
if [ -n "$manual_code" ]; then
local approve=$(oc_cmd pairing approve telegram "$manual_code" 2>&1)
if echo "$approve" | grep -qi "approved\|success\|ok"; then
echo -e " ${GREEN}${BOLD}🎉 Telegram 配对成功!${NC}"
paired=1
else
echo -e " ${YELLOW}配对失败: $approve${NC}"
fi
fi
fi
# 配对成功后重启网关,使配对关系立即生效
if [ "$paired" -eq 1 ]; then
echo ""
echo -e " ${CYAN}正在重启 Gateway 使配对生效...${NC}"
restart_gateway
echo -e " ${GREEN}✅ 现在可以在 Telegram 中与 Bot 对话了!${NC}"
fi
}
# ══════════════════════════════════════════════════════════════
# 配置渠道子菜单
# ══════════════════════════════════════════════════════════════
configure_channels() {
while true; do
echo ""
echo -e " ${BOLD}📡 配置消息渠道${NC}"
echo ""
echo -e " ${CYAN}1)${NC} QQ 机器人 ${GREEN}(腾讯QQ推荐国内用户)${NC}"
echo -e " ${CYAN}2)${NC} Telegram ${GREEN}(最常用,推荐)${NC}"
echo -e " ${CYAN}3)${NC} Discord"
echo -e " ${CYAN}4)${NC} 飞书 (Feishu)"
echo -e " ${CYAN}5)${NC} Slack"
echo -e " ${CYAN}6)${NC} WhatsApp ${YELLOW}(需通过 Web 控制台扫码)${NC}"
echo -e " ${CYAN}7)${NC} Telegram 配对助手"
echo -e " ${CYAN}8)${NC} 官方完整渠道配置向导"
echo -e " ${CYAN}0)${NC} 返回主菜单"
echo ""
prompt_with_default "请选择" "1" ch_choice
case "$ch_choice" in
1) configure_qq ;;
2) configure_telegram ;;
3) configure_discord ;;
4) configure_feishu ;;
5) configure_slack ;;
6)
echo ""
echo -e " ${YELLOW}WhatsApp 需要通过 Web 控制台扫码配对:${NC}"
local gw_token=$(json_get gateway.auth.token)
local gw_port=$(json_get gateway.port)
gw_port=${gw_port:-18789}
echo -e " ${CYAN}http://<你的路由器IP>:${gw_port}/?token=${gw_token}${NC}"
echo -e " 打开后进入 Channels → WhatsApp 扫码即可。"
;;
7) telegram_pairing ;;
8)
echo ""
echo -e " ${CYAN}启动官方渠道配置向导...${NC}"
(oc_cmd configure --section channels) || echo -e " ${YELLOW}配置向导已退出${NC}"
;;
0) return ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
done
}
# ══════════════════════════════════════════════════════════════
# 健康检查
# ══════════════════════════════════════════════════════════════
health_check() {
echo ""
echo -e " ${BOLD}🔍 健康检查${NC}"
echo ""
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 "
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
echo -e " ${RED}❌ Gateway 端口 ${gw_port} 未监听${NC}"
fi
local http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 "http://127.0.0.1:${gw_port}/" 2>/dev/null || echo "000")
if [ "$http_code" = "200" ] || [ "$http_code" = "302" ] || [ "$http_code" = "401" ]; then
echo -e " ${GREEN}✅ HTTP 响应正常 (${http_code})${NC}"
else
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
echo -e " ${RED}❌ 配置文件不存在${NC}"
fi
echo ""
echo -e " ${CYAN}运行官方诊断...${NC}"
oc_cmd doctor 2>/dev/null || true
echo ""
echo -e " ${CYAN}最近日志 (最后 10 行):${NC}"
logread -e openclaw 2>/dev/null | tail -10 || echo " (无日志)"
}
# ══════════════════════════════════════════════════════════════
# 恢复默认配置
# ══════════════════════════════════════════════════════════════
reset_to_defaults() {
echo ""
echo -e " ${BOLD}⚠️ 恢复默认配置${NC}"
echo ""
echo -e " ${YELLOW}请选择恢复级别:${NC}"
echo ""
echo -e " ${CYAN}1)${NC} 🔧 仅重置网关设置 (端口/绑定/模式恢复默认,保留模型和渠道)"
echo -e " ${CYAN}2)${NC} 🤖 清除模型配置 (移除所有 AI 模型和 API Key)"
echo -e " ${CYAN}3)${NC} 📡 清除渠道配置 (移除所有消息渠道配置)"
echo -e " ${CYAN}4)${NC} 🔄 完全恢复出厂 (删除所有配置,重新初始化)"
echo -e " ${CYAN}0)${NC} 返回"
echo ""
prompt_with_default "请选择" "0" reset_choice
case "$reset_choice" in
1)
echo ""
echo -e " ${YELLOW}将重置: 网关端口→18789, 绑定→lan, 模式→local${NC}"
echo -e " ${YELLOW}保留: 认证令牌、模型配置、消息渠道${NC}"
prompt_with_default "确认恢复网关默认设置? (yes/no)" "no" confirm
if [ "$confirm" = "yes" ]; then
echo ""
echo -e " ${CYAN}正在重置网关设置...${NC}"
json_set gateway.port 18789 2>&1
json_set gateway.bind lan 2>&1
json_set gateway.mode local 2>&1
json_set gateway.controlUi.allowInsecureAuth true 2>&1
json_set gateway.controlUi.dangerouslyDisableDeviceAuth true 2>&1
json_set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback true 2>&1
json_set gateway.tailscale.mode off 2>&1
echo -e " ${GREEN}✅ 网关设置已恢复默认${NC}"
ask_restart
else
echo -e " ${CYAN}已取消${NC}"
fi
;;
2)
echo ""
echo -e " ${RED}⚠️ 将清除: 所有模型配置、API Key、活跃模型设置${NC}"
prompt_with_default "确认清除所有模型配置? (yes/no)" "no" confirm
if [ "$confirm" = "yes" ]; then
echo ""
echo -e " ${CYAN}正在清除模型配置...${NC}"
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
else
echo -e " ${CYAN}已取消${NC}"
fi
;;
3)
echo ""
echo -e " ${RED}⚠️ 将清除: 所有消息渠道配置 (Telegram/Discord/飞书等)${NC}"
prompt_with_default "确认清除所有渠道配置? (yes/no)" "no" confirm
if [ "$confirm" = "yes" ]; then
echo ""
echo -e " ${CYAN}正在清除渠道配置...${NC}"
oc_cmd config unset channels >/dev/null 2>&1 || true
echo -e " ${GREEN}✅ 渠道配置已清除${NC}"
echo -e " ${YELLOW}请通过菜单 [4] 重新配置消息渠道${NC}"
ask_restart
else
echo -e " ${CYAN}已取消${NC}"
fi
;;
4)
echo ""
echo -e " ${RED}╔══════════════════════════════════════════════════════╗${NC}"
echo -e " ${RED}║ ⚠️ 完全恢复出厂设置 ║${NC}"
echo -e " ${RED}║ 此操作将删除所有配置并重新初始化 ║${NC}"
echo -e " ${RED}╚══════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${RED}此操作不可撤销!${NC}"
prompt_with_default "输入 RESET 确认恢复出厂设置" "" confirm
if [ "$confirm" = "RESET" ]; then
echo ""
echo -e " ${CYAN}[1/5] 停止 Gateway...${NC}"
# 只停止 gateway 实例, 不能停 pty (否则会断开当前终端连接)
local gw_pid=""
gw_pid=$(ubus call service list '{"name":"openclaw"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.pid' 2>/dev/null) || true
if [ -n "$gw_pid" ] && kill -0 "$gw_pid" 2>/dev/null; then
kill "$gw_pid" 2>/dev/null || true
sleep 2
else
# 按端口查找 gateway 进程
local gw_port_cur=$(json_get gateway.port)
gw_port_cur=${gw_port_cur:-18789}
local gw_pid2=$(get_pid_by_port "$gw_port_cur")
if [ -n "$gw_pid2" ]; then
kill "$gw_pid2" 2>/dev/null || true
sleep 2
fi
fi
echo -e " ${GREEN} Gateway 已停止${NC}"
echo -e " ${CYAN}[2/5] 备份当前配置...${NC}"
local backup_dir="${OC_STATE_DIR}/backups"
local backup_ts=$(date +%Y%m%d_%H%M%S)
mkdir -p "$backup_dir"
chown openclaw:openclaw "$backup_dir" 2>/dev/null || true
if [ -f "$CONFIG_FILE" ]; then
cp "$CONFIG_FILE" "${backup_dir}/openclaw_${backup_ts}.json"
echo -e " ${GREEN} 备份已保存: backups/openclaw_${backup_ts}.json${NC}"
fi
echo -e " ${CYAN}[3/5] 重置配置...${NC}"
# 直接删除配置文件 (避免 oc_cmd reset 可能的交互式阻塞)
rm -f "$CONFIG_FILE" 2>/dev/null || true
rm -f "${CONFIG_FILE}.bak" 2>/dev/null || true
echo '{}' > "$CONFIG_FILE"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
echo -e " ${GREEN} 配置已清除${NC}"
echo -e " ${CYAN}[4/5] 重新初始化...${NC}"
# 尝试 onboard超时 10 秒避免阻塞
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 --tools-profile coding" >/dev/null 2>&1 || true
else
"$_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
wait "$_ob_pid" 2>/dev/null || true
fi
echo -e " ${GREEN} 初始化完成${NC}"
echo -e " ${CYAN}[5/5] 应用 OpenWrt 适配配置...${NC}"
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)
json_set gateway.port 18789
json_set gateway.bind lan
json_set gateway.mode local
json_set gateway.auth.mode token
json_set gateway.auth.token "$new_token"
json_set gateway.controlUi.allowInsecureAuth true
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
uci set openclaw.main.token="$new_token" 2>/dev/null
uci commit openclaw 2>/dev/null
echo ""
echo -e " ${GREEN}✅ 出厂设置已恢复!${NC}"
echo ""
echo -e " ${CYAN}新认证令牌: ${new_token}${NC}"
echo ""
# 重启 gateway (通过 procd reload, 这样不会杀 pty)
/etc/init.d/openclaw start >/dev/null 2>&1 &
echo -e " ${YELLOW}⏳ Gateway 启动中,请稍候...${NC}"
local gw_port=18789
local waited=0
while [ $waited -lt 15 ]; do
sleep 2
waited=$((waited + 2))
if check_port_listening "$gw_port"; then
echo -e " ${GREEN}✅ Gateway 已重新启动${NC}"
break
fi
done
if [ $waited -ge 15 ]; then
echo -e " ${YELLOW}⏳ Gateway 可能仍在启动中,请稍后检查${NC}"
fi
else
echo -e " ${CYAN}已取消${NC}"
fi
;;
0|"") return ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
}
# ══════════════════════════════════════════════════════════════
# 主菜单
# ══════════════════════════════════════════════════════════════
# ══════════════════════════════════════════════════════════════
# 备份/还原配置菜单 (v2026.3.8+ openclaw backup create/verify)
# ══════════════════════════════════════════════════════════════
backup_restore_menu() {
echo ""
echo -e " ${BOLD}💾 备份/还原配置${NC}"
echo ""
echo -e " ${CYAN}1)${NC} 创建配置备份 (仅配置文件)"
echo -e " ${CYAN}2)${NC} 创建完整备份 (配置 + 状态数据)"
echo -e " ${CYAN}3)${NC} 验证最新备份"
echo -e " ${CYAN}4)${NC} 查看备份列表"
echo -e " ${CYAN}5)${NC} 从最新备份恢复配置"
echo -e " ${CYAN}0)${NC} 返回主菜单"
echo ""
prompt_with_default "请选择" "1" backup_choice
# 备份目录 (openclaw backup create 输出到 CWD)
local backup_dir="${OC_STATE_DIR}/backups"
mkdir -p "$backup_dir" 2>/dev/null
case "$backup_choice" in
1)
echo -e " ${CYAN}正在创建配置备份...${NC}"
local out
out=$(cd "$backup_dir" && oc_cmd backup create --only-config --no-include-workspace 2>&1)
local rc=$?
echo "$out"
if [ $rc -eq 0 ] && echo "$out" | grep -q "\.tar\.gz"; then
echo -e " ${GREEN}✅ 配置备份已创建${NC}"
else
echo -e " ${YELLOW}⚠️ 备份功能需要 OpenClaw v2026.3.8+${NC}"
echo -e " ${DIM}如果备份命令不可用,可手动备份: cp ${CONFIG_FILE} ${CONFIG_FILE}.bak${NC}"
fi
;;
2)
echo -e " ${CYAN}正在创建完整备份...${NC}"
echo -e " ${DIM}(包含配置和状态数据,可能需要较长时间)${NC}"
local out
out=$(cd "$backup_dir" && HOME="$backup_dir" oc_cmd backup create --no-include-workspace 2>&1)
local rc=$?
echo "$out"
# 完整备份可能输出到 HOME尝试移动到 backup_dir
for f in "${OC_DATA}"/*-openclaw-backup.tar.gz; do
[ -f "$f" ] && mv "$f" "$backup_dir/" 2>/dev/null
done
if [ $rc -eq 0 ] && echo "$out" | grep -q "\.tar\.gz"; then
echo -e " ${GREEN}✅ 完整备份已创建${NC}"
else
echo -e " ${YELLOW}⚠️ 备份失败${NC}"
echo -e " ${DIM}提示: 如果配置文件有校验警告,完整备份可能受限。请使用选项 1 (仅配置文件) 备份${NC}"
fi
;;
3)
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
if [ -z "$latest" ]; then
# 也检查旧位置
latest=$(ls -t "${OC_STATE_DIR}"/*-openclaw-backup.tar.gz "${OC_DATA}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
fi
if [ -z "$latest" ]; then
echo -e " ${YELLOW}未找到备份文件,请先创建备份${NC}"
else
echo -e " ${CYAN}验证备份: ${latest}${NC}"
oc_cmd backup verify "$latest" 2>&1
fi
;;
4)
echo ""
if [ -d "$backup_dir" ]; then
local count=$(ls "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | wc -l)
if [ "$count" -gt 0 ] 2>/dev/null; then
echo -e " ${BOLD}备份文件列表:${NC}"
ls -lh "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | while read line; do
echo -e " ${DIM}${line}${NC}"
done
else
echo -e " ${YELLOW}暂无备份文件${NC}"
fi
else
echo -e " ${YELLOW}暂无备份文件${NC}"
fi
echo ""
echo -e " ${DIM}备份目录: ${backup_dir}${NC}"
;;
5)
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
if [ -z "$latest" ]; then
echo -e " ${YELLOW}未找到备份文件,请先创建备份${NC}"
else
echo -e " ${CYAN}将从以下备份恢复:${NC}"
echo -e " ${DIM}${latest}${NC}"
echo ""
echo -e " ${YELLOW}⚠️ 这会覆盖当前的 openclaw.json 配置!${NC}"
prompt_with_default "确认恢复? (y/N)" "N" confirm_restore
if [ "$confirm_restore" = "y" ] || [ "$confirm_restore" = "Y" ]; then
# 备份当前配置
cp -f "$CONFIG_FILE" "${CONFIG_FILE}.pre-restore" 2>/dev/null
# 从 tar.gz 中提取 openclaw.json
local tmp_json="${CONFIG_FILE}.tmp"
tar -xzf "$latest" --wildcards '*/openclaw.json' -O > "$tmp_json" 2>/dev/null
if [ -s "$tmp_json" ] && "$NODE_BIN" -e "JSON.parse(require('fs').readFileSync('${tmp_json}','utf8'))" 2>/dev/null; then
mv -f "$tmp_json" "$CONFIG_FILE"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null
echo -e " ${GREEN}✅ 配置已恢复!原配置已保存为 openclaw.json.pre-restore${NC}"
echo ""
prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart
if [ "$do_restart" != "n" ] && [ "$do_restart" != "N" ]; then
restart_gateway
fi
else
rm -f "$tmp_json"
echo -e " ${RED}❌ 备份中的配置文件无效,恢复已取消${NC}"
fi
else
echo -e " ${DIM}已取消${NC}"
fi
fi
;;
0|"") return ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
}
main_menu() {
while true; do
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${NC} ${BOLD}OpenClaw AI Gateway — OpenWrt 配置管理${NC} ${GREEN}${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${CYAN}1)${NC} 📋 查看当前配置"
echo -e " ${CYAN}2)${NC} 🤖 配置 AI 模型提供商"
echo -e " ${CYAN}3)${NC} 🔄 设定当前活跃模型"
echo -e " ${CYAN}4)${NC} 📡 配置消息渠道 (QQ/Telegram/Discord/飞书/Slack)"
echo -e " ${CYAN}5)${NC} 🔍 健康检查 / 诊断"
echo -e " ${CYAN}6)${NC} 🔄 重启 Gateway"
echo -e " ${CYAN}7)${NC} 📝 查看原始配置文件"
echo -e " ${CYAN}8)${NC} 💾 备份/还原配置"
echo -e " ${CYAN}9)${NC} ⚠️ 恢复默认配置"
echo -e " ${CYAN}0)${NC} 退出"
echo ""
prompt_with_default "请选择" "1" menu_choice
case "$menu_choice" in
1) show_current_config ;;
2) configure_model ;;
3) set_active_model ;;
4) configure_channels ;;
5) health_check ;;
6) restart_gateway ;;
7)
echo ""
echo -e " ${CYAN}配置文件路径: ${CONFIG_FILE}${NC}"
echo ""
if [ -f "$CONFIG_FILE" ]; then
"$NODE_BIN" -e "console.log(JSON.stringify(JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8')),null,2))" 2>/dev/null || cat "$CONFIG_FILE"
else
echo " (配置文件不存在)"
fi
echo ""
prompt_with_default "是否用 vi 编辑? (y/n)" "n" do_edit
if confirm_yes "$do_edit"; then
vi "$CONFIG_FILE"
ask_restart
fi
;;
8) backup_restore_menu ;;
9) reset_to_defaults ;;
0)
echo -e " ${GREEN}再见!${NC}"
exit 0
;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
done
}
# ── 支持命令行参数 ──
case "${1:-}" in
--set)
if [ -n "${2:-}" ] && [ -n "${3:-}" ]; then
json_set "$2" "$3"
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
echo -e "${GREEN}✅ 已设置 $2${NC}"
else
echo "用法: oc-config.sh --set <key> <value>"
fi
;;
--get)
if [ -n "${2:-}" ]; then
oc_cmd config get "$2"
else
echo "用法: oc-config.sh --get <key>"
fi
;;
--restart)
restart_gateway
;;
--backup)
bk_dir="${OC_STATE_DIR}/backups"
mkdir -p "$bk_dir" 2>/dev/null
echo -e "${CYAN}正在创建配置备份...${NC}"
cd "$bk_dir" && oc_cmd backup create --only-config --no-include-workspace 2>&1
;;
--status)
show_current_config
health_check
;;
--help|-h)
echo ""
echo "OpenClaw AI Gateway — OpenWrt 配置管理工具"
echo ""
echo "用法:"
echo " oc-config.sh # 进入交互式菜单"
echo " oc-config.sh --set K V # 设置配置项"
echo " oc-config.sh --get K # 读取配置项"
echo " oc-config.sh --restart # 重启 Gateway"
echo " oc-config.sh --status # 查看状态"
echo ""
;;
"")
main_menu
;;
*)
echo "未知参数: $1 (使用 --help 查看帮助)"
exit 1
;;
esac