mirror of
https://github.com/hotwa/luci-app-openclaw.git
synced 2026-03-31 04:52:33 +00:00
2138 lines
90 KiB
Bash
Executable File
2138 lines
90 KiB
Bash
Executable File
#!/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_key,OpenClaw 会自动兑换 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 使用原生 API,baseUrl 不带 /v1
|
||
ollama_url=$(echo "$ollama_url" | sed 's|/v1/*$||;s|/*$||')
|
||
fi
|
||
;;
|
||
*)
|
||
ollama_url="http://127.0.0.1:11434"
|
||
;;
|
||
esac
|
||
if [ -n "$ollama_url" ]; then
|
||
# 检测 Ollama 是否可达
|
||
echo ""
|
||
echo -e " ${CYAN}检测 Ollama 连通性...${NC}"
|
||
local ollama_base=$(echo "$ollama_url" | sed 's|/v1$||')
|
||
local ollama_check=$(curl -sf --connect-timeout 3 --max-time 5 "${ollama_base}/api/tags" 2>/dev/null || echo "")
|
||
if [ -n "$ollama_check" ]; then
|
||
echo -e " ${GREEN}✅ Ollama 已连接${NC}"
|
||
# 列出已安装的模型
|
||
local model_list=$("$NODE_BIN" -e "
|
||
try{
|
||
const d=JSON.parse(process.argv[1]);
|
||
(d.models||[]).forEach((m,i)=>console.log(' '+(i+1)+') '+m.name));
|
||
}catch(e){}
|
||
" "$ollama_check" 2>/dev/null)
|
||
if [ -n "$model_list" ]; then
|
||
echo -e " ${CYAN}已安装的模型:${NC}"
|
||
echo "$model_list"
|
||
echo -e " ${CYAN}m)${NC} 手动输入模型名"
|
||
echo ""
|
||
prompt_with_default "请选择模型" "1" ollama_sel
|
||
if [ "$ollama_sel" = "m" ]; then
|
||
prompt_with_default "请输入模型名称" "llama3.3" model_name
|
||
elif echo "$ollama_sel" | grep -qE '^[0-9]+$'; then
|
||
model_name=$("$NODE_BIN" -e "
|
||
try{
|
||
const d=JSON.parse(process.argv[1]);
|
||
const m=(d.models||[])[parseInt(process.argv[2])-1];
|
||
console.log(m?m.name:'');
|
||
}catch(e){console.log('');}
|
||
" "$ollama_check" "$ollama_sel" 2>/dev/null)
|
||
if [ -z "$model_name" ]; then
|
||
echo -e " ${YELLOW}无效选择,使用默认模型${NC}"
|
||
model_name="llama3.3"
|
||
fi
|
||
fi
|
||
else
|
||
echo -e " ${YELLOW}未检测到已安装模型,请先在 Ollama 中拉取模型:${NC}"
|
||
echo -e " ${CYAN} ollama pull llama3.3${NC}"
|
||
echo ""
|
||
prompt_with_default "请输入模型名称" "llama3.3" model_name
|
||
fi
|
||
else
|
||
echo -e " ${YELLOW}⚠️ 无法连接 Ollama (${ollama_base})${NC}"
|
||
echo -e " ${YELLOW} 请确认 Ollama 已启动并可访问${NC}"
|
||
echo -e " ${CYAN} 提示: 如果 Ollama 在其他设备上,需设置 OLLAMA_HOST=0.0.0.0${NC}"
|
||
echo ""
|
||
prompt_with_default "仍要继续配置? (y/n)" "n" force_continue
|
||
if ! confirm_yes "$force_continue"; then
|
||
return
|
||
fi
|
||
prompt_with_default "请输入模型名称" "llama3.3" model_name
|
||
fi
|
||
if [ -n "$model_name" ]; then
|
||
# Ollama 无需 API Key,使用占位符
|
||
auth_set_apikey ollama "ollama-local" "ollama:local"
|
||
# v2026.3.2: Ollama 使用原生 ollama API,不再走 OpenAI 兼容层
|
||
_RCP_PROV="ollama" _RCP_URL="$ollama_url" _RCP_KEY="ollama-local" _RCP_MID="$model_name" _RCP_MNAME="$model_name" "$NODE_BIN" -e "
|
||
const fs=require('fs');
|
||
let d={};
|
||
try{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}
|
||
if(!d.models)d.models={};
|
||
if(!d.models.providers)d.models.providers={};
|
||
d.models.mode='merge';
|
||
d.models.providers['ollama']={
|
||
baseUrl:process.env._RCP_URL,
|
||
apiKey:process.env._RCP_KEY,
|
||
api:'ollama',
|
||
models:[{
|
||
id:process.env._RCP_MID,
|
||
name:process.env._RCP_MNAME,
|
||
reasoning:false,
|
||
input:['text'],
|
||
cost:{input:0,output:0,cacheRead:0,cacheWrite:0},
|
||
contextWindow:128000,
|
||
maxTokens:32000
|
||
}]
|
||
};
|
||
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
|
||
" 2>/dev/null
|
||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||
register_and_set_model "ollama/${model_name}"
|
||
echo -e " ${GREEN}✅ Ollama 已配置,活跃模型: ollama/${model_name}${NC}"
|
||
echo -e " ${CYAN} Ollama 地址: ${ollama_url}${NC}"
|
||
fi
|
||
fi
|
||
;;
|
||
13)
|
||
echo ""
|
||
echo -e " ${BOLD}腾讯云大模型 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
|