291 lines
8.6 KiB
JavaScript
291 lines
8.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// federation-agent/agent.js · v1.0
|
|
// 光湖联邦托管包 · 本地 Agent
|
|
// 用户一句唤醒语 → 自动装脑 → 连上铸渊服务器 → 被托管
|
|
// ICE-GL-ZY001 · D129 · 2026-06-10
|
|
|
|
const WebSocket = require("ws");
|
|
const { execSync, exec } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
const crypto = require("crypto");
|
|
|
|
// ===== 配置 =====
|
|
const CONFIG_PATH = path.join(os.homedir(), ".guanghu", "federation-agent.json");
|
|
const DEFAULT_CONFIG = {
|
|
server: "wss://guanghubingshuo.com/agent",
|
|
gatekeeper: "http://43.156.237.110:3911",
|
|
gatekeeperKey: "",
|
|
agentName: "",
|
|
personaId: "",
|
|
userId: "",
|
|
workDir: process.cwd(),
|
|
heartbeatInterval: 30000,
|
|
reconnectDelay: 5000,
|
|
maxReconnectDelay: 60000,
|
|
confirmDangerousOps: true,
|
|
};
|
|
|
|
// ===== 密钥管理 =====
|
|
function loadOrCreateConfig() {
|
|
const dir = path.dirname(CONFIG_PATH);
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
|
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
}
|
|
|
|
const config = { ...DEFAULT_CONFIG };
|
|
config.agentId = "AGENT-" + crypto.randomBytes(6).toString("hex");
|
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
return config;
|
|
}
|
|
|
|
// ===== 命令执行 =====
|
|
function execCommand(cmd, opts = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
exec(cmd, { cwd: opts.cwd || process.cwd(), timeout: opts.timeout || 30000, ...opts },
|
|
(err, stdout, stderr) => {
|
|
resolve({ ok: !err, stdout: stdout.trim(), stderr: stderr.trim(), code: err ? err.code : 0 });
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===== HLDP 指令解析 =====
|
|
function parseHLDP(command) {
|
|
// ⊢ 前缀 = 必须执行的锁定指令
|
|
const locked = command.startsWith("⊢");
|
|
const clean = command.replace(/^⊢\s*/, "").trim();
|
|
|
|
// 解析 HLDP 操作
|
|
if (clean.startsWith("git ")) {
|
|
return { type: "git", cmd: clean, locked };
|
|
}
|
|
if (clean.startsWith("HLDP://")) {
|
|
return { type: "hldp", path: clean.replace("HLDP://", ""), locked };
|
|
}
|
|
if (clean === "health") {
|
|
return { type: "health", locked };
|
|
}
|
|
if (clean.startsWith("read ")) {
|
|
return { type: "read", file: clean.replace("read ", "").trim(), locked };
|
|
}
|
|
if (clean.startsWith("exec ")) {
|
|
return { type: "exec", cmd: clean.replace("exec ", "").trim(), locked };
|
|
}
|
|
return { type: "raw", cmd: clean, locked };
|
|
}
|
|
|
|
// ===== 危险操作检测 =====
|
|
function isDangerous(op) {
|
|
const dangerous = ["rm -rf", "git push --force", "rm -r", "> /dev/", "dd if=", "mkfs.", "format "];
|
|
return dangerous.some(d => (op.cmd || "").includes(d));
|
|
}
|
|
|
|
// ===== Agent 主逻辑 =====
|
|
class FederationAgent {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.ws = null;
|
|
this.reconnectAttempts = 0;
|
|
this.heartbeatTimer = null;
|
|
this.connected = false;
|
|
}
|
|
|
|
log(level, msg) {
|
|
const ts = new Date().toISOString();
|
|
console.log(`[${ts}] [${level}] ${msg}`);
|
|
}
|
|
|
|
async start() {
|
|
this.log("INFO", `光湖联邦托管 Agent 启动`);
|
|
this.log("INFO", `AgentID: ${this.config.agentId}`);
|
|
this.log("INFO", `Persona: ${this.config.personaId || "(未注册)"}`);
|
|
this.log("INFO", `用户: ${this.config.userId || "(未指定)"}`);
|
|
this.log("INFO", `工作目录: ${this.config.workDir}`);
|
|
|
|
this.connect();
|
|
}
|
|
|
|
connect() {
|
|
const wsUrl = `${this.config.server}?agentId=${this.config.agentId}&persona=${this.config.personaId}&user=${this.config.userId}`;
|
|
this.log("INFO", `连接服务器: ${wsUrl}`);
|
|
|
|
try {
|
|
this.ws = new WebSocket(wsUrl);
|
|
|
|
this.ws.on("open", () => {
|
|
this.connected = true;
|
|
this.reconnectAttempts = 0;
|
|
this.log("INFO", "已连接 · 等待主控指令");
|
|
this.startHeartbeat();
|
|
this.register();
|
|
});
|
|
|
|
this.ws.on("message", async (data) => {
|
|
try {
|
|
const msg = JSON.parse(data.toString());
|
|
await this.handleMessage(msg);
|
|
} catch (e) {
|
|
this.log("ERROR", `消息解析失败: ${e.message}`);
|
|
}
|
|
});
|
|
|
|
this.ws.on("close", (code, reason) => {
|
|
this.connected = false;
|
|
this.stopHeartbeat();
|
|
this.log("WARN", `连接断开 (${code}): ${reason}`);
|
|
this.scheduleReconnect();
|
|
});
|
|
|
|
this.ws.on("error", (err) => {
|
|
this.log("ERROR", `连接错误: ${err.message}`);
|
|
});
|
|
} catch (e) {
|
|
this.log("ERROR", `连接失败: ${e.message}`);
|
|
this.scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
register() {
|
|
this.send({
|
|
type: "register",
|
|
agentId: this.config.agentId,
|
|
personaId: this.config.personaId,
|
|
userId: this.config.userId,
|
|
hostname: os.hostname(),
|
|
platform: os.platform(),
|
|
workDir: this.config.workDir,
|
|
version: "1.0.0",
|
|
});
|
|
}
|
|
|
|
startHeartbeat() {
|
|
this.stopHeartbeat();
|
|
this.heartbeatTimer = setInterval(() => {
|
|
this.send({ type: "heartbeat", ts: Date.now() });
|
|
}, this.config.heartbeatInterval);
|
|
}
|
|
|
|
stopHeartbeat() {
|
|
if (this.heartbeatTimer) {
|
|
clearInterval(this.heartbeatTimer);
|
|
this.heartbeatTimer = null;
|
|
}
|
|
}
|
|
|
|
scheduleReconnect() {
|
|
const delay = Math.min(
|
|
this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts),
|
|
this.config.maxReconnectDelay
|
|
);
|
|
this.reconnectAttempts++;
|
|
this.log("INFO", `${(delay / 1000).toFixed(0)}秒后重连 (第${this.reconnectAttempts}次)`);
|
|
setTimeout(() => this.connect(), delay);
|
|
}
|
|
|
|
send(data) {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify(data));
|
|
}
|
|
}
|
|
|
|
async handleMessage(msg) {
|
|
this.log("DEBUG", `收到指令: ${JSON.stringify(msg)}`);
|
|
|
|
switch (msg.type) {
|
|
case "command": {
|
|
const op = parseHLDP(msg.command);
|
|
await this.handleCommand(op);
|
|
break;
|
|
}
|
|
case "ping":
|
|
this.send({ type: "pong", ts: Date.now() });
|
|
break;
|
|
case "welcome":
|
|
this.log("INFO", `欢迎: ${msg.message || ""}`);
|
|
this.config.personaId = msg.personaId || this.config.personaId;
|
|
break;
|
|
default:
|
|
this.log("WARN", `未知消息类型: ${msg.type}`);
|
|
}
|
|
}
|
|
|
|
async handleCommand(op) {
|
|
// 危险操作检测
|
|
if (isDangerous(op) && this.config.confirmDangerousOps) {
|
|
this.log("WARN", `危险操作请求确认: ${op.cmd}`);
|
|
this.send({ type: "confirm_request", operation: op.cmd, agentId: this.config.agentId });
|
|
return;
|
|
}
|
|
|
|
switch (op.type) {
|
|
case "git": {
|
|
const result = await execCommand(op.cmd, { cwd: this.config.workDir });
|
|
this.send({ type: "result", command: op.cmd, ...result });
|
|
this.log("INFO", `git: ${op.cmd} → ${result.ok ? "OK" : "FAIL"}`);
|
|
break;
|
|
}
|
|
case "exec": {
|
|
const result = await execCommand(op.cmd, { cwd: this.config.workDir });
|
|
this.send({ type: "result", command: op.cmd, ...result });
|
|
break;
|
|
}
|
|
case "read": {
|
|
try {
|
|
const content = fs.readFileSync(path.resolve(this.config.workDir, op.file), "utf-8");
|
|
this.send({ type: "result", command: `read ${op.file}`, ok: true, stdout: content });
|
|
} catch (e) {
|
|
this.send({ type: "result", command: `read ${op.file}`, ok: false, stderr: e.message });
|
|
}
|
|
break;
|
|
}
|
|
case "health": {
|
|
this.send({
|
|
type: "health_report",
|
|
agentId: this.config.agentId,
|
|
personaId: this.config.personaId,
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage(),
|
|
cwd: this.config.workDir,
|
|
ts: Date.now(),
|
|
});
|
|
break;
|
|
}
|
|
default: {
|
|
this.send({ type: "result", command: op.cmd, ok: false, stderr: `未知操作类型: ${op.type}` });
|
|
}
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
this.stopHeartbeat();
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
this.connected = false;
|
|
this.log("INFO", "Agent 已停止");
|
|
}
|
|
}
|
|
|
|
// ===== 启动 =====
|
|
const config = loadOrCreateConfig();
|
|
|
|
// 从命令行参数覆盖配置
|
|
const args = process.argv.slice(2);
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === "--name" && args[i + 1]) config.agentName = args[++i];
|
|
if (args[i] === "--persona" && args[i + 1]) config.personaId = args[++i];
|
|
if (args[i] === "--user" && args[i + 1]) config.userId = args[++i];
|
|
if (args[i] === "--dir" && args[i + 1]) config.workDir = args[++i];
|
|
if (args[i] === "--server" && args[i + 1]) config.server = args[++i];
|
|
}
|
|
|
|
const agent = new FederationAgent(config);
|
|
agent.start();
|
|
|
|
process.on("SIGINT", () => { agent.stop(); process.exit(0); });
|
|
process.on("SIGTERM", () => { agent.stop(); process.exit(0); });
|