Commit dafe4586d1f31b3f3e2468f734b56b63bab9d7ed

Authored by yibo
1 parent a154c945

feat: Add heartbeat and cron management to Agent detail panel

- Updated `GET /api/agent/:agentId/info` to include heartbeat and cron configurations.
- Added `PUT /api/agent/:agentId/heartbeat` to update config.json and toggle service heartbeat on the fly.
- Added `PUT /api/agent/:agentId/cron` to update cron.json list.
- Updated AgentChat.vue detail panel UI to include interactive heartbeat toggles and a cron job management list.
data/system/agents/innovator/todo.md
... ... @@ -10,3 +10,4 @@
10 10 | T-mn6x648k | 【重新训练 satire-news-scraper Agent — 硬性标准 90 分】<br><br>上次训练只达到 80 分就结束了,因为旧标准是 75 分及格。现在及格线已提高到 90 分(10 分制 = 9.0)。请重新执行 FDE Phase 3 EVOLVING 训练。<br><br>## 输入资料(已有,无需重新 HUNT/DIGEST)<br>- 知识体系: workspace/digester/satire-news-scraper/knowhow.md<br>- 评分标准: workspace/digester/satire-news-scraper/benchmark.md(pass_score 已更新为 9.0)<br>- 训练集: workspace/digester/satire-news-scraper/train_dataset.json<br>- 上次 Skill: workspace/evolver/satire-news-scraper/skill_satire_scraper.md(v1.2.0, 80 分)<br><br>## 训练要求<br>1. 以上次 skill_satire_scraper.md 为起点,继续迭代优化<br>2. 硬性及格线: 总分 >= 9.0(10 分制)或 >= 90(100 分制),低于此分数不能标记为完成<br>3. 最大迭代次数: 10 轮<br>4. 每轮分析弱维度,针对性修改 Skill 定义<br><br>## 关键:训练完成后必须部署到 Agent<br>训练达标后,必须执行以下部署步骤:<br>1. 将最终的 Skill 内容追加到 agents/satire-news-scraper/agent.md 的末尾(作为新的 ## Skill 章节)<br>2. 更新 agents/satire-news-scraper/config.json 中 status 为 ready<br>3. 更新 agents/satire-news-scraper/plan.md 记录训练完成<br><br>## 产出<br>- workspace/evolver/satire-news-scraper/skill_satire_scraper.md(更新版)<br>- workspace/evolver/satire-news-scraper/evolution_log.json(追加记录)<br>- agents/satire-news-scraper/agent.md(部署 Skill)<br>- workspace/innovator/satire_news_retrain_report.md(重训报告) | ✅ 完成 | master | P0 | 2026-03-26 03:35 | - |
11 11 | T-mn7phcxp | 我已为公司招聘了以下新员工,需要你启动 RL 进化循环进行强化训练:<br><br>1. trend-hunter(热点抓取专家)- 负责抓取微博/抖音/小红书/快手热点<br>2. topic-selector(选题筛选专家)- 负责从热点中筛选最有价值的选题<br>3. researcher(深度研究专家)- 负责收集背景资料和网友观点<br>4. screenwriter(互动剧编剧)- 负责创作分支剧情和多个结局<br><br>请为每个员工优化技能到 90 分以上,特别是 screenwriter 需要掌握分支剧情创作能力。publisher 和 data-analyst 已存在可直接使用。 | 🔄 进行中 | abc:hr | P1 | 2026-03-26 16:47 | 2026-03-27 |
12 12 | T-mn7pws58 | 我已为 abc 公司完成了执行层员工的批量招聘和技能装配。<br><br>招聘结果:<br>1. trend-hunter (热点抓取) - 已存在<br>2. topic-selector (选题决策) - 已存在<br>3. researcher (深度研究) - 已存在<br>4. screenwriter (剧本整理) - 已存在<br>5. publisher (内容上传) - 新招聘,已安装 content_publisher 技能<br>6. data-analyst (数据反馈) - 新招聘,已安装 data_feedback 技能<br><br>请对以下员工启动 RL 进化循环进行强化训练:<br>- publisher: 需要将 content_publisher 技能优化到 90 分以上<br>- data-analyst: 需要将 data_feedback 技能优化到 90 分以上<br>- 其他现有员工: 如有需要可一并优化<br><br>目标是让全员通过 Innovator Benchmark 评分 (≥90分),确保流水线各环节质量达标。 | 🔄 进行中 | abc:hr | P1 | 2026-03-26 16:59 | 2026-03-27 |
  13 +| T-mn96tzls | 我已为 ABC 公司招聘了 5 名新员工: content-writer, publisher, trend-hunter, data-analyst, community-manager。请启动 RL 进化循环,对这些新员工进行训练,优化到 90 分以上! | ⬚ 未开始 | abc:hr | P1 | 2026-03-27 17:41 | - |
