pipeline-service/pipeline_service/deliverable_capability.py

183 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""交付件能力 — 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 记录对象转成 dictsqlor 行是 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 {}