/**
 * Cyrene ethend - 主入口
 *
 * 提供:
 * - REST API: 服务管理、状态查询、性能分析、健康检查代理
 * - WebSocket: 实时日志推送
 * - Web UI: 管理控制台
 */

import express from 'express';
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync, spawn } from 'child_process';

import { processManager } from './process-manager.js';
import { performanceMonitor } from './performance.js';
import { SERVICES, ETHEND_PORT, LOGS_DIR, logFile, GATEWAY_URL, PLUGIN_MANAGER_URL, ADMIN_USERNAME, ADMIN_PASSWORD } from './config.js';

const AI_CORE_URL = process.env.AI_CORE_URL || 'http://localhost:8081';
const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || 'http://localhost:8091';
const VOICE_SERVICE_URL = process.env.VOICE_SERVICE_URL || 'http://localhost:8093';
const PLATFORM_BRIDGE_URL = process.env.PLATFORM_BRIDGE_URL || 'http://localhost:8095';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// ========== 初始化 ==========
const app = express();
app.use(express.json());

// 请求延时追踪 — 记录每个 HTTP 请求的实际耗时
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    performanceMonitor.recordLatency(Date.now() - start);
  });
  next();
});

// 静态文件 - Web控制台（禁用缓存，开发阶段每次刷新拿最新）
app.use(express.static(path.join(__dirname, '../public'), {
  setHeaders: (res) => {
    res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
    res.set('Pragma', 'no-cache');
    res.set('Expires', '0');
  }
}));

// ========== WebSocket ==========
const server = http.createServer(app);
const wss = new WebSocketServer({ server, path: '/ws' });

/** @type {Set<WebSocket>} */
const wsClients = new Set();

wss.on('connection', (ws) => {
  wsClients.add(ws);
  ws.on('close', () => wsClients.delete(ws));
});

/** 广播到所有WebSocket客户端 */
function broadcast(type, data) {
  const msg = JSON.stringify({ type, data, ts: Date.now() });
  for (const ws of wsClients) {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(msg);
    }
  }
}

// 日志事件 -> WebSocket广播
processManager.on('log', (serviceId, stream, text) => {
  broadcast('log', { service: serviceId, stream, text: text.trimEnd() });
});

// 状态变化
processManager.on('log', (serviceId, stream, text) => {
  if (stream === 'system') {
    broadcast('status', processManager.getStatus());
  }
});

// ========== 平台桥接实时日志流 ==========

let logStreamWs = null;
let logStreamReconnectTimer = null;

function connectPlatformBridgeLogStream() {
  if (logStreamReconnectTimer) { clearTimeout(logStreamReconnectTimer); logStreamReconnectTimer = null; }
  if (logStreamWs && (logStreamWs.readyState === WebSocket.OPEN || logStreamWs.readyState === WebSocket.CONNECTING)) return;

  const wsUrl = PLATFORM_BRIDGE_URL.replace(/^http/, 'ws') + '/ws/logs';
  console.log(`[LogStream] 连接 ${wsUrl} ...`);
  try {
    logStreamWs = new WebSocket(wsUrl);
  } catch (err) {
    console.error(`[LogStream] 创建连接失败: ${err.message}`);
    scheduleLogStreamReconnect();
    return;
  }

  logStreamWs.on('open', () => {
    console.log('[LogStream] 已连接，实时日志推送中');
  });

  logStreamWs.on('message', (raw) => {
    try {
      const entry = JSON.parse(raw.toString());
      broadcast('chat-log', entry);
    } catch {}
  });

  logStreamWs.on('close', () => {
    console.log('[LogStream] 连接断开');
    logStreamWs = null;
    scheduleLogStreamReconnect();
  });

  logStreamWs.on('error', (err) => {
    console.error(`[LogStream] 错误: ${err.message}`);
    logStreamWs = null;
    scheduleLogStreamReconnect();
  });
}

function scheduleLogStreamReconnect() {
  if (logStreamReconnectTimer) return;
  logStreamReconnectTimer = setTimeout(() => {
    logStreamReconnectTimer = null;
    connectPlatformBridgeLogStream();
  }, 5000);
}

// 启动时连接，后续 platform-bridge 重启时通过状态变化自动重连。
connectPlatformBridgeLogStream();

// 监听服务状态：platform-bridge 上线后重连。
setInterval(() => {
  if (!logStreamWs || logStreamWs.readyState === WebSocket.CLOSED || logStreamWs.readyState === WebSocket.CLOSING) {
    connectPlatformBridgeLogStream();
  }
}, 15000);

// ========== Gateway 代理辅助函数 ==========

/** 缓存的 JWT token 和过期时间 */
let cachedToken = null;
let tokenExpiry = 0;

/**
 * 获取 Gateway JWT token (通过 admin 凭据登录，缓存直到过期，支持重试)
 */
async function getGatewayToken() {
  if (cachedToken && Date.now() < tokenExpiry - 60000) {
    return cachedToken;
  }

  const maxRetries = 3;
  const baseDelay = 1000;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const resp = await fetch(`${GATEWAY_URL}/api/v1/auth/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: ADMIN_USERNAME, password: ADMIN_PASSWORD }),
        signal: AbortSignal.timeout(5000),
      });
      if (!resp.ok) {
        if (attempt < maxRetries - 1) {
          const delay = baseDelay * Math.pow(2, attempt);
          console.log(`[Gateway代理] 登录失败 (HTTP ${resp.status})，${delay / 1000}s 后重试 (${attempt + 1}/${maxRetries})...`);
          await new Promise((r) => setTimeout(r, delay));
          continue;
        }
        console.error('[Gateway代理] 登录失败:', resp.status);
        return null;
      }
      const data = await resp.json();
      cachedToken = data.token;
      tokenExpiry = data.expires ? data.expires * 1000 : Date.now() + 3600000;
      console.log('[Gateway代理] 登录成功，token 已缓存');
      return cachedToken;
    } catch (err) {
      if (attempt < maxRetries - 1) {
        const delay = baseDelay * Math.pow(2, attempt);
        console.log(`[Gateway代理] 登录异常: ${err.message}，${delay / 1000}s 后重试 (${attempt + 1}/${maxRetries})...`);
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }
      console.error('[Gateway代理] 登录异常 (已达最大重试次数):', err.message);
      return null;
    }
  }

  return null;
}

/**
 * 代理请求到 Gateway，自动携带 JWT token
 * @param {string} path - Gateway API 路径 (如 /api/v1/memory/search?user_id=...)
 * @param {object} opts - fetch 选项
 */
async function proxyToGateway(path, opts = {}) {
  const token = await getGatewayToken();
  if (!token) {
    return {
      status: 502,
      body: {
        error: '无法连接到 Gateway 认证服务',
        errorType: 'gateway_auth_failed',
        hint: '请确认 Gateway 服务已启动 (端口 8080)',
      },
    };
  }

  const url = `${GATEWAY_URL}${path}`;
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
    ...opts.headers,
  };

  try {
    const resp = await fetch(url, { ...opts, headers, signal: AbortSignal.timeout(15000) });
    const body = await resp.json().catch(() => null);
    return { status: resp.status, body };
  } catch (err) {
    const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
    return {
      status: 502,
      body: {
        error: `Gateway 不可达: ${err.message}`,
        errorType: isConnRefused ? 'gateway_not_running' : 'gateway_unreachable',
        hint: isConnRefused
          ? 'Gateway 服务未启动，请先在「服务管理」面板中启动 Gateway'
          : 'Gateway 服务无响应，请检查网络连接和服务状态',
      },
    };
  }
}

/**
 * 代理请求到 AI-Core
 */
async function proxyToAICore(path, opts = {}) {
  const url = `${AI_CORE_URL}${path}`;
  try {
    const resp = await fetch(url, {
      ...opts,
      headers: { 'Content-Type': 'application/json', ...opts.headers },
      signal: AbortSignal.timeout(10000),
    });
    const body = await resp.json().catch(() => null);
    return { status: resp.status, body };
  } catch (err) {
    return {
      status: 502,
      body: {
        error: `AI-Core 不可达: ${err.message}`,
        errorType: 'ai_core_unreachable',
        hint: 'AI-Core 服务未启动，请先在「服务管理」面板中启动 AI-Core',
      },
    };
  }
}

// ========== REST API 路由 ==========

// ---- 健康检查 ----
app.get('/api/health', (_req, res) => {
  res.json({
    status: 'ok',
    service: 'cyrene-ethend',
    uptime: process.uptime(),
    wsClients: wsClients.size,
  });
});

// ---- ethend 自重启 (Windows-safe) ----
app.post('/api/ethend/restart', (_req, res) => {
  res.json({ success: true, message: 'ethend 正在重启...' });

  // 步骤1: 立即关闭 HTTP 服务器，释放端口 9090
  server.close(() => {
    console.log('ethend HTTP 服务已关闭');
  });

  // 步骤2: 延迟 2 秒确保端口完全释放，然后启动新进程
  setTimeout(() => {
    const scriptPath = path.join(__dirname, 'index.js');
    // 注意: 不使用 detached:true
    //   - detached 在 Windows 上会创建新进程组→弹出 cmd 窗口
    //   - Windows 默认子进程不随父进程退出而终止，所以不需要 detached
    //   - windowsHide:true 确保即使有窗口也被隐藏
    const child = spawn(process.execPath, [scriptPath], {
      cwd: path.join(ROOT, 'ethend'),
      detached: false,
      stdio: 'ignore',
      windowsHide: true,
    });
    child.unref();
    process.exit(0);
  }, 2000);
});

// ---- 仪表盘数据 (必须在 /api/services/:id 之前以避免路由冲突) ----
app.get('/api/dashboard', async (_req, res) => {
  try {
    const [services, perfSnapshot, sessionsResult] = await Promise.all([
      Promise.resolve(processManager.getStatus()),
      performanceMonitor.getSnapshot(),
      proxyToGateway('/api/v1/admin/sessions').catch(() => ({ status: 502, body: { sessions: [], total: 0 } })),
    ]);

    let runningCount = 0, totalCpu = 0, totalMem = 0;
    for (const svc of Object.values(services)) {
      if (svc.status === 'running') runningCount++;
    }
    for (const p of Object.values(perfSnapshot)) {
      totalCpu += p.cpu || 0;
      totalMem += p.mem || 0;
    }

    const sessionsData = sessionsResult.body || {};
    const activeSessions = sessionsData.total || sessionsData.sessions?.length || 0;
    let totalMessages = 0;
    if (sessionsData.sessions) {
      for (const s of sessionsData.sessions) {
        totalMessages += (s.message_count || 0);
      }
    }

    let memoryCount = null;
    try {
      const token = await getGatewayToken();
      if (token) {
        const memResp = await fetch(`${GATEWAY_URL}/api/v1/memory?user_id=admin`, {
          headers: { 'Authorization': `Bearer ${token}` },
          signal: AbortSignal.timeout(5000),
        });
        if (memResp.ok) {
          const memData = await memResp.json();
          memoryCount = Array.isArray(memData) ? memData.length : (memData.memories ? memData.memories.length : null);
        }
      }
    } catch { /* 忽略 */ }

    // 数据库状态（通过 TCP 端口检查 Docker 容器是否在运行）
    let dbStatus = { checked: false };
    try {
      const port5432Alive = await isPortOpen(5432, IS_DOCKER ? 'postgres' : undefined);
      dbStatus = { checked: true, postgresAlive: port5432Alive };
    } catch { /* 忽略 */ }

    const sysMem = process.memoryUsage();

    res.json({
      timestamp: Date.now(),
      services: { total: Object.keys(services).length, running: runningCount, list: services },
      performance: { totalCpu: Math.round(totalCpu * 100) / 100, totalMem: Math.round(totalMem * 100) / 100, perService: perfSnapshot },
      sessions: { active: activeSessions, totalMessages },
      memory: { total: memoryCount },
      database: dbStatus,
      system: { heapUsedMB: Math.round(sysMem.heapUsed / 1024 / 1024 * 100) / 100, heapTotalMB: Math.round(sysMem.heapTotal / 1024 / 1024 * 100) / 100, uptime: process.uptime() },
    });
  } catch (err) {
    res.status(500).json({ error: `获取仪表盘数据失败: ${err.message}` });
  }
});

// ---- 会话监看代理 (必须在 /api/services/:id 之前) ----
app.get('/api/sessions', async (_req, res) => {
  const result = await proxyToGateway('/api/v1/admin/sessions');
  res.status(result.status).json(result.body);
});

// GET /api/sessions/active — 获取按用户分组的活跃会话 (管理员权限)
app.get('/api/sessions/active', async (_req, res) => {
  const result = await proxyToGateway('/api/v1/admin/sessions/active');
  res.status(result.status).json(result.body);
});

app.get('/api/sessions/:id', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/sessions/${req.params.id}`);
  res.status(result.status).json(result.body);
});

