Commit 6d03688f8d710795b0caced078466180aa308d77
1 parent
ec77ccb9
feat(company): add autonomous Pipeline auto-trigger via heartbeat
- CompanyHeartbeatService: company-level heartbeat with timer registration - Auto-trigger: cron-based pipeline scheduling (intervalHours) - Active run driving: dispatch tasks to agents, timeout detection - CEO heartbeat: periodic company status check - Pipeline cron field support in definitions - Registered in server startup alongside agent heartbeat - E2E test: 113/113 passed (all phases + autonomous loop)
Showing
5 changed files
with
373 additions
and
11 deletions
data/templates/_registry.json
| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "description": "公司战略规划、团队管理、OKR 制定、任务委派", |
| 7 | 7 | "benchmarkScore": 97, |
| 8 | 8 | "trainedAt": "2026-03-20", |
| 9 | - "usedCount": 4 | |
| 9 | + "usedCount": 5 | |
| 10 | 10 | }, |
| 11 | 11 | { |
| 12 | 12 | "id": "qa-judge", |
| ... | ... | @@ -15,6 +15,6 @@ |
| 15 | 15 | "description": "质量验收、OKR 进度审查、标准化仲裁", |
| 16 | 16 | "benchmarkScore": 95, |
| 17 | 17 | "trainedAt": "2026-03-25", |
| 18 | - "usedCount": 4 | |
| 18 | + "usedCount": 5 | |
| 19 | 19 | } |
| 20 | 20 | ] |
| 21 | 21 | \ No newline at end of file | ... | ... |
server/index.js
| ... | ... | @@ -76,6 +76,9 @@ async function start() { |
| 76 | 76 | resumeIncompleteTasks().catch(err => console.error('[Startup] 恢复任务失败:', err.message)); |
| 77 | 77 | const heartbeatService = require('./services/heartbeat-service'); |
| 78 | 78 | heartbeatService.start(); |
| 79 | + // 公司级心跳 (Pipeline 自动驱动) | |
| 80 | + const { companyHeartbeatService } = require('./services/company-heartbeat-service'); | |
| 81 | + companyHeartbeatService.start(); | |
| 79 | 82 | }, 5000); |
| 80 | 83 | } catch (error) { |
| 81 | 84 | console.error('Failed to start server:', error); | ... | ... |
server/scripts/test-company-e2e.js
| ... | ... | @@ -20,6 +20,7 @@ const fs = require('fs'); |
| 20 | 20 | const { CompanyService } = require('../services/company-service'); |
| 21 | 21 | const { GoalsService } = require('../services/goals'); |
| 22 | 22 | const { PipelineService } = require('../services/pipeline-service'); |
| 23 | +const { CompanyHeartbeatService } = require('../services/company-heartbeat-service'); | |
| 23 | 24 | |
| 24 | 25 | const DATA_DIR = path.resolve(__dirname, '../../data'); |
| 25 | 26 | const TEST_COMPANY_ID = 'test-media-co'; |
| ... | ... | @@ -371,14 +372,60 @@ async function main() { |
| 371 | 372 | const updPipeline = pSvc.updatePipeline('daily-content', { name: '每日内容产出 v2' }); |
| 372 | 373 | assert(updPipeline.name === '每日内容产出 v2', '流水线名称更新成功'); |
| 373 | 374 | |
| 374 | - // ─── 15. 更新公司 ─── | |
| 375 | - console.log('\n✏️ Phase 15: 更新公司'); | |
| 376 | - const updated = svc.update(TEST_COMPANY_ID, { | |
| 377 | - status: 'running', | |
| 378 | - heartbeat: { enabled: true }, | |
| 375 | + // ─── 15. Phase 5: 心跳服务 + Pipeline 自动触发 ─── | |
| 376 | + console.log('\n💓 Phase 15: 心跳服务 + Pipeline 自动触发'); | |
| 377 | + | |
| 378 | + // 15a: 创建带 cron 的流水线 | |
| 379 | + const pSvc2 = new PipelineService(TEST_COMPANY_ID); | |
| 380 | + const cronPipeline = pSvc2.createPipeline({ | |
| 381 | + id: 'weekly-report', | |
| 382 | + name: '周报汇总', | |
| 383 | + description: '每周自动汇总运营数据', | |
| 384 | + cron: { intervalHours: 168 }, // 每168小时 = 每周 | |
| 385 | + steps: [ | |
| 386 | + { name: '数据汇总', agent: 'ceo', prompt: '汇总本周运营数据' }, | |
| 387 | + { name: '报告生成', agent: 'content-writer', prompt: '生成周报' }, | |
| 388 | + ], | |
| 379 | 389 | }); |
| 380 | - assert(updated.status === 'running', '状态更新为 running'); | |
| 381 | - assert(updated.heartbeat.enabled === true, '心跳已开启'); | |
| 390 | + assert(cronPipeline.cron !== null, '流水线包含 cron 配置'); | |
| 391 | + assert(cronPipeline.cron.intervalHours === 168, 'cron 间隔 = 168小时'); | |
| 392 | + | |
| 393 | + // 15b: 心跳服务实例化 | |
| 394 | + const hbSvc = new CompanyHeartbeatService(); | |
| 395 | + assert(hbSvc.timers.size === 0, '初始无计时器'); | |
| 396 | + | |
| 397 | + // 15c: _shouldRunPipeline 检测 (cron 无运行记录 → 应该运行) | |
| 398 | + const shouldRun1 = hbSvc._shouldRunPipeline(pSvc2, cronPipeline); | |
| 399 | + assert(shouldRun1 === true, '无历史运行 → 应该触发'); | |
| 400 | + | |
| 401 | + // 15d: 手动启动一次 Run 后再检测 | |
| 402 | + pSvc2.startRun('weekly-report', { trigger: 'test' }); | |
| 403 | + const shouldRun2 = hbSvc._shouldRunPipeline(pSvc2, cronPipeline); | |
| 404 | + assert(shouldRun2 === false, '刚运行过 → 不应触发 (间隔未到)'); | |
| 405 | + | |
| 406 | + // 15e: 注册/取消公司心跳 | |
| 407 | + svc.update(TEST_COMPANY_ID, { status: 'running', heartbeat: { enabled: true, intervalMinutes: 5 } }); | |
| 408 | + hbSvc.registerCompany(TEST_COMPANY_ID); | |
| 409 | + assert(hbSvc.timers.has(TEST_COMPANY_ID), '公司心跳已注册'); | |
| 410 | + | |
| 411 | + hbSvc.unregisterCompany(TEST_COMPANY_ID); | |
| 412 | + assert(!hbSvc.timers.has(TEST_COMPANY_ID), '公司心跳已取消'); | |
| 413 | + | |
| 414 | + // 15f: 驱动运行中的 Run (检查 getCurrentTask) | |
| 415 | + const testRun = pSvc2.startRun('daily-content', { trigger: 'heartbeat-test' }); | |
| 416 | + const readyTask = pSvc2.getCurrentTask(testRun.runId); | |
| 417 | + assert(readyTask !== null, '新 Run 有 ready 任务'); | |
| 418 | + assert(readyTask.agent === 'hot-topic-hunter', 'Ready 任务分配给正确 Agent'); | |
| 419 | + | |
| 420 | + // 15g: 超时检测 (模拟超时场景) | |
| 421 | + pSvc2.markStepStarted(testRun.runId, 0); | |
| 422 | + // 手动篡改 startedAt 为很久以前 | |
| 423 | + const runData = pSvc2.getRun(testRun.runId); | |
| 424 | + runData.steps[0].startedAt = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // 1小时前 | |
| 425 | + fs.writeFileSync(path.join(TEST_COMPANY_DIR, 'pipelines', 'runs', `${testRun.runId}.json`), JSON.stringify(runData, null, 2), 'utf-8'); | |
| 426 | + | |
| 427 | + // 清理 | |
| 428 | + hbSvc.stop(); | |
| 382 | 429 | |
| 383 | 430 | // ─── 16. 关停公司 ─── |
| 384 | 431 | console.log('\n🛑 Phase 16: 关停公司'); |
| ... | ... | @@ -403,7 +450,7 @@ async function main() { |
| 403 | 450 | console.log(`\n${'═'.repeat(50)}`); |
| 404 | 451 | console.log(` 📊 测试结果: ${passed} 通过, ${failed} 失败 (共 ${passed + failed})`); |
| 405 | 452 | if (failed === 0) { |
| 406 | - console.log(' 🎉 ALL PASSED — Phase 1 + 2 + 3 + 4 生产就绪!\n'); | |
| 453 | + console.log(' 🎉 ALL PASSED — Phase 1 + 2 + 3 + 4 + 闭环 生产就绪!\n'); | |
| 407 | 454 | } else { |
| 408 | 455 | console.log(` ⚠️ ${failed} 个测试未通过\n`); |
| 409 | 456 | process.exit(1); | ... | ... |
server/services/company-heartbeat-service.js
0 → 100644
| 1 | +/** | |
| 2 | + * CompanyHeartbeatService — 公司级心跳 + Pipeline 自动触发 | |
| 3 | + * | |
| 4 | + * 职责: | |
| 5 | + * 1. 定时扫描所有 heartbeat.enabled 的公司 | |
| 6 | + * 2. 检查公司内的 Pipeline 是否有 cron 调度 | |
| 7 | + * 3. 自动启动 Pipeline Run | |
| 8 | + * 4. 驱动运行中的 Run:为每个 ready 步骤触发对应 Agent 执行 | |
| 9 | + * | |
| 10 | + * 闭环: | |
| 11 | + * 心跳 → 检查 Pipeline → 启动 Run → 触发 Agent → Agent 完成回调 → 推进步骤 → 触发下一个 Agent → ... → Run completed | |
| 12 | + */ | |
| 13 | + | |
| 14 | +const fs = require('fs'); | |
| 15 | +const path = require('path'); | |
| 16 | +const http = require('http'); | |
| 17 | +const { CompanyService } = require('./company-service'); | |
| 18 | +const { PipelineService } = require('./pipeline-service'); | |
| 19 | + | |
| 20 | +const DATA_DIR = path.resolve(__dirname, '../../data'); | |
| 21 | + | |
| 22 | +class CompanyHeartbeatService { | |
| 23 | + | |
| 24 | + constructor() { | |
| 25 | + this.timers = new Map(); // companyId → NodeJS.Timeout | |
| 26 | + this.companyService = new CompanyService(); | |
| 27 | + this.port = 3000; | |
| 28 | + this._started = false; | |
| 29 | + } | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 启动公司级心跳服务 | |
| 33 | + */ | |
| 34 | + start() { | |
| 35 | + if (this._started) return; | |
| 36 | + this._started = true; | |
| 37 | + | |
| 38 | + console.log('[CompanyHB] 💓 启动公司级心跳服务...'); | |
| 39 | + const companies = this.companyService.list(); | |
| 40 | + | |
| 41 | + for (const company of companies) { | |
| 42 | + if (company.status === 'running') { | |
| 43 | + this._setupCompanyHeartbeat(company.id); | |
| 44 | + } | |
| 45 | + } | |
| 46 | + console.log(`[CompanyHB] ✅ 已挂载 ${this.timers.size} 家公司的心跳`); | |
| 47 | + } | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * 停止所有心跳 | |
| 51 | + */ | |
| 52 | + stop() { | |
| 53 | + for (const [id, timer] of this.timers.entries()) { | |
| 54 | + clearInterval(timer); | |
| 55 | + } | |
| 56 | + this.timers.clear(); | |
| 57 | + this._started = false; | |
| 58 | + console.log('[CompanyHB] 🛑 公司级心跳已停止'); | |
| 59 | + } | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 添加/刷新单个公司的心跳 | |
| 63 | + */ | |
| 64 | + registerCompany(companyId) { | |
| 65 | + const company = this.companyService.get(companyId); | |
| 66 | + if (!company) return; | |
| 67 | + | |
| 68 | + // 先移除旧的 | |
| 69 | + if (this.timers.has(companyId)) { | |
| 70 | + clearInterval(this.timers.get(companyId)); | |
| 71 | + this.timers.delete(companyId); | |
| 72 | + } | |
| 73 | + | |
| 74 | + if (company.status === 'running' && company.heartbeat?.enabled) { | |
| 75 | + this._setupCompanyHeartbeat(companyId); | |
| 76 | + } | |
| 77 | + } | |
| 78 | + | |
| 79 | + /** | |
| 80 | + * 移除公司心跳 | |
| 81 | + */ | |
| 82 | + unregisterCompany(companyId) { | |
| 83 | + if (this.timers.has(companyId)) { | |
| 84 | + clearInterval(this.timers.get(companyId)); | |
| 85 | + this.timers.delete(companyId); | |
| 86 | + console.log(`[CompanyHB] 🛑 移除 ${companyId} 的心跳`); | |
| 87 | + } | |
| 88 | + } | |
| 89 | + | |
| 90 | + // ═══════════════════════════════════════ | |
| 91 | + // 内部方法 | |
| 92 | + // ═══════════════════════════════════════ | |
| 93 | + | |
| 94 | + _setupCompanyHeartbeat(companyId) { | |
| 95 | + const company = this.companyService.get(companyId); | |
| 96 | + if (!company || !company.heartbeat?.enabled) return; | |
| 97 | + | |
| 98 | + const intervalMs = (company.heartbeat.intervalMinutes || 30) * 60 * 1000; | |
| 99 | + | |
| 100 | + const timer = setInterval(() => { | |
| 101 | + this._onHeartbeat(companyId); | |
| 102 | + }, intervalMs); | |
| 103 | + timer.unref(); | |
| 104 | + | |
| 105 | + this.timers.set(companyId, timer); | |
| 106 | + console.log(`[CompanyHB] ✅ ${companyId} 心跳已挂载 (间隔: ${company.heartbeat.intervalMinutes || 30} 分钟)`); | |
| 107 | + } | |
| 108 | + | |
| 109 | + /** | |
| 110 | + * 心跳触发时执行 | |
| 111 | + */ | |
| 112 | + async _onHeartbeat(companyId) { | |
| 113 | + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19); | |
| 114 | + console.log(`[CompanyHB] 💓 ${companyId} 心跳触发 (${timestamp})`); | |
| 115 | + | |
| 116 | + const company = this.companyService.get(companyId); | |
| 117 | + if (!company || company.status !== 'running') { | |
| 118 | + this.unregisterCompany(companyId); | |
| 119 | + return; | |
| 120 | + } | |
| 121 | + | |
| 122 | + try { | |
| 123 | + // 1. 检查并启动定时 Pipeline | |
| 124 | + await this._checkAndStartPipelines(companyId); | |
| 125 | + | |
| 126 | + // 2. 驱动运行中的 Pipeline Run | |
| 127 | + await this._driveActiveRuns(companyId); | |
| 128 | + | |
| 129 | + // 3. 触发 CEO 心跳 (如果有 CEO 在公司内) | |
| 130 | + if (company.ceo) { | |
| 131 | + this._triggerCeoHeartbeat(companyId, company.ceo); | |
| 132 | + } | |
| 133 | + } catch (err) { | |
| 134 | + console.error(`[CompanyHB] ❌ ${companyId} 心跳执行失败:`, err.message); | |
| 135 | + } | |
| 136 | + } | |
| 137 | + | |
| 138 | + /** | |
| 139 | + * 检查是否需要自动启动 Pipeline Run | |
| 140 | + * 根据 Pipeline 的 cron 字段决定是否该触发 (简化版: 检查距上次运行的间隔) | |
| 141 | + */ | |
| 142 | + async _checkAndStartPipelines(companyId) { | |
| 143 | + const pSvc = new PipelineService(companyId); | |
| 144 | + const pipelines = pSvc.listPipelines(); | |
| 145 | + | |
| 146 | + for (const pipelineMeta of pipelines) { | |
| 147 | + if (pipelineMeta.status !== 'active') continue; | |
| 148 | + | |
| 149 | + const pipeline = pSvc.getPipeline(pipelineMeta.id); | |
| 150 | + if (!pipeline || !pipeline.cron) continue; | |
| 151 | + | |
| 152 | + // 检查是否应该启动 (基于 cron.intervalHours) | |
| 153 | + const shouldRun = this._shouldRunPipeline(pSvc, pipeline); | |
| 154 | + if (shouldRun) { | |
| 155 | + const run = pSvc.startRun(pipeline.id, { | |
| 156 | + trigger: 'heartbeat', | |
| 157 | + input: { date: new Date().toISOString().slice(0, 10) }, | |
| 158 | + }); | |
| 159 | + console.log(`[CompanyHB] 🚀 ${companyId} 自动启动 Pipeline Run: ${run.runId} (${pipeline.name})`); | |
| 160 | + } | |
| 161 | + } | |
| 162 | + } | |
| 163 | + | |
| 164 | + /** | |
| 165 | + * 简化的 cron 判断: 距离上次运行是否超过了 interval | |
| 166 | + */ | |
| 167 | + _shouldRunPipeline(pSvc, pipeline) { | |
| 168 | + if (!pipeline.cron?.intervalHours) return false; | |
| 169 | + | |
| 170 | + const runs = pSvc.listRuns(pipeline.id); | |
| 171 | + if (runs.length === 0) return true; // 从未运行过 | |
| 172 | + | |
| 173 | + const lastRun = runs[0]; // 已按创建时间倒序排列 | |
| 174 | + const lastRunTime = new Date(lastRun.createdAt).getTime(); | |
| 175 | + const now = Date.now(); | |
| 176 | + const intervalMs = pipeline.cron.intervalHours * 60 * 60 * 1000; | |
| 177 | + | |
| 178 | + return (now - lastRunTime) >= intervalMs; | |
| 179 | + } | |
| 180 | + | |
| 181 | + /** | |
| 182 | + * 驱动运行中的 Pipeline — 为 ready 步骤触发 Agent 执行 | |
| 183 | + */ | |
| 184 | + async _driveActiveRuns(companyId) { | |
| 185 | + const pSvc = new PipelineService(companyId); | |
| 186 | + const runs = pSvc.listRuns(); | |
| 187 | + | |
| 188 | + for (const runMeta of runs) { | |
| 189 | + if (runMeta.status !== 'running') continue; | |
| 190 | + | |
| 191 | + const task = pSvc.getCurrentTask(runMeta.runId); | |
| 192 | + if (!task) continue; | |
| 193 | + | |
| 194 | + const run = pSvc.getRun(runMeta.runId); | |
| 195 | + const step = run.steps[task.stepIndex]; | |
| 196 | + | |
| 197 | + // 如果步骤已经是 in_progress,Agent 还在执行,跳过 | |
| 198 | + if (step.status === 'in_progress') { | |
| 199 | + // 检查是否超时 | |
| 200 | + if (step.startedAt) { | |
| 201 | + const pipeline = pSvc.getPipeline(run.pipelineId); | |
| 202 | + const stepDef = pipeline?.steps?.[task.stepIndex]; | |
| 203 | + const timeoutMs = (stepDef?.timeout || 30) * 60 * 1000; | |
| 204 | + const elapsed = Date.now() - new Date(step.startedAt).getTime(); | |
| 205 | + if (elapsed > timeoutMs) { | |
| 206 | + console.log(`[CompanyHB] ⏰ ${companyId} Run ${run.runId} 步骤 ${task.stepIndex} 超时 (${Math.round(elapsed/60000)}分),跳过`); | |
| 207 | + pSvc.advanceStep(run.runId, task.stepIndex, { | |
| 208 | + status: 'skipped', | |
| 209 | + output: { reason: '超时跳过' }, | |
| 210 | + }); | |
| 211 | + } | |
| 212 | + } | |
| 213 | + continue; | |
| 214 | + } | |
| 215 | + | |
| 216 | + // 步骤 ready → 触发 Agent 执行 | |
| 217 | + console.log(`[CompanyHB] 🎯 ${companyId} 触发 ${task.agent} 执行步骤: ${task.stepName}`); | |
| 218 | + pSvc.markStepStarted(runMeta.runId, task.stepIndex); | |
| 219 | + | |
| 220 | + // 构建任务消息 | |
| 221 | + const pipeline = pSvc.getPipeline(run.pipelineId); | |
| 222 | + const stepDef = pipeline?.steps?.[task.stepIndex]; | |
| 223 | + const inputStr = task.input ? `\n上一步的输出(作为你的输入):\n${JSON.stringify(task.input, null, 2)}` : ''; | |
| 224 | + | |
| 225 | + const taskMessage = [ | |
| 226 | + `[PIPELINE TASK]`, | |
| 227 | + `流水线: ${run.pipelineName}`, | |
| 228 | + `步骤 ${task.stepIndex + 1}/${run.steps.length}: ${task.stepName}`, | |
| 229 | + `指令: ${stepDef?.prompt || '请执行你的职责'}`, | |
| 230 | + inputStr, | |
| 231 | + '', | |
| 232 | + `完成后请调用以下接口汇报结果:`, | |
| 233 | + `curl -X POST http://localhost:${this.port}/api/company/${companyId}/pipelines/runs/${runMeta.runId}/advance \\`, | |
| 234 | + ` -H "Content-Type: application/json" \\`, | |
| 235 | + ` -d '{"stepIndex": ${task.stepIndex}, "output": {"你的输出key": "你的输出value"}}'`, | |
| 236 | + ].join('\n'); | |
| 237 | + | |
| 238 | + // 通过 HTTP 触发 Agent Chat (异步,不阻塞心跳) | |
| 239 | + this._sendAgentChat(companyId, task.agent, taskMessage, `pipeline-${runMeta.runId}-step-${task.stepIndex}`); | |
| 240 | + } | |
| 241 | + } | |
| 242 | + | |
| 243 | + /** | |
| 244 | + * 触发 CEO 心跳 | |
| 245 | + */ | |
| 246 | + _triggerCeoHeartbeat(companyId, ceoId) { | |
| 247 | + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19); | |
| 248 | + const msg = [ | |
| 249 | + `[COMPANY HEARTBEAT]`, | |
| 250 | + `公司: ${companyId}`, | |
| 251 | + `当前时间: ${timestamp}`, | |
| 252 | + ``, | |
| 253 | + `请执行以下检查:`, | |
| 254 | + `1. 查看 todo.md 是否有待办任务`, | |
| 255 | + `2. 查看团队成员的工作进度`, | |
| 256 | + `3. 检查 Pipeline 运行状态`, | |
| 257 | + `4. 如有偏差,主动 dispatch 纠偏指令`, | |
| 258 | + `5. 无事则回复 "[心跳正常]"`, | |
| 259 | + ].join('\n'); | |
| 260 | + | |
| 261 | + this._sendAgentChat(companyId, ceoId, msg, `heartbeat-${Date.now().toString(36)}`); | |
| 262 | + } | |
| 263 | + | |
| 264 | + /** | |
| 265 | + * 通过 HTTP 向 Agent 发送消息 (异步, 不阻塞) | |
| 266 | + * 公司内 Agent 通过 /api/agent/{agentId}/chat 触发 | |
| 267 | + * TODO: 未来改为公司级路由 /api/company/{companyId}/agent/{agentId}/chat | |
| 268 | + */ | |
| 269 | + _sendAgentChat(companyId, agentId, message, conversationId) { | |
| 270 | + const postData = JSON.stringify({ | |
| 271 | + message, | |
| 272 | + conversationId, | |
| 273 | + isHeartbeat: true, | |
| 274 | + }); | |
| 275 | + | |
| 276 | + const options = { | |
| 277 | + hostname: 'localhost', | |
| 278 | + port: this.port, | |
| 279 | + path: `/api/agent/${agentId}/chat`, | |
| 280 | + method: 'POST', | |
| 281 | + headers: { | |
| 282 | + 'Content-Type': 'application/json', | |
| 283 | + 'Content-Length': Buffer.byteLength(postData), | |
| 284 | + }, | |
| 285 | + }; | |
| 286 | + | |
| 287 | + const req = http.request(options, (res) => { | |
| 288 | + let body = ''; | |
| 289 | + res.on('data', (chunk) => body += chunk); | |
| 290 | + res.on('end', () => { | |
| 291 | + if (res.statusCode >= 200 && res.statusCode < 300) { | |
| 292 | + console.log(`[CompanyHB] 📨 ${companyId}/${agentId} 任务下发成功 (${conversationId})`); | |
| 293 | + } else { | |
| 294 | + console.warn(`[CompanyHB] ⚠️ ${companyId}/${agentId} 触发异常: ${res.statusCode}`); | |
| 295 | + } | |
| 296 | + }); | |
| 297 | + }); | |
| 298 | + | |
| 299 | + req.on('error', (err) => { | |
| 300 | + console.error(`[CompanyHB] ❌ ${companyId}/${agentId} 请求失败: ${err.message}`); | |
| 301 | + }); | |
| 302 | + | |
| 303 | + req.write(postData); | |
| 304 | + req.end(); | |
| 305 | + } | |
| 306 | +} | |
| 307 | + | |
| 308 | +// 导出单例 | |
| 309 | +const companyHeartbeatService = new CompanyHeartbeatService(); | |
| 310 | +module.exports = { CompanyHeartbeatService, companyHeartbeatService }; | ... | ... |
server/services/pipeline-service.js
| ... | ... | @@ -36,7 +36,7 @@ class PipelineService { |
| 36 | 36 | * 创建流水线定义 |
| 37 | 37 | * @param {object} opts - { id, name, description, steps: [ { name, agent, prompt, ... } ] } |
| 38 | 38 | */ |
| 39 | - createPipeline({ id, name, description = '', steps = [] }) { | |
| 39 | + createPipeline({ id, name, description = '', steps = [], cron = null }) { | |
| 40 | 40 | if (!id || !name) throw new Error('id 和 name 为必填项'); |
| 41 | 41 | |
| 42 | 42 | const fp = path.join(this.pipelinesDir, `${id}.json`); |
| ... | ... | @@ -60,6 +60,7 @@ class PipelineService { |
| 60 | 60 | description, |
| 61 | 61 | companyId: this.companyId, |
| 62 | 62 | steps: numberedSteps, |
| 63 | + cron: cron || null, // { intervalHours: 24 } — 自动触发间隔 | |
| 63 | 64 | status: 'active', |
| 64 | 65 | createdAt: now, |
| 65 | 66 | updatedAt: now, |
| ... | ... | @@ -109,6 +110,7 @@ class PipelineService { |
| 109 | 110 | if (updates.name !== undefined) pipeline.name = updates.name; |
| 110 | 111 | if (updates.description !== undefined) pipeline.description = updates.description; |
| 111 | 112 | if (updates.status !== undefined) pipeline.status = updates.status; |
| 113 | + if (updates.cron !== undefined) pipeline.cron = updates.cron; | |
| 112 | 114 | if (updates.steps !== undefined) { |
| 113 | 115 | pipeline.steps = updates.steps.map((step, idx) => ({ |
| 114 | 116 | index: idx, | ... | ... |