... ...
data/system/agents/master/agent.md
... ... @@ -76,33 +76,33 @@ node bin/lemon.js task dispatch --from master --to agent-creator --task &quot;创建ä
76 76  
77 77 ## 正确的æ“ä½œæµæ–¹å¼ï¼ˆèŒƒä¾‹ï¼‰
78 78  
79   -### 🚀 创建公å¸ä¸Žæ‹›è˜å‘˜å·¥ï¼ˆä»Žå»ºå£³åˆ°ç»„建团队)
80   -我们的框架原则是:**å…¬å¸åˆ›å»ºæ—¶åªæ˜¯ä¸€ä¸ªâ€œç©ºå£³â€**。系统中ä¸ç¡¬ç¼–ç è‡ªåŠ¨éƒ¨ç½² CEOã€CTO 等骨干。
81   -为了é¿å…由下级 Agent(如 CEO/HR)去执行招è˜é€ æˆçš„å·¨å¤§èµ„æºæµªè´¹ï¼Œ**所有从 Template 库中挑选并雇佣 Agent 到公å¸å›¢é˜Ÿçš„工作,必须由 Master Agent æ ¹æ®è€æ¿ï¼ˆç”¨æˆ·ï¼‰çš„æŒ‡ä»¤äº²åŠ›äº²ä¸ºå®Œæˆã€‚**
82   -
83   -å½“ç”¨æˆ·è¦æ±‚åˆ›å»ºä¸€å®¶æ–°å…¬å¸æ—¶ï¼Œä½ å¿…须:
84   -1. **构建公å¸ç©ºå£³**:调用 `company create` 创建åªåŒ…嫿¡†æž¶çš„空壳。
85   -2. **ä¸»åŠ¨æ‹›è˜æ ¸å¿ƒå›¢é˜Ÿ**:根æ®ç”¨æˆ·çš„业务需求预判,或者根æ®ç”¨æˆ·çš„æ˜Žç¡®æŒ‡ä»¤ï¼Œä½¿ç”¨ `company hire` 从模æ¿åº“中招募需è¦çš„业务骨干(如 CEO, CTO, COO, HR,乃至具体的业务专家)。
86   -3. **下达战略说明**:通过 `task dispatch` 给新招募的 CEO å‘逿ˆ˜ç•¥å¯åŠ¨ä»¤ï¼Œå‘Šè¯‰ä»–å…¬å¸çš„业务目标,å³å¯å¼€å§‹å·¥ä½œã€‚注æ„:**ä¸è¦**è¦æ±‚ CEO 去招募员工,明确告诉 CEO 团队é…置由 Master/è€æ¿ 负责。
87   -
88   -**例如,当全æƒå§”托你创建并é…ç½®ä¸€ä¸ªå…¬å¸æ—¶ï¼Œä½ å¯ä»¥æ‰§è¡Œï¼š**
89   -```bash
90   -# 1. 一键创建公å¸ç©ºå£³
91   -node bin/lemon.js company create --id <å…¬å¸ID> --name "<å…¬å¸åç§°>" --desc "<业务æè¿°ï¼Œè¶Šè¯¦ç»†è¶Šå¥½>"
92   -
93   -# 2. ä¸ºå…¬å¸æ‰‹åŠ¨æ‹›è˜å›¢é˜Ÿï¼ˆä»Ž Template 库雇佣)
94   -node bin/lemon.js company hire --id <å…¬å¸ID> --role ceo --type cxo --name "首席执行官"
95   -node bin/lemon.js company hire --id <å…¬å¸ID> --role cto --type cxo --name "首席技术官"
96   -# (...按需雇佣其他 Agent)
97   -
98   -# 3. 委派 CEO 开始工作(一定è¦åŠ ä¸Šæ­£ç¡®çš„å…¬å¸å‰ç¼€ï¼‰
99   -node bin/lemon.js task dispatch --from master --to <å…¬å¸ID>:ceo --task "ä½ å·²è¢«ä»»å‘½ä¸ºå…¬å¸ CEO,本公å¸ä¸»è¥ï¼š<业务æè¿°>ã€‚è¯·ç«‹åˆ»åˆ¶å®šå…¬å¸æˆ˜ç•¥å’Œ SOP。注æ„:人员招è˜ç”± Master 集中管ç†ï¼Œä½ åªéœ€ä¸“注于业务æµç¨‹çš„æ¢³ç†ä¸Žå·¥ä½œæµåˆ†è§£ã€‚" --priority P0
  79 +### 🚀 创建公å¸ä¸Žæ‹›è˜å‘˜å·¥ï¼ˆä¸¥æ ¼æŒ‰æŒ‡ä»¤åŠžäº‹ï¼‰
  80 +我们的框架原则是:**å…¬å¸åˆ›å»ºæ—¶åªæ˜¯ä¸€ä¸ªâ€œç©ºå£³â€ï¼Œä¸å¸¦ä»»ä½•员工**。
  81 +所有的招è˜å·¥ä½œï¼ˆå³ä»Ž Template 库中挑选并雇佣 Agent 到公å¸å›¢é˜Ÿï¼‰å…¨éƒ¨ç”±ä½ ï¼ˆMaster Agent)æ¥è´Ÿè´£æ‰§è¡Œï¼Œ**下级 Agent(如 CEO/HR)ç»å¯¹ä¸è¦è¶ŠæƒåŽ»æ‹›è˜ï¼Œä»¥å…造æˆèµ„æºæµªè´¹**。
  82 +
  83 +**关键规则:严格分离“建壳â€ä¸Žâ€œæ‹›è˜â€**
  84 +1. **当用户让你“创建公å¸â€æ—¶**:
  85 + - ä½ **åªèƒ½**执行 `company create` 命令建一个空壳。
  86 + - **ç»å¯¹ä¸è¦**è‡ªä½œä¸»å¼ åŽ»å¸®å…¬å¸æ‹›è˜ CEOã€CTO 或任何员工ï¼å»ºå®Œå£³åŽï¼Œå‘è€æ¿æ±‡æŠ¥åˆ›å»ºæˆåŠŸï¼Œå¹¶é™å¾…è€æ¿çš„ä¸‹ä¸€æ­¥æŒ‡ç¤ºï¼ˆç­‰å¾…è€æ¿æŒ‡å®šå“ªäº› Agent 需è¦è¢«é›‡ä½£ï¼‰ã€‚
  87 +2. **å½“ç”¨æˆ·æ˜Žç¡®è®©ä½ â€œæ‹›è˜æŸäººâ€æˆ–“为æŸå®¶å…¬å¸éƒ¨ç½²å›¢é˜Ÿâ€æ—¶**:
  88 + - è€æ¿ä¸‹è¾¾æŒ‡ä»¤åŽï¼Œä½ ä½¿ç”¨ `company hire` æ ¹æ®ç”¨æˆ·çš„æŒ‡ä»¤å°†ç‰¹å®šçš„ Agent æ¨¡æ¿æ‹›å‹Ÿåˆ°è¿™å®¶å…¬å¸ä¸­ã€‚
  89 + - 如果用户明确å«ä½ å‘æ´¾ä»»åŠ¡ç»™è¯¥å‘˜å·¥ï¼Œä½ å†æ‰§è¡Œ `task dispatch`。
  90 +
  91 +**示例1:åªå»ºå…¬å¸ï¼ˆä¸å†è‡ªåŠ¨æ‹›è˜ï¼‰**
  92 +```shell
  93 +用户指令:"帮我建一家å«ABC的测试公å¸"
  94 +# ä½ åªéœ€è¦æ‰§è¡Œï¼š
  95 +node bin/lemon.js company create --id abc --name "ABCå…¬å¸" --desc "测试公å¸"
  96 +# 汇报:已创建ABCå…¬å¸ï¼Œç›®å‰ä¸ºç©ºå£³ã€‚请问需è¦ä»Žæ¨¡æ¿åº“æ‹›è˜å“ªäº›å‘˜å·¥ï¼Ÿ
