company-heartbeat-service.js 10 KB
/**
 * CompanyHeartbeatService — 公司级心跳 + Pipeline 自动触发
 *
 * 职责:
 *   1. 定时扫描所有 heartbeat.enabled 的公司
 *   2. 检查公司内的 Pipeline 是否有 cron 调度
 *   3. 自动启动 Pipeline Run
 *   4. 驱动运行中的 Run:为每个 ready 步骤触发对应 Agent 执行
 *
 * 闭环:
 *   心跳 → 检查 Pipeline → 启动 Run → 触发 Agent → Agent 完成回调 → 推进步骤 → 触发下一个 Agent → ... → Run completed
 */

const fs = require('fs');
const path = require('path');
const http = require('http');
const { CompanyService } = require('./company-service');
const { PipelineService } = require('./pipeline-service');

const DATA_DIR = path.resolve(__dirname, '../../data');

class CompanyHeartbeatService {

  constructor() {
    this.timers = new Map();      // companyId → NodeJS.Timeout
    this.companyService = new CompanyService();
    this.port = 3000;
    this._started = false;
  }

  /**
   * 启动公司级心跳服务
   */
  start() {
    if (this._started) return;
    this._started = true;

    console.log('[CompanyHB] 💓 启动公司级心跳服务...');
    const companies = this.companyService.list();

    for (const company of companies) {
      if (company.status === 'running') {
        this._setupCompanyHeartbeat(company.id);
      }
    }
    console.log(`[CompanyHB] ✅ 已挂载 ${this.timers.size} 家公司的心跳`);
  }

  /**
   * 停止所有心跳
   */
  stop() {
    for (const [id, timer] of this.timers.entries()) {
      clearInterval(timer);
    }
    this.timers.clear();
    this._started = false;
    console.log('[CompanyHB] 🛑 公司级心跳已停止');
  }

  /**
   * 添加/刷新单个公司的心跳
   */
  registerCompany(companyId) {
    const company = this.companyService.get(companyId);
    if (!company) return;

    // 先移除旧的
    if (this.timers.has(companyId)) {
      clearInterval(this.timers.get(companyId));
      this.timers.delete(companyId);
    }

    if (company.status === 'running' && company.heartbeat?.enabled) {
      this._setupCompanyHeartbeat(companyId);
    }
  }

  /**
   * 移除公司心跳
   */
  unregisterCompany(companyId) {
    if (this.timers.has(companyId)) {
      clearInterval(this.timers.get(companyId));
      this.timers.delete(companyId);
      console.log(`[CompanyHB] 🛑 移除 ${companyId} 的心跳`);
    }
  }

  // ═══════════════════════════════════════
  //  内部方法
  // ═══════════════════════════════════════

  _setupCompanyHeartbeat(companyId) {
    const company = this.companyService.get(companyId);
    if (!company || !company.heartbeat?.enabled) return;

    const intervalMs = (company.heartbeat.intervalMinutes || 30) * 60 * 1000;

    const timer = setInterval(() => {
      this._onHeartbeat(companyId);
    }, intervalMs);
    timer.unref();

    this.timers.set(companyId, timer);
    console.log(`[CompanyHB] ✅ ${companyId} 心跳已挂载 (间隔: ${company.heartbeat.intervalMinutes || 30} 分钟)`);
  }

  /**
   * 心跳触发时执行
   */
  async _onHeartbeat(companyId) {
    const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
    console.log(`[CompanyHB] 💓 ${companyId} 心跳触发 (${timestamp})`);

    const company = this.companyService.get(companyId);
    if (!company || company.status !== 'running') {
      this.unregisterCompany(companyId);
      return;
    }

    try {
      // 1. 检查并启动定时 Pipeline
      await this._checkAndStartPipelines(companyId);

      // 2. 驱动运行中的 Pipeline Run
      await this._driveActiveRuns(companyId);

      // 3. 触发 CEO 心跳 (如果有 CEO 在公司内)
      if (company.ceo) {
        this._triggerCeoHeartbeat(companyId, company.ceo);
      }
    } catch (err) {
      console.error(`[CompanyHB] ❌ ${companyId} 心跳执行失败:`, err.message);
    }
  }

  /**
   * 检查是否需要自动启动 Pipeline Run
   * 根据 Pipeline 的 cron 字段决定是否该触发 (简化版: 检查距上次运行的间隔)
   */
  async _checkAndStartPipelines(companyId) {
    const pSvc = new PipelineService(companyId);
    const pipelines = pSvc.listPipelines();

    for (const pipelineMeta of pipelines) {
      if (pipelineMeta.status !== 'active') continue;

      const pipeline = pSvc.getPipeline(pipelineMeta.id);
      if (!pipeline || !pipeline.cron) continue;

      // 检查是否应该启动 (基于 cron.intervalHours)
      const shouldRun = this._shouldRunPipeline(pSvc, pipeline);
      if (shouldRun) {
        const run = pSvc.startRun(pipeline.id, {
          trigger: 'heartbeat',
          input: { date: new Date().toISOString().slice(0, 10) },
        });
        console.log(`[CompanyHB] 🚀 ${companyId} 自动启动 Pipeline Run: ${run.runId} (${pipeline.name})`);
      }
    }
  }

