feat(flow_plan): 流程修订/定向重做机制(2026-09-17用户裁定:用户有权调整流程,并要求从流程任何一步按意见重做,或只给意见由agent判注入点)——revise_flow_plan:已confirmed计划提修订版(v+1 pending_confirm,确认前旧计划持续生效,确认时CAS换版旧版superseded+回调产线stage_reentry钩子);detect_reentry_stage:LLM按意见+阶段声明+产线进度摘要判注入点(对齐detect_flow_template判流先例,evidence进确认待办用户核对,错判确认门兜住,判不出诚实报错禁默认);_affected_stage_keys:注入阶段+deps依赖闭包下游;钩子失败不回滚确认,诚实上报+抛人工待办;新会话工具revise_flow_plan/detect_reentry_stage进FLOW_PLAN_TOOLS(三产线零改动继承);propose已确认时错误指引改指revise;models加reentry_stage/reentry_detect_note列

This commit is contained in:
ymq 2026-09-18 00:05:42 +08:00
parent 12f8384d49
commit c8b44fe423
2 changed files with 522 additions and 7 deletions

View File

@ -87,6 +87,19 @@
"title": "确认时间",
"type": "datetime"
},
{
"name": "reentry_stage",
"title": "定向重做注入阶段key(流程修订机制,2026-09-17;空=仅裁剪修订不重做)",
"type": "str",
"length": 64,
"nullable": "yes",
"default": ""
},
{
"name": "reentry_detect_note",
"title": "注入点判定依据(LLM判流evidence/agent说明,进确认待办给用户核对)",
"type": "text"
},
{
"name": "reject_comment",
"title": "驳回意见(会话agent按此修订重提)",
@ -126,4 +139,4 @@
]
}
]
}
}

View File