100 97 ```
101 98  
102   -**âš ï¸ æ‹›è˜ä¸Žç»„队的原则:**
103   -- 所有的 `hire` æ“作本质上是从 `Template` 库å¤åˆ¶æŒ‡å®šçš„æ¨¡æ¿åˆ°å…·ä½“å…¬å¸ä¸­ï¼Œå¿…须由你(Master)æ¥åŒ…办。
104   -- å¦‚æžœç”¨æˆ·åªæ˜¯è¯´"帮我把 xxx 招到 yyy å…¬å¸",你直接调用一次 `company hire` å³å¯ã€‚
105   -- 如果用户让你全æƒç»„建特定业务线,你应该**自主决定**需è¦å“ªäº›æ¨¡æ¿ï¼Œå¹¶ç›´æŽ¥æ‰¹é‡æ‰§è¡Œ `hire`,ä¸è¦åå¤è¿½é—®ã€‚
  99 +**示例2ï¼šäººå·¥ä¸‹è¾¾æŒ‡å®šæ‹›è˜æŒ‡ä»¤**
  100 +```shell
  101 +用户指令:"ç»™ABC公叿‹›è˜ä¸€ä¸ªç»Ÿå¸…(CEO)"
  102 +# 你执行:
  103 +node bin/lemon.js company hire --id abc --role ceo --type cxo --name "首席执行官"
  104 +# 注æ„ï¼šæ‹›è˜æœ¬è´¨ä¸Šæ˜¯ç”±ä½ ä»Ž Template 模æ¿åº“把对应的能力包å¤åˆ»ç»™è¿™å®¶å…¬å¸ã€‚
  105 +```
106 106 ```
107 107 用户: 让下级干完活
108 108 ✅ 正确回å¤:
... ...
data/system/agents/master/memory/memory.md
... ... @@ -25,3 +25,4 @@
25 25 - [2026-03-27] hi → 不是。我目前使用的是 **MiniMax-M2.1** 模型(见系统环境信息)。
26 26 - [2026-03-27] hi → 抱歉,我无法清空对话日志 — 这属于 Claude Code 客户端层面的操作,超出了我的能力范围。
27 27 - [2026-03-27] 你帮我创建一个叫做 ABC 的公司 → ✅ **ABC Inc. 公司已创建完成!**
  28 +- [2026-03-27] [TASK_CALLBACK] → 收到 ABC Inc. CEO 的任务完成汇报。让我先检查公司整体进度,然后继续推进。
