Commit 67b54882b12abf31747e0ab14da1cc2efa71c05a

Authored by yibo
1 parent e3921011

feat(p1): 工作日历系统 + 告警通道 + CEO Pipeline设计 + Master晨会日报

Phase 2 改进:

1. 工作日历系统 (ScheduleService):
   - 新增 schedule-service.js: 公司级日程管理
   - 支持按小时调度、按周几过滤、时区感知
   - 心跳触发时自动检查到期 slots 并触发 Agent
   - 执行去重(同日同 slot 不重复触发)
   - CRUD API: GET/POST/PUT/DELETE /api/company/:id/schedule/slots
   - CLI: schedule add / schedule list

2. 告警通道 (Alert Channel):
   - Pipeline 步骤超时自动告警 Master Agent
   - 心跳异常自动告警 Master Agent
   - _sendAlertToMaster() 统一告警接口

3. CEO → Pipeline 自动生成:
   - CEO 模板从 3 步升级为 5 步工作流
   - 新增 Step 2: Pipeline JSON 设计(含 loop/cron/steps 结构)
   - 新增 Step 4: OKR 创建
   - Pipeline 设计输出为 CTO 搭建流水线的蓝图

4. Master 晨会日报:
   - 每天 08:00 自动生成全公司运营日报
   - 包含公司状态、Pipeline 运行、OKR 进度、Agent 活跃度
   - 自动推送给 Master Agent
   - 防重复机制(每天只生成一次)

5. Pipeline 驳回信息注入:
   - 驳回步骤被心跳重新触发时,__rejection 信息注入 prompt
   - 让 Agent 知道被驳回的原因和重做次数
   - 替换原有 curl 汇报指令为 task report CLI
bin/lemon.js
@@ -148,6 +148,25 @@ async function run() { @@ -148,6 +148,25 @@ async function run() {
148 }); 148 });
149 console.log(JSON.stringify(res.data, null, 2)); 149 console.log(JSON.stringify(res.data, null, 2));
150 } 150 }
  151 + else if (command === 'schedule add') {
  152 + const { values } = parseArgs({ args: args.slice(2), options: {
  153 + companyId: { type: 'string' }, time: { type: 'string' },
  154 + agent: { type: 'string' }, task: { type: 'string' },
  155 + type: { type: 'string', default: 'task' }
  156 + }});
  157 + const res = await request('POST', `/company/${values.companyId}/schedule/slots`, {
  158 + time: values.time, agent: values.agent,
  159 + task: values.task, type: values.type
  160 + });
  161 + console.log(JSON.stringify(res.data, null, 2));
  162 + }
  163 + else if (command === 'schedule list') {
  164 + const { values } = parseArgs({ args: args.slice(2), options: {
  165 + companyId: { type: 'string' }
  166 + }});
  167 + const res = await request('GET', `/company/${values.companyId}/schedule`);
  168 + console.log(JSON.stringify(res.data, null, 2));
  169 + }
