feat: 7个SDLC概念能力模块(project/iteration/deliverable/test-plan/test-case/bug/deploy-env状态机CAS+审计) + sdlc_ability接线36工具
This commit is contained in:
parent
4abe93a8f9
commit
3affc4dced
220
pipeline_service/bug_capability.py
Normal file
220
pipeline_service/bug_capability.py
Normal 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):
|
||||
"""确认 Bug:open → 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):
|
||||
"""关闭 Bug:verified → 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):
|
||||
"""驳回 Bug:open/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 记录对象转成 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 {}
|
||||
182
pipeline_service/deliverable_capability.py
Normal file
182
pipeline_service/deliverable_capability.py
Normal 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 记录对象转成 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 {}
|
||||
199
pipeline_service/deploy_capability.py
Normal file
199
pipeline_service/deploy_capability.py
Normal 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 记录对象转成 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 {}
|
||||
172
pipeline_service/iteration_capability.py
Normal file
172
pipeline_service/iteration_capability.py
Normal 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 记录对象转成 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 {}
|
||||
178
pipeline_service/project_capability.py
Normal file
178
pipeline_service/project_capability.py
Normal 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 记录对象转成 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 {}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
199
pipeline_service/test_case_capability.py
Normal file
199
pipeline_service/test_case_capability.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""测试用例能力 — sd_test_cases 的执行状态机语义化迁移(通用、产线无关)。
|
||||
|
||||
定位:用例 CRUD 走 xls2ui 生成的端点;本模块只做「执行结果状态流转」——
|
||||
每个迁移 CAS 原子(防并发/越权)+ 租户隔离 + 审计。
|
||||
|
||||
用例的执行状态机/流转规则在 test-case skill 里(LLM 读 skill 判断合法性);本模块只固化操作原语。
|
||||
失败用例联动建 Bug(case_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 记录对象转成 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 {}
|
||||
166
pipeline_service/test_plan_capability.py
Normal file
166
pipeline_service/test_plan_capability.py
Normal 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 记录对象转成 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 {}
|
||||
Loading…
x
Reference in New Issue
Block a user