// ---- 记忆管理代理 (必须在 /api/services/:id 之前) ----
app.get('/api/memory/search', async (req, res) => {
  const { user_id, q } = req.query;
  if (!user_id || !q) return res.status(400).json({ error: '缺少 user_id 或 q 参数' });
  const qs = new URLSearchParams({ user_id, q }).toString();
  const result = await proxyToGateway(`/api/v1/memory/search?${qs}`);
  res.status(result.status).json(result.body);
});

app.get('/api/memory/list', async (req, res) => {
  const { user_id, limit, offset } = req.query;
  if (!user_id) return res.status(400).json({ error: '缺少 user_id 参数' });
  const qs = new URLSearchParams({ user_id });
  if (limit) qs.set('limit', limit);
  if (offset) qs.set('offset', offset);
  const result = await proxyToGateway(`/api/v1/memory?${qs.toString()}`);
  res.status(result.status).json(result.body);
});

app.post('/api/memory/add', async (req, res) => {
  const { user_id, content, category, priority } = req.body;
  if (!user_id || !content) return res.status(400).json({ error: '缺少 user_id 或 content' });
  const result = await proxyToGateway('/api/v1/memory', {
    method: 'POST',
    body: JSON.stringify({ user_id, content, category: category || 'other', priority: priority || 1 }),
  });
  res.status(result.status).json(result.body);
});

app.delete('/api/memory/:id', async (req, res) => {
  const { id } = req.params;
  if (!id) return res.status(400).json({ error: '缺少 memory id' });
  const qs = new URLSearchParams({ id }).toString();
  const result = await proxyToGateway(`/api/v1/memory?${qs}`, { method: 'DELETE' });
  res.status(result.status).json(result.body);
});

// ---- 服务状态 ----
app.get('/api/services', (_req, res) => {
  res.json(processManager.getStatus());
});

app.get('/api/services/:id', (req, res) => {
  const status = processManager.getServiceStatus(req.params.id);
  if (!status) return res.status(404).json({ error: '未知服务' });
  res.json(status);
});

// ---- 服务控制 ----
app.post('/api/services/:id/start', async (req, res) => {
  try {
    const result = await processManager.start(req.params.id);
    if (result.error === 'docker_managed') {
      res.status(409).json(result);
    } else {
      broadcast('status', processManager.getStatus());
      res.json(result);
    }
  } catch (err) {
    res.status(400).json({ success: false, message: err.message });
  }
});

app.post('/api/services/:id/stop', async (req, res) => {
  try {
    const result = await processManager.stop(req.params.id);
    if (result.error === 'docker_managed') {
      res.status(409).json(result);
    } else {
      broadcast('status', processManager.getStatus());
      res.json(result);
    }
  } catch (err) {
    res.status(400).json({ success: false, message: err.message });
  }
});

app.post('/api/services/:id/restart', async (req, res) => {
  try {
    const result = await processManager.restart(req.params.id);
    if (result.error === 'docker_managed') {
      res.status(409).json(result);
      return;
    }
    // 异步重启，因为可能耗时较长
    res.json({ success: true, message: '重启中...' });
    broadcast('status', processManager.getStatus());
  } catch (err) {
    // 已经在上面res了
  }
});

app.post('/api/services/:id/build', async (req, res) => {
  try {
    const result = await processManager.build(req.params.id);
    if (result.error === 'docker_managed') {
      res.status(409).json(result);
    } else {
      res.json(result);
    }
  } catch (err) {
    res.status(400).json({ success: false, message: err.message });
  }
});

// 批量操作
// 一键按顺序启动 (接管已运行 + 健康检查)
app.post('/api/services/start-all', async (_req, res) => {
  const results = await processManager.startAllSequential();
  broadcast('status', processManager.getStatus());
  res.json(results);
});

// 强制重启全部 (先杀后启)
app.post('/api/services/start-all-fresh', async (_req, res) => {
  await processManager.stopAll();
  await new Promise((r) => setTimeout(r, 1000));
  const results = await processManager.startAllSequential();
  broadcast('status', processManager.getStatus());
  res.json(results);
});

app.post('/api/services/stop-all', async (_req, res) => {
  const results = await processManager.stopAll();
  broadcast('status', processManager.getStatus());
  res.json(results);
});

// ---- 性能监控 ----
app.get('/api/performance', async (_req, res) => {
  const snapshot = await performanceMonitor.getSnapshot();
  res.json(snapshot);
});

// 性能仪表盘聚合数据 (供首页仪表盘使用)
app.get('/api/performance/dashboard', async (_req, res) => {
  try {
    const dashboardData = await performanceMonitor.updateDashboard();
    res.json(dashboardData);
  } catch (err) {
    res.status(500).json({ error: `获取性能仪表盘数据失败: ${err.message}` });
  }
});

app.get('/api/performance/history', (_req, res) => {
  res.json(performanceMonitor.getAllHistory());
});

app.get('/api/performance/:id', (req, res) => {
  const history = performanceMonitor.getHistory(req.params.id);
  res.json(history);
});

