feat(flow): 批2多流程模板机制+三机制修复——pipeline_flow_plans加base_flow_key列;flow_plan_capability:detect_flow_template(LLM判流,置信度+判据,失败不静默降级发人工)/propose_flow_plan支持flow_key+确认待办渲染判流结论/get_plan_flow_key供流转分支/_latest_plan_rec复用;agent_loop:_skip_trimmed_roles按base_flow_key解析阶段+human_task_qc检查对象不存在改cancel+fault冒泡(不再qc_reject计轮死循环,首医实测)+PM approve答结并入qc_reject(幻影角标,pbls实测);project_capability:pause_project不再删pending待办(暂停=可逆停摆,删待办致resume后QC死循环)
This commit is contained in:
parent
d204ff4a9f
commit
5fe5c238ec
@ -31,6 +31,14 @@
|
|||||||
"length": 64,
|
"length": 64,
|
||||||
"nullable": "no"
|
"nullable": "no"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "base_flow_key",
|
||||||
|
"title": "基准流程模板key(多流程机制;空=产线单流程flow_stages)",
|
||||||
|
"type": "str",
|
||||||
|
"length": 64,
|
||||||
|
"nullable": "yes",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "version",
|
"name": "version",
|
||||||
"title": "版本(驳回重提+1)",
|
"title": "版本(驳回重提+1)",
|
||||||
|
|||||||
@ -1486,18 +1486,25 @@ async def _skip_trimmed_roles(pipeline_id, project_id, next_role):
|
|||||||
"""沿 next_role 链跳过被裁阶段的角色,返回第一个保留角色(或 "")。
|
"""沿 next_role 链跳过被裁阶段的角色,返回第一个保留角色(或 "")。
|
||||||
|
|
||||||
角色 → 阶段归属:能力包 flow_stages 里 roles 含该角色即属该阶段。
|
角色 → 阶段归属:能力包 flow_stages 里 roles 含该角色即属该阶段。
|
||||||
|
多流程机制(2026-09-14):已确认计划带 base_flow_key 时按该流程模板的阶段解析。
|
||||||
阶段不在任何 flow_stage 声明里的角色 = 不受裁剪影响,照常保留。
|
阶段不在任何 flow_stage 声明里的角色 = 不受裁剪影响,照常保留。
|
||||||
防环:最多跳 20 次(角色链本身有限,异常声明不至于死循环)。
|
防环:最多跳 20 次(角色链本身有限,异常声明不至于死循环)。
|
||||||
"""
|
"""
|
||||||
from pipeline_core import get_role_spec, get_flow_stages
|
from pipeline_core import get_role_spec, get_flow_stages
|
||||||
try:
|
try:
|
||||||
from .flow_plan_capability import plan_stage_enabled
|
from .flow_plan_capability import plan_stage_enabled, get_active_plan
|
||||||
enabled = await plan_stage_enabled(project_id)
|
enabled = await plan_stage_enabled(project_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
return next_role
|
return next_role
|
||||||
if enabled is None:
|
if enabled is None:
|
||||||
return next_role # 无确认计划 = 标准全流程
|
return next_role # 无确认计划 = 标准全流程
|
||||||
stages = get_flow_stages(pipeline_id) or []
|
flow_key = ""
|
||||||
|
try:
|
||||||
|
plan = await get_active_plan(project_id)
|
||||||
|
flow_key = (plan.get("base_flow_key") or "").strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
stages = get_flow_stages(pipeline_id, flow_key=flow_key) or []
|
||||||
role_stage = {}
|
role_stage = {}
|
||||||
for s in stages:
|
for s in stages:
|
||||||
for r in (s.roles or []):
|
for r in (s.roles or []):
|
||||||
@ -3322,11 +3329,14 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
|||||||
await sor.sqlExe("UPDATE pipeline_deliverables SET review_status='approved', review_comment=${cm}$ WHERE task_id=${tid}$", {"cm": comment, "tid": task_id})
|
await sor.sqlExe("UPDATE pipeline_deliverables SET review_status='approved', review_comment=${cm}$ WHERE task_id=${tid}$", {"cm": comment, "tid": task_id})
|
||||||
# 审核通过:该任务 review_reject 类的 pending 退回意见已由角色 agent 响应 → 解决,避免永久堆积。
|
# 审核通过:该任务 review_reject 类的 pending 退回意见已由角色 agent 响应 → 解决,避免永久堆积。
|
||||||
# 只答结 review_reject(归属角色 agent 的),不碰 need_info(归属主 agent)等其它 pending。
|
# 只答结 review_reject(归属角色 agent 的),不碰 need_info(归属主 agent)等其它 pending。
|
||||||
|
# 2026-09-15 补 qc_reject:QC 退回意见与 review_reject 语义对称——PM 审核通过
|
||||||
|
# 即代表 QC 退回意见已被响应(QC 已通过才会进入 PM 审核),不并入则 qc_reject
|
||||||
|
# 问题永久 pending 挂幻影角标(首处理方角色 agent 无后续任务去答它,pbls 实测)。
|
||||||
await sor.sqlExe(
|
await sor.sqlExe(
|
||||||
"UPDATE pipeline_agent_questions SET status='answered', answer=${a}$, "
|
"UPDATE pipeline_agent_questions SET status='answered', answer=${a}$, "
|
||||||
"answer_source='role_agent', answered_by=${role}$ "
|
"answer_source='role_agent', answered_by=${role}$ "
|
||||||
"WHERE task_id=${tid}$ AND status='pending' "
|
"WHERE task_id=${tid}$ AND status='pending' "
|
||||||
"AND (problem_type='review_reject' "
|
"AND (problem_type IN ('review_reject','qc_reject') "
|
||||||
" OR ((problem_type IS NULL OR problem_type='') AND from_role IN ('pm','cockpit')))",
|
" OR ((problem_type IS NULL OR problem_type='') AND from_role IN ('pm','cockpit')))",
|
||||||
{"a": "角色已响应,PM审核通过", "role": task_role or "role_agent", "tid": task_id})
|
{"a": "角色已响应,PM审核通过", "role": task_role or "role_agent", "tid": task_id})
|
||||||
from .task_capability import approve_task
|
from .task_capability import approve_task
|
||||||
@ -3512,9 +3522,24 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
|
|||||||
{"hid": human_task_id})
|
{"hid": human_task_id})
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
if not ht_recs:
|
if not ht_recs:
|
||||||
from .task_capability import qc_reject_task
|
# 2026-09-15 修复:检查对象不存在是「对象已被删除/归档」的系统性异常,
|
||||||
await qc_reject_task(task_id, project_id, who="agent.qc", agent_id=agent_id, comment="人类任务不存在")
|
# 不是 QC 可退回重来的质量问题——qc_reject 会计轮直到上限转 failed,
|
||||||
return {"status": "rejected", "task_id": task_id, "reason": "人类任务不存在"}
|
# 每轮都是对着不存在的对象空转(首医项目死循环实测)。
|
||||||
|
# 正解:取消本 QC 任务 + fault 冒泡一次性交人工,不进重试循环。
|
||||||
|
from .task_capability import cancel_task
|
||||||
|
from .communication import raise_problem
|
||||||
|
await cancel_task(task_id, project_id, who="agent.qc", agent_id=agent_id,
|
||||||
|
comment="人类任务不存在(对象已被删除),QC 无法检查")
|
||||||
|
await raise_problem(
|
||||||
|
"fault_report",
|
||||||
|
f"人类任务QC「{title}」的检查对象(人类任务 {human_task_id})已不存在,"
|
||||||
|
"无法继续 QC。请人工确认该事项是否已线下处理完毕;已处理则答结本问题即可。",
|
||||||
|
"agent.qc", tenant_id=project_id, task_id=task_id,
|
||||||
|
first_handler_role="agent.main_agent",
|
||||||
|
suspend_task=False, # 任务已 cancelled(终态),不得再置回 waiting 复活
|
||||||
|
context={"fault": True, "reason": "human_task_missing",
|
||||||
|
"human_task_id": human_task_id})
|
||||||
|
return {"status": "fault", "task_id": task_id, "reason": "人类任务不存在(已取消QC任务并冒泡人工)"}
|
||||||
ht_title = getattr(ht_recs[0], 'title', '') or ''
|
ht_title = getattr(ht_recs[0], 'title', '') or ''
|
||||||
ht_desc = getattr(ht_recs[0], 'description', '') or ''
|
ht_desc = getattr(ht_recs[0], 'description', '') or ''
|
||||||
ht_result = getattr(ht_recs[0], 'result_data', '') or ''
|
ht_result = getattr(ht_recs[0], 'result_data', '') or ''
|
||||||
|
|||||||
@ -85,10 +85,13 @@ def _parse_trim_keys(raw):
|
|||||||
return [x.strip() for x in s.replace(",", ",").split(",") if x.strip()]
|
return [x.strip() for x in s.replace(",", ",").split(",") if x.strip()]
|
||||||
|
|
||||||
|
|
||||||
def _load_stages(pipeline_id):
|
def _load_stages(pipeline_id, flow_key=""):
|
||||||
"""从能力包取产线声明的阶段,返回 (stage_map, ordered_list)。"""
|
"""从能力包取产线声明的阶段,返回 (stage_map, ordered_list)。
|
||||||
|
|
||||||
|
flow_key 非空时取多流程模板(FlowTemplate)对应阶段;空 = 单流程 flow_stages。
|
||||||
|
"""
|
||||||
from pipeline_core import get_flow_stages
|
from pipeline_core import get_flow_stages
|
||||||
stages = get_flow_stages(pipeline_id) or []
|
stages = get_flow_stages(pipeline_id, flow_key=flow_key or "") or []
|
||||||
return {s.key: s for s in stages}, list(stages)
|
return {s.key: s for s in stages}, list(stages)
|
||||||
|
|
||||||
|
|
||||||
@ -107,16 +110,99 @@ async def _resolve_pipeline(sor, project_id, pipeline_id=""):
|
|||||||
|
|
||||||
# ══════════════════════ 展示预制流程 ══════════════════════
|
# ══════════════════════ 展示预制流程 ══════════════════════
|
||||||
|
|
||||||
async def get_flow_template(project_id="", pipeline_id=""):
|
async def get_flow_template(project_id="", pipeline_id="", flow_key=""):
|
||||||
"""返回 (pid, [stage_dict,...]):产线预制流程阶段(含可裁性),供 show_flow_template 渲染。"""
|
"""返回 (pid, [stage_dict,...]):产线预制流程阶段(含可裁性),供 show_flow_template 渲染。
|
||||||
|
|
||||||
|
flow_key 空时:项目已有确认/待确认计划则取计划的 base_flow_key(展示实际将走的流程);
|
||||||
|
多流程产线无计划时展示全部模板概览由调用方处理。
|
||||||
|
"""
|
||||||
db, dbname = _get_db()
|
db, dbname = _get_db()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
pid, _proj = await _resolve_pipeline(sor, project_id, pipeline_id)
|
pid, _proj = await _resolve_pipeline(sor, project_id, pipeline_id)
|
||||||
|
fk = (flow_key or "").strip()
|
||||||
|
if not fk and project_id:
|
||||||
|
plan = await _latest_plan_rec(sor, project_id)
|
||||||
|
fk = (plan.get("base_flow_key") or "").strip()
|
||||||
from pipeline_core import flow_stage_to_dict
|
from pipeline_core import flow_stage_to_dict
|
||||||
_map, stages = _load_stages(pid)
|
_map, stages = _load_stages(pid, fk)
|
||||||
return pid, [flow_stage_to_dict(s) for s in stages]
|
return pid, [flow_stage_to_dict(s) for s in stages]
|
||||||
|
|
||||||
|
|
||||||
|
async def list_flow_templates(pipeline_id=""):
|
||||||
|
"""列出产线的多流程模板 [(key, name, description, n_stages)];单流程产线返回 []。"""
|
||||||
|
from pipeline_core import get_flow_templates
|
||||||
|
return [(t.key, t.name or t.key, t.description or "", len(t.stages or []))
|
||||||
|
for t in get_flow_templates(pipeline_id)]
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════ 文件自动判流(多流程机制,2026-09-14 批2) ══════════════════════
|
||||||
|
|
||||||
|
async def detect_flow_template(pipeline_id, file_name="", file_text="",
|
||||||
|
project_id="", org_id=""):
|
||||||
|
"""LLM 按各模板 detect_criteria 给导入文件判流(语义判断归 LLM,2026-09-11 用户铁律)。
|
||||||
|
|
||||||
|
返回 (flow_key, detect_note):
|
||||||
|
- 单流程产线(模板 <2)→ ("", ""),调用方走现状(不判流不确认);
|
||||||
|
- 判流成功 → (模板key, 识别结论文本:置信度+判据摘录,进确认待办给用户核对);
|
||||||
|
- LLM 失败/无法解析 → ("", 错误说明):调用方**不得**默认选流程硬跑,
|
||||||
|
应发人工介入待办(判流是流程选择的语义判断,禁词匹配兜底、禁静默降级)。
|
||||||
|
错判由 flow_plan_confirm 确认门兜住:用户在待办里看到识别结论,驳回附意见重提。
|
||||||
|
"""
|
||||||
|
from pipeline_core import get_flow_templates
|
||||||
|
tpls = get_flow_templates(pipeline_id)
|
||||||
|
if len(tpls) < 2:
|
||||||
|
return "", ""
|
||||||
|
snippet = (file_text or "").strip()[:6000]
|
||||||
|
if not snippet and not file_name:
|
||||||
|
return "", "判流失败:文件内容为空"
|
||||||
|
tpl_lines = []
|
||||||
|
for t in tpls:
|
||||||
|
tpl_lines.append("- key=%s 名称=%s\n 适用判据:%s" % (
|
||||||
|
t.key, t.name or t.key, (t.detect_criteria or t.description or "(未写判据)")))
|
||||||
|
prompt = (
|
||||||
|
"你是产线流程路由器。根据输入文件的特征,判断它应该走哪条预制流程。\n\n"
|
||||||
|
"【候选流程】\n%s\n\n"
|
||||||
|
"【文件名】%s\n\n【文件正文(截断)】\n%s\n\n"
|
||||||
|
"严格只输出一个 JSON 对象:\n"
|
||||||
|
"{\"flow_key\": \"候选之一的key\", \"confidence\": \"high|medium|low\",\n"
|
||||||
|
" \"evidence\": \"判据摘录:文件里哪些特征(原文引用≤3处)支持这个判断\"}\n"
|
||||||
|
"无法归入任何候选流程时 flow_key 输出空字符串并在 evidence 说明原因。"
|
||||||
|
% ("\n".join(tpl_lines), file_name or "(无)", snippet or "(无正文)")
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
from pipeline_service.llm_bridge import llm_call
|
||||||
|
raw = await llm_call(prompt, purpose="utility", org_id=org_id or "",
|
||||||
|
project_id=project_id or "", 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]
|
||||||
|
fk = str(data.get("flow_key") or "").strip()
|
||||||
|
conf = str(data.get("confidence") or "").strip()
|
||||||
|
ev = str(data.get("evidence") or "").strip()
|
||||||
|
valid = {t.key for t in tpls}
|
||||||
|
if fk and fk not in valid:
|
||||||
|
return "", "判流返回未知流程 key:%s(候选:%s)" % (fk, "、".join(sorted(valid)))
|
||||||
|
if not fk:
|
||||||
|
return "", "判流无法归类:%s" % (ev or "(LLM 未说明原因)")
|
||||||
|
note = "识别为「%s」流程(置信度 %s)。\n判据:%s" % (
|
||||||
|
fk, conf or "?", ev or "(未提供判据摘录)")
|
||||||
|
if conf == "low":
|
||||||
|
note += "\n⚠️ 识别置信度低,请重点核对——不对就驳回并说明应走哪条流程。"
|
||||||
|
return fk, note
|
||||||
|
|
||||||
|
|
||||||
def render_template_md(stages):
|
def render_template_md(stages):
|
||||||
"""把阶段列表渲染成 markdown 表(展示给用户/待办)。"""
|
"""把阶段列表渲染成 markdown 表(展示给用户/待办)。"""
|
||||||
if not stages:
|
if not stages:
|
||||||
@ -134,11 +220,17 @@ def render_template_md(stages):
|
|||||||
# ══════════════════════ 提案(裁剪草案 + 确认待办) ══════════════════════
|
# ══════════════════════ 提案(裁剪草案 + 确认待办) ══════════════════════
|
||||||
|
|
||||||
async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
|
async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
|
||||||
propose_note="", pipeline_id="", who=None, agent_id=None):
|
propose_note="", pipeline_id="", flow_key="",
|
||||||
|
detect_note="", who=None, agent_id=None):
|
||||||
"""按用户要求生成裁剪草案,落库 pending_confirm 并发确认待办。
|
"""按用户要求生成裁剪草案,落库 pending_confirm 并发确认待办。
|
||||||
|
|
||||||
|
flow_key(多流程机制,2026-09-14 批2):指定基准流程模板 key。
|
||||||
|
- 空 = 单流程产线(用 flow_stages 声明),或多流程产线沿用已有计划的模板;
|
||||||
|
- 非空 = 按该模板的阶段链出草案(base_flow_key 落库,流转机制按它走分支)。
|
||||||
|
detect_note:判流依据说明(LLM 自动判流时写识别结论+判据摘录,进确认待办给用户核对)。
|
||||||
|
|
||||||
代码硬校验(任一不过即拒绝,返回可行动原因,绝不静默降级):
|
代码硬校验(任一不过即拒绝,返回可行动原因,绝不静默降级):
|
||||||
1. 项目/产线存在,且产线声明了 flow_stages;
|
1. 项目/产线存在,且产线声明了 flow_stages(或 flow_key 对应模板);
|
||||||
2. 裁剪时机:项目流程已开跑(有角色任务进入 started 态)→ 拒绝;
|
2. 裁剪时机:项目流程已开跑(有角色任务进入 started 态)→ 拒绝;
|
||||||
3. 已有 confirmed 计划 → 拒绝(启动时裁剪一次,确认后不可改);
|
3. 已有 confirmed 计划 → 拒绝(启动时裁剪一次,确认后不可改);
|
||||||
4. trim_keys 都是已知阶段;
|
4. trim_keys 都是已知阶段;
|
||||||
@ -149,6 +241,7 @@ async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
|
|||||||
if not project_id:
|
if not project_id:
|
||||||
return False, "缺少 project_id(请先切换到项目)"
|
return False, "缺少 project_id(请先切换到项目)"
|
||||||
trim_keys = _parse_trim_keys(trim_keys)
|
trim_keys = _parse_trim_keys(trim_keys)
|
||||||
|
flow_key = (flow_key or "").strip()
|
||||||
db, dbname = _get_db()
|
db, dbname = _get_db()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
pid, proj = await _resolve_pipeline(sor, project_id, pipeline_id)
|
pid, proj = await _resolve_pipeline(sor, project_id, pipeline_id)
|
||||||
@ -156,7 +249,18 @@ async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
|
|||||||
return False, "项目不存在: %s" % project_id
|
return False, "项目不存在: %s" % project_id
|
||||||
if not pid:
|
if not pid:
|
||||||
return False, "项目未关联产线,无法裁剪流程"
|
return False, "项目未关联产线,无法裁剪流程"
|
||||||
stage_map, stages = _load_stages(pid)
|
# flow_key 校验:多流程产线必须是已声明的模板 key
|
||||||
|
if flow_key:
|
||||||
|
from pipeline_core import get_flow_template as _get_ftpl
|
||||||
|
if not _get_ftpl(pid, flow_key):
|
||||||
|
tpls = await list_flow_templates(pid)
|
||||||
|
avail = "、".join(k for k, _n, _d, _c in tpls) or "(该产线未声明多流程模板)"
|
||||||
|
return False, ("未知流程模板 key:%s。可用流程:%s" % (flow_key, avail))
|
||||||
|
else:
|
||||||
|
# 未指定:已有计划则沿用其 base_flow_key(驳回重提场景)
|
||||||
|
old_plan = await _latest_plan_rec(sor, project_id)
|
||||||
|
flow_key = (old_plan.get("base_flow_key") or "").strip()
|
||||||
|
stage_map, stages = _load_stages(pid, flow_key)
|
||||||
if not stages:
|
if not stages:
|
||||||
return False, ("产线「%s」未声明可裁剪流程(flow_stages 为空),"
|
return False, ("产线「%s」未声明可裁剪流程(flow_stages 为空),"
|
||||||
"不支持流程裁剪" % pid)
|
"不支持流程裁剪" % pid)
|
||||||
@ -250,6 +354,7 @@ async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
|
|||||||
plan_id = getID()
|
plan_id = getID()
|
||||||
await sor.C("pipeline_flow_plans", {
|
await sor.C("pipeline_flow_plans", {
|
||||||
"id": plan_id, "project_id": project_id, "pipeline_id": pid,
|
"id": plan_id, "project_id": project_id, "pipeline_id": pid,
|
||||||
|
"base_flow_key": flow_key,
|
||||||
"version": version, "status": S_PENDING,
|
"version": version, "status": S_PENDING,
|
||||||
"stages": json.dumps(stages_snap, ensure_ascii=False),
|
"stages": json.dumps(stages_snap, ensure_ascii=False),
|
||||||
"user_requirements": (user_requirements or "")[:8000],
|
"user_requirements": (user_requirements or "")[:8000],
|
||||||
@ -260,7 +365,9 @@ async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
|
|||||||
|
|
||||||
# 确认待办(平台待办中枢;专用确认端点 flow_plan_confirm.dspy 激活)
|
# 确认待办(平台待办中枢;专用确认端点 flow_plan_confirm.dspy 激活)
|
||||||
desc = _render_confirm_md(pname, stages_snap, trim_keys, warns,
|
desc = _render_confirm_md(pname, stages_snap, trim_keys, warns,
|
||||||
user_requirements, propose_note, version)
|
user_requirements, propose_note, version,
|
||||||
|
flow_key=flow_key, pipeline_id=pid,
|
||||||
|
detect_note=detect_note)
|
||||||
ok_ht, ht = await create_human_task(
|
ok_ht, ht = await create_human_task(
|
||||||
project_id,
|
project_id,
|
||||||
"流程裁剪确认:%s(v%d)" % (pname, version),
|
"流程裁剪确认:%s(v%d)" % (pname, version),
|
||||||
@ -273,25 +380,42 @@ async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
|
|||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
await record_audit(project_id, "pipeline_flow_plans", plan_id, "propose",
|
await record_audit(project_id, "pipeline_flow_plans", plan_id, "propose",
|
||||||
to_state=S_PENDING, who=who, agent_id=agent_id,
|
to_state=S_PENDING, who=who, agent_id=agent_id,
|
||||||
detail="version=%d trim=%s" % (version, ",".join(trim_keys) or "(无裁剪)"),
|
detail="version=%d flow_key=%s trim=%s" % (
|
||||||
|
version, flow_key or "(单流程)",
|
||||||
|
",".join(trim_keys) or "(无裁剪)"),
|
||||||
sor=sor)
|
sor=sor)
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
logger.info("flow_plan propose: project=%s plan=%s v=%d trim=%s",
|
logger.info("flow_plan propose: project=%s plan=%s v=%d flow_key=%s trim=%s",
|
||||||
project_id, plan_id, version, trim_keys)
|
project_id, plan_id, version, flow_key, trim_keys)
|
||||||
kept = [s["label"] or s["key"] for s in stages_snap if s["enabled"]]
|
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"]]
|
trimmed = [s["label"] or s["key"] for s in stages_snap if not s["enabled"]]
|
||||||
msg = ("已生成流程裁剪草案(plan %s,v%d)并发确认待办。\n保留阶段:%s\n裁剪阶段:%s"
|
msg = ("已生成流程裁剪草案(plan %s,v%d)并发确认待办。\n保留阶段:%s\n裁剪阶段:%s"
|
||||||
% (plan_id[:8], version, "、".join(kept) or "(无)",
|
% (plan_id[:8], version, "、".join(kept) or "(无)",
|
||||||
"、".join(trimmed) or "(无)"))
|
"、".join(trimmed) or "(无)"))
|
||||||
|
if flow_key:
|
||||||
|
msg = "基准流程:%s\n%s" % (flow_key, msg)
|
||||||
if warns:
|
if warns:
|
||||||
msg += "\n⚠️ 风险提示(请在待办里向用户说明):\n" + "\n".join("- " + w for w in warns)
|
msg += "\n⚠️ 风险提示(请在待办里向用户说明):\n" + "\n".join("- " + w for w in warns)
|
||||||
msg += "\n用户需在「流程裁剪确认」待办里点击确认后流程才生效;agent 不能代替确认。"
|
msg += "\n用户需在「流程裁剪确认」待办里点击确认后流程才生效;agent 不能代替确认。"
|
||||||
return True, msg
|
return True, msg
|
||||||
|
|
||||||
|
|
||||||
def _render_confirm_md(pname, stages_snap, trim_keys, warns, user_req, note, version):
|
def _render_confirm_md(pname, stages_snap, trim_keys, warns, user_req, note, version,
|
||||||
|
flow_key="", pipeline_id="", detect_note=""):
|
||||||
"""确认待办正文(markdown,todo_detail 直接渲染)。"""
|
"""确认待办正文(markdown,todo_detail 直接渲染)。"""
|
||||||
lines = ["## 项目「%s」流程裁剪方案(v%d)" % (pname, version), ""]
|
lines = ["## 项目「%s」流程裁剪方案(v%d)" % (pname, version), ""]
|
||||||
|
if flow_key:
|
||||||
|
# 多流程产线:展示判定的流程模板名 + 判流依据(用户核对识别结论)
|
||||||
|
try:
|
||||||
|
from pipeline_core import get_flow_template as _gft
|
||||||
|
tpl = _gft(pipeline_id, flow_key) if pipeline_id else None
|
||||||
|
tname = (tpl.name if tpl else "") or flow_key
|
||||||
|
except Exception:
|
||||||
|
tname = flow_key
|
||||||
|
lines += ["**将走的流程:** %s(`%s`)" % (tname, flow_key), ""]
|
||||||
|
if detect_note:
|
||||||
|
lines += ["### 文件识别结论(自动判流)", "", detect_note.strip(), "",
|
||||||
|
"> 识别不对?点「驳回」并说明应走哪条流程,助手会修订重提。", ""]
|
||||||
if user_req:
|
if user_req:
|
||||||
lines += ["**你的裁剪要求:**", "", user_req.strip(), ""]
|
lines += ["**你的裁剪要求:**", "", user_req.strip(), ""]
|
||||||
if note:
|
if note:
|
||||||
@ -508,11 +632,24 @@ async def get_latest_plan(project_id):
|
|||||||
return {}
|
return {}
|
||||||
db, dbname = _get_db()
|
db, dbname = _get_db()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
recs = await sor.sqlExe(
|
return await _latest_plan_rec(sor, project_id)
|
||||||
"SELECT * FROM pipeline_flow_plans WHERE project_id=${p}$ "
|
|
||||||
"ORDER BY version DESC, created_at DESC LIMIT 1", {"p": project_id})
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
async def _latest_plan_rec(sor, project_id):
|
||||||
return _rec(recs[0]) if recs else {}
|
"""取项目最新一条计划(已开 sor 上下文版,内部复用)。"""
|
||||||
|
if not project_id:
|
||||||
|
return {}
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT * FROM pipeline_flow_plans WHERE project_id=${p}$ "
|
||||||
|
"ORDER BY version DESC, created_at DESC LIMIT 1", {"p": project_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return _rec(recs[0]) if recs else {}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_plan_flow_key(project_id):
|
||||||
|
"""项目已确认计划的 base_flow_key('' = 无计划/单流程)。流转机制按它选流程分支。"""
|
||||||
|
plan = await get_active_plan(project_id)
|
||||||
|
return (plan.get("base_flow_key") or "").strip() if plan else ""
|
||||||
|
|
||||||
|
|
||||||
# ══════════════════════ PM 复杂度拆解:create_sub_tasks(通用角色工具) ══════════════════════
|
# ══════════════════════ PM 复杂度拆解:create_sub_tasks(通用角色工具) ══════════════════════
|
||||||
@ -611,17 +748,18 @@ if ToolDefinition is not None:
|
|||||||
FLOW_PLAN_TOOLS = [
|
FLOW_PLAN_TOOLS = [
|
||||||
ToolDefinition(
|
ToolDefinition(
|
||||||
name="show_flow_template",
|
name="show_flow_template",
|
||||||
description="查看当前项目所属产线的预制流程(各阶段作用 + 可裁性:不可裁/可裁需警告/可自由裁剪)。用户提出裁剪流程前先用它了解可裁项",
|
description="查看当前项目所属产线的预制流程(各阶段作用 + 可裁性:不可裁/可裁需警告/可自由裁剪)。多流程产线会列出全部可选流程模板。用户提出裁剪流程前先用它了解可裁项",
|
||||||
parameters={}, required=[], category="project"),
|
parameters={}, required=[], category="project"),
|
||||||
ToolDefinition(
|
ToolDefinition(
|
||||||
name="propose_flow_plan",
|
name="propose_flow_plan",
|
||||||
description=("按用户要求裁剪项目流程,生成裁剪草案并发「流程裁剪确认」待办给用户。"
|
description=("按用户要求裁剪项目流程(多流程产线可指定走哪条流程模板),生成草案并发「流程裁剪确认」待办给用户。"
|
||||||
"裁剪合法性由系统硬校验(不可裁阶段/依赖断裂/已开跑会被拒绝并说明原因)。"
|
"裁剪合法性由系统硬校验(不可裁阶段/依赖断裂/已开跑会被拒绝并说明原因)。"
|
||||||
"草案需用户在待办里确认才生效——你没有确认权,禁止声称已确认。"
|
"草案需用户在待办里确认才生效——你没有确认权,禁止声称已确认。"
|
||||||
"trim_keys 传要裁掉的阶段 key 数组(来自 show_flow_template)"),
|
"trim_keys 传要裁掉的阶段 key 数组(来自 show_flow_template)"),
|
||||||
parameters={"trim_keys": "要裁剪的阶段key数组(如[\"analysis_cost_benefit\"]),空数组=不裁剪仅走确认",
|
parameters={"trim_keys": "要裁剪的阶段key数组(如[\"analysis_cost_benefit\"]),空数组=不裁剪仅走确认",
|
||||||
"user_requirements": "用户裁剪要求原文(必填,写进确认待办)",
|
"user_requirements": "用户裁剪要求原文(必填,写进确认待办)",
|
||||||
"propose_note": "你对裁剪方案的说明/风险提示(可选)"},
|
"propose_note": "你对裁剪方案的说明/风险提示(可选)",
|
||||||
|
"flow_key": "多流程产线指定流程模板key(可选,来自show_flow_template;单流程产线留空)"},
|
||||||
required=["user_requirements"], category="project"),
|
required=["user_requirements"], category="project"),
|
||||||
ToolDefinition(
|
ToolDefinition(
|
||||||
name="get_flow_plan",
|
name="get_flow_plan",
|
||||||
@ -633,13 +771,19 @@ if ToolDefinition is not None:
|
|||||||
async def _h_show_flow_template(sor, params, ctx):
|
async def _h_show_flow_template(sor, params, ctx):
|
||||||
pid = (ctx.get("pipeline_id") or "").strip()
|
pid = (ctx.get("pipeline_id") or "").strip()
|
||||||
project_id = ctx.get("project_id") or ""
|
project_id = ctx.get("project_id") or ""
|
||||||
|
tpls = await list_flow_templates(pid)
|
||||||
|
head = ""
|
||||||
|
if len(tpls) >= 2:
|
||||||
|
head = ("本产线是多流程产线,可选流程模板(propose_flow_plan 用 flow_key 指定):\n"
|
||||||
|
+ "\n".join("- %s(%s):%s,%d 阶段" % (k, n, d[:80], c)
|
||||||
|
for k, n, d, c in tpls) + "\n\n")
|
||||||
pid2, stages = await get_flow_template(project_id=project_id, pipeline_id=pid)
|
pid2, stages = await get_flow_template(project_id=project_id, pipeline_id=pid)
|
||||||
if not stages:
|
if not stages:
|
||||||
return ("当前产线(%s)未声明可裁剪流程,按标准全流程执行,无需裁剪。"
|
return head + ("当前产线(%s)未声明可裁剪流程,按标准全流程执行,无需裁剪。"
|
||||||
% (pid2 or "未知"))
|
% (pid2 or "未知"))
|
||||||
return ("产线「%s」预制流程(共 %d 阶段):\n%s\n\n"
|
return head + ("产线「%s」预制流程(共 %d 阶段):\n%s\n\n"
|
||||||
"用户要裁剪时,把要裁掉的阶段 key 传给 propose_flow_plan。"
|
"用户要裁剪时,把要裁掉的阶段 key 传给 propose_flow_plan。"
|
||||||
% (pid2 or "?", len(stages), render_template_md(stages)))
|
% (pid2 or "?", len(stages), render_template_md(stages)))
|
||||||
|
|
||||||
|
|
||||||
async def _h_propose_flow_plan(sor, params, ctx):
|
async def _h_propose_flow_plan(sor, params, ctx):
|
||||||
@ -652,6 +796,7 @@ async def _h_propose_flow_plan(sor, params, ctx):
|
|||||||
user_requirements=params.get("user_requirements", "") or "",
|
user_requirements=params.get("user_requirements", "") or "",
|
||||||
propose_note=params.get("propose_note", "") or "",
|
propose_note=params.get("propose_note", "") or "",
|
||||||
pipeline_id=ctx.get("pipeline_id", "") or "",
|
pipeline_id=ctx.get("pipeline_id", "") or "",
|
||||||
|
flow_key=params.get("flow_key", "") or "",
|
||||||
who="agent.main_agent",
|
who="agent.main_agent",
|
||||||
agent_id=ctx.get("user_id", "") or "")
|
agent_id=ctx.get("user_id", "") or "")
|
||||||
return ("OK: " if ok else "ERROR: ") + msg
|
return ("OK: " if ok else "ERROR: ") + msg
|
||||||
|
|||||||
@ -307,8 +307,10 @@ async def pause_project(project_id, who=None, agent_id=None):
|
|||||||
from_state=cur, to_state=S_PAUSED,
|
from_state=cur, to_state=S_PAUSED,
|
||||||
who=_normalize_role(who), agent_id=agent_id,
|
who=_normalize_role(who), agent_id=agent_id,
|
||||||
detail=f"prev_state={cur}", sor=sor)
|
detail=f"prev_state={cur}", sor=sor)
|
||||||
# 停止推进后清空项目全部待办(2026-09-01):项目不再推进,遗留待办无意义且占角标
|
# 2026-09-15 修复:暂停不再删除待办。暂停语义是「可逆停摆」,删 pending 人类
|
||||||
await _clear_project_todos(sor, project_id)
|
# 任务/冒泡问题是破坏性动作——实测(首医项目)pause 删掉待办后 resume,
|
||||||
|
# human_task_qc 的检查对象消失 → qc_reject 计轮死循环 → 项目再次 failed 暂停。
|
||||||
|
# 待办清理只保留给 archive/delete(项目终态,遗留待办确属无意义)。
|
||||||
return True, S_PAUSED
|
return True, S_PAUSED
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user