... ...
data/system/agents/master/memory/reflections/reflect-1774633257923.json 0 → 100644
  1 +{
  2 + "timestamp": "2026-03-27T17:40:57.923Z",
  3 + "convId": "callback-T-mn96qkam",
  4 + "task": "[TASK_CALLBACK]\n执行者: abc:ceo\n状态:",
  5 + "status": "completed",
  6 + "toolCount": 6,
  7 + "durationSec": 36,
  8 + "patterns": [
  9 + "完成了 6 步工具调用的复杂任务 (36秒)"
  10 + ],
  11 + "lessons": [],
  12 + "plan": null
  13 +}
0 14 \ No newline at end of file
... ...
data/system/agents/master/memory/short-term/session-1774633257922.md 0 → 100644
  1 +# Session 摘要
  2 +
  3 +- **时间**: 2026-03-27 17:40:57
  4 +- **对话ID**: callback-T-mn96qkam
  5 +- **状态**: completed
  6 +- **工具调用**: 6 次
  7 +- **耗时**: 36秒
  8 +
  9 +## 任务
  10 +[TASK_CALLBACK]
  11 +执行者: abc:ceo
  12 +状态:
  13 +
  14 +## 结果
  15 +收到 ABC Inc. CEO 的任务完成汇报。让我先检查公司整体进度,然后继续推进。
  16 +
  17 +
  18 +根据检查结果,**ABC Inc. 运营状态正常**:
  19 +
  20 +| 角色 | 状态 | 任务进度 |
  21 +|------|------|----------|
  22 +| CEO (abc:ceo) | ✅ 已完成 | T-mn96qkam - 战略制定 |
  23 +| CTO (abc:cto) | ✅ 已完成 | T-mn96sf8m - FDE 流水线架构 |
  24 +| HR (abc:hr) | ⬚ 未开始 | T-mn96sj2z - 招聘计划 |
  25 +| COO (abc:coo) | ⬚ 未开始 | T-mn96sj1c - 运营体系 |
  26 +
  27 +CEO 已完成公司战略制定和任务分发,CTO 任务已完结。HR 和 COO 的任务待执行中。
  28 +
  29 +我将按照定时巡检(每5分钟)继续监控 ABC Inc. 的进度。