app.get('/api/performance/:id/summary', async (req, res) => {
  const snap = await performanceMonitor.getSnapshot();
  const history = performanceMonitor.getHistory(req.params.id);
  const svc = snap[req.params.id];
  if (!svc) return res.status(404).json({ error: '未知服务' });

  // 计算汇总统计
  let maxCpu = 0, maxMem = 0;
  const cpuValues = [], memValues = [];
  for (const h of history) {
    if (h.cpu > maxCpu) maxCpu = h.cpu;
    if (h.mem > maxMem) maxMem = h.mem;
    cpuValues.push(h.cpu);
    memValues.push(h.mem);
  }

  const avgCpu = cpuValues.length > 0 ? Math.round(cpuValues.reduce((a, b) => a + b, 0) / cpuValues.length * 100) / 100 : 0;
  const avgMem = memValues.length > 0 ? Math.round(memValues.reduce((a, b) => a + b, 0) / memValues.length * 100) / 100 : 0;

  res.json({
    current: svc,
    history: { count: history.length, maxCpu, maxMem, avgCpu, avgMem },
  });
});

// ---- 日志查询 ----
app.get('/api/logs/:id', (req, res) => {
  const id = req.params.id;
  if (!SERVICES[id]) return res.status(404).json({ error: '未知服务' });

  const filePath = logFile(id);
  const lines = req.query.lines ? parseInt(req.query.lines) : 200;
  const offset = req.query.offset ? parseInt(req.query.offset) : 0;

  if (!fs.existsSync(filePath)) {
    return res.json({ service: id, lines: [], total: 0 });
  }

  try {
    // 使用tail方式读取
    const content = fs.readFileSync(filePath, 'utf-8');
    const allLines = content.split('\n').filter(Boolean);
    const total = allLines.length;
    const sliced = allLines.slice(Math.max(0, total - lines - offset), total - offset);
    res.json({ service: id, lines: sliced, total });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.get('/api/logs/:id/recent', (req, res) => {
  const id = req.params.id;
  if (!SERVICES[id]) return res.status(404).json({ error: '未知服务' });

  const filePath = logFile(id);
  if (!fs.existsSync(filePath)) {
    return res.json({ service: id, lines: [], total: 0 });
  }

  try {
    const content = fs.readFileSync(filePath, 'utf-8');
    const allLines = content.split('\n').filter(Boolean);
    const total = allLines.length;
    res.json({ service: id, lines: allLines.slice(-100), total });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// 删除日志
app.delete('/api/logs/:id', (req, res) => {
  const id = req.params.id;
  if (!SERVICES[id]) return res.status(404).json({ error: '未知服务' });

  const filePath = logFile(id);
  try {
    if (fs.existsSync(filePath)) {
      fs.writeFileSync(filePath, '');
    }
    res.json({ success: true, message: '日志已清空' });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ---- IoT 设备管理 (代理到 iot-debug-service) ----
const IOT_SERVICE_URL = process.env.IOT_SERVICE_URL || process.env.IOT_DEBUG_SERVICE_URL || 'http://localhost:8083';

/** 通用 IoT 代理：转发请求到 iot-debug-service */
async function proxyToIoT(path, opts = {}) {
  const url = `${IOT_SERVICE_URL}${path}`;
  const logPrefix = `[IoT代理]`;
  try {
    console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
    const resp = await fetch(url, {
      ...opts,
      headers: { 'Content-Type': 'application/json', ...opts.headers },
      signal: AbortSignal.timeout(10000),
    });
    const body = await resp.json().catch(() => null);
    if (!resp.ok) {
      console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
    }
    return { status: resp.status, body };
  } catch (err) {
    const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
    console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
    return {
      status: 502,
      body: {
        error: `IoT 调试服务不可达: ${err.message}`,
        errorType: isConnRefused ? 'iot_not_running' : 'iot_unreachable',
        hint: isConnRefused
          ? 'IoT 调试服务未启动，请先在「服务管理」面板中启动 IoT Debug 服务'
          : 'IoT 调试服务无响应，请检查网络连接和服务状态',
      },
    };
  }
}

// GET /api/iot/devices — 获取所有模拟设备
app.get('/api/iot/devices', async (_req, res) => {
  console.log('[IoT] 获取设备列表');
  const result = await proxyToIoT('/api/v1/devices');
  res.status(result.status).json(result.body);
});

// GET /api/iot/devices/:id — 获取单个设备
app.get('/api/iot/devices/:id', async (req, res) => {
  console.log(`[IoT] 获取设备: ${req.params.id}`);
  const result = await proxyToIoT(`/api/v1/devices/${req.params.id}`);
  res.status(result.status).json(result.body);
});

// POST /api/iot/devices/:id/toggle — 切换设备开关
app.post('/api/iot/devices/:id/toggle', async (req, res) => {
  console.log(`[IoT] 切换设备: ${req.params.id}`);
  const result = await proxyToIoT(`/api/v1/devices/${req.params.id}/toggle`, { method: 'POST' });
  res.status(result.status).json(result.body);
});

// POST /api/iot/devices/:id/set — 设置设备属性
app.post('/api/iot/devices/:id/set', async (req, res) => {
  const { field, value } = req.body;
  if (!field) {
    return res.status(400).json({ error: '缺少 field 参数' });
  }
  console.log(`[IoT] 设置设备属性: ${req.params.id} -> ${field} = ${value}`);
  const result = await proxyToIoT(`/api/v1/devices/${req.params.id}/set`, {
    method: 'POST',
    body: JSON.stringify({ field, value }),
  });
  res.status(result.status).json(result.body);
});

// GET /api/iot/devices/:id/history — 获取设备操作历史
app.get('/api/iot/devices/:id/history', async (req, res) => {
  console.log(`[IoT] 获取设备历史: ${req.params.id}`);
  const result = await proxyToIoT(`/api/v1/devices/${req.params.id}/history`);
  res.status(result.status).json(result.body);
});

// ---- 插件管理代理 (转发到 plugin-manager) ----

/**
 * 代理请求到 Plugin-Manager
 */
async function proxyToPluginManager(path, opts = {}) {
  const url = `${PLUGIN_MANAGER_URL}${path}`;
  const logPrefix = `[PluginManager代理]`;
  try {
    console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
    const resp = await fetch(url, {
      ...opts,
      headers: { 'Content-Type': 'application/json', ...opts.headers },
      signal: AbortSignal.timeout(10000),
    });
    const body = await resp.json().catch(() => null);
    if (!resp.ok) {
      console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
    }
    return { status: resp.status, body };
  } catch (err) {
    const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
    console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
    return {
      status: 502,
      body: {
        error: `插件管理器不可达: ${err.message}`,
        errorType: isConnRefused ? 'plugin_manager_not_running' : 'plugin_manager_unreachable',
        hint: isConnRefused
          ? '插件管理器服务未启动，请先在「服务管理」面板中启动 plugin-manager'
          : '插件管理器服务无响应，请检查网络连接和服务状态',
      },
    };
  }
}

// ---- 第三方聊天平台配置代理 (转发到 platform-bridge) ----

/**
 * 代理请求到 Platform-Bridge
 * @param {string} path - Platform-Bridge API 路径
 * @param {object} opts - fetch 选项
 */
async function proxyToPlatformBridge(path, opts = {}) {
  const url = `${PLATFORM_BRIDGE_URL}${path}`;
  const logPrefix = `[PlatformBridge代理]`;
  try {
    console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
    const resp = await fetch(url, {
      ...opts,
      headers: { 'Content-Type': 'application/json', ...opts.headers },
      signal: AbortSignal.timeout(10000),
    });
    const body = await resp.json().catch(() => null);
    if (!resp.ok) {
      console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
    }
    return { status: resp.status, body };
  } catch (err) {
    const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
    console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
    return {
      status: 502,
      body: {
        error: `Platform-Bridge 不可达: ${err.message}`,
        errorType: isConnRefused ? 'bridge_not_running' : 'bridge_unreachable',
        hint: isConnRefused
          ? 'Platform-Bridge 服务未启动，请先在「服务管理」面板中启动该服务'
          : 'Platform-Bridge 服务无响应，请检查网络连接和服务状态',
      },
    };
  }
}

// GET /api/chat-platforms/configs — 列出所有平台配置
app.get('/api/chat-platforms/configs', async (_req, res) => {
  const result = await proxyToPlatformBridge('/api/v1/configs');
  res.status(result.status).json(result.body);
});

// GET /api/chat-platforms/configs/:name — 获取单个配置
app.get('/api/chat-platforms/configs/:name', async (req, res) => {
  const result = await proxyToPlatformBridge(`/api/v1/configs/${req.params.name}`);
  res.status(result.status).json(result.body);
});

// POST /api/chat-platforms/configs/:name — 创建或更新配置
app.post('/api/chat-platforms/configs/:name', async (req, res) => {
  const result = await proxyToPlatformBridge(`/api/v1/configs/${req.params.name}`, {
    method: 'POST',
    body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});

// DELETE /api/chat-platforms/configs/:name — 删除配置
app.delete('/api/chat-platforms/configs/:name', async (req, res) => {
  const result = await proxyToPlatformBridge(`/api/v1/configs/${req.params.name}`, {
    method: 'DELETE',
  });
  res.status(result.status).json(result.body);
});

// GET /api/chat-platforms/logs/:name — 获取消息日志
app.get('/api/chat-platforms/logs/:name', async (req, res) => {
  const limit = req.query.limit || '100';
  const result = await proxyToPlatformBridge(`/api/v1/logs/${req.params.name}?limit=${limit}`);
  res.status(result.status).json(result.body);
});

// GET /api/chat-platforms/platforms — 列出所有平台适配器 (含连接状态与能力)
app.get('/api/chat-platforms/platforms', async (_req, res) => {
  const result = await proxyToPlatformBridge('/api/v1/platforms');
  res.status(result.status).json(result.body);
});

// GET /api/chat-platforms/platforms/:name — 获取单个平台适配器详情
app.get('/api/chat-platforms/platforms/:name', async (req, res) => {
  const result = await proxyToPlatformBridge(`/api/v1/platforms/${req.params.name}`);
  res.status(result.status).json(result.body);
});

// GET /api/chat-platforms/identities — 列出所有已注册的身份映射
app.get('/api/chat-platforms/identities', async (_req, res) => {
  const result = await proxyToPlatformBridge('/api/v1/identities');
  res.status(result.status).json(result.body);
});

// GET /api/chat-platforms/health — platform-bridge 整体健康状态
app.get('/api/chat-platforms/health', async (_req, res) => {
  const result = await proxyToPlatformBridge('/health');
  res.status(result.status).json(result.body);
});

// ---- 黑名单/白名单设置代理 ----

// GET /api/chat-platforms/settings/blocklist
app.get('/api/chat-platforms/settings/blocklist', async (_req, res) => {
  const result = await proxyToPlatformBridge('/api/v1/settings/blocklist');
  res.status(result.status).json(result.body);
});

// POST /api/chat-platforms/settings/blocklist
app.post('/api/chat-platforms/settings/blocklist', async (req, res) => {
  const result = await proxyToPlatformBridge('/api/v1/settings/blocklist', {
    method: 'POST',
    body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});

// ---- 多端客户端管理代理 (转发到 Gateway) ----

// GET /api/clients — 获取已知客户端列表
app.get('/api/clients', async (req, res) => {
  const userID = req.query.user_id || 'admin';
  const result = await proxyToGateway(`/api/v1/admin/clients?user_id=${encodeURIComponent(userID)}`);
  res.status(result.status).json(result.body);
});

// PUT /api/clients/:id/note — 更新客户端备注
app.put('/api/clients/:id/note', async (req, res) => {
  const { note } = req.body;
  const result = await proxyToGateway(`/api/v1/admin/clients/${req.params.id}/note`, {
    method: 'PUT',
    body: JSON.stringify({ note }),
  });
  res.status(result.status).json(result.body);
});

// ---- 模型配置管理代理 (转发到 Gateway admin) ----

// Providers
app.get('/api/model-config/providers', async (_req, res) => {
  const result = await proxyToGateway('/api/v1/admin/models/providers');
  res.status(result.status).json(result.body);
});
app.get('/api/model-config/providers/:name', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/providers/${req.params.name}`);
  res.status(result.status).json(result.body);
});
app.post('/api/model-config/providers/:name', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/providers/${req.params.name}`, {
    method: 'POST', body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});
app.delete('/api/model-config/providers/:name', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/providers/${req.params.name}`, {
    method: 'DELETE',
  });
  res.status(result.status).json(result.body);
});

// Models
app.get('/api/model-config/models', async (_req, res) => {
  const result = await proxyToGateway('/api/v1/admin/models/models');
  res.status(result.status).json(result.body);
});
app.get('/api/model-config/models/:id', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/models/${req.params.id}`);
  res.status(result.status).json(result.body);
});
app.post('/api/model-config/models/:id', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/models/${req.params.id}`, {
    method: 'POST', body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});
app.delete('/api/model-config/models/:id', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/models/${req.params.id}`, {
    method: 'DELETE',
  });
  res.status(result.status).json(result.body);
});

// Routing
app.get('/api/model-config/routing', async (_req, res) => {
  const result = await proxyToGateway('/api/v1/admin/models/routing');
  res.status(result.status).json(result.body);
});
app.get('/api/model-config/routing/:purpose', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/routing/${req.params.purpose}`);
  res.status(result.status).json(result.body);
});
app.post('/api/model-config/routing/:purpose', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/routing/${req.params.purpose}`, {
    method: 'POST', body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});
app.delete('/api/model-config/routing/:purpose', async (req, res) => {
  const result = await proxyToGateway(`/api/v1/admin/models/routing/${req.params.purpose}`, {
    method: 'DELETE',
  });
  res.status(result.status).json(result.body);
});

// Health check
app.post('/api/model-config/health-check', async (req, res) => {
  const result = await proxyToGateway('/api/v1/admin/models/health-check', {
    method: 'POST', body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});

// GET /api/model-config/fetch-models/:name?url=... — 代理查询 Provider 模型列表
app.get('/api/model-config/fetch-models/:name', async (req, res) => {
  const urlParam = req.query.url ? '?url=' + encodeURIComponent(req.query.url) : '';
  const result = await proxyToGateway('/api/v1/admin/models/fetch-models/' + encodeURIComponent(req.params.name) + urlParam);
  res.status(result.status).json(result.body);
});

// ---- 思考调度配置代理 ----
app.get('/api/thinking-schedule', async (_req, res) => {
  const result = await proxyToGateway('/api/v1/admin/thinking-schedule');
  res.status(result.status).json(result.body);
});
app.put('/api/thinking-schedule', async (req, res) => {
  const result = await proxyToGateway('/api/v1/admin/thinking-schedule', {
    method: 'PUT', body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});

// GET /api/tool-calls — 查询工具调用记录
app.get('/api/tool-calls', async (req, res) => {
  const { tool_name, page, limit } = req.query;
  const params = new URLSearchParams();
  if (tool_name) params.set('tool_name', tool_name);
  params.set('page', page || '1');
  params.set('limit', limit || '20');
  const result = await proxyToAICore(`/api/v1/tools/calls?${params.toString()}`);
  res.status(result.status).json(result.body);
});

// GET /api/tool-calls/stats — 工具调用统计
app.get('/api/tool-calls/stats', async (_req, res) => {
  const result = await proxyToAICore('/api/v1/tools/calls/stats');
  res.status(result.status).json(result.body);
});

// ---- VM 监控 (OS 环境信息) ----
app.get('/api/vm-monitor/status', async (_req, res) => {
  const result = await proxyToAICore('/api/v1/system/info');
  res.status(result.status).json(result.body);
});

// ---- 插件管理代理 (转发到 plugin-manager) ----
app.get('/api/plugins', async (_req, res) => {
  const result = await proxyToPluginManager('/api/v1/plugins');
  res.status(result.status).json(result.body);
});
app.get('/api/plugins/:id', async (req, res) => {
  const result = await proxyToPluginManager('/api/v1/plugins/' + req.params.id);
  res.status(result.status).json(result.body);
});
app.post('/api/plugins/:id/enable', async (req, res) => {
  const result = await proxyToPluginManager('/api/v1/plugins/' + req.params.id + '/enable', { method: 'POST' });
  res.status(result.status).json(result.body);
});
app.post('/api/plugins/:id/disable', async (req, res) => {
  const result = await proxyToPluginManager('/api/v1/plugins/' + req.params.id + '/disable', { method: 'POST' });
  res.status(result.status).json(result.body);
});
app.post('/api/plugins/:id/reload', async (req, res) => {
  const result = await proxyToPluginManager('/api/v1/plugins/' + req.params.id + '/reload', { method: 'POST' });
  res.status(result.status).json(result.body);
});
app.get('/api/plugins/:id/tools', async (req, res) => {
  const result = await proxyToPluginManager('/api/v1/plugins/' + req.params.id + '/tools');
  res.status(result.status).json(result.body);
});
app.get('/api/tools', async (_req, res) => {
  const result = await proxyToPluginManager('/api/v1/tools');
  res.status(result.status).json(result.body);
});
app.post('/api/tools/:id/execute', async (req, res) => {
  const result = await proxyToPluginManager('/api/v1/tools/' + req.params.id + '/execute', {
    method: 'POST', body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});

// ---- STT 处理日志存储 (内存环形缓冲区) ----
const sttLogEntries = [];
const MAX_STT_LOGS = 200;

/**
 * 记录 STT 请求日志（ethend 自身维护，因为 voice-service 无持久化日志）
 */
function recordSTTLog(entry) {
  sttLogEntries.unshift(entry);
  if (sttLogEntries.length > MAX_STT_LOGS) {
    sttLogEntries.length = MAX_STT_LOGS;
  }
  // 通过 WebSocket 广播给前端面板实时更新
  broadcast('stt-log', entry);
}

// GET /api/voice/logs — 获取 STT 处理日志
app.get('/api/voice/logs', (req, res) => {
  const limit = parseInt(req.query.limit) || 50;
  const logs = sttLogEntries.slice(0, limit);
  res.json({
    total: sttLogEntries.length,
    logs,
  });
});

// POST /api/voice/transcribe — 代理到 Voice-Service 并记录日志
// 接受 JSON (base64 音频) 并转发为 multipart/form-data 到 Voice-Service
app.post('/api/voice/transcribe', async (req, res) => {
  const startTime = Date.now();
  let { audio_base64, language, filename } = req.body || {};

  // 也支持直接通过 FormData 上传 (express.raw 中间件处理后手动解析)
  if (!audio_base64 && req.is('multipart/form-data')) {
    return res.status(400).json({ error: 'multipart/form-data 暂不支持，请使用 JSON 格式发送 base64 编码的音频' });
  }

  if (!audio_base64) {
    return res.status(400).json({ error: '缺少 audio_base64 字段' });
  }

  // 计算音频大小 (解码后)
  let audioBuffer;
  try {
    audioBuffer = Buffer.from(audio_base64, 'base64');
  } catch {
    return res.status(400).json({ error: 'audio_base64 格式无效，无法解码' });
  }
  const audioSizeBytes = audioBuffer.length;
  // 估算音频时长 (WAV 16kHz 16bit mono: ~32000 bytes/sec)
  const estimatedDurationSec = audioSizeBytes > 0 ? (audioSizeBytes / 32000).toFixed(1) : '0';

  if (!filename) filename = 'audio.wav';

  try {
    // 构建 multipart/form-data 请求转发到 Voice-Service
    const boundary = '----ethendFormBoundary' + Math.random().toString(36).slice(2);
    const crlf = '\r\n';
    const headerParts = [
      '--' + boundary + crlf,
      'Content-Disposition: form-data; name="audio"; filename="' + filename + '"' + crlf,
      'Content-Type: application/octet-stream' + crlf,
      crlf,
    ];
    const headerBytes = Buffer.from(headerParts.join(''), 'utf-8');
    const footerBytes = Buffer.from(crlf + '--' + boundary + '--' + crlf, 'utf-8');

    // 如果有 language 参数
    let languagePart = Buffer.alloc(0);
    if (language) {
      const langHeader = [
        '--' + boundary + crlf,
        'Content-Disposition: form-data; name="language"' + crlf,
        crlf,
        language + crlf,
      ];
      languagePart = Buffer.from(langHeader.join(''), 'utf-8');
    }

    const multipartBody = Buffer.concat([headerBytes, audioBuffer, footerBytes]);
    // 如果需要 language 字段，插入在 audio 字段之后
    const finalBody = languagePart.length > 0
      ? Buffer.concat([headerBytes, audioBuffer, languagePart, footerBytes])
      : multipartBody;

    const voiceResp = await fetch(`${VOICE_SERVICE_URL}/api/v1/transcribe`, {
      method: 'POST',
      headers: {
        'Content-Type': 'multipart/form-data; boundary=' + boundary,
      },
      body: finalBody,
      signal: AbortSignal.timeout(60000),
    });

    const voiceBody = await voiceResp.json().catch(() => null);
    const elapsedMs = Date.now() - startTime;

    if (!voiceResp.ok || (voiceBody && voiceBody.error)) {
      const logEntry = {
        id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
        timestamp: new Date().toISOString(),
        status: 'error',
        audioSizeMB: (audioSizeBytes / 1024 / 1024).toFixed(2),
        estimatedDurationSec,
        language: language || 'zh',
        filename,
        durationMs: elapsedMs,
        text: null,
        error: voiceBody?.error || `HTTP ${voiceResp.status}`,
      };
      recordSTTLog(logEntry);
      return res.status(voiceResp.status).json({
        ...voiceBody,
        ethend_log_id: logEntry.id,
      });
    }

    const logEntry = {
      id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
      timestamp: new Date().toISOString(),
      status: 'success',
      audioSizeMB: (audioSizeBytes / 1024 / 1024).toFixed(2),
      estimatedDurationSec,
      language: voiceBody?.language || language || 'zh',
      filename,
      durationMs: voiceBody?.duration_ms || elapsedMs,
      text: voiceBody?.text || '',
      textLength: (voiceBody?.text || '').length,
    };
    recordSTTLog(logEntry);
    return res.json({
      ...voiceBody,
      ethend_log_id: logEntry.id,
    });
  } catch (err) {
    const elapsedMs = Date.now() - startTime;
    const logEntry = {
      id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
      timestamp: new Date().toISOString(),
      status: 'error',
      audioSizeMB: (audioSizeBytes / 1024 / 1024).toFixed(2),
      estimatedDurationSec,
      language: language || 'zh',
      filename,
      durationMs: elapsedMs,
      text: null,
      error: err.message,
    };
    recordSTTLog(logEntry);

    const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
    return res.status(502).json({
      error: `Voice-Service 不可达: ${err.message}`,
      errorType: isConnRefused ? 'voice_service_not_running' : 'voice_service_unreachable',
      hint: isConnRefused
        ? 'Voice-Service 服务未启动，请先在「服务管理」面板中启动 Voice-Service'
        : 'Voice-Service 服务无响应，请检查网络连接和服务状态',
      ethend_log_id: logEntry.id,
    });
  }
});

// ---- 自主思考日志代理 (转发到 memory-service) ----

// ---- 语音识别服务代理 (转发到 voice-service) ----

/**
 * 代理请求到 Voice-Service
 * @param {string} path - Voice-Service API 路径
 * @param {object} opts - fetch 选项
 */
async function proxyToVoiceService(path, opts = {}) {
  const url = `${VOICE_SERVICE_URL}${path}`;
  const logPrefix = `[VoiceService代理]`;
  try {
    console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
    const resp = await fetch(url, {
      ...opts,
      signal: AbortSignal.timeout(60000), // 语音转录可能需要较长时间
    });
    const body = await resp.json().catch(() => null);
    if (!resp.ok) {
      console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
    }
    return { status: resp.status, body };
  } catch (err) {
    const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
    console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
    return {
      status: 502,
      body: {
        error: `Voice-Service 不可达: ${err.message}`,
        errorType: isConnRefused ? 'voice_service_not_running' : 'voice_service_unreachable',
        hint: isConnRefused
          ? 'Voice-Service 服务未启动，请先在「服务管理」面板中启动 Voice-Service'
          : 'Voice-Service 服务无响应，请检查网络连接和服务状态',
      },
    };
  }
}

// GET /api/voice/status — 获取 STT 服务状态
app.get('/api/voice/status', async (_req, res) => {
  const result = await proxyToVoiceService('/api/v1/status');
  res.status(result.status).json(result.body);
});

// GET /api/voice/health — STT 健康检查
app.get('/api/voice/health', async (_req, res) => {
  const result = await proxyToVoiceService('/api/v1/health');
  res.status(result.status).json(result.body);
});

// GET /api/llm-calls — LLM 调用日志 (代理到 AI-Core)
app.get('/api/llm-calls', async (req, res) => {
  const limit = parseInt(req.query.limit) || 50;
  const result = await proxyToAICore(`/api/v1/llm-calls?limit=${Math.min(limit, 500)}`);
  res.status(result.status).json(result.body);
});

// ---- 全链路追踪 ----

/**
 * 从日志文件中搜索包含指定关键词的最近行
 */
function searchLogFile(serviceId, keyword, maxLines = 200) {
  const filePath = logFile(serviceId);
  if (!fs.existsSync(filePath)) return [];
  try {
    const content = fs.readFileSync(filePath, 'utf-8');
    const allLines = content.split('\n').filter(Boolean);
    const recent = allLines.slice(-maxLines);
    const kwLower = keyword.toLowerCase();
    return recent
      .filter(line => line.toLowerCase().includes(kwLower))
      .map(line => line.trim());
  } catch {
    return [];
  }
}

/**
 * 解析日志时间戳 (支持常见格式)
 */
function parseLogTimestamp(line) {
  // 2024-01-01T12:00:00Z, 2024/01/01 12:00:00, [2024-01-01 12:00:00], 12:00:00
  const match = line.match(/(\d{4}[-/]\d{2}[-/]\d{2}[T ]\d{2}:\d{2}:\d{2})/);
  if (match) return new Date(match[1]).getTime();
  const timeMatch = line.match(/(\d{2}:\d{2}:\d{2})/);
  if (timeMatch) {
    const today = new Date();
    const [h, m, s] = timeMatch[1].split(':').map(Number);
    today.setHours(h, m, s, 0);
    return today.getTime();
  }
  return Date.now();
}

// GET /api/trace/recent — 最近的全链路追踪数据
app.get('/api/trace/recent', async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit) || 50, 200);
  try {
    const [llmResult, toolResult, sessionsResult, bridgeLogsResult, traceEventsResult] = await Promise.all([
      proxyToAICore(`/api/v1/llm-calls?limit=${limit}`).catch(() => ({ status: 502, body: [] })),
      proxyToAICore(`/api/v1/tools/calls?limit=${limit}`).catch(() => ({ status: 502, body: { calls: [] } })),
      proxyToGateway('/api/v1/admin/sessions/active').catch(() => ({ status: 502, body: { users: {} } })),
      // 直接从磁盘读取 platform-bridge 日志文件
      Promise.resolve(readBridgeLogs(50)),
      // 拉 ai-core 追踪事件
      proxyToAICore(`/api/v1/trace/events?limit=${limit}`).catch(() => ({ status: 502, body: { events: [] } })),
    ]);

    const traces = [];

    // === 消息收发 (platform-bridge 日志文件) ===
    const entries = bridgeLogsResult || [];
    for (const entry of entries) {
        const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
        const isIncoming = entry.direction === 'incoming';
        const hop = isIncoming ? 'msg_received' : 'msg_sent';
        const icon = isIncoming ? '📥' : '📤';
        const sender = entry.sender_name || entry.sender_id || '?';
        const channel = entry.channel_id || '?';
        const groupName = entry.group_name ? ` (${entry.group_name})` : '';
        traces.push({
          id: `msg-${ts}-${Math.random().toString(36).slice(2, 6)}`,
          timestamp: formatLocalTime(ts), ts,
          service: 'platform-bridge',
          hop,
          label: `${icon} ${isIncoming ? '收到' : '发送'}消息 · ${entry.platform || '?'}${groupName}`,
          status: entry.success ? 'success' : 'error',
          durationMs: 0,
          detail: `${isIncoming ? '来自' : '发给'} ${sender} | 频道 ${channel} | ${(entry.content || '').substring(0, 80)}`,
          data: { platform: entry.platform, sender, channel, content: entry.content, direction: entry.direction, contentType: entry.content_type },
        });
      }

    // === LLM 调用 ===
    const llmCalls = Array.isArray(llmResult.body) ? llmResult.body : (llmResult.body?.calls || []);
    for (const call of llmCalls) {
      const ts = call.time ? new Date(call.time).getTime() : Date.now();
      traces.push({
        id: `llm-${ts}-${Math.random().toString(36).slice(2, 6)}`,
        timestamp: formatLocalTime(ts), ts,
        service: 'ai-core', hop: 'llm_call',
        label: `🧠 LLM: ${call.model || 'unknown'}`,
        status: call.success ? 'success' : 'error',
        durationMs: call.duration_ms || (call.Duration ? Math.round(call.Duration / 1e6) : 0),
        detail: call.error || `${call.prompt_tokens || 0}→${call.completion_tokens || 0} tokens`,
        data: call,
      });
    }

    // === 工具调用 ===
    const toolCalls = toolResult.body?.calls || (Array.isArray(toolResult.body) ? toolResult.body : []);
    for (const tc of toolCalls) {
      const ts = tc.time || tc.timestamp || tc.created_at;
      const tsNum = ts ? new Date(ts).getTime() : Date.now();
      traces.push({
        id: `tool-${tsNum}-${Math.random().toString(36).slice(2, 6)}`,
        timestamp: new Date(tsNum).toISOString(), ts: tsNum,
        service: 'ai-core', hop: 'tool_call',
        label: `🔧 ${tc.tool_name || tc.name || 'unknown'}`,
        status: tc.error ? 'error' : 'success',
        durationMs: tc.duration_ms || (tc.Duration ? Math.round(tc.Duration / 1e6) : 0),
        detail: tc.error || tc.result?.substring?.(0, 100) || tc.output?.substring?.(0, 100) || '',
        data: tc,
      });
    }

    // === ai-core 追踪事件 (intent, subsession, synthesis, etc.) ===
    const traceEvents = traceEventsResult.body?.events || [];
    for (const ev of traceEvents) {
      const ts = ev.timestamp ? new Date(ev.timestamp).getTime() : Date.now();
      traces.push({
        id: ev.id || `trace-${ts}`,
        timestamp: formatLocalTime(ts), ts,
        service: 'ai-core',
        hop: ev.hop || 'trace',
        label: ev.label || '',
        status: ev.status || 'success',
        durationMs: ev.duration_ms || 0,
        detail: ev.detail || '',
        data: ev.data || {},
      });
    }

    // === 活跃会话 ===
    const users = sessionsResult.body?.users || {};
    for (const [userID, sessions] of Object.entries(users)) {
      for (const s of sessions) {
        const ts = s.last_activity ? new Date(s.last_activity).getTime() : Date.now();
        traces.push({
          id: `session-${s.session_id || ''}`,
          timestamp: formatLocalTime(ts), ts,
          service: 'gateway', hop: 'session',
          label: `👤 会话: ${userID}`,
          status: s.state === 'streaming' ? 'running' : 'success',
          durationMs: 0,
          detail: `${(s.session_id || '').substring(0, 20)}... [${s.state || 'idle'}]`,
          data: { userID, sessionId: s.session_id, state: s.state },
        });
      }
    }

    // 按时间倒序，按 hop 分组
    traces.sort((a, b) => b.ts - a.ts);
    const recent = traces.slice(0, limit);

    // 按消息分组：将相近时间的 msg_received + llm_call + tool_call + msg_sent 聚合成消息管线
    const pipelines = [];
    let currentPipeline = null;
    const sorted = [...recent].sort((a, b) => a.ts - b.ts);
    for (const t of sorted) {
      if (t.hop === 'msg_received') {
        if (currentPipeline) pipelines.push(currentPipeline);
        currentPipeline = { id: t.id, ts: t.ts, timestamp: t.timestamp, sender: t.data?.sender, channel: t.data?.channel, platform: t.data?.platform, content: t.data?.content, steps: [t] };
      } else if (currentPipeline) {
        // 只聚合时间相近的事件 (60s 内)
        if (t.ts - currentPipeline.ts < 60000) {
          currentPipeline.steps.push(t);
        }
      } else {
        currentPipeline = { id: t.id, ts: t.ts, timestamp: t.timestamp, steps: [t] };
      }
    }
    if (currentPipeline) pipelines.push(currentPipeline);

    const services = [...new Set(recent.map(t => t.service))];
    const errors = recent.filter(t => t.status === 'error').length;

    res.json({
      timestamp: Date.now(),
      total: traces.length, shown: recent.length,
      pipelines: pipelines.slice(-limit),
      stats: { services, errors, pipelineCount: pipelines.length },
      traces: recent,
    });
  } catch (err) {
    res.status(500).json({ error: `获取链路追踪数据失败: ${err.message}` });
  }
});

// GET /api/trace/session/:sessionId — 特定会话的全链路追踪
app.get('/api/trace/session/:sessionId', async (req, res) => {
  const { sessionId } = req.params;
  if (!sessionId) return res.status(400).json({ error: '缺少 sessionId' });

  try {
    // 并行获取: 会话详情、LLM 调用、工具调用、日志搜索
    const [sessionResult, llmResult, toolResult] = await Promise.all([
      proxyToGateway(`/api/v1/admin/sessions/${sessionId}`).catch(() => ({ status: 502, body: null })),
      proxyToAICore('/api/v1/llm-calls?limit=500').catch(() => ({ status: 502, body: [] })),
      proxyToAICore('/api/v1/tools/calls?limit=200').catch(() => ({ status: 502, body: { calls: [] } })),
    ]);

    // 从日志文件中搜索 session 相关行
    const gatewayLogLines = searchLogFile('gateway', sessionId, 500);
    const aiCoreLogLines = searchLogFile('ai-core', sessionId, 500);

    const traces = [];
    const sessionData = sessionResult.body;

    // Gateway 日志 → 追踪节点
    for (const line of gatewayLogLines) {
      const ts = parseLogTimestamp(line);
      let hop = 'gateway_log';
      let label = 'Gateway 日志';
      if (line.includes('received') || line.includes('收到')) { hop = 'gateway_receive'; label = 'Gateway 接收消息'; }
      else if (line.includes('stream') || line.includes('流式')) { hop = 'gateway_stream'; label = 'Gateway 流式处理'; }
      else if (line.includes('send') || line.includes('发送') || line.includes('broadcast')) { hop = 'gateway_send'; label = 'Gateway 推送响应'; }
      else if (line.includes('error') || line.includes('错误')) { hop = 'gateway_error'; label = 'Gateway 错误'; }

      traces.push({
        id: `gwlog-${ts}-${Math.random().toString(36).slice(2, 6)}`,
        timestamp: new Date(ts).toISOString(),
        ts,
        service: 'gateway',
        hop,
        label,
        status: hop === 'gateway_error' ? 'error' : 'success',
        durationMs: 0,
        detail: line.substring(0, 200),
        data: { raw: line },
      });
    }

    // AI-Core 日志
    for (const line of aiCoreLogLines) {
      const ts = parseLogTimestamp(line);
      let hop = 'ai_core_log';
      let label = 'AI-Core 日志';
      if (line.includes('LLM') || line.includes('llm') || line.includes('chat')) { hop = 'ai_core_llm'; label = 'AI-Core LLM 处理'; }
      else if (line.includes('tool') || line.includes('Tool')) { hop = 'ai_core_tool'; label = 'AI-Core 工具调用'; }
      else if (line.includes('stream') || line.includes('SSE')) { hop = 'ai_core_stream'; label = 'AI-Core 流式输出'; }
      else if (line.includes('error') || line.includes('Error')) { hop = 'ai_core_error'; label = 'AI-Core 错误'; }

      traces.push({
        id: `aclog-${ts}-${Math.random().toString(36).slice(2, 6)}`,
        timestamp: new Date(ts).toISOString(),
        ts,
        service: 'ai-core',
        hop,
        label,
        status: hop === 'ai_core_error' ? 'error' : 'success',
        durationMs: 0,
        detail: line.substring(0, 200),
        data: { raw: line },
      });
    }

    // LLM 调用记录中如果有 session 相关信息也加入
    const llmCalls = Array.isArray(llmResult.body) ? llmResult.body : (llmResult.body?.calls || []);
    for (const call of llmCalls) {
      const ts = call.time ? new Date(call.time).getTime() : Date.now();
      traces.push({
        id: `llm-${ts}-${Math.random().toString(36).slice(2, 6)}`,
        timestamp: new Date(ts).toISOString(),
        ts,
        service: 'ai-core',
        hop: 'llm_call',
        label: `LLM: ${call.model || 'unknown'}`,
        status: call.success ? 'success' : 'error',
        durationMs: call.duration_ms || (call.Duration ? Math.round(call.Duration / 1e6) : 0),
        detail: call.error || `${call.prompt_tokens || 0}→${call.completion_tokens || 0} tokens`,
        data: call,
      });
    }

    // 按时间排序
    traces.sort((a, b) => a.ts - b.ts);

    // 计算每一跳的耗时
    for (let i = 1; i < traces.length; i++) {
      const gap = traces[i].ts - traces[i - 1].ts;
      if (gap > 0 && !traces[i].durationMs) {
        traces[i]._gapFromPrev = gap;
      }
    }

    const totalSpan = traces.length >= 2 ? traces[traces.length - 1].ts - traces[0].ts : 0;
    const errors = traces.filter(t => t.status === 'error').length;

    res.json({
      timestamp: Date.now(),
      sessionId,
      session: sessionData,
      stats: {
        totalHops: traces.length,
        errors,
        totalSpanMs: totalSpan,
        services: [...new Set(traces.map(t => t.service))],
      },
      traces,
    });
  } catch (err) {
    res.status(500).json({ error: `获取会话链路追踪失败: ${err.message}` });
  }
});

/**
 * 代理请求到 Memory-Service
 * @param {string} path - Memory-Service API 路径
 * @param {object} opts - fetch 选项
 */
async function proxyToMemoryService(path, opts = {}) {
  const url = `${MEMORY_SERVICE_URL}${path}`;
  const logPrefix = `[MemoryService代理]`;
  try {
    console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
    const resp = await fetch(url, {
      ...opts,
      headers: { 'Content-Type': 'application/json', ...opts.headers },
      signal: AbortSignal.timeout(10000),
    });
    const body = await resp.json().catch(() => null);
    if (!resp.ok) {
      console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
    }
    return { status: resp.status, body };
  } catch (err) {
    const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
    console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
    return {
      status: 502,
      body: {
        error: `Memory-Service 不可达: ${err.message}`,
        errorType: isConnRefused ? 'memory_service_not_running' : 'memory_service_unreachable',
        hint: isConnRefused
          ? 'Memory-Service 服务未启动，请先在「服务管理」面板中启动 Memory-Service'
          : 'Memory-Service 服务无响应，请检查网络连接和服务状态',
      },
    };
  }
}

// GET /api/v1/thinking — 查询自主思考日志列表
app.get('/api/v1/thinking', async (req, res) => {
  const { user_id, limit, offset } = req.query;
  const params = new URLSearchParams();
  if (user_id) params.set('user_id', user_id);
  if (limit) params.set('limit', limit);
  if (offset) params.set('offset', offset);
  const result = await proxyToMemoryService(`/api/v1/thinking?${params.toString()}`);
  res.status(result.status).json(result.body);
});

// POST /api/v1/thinking — 创建自主思考日志
app.post('/api/v1/thinking', async (req, res) => {
  const result = await proxyToMemoryService('/api/v1/thinking', {
    method: 'POST',
    body: JSON.stringify(req.body),
  });
  res.status(result.status).json(result.body);
});

// GET /api/v1/thinking/stats — 自主思考统计
app.get('/api/v1/thinking/stats', async (req, res) => {
  const { user_id } = req.query;
  const params = user_id ? `?user_id=${encodeURIComponent(user_id)}` : '';
  const result = await proxyToMemoryService(`/api/v1/thinking/stats${params}`);
  res.status(result.status).json(result.body);
});

// GET /api/v1/thinking/:id — 获取单条自主思考日志
app.get('/api/v1/thinking/:id', async (req, res) => {
  const result = await proxyToMemoryService(`/api/v1/thinking/${req.params.id}`);
  res.status(result.status).json(result.body);
});

// ---- 记忆时间线 (合并记忆 + 思考) ----
app.get('/api/memory-timeline', async (req, res) => {
  const { user_id, limit, offset } = req.query;
  if (!user_id) {
    return res.status(400).json({ error: '缺少 user_id 参数' });
  }
  const maxItems = parseInt(limit) || 100;
  const pageOffset = parseInt(offset) || 0;

  try {
    // 并行调用记忆和思考 API (带 offset)
    const memQs = new URLSearchParams({ user_id, limit: String(maxItems), offset: String(pageOffset) }).toString();
    const thinkQs = new URLSearchParams({ user_id, limit: String(maxItems), offset: String(pageOffset) }).toString();

    const [memResult, thinkResult] = await Promise.all([
      proxyToMemoryService(`/api/v1/memories?${memQs}`),
      proxyToMemoryService(`/api/v1/thinking?${thinkQs}`),
    ]);

    const memories = [];
    const thinkingLogs = [];

    // 解析记忆列表
    if (memResult.body && !memResult.body.error) {
      const memData = Array.isArray(memResult.body) ? memResult.body : (memResult.body.memories || memResult.body.results || []);
      for (const m of memData) {
        const createdAt = m.created_at || m.CreatedAt || m.timestamp;
        memories.push({
          id: m.id || m.ID || '',
          type: 'memory',
          title: m.title || (m.content || '').substring(0, 60),
          content: m.content || '',
          summary: m.summary || '',
          importance: m.importance || 1,
          category: m.category || 'other',
          source: m.source || 'unknown',
          session_id: m.session_id || '',
          keywords: m.keywords || [],
          access_count: m.access_count || 0,
          timestamp: createdAt ? new Date(createdAt).toISOString() : null,
          user_id: m.user_id || user_id,
        });
      }
    }

    // 解析思考日志列表
    if (thinkResult.body && !thinkResult.body.error) {
      const thinkData = thinkResult.body.logs || (Array.isArray(thinkResult.body) ? thinkResult.body : []);
      for (const t of thinkData) {
        const createdAt = t.created_at || t.CreatedAt || t.timestamp;
        // 解析思考主题：提取第一行或前80个字符
        const content = t.content || '';
        const firstLine = content.split('\n')[0] || '';
        const topic = firstLine.length > 80 ? firstLine.substring(0, 77) + '...' : firstLine;

        // 尝试从内容推断触发方式
        let trigger = '定时';
        if (content.includes('用户') || content.includes('手动') || content.includes('manual')) {
          trigger = '手动';
        } else if (content.includes('scheduled') || content.includes('定时') || content.includes('interval')) {
          trigger = '定时';
        }

        thinkingLogs.push({
          id: t.id || t.ID || '',
          type: 'thinking',
          title: topic || '自主思考',
          content: content,
          summary: content.length > 200 ? content.substring(0, 197) + '...' : content,
          tool_call_count: t.tool_call_count || 0,
          content_length: t.content_length || content.length,
          trigger: trigger,
          tool_calls: t.tool_calls || null,
          timestamp: createdAt ? new Date(createdAt).toISOString() : null,
          user_id: t.user_id || user_id,
          // 思考没有重要性，设为0用于排序
          importance: 0,
          source: 'thinking',
        });
      }
    }

    // 合并并按时间排序（降序：最新的在前）
    const timeline = [...memories, ...thinkingLogs].sort((a, b) => {
      const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
      const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
      return tb - ta;
    });

    // 截取限制条数
    const result = timeline.slice(0, maxItems);

    // 是否有更多数据 (两边都还有数据表示可能还有更多)
    const hasMore = memories.length >= maxItems || thinkingLogs.length >= maxItems;

    // 统计摘要
    const stats = {
      total_memories: memories.length,
      total_thinking: thinkingLogs.length,
      latest_memory_time: memories.length > 0 ? memories.reduce((max, m) => {
        const t = m.timestamp ? new Date(m.timestamp).getTime() : 0;
        return t > max ? t : max;
      }, 0) : null,
      latest_thinking_time: thinkingLogs.length > 0 ? thinkingLogs.reduce((max, t) => {
        const ts = t.timestamp ? new Date(t.timestamp).getTime() : 0;
        return ts > max ? ts : max;
      }, 0) : null,
    };

    res.json({
      timeline: result,
      stats,
      total: timeline.length,
      hasMore,
      user_id,
    });
  } catch (err) {
    console.error('[记忆时间线] 错误:', err.message);
    res.status(500).json({ error: `获取时间线失败: ${err.message}` });
  }
});

// ---- 健康检查代理 ----
app.get('/api/proxy/:id/health', async (req, res) => {
  const svc = SERVICES[req.params.id];
  if (!svc || !svc.healthUrl) {
    return res.status(404).json({ error: '未知服务或无健康检查端点' });
  }
  try {
    const resp = await fetch(svc.healthUrl, { signal: AbortSignal.timeout(5000) });
    const data = await resp.json();
    res.json({ proxy: true, service: req.params.id, status: resp.status, data });
  } catch {
    res.json({ proxy: true, service: req.params.id, status: 'unreachable', data: null });
  }
});

// ---- 数据库状态检查 ----
import net from 'net';

const IS_DOCKER = process.env.RUNNING_IN_DOCKER === 'true';

function isPortOpen(port, host) {
  return new Promise((resolve) => {
    const sock = new net.Socket();
    sock.setTimeout(2000);
    sock.on('connect', () => { sock.destroy(); resolve(true); });
    sock.on('error', () => { sock.destroy(); resolve(false); });
    sock.on('timeout', () => { sock.destroy(); resolve(false); });
    sock.connect(port, host || '127.0.0.1');
  });
}

app.get('/api/database/status', async (_req, res) => {
  const DB_PORTS = IS_DOCKER
    ? [
        { port: 5432, name: 'PostgreSQL', host: 'postgres' },
        { port: 6379, name: 'Redis', host: 'redis' },
        { port: 6334, name: 'Qdrant gRPC', host: 'qdrant' },
        { port: 9000, name: 'MinIO API', host: 'minio' },
      ]
    : [
        { port: 5432, name: 'PostgreSQL' },
        { port: 6379, name: 'Redis' },
        { port: 6334, name: 'Qdrant gRPC' },
        { port: 9000, name: 'MinIO API' },
        { port: 4222, name: 'NATS' },
      ];

  const results = await Promise.all(DB_PORTS.map(async ({ port, name, host }) => {
    const alive = await isPortOpen(port, host);
    return { port, name, alive };
  }));

  const allAlive = results.every(p => p.alive);
  const aliveCount = results.filter(p => p.alive).length;

  let pgDetails = null;
  const pgPort = results.find(p => p.port === 5432);
  if (pgPort?.alive) {
    try {
      const envPath = path.join(ROOT, 'backend', '.env');
      let pgUser = 'cyrene', pgPass = 'cyrene_pass', pgDb = 'cyrene_ai';
      if (fs.existsSync(envPath)) {
        const envContent = fs.readFileSync(envPath, 'utf-8');
        const mUser = envContent.match(/^POSTGRES_USER=(.+)$/m);
        const mPass = envContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
        const mDb = envContent.match(/^POSTGRES_DB=(.+)$/m);
        if (mUser) pgUser = mUser[1];
        if (mPass) pgPass = mPass[1];
        if (mDb) pgDb = mDb[1];
      }
      const out = execSync(
        `docker exec cyrene_postgres psql -U "${pgUser}" -d "${pgDb}" -t -c "SELECT count(*) FROM memories;" 2>nul`,
        { encoding: 'utf-8', timeout: 5000, windowsHide: true }
      );
      const match = out.match(/(\d+)/);
      pgDetails = { memories: match ? parseInt(match[1]) : 0, database: pgDb };
    } catch { /* pg query failed */ }
  }

  res.json({
    timestamp: Date.now(),
    ports: results,
    allAlive,
    aliveCount,
    totalPorts: DB_PORTS.length,
    pgDetails,
  });
});

// ---- 数据库控制 (Docker Compose) ----
const DB_COMPOSE_FILE = path.join(ROOT, 'docker-compose.dev.db.yml');
const DB_PORT = 5432;

// GET /api/db/status
app.get('/api/db/status', async (_req, res) => {
  try {
    const online = await isPortOpen(DB_PORT, IS_DOCKER ? 'postgres' : undefined);
    res.json({ online, port: DB_PORT, checked_at: new Date().toISOString() });
  } catch {
    res.json({ online: false, port: DB_PORT, checked_at: new Date().toISOString() });
  }
});

// POST /api/db/start
app.post('/api/db/start', (_req, res) => {
  try {
    const out = execSync(`docker compose -f "${DB_COMPOSE_FILE}" up -d`, {
      encoding: 'utf-8',
      timeout: 60000,
      stdio: 'pipe',
    });
    res.json({ success: true, action: 'start', output: out.trim() });
  } catch (err) {
    const stderr = err.stderr?.toString() || err.message;
    res.status(500).json({ success: false, action: 'start', error: stderr });
  }
});

// POST /api/db/stop
app.post('/api/db/stop', (_req, res) => {
  try {
    const out = execSync(`docker compose -f "${DB_COMPOSE_FILE}" down`, {
      encoding: 'utf-8',
      timeout: 30000,
      stdio: 'pipe',
    });
    res.json({ success: true, action: 'stop', output: out.trim() });
  } catch (err) {
    const stderr = err.stderr?.toString() || err.message;
    res.status(500).json({ success: false, action: 'stop', error: stderr });
  }
});

// POST /api/db/restart
app.post('/api/db/restart', (_req, res) => {
  try {
    const downOut = execSync(`docker compose -f "${DB_COMPOSE_FILE}" down`, {
      encoding: 'utf-8',
      timeout: 30000,
      stdio: 'pipe',
    });
    const upOut = execSync(`docker compose -f "${DB_COMPOSE_FILE}" up -d`, {
      encoding: 'utf-8',
      timeout: 60000,
      stdio: 'pipe',
    });
    res.json({
      success: true,
      action: 'restart',
      output: `down: ${downOut.trim()}\nup: ${upOut.trim()}`,
    });
  } catch (err) {
    const stderr = err.stderr?.toString() || err.message;
    res.status(500).json({ success: false, action: 'restart', error: stderr });
  }
});

// ========== 启动 ==========
// 启动性能监控
performanceMonitor.start();

// 确保日志目录存在
fs.mkdirSync(LOGS_DIR, { recursive: true });

server.listen(ETHEND_PORT, () => {
  console.log(`🛠️  Cyrene ethend 已启动: http://localhost:${ETHEND_PORT}`);
  console.log(`   API: http://localhost:${ETHEND_PORT}/api/health`);
  console.log(`   WebSocket: ws://localhost:${ETHEND_PORT}/ws`);
  console.log(`   Web控制台: http://localhost:${ETHEND_PORT}`);
  console.log('');
  console.log('   可用服务:');
  for (const [id, svc] of Object.entries(SERVICES)) {
    console.log(`   - ${svc.name} (${id}): ${svc.healthUrl || 'N/A'}`);
  }

  // ethend 重启后自动接管已运行的服务（延迟 2s 等待健康检查就绪）
  setTimeout(async () => {
    console.log('[ethend] 扫描已运行的服务...');
    for (const [id] of Object.entries(SERVICES)) {
      if (id === 'frontend') continue; // 前端不需要接管
      try {
        const adopted = await processManager.tryAdopt(id);
        if (adopted) {
          console.log(`  ✓ ${id} 已接管`);
        }
      } catch { /* skip */ }
    }
    console.log('[ethend] 服务扫描完成');
  }, 2000);
});

// readBridgeLogs 从磁盘读取 platform-bridge 的 JSONL 日志文件
function readBridgeLogs(limit) {
  const bridgeLogDir = path.join(ROOT, 'backend', 'platform-bridge', 'logs');
  const logFiles = ['obv11-main.log', 'obv11.log']; // 新旧平台名都试试
  const entries = [];
  for (const name of logFiles) {
    const logPath = path.join(bridgeLogDir, name);
    if (!fs.existsSync(logPath)) continue;
    try {
      const content = fs.readFileSync(logPath, 'utf-8');
      const lines = content.trim().split('\n');
      // 取最后 limit 行
      const recent = lines.slice(-limit);
      for (const line of recent) {
        try {
          entries.push(JSON.parse(line));
        } catch { /* skip bad lines */ }
      }
      break; // 优先用第一个找到的文件
    } catch { /* skip */ }
  }
  return entries.slice(-limit);
}

// 时间格式化：使用系统本地时区
function formatLocalTime(ts) {
  var d = new Date(ts);
  var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
  return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' +
    pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}

// 优雅退出
process.on('SIGINT', async () => {
  console.log('\n关闭所有服务...');
  await processManager.stopAll();
  performanceMonitor.stop();
  process.exit(0);
});

process.on('SIGTERM', async () => {
  await processManager.stopAll();
  performanceMonitor.stop();
  process.exit(0);
});
