Files
ONE-OS/axhub-make/scripts/switch-agent.mjs
王冕 a27e3b8e43 feat: sync full workspace including web modules, docs, and configurations to Gitea
Optimized the root .gitignore to exclude virtual environments, node modules,
and temp folders to ensure clean and lightweight version tracking.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 18:12:25 +08:00

215 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
/**
* switch-agent.mjs — Switch the active cc-connect agent.
*
* Usage:
* node scripts/switch-agent.mjs <agent> # Switch to agent (codex, claudecode, gemini)
* node scripts/switch-agent.mjs --status # Show current active agent
*
* Strategy:
* 1. Try HTTP API (dev server has write permissions to ~/.cc-connect/)
* 2. Fall back to direct file manipulation if HTTP fails
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
import { execSync } from 'child_process';
const SUPPORTED_AGENTS = ['codex', 'claudecode', 'gemini'];
const ALIASES = { claude: 'claudecode', cc: 'claudecode', gem: 'gemini', cx: 'codex' };
// Read dev server info to get the port
function getDevServerPort() {
const candidates = [
path.join(process.cwd(), '.axhub', 'make', '.dev-server-info.json'),
path.resolve(import.meta.dirname, '..', '.axhub', 'make', '.dev-server-info.json'),
];
for (const p of candidates) {
try {
const info = JSON.parse(fs.readFileSync(p, 'utf8'));
if (info?.port) return info.port;
} catch { /* ignore */ }
}
return 51720; // default
}
// ---------------------------------------------------------------------------
// HTTP-based switching (preferred — dev server has write permissions)
// ---------------------------------------------------------------------------
async function httpSwitch(target) {
const port = getDevServerPort();
const url = `http://127.0.0.1:${port}/api/cc-connect/switch-agent`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent: target }),
signal: AbortSignal.timeout(15000),
});
const data = await res.json();
if (data.success) {
return { ok: true, detail: data.message || `已切换到 ${target}` };
}
return { ok: false, detail: data.error || '切换失败' };
}
async function httpStatus() {
const port = getDevServerPort();
const url = `http://127.0.0.1:${port}/api/cc-connect/active-agent`;
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
return await res.json();
}
// ---------------------------------------------------------------------------
// File-based switching (fallback — requires write permissions to ~/.cc-connect/)
// ---------------------------------------------------------------------------
const CC_CONNECT_DIR = path.join(os.homedir(), '.cc-connect');
const STATE_PATH = path.join(CC_CONNECT_DIR, 'axhub-weixin-state.json');
const CONFIG_PATH = path.join(CC_CONNECT_DIR, 'config.toml');
function readState() {
if (!fs.existsSync(STATE_PATH)) return null;
try {
const parsed = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
if (parsed?.version === 1 && parsed?.activeAgent && parsed?.weixinOptions?.token) {
return parsed;
}
} catch { /* ignore */ }
return null;
}
function escapeToml(value) {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
}
function fileSwitch(target) {
const state = readState();
if (!state) {
return { ok: false, detail: '未找到微信绑定状态,请先在管理后台扫码绑定' };
}
if (state.activeAgent === target) {
return { ok: true, detail: `当前已是 ${target},无需切换` };
}
state.activeAgent = target;
fs.mkdirSync(CC_CONNECT_DIR, { recursive: true });
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), 'utf8');
const optionLines = Object.entries(state.weixinOptions)
.map(([k, v]) => `${k} = "${escapeToml(String(v))}"`)
.join('\n');
const config = `# Auto-generated by Axhub Make
# language = "zh"
[log]
level = "info"
[[projects]]
name = "axhub-${escapeToml(target)}"
[projects.agent]
type = "${escapeToml(target)}"
[projects.agent.options]
work_dir = "${escapeToml(state.workDir)}"
mode = "yolo"
[[projects.platforms]]
type = "weixin"
[projects.platforms.options]
${optionLines}
`;
fs.writeFileSync(CONFIG_PATH, config, 'utf8');
const ccCommand = process.platform === 'win32' ? 'cc-connect.cmd' : 'cc-connect';
try { execSync(`${ccCommand} daemon stop`, { stdio: 'ignore', timeout: 10000 }); } catch { /* */ }
try { execSync(`${ccCommand} daemon uninstall`, { stdio: 'ignore', timeout: 10000 }); } catch { /* */ }
try {
execSync(`${ccCommand} daemon install --config "${CONFIG_PATH}"`, { stdio: 'inherit', timeout: 20000 });
return { ok: true, detail: `已切换到 ${target}` };
} catch (error) {
return { ok: false, detail: `配置已更新,但 daemon 重启失败: ${error.message}` };
}
}
function fileStatus() {
const state = readState();
if (state) {
return { activeAgent: state.activeAgent, configuredAgents: state.configuredAgents };
}
return null;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === '--help') {
console.log('用法: node scripts/switch-agent.mjs <agent>');
console.log(`可用: ${SUPPORTED_AGENTS.join(', ')}`);
console.log('别名: claude→claudecode, gem→gemini, cx→codex');
console.log(' node scripts/switch-agent.mjs --status');
process.exit(0);
}
if (args[0] === '--status') {
const data = fileStatus();
if (data) {
console.log(JSON.stringify(data, null, 2));
} else {
try {
const httpData = await httpStatus();
console.log(JSON.stringify(httpData, null, 2));
} catch {
console.log('未找到微信绑定状态');
}
}
process.exit(0);
}
let target = args[0].toLowerCase();
if (ALIASES[target]) target = ALIASES[target];
if (!SUPPORTED_AGENTS.includes(target)) {
console.error(`❌ 不支持的 agent: ${target}`);
console.error(`可用: ${SUPPORTED_AGENTS.join(', ')}`);
process.exit(1);
}
// Strategy: try file-based first, fall back to HTTP API
try {
const result = fileSwitch(target);
if (result.ok) {
console.log(`${result.detail}`);
process.exit(0);
} else {
console.error(`❌ 本地切换失败: ${result.detail}`);
console.log('尝试 HTTP API 切换...');
}
} catch (fileError) {
console.log(`本地文件操作受限 (${fileError.message}),尝试 HTTP API 切换...`);
}
// Fallback to HTTP API
try {
const result = await httpSwitch(target);
if (result.ok) {
console.log(`${result.detail}`);
} else {
console.error(`${result.detail}`);
process.exit(1);
}
} catch (httpError) {
console.error(`❌ HTTP API 也不可达: ${httpError.message}`);
console.error('请在管理后台的微信连接对话框中进行切换');
process.exit(1);
}