183 lines
7.4 KiB
Python
183 lines
7.4 KiB
Python
"""交付件能力 — 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 {}
|