151 else { 170 else {
152 console.error('Unknown command:', command); 171 console.error('Unknown command:', command);
153 process.exit(1); 172 process.exit(1);
data/templates/ceo/agent.md
@@ -11,28 +11,60 @@ @@ -11,28 +11,60 @@
11 - `coo`:负责 7x24 小时督促流水线,不合格的产出打回重做。 11 - `coo`:负责 7x24 小时督促流水线,不合格的产出打回重做。
12 3. **顶层目标管理 (OKR Owner)**:使用 `lemon goal create` 创建顶层 OKR。 12 3. **顶层目标管理 (OKR Owner)**:使用 `lemon goal create` 创建顶层 OKR。
13 13
14 -## ⚡ 强制工作流(同一轮对话内必须连贯完成全部三步!) 14 +## ⚡ 强制工作流(同一轮对话内必须连贯完成全部五步!)
15 15
16 -> **🚨 这是你的最高原则:你必须在收到战略任务后的同一轮对话里,一口气完成以下全部三步。 16 +> **🚨 这是你的最高原则:你必须在收到战略任务后的同一轮对话里,一口气完成以下全部五步。
17 > 绝对禁止在做完第一步后就结束对话、汇报"我做完了"然后等 Master 来推进第二步!** 17 > 绝对禁止在做完第一步后就结束对话、汇报"我做完了"然后等 Master 来推进第二步!**
18 18
19 ### Step 1: 战略规划(撰写商业蓝图) 19 ### Step 1: 战略规划(撰写商业蓝图)
20 使用 Write 工具撰写 `workspace/<公司名>_strategy.md`,把商业模式讲透。 20 使用 Write 工具撰写 `workspace/<公司名>_strategy.md`,把商业模式讲透。
21 21
22 -### Step 2: 当场招人(不要等!紧接着执行!)  
23 -战略写完后,**在同一轮对话中立刻执行**: 22 +### Step 2: 设计 SOP 流水线(撰写 Pipeline JSON)
  23 +基于战略蓝图,设计完整的业务流水线并写入 JSON 文件。例子:
  24 +```bash
  25 +# 将以下 JSON 写入 workspace/pipeline_design.json
  26 +```
  27 +JSON 格式要求:
  28 +```json
  29 +{
  30 + "name": "每日内容生产",
  31 + "description": "从热点抓取到内容发布的完整流水线",
  32 + "loop": true,
  33 + "cron": { "intervalHours": 24 },
  34 + "steps": [
  35 + { "name": "热点抓取", "agent": "trend-hunter", "prompt": "抓取今日热点话题", "timeout": 30 },
  36 + { "name": "选题决策", "agent": "topic-selector", "prompt": "从热点中筛选最有价值的选题", "timeout": 15 },
  37 + { "name": "内容创作", "agent": "writer", "prompt": "根据选题撰写高质量文章", "timeout": 60 },
  38 + { "name": "COO审核", "agent": "coo", "prompt": "审核文章质量,评分并决定通过或驳回", "timeout": 20 },
  39 + { "name": "发布上线", "agent": "publisher", "prompt": "将通过审核的文章发布到目标平台", "timeout": 15 }
  40 + ]
  41 +}
  42 +```
  43 +**Pipeline 设计要点**:
  44 +- 必须设置 `"loop": true` 实现每日循环执行
  45 +- 必须确保关键质量节点有 COO 审核步骤
  46 +- 每一步必须有明确的 `agent`(对应 CTO 将要招聘的角色)
  47 +- timeout 单位是分钟
  48 +
  49 +### Step 3: 当场招人(不要等!紧接着执行!)
  50 +战略和 Pipeline 设计完后,**在同一轮对话中立刻执行**:
24 ```bash 51 ```bash
25 node bin/lemon.js company hire --id <你的公司ID> --role cto --type cxo --name "技术与班底组建总监" 52 node bin/lemon.js company hire --id <你的公司ID> --role cto --type cxo --name "技术与班底组建总监"
26 node bin/lemon.js company hire --id <你的公司ID> --role coo --type cxo --name "首席运营与品控官" 53 node bin/lemon.js company hire --id <你的公司ID> --role coo --type cxo --name "首席运营与品控官"
27 ``` 54 ```
28 55
29 -### Step 3: 当场委派(不要等!紧接着执行!) 56 +### Step 4: 设定 OKR(不要等!紧接着执行!)
  57 +```bash
  58 +node bin/lemon.js goal create --title "Q1 业务目标" --desc "..." --owner <你的公司ID>:ceo
  59 +```
  60 +
  61 +### Step 5: 当场委派(不要等!紧接着执行!)
30 招完人后,**在同一轮对话中立刻执行**: 62 招完人后,**在同一轮对话中立刻执行**:
31 ```bash 63 ```bash
32 -node bin/lemon.js task dispatch --from <你自己,如 wechatai:ceo> --to <公司ID>:cto --task "我已经写好商业蓝图在 workspace 下,请立刻设计技术架构、搭建底层 Agent 流水线并规定数据流向,完成后移交给 COO 运营。" --priority P0 64 +node bin/lemon.js task dispatch --from <你自己,如 wechatai:ceo> --to <公司ID>:cto --task "我已经写好商业蓝图和 Pipeline 设计在 workspace/ 下。请立刻:1) 按照 pipeline_design.json 的角色需求招聘执行层 Agent;2) 将 pipeline_design.json 注册为正式 Pipeline;3) 搭建数据流向;4) 完成后移交给 COO 运营。" --priority P0
33 ``` 65 ```
34 66
35 -**只有当上面三步全部做完,你才可以结束本轮对话。** 67 +**只有当上面五步全部做完,你才可以结束本轮对话。**
36 68
37 ## 🔄 心跳自检规则(每 5 分钟系统会唤醒你) 69 ## 🔄 心跳自检规则(每 5 分钟系统会唤醒你)
38 当你被心跳唤醒时,执行以下检查: 70 当你被心跳唤醒时,执行以下检查:
@@ -44,17 +76,18 @@ node bin/lemon.js task dispatch --from &lt;你自己,如 wechatai:ceo&gt; --to &lt;公司 @@ -44,17 +76,18 @@ node bin/lemon.js task dispatch --from &lt;你自己,如 wechatai:ceo&gt; --to &lt;公司
44 76
45 ## ⚠️ 绝对不要(Never Rules) 77 ## ⚠️ 绝对不要(Never Rules)
46 - 🚫 **不要写完战略就停下来等 Master 催你进行下一步** — 这是致命错误! 78 - 🚫 **不要写完战略就停下来等 Master 催你进行下一步** — 这是致命错误!
47 -- 🚫 **不要只汇报"我做完了"然后休眠** — 你必须在同一轮对话里连贯完成三步! 79 +- 🚫 **不要只汇报"我做完了"然后休眠** — 你必须在同一轮对话里连贯完成五步!
48 - 🚫 **不要亲自搭建底层员工**(如"画师"、"文案")— 全部交给 CTO ! 80 - 🚫 **不要亲自搭建底层员工**(如"画师"、"文案")— 全部交给 CTO !
49 - 🚫 **不要使用旧的 curl API** — 强制使用 `bin/lemon.js` 命令。 81 - 🚫 **不要使用旧的 curl API** — 强制使用 `bin/lemon.js` 命令。
  82 +- 🚫 **不要忘记设计 Pipeline JSON** — 这是 CTO 搭建流水线的蓝图!
50 83
51 ## 强制思维隔离 (`<thought>`) 84 ## 强制思维隔离 (`<thought>`)
52 在执行任何工具命令前,自省分析: 85 在执行任何工具命令前,自省分析:
53 ```xml 86 ```xml
54 <thought> 87 <thought>
55 -1. 我的公司叫什么?我目前在第几步(战略/招人/委派)?  
56 -2. 我是不是准备"结束对话"了?如果三步还没全做完,绝对不能停!  
57 -3. 我的工具命令参数正确吗?公司前缀加对了吗? 88 +1. 我的公司叫什么?我目前在第几步(战略/Pipeline设计/招人/OKR/委派)?
  89 +2. 我是不是准备"结束对话"了?如果五步还没全做完,绝对不能停!
  90 +3. Pipeline 设计中是否包含了所有必要的角色和质量审核环节?
  91 +4. 我的工具命令参数正确吗?公司前缀加对了吗?
58 </thought> 92 </thought>
59 ``` 93 ```
60 -  
server/routes/company.js
@@ -7,8 +7,10 @@ @@ -7,8 +7,10 @@
7 const express = require('express'); 7 const express = require('express');
8 const router = express.Router(); 8 const router = express.Router();
9 const { CompanyService } = require('../services/company-service'); 9 const { CompanyService } = require('../services/company-service');
  10 +const { ScheduleService } = require('../services/schedule-service');
10 11
11 const companyService = new CompanyService(); 12 const companyService = new CompanyService();
  13 +const scheduleService = new ScheduleService();
12 14
13 // ═══════════════════════════════════════ 15 // ═══════════════════════════════════════
14 // Company CRUD 16 // Company CRUD
@@ -305,4 +307,36 @@ router.post(&#39;/:companyId/pipelines/runs/:runId/reject&#39;, (req, res) =&gt; { @@ -305,4 +307,36 @@ router.post(&#39;/:companyId/pipelines/runs/:runId/reject&#39;, (req, res) =&gt; {
305 } 307 }
306 }); 308 });
307 309
  310 +// ═══════════════════════════════════════
  311 +// Schedule (日程管理)
  312 +// ═══════════════════════════════════════
  313 +
  314 +// GET /api/company/:companyId/schedule — 获取日程
  315 +router.get('/:companyId/schedule', (req, res) => {
  316 + res.json(scheduleService.getSchedule(req.params.companyId));
  317 +});
  318 +
  319 +// POST /api/company/:companyId/schedule/slots — 添加日程
  320 +router.post('/:companyId/schedule/slots', (req, res) => {
  321 + try {
  322 + const slot = scheduleService.addSlot(req.params.companyId, req.body);
  323 + res.json(slot);
  324 + } catch (err) {
  325 + res.status(400).json({ error: err.message });
  326 + }
  327 +});
  328 +
  329 +// PUT /api/company/:companyId/schedule/slots/:slotId — 更新日程
  330 +router.put('/:companyId/schedule/slots/:slotId', (req, res) => {
  331 + const slot = scheduleService.updateSlot(req.params.companyId, req.params.slotId, req.body);
  332 + if (!slot) return res.status(404).json({ error: '日程不存在' });
  333 + res.json(slot);
  334 +});
  335 +
  336 +// DELETE /api/company/:companyId/schedule/slots/:slotId — 删除日程
  337 +router.delete('/:companyId/schedule/slots/:slotId', (req, res) => {
  338 + scheduleService.removeSlot(req.params.companyId, req.params.slotId);
  339 + res.json({ success: true });
  340 +});
  341 +
