pipeline-service/pipeline_service/project_capability.py

179 lines
6.8 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.

"""项目能力 — sd_projects 的状态机语义化迁移(通用、产线无关)。
定位:项目 CRUD 走 xls2ui 生成的端点;本模块只做「生命周期状态流转」——
每个迁移 CAS 原子(防并发/越权)+ 租户隔离 + 审计。
项目的生命周期/流转规则在 project skill 里LLM 读 skill 判断合法性);本模块只固化操作原语。
角色规范role 参数用 agent.{role}(无前缀自动补 agent.);人角色用 {orgtype}.{role}
scope 约定sd_projects 是顶层容器,用 id 唯一标识CAS WHERE 带 id 即可),
org_id 用于 list 过滤租户隔离audit 的 tenant_id 用 project_id项目即最外层容器
"""
import logging
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
from .audit import record_audit
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.project_capability")
TABLE = "sd_projects"
# 项目状态SDLC 默认,状态机语义见 project skill
S_DRAFT = "draft" # 草稿(已创建,未启动)
S_ACTIVE = "active" # 进行中
S_COMPLETED = "completed" # 已完成
S_ARCHIVED = "archived" # 已归档
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(project_id, from_state, to_state, action,
who=None, agent_id=None, detail=None):
"""CAS 状态迁移 + 审计。返回 (ok, message)。"""
if not project_id:
return False, "缺少 project_id"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
await sor.sqlExe(
f"UPDATE {TABLE} SET status=${{to}}$, updated_at=NOW() "
"WHERE id=${pid}$ AND status=${from}$",
{"to": to_state, "pid": project_id, "from": from_state})
recs = await sor.R(TABLE, {'id': project_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "项目不存在"
cur = getattr(recs[0], 'status', '')
if cur != to_state:
return False, f"状态迁移失败(CAS): 期望 from={from_state} 实际 status={cur}"
await record_audit(project_id, TABLE, project_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 create_project(name, project_type="web_app", description="",
org_id="0", created_by="", who=None, agent_id=None):
"""创建项目新建记录status=draft。返回 (ok, project_id_or_message)。"""
if not name or not name.strip():
return False, "缺少项目名称"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
pid = getID()
await sor.C(TABLE, {
'id': pid,
'name': name.strip(),
'description': description or '',
'project_type': project_type or 'web_app',
'status': S_DRAFT,
'org_id': org_id or '0',
'created_by': created_by or '',
})
await record_audit(pid, TABLE, pid, 'create',
to_state=S_DRAFT, who=_normalize_role(who),
agent_id=agent_id, sor=sor)
logger.info("create_project: %s name=%s", pid, name.strip())
return True, pid
async def start_project(project_id, who=None, agent_id=None):
"""启动项目draft → active也兼容 archived → active 重新激活)。"""
ok, msg = await _transition(project_id, S_DRAFT, S_ACTIVE, 'start',
who=who, agent_id=agent_id)
if ok:
return ok, msg
return await _transition(project_id, S_ARCHIVED, S_ACTIVE, 'start',
who=who, agent_id=agent_id)
async def complete_project(project_id, who=None, agent_id=None):
"""完成项目active → completed。"""
return await _transition(project_id, S_ACTIVE, S_COMPLETED, 'complete',
who=who, agent_id=agent_id)
async def archive_project(project_id, who=None, agent_id=None):
"""归档项目completed → archived也兼容 active → archived 强制归档)。"""
ok, msg = await _transition(project_id, S_COMPLETED, S_ARCHIVED, 'archive',
who=who, agent_id=agent_id)
if ok:
return ok, msg
return await _transition(project_id, S_ACTIVE, S_ARCHIVED, 'archive',
who=who, agent_id=agent_id)
async def reopen_project(project_id, who=None, agent_id=None):
"""重新打开archived → active。"""
return await _transition(project_id, S_ARCHIVED, S_ACTIVE, 'reopen',
who=who, agent_id=agent_id)
async def set_project_state(project_id, from_state, to_state,
who=None, agent_id=None, detail=None):
"""通用 CAS 状态迁移兜底(跨产线自定义状态机用)。"""
return await _transition(project_id, from_state, to_state, 'set_state',
who=who, agent_id=agent_id, detail=detail)
async def list_projects(org_id=None, status=None, limit=50) -> list:
"""列出项目(可按组织/状态过滤org_id 空则列全部)。"""
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
conditions = ["1=1"]
params = {}
if org_id:
conditions.append("org_id=${org}$")
params["org"] = org_id
if status:
conditions.append("status=${status}$")
params["status"] = 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 {}