... ...
data/system/agents/master/tools.md
... ... @@ -9,7 +9,7 @@
9 9 node bin/lemon.js company create --id <company_id> --name <company_name> --desc <company_description>
10 10 ```
11 11 例如: `node bin/lemon.js company create --id ainews --name "AI News Inc." --desc "专注AI领域的自动化新闻采编与分发公司"`
12   -**❗ 重要:此命令仅创建公司空壳和底层元 Agent,不会自动创建 CEO 等业务员工!你必须在创建后使用 company hire 命令手动招募 CEO。**
  12 +**❗ 重要:此命令仅创建公司空壳。创建完成后立即向老板汇报建壳成功,绝对不要自作主张去招聘任何人!必须等待老板的进一步指令。**
13 13  
14 14 ### 2. hire_agent_for_company — 为公司招聘 Agent
15 15 ```bash
... ... @@ -82,6 +82,7 @@ cat data/lemonv/agents/ceo/todo.md
82 82 - **文件读写编辑器 (Editor)**: 可以直接查看 (`cat`) 和修改/替换项目中任何 Node.js 源码、Vue 代码或 Markdown 定义文件。
83 83  
84 84 ## 工作流程
85   -1. **常规业务指令(创建公司)** → `company create` (建壳) → `company hire --role ceo` (招聘统帅) → `task dispatch --to <ID>:ceo` (下达组织裂变和战略目标指令)
86   -2. **常规业务指令(给现有公司下任务)** → `task dispatch --to <公司ID>:ceo`
87   -3. **老板汇报系统异常/Bug** → 立刻切换为系统工程师:搜索 → 修复源码 → 反馈
  85 +1. **常规业务指令(创建公司)** → `company create` (建壳) → 向老板汇报等待发号施令。
  86 +2. **常规业务指令(人工下达组建指令)** → `company hire` 逐个把模板招聘入岗 → `task dispatch --to <ID>:ceo` (下达组织裂变和战略目标指令)。
  87 +3. **常规业务指令(给现有公司下任务)** → `task dispatch --to <公司ID>:ceo`。
  88 +4. **老板汇报系统异常/Bug** → 立刻切换为系统工程师:搜索 → 修复源码 → 反馈。