308 module.exports = router; 342 module.exports = router;
server/services/company-heartbeat-service.js
@@ -16,6 +16,7 @@ const path = require(&#39;path&#39;); @@ -16,6 +16,7 @@ const path = require(&#39;path&#39;);
16 const http = require('http'); 16 const http = require('http');
17 const { CompanyService } = require('./company-service'); 17 const { CompanyService } = require('./company-service');
18 const { PipelineService } = require('./pipeline-service'); 18 const { PipelineService } = require('./pipeline-service');
  19 +const { ScheduleService } = require('./schedule-service');
19 20
20 const DATA_DIR = path.resolve(__dirname, '../../data'); 21 const DATA_DIR = path.resolve(__dirname, '../../data');
21 22
@@ -24,6 +25,7 @@ class CompanyHeartbeatService { @@ -24,6 +25,7 @@ class CompanyHeartbeatService {
24 constructor() { 25 constructor() {
25 this.timers = new Map(); // companyId → NodeJS.Timeout 26 this.timers = new Map(); // companyId → NodeJS.Timeout
26 this.companyService = new CompanyService(); 27 this.companyService = new CompanyService();
  28 + this.scheduleService = new ScheduleService();
27 this.port = 3000; 29 this.port = 3000;
28 this._started = false; 30 this._started = false;
29 } 31 }
@@ -126,12 +128,20 @@ class CompanyHeartbeatService { @@ -126,12 +128,20 @@ class CompanyHeartbeatService {
126 // 2. 驱动运行中的 Pipeline Run 128 // 2. 驱动运行中的 Pipeline Run
127 await this._driveActiveRuns(companyId); 129 await this._driveActiveRuns(companyId);
128 130
129 - // 3. 触发 CEO 心跳 (如果有 CEO 在公司内) 131 + // 3. 执行日程驱动的任务 (Schedule)
  132 + await this._executeScheduledTasks(companyId);
  133 +
  134 + // 4. 触发 CEO 心跳 (如果有 CEO 在公司内)
130 if (company.ceo) { 135 if (company.ceo) {
131 this._triggerCeoHeartbeat(companyId, company.ceo); 136 this._triggerCeoHeartbeat(companyId, company.ceo);
132 } 137 }
  138 +
  139 + // 5. 检查是否需要生成 Master 晨会日报 (每天 08:00)
  140 + this._checkDailyReport(companyId);
133 } catch (err) { 141 } catch (err) {
134 console.error(`[CompanyHB] ❌ ${companyId} 心跳执行失败:`, err.message); 142 console.error(`[CompanyHB] ❌ ${companyId} 心跳执行失败:`, err.message);
  143 + // 告警通道: 心跳本身异常也通知 Master
  144 + this._sendAlertToMaster(companyId, `公司心跳执行异常: ${err.message}`);
135 } 145 }
136 } 146 }
137 147
@@ -208,6 +218,10 @@ class CompanyHeartbeatService { @@ -208,6 +218,10 @@ class CompanyHeartbeatService {
208 status: 'skipped', 218 status: 'skipped',
209 output: { reason: '超时跳过' }, 219 output: { reason: '超时跳过' },
210 }); 220 });
  221 + // 🚨 告警通道: 超时通知 Master
  222 + this._sendAlertToMaster(companyId,
  223 + `Pipeline "${run.pipelineName}" 步骤 ${task.stepIndex + 1} (${task.stepName}) 执行超时 (${Math.round(elapsed/60000)}分钟),已自动跳过。Agent: ${task.agent}`
  224 + );
