1 line
280 KiB
JSON
1 line
280 KiB
JSON
{"repo": "10000ge10000/luci-app-openclaw", "files": {"root/usr/share/openclaw/oc-config.sh": {"26646fa63dc245216dfffa1bf2f9e58036944749": {"t": 1785674143.3817928, "n": 104079, "h": "e2978c5f76a6e185", "c": "#!/bin/sh\n# ============================================================================\n# OpenClaw 配置管理工具 — OpenWrt 适配版\n# 基于原始 oc-config.sh 移植,适配 ash/busybox 环境\n# ============================================================================\n\n# ── 颜色 ──\nRED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[1;33m'\nCYAN='\\033[0;36m'; BOLD='\\033[1m'; DIM='\\033[2m'; NC='\\033[0m'\n\n# ── 交互式菜单引擎路径 ──\nOC_MENU_ENGINE=\"/usr/share/openclaw/oc-menu-engine.js\"\nOC_INTERACTIVE=\"/usr/share/openclaw/oc-config-interactive.js\"\n\n# ── 端口检查兼容函数 (ss 或 netstat) ──\n# check_port_listening <port> — 检查端口是否在监听,返回 0/1\ncheck_port_listening() {\n\tlocal p=\"$1\"\n\tif command -v ss >/dev/null 2>&1; then\n\t\tss -ltn 2>/dev/null | grep -q \":${p} \"\n\telse\n\t\tnetstat -tln 2>/dev/null | grep -q \":${p} \"\n\tfi\n}\n# get_pid_by_port <port> — 获取监听指定端口的进程 PID\nget_pid_by_port() {\n\tlocal p=\"$1\"\n\tif command -v ss >/dev/null 2>&1; then\n\t\tss -tlnp 2>/dev/null | grep \":${p} \" | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | head -1\n\telse\n\t\tnetstat -tlnp 2>/dev/null | grep \":${p} \" | sed -n 's|.* \\([0-9]*\\)/.*|\\1|p' | head -1\n\tfi\n}\n\n# ── 路径 (OpenWrt 适配,支持自定义安装路径) ──\n# 从 UCI 配置读取自定义路径,环境变量可覆盖。\n# 注意 OC_INSTALL_PATH 历史上被当作“基础路径”传入,统一解析后再展开到 openclaw 根目录。\n[ -r /usr/libexec/openclaw-paths.sh ] && . /usr/libexec/openclaw-paths.sh\nOC_CONFIGURED_PATH=\"${OC_INSTALL_PATH:-$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')}\"\nif command -v oc_load_paths >/dev/null 2>&1 && oc_load_paths \"$OC_CONFIGURED_PATH\"; then\n\tOC_INSTALL_PATH=\"$OC_ROOT\"\nelse\n\tOC_BASE_PATH=\"${OC_CONFIGURED_PATH%/}\"\n\tOC_INSTALL_PATH=\"${OC_BASE_PATH}/openclaw\"\n\tNODE_BASE=\"${NODE_BASE:-${OC_INSTALL_PATH}/node}\"\n\tOC_GLOBAL=\"${OC_GLOBAL:-${OC_INSTALL_PATH}/global}\"\n\tOC_DATA=\"${OC_DATA:-${OC_INSTALL_PATH}/data}\"\n\tCONFIG_FILE=\"${OC_DATA}/.openclaw/openclaw.json\"\nfi\nNODE_BASE=\"${NODE_BASE:-${OC_INSTALL_PATH}/node}\"\nOC_GLOBAL=\"${OC_GLOBAL:-${OC_INSTALL_PATH}/global}\"\nOC_DATA=\"${OC_DATA:-${OC_INSTALL_PATH}/data}\"\nNODE_BIN=\"${NODE_BASE}/bin/node\"\nOC_STATE_DIR=\"${OC_DATA}/.openclaw\"\nCONFIG_FILE=\"${OC_STATE_DIR}/openclaw.json\"\n\nexport HOME=\"$OC_DATA\"\nexport OPENCLAW_HOME=\"$OC_DATA\"\nexport OPENCLAW_STATE_DIR=\"$OC_STATE_DIR\"\nexport OPENCLAW_CONFIG_PATH=\"$CONFIG_FILE\"\nexport NODE_ICU_DATA=\"${NODE_BASE}/share/icu\"\nexport PATH=\"${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n\nfix_openclaw_state_permissions() {\n\t[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh fix-state \"$OC_STATE_DIR\"\n}\n\nprepare_openclaw_workdirs() {\n\t[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh prepare-workdirs \"$OC_DATA\"\n}\n\n# ── 查找 openclaw 入口 ──\nOC_PKG_DIR=\"\"\nfor d in \"${OC_GLOBAL}/lib/node_modules/openclaw\" \"${OC_GLOBAL}/node_modules/openclaw\" \"${NODE_BASE}/lib/node_modules/openclaw\"; do\n\tif [ -d \"$d\" ]; then\n\t\tOC_PKG_DIR=\"$d\"\n\t\tbreak\n\tfi\ndone\n\nOC_ENTRY=\"\"\nif [ -n \"$OC_PKG_DIR\" ]; then\n\tif [ -f \"${OC_PKG_DIR}/openclaw.mjs\" ]; then\n\t\tOC_ENTRY=\"${OC_PKG_DIR}/openclaw.mjs\"\n\telif [ -f \"${OC_PKG_DIR}/dist/cli.js\" ]; then\n\t\tOC_ENTRY=\"${OC_PKG_DIR}/dist/cli.js\"\n\tfi\nfi\n\noc_cmd() {\n\tif [ -n \"$OC_ENTRY\" ] && [ -x \"$NODE_BIN\" ]; then\n\t\t\"$NODE_BIN\" \"$OC_ENTRY\" \"$@\" 2>&1\n\t\tlocal rc=$?\n\t\t# 修复权限: oc_cmd 以 root 运行但配置文件应属于 openclaw 用户\n\t\tfix_openclaw_state_permissions 2>/dev/null || true\n\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\tchown openclaw:openclaw \"${CONFIG_FILE}.bak\" 2>/dev/null || true\n\t\treturn $rc\n\telse\n\t\techo \"ERROR: OpenClaw 未安装或 Node.js 不可用\"\n\t\treturn 1\n\tfi\n}\n\n# ── JSON 读写 (使用 Node.js) ──\njson_get() {\n\tif [ ! -f \"$CONFIG_FILE\" ]; then echo \"\"; return; fi\n\t_JS_KEY=\"$1\" \"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');\n\t\ttry{\n\t\t\tconst d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));\n\t\t\tconst ks=process.env._JS_KEY.split('.');let v=d;\n\t\t\tfor(const k of ks){v=v[k];if(v===undefined){console.log('');process.exit(0);}}\n\t\t\tif(typeof v==='object')console.log(JSON.stringify(v));else console.log(v);\n\t\t}catch(e){console.log('');}\n\t\" 2>/dev/null\n}\n\njson_set() {\n\tlocal key=\"$1\" value=\"$2\"\n\tlocal _js_err=\"\"\n\t\n\t# 步骤1: 确保配置文件存在\n\tif [ ! -f \"$CONFIG_FILE\" ]; then\n\t\t# 检查父目录是否可创建\n\t\tlocal parent_dir=\"$(dirname \"$CONFIG_FILE\")\"\n\t\tif ! mkdir -p \"$parent_dir\" 2>/dev/null; then\n\t\t\techo \"ERROR: 无法创建配置目录 $parent_dir\" >&2\n\t\t\techo \"HINT: 请检查 $OC_DATA 是否存在且有写权限\" >&2\n\t\t\treturn 1\n\t\tfi\n\t\t\n\t\t# 检查目录权限\n\t\tif [ ! -w \"$parent_dir\" ]; then\n\t\t\techo \"ERROR: 配置目录 $parent_dir 不可写\" >&2\n\t\t\techo \"HINT: 请运行: chmod 755 $parent_dir\" >&2\n\t\t\treturn 1\n\t\tfi\n\t\t\n\t\t# 尝试修复所有权\n\t\tfix_openclaw_state_permissions 2>/dev/null || true\n\t\t\n\t\t# 创建空配置文件\n\t\tif ! echo '{}' > \"$CONFIG_FILE\" 2>/dev/null; then\n\t\t\techo \"ERROR: 无法创建配置文件 $CONFIG_FILE\" >&2\n\t\t\techo \"HINT: 请检查磁盘空间和文件系统状态\" >&2\n\t\t\treturn 1\n\t\tfi\n\t\t\n\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\tfi\n\t\n\t# 步骤2: 检查配置文件是否可写\n\tif [ ! -w \"$CONFIG_FILE\" ]; then\n\t\techo \"ERROR: 配置文件 $CONFIG_FILE 不可写\" >&2\n\t\techo \"HINT: 请运行: chmod 644 $CONFIG_FILE\" >&2\n\t\treturn 1\n\tfi\n\t\n\t# 步骤3: 检查 Node.js 是否可用\n\tif [ ! -x \"$NODE_BIN\" ]; then\n\t\techo \"ERROR: Node.js 不可用: $NODE_BIN\" >&2\n\t\techo \"HINT: 请先运行 openclaw-env setup 安装 Node.js\" >&2\n\t\treturn 1\n\tfi\n\t\n\t# 步骤4: 使用临时文件传递值,避免环境变量转义问题\n\tlocal tmp_val_file=\"/tmp/.oc_json_val_$$\"\n\tif ! printf '%s' \"$value\" > \"$tmp_val_file\" 2>/dev/null; then\n\t\techo \"ERROR: 无法创建临时文件 $tmp_val_file\" >&2\n\t\treturn 1\n\tfi\n\t\n\t# 步骤5: 执行 JSON 写入\n\t_JS_KEY=\"$key\" _JS_DEBUG=\"${OC_CONFIG_DEBUG:-0}\" \"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');let d={};\n\t\tconst debug=process.env._JS_DEBUG==='1';\n\t\ttry{\n\t\t\tconst content=fs.readFileSync('${CONFIG_FILE}','utf8');\n\t\t\td=JSON.parse(content);\n\t\t}catch(e){\n\t\t\tif(debug)console.error('JSON parse warning:',e.message);\n\t\t}\n\t\tconst ks=process.env._JS_KEY.split('.');let o=d;\n\t\tfor(let i=0;i<ks.length-1;i++){\n\t\t\tif(!o[ks[i]]||typeof o[ks[i]]!=='object')o[ks[i]]={};\n\t\t\to=o[ks[i]];\n\t\t}\n\t\t// 读取值并作为字符串保存\n\t\tlet v=fs.readFileSync('${tmp_val_file}','utf8');\n\t\to[ks[ks.length-1]]=v;\n\t\ttry{\n\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\tif(debug)console.log('JSON saved successfully');\n\t\t}catch(e){\n\t\t\tconsole.error('ERROR: Failed to write config:',e.message);\n\t\t\tprocess.exit(1);\n\t\t}\n\t\" 2>&1\n\tlocal _js_rc=$?\n\t\n\t# 清理临时文件\n\trm -f \"$tmp_val_file\" 2>/dev/null\n\t\n\t# 步骤6: 检查执行结果\n\tif [ $_js_rc -ne 0 ]; then\n\t\techo \"ERROR: JSON 写入失败 (exit code: $_js_rc)\" >&2\n\t\treturn 1\n\tfi\n\t\n\t# 步骤7: 修复文件所有权\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\n\treturn 0\n}\n\n# ── 启用 auth 插件 ──\nenable_auth_plugins() {\n\t[ ! -f \"$CONFIG_FILE\" ] && return\n\t\"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');\n\t\ttry{\n\t\t\tconst d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));\n\t\t\tif(!d.plugins)d.plugins={};if(!d.plugins.entries)d.plugins.entries={};\n\t\t\tconst e=d.plugins.entries;\n\t\t\t// 启用有效的认证插件\n\t\t\t['copilot-proxy'].forEach(p=>{\n\t\t\t\tif(!e[p])e[p]={};e[p].enabled=true;\n\t\t\t});\n\t\t\t// 清理已废弃或未安装的插件配置 (v2026.4.9: 这些插件已移除或更名)\n\t\t\tdelete e['qwen-portal-auth'];\n\t\t\tdelete e['google-gemini-cli-auth'];\n\t\t\tdelete e['minimax-portal-auth'];\n\t\t\tdelete e['google-antigravity-auth'];\n\t\t\tdelete e['openclaw-weixin']; // 微信插件有独立安装流程,不应在 entries 中\n\t\t\t// 清理过时的 installs 配置\n\t\t\tif(d.plugins && d.plugins.installs){\n\t\t\t\tdelete d.plugins.installs['google-gemini-cli-auth'];\n\t\t\t\tdelete d.plugins.installs['minimax-portal-auth'];\n\t\t\t}\n\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t}catch(e){}\n\t\" 2>/dev/null\n}\n\n# ── 修复插件配置中的插件名称不匹配问题 ──\n# v2026.3.13: OpenClaw 加强了配置验证,plugins.allow 中的名称必须与实际插件名完全匹配\n# 问题: 旧版本写入的是 \"openclaw-qqbot\",但实际插件名是 \"@tencent-connect/openclaw-qqbot\"\n# 此函数会自动检测并修正不匹配的插件名称\nfix_plugin_config() {\n\t[ ! -f \"$CONFIG_FILE\" ] && return\n\t[ ! -x \"$NODE_BIN\" ] && return\n\t\n\t# 检查是否存在 qqbot 插件目录\n\tlocal qqbot_ext_dir=\"${OC_STATE_DIR}/extensions/openclaw-qqbot\"\n\t[ ! -d \"$qqbot_ext_dir\" ] && return\n\t\n\t# 读取并修复 plugins.allow 中的插件名称\n\tlocal fixed=0\n\t\"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');\n\t\ttry{\n\t\t\tconst d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));\n\t\t\tif(!d.plugins)d.plugins={};\n\t\t\t\n\t\t\t// 修复 plugins.allow 数组中的插件名称\n\t\t\tif(Array.isArray(d.plugins.allow)){\n\t\t\t\tconst oldName='openclaw-qqbot';\n\t\t\t\tconst newName='@tencent-connect/openclaw-qqbot';\n\t\t\t\tconst idx=d.plugins.allow.indexOf(oldName);\n\t\t\t\tif(idx!==-1){\n\t\t\t\t\t// 检查是否已有正确的名称\n\t\t\t\t\tif(!d.plugins.allow.includes(newName)){\n\t\t\t\t\t\td.plugins.allow[idx]=newName;\n\t\t\t\t\t\tconsole.log('FIXED');\n\t\t\t\t\t}else{\n\t\t\t\t\t\t// 已有正确名称,删除错误的\n\t\t\t\t\t\td.plugins.allow.splice(idx,1);\n\t\t\t\t\t\tconsole.log('REMOVED_DUPLICATE');\n\t\t\t\t\t}\n\t\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// 同时修复 plugins.entries 中的键名\n\t\t\tif(d.plugins.entries && d.plugins.entries['openclaw-qqbot']){\n\t\t\t\tif(!d.plugins.entries['@tencent-connect/openclaw-qqbot']){\n\t\t\t\t\td.plugins.entries['@tencent-connect/openclaw-qqbot']=d.plugins.entries['openclaw-qqbot'];\n\t\t\t\t}\n\t\t\t\tdelete d.plugins.entries['openclaw-qqbot'];\n\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\tconsole.log('FIXED_ENTRIES');\n\t\t\t}\n\t\t}catch(e){}\n\t\" 2>/dev/null | while read line; do\n\t\tcase \"$line\" in\n\t\t\tFIXED|REMOVED_DUPLICATE|FIXED_ENTRIES)\n\t\t\t\techo -e \" ${GREEN}✅ 已修复插件配置名称: openclaw-qqbot → @tencent-connect/openclaw-qqbot${NC}\"\n\t\t\t\tfixed=1\n\t\t\t\t;;\n\t\tesac\n\tdone\n\t\n\t# 确保配置文件权限正确\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\n\treturn $fixed\n}\n\n# ── 确保 qqbot 插件在 plugins.allow 中 ──\nensure_qqbot_plugin_allowed() {\n\t[ ! -f \"$CONFIG_FILE\" ] && return\n\t[ ! -x \"$NODE_BIN\" ] && return\n\t\n\t\"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');\n\t\ttry{\n\t\t\tconst d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));\n\t\t\tif(!d.plugins)d.plugins={};\n\t\t\tif(!Array.isArray(d.plugins.allow))d.plugins.allow=[];\n\t\t\t\n\t\t\tconst correctName='@tencent-connect/openclaw-qqbot';\n\t\t\tconst oldName='openclaw-qqbot';\n\t\t\t\n\t\t\t// 移除旧的不正确名称\n\t\t\td.plugins.allow=d.plugins.allow.filter(n=>n!==oldName);\n\t\t\t\n\t\t\t// 添加正确的名称(如果不存在)\n\t\t\tif(!d.plugins.allow.includes(correctName)){\n\t\t\t\td.plugins.allow.push(correctName);\n\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\tconsole.log('ADDED');\n\t\t\t}\n\t\t}catch(e){}\n\t\" 2>/dev/null\n\t\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n}\n\n# ── 模型认证: 将 API Key 写入 auth-profiles.json (而非 openclaw.json) ──\n# 用法: auth_set_apikey <provider> <api_key> [profile_id]\n# 例: auth_set_apikey openai sk-xxx\n# auth_set_apikey anthropic sk-ant-xxx\n# auth_set_apikey openai-compatible sk-xxx custom:manual\nauth_set_apikey() {\n\tlocal provider=\"$1\" api_key=\"$2\" profile_id=\"${3:-${1}:manual}\"\n\tlocal auth_dir=\"${OC_STATE_DIR}/agents/main/agent\"\n\tlocal auth_file=\"${auth_dir}/auth-profiles.json\"\n\tmkdir -p \"$auth_dir\"\n\tchown -R openclaw:openclaw \"${OC_STATE_DIR}/agents\" 2>/dev/null || true\n\t_AP_PROVIDER=\"$provider\" _AP_KEY=\"$api_key\" _AP_PROFILE=\"$profile_id\" \"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs'),f=process.env._AP_FILE||'${auth_file}';\n\t\tlet d={version:1,profiles:{},usageStats:{}};\n\t\ttry{d=JSON.parse(fs.readFileSync(f,'utf8'));}catch(e){}\n\t\tif(!d.profiles)d.profiles={};\n\t\td.profiles[process.env._AP_PROFILE]={\n\t\t\ttype:'api_key',\n\t\t\tprovider:process.env._AP_PROVIDER,\n\t\t\tkey:process.env._AP_KEY\n\t\t};\n\t\tfs.writeFileSync(f,JSON.stringify(d,null,2));\n\t\" 2>/dev/null\n\tchown openclaw:openclaw \"$auth_file\" 2>/dev/null || true\n}\n\n# ── GitHub Copilot Token 写入 auth-profiles.json (type:token) ──\n# GitHub Copilot 使用 token 类型而非 api_key,OpenClaw 会自动兑换 Copilot session token\n# 用法: auth_set_copilot_token <github_token>\nauth_set_copilot_token() {\n\tlocal github_token=\"$1\"\n\tlocal auth_dir=\"${OC_STATE_DIR}/agents/main/agent\"\n\tlocal auth_file=\"${auth_dir}/auth-profiles.json\"\n\tmkdir -p \"$auth_dir\"\n\tchown -R openclaw:openclaw \"${OC_STATE_DIR}/agents\" 2>/dev/null || true\n\t_AP_TOKEN=\"$github_token\" \"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs'),f='${auth_file}';\n\t\tlet d={version:1,profiles:{},usageStats:{}};\n\t\ttry{d=JSON.parse(fs.readFileSync(f,'utf8'));}catch(e){}\n\t\tif(!d.profiles)d.profiles={};\n\t\td.profiles['github-copilot:github']={\n\t\t\ttype:'token',\n\t\t\tprovider:'github-copilot',\n\t\t\ttoken:process.env._AP_TOKEN\n\t\t};\n\t\tfs.writeFileSync(f,JSON.stringify(d,null,2));\n\t\" 2>/dev/null\n\tchown openclaw:openclaw \"$auth_file\" 2>/dev/null || true\n}\n\n# ── 注册模型到 agents.defaults.models 并设为默认 ──\n# 用法: register_and_set_model <model_id>\n# 例: register_and_set_model openai/gpt-5.2\n# register_and_set_model anthropic/claude-sonnet-4\n# 注意: 模型 ID 可能含 \".\" (如 gpt-5.2),不能用 json_set (以 . 分割路径)\nregister_and_set_model() {\n\tlocal model_id=\"$1\"\n\t_RSM_MID=\"$model_id\" \"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');\n\t\tlet d={};\n\t\ttry{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}\n\t\tif(!d.agents)d.agents={};\n\t\tif(!d.agents.defaults)d.agents.defaults={};\n\t\tif(!d.agents.defaults.models)d.agents.defaults.models={};\n\t\tif(!d.agents.defaults.model)d.agents.defaults.model={};\n\t\tconst mid=process.env._RSM_MID;\n\t\td.agents.defaults.models[mid]={};\n\t\td.agents.defaults.model.primary=mid;\n\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\" 2>/dev/null\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n}\n\n# ── 注册自定义提供商 (需要 baseUrl 的 OpenAI 兼容提供商) ──\n# 用法: register_custom_provider <provider_name> <base_url> <api_key> <model_id> [model_display_name] [context_window] [max_tokens]\n# 例: register_custom_provider dashscope https://dashscope.aliyuncs.com/compatible-mode/v1 sk-xxx qwen-max \"Qwen Max\"\n# 例: register_custom_provider bailian https://coding.dashscope.aliyuncs.com/v1 sk-sp-xxx qwen3.5-plus \"qwen3.5-plus\" 1000000 65536\nregister_custom_provider() {\n\tlocal provider_name=\"$1\" base_url=\"$2\" api_key=\"$3\" model_id=\"$4\" model_display=\"${5:-$4}\"\n\tlocal ctx_window=\"${6:-128000}\" max_tok=\"${7:-32000}\"\n\t_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 \"\n\t\tconst fs=require('fs');\n\t\tlet d={};\n\t\ttry{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}\n\t\tif(!d.models)d.models={};\n\t\tif(!d.models.providers)d.models.providers={};\n\t\td.models.mode='merge';\n\t\tconst prov=process.env._RCP_PROV;\n\t\td.models.providers[prov]={\n\t\t\tbaseUrl:process.env._RCP_URL,\n\t\t\tapiKey:process.env._RCP_KEY,\n\t\t\tapi:'openai-completions',\n\t\t\tmodels:[{\n\t\t\t\tid:process.env._RCP_MID,\n\t\t\t\tname:process.env._RCP_MNAME,\n\t\t\t\treasoning:false,\n\t\t\t\tinput:['text','image'],\n\t\t\t\tcost:{input:0,output:0,cacheRead:0,cacheWrite:0},\n\t\t\t\tcontextWindow:parseInt(process.env._RCP_CTX)||128000,\n\t\t\t\tmaxTokens:parseInt(process.env._RCP_MAXTOK)||32000\n\t\t\t}]\n\t\t};\n\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\" 2>/dev/null\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n}\n\n# ── 注册 Coding Plan 提供商 (多模型批量注册) ──\n# 用法: register_codingplan_provider <api_key>\n# 按阿里云官方文档注册 bailian 提供商,包含所有 Coding Plan 套餐支持的模型\nregister_codingplan_provider() {\n\tlocal api_key=\"$1\"\n\t_RCP_KEY=\"$api_key\" \"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');\n\t\tlet d={};\n\t\ttry{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}\n\t\tif(!d.models)d.models={};\n\t\tif(!d.models.providers)d.models.providers={};\n\t\td.models.mode='merge';\n\t\td.models.providers['bailian']={\n\t\t\tbaseUrl:'https://coding.dashscope.aliyuncs.com/v1',\n\t\t\tapiKey:process.env._RCP_KEY,\n\t\t\tapi:'openai-completions',\n\t\t\tmodels:[\n\t\t\t\t{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},\n\t\t\t\t{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},\n\t\t\t\t{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},\n\t\t\t\t{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},\n\t\t\t\t{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},\n\t\t\t\t{id:'glm-5',name:'glm-5',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:202752,maxTokens:16384},\n\t\t\t\t{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},\n\t\t\t\t{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}\n\t\t\t]\n\t\t};\n\t\tif(!d.agents)d.agents={};\n\t\tif(!d.agents.defaults)d.agents.defaults={};\n\t\tif(!d.agents.defaults.models)d.agents.defaults.models={};\n\t\t['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=>{\n\t\t\td.agents.defaults.models['bailian/'+m]={};\n\t\t});\n\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\" 2>/dev/null\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n}\n\n# ── 注册腾讯云 Coding Plan 提供商 (多模型批量注册) ──\n# 用法: register_lkeap_codingplan_provider <api_key>\n# 按腾讯云官方文档 (https://cloud.tencent.com/document/product/1772/128949) 注册\n# provider name: lkeap, Base URL: https://api.lkeap.cloud.tencent.com/coding/v3\nregister_lkeap_codingplan_provider() {\n\tlocal api_key=\"$1\"\n\t_RCP_KEY=\"$api_key\" \"$NODE_BIN\" -e \"\n\t\tconst fs=require('fs');\n\t\tlet d={};\n\t\ttry{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}\n\t\tif(!d.models)d.models={};\n\t\tif(!d.models.providers)d.models.providers={};\n\t\td.models.mode='merge';\n\t\td.models.providers['lkeap']={\n\t\t\tbaseUrl:'https://api.lkeap.cloud.tencent.com/coding/v3',\n\t\t\tapiKey:process.env._RCP_KEY,\n\t\t\tapi:'openai-completions',\n\t\t\tmodels:[\n\t\t\t\t{id:'tc-code-latest',name:'Auto (智能匹配最优模型)',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:128000,maxTokens:8192},\n\t\t\t\t{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},\n\t\t\t\t{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},\n\t\t\t\t{id:'hunyuan-t1',name:'Hunyuan-T1',reasoning:true,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:32000,maxTokens:64000},\n\t\t\t\t{id:'hunyuan-turbos',name:'Hunyuan-TurboS',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:32000,maxTokens:16000},\n\t\t\t\t{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},\n\t\t\t\t{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},\n\t\t\t\t{id:'glm-5',name:'GLM-5',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:202752,maxTokens:8192}\n\t\t\t]\n\t\t};\n\t\tif(!d.agents)d.agents={};\n\t\tif(!d.agents.defaults)d.agents.defaults={};\n\t\tif(!d.agents.defaults.models)d.agents.defaults.models={};\n\t\t['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=>{\n\t\t\td.agents.defaults.models['lkeap/'+m]={};\n\t\t});\n\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\" 2>/dev/null\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n}\n\n# ── 辅助函数 ──\n\n# 清理输入: 去除 ANSI 转义序列、不可见字符,只保留 ASCII 可打印字符\nsanitize_input() {\n\t# 1) tr -cd ' -~' : 只保留 ASCII 0x20-0x7E (去除 ESC/控制字符/Unicode 不可见字符)\n\t# 2) sed : 去除 ESC 被剥离后残余的 CSI 序列 (如 bracketed paste 的 [200~ [201~)\n\t# 3) sed : 去除首尾空白\n\tprintf '%s' \"$1\" | tr -cd ' -~' | sed 's/\\[[0-9;]*[a-zA-Z~]//g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'\n}\n\nprompt_with_default() {\n\tlocal prompt=\"$1\" default=\"$2\" varname=\"$3\"\n\tif [ -n \"$default\" ]; then\n\t\tprintf \" ${CYAN}${prompt} [${default}]:${NC} \" >&2\n\telse\n\t\tprintf \" ${CYAN}${prompt}:${NC} \" >&2\n\tfi\n\tread input\n\tinput=$(sanitize_input \"$input\")\n\tlocal _result=\"${input:-$default}\"\n\texport \"__prompt_result__=$_result\"\n\teval \"$varname=\\\"\\$__prompt_result__\\\"\"\n\tunset __prompt_result__\n}\n\nconfirm_yes() {\n\tlocal ans=\"$1\"\n\tcase \"$ans\" in y|Y|yes|YES|Yes) return 0 ;; *) return 1 ;; esac\n}\n\nrestart_gateway() {\n\techo \"\"\n\techo -e \" ${YELLOW}正在重启 Gateway...${NC}\"\n\n\t# 修复数据目录权限 (root 用户操作可能改变了文件属主)\n\tfix_openclaw_state_permissions 2>/dev/null || true\n\n\t# 首次安装后 UCI 默认保持 disabled。用户在配置向导中确认“立即重启”\n\t# 即表示希望当前配置生效,这里同步启用服务,避免提示重启但实际仍为“已禁用”。\n\tif [ \"$(uci -q get openclaw.main.enabled 2>/dev/null || echo 0)\" != \"1\" ]; then\n\t\techo -e \" ${CYAN}检测到 Gateway 未启用,正在启用服务...${NC}\"\n\t\tuci -q set openclaw.main.enabled='1' 2>/dev/null || true\n\t\tuci -q commit openclaw 2>/dev/null || true\n\t\t/etc/init.d/openclaw enable >/dev/null 2>&1 || true\n\tfi\n\n\tlocal port\n\tport=$(json_get gateway.port)\n\tport=${port:-18789}\n\n\t# ── kill gateway 进程,让 procd respawn ──\n\t/etc/init.d/openclaw restart_gateway >/dev/null 2>&1\n\t/etc/init.d/openclaw start >/dev/null 2>&1\n\n\t# ── 等待端口恢复 (最多 30 秒,含端口释放 + Node.js 冷启动) ──\n\techo -e \" ${YELLOW}⏳ Gateway 启动中,请稍候 (约 15-30 秒)...${NC}\"\n\tlocal waited=0\n\twhile [ $waited -lt 30 ]; do\n\t\tsleep 3\n\t\twaited=$((waited + 3))\n\t\tif check_port_listening \"$port\"; then\n\t\t\techo -e \" ${GREEN}✅ Gateway 已重启成功 (${waited}秒)${NC}\"\n\t\t\treturn 0\n\t\tfi\n\tdone\n\n\t# 30 秒内没就绪,提示用户但不继续阻塞\n\techo -e \" ${YELLOW}⏳ Gateway 仍在启动中,请稍后确认${NC}\"\n\techo -e \" ${CYAN} 查看日志: logread -e openclaw${NC}\"\n\techo \"\"\n}\n\nask_restart() {\n\tprompt_with_default \"是否立即重启 Gateway? (y/n)\" \"y\" do_restart\n\tconfirm_yes \"$do_restart\" && restart_gateway\n}\n\n# ── 查看当前配置 ──\nshow_current_config() {\n\techo \"\"\n\techo -e \"${GREEN}┌──────────────────────────────────────────────────────────┐${NC}\"\n\techo -e \"${GREEN}│${NC} 📋 ${BOLD}当前配置概览${NC}\"\n\techo -e \"${GREEN}├──────────────────────────────────────────────────────────┤${NC}\"\n\n\tlocal port=$(json_get gateway.port)\n\tlocal bind=$(json_get gateway.bind)\n\tlocal mode=$(json_get gateway.mode)\n\techo -e \"${GREEN}│${NC} 网关端口 ............ ${CYAN}${port:-18789}${NC}\"\n\techo -e \"${GREEN}│${NC} 绑定模式 ............ ${CYAN}${bind:-lan}${NC}\"\n\techo -e \"${GREEN}│${NC} 运行模式 ............ ${CYAN}${mode:-local}${NC}\"\n\n\tlocal model=$(json_get agents.defaults.model.primary)\n\tif [ -n \"$model\" ]; then\n\t\techo -e \"${GREEN}│${NC} 活跃模型 ............ ${CYAN}${model}${NC}\"\n\telse\n\t\techo -e \"${GREEN}│${NC} 活跃模型 ............ ${YELLOW}未配置${NC}\"\n\tfi\n\n\techo -e \"${GREEN}├──────────────────────────────────────────────────────────┤${NC}\"\n\techo -e \"${GREEN}│${NC} ${BOLD}渠道配置状态${NC}\"\n\n\t# 检测微信渠道\n\tlocal wechat_ext_dir=\"${OC_STATE_DIR}/extensions/openclaw-weixin\"\n\tlocal wechat_npm_plugin=\"\"\n\tif [ -d \"${OC_STATE_DIR}/npm/projects\" ]; then\n\t\twechat_npm_plugin=$(find \"${OC_STATE_DIR}/npm/projects\" -path \"*/node_modules/@tencent-weixin/openclaw-weixin/openclaw.plugin.json\" -type f 2>/dev/null | head -n 1)\n\tfi\n\tif { [ -d \"$wechat_ext_dir\" ] && [ -f \"${wechat_ext_dir}/openclaw.plugin.json\" ]; } || [ -n \"$wechat_npm_plugin\" ]; then\n\t\techo -e \"${GREEN}│${NC} 微信 .............. ${GREEN}✅ 已配置${NC}\"\n\telse\n\t\techo -e \"${GREEN}│${NC} 微信 .............. ${YELLOW}❌ 未配置${NC}\"\n\tfi\n\n\tlocal tg_token=$(json_get channels.telegram.botToken)\n\tlocal dc_token=$(json_get channels.discord.botToken)\n\tlocal fs_appid=$(json_get channels.feishu.appId)\n\tlocal sk_token=$(json_get channels.slack.botToken)\n\tlocal qq_appid=$(json_get channels.qqbot.appId)\n\n\tif [ -n \"$qq_appid\" ]; then\n\t\tlocal qq_short=$(echo \"$qq_appid\" | cut -c1-8)\n\t\techo -e \"${GREEN}│${NC} QQ (qqbot) ......... ${GREEN}✅ 已配置${NC} (AppID: ${qq_short}...)\"\n\telse\n\t\techo -e \"${GREEN}│${NC} QQ (qqbot) ......... ${YELLOW}❌ 未配置${NC}\"\n\tfi\n\tif [ -n \"$tg_token\" ]; then\n\t\tlocal tg_short=$(echo \"$tg_token\" | cut -c1-12)\n\t\techo -e \"${GREEN}│${NC} Telegram ............ ${GREEN}✅ 已配置${NC} (${tg_short}...)\"\n\telse\n\t\techo -e \"${GREEN}│${NC} Telegram ............ ${YELLOW}❌ 未配置${NC}\"\n\tfi\n\tif [ -n \"$dc_token\" ]; then\n\t\techo -e \"${GREEN}│${NC} Discord ............. ${GREEN}✅ 已配置${NC}\"\n\telse\n\t\techo -e \"${GREEN}│${NC} Discord ............. ${YELLOW}❌ 未配置${NC}\"\n\tfi\n\tif [ -n \"$fs_appid\" ]; then\n\t\tlocal fs_short=$(echo \"$fs_appid\" | cut -c1-6)\n\t\techo -e \"${GREEN}│${NC} 飞书 ................ ${GREEN}✅ 已配置${NC} (AppID: ${fs_short}...)\"\n\telse\n\t\techo -e \"${GREEN}│${NC} 飞书 ................ ${YELLOW}❌ 未配置${NC}\"\n\tfi\n\tif [ -n \"$sk_token\" ]; then\n\t\techo -e \"${GREEN}│${NC} Slack ............... ${GREEN}✅ 已配置${NC}\"\n\telse\n\t\techo -e \"${GREEN}│${NC} Slack ............... ${YELLOW}❌ 未配置${NC}\"\n\tfi\n\n\techo -e \"${GREEN}└──────────────────────────────────────────────────────────┘${NC}\"\n\n\techo \"\"\n\techo -e \" ${BOLD}系统信息:${NC}\"\n\techo -e \" Node.js: $(\"$NODE_BIN\" -v 2>/dev/null || echo '未安装')\"\n\tif [ -n \"$OC_ENTRY\" ]; then\n\t\techo -e \" OpenClaw: $(oc_cmd --version 2>/dev/null || echo '未知')\"\n\telse\n\t\techo -e \" OpenClaw: 未安装\"\n\tfi\n\techo -e \" 架构: $(uname -m)\"\n\tlocal mem_total=$(awk '/MemTotal/{printf \"%.0f\", $2/1024}' /proc/meminfo 2>/dev/null || echo \"?\")\n\techo -e \" 内存: ${mem_total} MB\"\n}\n\n# ══════════════════════════════════════════════════════════════\n# 配置 AI 模型\n# ══════════════════════════════════════════════════════════════\nconfigure_model() {\n\techo \"\"\n\techo -e \" ${BOLD}🤖 配置 AI 模型和提供商${NC}\"\n\techo \"\"\n\techo -e \" ${GREEN}${BOLD}🌟 ── 推荐 ──${NC}\"\n\techo -e \" ${CYAN}w)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}\"\n\techo \"\"\n\techo -e \" ${BOLD}🌍 ── 国外模型提供商 ──${NC}\"\n\techo -e \" ${CYAN}a)${NC} OpenAI (GPT-5.2, GPT-5 mini, GPT-4.1)\"\n\techo -e \" ${CYAN}b)${NC} Anthropic (Claude Sonnet 4, Opus 4, Haiku)\"\n\techo -e \" ${CYAN}c)${NC} Google Gemini (Gemini 2.5 Pro/Flash, Gemini 3)\"\n\techo -e \" ${CYAN}d)${NC} OpenRouter (聚合多家模型)\"\n\techo -e \" ${CYAN}e)${NC} GitHub Copilot (需要 Copilot 订阅)\"\n\techo -e \" ${CYAN}f)${NC} xAI Grok (Grok-4/3)\"\n\techo \"\"\n\techo -e \" ${BOLD}🇨🇳 ── 国内模型提供商 ──${NC}\"\n\techo -e \" ${CYAN}g)${NC} 阿里云通义千问 Qwen (Portal/API/Coding Plan)\"\n\techo -e \" ${CYAN}h)${NC} 硅基流动 SiliconFlow\"\n\techo -e \" ${CYAN}i)${NC} 腾讯云 Coding Plan (HY T1/TurboS/GLM-5/Kimi)\"\n\techo -e \" ${CYAN}j)${NC} 百度千帆 (ERNIE-4.0, ERNIE-3.5)\"\n\techo -e \" ${CYAN}k)${NC} 智谱 GLM / Z.AI (GLM-5.1, GLM-5)\"\n\techo \"\"\n\techo -e \" ${BOLD}🏠 ── 本地模型 / 自定义 API ──${NC}\"\n\techo -e \" ${CYAN}l)${NC} Ollama (本地模型,无需 API Key)\"\n\techo -e \" ${CYAN}m)${NC} 自定义 OpenAI 兼容 API\"\n\techo -e \" ${CYAN}n)${NC} 自定义 Anthropic 兼容 API\"\n\techo -e \" ${CYAN}o)${NC} 一万AI分享 粉丝专享 API\"\n\techo \"\"\n\techo -e \" ${CYAN}q)${NC} 返回\"\n\techo \"\"\n\tprompt_with_default \"请选择\" \"w\" choice\n\n\tcase \"$choice\" in\n\t\tw)\n\t\t\techo \"\"\n\t\t\techo -e \" ${CYAN}启动官方完整模型配置向导...${NC}\"\n\t\t\techo -e \" ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${CYAN}清理过时插件配置...${NC}\"\n\t\t\tenable_auth_plugins\n\t\t\techo \"\"\n\t\t\t(oc_cmd configure --section model) || echo -e \" ${YELLOW}配置向导已退出${NC}\"\n\t\t\techo \"\"\n\t\t\task_restart\n\t\t\t;;\n\t\ta)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}OpenAI 配置${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: https://platform.openai.com/api-keys${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入 OpenAI API Key (sk-...)\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\tauth_set_apikey openai \"$api_key\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型:${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} gpt-5.2 — 最强编程与代理旗舰 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} gpt-5-mini — 高性价比推理\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} gpt-5-nano — 极速低成本\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} gpt-4.1 — 最强非推理模型\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} o3 — 推理模型\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} o4-mini — 推理轻量\"\n\t\t\t\techo -e \" ${CYAN}g)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"gpt-5.2\" ;;\n\t\t\t\t\tb) model_name=\"gpt-5-mini\" ;;\n\t\t\t\t\tc) model_name=\"gpt-5-nano\" ;;\n\t\t\t\t\td) model_name=\"gpt-4.1\" ;;\n\t\t\t\t\te) model_name=\"o3\" ;;\n\t\t\t\t\tf) model_name=\"o4-mini\" ;;\n\t\t\t\t\tg) prompt_with_default \"请输入模型名称\" \"gpt-5.2\" model_name ;;\n\t\t\t\t\t*) model_name=\"gpt-5.2\" ;;\n\t\t\t\tesac\n\t\t\t\tregister_and_set_model \"openai/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ OpenAI 已配置,活跃模型: openai/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tb)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}Anthropic 配置${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: https://console.anthropic.com/settings/keys${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入 Anthropic API Key (sk-ant-...)\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\tauth_set_apikey anthropic \"$api_key\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型:${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} claude-sonnet-4-20250514 — Claude Sonnet 4 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} claude-opus-4-20250514 — Claude Opus 4 顶级推理\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} claude-haiku-4-5 — Claude Haiku 4.5 轻量快速\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} claude-sonnet-4.5 — Claude Sonnet 4.5\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} claude-sonnet-4.6 — Claude Sonnet 4.6\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"claude-sonnet-4-20250514\" ;;\n\t\t\t\t\tb) model_name=\"claude-opus-4-20250514\" ;;\n\t\t\t\t\tc) model_name=\"claude-haiku-4-5\" ;;\n\t\t\t\t\td) model_name=\"claude-sonnet-4-5\" ;;\n\t\t\t\t\te) model_name=\"claude-sonnet-4-6\" ;;\n\t\t\t\t\tf) prompt_with_default \"请输入模型名称\" \"claude-sonnet-4-20250514\" model_name ;;\n\t\t\t\t\t*) model_name=\"claude-sonnet-4-20250514\" ;;\n\t\t\t\tesac\n\t\t\t\tregister_and_set_model \"anthropic/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ Anthropic 已配置,活跃模型: anthropic/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tc)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}Google Gemini 配置${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: https://aistudio.google.com/apikey${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入 Google AI API Key\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\tauth_set_apikey google \"$api_key\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型:${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} gemini-2.5-pro — 旗舰推理 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} gemini-2.5-flash — 快速均衡\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} gemini-2.5-flash-lite — 极速低成本\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} gemini-3-flash-preview — Gemini 3 Flash 预览\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} gemini-3-pro-preview — Gemini 3 Pro 预览\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"gemini-2.5-pro\" ;;\n\t\t\t\t\tb) model_name=\"gemini-2.5-flash\" ;;\n\t\t\t\t\tc) model_name=\"gemini-2.5-flash-lite\" ;;\n\t\t\t\t\td) model_name=\"gemini-3-flash-preview\" ;;\n\t\t\t\t\te) model_name=\"gemini-3-pro-preview\" ;;\n\t\t\t\t\tf) prompt_with_default \"请输入模型名称\" \"gemini-2.5-pro\" model_name ;;\n\t\t\t\t\t*) model_name=\"gemini-2.5-pro\" ;;\n\t\t\t\tesac\n\t\t\t\tregister_and_set_model \"google/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ Google Gemini 已配置,活跃模型: google/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\td)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}OpenRouter 配置${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: https://openrouter.ai/keys${NC}\"\n\t\t\techo -e \" ${YELLOW}聚合多家模型,一个 Key 可调用所有主流模型${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入 OpenRouter API Key\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\tauth_set_apikey openrouter \"$api_key\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}常用模型 (格式: provider/model):${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} anthropic/claude-sonnet-4 — Claude Sonnet 4 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} anthropic/claude-opus-4 — Claude Opus 4\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} openai/gpt-5.2 — GPT-5.2\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} google/gemini-2.5-pro — Gemini 2.5 Pro\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} deepseek/deepseek-r1 — DeepSeek R1\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} meta-llama/llama-4-maverick — Meta Llama 4\"\n\t\t\t\techo -e \" ${CYAN}g)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"anthropic/claude-sonnet-4\" ;;\n\t\t\t\t\tb) model_name=\"anthropic/claude-opus-4\" ;;\n\t\t\t\t\tc) model_name=\"openai/gpt-5.2\" ;;\n\t\t\t\t\td) model_name=\"google/gemini-2.5-pro\" ;;\n\t\t\t\t\te) model_name=\"deepseek/deepseek-r1\" ;;\n\t\t\t\t\tf) model_name=\"meta-llama/llama-4-maverick\" ;;\n\t\t\t\t\tg) prompt_with_default \"请输入模型名称\" \"anthropic/claude-sonnet-4\" model_name ;;\n\t\t\t\t\t*) model_name=\"anthropic/claude-sonnet-4\" ;;\n\t\t\t\tesac\n\t\t\t\tregister_and_set_model \"openrouter/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ OpenRouter 已配置,活跃模型: openrouter/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\te)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}GitHub Copilot 配置${NC}\"\n\t\t\techo -e \" ${YELLOW}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${CYAN}启动 GitHub Copilot OAuth 登录 (Device Flow)...${NC}\"\n\t\t\techo -e \" ${DIM}请在浏览器中打开显示的 URL,输入授权码完成登录${NC}\"\n\t\t\techo \"\"\n\t\t\tif oc_cmd models auth login-github-copilot --yes; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${GREEN}✅ GitHub Copilot OAuth 认证成功${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}选择默认模型:${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}── GPT 系列 ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} github-copilot/gpt-4.1 — GPT-4.1 ${GREEN}(推荐)${NC}\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} github-copilot/gpt-4o — GPT-4o\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} github-copilot/gpt-5 — GPT-5\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} github-copilot/gpt-5-mini — GPT-5 mini\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} github-copilot/gpt-5.1 — GPT-5.1\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} github-copilot/gpt-5.2 — GPT-5.2\"\n\t\t\t\techo -e \" ${CYAN}g)${NC} github-copilot/gpt-5.2-codex — GPT-5.2 Codex\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}── Claude 系列 ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}h)${NC} github-copilot/claude-sonnet-4 — Claude Sonnet 4\"\n\t\t\t\techo -e \" ${CYAN}i)${NC} github-copilot/claude-sonnet-4.5 — Claude Sonnet 4.5\"\n\t\t\t\techo -e \" ${CYAN}j)${NC} github-copilot/claude-sonnet-4.6 — Claude Sonnet 4.6\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}── Gemini 系列 ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}k)${NC} github-copilot/gemini-2.5-pro — Gemini 2.5 Pro\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}m)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"github-copilot/gpt-4.1\" ;;\n\t\t\t\t\tb) model_name=\"github-copilot/gpt-4o\" ;;\n\t\t\t\t\tc) model_name=\"github-copilot/gpt-5\" ;;\n\t\t\t\t\td) model_name=\"github-copilot/gpt-5-mini\" ;;\n\t\t\t\t\te) model_name=\"github-copilot/gpt-5.1\" ;;\n\t\t\t\t\tf) model_name=\"github-copilot/gpt-5.2\" ;;\n\t\t\t\t\tg) model_name=\"github-copilot/gpt-5.2-codex\" ;;\n\t\t\t\t\th) model_name=\"github-copilot/claude-sonnet-4\" ;;\n\t\t\t\t\ti) model_name=\"github-copilot/claude-sonnet-4.5\" ;;\n\t\t\t\t\tj) model_name=\"github-copilot/claude-sonnet-4.6\" ;;\n\t\t\t\t\tk) model_name=\"github-copilot/gemini-2.5-pro\" ;;\n\t\t\t\t\tm) prompt_with_default \"请输入模型名称\" \"github-copilot/gpt-4.1\" model_name ;;\n\t\t\t\t\t*) model_name=\"github-copilot/gpt-4.1\" ;;\n\t\t\t\tesac\n\t\t\t\tregister_and_set_model \"$model_name\"\n\t\t\t\techo -e \" ${GREEN}✅ 活跃模型已设置: ${model_name}${NC}\"\n\t\t\telse\n\t\t\t\techo -e \" ${YELLOW}OAuth 授权已退出或失败${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tg)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}阿里云通义千问 Qwen 配置${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${CYAN}配置方式:${NC}\"\n\t\t\techo -e \" ${CYAN}a)${NC} 通过官方向导配置 (Qwen Portal OAuth)\"\n\t\t\techo -e \" ${CYAN}b)${NC} 百炼按量付费 API Key (sk-xxx, 按 token 计费)\"\n\t\t\techo -e \" ${CYAN}c)${NC} ${GREEN}Coding Plan 套餐${NC} (sk-sp-xxx, 按套餐抵扣额度) ${GREEN}★ 推荐${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${DIM}提示: Coding Plan 套餐和百炼按量付费的 API Key / Base URL 不互通,请勿混用${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请选择\" \"c\" qwen_mode\n\t\t\tcase \"$qwen_mode\" in\n\t\t\t\ta)\n\t\t\t\t\techo \"\"\n\t\t\t\t\techo -e \" ${CYAN}启用 Qwen Portal Auth 插件...${NC}\"\n\t\t\t\t\tenable_auth_plugins\n\t\t\t\t\techo -e \" ${CYAN}启动 Qwen OAuth 授权...${NC}\"\n\t\t\t\t\toc_cmd models auth login --provider qwen-portal --set-default || echo -e \" ${YELLOW}OAuth 授权已退出${NC}\"\n\t\t\t\t\techo \"\"\n\t\t\t\t\task_restart\n\t\t\t\t\treturn\n\t\t\t\t\t;;\n\t\t\t\tb)\n\t\t\t\t\techo \"\"\n\t\t\t\t\techo -e \" ${BOLD}百炼按量付费配置${NC}\"\n\t\t\t\t\techo -e \" ${YELLOW}获取 API Key: https://dashscope.console.aliyun.com/apiKey${NC}\"\n\t\t\t\t\techo -e \" ${DIM}Base URL: https://dashscope.aliyuncs.com/compatible-mode/v1${NC}\"\n\t\t\t\t\techo \"\"\n\t\t\t\t\tprompt_with_default \"请输入百炼 API Key (sk-...)\" \"\" api_key\n\t\t\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\techo -e \" ${CYAN}── 千问商业版 ──${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN}a)${NC} qwen-max — 千问Max 旗舰模型 (推荐)\"\n\t\t\t\t\t\techo -e \" ${CYAN}b)${NC} qwen-plus — 千问Plus 均衡之选 (已升级Qwen3.5)\"\n\t\t\t\t\t\techo -e \" ${CYAN}c)${NC} qwen-flash — 千问Flash 速度最快 (已升级Qwen3.5)\"\n\t\t\t\t\t\techo -e \" ${CYAN}d)${NC} qwen-turbo — 千问Turbo 经济实惠\"\n\t\t\t\t\t\techo -e \" ${CYAN}e)${NC} qwen-long — 千问Long 超长上下文 (1000万Token)\"\n\t\t\t\t\t\techo -e \" ${CYAN}── 千问Coder ──${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN}f)${NC} qwen3-coder-plus — 代码专用旗舰 (100万上下文)\"\n\t\t\t\t\t\techo -e \" ${CYAN}g)${NC} qwen3-coder-flash — 代码专用极速\"\n\t\t\t\t\t\techo -e \" ${CYAN}── 推理模型 ──${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN}h)${NC} qwq-plus — QwQ推理模型 (数学/代码强化)\"\n\t\t\t\t\t\techo -e \" ${CYAN}── 千问开源版 ──${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN}i)${NC} qwen3-235b-a22b — Qwen3 235B MoE\"\n\t\t\t\t\t\techo -e \" ${CYAN}j)${NC} qwen3-32b — Qwen3 32B\"\n\t\t\t\t\t\techo -e \" ${CYAN}k)${NC} qwen3-30b-a3b — Qwen3 30B MoE\"\n\t\t\t\t\t\techo -e \" ${CYAN}── 第三方模型 ──${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN}l)${NC} deepseek-r1 — DeepSeek R1 推理\"\n\t\t\t\t\t\techo -e \" ${CYAN}m)${NC} deepseek-v3 — DeepSeek V3\"\n\t\t\t\t\t\techo -e \" ${CYAN}n)${NC} kimi-k2.5 — Kimi K2.5\"\n\t\t\t\t\t\techo -e \" ${CYAN}o)${NC} glm-5 — 智谱 GLM-5\"\n\t\t\t\t\t\techo -e \" ${CYAN}p)${NC} MiniMax-M2.5 — MiniMax M2.5\"\n\t\t\t\t\t\techo -e \" ${CYAN}────────────${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN}z)${NC} 手动输入模型名\"\n\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\t\t\ta) model_name=\"qwen-max\" ;;\n\t\t\t\t\t\t\tb) model_name=\"qwen-plus\" ;;\n\t\t\t\t\t\t\tc) model_name=\"qwen-flash\" ;;\n\t\t\t\t\t\t\td) model_name=\"qwen-turbo\" ;;\n\t\t\t\t\t\t\te) model_name=\"qwen-long\" ;;\n\t\t\t\t\t\t\tf) model_name=\"qwen3-coder-plus\" ;;\n\t\t\t\t\t\t\tg) model_name=\"qwen3-coder-flash\" ;;\n\t\t\t\t\t\t\th) model_name=\"qwq-plus\" ;;\n\t\t\t\t\t\t\ti) model_name=\"qwen3-235b-a22b\" ;;\n\t\t\t\t\t\t\tj) model_name=\"qwen3-32b\" ;;\n\t\t\t\t\t\t\tk) model_name=\"qwen3-30b-a3b\" ;;\n\t\t\t\t\t\t\tl) model_name=\"deepseek-r1\" ;;\n\t\t\t\t\t\t\tm) model_name=\"deepseek-v3\" ;;\n\t\t\t\t\t\t\tn) model_name=\"kimi-k2.5\" ;;\n\t\t\t\t\t\t\to) model_name=\"glm-5\" ;;\n\t\t\t\t\t\t\tp) model_name=\"MiniMax-M2.5\" ;;\n\t\t\t\t\t\t\tz) prompt_with_default \"请输入模型名称\" \"qwen-max\" model_name ;;\n\t\t\t\t\t\t\t*) model_name=\"qwen-max\" ;;\n\t\t\t\t\t\tesac\n\t\t\t\t\t\tauth_set_apikey dashscope \"$api_key\"\n\t\t\t\t\t\tregister_custom_provider dashscope \"https://dashscope.aliyuncs.com/compatible-mode/v1\" \"$api_key\" \"$model_name\" \"$model_name\"\n\t\t\t\t\t\tregister_and_set_model \"dashscope/${model_name}\"\n\t\t\t\t\t\techo -e \" ${GREEN}✅ 通义千问已配置 (按量付费),活跃模型: dashscope/${model_name}${NC}\"\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\tc|*)\n\t\t\t\t\techo \"\"\n\t\t\t\t\techo -e \" ${BOLD}Coding Plan 套餐配置${NC}\"\n\t\t\t\t\techo -e \" ${YELLOW}订阅套餐: https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/coding_plan${NC}\"\n\t\t\t\t\techo -e \" ${YELLOW}获取专属 API Key: 在上方页面获取 Coding Plan 专属 Key (sk-sp-...)${NC}\"\n\t\t\t\t\techo -e \" ${DIM}Base URL: https://coding.dashscope.aliyuncs.com/v1${NC}\"\n\t\t\t\t\techo -e \" ${DIM}文档: https://help.aliyun.com/zh/model-studio/openclaw-coding-plan${NC}\"\n\t\t\t\t\techo \"\"\n\t\t\t\t\tprompt_with_default \"请输入 Coding Plan 专属 API Key (sk-sp-...)\" \"\" api_key\n\t\t\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\techo -e \" ${CYAN}可用模型:${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN}a)${NC} qwen3.5-plus — Qwen3.5 Plus (推荐, 100万上下文)\"\n\t\t\t\t\t\techo -e \" ${CYAN}b)${NC} qwen3-coder-plus — Qwen3 Coder Plus (代码专用, 100万上下文)\"\n\t\t\t\t\t\techo -e \" ${CYAN}c)${NC} qwen3-coder-next — Qwen3 Coder Next\"\n\t\t\t\t\t\techo -e \" ${CYAN}d)${NC} qwen3-max-2026-01-23 — Qwen3 Max\"\n\t\t\t\t\t\techo -e \" ${CYAN}e)${NC} MiniMax-M2.5 — MiniMax M2.5\"\n\t\t\t\t\t\techo -e \" ${CYAN}f)${NC} glm-5 — 智谱 GLM-5\"\n\t\t\t\t\t\techo -e \" ${CYAN}g)${NC} kimi-k2.5 — Kimi K2.5\"\n\t\t\t\t\t\techo -e \" ${CYAN}h)${NC} 手动输入模型名\"\n\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\tprompt_with_default \"请选择默认模型\" \"a\" model_choice\n\t\t\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\t\t\ta) model_name=\"qwen3.5-plus\" ;;\n\t\t\t\t\t\t\tb) model_name=\"qwen3-coder-plus\" ;;\n\t\t\t\t\t\t\tc) model_name=\"qwen3-coder-next\" ;;\n\t\t\t\t\t\t\td) model_name=\"qwen3-max-2026-01-23\" ;;\n\t\t\t\t\t\t\te) model_name=\"MiniMax-M2.5\" ;;\n\t\t\t\t\t\t\tf) model_name=\"glm-5\" ;;\n\t\t\t\t\t\t\tg) model_name=\"kimi-k2.5\" ;;\n\t\t\t\t\t\t\th) prompt_with_default \"请输入模型名称\" \"qwen3.5-plus\" model_name ;;\n\t\t\t\t\t\t\t*) model_name=\"qwen3.5-plus\" ;;\n\t\t\t\t\t\tesac\n\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\techo -e \" ${CYAN}正在注册 Coding Plan 提供商 (含全部可用模型)...${NC}\"\n\t\t\t\t\t\tauth_set_apikey bailian \"$api_key\"\n\t\t\t\t\t\tregister_codingplan_provider \"$api_key\"\n\t\t\t\t\t\tregister_and_set_model \"bailian/${model_name}\"\n\t\t\t\t\t\techo -e \" ${GREEN}✅ Coding Plan 已配置,活跃模型: bailian/${model_name}${NC}\"\n\t\t\t\t\t\techo -e \" ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}\"\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\tesac\n\t\t\t;;\n\t\tf)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}xAI Grok 配置${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: https://console.x.ai${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入 xAI API Key\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型:${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} grok-4 — Grok 4 旗舰 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} grok-4-fast — Grok 4 Fast\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} grok-3 — Grok 3\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} grok-3-fast — Grok 3 Fast\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} grok-3-mini — Grok 3 Mini\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} grok-3-mini-fast — Grok 3 Mini Fast\"\n\t\t\t\techo -e \" ${CYAN}g)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"grok-4\" ;;\n\t\t\t\t\tb) model_name=\"grok-4-fast\" ;;\n\t\t\t\t\tc) model_name=\"grok-3\" ;;\n\t\t\t\t\td) model_name=\"grok-3-fast\" ;;\n\t\t\t\t\te) model_name=\"grok-3-mini\" ;;\n\t\t\t\t\tf) model_name=\"grok-3-mini-fast\" ;;\n\t\t\t\t\tg) prompt_with_default \"请输入模型名称\" \"grok-4\" model_name ;;\n\t\t\t\t\t*) model_name=\"grok-4\" ;;\n\t\t\t\tesac\n\t\t\t\tauth_set_apikey xai \"$api_key\"\n\t\t\t\tregister_and_set_model \"xai/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ xAI Grok 已配置,活跃模型: xai/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\th)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}硅基流动 SiliconFlow 配置${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: https://cloud.siliconflow.cn/account/ak${NC}\"\n\t\t\techo -e \" ${YELLOW}国内推理平台,支持多种开源模型${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入 SiliconFlow API Key\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型分类说明:${NC}\"\n\t\t\t\techo -e \" ${YELLOW}* Pro模型 (带 Pro/ 前缀): 仅支持充值余额支付,并发与速率(Rate Limits)可变。${NC}\"\n\t\t\t\techo -e \" ${YELLOW}* 非Pro模型 (无 Pro/ 前缀): 支持赠费余额(代金券)和充值余额支付,Rate Limits 固定。${NC}\"\n\t\t\t\techo -e \" ${GREEN}【建议】如果您是代金券/赠送余额用户,请务必选择【非Pro模型】。${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}── 非Pro模型 (支持代金券/免费额度) ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}1)${NC} deepseek-ai/DeepSeek-V3 — DeepSeek-V3 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}2)${NC} deepseek-ai/DeepSeek-R1 — DeepSeek-R1 (推理模型)\"\n\t\t\t\techo -e \" ${CYAN}3)${NC} Qwen/Qwen2.5-72B-Instruct — 通义千问 2.5 72B\"\n\t\t\t\techo -e \" ${CYAN}4)${NC} Qwen/Qwen2.5-7B-Instruct — 通义千问 2.5 7B\"\n\t\t\t\techo -e \" ${CYAN}5)${NC} THUDM/glm-4-9b-chat — 智谱 GLM-4 9B\"\n\t\t\t\techo -e \" ${CYAN}6)${NC} 01-ai/Yi-1.5-34B-Chat-16K — 零一万物 Yi-1.5 34B\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}── Pro模型 (仅支持充值余额) ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}7)${NC} Pro/deepseek-ai/DeepSeek-V3 — DeepSeek-V3 (Pro增强侧)\"\n\t\t\t\techo -e \" ${CYAN}8)${NC} Pro/zai-org/GLM-5 — 智谱 GLM-5\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}0)${NC} 手动输入其他任意模型名称\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型 [0-8]\" \"1\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\t1) model_name=\"deepseek-ai/DeepSeek-V3\" ;;\n\t\t\t\t\t2) model_name=\"deepseek-ai/DeepSeek-R1\" ;;\n\t\t\t\t\t3) model_name=\"Qwen/Qwen2.5-72B-Instruct\" ;;\n\t\t\t\t\t4) model_name=\"Qwen/Qwen2.5-7B-Instruct\" ;;\n\t\t\t\t\t5) model_name=\"THUDM/glm-4-9b-chat\" ;;\n\t\t\t\t\t6) model_name=\"01-ai/Yi-1.5-34B-Chat-16K\" ;;\n\t\t\t\t\t7) model_name=\"Pro/deepseek-ai/DeepSeek-V3\" ;;\n\t\t\t\t\t8) model_name=\"Pro/zai-org/GLM-5\" ;;\n\t\t\t\t\t0) prompt_with_default \"请输入模型详细名称\" \"deepseek-ai/DeepSeek-V3\" model_name ;;\n\t\t\t\t\t*) model_name=\"deepseek-ai/DeepSeek-V3\" ;;\n\t\t\t\tesac\n\t\t\t\tauth_set_apikey siliconflow \"$api_key\"\n\t\t\t\tregister_custom_provider siliconflow \"https://api.siliconflow.cn/v1\" \"$api_key\" \"$model_name\" \"$model_name\"\n\t\t\t\tregister_and_set_model \"siliconflow/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ SiliconFlow 已配置,活跃模型: siliconflow/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tl)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}🦙 Ollama 本地模型配置${NC}\"\n\t\t\techo -e \" ${YELLOW}Ollama 在本地或局域网运行大模型,无需 API Key${NC}\"\n\t\t\techo -e \" ${YELLOW}安装 Ollama: https://ollama.com${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${CYAN}连接方式:${NC}\"\n\t\t\techo -e \" ${CYAN}a)${NC} 本机运行 (localhost:11434)\"\n\t\t\techo -e \" ${CYAN}b)${NC} 局域网其他设备\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请选择\" \"a\" ollama_mode\n\t\t\tlocal ollama_url=\"\"\n\t\t\tcase \"$ollama_mode\" in\n\t\t\t\tb)\n\t\t\t\t\tprompt_with_default \"Ollama 地址 (如 192.168.1.100:11434)\" \"\" ollama_host\n\t\t\t\t\tif [ -n \"$ollama_host\" ]; then\n\t\t\t\t\t\t# 补全协议前缀\n\t\t\t\t\t\tcase \"$ollama_host\" in\n\t\t\t\t\t\t\thttp://*|https://*) ollama_url=\"${ollama_host}\" ;;\n\t\t\t\t\t\t\t*) ollama_url=\"http://${ollama_host}\" ;;\n\t\t\t\t\t\tesac\n\t\t\t\t\t\t# v2026.3.2: Ollama 使用原生 API,baseUrl 不带 /v1\n\t\t\t\t\t\tollama_url=$(echo \"$ollama_url\" | sed 's|/v1/*$||;s|/*$||')\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t*)\n\t\t\t\t\tollama_url=\"http://127.0.0.1:11434\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\t\tif [ -n \"$ollama_url\" ]; then\n\t\t\t\t# 检测 Ollama 是否可达\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}检测 Ollama 连通性...${NC}\"\n\t\t\t\tlocal ollama_base=$(echo \"$ollama_url\" | sed 's|/v1$||')\n\t\t\t\tlocal ollama_check=$(curl -sf --connect-timeout 3 --max-time 5 \"${ollama_base}/api/tags\" 2>/dev/null || echo \"\")\n\t\t\t\tif [ -n \"$ollama_check\" ]; then\n\t\t\t\t\techo -e \" ${GREEN}✅ Ollama 已连接${NC}\"\n\t\t\t\t\t# 列出已安装的模型\n\t\t\t\t\tlocal model_list=$(\"$NODE_BIN\" -e \"\n\t\t\t\t\t\ttry{\n\t\t\t\t\t\t\tconst d=JSON.parse(process.argv[1]);\n\t\t\t\t\t\t\t(d.models||[]).forEach((m,i)=>console.log(' '+(i+1)+') '+m.name));\n\t\t\t\t\t\t}catch(e){}\n\t\t\t\t\t\" \"$ollama_check\" 2>/dev/null)\n\t\t\t\t\tif [ -n \"$model_list\" ]; then\n\t\t\t\t\t\techo -e \" ${CYAN}已安装的模型:${NC}\"\n\t\t\t\t\t\techo \"$model_list\"\n\t\t\t\t\t\techo -e \" ${CYAN}m)${NC} 手动输入模型名\"\n\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\tprompt_with_default \"请选择模型\" \"1\" ollama_sel\n\t\t\t\t\t\tif [ \"$ollama_sel\" = \"m\" ]; then\n\t\t\t\t\t\t\tprompt_with_default \"请输入模型名称\" \"llama3.3\" model_name\n\t\t\t\t\t\telif echo \"$ollama_sel\" | grep -qE '^[0-9]+$'; then\n\t\t\t\t\t\t\tmodel_name=$(\"$NODE_BIN\" -e \"\n\t\t\t\t\t\t\t\ttry{\n\t\t\t\t\t\t\t\t\tconst d=JSON.parse(process.argv[1]);\n\t\t\t\t\t\t\t\t\tconst m=(d.models||[])[parseInt(process.argv[2])-1];\n\t\t\t\t\t\t\t\t\tconsole.log(m?m.name:'');\n\t\t\t\t\t\t\t\t}catch(e){console.log('');}\n\t\t\t\t\t\t\t\" \"$ollama_check\" \"$ollama_sel\" 2>/dev/null)\n\t\t\t\t\t\t\tif [ -z \"$model_name\" ]; then\n\t\t\t\t\t\t\t\techo -e \" ${YELLOW}无效选择,使用默认模型${NC}\"\n\t\t\t\t\t\t\t\tmodel_name=\"llama3.3\"\n\t\t\t\t\t\t\tfi\n\t\t\t\t\t\tfi\n\t\t\t\t\telse\n\t\t\t\t\t\techo -e \" ${YELLOW}未检测到已安装模型,请先在 Ollama 中拉取模型:${NC}\"\n\t\t\t\t\t\techo -e \" ${CYAN} ollama pull llama3.3${NC}\"\n\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\tprompt_with_default \"请输入模型名称\" \"llama3.3\" model_name\n\t\t\t\t\tfi\n\t\t\t\telse\n\t\t\t\t\techo -e \" ${YELLOW}⚠️ 无法连接 Ollama (${ollama_base})${NC}\"\n\t\t\t\t\techo -e \" ${YELLOW} 请确认 Ollama 已启动并可访问${NC}\"\n\t\t\t\t\techo -e \" ${CYAN} 提示: 如果 Ollama 在其他设备上,需设置 OLLAMA_HOST=0.0.0.0${NC}\"\n\t\t\t\t\techo \"\"\n\t\t\t\t\tprompt_with_default \"仍要继续配置? (y/n)\" \"n\" force_continue\n\t\t\t\t\tif ! confirm_yes \"$force_continue\"; then\n\t\t\t\t\t\treturn\n\t\t\t\t\tfi\n\t\t\t\t\tprompt_with_default \"请输入模型名称\" \"llama3.3\" model_name\n\t\t\t\tfi\n\t\t\t\tif [ -n \"$model_name\" ]; then\n\t\t\t\t\t# Ollama 无需 API Key,使用占位符\n\t\t\t\t\tauth_set_apikey ollama \"ollama-local\" \"ollama:local\"\n\t\t\t\t\t# v2026.3.2: Ollama 使用原生 ollama API,不再走 OpenAI 兼容层\n\t\t\t\t\t_RCP_PROV=\"ollama\" _RCP_URL=\"$ollama_url\" _RCP_KEY=\"ollama-local\" _RCP_MID=\"$model_name\" _RCP_MNAME=\"$model_name\" \"$NODE_BIN\" -e \"\n\t\t\t\t\t\tconst fs=require('fs');\n\t\t\t\t\t\tlet d={};\n\t\t\t\t\t\ttry{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}\n\t\t\t\t\t\tif(!d.models)d.models={};\n\t\t\t\t\t\tif(!d.models.providers)d.models.providers={};\n\t\t\t\t\t\td.models.mode='merge';\n\t\t\t\t\t\td.models.providers['ollama']={\n\t\t\t\t\t\t\tbaseUrl:process.env._RCP_URL,\n\t\t\t\t\t\t\tapiKey:process.env._RCP_KEY,\n\t\t\t\t\t\t\tapi:'ollama',\n\t\t\t\t\t\t\tmodels:[{\n\t\t\t\t\t\t\t\tid:process.env._RCP_MID,\n\t\t\t\t\t\t\t\tname:process.env._RCP_MNAME,\n\t\t\t\t\t\t\t\treasoning:false,\n\t\t\t\t\t\t\t\tinput:['text'],\n\t\t\t\t\t\t\t\tcost:{input:0,output:0,cacheRead:0,cacheWrite:0},\n\t\t\t\t\t\t\t\tcontextWindow:128000,\n\t\t\t\t\t\t\t\tmaxTokens:32000\n\t\t\t\t\t\t\t}]\n\t\t\t\t\t\t};\n\t\t\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\t\t\" 2>/dev/null\n\t\t\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\t\t\tregister_and_set_model \"ollama/${model_name}\"\n\t\t\t\t\techo -e \" ${GREEN}✅ Ollama 已配置,活跃模型: ollama/${model_name}${NC}\"\n\t\t\t\t\techo -e \" ${CYAN} Ollama 地址: ${ollama_url}${NC}\"\n\t\t\t\tfi\n\t\t\tfi\n\t\t\t;;\n\t\ti)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}腾讯云大模型 Coding Plan 套餐配置${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${YELLOW}订阅/管理套餐: https://hunyuan.cloud.tencent.com/#/app/subscription${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: 在上方页面创建 Coding Plan 专属 Key (sk-sp-...)${NC}\"\n\t\t\techo -e \" ${DIM}文档: https://cloud.tencent.com/document/product/1772/128947${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入 Coding Plan API Key (sk-sp-...)\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型 (Coding Plan 套餐内):${NC}\"\n\t\t\t\techo -e \" ${CYAN}── 智能推荐 ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} tc-code-latest — 自动路由 (由平台选择最佳模型) ${GREEN}★ 推荐${NC}\"\n\t\t\t\techo -e \" ${CYAN}── 推理模型 ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} hunyuan-t1 — 混元 T1 深度推理\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} hunyuan-2.0-thinking — 混元 2.0 Thinking\"\n\t\t\t\techo -e \" ${CYAN}── 旗舰模型 ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} hunyuan-turbos — 混元 TurboS 旗舰\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} hunyuan-2.0-instruct — 混元 2.0 Instruct\"\n\t\t\t\techo -e \" ${CYAN}── 第三方模型 ──${NC}\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} glm-5 — 智谱 GLM-5\"\n\t\t\t\techo -e \" ${CYAN}g)${NC} kimi-k2.5 — Moonshot Kimi K2.5\"\n\t\t\t\techo -e \" ${CYAN}h)${NC} minimax-m2.5 — MiniMax M2.5\"\n\t\t\t\techo -e \" ${CYAN}────────────${NC}\"\n\t\t\t\techo -e \" ${CYAN}z)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择默认模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"tc-code-latest\" ;;\n\t\t\t\t\tb) model_name=\"hunyuan-t1\" ;;\n\t\t\t\t\tc) model_name=\"hunyuan-2.0-thinking\" ;;\n\t\t\t\t\td) model_name=\"hunyuan-turbos\" ;;\n\t\t\t\t\te) model_name=\"hunyuan-2.0-instruct\" ;;\n\t\t\t\t\tf) model_name=\"glm-5\" ;;\n\t\t\t\t\tg) model_name=\"kimi-k2.5\" ;;\n\t\t\t\t\th) model_name=\"minimax-m2.5\" ;;\n\t\t\t\t\tz) prompt_with_default \"请输入模型名称\" \"tc-code-latest\" model_name ;;\n\t\t\t\t\t*) model_name=\"tc-code-latest\" ;;\n\t\t\t\tesac\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}正在注册腾讯云 Coding Plan 提供商 (含全部套餐模型)...${NC}\"\n\t\t\t\tauth_set_apikey lkeap \"$api_key\"\n\t\t\t\tregister_lkeap_codingplan_provider \"$api_key\"\n\t\t\t\tregister_and_set_model \"lkeap/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ 腾讯云 Coding Plan 已配置,活跃模型: lkeap/${model_name}${NC}\"\n\t\t\t\techo -e \" ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tj)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}百度千帆大模型配置${NC}\"\n\t\t\techo -e \" ${YELLOW}获取 API Key: https://console.bce.baidu.com/qianfan/ais/console/onlineService${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${DIM}提示: 需要 API Key (Access Token) 和可选的 Secret Key${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入百度千帆 API Key (Access Token)\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\tauth_set_apikey qianfan \"$api_key\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型:${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} ernie-4.0-8k — 文心一言 4.0 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} ernie-3.5-8k — 文心一言 3.5\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} ernie-4.0-turbo-8k — 文心一言 4.0 Turbo\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} ernie-speed-8k — 文心一言 Speed 极速\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"ernie-4.0-8k\" ;;\n\t\t\t\t\tb) model_name=\"ernie-3.5-8k\" ;;\n\t\t\t\t\tc) model_name=\"ernie-4.0-turbo-8k\" ;;\n\t\t\t\t\td) model_name=\"ernie-speed-8k\" ;;\n\t\t\t\t\te) prompt_with_default \"请输入模型名称\" \"ernie-4.0-8k\" model_name ;;\n\t\t\t\t\t*) model_name=\"ernie-4.0-8k\" ;;\n\t\t\t\tesac\n\t\t\t\t# 百度千帆使用 OpenAI 兼容接口\n\t\t\t\t# 注: OpenClaw v2026.3.28+ 支持 qianfan 原生 provider\n\t\t\t\tregister_custom_provider qianfan \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop\" \"$api_key\" \"$model_name\" \"$model_name\"\n\t\t\t\tregister_and_set_model \"qianfan/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ 百度千帆已配置,活跃模型: qianfan/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tk)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}智谱 GLM / Z.AI 配置${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${CYAN}认证方式:${NC}\"\n\t\t\techo -e \" ${CYAN}a)${NC} CN (open.bigmodel.cn) ${GREEN}★ 国内用户推荐${NC}\"\n\t\t\techo -e \" ${CYAN}b)${NC} Coding-Plan-CN (智谱 Coding Plan 套餐)\"\n\t\t\techo -e \" ${CYAN}c)${NC} Global (api.z.ai)\"\n\t\t\techo -e \" ${CYAN}d)${NC} 手动输入 API Key\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请选择认证方式\" \"a\" zai_method\n\t\t\tcase \"$zai_method\" in\n\t\t\t\ta)\n\t\t\t\t\tzai_base_url=\"https://open.bigmodel.cn/api/paas/v4\"\n\t\t\t\t\techo -e \" ${YELLOW}获取 API Key: https://open.bigmodel.cn/api-key${NC}\"\n\t\t\t\t\t;;\n\t\t\t\tb)\n\t\t\t\t\tzai_base_url=\"https://open.bigmodel.cn/api/coding/paas/v4\"\n\t\t\t\t\techo -e \" ${YELLOW}Coding Plan 套餐 API Key (sk-...)${NC}\"\n\t\t\t\t\t;;\n\t\t\t\tc)\n\t\t\t\t\tzai_base_url=\"https://api.z.ai/api/paas/v4\"\n\t\t\t\t\techo -e \" ${YELLOW}全球版 API Key${NC}\"\n\t\t\t\t\t;;\n\t\t\t\td)\n\t\t\t\t\tprompt_with_default \"请输入 Base URL\" \"https://open.bigmodel.cn/api/paas/v4\" zai_base_url\n\t\t\t\t\t;;\n\t\t\t\t*)\n\t\t\t\t\tzai_base_url=\"https://open.bigmodel.cn/api/paas/v4\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"请输入智谱 API Key\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}可用模型:${NC}\"\n\t\t\t\techo -e \" ${CYAN}a)${NC} glm-5.1 — GLM-5.1 (推荐)\"\n\t\t\t\techo -e \" ${CYAN}b)${NC} glm-5 — GLM-5\"\n\t\t\t\techo -e \" ${CYAN}c)${NC} glm-4.7 — GLM-4.7\"\n\t\t\t\techo -e \" ${CYAN}d)${NC} glm-4.7-flash — GLM-4.7 Flash\"\n\t\t\t\techo -e \" ${CYAN}e)${NC} glm-4.5 — GLM-4.5\"\n\t\t\t\techo -e \" ${CYAN}f)${NC} glm-4.5-flash — GLM-4.5 Flash (免费额度)\"\n\t\t\t\techo -e \" ${CYAN}g)${NC} 手动输入模型名\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请选择模型\" \"a\" model_choice\n\t\t\t\tcase \"$model_choice\" in\n\t\t\t\t\ta) model_name=\"glm-5.1\" ;;\n\t\t\t\t\tb) model_name=\"glm-5\" ;;\n\t\t\t\t\tc) model_name=\"glm-4.7\" ;;\n\t\t\t\t\td) model_name=\"glm-4.7-flash\" ;;\n\t\t\t\t\te) model_name=\"glm-4.5\" ;;\n\t\t\t\t\tf) model_name=\"glm-4.5-flash\" ;;\n\t\t\t\t\tg) prompt_with_default \"请输入模型名称\" \"glm-5.1\" model_name ;;\n\t\t\t\t\t*) model_name=\"glm-5.1\" ;;\n\t\t\t\tesac\n\t\t\t\t# 智谱 GLM 使用原生 zai provider (OpenClaw 内置支持)\n\t\t\t\tauth_set_apikey zai \"$api_key\"\n\t\t\t\tregister_custom_provider zai \"$zai_base_url\" \"$api_key\" \"$model_name\" \"$model_name\" \"128000\" \"4096\"\n\t\t\t\tregister_and_set_model \"zai/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ 智谱 GLM 已配置,活跃模型: zai/${model_name}${NC}\"\n\t\t\t\techo -e \" ${DIM} Base URL: ${zai_base_url}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tm)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}自定义 OpenAI 兼容 API${NC}\"\n\t\t\techo -e \" ${YELLOW}支持任何兼容 OpenAI API 格式的服务商${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"API Base URL (如 https://api.example.com/v1)\" \"\" base_url\n\t\t\tprompt_with_default \"API Key\" \"\" api_key\n\t\t\tprompt_with_default \"模型名称\" \"\" model_name\n\t\t\tif [ -n \"$base_url\" ] && [ -n \"$api_key\" ] && [ -n \"$model_name\" ]; then\n\t\t\t\tauth_set_apikey openai-compatible \"$api_key\" \"openai-compatible:manual\"\n\t\t\t\tregister_custom_provider openai-compatible \"$base_url\" \"$api_key\" \"$model_name\" \"$model_name\"\n\t\t\t\tregister_and_set_model \"openai-compatible/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ 自定义模型已配置,活跃模型: openai-compatible/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tn)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}自定义 Anthropic 兼容 API${NC}\"\n\t\t\techo -e \" ${YELLOW}支持任何兼容 Anthropic Messages API 格式的服务商${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"API Base URL (如 https://api.anthropic.com)\" \"\" base_url\n\t\t\tprompt_with_default \"API Key\" \"\" api_key\n\t\t\tprompt_with_default \"模型名称\" \"claude-sonnet-4-20250514\" model_name\n\t\t\tif [ -n \"$base_url\" ] && [ -n \"$api_key\" ] && [ -n \"$model_name\" ]; then\n\t\t\t\t_ACP_URL=\"${base_url%/}\" _ACP_KEY=\"$api_key\" _ACP_MID=\"$model_name\" \"$NODE_BIN\" -e \"\n\t\t\t\t\tconst fs=require('fs');\n\t\t\t\t\tlet d={};\n\t\t\t\t\ttry{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}\n\t\t\t\t\tif(!d.models)d.models={};\n\t\t\t\t\tif(!d.models.providers)d.models.providers={};\n\t\t\t\t\td.models.mode='merge';\n\t\t\t\t\td.models.providers['anthropic-compatible']={\n\t\t\t\t\t\tbaseUrl:process.env._ACP_URL,\n\t\t\t\t\t\tapiKey:process.env._ACP_KEY,\n\t\t\t\t\t\tapi:'anthropic-messages',\n\t\t\t\t\t\tmodels:[{\n\t\t\t\t\t\t\tid:process.env._ACP_MID,\n\t\t\t\t\t\t\tname:process.env._ACP_MID,\n\t\t\t\t\t\t\treasoning:false,\n\t\t\t\t\t\t\tinput:['text','image'],\n\t\t\t\t\t\t\tcost:{input:0,output:0,cacheRead:0,cacheWrite:0},\n\t\t\t\t\t\t\tcontextWindow:200000,\n\t\t\t\t\t\t\tmaxTokens:16000\n\t\t\t\t\t\t}]\n\t\t\t\t\t};\n\t\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\t\" 2>/dev/null\n\t\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\t\tauth_set_apikey anthropic-compatible \"$api_key\" \"anthropic-compatible:manual\"\n\t\t\t\tregister_and_set_model \"anthropic-compatible/${model_name}\"\n\t\t\t\techo -e \" ${GREEN}✅ 自定义 Anthropic API 已配置,活跃模型: anthropic-compatible/${model_name}${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\to)\n\t\t\techo \"\"\n\t\t\techo -e \" ${BOLD}一万AI分享 粉丝专享 API${NC}\"\n\t\t\techo -e \" ${YELLOW}OpenAI 兼容模式;Base URL 和模型已内置,只需要填写 API Key。${NC}\"\n\t\t\techo -e \" ${DIM}Base URL: https://api.910501.xyz/v1${NC}\"\n\t\t\techo -e \" ${DIM}Model: gpt-5.5${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"API Key\" \"\" api_key\n\t\t\tif [ -n \"$api_key\" ]; then\n\t\t\t\tauth_set_apikey yiwanai \"$api_key\" \"yiwanai:fan\"\n\t\t\t\tregister_custom_provider yiwanai \"https://api.910501.xyz/v1\" \"$api_key\" \"gpt-5.5\" \"gpt-5.5\" 1000000 32000\n\t\t\t\t_YW_PROV=\"yiwanai\" \"$NODE_BIN\" -e \"\n\t\t\t\t\tconst fs=require('fs');\n\t\t\t\t\tlet d={};\n\t\t\t\t\ttry{d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));}catch(e){}\n\t\t\t\t\tconst p=d.models&&d.models.providers&&d.models.providers[process.env._YW_PROV];\n\t\t\t\t\tif(p&&p.models&&p.models[0]){\n\t\t\t\t\t\tp.models[0].reasoning=true;\n\t\t\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\t\t}\n\t\t\t\t\" 2>/dev/null\n\t\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\t\tregister_and_set_model \"yiwanai/gpt-5.5\"\n\t\t\t\techo -e \" ${GREEN}✅ 一万AI分享粉丝专享 API 已配置,活跃模型: yiwanai/gpt-5.5${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tq) return ;;\n\tesac\n\n\tif [ \"$choice\" != \"0\" ] && [ \"$choice\" != \"1\" ]; then\n\t\techo \"\"\n\t\task_restart\n\tfi\n}\n\n# ══════════════════════════════════════════════════════════════\n# 设定当前活跃模型\n# ══════════════════════════════════════════════════════════════\nset_active_model() {\n\techo \"\"\n\techo -e \" ${BOLD}🔄 设定当前活跃模型${NC}\"\n\techo \"\"\n\n\tlocal current_model=$(json_get agents.defaults.model.primary)\n\techo -e \" 当前活跃模型: ${GREEN}${BOLD}${current_model:-未设置}${NC}\"\n\techo \"\"\n\n\tlocal models_json=\"\"\n\tmodels_json=$(oc_cmd models list --json 2>/dev/null || echo \"\")\n\tlocal model_count=0\n\tif [ -n \"$models_json\" ]; then\n\t\tmodel_count=$(\"$NODE_BIN\" -e \"\n\t\t\ttry{const d=JSON.parse(process.argv[1]);console.log((d.models||[]).length);}catch(e){console.log(0);}\n\t\t\" \"$models_json\" 2>/dev/null || echo \"0\")\n\tfi\n\n\tif [ \"$model_count\" -gt 0 ] 2>/dev/null; then\n\t\techo -e \" ${CYAN}已配置的模型:${NC}\"\n\t\techo \"\"\n\t\t\"$NODE_BIN\" -e \"\n\t\t\tconst d=JSON.parse(process.argv[1]);\n\t\t\t(d.models||[]).forEach((m,i)=>{\n\t\t\t\tconst mark=m.key===process.argv[2]?' ← 当前活跃':'';\n\t\t\t\tconst n=m.name&&m.name!==m.key?' ('+m.name+')':'';\n\t\t\t\tconsole.log(' '+(i+1)+') '+m.key+n+mark);\n\t\t\t});\n\t\t\" \"$models_json\" \"$current_model\" 2>/dev/null\n\t\techo \"\"\n\t\techo -e \" ${CYAN}m)${NC} 手动输入模型 ID\"\n\t\techo -e \" ${CYAN}0)${NC} 返回\"\n\t\techo \"\"\n\t\tprompt_with_default \"请选择\" \"0\" model_sel\n\n\t\tif [ \"$model_sel\" = \"0\" ]; then\n\t\t\treturn\n\t\telif [ \"$model_sel\" = \"m\" ]; then\n\t\t\tprompt_with_default \"请输入模型 ID (如 openai/gpt-4o)\" \"${current_model:-}\" manual_model\n\t\t\tif [ -n \"$manual_model\" ]; then\n\t\t\t\tregister_and_set_model \"$manual_model\"\n\t\t\t\techo -e \" ${GREEN}✅ 活跃模型已设为: ${manual_model}${NC}\"\n\t\t\t\task_restart\n\t\t\tfi\n\t\telif echo \"$model_sel\" | grep -qE '^[0-9]+$'; then\n\t\t\tlocal selected=$(\"$NODE_BIN\" -e \"\n\t\t\t\tconst d=JSON.parse(process.argv[1]);\n\t\t\t\tconst m=(d.models||[])[parseInt(process.argv[2])-1];\n\t\t\t\tconsole.log(m?m.key:'');\n\t\t\t\" \"$models_json\" \"$model_sel\" 2>/dev/null)\n\t\t\tif [ -n \"$selected\" ]; then\n\t\t\t\tregister_and_set_model \"$selected\"\n\t\t\t\techo -e \" ${GREEN}✅ 活跃模型已切换为: ${selected}${NC}\"\n\t\t\t\task_restart\n\t\t\telse\n\t\t\t\techo -e \" ${YELLOW}无效选择${NC}\"\n\t\t\tfi\n\t\tfi\n\telse\n\t\techo -e \" ${YELLOW}尚未配置任何模型。${NC}\"\n\t\techo -e \" ${YELLOW}请先通过「配置 AI 模型提供商」(菜单 2) 添加模型。${NC}\"\n\t\techo \"\"\n\t\tprompt_with_default \"直接输入模型 ID 设置? (留空返回)\" \"\" manual_model\n\t\tif [ -n \"$manual_model\" ]; then\n\t\t\tregister_and_set_model \"$manual_model\"\n\t\t\techo -e \" ${GREEN}✅ 活跃模型已设为: ${manual_model}${NC}\"\n\t\t\task_restart\n\t\tfi\n\tfi\n}\n\n# ══════════════════════════════════════════════════════════════\n# 配置 QQ 机器人 (通过 qqbot 插件 @tencent-connect/openclaw-qqbot)\n# ══════════════════════════════════════════════════════════════\nconfigure_qq() {\n\techo \"\"\n\techo -e \" ${BOLD}🐧 QQ 机器人配置${NC}\"\n\techo \"\"\n\n\t# 检查 qqbot 插件是否已安装并正常加载\n\tlocal plugin_installed=0\n\tlocal plugin_blocked=0\n\tlocal qqbot_ext_dir=\"${OC_STATE_DIR}/extensions/openclaw-qqbot\"\n\tif [ -n \"$OC_ENTRY\" ] && [ -x \"$NODE_BIN\" ]; then\n\t\tlocal plugin_list=$(oc_cmd plugins list 2>&1)\n\t\t# 在表格输出中查找含 qqbot 的行是否也包含 loaded\n\t\tif echo \"$plugin_list\" | grep -i \"qqbot\" | grep -qi \"loaded\"; then\n\t\t\tplugin_installed=1\n\t\t\techo -e \" ${GREEN}✅ qqbot 插件已安装并加载${NC}\"\n\t\telif echo \"$plugin_list\" | grep -qi \"plugin not found.*openclaw-qqbot\\|suspicious ownership\"; then\n\t\t\t# 插件目录存在但被阻止 (权限问题或 stale config)\n\t\t\tif [ -d \"$qqbot_ext_dir\" ]; then\n\t\t\t\tplugin_blocked=1\n\t\t\t\techo -e \" ${YELLOW}⚠️ qqbot 插件已安装但未能正常加载${NC}\"\n\t\t\t\techo -e \" ${CYAN}正在修复插件目录权限...${NC}\"\n\t\t\t\tchown -R root:root \"$qqbot_ext_dir\" 2>/dev/null\n\t\t\t\tchmod -R 755 \"$qqbot_ext_dir\" 2>/dev/null\n\t\t\t\techo -e \" ${GREEN}✅ 权限已修复,重启 Gateway 后生效${NC}\"\n\t\t\t\tplugin_installed=1\n\t\t\tfi\n\t\telif [ -d \"$qqbot_ext_dir\" ] && [ -f \"${qqbot_ext_dir}/openclaw.plugin.json\" ]; then\n\t\t\t# 目录存在、有 plugin.json 但未出现在插件列表 — 修复权限\n\t\t\techo -e \" ${YELLOW}⚠️ qqbot 插件目录存在但未能加载${NC}\"\n\t\t\techo -e \" ${CYAN}正在修复插件目录权限...${NC}\"\n\t\t\tchown -R root:root \"$qqbot_ext_dir\" 2>/dev/null\n\t\t\tchmod -R 755 \"$qqbot_ext_dir\" 2>/dev/null\n\t\t\techo -e \" ${GREEN}✅ 权限已修复${NC}\"\n\t\t\tplugin_installed=1\n\t\tfi\n\tfi\n\n\tif [ \"$plugin_installed\" -eq 0 ]; then\n\t\techo -e \" ${YELLOW}⚠️ qqbot 插件尚未安装${NC}\"\n\t\techo -e \" QQ 渠道需要先安装 qqbot 插件才能使用。\"\n\t\techo \"\"\n\t\tprompt_with_default \"是否立即安装 qqbot 插件? (y/n)\" \"y\" install_qqbot\n\t\tif confirm_yes \"$install_qqbot\"; then\n\t\t\techo -e \" ${CYAN}正在安装 @tencent-connect/openclaw-qqbot ...${NC}\"\n\t\t\techo -e \" ${DIM}(首次安装可能需要几分钟)${NC}\"\n\t\t\tlocal install_out\n\t\t\tinstall_out=$(oc_cmd plugins install @tencent-connect/openclaw-qqbot@latest 2>&1)\n\t\t\tlocal install_rc=$?\n\n\t\t\t# 关键: 安装后立即修复插件目录权限为 root (OpenClaw 安全策略要求)\n\t\t\t# 同时修复权限模式为 755,确保 Gateway 可读取插件\n\t\t\tif [ -d \"$qqbot_ext_dir\" ]; then\n\t\t\t\tchown -R root:root \"$qqbot_ext_dir\" 2>/dev/null\n\t\t\t\tchmod -R 755 \"$qqbot_ext_dir\" 2>/dev/null\n\t\t\tfi\n\n\t\t\tif [ $install_rc -eq 0 ]; then\n\t\t\t\techo -e \" ${GREEN}✅ qqbot 插件安装成功${NC}\"\n\t\t\t\tplugin_installed=1\n\t\t\telse\n\t\t\t\t# 安装命令返回非零,可能是因为 config invalid (死锁)\n\t\t\t\t# 检查插件目录是否实际已存在 (说明下载成功但校验报错)\n\t\t\t\tif [ -d \"$qqbot_ext_dir\" ] && [ -f \"${qqbot_ext_dir}/openclaw.plugin.json\" ]; then\n\t\t\t\t\techo -e \" ${YELLOW}⚠️ 插件已下载但加载校验未通过 (exit: $install_rc)${NC}\"\n\t\t\t\t\techo -e \" ${CYAN}这通常是因为配置中已有 qqbot 设置但插件未被信任。${NC}\"\n\t\t\t\t\techo -e \" ${CYAN}已自动修复权限,重启 Gateway 后应能正常加载。${NC}\"\n\t\t\t\t\tplugin_installed=1\n\t\t\t\telse\n\t\t\t\t\techo -e \" ${RED}❌ 插件安装失败 (exit: $install_rc)${NC}\"\n\t\t\t\t\techo -e \" ${DIM}${install_out}${NC}\" | tail -5\n\t\t\t\t\techo \"\"\n\t\t\t\t\techo -e \" ${YELLOW}插件安装失败,但你仍然可以先配置 QQ 机器人参数。${NC}\"\n\t\t\t\t\techo -e \" ${YELLOW}稍后可手动安装: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}\"\n\t\t\t\tfi\n\t\t\t\techo \"\"\n\t\t\tfi\n\t\t\t\n\t\t\t# v2026.3.13: 确保插件名称正确写入 plugins.allow\n\t\t\t# OpenClaw 要求 plugins.allow 中的名称必须与实际插件名完全匹配\n\t\t\tif [ \"$plugin_installed\" -eq 1 ]; then\n\t\t\t\tensure_qqbot_plugin_allowed\n\t\t\tfi\n\t\telse\n\t\t\techo -e \" ${YELLOW}已跳过插件安装,继续配置 QQ 机器人参数。${NC}\"\n\t\t\techo -e \" ${CYAN}稍后安装命令: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}\"\n\t\t\techo \"\"\n\t\tfi\n\tfi\n\n\techo \"\"\n\techo -e \" ${YELLOW}获取 App ID 和 App Secret 步骤:${NC}\"\n\techo -e \" 1. 前往 ${CYAN}QQ 开放平台${NC}: ${CYAN}https://q.qq.com/qqbot/openclaw/login.html${NC}\"\n\techo -e \" 2. 用手机 QQ 扫码注册/登录\"\n\techo -e \" 3. 进入 QQ 机器人页面 → 点击「创建机器人」\"\n\techo -e \" 4. 创建完成后,复制页面中的 ${CYAN}App ID${NC} 和 ${CYAN}App Secret${NC}\"\n\techo \"\"\n\techo -e \" ${RED}⚠️ 注意: App Secret 不支持二次查看(会强制重置),请妥善保存!${NC}\"\n\techo \"\"\n\n\tlocal current_appid=$(json_get channels.qqbot.appId)\n\tif [ -n \"$current_appid\" ]; then\n\t\techo -e \" ${GREEN}当前已配置 App ID: ${current_appid}${NC}\"\n\tfi\n\n\tprompt_with_default \"请输入 QQ 机器人 App ID\" \"\" qq_appid\n\tprompt_with_default \"请输入 QQ 机器人 App Secret\" \"\" qq_secret\n\tqq_appid=$(sanitize_input \"$qq_appid\" | tr -d '[:space:]')\n\tqq_secret=$(sanitize_input \"$qq_secret\" | tr -d '[:space:]')\n\n\tif [ -n \"$qq_appid\" ] && [ -n \"$qq_secret\" ]; then\n\t\t# App ID 应为纯数字\n\t\tif ! printf '%s' \"$qq_appid\" | grep -qE '^[0-9]+$'; then\n\t\t\techo -e \" ${RED}❌ App ID 格式错误,应为纯数字${NC}\"\n\t\t\treturn\n\t\tfi\n\n\t\t# App Secret 基本格式检查 (非空即可,长度通常 32 位)\n\t\tif [ ${#qq_secret} -lt 10 ]; then\n\t\t\techo -e \" ${YELLOW}⚠️ App Secret 长度过短(${#qq_secret} 字符),请确认是否完整粘贴。${NC}\"\n\t\t\tprompt_with_default \"是否仍然保存? (y/n)\" \"n\" force_save\n\t\t\tif ! confirm_yes \"$force_save\"; then\n\t\t\t\techo -e \" ${YELLOW}已取消,配置未保存。${NC}\"\n\t\t\t\treturn\n\t\t\tfi\n\t\tfi\n\n\t\t# 使用 openclaw CLI 一键配置 (推荐方式)\n\t\techo -e \" ${CYAN}正在配置 qqbot 渠道...${NC}\"\n\t\tlocal add_out\n\t\tadd_out=$(oc_cmd channels add --channel qqbot --token \"${qq_appid}:${qq_secret}\" 2>&1)\n\t\tlocal add_rc=$?\n\n\t\tif [ $add_rc -ne 0 ]; then\n\t\t\techo -e \" ${YELLOW}CLI 配置未成功,尝试直接写入配置文件...${NC}\"\n\t\t\t# 回退: 直接写入 JSON 配置\n\t\t\tjson_set channels.qqbot.enabled true\n\t\t\tjson_set channels.qqbot.appId \"$qq_appid\"\n\t\t\tjson_set channels.qqbot.clientSecret \"$qq_secret\"\n\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\tfi\n\n\t\t# 保存后验证\n\t\tlocal saved_appid=$(json_get channels.qqbot.appId)\n\t\tif [ -z \"$saved_appid\" ]; then\n\t\t\techo -e \" ${RED}❌ 配置保存异常! 请检查配置文件${NC}\"\n\t\t\treturn\n\t\tfi\n\n\t\techo -e \" ${GREEN}✅ QQ 机器人配置已保存${NC}\"\n\t\techo -e \" ${CYAN} App ID: ${qq_appid}${NC}\"\n\t\techo \"\"\n\t\techo -e \" ${YELLOW}提示:${NC}\"\n\t\techo -e \" • 配置完成后需重启 Gateway 使配置生效\"\n\t\techo -e \" • 如果机器人回复「该机器人去火星了」,请检查 App ID 和 App Secret 是否正确\"\n\t\techo -e \" • 当前不建议将 QQ 机器人添加进 QQ 群聊\"\n\t\techo -e \" • 插件升级: ${CYAN}openclaw plugins update openclaw-qqbot${NC}\"\n\t\techo \"\"\n\n\t\t# 重启 Gateway 使配置生效\n\t\task_restart\n\telse\n\t\techo -e \" ${YELLOW}信息不完整,已取消。${NC}\"\n\tfi\n}\n\n# ══════════════════════════════════════════════════════════════\n# 配置 Telegram\n# ══════════════════════════════════════════════════════════════\nconfigure_telegram() {\n\techo \"\"\n\techo -e \" ${BOLD}📱 Telegram Bot 配置${NC}\"\n\techo \"\"\n\techo -e \" ${YELLOW}获取 Bot Token 步骤:${NC}\"\n\techo -e \" 1. 打开 Telegram → 搜索 ${CYAN}@BotFather${NC}\"\n\techo -e \" 2. 发送 ${CYAN}/newbot${NC} → 按提示创建\"\n\techo -e \" 3. 复制生成的 Token\"\n\techo \"\"\n\n\tlocal current_token=$(json_get channels.telegram.botToken)\n\tif [ -n \"$current_token\" ]; then\n\t\tlocal ct_short=$(echo \"$current_token\" | cut -c1-12)\n\t\techo -e \" ${GREEN}当前已配置 Token: ${ct_short}...${NC}\"\n\tfi\n\n\tprompt_with_default \"请输入 Telegram Bot Token\" \"\" tg_token\n\n\tif [ -n \"$tg_token\" ]; then\n\t\t# ── 强力清洗: 先用 sanitize_input 去除 ANSI 转义序列,再白名单过滤 ──\n\t\ttg_token=$(sanitize_input \"$tg_token\")\n\t\ttg_token=$(printf '%s' \"$tg_token\" | tr -cd 'A-Za-z0-9:_-')\n\n\t\t# ── 格式验证: 使用 grep 正则匹配 \"数字:字母数字\" ──\n\t\tif ! printf '%s' \"$tg_token\" | grep -qE '^[0-9]+:[A-Za-z0-9_-]+$'; then\n\t\t\techo -e \" ${RED}❌ Token 格式错误${NC}\"\n\t\t\techo -e \" ${YELLOW} 正确格式: 123456789:ABCdefGHIjklMNOpqr${NC}\"\n\t\t\techo -e \" ${YELLOW} 请检查粘贴是否完整,重试。${NC}\"\n\t\t\treturn\n\t\tfi\n\n\t\techo -e \" ${CYAN}验证 Token...${NC}\"\n\t\tlocal verify=\"\"\n\t\tverify=$(curl -s --connect-timeout 5 --max-time 10 \"https://api.telegram.org/bot${tg_token}/getMe\" 2>/dev/null || echo '{\"ok\":false}')\n\t\tif echo \"$verify\" | grep -q '\"ok\":true'; then\n\t\t\tlocal bot_name=$(echo \"$verify\" | grep -o '\"username\":\"[^\"]*\"' | head -1 | cut -d'\"' -f4)\n\t\t\techo -e \" ${GREEN}✅ Token 验证成功 — @${bot_name}${NC}\"\n\t\telse\n\t\t\techo -e \" ${RED}❌ Token 验证失败${NC}\"\n\t\t\techo -e \" ${YELLOW} 可能原因: Token 不正确 或 无法连接 Telegram API${NC}\"\n\t\t\tprompt_with_default \"是否仍然保存此 Token? (y/n)\" \"n\" force_save\n\t\t\tif ! confirm_yes \"$force_save\"; then\n\t\t\t\techo -e \" ${YELLOW}已取消,Token 未保存。${NC}\"\n\t\t\t\treturn\n\t\t\tfi\n\t\tfi\n\n\t\t# ── 使用 json_set 直接写入 (避免 oc_cmd CLI 参数解析问题) ──\n\t\tjson_set channels.telegram.botToken \"$tg_token\"\n\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\tchown openclaw:openclaw \"${CONFIG_FILE}.bak\" 2>/dev/null || true\n\n\t\t# ── 保存后验证: 读回检查 Token 是否完整 ──\n\t\tlocal saved_token=$(json_get channels.telegram.botToken)\n\t\tif [ \"$saved_token\" != \"$tg_token\" ]; then\n\t\t\techo -e \" ${RED}❌ Token 保存异常! 已保存的值与输入不一致${NC}\"\n\t\t\techo -e \" ${YELLOW} 期望: ${tg_token}${NC}\"\n\t\t\techo -e \" ${YELLOW} 实际: ${saved_token}${NC}\"\n\t\t\techo -e \" ${YELLOW} 请重新配置。${NC}\"\n\t\t\treturn\n\t\tfi\n\t\techo -e \" ${GREEN}✅ Telegram Bot Token 已保存${NC}\"\n\n\t\t# 重启 Gateway 使 Token 生效 (必须重启,否则 Bot 无法连接 Telegram)\n\t\techo -e \" ${CYAN}正在重启 Gateway 使 Token 生效...${NC}\"\n\t\trestart_gateway\n\n\t\t# Token 保存且 Gateway 重启后,自动进入配对流程\n\t\techo \"\"\n\t\techo -e \" ${CYAN}接下来进行 Telegram 配对,让 Bot 关联您的账号。${NC}\"\n\t\tprompt_with_default \"是否现在进行配对? (y/n)\" \"y\" do_pair\n\t\tif confirm_yes \"$do_pair\"; then\n\t\t\ttelegram_pairing\n\t\tfi\n\telse\n\t\techo -e \" ${YELLOW}未输入 Token,已取消。${NC}\"\n\tfi\n}\n\n# ── 配置 Discord ──\nconfigure_discord() {\n\techo \"\"\n\techo -e \" ${BOLD}🎮 Discord Bot 配置${NC}\"\n\techo \"\"\n\techo -e \" ${YELLOW}获取 Bot Token:${NC} ${CYAN}https://discord.com/developers/applications${NC}\"\n\techo \"\"\n\tprompt_with_default \"请输入 Discord Bot Token\" \"\" dc_token\n\tdc_token=$(sanitize_input \"$dc_token\" | tr -d '[:space:]')\n\tif [ -n \"$dc_token\" ]; then\n\t\tjson_set channels.discord.botToken \"$dc_token\"\n\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\techo -e \" ${GREEN}✅ Discord Bot Token 已保存${NC}\"\n\t\task_restart\n\tfi\n}\n\n# ── 配置飞书 ──\nconfigure_feishu() {\n\techo \"\"\n\techo -e \" ${BOLD}🐦 飞书 Bot 配置${NC}\"\n\techo \"\"\n\n\t# 检查 Node.js 是否可用\n\tif [ ! -x \"$NODE_BIN\" ]; then\n\t\techo -e \" ${RED}❌ Node.js 不可用,请先安装运行环境${NC}\"\n\t\treturn 1\n\tfi\n\n\t# 检查并安装 Python 3 (飞书插件依赖)\n\tif ! command -v python3 >/dev/null 2>&1; then\n\t\techo -e \" ${YELLOW}⚠️ 飞书插件需要 Python 3 支持${NC}\"\n\t\techo -e \" ${CYAN}正在尝试安装 python3-light...${NC}\"\n\t\topkg update >/dev/null 2>&1\n\t\t# 使用 python3-light 减少安装体积 (约 2MB vs 完整版 30MB+)\n\t\topkg install python3-light 2>&1 | tail -5 || true\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\techo -e \" ${GREEN}✅ Python 3 安装成功${NC}\"\n\t\telse\n\t\t\techo -e \" ${RED}❌ Python 3 安装失败${NC}\"\n\t\t\techo -e \" ${YELLOW}请手动安装: opkg install python3-light${NC}\"\n\t\t\techo \"\"\n\t\t\tprompt_with_default \"是否继续尝试安装飞书? (y/n)\" \"n\" continue_install\n\t\t\tif ! confirm_yes \"$continue_install\"; then\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tfi\n\n\techo -e \" ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n\techo -e \" ${CYAN}飞书官方安装向导${NC}\"\n\techo -e \" ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n\techo \"\"\n\techo -e \" ${YELLOW}即将执行飞书官方安装命令:${NC}\"\n\techo -e \" ${CYAN}npx -y @larksuite/openclaw-lark-tools install${NC}\"\n\techo \"\"\n\techo -e \" ${DIM}安装过程中可以:${NC}\"\n\techo -e \" ${DIM} • 新建机器人: 扫描二维码一键创建${NC}\"\n\techo -e \" ${DIM} • 关联已有机器人: 输入 App ID 和 App Secret${NC}\"\n\techo \"\"\n\techo -e \" ${YELLOW}提示: 若命令行出错,可在命令前增加 sudo 重新执行${NC}\"\n\techo \"\"\n\n\tprompt_with_default \"是否开始安装? (y/n)\" \"y\" do_install\n\tif ! confirm_yes \"$do_install\"; then\n\t\techo -e \" ${YELLOW}已取消安装${NC}\"\n\t\treturn\n\tfi\n\n\techo \"\"\n\techo -e \" ${CYAN}正在启动飞书安装向导...${NC}\"\n\techo \"\"\n\n\t# 执行官方安装命令\n\tcd \"$OC_DATA\"\n\tNPX_BIN=\"${NODE_BASE}/bin/npx\"\n\tlocal install_rc=0\n\tif [ -x \"$NPX_BIN\" ]; then\n\t\t\"$NPX_BIN\" -y @larksuite/openclaw-lark-tools install || install_rc=$?\n\telse\n\t\t# 如果 npx 不存在,使用 node 运行\n\t\t\"$NODE_BIN\" \"$NODE_BASE/lib/node_modules/npm/bin/npx-cli.js\" -y @larksuite/openclaw-lark-tools install || install_rc=$?\n\tfi\n\n\techo \"\"\n\tif [ $install_rc -eq 0 ]; then\n\t\techo -e \" ${GREEN}✅ 飞书安装完成!${NC}\"\n\t\techo \"\"\n\t\techo -e \" ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n\t\techo -e \" ${YELLOW}下一步:${NC}\"\n\t\techo -e \" ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n\t\techo \"\"\n\t\techo -e \" 1. 在飞书中向机器人发送任意消息,即可开始对话\"\n\t\techo -e \" 2. 发送 ${CYAN}/feishu auth${NC} 完成批量授权\"\n\t\techo -e \" 3. 发送 ${CYAN}/feishu start${NC} 验证安装是否成功\"\n\t\techo -e \" 4. 发送 ${CYAN}学习一下我安装的新飞书插件,列出有哪些能力${NC}\"\n\t\techo \"\"\n\telse\n\t\techo -e \" ${YELLOW}⚠️ 安装向导退出 (exit: $install_rc)${NC}\"\n\t\techo \"\"\n\t\techo -e \" ${CYAN}手动安装命令:${NC}\"\n\t\techo -e \" ${CYAN}npx -y @larksuite/openclaw-lark-tools install${NC}\"\n\t\techo \"\"\n\tfi\n}\n\n# ── 配置 Slack ──\nconfigure_slack() {\n\techo \"\"\n\techo -e \" ${BOLD}💬 Slack Bot 配置${NC}\"\n\techo \"\"\n\techo -e \" ${YELLOW}获取 Bot Token:${NC} ${CYAN}https://api.slack.com/apps${NC} → Create App\"\n\techo \"\"\n\tprompt_with_default \"请输入 Slack Bot Token (xoxb-...)\" \"\" sk_token\n\tsk_token=$(sanitize_input \"$sk_token\" | tr -d '[:space:]')\n\tif [ -n \"$sk_token\" ]; then\n\t\tjson_set channels.slack.botToken \"$sk_token\"\n\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\techo -e \" ${GREEN}✅ Slack Bot Token 已保存${NC}\"\n\t\task_restart\n\tfi\n}\n\n# ══════════════════════════════════════════════════════════════\n# Telegram 配对助手\n# ══════════════════════════════════════════════════════════════\ntelegram_pairing() {\n\techo \"\"\n\techo -e \" ${BOLD}🤝 Telegram 配对助手${NC}\"\n\techo \"\"\n\n\tlocal tg_token=$(json_get channels.telegram.botToken)\n\tif [ -z \"$tg_token\" ]; then\n\t\techo -e \" ${YELLOW}未检测到 Telegram Bot Token,请先配置 Telegram。${NC}\"\n\t\treturn\n\tfi\n\n\techo -e \" ${CYAN}诊断 Telegram API 连通性...${NC}\"\n\tlocal verify=\"\"\n\tverify=$(curl -s --connect-timeout 5 --max-time 10 \"https://api.telegram.org/bot${tg_token}/getMe\" 2>/dev/null || echo '{\"ok\":false}')\n\tif echo \"$verify\" | grep -q '\"ok\":true'; then\n\t\tlocal bot_name=$(echo \"$verify\" | grep -o '\"username\":\"[^\"]*\"' | head -1 | cut -d'\"' -f4)\n\t\techo -e \" ${GREEN}✅ Telegram API 连通正常 — @${bot_name}${NC}\"\n\telse\n\t\techo -e \" ${RED}❌ Telegram API 连通检测未通过${NC}\"\n\t\techo -e \" ${YELLOW} 可能原因: Token 不正确、网络不通 或 Telegram 被屏蔽${NC}\"\n\t\techo -e \" ${CYAN} 建议: 返回重新配置 Token 或检查代理/网络设置${NC}\"\n\t\treturn\n\tfi\n\n\techo \"\"\n\techo -e \" ${GREEN}╔══════════════════════════════════════════════════╗${NC}\"\n\techo -e \" ${GREEN}║ 请在 Telegram 中向 Bot 发送 /start ║${NC}\"\n\techo -e \" ${GREEN}║ 然后回到这里按回车,脚本自动检测配对请求 ║${NC}\"\n\techo -e \" ${GREEN}╚══════════════════════════════════════════════════╝${NC}\"\n\techo \"\"\n\techo -e \" ${YELLOW}发送 /start 后按回车继续 (输入 q 退出)...${NC}\"\n\tread _wait\n\tcase \"$_wait\" in q|Q) return ;; esac\n\n\tlocal paired=0\n\tlocal attempt=1\n\twhile [ $attempt -le 3 ]; do\n\t\techo -e \" ${CYAN}检测配对请求... (第 ${attempt}/3 轮)${NC}\"\n\t\tlocal pair_json=$(oc_cmd pairing list telegram --json 2>/dev/null || echo \"\")\n\t\tlocal codes=\"\"\n\t\tif [ -n \"$pair_json\" ]; then\n\t\t\tcodes=$(echo \"$pair_json\" | grep -o '\"code\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | cut -d'\"' -f4)\n\t\tfi\n\n\t\tif [ -n \"$codes\" ]; then\n\t\t\t# 逐个处理配对码 (避免管道子 shell 变量丢失问题)\n\t\t\tlocal _codes_tmp=\"/tmp/oc_pair_codes_$$\"\n\t\t\techo \"$codes\" > \"$_codes_tmp\"\n\t\t\twhile IFS= read -r code; do\n\t\t\t\t[ -z \"$code\" ] && continue\n\t\t\t\techo -e \" ${CYAN}发现配对请求: ${code}${NC}\"\n\t\t\t\tlocal approve=$(oc_cmd pairing approve telegram \"$code\" 2>&1)\n\t\t\t\tif echo \"$approve\" | grep -qi \"approved\\|success\\|ok\"; then\n\t\t\t\t\techo \"\"\n\t\t\t\t\techo -e \" ${GREEN}${BOLD}🎉 Telegram 配对成功!${NC}\"\n\t\t\t\tfi\n\t\t\tdone < \"$_codes_tmp\"\n\t\t\trm -f \"$_codes_tmp\"\n\t\t\t# 检查是否还有待配对的\n\t\t\tlocal re_check=$(oc_cmd pairing list telegram --json 2>/dev/null || echo \"\")\n\t\t\tlocal re_codes=$(echo \"$re_check\" | grep -o '\"code\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | cut -d'\"' -f4)\n\t\t\tif [ -z \"$re_codes\" ]; then\n\t\t\t\tpaired=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\tfi\n\n\t\tif [ $attempt -lt 3 ] && [ $paired -eq 0 ]; then\n\t\t\techo -e \" ${YELLOW}未检测到,等待 8 秒后重试...${NC}\"\n\t\t\tsleep 8\n\t\tfi\n\t\tattempt=$((attempt + 1))\n\tdone\n\n\tif [ \"$paired\" -eq 0 ]; then\n\t\techo \"\"\n\t\techo -e \" ${YELLOW}未自动检测到配对请求。${NC}\"\n\t\techo -e \" ${CYAN}如果 Bot 已回复配对码,请手动输入 (回车跳过):${NC}\"\n\t\tprompt_with_default \"配对码\" \"\" manual_code\n\t\tif [ -n \"$manual_code\" ]; then\n\t\t\tlocal approve=$(oc_cmd pairing approve telegram \"$manual_code\" 2>&1)\n\t\t\tif echo \"$approve\" | grep -qi \"approved\\|success\\|ok\"; then\n\t\t\t\techo -e \" ${GREEN}${BOLD}🎉 Telegram 配对成功!${NC}\"\n\t\t\t\tpaired=1\n\t\t\telse\n\t\t\t\techo -e \" ${YELLOW}配对失败: $approve${NC}\"\n\t\t\tfi\n\t\tfi\n\tfi\n\n\t# 配对成功后重启网关,使配对关系立即生效\n\tif [ \"$paired\" -eq 1 ]; then\n\t\techo \"\"\n\t\techo -e \" ${CYAN}正在重启 Gateway 使配对生效...${NC}\"\n\t\trestart_gateway\n\t\techo -e \" ${GREEN}✅ 现在可以在 Telegram 中与 Bot 对话了!${NC}\"\n\tfi\n}\n\n# ══════════════════════════════════════════════════════════════\n# 配置渠道子菜单\n# ══════════════════════════════════════════════════════════════\nconfigure_channels() {\n\twhile true; do\n\t\techo \"\"\n\t\techo -e \" ${BOLD}📡 配置消息渠道${NC}\"\n\t\techo \"\"\n\t\techo -e \" ${CYAN}提示: 微信配置请使用 LuCI 界面「微信配置」菜单${NC}\"\n\t\techo \"\"\n\t\techo -e \" ${CYAN}1)${NC} QQ 机器人 ${GREEN}(腾讯QQ)${NC}\"\n\t\techo -e \" ${CYAN}2)${NC} Telegram ${GREEN}(最常用)${NC}\"\n\t\techo -e \" ${CYAN}3)${NC} Discord\"\n\t\techo -e \" ${CYAN}4)${NC} 飞书 (Feishu)\"\n\t\techo -e \" ${CYAN}5)${NC} Slack\"\n\t\techo -e \" ${CYAN}6)${NC} WhatsApp ${YELLOW}(需通过 Web 控制台扫码)${NC}\"\n\t\techo -e \" ${CYAN}7)${NC} Telegram 配对助手\"\n\t\techo -e \" ${CYAN}8)${NC} 官方完整渠道配置向导\"\n\t\techo -e \" ${CYAN}0)${NC} 返回主菜单\"\n\t\techo \"\"\n\t\tprompt_with_default \"请选择\" \"1\" ch_choice\n\n\t\tcase \"$ch_choice\" in\n\t\t\t1) configure_qq ;;\n\t\t\t2) configure_telegram ;;\n\t\t\t3) configure_discord ;;\n\t\t\t4) configure_feishu ;;\n\t\t\t5) configure_slack ;;\n\t\t\t6)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${YELLOW}WhatsApp 需要通过 Web 控制台扫码配对:${NC}\"\n\t\t\t\tlocal gw_token=$(json_get gateway.auth.token)\n\t\t\t\tlocal gw_port=$(json_get gateway.port)\n\t\t\t\tgw_port=${gw_port:-18789}\n\t\t\t\techo -e \" ${CYAN}http://<你的路由器IP>:${gw_port}/?token=${gw_token}${NC}\"\n\t\t\t\techo -e \" 打开后进入 Channels → WhatsApp 扫码即可。\"\n\t\t\t\t;;\n\t\t\t7) telegram_pairing ;;\n\t\t\t8)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}启动官方渠道配置向导...${NC}\"\n\t\t\t\t(oc_cmd configure --section channels) || echo -e \" ${YELLOW}配置向导已退出${NC}\"\n\t\t\t\t;;\n\t\t\t0) return ;;\n\t\t\t*) echo -e \" ${YELLOW}无效选择${NC}\" ;;\n\t\tesac\n\tdone\n}\n\n# ══════════════════════════════════════════════════════════════\n# 健康检查\n# ══════════════════════════════════════════════════════════════\nhealth_check() {\n\techo \"\"\n\techo -e \" ${BOLD}🔍 健康检查${NC}\"\n\techo \"\"\n\n\tlocal gw_port=$(json_get gateway.port)\n\tgw_port=${gw_port:-18789}\n\n\t# ── v2026.3.2: 使用官方 config validate 验证配置 ──\n\tif command -v openclaw >/dev/null 2>&1 || [ -n \"$OC_ENTRY\" ]; then\n\t\techo -e \" ${CYAN}验证配置文件格式...${NC}\"\n\t\tlocal validate_out=\"\"\n\t\tvalidate_out=$(oc_cmd config validate --json 2>/dev/null) || true\n\t\tif [ -n \"$validate_out\" ]; then\n\t\t\tlocal has_errors=$(\"$NODE_BIN\" -e \"\n\t\t\t\ttry{const r=JSON.parse(process.argv[1]);\n\t\t\t\tif(r.valid===true){console.log('OK');}\n\t\t\t\telse if(r.errors&&r.errors.length>0){r.errors.forEach(e=>console.log('ERR:'+e.message));}\n\t\t\t\telse{console.log('OK');}}catch(e){console.log('SKIP');}\n\t\t\t\" \"$validate_out\" 2>/dev/null)\n\t\t\tif [ \"$has_errors\" = \"OK\" ]; then\n\t\t\t\techo -e \" ${GREEN}✅ 配置文件格式有效${NC}\"\n\t\t\telif [ \"$has_errors\" = \"SKIP\" ]; then\n\t\t\t\techo -e \" ${YELLOW}⚠️ 无法解析验证结果,跳过${NC}\"\n\t\t\telse\n\t\t\t\techo -e \" ${RED}❌ 配置文件存在错误:${NC}\"\n\t\t\t\techo \"$has_errors\" | while IFS= read -r line; do\n\t\t\t\t\techo -e \" ${YELLOW}• ${line#ERR:}${NC}\"\n\t\t\t\tdone\n\t\t\tfi\n\t\telse\n\t\t\techo -e \" ${YELLOW}⚠️ config validate 不可用,跳过格式验证${NC}\"\n\t\tfi\n\t\techo \"\"\n\tfi\n\n\t# ── 自动修复: 移除旧版错误写入的顶层 models.xxx 无效键 ──\n\tif [ -f \"$CONFIG_FILE\" ]; then\n\t\tlocal has_bad_models=$(\"$NODE_BIN\" -e \"\n\t\t\tconst d=JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8'));\n\t\t\tconst m=d.models;\n\t\t\tif(m&&typeof m==='object'){\n\t\t\t\tconst bad=Object.keys(m).filter(k=>['openai','anthropic','google','openrouter','deepseek','github-copilot','dashscope','xai','groq','siliconflow','custom'].includes(k));\n\t\t\t\tif(bad.length>0){console.log(bad.join(','));}\n\t\t\t}\n\t\t\" 2>/dev/null)\n\t\tif [ -n \"$has_bad_models\" ]; then\n\t\t\techo -e \" ${YELLOW}⚠️ 检测到旧版配置错误: 顶层 models 包含无效键 (${has_bad_models})${NC}\"\n\t\t\techo -e \" ${CYAN}正在自动修复...${NC}\"\n\t\t\t\"$NODE_BIN\" -e \"\n\t\t\t\tconst fs=require('fs');\n\t\t\t\tconst d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));\n\t\t\t\tconst bad=['openai','anthropic','google','openrouter','deepseek','github-copilot','dashscope','xai','groq','siliconflow','custom'];\n\t\t\t\tif(d.models&&typeof d.models==='object'){\n\t\t\t\t\tbad.forEach(k=>delete d.models[k]);\n\t\t\t\t\tif(Object.keys(d.models).length===0||(Object.keys(d.models).length===1&&d.models.providers)){}\n\t\t\t\t\telse if(Object.keys(d.models).filter(k=>k!=='mode'&&k!=='providers').length===0){}\n\t\t\t\t\tif(!d.models.providers&&Object.keys(d.models).every(k=>bad.includes(k)||k==='mode'))delete d.models;\n\t\t\t\t}\n\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\" 2>/dev/null\n\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\techo -e \" ${GREEN}✅ 已移除无效的 models 配置键${NC}\"\n\t\t\techo \"\"\n\t\tfi\n\tfi\n\n\tif check_port_listening \"$gw_port\"; then\n\t\techo -e \" ${GREEN}✅ Gateway 端口 ${gw_port} 正在监听${NC}\"\n\telse\n\t\techo -e \" ${RED}❌ Gateway 端口 ${gw_port} 未监听${NC}\"\n\tfi\n\n\tlocal 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\")\n\tif [ \"$http_code\" = \"200\" ] || [ \"$http_code\" = \"302\" ] || [ \"$http_code\" = \"401\" ]; then\n\t\techo -e \" ${GREEN}✅ HTTP 响应正常 (${http_code})${NC}\"\n\telse\n\t\techo -e \" ${RED}❌ HTTP 响应异常 (${http_code})${NC}\"\n\tfi\n\n\t# v2026.3.2: 使用 gateway health --json 做深度健康检查 (HTTP /health 已被 SPA 接管)\n\tlocal health_resp=$(oc_cmd gateway health --json 2>/dev/null)\n\tif [ -n \"$health_resp\" ]; then\n\t\tlocal 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)\n\t\tif [ \"$health_ok\" = \"ok\" ]; then\n\t\t\techo -e \" ${GREEN}✅ Gateway 健康检查正常${NC}\"\n\t\telif [ \"$health_ok\" = \"parse_error\" ]; then\n\t\t\techo -e \" ${YELLOW}⚠️ Gateway 健康检查响应无法解析${NC}\"\n\t\telse\n\t\t\techo -e \" ${YELLOW}⚠️ Gateway 健康检查异常${NC}\"\n\t\tfi\n\tfi\n\n\tif [ -f \"$CONFIG_FILE\" ]; then\n\t\techo -e \" ${GREEN}✅ 配置文件存在${NC}\"\n\telse\n\t\techo -e \" ${RED}❌ 配置文件不存在${NC}\"\n\tfi\n\n\techo \"\"\n\techo -e \" ${CYAN}运行官方诊断...${NC}\"\n\toc_cmd doctor 2>/dev/null || true\n\n\techo \"\"\n\techo -e \" ${CYAN}最近日志 (最后 10 行):${NC}\"\n\tlogread -e openclaw 2>/dev/null | tail -10 || echo \" (无日志)\"\n}\n\n# ══════════════════════════════════════════════════════════════\n# 恢复默认配置\n# ══════════════════════════════════════════════════════════════\nreset_to_defaults() {\n\techo \"\"\n\techo -e \" ${BOLD}⚠️ 恢复默认配置${NC}\"\n\techo \"\"\n\techo -e \" ${YELLOW}请选择恢复级别:${NC}\"\n\techo \"\"\n\techo -e \" ${CYAN}1)${NC} 🔧 仅重置网关设置 (端口/绑定/模式恢复默认,保留模型和渠道)\"\n\techo -e \" ${CYAN}2)${NC} 🤖 清除模型配置 (移除所有 AI 模型和 API Key)\"\n\techo -e \" ${CYAN}3)${NC} 📡 清除渠道配置 (移除所有消息渠道配置)\"\n\techo -e \" ${CYAN}4)${NC} 🔄 完全恢复出厂 (删除所有配置,重新初始化)\"\n\techo -e \" ${CYAN}0)${NC} 返回\"\n\techo \"\"\n\tprompt_with_default \"请选择\" \"0\" reset_choice\n\n\tcase \"$reset_choice\" in\n\t\t1)\n\t\t\techo \"\"\n\t\t\techo -e \" ${YELLOW}将重置: 网关端口→18789, 绑定→lan, 模式→local${NC}\"\n\t\t\techo -e \" ${YELLOW}保留: 认证令牌、模型配置、消息渠道${NC}\"\n\t\t\tprompt_with_default \"确认恢复网关默认设置? (yes/no)\" \"no\" confirm\n\t\t\tif [ \"$confirm\" = \"yes\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}正在重置网关设置...${NC}\"\n\t\t\t\tjson_set gateway.port 18789 2>&1\n\t\t\t\tjson_set gateway.bind lan 2>&1\n\t\t\t\tjson_set gateway.mode local 2>&1\n\t\t\t\tjson_set gateway.controlUi.allowInsecureAuth true 2>&1\n\t\t\t\tjson_set gateway.controlUi.dangerouslyDisableDeviceAuth true 2>&1\n\t\t\t\tjson_set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback true 2>&1\n\t\t\t\tjson_set gateway.tailscale.mode off 2>&1\n\t\t\t\techo -e \" ${GREEN}✅ 网关设置已恢复默认${NC}\"\n\t\t\t\task_restart\n\t\t\telse\n\t\t\t\techo -e \" ${CYAN}已取消${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\t2)\n\t\t\techo \"\"\n\t\t\techo -e \" ${RED}⚠️ 将清除: 所有模型配置、API Key、活跃模型设置${NC}\"\n\t\t\tprompt_with_default \"确认清除所有模型配置? (yes/no)\" \"no\" confirm\n\t\t\tif [ \"$confirm\" = \"yes\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}正在清除模型配置...${NC}\"\n\t\t\t\toc_cmd config unset models >/dev/null 2>&1 || true\n\t\t\t\toc_cmd config unset agents.defaults.model >/dev/null 2>&1 || true\n\t\t\t\toc_cmd config unset agents.defaults.models >/dev/null 2>&1 || true\n\t\t\t\t# 同时清除 auth-profiles.json 中的认证信息\n\t\t\t\tlocal auth_file=\"${OC_STATE_DIR}/agents/main/agent/auth-profiles.json\"\n\t\t\t\tif [ -f \"$auth_file\" ]; then\n\t\t\t\t\techo '{\"version\":1,\"profiles\":{},\"usageStats\":{}}' > \"$auth_file\"\n\t\t\t\t\tchown openclaw:openclaw \"$auth_file\" 2>/dev/null || true\n\t\t\t\tfi\n\t\t\t\techo -e \" ${GREEN}✅ 模型配置已清除${NC}\"\n\t\t\t\techo -e \" ${YELLOW}请通过菜单 [2] 重新配置 AI 模型${NC}\"\n\t\t\t\task_restart\n\t\t\telse\n\t\t\t\techo -e \" ${CYAN}已取消${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\t3)\n\t\t\techo \"\"\n\t\t\techo -e \" ${RED}⚠️ 将清除: 所有消息渠道配置 (Telegram/Discord/飞书等)${NC}\"\n\t\t\tprompt_with_default \"确认清除所有渠道配置? (yes/no)\" \"no\" confirm\n\t\t\tif [ \"$confirm\" = \"yes\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}正在清除渠道配置...${NC}\"\n\t\t\t\t# 清除 openclaw.json 中的 channels 配置\n\t\t\t\toc_cmd config unset channels >/dev/null 2>&1 || true\n\t\t\t\t\n\t\t\t\t# v2026.3.14: 同时清除 plugins 中与渠道相关的配置\n\t\t\t\t# 防止重置后插件配置残留导致状态不一致\n\t\t\t\tif [ -f \"$CONFIG_FILE\" ] && [ -x \"$NODE_BIN\" ]; then\n\t\t\t\t\t\"$NODE_BIN\" -e \"\n\t\t\t\t\t\tconst fs=require('fs');\n\t\t\t\t\t\ttry{\n\t\t\t\t\t\t\tconst d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));\n\t\t\t\t\t\t\tlet modified=false;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// 清除 plugins.entries 中与消息渠道相关的插件\n\t\t\t\t\t\t\tif(d.plugins && d.plugins.entries){\n\t\t\t\t\t\t\t\tconst channelPlugins=['openclaw-qqbot','@tencent-connect/openclaw-qqbot','openclaw-lark','@larksuite/openclaw-lark'];\n\t\t\t\t\t\t\t\tchannelPlugins.forEach(p=>{\n\t\t\t\t\t\t\t\t\tif(d.plugins.entries[p]){\n\t\t\t\t\t\t\t\t\t\tdelete d.plugins.entries[p];\n\t\t\t\t\t\t\t\t\t\tmodified=true;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t// 清除 plugins.allow 中的渠道插件\n\t\t\t\t\t\t\tif(Array.isArray(d.plugins && d.plugins.allow)){\n\t\t\t\t\t\t\t\tconst beforeLen=d.plugins.allow.length;\n\t\t\t\t\t\t\t\td.plugins.allow=d.plugins.allow.filter(p=>\n\t\t\t\t\t\t\t\t\t!p.includes('qqbot') && \n\t\t\t\t\t\t\t\t\t!p.includes('lark') && \n\t\t\t\t\t\t\t\t\t!p.includes('telegram') &&\n\t\t\t\t\t\t\t\t\t!p.includes('discord') &&\n\t\t\t\t\t\t\t\t\t!p.includes('slack') &&\n\t\t\t\t\t\t\t\t\t!p.includes('whatsapp')\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tif(d.plugins.allow.length!==beforeLen)modified=true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\tif(modified){\n\t\t\t\t\t\t\t\tfs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));\n\t\t\t\t\t\t\t\tconsole.log('CLEANED');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}catch(e){}\n\t\t\t\t\t\" 2>/dev/null\n\t\t\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\t\tfi\n\t\t\t\t\n\t\t\t\t# 清除飞书扩展目录中的敏感数据 (保留插件本体)\n\t\t\t\tlocal feishu_ext_dir=\"${OC_STATE_DIR}/extensions/openclaw-lark\"\n\t\t\t\tif [ -d \"$feishu_ext_dir\" ]; then\n\t\t\t\t\t# 只清除配置文件,保留插件代码\n\t\t\t\t\trm -f \"${feishu_ext_dir}/.credentials\"* 2>/dev/null\n\t\t\t\t\trm -f \"${feishu_ext_dir}/config.json\" 2>/dev/null\n\t\t\t\t\trm -rf \"${feishu_ext_dir}/.cache\" 2>/dev/null\n\t\t\t\t\techo -e \" ${CYAN}已清理飞书插件缓存数据${NC}\"\n\t\t\t\tfi\n\t\t\t\t\n\t\t\t\t# 清除 QQ 机器人扩展目录中的敏感数据\n\t\t\t\tlocal qqbot_ext_dir=\"${OC_STATE_DIR}/extensions/openclaw-qqbot\"\n\t\t\t\tif [ -d \"$qqbot_ext_dir\" ]; then\n\t\t\t\t\trm -f \"${qqbot_ext_dir}/credentials\"* 2>/dev/null\n\t\t\t\t\trm -f \"${qqbot_ext_dir}/config.json\" 2>/dev/null\n\t\t\t\tfi\n\t\t\t\t\n\t\t\t\techo -e \" ${GREEN}✅ 渠道配置已清除${NC}\"\n\t\t\t\techo -e \" ${YELLOW}请通过菜单 [4] 重新配置消息渠道${NC}\"\n\t\t\t\task_restart\n\t\t\telse\n\t\t\t\techo -e \" ${CYAN}已取消${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\tc)\n\t\t\techo \"\"\n\t\t\techo -e \" ${RED}╔══════════════════════════════════════════════════════╗${NC}\"\n\t\t\techo -e \" ${RED}║ ⚠️ 完全恢复出厂设置 ║${NC}\"\n\t\t\techo -e \" ${RED}║ 此操作将删除所有配置并重新初始化 ║${NC}\"\n\t\t\techo -e \" ${RED}╚══════════════════════════════════════════════════════╝${NC}\"\n\t\t\techo \"\"\n\t\t\techo -e \" ${RED}此操作不可撤销!${NC}\"\n\t\t\tprompt_with_default \"输入 RESET 确认恢复出厂设置\" \"\" confirm\n\t\t\tif [ \"$confirm\" = \"RESET\" ]; then\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}[1/5] 停止 Gateway...${NC}\"\n\t\t\t\t# 只停止 gateway 实例, 不能停 pty (否则会断开当前终端连接)\n\t\t\t\tlocal gw_pid=\"\"\n\t\t\t\tgw_pid=$(ubus call service list '{\"name\":\"openclaw\"}' 2>/dev/null | jsonfilter -e '$.openclaw.instances.gateway.pid' 2>/dev/null) || true\n\t\t\t\tif [ -n \"$gw_pid\" ] && kill -0 \"$gw_pid\" 2>/dev/null; then\n\t\t\t\t\tkill \"$gw_pid\" 2>/dev/null || true\n\t\t\t\t\tsleep 2\n\t\t\t\telse\n\t\t\t\t\t# 按端口查找 gateway 进程\n\t\t\t\t\tlocal gw_port_cur=$(json_get gateway.port)\n\t\t\t\t\tgw_port_cur=${gw_port_cur:-18789}\n\t\t\t\t\tlocal gw_pid2=$(get_pid_by_port \"$gw_port_cur\")\n\t\t\t\t\tif [ -n \"$gw_pid2\" ]; then\n\t\t\t\t\t\tkill \"$gw_pid2\" 2>/dev/null || true\n\t\t\t\t\t\tsleep 2\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\t\techo -e \" ${GREEN} Gateway 已停止${NC}\"\n\n\t\t\t\techo -e \" ${CYAN}[2/5] 备份当前配置...${NC}\"\n\t\t\t\tlocal backup_dir=\"${OC_STATE_DIR}/backups\"\n\t\t\t\tlocal backup_ts=$(date +%Y%m%d_%H%M%S)\n\t\t\t\tmkdir -p \"$backup_dir\"\n\t\t\t\tchown openclaw:openclaw \"$backup_dir\" 2>/dev/null || true\n\t\t\t\tif [ -f \"$CONFIG_FILE\" ]; then\n\t\t\t\t\tcp \"$CONFIG_FILE\" \"${backup_dir}/openclaw_${backup_ts}.json\"\n\t\t\t\t\techo -e \" ${GREEN} 备份已保存: backups/openclaw_${backup_ts}.json${NC}\"\n\t\t\t\tfi\n\n\t\t\t\techo -e \" ${CYAN}[3/5] 重置配置...${NC}\"\n\t\t\t\t# 直接删除配置文件 (避免 oc_cmd reset 可能的交互式阻塞)\n\t\t\t\trm -f \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\t\trm -f \"${CONFIG_FILE}.bak\" 2>/dev/null || true\n\t\t\t\techo '{}' > \"$CONFIG_FILE\"\n\t\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\t\techo -e \" ${GREEN} 配置已清除${NC}\"\n\n\t\t\t\techo -e \" ${CYAN}[4/5] 重新初始化...${NC}\"\n\t\t\t\t# 尝试 onboard,超时 10 秒避免阻塞\n\t\t\t\tlocal _node_bin\n\t\t\t\t_node_bin=$(which node 2>/dev/null || echo \"$NODE_BIN\")\n\t\t\t\tif command -v timeout >/dev/null 2>&1; then\n\t\t\t\t\ttimeout 10 sh -c \"\\\"$_node_bin\\\" \\\"$OC_ENTRY\\\" onboard --non-interactive --accept-risk --tools-profile coding\" >/dev/null 2>&1 || true\n\t\t\t\telse\n\t\t\t\t\t\"$_node_bin\" \"$OC_ENTRY\" onboard --non-interactive --accept-risk --tools-profile coding >/dev/null 2>&1 &\n\t\t\t\t\tlocal _ob_pid=$!\n\t\t\t\t\tsleep 10\n\t\t\t\t\tkill \"$_ob_pid\" 2>/dev/null || true\n\t\t\t\t\twait \"$_ob_pid\" 2>/dev/null || true\n\t\t\t\tfi\n\t\t\t\techo -e \" ${GREEN} 初始化完成${NC}\"\n\n\t\t\t\techo -e \" ${CYAN}[5/5] 应用 OpenWrt 适配配置...${NC}\"\n\t\t\t\tlocal new_token\n\t\t\t\tnew_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)\n\t\t\t\tjson_set gateway.port 18789\n\t\t\t\tjson_set gateway.bind lan\n\t\t\t\tjson_set gateway.mode local\n\t\t\t\tjson_set gateway.auth.mode token\n\t\t\t\tjson_set gateway.auth.token \"$new_token\"\n\t\t\t\tjson_set gateway.controlUi.allowInsecureAuth true\n\t\t\t\tjson_set gateway.controlUi.dangerouslyDisableDeviceAuth true\n\t\t\t\tjson_set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback true\n\t\t\t\tjson_set gateway.tailscale.mode off\n\t\t\t\tjson_set acp.dispatch.enabled false\n\t\t\t\tjson_set tools.profile coding\n\n\t\t\t\t# 同步 token 到 UCI\n\t\t\t\t. /lib/functions.sh 2>/dev/null || true\n\t\t\t\tuci set openclaw.main.token=\"$new_token\" 2>/dev/null\n\t\t\t\tuci commit openclaw 2>/dev/null\n\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${GREEN}✅ 出厂设置已恢复!${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}新认证令牌: ${new_token}${NC}\"\n\t\t\t\techo \"\"\n\n\t\t\t\t# 重启 gateway (通过 procd reload, 这样不会杀 pty)\n\t\t\t\t/etc/init.d/openclaw start >/dev/null 2>&1 &\n\t\t\t\techo -e \" ${YELLOW}⏳ Gateway 启动中,请稍候...${NC}\"\n\t\t\t\tlocal gw_port=18789\n\t\t\t\tlocal waited=0\n\t\t\t\twhile [ $waited -lt 15 ]; do\n\t\t\t\t\tsleep 2\n\t\t\t\t\twaited=$((waited + 2))\n\t\t\t\t\tif check_port_listening \"$gw_port\"; then\n\t\t\t\t\t\techo -e \" ${GREEN}✅ Gateway 已重新启动${NC}\"\n\t\t\t\t\t\tbreak\n\t\t\t\t\tfi\n\t\t\t\tdone\n\t\t\t\tif [ $waited -ge 15 ]; then\n\t\t\t\t\techo -e \" ${YELLOW}⏳ Gateway 可能仍在启动中,请稍后检查${NC}\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\techo -e \" ${CYAN}已取消${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\t0|\"\") return ;;\n\t\t*) echo -e \" ${YELLOW}无效选择${NC}\" ;;\n\tesac\n}\n\n# ══════════════════════════════════════════════════════════════\n# 主菜单\n# ══════════════════════════════════════════════════════════════\n# ══════════════════════════════════════════════════════════════\n# 备份/还原配置菜单 (v2026.3.8+ openclaw backup create/verify)\n# ══════════════════════════════════════════════════════════════\nbackup_restore_menu() {\n\techo \"\"\n\techo -e \" ${BOLD}💾 备份/还原配置${NC}\"\n\techo \"\"\n\techo -e \" ${CYAN}1)${NC} 创建配置备份 (仅配置文件)\"\n\techo -e \" ${CYAN}2)${NC} 创建完整备份 (配置 + 状态数据)\"\n\techo -e \" ${CYAN}3)${NC} 验证最新备份\"\n\techo -e \" ${CYAN}4)${NC} 查看备份列表\"\n\techo -e \" ${CYAN}5)${NC} 从最新备份恢复配置\"\n\techo -e \" ${CYAN}0)${NC} 返回主菜单\"\n\techo \"\"\n\tprompt_with_default \"请选择\" \"1\" backup_choice\n\n\t# 备份目录 (openclaw backup create 输出到 CWD)\n\tlocal backup_dir=\"${OC_STATE_DIR}/backups\"\n\tmkdir -p \"$backup_dir\" 2>/dev/null\n\n\tcase \"$backup_choice\" in\n\t\t1)\n\t\t\techo -e \" ${CYAN}正在创建配置备份...${NC}\"\n\t\t\tlocal out\n\t\t\tout=$(cd \"$backup_dir\" && oc_cmd backup create --only-config --no-include-workspace 2>&1)\n\t\t\tlocal rc=$?\n\t\t\techo \"$out\"\n\t\t\tif [ $rc -eq 0 ] && echo \"$out\" | grep -q \"\\.tar\\.gz\"; then\n\t\t\t\techo -e \" ${GREEN}✅ 配置备份已创建${NC}\"\n\t\t\telse\n\t\t\t\techo -e \" ${YELLOW}⚠️ 备份功能需要 OpenClaw v2026.3.8+${NC}\"\n\t\t\t\techo -e \" ${DIM}如果备份命令不可用,可手动备份: cp ${CONFIG_FILE} ${CONFIG_FILE}.bak${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\t2)\n\t\t\techo -e \" ${CYAN}正在创建完整备份...${NC}\"\n\t\t\techo -e \" ${DIM}(包含配置和状态数据,可能需要较长时间)${NC}\"\n\t\t\tlocal out\n\t\t\tout=$(cd \"$backup_dir\" && HOME=\"$backup_dir\" oc_cmd backup create --no-include-workspace 2>&1)\n\t\t\tlocal rc=$?\n\t\t\techo \"$out\"\n\t\t\t# 完整备份可能输出到 HOME,尝试移动到 backup_dir\n\t\t\tfor f in \"${OC_DATA}\"/*-openclaw-backup.tar.gz; do\n\t\t\t\t[ -f \"$f\" ] && mv \"$f\" \"$backup_dir/\" 2>/dev/null\n\t\t\tdone\n\t\t\tif [ $rc -eq 0 ] && echo \"$out\" | grep -q \"\\.tar\\.gz\"; then\n\t\t\t\techo -e \" ${GREEN}✅ 完整备份已创建${NC}\"\n\t\t\telse\n\t\t\t\techo -e \" ${YELLOW}⚠️ 备份失败${NC}\"\n\t\t\t\techo -e \" ${DIM}提示: 如果配置文件有校验警告,完整备份可能受限。请使用选项 1 (仅配置文件) 备份${NC}\"\n\t\t\tfi\n\t\t\t;;\n\t\t3)\n\t\t\tlocal latest=$(ls -t \"${backup_dir}\"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)\n\t\t\tif [ -z \"$latest\" ]; then\n\t\t\t\t# 也检查旧位置\n\t\t\t\tlatest=$(ls -t \"${OC_STATE_DIR}\"/*-openclaw-backup.tar.gz \"${OC_DATA}\"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)\n\t\t\tfi\n\t\t\tif [ -z \"$latest\" ]; then\n\t\t\t\techo -e \" ${YELLOW}未找到备份文件,请先创建备份${NC}\"\n\t\t\telse\n\t\t\t\techo -e \" ${CYAN}验证备份: ${latest}${NC}\"\n\t\t\t\toc_cmd backup verify \"$latest\" 2>&1\n\t\t\tfi\n\t\t\t;;\n\t\tc)\n\t\t\techo \"\"\n\t\t\tif [ -d \"$backup_dir\" ]; then\n\t\t\t\tlocal count=$(ls \"${backup_dir}\"/*-openclaw-backup.tar.gz 2>/dev/null | wc -l)\n\t\t\t\tif [ \"$count\" -gt 0 ] 2>/dev/null; then\n\t\t\t\t\techo -e \" ${BOLD}备份文件列表:${NC}\"\n\t\t\t\t\tls -lh \"${backup_dir}\"/*-openclaw-backup.tar.gz 2>/dev/null | while read line; do\n\t\t\t\t\t\techo -e \" ${DIM}${line}${NC}\"\n\t\t\t\t\tdone\n\t\t\t\telse\n\t\t\t\t\techo -e \" ${YELLOW}暂无备份文件${NC}\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\techo -e \" ${YELLOW}暂无备份文件${NC}\"\n\t\t\tfi\n\t\t\techo \"\"\n\t\t\techo -e \" ${DIM}备份目录: ${backup_dir}${NC}\"\n\t\t\t;;\n\t\td)\n\t\t\tlocal latest=$(ls -t \"${backup_dir}\"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)\n\t\t\tif [ -z \"$latest\" ]; then\n\t\t\t\techo -e \" ${YELLOW}未找到备份文件,请先创建备份${NC}\"\n\t\t\telse\n\t\t\t\techo -e \" ${CYAN}将从以下备份恢复:${NC}\"\n\t\t\t\techo -e \" ${DIM}${latest}${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${YELLOW}⚠️ 这会还原备份中的所有配置和数据文件到原路径!${NC}\"\n\t\t\t\tprompt_with_default \"确认恢复? (y/N)\" \"N\" confirm_restore\n\t\t\t\tif [ \"$confirm_restore\" = \"y\" ] || [ \"$confirm_restore\" = \"Y\" ]; then\n\t\t\t\t\t# 验证备份中 openclaw.json 有效\n\t\t\t\t\tlocal tmp_json=\"/tmp/oc-restore-check.json\"\n\t\t\t\t\ttar -xzf \"$latest\" --wildcards '*/openclaw.json' -O > \"$tmp_json\" 2>/dev/null\n\t\t\t\t\tif [ ! -s \"$tmp_json\" ] || ! \"$NODE_BIN\" -e \"JSON.parse(require('fs').readFileSync('${tmp_json}','utf8'))\" 2>/dev/null; then\n\t\t\t\t\t\trm -f \"$tmp_json\"\n\t\t\t\t\t\techo -e \" ${RED}❌ 备份中的配置文件无效,恢复已取消${NC}\"\n\t\t\t\t\telse\n\t\t\t\t\t\trm -f \"$tmp_json\"\n\t\t\t\t\t\t# 备份当前配置\n\t\t\t\t\t\tcp -f \"$CONFIG_FILE\" \"${CONFIG_FILE}.pre-restore\" 2>/dev/null\n\t\t\t\t\t\t# 获取备份名前缀\n\t\t\t\t\t\tlocal backup_name=$(tar -tzf \"$latest\" 2>/dev/null | head -1 | cut -d/ -f1)\n\t\t\t\t\t\tif [ -z \"$backup_name\" ]; then\n\t\t\t\t\t\t\techo -e \" ${RED}❌ 备份文件格式无法识别${NC}\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\techo -e \" ${DIM}正在还原文件...${NC}\"\n\t\t\t\t\t\t\t# 停止服务\n\t\t\t\t\t\t\t/etc/init.d/openclaw stop >/dev/null 2>&1\n\t\t\t\t\t\t\tsleep 2\n\t\t\t\t\t\t\t# 提取 payload 到根目录 (还原到原始绝对路径)\n\t\t\t\t\t\t\ttar -xzf \"$latest\" --strip-components=3 -C / \"${backup_name}/payload/posix/\" 2>&1\n\t\t\t\t\t\t\t# 修复权限\n\t\t\t\t\t\t\tfix_openclaw_state_permissions 2>/dev/null || true\n\t\t\t\t\t\t\techo -e \" ${GREEN}✅ 配置和数据已完整恢复!原配置已保存为 openclaw.json.pre-restore${NC}\"\n\t\t\t\t\t\t\techo \"\"\n\t\t\t\t\t\t\tprompt_with_default \"是否重启服务使配置生效? (Y/n)\" \"Y\" do_restart\n\t\t\t\t\t\t\tif [ \"$do_restart\" != \"n\" ] && [ \"$do_restart\" != \"N\" ]; then\n\t\t\t\t\t\t\t\trestart_gateway\n\t\t\t\t\t\t\tfi\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\telse\n\t\t\t\t\techo -e \" ${DIM}已取消${NC}\"\n\t\t\t\tfi\n\t\t\tfi\n\t\t\t;;\n\t\t0|\"\") return ;;\n\t\t*) echo -e \" ${YELLOW}无效选择${NC}\" ;;\n\tesac\n}\n\n# ══════════════════════════════════════════════════════════════════════════\n# 交互式菜单 (方向键导航)\n# ══════════════════════════════════════════════════════════════════════════\n\n# 检测是否支持交互模式 (需要 Node.js + TTY 终端)\ncan_use_interactive() {\n\t[ -x \"$NODE_BIN\" ] || return 1\n\t[ -f \"$OC_INTERACTIVE\" ] || return 1\n\t[ -f \"$OC_MENU_ENGINE\" ] || return 1\n\t# 检测是否为真实 TTY (排除某些非交互环境)\n\t[ -t 0 ] && [ -t 1 ] || return 1\n\treturn 0\n}\n\n# 启动交互式菜单 (方向键导航版本)\nlaunch_interactive_menu() {\n\tif ! can_use_interactive; then\n\t\techo -e \" ${YELLOW}⚠️ 当前环境不支持交互模式,使用传统菜单${NC}\"\n\t\treturn 1\n\tfi\n\n\t# 调用 Node.js 交互式前端\n\t\"$NODE_BIN\" \"$OC_INTERACTIVE\" 2>&1\n\tlocal rc=$?\n\n\tfix_openclaw_state_permissions 2>/dev/null || true\n\t# 返回后刷新配置权限\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\treturn $rc\n}\n\n# 启动交互式模型配置\nlaunch_interactive_model_config() {\n\tif ! can_use_interactive; then\n\t\techo -e \" ${YELLOW}⚠️ 当前环境不支持交互模式,使用传统菜单${NC}\"\n\t\tconfigure_model # 回退到传统菜单\n\t\treturn $?\n\tfi\n\n\t# 调用 Node.js 交互式前端的模型配置模块\n\t\"$NODE_BIN\" \"$OC_INTERACTIVE\" model 2>&1\n\tlocal rc=$?\n\n\tfix_openclaw_state_permissions 2>/dev/null || true\n\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\treturn $rc\n}\n\nmain_menu() {\n\t# 直接启动交互模式 (如果支持)\n\tif can_use_interactive && [ \"${OC_FORCE_TRADITIONAL:-0}\" != \"1\" ]; then\n\t\tlaunch_interactive_menu\n\t\treturn $?\n\tfi\n\n\t# 传统菜单 (回退方案)\n\twhile true; do\n\t\techo \"\"\n\t\techo -e \"${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}\"\n\t\techo -e \"${GREEN}║${NC} ${BOLD}OpenClaw AI Gateway — OpenWrt 配置管理${NC} ${GREEN}║${NC}\"\n\t\techo -e \"${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}\"\n\t\techo \"\"\n\t\techo -e \" ${DIM}━━━ AI 模型配置 ━━━${NC}\"\n\t\techo -e \" ${CYAN}1)${NC} 🤖 配置 AI 模型和提供商\"\n\t\techo -e \" ${CYAN}2)${NC} 🎯 设置活动模型\"\n\t\techo \"\"\n\t\techo -e \" ${DIM}━━━ 消息渠道 ━━━${NC}\"\n\t\techo -e \" ${CYAN}3)${NC} 📡 配置消息渠道 (电报/QQ/飞书)\"\n\t\techo \"\"\n\t\techo -e \" ${DIM}━━━ 系统管理 ━━━${NC}\"\n\t\techo -e \" ${CYAN}4)${NC} 🩺 健康检查与状态\"\n\t\techo -e \" ${CYAN}5)${NC} 📋 查看日志\"\n\t\techo -e \" ${CYAN}6)${NC} 🔄 重启 Gateway\"\n\t\techo \"\"\n\t\techo -e \" ${DIM}━━━ 高级选项 ━━━${NC}\"\n\t\techo -e \" ${CYAN}7)${NC} 🔧 高级配置\"\n\t\techo -e \" ${CYAN}8)${NC} ♻️ 重置配置\"\n\t\techo -e \" ${CYAN}9)${NC} 📊 显示当前配置概览\"\n\t\techo \"\"\n\t\techo -e \" ${CYAN}0)${NC} 退出\"\n\t\techo \"\"\n\t\tprompt_with_default \"请选择\" \"1\" menu_choice\n\n\t\tcase \"$menu_choice\" in\n\t\t\t1) configure_model ;;\n\t\t\t2) set_active_model ;;\n\t\t\t3) configure_channels ;;\n\t\t\t4) health_check ;;\n\t\t\t5)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}=== OpenClaw 日志 ===${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\tlogread -e openclaw 2>/dev/null | tail -100 || echo \" (无法读取日志)\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"按回车继续\" \"\" _\n\t\t\t\t;;\n\t\t\t6) restart_gateway ;;\n\t\t\t7) advanced_menu ;;\n\t\t\t8) reset_to_defaults ;;\n\t\t\t9) show_current_config ;;\n\t\t\t0)\n\t\t\t\techo -e \" ${GREEN}再见!${NC}\"\n\t\t\t\texit 0\n\t\t\t\t;;\n\t\t\t*) echo -e \" ${YELLOW}无效选择${NC}\" ;;\n\t\tesac\n\tdone\n}\n\n# ── 高级配置菜单 ──\nadvanced_menu() {\n\twhile true; do\n\t\tlocal gw_port gw_bind gw_mode log_level acp_dispatch\n\t\tgw_port=$(json_get \"gateway.port\" 2>/dev/null || echo \"18789\")\n\t\tgw_bind=$(json_get \"gateway.bind\" 2>/dev/null || echo \"lan\")\n\t\tgw_mode=$(json_get \"gateway.mode\" 2>/dev/null || echo \"local\")\n\t\tlog_level=$(json_get \"gateway.logLevel\" 2>/dev/null || echo \"\")\n\t\tacp_dispatch=$(json_get \"acp.dispatch.enabled\" 2>/dev/null || echo \"false\")\n\n\t\techo \"\"\n\t\techo -e \" ${BOLD}🔧 高级配置${NC}\"\n\t\techo \"\"\n\t\techo -e \" ${CYAN}1)${NC} Gateway 端口 ${DIM}(当前: ${gw_port})${NC}\"\n\t\techo -e \" ${CYAN}2)${NC} Gateway 绑定地址 ${DIM}(当前: ${gw_bind})${NC}\"\n\t\techo -e \" ${CYAN}3)${NC} Gateway 运行模式 ${DIM}(当前: ${gw_mode})${NC}\"\n\t\techo -e \" ${CYAN}4)${NC} 日志级别 ${DIM}(当前: ${log_level:-未设置})${NC}\"\n\t\techo -e \" ${CYAN}5)${NC} ACP Dispatch 设置 ${DIM}(当前: ${acp_dispatch})${NC}\"\n\t\techo -e \" ${CYAN}6)${NC} 官方完整配置向导 ${DIM}(oc configure)${NC}\"\n\t\techo -e \" ${CYAN}7)${NC} 查看原始配置 JSON\"\n\t\techo -e \" ${CYAN}8)${NC} 编辑配置文件 ${DIM}(vi / nano)${NC}\"\n\t\techo -e \" ${CYAN}9)${NC} 导出配置备份\"\n\t\techo -e \" ${CYAN}10)${NC} 导入配置\"\n\t\techo -e \" ${CYAN}0)${NC} 返回主菜单\"\n\t\techo \"\"\n\t\tprompt_with_default \"请选择\" \"0\" adv_choice\n\n\t\tcase \"$adv_choice\" in\n\t\t\t1)\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请输入 Gateway 端口\" \"$gw_port\" new_port\n\t\t\t\tif [ -n \"$new_port\" ] && [ \"$new_port\" != \"$gw_port\" ]; then\n\t\t\t\t\tjson_set \"gateway.port\" \"$new_port\"\n\t\t\t\t\t# 同步到 UCI\n\t\t\t\t\tuci set openclaw.main.port=\"$new_port\" 2>/dev/null\n\t\t\t\t\tuci commit openclaw 2>/dev/null\n\t\t\t\t\techo -e \" ${GREEN}✅ 端口已设置为 ${new_port}${NC}\"\n\t\t\t\t\task_restart\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t2)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}绑定地址选项:${NC}\"\n\t\t\t\techo \" lan - 仅 LAN 接口 (推荐)\"\n\t\t\t\techo \" loopback - 仅本机访问\"\n\t\t\t\techo \" all - 所有接口 (0.0.0.0)\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请输入绑定地址\" \"$gw_bind\" new_bind\n\t\t\t\tif [ -n \"$new_bind\" ]; then\n\t\t\t\t\tcase \"$new_bind\" in\n\t\t\t\t\t\tlan|loopback|all)\n\t\t\t\t\t\t\tjson_set \"gateway.bind\" \"$new_bind\"\n\t\t\t\t\t\t\tuci set openclaw.main.bind=\"$new_bind\" 2>/dev/null\n\t\t\t\t\t\t\tuci commit openclaw 2>/dev/null\n\t\t\t\t\t\t\techo -e \" ${GREEN}✅ 绑定地址已设置为 ${new_bind}${NC}\"\n\t\t\t\t\t\t\task_restart\n\t\t\t\t\t\t\t;;\n\t\t\t\t\t\t*) echo -e \" ${YELLOW}无效选项${NC}\" ;;\n\t\t\t\t\tesac\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t3)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}运行模式选项:${NC}\"\n\t\t\t\techo \" local - 本地模式 (推荐)\"\n\t\t\t\techo \" remote - 远程模式\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请输入运行模式\" \"$gw_mode\" new_mode\n\t\t\t\tif [ -n \"$new_mode\" ] && [ \"$new_mode\" != \"$gw_mode\" ]; then\n\t\t\t\t\tjson_set \"gateway.mode\" \"$new_mode\"\n\t\t\t\t\techo -e \" ${GREEN}✅ 运行模式已设置为 ${new_mode}${NC}\"\n\t\t\t\t\task_restart\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t4)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}日志级别选项:${NC}\"\n\t\t\t\techo \" debug, info, warn, error\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请输入日志级别\" \"${log_level:-info}\" new_level\n\t\t\t\tif [ -n \"$new_level\" ]; then\n\t\t\t\t\tjson_set \"gateway.logLevel\" \"$new_level\"\n\t\t\t\t\techo -e \" ${GREEN}✅ 日志级别已设置为 ${new_level}${NC}\"\n\t\t\t\t\task_restart\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t5)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}ACP Dispatch 选项:${NC}\"\n\t\t\t\techo \" true - 启用 (可能占用大量内存)\"\n\t\t\t\techo \" false - 禁用 (推荐路由器使用)\"\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请输入设置\" \"$acp_dispatch\" new_acp\n\t\t\t\tcase \"$new_acp\" in\n\t\t\t\t\ttrue|false)\n\t\t\t\t\t\tjson_set \"acp.dispatch.enabled\" \"$new_acp\"\n\t\t\t\t\t\techo -e \" ${GREEN}✅ ACP Dispatch 已设置为 ${new_acp}${NC}\"\n\t\t\t\t\t\task_restart\n\t\t\t\t\t\t;;\n\t\t\t\t\t*) echo -e \" ${YELLOW}无效选项${NC}\" ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t6)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}启动官方配置向导...${NC}\"\n\t\t\t\toc_cmd configure\n\t\t\t\task_restart\n\t\t\t\t;;\n\t\t\t7)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}配置文件路径: ${CONFIG_FILE}${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\tif [ -f \"$CONFIG_FILE\" ]; then\n\t\t\t\t\t\"$NODE_BIN\" -e \"console.log(JSON.stringify(JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8')),null,2))\" 2>/dev/null || cat \"$CONFIG_FILE\"\n\t\t\t\telse\n\t\t\t\t\techo \" (配置文件不存在)\"\n\t\t\t\tfi\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"按回车继续\" \"\" _\n\t\t\t\t;;\n\t\t\t8)\n\t\t\t\techo \"\"\n\t\t\t\tif [ -f \"$CONFIG_FILE\" ]; then\n\t\t\t\t\tvi \"$CONFIG_FILE\"\n\t\t\t\t\task_restart\n\t\t\t\telse\n\t\t\t\t\techo -e \" ${YELLOW}配置文件不存在${NC}\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t9) backup_restore_menu ;;\n\t\t\t10)\n\t\t\t\techo \"\"\n\t\t\t\techo -e \" ${CYAN}导入配置${NC}\"\n\t\t\t\techo \"\"\n\t\t\t\tlocal backup_dir=\"${OC_STATE_DIR}/backups\"\n\t\t\t\tif [ -d \"$backup_dir\" ]; then\n\t\t\t\t\techo \" 可用备份:\"\n\t\t\t\t\tls -lt \"$backup_dir\"/*.json 2>/dev/null | head -5 | while read -r line; do\n\t\t\t\t\t\techo \" $(echo \"$line\" | awk '{print $NF}')\"\n\t\t\t\t\tdone\n\t\t\t\tfi\n\t\t\t\techo \"\"\n\t\t\t\tprompt_with_default \"请输入备份文件路径\" \"\" import_path\n\t\t\t\tif [ -n \"$import_path\" ] && [ -f \"$import_path\" ]; then\n\t\t\t\t\tcp \"$import_path\" \"$CONFIG_FILE\"\n\t\t\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\"\n\t\t\t\t\techo -e \" ${GREEN}✅ 配置已导入${NC}\"\n\t\t\t\t\task_restart\n\t\t\t\telse\n\t\t\t\t\techo -e \" ${YELLOW}文件不存在${NC}\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t0) return ;;\n\t\t\t*) echo -e \" ${YELLOW}无效选择${NC}\" ;;\n\t\tesac\n\tdone\n}\n\n# ── 支持命令行参数 ──\ncase \"${1:-}\" in\n\t--tui|--interactive)\n\t\t# 强制启动交互式菜单 (方向键导航)\n\t\tif can_use_interactive; then\n\t\t\tlaunch_interactive_menu\n\t\telse\n\t\t\techo -e \"${YELLOW}⚠️ 当前环境不支持交互模式${NC}\"\n\t\t\techo \"需要: Node.js + TTY 终端\"\n\t\t\texit 1\n\t\tfi\n\t\t;;\n\t--traditional)\n\t\t# 强制使用传统菜单\n\t\texport OC_FORCE_TRADITIONAL=1\n\t\tmain_menu\n\t\t;;\n\t--model)\n\t\t# 直接进入模型配置\n\t\tif can_use_interactive; then\n\t\t\tlaunch_interactive_model_config\n\t\telse\n\t\t\tconfigure_model\n\t\tfi\n\t\t;;\n\t--set)\n\t\tif [ -n \"${2:-}\" ] && [ -n \"${3:-}\" ]; then\n\t\t\tjson_set \"$2\" \"$3\"\n\t\t\tchown openclaw:openclaw \"$CONFIG_FILE\" 2>/dev/null || true\n\t\t\techo -e \"${GREEN}✅ 已设置 $2${NC}\"\n\t\telse\n\t\t\techo \"用法: oc-config.sh --set <key> <value>\"\n\t\tfi\n\t\t;;\n\t--get)\n\t\tif [ -n \"${2:-}\" ]; then\n\t\t\toc_cmd config get \"$2\"\n\t\telse\n\t\t\techo \"用法: oc-config.sh --get <key>\"\n\t\tfi\n\t\t;;\n\t--restart)\n\t\trestart_gateway\n\t\t;;\n\t--backup)\n\t\tbk_dir=\"${OC_STATE_DIR}/backups\"\n\t\tmkdir -p \"$bk_dir\" 2>/dev/null\n\t\techo -e \"${CYAN}正在创建配置备份...${NC}\"\n\t\tcd \"$bk_dir\" && oc_cmd backup create --only-config --no-include-workspace 2>&1\n\t\t;;\n\t--status)\n\t\tshow_current_config\n\t\thealth_check\n\t\t;;\n\t--help|-h)\n\t\techo \"\"\n\t\techo \"OpenClaw AI Gateway — OpenWrt 配置管理工具\"\n\t\techo \"\"\n\t\techo \"用法:\"\n\t\techo \" oc-config.sh # 进入交互式菜单 (自动检测)\"\n\t\techo \" oc-config.sh --tui # 强制使用方向键交互模式\"\n\t\techo \" oc-config.sh --traditional # 强制使用传统数字菜单\"\n\t\techo \" oc-config.sh --model # 直接进入模型配置\"\n\t\techo \" oc-config.sh --set K V # 设置配置项\"\n\t\techo \" oc-config.sh --get K # 读取配置项\"\n\t\techo \" oc-config.sh --restart # 重启 Gateway\"\n\t\techo \" oc-config.sh --status # 查看状态\"\n\t\techo \"\"\n\t\techo \"交互模式快捷键:\"\n\t\techo \" ↑↓ 导航选项\"\n\t\techo \" 回车 确认选择\"\n\t\techo \" ESC/q 返回上级\"\n\t\techo \" 字符 搜索过滤\"\n\t\techo \"\"\n\t\t;;\n\t\"\")\n\t\tmain_menu\n\t\t;;\n\t*)\n\t\techo \"未知参数: $1 (使用 --help 查看帮助)\"\n\t\texit 1\n\t\t;;\nesac\n"}}, "CHANGELOG.md": {"26646fa63dc245216dfffa1bf2f9e58036944749": {"t": 1785674200.2267966, "n": 31701, "h": "a1a65fe94b5b8935", "c": "# Changelog\n\n本项目所有重大变更都将记录在此文件中。\n\n格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。\n\n## [2.0.11] - 2026-07-10\n\n### 修复微信扫码后 Gateway 丢失插件\n\n- 微信安装、升级和登录前置检查改为使用官方 `plugins enable openclaw-weixin`、`plugins registry --refresh` 和 `plugins inspect` 完成闭环。\n- 只有官方 npm 安装记录、SQLite 注册表和微信 channel capability 全部验证通过后,LuCI 才返回安装成功。\n- 启动自愈不再删除 OpenClaw 官方生成的 `plugins.entries.openclaw-weixin`,并在 Gateway 启动前以 `openclaw` 用户刷新和验证注册表。\n- 修复扫码认证已保存,但 Gateway 重启后只加载 `memory-core` 并报 `invalid channels.start channel` 的问题。\n\n### 验证\n\n- 增加插件启用、SQLite 注册表刷新、加载能力校验和冷启动自愈的契约断言。\n\n---\n\n## [2.0.10] - 2026-07-08\n\n### 修复微信渠道配对\n\n- 适配 OpenClaw 2026.6.11 SQLite `installed_plugin_index`,微信安装与升级改用官方 `openclaw plugins install --pin` 写入插件索引。\n- 启动时检测旧 npm 直装目录并一次性迁移到官方插件索引,同时补齐 `plugins.allow` 和 `channels.openclaw-weixin.enabled`。\n- 微信安装、升级和登录流程增加 `https://ilinkai.weixin.qq.com` 连通性检查,日志直接显示 HTTP、TLS 或 timeout 结果。\n- 兼容没有 `su` / `runuser` 的 OpenWrt 固件,微信安装、升级、登录和下线改用 `start-stop-daemon` 兜底以 `openclaw` 用户执行。\n- 微信安装网络探测优先使用 `curl`,避免低内存/精简 musl 固件上 Node `fetch` 触发 undici Wasm OOM。\n- 不再只用 npm 直装并写入已废弃的 `plugins.installs`,避免扫码成功后配置保存导致插件注册丢失。\n- 微信插件安装/升级不再预先停止 Gateway,避免反复安装触发 procd crash-loop。\n- LuCI 状态页和 `status_service` 增强 procd 状态识别,能区分真实启动中、stale pidfile 和 crash-loop 抑制,不再误显示“正在启动”。\n- 微信登录前补充 Node、python3、插件目录、账号状态目录和配置写权限检查,并清理残留登录进程与旧二维码状态。\n- 登录失败时在 LuCI 页面展示真实日志详情,不再只显示“登录失败”。\n- 二维码链接提取更稳,页面明确提示“点击链接后用微信扫码”。\n- README 补充微信插件正确配对流程和常见失败原因。\n\n### 修复权限混乱\n\n- 新增统一权限修复工具 `openclaw-permissions.sh`。\n- 避免把整个 `OC_DATA` 或 `OC_STATE_DIR` 递归改成 `openclaw`。\n- `npm/projects` 及插件源码保持 `openclaw` 可写,匹配官方 managed npm generation 生命周期。\n- retained npm generation 目录保持可清理,修复 Gateway 清理旧 generation 时的 `EACCES`。\n- legacy `extensions` / `archived-extensions` 保持 root-owned,降低 OpenClaw 插件安全检查误报风险。\n\n### 更新一万AI分享粉丝专享 API\n\n- `gpt-5.5` 默认上下文声明调整为 1,000,000 tokens,单次请求的实际可用上限仍以上游 API 为准。\n\n### 修复检测升级\n\n- “快捷操作 → 检测升级”改为语义版本比较,只在远端版本真正高于当前版本时提示升级。\n- 本地版本高于 GitHub latest 时不再误判为可升级。\n- 已实测旧版 `2.0.8` 可通过 LuCI 检测并一键升级到 `2.0.9`,升级日志返回成功且服务保持正常。\n\n### 修复干净安装启动失败\n\n- 修复首次安装后 `doctor --fix` 可能移除 `gateway.auth.token`,导致 Gateway 绑定 LAN 时因缺少认证直接退出,页面显示“启动失败,退出码 78”。\n- `doctor --fix` 执行环境补齐 `NODE_ICU_DATA` 和 Node/OpenClaw PATH,避免部分 musl 固件上配置迁移阶段触发 ICU/Unicode 正则错误。\n- Gateway 启动时增加 `OPENCLAW_GATEWAY_TOKEN` 环境变量兜底,确保 JSON 配置被迁移工具改写后仍能使用 UCI token 启动。\n- `.run` 安装器补齐运行依赖安装,避免精简固件缺少 GNU tar 时 Node.js `.tar.xz` 解压失败。\n- `openclaw-env setup` 改为先完整解压验证 Node.js,再替换正式目录;安装失败时保留已有运行目录,避免重装失败清空 `/opt/openclaw`。\n\n### 修复 Web 控制台连续对话\n\n- 启动 Gateway 前自动修补 OpenClaw 2026.6.11 的 WebChat 会话初始化冲突,避免 Web 控制台第一条能回答、第二条报 `reply session initialization conflicted`。\n- WebChat 会话不再因上一轮 assistant transcript 写入时间被误判为需要 rollover。\n- 当同一 sessionId 的 WebChat 写入在用户消息追加后发生 revision 变化时,自动合并当前 session entry 后继续提交。\n\n### 验证\n\n- 通过 shell / Node / Lua 语法检查。\n- 通过契约测试 `tests/test_openclaw_contracts.sh`。\n\n---\n\n## [2.0.9] - 2026-07-04\n\n### 修复微信 npm 插件注册\n\n- 微信插件安装和升级成功后,自动把 npm 安装目录注册到 OpenClaw 配置的 `plugins.installs.openclaw-weixin`。\n- 同步确保 `plugins.allow` 包含 `openclaw-weixin`,并启用 `channels.openclaw-weixin.enabled`,避免登录时再次提示安装插件。\n\n### 测试\n\n- 增加微信 npm 插件安装后配置注册的契约断言。\n\n---\n\n## [2.0.8] - 2026-07-04\n\n### 修复 ARM64 Node.js 下载资产不一致\n\n- 默认 Node.js 更新为 `22.23.0`,与 GitHub Release 和长期 `node-bins` 中的 ARM64 musl 资产保持一致。\n- 同步更新 `build-node-musl.yml` 手动构建参数,避免后续手动构建继续产出旧版本 Node.js。\n- `node-bins` release 已补齐 `node-v22.23.0-linux-arm64-musl.tar.xz`,用于 ARM64 OpenWrt/iStoreOS 在线安装。\n\n### 测试\n\n- 更新 Node.js 默认版本、Node 版本比较和 ARM64 musl 打包契约测试。\n\n---\n\n## [2.0.7] - 2026-07-04\n\n### 适配 OpenClaw v2026.6.11\n\n- `OC_TESTED_VERSION` 更新为 `2026.6.11`,继续默认安装 npm `latest` 稳定标签。\n- 保持默认 Node.js `22.22.2` 和最低要求 `22.19.0`,满足 OpenClaw `2026.6.11` 的 `engines.node`。\n- README 同步当前适配版本:OpenClaw `2026.6.11`、微信插件 `@tencent-weixin/openclaw-weixin@2.4.6`、微信 CLI `2.1.4`。\n\n### 修复微信插件安装、登录和卸载\n\n- 微信插件安装和升级时,如缺少 `python3`,后台自动尝试 `opkg update && opkg install python3-light`,失败后在安装日志给出手动命令。\n- 微信登录前只修复 npm cache、tmp、微信账号状态目录和配置文件权限,避免递归改写插件代码目录属主。\n- 微信登录增加 `openclaw` 用户写权限预检,目录不可写时直接在二维码日志返回明确错误。\n- 微信卸载会清理 legacy extensions、npm projects 中的微信插件包、微信账号状态目录,并用 Node.js 结构化删除 `plugins.allow`、`plugins.installs`、`plugins.entries`、`channels`、`channel` 中的 `openclaw-weixin/weixin` 配置。\n- 备份 API 改用统一安装路径 helper,兼容用户把 `install_path` 误填成 `/mnt/data/openclaw` 的场景。\n\n### 测试\n\n- 更新 OpenClaw 默认版本契约到 `2026.6.11`。\n- 增加微信插件自动安装 `python3-light`、登录权限预检和卸载清理 npm projects/config 的契约断言。\n\n### 致谢\n\n感谢以下用户通过 Issue 或 PR 提供问题反馈、复现信息和修复思路:\n\n- @hotwa (#85)\n- @djbadboyvip (#94)、@alan9771 (#93, #82)、@luckymai3688 (#92, #91)、@lucian521 (#90)\n- @svgr110 (#89)、@Cuscito (#88)、@hwliu11 (#84)、@sam528300-lab (#78)\n- @yiyibuguai (#75)、@xuguoliang189 (#64)、@ampcwin (#70)、@okareyouok (#60)\n\n---\n\n## [2.0.6] - 2026-06-08\n\n### 适配 OpenClaw v2026.6.1\n\n- `OC_TESTED_VERSION` 更新为 `2026.6.1`,默认稳定版跟随 npm `latest` 稳定标签。\n- 默认 Node.js 运行时更新为 Alpine musl LTS `22.22.2`,最低要求同步提升到 `22.19.0`,匹配 OpenClaw 2026.6.x 的 `engines.node`。\n- 安装脚本不再从当前 Node 版本静默回退到旧版 V1 tarball,避免下载失败后装入过低 Node.js 并在 OpenClaw 安装后失败。\n- ARM64 musl 预编译 Node.js 构建流程改用 Alpine 原生 LTS 包,避免 glibc 二进制转换为 musl 后运行失败,并校验默认路径和自定义安装路径均可运行。\n\n### 修复安装、控制台与状态体验\n\n- Node.js 下载失败时保留明确失败路径,避免 ARM64 musl 缺少当前版本资产时误判为可继续安装。\n- Web 控制台改为新窗口打开,继续强制使用 HTTP Gateway URL 和最新 token,规避 LuCI HTTPS、iframe 安全头和旧 token 混用问题。\n- 状态接口和页面继续区分“运行中 / 启动中 / 启动失败”,并保留 procd Gateway 退出码,便于定位升级后的启动失败。\n- 保留微信插件安装预检和权限修复链路,覆盖 `python3`、`openclaw` 用户、npm cache、tmp 和数据目录权限。\n\n### 测试\n\n- 更新 OpenClaw/Node.js 版本契约测试。\n- 新增控制台新窗口、状态失败展示、ARM64 musl Node 打包与路径兼容契约测试。\n\n### 致谢\n\n感谢以下用户通过 Issue 或 PR 提供问题反馈、复现信息和修复思路:\n\n- @djbadboyvip (#94)、@alan9771 (#93, #82)、@luckymai3688 (#92, #91)、@lucian521 (#90)\n- @svgr110 (#89)、@Cuscito (#88)、@ayysama (#87)、@jameslau-tech (#86)、@hwliu11 (#84)\n- @zhuxf8 (#81)、@yiyibuguai (#80)、@nbagui (#79)、@sam528300-lab (#78)、@hotwa (#85)\n\n---\n\n## [2.0.5] - 2026-05-15\n\n### 适配 OpenClaw v2026.5.12\n\n- `OC_TESTED_VERSION` 更新为 `2026.5.12`,默认稳定版固定到 npm `latest` 稳定标签。\n- 保持 Node.js 默认运行时 `22.16.0`,安装后按 OpenClaw `engines.node` 最低要求执行强校验;版本过低会直接失败并输出明确日志。\n- 文档补充 `2026.5.14-beta.2` 不作为默认稳定版的说明。\n\n### 修复权限与路径问题\n\n- 保留公开 UCI 字段 `openclaw.main.install_path`,新增统一路径规范化逻辑。\n- 兼容用户误填 `/mnt/data/openclaw`,避免实际路径变成 `/mnt/data/openclaw/openclaw`。\n- 拒绝相对路径、空白字符、shell 特殊字符和 `/tmp`、`/usr`、`/overlay` 等危险根目录。\n- `profile.d/openclaw.sh` 不再全局 `export HOME=\"$OC_DATA\"`,仅在 `openclaw` 命令包装器内注入 HOME,避免影响 SSH/zsh/oh-my-zsh。\n- 卸载逻辑移除 `chmod -R 777`,并将 `rm -rf` 限定到规范化后的 OpenClaw 运行根目录。\n- 安装前新增写入探针,提前识别 overlay 已满、只读挂载或目标挂载点不可写。\n\n### 修复微信插件链路\n\n- 微信插件安装和升级前检查 `python3`、`openclaw` 系统用户、npm cache/tmp/data 写权限。\n- 统一以 `openclaw` 用户运行微信插件 CLI,并显式传入 `OPENCLAW_CONFIG_PATH`、`NPM_CONFIG_CACHE`、`TMPDIR`。\n- 自动迁移旧渠道名 `weixin` 到 `openclaw-weixin`。\n- 保留 Web PTY `cmd=wechat` 初始化能力,微信向导入口不被破坏。\n\n### 修复 LuCI 与打包\n\n- 状态页区分“启动中”和“启动失败”,并显示 procd gateway 退出码。\n- Web 控制台每次加载前重新读取 token,强制使用正确的 HTTP Gateway URL,避免旧 token 或 HTTPS 混用。\n- `Makefile`、`.run`、`.ipk` 打包清单补齐 `wechat.htm`、rpcd ACL、共享 shell/Lua helper。\n- 主包不再直接生成 `openclaw.zh-cn.lmo`,避免与 `luci-i18n-openclaw-zh-cn` 文件冲突。\n\n### 测试\n\n- 新增路径规范化、卸载保护、profile HOME、Node 版本比较、打包清单、微信安装预检和控制台 token URL 的契约测试入口。\n\n### 致谢\n\n感谢以下用户通过 Issue 或 PR 提供问题反馈、复现信息和修复思路:\n\n- @svgr110 (#89)、@Cuscito (#88)、@ayysama (#87)、@Jameslau-tech (#86, #66)\n- @hotwa (#85, #83, #63)、@hwliu11 (#84)、@alan9771 (#82, #65)、@zhuxf8 (#81)\n- @yiyibuguai (#80, #75)、@nbagui (#79)、@sam528300-lab (#78)、@392377870 (#77)\n- @ouyangmland (#76, #68, #67)、@lucian521 (#74)、@jiangxiaoqiang-cloud (#73)、@LuckyMai (#72, #62)\n- @xuguoliang189 (#71, #64)、@ampcwin (#70)、@natserver (#69)、@jh620026 (#61)、@okareyouok (#60)\n\n---\n\n## [2.0.4] - 2026-04-10\n\n### 适配 OpenClaw v2026.4.9\n\n本次更新适配 OpenClaw 最新稳定版本 (2026.4.9),包含多项破坏性配置变更处理和安全策略适配。\n\n#### 版本变更\n- **OC_TESTED_VERSION**: 从 2026.3.28 更新到 2026.4.9 (跨 12 个版本)\n- **Node.js 版本**: 保持 22.16.0 (v2026.4.x 最低要求 >= 22.14.0)\n\n#### 更新日志 UI 深度优化\n\n参照 GitHub Releases 和 App Store 更新页面的设计规范,重构「检测升级」按钮触发的更新日志显示区域:\n\n- **Markdown 渲染引擎重构** (`ocMarkdownToHtml` 函数):\n - 中英文混排字体栈优化:`PingFang SC` / `Microsoft YaHei` / `Noto Sans SC` + 系统字体回退\n - 字体渲染增强:`text-rendering: optimizeLegibility` + 抗锯齿平滑\n - 标题层级比例重构:一级标题 (20px) → 二级 (17px) → 三级 (15px) → 四级 (14px)\n - 代码块样式:圆角边框 + 等宽字体栈 (`SF Mono`, Consolas, Menlo)\n - 链接悬停效果:底部边框渐显动画\n - 列表标记美化:统一的 `list-style-position: inside` + 行高优化 (1.75)\n\n- **版本状态徽章设计**:\n - 「有新版本」状态:渐变金色背景 + 橙色边框\n - 「已是最新」状态:渐变绿色背景 + 绿色边框\n - 「无法检查」状态:灰色背景 + 中性边框\n - 版本号使用等宽字体徽章样式,增强可读性\n\n- **更新日志容器设计**:\n - 卡片式布局:圆角 + 微阴影 + 白色背景\n - 版本标题栏:渐变背景 + 蓝色版本徽章 + GitHub 跳转链接\n - 内容区滚动条美化:`scrollbar-width: thin` + 自定义颜色\n - 操作按钮区:分组设计 + 阴影增强\n\n- **视觉细节优化**:\n - 段落间距:12px 统一间距\n - 行高优化:中文场景 1.75-1.8\n - 字间距微调:标题 `-0.02em`,正文 `0.01em`\n - 颜色层级:主标题 `#1f2328` → 正文 `#32383f` → 次要文本 `#656d76`\n\n#### 破坏性变更处理 (v2026.4.5)\n\nOpenClaw v2026.4.5 移除了多项旧版配置别名,本版本新增自动清理逻辑:\n\n- **已废弃配置清理**: `sync_uci_to_json()` 新增清理以下废弃字段\n - `talk.voiceId` / `talk.apiKey` — 语音功能配置迁移\n - `browser.ssrfPolicy.allowPrivateNetwork` — SSRF 策略重构\n - `hooks.internal.handlers` — 内部钩子迁移\n - `channel.*.allow` / `group.*.allow` / `room.*.allow` — 迁移到 `enabled` 字段\n - `agents.defaults.cliBackends` — CLI 后端配置废弃\n\n- **配置迁移工具集成**: 服务启动时自动调用 `openclaw doctor --fix` 迁移遗留配置\n\n#### 权限模型适配 (v2026.4.7/4.9)\n\nOpenClaw 新增了严格的环境变量安全检查,阻止危险环境变量覆盖:\n\n- **环境变量安全审计**: `start_service()` 注入环境变量前进行路径安全验证\n - 阻止包含特殊字符 (`$`, `` ` ``, `|`, `&`) 的路径\n - 阻止 `/proc/*`, `/sys/*`, `/dev/*` 等危险目录\n - 确保 `JAVA_HOME`, `RUST_*`, `GIT_*`, `KUBECONFIG`, `AWS_*` 等危险变量不被意外注入\n\n- **命令授权策略更新**: 适配 v2026.4.7 的权限收紧\n - `/allowlist add/remove` 现需要所有者授权 (LuCI 会话验证已满足)\n - `gateway config.apply` 阻止修改 exec 审批路径\n\n#### 安全更新\n\n- **依赖安全**: `basic-ftp` 强制升级到 5.2.1 (修复 CRLF 命令注入漏洞 CVE)\n- **浏览器 SSRF**: 新增安全策略配置选项 (默认不启用内网访问白名单)\n\n#### 改进\n\n- **配置验证增强**: 服务启动时先执行配置 schema 验证,失败时自动尝试修复\n- **iframe 安全头修补**: 适配 v2026.4.x 新的安全头位置\n - v2026.4.x 将 `X-Frame-Options` 和 `frame-ancestors` 设置从 `gateway-cli-*.js` 迁移到 `server.impl-*.js`\n - `patch_iframe_headers()` 函数现已支持两个位置的安全头修补\n - 解决 Web 控制台 iframe 嵌入被阻止的问题\n- **错误提示优化**: 配置迁移失败时输出详细的诊断信息\n- **移除 Gemini CLI 安装**: OpenClaw v2026.4.x 已废弃 `google-gemini-cli-auth` 插件\n - 节省约 155MB 磁盘空间\n - 减少安装时间约 30~60 秒\n - 用户配置 Google Gemini 请使用 API Key 方式 (推荐)\n- **修复微信插件加载失败**: 清理 jiti 缓存目录解决权限冲突\n - jiti 编译 TypeScript 时在 `/tmp/jiti` 创建缓存\n - 服务启动前自动清理,避免 openclaw 用户无法写入\n\n#### 修复 (2026-04-10)\n\n- **权限修复 EACCES 错误**: `doctor --fix` 以 root 运行后创建的文件导致 Gateway 无法写入\n - 根因: `doctor --fix` 在服务启动时以 root 身份执行,可能创建 root 所有的配置文件\n - 症状: `EACCES: permission denied, open '.../agents/main/agent/models.json.xxx.tmp'`\n - 修复: `init.d/openclaw` 在 `doctor --fix` 后自动修复非 extensions 目录权限\n - 同时修复 `openclaw-env factory-reset` 后的权限问题\n\n- **OpenWrt 兼容性**: 无 `ss` 命令时 Gateway 重启检测失败\n - 根因: `oc-config-interactive.js` 直接调用 `ss` 命令,部分精简固件未安装\n - 修复: 优先使用 `ss`,不存在时回退到 `netstat`\n\n- **新增交互式菜单引擎**: `oc-menu-engine.js`\n - 方向键导航、搜索过滤、粘贴支持 (Bracketed Paste Mode)\n - 纯 Node.js 实现,零外部依赖\n - 用于 SSH 命令行配置体验优化\n\n#### 技术细节\n\n**新增函数**:\n- `_validate_path()`: 环境变量路径安全验证\n- `_run_config_migration()`: 执行 OpenClaw 官方配置迁移工具\n\n**修改文件**:\n- `root/etc/init.d/openclaw`: 配置同步逻辑、环境变量注入、权限修复\n- `root/usr/bin/openclaw-env`: OC_TESTED_VERSION 版本号、factory-reset 权限修复\n- `root/usr/share/openclaw/oc-config-interactive.js`: ss/netstat 兼容性\n- `root/usr/share/openclaw/oc-menu-engine.js`: 新增交互式菜单引擎\n\n---\n\n## [2.0.3] - 2026-04-03\n\n### 修复\n\n- **微信插件安装失败 (exit: 127)**: 当 Gateway 未运行或路径检测失败时,`npx` 命令指向不存在的路径导致安装失败\n - 根因: `get_actual_install_path()` 依赖 Gateway 进程运行时返回正确路径,Gateway 未启动时返回默认值 `/opt/openclaw`,但实际安装路径可能是 `/mnt/data/openclaw` 等自定义路径\n - 修复: `action_wechat_install()` 和 `action_wechat_upgrade_plugin()` 新增多层路径校验逻辑:\n 1. 先检查默认检测到的 `npx` 是否存在\n 2. 不存在时回退到 UCI 配置的 `install_path`\n 3. 最后遍历常见备用路径 (`/mnt/data/openclaw`, `/opt/openclaw`, `/overlay/upper/opt/openclaw`)\n 4. 所有路径都无效时返回明确的错误提示,而非模糊的 exit 127\n\n---\n\n## [2.0.2] - 2026-03-31\n\n### 新增功能\n\n#### 适配微信通道插件 (openclaw-weixin)\n\n- **多账号操作面板**: 新增完善的多账号登录登出功能机制,重构 Lua API (`login_cmd`/`logout_cmd`/`install_cmd`/`upgrade_cmd`) 使其支持精细化的多实例运行调度。\n- **登录交互优化**: 解决由于未完成登录流程导致的环境锁定问题,后台新增自愈能力 (`pkill -f 'channels login.*openclaw-weixin'`)。\n- **安全沙箱无缝衔接**: 修复了 Web 触发命令时的 `blocked plugin candidate: suspicious ownership` 安全警告,完整平滑降权至 `openclaw` 用户组执行。\n\n**💡 微信通道使用细节提醒:**\n1. **多开隔离**:所有上号的微信机器人互不干扰,尽管在 Web 控制台大盘上聊天日志看起来像在一起,但由于后端存在独立的 `BotID / AccountID / Session` 多元结构体鉴权区分,他们在底层是**绝对隔离**的记忆!\n2. **退出登录**:遇到登录卡死或想强制下线,点击界面对应微信号的“退出登录”即可强制切除对应后端通道进程。\n\n#### 支持自定义安装路径\n\n- **功能描述**: 用户现在可以将 OpenClaw 运行环境安装到自定义路径(如第二块硬盘 `/mnt/data`)\n- **LuCI 界面**: 安装对话框增加自定义路径输入框,支持实时检测目标路径的可用空间\n- **系统检测**: 安装前系统配置检测会检测自定义路径的磁盘空间,而非默认的 `/opt`\n- **服务状态**: 状态 API 返回当前安装路径信息\n- **最小空间提示**: 安装界面提示最小需要 2GB 可用空间\n\n#### 技术实现\n\n- 新增 UCI 配置项 `openclaw.main.install_path`,默认值为 `/opt`\n- 程序会在用户指定路径下自动创建 `openclaw` 目录进行安装\n - 例如:用户输入 `/mnt/data`,实际安装路径为 `/mnt/data/openclaw`\n- 所有脚本和配置文件支持从 UCI 读取自定义路径:\n - `openclaw-env`: 通过 `OC_INSTALL_PATH` 环境变量或 UCI 配置\n - `init.d/openclaw`: 启动时从 UCI 读取路径\n - `oc-config.sh`: 支持自定义路径的环境变量\n - `profile.d/openclaw.sh`: SSH 环境变量支持\n - `uci-defaults/99-openclaw`: 首次安装初始化支持\n\n\n### 感谢\n\n感谢 @hotwa 提供的修改思路和建议。\n\n---\n\n## [2.0.1] - 2026-03-30\n\n### 适配 OpenClaw v2026.3.28\n\n#### 版本变更\n- **OC_TESTED_VERSION**: 从 2026.3.13 更新到 2026.3.28\n- **磁盘空间要求**: 从 1.5GB 提升到 2GB (OpenClaw v2026.3.28 包体积约 200MB)\n\n#### 兼容性分析\n- **Node.js 版本**: v2026.3.28 要求 >= 22.14.0 (降低了要求,从 v2026.3.13 的 >= 22.16.0)\n- **入口文件**: 无变化,仍为 `openclaw.mjs`\n- **配置 Schema**: 向后兼容,无需迁移\n- **API**: 向后兼容\n\n#### 包体积变化\n- v2026.3.13: ~94MB (4,730 文件)\n- v2026.3.28: ~200MB (19,887 文件)\n- 文件数量增加 4x+,包体积增加 2x+\n\n#### 新增 Plugin SDK 导出 (20+)\n\n新增 AI 提供商原生支持 SDK:\n- `plugin-sdk/xai` — xAI (Grok) API 支持\n- `plugin-sdk/vllm` — vLLM 高性能推理引擎支持\n- `plugin-sdk/ollama` — Ollama 本地模型原生 SDK\n- `plugin-sdk/openai` — OpenAI 原生 SDK\n- `plugin-sdk/sglang` — SGLang 推理引擎支持\n- `plugin-sdk/chutes` — Chutes AI 平台支持\n- `plugin-sdk/google` — Google AI SDK\n- `plugin-sdk/nvidia` — NVIDIA NIM API 支持\n- `plugin-sdk/venice` — Venice AI 支持\n- `plugin-sdk/minimax` — MiniMax API 原生 SDK\n- `plugin-sdk/mistral` — Mistral AI 原生 SDK\n- `plugin-sdk/qianfan` — 百度千帆大模型 SDK\n\n新增功能模块 SDK:\n- `plugin-sdk/zod` — Zod schema 验证支持\n- `plugin-sdk/setup` — 安装配置向导 SDK\n- `plugin-sdk/routing` — 模型路由配置 SDK\n- `plugin-sdk/speech` — 语音处理 SDK\n- `plugin-sdk/browser` — 浏览器自动化 SDK\n\n新增顶级导出:\n- `extension-api` — 扩展 API 入口 (用于插件开发)\n\n#### 移除的依赖\n\n以下渠道依赖被移除 (功能整合到核心或不再维护):\n- `grammy` — Telegram Bot 框架 (改用内置实现)\n- `@grammyjs/runner` — Telegram 运行器\n- `@grammyjs/transformer-throttler` — Telegram 限流器\n- `@whiskeysockets/baileys` — WhatsApp Web API (改用 matrix-js-sdk)\n\n#### 新增依赖\n\n核心依赖:\n- `uuid@^13.0.0` — UUID 生成\n- `gaxios@7.1.4` — Google API HTTP 客户端\n- `matrix-js-sdk@41.2.0` — Matrix 协议支持 (替代 WhatsApp)\n- `@anthropic-ai/vertex-sdk@^0.14.4` — Anthropic Vertex AI 支持\n\n#### 依赖版本升级\n\n核心依赖:\n- `ws`: 8.19.0 → 8.20.0\n- `hono`: 4.12.7 → 4.12.9\n- `file-type`: 21.3.2 → 22.0.0\n- `undici`: 7.24.1 → 7.24.6\n- `sqlite-vec`: 0.1.7-alpha.2 → 0.1.7\n\nAI/ML 依赖:\n- `@mariozechner/pi-ai`: 0.58.0 → 0.63.1\n- `@mariozechner/pi-tui`: 0.58.0 → 0.63.1\n- `@mariozechner/pi-agent-core`: 0.58.0 → 0.63.1\n- `@mariozechner/pi-coding-agent`: 0.58.0 → 0.63.1\n- `@modelcontextprotocol/sdk`: 1.27.1 → 1.28.0\n- `@agentclientprotocol/sdk`: 0.16.1 → 0.17.0\n- `@aws-sdk/client-bedrock`: 3.1009.0 → 3.1019.0\n\n#### pnpm 配置变更\n\n新增 `ignoredBuiltDependencies`:\n- `@discordjs/opus` — 跳过构建\n- `koffi` — 跳过构建\n\n新增 `onlyBuiltDependencies`:\n- `@tloncorp/tlon-skill` — 需要构建\n\n#### 中间版本变更 (v2026.3.22 ~ v2026.3.24)\n\nv2026.3.22:\n- Node.js 最低版本从 22.16.0 降低到 22.14.0\n- 大量 plugin-sdk 模块重构\n\nv2026.3.23:\n- 修复版本发布问题\n- 稳定性改进\n\nv2026.3.24:\n- 依赖安全更新\n- 性能优化\n\n#### 升级建议\n\n1. **磁盘空间**: 确保至少 2GB 可用空间\n2. **Node.js**: v22.16.0 完全兼容,无需降级\n3. **配置迁移**: 现有配置向后兼容,无需手动干预\n4. **备份**: 升级前建议执行 `openclaw backup create --only-config`\n\n## [2.0.0] - 2026-03-16\n\n### 重大变更\n- **配置管理菜单重构**: 主菜单采用分组样式,更清晰的导航结构\n - AI 模型配置:配置 AI 模型和提供商、设置活动模型\n - 消息渠道:配置消息渠道 (电报/QQ/飞书)\n - 系统管理:健康检查与状态、查看日志、重启 Gateway\n - 高级选项:高级配置、重置配置、显示当前配置概览\n- **新增高级配置菜单**: 独立的高级配置入口,包含:\n - Gateway 端口/绑定地址/运行模式配置\n - 日志级别设置\n - ACP Dispatch 开关\n - 官方完整配置向导入口\n - 原始 JSON 查看/编辑\n - 配置备份导出/导入\n\n### 修复\n- **QQ 机器人插件配置名称不匹配** (#XX): OpenClaw v2026.3.13 加强了配置验证,`plugins.allow` 中的插件名称必须与实际安装的插件名完全匹配\n - 问题:旧版本写入的是 `openclaw-qqbot`,但实际插件名是 `@tencent-connect/openclaw-qqbot`\n - 影响:配置验证失败导致 Gateway 启动后立即退出,procd 进入 crash loop 保护\n - 修复:新增 `fix_plugin_config` 函数自动检测并修正不匹配的插件名称\n - 修复:`configure_qq` 安装插件后调用 `ensure_qqbot_plugin_allowed` 确保正确的插件名写入配置\n - 修复:`init.d` 服务启动前自动修复配置中的插件名称\n- **安装运行环境报错** (#28): 部分系统缺少 `libstdcpp6` 导致 Node.js 无法运行,安装 pnpm 时卡住\n - 依赖声明新增 `libstdcpp6`,安装时自动拉取 C++ 标准库\n - Node.js 验证逻辑改进:检测到运行失败时提示缺失的库并给出修复命令\n- **环境变量路径混乱** (#42): 用户通过 SSH 直接运行 `openclaw` 命令时,CLI 使用默认 `HOME=/root` 导致配置文件和 skills 散落在 `/root/.openclaw/` 而非正确的 `/opt/openclaw/data/.openclaw/`\n - 新增 `/etc/profile.d/openclaw.sh` 全局环境变量脚本,SSH 登录后自动设置正确的 PATH、OPENCLAW_HOME 等变量\n - 升级时自动迁移 `/root/.openclaw/` 下的 skills、sessions、openclaw.json 到正确路径\n - 用户现在可以直接运行 `npm`、`npx`、`openclaw config` 等命令\n\n### 新增\n- **全局环境变量**: `/etc/profile.d/openclaw.sh` 为 SSH 用户提供:\n - PATH 包含 Node.js 和 OpenClaw bin 目录\n - OPENCLAW_HOME、OPENCLAW_STATE_DIR、OPENCLAW_CONFIG_PATH 正确指向安装路径\n - `openclaw` 命令别名(当全局安装时)\n- **查看日志功能**: 主菜单新增「查看日志」选项,显示最近 100 条 OpenClaw 日志\n\n### 变更\n- Makefile 新增 `/etc/profile.d/openclaw.sh` 安装步骤\n- 依赖声明新增 `libstdcpp6`\n- **飞书 Bot 配置流程优化**: 参考 QQ Bot 实现,大幅简化配置步骤\n - 新增 App ID 格式验证(`cli_xxx` 格式)\n - 新增 App Secret 长度检查\n - 使用 OpenClaw CLI 一键配置(`oc_cmd channels add --channel feishu`)\n - 配置保存后自动验证\n - 新增详细的事件订阅、权限配置、插件安装指引\n\n### 适配 OpenClaw v2026.3.13\n\n#### 升级说明\n- **Node.js 版本升级**: 从 22.15.1 升级到 22.16.0 (OpenClaw v2026.3.11+ 最低要求)\n- **OpenClaw 版本升级**: 从 v2026.3.8 升级到 v2026.3.13\n\n#### 重要安全修复\n- WebSocket 跨站劫持漏洞修复 (GHSA-5wcw-8jjv-m286)\n- 设备配对安全增强:切换到短期引导令牌 (GHSA-99qw-6mr3-36qr)\n- 命令审批安全加固:Unicode 不可见字符转义、执行检测规范化\n- 多渠道 Webhook 安全增强:飞书/LINE/Zalo 签名验证强化\n\n#### 新功能支持\n- **Fast Mode**: OpenAI/Anthropic 快速响应模式支持\n- **Control UI 重构**: 新版 Dashboard-v2 模块化界面\n- **Ollama 本地向导**: 支持本地或云端+本地混合模式\n- **Kubernetes 部署**: 新增 K8s 部署清单和文档\n- **Docker 时区**: 新增 `OPENCLAW_TZ` 环境变量支持\n\n#### 破坏性变更处理\n- **Cron 主动投递收紧**: 升级后建议运行 `openclaw doctor --fix` 迁移旧版 cron 存储\n- **插件安全策略**: 禁用隐式工作区插件自动加载,需显式信任决策\n- **Node.js 最低版本**: 要求 >= 22.16.0 (已在 openclaw-env 中更新)\n\n#### 配置兼容性\n- 现有配置文件完全兼容,无需手动迁移\n- 已预设 `gateway.controlUi.dangerouslyDisableDeviceAuth=true` 禁用设备认证\n- 已预设 `gateway.controlUi.allowInsecureAuth=true` 允许不安全认证\n\n## [1.0.15] - 2026-03-13\n\n### 修复\n- **QQBot 插件 3 层死锁修复**: 解决插件安装后因 uid 权限→安全策略阻止→配置校验失败的连锁问题\n - 自动检测插件 blocked/loaded/目录存在 3 种状态\n - 插件安装后自动 `chown root:root` 修复权限\n - 安装失败但目录存在时不再阻断配置流程\n\n### 新增\n- **覆盖安装防护**: 离线安装器在覆盖安装前先停止已有服务,避免文件被占用\n- **离线 .run 安装包**: 构建包含 Node.js + OpenClaw + LuCI 插件的全合一自解压包,用户**无需联网**即可完成安装\n- **musl 架构支持**: 离线包支持 x86_64-musl、aarch64-musl 两种架构 (OpenWrt/iStoreOS 均使用 musl)\n- **依赖预下载脚本** (`scripts/download_deps.sh`): 在构建机上预下载所有离线依赖\n- **node_modules 精简**: 自动删除文档、测试、TypeScript 源码等非必要文件,减小 30%+ 体积\n- **磁盘空间预检查**: 安装前检测可用空间是否满足 500MB 最低要求\n- **架构/libc 自动检测**: 安装时自动校验当前设备是否匹配安装包架构\n\n### 文档\n- **README**: 添加离线安装方式(无需联网),更新目录结构\n\n### 变更\n- **离线包不依赖 curl/openssl/git**: 离线安装模式下 opkg 注册的依赖简化为 luci-compat + luci-base\n\n## [1.0.14] - 2026-03-12\n\n### 备份管理增强 & QQ 机器人支持\n\n#### 新增\n- **备份列表可视化**: LuCI「💾 备份/恢复」对话框现在展示所有备份的结构化列表:\n - 📄 仅配置 / 📦 完整备份 类型标签(从 manifest.json 读取 `onlyConfig` 字段精确判断)\n - 备份时间、文件大小\n - 每个备份支持**单独恢复**和**删除**操作\n - 创建/删除备份后列表自动刷新\n- **备份删除 API**: Controller 新增 `action=delete` 操作(含路径穿越安全校验)\n- **QQ 机器人配置**: `oc-config.sh` 渠道菜单新增「QQ 机器人」选项(选项 1,推荐国内用户),支持:\n - 自动安装 `@tencent-connect/openclaw-qqbot` 插件\n - App ID / App Secret 输入校验\n - 通过 `openclaw channels add` CLI 一键配置\n- **消息渠道状态显示**: 状态面板新增「消息渠道」行,自动检测已配置的渠道(QQ、Telegram、Discord、飞书、Slack)\n\n#### 变更\n- **备份恢复**: 从\"从最新备份恢复\"改为在列表中选择任意备份进行恢复\n- **描述文本**: 各页面描述新增\"QQ\"渠道说明\n\n#### 修复\n- **JS 语法错误导致所有按钮失效**: 备份对话框 HTML 被错误地插入 `<script>` 标签内部,导致 JavaScript 语法错误。修复: 在对话框 HTML 前后正确分割 `<script>` 标签\n\n## [1.0.13] - 2026-03-12\n\n### 适配 OpenClaw v2026.3.8 & 新增备份/恢复功能\n\n#### 新增\n- **配置备份/恢复**: LuCI 基本设置页「💾 备份/恢复」按钮,弹出对话框支持:\n - 📄 仅配置文件备份(~2KB,包含模型、渠道、插件设置)\n - 📦 完整备份(配置 + 状态数据)\n - 🔄 从最新备份恢复配置(自动重启服务)\n- **Shell 备份菜单**: `oc-config.sh` 主菜单新增「8) 💾 备份/还原配置」,支持:\n - 创建仅配置 / 完整备份\n - 验证备份完整性 (`openclaw backup verify`)\n - 列出已有备份文件\n - 从最新备份恢复配置(交互确认 + 自动重启)\n- **命令行备份**: `oc-config.sh --backup` 快捷参数,适合 cron 定时任务\n- **OpenClaw 版本显示**: 状态面板新增 OpenClaw 版本行(从 package.json 读取)\n\n#### 修复\n- **备份文件路径**: 备份文件统一保存到 `~/.openclaw/backups/` 目录(OpenClaw CLI 默认输出到 CWD)\n- **完整备份失败**: 含未注册插件的 channel ID 时,`backup create` 因 config invalid 失败;改用 `--no-include-workspace` 跳过工作区发现\n\n#### 变更\n- **OC_TESTED_VERSION**: 2026.3.2 → 2026.3.8\n- **Control UI iframe 资源查找**: `patch_iframe_headers()` 扩展搜索路径,覆盖 `$NODE_BASE/lib/node_modules`、pnpm 全局存储,并使用 `readlink -f` 解析 v2026.3.8 新增的符号链接资源路径\n- **Gateway 入口查找**: `get_oc_entry()` 新增 `readlink -f` 符号链接解析,兼容 v2026.3.8 的 bundled 插件优先级调整\n- **配置同步清理**: `sync_uci_to_json()` 自动删除 v2026.3.7/3.8 已废弃的配置字段:\n - `gateway.controlUi.dangerouslyAllowCors`\n - `gateway.controlUi.dangerouslyAllowRemoteConnections`\n - `commands.ownerDisplay`\n\n#### 兼容性说明\n- **v2026.3.7 BREAKING CHANGE**: `gateway.auth.mode` 必须显式指定(不再有默认值)。本插件已在 v1.0.3 起始终写入 `\"mode\": \"token\"`,无需用户操作\n- **v2026.3.8**: Control UI 资源分发改为符号链接方式,本版本已完整适配\n\n## [1.0.12] - 2026-03-11\n\n### 移除 OpenClaw 版本检测 & 修复 BusyBox tar 兼容性\n\n#### 变更\n- **「检测升级」按钮**: 不再检查 OpenClaw (npm) 版本,仅检查插件 (luci-app-openclaw) 是否有新版本\n- **「检测升级」显示更新内容**: 检测到新插件版本时,直接展示该版本的 Release Notes,告知用户升级了什么\n- **状态面板**: 移除「OpenClaw」版本显示行,保留 Node.js 和插件版本\n- **内部清理**: 移除 `get_openclaw_version()` 函数、`action_do_update`、`action_upgrade_log` 等已废弃后端 API\n\n#### 修复\n- **BusyBox tar 兼容性** (#18, #30): `openclaw-env` 安装 Node.js 时的解压命令优先使用 GNU tar `--strip-components=1`;若不支持则自动回退到 BusyBox tar 兼容方式(解压到临时目录后移动),无需用户手动安装 `tar`\n- **插件升级网络错误提示**: 下载后检测文件内容,若 GitHub 返回 `Not Found`(GFW 拦截等情况)则显示明确提示,并附手动下载链接\n\n## [1.0.11] - 2026-03-09\n\n### 修复 Telegram 配对后无法使用的严重 Bug\n\n#### 修复\n- **Telegram 配置流程**: Token 保存后强制重启网关(不再可选),确保 Bot Token 立即生效后再进入配对流程\n- **Telegram 配对流程**: 配对成功后自动重启网关,确保配对关系立即生效,用户可以直接开始对话\n\n## [1.0.10] - 2026-03-08\n\n### 新增腾讯云大模型 Coding Plan 套餐支持\n\n#### 新增\n- **腾讯云 Coding Plan 套餐**: 新增菜单选项 13,支持一键配置腾讯云大模型 Coding Plan 套餐\n - Base URL: `https://api.lkeap.cloud.tencent.com/coding/v3`,Provider: `lkeap`\n - 支持全部 8 个套餐模型: tc-code-latest (智能路由)、hunyuan-t1、hunyuan-turbos、hunyuan-2.0-thinking、hunyuan-2.0-instruct、glm-5、kimi-k2.5、minimax-m2.5\n - 按类别分组展示: 智能推荐 / 推理模型 / 旗舰模型 / 第三方模型\n\n#### 修复\n- **Coding Plan 配置信息修正**: 订阅地址更正为官方页面,移除不必要的 Base URL 显示\n\n## [1.0.9] - 2026-03-08\n\n### 插件一键升级 & 百炼模型列表扩充\n\n#### 新增\n- **插件一键升级**: LuCI 界面\"检测升级\"发现新版后,可直接点击\"⬆️ 升级插件\"按钮完成在线升级\n - 后台自动从 GitHub Releases 下载 `.run` 安装包并执行\n - 实时升级日志显示,带容错处理 (安装过程替换 LuCI 文件导致 API 暂时不可用时自动判定成功)\n - 同时保留\"📥 手动下载\"链接作为备选\n- **百炼按量付费模型列表扩充**: 从 4 个模型扩充至 16 个,按类别分组显示\n - 千问商业版: qwen-max、qwen-plus (Qwen3.5)、qwen-flash (Qwen3.5)、qwen-turbo、qwen-long (1000万Token上下文)\n - 千问Coder: qwen3-coder-plus (100万上下文)、qwen3-coder-flash\n - 推理模型: qwq-plus\n - 千问开源版: qwen3-235b-a22b、qwen3-32b、qwen3-30b-a3b\n - 第三方模型: deepseek-r1、deepseek-v3、kimi-k2.5、glm-5、MiniMax-M2.5\n\n#### 修复\n- **CBI 底部按钮未隐藏**: \"保存并应用/保存/复位\"按钮在基本设置页仍然显示\n - 根因: `m.submit = false` 和 `m.reset = false` 不被 CBI 框架识别\n - 修复: 改为 `m.pageaction = false` (dispatcher.lua 第 294 行检查的正确属性)\n- **插件升级后配置管理无法连接**: 升级后 PTY WebSocket 一直转圈 \"等待服务就绪\"\n - 根因: `.run` 安装器覆盖 `/etc/config/openclaw` 导致 `pty_token` 丢失,PTY 认证失败\n - 修复: 升级时保留用户 UCI 配置 (仅首次安装部署默认配置);安装后自动重启 PTY 服务\n\n## [1.0.8] - 2026-03-07\n\n### 修复第三方模型配置导致 Gateway 崩溃 & 新增 Coding Plan 套餐支持\n\n#### 修复\n- **EACCES 权限错误** (#8): Web PTY 以 root 运行,创建的目录 (`sessions/`, `auth-profiles.json` 等) 归 root 所有,Gateway 以 `openclaw` 用户运行时无法写入,报 `EACCES: permission denied, mkdir`\n - `oc-config.sh`: `auth_set_apikey`、`json_set`、备份目录创建后均执行 `chown openclaw:openclaw`\n - `web-pty.js`: 子进程退出时自动 `chown -R openclaw:openclaw` 整个数据目录\n- **第三方模型 404/405 错误** (#11, #13, #14, #15): DeepSeek、xAI Grok、Groq 等 OpenAI 兼容提供商配置后返回 404/405\n - 根因: 这些提供商缺少 `register_custom_provider` 调用,未写入 `baseUrl` 导致 Gateway 请求发到错误地址\n - 修复: 为 DeepSeek、xAI、Groq 快速配置补充 `register_custom_provider` 调用\n- **API 类型错误导致 Gateway 崩溃**: `register_custom_provider` 中 `api` 值设为 `openai-chat-completions`,但该值在 OpenClaw v2026.3.2 中不存在\n - 正确值为 `openai-completions`,错误值会导致 Gateway 启动时 JSON schema 校验失败,进入 crash loop\n - 有效 api 类型: `openai-completions` | `openai-responses` | `openai-codex-responses` | `anthropic-messages` | `google-generative-ai` | `github-copilot` | `bedrock-converse-stream` | `ollama`\n\n#### 新增\n- **阿里云 Coding Plan 套餐快速配置**: 千问配置菜单新增 Coding Plan 选项 (选项 c,默认推荐)\n - Provider: `bailian`,Base URL: `https://coding.dashscope.aliyuncs.com/v1`\n - 一键注册套餐内全部模型: qwen3.5-plus、qwen3-coder-plus、qwen3-coder-next、qwen3-max、MiniMax-M2.5、glm-5、glm-4.7、kimi-k2.5\n - contextWindow / maxTokens 按阿里云官方文档设定 (最大 100万上下文)\n - 参考: [阿里云 Coding Plan 文档](https://help.aliyun.com/zh/model-studio/openclaw-coding-plan)\n\n#### 变更\n- **千问配置菜单重构**: 从 2 种模式扩展为 3 种\n - `a)` Qwen Portal OAuth (官方向导)\n - `b)` 百炼按量付费 API Key (`sk-xxx` + `dashscope.aliyuncs.com`)\n - `c)` Coding Plan 套餐 (`sk-sp-xxx` + `coding.dashscope.aliyuncs.com`) ★ 默认推荐\n - 明确提示两套 API Key / Base URL 不互通\n- **`register_custom_provider` 增强**: 新增可选参数 `context_window` (默认 128000) 和 `max_tokens` (默认 32000)\n\n## [1.0.7] - 2026-03-06\n\n### 修复依赖包名错误 & 补充 GNU tar 依赖 (感谢 [@esir](https://github.com/esirplayground) 建议)\n\n#### 修复\n- **依赖包名修正**: `util-linux-script` 在 OpenWrt/iStoreOS 软件源中不存在,正确的包名是 `script-utils` (提供 `/usr/bin/script` 命令)。此错误会导致通过 iStore/opkg 安装插件时依赖解析失败\n- **补充 GNU tar 依赖**: `openclaw-env` 安装脚本使用 `tar --strip-components=1` 解压 Node.js,但 busybox 内置的 tar 不支持该参数。新增 `tar` (GNU tar) 为必需依赖,确保解压操作正常\n\n#### 变更\n- `Makefile`: `LUCI_DEPENDS` 中 `+util-linux-script` → `+script-utils +tar`\n- `scripts/build_ipk.sh`: 同步更新 Depends 字段\n- `scripts/build_run.sh`: 同步更新 Depends 字段\n\n### 修复重启服务时 Gateway crash loop 端口冲突\n\n#### 修复\n- **端口冲突 crash loop**: OpenClaw gateway 的架构是主进程 (`openclaw`) fork 出子进程 (`openclaw-gateway`) 监听端口,restart 时 procd 只杀主进程,子进程退出慢导致新实例端口冲突反复崩溃\n - `stop_service()`: 从空函数改为主动清理 `openclaw-gateway` 子进程 + 等待端口释放 (最长 8 秒)\n - `start_service()`: 启动前预检查端口,清理残留进程后再注册 procd 实例\n - `reload_service()`: stop 和 start 之间增加等待确保内核回收端口\n - LuCI controller: restart 改为先同步 stop 等端口释放,再后台 start\n - procd respawn 间隔从 5s → 10s,降低连续端口冲突概率\n\n## [1.0.6] - 2026-03-06\n\n### 修复 Docker 环境下安装失败 \"mkdir: can't create directory: Directory not empty\"\n\n#### 修复\n- **OverlayFS 兼容性**: iStoreOS/OpenWrt 安装 Docker 后,Docker 的 bind mount (`/overlay/upper/opt/docker`) 导致 OverlayFS 合并视图中 `/opt` 目录完全不可写,所有 `mkdir`/`touch`/`ln` 操作均报 \"Directory not empty\"\n - 新增 `_oc_fix_opt()` 检测函数,自动检测 `/opt` 是否可写\n - 不可写时自动执行 `mount --bind /overlay/upper/opt /opt` 绕过 OverlayFS 冲突\n - 三重保障: `uci-defaults` (首次安装)、`init.d` (每次开机)、`openclaw-env` (手动操作) 均包含修复逻辑\n - 正常系统 (无 Docker) 不受影响,检测到可写后直接跳过\n- **openclaw-env**: 新增 `ensure_mkdir()` 安全目录创建函数,替代所有裸 `mkdir -p` 调用\n\n## [1.0.5] - 2026-03-05\n\n### 修复配置管理页面 \"spawn script ENOENT\" 启动失败 (#3, #4)\n\n#### 修复\n- **Web PTY 启动失败**: `web-pty.js` 硬编码依赖 `script` 命令 (来自 `util-linux-script`),但部分 OpenWrt 固件默认不包含该命令,导致 `spawn script ENOENT` 错误并无限循环重启\n - 新增 `script` 命令自动检测,不存在时回退到 `sh` 直接执行 `oc-config.sh`\n - 新增连续失败计数器 (最多 5 次),防止启动失败时的无限重试循环\n - 失败时向用户终端显示明确的错误提示和修复命令\n- **Makefile 依赖补全**: `LUCI_DEPENDS` 新增 `+util-linux-script`,确保新安装自动拉取 `script` 命令\n\n## [1.0.4] - 2026-03-05\n\n### 适配 OpenClaw 2026.3.2\n\n#### 破坏性变更修复\n- **tools.profile 默认值变更**: 2026.3.2 将 `tools.profile` 默认从 `coding` 改为 `messaging`\n - `sync_uci_to_json()` 每次启动强制写入 `tools.profile=coding`\n - `openclaw-env init_openclaw()` onboard 命令添加 `--tools-profile coding`\n - `openclaw-env do_factory_reset()` onboard 命令添加 `--tools-profile coding`\n - `oc-config.sh` 工厂重置 onboard 命令添加 `--tools-profile coding`\n - `oc-config.sh` 工厂重置配置写入新增 `tools.profile=coding`\n- **ACP dispatch 默认启用**: 2026.3.2 默认开启 ACP dispatch,路由器内存有限可能导致 OOM\n - `sync_uci_to_json()` 每次启动强制写入 `acp.dispatch.enabled=false`\n - `openclaw-env do_factory_reset()` 配置写入新增 `acp.dispatch.enabled=false`\n - `oc-config.sh` 工厂重置配置写入新增 `acp.dispatch.enabled=false`\n\n#### 新增\n- 健康检查集成 `openclaw config validate --json` 官方配置验证命令\n- 健康检查新增 `gateway health --json` CLI 深度检查 (v2026.3.2 HTTP `/health` 已被 SPA 接管)\n\n#### 修复\n- **Ollama 配置适配**: `api` 从废弃的 `openai-chat-completions` 改为原生 `ollama` API 类型\n- **Ollama baseUrl 格式**: 去掉 `/v1` 后缀,使用官方原生地址格式 (`http://host:11434`)\n- **Ollama apiKey 对齐**: 从 `ollama` 改为官方默认值 `ollama-local`\n- **启动自动迁移**: `sync_uci_to_json` 自动将旧版 Ollama 配置迁移到 v2026.3.2 格式\n\n#### 改进\n- 配置管理页面移除「菜单功能说明」信息框,减少视觉干扰\n- `OC_TESTED_VERSION` 更新至 `2026.3.2`\n\n## [1.0.3] - 2026-03-05\n\n### 修复\n- **P0** 配置管理写入错误的 JSON 路径导致 Gateway 崩溃且无法恢复 (#1)\n - `json_set models.openai.apiKey` 在 `openclaw.json` 创建了非法的顶层 `models` 键\n - OpenClaw 2026.3.1 严格校验配置 schema,拒绝启动并报 `Unknown config keys: models.openai`\n - 修复: API Key 改写入 `auth-profiles.json`,模型注册到 `agents.defaults.models`\n - 影响: 所有 11 个供应商的快速配置 (OpenAI/Anthropic/Gemini/OpenRouter/DeepSeek/GitHub Copilot/Qwen/xAI/Groq/SiliconFlow/自定义)\n- **P0** 恢复默认配置 → \"清除模型配置\" 未清理 `auth-profiles.json` 认证信息\n- **P1** 健康检查新增自动修复: 检测并移除旧版错误写入的顶层 `models` 无效键\n- **P1** `set_active_model` 手动切换模型时未注册到 `agents.defaults.models`\n\n### 新增\n- **Ollama 本地模型支持**: 快速配置菜单新增 Ollama 选项 (12),支持 localhost/局域网连接、自动检测连通性、自动列出已安装模型、兼容 OpenAI chat completions 格式\n- `openclaw-env factory-reset` 非交互式恢复出厂设置命令\n- `auth_set_apikey` 函数: 正确写入 API Key 到 `auth-profiles.json`\n- `register_and_set_model` 函数: 注册模型到 `agents.defaults.models` 并设为默认\n- `register_custom_provider` 函数: 为需要 `baseUrl` 的 OpenAI 兼容供应商注册 `models.providers`\n- 「检测升级」同时检查 OpenClaw 和**插件版本** (通过 GitHub API 获取最新 release)\n- 页面加载时自动静默检查更新,有新版本时「检测升级」按钮显示橙色小红点提醒\n- 状态面板显示当前安装的插件版本号\n- 构建/安装流程部署 `VERSION` 文件到 `/usr/share/openclaw/VERSION`\n- `openclaw-env setup` 安装环境时自动安装 Gemini CLI (Google OAuth 依赖)\n\n### 改进\n- 使用指南顺序调整: ② 配置管理 → ③ Web 控制台 (首次使用更合理的引导顺序)\n- Gemini CLI 安装从配置向导选项 1 移至环境安装阶段,避免进入向导时临时等待\n\n## [1.0.2] - 2026-03-02\n\n### 修复\n- **P0** ARM64 musl: Gateway 崩溃循环 — `process.execPath` 返回 musl 链接器路径导致 `child_process.fork()` 失败\n - 使用 `patchelf` 直接修改 node ELF 二进制的 interpreter 和 rpath,替代 ld-musl wrapper 方案\n - 子进程通过 `process.execPath` fork 时可正确找到 node 二进制\n- **P0** ARM64 musl: Unicode property escapes 正则失败 (`\\p{Emoji_Presentation}`) — 缺少 `NODE_ICU_DATA` 环境变量\n - init.d、openclaw-env、oc-config.sh 所有入口均添加 `NODE_ICU_DATA` 环境变量\n\n### 改进\n- `build-node-musl.sh` 构建验证阶段新增 `process.execPath` 输出检查\n\n## [1.0.1] - 2026-03-02\n\n### 修复\n- **P0** web-pty.js `loadAuthToken` 读取错误的 UCI key `luci_token` → `pty_token`\n- **P0** init.d `get_oc_entry()` 管道子 shell 导致返回值丢失,改用临时文件重定向\n- **P1** Gateway procd respawn 无限重试 (`3600 5 0`) → 限制最多 5 次 (`3600 5 5`)\n- **P1** Telegram 配对流程管道子 shell 变量丢失,改用临时文件避免子 shell\n- **P1** `openclaw.lua` PID 提取 `sed` 正则不可靠,改用 `awk` + `split`\n- **P2** init.d 和 uci-defaults 弱 token fallback (`echo \"auto_$(date +%s)\"`) → `dd if=/dev/urandom`\n- **P2** `oc-config.sh` 恢复出厂 `timeout` 命令可能不存在,添加 `command -v` 检查和降级方案\n- **P2** web-pty.js SIGTERM 不清理 HTTPS server,统一 `shutdown()` 函数\n\n### 新增\n- GitHub Copilot 配置新增 OAuth 授权登录方式 (通过 `copilot-proxy` 插件)\n- `uci-defaults` 首次安装时自动生成 `pty_token`\n- Web 控制台和状态面板显示当前活跃模型名称\n\n### 改进\n- Qwen 使用 `models.dashscope` 键名、SiliconFlow 使用 `models.siliconflow`,避免 `models.custom` 键冲突\n- `get_openclaw_version()` 从 `package.json` 读取版本号,不再每次启动 Node.js 进程\n- PTY 终端 WebSocket 重连策略改为无限重连 (`MAX_RETRY=Infinity`)\n- Makefile `PKG_VERSION` 从 `VERSION` 文件动态读取\n\n## [1.0.0] - 2026-03-02\n\n### 新增\n- LuCI 管理界面:基本设置、配置管理(Web 终端)、Web 控制台\n- 一键安装 Node.js + OpenClaw 运行环境\n- 支持 x86_64 和 aarch64 架构,glibc / musl 自动检测\n- 支持 12+ AI 模型提供商配置向导\n- 支持 Telegram / Discord / 飞书 / Slack 消息渠道\n- `.run` 自解压包和 `.ipk` 安装包两种分发方式\n- OpenWrt SDK feeds 集成支持\n- GitHub Actions 自动构建与发布\n\n### 安全\n- WebSocket PTY 服务添加 token 认证\n- WebSocket 最大并发会话限制(默认 5)\n- PTY 服务默认绑定 127.0.0.1,不对外暴露\n- Token 不再嵌入 HTML 源码,改为 AJAX 动态获取\n- sync_uci_to_json 通过环境变量传递 token,避免 ps 泄露\n- 所有渠道 Token 输入统一 sanitize_input 清洗\n\n### 修复\n- Telegram Bot Token 粘贴时被 bracketed paste 转义序列污染\n- Web PTY 终端粘贴包含 ANSI 转义序列问题\n- 恢复出厂配置流程异常退出\n- Gemini CLI OAuth 登录在 OpenWrt 上失败\n- init.d status_service() 在无 netstat 的系统上报错\n- Makefile 损坏导致 OpenWrt SDK 编译失败\n\n### 改进\n- 所有 AI 提供商模型列表更新到最新版本\n- UID/GID 动态分配,避免与已有系统用户冲突\n- 版本号统一由 VERSION 文件管理\n- README.md 完善安装说明、FAQ 和项目结构\n"}}, "root/usr/share/openclaw/oc-config-interactive.js": {"26646fa63dc245216dfffa1bf2f9e58036944749": {"t": 1785674200.8530872, "n": 82439, "h": "31994a356171acd1", "c": "#!/usr/bin/env node\n/**\n * ============================================================================\n * OpenClaw 配置工具 — 交互式菜单前端\n * ============================================================================\n *\n * 功能特性:\n * - 方向键 (↑↓) 导航菜单选项 (使用 oc-menu-engine 引擎)\n * - 回车/空格 确认选择\n * - 实时搜索过滤\n * - 与 oc-config.sh 业务逻辑完全一致\n * - 纯 Node.js 实现,零外部依赖\n * - 完全兼容 xterm.js / Web PTY 环境\n */\n\nconst path = require('path');\nconst fs = require('fs');\nconst { spawn, execSync, execFileSync } = require('child_process');\nconst menu = require('./oc-menu-engine');\nconst { C, select, input, confirm, spinner, resetRenderCount } = menu;\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 配置路径 (与 oc-config.sh 保持一致)\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst OC_BASE_PATH = process.env.OC_BASE_PATH || '/opt';\nconst OC_INSTALL_PATH = process.env.OC_INSTALL_PATH || `${OC_BASE_PATH}/openclaw`;\nconst NODE_BASE = process.env.NODE_BASE || `${OC_INSTALL_PATH}/node`;\nconst OC_GLOBAL = process.env.OC_GLOBAL || `${OC_INSTALL_PATH}/global`;\nconst OC_DATA = process.env.OC_DATA || `${OC_INSTALL_PATH}/data`;\nconst OC_STATE_DIR = process.env.OPENCLAW_STATE_DIR || `${OC_DATA}/.openclaw`;\nconst CONFIG_FILE = process.env.OPENCLAW_CONFIG_PATH || `${OC_STATE_DIR}/openclaw.json`;\nconst NODE_BIN = `${NODE_BASE}/bin/node`;\nconst PERMISSIONS_HELPER = '/usr/libexec/openclaw-permissions.sh';\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 辅助函数 (与 oc-config.sh 逻辑对应)\n// ═══════════════════════════════════════════════════════════════════════════\n\n/**\n * 执行命令并返回结果\n */\nfunction runCommand(cmd, args = [], options = {}) {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, {\n stdio: ['inherit', 'pipe', 'pipe'],\n shell: true,\n env: { ...process.env, ...options.env },\n });\n\n let stdout = '';\n let stderr = '';\n\n child.stdout.on('data', (data) => {\n stdout += data;\n process.stdout.write(data);\n });\n\n child.stderr.on('data', (data) => {\n stderr += data;\n process.stderr.write(data);\n });\n\n child.on('close', (code) => {\n if (code === 0) {\n resolve({ stdout, stderr, code });\n } else {\n reject(new Error(`Command failed with code ${code}: ${stderr}`));\n }\n });\n });\n}\n\n/**\n * 读取 JSON 配置文件\n */\nfunction fixStatePermissions() {\n try {\n execFileSync(PERMISSIONS_HELPER, ['fix-state', OC_STATE_DIR], { stdio: 'ignore', timeout: 10000 });\n } catch {\n try {\n execSync(`find \"${OC_STATE_DIR}\" -user root ! -path \"*/extensions*\" ! -path \"*/archived-extensions*\" ! -path \"*/npm/projects*\" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });\n } catch {}\n }\n}\n\nfunction readConfig() {\n try {\n if (fs.existsSync(CONFIG_FILE)) {\n return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));\n }\n } catch (e) {\n console.error(`${C.red}读取配置失败:${C.reset} ${e.message}`);\n }\n return {};\n}\n\n/**\n * 写入 JSON 配置文件\n */\nfunction writeConfig(config) {\n const dir = path.dirname(CONFIG_FILE);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true }); try { execSync(`chown openclaw:openclaw \"${dir}\"`, { stdio: \"ignore\" }); } catch {}\n }\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));\n try {\n execSync(`chown openclaw:openclaw \"${CONFIG_FILE}\"`, { stdio: 'ignore' });\n fixStatePermissions();\n } catch {}\n}\n\n/**\n * 获取 JSON 配置值 (对应 json_get)\n */\nfunction jsonGet(keyPath) {\n const config = readConfig();\n const keys = keyPath.split('.');\n let value = config;\n for (const key of keys) {\n if (value && typeof value === 'object' && key in value) {\n value = value[key];\n } else {\n return null;\n }\n }\n return value;\n}\n\n/**\n * 设置 JSON 配置值 (对应 json_set)\n */\nfunction jsonSet(keyPath, value) {\n const config = readConfig();\n const keys = keyPath.split('.');\n let obj = config;\n for (let i = 0; i < keys.length - 1; i++) {\n if (!obj[keys[i]]) obj[keys[i]] = {};\n obj = obj[keys[i]];\n }\n obj[keys[keys.length - 1]] = value;\n writeConfig(config);\n}\n\n/**\n * 获取当前活跃模型\n */\nfunction getCurrentModel() {\n const primary = jsonGet('agents.defaults.model.primary');\n if (primary) return primary;\n const config = readConfig();\n if (config.models?.defaultModel) return config.models.defaultModel;\n return null;\n}\n\n/**\n * 查找 OpenClaw 入口文件\n */\nfunction findOpenClawEntry() {\n const searchPaths = [\n `${OC_GLOBAL}/lib/node_modules/openclaw/openclaw.mjs`,\n `${OC_GLOBAL}/lib/node_modules/openclaw/dist/cli.js`,\n `${OC_GLOBAL}/node_modules/openclaw/openclaw.mjs`,\n `${OC_GLOBAL}/node_modules/openclaw/dist/cli.js`,\n ];\n for (const p of searchPaths) {\n if (fs.existsSync(p)) return p;\n }\n try {\n const dirs = fs.readdirSync(OC_GLOBAL);\n for (const dir of dirs) {\n const p = `${OC_GLOBAL}/${dir}/node_modules/openclaw/openclaw.mjs`;\n if (fs.existsSync(p)) return p;\n }\n } catch {}\n return null;\n}\n\n/**\n * 执行 OpenClaw CLI 命令 (对应 oc_cmd)\n */\nasync function ocCmd(...args) {\n const ocEntry = findOpenClawEntry();\n if (!ocEntry) throw new Error('OpenClaw 未安装');\n return runCommand(NODE_BIN, [ocEntry, ...args], {\n env: {\n OPENCLAW_HOME: OC_DATA,\n OPENCLAW_CONFIG_PATH: CONFIG_FILE,\n OPENCLAW_STATE_DIR: OC_STATE_DIR,\n HOME: OC_DATA,\n }\n });\n}\n\n/**\n * 注册模型并设为默认 (对应 register_and_set_model)\n */\nfunction registerAndSetModel(modelId) {\n const config = readConfig();\n if (!config.agents) config.agents = {};\n if (!config.agents.defaults) config.agents.defaults = {};\n if (!config.agents.defaults.models) config.agents.defaults.models = {};\n if (!config.agents.defaults.model) config.agents.defaults.model = {};\n config.agents.defaults.models[modelId] = {};\n config.agents.defaults.model.primary = modelId;\n writeConfig(config);\n}\n\n/**\n * 写入 API Key 到 auth-profiles.json (对应 auth_set_apikey)\n */\nfunction authSetApikey(provider, apiKey, profileId) {\n const authDir = `${OC_STATE_DIR}/agents/main/agent`;\n const authFile = `${authDir}/auth-profiles.json`;\n try {\n fs.mkdirSync(authDir, { recursive: true }); try { execSync(`chown openclaw:openclaw \"${authDir}\"`, { stdio: \"ignore\" }); } catch {}\n } catch {}\n\n let authData = { version: 1, profiles: {}, usageStats: {} };\n try {\n if (fs.existsSync(authFile)) {\n authData = JSON.parse(fs.readFileSync(authFile, 'utf8'));\n }\n } catch {}\n\n if (!authData.profiles) authData.profiles = {};\n authData.profiles[profileId || `${provider}:manual`] = {\n type: 'api_key',\n provider: provider,\n key: apiKey,\n };\n\n fs.writeFileSync(authFile, JSON.stringify(authData, null, 2));\n try {\n execSync(`chown openclaw:openclaw \"${authFile}\"`, { stdio: 'ignore' });\n fixStatePermissions();\n } catch {}\n}\n\n/**\n * 注册自定义提供商 (对应 register_custom_provider)\n */\nfunction registerCustomProvider(providerName, baseUrl, apiKey, modelId, modelDisplay, ctxWindow, maxTok) {\n const config = readConfig();\n if (!config.models) config.models = {};\n if (!config.models.providers) config.models.providers = {};\n config.models.mode = 'merge';\n config.models.providers[providerName] = {\n baseUrl: baseUrl,\n apiKey: apiKey,\n api: 'openai-completions',\n models: [{\n id: modelId,\n name: modelDisplay || modelId,\n reasoning: false,\n input: ['text', 'image'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: parseInt(ctxWindow) || 128000,\n maxTokens: parseInt(maxTok) || 32000,\n }]\n };\n writeConfig(config);\n}\n\n/**\n * 注册 Coding Plan 提供商 (对应 register_codingplan_provider)\n */\nfunction registerCodingPlanProvider(apiKey) {\n const config = readConfig();\n if (!config.models) config.models = {};\n if (!config.models.providers) config.models.providers = {};\n config.models.mode = 'merge';\n config.models.providers['bailian'] = {\n baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',\n apiKey: apiKey,\n api: 'openai-completions',\n models: [\n { 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 },\n { 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 },\n { 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 },\n { 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 },\n { 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 },\n { id: 'glm-5', name: 'glm-5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 16384 },\n { 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 },\n { 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 },\n ]\n };\n\n if (!config.agents) config.agents = {};\n if (!config.agents.defaults) config.agents.defaults = {};\n if (!config.agents.defaults.models) config.agents.defaults.models = {};\n ['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 => {\n config.agents.defaults.models[`bailian/${m}`] = {};\n });\n\n writeConfig(config);\n}\n\n/**\n * 注册腾讯云 Coding Plan 提供商 (对应 register_lkeap_codingplan_provider)\n */\nfunction registerLkeapCodingPlanProvider(apiKey) {\n const config = readConfig();\n if (!config.models) config.models = {};\n if (!config.models.providers) config.models.providers = {};\n config.models.mode = 'merge';\n config.models.providers['lkeap'] = {\n baseUrl: 'https://api.lkeap.cloud.tencent.com/coding/v3',\n apiKey: apiKey,\n api: 'openai-completions',\n models: [\n { id: 'tc-code-latest', name: 'Auto (智能匹配最优模型)', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },\n { 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 },\n { 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 },\n { id: 'hunyuan-t1', name: 'Hunyuan-T1', reasoning: true, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 64000 },\n { id: 'hunyuan-turbos', name: 'Hunyuan-TurboS', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 16000 },\n { 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 },\n { 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 },\n { id: 'glm-5', name: 'GLM-5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 8192 },\n ]\n };\n\n if (!config.agents) config.agents = {};\n if (!config.agents.defaults) config.agents.defaults = {};\n if (!config.agents.defaults.models) config.agents.defaults.models = {};\n ['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 => {\n config.agents.defaults.models[`lkeap/${m}`] = {};\n });\n\n writeConfig(config);\n}\n\n/**\n * 重启 Gateway\n */\nasync function restartGateway() {\n resetRenderCount();\n console.log(`\\n${C.yellow}正在重启 Gateway...${C.reset}`);\n\n try {\n await runCommand('/bin/sh', ['-c', [\n \"if [ \\\"$(uci -q get openclaw.main.enabled 2>/dev/null || echo 0)\\\" != \\\"1\\\" ]; then\",\n \" uci -q set openclaw.main.enabled='1';\",\n \" uci -q commit openclaw;\",\n \" /etc/init.d/openclaw enable >/dev/null 2>&1 || true;\",\n 'fi;',\n '/etc/init.d/openclaw restart_gateway >/dev/null 2>&1 || true;',\n '/etc/init.d/openclaw start >/dev/null 2>&1 || true'\n ].join(' ')]);\n } catch {}\n\n const spin = spinner({ text: 'Gateway 启动中,请稍候 (约 15-30 秒)...' });\n spin.start();\n\n const gwPort = jsonGet('gateway.port') || '18789';\n let waited = 0;\n const maxWait = 30;\n\n while (waited < maxWait) {\n await new Promise(r => setTimeout(r, 3000));\n waited += 3;\n try {\n // 兼容 OpenWrt: 优先 ss,回退 netstat\n let stdout = '';\n try {\n stdout = execSync('ss -tulnp 2>/dev/null', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });\n } catch {\n // ss 不存在,使用 netstat\n try {\n stdout = execSync('netstat -tulnp 2>/dev/null', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });\n } catch {}\n }\n if (stdout && stdout.includes(`:${gwPort} `)) {\n spin.succeed(`Gateway 已重启成功 (${waited}秒)`);\n return;\n }\n } catch {}\n }\n\n spin.stop(`${C.yellow}Gateway 仍在启动中,请稍后确认${C.reset}`);\n console.log(`${C.cyan} 查看日志: logread -e openclaw${C.reset}\\n`);\n}\n\n/**\n * 询问是否重启\n */\nasync function askRestart() {\n const ok = await confirm({ prompt: '是否立即重启服务以应用配置?', defaultYes: true });\n if (ok) {\n await restartGateway();\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 主菜单 (对应 main_menu)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function showMainMenu() {\n const currentModel = getCurrentModel();\n const statusLine = currentModel\n ? `${C.green}当前模型: ${currentModel}${C.reset}`\n : `${C.yellow}未配置模型${C.reset}`;\n\n const result = await select({\n title: 'OpenClaw AI Gateway — OpenWrt 配置管理',\n header: statusLine,\n showSearch: false,\n items: [\n { label: `${C.dim}━━━ AI 模型配置 ━━━${C.reset}`, disabled: true },\n { key: '1', label: '配置 AI 模型和提供商', desc: '', value: 'model' },\n { key: '2', label: '设置活动模型', desc: '', value: 'set-active-model' },\n\n { label: `${C.dim}━━━ 消息渠道 ━━━${C.reset}`, disabled: true },\n { key: '3', label: '配置消息渠道', desc: '电报/QQ/飞书', value: 'channels' },\n\n { label: `${C.dim}━━━ 系统管理 ━━━${C.reset}`, disabled: true },\n { key: '4', label: '健康检查与状态', desc: '', value: 'health' },\n { key: '5', label: '查看日志', desc: '', value: 'logs' },\n { key: '6', label: '重启 Gateway', desc: '', value: 'restart' },\n\n { label: `${C.dim}━━━ 高级选项 ━━━${C.reset}`, disabled: true },\n { key: '7', label: '高级配置', desc: '', value: 'advanced' },\n { key: '8', label: '重置配置', desc: '', value: 'reset' },\n { key: '9', label: '显示当前配置概览', desc: '', value: 'show-config' },\n { key: '10', label: '备份/还原配置', desc: '', value: 'backup' },\n\n { label: '', disabled: true },\n { key: '0', label: '退出', desc: '', value: 'quit' },\n ],\n });\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 模型配置菜单 (对应 configure_model)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function showModelMenu() {\n const result = await select({\n title: '配置 AI 模型和提供商',\n showSearch: true,\n items: [\n { label: `${C.green}${C.bold}── 推荐 ──${C.reset}`, disabled: true },\n { key: 'w', label: '官方完整模型配置向导', desc: `${C.green}(推荐,支持所有提供商)${C.reset}`, value: 'wizard' },\n\n { label: `${C.bold}── 国外模型提供商 ──${C.reset}`, disabled: true },\n { key: 'a', label: 'OpenAI', desc: 'GPT-5.2, GPT-5 mini, GPT-4.1', value: 'openai' },\n { key: 'b', label: 'Anthropic', desc: 'Claude Sonnet 4, Opus 4, Haiku', value: 'anthropic' },\n { key: 'c', label: 'Google Gemini', desc: 'Gemini 2.5 Pro/Flash, Gemini 3', value: 'google' },\n { key: 'd', label: 'OpenRouter', desc: '聚合多家模型', value: 'openrouter' },\n { key: 'e', label: 'GitHub Copilot', desc: '需要 Copilot 订阅', value: 'copilot' },\n { key: 'f', label: 'xAI Grok', desc: 'Grok-4/3', value: 'xai' },\n\n { label: `${C.bold}── 国内模型提供商 ──${C.reset}`, disabled: true },\n { key: 'g', label: '阿里云通义千问 Qwen', desc: 'Portal/API/Coding Plan', value: 'qwen' },\n { key: 'h', label: '硅基流动 SiliconFlow', desc: '', value: 'siliconflow' },\n { key: 'i', label: '腾讯云 Coding Plan', desc: 'HY T1/TurboS/GLM-5/Kimi', value: 'tencent' },\n { key: 'j', label: '百度千帆', desc: 'ERNIE-4.0, ERNIE-3.5', value: 'baidu' },\n { key: 'k', label: '智谱 GLM / Z.AI', desc: 'GLM-5.1, GLM-5, GLM-4.7', value: 'zhipu' },\n\n { label: `${C.bold}── 本地模型 / 自定义 API ──${C.reset}`, disabled: true },\n { key: 'l', label: 'Ollama', desc: '本地模型,无需 API Key', value: 'ollama' },\n { key: 'm', label: '自定义 OpenAI 兼容 API', desc: '', value: 'custom' },\n { key: 'n', label: '自定义 Anthropic 兼容 API', desc: '', value: 'custom-anthropic' },\n { key: 'o', label: '一万AI分享 粉丝专享 API', desc: '', value: 'yiwanai-fan' },\n\n { label: '', disabled: true },\n { key: '0', label: '返回', desc: '', value: 'back' },\n ],\n });\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 消息渠道菜单 (对应 configure_channels)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function showChannelsMenu() {\n const result = await select({\n title: '配置消息渠道',\n showSearch: false,\n items: [\n { label: `${C.dim}提示: 微信配置请使用 LuCI 界面「微信配置」菜单${C.reset}`, disabled: true },\n { label: '', disabled: true },\n { key: '1', label: 'QQ 机器人', desc: '腾讯QQ', value: 'qq' },\n { key: '2', label: 'Telegram', desc: `${C.green}最常用${C.reset}`, value: 'telegram' },\n { key: '3', label: 'Discord', desc: '', value: 'discord' },\n { key: '4', label: '飞书 (Feishu)', desc: '', value: 'feishu' },\n { key: '5', label: 'Slack', desc: '', value: 'slack' },\n { key: '6', label: 'WhatsApp', desc: '需通过 Web 控制台扫码', value: 'whatsapp' },\n { key: '7', label: 'Telegram 配对助手', desc: '', value: 'telegram-pairing' },\n { key: '8', label: '官方完整渠道配置向导', desc: '', value: 'wizard' },\n { label: '', disabled: true },\n { key: '0', label: '返回', desc: '', value: 'back' },\n ],\n });\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 高级配置菜单 (对应 advanced_menu)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function showAdvancedMenu() {\n const gwPort = jsonGet('gateway.port') || '18789';\n const gwBind = jsonGet('gateway.bind') || 'lan';\n const gwMode = jsonGet('gateway.mode') || 'local';\n const logLevel = jsonGet('gateway.logLevel') || '未设置';\n const acpDispatch = jsonGet('acp.dispatch.enabled') || 'false';\n\n const result = await select({\n title: '高级配置',\n showSearch: false,\n items: [\n { key: '1', label: 'Gateway 端口', desc: `当前: ${gwPort}`, value: 'port' },\n { key: '2', label: 'Gateway 绑定地址', desc: `当前: ${gwBind}`, value: 'bind' },\n { key: '3', label: 'Gateway 运行模式', desc: `当前: ${gwMode}`, value: 'mode' },\n { key: '4', label: '日志级别', desc: `当前: ${logLevel}`, value: 'loglevel' },\n { key: '5', label: 'ACP Dispatch 设置', desc: `当前: ${acpDispatch}`, value: 'acp' },\n { key: '6', label: '官方完整配置向导', desc: 'oc configure', value: 'wizard' },\n { key: '7', label: '查看原始配置 JSON', desc: '', value: 'view-json' },\n { key: '8', label: '编辑配置文件', desc: 'vi / nano', value: 'edit' },\n { key: '9', label: '导出配置备份', desc: '', value: 'backup' },\n { key: '10', label: '导入配置', desc: '', value: 'import' },\n { label: '', disabled: true },\n { key: '0', label: '返回', desc: '', value: 'back' },\n ],\n });\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 重置配置菜单 (对应 reset_to_defaults)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function showResetMenu() {\n const result = await select({\n title: '恢复默认配置',\n showSearch: false,\n items: [\n { label: `${C.yellow}请选择恢复级别:${C.reset}`, disabled: true },\n { label: '', disabled: true },\n { key: '1', label: '仅重置网关设置', desc: '端口/绑定/模式恢复默认,保留模型和渠道', value: 'gateway' },\n { key: '2', label: '清除模型配置', desc: '移除所有 AI 模型和 API Key', value: 'models' },\n { key: '3', label: '清除渠道配置', desc: '移除所有消息渠道配置', value: 'channels' },\n { key: '4', label: '完全恢复出厂', desc: '删除所有配置,重新初始化', value: 'full' },\n { label: '', disabled: true },\n { key: '0', label: '返回', desc: '', value: 'back' },\n ],\n });\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 备份/还原菜单 (对应 backup_restore_menu)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function showBackupMenu() {\n const result = await select({\n title: '备份/还原配置',\n showSearch: false,\n items: [\n { key: '1', label: '创建配置备份', desc: '仅配置文件', value: 'create-config' },\n { key: '2', label: '创建完整备份', desc: '配置 + 状态数据', value: 'create-full' },\n { key: '3', label: '验证最新备份', desc: '', value: 'verify' },\n { key: '4', label: '查看备份列表', desc: '', value: 'list' },\n { key: '5', label: '从最新备份恢复配置', desc: '', value: 'restore' },\n { label: '', disabled: true },\n { key: '0', label: '返回', desc: '', value: 'back' },\n ],\n });\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 模型配置处理函数 (与 oc-config.sh 完全对应)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function configureOpenAI() {\n resetRenderCount();\n console.log(`\\n${C.bold}OpenAI 配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://platform.openai.com/api-keys${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 OpenAI API Key (sk-...)', placeholder: 'sk-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: 'OpenAI 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'gpt-5.2', desc: '最强编程与代理旗舰 (推荐)', value: 'gpt-5.2' },\n { key: 'b', label: 'gpt-5-mini', desc: '高性价比推理', value: 'gpt-5-mini' },\n { key: 'c', label: 'gpt-5-nano', desc: '极速低成本', value: 'gpt-5-nano' },\n { key: 'd', label: 'gpt-4.1', desc: '最强非推理模型', value: 'gpt-4.1' },\n { key: 'e', label: 'o3', desc: '推理模型', value: 'o3' },\n { key: 'f', label: 'o4-mini', desc: '推理轻量', value: 'o4-mini' },\n { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gpt-5.2' });\n if (!modelName) return false;\n }\n\n authSetApikey('openai', apiKey);\n registerAndSetModel(`openai/${modelName}`);\n console.log(`\\n${C.green}✅ OpenAI 已配置,活跃模型: openai/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureAnthropic() {\n resetRenderCount();\n console.log(`\\n${C.bold}Anthropic 配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://console.anthropic.com/settings/keys${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 Anthropic API Key (sk-ant-...)', placeholder: 'sk-ant-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: 'Anthropic 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'claude-sonnet-4-20250514', desc: 'Claude Sonnet 4 (推荐)', value: 'claude-sonnet-4-20250514' },\n { key: 'b', label: 'claude-opus-4-20250514', desc: 'Claude Opus 4 顶级推理', value: 'claude-opus-4-20250514' },\n { key: 'c', label: 'claude-haiku-4-5', desc: 'Claude Haiku 4.5 轻量快速', value: 'claude-haiku-4-5' },\n { key: 'd', label: 'claude-sonnet-4.5', desc: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },\n { key: 'e', label: 'claude-sonnet-4.6', desc: 'Claude Sonnet 4.6', value: 'claude-sonnet-4.6' },\n { key: 'f', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'claude-sonnet-4-20250514' });\n if (!modelName) return false;\n }\n\n authSetApikey('anthropic', apiKey);\n registerAndSetModel(`anthropic/${modelName}`);\n console.log(`\\n${C.green}✅ Anthropic 已配置,活跃模型: anthropic/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureGoogle() {\n resetRenderCount();\n console.log(`\\n${C.bold}Google Gemini 配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://aistudio.google.com/apikey${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 Google AI API Key', placeholder: 'AIza...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: 'Google Gemini 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'gemini-2.5-pro', desc: '旗舰推理 (推荐)', value: 'gemini-2.5-pro' },\n { key: 'b', label: 'gemini-2.5-flash', desc: '快速均衡', value: 'gemini-2.5-flash' },\n { key: 'c', label: 'gemini-2.5-flash-lite', desc: '极速低成本', value: 'gemini-2.5-flash-lite' },\n { key: 'd', label: 'gemini-3-flash-preview', desc: 'Gemini 3 Flash 预览', value: 'gemini-3-flash-preview' },\n { key: 'e', label: 'gemini-3-pro-preview', desc: 'Gemini 3 Pro 预览', value: 'gemini-3-pro-preview' },\n { key: 'f', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gemini-2.5-pro' });\n if (!modelName) return false;\n }\n\n authSetApikey('google', apiKey);\n registerAndSetModel(`google/${modelName}`);\n console.log(`\\n${C.green}✅ Google Gemini 已配置,活跃模型: google/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureOpenRouter() {\n resetRenderCount();\n console.log(`\\n${C.bold}OpenRouter 配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://openrouter.ai/keys${C.reset}`);\n console.log(`${C.dim}聚合多家模型,一个 Key 可调用所有主流模型${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 OpenRouter API Key', placeholder: 'sk-or-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: 'OpenRouter 常用模型',\n showSearch: false,\n items: [\n { key: 'a', label: 'anthropic/claude-sonnet-4', desc: 'Claude Sonnet 4 (推荐)', value: 'anthropic/claude-sonnet-4' },\n { key: 'b', label: 'anthropic/claude-opus-4', desc: 'Claude Opus 4', value: 'anthropic/claude-opus-4' },\n { key: 'c', label: 'openai/gpt-5.2', desc: 'GPT-5.2', value: 'openai/gpt-5.2' },\n { key: 'd', label: 'google/gemini-2.5-pro', desc: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' },\n { key: 'e', label: 'deepseek/deepseek-r1', desc: 'DeepSeek R1', value: 'deepseek/deepseek-r1' },\n { key: 'f', label: 'meta-llama/llama-4-maverick', desc: 'Meta Llama 4', value: 'meta-llama/llama-4-maverick' },\n { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称 (格式: provider/model)', defaultValue: 'anthropic/claude-sonnet-4' });\n if (!modelName) return false;\n }\n\n authSetApikey('openrouter', apiKey);\n registerCustomProvider('openrouter', 'https://openrouter.ai/api/v1', apiKey, modelName, modelName);\n registerAndSetModel(`openrouter/${modelName}`);\n console.log(`\\n${C.green}✅ OpenRouter 已配置,活跃模型: openrouter/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureCopilot() {\n resetRenderCount();\n console.log(`\\n${C.bold}GitHub Copilot 配置${C.reset}`);\n console.log(`${C.yellow}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${C.reset}\\n`);\n\n console.log(`${C.cyan}启动 GitHub Copilot OAuth 登录...${C.reset}`);\n console.log(`${C.dim}请在浏览器中打开显示的 URL,输入授权码完成登录${C.reset}\\n`);\n\n try {\n await ocCmd('models', 'auth', 'login-github-copilot', '--yes');\n console.log(`\\n${C.green}✅ GitHub Copilot OAuth 认证成功${C.reset}\\n`);\n\n const modelChoice = await select({\n title: 'GitHub Copilot 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'gpt-4.1', desc: 'GPT-4.1 (推荐)', value: 'gpt-4.1' },\n { key: 'b', label: 'gpt-4o', desc: 'GPT-4o', value: 'gpt-4o' },\n { key: 'c', label: 'gpt-5', desc: 'GPT-5', value: 'gpt-5' },\n { key: 'd', label: 'gpt-5-mini', desc: 'GPT-5 mini', value: 'gpt-5-mini' },\n { key: 'e', label: 'gpt-5.1', desc: 'GPT-5.1', value: 'gpt-5.1' },\n { key: 'f', label: 'gpt-5.2', desc: 'GPT-5.2', value: 'gpt-5.2' },\n { key: 'g', label: 'gpt-5.2-codex', desc: 'GPT-5.2 Codex', value: 'gpt-5.2-codex' },\n { key: 'h', label: 'claude-sonnet-4', desc: 'Claude Sonnet 4', value: 'claude-sonnet-4' },\n { key: 'i', label: 'claude-sonnet-4.5', desc: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },\n { key: 'j', label: 'claude-sonnet-4.6', desc: 'Claude Sonnet 4.6', value: 'claude-sonnet-4.6' },\n { key: 'k', label: 'gemini-2.5-pro', desc: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro' },\n { key: 'm', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n\n if (modelChoice) {\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gpt-4.1' });\n if (!modelName) return false;\n }\n registerAndSetModel(`github-copilot/${modelName}`);\n console.log(`\\n${C.green}✅ 活跃模型已设置: github-copilot/${modelName}${C.reset}\\n`);\n }\n return true;\n } catch (e) {\n console.log(`\\n${C.yellow}OAuth 授权已退出或失败${C.reset}\\n`);\n return false;\n }\n}\n\nasync function configureXAI() {\n resetRenderCount();\n console.log(`\\n${C.bold}xAI Grok 配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://console.x.ai${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 xAI API Key', placeholder: '' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: 'xAI Grok 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'grok-4', desc: 'Grok 4 旗舰 (推荐)', value: 'grok-4' },\n { key: 'b', label: 'grok-4-fast', desc: 'Grok 4 Fast', value: 'grok-4-fast' },\n { key: 'c', label: 'grok-3', desc: 'Grok 3', value: 'grok-3' },\n { key: 'd', label: 'grok-3-fast', desc: 'Grok 3 Fast', value: 'grok-3-fast' },\n { key: 'e', label: 'grok-3-mini', desc: 'Grok 3 Mini', value: 'grok-3-mini' },\n { key: 'f', label: 'grok-3-mini-fast', desc: 'Grok 3 Mini Fast', value: 'grok-3-mini-fast' },\n { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'grok-4' });\n if (!modelName) return false;\n }\n\n authSetApikey('xai', apiKey);\n registerAndSetModel(`xai/${modelName}`);\n console.log(`\\n${C.green}✅ xAI Grok 已配置,活跃模型: xai/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureQwen() {\n resetRenderCount();\n console.log(`\\n${C.bold}阿里云通义千问 Qwen 配置${C.reset}\\n`);\n\n const modeChoice = await select({\n title: '配置方式',\n showSearch: false,\n items: [\n { key: 'a', label: '通过官方向导配置', desc: 'Qwen Portal OAuth', value: 'portal' },\n { key: 'b', label: '百炼按量付费 API Key', desc: 'sk-xxx, 按 token 计费', value: 'bailian' },\n { key: 'c', label: 'Coding Plan 套餐', desc: `${C.green}★ 推荐${C.reset}`, value: 'codingplan' },\n ],\n });\n if (!modeChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n if (modeChoice.value === 'portal') {\n console.log(`\\n${C.cyan}启动 Qwen OAuth 授权...${C.reset}\\n`);\n try {\n await ocCmd('models', 'auth', 'login', '--provider', 'qwen-portal', '--set-default');\n } catch {}\n console.log(`\\n${C.yellow}OAuth 授权已退出${C.reset}\\n`);\n return true;\n }\n\n if (modeChoice.value === 'bailian') {\n console.log(`\\n${C.bold}百炼按量付费配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://dashscope.console.aliyun.com/apiKey${C.reset}`);\n console.log(`${C.dim}Base URL: https://dashscope.aliyuncs.com/compatible-mode/v1${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入百炼 API Key (sk-...)', placeholder: 'sk-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: '百炼模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'qwen-max', desc: '千问Max 旗舰模型 (推荐)', value: 'qwen-max' },\n { key: 'b', label: 'qwen-plus', desc: '千问Plus 均衡之选', value: 'qwen-plus' },\n { key: 'c', label: 'qwen-flash', desc: '千问Flash 速度最快', value: 'qwen-flash' },\n { key: 'd', label: 'qwen-turbo', desc: '千问Turbo 经济实惠', value: 'qwen-turbo' },\n { key: 'e', label: 'qwen-long', desc: '千问Long 超长上下文', value: 'qwen-long' },\n { key: 'f', label: 'qwen3-coder-plus', desc: '代码专用旗舰', value: 'qwen3-coder-plus' },\n { key: 'g', label: 'qwq-plus', desc: 'QwQ推理模型', value: 'qwq-plus' },\n { key: 'h', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'qwen-max' });\n if (!modelName) return false;\n }\n\n authSetApikey('dashscope', apiKey);\n registerCustomProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/v1', apiKey, modelName, modelName);\n registerAndSetModel(`dashscope/${modelName}`);\n console.log(`\\n${C.green}✅ 通义千问已配置 (按量付费),活跃模型: dashscope/${modelName}${C.reset}\\n`);\n return true;\n }\n\n if (modeChoice.value === 'codingplan') {\n console.log(`\\n${C.bold}Coding Plan 套餐配置${C.reset}`);\n console.log(`${C.yellow}订阅套餐: https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/coding_plan${C.reset}`);\n console.log(`${C.dim}Base URL: https://coding.dashscope.aliyuncs.com/v1${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 Coding Plan 专属 API Key (sk-sp-...)', placeholder: 'sk-sp-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: 'Coding Plan 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'qwen3.5-plus', desc: 'Qwen3.5 Plus (推荐, 100万上下文)', value: 'qwen3.5-plus' },\n { key: 'b', label: 'qwen3-coder-plus', desc: 'Qwen3 Coder Plus', value: 'qwen3-coder-plus' },\n { key: 'c', label: 'qwen3-coder-next', desc: 'Qwen3 Coder Next', value: 'qwen3-coder-next' },\n { key: 'd', label: 'qwen3-max-2026-01-23', desc: 'Qwen3 Max', value: 'qwen3-max-2026-01-23' },\n { key: 'e', label: 'MiniMax-M2.5', desc: 'MiniMax M2.5', value: 'MiniMax-M2.5' },\n { key: 'f', label: 'glm-5', desc: '智谱 GLM-5', value: 'glm-5' },\n { key: 'g', label: 'kimi-k2.5', desc: 'Kimi K2.5', value: 'kimi-k2.5' },\n { key: 'h', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'qwen3.5-plus' });\n if (!modelName) return false;\n }\n\n console.log(`\\n${C.cyan}正在注册 Coding Plan 提供商 (含全部可用模型)...${C.reset}`);\n authSetApikey('bailian', apiKey);\n registerCodingPlanProvider(apiKey);\n registerAndSetModel(`bailian/${modelName}`);\n console.log(`\\n${C.green}✅ Coding Plan 已配置,活跃模型: bailian/${modelName}${C.reset}`);\n console.log(`${C.dim}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${C.reset}\\n`);\n return true;\n }\n\n return false;\n}\n\nasync function configureSiliconFlow() {\n resetRenderCount();\n console.log(`\\n${C.bold}硅基流动 SiliconFlow 配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://cloud.siliconflow.cn/account/ak${C.reset}`);\n console.log(`${C.yellow}国内推理平台,支持多种开源模型${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 SiliconFlow API Key', placeholder: '' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n console.log(`\\n${C.cyan}可用模型分类说明:${C.reset}`);\n console.log(`${C.yellow}* Pro模型: 仅支持充值余额支付${C.reset}`);\n console.log(`${C.yellow}* 非Pro模型: 支持代金券/免费额度${C.reset}\\n`);\n\n const modelChoice = await select({\n title: 'SiliconFlow 模型选择',\n showSearch: false,\n items: [\n { label: `${C.cyan}── 非Pro模型 (支持代金券) ──${C.reset}`, disabled: true },\n { key: '1', label: 'deepseek-ai/DeepSeek-V3', desc: 'DeepSeek-V3 (推荐)', value: 'deepseek-ai/DeepSeek-V3' },\n { key: '2', label: 'deepseek-ai/DeepSeek-R1', desc: 'DeepSeek-R1 推理模型', value: 'deepseek-ai/DeepSeek-R1' },\n { key: '3', label: 'Qwen/Qwen2.5-72B-Instruct', desc: '通义千问 2.5 72B', value: 'Qwen/Qwen2.5-72B-Instruct' },\n { key: '4', label: 'Qwen/Qwen2.5-7B-Instruct', desc: '通义千问 2.5 7B', value: 'Qwen/Qwen2.5-7B-Instruct' },\n { key: '5', label: 'THUDM/glm-4-9b-chat', desc: '智谱 GLM-4 9B', value: 'THUDM/glm-4-9b-chat' },\n { key: '6', label: '01-ai/Yi-1.5-34B-Chat-16K', desc: '零一万物 Yi-1.5', value: '01-ai/Yi-1.5-34B-Chat-16K' },\n { label: `${C.cyan}── Pro模型 (仅支持充值余额) ──${C.reset}`, disabled: true },\n { key: '7', label: 'Pro/deepseek-ai/DeepSeek-V3', desc: 'DeepSeek-V3 (Pro)', value: 'Pro/deepseek-ai/DeepSeek-V3' },\n { key: '8', label: 'Pro/zai-org/GLM-5', desc: '智谱 GLM-5', value: 'Pro/zai-org/GLM-5' },\n { key: '0', label: '手动输入其他模型名称', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型详细名称', defaultValue: 'deepseek-ai/DeepSeek-V3' });\n if (!modelName) return false;\n }\n\n authSetApikey('siliconflow', apiKey);\n registerCustomProvider('siliconflow', 'https://api.siliconflow.cn/v1', apiKey, modelName, modelName);\n registerAndSetModel(`siliconflow/${modelName}`);\n console.log(`\\n${C.green}✅ SiliconFlow 已配置,活跃模型: siliconflow/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureTencent() {\n resetRenderCount();\n console.log(`\\n${C.bold}腾讯云大模型 Coding Plan 套餐配置${C.reset}`);\n console.log(`${C.yellow}订阅/管理套餐: https://hunyuan.cloud.tencent.com/#/app/subscription${C.reset}`);\n console.log(`${C.dim}文档: https://cloud.tencent.com/document/product/1772/128947${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入 Coding Plan API Key (sk-sp-...)', placeholder: 'sk-sp-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: '腾讯云 Coding Plan 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'tc-code-latest', desc: '自动路由 (推荐)', value: 'tc-code-latest' },\n { key: 'b', label: 'hunyuan-t1', desc: '混元 T1 深度推理', value: 'hunyuan-t1' },\n { key: 'c', label: 'hunyuan-2.0-thinking', desc: '混元 2.0 Thinking', value: 'hunyuan-2.0-thinking' },\n { key: 'd', label: 'hunyuan-turbos', desc: '混元 TurboS 旗舰', value: 'hunyuan-turbos' },\n { key: 'e', label: 'hunyuan-2.0-instruct', desc: '混元 2.0 Instruct', value: 'hunyuan-2.0-instruct' },\n { key: 'f', label: 'glm-5', desc: '智谱 GLM-5', value: 'glm-5' },\n { key: 'g', label: 'kimi-k2.5', desc: 'Moonshot Kimi K2.5', value: 'kimi-k2.5' },\n { key: 'h', label: 'minimax-m2.5', desc: 'MiniMax M2.5', value: 'minimax-m2.5' },\n { key: 'z', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'tc-code-latest' });\n if (!modelName) return false;\n }\n\n console.log(`\\n${C.cyan}正在注册腾讯云 Coding Plan 提供商 (含全部套餐模型)...${C.reset}`);\n authSetApikey('lkeap', apiKey);\n registerLkeapCodingPlanProvider(apiKey);\n registerAndSetModel(`lkeap/${modelName}`);\n console.log(`\\n${C.green}✅ 腾讯云 Coding Plan 已配置,活跃模型: lkeap/${modelName}${C.reset}`);\n console.log(`${C.dim}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${C.reset}\\n`);\n return true;\n}\n\nasync function configureBaidu() {\n resetRenderCount();\n console.log(`\\n${C.bold}百度千帆大模型配置${C.reset}`);\n console.log(`${C.yellow}获取 API Key: https://console.bce.baidu.com/qianfan/ais/console/onlineService${C.reset}\\n`);\n\n const apiKey = await input({ prompt: '请输入百度千帆 API Key (Access Token)', placeholder: '' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: '百度千帆模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'ernie-4.0-8k', desc: '文心一言 4.0 (推荐)', value: 'ernie-4.0-8k' },\n { key: 'b', label: 'ernie-3.5-8k', desc: '文心一言 3.5', value: 'ernie-3.5-8k' },\n { key: 'c', label: 'ernie-4.0-turbo-8k', desc: '文心一言 4.0 Turbo', value: 'ernie-4.0-turbo-8k' },\n { key: 'd', label: 'ernie-speed-8k', desc: '文心一言 Speed 极速', value: 'ernie-speed-8k' },\n { key: 'e', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'ernie-4.0-8k' });\n if (!modelName) return false;\n }\n\n authSetApikey('qianfan', apiKey);\n registerCustomProvider('qianfan', 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop', apiKey, modelName, modelName);\n registerAndSetModel(`qianfan/${modelName}`);\n console.log(`\\n${C.green}✅ 百度千帆已配置,活跃模型: qianfan/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureZhipu() {\n resetRenderCount();\n console.log(`\\n${C.bold}智谱 GLM / Z.AI 配置${C.reset}\\n`);\n\n const methodChoice = await select({\n title: '认证方式',\n showSearch: false,\n items: [\n { key: 'a', label: 'CN (open.bigmodel.cn)', desc: `${C.green}★ 国内用户推荐${C.reset}`, value: 'cn' },\n { key: 'b', label: 'Coding-Plan-CN', desc: '智谱 Coding Plan 套餐', value: 'coding-plan-cn' },\n { key: 'c', label: 'Global (api.z.ai)', desc: '全球版', value: 'global' },\n { key: 'd', label: '手动输入', desc: '', value: 'custom' },\n ],\n });\n if (!methodChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let baseUrl = 'https://open.bigmodel.cn/api/paas/v4';\n switch (methodChoice.value) {\n case 'cn':\n console.log(`${C.yellow}获取 API Key: https://open.bigmodel.cn/api-key${C.reset}`);\n break;\n case 'coding-plan-cn':\n baseUrl = 'https://open.bigmodel.cn/api/coding/paas/v4';\n console.log(`${C.yellow}Coding Plan 套餐 API Key${C.reset}`);\n break;\n case 'global':\n baseUrl = 'https://api.z.ai/api/paas/v4';\n console.log(`${C.yellow}全球版 API Key${C.reset}`);\n break;\n case 'custom':\n const customUrl = await input({ prompt: '请输入 Base URL', defaultValue: 'https://open.bigmodel.cn/api/paas/v4' });\n if (customUrl) baseUrl = customUrl;\n break;\n }\n\n const apiKey = await input({ prompt: '请输入智谱 API Key', placeholder: '' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: '智谱 GLM 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'glm-5.1', desc: 'GLM-5.1 (推荐)', value: 'glm-5.1' },\n { key: 'b', label: 'glm-5', desc: 'GLM-5', value: 'glm-5' },\n { key: 'c', label: 'glm-4.7', desc: 'GLM-4.7', value: 'glm-4.7' },\n { key: 'd', label: 'glm-4.7-flash', desc: 'GLM-4.7 Flash', value: 'glm-4.7-flash' },\n { key: 'e', label: 'glm-4.5', desc: 'GLM-4.5', value: 'glm-4.5' },\n { key: 'f', label: 'glm-4.5-flash', desc: 'GLM-4.5 Flash (免费)', value: 'glm-4.5-flash' },\n { key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'glm-5.1' });\n if (!modelName) return false;\n }\n\n // 智谱 GLM 使用原生 zai provider (OpenClaw 内置支持)\n registerCustomProvider('zai', baseUrl, apiKey, modelName, modelName, 128000, 4096);\n authSetApikey('zai', apiKey);\n registerAndSetModel(`zai/${modelName}`);\n console.log(`\\n${C.green}✅ 智谱 GLM 已配置,活跃模型: zai/${modelName}${C.reset}`);\n console.log(`${C.dim} Base URL: ${baseUrl}${C.reset}\\n`);\n return true;\n}\n\nasync function configureOllama() {\n resetRenderCount();\n console.log(`\\n${C.bold}Ollama 本地模型配置${C.reset}`);\n console.log(`${C.yellow}Ollama 在本地或局域网运行大模型,无需 API Key${C.reset}`);\n console.log(`${C.yellow}安装 Ollama: https://ollama.com${C.reset}\\n`);\n\n const modeChoice = await select({\n title: '连接方式',\n showSearch: false,\n items: [\n { key: 'a', label: '本机运行', desc: 'localhost:11434', value: 'local' },\n { key: 'b', label: '局域网其他设备', desc: '', value: 'remote' },\n ],\n });\n if (!modeChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let ollamaUrl = 'http://127.0.0.1:11434';\n if (modeChoice.value === 'remote') {\n const host = await input({ prompt: 'Ollama 地址 (如 192.168.1.100:11434)', placeholder: '' });\n if (!host) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n ollamaUrl = host.startsWith('http') ? host : `http://${host}`;\n ollamaUrl = ollamaUrl.replace(/\\/v1$/, '').replace(/\\/$/, '');\n }\n\n // 尝试检测 Ollama 连通性\n console.log(`\\n${C.cyan}检测 Ollama 连通性...${C.reset}`);\n let modelList = [];\n try {\n const stdout = execSync(`curl -sf --connect-timeout 3 --max-time 5 ${ollamaUrl}/api/tags`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });\n const data = JSON.parse(stdout);\n modelList = data.models || [];\n console.log(`${C.green}✅ Ollama 已连接${C.reset}`);\n } catch {\n console.log(`${C.yellow}⚠️ 无法连接 Ollama (${ollamaUrl})${C.reset}`);\n const continueAnyway = await confirm({ prompt: '仍要继续配置?', defaultYes: false });\n if (!continueAnyway) return false;\n }\n\n let modelName = '';\n if (modelList.length > 0) {\n const items = modelList.map((m, i) => ({\n key: String(i + 1),\n label: m.name,\n desc: '',\n value: m.name,\n }));\n items.push({ key: 'm', label: '手动输入模型名', desc: '', value: 'custom' });\n\n const modelChoice = await select({\n title: '已安装的模型',\n showSearch: false,\n items,\n });\n\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n modelName = modelChoice.value === 'custom'\n ? await input({ prompt: '请输入模型名称', defaultValue: 'llama3.3' })\n : modelChoice.value;\n } else {\n modelName = await input({ prompt: '请输入模型名称', placeholder: 'llama3.3, qwen2.5...' });\n }\n\n if (!modelName) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n // 注册 Ollama 提供商\n const config = readConfig();\n if (!config.models) config.models = {};\n if (!config.models.providers) config.models.providers = {};\n config.models.mode = 'merge';\n config.models.providers['ollama'] = {\n baseUrl: ollamaUrl,\n apiKey: 'ollama-local',\n api: 'ollama',\n models: [{\n id: modelName,\n name: modelName,\n reasoning: false,\n input: ['text'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: 128000,\n maxTokens: 32000,\n }]\n };\n writeConfig(config);\n\n authSetApikey('ollama', 'ollama-local', 'ollama:local');\n registerAndSetModel(`ollama/${modelName}`);\n console.log(`\\n${C.green}✅ Ollama 已配置,活跃模型: ollama/${modelName}${C.reset}`);\n console.log(`${C.cyan} Ollama 地址: ${ollamaUrl}${C.reset}\\n`);\n return true;\n}\n\nasync function configureCustomAPI() {\n resetRenderCount();\n console.log(`\\n${C.bold}自定义 OpenAI 兼容 API 配置${C.reset}`);\n console.log(`${C.yellow}支持任何兼容 OpenAI API 格式的服务商${C.reset}\\n`);\n\n const baseUrl = await input({ prompt: 'API Base URL (如 https://api.example.com/v1)', placeholder: '' });\n if (!baseUrl) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelName = await input({ prompt: '模型名称', placeholder: '' });\n if (!modelName) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n authSetApikey('openai-compatible', apiKey, 'openai-compatible:manual');\n registerCustomProvider('openai-compatible', baseUrl.replace(/\\/$/, ''), apiKey, modelName, modelName);\n registerAndSetModel(`openai-compatible/${modelName}`);\n console.log(`\\n${C.green}✅ 自定义 API 已配置,活跃模型: openai-compatible/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureCustomAnthropic() {\n resetRenderCount();\n console.log(`\\n${C.bold}自定义 Anthropic 兼容 API 配置${C.reset}`);\n console.log(`${C.yellow}支持任何兼容 Anthropic Messages API 格式的服务商${C.reset}\\n`);\n\n const baseUrl = await input({ prompt: 'API Base URL', placeholder: 'https://api.anthropic.com' });\n if (!baseUrl) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-ant-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n const modelChoice = await select({\n title: 'Anthropic 模型选择',\n showSearch: false,\n items: [\n { key: 'a', label: 'claude-sonnet-4-20250514', desc: 'Claude Sonnet 4 (推荐)', value: 'claude-sonnet-4-20250514' },\n { key: 'b', label: 'claude-opus-4-20250514', desc: 'Claude Opus 4', value: 'claude-opus-4-20250514' },\n { key: 'c', label: '手动输入模型名', desc: '', value: 'custom' },\n ],\n });\n if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n let modelName = modelChoice.value;\n if (modelChoice.value === 'custom') {\n modelName = await input({ prompt: '请输入模型名称', defaultValue: 'claude-sonnet-4-20250514' });\n if (!modelName) return false;\n }\n\n const config = readConfig();\n if (!config.models) config.models = {};\n if (!config.models.providers) config.models.providers = {};\n config.models.mode = 'merge';\n config.models.providers['anthropic-compatible'] = {\n baseUrl: baseUrl.replace(/\\/$/, ''),\n apiKey: apiKey,\n api: 'anthropic-messages',\n models: [{\n id: modelName,\n name: modelName,\n reasoning: false,\n input: ['text', 'image'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: 200000,\n maxTokens: 16000,\n }]\n };\n writeConfig(config);\n\n authSetApikey('anthropic-compatible', apiKey, 'anthropic-compatible:manual');\n registerAndSetModel(`anthropic-compatible/${modelName}`);\n console.log(`\\n${C.green}✅ 自定义 Anthropic API 已配置,活跃模型: anthropic-compatible/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function configureYiwanAIFanAPI() {\n resetRenderCount();\n const providerName = 'yiwanai';\n const baseUrl = 'https://api.910501.xyz/v1';\n const modelName = 'gpt-5.5';\n\n console.log(`\\n${C.bold}一万AI分享 粉丝专享 API 配置${C.reset}`);\n console.log(`${C.yellow}OpenAI 兼容模式;Base URL 和模型已内置,只需要填写 API Key。${C.reset}`);\n console.log(`${C.dim}Base URL: ${baseUrl}${C.reset}`);\n console.log(`${C.dim}Model: ${modelName}${C.reset}\\n`);\n\n const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-...' });\n if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }\n\n authSetApikey(providerName, apiKey, `${providerName}:fan`);\n registerCustomProvider(providerName, baseUrl, apiKey, modelName, modelName, 1000000, 32000);\n const config = readConfig();\n const provider = config.models?.providers?.[providerName];\n if (provider?.models?.[0]) {\n provider.models[0].reasoning = true;\n }\n writeConfig(config);\n registerAndSetModel(`${providerName}/${modelName}`);\n console.log(`\\n${C.green}✅ 一万AI分享粉丝专享 API 已配置,活跃模型: ${providerName}/${modelName}${C.reset}\\n`);\n return true;\n}\n\nasync function launchWizard() {\n resetRenderCount();\n console.log(`\\n${C.cyan}启动官方完整模型配置向导...${C.reset}\\n`);\n console.log(`${C.yellow}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${C.reset}\\n`);\n try {\n await ocCmd('configure', '--section', 'model');\n } catch (e) {\n console.log(`${C.yellow}配置向导已退出${C.reset}\\n`);\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 模型配置处理入口\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleModelConfig() {\n while (true) {\n const choice = await showModelMenu();\n if (!choice || choice.value === 'back') break;\n\n resetRenderCount();\n let configured = false;\n\n switch (choice.value) {\n case 'wizard': await launchWizard(); break;\n case 'openai': configured = await configureOpenAI(); break;\n case 'anthropic': configured = await configureAnthropic(); break;\n case 'google': configured = await configureGoogle(); break;\n case 'openrouter': configured = await configureOpenRouter(); break;\n case 'copilot': configured = await configureCopilot(); break;\n case 'xai': configured = await configureXAI(); break;\n case 'qwen': configured = await configureQwen(); break;\n case 'siliconflow': configured = await configureSiliconFlow(); break;\n case 'tencent': configured = await configureTencent(); break;\n case 'baidu': configured = await configureBaidu(); break;\n case 'zhipu': configured = await configureZhipu(); break;\n case 'ollama': configured = await configureOllama(); break;\n case 'custom': configured = await configureCustomAPI(); break;\n case 'custom-anthropic': configured = await configureCustomAnthropic(); break;\n case 'yiwanai-fan': configured = await configureYiwanAIFanAPI(); break;\n }\n\n if (configured) await askRestart();\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 设置活动模型 (对应 set_active_model)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleSetActiveModel() {\n resetRenderCount();\n console.log(`\\n${C.bold}设定当前活跃模型${C.reset}\\n`);\n\n const currentModel = getCurrentModel();\n console.log(` 当前活跃模型: ${C.green}${C.bold}${currentModel || '未设置'}${C.reset}\\n`);\n\n try {\n const result = await ocCmd('models', 'list', '--json');\n const modelsData = JSON.parse(result.stdout);\n const models = modelsData.models || [];\n\n if (models.length > 0) {\n const items = models.map((m, i) => ({\n key: String(i + 1),\n label: m.key,\n desc: m.name && m.name !== m.key ? `(${m.name})` : '',\n value: m.key,\n selected: m.key === currentModel,\n }));\n\n items.push({ label: '', disabled: true });\n items.push({ key: 'm', label: '手动输入模型 ID', desc: '', value: 'manual' });\n items.push({ key: '0', label: '返回', desc: '', value: 'back' });\n\n const choice = await select({\n title: '已配置的模型',\n showSearch: true,\n items,\n });\n\n if (!choice || choice.value === 'back') return;\n\n if (choice.value === 'manual') {\n const manualModel = await input({\n prompt: '请输入模型 ID',\n placeholder: 'openai/gpt-4o',\n defaultValue: currentModel || '',\n });\n if (manualModel) {\n registerAndSetModel(manualModel);\n console.log(`\\n${C.green}✅ 活跃模型已设为: ${manualModel}${C.reset}\\n`);\n await askRestart();\n }\n } else {\n registerAndSetModel(choice.value);\n console.log(`\\n${C.green}✅ 活跃模型已切换为: ${choice.value}${C.reset}\\n`);\n await askRestart();\n }\n } else {\n console.log(`${C.yellow}尚未配置任何模型。${C.reset}`);\n console.log(`${C.yellow}请先通过「配置 AI 模型提供商」添加模型。${C.reset}\\n`);\n\n const manualModel = await input({\n prompt: '直接输入模型 ID 设置 (留空返回)',\n defaultValue: '',\n });\n if (manualModel) {\n registerAndSetModel(manualModel);\n console.log(`\\n${C.green}✅ 活跃模型已设为: ${manualModel}${C.reset}\\n`);\n await askRestart();\n }\n }\n } catch (e) {\n console.log(`${C.red}获取模型列表失败: ${e.message}${C.reset}\\n`);\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 消息渠道处理 (对应 configure_channels 等函数)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleChannels() {\n while (true) {\n const choice = await showChannelsMenu();\n if (!choice || choice.value === 'back') break;\n\n resetRenderCount();\n\n switch (choice.value) {\n case 'qq': {\n console.log(`\\n${C.bold}QQ 机器人配置${C.reset}\\n`);\n console.log(`${C.yellow}获取 App ID 和 App Secret:${C.reset}`);\n console.log(` 1. 前往 ${C.cyan}https://q.qq.com/qqbot/openclaw/login.html${C.reset}`);\n console.log(` 2. 用手机 QQ 扫码注册/登录`);\n console.log(` 3. 创建机器人后复制 App ID 和 App Secret\\n`);\n\n const appId = await input({ prompt: '请输入 QQ 机器人 App ID', placeholder: '' });\n if (!appId) { console.log(`${C.yellow}已取消${C.reset}`); break; }\n\n const appSecret = await input({ prompt: '请输入 QQ 机器人 App Secret', placeholder: '' });\n if (!appSecret) { console.log(`${C.yellow}已取消${C.reset}`); break; }\n\n // 写入配置\n const cfg = readConfig();\n if (!cfg.channels) cfg.channels = {};\n if (!cfg.channels.qqbot) cfg.channels.qqbot = {};\n cfg.channels.qqbot.appId = appId;\n cfg.channels.qqbot.clientSecret = appSecret;\n cfg.channels.qqbot.enabled = true;\n writeConfig(cfg);\n\n console.log(`\\n${C.green}✅ QQ 机器人配置已保存${C.reset}\\n`);\n await askRestart();\n break;\n }\n case 'telegram': {\n console.log(`\\n${C.bold}Telegram Bot 配置${C.reset}\\n`);\n console.log(`${C.yellow}获取 Bot Token:${C.reset}`);\n console.log(` 1. 打开 Telegram → 搜索 ${C.cyan}@BotFather${C.reset}`);\n console.log(` 2. 发送 ${C.cyan}/newbot${C.reset} → 按提示创建`);\n console.log(` 3. 复制生成的 Token\\n`);\n\n const token = await input({ prompt: '请输入 Telegram Bot Token', placeholder: '123456:ABC...' });\n if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }\n\n // 验证格式\n if (!/^[0-9]+:[A-Za-z0-9_-]+$/.test(token)) {\n console.log(`${C.red}✗ Token 格式错误${C.reset}`);\n console.log(`${C.yellow}正确格式: 123456789:ABCdefGHIjklMNOpqr${C.reset}\\n`);\n break;\n }\n\n const cfg = readConfig();\n if (!cfg.channels) cfg.channels = {};\n if (!cfg.channels.telegram) cfg.channels.telegram = {};\n cfg.channels.telegram.botToken = token;\n writeConfig(cfg);\n\n console.log(`\\n${C.green}✅ Telegram Bot Token 已保存${C.reset}\\n`);\n await askRestart();\n break;\n }\n case 'discord': {\n console.log(`\\n${C.bold}Discord Bot 配置${C.reset}\\n`);\n console.log(`${C.yellow}获取 Bot Token:${C.cyan} https://discord.com/developers/applications${C.reset}\\n`);\n\n const token = await input({ prompt: '请输入 Discord Bot Token', placeholder: '' });\n if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }\n\n const cfg = readConfig();\n if (!cfg.channels) cfg.channels = {};\n if (!cfg.channels.discord) cfg.channels.discord = {};\n cfg.channels.discord.botToken = token;\n writeConfig(cfg);\n\n console.log(`\\n${C.green}✅ Discord Bot Token 已保存${C.reset}\\n`);\n await askRestart();\n break;\n }\n case 'feishu': {\n console.log(`\\n${C.bold}飞书 Bot 配置${C.reset}\\n`);\n console.log(`${C.cyan}即将执行飞书官方安装向导...${C.reset}\\n`);\n\n const doInstall = await confirm({ prompt: '是否开始安装?', defaultYes: true });\n if (!doInstall) break;\n\n console.log(`\\n${C.cyan}正在启动飞书安装向导...${C.reset}\\n`);\n try {\n await runCommand('npx', ['-y', '@larksuite/openclaw-lark-tools', 'install'], { env: { HOME: OC_DATA } });\n } catch (e) {\n console.log(`${C.yellow}安装向导已退出${C.reset}\\n`);\n }\n break;\n }\n case 'slack': {\n console.log(`\\n${C.bold}Slack Bot 配置${C.reset}\\n`);\n console.log(`${C.yellow}获取 Bot Token:${C.cyan} https://api.slack.com/apps${C.reset}\\n`);\n\n const token = await input({ prompt: '请输入 Slack Bot Token (xoxb-...)', placeholder: '' });\n if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }\n\n const cfg = readConfig();\n if (!cfg.channels) cfg.channels = {};\n if (!cfg.channels.slack) cfg.channels.slack = {};\n cfg.channels.slack.botToken = token;\n writeConfig(cfg);\n\n console.log(`\\n${C.green}✅ Slack Bot Token 已保存${C.reset}\\n`);\n await askRestart();\n break;\n }\n case 'whatsapp': {\n const gwToken = jsonGet('gateway.auth.token') || '';\n const gwPort = jsonGet('gateway.port') || '18789';\n console.log(`\\n${C.yellow}WhatsApp 需要通过 Web 控制台扫码配对:${C.reset}`);\n console.log(`${C.cyan}http://<你的路由器IP>:${gwPort}/?token=${gwToken}${C.reset}`);\n console.log(`打开后进入 Channels → WhatsApp 扫码即可。\\n`);\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n }\n case 'telegram-pairing': {\n console.log(`\\n${C.cyan}启动 Telegram 配对助手...${C.reset}\\n`);\n try {\n await ocCmd('models', 'auth', 'login-telegram-bot');\n } catch (e) {}\n break;\n }\n case 'wizard': {\n console.log(`\\n${C.cyan}启动官方渠道配置向导...${C.reset}\\n`);\n try {\n await ocCmd('configure', '--section', 'channels');\n } catch (e) {}\n break;\n }\n }\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 健康检查 (对应 health_check)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleHealthCheck() {\n resetRenderCount();\n console.log(`\\n${C.bold}健康检查${C.reset}\\n`);\n\n // 验证配置文件\n console.log(`${C.cyan}验证配置文件格式...${C.reset}`);\n try {\n const result = await ocCmd('config', 'validate', '--json');\n const validateData = JSON.parse(result.stdout);\n if (validateData.valid) {\n console.log(`${C.green}✅ 配置文件格式有效${C.reset}\\n`);\n } else if (validateData.errors && validateData.errors.length > 0) {\n console.log(`${C.red}❌ 配置文件存在错误:${C.reset}`);\n validateData.errors.forEach(e => console.log(` ${C.yellow}• ${e.message}${C.reset}`));\n console.log('');\n }\n } catch (e) {\n console.log(`${C.yellow}⚠️ 无法验证配置文件${C.reset}\\n`);\n }\n\n // 检查端口\n console.log(`${C.cyan}检查服务状态...${C.reset}`);\n try {\n await runCommand('/etc/init.d/openclaw', ['status_service']);\n } catch (e) {}\n\n console.log(`\\n${C.cyan}提示: 查看详细日志请运行 logread -e openclaw${C.reset}`);\n await input({ prompt: '按回车继续', defaultValue: '' });\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 显示当前配置 (对应 show_current_config)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleShowConfig() {\n resetRenderCount();\n\n const gwPort = jsonGet('gateway.port') || '18789';\n const gwBind = jsonGet('gateway.bind') || 'lan';\n const gwMode = jsonGet('gateway.mode') || 'local';\n const currentModel = getCurrentModel();\n\n console.log(`\\n${C.green}┌──────────────────────────────────────────────────────────┐${C.reset}`);\n console.log(`${C.green}│${C.reset} 📋 ${C.bold}当前配置概览${C.reset}`);\n console.log(`${C.green}├──────────────────────────────────────────────────────────┤${C.reset}`);\n console.log(`${C.green}│${C.reset} 网关端口 ............ ${C.cyan}${gwPort}${C.reset}`);\n console.log(`${C.green}│${C.reset} 绑定模式 ............ ${C.cyan}${gwBind}${C.reset}`);\n console.log(`${C.green}│${C.reset} 运行模式 ............ ${C.cyan}${gwMode}${C.reset}`);\n console.log(`${C.green}│${C.reset} 活跃模型 ............ ${currentModel ? C.cyan + currentModel : C.yellow + '未配置'}${C.reset}`);\n\n // 渠道状态\n console.log(`${C.green}├──────────────────────────────────────────────────────────┤${C.reset}`);\n console.log(`${C.green}│${C.reset} ${C.bold}渠道配置状态${C.reset}`);\n\n const tgToken = jsonGet('channels.telegram.botToken');\n const dcToken = jsonGet('channels.discord.botToken');\n const fsAppId = jsonGet('channels.feishu.appId');\n const skToken = jsonGet('channels.slack.botToken');\n const qqAppId = jsonGet('channels.qqbot.appId');\n\n if (qqAppId) {\n console.log(`${C.green}│${C.reset} QQ (qqbot) ......... ${C.green}✅ 已配置${C.reset} (AppID: ${qqAppId.slice(0, 8)}...)`);\n } else {\n console.log(`${C.green}│${C.reset} QQ (qqbot) ......... ${C.yellow}❌ 未配置${C.reset}`);\n }\n if (tgToken) {\n console.log(`${C.green}│${C.reset} Telegram ........... ${C.green}✅ 已配置${C.reset} (${tgToken.slice(0, 12)}...)`);\n } else {\n console.log(`${C.green}│${C.reset} Telegram ........... ${C.yellow}❌ 未配置${C.reset}`);\n }\n if (dcToken) {\n console.log(`${C.green}│${C.reset} Discord ............ ${C.green}✅ 已配置${C.reset}`);\n } else {\n console.log(`${C.green}│${C.reset} Discord ............ ${C.yellow}❌ 未配置${C.reset}`);\n }\n if (fsAppId) {\n console.log(`${C.green}│${C.reset} 飞书 ............... ${C.green}✅ 已配置${C.reset} (AppID: ${fsAppId.slice(0, 6)}...)`);\n } else {\n console.log(`${C.green}│${C.reset} 飞书 ............... ${C.yellow}❌ 未配置${C.reset}`);\n }\n if (skToken) {\n console.log(`${C.green}│${C.reset} Slack .............. ${C.green}✅ 已配置${C.reset}`);\n } else {\n console.log(`${C.green}│${C.reset} Slack .............. ${C.yellow}❌ 未配置${C.reset}`);\n }\n\n console.log(`${C.green}└──────────────────────────────────────────────────────────┘${C.reset}\\n`);\n\n await input({ prompt: '按回车继续', defaultValue: '' });\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 高级配置处理 (对应 advanced_menu)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleAdvancedConfig() {\n while (true) {\n const choice = await showAdvancedMenu();\n if (!choice || choice.value === 'back') break;\n\n resetRenderCount();\n\n switch (choice.value) {\n case 'port': {\n const newPort = await input({\n prompt: '请输入 Gateway 端口',\n defaultValue: String(jsonGet('gateway.port') || '18789'),\n });\n if (newPort) {\n jsonSet('gateway.port', parseInt(newPort));\n try { execSync(`uci set openclaw.main.port=\"${newPort}\" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}\n console.log(`\\n${C.green}✅ 端口已设置为 ${newPort}${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'bind': {\n const bindChoice = await select({\n title: '绑定地址选项',\n showSearch: false,\n items: [\n { key: '1', label: 'lan', desc: '仅 LAN 接口 (推荐)', value: 'lan' },\n { key: '2', label: 'loopback', desc: '仅本机访问', value: 'loopback' },\n { key: '3', label: 'all', desc: '所有接口 (0.0.0.0)', value: 'all' },\n ],\n });\n if (bindChoice) {\n jsonSet('gateway.bind', bindChoice.value);\n try { execSync(`uci set openclaw.main.bind=\"${bindChoice.value}\" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}\n console.log(`\\n${C.green}✅ 绑定地址已设置为 ${bindChoice.value}${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'mode': {\n const modeChoice = await select({\n title: '运行模式选项',\n showSearch: false,\n items: [\n { key: '1', label: 'local', desc: '本地模式 (推荐)', value: 'local' },\n { key: '2', label: 'remote', desc: '远程模式', value: 'remote' },\n ],\n });\n if (modeChoice) {\n jsonSet('gateway.mode', modeChoice.value);\n console.log(`\\n${C.green}✅ 运行模式已设置为 ${modeChoice.value}${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'loglevel': {\n const levelChoice = await select({\n title: '日志级别选项',\n showSearch: false,\n items: [\n { key: '1', label: 'debug', desc: '详细调试', value: 'debug' },\n { key: '2', label: 'info', desc: '常规信息', value: 'info' },\n { key: '3', label: 'warn', desc: '警告及以上', value: 'warn' },\n { key: '4', label: 'error', desc: '仅错误', value: 'error' },\n ],\n });\n if (levelChoice) {\n jsonSet('gateway.logLevel', levelChoice.value);\n console.log(`\\n${C.green}✅ 日志级别已设置为 ${levelChoice.value}${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'acp': {\n const acpChoice = await select({\n title: 'ACP Dispatch 选项',\n showSearch: false,\n items: [\n { key: '1', label: 'false', desc: '禁用 (推荐路由器使用)', value: 'false' },\n { key: '2', label: 'true', desc: '启用 (可能占用大量内存)', value: 'true' },\n ],\n });\n if (acpChoice) {\n jsonSet('acp.dispatch.enabled', acpChoice.value === 'true');\n console.log(`\\n${C.green}✅ ACP Dispatch 已设置为 ${acpChoice.value}${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'wizard':\n console.log(`\\n${C.cyan}启动官方配置向导...${C.reset}\\n`);\n try {\n await ocCmd('configure');\n } catch (e) {}\n break;\n case 'view-json':\n console.log(`\\n${C.cyan}配置文件路径: ${CONFIG_FILE}${C.reset}\\n`);\n try {\n const content = fs.readFileSync(CONFIG_FILE, 'utf8');\n console.log(content);\n } catch (e) {\n console.log(`${C.red}无法读取配置文件${C.reset}`);\n }\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n case 'edit':\n console.log(`\\n${C.yellow}请在 SSH 终端中手动编辑配置文件:${C.reset}`);\n console.log(` vi ${CONFIG_FILE}\\n`);\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n case 'backup':\n await handleBackup();\n break;\n case 'import': {\n const importPath = await input({\n prompt: '请输入备份文件路径',\n defaultValue: '',\n });\n if (importPath && fs.existsSync(importPath)) {\n try {\n fs.copyFileSync(importPath, CONFIG_FILE);\n console.log(`\\n${C.green}✅ 配置已导入${C.reset}\\n`);\n await askRestart();\n } catch (e) {\n console.log(`${C.red}导入失败: ${e.message}${C.reset}\\n`);\n }\n } else {\n console.log(`${C.yellow}文件不存在${C.reset}\\n`);\n }\n break;\n }\n }\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 重置配置处理 (对应 reset_to_defaults)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleReset() {\n while (true) {\n const choice = await showResetMenu();\n if (!choice || choice.value === 'back') break;\n\n resetRenderCount();\n\n switch (choice.value) {\n case 'gateway': {\n console.log(`\\n${C.yellow}将重置: 网关端口→18789, 绑定→lan, 模式→local${C.reset}`);\n console.log(`${C.yellow}保留: 认证令牌、模型配置、消息渠道${C.reset}\\n`);\n const ok = await confirm({ prompt: '确认恢复网关默认设置?', defaultYes: false });\n if (ok) {\n jsonSet('gateway.port', 18789);\n jsonSet('gateway.bind', 'lan');\n jsonSet('gateway.mode', 'local');\n jsonSet('gateway.controlUi.allowInsecureAuth', true);\n jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);\n jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);\n jsonSet('gateway.tailscale.mode', 'off');\n console.log(`\\n${C.green}✅ 网关设置已恢复默认${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'models': {\n console.log(`\\n${C.red}⚠️ 将清除: 所有模型配置、API Key、活跃模型设置${C.reset}\\n`);\n const ok = await confirm({ prompt: '确认清除所有模型配置?', defaultYes: false });\n if (ok) {\n const cfg = readConfig();\n delete cfg.models;\n if (cfg.agents?.defaults?.model) delete cfg.agents.defaults.model;\n if (cfg.agents?.defaults?.models) delete cfg.agents.defaults.models;\n writeConfig(cfg);\n // 清除 auth-profiles.json\n const authFile = `${OC_STATE_DIR}/agents/main/agent/auth-profiles.json`;\n try {\n fs.writeFileSync(authFile, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }, null, 2));\n } catch {}\n console.log(`\\n${C.green}✅ 模型配置已清除${C.reset}`);\n console.log(`${C.yellow}请通过菜单 [1] 重新配置 AI 模型${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'channels': {\n console.log(`\\n${C.red}⚠️ 将清除: 所有消息渠道配置 (Telegram/Discord/飞书等)${C.reset}\\n`);\n const ok = await confirm({ prompt: '确认清除所有渠道配置?', defaultYes: false });\n if (ok) {\n const cfg = readConfig();\n delete cfg.channels;\n writeConfig(cfg);\n console.log(`\\n${C.green}✅ 渠道配置已清除${C.reset}\\n`);\n await askRestart();\n }\n break;\n }\n case 'full': {\n console.log(`\\n${C.red}╔══════════════════════════════════════════════════════╗${C.reset}`);\n console.log(`${C.red}║ ⚠️ 完全恢复出厂设置 ║${C.reset}`);\n console.log(`${C.red}║ 此操作将删除所有配置并重新初始化 ║${C.reset}`);\n console.log(`${C.red}╚══════════════════════════════════════════════════════╝${C.reset}\\n`);\n\n const confirmStr = await input({ prompt: '输入 RESET 确认恢复出厂设置', defaultValue: '' });\n if (confirmStr !== 'RESET') {\n console.log(`${C.cyan}已取消${C.reset}\\n`);\n break;\n }\n\n // 执行恢复\n console.log(`\\n${C.cyan}[1/5] 停止 Gateway...${C.reset}`);\n try { await runCommand('/etc/init.d/openclaw', ['stop']); } catch {}\n\n console.log(`${C.cyan}[2/5] 备份当前配置...${C.reset}`);\n const backupDir = `${OC_STATE_DIR}/backups`;\n const backupTs = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);\n try {\n if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true }); try { execSync(`chown openclaw:openclaw \"${backupDir}\"`, { stdio: \"ignore\" }); } catch {}\n fs.copyFileSync(CONFIG_FILE, `${backupDir}/openclaw_${backupTs}.json`);\n console.log(`${C.green} 备份已保存: backups/openclaw_${backupTs}.json${C.reset}`);\n } catch {}\n\n console.log(`${C.cyan}[3/5] 重置配置...${C.reset}`);\n writeConfig({});\n\n console.log(`${C.cyan}[4/5] 重新初始化...${C.reset}`);\n // 生成新 token\n const crypto = require('crypto');\n const newToken = crypto.randomBytes(24).toString('hex');\n\n console.log(`${C.cyan}[5/5] 应用 OpenWrt 适配配置...${C.reset}`);\n jsonSet('gateway.port', 18789);\n jsonSet('gateway.bind', 'lan');\n jsonSet('gateway.mode', 'local');\n jsonSet('gateway.auth.mode', 'token');\n jsonSet('gateway.auth.token', newToken);\n jsonSet('gateway.controlUi.allowInsecureAuth', true);\n jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);\n jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);\n jsonSet('gateway.tailscale.mode', 'off');\n jsonSet('acp.dispatch.enabled', false);\n jsonSet('tools.profile', 'coding');\n\n try { execSync(`uci set openclaw.main.token=\"${newToken}\" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}\n\n console.log(`\\n${C.green}✅ 出厂设置已恢复!${C.reset}\\n`);\n console.log(`${C.cyan}新认证令牌: ${newToken}${C.reset}\\n`);\n\n await restartGateway();\n break;\n }\n }\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 备份/还原处理 (对应 backup_restore_menu)\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function handleBackup() {\n while (true) {\n const choice = await showBackupMenu();\n if (!choice || choice.value === 'back') break;\n\n resetRenderCount();\n const backupDir = `${OC_STATE_DIR}/backups`;\n try { if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true }); try { execSync(`chown openclaw:openclaw \"${backupDir}\"`, { stdio: \"ignore\" }); } catch {} } catch {}\n\n switch (choice.value) {\n case 'create-config': {\n console.log(`\\n${C.cyan}正在创建配置备份...${C.reset}`);\n try {\n await ocCmd('backup', 'create', '--only-config', '--no-include-workspace');\n console.log(`${C.green}✅ 配置备份已创建${C.reset}\\n`);\n } catch (e) {\n console.log(`${C.yellow}⚠️ 备份功能需要 OpenClaw v2026.3.8+${C.reset}\\n`);\n }\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n }\n case 'create-full': {\n console.log(`\\n${C.cyan}正在创建完整备份...${C.reset}`);\n try {\n await ocCmd('backup', 'create', '--no-include-workspace');\n console.log(`${C.green}✅ 完整备份已创建${C.reset}\\n`);\n } catch (e) {\n console.log(`${C.yellow}⚠️ 备份失败${C.reset}\\n`);\n }\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n }\n case 'verify': {\n const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.tar.gz')).sort().reverse();\n if (files.length === 0) {\n console.log(`${C.yellow}未找到备份文件${C.reset}\\n`);\n } else {\n const latest = `${backupDir}/${files[0]}`;\n console.log(`${C.cyan}验证备份: ${latest}${C.reset}`);\n try {\n await ocCmd('backup', 'verify', latest);\n } catch {}\n }\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n }\n case 'list': {\n const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.tar.gz') || f.endsWith('.json')).sort().reverse();\n if (files.length === 0) {\n console.log(`${C.yellow}暂无备份文件${C.reset}\\n`);\n } else {\n console.log(`\\n${C.bold}备份文件列表:${C.reset}`);\n files.slice(0, 10).forEach(f => {\n const stat = fs.statSync(`${backupDir}/${f}`);\n console.log(` ${C.dim}${f} (${(stat.size / 1024).toFixed(1)} KB)${C.reset}`);\n });\n console.log(`${C.dim}\\n备份目录: ${backupDir}${C.reset}\\n`);\n }\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n }\n case 'restore': {\n const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.json')).sort().reverse();\n if (files.length === 0) {\n console.log(`${C.yellow}未找到配置备份文件${C.reset}\\n`);\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n }\n\n const fileChoice = await select({\n title: '选择备份文件',\n showSearch: false,\n items: files.slice(0, 10).map((f, i) => ({\n key: String(i + 1),\n label: f,\n desc: '',\n value: f,\n })),\n });\n\n if (!fileChoice) break;\n\n const ok = await confirm({ prompt: '确认恢复此备份?', defaultYes: false });\n if (ok) {\n try {\n fs.copyFileSync(`${backupDir}/${fileChoice.value}`, CONFIG_FILE);\n console.log(`\\n${C.green}✅ 配置已恢复${C.reset}\\n`);\n await askRestart();\n } catch (e) {\n console.log(`${C.red}恢复失败: ${e.message}${C.reset}\\n`);\n }\n }\n break;\n }\n }\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// 主函数\n// ═══════════════════════════════════════════════════════════════════════════\n\nasync function main() {\n const command = process.argv[2];\n\n if (command === 'model') {\n await handleModelConfig();\n return;\n }\n\n if (command === 'status') {\n await runCommand('/etc/init.d/openclaw', ['status_service']);\n return;\n }\n\n if (command === 'restart') {\n const ok = await confirm({ prompt: '确认重启 OpenClaw 服务?' });\n if (ok) await runCommand('/etc/init.d/openclaw', ['restart']);\n return;\n }\n\n // 交互式主菜单\n while (true) {\n const choice = await showMainMenu();\n\n if (!choice || choice.value === 'quit') {\n console.log(`\\n${C.green}再见!${C.reset}\\n`);\n break;\n }\n\n switch (choice.value) {\n case 'model':\n await handleModelConfig();\n break;\n case 'set-active-model':\n await handleSetActiveModel();\n break;\n case 'channels':\n await handleChannels();\n break;\n case 'health':\n await handleHealthCheck();\n break;\n case 'logs':\n resetRenderCount();\n console.log(`\\n${C.cyan}=== OpenClaw 日志 ===${C.reset}\\n`);\n await runCommand('logread', ['-e', 'openclaw']);\n console.log('');\n await input({ prompt: '按回车继续', defaultValue: '' });\n break;\n case 'restart':\n resetRenderCount();\n await restartGateway();\n break;\n case 'advanced':\n await handleAdvancedConfig();\n break;\n case 'reset':\n await handleReset();\n break;\n case 'show-config':\n await handleShowConfig();\n break;\n case 'backup':\n await handleBackup();\n break;\n }\n }\n}\n\nmain().catch(e => {\n console.error(`${C.red}错误:${C.reset}`, e.message);\n process.exit(1);\n});\n"}}}} |