"""交付件能力 — pipeline_deliverables 的评审状态机语义化迁移(通用、产线无关)。 定位:交付件 CRUD 走 xls2ui 生成的端点;本模块只做「评审状态流转」—— 每个迁移 CAS 原子(防并发/越权)+ 租户隔离 + 审计。 交付件的评审状态机/流转规则在 deliverable skill 里(LLM 读 skill 判断合法性);本模块只固化操作原语。 角色规范:role 参数用 agent.{role}(无前缀自动补 agent.);人角色用 {orgtype}.{role}。 scope 约定:pipeline_deliverables 用 project_id 做范围校验(CAS 的 WHERE 里带 project_id)。 注意:评审状态列名是 review_status(非 status)。 """ import logging from sqlor.dbpools import DBPools from appPublic.uniqueID import getID from .audit import record_audit DBNAME = "pipeline" logger = logging.getLogger("pipeline.deliverable_capability") TABLE = "pipeline_deliverables" STATUS_COL = "review_status" # 交付件评审状态(SDLC 默认,状态机语义见 deliverable skill) S_PENDING = "pending" # 待评审 S_APPROVED = "approved" # 评审通过 S_REJECTED = "rejected" # 评审驳回 def _get_db(): db = DBPools() if not db.databases: from appPublic.jsonConfig import getConfig config = getConfig() if config.databases: db.databases = config.databases return db, DBNAME def _normalize_role(role): """角色规范:agent 角色补 agent. 前缀;人角色 {orgtype}.{role} 保留原样。""" role = (role or "").strip() if not role: return "" if "." in role: return role return f"agent.{role}" async def _transition(deliverable_id, project_id, from_state, to_state, action, who=None, agent_id=None, detail=None): """CAS 评审状态迁移 + 审计。返回 (ok, message)。""" if not deliverable_id or not project_id: return False, "缺少 deliverable_id 或 project_id" db, dbname = _get_db() async with db.sqlorContext(dbname) as sor: await sor.sqlExe( f"UPDATE {TABLE} SET {STATUS_COL}=${{to}}$, reviewed_by=${{who}}$ " "WHERE id=${did}$ AND project_id=${pid}$ AND " f"{STATUS_COL}=${{from}}$", {"to": to_state, "who": _normalize_role(who) or '', "did": deliverable_id, "pid": project_id, "from": from_state}) recs = await sor.R(TABLE, {'id': deliverable_id}) await sor.sqlExe("COMMIT", {}) if not recs: return False, "交付件不存在" cur = getattr(recs[0], STATUS_COL, '') if cur != to_state: return False, f"状态迁移失败(CAS): 期望 from={from_state} 实际 {STATUS_COL}={cur}" await record_audit(project_id, TABLE, deliverable_id, action, from_state=from_state, to_state=to_state, who=who, agent_id=agent_id, detail=detail, sor=sor) return True, to_state async def submit_deliverable(project_id, title, deliverable_type="code", task_id="", content="", repo_name="", target_path="", file_path="", quality_score=80, created_by="", who=None, agent_id=None): """提交交付件:新建记录,review_status=pending。返回 (ok, deliverable_id_or_message)。""" if not project_id: return False, "缺少 project_id" if not title or not title.strip(): return False, "缺少交付件标题" db, dbname = _get_db() async with db.sqlorContext(dbname) as sor: did = getID() try: quality_score = int(quality_score) except (TypeError, ValueError): quality_score = 80 await sor.C(TABLE, { 'id': did, 'project_id': project_id, 'task_id': task_id or '', 'title': title.strip(), 'deliverable_type': deliverable_type or 'code', 'repo_name': repo_name or '', 'target_path': target_path or '', 'file_path': file_path or '', 'content': content or '', 'quality_score': quality_score, STATUS_COL: S_PENDING, 'created_by': created_by or '', }) await record_audit(project_id, TABLE, did, 'submit', to_state=S_PENDING, who=_normalize_role(who), agent_id=agent_id, sor=sor) logger.info("submit_deliverable: %s project=%s", did, project_id) return True, did async def approve_deliverable(deliverable_id, project_id, who=None, agent_id=None, comment=None): """评审通过:pending → approved。""" return await _transition(deliverable_id, project_id, S_PENDING, S_APPROVED, 'approve', who=who, agent_id=agent_id, detail=comment) async def reject_deliverable(deliverable_id, project_id, who=None, agent_id=None, comment=None): """评审驳回:pending → rejected(意见必填)。""" if not comment or not comment.strip(): return False, "驳回必须附评审意见" return await _transition(deliverable_id, project_id, S_PENDING, S_REJECTED, 'reject', who=who, agent_id=agent_id, detail=comment) async def reopen_deliverable(deliverable_id, project_id, who=None, agent_id=None, comment=None): """重新提交评审:rejected → pending(修改后重新提交)。""" return await _transition(deliverable_id, project_id, S_REJECTED, S_PENDING, 'reopen', who=who, agent_id=agent_id, detail=comment) async def set_deliverable_state(deliverable_id, project_id, from_state, to_state, who=None, agent_id=None, detail=None): """通用 CAS 状态迁移兜底(跨产线自定义状态机用)。""" return await _transition(deliverable_id, project_id, from_state, to_state, 'set_state', who=who, agent_id=agent_id, detail=detail) async def list_deliverables(project_id, task_id=None, review_status=None, limit=50) -> list: """列出交付件(可按任务/评审状态过滤)。""" db, dbname = _get_db() async with db.sqlorContext(dbname) as sor: conditions = ["project_id=${pid}$"] params = {"pid": project_id} if task_id: conditions.append("task_id=${tid}$") params["tid"] = task_id if review_status: conditions.append(f"{STATUS_COL}=${{rs}}$") params["rs"] = review_status where = " AND ".join(conditions) try: limit = int(limit) except (TypeError, ValueError): limit = 50 sql = (f"SELECT * FROM {TABLE} WHERE {where} " f"ORDER BY created_at DESC LIMIT {limit}") recs = await sor.sqlExe(sql, params) # 释放 SELECT 元数据锁 await sor.sqlExe("COMMIT", {}) result = [] for rec in (recs or []): result.append(_rec_to_dict(rec)) return result def _rec_to_dict(rec): """把 sqlor 记录对象转成 dict(sqlor 行是 DictObject,必须 dict(rec) 取列)。""" if isinstance(rec, dict): return dict(rec) try: return dict(rec) except (TypeError, ValueError): if hasattr(rec, 'to_dict'): try: return rec.to_dict() except Exception: pass return {}