Commit e39210113cee794dfaa2afbe99082c2c0ea4d924

Authored by yibo
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: 结构化巡检清单 + 升级规则
bin/lemon.js
... ... @@ -135,6 +135,19 @@ async function run() {
135 135 });
136 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 151 else {
139 152 console.error('Unknown command:', command);
140 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 52 ## 强制思维隔离 (`<thought>`)
15   -当收到通知要验收产出时,不要盲目回复“收到”。你必须执行强力自检:
16   -1. 它提供的文件我检查了吗(使用 `cat` 或脚本拉取报告内容并用你的专业眼光审视)?
17   -2. 满分 100 分,你打多少分?超过及格线吗?
18   -3. 如果不及格,我需要在下一步 `task dispatch` 里提供什么修改意见并发回原员工?如果及格,我又该把它发给谁?
  53 +每次审核产出物之前,必须执行:
19 54 ```xml
20 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 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 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 19 ```bash
8 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 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 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(&#39;/task/report&#39;, (req, res) =&gt; {
1180 1180  
1181 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 1216 if (status === 'done' || status === 'failed') {
1185 1217 setImmediate(async () => {
1186 1218 try {
... ... @@ -1233,6 +1265,88 @@ router.post(&#39;/task/report&#39;, (req, res) =&gt; {
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 1350 // POST /api/agent/:agentId/chat — SSE 流式对话
1237 1351 // ═══════════════════════════════════════
1238 1352  
... ...
server/routes/company.js
... ... @@ -289,4 +289,20 @@ router.get(&#39;/:companyId/pipelines/runs/:runId/current&#39;, (req, res) =&gt; {
289 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 308 module.exports = router;
... ...
server/services/pipeline-service.js
... ... @@ -220,7 +220,26 @@ class PipelineService {
220 220 // 所有步骤完成
221 221 run.status = 'completed';
222 222 run.output = output;
  223 + run.completedAt = now;
223 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 245 run.updatedAt = now;
... ... @@ -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  
... ...