register-remote-agent.js 6.96 KB
#!/usr/bin/env node
/**
 * register-remote-agent.js — 远程 Agent 自动注册
 *
 * 用法:
 *   node scripts/register-remote-agent.js <BRIDGE_HOST> [OPTIONS]
 *
 * 示例:
 *   node scripts/register-remote-agent.js 192.168.1.100
 *   node scripts/register-remote-agent.js 10.0.0.5 --port 9002 --name remote-claude --type coder
 *   node scripts/register-remote-agent.js ws://10.0.0.5:9001 --server http://localhost:3000
 *
 * 功能:
 *   1. 探测远程 Bridge 健康状态
 *   2. 向主控 Server 注册 Agent
 *   3. 验证注册结果
 */

const http = require('http');
const url = require('url');

// ─── CLI 解析 ───

const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
  console.log(`
用法: node register-remote-agent.js <BRIDGE_HOST> [OPTIONS]

  BRIDGE_HOST    远程 Bridge 地址 (IP / hostname / ws://...)

选项:
  --port <PORT>       Bridge WebSocket 端口 (默认 9001)
  --name <NAME>       Agent 名称 (默认 remote-agent)
  --type <TYPE>       Agent 类型: coder | validator | executor (默认 coder)
  --protocol <PROTO>  协议: acp | claude-stream (默认 acp)
  --server <URL>      主控 Server 地址 (默认 http://localhost:3000)
  --description <D>   Agent 描述
  --skip-health       跳过 Bridge 健康检查
  --unregister        注销而非注册
`);
  process.exit(0);
}

function opt(name, fallback) {
  const i = args.indexOf(`--${name}`);
  return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
}

const BRIDGE_HOST_RAW = args.find(a => !a.startsWith('--'));
const PORT = opt('port', '9001');
const NAME = opt('name', 'remote-agent');
const TYPE = opt('type', 'coder');
const PROTOCOL = opt('protocol', 'acp');
const SERVER = opt('server', 'http://localhost:3000');
const DESC = opt('description', `远程 Agent (${BRIDGE_HOST_RAW}:${PORT})`);
const SKIP_HEALTH = args.includes('--skip-health');
const UNREGISTER = args.includes('--unregister');

// 解析 Bridge 地址
let bridgeHost, bridgePort;
if (BRIDGE_HOST_RAW.startsWith('ws://') || BRIDGE_HOST_RAW.startsWith('wss://')) {
  const parsed = new URL(BRIDGE_HOST_RAW);
  bridgeHost = parsed.hostname;
  bridgePort = parsed.port || PORT;
} else if (BRIDGE_HOST_RAW.includes(':')) {
  [bridgeHost, bridgePort] = BRIDGE_HOST_RAW.split(':');
} else {
  bridgeHost = BRIDGE_HOST_RAW;
  bridgePort = PORT;
}

const WS_URL = `ws://${bridgeHost}:${bridgePort}`;

// ─── HTTP 工具 ───

function request(method, urlStr, body = null) {
  return new Promise((resolve, reject) => {
    const { hostname, port, pathname } = new URL(urlStr);
    const data = body ? JSON.stringify(body) : null;

    const req = http.request({
      hostname, port, path: pathname, method,
      headers: {
        'Content-Type': 'application/json',
        ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
      },
      timeout: 5000,
    }, (res) => {
      let buf = '';
      res.on('data', c => buf += c);
      res.on('end', () => {
        try { resolve({ status: res.statusCode, data: JSON.parse(buf) }); }
        catch { resolve({ status: res.statusCode, data: buf }); }
      });
    });

    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
    if (data) req.write(data);
    req.end();
  });
}

function ok(msg) { console.log(`  \x1b[32m✅\x1b[0m ${msg}`); }
function fail(msg) { console.log(`  \x1b[31m❌\x1b[0m ${msg}`); }
function step(n, msg) { console.log(`\n\x1b[36m[${n}]\x1b[0m ${msg}`); }

