// Genesis - DetectorAgent 检测与发现 Agent // 专职负责环境资源探测、外部Agent发现、运行时能力盘点 import { BaseAgent } from './base-agent.js' import { MessageType, createMessage, Priority } from '../shared/types.js' import { execSync } from 'child_process' import fs from 'fs' import path from 'path' import os from 'os' const IS_WIN = process.platform === 'win32' const IS_MAC = process.platform === 'darwin' export class DetectorAgent extends BaseAgent { constructor(id = 'detector', config = {}) { super(id, config) /** @type {Map} */ this._probeCache = new Map() this._probeCacheTTL = config.probeCacheTTL || 300_000 // 5 min } async onInit(ctx) { await super.onInit(ctx) // 订阅探测相关消息 ctx.bus.subscribe(MessageType.TOOL_RESULT, this._handleToolResult.bind(this), { filter: { target: this.id } }) } async onMessage(msg) { await super.onMessage(msg) const action = msg.payload?.action switch (action) { case 'detect-agents': return this._detectExternalAgents(msg) case 'probe-port': return this._probePort(msg) case 'probe-command': return this._probeCommand(msg) case 'scan-environment': return this._scanEnvironment(msg) default: this._publishResult(msg, { error: `Unknown action: ${action}` }) } } // ── 外部 Agent 探测 ────────────────────────────────────────────── async _detectExternalAgents(msg) { const cacheKey = 'external-agents' const cached = this._getCache(cacheKey) if (cached) return this._publishResult(msg, cached) const probes = [ { id: 'claude-code', probe: () => this._probeClaudeCode() }, { id: 'codex', probe: () => this._probeCodex() }, { id: 'ollama', probe: () => this._probeOllama() }, ] const results = [] for (const def of probes) { try { const result = def.probe() results.push({ id: def.id, ...result }) } catch (err) { results.push({ id: def.id, available: false, error: err.message }) } } this._setCache(cacheKey, results) this._publishResult(msg, results) } _probeClaudeCode() { const cli = this._findInPath('claude') if (cli) { const ver = this._tryExec('claude --version') || 'unknown' return { available: true, version: ver, invokeType: 'cli', invokeCmd: 'claude', notes: `CLI: ${cli}` } } const configDir = path.join(os.homedir(), '.claude') if (fs.existsSync(configDir)) { return { available: true, version: 'config-only', invokeType: 'cli', invokeCmd: 'claude', notes: `Config: ${configDir}` } } return { available: false } } _probeCodex() { const cli = this._findInPath('codex') if (cli) { const ver = this._tryExec('codex --version') || 'unknown' return { available: true, version: ver, invokeType: 'cli', invokeCmd: 'codex', notes: `CLI: ${cli}` } } return { available: false } } _probeOllama() { const cli = this._findInPath('ollama') if (cli) { const models = this._tryExec('ollama list') || '' return { available: true, version: 'installed', invokeType: 'cli', invokeCmd: 'ollama', notes: `Models: ${models.split('\n').length - 1}` } } return { available: false } } // ── 端口/命令探测 ──────────────────────────────────────────────── async _probePort(msg) { const { port } = msg.payload || {} if (!port) return this._publishResult(msg, { error: 'Missing port' }) const result = this._isPortListening(Number(port)) this._publishResult(msg, { port, listening: result }) } async _probeCommand(msg) { const { command } = msg.payload || {} if (!command) return this._publishResult(msg, { error: 'Missing command' }) const found = this._findInPath(command) this._publishResult(msg, { command, found: !!found, path: found }) } async _scanEnvironment(msg) { const info = { platform: process.platform, arch: process.arch, nodeVersion: process.version, homedir: os.homedir(), tmpdir: os.tmpdir(), cpus: os.cpus().length, totalMemory: os.totalmem(), freeMemory: os.freemem(), } this._publishResult(msg, info) } // ── 工具函数 ────────────────────────────────────────────────────── _findInPath(name) { try { const cmd = IS_WIN ? `where ${name}` : `which ${name}` const result = execSync(cmd, { timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }) return result.trim().split('\n')[0]?.trim() || null } catch { return null } } _tryExec(cmd) { try { return execSync(cmd, { timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() } catch { return null } } _isPortListening(port) { try { if (IS_WIN) { const out = execSync(`netstat -ano | findstr ":${port} "`, { timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }) return out.includes(`0.0.0.0:${port}`) || out.includes(`127.0.0.1:${port}`) || out.includes(`[::]:${port}`) } const out = execSync(`lsof -iTCP:${port} -sTCP:LISTEN -n -P 2>/dev/null`, { timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }) return out.trim().length > 0 } catch { return false } } _getCache(key) { const entry = this._probeCache.get(key) if (!entry) return null if (Date.now() - entry.ts > this._probeCacheTTL) { this._probeCache.delete(key); return null } return entry.data } _setCache(key, data) { this._probeCache.set(key, { data, ts: Date.now() }) } _publishResult(msg, result) { this.ctx?.bus?.publish(createMessage(MessageType.TOOL_RESULT, this.id, msg.source, { correlationId: msg.id, result, }, { priority: Priority.NORMAL })) } }