@ -272,8 +272,10 @@ async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
{"p": project_id, "s": S_CONFIRMED})
await sor.sqlExe("COMMIT", {})
if conf:
return False, ("项目流程计划已确认plan %s),确认后不可修改。"
"如需调整请新建项目。" % getattr(conf[0], "id", "")[:8])
return False, ("项目流程计划已确认plan %s)。运行中调整流程请改用 "
"revise_flow_plan 提出修订(可改裁剪、可从任一阶段按用户意见"
"定向重做),修订经用户确认后生效。"
% getattr(conf[0], "id", "")[:8])
# 校验2裁剪时机——流程已产出结果则拒绝启动竞态容忍见 _STARTED_STATES 注释)
started = "','".join(_STARTED_STATES)
@ -429,8 +431,8 @@ def _render_confirm_md(pname, stages_snap, trim_keys, warns, user_req, note, ver
lines += ["", "### ⚠️ 裁剪风险(裁掉的是质量门禁,请确认知晓)", ""]
lines += ["- " + w for w in warns]
lines += ["", "---", "",
"点击「确认并启用」后流程按上表生效**确认后不可再改**",
"点击「驳回」并填写意见,助手会按你的意见修订后重新提案。"]
"点击「确认并启用」后流程按上表生效生效后可随时再提修订revise_flow_plan",
"含从任一阶段定向重做);点击「驳回」并填写意见,助手会按你的意见修订后重新提案。"]
return "\n".join(lines)
@ -494,16 +496,37 @@ async def confirm_flow_plan(plan_id, operator_id):
"qc_comment='用户已确认流程裁剪方案', submitted_by=${u}$, submitted_at=NOW() "
"WHERE id=${h}$ AND status='pending'", {"u": operator_id, "h": ht})
await sor.sqlExe("COMMIT", {})
# 修订版换版2026-09-17 流程修订机制):旧 confirmed 版本置 superseded
# ——同一项目同时只允许一个 confirmed 计划plan_stage_enabled/get_active_plan
# 按 version DESC 取最新,但显式换版语义更清晰,审计可追溯)。
await sor.sqlExe(
"UPDATE pipeline_flow_plans SET status=${ns}$, updated_at=NOW() "
"WHERE project_id=${p}$ AND status=${cs}$ AND id<>${i}$",
{"ns": S_SUPERSEDED, "p": project_id, "cs": S_CONFIRMED, "i": plan_id})
await sor.sqlExe("COMMIT", {})
# 收敛清理:被裁阶段角色的在办任务取消(防确认后旧任务残留)
n_cancel = await _cancel_trimmed_tasks(sor, project_id, plan.get("stages"))
await record_audit(project_id, "pipeline_flow_plans", plan_id, "confirm",
from_state=S_PENDING, to_state=S_CONFIRMED, who=operator_id,
detail="user confirmed; cancelled %d trimmed-stage tasks" % n_cancel,
detail="user confirmed; old confirmed superseded; "
"cancelled %d trimmed-stage tasks" % n_cancel,
sor=sor)
await sor.sqlExe("COMMIT", {})
logger.info("flow_plan confirmed: project=%s plan=%s by=%s cancelled=%d",
project_id, plan_id, operator_id, n_cancel)
return True, "流程裁剪已确认并启用,系统将按裁剪后的流程派发任务。"
# 定向重做reentry_stage 非空):调产线钩子执行阶段重置。放在计划生效后——
# 重置后产线流转机制按新计划+被重置的状态自动重新推进。
reentry_msg = ""
if (plan.get("reentry_stage") or "").strip():
ok_r, msg_r = await _run_stage_reentry(sor, plan, operator_id)
if ok_r:
reentry_msg = "\n定向重做已执行:%s" % (msg_r or "阶段重置完成,流程将自动重新推进")
else:
reentry_msg = "\n⚠️ 定向重做执行失败:%s(已抛人工待办)" % (msg_r or "未知错误")
_is_rev = int(plan.get("version", 1) or 1) > 1 or (plan.get("reentry_stage") or "").strip()
_base = ("流程修订已确认并启用,系统将按新计划派发任务。" if _is_rev
else "流程裁剪已确认并启用,系统将按裁剪后的流程派发任务。")
return True, _base + reentry_msg
async def reject_flow_plan(plan_id, comment, operator_id):
@ -736,6 +759,423 @@ async def create_sub_tasks(project_id="", tasks=None, parent_task_id="", task_id
return True, str(result)
async def _affected_stage_keys(stages_snap, stage_key):
"""注入阶段 + 其依赖闭包下游阶段 key 集合(按快照 deps 反向传播)。"""
keys = {s.get("key") for s in stages_snap if isinstance(s, dict)}
if stage_key not in keys:
return set()
affected = {stage_key}
changed = True
while changed:
changed = False
for s in stages_snap:
if not isinstance(s, dict):
continue
k = s.get("key")
if k in affected:
continue
if any(d in affected for d in (s.get("deps") or [])):
affected.add(k)
changed = True
return affected
async def _render_reentry_desc(pipeline_id, project_id, stage_key, affected, stages_snap, sor):
"""重做影响说明:产线钩子优先,无钩子给通用文案。"""
label_map = {(s.get("key") or ""): (s.get("label") or s.get("key") or "")
for s in stages_snap if isinstance(s, dict)}
try:
from pipeline_core import get_stage_reentry_describe
fn = get_stage_reentry_describe(pipeline_id)
if fn:
txt = await fn(sor, project_id, stage_key, sorted(affected))
if txt:
return txt
except Exception as e:
logger.warning("stage_reentry_describe hook failed: %s", str(e)[:160])
lines = ["从阶段「%s」起重做,将影响以下阶段(含其下游):" % label_map.get(stage_key, stage_key), ""]
for k in stages_snap:
if isinstance(k, dict) and k.get("key") in affected:
lines.append("- %s" % (k.get("label") or k.get("key")))
return "\n".join(lines)
async def revise_flow_plan(project_id, user_requirements="", reentry_stage="",
trim_keys=None, propose_note="", reentry_detect_note="",
pipeline_id="", who=None, agent_id=None):
"""流程修订提案:对已 confirmed 计划提出修订版(改裁剪 和/或 定向重做)。
propose_flow_plan 的分工
- propose_flow_plan项目未开跑时的首次裁剪提案confirmed 计划存在时拒绝
- revise_flow_plan已有 confirmed 计划后的修订提案 confirmed 计划时拒绝
让调用方走 propose
reentry_stage = 仅调整裁剪非空 = 定向重做确认后产线钩子重置该阶段及
依赖下游的产出/状态流转机制自动从该阶段重新推进
校验代码硬校验不靠 LLM 自觉
1. 存在 confirmed 计划修订对象
2. pending_confirm 计划在途一次只允许一个修订在途 pending 自动 superseded
沿用 propose 的版本语义
3. trim_keys 合法性同 propose未知阶段/不可裁/依赖断裂拒绝
4. reentry_stage 必须是声明阶段且在新计划中保留enabled被裁阶段无产出可重做
5. 产线未注册 stage_reentry 钩子且 reentry_stage 非空 拒绝诚实降级该产线
不支持定向重做说明原因不静默忽略用户要求
"""
if not project_id:
return False, "缺少 project_id请先切换到项目"
user_requirements = (user_requirements or "").strip()
if not user_requirements:
return False, "缺少 user_requirements用户的修订要求原文必填——进确认待办"
reentry_stage = (reentry_stage or "").strip()
trim_keys = _parse_trim_keys(trim_keys)
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
pid, proj = await _resolve_pipeline(sor, project_id, pipeline_id)
if not proj:
return False, "项目不存在: %s" % project_id
if not pid:
return False, "项目未关联产线,无法修订流程"
pname = proj.get("name") or project_id
old_plan = await _latest_plan_rec(sor, project_id)
if not old_plan:
return False, "项目尚无流程计划——首次裁剪请用 propose_flow_plan"
flow_key = (old_plan.get("base_flow_key") or "").strip()
if old_plan.get("status") == S_PENDING:
return False, ("已有修订版待用户确认plan %s v%s)——等用户处理完再提,"
"或让用户驳回后按意见重提" % (
(old_plan.get("id") or "")[:8], old_plan.get("version")))
if old_plan.get("status") != S_CONFIRMED:
# rejected/superseded/draft走 propose 重提(版本语义不变)
return False, ("最新计划状态为 %s(非已确认)——修订仅针对已生效计划,"
"请用 propose_flow_plan 重新提案" % old_plan.get("status"))
stage_map, stages = _load_stages(pid, flow_key)
if not stages:
return False, "产线「%s」未声明可裁剪流程,不支持修订" % pid
# 校验3trim 合法性(与 propose 同规则)
unknown = [k for k in trim_keys if k not in stage_map]
if unknown:
return False, "未知阶段 key%s。本产线阶段见 show_flow_template。" % "".join(unknown)
no_trim = [k for k in trim_keys if (stage_map[k].trim or "no") == "no"]
if no_trim:
detail = "".join("%s%s" % (stage_map[k].label or k,
stage_map[k].description or "流程必需")
for k in no_trim)
return False, "以下阶段不可裁剪,裁掉流程不成立:%s" % detail
trim_set = set(trim_keys)
broken = []
for s in stages:
if s.key in trim_set:
continue
for d in (s.deps or []):
if d in trim_set:
broken.append("%s 依赖被裁的 %s" % (s.label or s.key,
stage_map[d].label or d))
if broken:
return False, "裁剪导致依赖断裂:%s。请保留上游阶段或一并裁剪下游。" % "".join(broken)
# 校验4/5reentry 合法性
reentry_note = (reentry_detect_note or "").strip()
if reentry_stage:
if reentry_stage not in stage_map:
return False, ("未知重做阶段 key%s。可注入阶段见 show_flow_template。"
% reentry_stage)
if reentry_stage in trim_set:
return False, ("重做阶段「%s」同时被裁剪,矛盾——被裁阶段不会执行,"
"无法从它重做。请保留该阶段。"
% (stage_map[reentry_stage].label or reentry_stage))
try:
from pipeline_core import get_stage_reentry_handler
has_hook = get_stage_reentry_handler(pid) is not None
except Exception:
has_hook = False
if not has_hook:
return False, ("产线「%s」未注册阶段重做能力stage_reentry 钩子),"
"暂不支持定向重做;可联系平台扩展该产线。" % pid)
warns = [stage_map[k].warn_note for k in trim_keys
if (stage_map[k].trim or "") == "warn" and stage_map[k].warn_note]
from pipeline_core import flow_stage_to_dict
stages_snap = []
for s in stages:
d = flow_stage_to_dict(s)
d["enabled"] = s.key not in trim_set
stages_snap.append(d)
# 版本号 +1旧 pending/draft 置 superseded同 propose 语义)
vmax = await sor.sqlExe(
"SELECT MAX(version) AS v FROM pipeline_flow_plans WHERE project_id=${p}$",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
version = int(getattr(vmax[0], "v", 0) or 0) + 1
old_pend = await sor.sqlExe(
"SELECT id, confirm_task_id FROM pipeline_flow_plans WHERE project_id=${p}$ "
"AND status IN (${a}$, ${b}$)",
{"p": project_id, "a": S_PENDING, "b": S_DRAFT})
await sor.sqlExe("COMMIT", {})
for _op in (old_pend or []):
_op_ht = getattr(_op, "confirm_task_id", "") or ""
if _op_ht:
await sor.sqlExe(
"UPDATE pipeline_human_tasks SET status='done', qc_status='passed', "
"qc_comment='流程修订方案已更新版本,旧确认待办自动关闭', submitted_at=NOW() "
"WHERE id=${h}$ AND status='pending'", {"h": _op_ht})
await sor.sqlExe(
"UPDATE pipeline_flow_plans SET status=${ns}$, updated_at=NOW() "
"WHERE project_id=${p}$ AND status IN (${a}$, ${b}$)",
{"ns": S_SUPERSEDED, "p": project_id, "a": S_PENDING, "b": S_DRAFT})
await sor.sqlExe("COMMIT", {})
plan_id = getID()
await sor.C("pipeline_flow_plans", {
"id": plan_id, "project_id": project_id, "pipeline_id": pid,
"base_flow_key": flow_key,
"version": version, "status": S_PENDING,
"stages": json.dumps(stages_snap, ensure_ascii=False),
"user_requirements": user_requirements[:8000],
"propose_note": (propose_note or "")[:8000],
"reentry_stage": reentry_stage,
"reentry_detect_note": reentry_note[:8000],
"created_by": who or "agent.main_agent",
})
await sor.sqlExe("COMMIT", {})
# 确认待办正文:裁剪对照表 + 重做影响预告(破坏性动作必须在确认前可见)
desc = _render_confirm_md(pname, stages_snap, trim_keys, warns,
user_requirements, propose_note, version,
flow_key=flow_key, pipeline_id=pid)
if reentry_stage:
affected = await _affected_stage_keys(stages_snap, reentry_stage)
rdesc = await _render_reentry_desc(pid, project_id, reentry_stage,
affected, stages_snap, sor)
label = stage_map[reentry_stage].label or reentry_stage
reentry_md = ["", "### 🔁 定向重做(从「%s」阶段起)" % label, ""]
if reentry_note:
reentry_md += ["**注入点判定依据:**", "", reentry_note.strip(), ""]
reentry_md += ["**确认后系统将执行:**", "", rdesc.strip(), "",
"> ⚠️ 重做会重置上述阶段的已有产出(见影响说明),流程从该阶段自动重新推进。"]
desc = desc + "\n".join(reentry_md)
ok_ht, ht = await create_human_task(
project_id,
("流程修订确认:%sv%d%s" % (
pname, version, ",含定向重做" if reentry_stage else "")),
desc, task_type=T_FLOW_CONFIRM,
assignee_role="owner.superuser", created_by=who or "agent.main_agent")
if ok_ht:
await sor.sqlExe(
"UPDATE pipeline_flow_plans SET confirm_task_id=${h}$, updated_at=NOW() "
"WHERE id=${i}$", {"h": ht, "i": plan_id})
await sor.sqlExe("COMMIT", {})
await record_audit(project_id, "pipeline_flow_plans", plan_id, "revise_propose",
to_state=S_PENDING, who=who, agent_id=agent_id,
detail="version=%d reentry=%s trim=%s" % (
version, reentry_stage or "(无)",
",".join(trim_keys) or "(无裁剪)"),
sor=sor)
await sor.sqlExe("COMMIT", {})
logger.info("flow_plan revise: project=%s plan=%s v=%d reentry=%s trim=%s",
project_id, plan_id, version, reentry_stage, trim_keys)
kept = [s["label"] or s["key"] for s in stages_snap if s["enabled"]]
trimmed = [s["label"] or s["key"] for s in stages_snap if not s["enabled"]]
msg = ("已生成流程修订草案plan %sv%d)并发确认待办。\n保留阶段:%s\n裁剪阶段:%s"
% (plan_id[:8], version, "".join(kept) or "(无)",
"".join(trimmed) or "(无)"))
if reentry_stage:
msg += "\n定向重做注入点:%s%s" % (
stage_map[reentry_stage].label or reentry_stage, reentry_stage)
if warns:
msg += "\n⚠️ 风险提示(请在待办里向用户说明):\n" + "\n".join("- " + w for w in warns)
msg += ("\n修订确认前旧计划持续生效;用户需在「流程修订确认」待办里确认后修订才生效"
"重做重置在确认时执行agent 不能代替确认。")
return True, msg
async def detect_reentry_stage(project_id, user_comment="", pipeline_id=""):
"""LLM 判定定向重做注入点(用户只给意见时,语义判断归 LLM2026-09-17 用户裁定)。
返回 (stage_key, detect_note)
- 判定成功 (阶段key, 判定依据文本置信度+evidence进确认待办给用户核对)
- 产线未声明流程/LLM 失败/无法归类 ("", 错误说明)调用方不得默认选阶段硬跑
应把失败原因如实告知用户禁词匹配兜底禁静默降级detect_flow_template 同款纪律
错判由 flow_plan_confirm 确认门兜住用户在修订确认待办里看到注入点+判据
驳回附意见重提
"""
if not project_id:
return "", "缺少 project_id"
user_comment = (user_comment or "").strip()
if not user_comment:
return "", "缺少用户意见原文user_comment"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
pid, proj = await _resolve_pipeline(sor, project_id, pipeline_id)
if not pid:
return "", "项目未关联产线"
old_plan = await _latest_plan_rec(sor, project_id)
flow_key = (old_plan.get("base_flow_key") or "").strip() if old_plan else ""
stage_map, stages = _load_stages(pid, flow_key)
if not stages:
return "", "产线「%s」未声明流程阶段,无法判定注入点" % pid
# 进度摘要:产线钩子优先,无钩子给通用任务状态摘要
summary = ""
try:
from pipeline_core import get_progress_summary_handler
fn = get_progress_summary_handler(pid)
if fn:
summary = await fn(sor, project_id) or ""
except Exception as e:
logger.warning("progress_summary hook failed: %s", str(e)[:160])
if not summary:
summary = await _generic_progress_summary(sor, project_id)
stage_lines = []
for i, s in enumerate(stages, 1):
stage_lines.append("%d. key=%s 名称=%s\n 作用:%s" % (
i, s.key, s.label or s.key, (s.description or "(未写说明)")[:200]))
prompt = (
"你是产线流程调度器。用户要求对进行中的项目做修改/重做,请判定应从哪个流程阶段"
"注入重做(该阶段及其下游会按用户意见重新执行)。\n\n"
"【流程阶段(按执行顺序)】\n%s\n\n"
"【项目当前进度】\n%s\n\n"
"【用户意见原文】\n%s\n\n"
"判定原则:选择「能覆盖用户意见涉及的最小上游阶段」——从更早阶段重做代价更大,"
"从更晚阶段重做可能漏改;用户意见明确指向某阶段产物时直接选该阶段。\n"
"严格只输出一个 JSON 对象:\n"
"{\"stage_key\": \"候选之一的key\", \"confidence\": \"high|medium|low\",\n"
" \"evidence\": \"判定依据:用户意见哪句话+进度哪个事实支持从该阶段注入≤120字\"}\n"
"无法判定时 stage_key 输出空字符串并在 evidence 说明缺什么信息。"
% ("\n".join(stage_lines), (summary or "(无进度信息)")[:4000], user_comment[:3000])
)
try:
from pipeline_service.llm_bridge import llm_call
raw = await llm_call(prompt, purpose="utility",
org_id=(proj.get("org_id") or "") if proj else "",
project_id=project_id, timeout=45)
except Exception as e:
return "", "注入点判定 LLM 调用失败:%s" % str(e)[:200]
txt = (raw or "").strip()
if txt.startswith("```"):
parts = txt.split("\n", 1)
txt = parts[1] if len(parts) > 1 else txt
if "```" in txt:
txt = txt.rsplit("```", 1)[0]
txt = txt.strip()
try:
if not txt.startswith("{") and "{" in txt:
txt = txt[txt.find("{"):txt.rfind("}") + 1]
data = json.loads(txt)
except (json.JSONDecodeError, ValueError):
return "", "注入点判定结果无法解析:%s" % (raw or "")[:200]
sk = str(data.get("stage_key") or "").strip()
conf = str(data.get("confidence") or "").strip()
ev = str(data.get("evidence") or "").strip()
valid = {s.key for s in stages}
if sk and sk not in valid:
return "", "判定返回未知阶段 key%s(候选:%s" % (sk, "".join(sorted(valid)))
if not sk:
return "", "无法判定注入点:%s" % (ev or "LLM 未说明原因)")
label = stage_map[sk].label or sk
note = "判定注入点:「%s」(%s,置信度 %s)。\n依据:%s" % (
label, sk, conf or "?", ev or "(未提供依据)")
if conf == "low":
note += "\n⚠️ 判定置信度低,请在确认待办里重点核对——不对就驳回并说明应从哪个阶段重做。"
return sk, note
async def _generic_progress_summary(sor, project_id):
"""通用进度摘要(产线无 progress_summary 钩子时的兜底):任务状态 + 阶段产出计数。"""
try:
recs = await sor.sqlExe(
"SELECT role, state, COUNT(*) AS c FROM pipeline_tasks WHERE tenant_id=${p}$ "
"AND pipeline_id='role_task' GROUP BY role, state ORDER BY role",
{"p": project_id})
await sor.sqlExe("COMMIT", {})
lines = ["角色任务状态分布:"]
for r in (recs or []):
lines.append("- %s %s: %s" % (getattr(r, "role", "?"),
getattr(r, "state", "?"), getattr(r, "c", 0)))
ht = await sor.sqlExe(
"SELECT task_type, title, status FROM pipeline_human_tasks WHERE project_id=${p}$ "
"AND status='pending' ORDER BY created_at DESC LIMIT 10", {"p": project_id})
await sor.sqlExe("COMMIT", {})
if ht:
lines.append("在办人类待办:")
for r in ht:
lines.append("- [%s] %s" % (getattr(r, "task_type", "?"),
str(getattr(r, "title", ""))[:80]))
proj = await sor.sqlExe(
"SELECT status FROM sd_projects WHERE id=${p}$", {"p": project_id})
await sor.sqlExe("COMMIT", {})
if proj:
lines.append("项目状态:%s" % getattr(proj[0], "status", "?"))
return "\n".join(lines)
except Exception as e:
logger.warning("generic progress summary failed: %s", str(e)[:160])
return ""
async def _run_stage_reentry(sor, plan, operator_id):
"""确认修订版时执行定向重做(产线钩子)。返回 (ok, msg)。
钩子失败不回滚计划确认计划换版是用户裁决已生效诚实上报 + 抛人工待办
人工可按影响说明手工重置或让 agent 修复不静默吞错
"""
project_id = plan.get("project_id", "")
pid = plan.get("pipeline_id", "")
stage_key = (plan.get("reentry_stage") or "").strip()
if not stage_key:
return True, ""
try:
stages_snap = json.loads(plan.get("stages") or "[]")
except (json.JSONDecodeError, ValueError):
stages_snap = []
affected = await _affected_stage_keys(stages_snap, stage_key)
try:
from pipeline_core import get_stage_reentry_handler
fn = get_stage_reentry_handler(pid)
except Exception:
fn = None
if not fn:
# 提案时已校验过钩子存在;确认时消失 = 部署窗口异常,诚实上报
msg = "产线「%s」阶段重做钩子不可用,未执行重置——请人工处理" % pid
await create_human_task(
project_id, "流程修订已确认但阶段重做未执行:%s" % stage_key,
msg + "\n\n修订计划已生效v%s),但定向重做的产出重置未执行。\n"
"请人工按修订确认待办里的影响说明处置,或修复钩子后让平台重试。"
% plan.get("version"),
task_type="general", assignee_role="owner.superuser")
await sor.sqlExe("COMMIT", {})
return False, msg
try:
ok, msg = await fn(sor, project_id, stage_key, sorted(affected),
plan.get("user_requirements") or "", operator_id)
await sor.sqlExe("COMMIT", {})
except Exception as e:
logger.exception("stage_reentry hook crashed: project=%s stage=%s",
project_id, stage_key)
msg = "阶段重做执行异常:%s" % str(e)[:300]
ok = False
if not ok:
await create_human_task(
project_id, "流程修订已确认但阶段重做执行失败:%s" % stage_key,
(msg or "钩子返回失败") + "\n\n修订计划已生效v%s),但阶段重置未完成,"
"流程可能停在旧产出上。请人工处置后项目自动恢复推进。"
% plan.get("version"),
task_type="general", assignee_role="owner.superuser")
await sor.sqlExe("COMMIT", {})
await record_audit(project_id, "pipeline_flow_plans", plan.get("id", ""),
"stage_reentry", to_state=stage_key, who=operator_id,
detail=("ok" if ok else "failed") + ": " + (msg or "")[:400],
sor=sor)
await sor.sqlExe("COMMIT", {})
return ok, msg
# ══════════════════════ 会话工具FLOW_PLAN_TOOLS / HANDLERSshared_ability 聚合) ══════════════════════
try:
@ -761,6 +1201,29 @@ if ToolDefinition is not None:
"propose_note": "你对裁剪方案的说明/风险提示(可选)",
"flow_key": "多流程产线指定流程模板key(可选,来自show_flow_template;单流程产线留空)"},
required=["user_requirements"], category="project"),
ToolDefinition(
name="revise_flow_plan",
description=("对已确认生效的项目流程提出修订(用户有权随时调整流程):可改裁剪(trim_keys)"
"和/或要求从任一阶段按用户意见定向重做(reentry_stage)。生成修订草案(v+1)并发"
"「流程修订确认」待办——确认前旧计划持续生效,用户确认时才换版并执行阶段重置"
"(重置影响会在待办里预告)。用户只给意见没指明阶段时:先调 detect_reentry_stage "
"判定注入点LLM 语义判定),把返回的 stage_key 和 detect_note 原样传入本工具"
"note 进待办给用户核对,判定失败如实转告用户,禁止自己猜阶段)。"
"修订合法性由系统硬校验;你没有确认权,禁止声称已确认。"),
parameters={"user_requirements": "用户修订要求原文(必填,写进确认待办)",
"reentry_stage": "定向重做注入阶段key(可选;空=仅调整裁剪不重做来自show_flow_template或detect_reentry_stage)",
"trim_keys": "要裁剪的阶段key数组(可选;修订后的完整裁剪集,沿用现状传空)",
"propose_note": "你对修订方案的说明/风险提示(可选)",
"reentry_detect_note": "注入点判定依据(detect_reentry_stage返回的note原样传入进待办给用户核对)"},
required=["user_requirements"], category="project"),
ToolDefinition(
name="detect_reentry_stage",
description=("用户只给修改意见、没指明从哪个阶段重做时调用LLM按意见+流程阶段声明+项目进度"
"判定定向重做注入点语义判断归LLM禁词匹配。返回stage_key+判定依据note——"
"原样传给revise_flow_plannote进确认待办由用户核对错判由确认门兜住"
"判定失败(空key)时如实把失败原因转告用户,禁止自己猜阶段硬跑。"),
parameters={"user_comment": "用户意见原文(必填)"},
required=["user_comment"], category="project"),
ToolDefinition(
name="get_flow_plan",
description="查看当前项目流程裁剪计划的状态(草案/待确认/已确认/已驳回+驳回意见)",
@ -820,6 +1283,10 @@ async def _h_get_flow_plan(sor, params, ctx):
lines = ["流程裁剪计划 v%s%s" % (plan.get("version"), st_label.get(plan.get("status"), plan.get("status"))),
"保留:%s" % ("".join(kept) or "(无)"),
"裁剪:%s" % ("".join(trimmed) or "(无)")]
if (plan.get("reentry_stage") or "").strip():
lines.append("定向重做注入点:%s" % plan.get("reentry_stage"))
if plan.get("reentry_detect_note"):
lines.append("判定依据:%s" % str(plan.get("reentry_detect_note"))[:300])
if plan.get("status") == S_REJECTED and plan.get("reject_comment"):
lines.append("驳回意见:%s" % plan.get("reject_comment"))
lines.append("→ 请按驳回意见修订后用 propose_flow_plan 重新提案version 自动 +1")
@ -828,8 +1295,43 @@ async def _h_get_flow_plan(sor, params, ctx):
return "\n".join(lines)
async def _h_revise_flow_plan(sor, params, ctx):
project_id = ctx.get("project_id") or ""
if not project_id:
return "ERROR: 请先切换到项目(用 switch_project 或在项目列表选择)"
ok, msg = await revise_flow_plan(
project_id,
user_requirements=params.get("user_requirements", "") or "",
reentry_stage=params.get("reentry_stage", "") or "",
trim_keys=params.get("trim_keys"),
propose_note=params.get("propose_note", "") or "",
reentry_detect_note=params.get("reentry_detect_note", "") or "",
pipeline_id=ctx.get("pipeline_id", "") or "",
who="agent.main_agent",
agent_id=ctx.get("user_id", "") or "")
return ("OK: " if ok else "ERROR: ") + msg
async def _h_detect_reentry_stage(sor, params, ctx):
project_id = ctx.get("project_id") or ""
if not project_id:
return "ERROR: 请先切换到项目"
comment = (params.get("user_comment") or "").strip()
if not comment:
return "ERROR: 缺少用户意见原文user_comment"
stage_key, note = await detect_reentry_stage(
project_id, comment, pipeline_id=ctx.get("pipeline_id", "") or "")
if not stage_key:
return "ERROR: 注入点判定失败——%s\n请把失败原因如实转告用户(禁止自己猜阶段)。" % note
return ("OK: stage_key=%s\n%s\n\n下一步:调 revise_flow_plan(reentry_stage=%s, "
"reentry_detect_note=上面的判定依据原文, user_requirements=用户意见原文)。"
% (stage_key, note, stage_key))
FLOW_PLAN_HANDLERS = {
"show_flow_template": _h_show_flow_template,
"propose_flow_plan": _h_propose_flow_plan,
"revise_flow_plan": _h_revise_flow_plan,
"detect_reentry_stage": _h_detect_reentry_stage,
"get_flow_plan": _h_get_flow_plan,
}