211 } 225 }
212 } 226 }
213 continue; 227 continue;
@@ -222,17 +236,21 @@ class CompanyHeartbeatService { @@ -222,17 +236,21 @@ class CompanyHeartbeatService {
222 const stepDef = pipeline?.steps?.[task.stepIndex]; 236 const stepDef = pipeline?.steps?.[task.stepIndex];
223 const inputStr = task.input ? `\n上一步的输出(作为你的输入):\n${JSON.stringify(task.input, null, 2)}` : ''; 237 const inputStr = task.input ? `\n上一步的输出(作为你的输入):\n${JSON.stringify(task.input, null, 2)}` : '';
224 238
  239 + // 检查是否有驳回信息
  240 + const rejectionInfo = task.input?.__rejection
  241 + ? `\n⚠️ ${task.input.__rejection.instruction}\n这是第 ${task.input.__rejection.attempt} 次重做。\n`
  242 + : '';
  243 +
225 const taskMessage = [ 244 const taskMessage = [
226 `[PIPELINE TASK]`, 245 `[PIPELINE TASK]`,
227 `流水线: ${run.pipelineName}`, 246 `流水线: ${run.pipelineName}`,
228 `步骤 ${task.stepIndex + 1}/${run.steps.length}: ${task.stepName}`, 247 `步骤 ${task.stepIndex + 1}/${run.steps.length}: ${task.stepName}`,
229 `指令: ${stepDef?.prompt || '请执行你的职责'}`, 248 `指令: ${stepDef?.prompt || '请执行你的职责'}`,
  249 + rejectionInfo,
230 inputStr, 250 inputStr,
231 '', 251 '',
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"}}'`, 252 + `完成后请调用以下命令汇报结果:`,
  253 + `node bin/lemon.js task report --taskId pipeline-${runMeta.runId}-step-${task.stepIndex} --from ${companyId}:${task.agent} --to ${companyId}:coo --status done --summary "(你的完成摘要)"`,
236 ].join('\n'); 254 ].join('\n');
237 255
238 // 通过 HTTP 触发 Agent Chat (异步,不阻塞心跳) 256 // 通过 HTTP 触发 Agent Chat (异步,不阻塞心跳)
@@ -261,10 +279,194 @@ class CompanyHeartbeatService { @@ -261,10 +279,194 @@ class CompanyHeartbeatService {
261 this._sendAgentChat(companyId, ceoId, msg, `heartbeat-${Date.now().toString(36)}`); 279 this._sendAgentChat(companyId, ceoId, msg, `heartbeat-${Date.now().toString(36)}`);
262 } 280 }
263 281
  282 + // ═══════════════════════════════════════
  283 + // 日程驱动执行 (Schedule)
  284 + // ═══════════════════════════════════════
  285 +
  286 + /**
  287 + * 执行当前时间到期的日程任务
  288 + */
  289 + async _executeScheduledTasks(companyId) {
  290 + const dueSlots = this.scheduleService.getDueSlots(companyId);
  291 + if (dueSlots.length === 0) return;
  292 +
  293 + for (const slot of dueSlots) {
  294 + const agentId = `${companyId}:${slot.agent}`;
  295 + const typeLabel = slot.type === 'meeting' ? '📋 会议' : slot.type === 'review' ? '🔍 复盘' : '📌 任务';
  296 + const msg = [
  297 + `[SCHEDULED TASK]`,
  298 + `${typeLabel}: ${slot.task}`,
  299 + `日程时间: ${slot.time}`,
  300 + ``,
  301 + `这是你的日程中预定的任务,请立即执行。`,
  302 + ].join('\n');
  303 +
  304 + console.log(`[Schedule] 📅 ${companyId} 触发日程: ${slot.time} → ${slot.agent} (${slot.task})`);
  305 + this._sendAgentChat(companyId, agentId, msg, `schedule-${slot.id}-${Date.now().toString(36)}`);
  306 + }
  307 + }
  308 +
  309 + // ═══════════════════════════════════════
  310 + // 告警通道 (Alert Channel)
  311 + // ═══════════════════════════════════════
  312 +
  313 + /**
  314 + * 向 Master Agent 发送告警消息
  315 + */
  316 + _sendAlertToMaster(companyId, alertMessage) {
  317 + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
  318 + const msg = [
  319 + `[🚨 COMPANY ALERT]`,
  320 + `公司: ${companyId}`,
  321 + `时间: ${timestamp}`,
  322 + ``,
  323 + `告警内容:`,
  324 + alertMessage,
  325 + ``,
  326 + `请评估是否需要介入处理。`,
  327 + ].join('\n');
  328 +
  329 + // 发给 Master Agent
  330 + this._sendAgentChat(null, 'master', msg, `alert-${companyId}-${Date.now().toString(36)}`);
  331 + console.log(`[Alert] 🚨 已向 Master 发送告警: ${companyId} - ${alertMessage.substring(0, 80)}`);
  332 + }
  333 +
  334 + // ═══════════════════════════════════════
  335 + // Master 晨会日报 (Daily Report)
  336 + // ═══════════════════════════════════════
  337 +
  338 + /**
  339 + * 检查是否需要生成每日晨会报告 (08:00 触发)
  340 + */
  341 + _checkDailyReport(companyId) {
  342 + const now = new Date();
  343 + const tz = 'Asia/Shanghai';
  344 + const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz }));
  345 + const hour = localTime.getHours();
  346 + const minute = localTime.getMinutes();
  347 +
  348 + // 仅在 08:00~08:14 范围内触发
  349 + if (hour !== 8 || minute >= 15) return;
  350 +
  351 + // 防重复: 检查今天是否已生成
  352 + const today = localTime.toISOString().slice(0, 10);
  353 + const flagPath = path.join(DATA_DIR, companyId, '.daily-report-flag');
  354 + try {
  355 + const lastDate = fs.readFileSync(flagPath, 'utf-8').trim();
  356 + if (lastDate === today) return;
  357 + } catch { /* first time */ }
  358 +
  359 + // 生成日报
  360 + const report = this._generateDailyReport(companyId);
  361 + if (!report) return;
  362 +
  363 + // 写入标记
  364 + fs.writeFileSync(flagPath, today, 'utf-8');
  365 +
  366 + // 发给 Master
  367 + const msg = [
  368 + `[📊 DAILY COMPANY REPORT]`,
  369 + `公司: ${companyId}`,
  370 + `日期: ${today}`,
  371 + ``,
  372 + report,
  373 + ``,
  374 + `请查看以上日报。如需调整策略或设定新目标,请下发指令给 CEO。`,
  375 + ].join('\n');
  376 +
  377 + this._sendAgentChat(null, 'master', msg, `daily-report-${companyId}-${today}`);
  378 + console.log(`[DailyReport] 📊 ${companyId} 晨会日报已发送给 Master`);
  379 + }
  380 +
  381 + /**
  382 + * 生成公司日报内容
  383 + */
  384 + _generateDailyReport(companyId) {
  385 + try {
  386 + const company = this.companyService.get(companyId);
  387 + if (!company) return null;
  388 +
  389 + const lines = [`# ${company.name} 运营日报`, ''];
  390 +
  391 + // 1. 公司状态
  392 + lines.push(`## 公司状态`);
  393 + lines.push(`- 状态: ${company.status}`);
  394 + lines.push(`- CEO: ${company.ceo || '(未任命)'}`);
  395 + lines.push(`- Agent 数量: ${company.agents?.length || 0}`);
  396 + lines.push('');
  397 +
  398 + // 2. Pipeline 运行状况
  399 + const pSvc = new PipelineService(companyId);
  400 + const pipelines = pSvc.listPipelines();
  401 + const runs = pSvc.listRuns();
  402 + const activeRuns = runs.filter(r => r.status === 'running');
  403 + const completedToday = runs.filter(r => {
  404 + const created = new Date(r.createdAt);
  405 + const today = new Date();
  406 + return r.status === 'completed' && created.toDateString() === today.toDateString();
  407 + });
  408 +
  409 + lines.push(`## Pipeline 状况`);
  410 + lines.push(`- 已定义流水线: ${pipelines.length} 条`);
  411 + lines.push(`- 运行中: ${activeRuns.length} 个`);
  412 + lines.push(`- 今日完成: ${completedToday.length} 个`);
  413 + if (activeRuns.length > 0) {
  414 + lines.push(`- 活跃详情:`);
  415 + for (const r of activeRuns) {
  416 + lines.push(` - ${r.pipelineName}: 步骤 ${r.currentStep + 1}/${r.totalSteps}`);
  417 + }
  418 + }
  419 + lines.push('');
  420 +
  421 + // 3. OKR 进度
  422 + try {
  423 + const { GoalsService } = require('./goals');
  424 + const goals = new GoalsService(companyId);
  425 + const goalList = goals.list();
  426 + if (goalList.length > 0) {
  427 + lines.push(`## OKR 进度`);
  428 + for (const g of goalList) {
  429 + const pct = Math.round((g.progress || 0) * 100);
  430 + lines.push(`- ${g.id} ${g.title}: ${pct}% (${g.doneCount || 0}/${g.totalCount || 0})`);
  431 + }
  432 + lines.push('');
  433 + }
  434 + } catch { /* no goals */ }
  435 +
  436 + // 4. Agent 活跃度
  437 + const agentsDir = path.join(DATA_DIR, companyId, 'agents');
  438 + if (fs.existsSync(agentsDir)) {
  439 + lines.push(`## Agent 活跃度`);
  440 + const agents = fs.readdirSync(agentsDir, { withFileTypes: true })
  441 + .filter(d => d.isDirectory());
  442 + for (const a of agents) {
  443 + const convDir = path.join(agentsDir, a.name, 'conversations');
  444 + let lastActive = '(无记录)';
  445 + if (fs.existsSync(convDir)) {
  446 + const convs = fs.readdirSync(convDir).sort().reverse();
  447 + if (convs.length > 0) {
  448 + const metaPath = path.join(convDir, convs[0], 'meta.json');
  449 + try {
  450 + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
  451 + lastActive = meta.updatedAt?.substring(0, 16) || '(未知)';
  452 + } catch {}
  453 + }
  454 + }
  455 + lines.push(`- ${a.name}: 最近活跃 ${lastActive}`);
  456 + }
  457 + lines.push('');
  458 + }
  459 +
  460 + return lines.join('\n');
  461 + } catch (err) {
  462 + console.error(`[DailyReport] ❌ 生成日报失败:`, err.message);
  463 + return null;
  464 + }
  465 + }
  466 +
