Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/10000ge10000_luci-app-openclaw.json

1 line
280 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}