feat: 7个SDLC概念能力模块(project/iteration/deliverable/test-plan/test-case/bug/deploy-env状态机CAS+审计) + sdlc_ability接线36工具

This commit is contained in:
ymq 2026-08-17 11:00:10 +08:00
parent 4abe93a8f9
commit 3affc4dced
8 changed files with 2004 additions and 1 deletions

View File

@ -0,0 +1,220 @@
"""Bug 能力 — sd_bugs 的状态机语义化迁移(通用、产线无关)。
定位Bug CRUD xls2ui 生成的端点本模块只做生命周期状态流转
每个迁移 CAS 原子防并发/越权+ 租户隔离 + 审计
Bug 的状态机/流转规则在 bug skill LLM skill 判断合法性本模块只固化操作原语
角色规范role 参数用 agent.{role}无前缀自动补 agent.人角色用 {orgtype}.{role}
scope 约定sd_bugs project_id iteration_id 做范围校验CAS WHERE 里带 iteration_id
"""
import logging
from datetime import datetime
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
from .audit import record_audit
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.bug_capability")
TABLE = "sd_bugs"
# Bug 状态SDLC 默认,状态机语义见 bug skill
S_OPEN = "open" # 新建
S_CONFIRMED = "confirmed" # 已确认
S_FIXING = "fixing" # 修复中
S_FIXED = "fixed" # 已修复
S_VERIFIED = "verified" # 已验证
S_CLOSED = "closed" # 已关闭
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(bug_id, iteration_id, from_states, to_state, action,
extra_updates=None, who=None, agent_id=None, detail=None):
"""CAS 状态迁移(支持多 from 状态 + 额外字段更新)+ 审计。返回 (ok, message)。"""
if not bug_id or not iteration_id:
return False, "缺少 bug_id 或 iteration_id"
if isinstance(from_states, str):
from_states = [from_states]
from_in = ",".join(f"${{s{i}}}$" for i in range(len(from_states)))
params = {"to": to_state, "bid": bug_id, "iid": iteration_id}
for i, s in enumerate(from_states):
params[f"s{i}"] = s
extra_sets = ""
for col, val in (extra_updates or {}).items():
key = col
params[key] = val
extra_sets += f", {col}=${{{key}}}$"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
await sor.sqlExe(
f"UPDATE {TABLE} SET status=${{to}}$, updated_at=NOW(){extra_sets} "
f"WHERE id=${{bid}}$ AND iteration_id=${{iid}}$ AND status IN ({from_in})",
params)
recs = await sor.R(TABLE, {'id': bug_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, "Bug 不存在"
cur = getattr(recs[0], 'status', '')
if cur != to_state:
return False, f"状态迁移失败(CAS): 期望 from={from_states} 实际 status={cur}"
await record_audit(iteration_id, TABLE, bug_id, action,
from_state="|".join(from_states), to_state=to_state,
who=who, agent_id=agent_id, detail=detail, sor=sor)
return True, to_state
async def report_bug(iteration_id, title, description="", severity="major",
priority="P1", case_id="", step_name="", reporter_type="agent",
reporter_id="", assignee_id="", who=None, agent_id=None):
"""上报 Bug新建记录status=open。返回 (ok, bug_id_or_message)。"""
if not iteration_id:
return False, "缺少 iteration_id"
if not title or not title.strip():
return False, "缺少 Bug 标题"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
bid = getID()
await sor.C(TABLE, {
'id': bid,
'iteration_id': iteration_id,
'case_id': case_id or '',
'step_name': step_name or '',
'title': title.strip(),
'description': description or '',
'severity': severity or 'major',
'priority': priority or 'P1',
'status': S_OPEN,
'reporter_type': reporter_type or 'agent',
'reporter_id': reporter_id or '',
'assignee_id': assignee_id or '',
})
await record_audit(iteration_id, TABLE, bid, 'report',
to_state=S_OPEN, who=_normalize_role(who),
agent_id=agent_id, sor=sor)
logger.info("report_bug: %s iteration=%s", bid, iteration_id)
return True, bid
async def confirm_bug(bug_id, iteration_id, who=None, agent_id=None):
"""确认 Bugopen → confirmed。"""
return await _transition(bug_id, iteration_id, [S_OPEN], S_CONFIRMED, 'confirm',
who=who, agent_id=agent_id)
async def start_fix(bug_id, iteration_id, assignee_id="", who=None, agent_id=None):
"""开始修复confirmed → fixing记录处理人"""
return await _transition(bug_id, iteration_id, [S_CONFIRMED], S_FIXING, 'start_fix',
extra_updates={"assignee_id": assignee_id or ''},
who=who, agent_id=agent_id)
async def fix_bug(bug_id, iteration_id, fix_description="", fix_commit="",
who=None, agent_id=None):
"""修复完成fixing → fixed附修复说明 + 提交)。"""
return await _transition(bug_id, iteration_id, [S_FIXING], S_FIXED, 'fix',
extra_updates={"fix_description": fix_description or '',
"fix_commit": fix_commit or ''},
who=who, agent_id=agent_id, detail=fix_description)
async def verify_bug(bug_id, iteration_id, verified_by="", who=None, agent_id=None):
"""验证修复fixed → verified记录验证人"""
return await _transition(bug_id, iteration_id, [S_FIXED], S_VERIFIED, 'verify',
extra_updates={"verified_by": verified_by or ''},
who=who, agent_id=agent_id)
async def close_bug(bug_id, iteration_id, who=None, agent_id=None):
"""关闭 Bugverified → closed记录关闭时间"""
return await _transition(bug_id, iteration_id, [S_VERIFIED], S_CLOSED, 'close',
extra_updates={"closed_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S')},
who=who, agent_id=agent_id)
async def reject_bug(bug_id, iteration_id, who=None, agent_id=None, comment=None):
"""驳回 Bugopen/confirmed → rejected附意见"""
if not comment or not comment.strip():
return False, "驳回必须附意见"
return await _transition(bug_id, iteration_id, [S_OPEN, S_CONFIRMED], S_REJECTED, 'reject',
who=who, agent_id=agent_id, detail=comment)
async def reopen_bug(bug_id, iteration_id, who=None, agent_id=None, comment=None):
"""重新打开closed/rejected → open。"""
return await _transition(bug_id, iteration_id, [S_CLOSED, S_REJECTED], S_OPEN, 'reopen',
who=who, agent_id=agent_id, detail=comment)
async def set_bug_state(bug_id, iteration_id, from_state, to_state,
who=None, agent_id=None, detail=None):
"""通用 CAS 状态迁移兜底(跨产线自定义状态机用)。"""
return await _transition(bug_id, iteration_id, [from_state], to_state, 'set_state',
who=who, agent_id=agent_id, detail=detail)
async def list_bugs(iteration_id, status=None, severity=None, limit=100) -> list:
"""列出 Bug可按状态/严重度过滤)。"""
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
conditions = ["iteration_id=${iid}$"]
params = {"iid": iteration_id}
if status:
conditions.append("status=${status}$")
params["status"] = status
if severity:
conditions.append("severity=${sev}$")
params["sev"] = severity
where = " AND ".join(conditions)
try:
limit = int(limit)
except (TypeError, ValueError):
limit = 100
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 {}

View File

@ -0,0 +1,182 @@
"""交付件能力 — 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 {}

View File

@ -0,0 +1,199 @@
"""部署环境能力 — sd_deploy_envs 的状态机语义化迁移(通用、产线无关)。
定位环境 CRUD xls2ui 生成的端点本模块只做验证状态流转
每个迁移 CAS 原子防并发/越权+ 租户隔离 + 审计
环境的状态机/流转规则在 deploy-env skill LLM skill 判断合法性本模块只固化操作原语
发布本体部署动作复用 task 状态机deploy 角色本模块管环境这个可追踪实体
角色规范role 参数用 agent.{role}无前缀自动补 agent.人角色用 {orgtype}.{role}
scope 约定sd_deploy_envs project_id 做范围校验CAS WHERE 里带 project_id
"""
import logging
from datetime import datetime
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
from .audit import record_audit
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.deploy_capability")
TABLE = "sd_deploy_envs"
# 部署环境状态SDLC 默认,状态机语义见 deploy-env skill
S_CONFIGURED = "configured" # 已配置
S_VERIFIED = "verified" # 已验证
S_FAILED = "failed" # 验证失败
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(env_id, project_id, from_states, to_state, action,
extra_updates=None, who=None, agent_id=None, detail=None):
"""CAS 状态迁移(支持多 from 状态 + 额外字段更新)+ 审计。返回 (ok, message)。"""
if not env_id or not project_id:
return False, "缺少 env_id 或 project_id"
if isinstance(from_states, str):
from_states = [from_states]
from_in = ",".join(f"${{s{i}}}$" for i in range(len(from_states)))
params = {"to": to_state, "eid": env_id, "pid": project_id}
for i, s in enumerate(from_states):
params[f"s{i}"] = s
extra_sets = ""
for col, val in (extra_updates or {}).items():
key = col
params[key] = val
extra_sets += f", {col}=${{{key}}}$"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
await sor.sqlExe(
f"UPDATE {TABLE} SET status=${{to}}$, updated_at=NOW(){extra_sets} "
f"WHERE id=${{eid}}$ AND project_id=${{pid}}$ AND status IN ({from_in})",
params)
recs = await sor.R(TABLE, {'id': env_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_states} 实际 status={cur}"
await record_audit(project_id, TABLE, env_id, action,
from_state="|".join(from_states), to_state=to_state,
who=who, agent_id=agent_id, detail=detail, sor=sor)
return True, to_state
async def configure_env(project_id, env_type, host, user, deploy_path,
port=22, ssh_key_path="", sudo_enabled="", python_path="",
db_host="", db_port=3306, db_name="", db_user="", db_password="",
who=None, agent_id=None):
"""配置环境新建记录status=configured。返回 (ok, env_id_or_message)。"""
if not project_id:
return False, "缺少 project_id"
if not env_type or not env_type.strip():
return False, "缺少 env_type"
if not host or not host.strip():
return False, "缺少 host"
if not user or not user.strip():
return False, "缺少 user"
if not deploy_path or not deploy_path.strip():
return False, "缺少 deploy_path"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
eid = getID()
try:
port = int(port)
except (TypeError, ValueError):
port = 22
try:
db_port = int(db_port)
except (TypeError, ValueError):
db_port = 3306
await sor.C(TABLE, {
'id': eid,
'project_id': project_id,
'env_type': env_type.strip(),
'host': host.strip(),
'port': port,
'user': user.strip(),
'ssh_key_path': ssh_key_path or '',
'sudo_enabled': sudo_enabled or '',
'deploy_path': deploy_path.strip(),
'python_path': python_path or '',
'db_host': db_host or '',
'db_port': db_port,
'db_name': db_name or '',
'db_user': db_user or '',
'db_password': db_password or '',
'status': S_CONFIGURED,
})
await record_audit(project_id, TABLE, eid, 'configure',
to_state=S_CONFIGURED, who=_normalize_role(who),
agent_id=agent_id, sor=sor)
logger.info("configure_env: %s project=%s type=%s", eid, project_id, env_type.strip())
return True, eid
async def verify_env(env_id, project_id, who=None, agent_id=None):
"""验证环境通过configured/failed → verified记录验证时间"""
return await _transition(env_id, project_id, [S_CONFIGURED, S_FAILED], S_VERIFIED, 'verify',
extra_updates={"verified_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S')},
who=who, agent_id=agent_id)
async def fail_env(env_id, project_id, reason="", who=None, agent_id=None):
"""验证环境失败configured/verified → failed附失败原因"""
return await _transition(env_id, project_id, [S_CONFIGURED, S_VERIFIED], S_FAILED, 'fail',
who=who, agent_id=agent_id, detail=reason)
async def set_env_state(env_id, project_id, from_state, to_state,
who=None, agent_id=None, detail=None):
"""通用 CAS 状态迁移兜底(跨产线自定义状态机用)。"""
return await _transition(env_id, project_id, [from_state], to_state, 'set_state',
who=who, agent_id=agent_id, detail=detail)
async def list_envs(project_id, env_type=None, 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 env_type:
conditions.append("env_type=${et}$")
params["et"] = env_type
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 ASC 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 {}

View File

@ -0,0 +1,172 @@
"""迭代能力 — sd_iterations 的状态机语义化迁移(通用、产线无关)。
定位迭代 CRUD xls2ui 生成的端点本模块只做生命周期状态流转
每个迁移 CAS 原子防并发/越权+ 租户隔离 + 审计
迭代的生命周期/流转规则在 iteration skill LLM skill 判断合法性本模块只固化操作原语
角色规范role 参数用 agent.{role}无前缀自动补 agent.人角色用 {orgtype}.{role}
scope 约定sd_iterations project_id 做范围校验CAS WHERE 里带 project_id
pipeline_tasks tenant_id 不同sd_* 业务表用自己的归属列
"""
import logging
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
from .audit import record_audit
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.iteration_capability")
TABLE = "sd_iterations"
# 迭代状态SDLC 默认,状态机语义见 iteration skill
S_PLANNING = "planning" # 规划中
S_IN_PROGRESS = "in_progress" # 进行中
S_COMPLETED = "completed" # 已完成
S_CANCELLED = "cancelled" # 已取消
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(iteration_id, project_id, from_state, to_state, action,
who=None, agent_id=None, detail=None):
"""CAS 状态迁移 + 审计。返回 (ok, message)。"""
if not iteration_id or not project_id:
return False, "缺少 iteration_id 或 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=${iid}$ AND project_id=${pid}$ AND status=${from}$",
{"to": to_state, "iid": iteration_id, "pid": project_id, "from": from_state})
recs = await sor.R(TABLE, {'id': iteration_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, iteration_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_iteration(project_id, iteration_name, iteration_type="new_feature",
scope="", priority=5, created_by="", who=None, agent_id=None):
"""创建迭代新建记录status=planning。返回 (ok, iteration_id_or_message)。"""
if not project_id:
return False, "缺少 project_id"
if not iteration_name or not iteration_name.strip():
return False, "缺少 iteration_name"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
iid = getID()
try:
priority = int(priority)
except (TypeError, ValueError):
priority = 5
await sor.C(TABLE, {
'id': iid,
'project_id': project_id,
'iteration_name': iteration_name.strip(),
'iteration_type': iteration_type or 'new_feature',
'scope': scope or '',
'status': S_PLANNING,
'priority': priority,
'created_by': created_by or '',
})
await record_audit(project_id, TABLE, iid, 'create',
to_state=S_PLANNING, who=_normalize_role(who),
agent_id=agent_id, sor=sor)
logger.info("create_iteration: %s project=%s", iid, project_id)
return True, iid
async def start_iteration(iteration_id, project_id, who=None, agent_id=None):
"""开始迭代planning → in_progress。"""
return await _transition(iteration_id, project_id, S_PLANNING, S_IN_PROGRESS, 'start',
who=who, agent_id=agent_id)
async def complete_iteration(iteration_id, project_id, who=None, agent_id=None):
"""完成迭代in_progress → completed。"""
return await _transition(iteration_id, project_id, S_IN_PROGRESS, S_COMPLETED, 'complete',
who=who, agent_id=agent_id)
async def cancel_iteration(iteration_id, project_id, who=None, agent_id=None, comment=None):
"""取消迭代planning/in_progress → cancelled。"""
ok, msg = await _transition(iteration_id, project_id, S_PLANNING, S_CANCELLED, 'cancel',
who=who, agent_id=agent_id, detail=comment)
if ok:
return ok, msg
return await _transition(iteration_id, project_id, S_IN_PROGRESS, S_CANCELLED, 'cancel',
who=who, agent_id=agent_id, detail=comment)
async def set_iteration_state(iteration_id, project_id, from_state, to_state,
who=None, agent_id=None, detail=None):
"""通用 CAS 状态迁移兜底(跨产线自定义状态机用)。"""
return await _transition(iteration_id, project_id, from_state, to_state, 'set_state',
who=who, agent_id=agent_id, detail=detail)
async def list_iterations(project_id, 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 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 ASC 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 {}

View File

@ -0,0 +1,178 @@
"""项目能力 — 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 {}

View File

@ -151,6 +151,49 @@ SDL_TOOLS = [
parameters={"feature_id": "功能ID"},
category="feature",
),
# ── 项目生命周期 ──
ToolDefinition(name="list_projects", description="列出项目列表(含状态)", parameters={"status": "按状态筛选(可选)"}, category="project"),
ToolDefinition(name="start_project", description="启动项目(draft/archived→active)", parameters={"project_id": "项目ID"}, category="project"),
ToolDefinition(name="complete_project", description="完成项目(active→completed)", parameters={"project_id": "项目ID"}, category="project"),
ToolDefinition(name="archive_project", description="归档项目(completed→archived)", parameters={"project_id": "项目ID"}, category="project"),
ToolDefinition(name="reopen_project", description="重新打开归档项目(archived→active)", parameters={"project_id": "项目ID"}, category="project"),
# ── 迭代 ──
ToolDefinition(name="list_iterations", description="查看当前项目迭代列表", parameters={"status": "按状态筛选(可选)"}, category="iteration"),
ToolDefinition(name="create_iteration", description="创建迭代(planning)", parameters={"iteration_name": "迭代名称", "iteration_type": "迭代类型(可选)", "scope": "迭代范围(可选)"}, category="iteration"),
ToolDefinition(name="start_iteration", description="开始迭代(planning→in_progress)", parameters={"iteration_id": "迭代ID"}, category="iteration"),
ToolDefinition(name="complete_iteration", description="完成迭代(in_progress→completed)", parameters={"iteration_id": "迭代ID"}, category="iteration"),
ToolDefinition(name="cancel_iteration", description="取消迭代(planning/in_progress→cancelled)", parameters={"iteration_id": "迭代ID", "comment": "取消原因(可选)"}, category="iteration"),
# ── 交付件评审 ──
ToolDefinition(name="submit_deliverable", description="提交交付件(pending)", parameters={"title": "交付件标题", "deliverable_type": "类型(code/doc/config等)", "content": "交付件内容", "task_id": "关联任务ID(可选)"}, category="deliverable"),
ToolDefinition(name="approve_deliverable", description="评审通过交付件(pending→approved)", parameters={"deliverable_id": "交付件ID"}, category="deliverable"),
ToolDefinition(name="reject_deliverable", description="评审驳回交付件(pending→rejected附意见)", parameters={"deliverable_id": "交付件ID", "comment": "评审意见(必填)"}, category="deliverable"),
# ── 测试计划 ──
ToolDefinition(name="list_plans", description="查看迭代测试计划列表", parameters={"iteration_id": "迭代ID(可选)", "status": "按状态筛选(可选)"}, category="test-plan"),
ToolDefinition(name="create_test_plan", description="创建测试计划(draft)", parameters={"iteration_id": "迭代ID", "plan_name": "方案名称", "plan_type": "方案类型(可选)", "scope": "测试范围(可选)"}, category="test-plan"),
ToolDefinition(name="approve_plan", description="审批测试计划(draft→approved)", parameters={"plan_id": "计划ID"}, category="test-plan"),
ToolDefinition(name="start_plan", description="开始执行测试计划(approved→executing)", parameters={"plan_id": "计划ID"}, category="test-plan"),
ToolDefinition(name="complete_plan", description="完成测试计划(executing→completed)", parameters={"plan_id": "计划ID"}, category="test-plan"),
# ── 测试用例 ──
ToolDefinition(name="list_cases", description="查看测试计划用例列表", parameters={"plan_id": "计划ID", "status": "按状态筛选(可选)"}, category="test-case"),
ToolDefinition(name="create_case", description="创建测试用例(pending)", parameters={"plan_id": "计划ID", "case_name": "用例名称", "case_type": "用例类型(可选)", "steps": "测试步骤(可选)", "expected_result": "预期结果(可选)"}, category="test-case"),
ToolDefinition(name="pass_case", description="用例通过(pending/fail→pass)", parameters={"case_id": "用例ID", "actual_result": "实际结果(可选)"}, category="test-case"),
ToolDefinition(name="fail_case", description="用例失败(pending/pass→fail应联动建Bug)", parameters={"case_id": "用例ID", "actual_result": "实际结果(必填)"}, category="test-case"),
ToolDefinition(name="skip_case", description="用例跳过(pending→skipped注明原因)", parameters={"case_id": "用例ID", "actual_result": "跳过原因(可选)"}, category="test-case"),
ToolDefinition(name="block_case", description="用例阻塞(pending→blocked注明原因)", parameters={"case_id": "用例ID", "actual_result": "阻塞原因(可选)"}, category="test-case"),
# ── Bug ──
ToolDefinition(name="list_bugs", description="查看迭代Bug列表", parameters={"iteration_id": "迭代ID(可选)", "status": "按状态筛选(可选)"}, category="bug"),
ToolDefinition(name="report_bug", description="上报Bug(open)", parameters={"title": "Bug标题", "description": "描述", "severity": "严重度(可选)", "priority": "优先级(可选)", "iteration_id": "迭代ID(可选)", "case_id": "关联用例ID(可选)"}, category="bug"),
ToolDefinition(name="confirm_bug", description="确认Bug(open→confirmed)", parameters={"bug_id": "BugID"}, category="bug"),
ToolDefinition(name="start_fix", description="开始修复Bug(confirmed→fixing)", parameters={"bug_id": "BugID", "assignee_id": "处理人(可选)"}, category="bug"),
ToolDefinition(name="fix_bug", description="修复完成(fixing→fixed附修复说明)", parameters={"bug_id": "BugID", "fix_description": "修复说明", "fix_commit": "修复Commit(可选)"}, category="bug"),
ToolDefinition(name="verify_bug", description="验证修复(fixed→verified)", parameters={"bug_id": "BugID"}, category="bug"),
ToolDefinition(name="close_bug", description="关闭Bug(verified→closed)", parameters={"bug_id": "BugID"}, category="bug"),
ToolDefinition(name="reject_bug", description="驳回Bug(open/confirmed→rejected附意见)", parameters={"bug_id": "BugID", "comment": "驳回意见(必填)"}, category="bug"),
ToolDefinition(name="reopen_bug", description="重新打开Bug(closed/rejected→open)", parameters={"bug_id": "BugID"}, category="bug"),
# ── 部署环境 ──
ToolDefinition(name="list_envs", description="查看项目部署环境列表", parameters={"env_type": "环境类型(可选)", "status": "按状态筛选(可选)"}, category="deploy"),
ToolDefinition(name="configure_env", description="配置部署环境(configured)", parameters={"env_type": "环境类型(test/staging/production)", "host": "SSH主机", "user": "SSH用户", "deploy_path": "部署目录", "port": "SSH端口(可选)"}, category="deploy"),
ToolDefinition(name="verify_env", description="验证环境通过(configured→verified)", parameters={"env_id": "环境ID"}, category="deploy"),
]
@ -166,7 +209,14 @@ SDL_PROMPT = """你是「开发产线」的驾驶舱 agent负责软件项目
- 仓库管理 add_repo / list_repos / clone_repo
- 发现卡点任务卡死审核超时失败僵尸 claimed_by主动定位根因并推动修复
- 发现待回答问题> 0 或角色提问时加载 team-communication 技能按冒泡链处理list_questions 列问题 能答则 answer_question 不能答 escalate_question 沿冒泡路径转下一个处理方
- 功能/需求管理 用户提新需求时 propose_feature查功能清单 list_features需求评审/验收时 approve_feature / reject_feature / verify_feature状态机规范见 feature 技能"""
- 功能/需求管理 用户提新需求时 propose_feature查功能清单 list_features需求评审/验收时 approve_feature / reject_feature / verify_feature状态机规范见 feature 技能
- 项目管理 list_projects 列项目启动/完成/归档/重开项目用 start_project / complete_project / archive_project / reopen_project状态机规范见 project 技能
- 迭代管理 create_iteration 建迭代list_iterations 列迭代开始/完成/取消迭代用 start_iteration / complete_iteration / cancel_iteration规范见 iteration 技能
- 交付件评审 submit_deliverable 提交list_deliverables 列交付件评审用 approve_deliverable / reject_deliverable规范见 deliverable 技能
- 测试计划 create_test_plan 建计划list_plans 列计划审批/执行/完成用 approve_plan / start_plan / complete_plan规范见 test-plan 技能
- 测试用例 create_case 建用例list_cases 列用例执行结果用 pass_case / fail_case / skip_case / block_case失败用例应联动 report_bug规范见 test-case 技能
- Bug 管理 report_bug 上报list_bugs Bug确认/修复/验证/关闭/驳回/重开用 confirm_bug / start_fix / fix_bug / verify_bug / close_bug / reject_bug / reopen_bug规范见 bug 技能
- 部署环境 configure_env 配环境list_envs 列环境验证用 verify_env规范见 deploy-env 技能"""
# ── 产线角色集(可插拔,替代硬编码 ROLE_SPECIFICS/ROLE_ALIASES/ROLE_CHAIN ──
@ -728,6 +778,600 @@ async def _h_verify_feature(sor, p, ctx):
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── 通用 ID 前缀兜底 + scope 反查 ──
async def _resolve_id(sor, table, fid):
"""通用 ID 前缀兜底精确匹配失败且入参≥6位时用 LIKE 前缀解析回完整 ID。"""
if not fid:
return ""
recs = await sor.sqlExe(
f"SELECT id FROM {table} WHERE id=${{fid}}$", {"fid": fid})
if recs:
return getattr(recs[0], "id", "")
if len(fid) >= 6:
recs = await sor.sqlExe(
f"SELECT id FROM {table} WHERE id LIKE ${{prefix}}$ LIMIT 1",
{"prefix": fid + "%"})
if recs:
return getattr(recs[0], "id", "")
return ""
async def _get_scope(sor, table, id_field, id_val, scope_field):
"""反查实体的 scope 列值(如 iteration_id/plan_id。返回 scope_val 或空。"""
recs = await sor.sqlExe(
f"SELECT {scope_field} FROM {table} WHERE {id_field}=${{id}}$",
{"id": id_val})
return getattr(recs[0], scope_field, "") if recs else ""
async def _resolve_iteration_id(sor, pid, iteration_id):
"""解析迭代 ID显式传入则用否则取项目第一个迭代。返回 (iteration_id, error)。"""
if iteration_id:
full = await _resolve_id(sor, "sd_iterations", iteration_id)
if full:
return full, ""
return "", f"迭代不存在: {iteration_id}"
recs = await sor.sqlExe(
"SELECT id FROM sd_iterations WHERE project_id=${pid}$ ORDER BY created_at ASC LIMIT 1",
{"pid": pid})
if recs:
return getattr(recs[0], "id", ""), ""
return "", "项目无迭代,请先 create_iteration"
async def _resolve_plan_id(sor, pid, plan_id):
"""解析计划 ID显式传入则用否则取项目第一个计划经迭代。返回 (plan_id, error)。"""
if plan_id:
full = await _resolve_id(sor, "sd_test_plans", plan_id)
if full:
return full, ""
return "", f"计划不存在: {plan_id}"
iid, err = await _resolve_iteration_id(sor, pid, "")
if err:
return "", err
plans = await sor.sqlExe(
"SELECT id FROM sd_test_plans WHERE iteration_id=${iid}$ ORDER BY created_at ASC LIMIT 1",
{"iid": iid})
if not plans:
return "", "项目无测试计划,请先 create_test_plan"
return getattr(plans[0], "id", ""), ""
# ── 项目生命周期 handler ──
async def _h_list_projects(sor, p, ctx):
from .project_capability import list_projects
# 按当前用户 org 过滤租户隔离org_id 空则列全部
org_id = ""
uid = ctx.get("user_id", "")
if uid:
_u = await sor.sqlExe("SELECT orgid FROM users WHERE id=${u}$ LIMIT 1", {"u": uid})
if _u:
org_id = getattr(_u[0], "orgid", "") or ""
status = (p.get("status", "") or "").strip()
lst = await list_projects(org_id=org_id or None, status=status or None)
if not lst:
return "暂无项目"
lines = []
for r in lst:
lines.append(f"- [{r.get('status', '?')}] {r.get('name', '')} (id={r.get('id', '')})")
return "\n".join(lines)
async def _h_start_project(sor, p, ctx):
pid = (p.get("project_id", "") or "").strip()
if not pid:
return "需要项目ID"
pid = await _resolve_id(sor, "sd_projects", pid) or pid
from .project_capability import start_project
ok, msg = await start_project(pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_complete_project(sor, p, ctx):
pid = (p.get("project_id", "") or "").strip()
if not pid:
return "需要项目ID"
pid = await _resolve_id(sor, "sd_projects", pid) or pid
from .project_capability import complete_project
ok, msg = await complete_project(pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_archive_project(sor, p, ctx):
pid = (p.get("project_id", "") or "").strip()
if not pid:
return "需要项目ID"
pid = await _resolve_id(sor, "sd_projects", pid) or pid
from .project_capability import archive_project
ok, msg = await archive_project(pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_reopen_project(sor, p, ctx):
pid = (p.get("project_id", "") or "").strip()
if not pid:
return "需要项目ID"
pid = await _resolve_id(sor, "sd_projects", pid) or pid
from .project_capability import reopen_project
ok, msg = await reopen_project(pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── 迭代 handler ──
async def _h_list_iterations(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
from .iteration_capability import list_iterations
status = (p.get("status", "") or "").strip()
lst = await list_iterations(pid, status=status or None)
if not lst:
return "暂无迭代"
lines = []
for r in lst:
lines.append(f"- [{r.get('status', '?')}] {r.get('iteration_name', '')} (id={r.get('id', '')})")
return "\n".join(lines)
async def _h_create_iteration(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
name = (p.get("iteration_name", "") or "").strip()
if not name:
return "需要迭代名称"
from .iteration_capability import create_iteration
ok, iid = await create_iteration(
project_id=pid,
iteration_name=name,
iteration_type=p.get("iteration_type", "new_feature") or "new_feature",
scope=p.get("scope", "") or "",
created_by=ctx.get("user_id", "") or "",
who="agent.main_agent",
)
if not ok:
return f"ERROR: {iid}"
return f"OK: 已创建迭代「{name}id={iid},状态 planning"
async def _h_start_iteration(sor, p, ctx):
iid = (p.get("iteration_id", "") or "").strip()
if not iid:
return "需要迭代ID"
pid = ctx.get("project_id", "")
iid = await _resolve_id(sor, "sd_iterations", iid) or iid
from .iteration_capability import start_iteration
ok, msg = await start_iteration(iid, pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_complete_iteration(sor, p, ctx):
iid = (p.get("iteration_id", "") or "").strip()
if not iid:
return "需要迭代ID"
pid = ctx.get("project_id", "")
iid = await _resolve_id(sor, "sd_iterations", iid) or iid
from .iteration_capability import complete_iteration
ok, msg = await complete_iteration(iid, pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_cancel_iteration(sor, p, ctx):
iid = (p.get("iteration_id", "") or "").strip()
if not iid:
return "需要迭代ID"
pid = ctx.get("project_id", "")
iid = await _resolve_id(sor, "sd_iterations", iid) or iid
from .iteration_capability import cancel_iteration
ok, msg = await cancel_iteration(iid, pid, who="agent.main_agent",
comment=p.get("comment", "") or "")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── 交付件评审 handler ──
async def _h_submit_deliverable(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
title = (p.get("title", "") or "").strip()
if not title:
return "需要交付件标题"
from .deliverable_capability import submit_deliverable
ok, did = await submit_deliverable(
project_id=pid,
title=title,
deliverable_type=p.get("deliverable_type", "code") or "code",
task_id=p.get("task_id", "") or "",
content=p.get("content", "") or "",
created_by=ctx.get("user_id", "") or "",
who="agent.main_agent",
)
if not ok:
return f"ERROR: {did}"
return f"OK: 已提交交付件「{title}id={did},状态 pending"
async def _h_approve_deliverable(sor, p, ctx):
did = (p.get("deliverable_id", "") or "").strip()
if not did:
return "需要交付件ID"
pid = ctx.get("project_id", "")
did = await _resolve_id(sor, "pipeline_deliverables", did) or did
from .deliverable_capability import approve_deliverable
ok, msg = await approve_deliverable(did, pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_reject_deliverable(sor, p, ctx):
did = (p.get("deliverable_id", "") or "").strip()
if not did:
return "需要交付件ID"
pid = ctx.get("project_id", "")
did = await _resolve_id(sor, "pipeline_deliverables", did) or did
from .deliverable_capability import reject_deliverable
ok, msg = await reject_deliverable(did, pid, who="agent.main_agent",
comment=p.get("comment", "") or "")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── 测试计划 handler ──
async def _h_list_plans(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
from .test_plan_capability import list_plans
iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip())
if err:
return err
status = (p.get("status", "") or "").strip()
lst = await list_plans(iid, status=status or None)
if not lst:
return "暂无测试计划"
lines = []
for r in lst:
lines.append(f"- [{r.get('status', '?')}] {r.get('plan_name', '')} (id={r.get('id', '')})")
return "\n".join(lines)
async def _h_create_test_plan(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
name = (p.get("plan_name", "") or "").strip()
if not name:
return "需要方案名称"
iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip())
if err:
return err
from .test_plan_capability import create_test_plan
ok, plan_id = await create_test_plan(
iteration_id=iid,
plan_name=name,
plan_type=p.get("plan_type", "functional") or "functional",
scope=p.get("scope", "") or "",
created_by=ctx.get("user_id", "") or "",
who="agent.main_agent",
)
if not ok:
return f"ERROR: {plan_id}"
return f"OK: 已创建测试计划「{name}id={plan_id},状态 draft"
async def _h_approve_plan(sor, p, ctx):
plan_id = (p.get("plan_id", "") or "").strip()
if not plan_id:
return "需要计划ID"
plan_id = await _resolve_id(sor, "sd_test_plans", plan_id) or plan_id
iid = await _get_scope(sor, "sd_test_plans", "id", plan_id, "iteration_id")
from .test_plan_capability import approve_plan
ok, msg = await approve_plan(plan_id, iid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_start_plan(sor, p, ctx):
plan_id = (p.get("plan_id", "") or "").strip()
if not plan_id:
return "需要计划ID"
plan_id = await _resolve_id(sor, "sd_test_plans", plan_id) or plan_id
iid = await _get_scope(sor, "sd_test_plans", "id", plan_id, "iteration_id")
from .test_plan_capability import start_plan
ok, msg = await start_plan(plan_id, iid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_complete_plan(sor, p, ctx):
plan_id = (p.get("plan_id", "") or "").strip()
if not plan_id:
return "需要计划ID"
plan_id = await _resolve_id(sor, "sd_test_plans", plan_id) or plan_id
iid = await _get_scope(sor, "sd_test_plans", "id", plan_id, "iteration_id")
from .test_plan_capability import complete_plan
ok, msg = await complete_plan(plan_id, iid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── 测试用例 handler ──
async def _h_list_cases(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
from .test_case_capability import list_cases
plan_id, err = await _resolve_plan_id(sor, pid, (p.get("plan_id", "") or "").strip())
if err:
return err
status = (p.get("status", "") or "").strip()
lst = await list_cases(plan_id, status=status or None)
if not lst:
return "暂无测试用例"
lines = []
for r in lst:
lines.append(f"- [{r.get('status', '?')}] {r.get('case_name', '')} (id={r.get('id', '')})")
return "\n".join(lines)
async def _h_create_case(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
name = (p.get("case_name", "") or "").strip()
if not name:
return "需要用例名称"
plan_id, err = await _resolve_plan_id(sor, pid, (p.get("plan_id", "") or "").strip())
if err:
return err
from .test_case_capability import create_case
ok, cid = await create_case(
plan_id=plan_id,
case_name=name,
case_type=p.get("case_type", "functional") or "functional",
steps=p.get("steps", "") or "",
expected_result=p.get("expected_result", "") or "",
who="agent.main_agent",
)
if not ok:
return f"ERROR: {cid}"
return f"OK: 已创建测试用例「{name}id={cid},状态 pending"
async def _h_pass_case(sor, p, ctx):
cid = (p.get("case_id", "") or "").strip()
if not cid:
return "需要用例ID"
cid = await _resolve_id(sor, "sd_test_cases", cid) or cid
plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id")
from .test_case_capability import pass_case
ok, msg = await pass_case(cid, plan_id, actual_result=p.get("actual_result", "") or "",
who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_fail_case(sor, p, ctx):
cid = (p.get("case_id", "") or "").strip()
if not cid:
return "需要用例ID"
cid = await _resolve_id(sor, "sd_test_cases", cid) or cid
plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id")
from .test_case_capability import fail_case
ok, msg = await fail_case(cid, plan_id, actual_result=p.get("actual_result", "") or "",
who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_skip_case(sor, p, ctx):
cid = (p.get("case_id", "") or "").strip()
if not cid:
return "需要用例ID"
cid = await _resolve_id(sor, "sd_test_cases", cid) or cid
plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id")
from .test_case_capability import skip_case
ok, msg = await skip_case(cid, plan_id, actual_result=p.get("actual_result", "") or "",
who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_block_case(sor, p, ctx):
cid = (p.get("case_id", "") or "").strip()
if not cid:
return "需要用例ID"
cid = await _resolve_id(sor, "sd_test_cases", cid) or cid
plan_id = await _get_scope(sor, "sd_test_cases", "id", cid, "plan_id")
from .test_case_capability import block_case
ok, msg = await block_case(cid, plan_id, actual_result=p.get("actual_result", "") or "",
who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── Bug handler ──
async def _h_list_bugs(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
from .bug_capability import list_bugs
iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip())
if err:
return err
status = (p.get("status", "") or "").strip()
lst = await list_bugs(iid, status=status or None)
if not lst:
return "暂无Bug"
lines = []
for r in lst:
lines.append(f"- [{r.get('status', '?')}][{r.get('severity', '') or ''}] "
f"{r.get('title', '')} (id={r.get('id', '')})")
return "\n".join(lines)
async def _h_report_bug(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
title = (p.get("title", "") or "").strip()
if not title:
return "需要Bug标题"
iid, err = await _resolve_iteration_id(sor, pid, (p.get("iteration_id", "") or "").strip())
if err:
return err
from .bug_capability import report_bug
ok, bid = await report_bug(
iteration_id=iid,
title=title,
description=p.get("description", "") or "",
severity=p.get("severity", "major") or "major",
priority=p.get("priority", "P1") or "P1",
case_id=p.get("case_id", "") or "",
reporter_type="agent",
reporter_id=ctx.get("user_id", "") or "",
who="agent.main_agent",
)
if not ok:
return f"ERROR: {bid}"
return f"OK: 已上报Bug「{title}id={bid},状态 open"
async def _h_confirm_bug(sor, p, ctx):
bid = (p.get("bug_id", "") or "").strip()
if not bid:
return "需要BugID"
bid = await _resolve_id(sor, "sd_bugs", bid) or bid
iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id")
from .bug_capability import confirm_bug
ok, msg = await confirm_bug(bid, iid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_start_fix(sor, p, ctx):
bid = (p.get("bug_id", "") or "").strip()
if not bid:
return "需要BugID"
bid = await _resolve_id(sor, "sd_bugs", bid) or bid
iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id")
from .bug_capability import start_fix
ok, msg = await start_fix(bid, iid, assignee_id=p.get("assignee_id", "") or "",
who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_fix_bug(sor, p, ctx):
bid = (p.get("bug_id", "") or "").strip()
if not bid:
return "需要BugID"
bid = await _resolve_id(sor, "sd_bugs", bid) or bid
iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id")
from .bug_capability import fix_bug
ok, msg = await fix_bug(bid, iid, fix_description=p.get("fix_description", "") or "",
fix_commit=p.get("fix_commit", "") or "",
who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_verify_bug(sor, p, ctx):
bid = (p.get("bug_id", "") or "").strip()
if not bid:
return "需要BugID"
bid = await _resolve_id(sor, "sd_bugs", bid) or bid
iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id")
from .bug_capability import verify_bug
ok, msg = await verify_bug(bid, iid, verified_by=ctx.get("user_id", "") or "",
who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_close_bug(sor, p, ctx):
bid = (p.get("bug_id", "") or "").strip()
if not bid:
return "需要BugID"
bid = await _resolve_id(sor, "sd_bugs", bid) or bid
iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id")
from .bug_capability import close_bug
ok, msg = await close_bug(bid, iid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_reject_bug(sor, p, ctx):
bid = (p.get("bug_id", "") or "").strip()
if not bid:
return "需要BugID"
bid = await _resolve_id(sor, "sd_bugs", bid) or bid
iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id")
from .bug_capability import reject_bug
ok, msg = await reject_bug(bid, iid, who="agent.main_agent",
comment=p.get("comment", "") or "")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_reopen_bug(sor, p, ctx):
bid = (p.get("bug_id", "") or "").strip()
if not bid:
return "需要BugID"
bid = await _resolve_id(sor, "sd_bugs", bid) or bid
iid = await _get_scope(sor, "sd_bugs", "id", bid, "iteration_id")
from .bug_capability import reopen_bug
ok, msg = await reopen_bug(bid, iid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── 部署环境 handler ──
async def _h_list_envs(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
from .deploy_capability import list_envs
env_type = (p.get("env_type", "") or "").strip()
status = (p.get("status", "") or "").strip()
lst = await list_envs(pid, env_type=env_type or None, status=status or None)
if not lst:
return "暂无部署环境"
lines = []
for r in lst:
lines.append(f"- [{r.get('env_type', '?')}][{r.get('status', '?')}] "
f"{r.get('host', '')} ({r.get('user', '')}@{r.get('deploy_path', '')}) id={r.get('id', '')}")
return "\n".join(lines)
async def _h_configure_env(sor, p, ctx):
pid = ctx.get("project_id", "")
if not pid:
return "请先切换到项目"
env_type = (p.get("env_type", "") or "").strip()
host = (p.get("host", "") or "").strip()
user = (p.get("user", "") or "").strip()
deploy_path = (p.get("deploy_path", "") or "").strip()
if not env_type or not host or not user or not deploy_path:
return "需要 env_type/host/user/deploy_path"
from .deploy_capability import configure_env
ok, eid = await configure_env(
project_id=pid,
env_type=env_type,
host=host,
user=user,
deploy_path=deploy_path,
port=p.get("port", 22),
who="agent.main_agent",
)
if not ok:
return f"ERROR: {eid}"
return f"OK: 已配置环境「{env_type}id={eid},状态 configured"
async def _h_verify_env(sor, p, ctx):
eid = (p.get("env_id", "") or "").strip()
if not eid:
return "需要环境ID"
pid = ctx.get("project_id", "")
eid = await _resolve_id(sor, "sd_deploy_envs", eid) or eid
from .deploy_capability import verify_env
ok, msg = await verify_env(eid, pid, who="agent.main_agent")
return f"OK: {msg}" if ok else f"ERROR: {msg}"
# ── 注册能力包 ──
SDL_HANDLERS = {
@ -751,6 +1395,49 @@ SDL_HANDLERS = {
"approve_feature": _h_approve_feature,
"reject_feature": _h_reject_feature,
"verify_feature": _h_verify_feature,
# ── 项目生命周期 ──
"list_projects": _h_list_projects,
"start_project": _h_start_project,
"complete_project": _h_complete_project,
"archive_project": _h_archive_project,
"reopen_project": _h_reopen_project,
# ── 迭代 ──
"list_iterations": _h_list_iterations,
"create_iteration": _h_create_iteration,
"start_iteration": _h_start_iteration,
"complete_iteration": _h_complete_iteration,
"cancel_iteration": _h_cancel_iteration,
# ── 交付件评审 ──
"submit_deliverable": _h_submit_deliverable,
"approve_deliverable": _h_approve_deliverable,
"reject_deliverable": _h_reject_deliverable,
# ── 测试计划 ──
"list_plans": _h_list_plans,
"create_test_plan": _h_create_test_plan,
"approve_plan": _h_approve_plan,
"start_plan": _h_start_plan,
"complete_plan": _h_complete_plan,
# ── 测试用例 ──
"list_cases": _h_list_cases,
"create_case": _h_create_case,
"pass_case": _h_pass_case,
"fail_case": _h_fail_case,
"skip_case": _h_skip_case,
"block_case": _h_block_case,
# ── Bug ──
"list_bugs": _h_list_bugs,
"report_bug": _h_report_bug,
"confirm_bug": _h_confirm_bug,
"start_fix": _h_start_fix,
"fix_bug": _h_fix_bug,
"verify_bug": _h_verify_bug,
"close_bug": _h_close_bug,
"reject_bug": _h_reject_bug,
"reopen_bug": _h_reopen_bug,
# ── 部署环境 ──
"list_envs": _h_list_envs,
"configure_env": _h_configure_env,
"verify_env": _h_verify_env,
}

View File

@ -0,0 +1,199 @@
"""测试用例能力 — sd_test_cases 的执行状态机语义化迁移(通用、产线无关)。
定位用例 CRUD xls2ui 生成的端点本模块只做执行结果状态流转
每个迁移 CAS 原子防并发/越权+ 租户隔离 + 审计
用例的执行状态机/流转规则在 test-case skill LLM skill 判断合法性本模块只固化操作原语
失败用例联动建 Bugcase_id sd_bugs.case_id是规范 test-case skill
角色规范role 参数用 agent.{role}无前缀自动补 agent.人角色用 {orgtype}.{role}
scope 约定sd_test_cases project_id/iteration_id plan_id 做范围校验CAS WHERE 里带 plan_id
"""
import logging
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
from .audit import record_audit
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.test_case_capability")
TABLE = "sd_test_cases"
# 测试用例执行状态SDLC 默认,状态机语义见 test-case skill
S_PENDING = "pending" # 待执行
S_PASS = "pass" # 通过
S_FAIL = "fail" # 失败
S_BLOCKED = "blocked" # 阻塞
S_SKIPPED = "skipped" # 跳过
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 _execute_transition(case_id, plan_id, from_states, to_state, action,
actual_result="", executed_by="", duration_ms=None,
who=None, agent_id=None):
"""执行结果 CAS 迁移 + 审计(记录执行人/实际结果/执行时间)。返回 (ok, message)。"""
if not case_id or not plan_id:
return False, "缺少 case_id 或 plan_id"
if isinstance(from_states, str):
from_states = [from_states]
from_in = ",".join(f"${{s{i}}}$" for i in range(len(from_states)))
params = {"to": to_state, "cid": case_id, "pid": plan_id,
"ar": actual_result or '', "eb": executed_by or ''}
for i, s in enumerate(from_states):
params[f"s{i}"] = s
if duration_ms is not None:
try:
params["dur"] = int(duration_ms)
dur_set = ", duration_ms=${dur}$"
except (TypeError, ValueError):
dur_set = ""
else:
dur_set = ""
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
await sor.sqlExe(
f"UPDATE {TABLE} SET status=${{to}}$, actual_result=${{ar}}$, "
f"executed_by=${{eb}}$, executed_at=NOW(){dur_set} "
f"WHERE id=${{cid}}$ AND plan_id=${{pid}}$ AND status IN ({from_in})",
params)
recs = await sor.R(TABLE, {'id': case_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_states} 实际 status={cur}"
await record_audit(plan_id, TABLE, case_id, action,
from_state="|".join(from_states), to_state=to_state,
who=who, agent_id=agent_id, detail=actual_result, sor=sor)
return True, to_state
async def create_case(plan_id, case_name, case_type="functional", priority="P2",
precondition="", steps="", expected_result="",
created_by="", who=None, agent_id=None):
"""创建用例新建记录status=pending。返回 (ok, case_id_or_message)。"""
if not plan_id:
return False, "缺少 plan_id"
if not case_name or not case_name.strip():
return False, "缺少 case_name"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
cid = getID()
await sor.C(TABLE, {
'id': cid,
'plan_id': plan_id,
'case_name': case_name.strip(),
'case_type': case_type or 'functional',
'priority': priority or 'P2',
'precondition': precondition or '',
'steps': steps or '',
'expected_result': expected_result or '',
'status': S_PENDING,
})
await record_audit(plan_id, TABLE, cid, 'create',
to_state=S_PENDING, who=_normalize_role(who),
agent_id=agent_id, sor=sor)
logger.info("create_case: %s plan=%s", cid, plan_id)
return True, cid
async def pass_case(case_id, plan_id, actual_result="", executed_by="",
duration_ms=None, who=None, agent_id=None):
"""用例通过pending/fail → pass。"""
return await _execute_transition(case_id, plan_id, [S_PENDING, S_FAIL], S_PASS, 'pass',
actual_result=actual_result, executed_by=executed_by,
duration_ms=duration_ms, who=who, agent_id=agent_id)
async def fail_case(case_id, plan_id, actual_result="", executed_by="",
duration_ms=None, who=None, agent_id=None):
"""用例失败pending/pass → fail失败应联动建 Bug规范见 test-case skill"""
return await _execute_transition(case_id, plan_id, [S_PENDING, S_PASS], S_FAIL, 'fail',
actual_result=actual_result, executed_by=executed_by,
duration_ms=duration_ms, who=who, agent_id=agent_id)
async def block_case(case_id, plan_id, actual_result="", executed_by="",
who=None, agent_id=None):
"""用例阻塞pending → blocked因环境/依赖无法执行)。"""
return await _execute_transition(case_id, plan_id, [S_PENDING], S_BLOCKED, 'block',
actual_result=actual_result, executed_by=executed_by,
who=who, agent_id=agent_id)
async def skip_case(case_id, plan_id, actual_result="", executed_by="",
who=None, agent_id=None):
"""用例跳过pending → skipped有意不测须注明原因"""
return await _execute_transition(case_id, plan_id, [S_PENDING], S_SKIPPED, 'skip',
actual_result=actual_result, executed_by=executed_by,
who=who, agent_id=agent_id)
async def set_case_state(case_id, plan_id, from_state, to_state,
who=None, agent_id=None, detail=None):
"""通用 CAS 状态迁移兜底(跨产线自定义状态机用)。"""
return await _execute_transition(case_id, plan_id, [from_state], to_state, 'set_state',
actual_result=detail or '', who=who, agent_id=agent_id)
async def list_cases(plan_id, status=None, limit=100) -> list:
"""列出用例(可按状态过滤)。"""
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
conditions = ["plan_id=${pid}$"]
params = {"pid": plan_id}
if status:
conditions.append("status=${status}$")
params["status"] = status
where = " AND ".join(conditions)
try:
limit = int(limit)
except (TypeError, ValueError):
limit = 100
sql = (f"SELECT * FROM {TABLE} WHERE {where} "
f"ORDER BY created_at ASC 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 {}

View File

@ -0,0 +1,166 @@
"""测试计划能力 — sd_test_plans 的状态机语义化迁移(通用、产线无关)。
定位测试计划 CRUD xls2ui 生成的端点本模块只做生命周期状态流转
每个迁移 CAS 原子防并发/越权+ 租户隔离 + 审计
测试计划的生命周期/流转规则在 test-plan skill LLM skill 判断合法性本模块只固化操作原语
角色规范role 参数用 agent.{role}无前缀自动补 agent.人角色用 {orgtype}.{role}
scope 约定sd_test_plans project_id iteration_id 做范围校验CAS WHERE 里带 iteration_id
"""
import logging
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
from .audit import record_audit
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.test_plan_capability")
TABLE = "sd_test_plans"
# 测试计划状态SDLC 默认,状态机语义见 test-plan skill
S_DRAFT = "draft" # 草稿
S_APPROVED = "approved" # 已审批
S_EXECUTING = "executing" # 执行中
S_COMPLETED = "completed" # 已完成
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(plan_id, iteration_id, from_state, to_state, action,
who=None, agent_id=None, detail=None):
"""CAS 状态迁移 + 审计。返回 (ok, message)。"""
if not plan_id or not iteration_id:
return False, "缺少 plan_id 或 iteration_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 iteration_id=${iid}$ AND status=${from}$",
{"to": to_state, "pid": plan_id, "iid": iteration_id, "from": from_state})
recs = await sor.R(TABLE, {'id': plan_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(iteration_id, TABLE, plan_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_test_plan(iteration_id, plan_name, plan_type="functional",
scope="", environment="", entry_criteria="", exit_criteria="",
created_by="", who=None, agent_id=None):
"""创建测试计划新建记录status=draft。返回 (ok, plan_id_or_message)。"""
if not iteration_id:
return False, "缺少 iteration_id"
if not plan_name or not plan_name.strip():
return False, "缺少 plan_name"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
pid = getID()
await sor.C(TABLE, {
'id': pid,
'iteration_id': iteration_id,
'plan_name': plan_name.strip(),
'plan_type': plan_type or 'functional',
'scope': scope or '',
'environment': environment or '',
'entry_criteria': entry_criteria or '',
'exit_criteria': exit_criteria or '',
'status': S_DRAFT,
'created_by': created_by or '',
})
await record_audit(iteration_id, TABLE, pid, 'create',
to_state=S_DRAFT, who=_normalize_role(who),
agent_id=agent_id, sor=sor)
logger.info("create_test_plan: %s iteration=%s", pid, iteration_id)
return True, pid
async def approve_plan(plan_id, iteration_id, who=None, agent_id=None):
"""审批通过draft → approved。"""
return await _transition(plan_id, iteration_id, S_DRAFT, S_APPROVED, 'approve',
who=who, agent_id=agent_id)
async def start_plan(plan_id, iteration_id, who=None, agent_id=None):
"""开始执行approved → executing。"""
return await _transition(plan_id, iteration_id, S_APPROVED, S_EXECUTING, 'start',
who=who, agent_id=agent_id)
async def complete_plan(plan_id, iteration_id, who=None, agent_id=None):
"""完成测试executing → completed。"""
return await _transition(plan_id, iteration_id, S_EXECUTING, S_COMPLETED, 'complete',
who=who, agent_id=agent_id)
async def set_plan_state(plan_id, iteration_id, from_state, to_state,
who=None, agent_id=None, detail=None):
"""通用 CAS 状态迁移兜底(跨产线自定义状态机用)。"""
return await _transition(plan_id, iteration_id, from_state, to_state, 'set_state',
who=who, agent_id=agent_id, detail=detail)
async def list_plans(iteration_id, status=None, limit=50) -> list:
"""列出测试计划(可按状态过滤)。"""
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
conditions = ["iteration_id=${iid}$"]
params = {"iid": iteration_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 ASC 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 {}