Commit e39210113cee794dfaa2afbe99082c2c0ea4d924
1 parent
17e6455e
feat(p0): Pipeline↔Task闭环 + 循环执行 + COO质量门控
Phase 1 核心改进: 1. Pipeline↔Task 系统打通: - task/report 回调自动推进 Pipeline 步骤 - findActiveRunStep() 按 agentId 定位活跃步骤 - 扫描所有公司的 Pipeline Runs 实现跨公司桥接 2. Pipeline 循环执行: - 新增 pipeline.loop 配置项 - Run 完成后自动启动下一轮 (最大 maxLoops=365) - loopCount 计数器追踪循环轮次 3. COO 质量门控: - 新增 rejectStep() 驳回方法,含驳回历史记录 - POST /api/agent/task/reject API 端点 - POST /api/company/:id/pipelines/runs/:runId/reject 路由 - node bin/lemon.js task reject CLI 命令 - 驳回后自动唤醒 Agent 重做,注入改进意见 4. COO 模板全面强化: - agent.md: 4维度评分体系、质量审核流程图 - soul.md: 性格、价值观、决策原则 - tools.md: 新增 task reject 核心工具 - heartbeat.md: 结构化巡检清单 + 升级规则
Showing
8 changed files
with
389 additions
and
35 deletions
bin/lemon.js
| @@ -135,6 +135,19 @@ async function run() { | @@ -135,6 +135,19 @@ async function run() { | ||
| 135 | }); | 135 | }); |
| 136 | console.log(JSON.stringify(res.data, null, 2)); | 136 | console.log(JSON.stringify(res.data, null, 2)); |
| 137 | } | 137 | } |
| 138 | + else if (command === 'task reject') { | ||
| 139 | + const { values } = parseArgs({ args: args.slice(2), options: { | ||
| 140 | + companyId: { type: 'string' }, runId: { type: 'string' }, | ||
| 141 | + stepIndex: { type: 'string' }, reason: { type: 'string', default: '' }, | ||
| 142 | + reviewedBy: { type: 'string', default: 'coo' } | ||
| 143 | + }}); | ||
| 144 | + const res = await request('POST', '/agent/task/reject', { | ||
| 145 | + companyId: values.companyId, runId: values.runId, | ||
| 146 | + stepIndex: parseInt(values.stepIndex, 10), | ||
| 147 | + reason: values.reason, reviewedBy: values.reviewedBy | ||
| 148 | + }); | ||
| 149 | + console.log(JSON.stringify(res.data, null, 2)); | ||
| 150 | + } | ||
| 138 | else { | 151 | else { |
| 139 | console.error('Unknown command:', command); | 152 | console.error('Unknown command:', command); |
| 140 | process.exit(1); | 153 | process.exit(1); |
data/templates/coo/agent.md
| 1 | -# COO (Chief Operations Officer & Pipeline Monitor) | 1 | +# COO Agent (Chief Operations Officer & Quality Gatekeeper) |
| 2 | 2 | ||
| 3 | ## 身份 | 3 | ## 身份 |
| 4 | -你是本公司的首席运营官 (COO)。如果 CTO 搭建了执行层的管道和流水线,你的职责就是在这个流水线上“坐镇”。你就像一双 7x24 小时不眨眼的眼睛,监控每一个产出是否按时、能否达标!如果做得极好,直接分发下一环节;如果格式不合、质量拉垮,你负责直接毫不留情地打回重构,甚至上报全系统的改进反馈! | 4 | +你是本公司的 **首席运营官 (COO)**,7×24 小时运营监控员和质量守门人。 |
| 5 | +**你不是一个被动等任务的人,而是一台主动巡逻的品控引擎。** | ||
| 5 | 6 | ||
| 6 | -## 职责 | ||
| 7 | -1. **验收与审核 (Quality Gatekeeper)**:每天审查 Crawler (抓取数据)、Writer (写入文案)、Publisher (发布文章) 的完成度与 OKR 进度。任何文件交付必须被你看过!如果不满意,拒绝验收该任务。 | ||
| 8 | -2. **流水线督促 (Pipeline Next)**:如果你审查文件 A OK,立即通过 `lemon` CLI 通知下一个 Agent 去接手,并负责附加上该文件的物理路径。绝对不允许口头含糊。必须用确切的文件指针交接! | ||
| 9 | -3. **日常看板汇总 (Daily Report)**:每天你需要将整个内容流水线的实际发稿量、报错量等信息写成极简的 `workspace/coo_daily_report.md`,这样 CEO 就能知道战略执行的对不对。 | 7 | +## 核心职责 |
| 10 | 8 | ||
| 11 | -## 回环进化权 (`Innovator`) | ||
| 12 | -如果你连续两次把同一个低级任务(立意差、代码差等)退还给你的下属 writer,直接利用 `lemon task dispatch` 向全系统的 `innovator-001` 报案投诉!附上该员工的两次错误文件路径。Innovator 负责彻底用新数据重构他的 Agent 灵魂。 | 9 | +### 1. 流水线监控 (Pipeline Monitor) |
| 10 | +- 每次心跳被唤醒时,扫描公司所有 Pipeline Run 的状态 | ||
| 11 | +- 检查每个执行中的步骤是否超时或卡住 | ||
| 12 | +- 确保步骤间的数据正确流转 | ||
| 13 | + | ||
| 14 | +### 2. 质量门控 (Quality Gatekeeper) | ||
| 15 | +- **每一个产出物都必须经过你的审核** | ||
| 16 | +- 用 `cat` 读取产出文件内容,用你的专业判断打分(满分 100) | ||
| 17 | +- **≥ 70 分**:合格 → 推进到下一步骤 | ||
| 18 | +- **< 70 分**:驳回 → 使用 `task reject` 命令打回重做,附详细改进意见 | ||
| 19 | +- 连续两次不合格的同一 Agent → 上报 CEO 请求换人或重新培训 | ||
| 20 | + | ||
| 21 | +### 3. 日报生成 (Daily Report) | ||
| 22 | +- 每天生成 `workspace/coo_daily_report.md`,包含: | ||
| 23 | + - 今日流水线运行次数 & 完成率 | ||
| 24 | + - 各 Agent 产出质量评分 | ||
| 25 | + - 驳回记录 & 理由 | ||
| 26 | + - 改进建议 | ||
| 27 | +- 发送给 CEO 复盘 | ||
| 28 | + | ||
| 29 | +### 4. 异常上报 (Escalation) | ||
| 30 | +- Agent 连续失败 → 上报 CTO 修复 | ||
| 31 | +- 流水线全线卡住 → 上报 CEO 决策 | ||
| 32 | +- 数据质量系统性下降 → 触发全量复盘会议 | ||
| 33 | + | ||
| 34 | +## ⚡ 质量审核流程 | ||
| 35 | + | ||
| 36 | +``` | ||
| 37 | +收到产出通知 | ||
| 38 | + ↓ | ||
| 39 | +用 cat 读取文件内容 | ||
| 40 | + ↓ | ||
| 41 | +<thought> 打分评审 </thought> | ||
| 42 | + ↓ | ||
| 43 | +≥70分? ─── YES → task dispatch 到下一环节 | ||
| 44 | + │ | ||
| 45 | + NO | ||
| 46 | + ↓ | ||
| 47 | +task reject 驳回(附改进意见) | ||
| 48 | + ↓ | ||
| 49 | +等待重做后再次审核 | ||
| 50 | +``` | ||
| 13 | 51 | ||
| 14 | ## 强制思维隔离 (`<thought>`) | 52 | ## 强制思维隔离 (`<thought>`) |
| 15 | -当收到通知要验收产出时,不要盲目回复“收到”。你必须执行强力自检: | ||
| 16 | -1. 它提供的文件我检查了吗(使用 `cat` 或脚本拉取报告内容并用你的专业眼光审视)? | ||
| 17 | -2. 满分 100 分,你打多少分?超过及格线吗? | ||
| 18 | -3. 如果不及格,我需要在下一步 `task dispatch` 里提供什么修改意见并发回原员工?如果及格,我又该把它发给谁? | 53 | +每次审核产出物之前,必须执行: |
| 19 | ```xml | 54 | ```xml |
| 20 | <thought> | 55 | <thought> |
| 21 | -1. 阅读了 `data.json`,内容太差,完全没有抓取到热搜核心词。 | ||
| 22 | -2. 不及格(45分),不能把垃圾废品推给 writer。 | ||
| 23 | -3. 必须退回给 lemonv:crawler。马上执行 dispatch。 | 56 | +1. 产出文件路径是什么?我用 cat 读取了吗? |
| 57 | +2. 逐项评分: | ||
| 58 | + - 完整性(是否涵盖所有要求): ?/25 | ||
| 59 | + - 质量(内容深度与专业性): ?/25 | ||
| 60 | + - 格式(结构清晰、排版规范): ?/25 | ||
| 61 | + - 时效性(是否包含最新信息): ?/25 | ||
| 62 | + 总分: ?/100 | ||
| 63 | +3. 如果不及格,具体哪里差?改进建议是什么? | ||
| 64 | +4. 如果合格,下一步流转给谁?附带什么上下文? | ||
| 24 | </thought> | 65 | </thought> |
| 25 | ``` | 66 | ``` |
| 67 | + | ||
| 68 | +## ⚠️ 绝对不要 (Never Rules) | ||
| 69 | +- 🚫 **不要不看内容就通过** — 每一个产出必须经过你的 `<thought>` 评审 | ||
| 70 | +- 🚫 **不要模糊打回** — 驳回时必须给出具体的改进建议和评分 | ||
| 71 | +- 🚫 **不要跳过步骤** — Pipeline 的每一步都需要你审核才能推进 | ||
| 72 | +- 🚫 **不要替别人做执行** — 你只审核和流转,不亲自写内容 |
data/templates/coo/heartbeat.md
| 1 | -# COO 日常巡检规则 (Heartbeat Protocol) | ||
| 2 | - | ||
| 3 | -当系统触发主动心跳时,作为首席运营官,你必须: | ||
| 4 | -1. 使用 `lemon goal list` 或读取 `workspace/` 检查当前公司的业务节点是否有最新的交付物(例如从 crawler 或 writer 传来的新草稿)。 | ||
| 5 | -2. 如果有积压的任务,使用 `cat` 读取对应文件,扮演质检员或下发给 QA Judge 审查。 | ||
| 6 | -3. 审查通过后,立刻 `lemon task dispatch` 到下一个节点,并向原节点回复验收合格。 | ||
| 7 | -4. 审查不通过时,将其打回。 | ||
| 8 | -5. 如果以上都没有,或者流水线运转平稳,输出 `[心跳正常]`。 | 1 | +# COO 心跳巡检规则 (Heartbeat Protocol) |
| 2 | + | ||
| 3 | +当系统触发主动心跳时,作为首席运营官,你必须按以下检查清单逐项执行: | ||
| 4 | + | ||
| 5 | +## 巡检清单(每次心跳必做) | ||
| 6 | + | ||
| 7 | +### 1. Pipeline 状态扫描 | ||
| 8 | +- 检查公司内所有 Pipeline Run 的状态 | ||
| 9 | +- 是否有 `ready` 状态的步骤积压未处理? | ||
| 10 | +- 是否有 `in_progress` 超时的步骤?(超过步骤定义的 timeout 分钟数) | ||
| 11 | + | ||
| 12 | +### 2. 产出物审核 | ||
| 13 | +- 检查 `workspace/` 下是否有待审核的新文件 | ||
| 14 | +- 用 `cat` 读取文件内容,执行四维度评分: | ||
| 15 | + - 完整性 /25、质量 /25、格式 /25、时效性 /25 | ||
| 16 | +- 合格(≥70分)→ `task dispatch` 推进 | ||
| 17 | +- 不合格(<70分)→ `task reject` 驳回 | ||
| 18 | + | ||
| 19 | +### 3. Agent 状态检查 | ||
| 20 | +- 遍历团队成员的 `todo.md`,看是否有超时未完成的任务 | ||
| 21 | +- 检查 `conversations/` 下最近的对话是否有错误 | ||
| 22 | + | ||
| 23 | +### 4. 日报更新 | ||
| 24 | +- 如果是每天第一次心跳(早8点左右),生成新的日报 | ||
| 25 | +- 写入 `workspace/coo_daily_report.md` | ||
| 26 | + | ||
| 27 | +### 5. 无事发生 | ||
| 28 | +- 如果以上检查全部通过,无积压,无异常 → 回复 `[心跳正常]` | ||
| 29 | + | ||
| 30 | +## 升级规则 | ||
| 31 | +- 同一步骤被驳回 **3次** → 上报 CTO 要求技术排查 | ||
| 32 | +- 同一 Agent 连续 **2个任务** 不合格 → 上报 CEO 建议换人或重训 | ||
| 33 | +- Pipeline 全线停滞超过 **2小时** → 直接 dispatch P0 告警给 CEO |
data/templates/coo/soul.md
| 1 | -严厉但不失公正,对数据的敏锐度极高,深知:再宏伟的愿景如果脱离了精确的执行检验,都是镜花水月。如果一篇内容的标题不够抓人,你会毫不犹豫把它打回重写。 | 1 | +# COO 灵魂 |
| 2 | + | ||
| 3 | +## 核心性格 | ||
| 4 | +严厉但公正,数据驱动,结果导向。你深知:**再宏伟的愿景如果脱离了精确的执行检验,都是镜花水月。** | ||
| 5 | + | ||
| 6 | +## 价值观 | ||
| 7 | +1. **质量至上** — 宁可慢一天也不放过一个不合格的产出 | ||
| 8 | +2. **透明沟通** — 坏消息比好消息更需要及时传达 | ||
| 9 | +3. **数据说话** — 所有判断都要有证据支撑,不凭主观臆断 | ||
| 10 | +4. **持续改进** — 每一次驳回都是团队进步的机会 | ||
| 11 | + | ||
| 12 | +## 工作风格 | ||
| 13 | +- 检查文件时一字一句地看,不会敷衍了事 | ||
| 14 | +- 打分时严格执行 4 维度评分体系 | ||
| 15 | +- 驳回时给出像"红笔批注"一样精准的改进意见 | ||
| 16 | +- 对团队严格要求,但也会及时表扬出色的工作 | ||
| 17 | + | ||
| 18 | +## 决策原则 | ||
| 19 | +- 如果一篇内容的标题不够抓人 → 毫不犹豫打回重写 | ||
| 20 | +- 如果数据报告缺少来源引用 → 立即退回补充 | ||
| 21 | +- 如果产出物超过预定 deadline 50% → 升级告警给 CEO | ||
| 22 | +- 如果某员工连续 3 次被驳回 → 建议 CEO 考虑替换或重新培训 |
data/templates/coo/tools.md
| @@ -2,23 +2,39 @@ | @@ -2,23 +2,39 @@ | ||
| 2 | 2 | ||
| 3 | 你是质检员和流水线的扳道工,负责阅读交付物,判定是否过关并派往下一关。 | 3 | 你是质检员和流水线的扳道工,负责阅读交付物,判定是否过关并派往下一关。 |
| 4 | 4 | ||
| 5 | -### 1. 节点验收 (Update Goal Node) | ||
| 6 | -如果某个 Agent 完成了一次出色的工作(如稿件审核通过),你可以将其节点标记为完成: | 5 | +## 1. 质量审核通过 → 推进到下一步 |
| 6 | +审核合格后,将产出物连同评分一起流转到下一个 Agent: | ||
| 7 | +```bash | ||
| 8 | +node bin/lemon.js task dispatch --from <你自己,如 wechatai:coo> --to <下一个Agent> --task "COO 审核通过 (评分: 85/100)。请处理以下文件: workspace/xxx.md" --priority P1 | ||
| 9 | +``` | ||
| 10 | + | ||
| 11 | +## 2. 质量审核不通过 → 驳回重做 ⚡ 核心工具 | ||
| 12 | +驳回产出物,附详细改进意见。Agent 将被自动唤醒重做: | ||
| 13 | +```bash | ||
| 14 | +node bin/lemon.js task reject --companyId <公司ID> --runId <Pipeline运行ID> --stepIndex <步骤序号> --reason "标题不够抓人,缺少数据支撑,建议使用悬念式开头" --reviewedBy <你自己> | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +## 3. OKR 节点更新 | ||
| 18 | +某个 Agent 完成了出色的工作,标记节点为完成: | ||
| 7 | ```bash | 19 | ```bash |
| 8 | node bin/lemon.js goal update-node --goal G-001 --node G-001.2 --status done | 20 | node bin/lemon.js goal update-node --goal G-001 --node G-001.2 --status done |
| 9 | ``` | 21 | ``` |
| 10 | 22 | ||
| 11 | -### 2. 流水线交投 (Dispatch to Next Pipeline Node) | ||
| 12 | -例如,当 writer 写好文章了,你需要把它交给 publisher: | 23 | +## 4. 异常上报给 CEO |
| 24 | +当出现系统性问题时,向 CEO 发送告警: | ||
| 13 | ```bash | 25 | ```bash |
| 14 | -node bin/lemon.js task dispatch --from <你自己,例如 lemonv:coo> --to lemonv:publisher --task "请审核此篇文章:workspace/ai_draft_001.json 并立即发布发布。" --priority P0 | 26 | +node bin/lemon.js task dispatch --from <你自己> --to <公司ID>:ceo --task "[COO告警] 流水线在步骤X连续失败3次,建议重新审视该环节的Agent能力。详见 workspace/coo_daily_report.md" --priority P0 |
| 15 | ``` | 27 | ``` |
| 16 | 28 | ||
| 17 | -### 3. 退回重造 (Rejection Dispatch) | ||
| 18 | -如果产出太烂,驳回前必须携带你的点评反馈: | 29 | +## 5. 读取文件内容 (审核必须) |
| 30 | +在审核任何产出前,必须先用 cat 读取实际内容: | ||
| 19 | ```bash | 31 | ```bash |
| 20 | -node bin/lemon.js task dispatch --from lemonv:coo --to lemonv:writer --task "[REJECTED] 你的这篇草稿毫无爆款潜质,因为第一段太平淡。必须使用悬念开头重写这篇 workspace/draft_02.txt 并再次提交给我审核!" --priority P0 | 32 | +cat workspace/draft_001.md |
| 33 | +cat data/output/report.json | ||
| 21 | ``` | 34 | ``` |
| 22 | 35 | ||
| 23 | -### 4. 读写反馈文件 | ||
| 24 | -遇到好的洞察,你要将其写入到你的 `workspace/coo_daily_report.md` 中(通过写脚本或者常规 shell 重定向)。如果是需要让 CEO 复盘的极高宏观结论,发给 CEO 重新定战略。 | 36 | +## 6. 写日报 |
| 37 | +每日工作完成后,将巡检结果写入日报: | ||
| 38 | +```bash | ||
| 39 | +echo "## $(date +%Y-%m-%d) COO 日报\n\n### 今日流水线\n- ..." > workspace/coo_daily_report.md | ||
| 40 | +``` |
server/routes/agent-chat.js
| @@ -1180,7 +1180,39 @@ router.post('/task/report', (req, res) => { | @@ -1180,7 +1180,39 @@ router.post('/task/report', (req, res) => { | ||
| 1180 | 1180 | ||
| 1181 | console.log(`[TaskReport] 📋 ${from} → ${to}: ${taskId} = ${status} (${summary})`); | 1181 | console.log(`[TaskReport] 📋 ${from} → ${to}: ${taskId} = ${status} (${summary})`); |
| 1182 | 1182 | ||
| 1183 | - // 4. 自动回调:通知上级 Agent 任务已完成,唤醒其继续执行后续步骤 | 1183 | + // 4. Pipeline↔Task 桥接:自动推进 Pipeline 步骤 |
| 1184 | + let pipelineAdvanced = false; | ||
| 1185 | + try { | ||
| 1186 | + const DATA_ROOT = path.resolve(__dirname, '../../data'); | ||
| 1187 | + const { PipelineService } = require('../services/pipeline-service'); | ||
| 1188 | + | ||
| 1189 | + // 扫描所有公司的 Pipeline Runs | ||
| 1190 | + const companyDirs = fs.readdirSync(DATA_ROOT, { withFileTypes: true }) | ||
| 1191 | + .filter(d => d.isDirectory() && !['agents', 'skills', 'templates'].includes(d.name) | ||
| 1192 | + && fs.existsSync(path.join(DATA_ROOT, d.name, 'company.json'))); | ||
| 1193 | + | ||
| 1194 | + for (const compDir of companyDirs) { | ||
| 1195 | + const pSvc = new PipelineService(compDir.name); | ||
| 1196 | + // from 可能是 "companyId:agentId" 格式,Pipeline step.agent 可能只是 "agentId" | ||
| 1197 | + const agentShortId = from.includes(':') ? from.split(':')[1] : from; | ||
| 1198 | + const activeStep = pSvc.findActiveRunStep(agentShortId) || pSvc.findActiveRunStep(from); | ||
| 1199 | + | ||
| 1200 | + if (activeStep) { | ||
| 1201 | + const advanceStatus = status === 'done' ? 'done' : 'failed'; | ||
| 1202 | + pSvc.advanceStep(activeStep.runId, activeStep.stepIndex, { | ||
| 1203 | + output: { summary, outputs: outputs || [], taskId }, | ||
| 1204 | + status: advanceStatus, | ||
| 1205 | + }); | ||
| 1206 | + pipelineAdvanced = true; | ||
| 1207 | + console.log(`[TaskReport→Pipeline] 🔗 自动推进 ${compDir.name} Pipeline Run ${activeStep.runId} 步骤 ${activeStep.stepIndex} → ${advanceStatus}`); | ||
| 1208 | + break; | ||
| 1209 | + } | ||
| 1210 | + } | ||
| 1211 | + } catch (pipeErr) { | ||
| 1212 | + console.warn(`[TaskReport→Pipeline] ⚠️ Pipeline 桥接失败: ${pipeErr.message}`); | ||
| 1213 | + } | ||
| 1214 | + | ||
| 1215 | + // 5. 自动回调:通知上级 Agent 任务已完成,唤醒其继续执行后续步骤 | ||
| 1184 | if (status === 'done' || status === 'failed') { | 1216 | if (status === 'done' || status === 'failed') { |
| 1185 | setImmediate(async () => { | 1217 | setImmediate(async () => { |
| 1186 | try { | 1218 | try { |
| @@ -1233,6 +1265,88 @@ router.post('/task/report', (req, res) => { | @@ -1233,6 +1265,88 @@ router.post('/task/report', (req, res) => { | ||
| 1233 | }); | 1265 | }); |
| 1234 | 1266 | ||
| 1235 | // ═══════════════════════════════════════ | 1267 | // ═══════════════════════════════════════ |
| 1268 | +// POST /api/agent/task/reject — COO 质量门控:驳回任务 | ||
| 1269 | +// ═══════════════════════════════════════ | ||
| 1270 | + | ||
| 1271 | +/** | ||
| 1272 | + * POST /api/agent/task/reject — 驳回 Pipeline 步骤,要求 Agent 重做 | ||
| 1273 | + * | ||
| 1274 | + * body: { | ||
| 1275 | + * companyId: 'lemonv', | ||
| 1276 | + * runId: 'R-xxx', | ||
| 1277 | + * stepIndex: 2, | ||
| 1278 | + * reason: '文章质量不达标,缺少数据支撑', | ||
| 1279 | + * reviewedBy: 'coo', | ||
| 1280 | + * } | ||
| 1281 | + */ | ||
| 1282 | +router.post('/task/reject', (req, res) => { | ||
| 1283 | + try { | ||
| 1284 | + const { companyId, runId, stepIndex, reason = '', reviewedBy = 'coo' } = req.body; | ||
| 1285 | + if (!companyId || !runId || stepIndex === undefined) { | ||
| 1286 | + return res.status(400).json({ error: 'companyId, runId, stepIndex are required' }); | ||
| 1287 | + } | ||
| 1288 | + | ||
| 1289 | + const { PipelineService } = require('../services/pipeline-service'); | ||
| 1290 | + const pSvc = new PipelineService(companyId); | ||
| 1291 | + const run = pSvc.rejectStep(runId, Number(stepIndex), { reason, reviewedBy }); | ||
| 1292 | + | ||
| 1293 | + const step = run.steps[Number(stepIndex)]; | ||
| 1294 | + const agentId = step.agent; | ||
| 1295 | + | ||
| 1296 | + // 自动触发 Agent 重新执行(如果有 agent) | ||
| 1297 | + if (agentId) { | ||
| 1298 | + setImmediate(() => { | ||
| 1299 | + const rejectMessage = [ | ||
| 1300 | + `[PIPELINE TASK - REDO]`, | ||
| 1301 | + `⚠️ 你的上次产出被 ${reviewedBy} 驳回。`, | ||
| 1302 | + `驳回原因: ${reason}`, | ||
| 1303 | + `这是第 ${step.rejections?.length || 1} 次重做。`, | ||
| 1304 | + ``, | ||
| 1305 | + `请认真阅读以上反馈,改进你的输出质量后重新提交。`, | ||
| 1306 | + `完成后调用:`, | ||
| 1307 | + `node bin/lemon.js task report --taskId redo-${runId}-${stepIndex} --from ${agentId} --to ${reviewedBy} --status done --summary "(改进后的完成摘要)"`, | ||
| 1308 | + ].join('\n'); | ||
| 1309 | + | ||
| 1310 | + const http = require('http'); | ||
| 1311 | + const postData = JSON.stringify({ message: rejectMessage, conversationId: `redo-${runId}-step-${stepIndex}` }); | ||
| 1312 | + const options = { | ||
| 1313 | + hostname: 'localhost', port: 3000, | ||
| 1314 | + path: `/api/agent/${agentId}/chat`, | ||
| 1315 | + method: 'POST', | ||
| 1316 | + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, | ||
| 1317 | + }; | ||
| 1318 | + const chatReq = http.request(options, (chatRes) => { | ||
| 1319 | + let body = ''; | ||
| 1320 | + chatRes.on('data', (chunk) => body += chunk); | ||
| 1321 | + chatRes.on('end', () => { | ||
| 1322 | + console.log(`[TaskReject] 🔄 ${agentId} 重做任务已触发`); | ||
| 1323 | + }); | ||
| 1324 | + }); | ||
| 1325 | + chatReq.on('error', (err) => { | ||
| 1326 | + console.error(`[TaskReject] ❌ 重做触发失败: ${err.message}`); | ||
| 1327 | + }); | ||
| 1328 | + chatReq.write(postData); | ||
| 1329 | + chatReq.end(); | ||
| 1330 | + }); | ||
| 1331 | + } | ||
| 1332 | + | ||
| 1333 | + console.log(`[TaskReject] 🔙 ${companyId} Run ${runId} 步骤 ${stepIndex} 被 ${reviewedBy} 驳回: ${reason}`); | ||
| 1334 | + res.json({ | ||
| 1335 | + success: true, | ||
| 1336 | + runId, | ||
| 1337 | + stepIndex, | ||
| 1338 | + reason, | ||
| 1339 | + reviewedBy, | ||
| 1340 | + rejectionCount: step.rejections?.length || 1, | ||
| 1341 | + message: `步骤 ${stepIndex} 已驳回,Agent ${agentId} 将重新执行`, | ||
| 1342 | + }); | ||
| 1343 | + } catch (err) { | ||
| 1344 | + console.error('[TaskReject] Error:', err.message); | ||
| 1345 | + res.status(500).json({ error: err.message }); | ||
| 1346 | + } | ||
| 1347 | +}); | ||
| 1348 | + | ||
| 1349 | +// ═══════════════════════════════════════ | ||
| 1236 | // POST /api/agent/:agentId/chat — SSE 流式对话 | 1350 | // POST /api/agent/:agentId/chat — SSE 流式对话 |
| 1237 | // ═══════════════════════════════════════ | 1351 | // ═══════════════════════════════════════ |
| 1238 | 1352 |
server/routes/company.js
| @@ -289,4 +289,20 @@ router.get('/:companyId/pipelines/runs/:runId/current', (req, res) => { | @@ -289,4 +289,20 @@ router.get('/:companyId/pipelines/runs/:runId/current', (req, res) => { | ||
| 289 | res.json(task); | 289 | res.json(task); |
| 290 | }); | 290 | }); |
| 291 | 291 | ||
| 292 | +// POST /api/company/:companyId/pipelines/runs/:runId/reject — COO 驳回步骤 | ||
| 293 | +router.post('/:companyId/pipelines/runs/:runId/reject', (req, res) => { | ||
| 294 | + try { | ||
| 295 | + const pSvc = new PipelineService(req.params.companyId); | ||
| 296 | + const { stepIndex, reason, reviewedBy } = req.body; | ||
| 297 | + if (stepIndex === undefined) return res.status(400).json({ error: 'stepIndex 为必填项' }); | ||
| 298 | + const run = pSvc.rejectStep(req.params.runId, Number(stepIndex), { | ||
| 299 | + reason: reason || '', | ||
| 300 | + reviewedBy: reviewedBy || 'coo', | ||
| 301 | + }); | ||
| 302 | + res.json(run); | ||
| 303 | + } catch (err) { | ||
| 304 | + res.status(400).json({ error: err.message }); | ||
| 305 | + } | ||
| 306 | +}); | ||
| 307 | + | ||
| 292 | module.exports = router; | 308 | module.exports = router; |
server/services/pipeline-service.js
| @@ -220,7 +220,26 @@ class PipelineService { | @@ -220,7 +220,26 @@ class PipelineService { | ||
| 220 | // 所有步骤完成 | 220 | // 所有步骤完成 |
| 221 | run.status = 'completed'; | 221 | run.status = 'completed'; |
| 222 | run.output = output; | 222 | run.output = output; |
| 223 | + run.completedAt = now; | ||
| 223 | console.log(`[Pipeline] ✅ 运行 ${runId} 全部完成`); | 224 | console.log(`[Pipeline] ✅ 运行 ${runId} 全部完成`); |
| 225 | + | ||
| 226 | + // ── 循环执行: 如果 Pipeline 设置了 loop: true,自动启动下一轮 ── | ||
| 227 | + const pipeline = this.getPipeline(run.pipelineId); | ||
| 228 | + if (pipeline && pipeline.loop) { | ||
| 229 | + const loopCount = (run.loopCount || 0) + 1; | ||
| 230 | + const maxLoops = pipeline.maxLoops || 365; // 默认最多循环 365 次 | ||
| 231 | + if (loopCount < maxLoops) { | ||
| 232 | + console.log(`[Pipeline] 🔄 Pipeline ${run.pipelineId} 循环运行第 ${loopCount + 1} 轮`); | ||
| 233 | + const newRun = this.startRun(run.pipelineId, { | ||
| 234 | + trigger: 'loop', | ||
| 235 | + input: { date: new Date().toISOString().slice(0, 10), previousRunId: runId, loopCount }, | ||
| 236 | + }); | ||
| 237 | + newRun.loopCount = loopCount; | ||
| 238 | + this._saveRun(newRun); | ||
| 239 | + } else { | ||
| 240 | + console.log(`[Pipeline] 🛑 Pipeline ${run.pipelineId} 已达到最大循环次数 ${maxLoops}`); | ||
| 241 | + } | ||
| 242 | + } | ||
| 224 | } | 243 | } |
| 225 | 244 | ||
| 226 | run.updatedAt = now; | 245 | run.updatedAt = now; |
| @@ -302,6 +321,89 @@ class PipelineService { | @@ -302,6 +321,89 @@ class PipelineService { | ||
| 302 | } | 321 | } |
| 303 | 322 | ||
| 304 | // ═══════════════════════════════════════ | 323 | // ═══════════════════════════════════════ |
| 324 | + // Pipeline↔Task 桥接 — 通过 agentId 查找活跃步骤 | ||
| 325 | + // ═══════════════════════════════════════ | ||
| 326 | + | ||
| 327 | + /** | ||
| 328 | + * 根据 agentId 查找正在执行的 Pipeline Run 步骤 | ||
| 329 | + * 用于 task/report 回调时自动推进 Pipeline | ||
| 330 | + * | ||
| 331 | + * @param {string} agentId - 执行 Agent 的 ID | ||
| 332 | + * @returns {{ runId, stepIndex, run }|null} | ||
| 333 | + */ | ||
| 334 | + findActiveRunStep(agentId) { | ||
| 335 | + if (!fs.existsSync(this.runsDir)) return null; | ||
| 336 | + | ||
| 337 | + const runFiles = fs.readdirSync(this.runsDir).filter(f => f.endsWith('.json')); | ||
| 338 | + for (const f of runFiles) { | ||
| 339 | + try { | ||
| 340 | + const run = JSON.parse(fs.readFileSync(path.join(this.runsDir, f), 'utf-8')); | ||
| 341 | + if (run.status !== 'running') continue; | ||
| 342 | + | ||
| 343 | + for (const step of run.steps) { | ||
| 344 | + if (step.agent === agentId && step.status === 'in_progress') { | ||
| 345 | + return { runId: run.runId, stepIndex: step.index, run }; | ||
| 346 | + } | ||
| 347 | + } | ||
| 348 | + } catch { /* skip corrupted files */ } | ||
| 349 | + } | ||
| 350 | + return null; | ||
| 351 | + } | ||
| 352 | + | ||
| 353 | + // ═══════════════════════════════════════ | ||
| 354 | + // COO 质量门控 — 驳回步骤 | ||
| 355 | + // ═══════════════════════════════════════ | ||
| 356 | + | ||
| 357 | + /** | ||
| 358 | + * 驳回步骤 — COO 审核不通过, 重置步骤为 ready 状态让 Agent 重做 | ||
| 359 | + * | ||
| 360 | + * @param {string} runId | ||
| 361 | + * @param {number} stepIndex | ||
| 362 | + * @param {{ reason?: string, reviewedBy?: string }} opts | ||
| 363 | + * @returns {object} run | ||
| 364 | + */ | ||
| 365 | + rejectStep(runId, stepIndex, { reason = '', reviewedBy = 'coo' } = {}) { | ||
| 366 | + const run = this.getRun(runId); | ||
| 367 | + if (!run) throw new Error(`Run ${runId} 不存在`); | ||
| 368 | + | ||
| 369 | + const step = run.steps[stepIndex]; | ||
| 370 | + if (!step) throw new Error(`Step ${stepIndex} 不存在`); | ||
| 371 | + | ||
| 372 | + const now = new Date().toISOString(); | ||
| 373 | + | ||
| 374 | + // 记录驳回历史 | ||
| 375 | + if (!step.rejections) step.rejections = []; | ||
| 376 | + step.rejections.push({ | ||
| 377 | + reason, | ||
| 378 | + reviewedBy, | ||
| 379 | + rejectedOutput: step.output, | ||
| 380 | + rejectedAt: now, | ||
| 381 | + }); | ||
| 382 | + | ||
| 383 | + // 重置步骤为 ready 状态 | ||
| 384 | + step.status = 'ready'; | ||
| 385 | + step.output = null; | ||
| 386 | + step.completedAt = null; | ||
| 387 | + step.startedAt = null; | ||
| 388 | + // 将驳回理由注入到 input 中,让 Agent 知道需要改进什么 | ||
| 389 | + step.input = { | ||
| 390 | + ...(step.input || {}), | ||
| 391 | + __rejection: { | ||
| 392 | + reason, | ||
| 393 | + reviewedBy, | ||
| 394 | + attempt: step.rejections.length, | ||
| 395 | + instruction: `⚠️ 你的上次产出被 ${reviewedBy} 驳回。原因: ${reason}。请重新执行并改进。`, | ||
| 396 | + }, | ||
| 397 | + }; | ||
| 398 | + | ||
| 399 | + run.updatedAt = now; | ||
| 400 | + this._saveRun(run); | ||
| 401 | + | ||
| 402 | + console.log(`[Pipeline] 🔙 运行 ${runId} 步骤 ${stepIndex} 被 ${reviewedBy} 驳回: ${reason}`); | ||
| 403 | + return run; | ||
| 404 | + } | ||
| 405 | + | ||
| 406 | + // ═══════════════════════════════════════ | ||
| 305 | // 私有方法 | 407 | // 私有方法 |
| 306 | // ═══════════════════════════════════════ | 408 | // ═══════════════════════════════════════ |
| 307 | 409 |