"""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 import json 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/open → fixing(记录处理人)。 from 允许 open:PM review_rollback 回退 develop 处理部署/功能 Bug 时,走的是「回退重做」任务链 (隐含确认缺陷有效),不经过 confirm_bug,bug 停在 open。若 start_fix 只认 confirmed, develop 在回退重做任务里修完缺陷后无法 start_fix→fix_bug,bug 永远 open、闭环断在 fixed 环节。 """ return await _transition(bug_id, iteration_id, [S_CONFIRMED, S_OPEN], 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 {} async def classify_task(task_id, iteration_id="", who=None, agent_id=None): """判断任务来源:new_dev(新开发) / bug_fix(修复 Bug) / rework(质检重做)。 优先读任务 params.task_kind 标记(三条创建路径已打);老任务无标记则按 params.rollback_from / title 兜底推断。develop 据此决定是否走 fix_bug 状态机: - bug_fix:PM 派发的「修复 Bug」任务,开始 start_fix、完成 fix_bug - rework:回退重做(QC/PM 退回重做交付件),不调 bug 工具,只重做 - new_dev:纯开发,不走 bug 状态机(除非过程中自己 report 了新 bug) 返回 (ok, {"task_kind": ..., "reason": ..., "title": ...} 或错误消息)。 """ if not task_id: return False, "缺少 task_id" db, dbname = _get_db() async with db.sqlorContext(dbname) as sor: recs = await sor.sqlExe( "SELECT id, title, params FROM pipeline_tasks WHERE id=${tid}$", {"tid": task_id}) await sor.sqlExe("COMMIT", {}) if not recs: return False, f"任务不存在: {task_id}" rec = recs[0] title = getattr(rec, 'title', '') or '' params_str = getattr(rec, 'params', '{}') or '{}' try: params = json.loads(params_str) if isinstance(params_str, str) else (params_str or {}) except (json.JSONDecodeError, TypeError): params = {} kind = (params.get('task_kind') or '').strip() reason = "params.task_kind 标记" if not kind: # 老任务兜底:rollback_from 存在 → rework;title 含「修复 Bug」→ bug_fix;否则 new_dev if params.get('rollback_from'): kind = 'rework' reason = "params.rollback_from 存在(回退重做)" elif ('修复 Bug' in title) or ('修复Bug' in title) or ('修复bug' in title): kind = 'bug_fix' reason = "title 含「修复 Bug」" else: kind = 'new_dev' reason = "无 bug 相关标记,默认新开发" return True, {"task_id": task_id, "task_kind": kind, "title": title, "reason": reason}