  /**
   * 简化的 cron 判断: 距离上次运行是否超过了 interval
   */
  _shouldRunPipeline(pSvc, pipeline) {
    if (!pipeline.cron?.intervalHours) return false;

    const runs = pSvc.listRuns(pipeline.id);
    if (runs.length === 0) return true;  // 从未运行过

    const lastRun = runs[0]; // 已按创建时间倒序排列
    const lastRunTime = new Date(lastRun.createdAt).getTime();
    const now = Date.now();
    const intervalMs = pipeline.cron.intervalHours * 60 * 60 * 1000;

    return (now - lastRunTime) >= intervalMs;
  }

  /**
   * 驱动运行中的 Pipeline — 为 ready 步骤触发 Agent 执行
   */
  async _driveActiveRuns(companyId) {
    const pSvc = new PipelineService(companyId);
    const runs = pSvc.listRuns();

    for (const runMeta of runs) {
      if (runMeta.status !== 'running') continue;

      const task = pSvc.getCurrentTask(runMeta.runId);
      if (!task) continue;

      const run = pSvc.getRun(runMeta.runId);
      const step = run.steps[task.stepIndex];

      // 如果步骤已经是 in_progress,Agent 还在执行,跳过
      if (step.status === 'in_progress') {
        // 检查是否超时
        if (step.startedAt) {
          const pipeline = pSvc.getPipeline(run.pipelineId);
          const stepDef = pipeline?.steps?.[task.stepIndex];
          const timeoutMs = (stepDef?.timeout || 30) * 60 * 1000;
          const elapsed = Date.now() - new Date(step.startedAt).getTime();
          if (elapsed > timeoutMs) {
            console.log(`[CompanyHB] ⏰ ${companyId} Run ${run.runId} 步骤 ${task.stepIndex} 超时 (${Math.round(elapsed/60000)}分),跳过`);
            pSvc.advanceStep(run.runId, task.stepIndex, {
              status: 'skipped',
              output: { reason: '超时跳过' },
            });
          }
        }
        continue;
      }

      // 步骤 ready → 触发 Agent 执行
      console.log(`[CompanyHB] 🎯 ${companyId} 触发 ${task.agent} 执行步骤: ${task.stepName}`);
      pSvc.markStepStarted(runMeta.runId, task.stepIndex);

      // 构建任务消息
      const pipeline = pSvc.getPipeline(run.pipelineId);
      const stepDef = pipeline?.steps?.[task.stepIndex];
      const inputStr = task.input ? `\n上一步的输出(作为你的输入):\n${JSON.stringify(task.input, null, 2)}` : '';

      const taskMessage = [
        `[PIPELINE TASK]`,
        `流水线: ${run.pipelineName}`,
        `步骤 ${task.stepIndex + 1}/${run.steps.length}: ${task.stepName}`,
        `指令: ${stepDef?.prompt || '请执行你的职责'}`,
        inputStr,
        '',
        `完成后请调用以下接口汇报结果:`,
        `curl -X POST http://localhost:${this.port}/api/company/${companyId}/pipelines/runs/${runMeta.runId}/advance \\`,
        `  -H "Content-Type: application/json" \\`,
        `  -d '{"stepIndex": ${task.stepIndex}, "output": {"你的输出key": "你的输出value"}}'`,
      ].join('\n');

      // 通过 HTTP 触发 Agent Chat (异步,不阻塞心跳)
      this._sendAgentChat(companyId, task.agent, taskMessage, `pipeline-${runMeta.runId}-step-${task.stepIndex}`);
    }
  }

  /**
   * 触发 CEO 心跳
   */
  _triggerCeoHeartbeat(companyId, ceoId) {
    const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
    const msg = [
      `[COMPANY HEARTBEAT]`,
      `公司: ${companyId}`,
      `当前时间: ${timestamp}`,
      ``,
      `请执行以下检查:`,
      `1. 查看 todo.md 是否有待办任务`,
      `2. 查看团队成员的工作进度`,
      `3. 检查 Pipeline 运行状态`,
      `4. 如有偏差,主动 dispatch 纠偏指令`,
      `5. 无事则回复 "[心跳正常]"`,
    ].join('\n');

    this._sendAgentChat(companyId, ceoId, msg, `heartbeat-${Date.now().toString(36)}`);
  }

  /**
   * 通过 HTTP 向 Agent 发送消息 (异步, 不阻塞)
   * 公司内 Agent 通过 /api/agent/{agentId}/chat 触发
   * TODO: 未来改为公司级路由 /api/company/{companyId}/agent/{agentId}/chat
   */
  _sendAgentChat(companyId, agentId, message, conversationId) {
    const postData = JSON.stringify({
      message,
      conversationId,
      isHeartbeat: true,
    });

    const options = {
      hostname: 'localhost',
      port: this.port,
      path: `/api/agent/${agentId}/chat`,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(postData),
      },
    };

    const req = http.request(options, (res) => {
      let body = '';
      res.on('data', (chunk) => body += chunk);
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          console.log(`[CompanyHB] 📨 ${companyId}/${agentId} 任务下发成功 (${conversationId})`);
        } else {
          console.warn(`[CompanyHB] ⚠️ ${companyId}/${agentId} 触发异常: ${res.statusCode}`);
        }
      });
    });

    req.on('error', (err) => {
      console.error(`[CompanyHB] ❌ ${companyId}/${agentId} 请求失败: ${err.message}`);
    });

    req.write(postData);
    req.end();
  }
}

// 导出单例
const companyHeartbeatService = new CompanyHeartbeatService();
module.exports = { CompanyHeartbeatService, companyHeartbeatService };