diff --git a/bin/lemon.js b/bin/lemon.js index 2d7024f..13a4e28 100755 --- a/bin/lemon.js +++ b/bin/lemon.js @@ -148,6 +148,25 @@ async function run() { }); console.log(JSON.stringify(res.data, null, 2)); } + else if (command === 'schedule add') { + const { values } = parseArgs({ args: args.slice(2), options: { + companyId: { type: 'string' }, time: { type: 'string' }, + agent: { type: 'string' }, task: { type: 'string' }, + type: { type: 'string', default: 'task' } + }}); + const res = await request('POST', `/company/${values.companyId}/schedule/slots`, { + time: values.time, agent: values.agent, + task: values.task, type: values.type + }); + console.log(JSON.stringify(res.data, null, 2)); + } + else if (command === 'schedule list') { + const { values } = parseArgs({ args: args.slice(2), options: { + companyId: { type: 'string' } + }}); + const res = await request('GET', `/company/${values.companyId}/schedule`); + console.log(JSON.stringify(res.data, null, 2)); + } else { console.error('Unknown command:', command); process.exit(1); diff --git a/data/templates/ceo/agent.md b/data/templates/ceo/agent.md index 4dca98f..4409335 100644 --- a/data/templates/ceo/agent.md +++ b/data/templates/ceo/agent.md @@ -11,28 +11,60 @@ - `coo`:负责 7x24 小时督促流水线,不合格的产出打回重做。 3. **顶层目标管理 (OKR Owner)**:使用 `lemon goal create` 创建顶层 OKR。 -## ⚡ 强制工作流(同一轮对话内必须连贯完成全部三步!) +## ⚡ 强制工作流(同一轮对话内必须连贯完成全部五步!) -> **🚨 这是你的最高原则:你必须在收到战略任务后的同一轮对话里,一口气完成以下全部三步。 +> **🚨 这是你的最高原则:你必须在收到战略任务后的同一轮对话里,一口气完成以下全部五步。 > 绝对禁止在做完第一步后就结束对话、汇报"我做完了"然后等 Master 来推进第二步!** ### Step 1: 战略规划(撰写商业蓝图) 使用 Write 工具撰写 `workspace/<公司名>_strategy.md`,把商业模式讲透。 -### Step 2: 当场招人(不要等!紧接着执行!) -战略写完后,**在同一轮对话中立刻执行**: +### Step 2: 设计 SOP 流水线(撰写 Pipeline JSON) +基于战略蓝图,设计完整的业务流水线并写入 JSON 文件。例子: +```bash +# 将以下 JSON 写入 workspace/pipeline_design.json +``` +JSON 格式要求: +```json +{ + "name": "每日内容生产", + "description": "从热点抓取到内容发布的完整流水线", + "loop": true, + "cron": { "intervalHours": 24 }, + "steps": [ + { "name": "热点抓取", "agent": "trend-hunter", "prompt": "抓取今日热点话题", "timeout": 30 }, + { "name": "选题决策", "agent": "topic-selector", "prompt": "从热点中筛选最有价值的选题", "timeout": 15 }, + { "name": "内容创作", "agent": "writer", "prompt": "根据选题撰写高质量文章", "timeout": 60 }, + { "name": "COO审核", "agent": "coo", "prompt": "审核文章质量,评分并决定通过或驳回", "timeout": 20 }, + { "name": "发布上线", "agent": "publisher", "prompt": "将通过审核的文章发布到目标平台", "timeout": 15 } + ] +} +``` +**Pipeline 设计要点**: +- 必须设置 `"loop": true` 实现每日循环执行 +- 必须确保关键质量节点有 COO 审核步骤 +- 每一步必须有明确的 `agent`(对应 CTO 将要招聘的角色) +- timeout 单位是分钟 + +### Step 3: 当场招人(不要等!紧接着执行!) +战略和 Pipeline 设计完后,**在同一轮对话中立刻执行**: ```bash node bin/lemon.js company hire --id <你的公司ID> --role cto --type cxo --name "技术与班底组建总监" node bin/lemon.js company hire --id <你的公司ID> --role coo --type cxo --name "首席运营与品控官" ``` -### Step 3: 当场委派(不要等!紧接着执行!) +### Step 4: 设定 OKR(不要等!紧接着执行!) +```bash +node bin/lemon.js goal create --title "Q1 业务目标" --desc "..." --owner <你的公司ID>:ceo +``` + +### Step 5: 当场委派(不要等!紧接着执行!) 招完人后,**在同一轮对话中立刻执行**: ```bash -node bin/lemon.js task dispatch --from <你自己,如 wechatai:ceo> --to <公司ID>:cto --task "我已经写好商业蓝图在 workspace 下,请立刻设计技术架构、搭建底层 Agent 流水线并规定数据流向,完成后移交给 COO 运营。" --priority P0 +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 ``` -**只有当上面三步全部做完,你才可以结束本轮对话。** +**只有当上面五步全部做完,你才可以结束本轮对话。** ## 🔄 心跳自检规则(每 5 分钟系统会唤醒你) 当你被心跳唤醒时,执行以下检查: @@ -44,17 +76,18 @@ node bin/lemon.js task dispatch --from <你自己,如 wechatai:ceo> --to <公司 ## ⚠️ 绝对不要(Never Rules) - 🚫 **不要写完战略就停下来等 Master 催你进行下一步** — 这是致命错误! -- 🚫 **不要只汇报"我做完了"然后休眠** — 你必须在同一轮对话里连贯完成三步! +- 🚫 **不要只汇报"我做完了"然后休眠** — 你必须在同一轮对话里连贯完成五步! - 🚫 **不要亲自搭建底层员工**(如"画师"、"文案")— 全部交给 CTO ! - 🚫 **不要使用旧的 curl API** — 强制使用 `bin/lemon.js` 命令。 +- 🚫 **不要忘记设计 Pipeline JSON** — 这是 CTO 搭建流水线的蓝图! ## 强制思维隔离 (``) 在执行任何工具命令前,自省分析: ```xml -1. 我的公司叫什么?我目前在第几步(战略/招人/委派)? -2. 我是不是准备"结束对话"了?如果三步还没全做完,绝对不能停! -3. 我的工具命令参数正确吗?公司前缀加对了吗? +1. 我的公司叫什么?我目前在第几步(战略/Pipeline设计/招人/OKR/委派)? +2. 我是不是准备"结束对话"了?如果五步还没全做完,绝对不能停! +3. Pipeline 设计中是否包含了所有必要的角色和质量审核环节? +4. 我的工具命令参数正确吗?公司前缀加对了吗? ``` - diff --git a/server/routes/company.js b/server/routes/company.js index b257b2f..5eb6f95 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -7,8 +7,10 @@ const express = require('express'); const router = express.Router(); const { CompanyService } = require('../services/company-service'); +const { ScheduleService } = require('../services/schedule-service'); const companyService = new CompanyService(); +const scheduleService = new ScheduleService(); // ═══════════════════════════════════════ // Company CRUD @@ -305,4 +307,36 @@ router.post('/:companyId/pipelines/runs/:runId/reject', (req, res) => { } }); +// ═══════════════════════════════════════ +// Schedule (日程管理) +// ═══════════════════════════════════════ + +// GET /api/company/:companyId/schedule — 获取日程 +router.get('/:companyId/schedule', (req, res) => { + res.json(scheduleService.getSchedule(req.params.companyId)); +}); + +// POST /api/company/:companyId/schedule/slots — 添加日程 +router.post('/:companyId/schedule/slots', (req, res) => { + try { + const slot = scheduleService.addSlot(req.params.companyId, req.body); + res.json(slot); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// PUT /api/company/:companyId/schedule/slots/:slotId — 更新日程 +router.put('/:companyId/schedule/slots/:slotId', (req, res) => { + const slot = scheduleService.updateSlot(req.params.companyId, req.params.slotId, req.body); + if (!slot) return res.status(404).json({ error: '日程不存在' }); + res.json(slot); +}); + +// DELETE /api/company/:companyId/schedule/slots/:slotId — 删除日程 +router.delete('/:companyId/schedule/slots/:slotId', (req, res) => { + scheduleService.removeSlot(req.params.companyId, req.params.slotId); + res.json({ success: true }); +}); + module.exports = router; diff --git a/server/services/company-heartbeat-service.js b/server/services/company-heartbeat-service.js index 9f82365..517fbdb 100644 --- a/server/services/company-heartbeat-service.js +++ b/server/services/company-heartbeat-service.js @@ -16,6 +16,7 @@ const path = require('path'); const http = require('http'); const { CompanyService } = require('./company-service'); const { PipelineService } = require('./pipeline-service'); +const { ScheduleService } = require('./schedule-service'); const DATA_DIR = path.resolve(__dirname, '../../data'); @@ -24,6 +25,7 @@ class CompanyHeartbeatService { constructor() { this.timers = new Map(); // companyId → NodeJS.Timeout this.companyService = new CompanyService(); + this.scheduleService = new ScheduleService(); this.port = 3000; this._started = false; } @@ -126,12 +128,20 @@ class CompanyHeartbeatService { // 2. 驱动运行中的 Pipeline Run await this._driveActiveRuns(companyId); - // 3. 触发 CEO 心跳 (如果有 CEO 在公司内) + // 3. 执行日程驱动的任务 (Schedule) + await this._executeScheduledTasks(companyId); + + // 4. 触发 CEO 心跳 (如果有 CEO 在公司内) if (company.ceo) { this._triggerCeoHeartbeat(companyId, company.ceo); } + + // 5. 检查是否需要生成 Master 晨会日报 (每天 08:00) + this._checkDailyReport(companyId); } catch (err) { console.error(`[CompanyHB] ❌ ${companyId} 心跳执行失败:`, err.message); + // 告警通道: 心跳本身异常也通知 Master + this._sendAlertToMaster(companyId, `公司心跳执行异常: ${err.message}`); } } @@ -208,6 +218,10 @@ class CompanyHeartbeatService { status: 'skipped', output: { reason: '超时跳过' }, }); + // 🚨 告警通道: 超时通知 Master + this._sendAlertToMaster(companyId, + `Pipeline "${run.pipelineName}" 步骤 ${task.stepIndex + 1} (${task.stepName}) 执行超时 (${Math.round(elapsed/60000)}分钟),已自动跳过。Agent: ${task.agent}` + ); } } continue; @@ -222,17 +236,21 @@ class CompanyHeartbeatService { const stepDef = pipeline?.steps?.[task.stepIndex]; const inputStr = task.input ? `\n上一步的输出(作为你的输入):\n${JSON.stringify(task.input, null, 2)}` : ''; + // 检查是否有驳回信息 + const rejectionInfo = task.input?.__rejection + ? `\n⚠️ ${task.input.__rejection.instruction}\n这是第 ${task.input.__rejection.attempt} 次重做。\n` + : ''; + const taskMessage = [ `[PIPELINE TASK]`, `流水线: ${run.pipelineName}`, `步骤 ${task.stepIndex + 1}/${run.steps.length}: ${task.stepName}`, `指令: ${stepDef?.prompt || '请执行你的职责'}`, + rejectionInfo, 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"}}'`, + `完成后请调用以下命令汇报结果:`, + `node bin/lemon.js task report --taskId pipeline-${runMeta.runId}-step-${task.stepIndex} --from ${companyId}:${task.agent} --to ${companyId}:coo --status done --summary "(你的完成摘要)"`, ].join('\n'); // 通过 HTTP 触发 Agent Chat (异步,不阻塞心跳) @@ -261,10 +279,194 @@ class CompanyHeartbeatService { this._sendAgentChat(companyId, ceoId, msg, `heartbeat-${Date.now().toString(36)}`); } + // ═══════════════════════════════════════ + // 日程驱动执行 (Schedule) + // ═══════════════════════════════════════ + + /** + * 执行当前时间到期的日程任务 + */ + async _executeScheduledTasks(companyId) { + const dueSlots = this.scheduleService.getDueSlots(companyId); + if (dueSlots.length === 0) return; + + for (const slot of dueSlots) { + const agentId = `${companyId}:${slot.agent}`; + const typeLabel = slot.type === 'meeting' ? '📋 会议' : slot.type === 'review' ? '🔍 复盘' : '📌 任务'; + const msg = [ + `[SCHEDULED TASK]`, + `${typeLabel}: ${slot.task}`, + `日程时间: ${slot.time}`, + ``, + `这是你的日程中预定的任务,请立即执行。`, + ].join('\n'); + + console.log(`[Schedule] 📅 ${companyId} 触发日程: ${slot.time} → ${slot.agent} (${slot.task})`); + this._sendAgentChat(companyId, agentId, msg, `schedule-${slot.id}-${Date.now().toString(36)}`); + } + } + + // ═══════════════════════════════════════ + // 告警通道 (Alert Channel) + // ═══════════════════════════════════════ + + /** + * 向 Master Agent 发送告警消息 + */ + _sendAlertToMaster(companyId, alertMessage) { + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19); + const msg = [ + `[🚨 COMPANY ALERT]`, + `公司: ${companyId}`, + `时间: ${timestamp}`, + ``, + `告警内容:`, + alertMessage, + ``, + `请评估是否需要介入处理。`, + ].join('\n'); + + // 发给 Master Agent + this._sendAgentChat(null, 'master', msg, `alert-${companyId}-${Date.now().toString(36)}`); + console.log(`[Alert] 🚨 已向 Master 发送告警: ${companyId} - ${alertMessage.substring(0, 80)}`); + } + + // ═══════════════════════════════════════ + // Master 晨会日报 (Daily Report) + // ═══════════════════════════════════════ + + /** + * 检查是否需要生成每日晨会报告 (08:00 触发) + */ + _checkDailyReport(companyId) { + const now = new Date(); + const tz = 'Asia/Shanghai'; + const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz })); + const hour = localTime.getHours(); + const minute = localTime.getMinutes(); + + // 仅在 08:00~08:14 范围内触发 + if (hour !== 8 || minute >= 15) return; + + // 防重复: 检查今天是否已生成 + const today = localTime.toISOString().slice(0, 10); + const flagPath = path.join(DATA_DIR, companyId, '.daily-report-flag'); + try { + const lastDate = fs.readFileSync(flagPath, 'utf-8').trim(); + if (lastDate === today) return; + } catch { /* first time */ } + + // 生成日报 + const report = this._generateDailyReport(companyId); + if (!report) return; + + // 写入标记 + fs.writeFileSync(flagPath, today, 'utf-8'); + + // 发给 Master + const msg = [ + `[📊 DAILY COMPANY REPORT]`, + `公司: ${companyId}`, + `日期: ${today}`, + ``, + report, + ``, + `请查看以上日报。如需调整策略或设定新目标,请下发指令给 CEO。`, + ].join('\n'); + + this._sendAgentChat(null, 'master', msg, `daily-report-${companyId}-${today}`); + console.log(`[DailyReport] 📊 ${companyId} 晨会日报已发送给 Master`); + } + + /** + * 生成公司日报内容 + */ + _generateDailyReport(companyId) { + try { + const company = this.companyService.get(companyId); + if (!company) return null; + + const lines = [`# ${company.name} 运营日报`, '']; + + // 1. 公司状态 + lines.push(`## 公司状态`); + lines.push(`- 状态: ${company.status}`); + lines.push(`- CEO: ${company.ceo || '(未任命)'}`); + lines.push(`- Agent 数量: ${company.agents?.length || 0}`); + lines.push(''); + + // 2. Pipeline 运行状况 + const pSvc = new PipelineService(companyId); + const pipelines = pSvc.listPipelines(); + const runs = pSvc.listRuns(); + const activeRuns = runs.filter(r => r.status === 'running'); + const completedToday = runs.filter(r => { + const created = new Date(r.createdAt); + const today = new Date(); + return r.status === 'completed' && created.toDateString() === today.toDateString(); + }); + + lines.push(`## Pipeline 状况`); + lines.push(`- 已定义流水线: ${pipelines.length} 条`); + lines.push(`- 运行中: ${activeRuns.length} 个`); + lines.push(`- 今日完成: ${completedToday.length} 个`); + if (activeRuns.length > 0) { + lines.push(`- 活跃详情:`); + for (const r of activeRuns) { + lines.push(` - ${r.pipelineName}: 步骤 ${r.currentStep + 1}/${r.totalSteps}`); + } + } + lines.push(''); + + // 3. OKR 进度 + try { + const { GoalsService } = require('./goals'); + const goals = new GoalsService(companyId); + const goalList = goals.list(); + if (goalList.length > 0) { + lines.push(`## OKR 进度`); + for (const g of goalList) { + const pct = Math.round((g.progress || 0) * 100); + lines.push(`- ${g.id} ${g.title}: ${pct}% (${g.doneCount || 0}/${g.totalCount || 0})`); + } + lines.push(''); + } + } catch { /* no goals */ } + + // 4. Agent 活跃度 + const agentsDir = path.join(DATA_DIR, companyId, 'agents'); + if (fs.existsSync(agentsDir)) { + lines.push(`## Agent 活跃度`); + const agents = fs.readdirSync(agentsDir, { withFileTypes: true }) + .filter(d => d.isDirectory()); + for (const a of agents) { + const convDir = path.join(agentsDir, a.name, 'conversations'); + let lastActive = '(无记录)'; + if (fs.existsSync(convDir)) { + const convs = fs.readdirSync(convDir).sort().reverse(); + if (convs.length > 0) { + const metaPath = path.join(convDir, convs[0], 'meta.json'); + try { + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); + lastActive = meta.updatedAt?.substring(0, 16) || '(未知)'; + } catch {} + } + } + lines.push(`- ${a.name}: 最近活跃 ${lastActive}`); + } + lines.push(''); + } + + return lines.join('\n'); + } catch (err) { + console.error(`[DailyReport] ❌ 生成日报失败:`, err.message); + return null; + } + } + /** * 通过 HTTP 向 Agent 发送消息 (异步, 不阻塞) - * 公司内 Agent 通过 /api/agent/{agentId}/chat 触发 - * TODO: 未来改为公司级路由 /api/company/{companyId}/agent/{agentId}/chat + * companyId 为 null 时发给全局 Agent (如 master) */ _sendAgentChat(companyId, agentId, message, conversationId) { const postData = JSON.stringify({ diff --git a/server/services/schedule-service.js b/server/services/schedule-service.js new file mode 100644 index 0000000..14f8bd9 --- /dev/null +++ b/server/services/schedule-service.js @@ -0,0 +1,182 @@ +/** + * ScheduleService — 公司级工作日历系统 + * + * 数据目录: data/{companyId}/schedule.json + * + * 核心概念: + * Schedule = 公司的每日时间表 + * TimeSlot = 某个Agent在某个时间点执行的固定任务 + * + * 与 HeartbeatService 集成: + * 心跳触发时,ScheduleService 检查当前时间是否有需要执行的日程 + * 匹配到的日程自动触发对应 Agent 的 Chat + */ + +const fs = require('fs'); +const path = require('path'); + +const DATA_DIR = path.resolve(__dirname, '../../data'); + +class ScheduleService { + + /** + * 获取公司的日程配置 + * @param {string} companyId + * @returns {object} schedule config + */ + getSchedule(companyId) { + const fp = path.join(DATA_DIR, companyId, 'schedule.json'); + if (!fs.existsSync(fp)) return { timezone: 'Asia/Shanghai', slots: [] }; + try { + return JSON.parse(fs.readFileSync(fp, 'utf-8')); + } catch { + return { timezone: 'Asia/Shanghai', slots: [] }; + } + } + + /** + * 保存公司日程配置 + * @param {string} companyId + * @param {object} schedule + */ + saveSchedule(companyId, schedule) { + const fp = path.join(DATA_DIR, companyId, 'schedule.json'); + fs.writeFileSync(fp, JSON.stringify(schedule, null, 2), 'utf-8'); + } + + /** + * 添加日程时间槽 + * @param {string} companyId + * @param {object} slot - { time, agent, task, days?, enabled? } + * time: "09:00" (24小时制) + * agent: "trend-hunter" (Agent ID) + * task: "执行每日热点抓取" (任务描述) + * days: ["mon","tue","wed","thu","fri","sat","sun"] (可选,默认每天) + * enabled: true (可选) + */ + addSlot(companyId, slot) { + const schedule = this.getSchedule(companyId); + const newSlot = { + id: `S-${Date.now().toString(36)}`, + time: slot.time, + agent: slot.agent, + task: slot.task, + days: slot.days || ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + enabled: slot.enabled !== false, + type: slot.type || 'task', // task | meeting | review + createdAt: new Date().toISOString(), + }; + schedule.slots.push(newSlot); + this.saveSchedule(companyId, schedule); + console.log(`[Schedule] 📅 ${companyId} 新增日程: ${newSlot.time} ${newSlot.agent} - ${newSlot.task}`); + return newSlot; + } + + /** + * 删除日程 + */ + removeSlot(companyId, slotId) { + const schedule = this.getSchedule(companyId); + schedule.slots = schedule.slots.filter(s => s.id !== slotId); + this.saveSchedule(companyId, schedule); + return true; + } + + /** + * 更新日程 + */ + updateSlot(companyId, slotId, updates) { + const schedule = this.getSchedule(companyId); + const slot = schedule.slots.find(s => s.id === slotId); + if (!slot) return null; + if (updates.time !== undefined) slot.time = updates.time; + if (updates.agent !== undefined) slot.agent = updates.agent; + if (updates.task !== undefined) slot.task = updates.task; + if (updates.days !== undefined) slot.days = updates.days; + if (updates.enabled !== undefined) slot.enabled = updates.enabled; + this.saveSchedule(companyId, schedule); + return slot; + } + + /** + * 获取当前时间应该触发的日程 + * + * @param {string} companyId + * @param {number} toleranceMinutes - 容差(分钟),默认 15 分钟 + * @returns {Array} 到期的 slots + */ + getDueSlots(companyId, toleranceMinutes = 15) { + const schedule = this.getSchedule(companyId); + if (!schedule.slots || schedule.slots.length === 0) return []; + + const now = new Date(); + // 转换到公司时区 + const tz = schedule.timezone || 'Asia/Shanghai'; + const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz })); + const currentHour = localTime.getHours(); + const currentMinute = localTime.getMinutes(); + const dayNames = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + const currentDay = dayNames[localTime.getDay()]; + + // 记录已执行的日程(防重复触发) + const executedPath = path.join(DATA_DIR, companyId, '.schedule-executed.json'); + let executed = {}; + try { executed = JSON.parse(fs.readFileSync(executedPath, 'utf-8')); } catch {} + + const today = localTime.toISOString().slice(0, 10); + // 清理前一天的记录 + for (const key of Object.keys(executed)) { + if (!key.startsWith(today)) delete executed[key]; + } + + const dueSlots = []; + + for (const slot of schedule.slots) { + if (!slot.enabled) continue; + if (!slot.days.includes(currentDay)) continue; + + // 解析 slot 时间 + const [slotHour, slotMinute] = slot.time.split(':').map(Number); + const diffMinutes = (currentHour * 60 + currentMinute) - (slotHour * 60 + slotMinute); + + // 在容差范围内且未执行过 + if (diffMinutes >= 0 && diffMinutes < toleranceMinutes) { + const execKey = `${today}:${slot.id}`; + if (!executed[execKey]) { + dueSlots.push(slot); + executed[execKey] = new Date().toISOString(); + } + } + } + + // 保存执行记录 + if (dueSlots.length > 0) { + fs.writeFileSync(executedPath, JSON.stringify(executed, null, 2), 'utf-8'); + } + + return dueSlots; + } + + /** + * 生成人类可读的日程表文本 (用于注入 Agent prompt) + */ + formatScheduleForAgent(companyId, agentId) { + const schedule = this.getSchedule(companyId); + const mySlots = schedule.slots.filter(s => s.agent === agentId && s.enabled); + + if (mySlots.length === 0) return ''; + + const lines = ['## 📅 我的每日日程', '']; + const sorted = [...mySlots].sort((a, b) => a.time.localeCompare(b.time)); + + for (const slot of sorted) { + const daysStr = slot.days.length === 7 ? '每天' : slot.days.join('/'); + const typeEmoji = slot.type === 'meeting' ? '📋' : slot.type === 'review' ? '🔍' : '📌'; + lines.push(`- **${slot.time}** ${typeEmoji} ${slot.task} (${daysStr})`); + } + + return lines.join('\n'); + } +} + +module.exports = { ScheduleService };