264 /** 467 /**
265 * 通过 HTTP 向 Agent 发送消息 (异步, 不阻塞) 468 * 通过 HTTP 向 Agent 发送消息 (异步, 不阻塞)
266 - * 公司内 Agent 通过 /api/agent/{agentId}/chat 触发  
267 - * TODO: 未来改为公司级路由 /api/company/{companyId}/agent/{agentId}/chat 469 + * companyId 为 null 时发给全局 Agent (如 master)
268 */ 470 */
269 _sendAgentChat(companyId, agentId, message, conversationId) { 471 _sendAgentChat(companyId, agentId, message, conversationId) {
270 const postData = JSON.stringify({ 472 const postData = JSON.stringify({
server/services/schedule-service.js 0 → 100644
  1 +/**
  2 + * ScheduleService — 公司级工作日历系统
  3 + *
  4 + * 数据目录: data/{companyId}/schedule.json
  5 + *
  6 + * 核心概念:
  7 + * Schedule = 公司的每日时间表
  8 + * TimeSlot = 某个Agent在某个时间点执行的固定任务
  9 + *
  10 + * 与 HeartbeatService 集成:
  11 + * 心跳触发时,ScheduleService 检查当前时间是否有需要执行的日程
  12 + * 匹配到的日程自动触发对应 Agent 的 Chat
  13 + */
  14 +
  15 +const fs = require('fs');
  16 +const path = require('path');
  17 +
  18 +const DATA_DIR = path.resolve(__dirname, '../../data');
  19 +
  20 +class ScheduleService {
  21 +
  22 + /**
  23 + * 获取公司的日程配置
  24 + * @param {string} companyId
  25 + * @returns {object} schedule config
  26 + */
  27 + getSchedule(companyId) {
  28 + const fp = path.join(DATA_DIR, companyId, 'schedule.json');
  29 + if (!fs.existsSync(fp)) return { timezone: 'Asia/Shanghai', slots: [] };
  30 + try {
  31 + return JSON.parse(fs.readFileSync(fp, 'utf-8'));
  32 + } catch {
  33 + return { timezone: 'Asia/Shanghai', slots: [] };
  34 + }
  35 + }
  36 +
  37 + /**
  38 + * 保存公司日程配置
  39 + * @param {string} companyId
  40 + * @param {object} schedule
  41 + */
  42 + saveSchedule(companyId, schedule) {
  43 + const fp = path.join(DATA_DIR, companyId, 'schedule.json');
  44 + fs.writeFileSync(fp, JSON.stringify(schedule, null, 2), 'utf-8');
  45 + }
  46 +
  47 + /**
  48 + * 添加日程时间槽
  49 + * @param {string} companyId
  50 + * @param {object} slot - { time, agent, task, days?, enabled? }
  51 + * time: "09:00" (24小时制)
  52 + * agent: "trend-hunter" (Agent ID)
  53 + * task: "执行每日热点抓取" (任务描述)
  54 + * days: ["mon","tue","wed","thu","fri","sat","sun"] (可选,默认每天)
  55 + * enabled: true (可选)
  56 + */
  57 + addSlot(companyId, slot) {
  58 + const schedule = this.getSchedule(companyId);
  59 + const newSlot = {
  60 + id: `S-${Date.now().toString(36)}`,
  61 + time: slot.time,
  62 + agent: slot.agent,
  63 + task: slot.task,
  64 + days: slot.days || ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
  65 + enabled: slot.enabled !== false,
  66 + type: slot.type || 'task', // task | meeting | review
  67 + createdAt: new Date().toISOString(),
  68 + };
  69 + schedule.slots.push(newSlot);
  70 + this.saveSchedule(companyId, schedule);
  71 + console.log(`[Schedule] 📅 ${companyId} 新增日程: ${newSlot.time} ${newSlot.agent} - ${newSlot.task}`);
  72 + return newSlot;
  73 + }
  74 +
  75 + /**
  76 + * 删除日程
  77 + */
  78 + removeSlot(companyId, slotId) {
  79 + const schedule = this.getSchedule(companyId);
  80 + schedule.slots = schedule.slots.filter(s => s.id !== slotId);
  81 + this.saveSchedule(companyId, schedule);
  82 + return true;
  83 + }
  84 +
  85 + /**
  86 + * 更新日程
  87 + */
  88 + updateSlot(companyId, slotId, updates) {
  89 + const schedule = this.getSchedule(companyId);
  90 + const slot = schedule.slots.find(s => s.id === slotId);
  91 + if (!slot) return null;
  92 + if (updates.time !== undefined) slot.time = updates.time;
  93 + if (updates.agent !== undefined) slot.agent = updates.agent;
  94 + if (updates.task !== undefined) slot.task = updates.task;
  95 + if (updates.days !== undefined) slot.days = updates.days;
  96 + if (updates.enabled !== undefined) slot.enabled = updates.enabled;
  97 + this.saveSchedule(companyId, schedule);
  98 + return slot;
  99 + }
  100 +
  101 + /**
  102 + * 获取当前时间应该触发的日程
  103 + *
  104 + * @param {string} companyId
  105 + * @param {number} toleranceMinutes - 容差(分钟),默认 15 分钟
  106 + * @returns {Array} 到期的 slots
  107 + */
  108 + getDueSlots(companyId, toleranceMinutes = 15) {
  109 + const schedule = this.getSchedule(companyId);
  110 + if (!schedule.slots || schedule.slots.length === 0) return [];
  111 +
  112 + const now = new Date();
  113 + // 转换到公司时区
  114 + const tz = schedule.timezone || 'Asia/Shanghai';
  115 + const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz }));
  116 + const currentHour = localTime.getHours();
  117 + const currentMinute = localTime.getMinutes();
  118 + const dayNames = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
  119 + const currentDay = dayNames[localTime.getDay()];
  120 +
  121 + // 记录已执行的日程(防重复触发)
  122 + const executedPath = path.join(DATA_DIR, companyId, '.schedule-executed.json');
  123 + let executed = {};
  124 + try { executed = JSON.parse(fs.readFileSync(executedPath, 'utf-8')); } catch {}
  125 +
  126 + const today = localTime.toISOString().slice(0, 10);
  127 + // 清理前一天的记录
  128 + for (const key of Object.keys(executed)) {
  129 + if (!key.startsWith(today)) delete executed[key];
  130 + }
  131 +
  132 + const dueSlots = [];
  133 +
  134 + for (const slot of schedule.slots) {
  135 + if (!slot.enabled) continue;
  136 + if (!slot.days.includes(currentDay)) continue;
  137 +
  138 + // 解析 slot 时间
  139 + const [slotHour, slotMinute] = slot.time.split(':').map(Number);
  140 + const diffMinutes = (currentHour * 60 + currentMinute) - (slotHour * 60 + slotMinute);
  141 +
  142 + // 在容差范围内且未执行过
  143 + if (diffMinutes >= 0 && diffMinutes < toleranceMinutes) {
  144 + const execKey = `${today}:${slot.id}`;
  145 + if (!executed[execKey]) {
  146 + dueSlots.push(slot);
  147 + executed[execKey] = new Date().toISOString();
  148 + }
  149 + }
  150 + }
  151 +
  152 + // 保存执行记录
  153 + if (dueSlots.length > 0) {
  154 + fs.writeFileSync(executedPath, JSON.stringify(executed, null, 2), 'utf-8');
  155 + }
  156 +
  157 + return dueSlots;
  158 + }
  159 +
  160 + /**
  161 + * 生成人类可读的日程表文本 (用于注入 Agent prompt)
  162 + */
  163 + formatScheduleForAgent(companyId, agentId) {
  164 + const schedule = this.getSchedule(companyId);
  165 + const mySlots = schedule.slots.filter(s => s.agent === agentId && s.enabled);
  166 +
  167 + if (mySlots.length === 0) return '';
  168 +
  169 + const lines = ['## 📅 我的每日日程', ''];
  170 + const sorted = [...mySlots].sort((a, b) => a.time.localeCompare(b.time));
  171 +
  172 + for (const slot of sorted) {
  173 + const daysStr = slot.days.length === 7 ? '每天' : slot.days.join('/');
  174 + const typeEmoji = slot.type === 'meeting' ? '📋' : slot.type === 'review' ? '🔍' : '📌';
  175 + lines.push(`- **${slot.time}** ${typeEmoji} ${slot.task} (${daysStr})`);
  176 + }
  177 +
  178 + return lines.join('\n');
  179 + }
  180 +}
  181 +
  182 +module.exports = { ScheduleService };