From ccc7baafbd6e67eb28a6b91e227dfe25b7f9497f Mon Sep 17 00:00:00 2001 From: ymq Date: Wed, 19 Aug 2026 17:17:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(iteration):=20start=5Fnext=5Fiteration=20?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4=E2=80=94=E2=80=94=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E8=BF=AD=E4=BB=A3=E6=9C=89=E6=9C=AA=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=97=B6=E9=A1=BB=20confirm=3Dtrue=20?= =?UTF-8?q?=E6=89=8D=E4=BD=9C=E5=BA=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当前迭代存在活跃任务时,start_next_iteration 不带 confirm 返回 CONFIRM 提示 ("还有 N 个未完成任务,确认作废并启动下一迭代?"),用户确认后 agent 传 confirm=true 才执行。 无活跃任务时无需确认直接推进。抽 _active_task_ids 供计数与作废复用。 --- pipeline_service/iteration_capability.py | 30 +++++++++++++++++------- pipeline_service/sdlc_ability.py | 12 +++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pipeline_service/iteration_capability.py b/pipeline_service/iteration_capability.py index c9ebff2..39d4e2a 100644 --- a/pipeline_service/iteration_capability.py +++ b/pipeline_service/iteration_capability.py @@ -180,21 +180,25 @@ async def get_current_iteration(sor, project_id): return None -async def _cancel_active_tasks(sor, project_id, iteration_name): - """作废某迭代内所有活跃任务(非终态 → cancelled + 清 claimed_by)。 +async def _active_task_ids(sor, project_id, iteration_name): + """某迭代内所有活跃任务的 ID 列表(非终态:submitted/running/review/qc_review/waiting)。 - 任务经 params.iteration_id(存迭代名)归属迭代;只作废该迭代的活跃任务, - 已终态(completed/approved/cancelled/failed)的不动。返回作废数量。 + 任务经 params.iteration_id(存迭代名)归属迭代;已终态(completed/approved/cancelled/failed)不计。 """ if not iteration_name: - return 0 + return [] recs = await sor.sqlExe( "SELECT id FROM pipeline_tasks WHERE tenant_id=${pid}$ " "AND state NOT IN ('completed','approved','cancelled','failed') " "AND JSON_UNQUOTE(JSON_EXTRACT(params,'$.iteration_id'))=${nm}$", {"pid": project_id, "nm": iteration_name}) await sor.sqlExe("COMMIT", {}) - ids = [getattr(r, 'id', '') for r in (recs or []) if getattr(r, 'id', '')] + return [getattr(r, 'id', '') for r in (recs or []) if getattr(r, 'id', '')] + + +async def _cancel_active_tasks(sor, project_id, iteration_name): + """作废某迭代内所有活跃任务(非终态 → cancelled + 清 claimed_by)。返回作废数量。""" + ids = await _active_task_ids(sor, project_id, iteration_name) if ids: in_clause = ",".join(["'" + x.replace("'", "''") + "'" for x in ids]) await sor.sqlExe( @@ -203,10 +207,11 @@ async def _cancel_active_tasks(sor, project_id, iteration_name): return len(ids) -async def start_next_iteration(project_id, who=None, agent_id=None): +async def start_next_iteration(project_id, who=None, agent_id=None, confirm=False): """启动下一个迭代(用户指令推进): - 1. 若存在当前迭代(in_progress),先强制完成它 + 作废其活跃任务。 + 1. 若存在当前迭代(in_progress)且有活跃任务,须 confirm=True 才强制完成它 + 作废活跃任务 + (否则返回 CONFIRM 提示,等用户确认)。 2. 找下一个 planning 迭代(seq_no 最小且 > 已结束迭代的最大 seq_no),start 它。 返回 (ok, message)。 """ @@ -214,8 +219,15 @@ async def start_next_iteration(project_id, who=None, agent_id=None): return False, "缺少 project_id" db, dbname = _get_db() async with db.sqlorContext(dbname) as sor: - # 1. 强制完成当前迭代 + 作废其活跃任务 cur = await get_current_iteration(sor, project_id) + # 二次确认:当前迭代有活跃任务时,须 confirm=True 才作废(否则返回确认提示) + if cur and not confirm: + cur_name = cur.get('iteration_name', '') or '' + active_ids = await _active_task_ids(sor, project_id, cur_name) + if active_ids: + return False, (f"CONFIRM: 当前迭代「{cur_name}」还有 {len(active_ids)} 个未完成任务," + f"确认作废这些任务并启动下一迭代?请回复「确认」,agent 将传 confirm=true 执行") + # 1. 强制完成当前迭代 + 作废其活跃任务 if cur: cur_id = cur.get('id', '') cur_name = cur.get('iteration_name', '') or '' diff --git a/pipeline_service/sdlc_ability.py b/pipeline_service/sdlc_ability.py index 669dd88..f77c637 100644 --- a/pipeline_service/sdlc_ability.py +++ b/pipeline_service/sdlc_ability.py @@ -174,7 +174,7 @@ SDL_TOOLS = [ ToolDefinition(name="start_iteration", description="开始迭代(planning→in_progress)", parameters={"iteration_id": "迭代ID"}, category="iteration"), ToolDefinition(name="complete_iteration", description="完成迭代(in_progress→completed)", parameters={"iteration_id": "迭代ID"}, category="iteration"), ToolDefinition(name="cancel_iteration", description="取消迭代(planning/in_progress→cancelled)", parameters={"iteration_id": "迭代ID", "comment": "取消原因(可选)"}, category="iteration"), - ToolDefinition(name="start_next_iteration", description="启动下一个迭代(强制完成当前迭代+作废其活跃任务,再启动编号最小的下一个planning迭代)", parameters={}, category="iteration"), + ToolDefinition(name="start_next_iteration", description="启动下一个迭代(若当前迭代有未完成任务需二次确认confirm=true,否则强制完成当前+作废其活跃任务+启动下一planning迭代)", parameters={"confirm": "二次确认(当前迭代有未完成任务时必须为true)"}, category="iteration"), # ── 交付件评审 ── ToolDefinition(name="submit_deliverable", description="提交交付件(pending)", parameters={"title": "交付件标题", "deliverable_type": "类型(code/doc/config等)", "content": "交付件内容", "task_id": "关联任务ID(可选)"}, category="deliverable"), ToolDefinition(name="approve_deliverable", description="评审通过交付件(pending→approved)", parameters={"deliverable_id": "交付件ID"}, category="deliverable"), @@ -1081,10 +1081,16 @@ async def _h_start_next_iteration(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: return "请先切换到项目" + confirm = (str(p.get("confirm", "") or "")).strip().lower() == "true" from .iteration_capability import start_next_iteration ok, msg = await start_next_iteration(pid, who="agent.main_agent", - agent_id=ctx.get("user_id", "") or "") - return f"OK: {msg}" if ok else f"ERROR: {msg}" + agent_id=ctx.get("user_id", "") or "", + confirm=confirm) + if ok: + return f"OK: {msg}" + if msg.startswith("CONFIRM:"): + return msg # 确认提示原样返回,让 agent 引导用户确认 + return f"ERROR: {msg}" async def _h_create_iteration(sor, p, ctx):