// ─── 主流程 ───

async function main() {
  console.log(`
╔═══════════════════════════════════════╗
║  远程 Agent ${UNREGISTER ? '注销' : '注册'}                     ║
╠═══════════════════════════════════════╣
║  Bridge:  ${WS_URL.padEnd(27)}║
║  Agent:   ${NAME.padEnd(27)}║
║  Server:  ${SERVER.padEnd(27)}║
╚═══════════════════════════════════════╝`);

  // ── 注销模式 ──
  if (UNREGISTER) {
    step(1, `注销 ${NAME}`);
    try {
      const res = await request('DELETE', `${SERVER}/api/agents/${NAME}`);
      if (res.status === 200) {
        ok(`${NAME} 已注销`);
      } else {
        fail(`注销失败: ${JSON.stringify(res.data)}`);
        process.exit(1);
      }
    } catch (e) {
      fail(`Server 不可达: ${e.message}`);
      process.exit(1);
    }

    step(2, '验证');
    const check = await request('GET', `${SERVER}/api/agents/${NAME}`);
    if (check.status === 404 || check.data?.error) {
      ok('确认已注销');
    } else {
      fail('仍然存在');
    }
    return;
  }

  // ── 注册模式 ──

  // Step 1: 检查 Server 可达
  step(1, '检查主控 Server');
  try {
    const res = await request('GET', `${SERVER}/api/agents`);
    ok(`Server 可达,当前 ${res.data?.length || '?'} 个 Agent`);
  } catch (e) {
    fail(`Server 不可达 (${SERVER}): ${e.message}`);
    process.exit(1);
  }

  // Step 2: 探测 Bridge
  if (!SKIP_HEALTH) {
    step(2, `探测 Bridge (${bridgeHost}:${bridgePort})`);
    try {
      const res = await request('GET', `http://${bridgeHost}:${bridgePort}/health`);
      ok(`Bridge 在线: ${JSON.stringify(res.data)}`);
    } catch (e) {
      fail(`Bridge 不可达: ${e.message}`);
      console.log('    提示: 使用 --skip-health 跳过此检查');
      process.exit(1);
    }
  } else {
    step(2, '跳过 Bridge 健康检查 (--skip-health)');
  }

  // Step 3: 注册
  step(3, `注册 Agent: ${NAME}`);
  const payload = {
    name: NAME,
    type: TYPE,
    protocol: PROTOCOL,
    transport: 'websocket',
    transportConfig: { url: WS_URL },
    launcher: 'none',
    description: DESC,
  };

  try {
    const res = await request('POST', `${SERVER}/api/agents`, payload);
    if (res.status === 200 || res.status === 201) {
      ok(`注册成功`);
    } else {
      fail(`注册失败: ${JSON.stringify(res.data)}`);
      process.exit(1);
    }
  } catch (e) {
    fail(`请求失败: ${e.message}`);
    process.exit(1);
  }

  // Step 4: 验证
  step(4, '验证注册结果');
  try {
    const res = await request('GET', `${SERVER}/api/agents/${NAME}`);
    if (res.data?.name === NAME || res.data?.agent) {
      ok(`Agent "${NAME}" 已注册`);
      console.log(`    类型: ${TYPE}`);
      console.log(`    协议: ${PROTOCOL}`);
      console.log(`    地址: ${WS_URL}`);
    } else {
      fail(`验证失败: ${JSON.stringify(res.data)}`);
    }
  } catch (e) {
    fail(`验证请求失败: ${e.message}`);
  }

  console.log(`
\x1b[32m注册完成!\x1b[0m

后续操作:
  # 查看所有 Agent
  curl ${SERVER}/api/agents

  # 健康检查
  curl ${SERVER}/api/agents/${NAME}/health

  # 注销
  node scripts/register-remote-agent.js ${BRIDGE_HOST_RAW} --name ${NAME} --unregister
`);
}

main().catch(e => {
  console.error('💥', e.message);
  process.exit(1);
});