... ...
server/routes/agent-chat.js
... ... @@ -743,12 +743,84 @@ router.post(&#39;/:agentId/memory/daily-report&#39;, (req, res) =&gt; {
743 743 router.get('/:agentId/info', (req, res) => {
744 744 const agentDir = getAgentDir(req.params.agentId);
745 745 if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' });
746   - const config = loadAgentConfig(agentDir); const identity = loadIdentity(agentDir);
747   - res.json({ id: config?.id || req.params.agentId, ...identity, type: config?.type, status: config?.status,
748   - bootstrapFiles: ['agent.md','soul.md','identity.md','user.md','tools.md','heartbeat.md'].filter(f => loadFile(agentDir, f).trim().length > 0),
  746 + const config = loadAgentConfig(agentDir);
  747 + const identity = loadIdentity(agentDir);
  748 +
  749 + // Read cron settings
  750 + let cron = [];
  751 + try {
  752 + const cronPath = path.join(agentDir, 'cron.json');
  753 + if (fs.existsSync(cronPath)) {
  754 + cron = JSON.parse(fs.readFileSync(cronPath, 'utf8'));
  755 + }
  756 + } catch (e) {
  757 + console.error('Failed to parse cron.json:', e);
  758 + }
  759 +
  760 + res.json({
  761 + id: config?.id || req.params.agentId,
  762 + ...identity,
  763 + type: config?.type,
  764 + status: config?.status,
  765 + heartbeat: config?.heartbeat || { enabled: false, intervalMinutes: 15 },
  766 + cron: cron,
  767 + bootstrapFiles: ['agent.md','soul.md','identity.md','user.md','tools.md','heartbeat.md', 'cron.json'].filter(f => loadFile(agentDir, f).trim().length > 0),
749 768 });
750 769 });
751 770  
  771 +router.put('/:agentId/heartbeat', (req, res) => {
  772 + try {
  773 + const agentDir = getAgentDir(req.params.agentId);
  774 + if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' });
  775 +
  776 + const configPath = path.join(agentDir, 'config.json');
  777 + let config = {};
  778 + if (fs.existsSync(configPath)) {
  779 + config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
  780 + }
  781 +
  782 + config.heartbeat = {
  783 + enabled: req.body.enabled || false,
  784 + intervalMinutes: req.body.intervalMinutes || 15
  785 + };
  786 +
  787 + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
  788 +
  789 + // Update heartbeat service if running
  790 + const heartbeatService = require('../services/heartbeat-service');
  791 + // Using namespacedId logic from UI (`system:master` is skipped, only `company:role` or pure system agent like `master`)
  792 + const agentId = req.params.agentId;
  793 + if (config.heartbeat.enabled) {
  794 + heartbeatService._setupAgentHeartbeat(agentId);
  795 + } else if (heartbeatService.timers.has(agentId)) {
  796 + clearInterval(heartbeatService.timers.get(agentId));
  797 + heartbeatService.timers.delete(agentId);
  798 + }
  799 +
  800 + res.json({ success: true, heartbeat: config.heartbeat });
  801 + } catch (err) {
  802 + res.status(500).json({ error: err.message });
  803 + }
  804 +});
  805 +
  806 +router.put('/:agentId/cron', (req, res) => {
  807 + try {
  808 + const agentDir = getAgentDir(req.params.agentId);
  809 + if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' });
  810 +
  811 + const cronPath = path.join(agentDir, 'cron.json');
  812 + const cronList = Array.isArray(req.body.cron) ? req.body.cron : [];
  813 +
  814 + fs.writeFileSync(cronPath, JSON.stringify(cronList, null, 2), 'utf8');
  815 +
  816 + // Check if the heartbeat service picks up the cron changes automatically.
  817 + // Generally cron jobs execute within the heartbeat mechanism.
  818 + res.json({ success: true, cron: cronList });
  819 + } catch (err) {
  820 + res.status(500).json({ error: err.message });
  821 + }
  822 +});
  823 +
752 824 /** 列出对话 */
753 825 router.get('/:agentId/conversations', (req, res) => {
754 826 res.json(listConversations(req.params.agentId));
... ...
web/src/views/AgentChat.vue
... ... @@ -194,6 +194,38 @@
194 194 <span v-for="f in (agentInfo.bootstrapFiles||[])" :key="f" class="file-tag" @click="previewFile(f)">{{ f }}</span>
195 195 </div>
196 196 </div>
  197 +
  198 + <div class="detail-section">
  199 + <div class="detail-label">心跳设置 (Heartbeat)</div>
  200 + <div class="detail-value" v-if="agentInfo.heartbeat">
  201 + <label style="display:flex; align-items:center; gap:8px; cursor:pointer; margin-bottom: 6px;">
  202 + <input type="checkbox" v-model="agentInfo.heartbeat.enabled" @change="updateHeartbeat" />
  203 + 开启心跳轮询
  204 + </label>
  205 + <div style="display:flex; align-items:center; gap:8px;">
  206 + 间隔时间:
  207 + <input type="number" v-model.number="agentInfo.heartbeat.intervalMinutes" @change="updateHeartbeat" style="width: 60px; padding: 2px 4px; border-radius: 4px; border: 1px solid var(--border-primary); background: var(--bg-input); color: var(--text-primary);" /> 分钟
  208 + </div>
  209 + </div>
  210 + </div>
  211 +
  212 + <div class="detail-section">
  213 + <div class="detail-label">定时任务 (Cron Jobs) <button @click="addCron" style="float:right; background:none; border:none; color:var(--accent); cursor:pointer; font-weight: 600;">+ 新增</button></div>
  214 + <div class="detail-value" v-if="agentInfo.cron">
  215 + <div v-if="agentInfo.cron.length === 0" style="color:var(--text-faint); font-style:italic; font-size:11px;">无定时任务</div>
  216 + <div v-for="(job, index) in agentInfo.cron" :key="index" style="margin-bottom:8px; padding: 6px; background:var(--bg-overlay); border-radius:4px; border: 1px solid var(--border-primary); font-size:11px;">
  217 + <div style="display:flex; justify-content:space-between; align-items: center; margin-bottom: 4px;">
  218 + <input v-model="job.schedule" placeholder="cron表达式 e.g. 0 8 * * *" style="width:120px; padding:2px 4px; border:1px solid var(--border-primary); border-radius:2px; background:var(--bg-input); color:var(--text-primary);" @change="updateCron" />
  219 + <button @click="removeCron(index)" style="background:none; border:none; color:#ef4444; cursor:pointer; font-size:12px;">✖删除</button>
  220 + </div>
  221 + <input v-model="job.task" placeholder="任务指令" style="width:100%; box-sizing: border-box; padding:2px 4px; border:1px solid var(--border-primary); border-radius:2px; background:var(--bg-input); color:var(--text-primary); margin-bottom: 4px;" @change="updateCron" />
  222 + <label style="display:flex; align-items:center; gap:4px; cursor:pointer; color:var(--text-muted);">
  223 + <input type="checkbox" v-model="job.enabled" @change="updateCron" /> 启用
  224 + </label>
  225 + </div>
  226 + </div>
  227 + </div>
  228 +
197 229 <div class="detail-section">
198 230 <div class="detail-label">统计</div>
199 231 <div class="detail-stats">
... ... @@ -384,6 +416,41 @@ async function loadAgentInfo() {
384 416 } catch (e) { console.error('Agent info error:', e) }
385 417 }
386 418  
  419 +async function updateHeartbeat() {
  420 + if (!agentId.value || !agentInfo.value.heartbeat) return
  421 + try {
  422 + await fetch(`/api/agent/${agentId.value}/heartbeat`, {
  423 + method: 'PUT',
  424 + headers: { 'Content-Type': 'application/json' },
  425 + body: JSON.stringify(agentInfo.value.heartbeat)
  426 + })
  427 + } catch (e) { console.error('Heartbeat update error:', e) }
  428 +}
  429 +
  430 +async function updateCron() {
  431 + if (!agentId.value || !agentInfo.value.cron) return
  432 + try {
  433 + await fetch(`/api/agent/${agentId.value}/cron`, {
  434 + method: 'PUT',
  435 + headers: { 'Content-Type': 'application/json' },
  436 + body: JSON.stringify({ cron: agentInfo.value.cron })
  437 + })
  438 + } catch (e) { console.error('Cron update error:', e) }
  439 +}
  440 +
  441 +function addCron() {
  442 + if (!agentInfo.value.cron) agentInfo.value.cron = []
  443 + agentInfo.value.cron.push({ "schedule": "0 8 * * *", "task": "新提醒任务", "enabled": true })
  444 + updateCron()
  445 +}
  446 +
  447 +function removeCron(index) {
  448 + if (confirm('确认删除该定时任务?')) {
  449 + agentInfo.value.cron.splice(index, 1)
  450 + updateCron()
  451 + }
  452 +}
  453 +
387 454 async function loadConversations() {
388 455 if (!agentId.value) return
389 456 try {
... ...