mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-30 22:11:26 +08:00
2034 lines
91 KiB
JavaScript
2034 lines
91 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* ============================================================================
|
||
* OpenClaw 配置工具 — 交互式菜单前端
|
||
* ============================================================================
|
||
*
|
||
* 功能特性:
|
||
* - 方向键 (↑↓) 导航菜单选项 (使用 oc-menu-engine 引擎)
|
||
* - 回车/空格 确认选择
|
||
* - 实时搜索过滤
|
||
* - 与 oc-config.sh 业务逻辑完全一致
|
||
* - 纯 Node.js 实现,零外部依赖
|
||
* - 完全兼容 xterm.js / Web PTY 环境
|
||
*/
|
||
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const { spawn, execSync } = require('child_process');
|
||
const menu = require('./oc-menu-engine');
|
||
const { C, select, input, confirm, spinner, resetRenderCount } = menu;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 配置路径 (与 oc-config.sh 保持一致)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
const OC_BASE_PATH = process.env.OC_BASE_PATH || '/opt';
|
||
const OC_INSTALL_PATH = process.env.OC_INSTALL_PATH || `${OC_BASE_PATH}/openclaw`;
|
||
const NODE_BASE = process.env.NODE_BASE || `${OC_INSTALL_PATH}/node`;
|
||
const OC_GLOBAL = process.env.OC_GLOBAL || `${OC_INSTALL_PATH}/global`;
|
||
const OC_DATA = process.env.OC_DATA || `${OC_INSTALL_PATH}/data`;
|
||
const OC_STATE_DIR = process.env.OPENCLAW_STATE_DIR || `${OC_DATA}/.openclaw`;
|
||
const CONFIG_FILE = process.env.OPENCLAW_CONFIG_PATH || `${OC_STATE_DIR}/openclaw.json`;
|
||
const NODE_BIN = `${NODE_BASE}/bin/node`;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 辅助函数 (与 oc-config.sh 逻辑对应)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* 执行命令并返回结果
|
||
*/
|
||
function runCommand(cmd, args = [], options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const child = spawn(cmd, args, {
|
||
stdio: ['inherit', 'pipe', 'pipe'],
|
||
shell: true,
|
||
env: { ...process.env, ...options.env },
|
||
});
|
||
|
||
let stdout = '';
|
||
let stderr = '';
|
||
|
||
child.stdout.on('data', (data) => {
|
||
stdout += data;
|
||
process.stdout.write(data);
|
||
});
|
||
|
||
child.stderr.on('data', (data) => {
|
||
stderr += data;
|
||
process.stderr.write(data);
|
||
});
|
||
|
||
child.on('close', (code) => {
|
||
if (code === 0) {
|
||
resolve({ stdout, stderr, code });
|
||
} else {
|
||
reject(new Error(`Command failed with code ${code}: ${stderr}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 读取 JSON 配置文件
|
||
*/
|
||
function readConfig() {
|
||
try {
|
||
if (fs.existsSync(CONFIG_FILE)) {
|
||
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||
}
|
||
} catch (e) {
|
||
console.error(`${C.red}读取配置失败:${C.reset} ${e.message}`);
|
||
}
|
||
return {};
|
||
}
|
||
|
||
/**
|
||
* 写入 JSON 配置文件
|
||
*/
|
||
function writeConfig(config) {
|
||
const dir = path.dirname(CONFIG_FILE);
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true }); try { execSync(`chown openclaw:openclaw "${dir}"`, { stdio: "ignore" }); } catch {}
|
||
}
|
||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||
try {
|
||
execSync(`chown openclaw:openclaw "${CONFIG_FILE}"`, { stdio: 'ignore' });
|
||
execSync(`find "${OC_STATE_DIR}" -user root ! -path "*/extensions*" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });
|
||
} catch {}
|
||
}
|
||
|
||
/**
|
||
* 获取 JSON 配置值 (对应 json_get)
|
||
*/
|
||
function jsonGet(keyPath) {
|
||
const config = readConfig();
|
||
const keys = keyPath.split('.');
|
||
let value = config;
|
||
for (const key of keys) {
|
||
if (value && typeof value === 'object' && key in value) {
|
||
value = value[key];
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* 设置 JSON 配置值 (对应 json_set)
|
||
*/
|
||
function jsonSet(keyPath, value) {
|
||
const config = readConfig();
|
||
const keys = keyPath.split('.');
|
||
let obj = config;
|
||
for (let i = 0; i < keys.length - 1; i++) {
|
||
if (!obj[keys[i]]) obj[keys[i]] = {};
|
||
obj = obj[keys[i]];
|
||
}
|
||
obj[keys[keys.length - 1]] = value;
|
||
writeConfig(config);
|
||
}
|
||
|
||
/**
|
||
* 获取当前活跃模型
|
||
*/
|
||
function getCurrentModel() {
|
||
const primary = jsonGet('agents.defaults.model.primary');
|
||
if (primary) return primary;
|
||
const config = readConfig();
|
||
if (config.models?.defaultModel) return config.models.defaultModel;
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 查找 OpenClaw 入口文件
|
||
*/
|
||
function findOpenClawEntry() {
|
||
const searchPaths = [
|
||
`${OC_GLOBAL}/lib/node_modules/openclaw/openclaw.mjs`,
|
||
`${OC_GLOBAL}/lib/node_modules/openclaw/dist/cli.js`,
|
||
`${OC_GLOBAL}/node_modules/openclaw/openclaw.mjs`,
|
||
`${OC_GLOBAL}/node_modules/openclaw/dist/cli.js`,
|
||
];
|
||
for (const p of searchPaths) {
|
||
if (fs.existsSync(p)) return p;
|
||
}
|
||
try {
|
||
const dirs = fs.readdirSync(OC_GLOBAL);
|
||
for (const dir of dirs) {
|
||
const p = `${OC_GLOBAL}/${dir}/node_modules/openclaw/openclaw.mjs`;
|
||
if (fs.existsSync(p)) return p;
|
||
}
|
||
} catch {}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 执行 OpenClaw CLI 命令 (对应 oc_cmd)
|
||
*/
|
||
async function ocCmd(...args) {
|
||
const ocEntry = findOpenClawEntry();
|
||
if (!ocEntry) throw new Error('OpenClaw 未安装');
|
||
return runCommand(NODE_BIN, [ocEntry, ...args], {
|
||
env: {
|
||
OPENCLAW_HOME: OC_DATA,
|
||
OPENCLAW_CONFIG_PATH: CONFIG_FILE,
|
||
OPENCLAW_STATE_DIR: OC_STATE_DIR,
|
||
HOME: OC_DATA,
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 注册模型并设为默认 (对应 register_and_set_model)
|
||
*/
|
||
function registerAndSetModel(modelId) {
|
||
const config = readConfig();
|
||
if (!config.agents) config.agents = {};
|
||
if (!config.agents.defaults) config.agents.defaults = {};
|
||
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
||
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
||
config.agents.defaults.models[modelId] = {};
|
||
config.agents.defaults.model.primary = modelId;
|
||
writeConfig(config);
|
||
}
|
||
|
||
/**
|
||
* 写入 API Key 到 auth-profiles.json (对应 auth_set_apikey)
|
||
*/
|
||
function authSetApikey(provider, apiKey, profileId) {
|
||
const authDir = `${OC_STATE_DIR}/agents/main/agent`;
|
||
const authFile = `${authDir}/auth-profiles.json`;
|
||
try {
|
||
fs.mkdirSync(authDir, { recursive: true }); try { execSync(`chown openclaw:openclaw "${authDir}"`, { stdio: "ignore" }); } catch {}
|
||
} catch {}
|
||
|
||
let authData = { version: 1, profiles: {}, usageStats: {} };
|
||
try {
|
||
if (fs.existsSync(authFile)) {
|
||
authData = JSON.parse(fs.readFileSync(authFile, 'utf8'));
|
||
}
|
||
} catch {}
|
||
|
||
if (!authData.profiles) authData.profiles = {};
|
||
authData.profiles[profileId || `${provider}:manual`] = {
|
||
type: 'api_key',
|
||
provider: provider,
|
||
key: apiKey,
|
||
};
|
||
|
||
fs.writeFileSync(authFile, JSON.stringify(authData, null, 2));
|
||
try {
|
||
execSync(`chown openclaw:openclaw "${authFile}"`, { stdio: 'ignore' });
|
||
execSync(`find "${OC_STATE_DIR}" -user root ! -path "*/extensions*" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });
|
||
} catch {}
|
||
}
|
||
|
||
/**
|
||
* 注册自定义提供商 (对应 register_custom_provider)
|
||
*/
|
||
function registerCustomProvider(providerName, baseUrl, apiKey, modelId, modelDisplay, ctxWindow, maxTok) {
|
||
const config = readConfig();
|
||
if (!config.models) config.models = {};
|
||
if (!config.models.providers) config.models.providers = {};
|
||
config.models.mode = 'merge';
|
||
config.models.providers[providerName] = {
|
||
baseUrl: baseUrl,
|
||
apiKey: apiKey,
|
||
api: 'openai-completions',
|
||
models: [{
|
||
id: modelId,
|
||
name: modelDisplay || modelId,
|
||
reasoning: false,
|
||
input: ['text', 'image'],
|
||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||
contextWindow: parseInt(ctxWindow) || 128000,
|
||
maxTokens: parseInt(maxTok) || 32000,
|
||
}]
|
||
};
|
||
writeConfig(config);
|
||
}
|
||
|
||
/**
|
||
* 注册 Coding Plan 提供商 (对应 register_codingplan_provider)
|
||
*/
|
||
function registerCodingPlanProvider(apiKey) {
|
||
const config = readConfig();
|
||
if (!config.models) config.models = {};
|
||
if (!config.models.providers) config.models.providers = {};
|
||
config.models.mode = 'merge';
|
||
config.models.providers['bailian'] = {
|
||
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
|
||
apiKey: apiKey,
|
||
api: 'openai-completions',
|
||
models: [
|
||
{ id: 'qwen3.5-plus', name: 'qwen3.5-plus', reasoning: false, input: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 65536 },
|
||
{ id: 'qwen3-coder-plus', name: 'qwen3-coder-plus', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 65536 },
|
||
{ id: 'qwen3-coder-next', name: 'qwen3-coder-next', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 65536 },
|
||
{ id: 'qwen3-max-2026-01-23', name: 'qwen3-max-2026-01-23', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 65536 },
|
||
{ id: 'MiniMax-M2.5', name: 'MiniMax-M2.5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204800, maxTokens: 131072 },
|
||
{ id: 'glm-5', name: 'glm-5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 16384 },
|
||
{ id: 'glm-4.7', name: 'glm-4.7', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 16384 },
|
||
{ id: 'kimi-k2.5', name: 'kimi-k2.5', reasoning: false, input: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 32768 },
|
||
]
|
||
};
|
||
|
||
if (!config.agents) config.agents = {};
|
||
if (!config.agents.defaults) config.agents.defaults = {};
|
||
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
||
['qwen3.5-plus', 'qwen3-coder-plus', 'qwen3-coder-next', 'qwen3-max-2026-01-23', 'MiniMax-M2.5', 'glm-5', 'glm-4.7', 'kimi-k2.5'].forEach(m => {
|
||
config.agents.defaults.models[`bailian/${m}`] = {};
|
||
});
|
||
|
||
writeConfig(config);
|
||
}
|
||
|
||
/**
|
||
* 注册腾讯云 Coding Plan 提供商 (对应 register_lkeap_codingplan_provider)
|
||
*/
|
||
function registerLkeapCodingPlanProvider(apiKey) {
|
||
const config = readConfig();
|
||
if (!config.models) config.models = {};
|
||
if (!config.models.providers) config.models.providers = {};
|
||
config.models.mode = 'merge';
|
||
config.models.providers['lkeap'] = {
|
||
baseUrl: 'https://api.lkeap.cloud.tencent.com/coding/v3',
|
||
apiKey: apiKey,
|
||
api: 'openai-completions',
|
||
models: [
|
||
{ id: 'tc-code-latest', name: 'Auto (智能匹配最优模型)', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
|
||
{ id: 'hunyuan-2.0-instruct', name: 'Tencent HY 2.0 Instruct', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 16000 },
|
||
{ id: 'hunyuan-2.0-thinking', name: 'Tencent HY 2.0 Think', reasoning: true, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 64000 },
|
||
{ id: 'hunyuan-t1', name: 'Hunyuan-T1', reasoning: true, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 64000 },
|
||
{ id: 'hunyuan-turbos', name: 'Hunyuan-TurboS', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 16000 },
|
||
{ id: 'minimax-m2.5', name: 'MiniMax-M2.5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 204800, maxTokens: 131072 },
|
||
{ id: 'kimi-k2.5', name: 'Kimi-K2.5', reasoning: false, input: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 32768 },
|
||
{ id: 'glm-5', name: 'GLM-5', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 202752, maxTokens: 8192 },
|
||
]
|
||
};
|
||
|
||
if (!config.agents) config.agents = {};
|
||
if (!config.agents.defaults) config.agents.defaults = {};
|
||
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
||
['tc-code-latest', 'hunyuan-2.0-instruct', 'hunyuan-2.0-thinking', 'hunyuan-t1', 'hunyuan-turbos', 'minimax-m2.5', 'kimi-k2.5', 'glm-5'].forEach(m => {
|
||
config.agents.defaults.models[`lkeap/${m}`] = {};
|
||
});
|
||
|
||
writeConfig(config);
|
||
}
|
||
|
||
/**
|
||
* 重启 Gateway
|
||
*/
|
||
async function restartGateway() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.yellow}正在重启 Gateway...${C.reset}`);
|
||
|
||
try {
|
||
await runCommand('/etc/init.d/openclaw', ['restart_gateway']);
|
||
} catch {}
|
||
|
||
const spin = spinner({ text: 'Gateway 启动中,请稍候 (约 15-30 秒)...' });
|
||
spin.start();
|
||
|
||
const gwPort = jsonGet('gateway.port') || '18789';
|
||
let waited = 0;
|
||
const maxWait = 30;
|
||
|
||
while (waited < maxWait) {
|
||
await new Promise(r => setTimeout(r, 3000));
|
||
waited += 3;
|
||
try {
|
||
// 兼容 OpenWrt: 优先 ss,回退 netstat
|
||
let stdout = '';
|
||
try {
|
||
stdout = execSync('ss -tulnp 2>/dev/null', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||
} catch {
|
||
// ss 不存在,使用 netstat
|
||
try {
|
||
stdout = execSync('netstat -tulnp 2>/dev/null', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||
} catch {}
|
||
}
|
||
if (stdout && stdout.includes(`:${gwPort} `)) {
|
||
spin.succeed(`Gateway 已重启成功 (${waited}秒)`);
|
||
return;
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
spin.stop(`${C.yellow}Gateway 仍在启动中,请稍后确认${C.reset}`);
|
||
console.log(`${C.cyan} 查看日志: logread -e openclaw${C.reset}\n`);
|
||
}
|
||
|
||
/**
|
||
* 询问是否重启
|
||
*/
|
||
async function askRestart() {
|
||
const ok = await confirm({ prompt: '是否立即重启服务以应用配置?', defaultYes: true });
|
||
if (ok) {
|
||
await restartGateway();
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 主菜单 (对应 main_menu)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function showMainMenu() {
|
||
const currentModel = getCurrentModel();
|
||
const statusLine = currentModel
|
||
? `${C.green}当前模型: ${currentModel}${C.reset}`
|
||
: `${C.yellow}未配置模型${C.reset}`;
|
||
|
||
const result = await select({
|
||
title: 'OpenClaw AI Gateway — OpenWrt 配置管理',
|
||
header: statusLine,
|
||
showSearch: false,
|
||
items: [
|
||
{ label: `${C.dim}━━━ AI 模型配置 ━━━${C.reset}`, disabled: true },
|
||
{ key: '1', label: '配置 AI 模型和提供商', desc: '', value: 'model' },
|
||
{ key: '2', label: '设置活动模型', desc: '', value: 'set-active-model' },
|
||
|
||
{ label: `${C.dim}━━━ 消息渠道 ━━━${C.reset}`, disabled: true },
|
||
{ key: '3', label: '配置消息渠道', desc: '电报/QQ/飞书', value: 'channels' },
|
||
|
||
{ label: `${C.dim}━━━ 系统管理 ━━━${C.reset}`, disabled: true },
|
||
{ key: '4', label: '健康检查与状态', desc: '', value: 'health' },
|
||
{ key: '5', label: '查看日志', desc: '', value: 'logs' },
|
||
{ key: '6', label: '重启 Gateway', desc: '', value: 'restart' },
|
||
|
||
{ label: `${C.dim}━━━ 高级选项 ━━━${C.reset}`, disabled: true },
|
||
{ key: '7', label: '高级配置', desc: '', value: 'advanced' },
|
||
{ key: '8', label: '重置配置', desc: '', value: 'reset' },
|
||
{ key: '9', label: '显示当前配置概览', desc: '', value: 'show-config' },
|
||
{ key: '10', label: '备份/还原配置', desc: '', value: 'backup' },
|
||
|
||
{ label: '', disabled: true },
|
||
{ key: '0', label: '退出', desc: '', value: 'quit' },
|
||
],
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 模型配置菜单 (对应 configure_model)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function showModelMenu() {
|
||
const result = await select({
|
||
title: '配置 AI 模型和提供商',
|
||
showSearch: true,
|
||
items: [
|
||
{ label: `${C.green}${C.bold}── 推荐 ──${C.reset}`, disabled: true },
|
||
{ key: 'w', label: '官方完整模型配置向导', desc: `${C.green}(推荐,支持所有提供商)${C.reset}`, value: 'wizard' },
|
||
|
||
{ label: `${C.bold}── 国外模型提供商 ──${C.reset}`, disabled: true },
|
||
{ key: 'a', label: 'OpenAI', desc: 'GPT-5.2, GPT-5 mini, GPT-4.1', value: 'openai' },
|
||
{ key: 'b', label: 'Anthropic', desc: 'Claude Sonnet 4, Opus 4, Haiku', value: 'anthropic' },
|
||
{ key: 'c', label: 'Google Gemini', desc: 'Gemini 2.5 Pro/Flash, Gemini 3', value: 'google' },
|
||
{ key: 'd', label: 'OpenRouter', desc: '聚合多家模型', value: 'openrouter' },
|
||
{ key: 'e', label: 'GitHub Copilot', desc: '需要 Copilot 订阅', value: 'copilot' },
|
||
{ key: 'f', label: 'xAI Grok', desc: 'Grok-4/3', value: 'xai' },
|
||
|
||
{ label: `${C.bold}── 国内模型提供商 ──${C.reset}`, disabled: true },
|
||
{ key: 'g', label: '阿里云通义千问 Qwen', desc: 'Portal/API/Coding Plan', value: 'qwen' },
|
||
{ key: 'h', label: '硅基流动 SiliconFlow', desc: '', value: 'siliconflow' },
|
||
{ key: 'i', label: '腾讯云 Coding Plan', desc: 'HY T1/TurboS/GLM-5/Kimi', value: 'tencent' },
|
||
{ key: 'j', label: '百度千帆', desc: 'ERNIE-4.0, ERNIE-3.5', value: 'baidu' },
|
||
{ key: 'k', label: '智谱 GLM / Z.AI', desc: 'GLM-5.1, GLM-5, GLM-4.7', value: 'zhipu' },
|
||
|
||
{ label: `${C.bold}── 本地模型 / 自定义 API ──${C.reset}`, disabled: true },
|
||
{ key: 'l', label: 'Ollama', desc: '本地模型,无需 API Key', value: 'ollama' },
|
||
{ key: 'm', label: '自定义 OpenAI 兼容 API', desc: '', value: 'custom' },
|
||
{ key: 'n', label: '自定义 Anthropic 兼容 API', desc: '', value: 'custom-anthropic' },
|
||
|
||
{ label: '', disabled: true },
|
||
{ key: '0', label: '返回', desc: '', value: 'back' },
|
||
],
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 消息渠道菜单 (对应 configure_channels)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function showChannelsMenu() {
|
||
const result = await select({
|
||
title: '配置消息渠道',
|
||
showSearch: false,
|
||
items: [
|
||
{ label: `${C.dim}提示: 微信配置请使用 LuCI 界面「微信配置」菜单${C.reset}`, disabled: true },
|
||
{ label: '', disabled: true },
|
||
{ key: '1', label: 'QQ 机器人', desc: '腾讯QQ', value: 'qq' },
|
||
{ key: '2', label: 'Telegram', desc: `${C.green}最常用${C.reset}`, value: 'telegram' },
|
||
{ key: '3', label: 'Discord', desc: '', value: 'discord' },
|
||
{ key: '4', label: '飞书 (Feishu)', desc: '', value: 'feishu' },
|
||
{ key: '5', label: 'Slack', desc: '', value: 'slack' },
|
||
{ key: '6', label: 'WhatsApp', desc: '需通过 Web 控制台扫码', value: 'whatsapp' },
|
||
{ key: '7', label: 'Telegram 配对助手', desc: '', value: 'telegram-pairing' },
|
||
{ key: '8', label: '官方完整渠道配置向导', desc: '', value: 'wizard' },
|
||
{ label: '', disabled: true },
|
||
{ key: '0', label: '返回', desc: '', value: 'back' },
|
||
],
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 高级配置菜单 (对应 advanced_menu)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function showAdvancedMenu() {
|
||
const gwPort = jsonGet('gateway.port') || '18789';
|
||
const gwBind = jsonGet('gateway.bind') || 'lan';
|
||
const gwMode = jsonGet('gateway.mode') || 'local';
|
||
const logLevel = jsonGet('gateway.logLevel') || '未设置';
|
||
const acpDispatch = jsonGet('acp.dispatch.enabled') || 'false';
|
||
|
||
const result = await select({
|
||
title: '高级配置',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: '1', label: 'Gateway 端口', desc: `当前: ${gwPort}`, value: 'port' },
|
||
{ key: '2', label: 'Gateway 绑定地址', desc: `当前: ${gwBind}`, value: 'bind' },
|
||
{ key: '3', label: 'Gateway 运行模式', desc: `当前: ${gwMode}`, value: 'mode' },
|
||
{ key: '4', label: '日志级别', desc: `当前: ${logLevel}`, value: 'loglevel' },
|
||
{ key: '5', label: 'ACP Dispatch 设置', desc: `当前: ${acpDispatch}`, value: 'acp' },
|
||
{ key: '6', label: '官方完整配置向导', desc: 'oc configure', value: 'wizard' },
|
||
{ key: '7', label: '查看原始配置 JSON', desc: '', value: 'view-json' },
|
||
{ key: '8', label: '编辑配置文件', desc: 'vi / nano', value: 'edit' },
|
||
{ key: '9', label: '导出配置备份', desc: '', value: 'backup' },
|
||
{ key: '10', label: '导入配置', desc: '', value: 'import' },
|
||
{ label: '', disabled: true },
|
||
{ key: '0', label: '返回', desc: '', value: 'back' },
|
||
],
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 重置配置菜单 (对应 reset_to_defaults)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function showResetMenu() {
|
||
const result = await select({
|
||
title: '恢复默认配置',
|
||
showSearch: false,
|
||
items: [
|
||
{ label: `${C.yellow}请选择恢复级别:${C.reset}`, disabled: true },
|
||
{ label: '', disabled: true },
|
||
{ key: '1', label: '仅重置网关设置', desc: '端口/绑定/模式恢复默认,保留模型和渠道', value: 'gateway' },
|
||
{ key: '2', label: '清除模型配置', desc: '移除所有 AI 模型和 API Key', value: 'models' },
|
||
{ key: '3', label: '清除渠道配置', desc: '移除所有消息渠道配置', value: 'channels' },
|
||
{ key: '4', label: '完全恢复出厂', desc: '删除所有配置,重新初始化', value: 'full' },
|
||
{ label: '', disabled: true },
|
||
{ key: '0', label: '返回', desc: '', value: 'back' },
|
||
],
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 备份/还原菜单 (对应 backup_restore_menu)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function showBackupMenu() {
|
||
const result = await select({
|
||
title: '备份/还原配置',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: '1', label: '创建配置备份', desc: '仅配置文件', value: 'create-config' },
|
||
{ key: '2', label: '创建完整备份', desc: '配置 + 状态数据', value: 'create-full' },
|
||
{ key: '3', label: '验证最新备份', desc: '', value: 'verify' },
|
||
{ key: '4', label: '查看备份列表', desc: '', value: 'list' },
|
||
{ key: '5', label: '从最新备份恢复配置', desc: '', value: 'restore' },
|
||
{ label: '', disabled: true },
|
||
{ key: '0', label: '返回', desc: '', value: 'back' },
|
||
],
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 模型配置处理函数 (与 oc-config.sh 完全对应)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function configureOpenAI() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}OpenAI 配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://platform.openai.com/api-keys${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 OpenAI API Key (sk-...)', placeholder: 'sk-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: 'OpenAI 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'gpt-5.2', desc: '最强编程与代理旗舰 (推荐)', value: 'gpt-5.2' },
|
||
{ key: 'b', label: 'gpt-5-mini', desc: '高性价比推理', value: 'gpt-5-mini' },
|
||
{ key: 'c', label: 'gpt-5-nano', desc: '极速低成本', value: 'gpt-5-nano' },
|
||
{ key: 'd', label: 'gpt-4.1', desc: '最强非推理模型', value: 'gpt-4.1' },
|
||
{ key: 'e', label: 'o3', desc: '推理模型', value: 'o3' },
|
||
{ key: 'f', label: 'o4-mini', desc: '推理轻量', value: 'o4-mini' },
|
||
{ key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gpt-5.2' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('openai', apiKey);
|
||
registerAndSetModel(`openai/${modelName}`);
|
||
console.log(`\n${C.green}✅ OpenAI 已配置,活跃模型: openai/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureAnthropic() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}Anthropic 配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://console.anthropic.com/settings/keys${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 Anthropic API Key (sk-ant-...)', placeholder: 'sk-ant-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: 'Anthropic 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'claude-sonnet-4-20250514', desc: 'Claude Sonnet 4 (推荐)', value: 'claude-sonnet-4-20250514' },
|
||
{ key: 'b', label: 'claude-opus-4-20250514', desc: 'Claude Opus 4 顶级推理', value: 'claude-opus-4-20250514' },
|
||
{ key: 'c', label: 'claude-haiku-4-5', desc: 'Claude Haiku 4.5 轻量快速', value: 'claude-haiku-4-5' },
|
||
{ key: 'd', label: 'claude-sonnet-4.5', desc: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },
|
||
{ key: 'e', label: 'claude-sonnet-4.6', desc: 'Claude Sonnet 4.6', value: 'claude-sonnet-4.6' },
|
||
{ key: 'f', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'claude-sonnet-4-20250514' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('anthropic', apiKey);
|
||
registerAndSetModel(`anthropic/${modelName}`);
|
||
console.log(`\n${C.green}✅ Anthropic 已配置,活跃模型: anthropic/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureGoogle() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}Google Gemini 配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://aistudio.google.com/apikey${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 Google AI API Key', placeholder: 'AIza...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: 'Google Gemini 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'gemini-2.5-pro', desc: '旗舰推理 (推荐)', value: 'gemini-2.5-pro' },
|
||
{ key: 'b', label: 'gemini-2.5-flash', desc: '快速均衡', value: 'gemini-2.5-flash' },
|
||
{ key: 'c', label: 'gemini-2.5-flash-lite', desc: '极速低成本', value: 'gemini-2.5-flash-lite' },
|
||
{ key: 'd', label: 'gemini-3-flash-preview', desc: 'Gemini 3 Flash 预览', value: 'gemini-3-flash-preview' },
|
||
{ key: 'e', label: 'gemini-3-pro-preview', desc: 'Gemini 3 Pro 预览', value: 'gemini-3-pro-preview' },
|
||
{ key: 'f', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gemini-2.5-pro' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('google', apiKey);
|
||
registerAndSetModel(`google/${modelName}`);
|
||
console.log(`\n${C.green}✅ Google Gemini 已配置,活跃模型: google/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureOpenRouter() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}OpenRouter 配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://openrouter.ai/keys${C.reset}`);
|
||
console.log(`${C.dim}聚合多家模型,一个 Key 可调用所有主流模型${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 OpenRouter API Key', placeholder: 'sk-or-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: 'OpenRouter 常用模型',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'anthropic/claude-sonnet-4', desc: 'Claude Sonnet 4 (推荐)', value: 'anthropic/claude-sonnet-4' },
|
||
{ key: 'b', label: 'anthropic/claude-opus-4', desc: 'Claude Opus 4', value: 'anthropic/claude-opus-4' },
|
||
{ key: 'c', label: 'openai/gpt-5.2', desc: 'GPT-5.2', value: 'openai/gpt-5.2' },
|
||
{ key: 'd', label: 'google/gemini-2.5-pro', desc: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' },
|
||
{ key: 'e', label: 'deepseek/deepseek-r1', desc: 'DeepSeek R1', value: 'deepseek/deepseek-r1' },
|
||
{ key: 'f', label: 'meta-llama/llama-4-maverick', desc: 'Meta Llama 4', value: 'meta-llama/llama-4-maverick' },
|
||
{ key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称 (格式: provider/model)', defaultValue: 'anthropic/claude-sonnet-4' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('openrouter', apiKey);
|
||
registerCustomProvider('openrouter', 'https://openrouter.ai/api/v1', apiKey, modelName, modelName);
|
||
registerAndSetModel(`openrouter/${modelName}`);
|
||
console.log(`\n${C.green}✅ OpenRouter 已配置,活跃模型: openrouter/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureCopilot() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}GitHub Copilot 配置${C.reset}`);
|
||
console.log(`${C.yellow}需要有效的 GitHub Copilot 订阅 (Free/Pro/Business 均可)${C.reset}\n`);
|
||
|
||
console.log(`${C.cyan}启动 GitHub Copilot OAuth 登录...${C.reset}`);
|
||
console.log(`${C.dim}请在浏览器中打开显示的 URL,输入授权码完成登录${C.reset}\n`);
|
||
|
||
try {
|
||
await ocCmd('models', 'auth', 'login-github-copilot', '--yes');
|
||
console.log(`\n${C.green}✅ GitHub Copilot OAuth 认证成功${C.reset}\n`);
|
||
|
||
const modelChoice = await select({
|
||
title: 'GitHub Copilot 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'gpt-4.1', desc: 'GPT-4.1 (推荐)', value: 'gpt-4.1' },
|
||
{ key: 'b', label: 'gpt-4o', desc: 'GPT-4o', value: 'gpt-4o' },
|
||
{ key: 'c', label: 'gpt-5', desc: 'GPT-5', value: 'gpt-5' },
|
||
{ key: 'd', label: 'gpt-5-mini', desc: 'GPT-5 mini', value: 'gpt-5-mini' },
|
||
{ key: 'e', label: 'gpt-5.1', desc: 'GPT-5.1', value: 'gpt-5.1' },
|
||
{ key: 'f', label: 'gpt-5.2', desc: 'GPT-5.2', value: 'gpt-5.2' },
|
||
{ key: 'g', label: 'gpt-5.2-codex', desc: 'GPT-5.2 Codex', value: 'gpt-5.2-codex' },
|
||
{ key: 'h', label: 'claude-sonnet-4', desc: 'Claude Sonnet 4', value: 'claude-sonnet-4' },
|
||
{ key: 'i', label: 'claude-sonnet-4.5', desc: 'Claude Sonnet 4.5', value: 'claude-sonnet-4.5' },
|
||
{ key: 'j', label: 'claude-sonnet-4.6', desc: 'Claude Sonnet 4.6', value: 'claude-sonnet-4.6' },
|
||
{ key: 'k', label: 'gemini-2.5-pro', desc: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro' },
|
||
{ key: 'm', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
|
||
if (modelChoice) {
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'gpt-4.1' });
|
||
if (!modelName) return false;
|
||
}
|
||
registerAndSetModel(`github-copilot/${modelName}`);
|
||
console.log(`\n${C.green}✅ 活跃模型已设置: github-copilot/${modelName}${C.reset}\n`);
|
||
}
|
||
return true;
|
||
} catch (e) {
|
||
console.log(`\n${C.yellow}OAuth 授权已退出或失败${C.reset}\n`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function configureXAI() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}xAI Grok 配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://console.x.ai${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 xAI API Key', placeholder: '' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: 'xAI Grok 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'grok-4', desc: 'Grok 4 旗舰 (推荐)', value: 'grok-4' },
|
||
{ key: 'b', label: 'grok-4-fast', desc: 'Grok 4 Fast', value: 'grok-4-fast' },
|
||
{ key: 'c', label: 'grok-3', desc: 'Grok 3', value: 'grok-3' },
|
||
{ key: 'd', label: 'grok-3-fast', desc: 'Grok 3 Fast', value: 'grok-3-fast' },
|
||
{ key: 'e', label: 'grok-3-mini', desc: 'Grok 3 Mini', value: 'grok-3-mini' },
|
||
{ key: 'f', label: 'grok-3-mini-fast', desc: 'Grok 3 Mini Fast', value: 'grok-3-mini-fast' },
|
||
{ key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'grok-4' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('xai', apiKey);
|
||
registerAndSetModel(`xai/${modelName}`);
|
||
console.log(`\n${C.green}✅ xAI Grok 已配置,活跃模型: xai/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureQwen() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}阿里云通义千问 Qwen 配置${C.reset}\n`);
|
||
|
||
const modeChoice = await select({
|
||
title: '配置方式',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: '通过官方向导配置', desc: 'Qwen Portal OAuth', value: 'portal' },
|
||
{ key: 'b', label: '百炼按量付费 API Key', desc: 'sk-xxx, 按 token 计费', value: 'bailian' },
|
||
{ key: 'c', label: 'Coding Plan 套餐', desc: `${C.green}★ 推荐${C.reset}`, value: 'codingplan' },
|
||
],
|
||
});
|
||
if (!modeChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
if (modeChoice.value === 'portal') {
|
||
console.log(`\n${C.cyan}启动 Qwen OAuth 授权...${C.reset}\n`);
|
||
try {
|
||
await ocCmd('models', 'auth', 'login', '--provider', 'qwen-portal', '--set-default');
|
||
} catch {}
|
||
console.log(`\n${C.yellow}OAuth 授权已退出${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
if (modeChoice.value === 'bailian') {
|
||
console.log(`\n${C.bold}百炼按量付费配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://dashscope.console.aliyun.com/apiKey${C.reset}`);
|
||
console.log(`${C.dim}Base URL: https://dashscope.aliyuncs.com/compatible-mode/v1${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入百炼 API Key (sk-...)', placeholder: 'sk-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: '百炼模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'qwen-max', desc: '千问Max 旗舰模型 (推荐)', value: 'qwen-max' },
|
||
{ key: 'b', label: 'qwen-plus', desc: '千问Plus 均衡之选', value: 'qwen-plus' },
|
||
{ key: 'c', label: 'qwen-flash', desc: '千问Flash 速度最快', value: 'qwen-flash' },
|
||
{ key: 'd', label: 'qwen-turbo', desc: '千问Turbo 经济实惠', value: 'qwen-turbo' },
|
||
{ key: 'e', label: 'qwen-long', desc: '千问Long 超长上下文', value: 'qwen-long' },
|
||
{ key: 'f', label: 'qwen3-coder-plus', desc: '代码专用旗舰', value: 'qwen3-coder-plus' },
|
||
{ key: 'g', label: 'qwq-plus', desc: 'QwQ推理模型', value: 'qwq-plus' },
|
||
{ key: 'h', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'qwen-max' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('dashscope', apiKey);
|
||
registerCustomProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/v1', apiKey, modelName, modelName);
|
||
registerAndSetModel(`dashscope/${modelName}`);
|
||
console.log(`\n${C.green}✅ 通义千问已配置 (按量付费),活跃模型: dashscope/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
if (modeChoice.value === 'codingplan') {
|
||
console.log(`\n${C.bold}Coding Plan 套餐配置${C.reset}`);
|
||
console.log(`${C.yellow}订阅套餐: https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/coding_plan${C.reset}`);
|
||
console.log(`${C.dim}Base URL: https://coding.dashscope.aliyuncs.com/v1${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 Coding Plan 专属 API Key (sk-sp-...)', placeholder: 'sk-sp-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: 'Coding Plan 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'qwen3.5-plus', desc: 'Qwen3.5 Plus (推荐, 100万上下文)', value: 'qwen3.5-plus' },
|
||
{ key: 'b', label: 'qwen3-coder-plus', desc: 'Qwen3 Coder Plus', value: 'qwen3-coder-plus' },
|
||
{ key: 'c', label: 'qwen3-coder-next', desc: 'Qwen3 Coder Next', value: 'qwen3-coder-next' },
|
||
{ key: 'd', label: 'qwen3-max-2026-01-23', desc: 'Qwen3 Max', value: 'qwen3-max-2026-01-23' },
|
||
{ key: 'e', label: 'MiniMax-M2.5', desc: 'MiniMax M2.5', value: 'MiniMax-M2.5' },
|
||
{ key: 'f', label: 'glm-5', desc: '智谱 GLM-5', value: 'glm-5' },
|
||
{ key: 'g', label: 'kimi-k2.5', desc: 'Kimi K2.5', value: 'kimi-k2.5' },
|
||
{ key: 'h', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'qwen3.5-plus' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
console.log(`\n${C.cyan}正在注册 Coding Plan 提供商 (含全部可用模型)...${C.reset}`);
|
||
authSetApikey('bailian', apiKey);
|
||
registerCodingPlanProvider(apiKey);
|
||
registerAndSetModel(`bailian/${modelName}`);
|
||
console.log(`\n${C.green}✅ Coding Plan 已配置,活跃模型: bailian/${modelName}${C.reset}`);
|
||
console.log(`${C.dim}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
async function configureSiliconFlow() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}硅基流动 SiliconFlow 配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://cloud.siliconflow.cn/account/ak${C.reset}`);
|
||
console.log(`${C.yellow}国内推理平台,支持多种开源模型${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 SiliconFlow API Key', placeholder: '' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
console.log(`\n${C.cyan}可用模型分类说明:${C.reset}`);
|
||
console.log(`${C.yellow}* Pro模型: 仅支持充值余额支付${C.reset}`);
|
||
console.log(`${C.yellow}* 非Pro模型: 支持代金券/免费额度${C.reset}\n`);
|
||
|
||
const modelChoice = await select({
|
||
title: 'SiliconFlow 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ label: `${C.cyan}── 非Pro模型 (支持代金券) ──${C.reset}`, disabled: true },
|
||
{ key: '1', label: 'deepseek-ai/DeepSeek-V3', desc: 'DeepSeek-V3 (推荐)', value: 'deepseek-ai/DeepSeek-V3' },
|
||
{ key: '2', label: 'deepseek-ai/DeepSeek-R1', desc: 'DeepSeek-R1 推理模型', value: 'deepseek-ai/DeepSeek-R1' },
|
||
{ key: '3', label: 'Qwen/Qwen2.5-72B-Instruct', desc: '通义千问 2.5 72B', value: 'Qwen/Qwen2.5-72B-Instruct' },
|
||
{ key: '4', label: 'Qwen/Qwen2.5-7B-Instruct', desc: '通义千问 2.5 7B', value: 'Qwen/Qwen2.5-7B-Instruct' },
|
||
{ key: '5', label: 'THUDM/glm-4-9b-chat', desc: '智谱 GLM-4 9B', value: 'THUDM/glm-4-9b-chat' },
|
||
{ key: '6', label: '01-ai/Yi-1.5-34B-Chat-16K', desc: '零一万物 Yi-1.5', value: '01-ai/Yi-1.5-34B-Chat-16K' },
|
||
{ label: `${C.cyan}── Pro模型 (仅支持充值余额) ──${C.reset}`, disabled: true },
|
||
{ key: '7', label: 'Pro/deepseek-ai/DeepSeek-V3', desc: 'DeepSeek-V3 (Pro)', value: 'Pro/deepseek-ai/DeepSeek-V3' },
|
||
{ key: '8', label: 'Pro/zai-org/GLM-5', desc: '智谱 GLM-5', value: 'Pro/zai-org/GLM-5' },
|
||
{ key: '0', label: '手动输入其他模型名称', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型详细名称', defaultValue: 'deepseek-ai/DeepSeek-V3' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('siliconflow', apiKey);
|
||
registerCustomProvider('siliconflow', 'https://api.siliconflow.cn/v1', apiKey, modelName, modelName);
|
||
registerAndSetModel(`siliconflow/${modelName}`);
|
||
console.log(`\n${C.green}✅ SiliconFlow 已配置,活跃模型: siliconflow/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureTencent() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}腾讯云大模型 Coding Plan 套餐配置${C.reset}`);
|
||
console.log(`${C.yellow}订阅/管理套餐: https://hunyuan.cloud.tencent.com/#/app/subscription${C.reset}`);
|
||
console.log(`${C.dim}文档: https://cloud.tencent.com/document/product/1772/128947${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入 Coding Plan API Key (sk-sp-...)', placeholder: 'sk-sp-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: '腾讯云 Coding Plan 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'tc-code-latest', desc: '自动路由 (推荐)', value: 'tc-code-latest' },
|
||
{ key: 'b', label: 'hunyuan-t1', desc: '混元 T1 深度推理', value: 'hunyuan-t1' },
|
||
{ key: 'c', label: 'hunyuan-2.0-thinking', desc: '混元 2.0 Thinking', value: 'hunyuan-2.0-thinking' },
|
||
{ key: 'd', label: 'hunyuan-turbos', desc: '混元 TurboS 旗舰', value: 'hunyuan-turbos' },
|
||
{ key: 'e', label: 'hunyuan-2.0-instruct', desc: '混元 2.0 Instruct', value: 'hunyuan-2.0-instruct' },
|
||
{ key: 'f', label: 'glm-5', desc: '智谱 GLM-5', value: 'glm-5' },
|
||
{ key: 'g', label: 'kimi-k2.5', desc: 'Moonshot Kimi K2.5', value: 'kimi-k2.5' },
|
||
{ key: 'h', label: 'minimax-m2.5', desc: 'MiniMax M2.5', value: 'minimax-m2.5' },
|
||
{ key: 'z', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'tc-code-latest' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
console.log(`\n${C.cyan}正在注册腾讯云 Coding Plan 提供商 (含全部套餐模型)...${C.reset}`);
|
||
authSetApikey('lkeap', apiKey);
|
||
registerLkeapCodingPlanProvider(apiKey);
|
||
registerAndSetModel(`lkeap/${modelName}`);
|
||
console.log(`\n${C.green}✅ 腾讯云 Coding Plan 已配置,活跃模型: lkeap/${modelName}${C.reset}`);
|
||
console.log(`${C.dim}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureBaidu() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}百度千帆大模型配置${C.reset}`);
|
||
console.log(`${C.yellow}获取 API Key: https://console.bce.baidu.com/qianfan/ais/console/onlineService${C.reset}\n`);
|
||
|
||
const apiKey = await input({ prompt: '请输入百度千帆 API Key (Access Token)', placeholder: '' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: '百度千帆模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'ernie-4.0-8k', desc: '文心一言 4.0 (推荐)', value: 'ernie-4.0-8k' },
|
||
{ key: 'b', label: 'ernie-3.5-8k', desc: '文心一言 3.5', value: 'ernie-3.5-8k' },
|
||
{ key: 'c', label: 'ernie-4.0-turbo-8k', desc: '文心一言 4.0 Turbo', value: 'ernie-4.0-turbo-8k' },
|
||
{ key: 'd', label: 'ernie-speed-8k', desc: '文心一言 Speed 极速', value: 'ernie-speed-8k' },
|
||
{ key: 'e', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'ernie-4.0-8k' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
authSetApikey('qianfan', apiKey);
|
||
registerCustomProvider('qianfan', 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop', apiKey, modelName, modelName);
|
||
registerAndSetModel(`qianfan/${modelName}`);
|
||
console.log(`\n${C.green}✅ 百度千帆已配置,活跃模型: qianfan/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureZhipu() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}智谱 GLM / Z.AI 配置${C.reset}\n`);
|
||
|
||
const methodChoice = await select({
|
||
title: '认证方式',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'CN (open.bigmodel.cn)', desc: `${C.green}★ 国内用户推荐${C.reset}`, value: 'cn' },
|
||
{ key: 'b', label: 'Coding-Plan-CN', desc: '智谱 Coding Plan 套餐', value: 'coding-plan-cn' },
|
||
{ key: 'c', label: 'Global (api.z.ai)', desc: '全球版', value: 'global' },
|
||
{ key: 'd', label: '手动输入', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!methodChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let baseUrl = 'https://open.bigmodel.cn/api/paas/v4';
|
||
switch (methodChoice.value) {
|
||
case 'cn':
|
||
console.log(`${C.yellow}获取 API Key: https://open.bigmodel.cn/api-key${C.reset}`);
|
||
break;
|
||
case 'coding-plan-cn':
|
||
baseUrl = 'https://open.bigmodel.cn/api/coding/paas/v4';
|
||
console.log(`${C.yellow}Coding Plan 套餐 API Key${C.reset}`);
|
||
break;
|
||
case 'global':
|
||
baseUrl = 'https://api.z.ai/api/paas/v4';
|
||
console.log(`${C.yellow}全球版 API Key${C.reset}`);
|
||
break;
|
||
case 'custom':
|
||
const customUrl = await input({ prompt: '请输入 Base URL', defaultValue: 'https://open.bigmodel.cn/api/paas/v4' });
|
||
if (customUrl) baseUrl = customUrl;
|
||
break;
|
||
}
|
||
|
||
const apiKey = await input({ prompt: '请输入智谱 API Key', placeholder: '' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: '智谱 GLM 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'glm-5.1', desc: 'GLM-5.1 (推荐)', value: 'glm-5.1' },
|
||
{ key: 'b', label: 'glm-5', desc: 'GLM-5', value: 'glm-5' },
|
||
{ key: 'c', label: 'glm-4.7', desc: 'GLM-4.7', value: 'glm-4.7' },
|
||
{ key: 'd', label: 'glm-4.7-flash', desc: 'GLM-4.7 Flash', value: 'glm-4.7-flash' },
|
||
{ key: 'e', label: 'glm-4.5', desc: 'GLM-4.5', value: 'glm-4.5' },
|
||
{ key: 'f', label: 'glm-4.5-flash', desc: 'GLM-4.5 Flash (免费)', value: 'glm-4.5-flash' },
|
||
{ key: 'g', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'glm-5.1' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
// 智谱 GLM 使用原生 zai provider (OpenClaw 内置支持)
|
||
registerCustomProvider('zai', baseUrl, apiKey, modelName, modelName, 128000, 4096);
|
||
authSetApikey('zai', apiKey);
|
||
registerAndSetModel(`zai/${modelName}`);
|
||
console.log(`\n${C.green}✅ 智谱 GLM 已配置,活跃模型: zai/${modelName}${C.reset}`);
|
||
console.log(`${C.dim} Base URL: ${baseUrl}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureOllama() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}Ollama 本地模型配置${C.reset}`);
|
||
console.log(`${C.yellow}Ollama 在本地或局域网运行大模型,无需 API Key${C.reset}`);
|
||
console.log(`${C.yellow}安装 Ollama: https://ollama.com${C.reset}\n`);
|
||
|
||
const modeChoice = await select({
|
||
title: '连接方式',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: '本机运行', desc: 'localhost:11434', value: 'local' },
|
||
{ key: 'b', label: '局域网其他设备', desc: '', value: 'remote' },
|
||
],
|
||
});
|
||
if (!modeChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let ollamaUrl = 'http://127.0.0.1:11434';
|
||
if (modeChoice.value === 'remote') {
|
||
const host = await input({ prompt: 'Ollama 地址 (如 192.168.1.100:11434)', placeholder: '' });
|
||
if (!host) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
ollamaUrl = host.startsWith('http') ? host : `http://${host}`;
|
||
ollamaUrl = ollamaUrl.replace(/\/v1$/, '').replace(/\/$/, '');
|
||
}
|
||
|
||
// 尝试检测 Ollama 连通性
|
||
console.log(`\n${C.cyan}检测 Ollama 连通性...${C.reset}`);
|
||
let modelList = [];
|
||
try {
|
||
const stdout = execSync(`curl -sf --connect-timeout 3 --max-time 5 ${ollamaUrl}/api/tags`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||
const data = JSON.parse(stdout);
|
||
modelList = data.models || [];
|
||
console.log(`${C.green}✅ Ollama 已连接${C.reset}`);
|
||
} catch {
|
||
console.log(`${C.yellow}⚠️ 无法连接 Ollama (${ollamaUrl})${C.reset}`);
|
||
const continueAnyway = await confirm({ prompt: '仍要继续配置?', defaultYes: false });
|
||
if (!continueAnyway) return false;
|
||
}
|
||
|
||
let modelName = '';
|
||
if (modelList.length > 0) {
|
||
const items = modelList.map((m, i) => ({
|
||
key: String(i + 1),
|
||
label: m.name,
|
||
desc: '',
|
||
value: m.name,
|
||
}));
|
||
items.push({ key: 'm', label: '手动输入模型名', desc: '', value: 'custom' });
|
||
|
||
const modelChoice = await select({
|
||
title: '已安装的模型',
|
||
showSearch: false,
|
||
items,
|
||
});
|
||
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
modelName = modelChoice.value === 'custom'
|
||
? await input({ prompt: '请输入模型名称', defaultValue: 'llama3.3' })
|
||
: modelChoice.value;
|
||
} else {
|
||
modelName = await input({ prompt: '请输入模型名称', placeholder: 'llama3.3, qwen2.5...' });
|
||
}
|
||
|
||
if (!modelName) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
// 注册 Ollama 提供商
|
||
const config = readConfig();
|
||
if (!config.models) config.models = {};
|
||
if (!config.models.providers) config.models.providers = {};
|
||
config.models.mode = 'merge';
|
||
config.models.providers['ollama'] = {
|
||
baseUrl: ollamaUrl,
|
||
apiKey: 'ollama-local',
|
||
api: 'ollama',
|
||
models: [{
|
||
id: modelName,
|
||
name: modelName,
|
||
reasoning: false,
|
||
input: ['text'],
|
||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||
contextWindow: 128000,
|
||
maxTokens: 32000,
|
||
}]
|
||
};
|
||
writeConfig(config);
|
||
|
||
authSetApikey('ollama', 'ollama-local', 'ollama:local');
|
||
registerAndSetModel(`ollama/${modelName}`);
|
||
console.log(`\n${C.green}✅ Ollama 已配置,活跃模型: ollama/${modelName}${C.reset}`);
|
||
console.log(`${C.cyan} Ollama 地址: ${ollamaUrl}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureCustomAPI() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}自定义 OpenAI 兼容 API 配置${C.reset}`);
|
||
console.log(`${C.yellow}支持任何兼容 OpenAI API 格式的服务商${C.reset}\n`);
|
||
|
||
const baseUrl = await input({ prompt: 'API Base URL (如 https://api.example.com/v1)', placeholder: '' });
|
||
if (!baseUrl) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelName = await input({ prompt: '模型名称', placeholder: '' });
|
||
if (!modelName) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
authSetApikey('openai-compatible', apiKey, 'openai-compatible:manual');
|
||
registerCustomProvider('openai-compatible', baseUrl.replace(/\/$/, ''), apiKey, modelName, modelName);
|
||
registerAndSetModel(`openai-compatible/${modelName}`);
|
||
console.log(`\n${C.green}✅ 自定义 API 已配置,活跃模型: openai-compatible/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function configureCustomAnthropic() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}自定义 Anthropic 兼容 API 配置${C.reset}`);
|
||
console.log(`${C.yellow}支持任何兼容 Anthropic Messages API 格式的服务商${C.reset}\n`);
|
||
|
||
const baseUrl = await input({ prompt: 'API Base URL', placeholder: 'https://api.anthropic.com' });
|
||
if (!baseUrl) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const apiKey = await input({ prompt: 'API Key', placeholder: 'sk-ant-...' });
|
||
if (!apiKey) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
const modelChoice = await select({
|
||
title: 'Anthropic 模型选择',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: 'a', label: 'claude-sonnet-4-20250514', desc: 'Claude Sonnet 4 (推荐)', value: 'claude-sonnet-4-20250514' },
|
||
{ key: 'b', label: 'claude-opus-4-20250514', desc: 'Claude Opus 4', value: 'claude-opus-4-20250514' },
|
||
{ key: 'c', label: '手动输入模型名', desc: '', value: 'custom' },
|
||
],
|
||
});
|
||
if (!modelChoice) { console.log(`${C.yellow}已取消${C.reset}`); return false; }
|
||
|
||
let modelName = modelChoice.value;
|
||
if (modelChoice.value === 'custom') {
|
||
modelName = await input({ prompt: '请输入模型名称', defaultValue: 'claude-sonnet-4-20250514' });
|
||
if (!modelName) return false;
|
||
}
|
||
|
||
const config = readConfig();
|
||
if (!config.models) config.models = {};
|
||
if (!config.models.providers) config.models.providers = {};
|
||
config.models.mode = 'merge';
|
||
config.models.providers['anthropic-compatible'] = {
|
||
baseUrl: baseUrl.replace(/\/$/, ''),
|
||
apiKey: apiKey,
|
||
api: 'anthropic-messages',
|
||
models: [{
|
||
id: modelName,
|
||
name: modelName,
|
||
reasoning: false,
|
||
input: ['text', 'image'],
|
||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||
contextWindow: 200000,
|
||
maxTokens: 16000,
|
||
}]
|
||
};
|
||
writeConfig(config);
|
||
|
||
authSetApikey('anthropic-compatible', apiKey, 'anthropic-compatible:manual');
|
||
registerAndSetModel(`anthropic-compatible/${modelName}`);
|
||
console.log(`\n${C.green}✅ 自定义 Anthropic API 已配置,活跃模型: anthropic-compatible/${modelName}${C.reset}\n`);
|
||
return true;
|
||
}
|
||
|
||
async function launchWizard() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.cyan}启动官方完整模型配置向导...${C.reset}\n`);
|
||
console.log(`${C.yellow}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${C.reset}\n`);
|
||
try {
|
||
await ocCmd('configure', '--section', 'model');
|
||
} catch (e) {
|
||
console.log(`${C.yellow}配置向导已退出${C.reset}\n`);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 模型配置处理入口
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleModelConfig() {
|
||
while (true) {
|
||
const choice = await showModelMenu();
|
||
if (!choice || choice.value === 'back') break;
|
||
|
||
resetRenderCount();
|
||
let configured = false;
|
||
|
||
switch (choice.value) {
|
||
case 'wizard': await launchWizard(); break;
|
||
case 'openai': configured = await configureOpenAI(); break;
|
||
case 'anthropic': configured = await configureAnthropic(); break;
|
||
case 'google': configured = await configureGoogle(); break;
|
||
case 'openrouter': configured = await configureOpenRouter(); break;
|
||
case 'copilot': configured = await configureCopilot(); break;
|
||
case 'xai': configured = await configureXAI(); break;
|
||
case 'qwen': configured = await configureQwen(); break;
|
||
case 'siliconflow': configured = await configureSiliconFlow(); break;
|
||
case 'tencent': configured = await configureTencent(); break;
|
||
case 'baidu': configured = await configureBaidu(); break;
|
||
case 'zhipu': configured = await configureZhipu(); break;
|
||
case 'ollama': configured = await configureOllama(); break;
|
||
case 'custom': configured = await configureCustomAPI(); break;
|
||
case 'custom-anthropic': configured = await configureCustomAnthropic(); break;
|
||
}
|
||
|
||
if (configured) await askRestart();
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 设置活动模型 (对应 set_active_model)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleSetActiveModel() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}设定当前活跃模型${C.reset}\n`);
|
||
|
||
const currentModel = getCurrentModel();
|
||
console.log(` 当前活跃模型: ${C.green}${C.bold}${currentModel || '未设置'}${C.reset}\n`);
|
||
|
||
try {
|
||
const result = await ocCmd('models', 'list', '--json');
|
||
const modelsData = JSON.parse(result.stdout);
|
||
const models = modelsData.models || [];
|
||
|
||
if (models.length > 0) {
|
||
const items = models.map((m, i) => ({
|
||
key: String(i + 1),
|
||
label: m.key,
|
||
desc: m.name && m.name !== m.key ? `(${m.name})` : '',
|
||
value: m.key,
|
||
selected: m.key === currentModel,
|
||
}));
|
||
|
||
items.push({ label: '', disabled: true });
|
||
items.push({ key: 'm', label: '手动输入模型 ID', desc: '', value: 'manual' });
|
||
items.push({ key: '0', label: '返回', desc: '', value: 'back' });
|
||
|
||
const choice = await select({
|
||
title: '已配置的模型',
|
||
showSearch: true,
|
||
items,
|
||
});
|
||
|
||
if (!choice || choice.value === 'back') return;
|
||
|
||
if (choice.value === 'manual') {
|
||
const manualModel = await input({
|
||
prompt: '请输入模型 ID',
|
||
placeholder: 'openai/gpt-4o',
|
||
defaultValue: currentModel || '',
|
||
});
|
||
if (manualModel) {
|
||
registerAndSetModel(manualModel);
|
||
console.log(`\n${C.green}✅ 活跃模型已设为: ${manualModel}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
} else {
|
||
registerAndSetModel(choice.value);
|
||
console.log(`\n${C.green}✅ 活跃模型已切换为: ${choice.value}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
} else {
|
||
console.log(`${C.yellow}尚未配置任何模型。${C.reset}`);
|
||
console.log(`${C.yellow}请先通过「配置 AI 模型提供商」添加模型。${C.reset}\n`);
|
||
|
||
const manualModel = await input({
|
||
prompt: '直接输入模型 ID 设置 (留空返回)',
|
||
defaultValue: '',
|
||
});
|
||
if (manualModel) {
|
||
registerAndSetModel(manualModel);
|
||
console.log(`\n${C.green}✅ 活跃模型已设为: ${manualModel}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log(`${C.red}获取模型列表失败: ${e.message}${C.reset}\n`);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 消息渠道处理 (对应 configure_channels 等函数)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleChannels() {
|
||
while (true) {
|
||
const choice = await showChannelsMenu();
|
||
if (!choice || choice.value === 'back') break;
|
||
|
||
resetRenderCount();
|
||
|
||
switch (choice.value) {
|
||
case 'qq': {
|
||
console.log(`\n${C.bold}QQ 机器人配置${C.reset}\n`);
|
||
console.log(`${C.yellow}获取 App ID 和 App Secret:${C.reset}`);
|
||
console.log(` 1. 前往 ${C.cyan}https://q.qq.com/qqbot/openclaw/login.html${C.reset}`);
|
||
console.log(` 2. 用手机 QQ 扫码注册/登录`);
|
||
console.log(` 3. 创建机器人后复制 App ID 和 App Secret\n`);
|
||
|
||
const appId = await input({ prompt: '请输入 QQ 机器人 App ID', placeholder: '' });
|
||
if (!appId) { console.log(`${C.yellow}已取消${C.reset}`); break; }
|
||
|
||
const appSecret = await input({ prompt: '请输入 QQ 机器人 App Secret', placeholder: '' });
|
||
if (!appSecret) { console.log(`${C.yellow}已取消${C.reset}`); break; }
|
||
|
||
// 写入配置
|
||
const cfg = readConfig();
|
||
if (!cfg.channels) cfg.channels = {};
|
||
if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
|
||
cfg.channels.qqbot.appId = appId;
|
||
cfg.channels.qqbot.clientSecret = appSecret;
|
||
cfg.channels.qqbot.enabled = true;
|
||
writeConfig(cfg);
|
||
|
||
console.log(`\n${C.green}✅ QQ 机器人配置已保存${C.reset}\n`);
|
||
await askRestart();
|
||
break;
|
||
}
|
||
case 'telegram': {
|
||
console.log(`\n${C.bold}Telegram Bot 配置${C.reset}\n`);
|
||
console.log(`${C.yellow}获取 Bot Token:${C.reset}`);
|
||
console.log(` 1. 打开 Telegram → 搜索 ${C.cyan}@BotFather${C.reset}`);
|
||
console.log(` 2. 发送 ${C.cyan}/newbot${C.reset} → 按提示创建`);
|
||
console.log(` 3. 复制生成的 Token\n`);
|
||
|
||
const token = await input({ prompt: '请输入 Telegram Bot Token', placeholder: '123456:ABC...' });
|
||
if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }
|
||
|
||
// 验证格式
|
||
if (!/^[0-9]+:[A-Za-z0-9_-]+$/.test(token)) {
|
||
console.log(`${C.red}✗ Token 格式错误${C.reset}`);
|
||
console.log(`${C.yellow}正确格式: 123456789:ABCdefGHIjklMNOpqr${C.reset}\n`);
|
||
break;
|
||
}
|
||
|
||
const cfg = readConfig();
|
||
if (!cfg.channels) cfg.channels = {};
|
||
if (!cfg.channels.telegram) cfg.channels.telegram = {};
|
||
cfg.channels.telegram.botToken = token;
|
||
writeConfig(cfg);
|
||
|
||
console.log(`\n${C.green}✅ Telegram Bot Token 已保存${C.reset}\n`);
|
||
await askRestart();
|
||
break;
|
||
}
|
||
case 'discord': {
|
||
console.log(`\n${C.bold}Discord Bot 配置${C.reset}\n`);
|
||
console.log(`${C.yellow}获取 Bot Token:${C.cyan} https://discord.com/developers/applications${C.reset}\n`);
|
||
|
||
const token = await input({ prompt: '请输入 Discord Bot Token', placeholder: '' });
|
||
if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }
|
||
|
||
const cfg = readConfig();
|
||
if (!cfg.channels) cfg.channels = {};
|
||
if (!cfg.channels.discord) cfg.channels.discord = {};
|
||
cfg.channels.discord.botToken = token;
|
||
writeConfig(cfg);
|
||
|
||
console.log(`\n${C.green}✅ Discord Bot Token 已保存${C.reset}\n`);
|
||
await askRestart();
|
||
break;
|
||
}
|
||
case 'feishu': {
|
||
console.log(`\n${C.bold}飞书 Bot 配置${C.reset}\n`);
|
||
console.log(`${C.cyan}即将执行飞书官方安装向导...${C.reset}\n`);
|
||
|
||
const doInstall = await confirm({ prompt: '是否开始安装?', defaultYes: true });
|
||
if (!doInstall) break;
|
||
|
||
console.log(`\n${C.cyan}正在启动飞书安装向导...${C.reset}\n`);
|
||
try {
|
||
await runCommand('npx', ['-y', '@larksuite/openclaw-lark-tools', 'install'], { env: { HOME: OC_DATA } });
|
||
} catch (e) {
|
||
console.log(`${C.yellow}安装向导已退出${C.reset}\n`);
|
||
}
|
||
break;
|
||
}
|
||
case 'slack': {
|
||
console.log(`\n${C.bold}Slack Bot 配置${C.reset}\n`);
|
||
console.log(`${C.yellow}获取 Bot Token:${C.cyan} https://api.slack.com/apps${C.reset}\n`);
|
||
|
||
const token = await input({ prompt: '请输入 Slack Bot Token (xoxb-...)', placeholder: '' });
|
||
if (!token) { console.log(`${C.yellow}已取消${C.reset}`); break; }
|
||
|
||
const cfg = readConfig();
|
||
if (!cfg.channels) cfg.channels = {};
|
||
if (!cfg.channels.slack) cfg.channels.slack = {};
|
||
cfg.channels.slack.botToken = token;
|
||
writeConfig(cfg);
|
||
|
||
console.log(`\n${C.green}✅ Slack Bot Token 已保存${C.reset}\n`);
|
||
await askRestart();
|
||
break;
|
||
}
|
||
case 'whatsapp': {
|
||
const gwToken = jsonGet('gateway.auth.token') || '';
|
||
const gwPort = jsonGet('gateway.port') || '18789';
|
||
console.log(`\n${C.yellow}WhatsApp 需要通过 Web 控制台扫码配对:${C.reset}`);
|
||
console.log(`${C.cyan}http://<你的路由器IP>:${gwPort}/?token=${gwToken}${C.reset}`);
|
||
console.log(`打开后进入 Channels → WhatsApp 扫码即可。\n`);
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
}
|
||
case 'telegram-pairing': {
|
||
console.log(`\n${C.cyan}启动 Telegram 配对助手...${C.reset}\n`);
|
||
try {
|
||
await ocCmd('models', 'auth', 'login-telegram-bot');
|
||
} catch (e) {}
|
||
break;
|
||
}
|
||
case 'wizard': {
|
||
console.log(`\n${C.cyan}启动官方渠道配置向导...${C.reset}\n`);
|
||
try {
|
||
await ocCmd('configure', '--section', 'channels');
|
||
} catch (e) {}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 健康检查 (对应 health_check)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleHealthCheck() {
|
||
resetRenderCount();
|
||
console.log(`\n${C.bold}健康检查${C.reset}\n`);
|
||
|
||
// 验证配置文件
|
||
console.log(`${C.cyan}验证配置文件格式...${C.reset}`);
|
||
try {
|
||
const result = await ocCmd('config', 'validate', '--json');
|
||
const validateData = JSON.parse(result.stdout);
|
||
if (validateData.valid) {
|
||
console.log(`${C.green}✅ 配置文件格式有效${C.reset}\n`);
|
||
} else if (validateData.errors && validateData.errors.length > 0) {
|
||
console.log(`${C.red}❌ 配置文件存在错误:${C.reset}`);
|
||
validateData.errors.forEach(e => console.log(` ${C.yellow}• ${e.message}${C.reset}`));
|
||
console.log('');
|
||
}
|
||
} catch (e) {
|
||
console.log(`${C.yellow}⚠️ 无法验证配置文件${C.reset}\n`);
|
||
}
|
||
|
||
// 检查端口
|
||
console.log(`${C.cyan}检查服务状态...${C.reset}`);
|
||
try {
|
||
await runCommand('/etc/init.d/openclaw', ['status_service']);
|
||
} catch (e) {}
|
||
|
||
console.log(`\n${C.cyan}提示: 查看详细日志请运行 logread -e openclaw${C.reset}`);
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 显示当前配置 (对应 show_current_config)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleShowConfig() {
|
||
resetRenderCount();
|
||
|
||
const gwPort = jsonGet('gateway.port') || '18789';
|
||
const gwBind = jsonGet('gateway.bind') || 'lan';
|
||
const gwMode = jsonGet('gateway.mode') || 'local';
|
||
const currentModel = getCurrentModel();
|
||
|
||
console.log(`\n${C.green}┌──────────────────────────────────────────────────────────┐${C.reset}`);
|
||
console.log(`${C.green}│${C.reset} 📋 ${C.bold}当前配置概览${C.reset}`);
|
||
console.log(`${C.green}├──────────────────────────────────────────────────────────┤${C.reset}`);
|
||
console.log(`${C.green}│${C.reset} 网关端口 ............ ${C.cyan}${gwPort}${C.reset}`);
|
||
console.log(`${C.green}│${C.reset} 绑定模式 ............ ${C.cyan}${gwBind}${C.reset}`);
|
||
console.log(`${C.green}│${C.reset} 运行模式 ............ ${C.cyan}${gwMode}${C.reset}`);
|
||
console.log(`${C.green}│${C.reset} 活跃模型 ............ ${currentModel ? C.cyan + currentModel : C.yellow + '未配置'}${C.reset}`);
|
||
|
||
// 渠道状态
|
||
console.log(`${C.green}├──────────────────────────────────────────────────────────┤${C.reset}`);
|
||
console.log(`${C.green}│${C.reset} ${C.bold}渠道配置状态${C.reset}`);
|
||
|
||
const tgToken = jsonGet('channels.telegram.botToken');
|
||
const dcToken = jsonGet('channels.discord.botToken');
|
||
const fsAppId = jsonGet('channels.feishu.appId');
|
||
const skToken = jsonGet('channels.slack.botToken');
|
||
const qqAppId = jsonGet('channels.qqbot.appId');
|
||
|
||
if (qqAppId) {
|
||
console.log(`${C.green}│${C.reset} QQ (qqbot) ......... ${C.green}✅ 已配置${C.reset} (AppID: ${qqAppId.slice(0, 8)}...)`);
|
||
} else {
|
||
console.log(`${C.green}│${C.reset} QQ (qqbot) ......... ${C.yellow}❌ 未配置${C.reset}`);
|
||
}
|
||
if (tgToken) {
|
||
console.log(`${C.green}│${C.reset} Telegram ........... ${C.green}✅ 已配置${C.reset} (${tgToken.slice(0, 12)}...)`);
|
||
} else {
|
||
console.log(`${C.green}│${C.reset} Telegram ........... ${C.yellow}❌ 未配置${C.reset}`);
|
||
}
|
||
if (dcToken) {
|
||
console.log(`${C.green}│${C.reset} Discord ............ ${C.green}✅ 已配置${C.reset}`);
|
||
} else {
|
||
console.log(`${C.green}│${C.reset} Discord ............ ${C.yellow}❌ 未配置${C.reset}`);
|
||
}
|
||
if (fsAppId) {
|
||
console.log(`${C.green}│${C.reset} 飞书 ............... ${C.green}✅ 已配置${C.reset} (AppID: ${fsAppId.slice(0, 6)}...)`);
|
||
} else {
|
||
console.log(`${C.green}│${C.reset} 飞书 ............... ${C.yellow}❌ 未配置${C.reset}`);
|
||
}
|
||
if (skToken) {
|
||
console.log(`${C.green}│${C.reset} Slack .............. ${C.green}✅ 已配置${C.reset}`);
|
||
} else {
|
||
console.log(`${C.green}│${C.reset} Slack .............. ${C.yellow}❌ 未配置${C.reset}`);
|
||
}
|
||
|
||
console.log(`${C.green}└──────────────────────────────────────────────────────────┘${C.reset}\n`);
|
||
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 高级配置处理 (对应 advanced_menu)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleAdvancedConfig() {
|
||
while (true) {
|
||
const choice = await showAdvancedMenu();
|
||
if (!choice || choice.value === 'back') break;
|
||
|
||
resetRenderCount();
|
||
|
||
switch (choice.value) {
|
||
case 'port': {
|
||
const newPort = await input({
|
||
prompt: '请输入 Gateway 端口',
|
||
defaultValue: String(jsonGet('gateway.port') || '18789'),
|
||
});
|
||
if (newPort) {
|
||
jsonSet('gateway.port', parseInt(newPort));
|
||
try { execSync(`uci set openclaw.main.port="${newPort}" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}
|
||
console.log(`\n${C.green}✅ 端口已设置为 ${newPort}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'bind': {
|
||
const bindChoice = await select({
|
||
title: '绑定地址选项',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: '1', label: 'lan', desc: '仅 LAN 接口 (推荐)', value: 'lan' },
|
||
{ key: '2', label: 'loopback', desc: '仅本机访问', value: 'loopback' },
|
||
{ key: '3', label: 'all', desc: '所有接口 (0.0.0.0)', value: 'all' },
|
||
],
|
||
});
|
||
if (bindChoice) {
|
||
jsonSet('gateway.bind', bindChoice.value);
|
||
try { execSync(`uci set openclaw.main.bind="${bindChoice.value}" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}
|
||
console.log(`\n${C.green}✅ 绑定地址已设置为 ${bindChoice.value}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'mode': {
|
||
const modeChoice = await select({
|
||
title: '运行模式选项',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: '1', label: 'local', desc: '本地模式 (推荐)', value: 'local' },
|
||
{ key: '2', label: 'remote', desc: '远程模式', value: 'remote' },
|
||
],
|
||
});
|
||
if (modeChoice) {
|
||
jsonSet('gateway.mode', modeChoice.value);
|
||
console.log(`\n${C.green}✅ 运行模式已设置为 ${modeChoice.value}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'loglevel': {
|
||
const levelChoice = await select({
|
||
title: '日志级别选项',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: '1', label: 'debug', desc: '详细调试', value: 'debug' },
|
||
{ key: '2', label: 'info', desc: '常规信息', value: 'info' },
|
||
{ key: '3', label: 'warn', desc: '警告及以上', value: 'warn' },
|
||
{ key: '4', label: 'error', desc: '仅错误', value: 'error' },
|
||
],
|
||
});
|
||
if (levelChoice) {
|
||
jsonSet('gateway.logLevel', levelChoice.value);
|
||
console.log(`\n${C.green}✅ 日志级别已设置为 ${levelChoice.value}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'acp': {
|
||
const acpChoice = await select({
|
||
title: 'ACP Dispatch 选项',
|
||
showSearch: false,
|
||
items: [
|
||
{ key: '1', label: 'false', desc: '禁用 (推荐路由器使用)', value: 'false' },
|
||
{ key: '2', label: 'true', desc: '启用 (可能占用大量内存)', value: 'true' },
|
||
],
|
||
});
|
||
if (acpChoice) {
|
||
jsonSet('acp.dispatch.enabled', acpChoice.value === 'true');
|
||
console.log(`\n${C.green}✅ ACP Dispatch 已设置为 ${acpChoice.value}${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'wizard':
|
||
console.log(`\n${C.cyan}启动官方配置向导...${C.reset}\n`);
|
||
try {
|
||
await ocCmd('configure');
|
||
} catch (e) {}
|
||
break;
|
||
case 'view-json':
|
||
console.log(`\n${C.cyan}配置文件路径: ${CONFIG_FILE}${C.reset}\n`);
|
||
try {
|
||
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||
console.log(content);
|
||
} catch (e) {
|
||
console.log(`${C.red}无法读取配置文件${C.reset}`);
|
||
}
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
case 'edit':
|
||
console.log(`\n${C.yellow}请在 SSH 终端中手动编辑配置文件:${C.reset}`);
|
||
console.log(` vi ${CONFIG_FILE}\n`);
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
case 'backup':
|
||
await handleBackup();
|
||
break;
|
||
case 'import': {
|
||
const importPath = await input({
|
||
prompt: '请输入备份文件路径',
|
||
defaultValue: '',
|
||
});
|
||
if (importPath && fs.existsSync(importPath)) {
|
||
try {
|
||
fs.copyFileSync(importPath, CONFIG_FILE);
|
||
console.log(`\n${C.green}✅ 配置已导入${C.reset}\n`);
|
||
await askRestart();
|
||
} catch (e) {
|
||
console.log(`${C.red}导入失败: ${e.message}${C.reset}\n`);
|
||
}
|
||
} else {
|
||
console.log(`${C.yellow}文件不存在${C.reset}\n`);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 重置配置处理 (对应 reset_to_defaults)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleReset() {
|
||
while (true) {
|
||
const choice = await showResetMenu();
|
||
if (!choice || choice.value === 'back') break;
|
||
|
||
resetRenderCount();
|
||
|
||
switch (choice.value) {
|
||
case 'gateway': {
|
||
console.log(`\n${C.yellow}将重置: 网关端口→18789, 绑定→lan, 模式→local${C.reset}`);
|
||
console.log(`${C.yellow}保留: 认证令牌、模型配置、消息渠道${C.reset}\n`);
|
||
const ok = await confirm({ prompt: '确认恢复网关默认设置?', defaultYes: false });
|
||
if (ok) {
|
||
jsonSet('gateway.port', 18789);
|
||
jsonSet('gateway.bind', 'lan');
|
||
jsonSet('gateway.mode', 'local');
|
||
jsonSet('gateway.controlUi.allowInsecureAuth', true);
|
||
jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);
|
||
jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);
|
||
jsonSet('gateway.tailscale.mode', 'off');
|
||
console.log(`\n${C.green}✅ 网关设置已恢复默认${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'models': {
|
||
console.log(`\n${C.red}⚠️ 将清除: 所有模型配置、API Key、活跃模型设置${C.reset}\n`);
|
||
const ok = await confirm({ prompt: '确认清除所有模型配置?', defaultYes: false });
|
||
if (ok) {
|
||
const cfg = readConfig();
|
||
delete cfg.models;
|
||
if (cfg.agents?.defaults?.model) delete cfg.agents.defaults.model;
|
||
if (cfg.agents?.defaults?.models) delete cfg.agents.defaults.models;
|
||
writeConfig(cfg);
|
||
// 清除 auth-profiles.json
|
||
const authFile = `${OC_STATE_DIR}/agents/main/agent/auth-profiles.json`;
|
||
try {
|
||
fs.writeFileSync(authFile, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }, null, 2));
|
||
} catch {}
|
||
console.log(`\n${C.green}✅ 模型配置已清除${C.reset}`);
|
||
console.log(`${C.yellow}请通过菜单 [1] 重新配置 AI 模型${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'channels': {
|
||
console.log(`\n${C.red}⚠️ 将清除: 所有消息渠道配置 (Telegram/Discord/飞书等)${C.reset}\n`);
|
||
const ok = await confirm({ prompt: '确认清除所有渠道配置?', defaultYes: false });
|
||
if (ok) {
|
||
const cfg = readConfig();
|
||
delete cfg.channels;
|
||
writeConfig(cfg);
|
||
console.log(`\n${C.green}✅ 渠道配置已清除${C.reset}\n`);
|
||
await askRestart();
|
||
}
|
||
break;
|
||
}
|
||
case 'full': {
|
||
console.log(`\n${C.red}╔══════════════════════════════════════════════════════╗${C.reset}`);
|
||
console.log(`${C.red}║ ⚠️ 完全恢复出厂设置 ║${C.reset}`);
|
||
console.log(`${C.red}║ 此操作将删除所有配置并重新初始化 ║${C.reset}`);
|
||
console.log(`${C.red}╚══════════════════════════════════════════════════════╝${C.reset}\n`);
|
||
|
||
const confirmStr = await input({ prompt: '输入 RESET 确认恢复出厂设置', defaultValue: '' });
|
||
if (confirmStr !== 'RESET') {
|
||
console.log(`${C.cyan}已取消${C.reset}\n`);
|
||
break;
|
||
}
|
||
|
||
// 执行恢复
|
||
console.log(`\n${C.cyan}[1/5] 停止 Gateway...${C.reset}`);
|
||
try { await runCommand('/etc/init.d/openclaw', ['stop']); } catch {}
|
||
|
||
console.log(`${C.cyan}[2/5] 备份当前配置...${C.reset}`);
|
||
const backupDir = `${OC_STATE_DIR}/backups`;
|
||
const backupTs = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||
try {
|
||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true }); try { execSync(`chown openclaw:openclaw "${backupDir}"`, { stdio: "ignore" }); } catch {}
|
||
fs.copyFileSync(CONFIG_FILE, `${backupDir}/openclaw_${backupTs}.json`);
|
||
console.log(`${C.green} 备份已保存: backups/openclaw_${backupTs}.json${C.reset}`);
|
||
} catch {}
|
||
|
||
console.log(`${C.cyan}[3/5] 重置配置...${C.reset}`);
|
||
writeConfig({});
|
||
|
||
console.log(`${C.cyan}[4/5] 重新初始化...${C.reset}`);
|
||
// 生成新 token
|
||
const crypto = require('crypto');
|
||
const newToken = crypto.randomBytes(24).toString('hex');
|
||
|
||
console.log(`${C.cyan}[5/5] 应用 OpenWrt 适配配置...${C.reset}`);
|
||
jsonSet('gateway.port', 18789);
|
||
jsonSet('gateway.bind', 'lan');
|
||
jsonSet('gateway.mode', 'local');
|
||
jsonSet('gateway.auth.mode', 'token');
|
||
jsonSet('gateway.auth.token', newToken);
|
||
jsonSet('gateway.controlUi.allowInsecureAuth', true);
|
||
jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);
|
||
jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);
|
||
jsonSet('gateway.tailscale.mode', 'off');
|
||
jsonSet('acp.dispatch.enabled', false);
|
||
jsonSet('tools.profile', 'coding');
|
||
|
||
try { execSync(`uci set openclaw.main.token="${newToken}" && uci commit openclaw`, { stdio: 'ignore' }); } catch {}
|
||
|
||
console.log(`\n${C.green}✅ 出厂设置已恢复!${C.reset}\n`);
|
||
console.log(`${C.cyan}新认证令牌: ${newToken}${C.reset}\n`);
|
||
|
||
await restartGateway();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 备份/还原处理 (对应 backup_restore_menu)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleBackup() {
|
||
while (true) {
|
||
const choice = await showBackupMenu();
|
||
if (!choice || choice.value === 'back') break;
|
||
|
||
resetRenderCount();
|
||
const backupDir = `${OC_STATE_DIR}/backups`;
|
||
try { if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true }); try { execSync(`chown openclaw:openclaw "${backupDir}"`, { stdio: "ignore" }); } catch {} } catch {}
|
||
|
||
switch (choice.value) {
|
||
case 'create-config': {
|
||
console.log(`\n${C.cyan}正在创建配置备份...${C.reset}`);
|
||
try {
|
||
await ocCmd('backup', 'create', '--only-config', '--no-include-workspace');
|
||
console.log(`${C.green}✅ 配置备份已创建${C.reset}\n`);
|
||
} catch (e) {
|
||
console.log(`${C.yellow}⚠️ 备份功能需要 OpenClaw v2026.3.8+${C.reset}\n`);
|
||
}
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
}
|
||
case 'create-full': {
|
||
console.log(`\n${C.cyan}正在创建完整备份...${C.reset}`);
|
||
try {
|
||
await ocCmd('backup', 'create', '--no-include-workspace');
|
||
console.log(`${C.green}✅ 完整备份已创建${C.reset}\n`);
|
||
} catch (e) {
|
||
console.log(`${C.yellow}⚠️ 备份失败${C.reset}\n`);
|
||
}
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
}
|
||
case 'verify': {
|
||
const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.tar.gz')).sort().reverse();
|
||
if (files.length === 0) {
|
||
console.log(`${C.yellow}未找到备份文件${C.reset}\n`);
|
||
} else {
|
||
const latest = `${backupDir}/${files[0]}`;
|
||
console.log(`${C.cyan}验证备份: ${latest}${C.reset}`);
|
||
try {
|
||
await ocCmd('backup', 'verify', latest);
|
||
} catch {}
|
||
}
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
}
|
||
case 'list': {
|
||
const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.tar.gz') || f.endsWith('.json')).sort().reverse();
|
||
if (files.length === 0) {
|
||
console.log(`${C.yellow}暂无备份文件${C.reset}\n`);
|
||
} else {
|
||
console.log(`\n${C.bold}备份文件列表:${C.reset}`);
|
||
files.slice(0, 10).forEach(f => {
|
||
const stat = fs.statSync(`${backupDir}/${f}`);
|
||
console.log(` ${C.dim}${f} (${(stat.size / 1024).toFixed(1)} KB)${C.reset}`);
|
||
});
|
||
console.log(`${C.dim}\n备份目录: ${backupDir}${C.reset}\n`);
|
||
}
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
}
|
||
case 'restore': {
|
||
const files = fs.readdirSync(backupDir).filter(f => f.endsWith('.json')).sort().reverse();
|
||
if (files.length === 0) {
|
||
console.log(`${C.yellow}未找到配置备份文件${C.reset}\n`);
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
}
|
||
|
||
const fileChoice = await select({
|
||
title: '选择备份文件',
|
||
showSearch: false,
|
||
items: files.slice(0, 10).map((f, i) => ({
|
||
key: String(i + 1),
|
||
label: f,
|
||
desc: '',
|
||
value: f,
|
||
})),
|
||
});
|
||
|
||
if (!fileChoice) break;
|
||
|
||
const ok = await confirm({ prompt: '确认恢复此备份?', defaultYes: false });
|
||
if (ok) {
|
||
try {
|
||
fs.copyFileSync(`${backupDir}/${fileChoice.value}`, CONFIG_FILE);
|
||
console.log(`\n${C.green}✅ 配置已恢复${C.reset}\n`);
|
||
await askRestart();
|
||
} catch (e) {
|
||
console.log(`${C.red}恢复失败: ${e.message}${C.reset}\n`);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 主函数
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function main() {
|
||
const command = process.argv[2];
|
||
|
||
if (command === 'model') {
|
||
await handleModelConfig();
|
||
return;
|
||
}
|
||
|
||
if (command === 'status') {
|
||
await runCommand('/etc/init.d/openclaw', ['status_service']);
|
||
return;
|
||
}
|
||
|
||
if (command === 'restart') {
|
||
const ok = await confirm({ prompt: '确认重启 OpenClaw 服务?' });
|
||
if (ok) await runCommand('/etc/init.d/openclaw', ['restart']);
|
||
return;
|
||
}
|
||
|
||
// 交互式主菜单
|
||
while (true) {
|
||
const choice = await showMainMenu();
|
||
|
||
if (!choice || choice.value === 'quit') {
|
||
console.log(`\n${C.green}再见!${C.reset}\n`);
|
||
break;
|
||
}
|
||
|
||
switch (choice.value) {
|
||
case 'model':
|
||
await handleModelConfig();
|
||
break;
|
||
case 'set-active-model':
|
||
await handleSetActiveModel();
|
||
break;
|
||
case 'channels':
|
||
await handleChannels();
|
||
break;
|
||
case 'health':
|
||
await handleHealthCheck();
|
||
break;
|
||
case 'logs':
|
||
resetRenderCount();
|
||
console.log(`\n${C.cyan}=== OpenClaw 日志 ===${C.reset}\n`);
|
||
await runCommand('logread', ['-e', 'openclaw']);
|
||
console.log('');
|
||
await input({ prompt: '按回车继续', defaultValue: '' });
|
||
break;
|
||
case 'restart':
|
||
resetRenderCount();
|
||
await restartGateway();
|
||
break;
|
||
case 'advanced':
|
||
await handleAdvancedConfig();
|
||
break;
|
||
case 'reset':
|
||
await handleReset();
|
||
break;
|
||
case 'show-config':
|
||
await handleShowConfig();
|
||
break;
|
||
case 'backup':
|
||
await handleBackup();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
main().catch(e => {
|
||
console.error(`${C.red}错误:${C.reset}`, e.message);
|
||
process.exit(1);
|
||
});
|