Files
BaiLongma/src/agents/detector-agent.js

179 lines
6.0 KiB
JavaScript

// 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<string, object>} */
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 }))
}
}