fix(human_task): 待办归属动态解析+expired_at地雷修复(2026-09-18银联事故实锤)——①新增resolve_project_owner_assignee:项目真人创建者进其我的待办,无主回退owner.superuser;flow_plan确认/修订/重做失败4处调用点改动态;②create_human_task显式写expired_at=+7d(列默认current_timestamp()→创建即过期,实测全部历史待办expired_at==created_at)

This commit is contained in:
ymq 2026-09-18 13:23:29 +08:00
parent 594d4f0501
commit 82257290ee
2 changed files with 53 additions and 6 deletions

View File

@ -25,9 +25,19 @@ from appPublic.uniqueID import getID
from .audit import record_audit
from .human_task_capability import (
create_human_task, _get_user_org, _get_user_roles,
resolve_project_owner_assignee,
)
DBNAME = "pipeline"
async def _owner_assignee(sor, project_id):
"""确认/介入待办归属 (role, id):项目真人创建者优先(进其「我的待办」),
无主项目回退 owner.superuser2026-09-18 银联事故硬编码 owner.superuser
导致非超管机构创建者待办 0 确认无门"""
return await resolve_project_owner_assignee(sor, project_id)
logger = logging.getLogger("pipeline.flow_plan")
# 计划状态
@ -370,11 +380,13 @@ async def propose_flow_plan(project_id, trim_keys=None, user_requirements="",
user_requirements, propose_note, version,
flow_key=flow_key, pipeline_id=pid,
detect_note=detect_note)
_ar, _ai = await _owner_assignee(sor, project_id)
ok_ht, ht = await create_human_task(
project_id,
"流程裁剪确认:%sv%d" % (pname, version),
desc, task_type=T_FLOW_CONFIRM,
assignee_role="owner.superuser", created_by=who or "agent.main_agent")
assignee_role=_ar, assignee_id=_ai,
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() "
@ -961,12 +973,14 @@ async def revise_flow_plan(project_id, user_requirements="", reentry_stage="",
reentry_md += ["**确认后系统将执行:**", "", rdesc.strip(), "",
"> ⚠️ 重做会重置上述阶段的已有产出(见影响说明),流程从该阶段自动重新推进。"]
desc = desc + "\n".join(reentry_md)
_ar2, _ai2 = await _owner_assignee(sor, project_id)
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")
assignee_role=_ar2, assignee_id=_ai2,
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() "
@ -1143,12 +1157,13 @@ async def _run_stage_reentry(sor, plan, operator_id):
if not fn:
# 提案时已校验过钩子存在;确认时消失 = 部署窗口异常,诚实上报
msg = "产线「%s」阶段重做钩子不可用,未执行重置——请人工处理" % pid
_ar3, _ai3 = await _owner_assignee(sor, project_id)
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")
task_type="general", assignee_role=_ar3, assignee_id=_ai3)
await sor.sqlExe("COMMIT", {})
return False, msg
try:
@ -1161,12 +1176,13 @@ async def _run_stage_reentry(sor, plan, operator_id):
msg = "阶段重做执行异常:%s" % str(e)[:300]
ok = False
if not ok:
_ar4, _ai4 = await _owner_assignee(sor, project_id)
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")
"流程可能停在旧产出上。请人工处置后项目自动恢复推进。"
% plan.get("version"),
task_type="general", assignee_role=_ar4, assignee_id=_ai4)
await sor.sqlExe("COMMIT", {})
await record_audit(project_id, "pipeline_flow_plans", plan.get("id", ""),
"stage_reentry", to_state=stage_key, who=operator_id,

View File

@ -12,6 +12,7 @@
import json
import logging
from datetime import datetime, timedelta
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
@ -95,6 +96,32 @@ async def _get_user_roles(sor, user_id):
return roles
async def resolve_project_owner_assignee(sor, project_id):
"""项目人类待办的归属人解析2026-09-18 银联事故实锤修复)。
返回 (assignee_role, assignee_id)
- 项目有真人创建者 ('', created_by)待办直接进创建者我的待办
旧实现硬编码 assignee_role='owner.superuser'而该角色只有平台超管
user-01持有非超管机构用户创建的项目确认/介入待办全派给超管
创建者我的待办 0 实测 ymq 银联项目 5 flow_plan_confirm
assignee_role=owner.superuserassignee_id
- 无真人创建者自动立项/agent 创建 ('owner.superuser', '')回退
平台超管兜底保持旧行为不丢待办
角色名不硬编码进业务判断owner.superuser 仅作为无主项目的兜底指派
与运行时角色 CRUD 不冲突它仍是 role 表里的真实角色
"""
if not project_id:
return "owner.superuser", ""
recs = await sor.sqlExe(
"SELECT created_by FROM sd_projects WHERE id=${pid}$ LIMIT 1",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
owner = str(getattr(recs[0], "created_by", "") or "") if recs else ""
if owner and not owner.startswith("agent.") and owner != "system":
return "", owner
return "owner.superuser", ""
async def create_human_task(project_id, title, description="", task_type=T_GENERAL,
assignee_role=None, assignee_id=None,
iteration_id=None, bug_id=None, created_by=None, task_id="",
@ -135,6 +162,9 @@ async def create_human_task(project_id, title, description="", task_type=T_GENER
hid = getID()
_fs_json = json.dumps(form_schema, ensure_ascii=False) if form_schema else None
# expired_at 显式写2026-09-18 实锤地雷):列 DDL 默认 current_timestamp()
# → 不显式写则创建即过期(实测全部历史待办 expired_at==created_at
# 人类待办默认 7 天有效NULL 语义不可用(列 NOT NULL
await sor.C('pipeline_human_tasks', {
'id': hid,
'task_id': task_id or '', # 确认类任务绑定原引擎任务 id
@ -153,6 +183,7 @@ async def create_human_task(project_id, title, description="", task_type=T_GENER
'qc_status': QC_PENDING,
'title': title.strip(),
'description': description or '',
'expired_at': datetime.now() + timedelta(days=7),
})
await sor.sqlExe("COMMIT", {})
logger.info("create_human_task: %s project=%s type=%s title=%s",